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/
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
@ -7,18 +26,59 @@ yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
|
||||
node_modules
|
||||
dist
|
||||
dist-ssr
|
||||
*.local
|
||||
# Package manager files
|
||||
package-lock.json
|
||||
yarn.lock
|
||||
.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
|
||||
.vscode/*
|
||||
!.vscode/extensions.json
|
||||
.idea
|
||||
.DS_Store
|
||||
!.vscode/settings.json
|
||||
!.vscode/tasks.json
|
||||
!.vscode/launch.json
|
||||
.idea/
|
||||
*.swp
|
||||
*.swo
|
||||
*~
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.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"
|
||||
}
|
||||
20
CLAUDE.md
20
CLAUDE.md
@ -16,12 +16,15 @@ CoolSoup is a React + TypeScript + Vite application that generates visual patter
|
||||
## Architecture Overview
|
||||
|
||||
### Core Application Structure
|
||||
|
||||
- **State Management**: Uses Nanostores for reactive state with persistent atoms
|
||||
- **UI Layout**: Two-panel design - main content area (generator + image grid) and collapsible audio panel
|
||||
- **Generator Pattern**: Pluggable generator system where each generator implements a common interface
|
||||
|
||||
### Main Store (`src/stores/index.ts`)
|
||||
|
||||
Central state management with these key atoms:
|
||||
|
||||
- `appSettings` - Generator selection, grid size, colors
|
||||
- `generatedImages` - Array of generated images with metadata
|
||||
- `selectedImage` - Current image for audio synthesis
|
||||
@ -29,13 +32,16 @@ Central state management with these key atoms:
|
||||
- `panelOpen` - Audio panel visibility
|
||||
|
||||
### Generator System
|
||||
|
||||
Four built-in generators located in `src/generators/`:
|
||||
|
||||
- **Tixy** (`tixy.ts`) - Mathematical expressions using t,i,x,y variables
|
||||
- **Waveform** (`waveform.ts`) - Procedural random waveforms with various interpolation curves
|
||||
- **Picsum** (`picsum.ts`) - External random images API
|
||||
- **Art Institute** (`art-institute.ts`) - Art Institute of Chicago API
|
||||
|
||||
Each generator returns `GeneratedImage[]` with:
|
||||
|
||||
- `id` - Unique identifier
|
||||
- `canvas` - HTMLCanvasElement
|
||||
- `imageData` - ImageData for synthesis
|
||||
@ -43,13 +49,16 @@ Each generator returns `GeneratedImage[]` with:
|
||||
- `params` - Generation parameters
|
||||
|
||||
### Spectral Synthesis Engine (`src/spectral-synthesis/`)
|
||||
|
||||
Advanced image-to-audio synthesis library:
|
||||
|
||||
- **Core Logic** (`core/synthesizer.ts`) - Main ImageToAudioSynthesizer class
|
||||
- **Types** (`core/types.ts`) - SynthesisParams and related interfaces
|
||||
- **Audio Export** (`audio/export.ts`) - WAV file generation and download
|
||||
- **Utilities** (`core/utils.ts`) - Helper functions for frequency mapping and peak detection
|
||||
|
||||
Key features:
|
||||
|
||||
- Mel-scale frequency mapping for perceptual accuracy
|
||||
- Spectral peak detection to reduce noise
|
||||
- Temporal smoothing for coherent audio trajectories
|
||||
@ -57,7 +66,9 @@ Key features:
|
||||
- Configurable synthesis parameters (duration, frequency range, resolution)
|
||||
|
||||
### Audio Export System (`src/audio-export/`)
|
||||
|
||||
Batch audio processing capabilities:
|
||||
|
||||
- Single image export with custom parameters
|
||||
- Batch export of all generated images
|
||||
- ZIP file generation for batch downloads
|
||||
@ -66,6 +77,7 @@ Batch audio processing capabilities:
|
||||
## Key Implementation Details
|
||||
|
||||
### Image Generation Flow
|
||||
|
||||
1. User selects generator and parameters in `GeneratorSelector`
|
||||
2. `App.tsx` calls appropriate generator function
|
||||
3. Generator returns array of `GeneratedImage` objects
|
||||
@ -73,6 +85,7 @@ Batch audio processing capabilities:
|
||||
5. User can click image to select for audio synthesis
|
||||
|
||||
### Audio Synthesis Flow
|
||||
|
||||
1. User selects image from grid (sets `selectedImage`)
|
||||
2. `AudioPanel` provides synthesis parameter controls
|
||||
3. Synthesis triggered via spectral-synthesis library
|
||||
@ -80,6 +93,7 @@ Batch audio processing capabilities:
|
||||
5. Batch export processes all generated images
|
||||
|
||||
### Component Architecture
|
||||
|
||||
- **App.tsx** - Main layout and generation orchestration
|
||||
- **GeneratorSelector** - Generator picker with settings
|
||||
- **ImageGrid** - Grid display of generated images
|
||||
@ -87,15 +101,18 @@ Batch audio processing capabilities:
|
||||
- **AudioControls** - Playback controls for generated audio
|
||||
|
||||
## Generator Module Structure
|
||||
|
||||
Both tixy-generator and waveform-generator modules are designed as standalone packages:
|
||||
|
||||
**Tixy Generator**:
|
||||
|
||||
- `README.md` - Documentation and usage examples
|
||||
- `core/types.ts` - Type definitions
|
||||
- `index.ts` - Main exports
|
||||
- Generator-specific files (evaluator, patterns, etc.)
|
||||
|
||||
**Waveform Generator**:
|
||||
|
||||
- `README.md` - Documentation and usage examples
|
||||
- `core/types.ts` - Interfaces and configuration
|
||||
- `core/interpolation.ts` - Curve interpolation functions (linear, exponential, logarithmic, cubic)
|
||||
@ -104,8 +121,9 @@ Both tixy-generator and waveform-generator modules are designed as standalone pa
|
||||
- `index.ts` - Main exports and convenience functions
|
||||
|
||||
## Development Notes
|
||||
|
||||
- Uses Rolldown Vite for improved performance
|
||||
- Tailwind CSS for styling (no rounded corners per user preference)
|
||||
- ESLint configured for React + TypeScript
|
||||
- All generators support time-based animation parameters
|
||||
- Image synthesis supports real-time parameter adjustment
|
||||
- Image synthesis supports real-time parameter adjustment
|
||||
|
||||
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": {
|
||||
"dev": "vite",
|
||||
"build": "tsc -b && vite build",
|
||||
"build:vite": "vite build",
|
||||
"typecheck": "tsc -b",
|
||||
"lint": "eslint .",
|
||||
"format": "prettier --write .",
|
||||
"format:check": "prettier --check .",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
@ -31,6 +35,7 @@
|
||||
"eslint-plugin-react-refresh": "^0.4.20",
|
||||
"globals": "^16.4.0",
|
||||
"postcss": "^8.5.6",
|
||||
"prettier": "^3.6.2",
|
||||
"tailwindcss": "^4.1.13",
|
||||
"typescript": "~5.8.3",
|
||||
"typescript-eslint": "^8.44.0",
|
||||
|
||||
10
pnpm-lock.yaml
generated
10
pnpm-lock.yaml
generated
@ -66,6 +66,9 @@ importers:
|
||||
postcss:
|
||||
specifier: ^8.5.6
|
||||
version: 8.5.6
|
||||
prettier:
|
||||
specifier: ^3.6.2
|
||||
version: 3.6.2
|
||||
tailwindcss:
|
||||
specifier: ^4.1.13
|
||||
version: 4.1.13
|
||||
@ -1078,6 +1081,11 @@ packages:
|
||||
resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==}
|
||||
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:
|
||||
resolution: {integrity: sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==}
|
||||
|
||||
@ -2242,6 +2250,8 @@ snapshots:
|
||||
|
||||
prelude-ls@1.2.1: {}
|
||||
|
||||
prettier@3.6.2: {}
|
||||
|
||||
process-nextick-args@2.0.1: {}
|
||||
|
||||
punycode@2.3.1: {}
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
export default {
|
||||
plugins: {
|
||||
"@tailwindcss/postcss": {},
|
||||
'@tailwindcss/postcss': {},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
import { useStore } from '@nanostores/react'
|
||||
import { appSettings, generatedImages, isGenerating, helpPopupOpen } from './stores'
|
||||
import type { GeneratedImage } from './stores'
|
||||
import { generateTixyImages } from './generators/tixy'
|
||||
import { generatePicsumImages } from './generators/picsum'
|
||||
import { generateArtInstituteImages } from './generators/art-institute'
|
||||
@ -27,7 +28,7 @@ function App() {
|
||||
isGenerating.set(true)
|
||||
|
||||
try {
|
||||
let newImages
|
||||
let newImages: GeneratedImage[]
|
||||
|
||||
if (settings.selectedGenerator === 'tixy') {
|
||||
newImages = generateTixyImages(settings.gridSize, 64)
|
||||
@ -76,10 +77,7 @@ function App() {
|
||||
)}
|
||||
</div>
|
||||
<AudioPanel />
|
||||
<HelpPopup
|
||||
isOpen={helpOpen}
|
||||
onClose={() => helpPopupOpen.set(false)}
|
||||
/>
|
||||
<HelpPopup isOpen={helpOpen} onClose={() => helpPopupOpen.set(false)} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@ -104,20 +104,20 @@ export function exportSingleAudio(
|
||||
|
||||
const filename = generateFilename(image.id, image.generator, {
|
||||
...options,
|
||||
includeGeneratorInName: true
|
||||
includeGeneratorInName: true,
|
||||
})
|
||||
|
||||
downloadFile(blob, filename)
|
||||
|
||||
return {
|
||||
success: true,
|
||||
filename
|
||||
filename,
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error exporting single audio:', error)
|
||||
return {
|
||||
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))
|
||||
} 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,
|
||||
totalFiles: images.length,
|
||||
successfulFiles,
|
||||
errors
|
||||
errors,
|
||||
}
|
||||
}
|
||||
|
||||
@ -167,14 +169,16 @@ export async function exportBatchAudio(
|
||||
|
||||
const filename = generateFilename(image.id, image.generator, {
|
||||
...options,
|
||||
includeGeneratorInName: true
|
||||
includeGeneratorInName: true,
|
||||
})
|
||||
|
||||
zip.file(filename, buffer)
|
||||
successfulFiles++
|
||||
} catch (error) {
|
||||
console.error(`Error processing image ${image.id}:`, error)
|
||||
errors.push(`Failed to process ${image.id}: ${error instanceof Error ? error.message : 'Unknown error'}`)
|
||||
errors.push(
|
||||
`Failed to process ${image.id}: ${error instanceof Error ? error.message : 'Unknown error'}`
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@ -183,7 +187,7 @@ export async function exportBatchAudio(
|
||||
success: false,
|
||||
totalFiles: images.length,
|
||||
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,
|
||||
successfulFiles,
|
||||
zipFilename,
|
||||
errors
|
||||
errors,
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error creating ZIP file:', error)
|
||||
@ -206,7 +210,10 @@ export async function exportBatchAudio(
|
||||
success: false,
|
||||
totalFiles: images.length,
|
||||
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 {
|
||||
return exportSingleAudio(image, synthesisParams, {
|
||||
includeGeneratorInName: true,
|
||||
includeTimestamp: false
|
||||
includeTimestamp: false,
|
||||
})
|
||||
}
|
||||
|
||||
@ -234,6 +241,6 @@ export async function quickExportBatch(
|
||||
return exportBatchAudio(images, synthesisParams, {
|
||||
includeGeneratorInName: true,
|
||||
includeTimestamp: true,
|
||||
createZip: true
|
||||
createZip: true,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,4 +1,3 @@
|
||||
import { useState, useRef } from 'react'
|
||||
|
||||
interface AudioControlsProps {
|
||||
isPlaying: boolean
|
||||
@ -17,9 +16,8 @@ export default function AudioControls({
|
||||
onPause,
|
||||
onStop,
|
||||
onVolumeChange,
|
||||
disabled = false
|
||||
disabled = false,
|
||||
}: AudioControlsProps) {
|
||||
const [isDragging, setIsDragging] = useState(false)
|
||||
|
||||
const handleVolumeChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
onVolumeChange(Number(e.target.value))
|
||||
@ -58,7 +56,13 @@ export default function AudioControls({
|
||||
</div>
|
||||
|
||||
<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" />
|
||||
</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"
|
||||
title={`Volume: ${Math.round(volume * 100)}%`}
|
||||
/>
|
||||
<style dangerouslySetInnerHTML={{
|
||||
__html: `
|
||||
<style
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: `
|
||||
.volume-slider::-webkit-slider-thumb {
|
||||
appearance: none;
|
||||
width: 16px;
|
||||
@ -100,14 +105,13 @@ export default function AudioControls({
|
||||
.volume-slider::-moz-range-thumb:hover {
|
||||
background: ${disabled ? '#6b7280' : '#e5e7eb'};
|
||||
}
|
||||
`
|
||||
}} />
|
||||
`,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<span className="text-xs text-gray-400 w-10 text-right">
|
||||
{Math.round(volume * 100)}%
|
||||
</span>
|
||||
<span className="text-xs text-gray-400 w-10 text-right">{Math.round(volume * 100)}%</span>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,6 +1,10 @@
|
||||
import { useStore } from '@nanostores/react'
|
||||
import { selectedImage, panelOpen, generatedImages, synthesisParams } from '../stores'
|
||||
import { synthesizeFromImage, playAudio, createAudioPlayer, type WindowType, type AudioPlayer } from '../spectral-synthesis'
|
||||
import { selectedImage, generatedImages, synthesisParams } from '../stores'
|
||||
import {
|
||||
synthesizeFromImage,
|
||||
createAudioPlayer,
|
||||
type AudioPlayer,
|
||||
} from '../spectral-synthesis'
|
||||
import { quickExportSingle, quickExportBatch } from '../audio-export'
|
||||
import { useState, useRef, useEffect } from 'react'
|
||||
import AudioControls from './AudioControls'
|
||||
@ -14,14 +18,8 @@ export default function AudioPanel() {
|
||||
const [volume, setVolume] = useState(0.7)
|
||||
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 })
|
||||
}
|
||||
|
||||
@ -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
|
||||
useEffect(() => {
|
||||
@ -131,7 +117,6 @@ export default function AudioPanel() {
|
||||
if (!result.success) {
|
||||
console.error('Batch export failed:', result.errors)
|
||||
} else {
|
||||
console.log(`Exported ${result.successfulFiles}/${result.totalFiles} files to ${result.zipFilename}`)
|
||||
}
|
||||
} catch (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="w-24 h-24 border border-gray-600">
|
||||
<canvas
|
||||
ref={(canvas) => {
|
||||
ref={canvas => {
|
||||
if (canvas && selected.canvas) {
|
||||
const ctx = canvas.getContext('2d')!
|
||||
canvas.width = selected.canvas.width
|
||||
@ -186,9 +171,7 @@ export default function AudioPanel() {
|
||||
<div className="p-6 space-y-4">
|
||||
{/* Synthesis Mode Selection - Top Priority */}
|
||||
<div>
|
||||
<label className="block text-xs text-gray-400 mb-1">
|
||||
Synthesis Mode
|
||||
</label>
|
||||
<label className="block text-xs text-gray-400 mb-1">Synthesis Mode</label>
|
||||
<div className="grid grid-cols-2 gap-1">
|
||||
<button
|
||||
onClick={() => updateParam('synthesisMode', 'direct')}
|
||||
@ -216,265 +199,20 @@ export default function AudioPanel() {
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<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 className="space-y-3">
|
||||
<div>
|
||||
<label className="block text-xs text-gray-400 mb-1">
|
||||
Color
|
||||
</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)}
|
||||
Duration: {params.duration}s
|
||||
</label>
|
||||
<input
|
||||
type="range"
|
||||
min="1"
|
||||
max="5"
|
||||
step="0.1"
|
||||
value={params.contrast || 2.2}
|
||||
onChange={(e) => updateParam('contrast', Number(e.target.value))}
|
||||
max="30"
|
||||
step="1"
|
||||
value={params.duration}
|
||||
onChange={e => updateParam('duration', 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 */}
|
||||
@ -482,89 +220,345 @@ export default function AudioPanel() {
|
||||
<>
|
||||
<div>
|
||||
<label className="block text-xs text-gray-400 mb-1">
|
||||
Frequency Mapping
|
||||
Max Partials: {params.maxPartials}
|
||||
</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>
|
||||
<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">
|
||||
Spectral Density: {params.spectralDensity || 3}
|
||||
Frequency Density: {params.frequencyResolution}x
|
||||
</label>
|
||||
<input
|
||||
type="range"
|
||||
min="1"
|
||||
max="7"
|
||||
max="10"
|
||||
step="1"
|
||||
value={params.spectralDensity || 3}
|
||||
onChange={(e) => updateParam('spectralDensity', Number(e.target.value))}
|
||||
value={params.frequencyResolution}
|
||||
onChange={e => updateParam('frequencyResolution', Number(e.target.value))}
|
||||
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 className="flex items-center justify-between">
|
||||
<label className="text-xs text-gray-400">Perceptual RGB Weighting</label>
|
||||
<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>
|
||||
<label className="block text-xs text-gray-400 mb-1">Color</label>
|
||||
<div className="grid grid-cols-2 gap-1">
|
||||
<button
|
||||
onClick={() => updateParam('usePerceptualWeighting', !params.usePerceptualWeighting)}
|
||||
onClick={() => updateParam('invert', false)}
|
||||
className={`px-2 py-1 text-xs border ${
|
||||
params.usePerceptualWeighting !== false
|
||||
!params.invert
|
||||
? 'bg-white text-black 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>
|
||||
</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>
|
||||
|
||||
{/* Audio Controls - Below Parameters */}
|
||||
@ -603,4 +597,4 @@ export default function AudioPanel() {
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@ -9,13 +9,9 @@ interface GeneratorSelectorProps {
|
||||
}
|
||||
|
||||
export default function GeneratorSelector({ onGenerate, isGenerating }: GeneratorSelectorProps) {
|
||||
console.log('GeneratorSelector rendering with props:', { onGenerate, isGenerating })
|
||||
|
||||
const settings = useStore(appSettings)
|
||||
console.log('GeneratorSelector settings:', settings)
|
||||
|
||||
const handleGeneratorChange = (generator: GeneratorType) => {
|
||||
console.log('Changing generator to:', generator)
|
||||
appSettings.set({ ...settings, selectedGenerator: generator })
|
||||
|
||||
// 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 = () => {
|
||||
console.log('Generate button clicked in GeneratorSelector!')
|
||||
console.log('onGenerate function:', 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: 'webcam' as const, name: 'Webcam', description: 'Capture from camera' },
|
||||
{ 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
|
||||
@ -55,7 +49,10 @@ export default function GeneratorSelector({ onGenerate, isGenerating }: Generato
|
||||
const handleKeyPress = (event: KeyboardEvent) => {
|
||||
if (event.key.toLowerCase() === 'g' && !isGenerating) {
|
||||
// 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
|
||||
}
|
||||
event.preventDefault()
|
||||
@ -81,22 +78,23 @@ export default function GeneratorSelector({ onGenerate, isGenerating }: Generato
|
||||
</h1>
|
||||
</div>
|
||||
<div className="flex items-center justify-center h-10">
|
||||
{settings.selectedGenerator !== 'from-photo' && settings.selectedGenerator !== 'webcam' && (
|
||||
<button
|
||||
onClick={handleGenerateClick}
|
||||
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>
|
||||
)}
|
||||
{settings.selectedGenerator !== 'from-photo' &&
|
||||
settings.selectedGenerator !== 'webcam' && (
|
||||
<button
|
||||
onClick={handleGenerateClick}
|
||||
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>
|
||||
</div>
|
||||
|
||||
{/* Second container: 85% width - Generator modes in two lines */}
|
||||
<div className="w-[85%] flex flex-col space-y-2">
|
||||
<div className="flex items-center space-x-4 h-10">
|
||||
{firstRowGenerators.map((generator) => (
|
||||
{firstRowGenerators.map(generator => (
|
||||
<Tooltip key={generator.id} content={generator.description}>
|
||||
<button
|
||||
onClick={() => handleGeneratorChange(generator.id)}
|
||||
@ -112,7 +110,7 @@ export default function GeneratorSelector({ onGenerate, isGenerating }: Generato
|
||||
))}
|
||||
</div>
|
||||
<div className="flex items-center space-x-4 h-10">
|
||||
{secondRowGenerators.map((generator) => (
|
||||
{secondRowGenerators.map(generator => (
|
||||
<Tooltip key={generator.id} content={generator.description}>
|
||||
<button
|
||||
onClick={() => handleGeneratorChange(generator.id)}
|
||||
@ -131,4 +129,4 @@ export default function GeneratorSelector({ onGenerate, isGenerating }: Generato
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@ -28,21 +28,21 @@ export default function HelpPopup({ isOpen, onClose }: HelpPopupProps) {
|
||||
>
|
||||
<div
|
||||
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">
|
||||
<h2 className="text-xl font-bold text-white">About CoolSoup</h2>
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="text-white hover:text-gray-300 text-xl"
|
||||
>
|
||||
<button onClick={onClose} className="text-white hover:text-gray-300 text-xl">
|
||||
×
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="text-white space-y-4">
|
||||
<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>
|
||||
|
||||
<div className="pt-4 border-t border-gray-600">
|
||||
@ -63,4 +63,4 @@ export default function HelpPopup({ isOpen, onClose }: HelpPopupProps) {
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@ -21,7 +21,7 @@ export default function ImageGrid() {
|
||||
const rect = event.currentTarget.getBoundingClientRect()
|
||||
setLocalMousePosition({
|
||||
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()
|
||||
setLocalMousePosition({
|
||||
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-sm">Please wait while we fetch your images</div>
|
||||
<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>
|
||||
@ -69,21 +72,19 @@ export default function ImageGrid() {
|
||||
<div className="flex-1 overflow-hidden relative">
|
||||
<div className="h-full overflow-y-auto p-6">
|
||||
<div className="grid grid-cols-5 gap-3">
|
||||
{images.map((image) => (
|
||||
{images.map(image => (
|
||||
<button
|
||||
key={image.id}
|
||||
onClick={() => handleImageClick(image)}
|
||||
onMouseEnter={(e) => handleMouseEnter(image, e)}
|
||||
onMouseEnter={e => handleMouseEnter(image, e)}
|
||||
onMouseMove={handleMouseMove}
|
||||
onMouseLeave={handleMouseLeave}
|
||||
className={`aspect-square border-2 transition-colors ${
|
||||
selected?.id === image.id
|
||||
? 'border-white'
|
||||
: 'border-gray-700 hover:border-gray-500'
|
||||
selected?.id === image.id ? 'border-white' : 'border-gray-700 hover:border-gray-500'
|
||||
}`}
|
||||
>
|
||||
<canvas
|
||||
ref={(canvas) => {
|
||||
ref={canvas => {
|
||||
if (canvas && image.canvas) {
|
||||
const ctx = canvas.getContext('2d')!
|
||||
canvas.width = image.canvas.width
|
||||
@ -119,7 +120,7 @@ export default function ImageGrid() {
|
||||
}}
|
||||
>
|
||||
<canvas
|
||||
ref={(canvas) => {
|
||||
ref={canvas => {
|
||||
if (canvas && hoveredImage.canvas) {
|
||||
const ctx = canvas.getContext('2d')!
|
||||
canvas.width = 200
|
||||
@ -143,11 +144,7 @@ export default function ImageGrid() {
|
||||
ctx.imageSmoothingEnabled = true
|
||||
}
|
||||
|
||||
ctx.drawImage(
|
||||
sourceCanvas,
|
||||
cropX, cropY, cropSize, cropSize,
|
||||
0, 0, 200, 200
|
||||
)
|
||||
ctx.drawImage(sourceCanvas, cropX, cropY, cropSize, cropSize, 0, 0, 200, 200)
|
||||
}
|
||||
}}
|
||||
className="w-full h-full block"
|
||||
@ -157,4 +154,4 @@ export default function ImageGrid() {
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@ -2,7 +2,6 @@ import { useState, useCallback } from 'react'
|
||||
import { useStore } from '@nanostores/react'
|
||||
import { generateFromPhotoImage } from '../generators/from-photo'
|
||||
import { generatedImages, selectedImage } from '../stores'
|
||||
import type { GeneratedImage } from '../stores'
|
||||
|
||||
interface PhotoDropZoneProps {
|
||||
size: number
|
||||
@ -13,7 +12,6 @@ export default function PhotoDropZone({ size }: PhotoDropZoneProps) {
|
||||
const [isProcessing, setIsProcessing] = useState(false)
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
const images = useStore(generatedImages)
|
||||
const selected = useStore(selectedImage)
|
||||
|
||||
const handleDragOver = useCallback((e: React.DragEvent) => {
|
||||
e.preventDefault()
|
||||
@ -25,71 +23,45 @@ export default function PhotoDropZone({ size }: PhotoDropZoneProps) {
|
||||
setIsDragOver(false)
|
||||
}, [])
|
||||
|
||||
const handleDrop = useCallback(async (e: React.DragEvent) => {
|
||||
e.preventDefault()
|
||||
setIsDragOver(false)
|
||||
setError(null)
|
||||
|
||||
const files = Array.from(e.dataTransfer.files)
|
||||
const imageFiles = files.filter(file => file.type.startsWith('image/'))
|
||||
|
||||
if (imageFiles.length === 0) {
|
||||
setError('Please drop an image file (PNG, JPG, GIF, etc.)')
|
||||
return
|
||||
}
|
||||
|
||||
if (imageFiles.length > 1) {
|
||||
setError('Please drop only one image at a time')
|
||||
return
|
||||
}
|
||||
|
||||
const file = imageFiles[0]
|
||||
|
||||
try {
|
||||
setIsProcessing(true)
|
||||
const processedImage = await generateFromPhotoImage(file, size)
|
||||
|
||||
// Set the single processed image and automatically select it
|
||||
generatedImages.set([processedImage])
|
||||
selectedImage.set(processedImage)
|
||||
|
||||
console.log('Photo processed successfully:', processedImage)
|
||||
} catch (error) {
|
||||
console.error('Error processing image:', error)
|
||||
setError('Failed to process the image. Please try again.')
|
||||
} finally {
|
||||
setIsProcessing(false)
|
||||
}
|
||||
}, [size])
|
||||
|
||||
const handleFileInput = useCallback(async (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const files = e.target.files
|
||||
if (!files || files.length === 0) return
|
||||
|
||||
const file = files[0]
|
||||
|
||||
if (!file.type.startsWith('image/')) {
|
||||
setError('Please select an image file (PNG, JPG, GIF, etc.)')
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
setIsProcessing(true)
|
||||
const handleDrop = useCallback(
|
||||
async (e: React.DragEvent) => {
|
||||
e.preventDefault()
|
||||
setIsDragOver(false)
|
||||
setError(null)
|
||||
const processedImage = await generateFromPhotoImage(file, size)
|
||||
|
||||
// Set the single processed image and automatically select it
|
||||
generatedImages.set([processedImage])
|
||||
selectedImage.set(processedImage)
|
||||
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)
|
||||
|
||||
} 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
|
||||
|
||||
@ -107,7 +79,7 @@ export default function PhotoDropZone({ size }: PhotoDropZoneProps) {
|
||||
onDrop={handleDrop}
|
||||
>
|
||||
<canvas
|
||||
ref={(canvas) => {
|
||||
ref={canvas => {
|
||||
if (canvas && processedImage.canvas) {
|
||||
const ctx = canvas.getContext('2d')!
|
||||
canvas.width = processedImage.canvas.width
|
||||
@ -131,9 +103,7 @@ export default function PhotoDropZone({ size }: PhotoDropZoneProps) {
|
||||
// Show the drop zone
|
||||
<div
|
||||
className={`w-full max-w-2xl h-96 border-2 border-dashed flex flex-col items-center justify-center transition-colors ${
|
||||
isDragOver
|
||||
? 'border-white bg-gray-800'
|
||||
: 'border-gray-600 hover:border-gray-400'
|
||||
isDragOver ? 'border-white bg-gray-800' : 'border-gray-600 hover:border-gray-400'
|
||||
} ${isProcessing ? 'opacity-50 pointer-events-none' : ''}`}
|
||||
onDragOver={handleDragOver}
|
||||
onDragLeave={handleDragLeave}
|
||||
@ -150,11 +120,7 @@ export default function PhotoDropZone({ size }: PhotoDropZoneProps) {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{error && (
|
||||
<div className="mt-4 text-red-400 text-center">
|
||||
{error}
|
||||
</div>
|
||||
)}
|
||||
{error && <div className="mt-4 text-red-400 text-center">{error}</div>}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@ -30,24 +30,27 @@ function GridSquare({ index, image, onDrop, onSelect, selected }: GridSquareProp
|
||||
setIsDragOver(false)
|
||||
}, [])
|
||||
|
||||
const handleDrop = useCallback(async (e: React.DragEvent) => {
|
||||
e.preventDefault()
|
||||
setIsDragOver(false)
|
||||
const handleDrop = useCallback(
|
||||
async (e: React.DragEvent) => {
|
||||
e.preventDefault()
|
||||
setIsDragOver(false)
|
||||
|
||||
const files = Array.from(e.dataTransfer.files)
|
||||
const imageFiles = files.filter(file => file.type.startsWith('image/'))
|
||||
const files = Array.from(e.dataTransfer.files)
|
||||
const imageFiles = files.filter(file => file.type.startsWith('image/'))
|
||||
|
||||
if (imageFiles.length === 0) return
|
||||
if (imageFiles.length === 0) return
|
||||
|
||||
const file = imageFiles[0]
|
||||
setIsProcessing(true)
|
||||
const file = imageFiles[0]
|
||||
setIsProcessing(true)
|
||||
|
||||
try {
|
||||
await onDrop(file, index)
|
||||
} finally {
|
||||
setIsProcessing(false)
|
||||
}
|
||||
}, [index, onDrop])
|
||||
try {
|
||||
await onDrop(file, index)
|
||||
} finally {
|
||||
setIsProcessing(false)
|
||||
}
|
||||
},
|
||||
[index, onDrop]
|
||||
)
|
||||
|
||||
const handleClick = useCallback(() => {
|
||||
if (image) {
|
||||
@ -72,7 +75,7 @@ function GridSquare({ index, image, onDrop, onSelect, selected }: GridSquareProp
|
||||
</div>
|
||||
) : image ? (
|
||||
<canvas
|
||||
ref={(canvas) => {
|
||||
ref={canvas => {
|
||||
if (canvas && image.canvas) {
|
||||
const ctx = canvas.getContext('2d')!
|
||||
canvas.width = image.canvas.width
|
||||
@ -102,26 +105,28 @@ export default function PhotoGrid({ size }: PhotoGridProps) {
|
||||
const images = useStore(generatedImages)
|
||||
const selected = useStore(selectedImage)
|
||||
|
||||
const handleDrop = useCallback(async (file: File, index: number) => {
|
||||
try {
|
||||
const processedImage = await generateFromPhotoImage(file, size)
|
||||
const handleDrop = useCallback(
|
||||
async (file: File, index: number) => {
|
||||
try {
|
||||
const processedImage = await generateFromPhotoImage(file, size)
|
||||
|
||||
// Create new images array with the processed image at the specified index
|
||||
const newImages = [...images]
|
||||
newImages[index] = processedImage
|
||||
// Create new images array with the processed image at the specified index
|
||||
const newImages = [...images]
|
||||
newImages[index] = processedImage
|
||||
|
||||
// Filter out any null/undefined values and update the store
|
||||
const filteredImages = newImages.filter(Boolean) as GeneratedImage[]
|
||||
generatedImages.set(filteredImages)
|
||||
// Filter out any null/undefined values and update the store
|
||||
const filteredImages = newImages.filter(Boolean) as GeneratedImage[]
|
||||
generatedImages.set(filteredImages)
|
||||
|
||||
// Auto-select the newly processed image
|
||||
selectedImage.set(processedImage)
|
||||
// Auto-select the newly processed image
|
||||
selectedImage.set(processedImage)
|
||||
|
||||
console.log('Photo processed and added to grid at index:', index)
|
||||
} catch (error) {
|
||||
console.error('Error processing photo:', error)
|
||||
}
|
||||
}, [images, size])
|
||||
} catch (error) {
|
||||
console.error('Error processing photo:', error)
|
||||
}
|
||||
},
|
||||
[images, size]
|
||||
)
|
||||
|
||||
const handleSelect = useCallback((image: GeneratedImage) => {
|
||||
selectedImage.set(image)
|
||||
@ -146,9 +151,7 @@ export default function PhotoGrid({ size }: PhotoGridProps) {
|
||||
|
||||
return (
|
||||
<div className="flex-1 overflow-auto p-6">
|
||||
<div className="grid grid-cols-5 gap-2">
|
||||
{gridSquares}
|
||||
</div>
|
||||
<div className="grid grid-cols-5 gap-2">{gridSquares}</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
import { ReactNode, useState } from 'react'
|
||||
import { useState } from 'react'
|
||||
import type { ReactNode } from 'react'
|
||||
|
||||
interface TooltipProps {
|
||||
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',
|
||||
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',
|
||||
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 (
|
||||
@ -24,10 +25,12 @@ export default function Tooltip({ content, children, position = 'bottom' }: Tool
|
||||
>
|
||||
{children}
|
||||
{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}
|
||||
</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 { generateFromWebcamImage } from '../generators/webcam'
|
||||
import { WebcamProcessor } from '../generators/webcam-generator'
|
||||
@ -37,7 +37,7 @@ function GridSquare({ index, image, onCapture, onSelect, selected, isCapturing }
|
||||
onClick={handleImageClick}
|
||||
>
|
||||
<canvas
|
||||
ref={(canvas) => {
|
||||
ref={canvas => {
|
||||
if (canvas && image.canvas) {
|
||||
const ctx = canvas.getContext('2d')!
|
||||
canvas.width = image.canvas.width
|
||||
@ -60,9 +60,7 @@ function GridSquare({ index, image, onCapture, onSelect, selected, isCapturing }
|
||||
{isCapturing ? (
|
||||
<div className="text-xs">Capturing...</div>
|
||||
) : (
|
||||
<div className="text-xs text-center">
|
||||
Capture
|
||||
</div>
|
||||
<div className="text-xs text-center">Capture</div>
|
||||
)}
|
||||
</button>
|
||||
)}
|
||||
@ -100,37 +98,39 @@ export default function WebcamGrid({ size }: WebcamGridProps) {
|
||||
}
|
||||
}, [processor])
|
||||
|
||||
const handleCapture = useCallback(async (index: number) => {
|
||||
if (!cameraInitialized) {
|
||||
console.error('Camera not initialized')
|
||||
return
|
||||
}
|
||||
const handleCapture = useCallback(
|
||||
async (index: number) => {
|
||||
if (!cameraInitialized) {
|
||||
console.error('Camera not initialized')
|
||||
return
|
||||
}
|
||||
|
||||
setIsCapturing(true)
|
||||
setCapturingIndex(index)
|
||||
setIsCapturing(true)
|
||||
setCapturingIndex(index)
|
||||
|
||||
try {
|
||||
const capturedImage = await generateFromWebcamImage(processor, size)
|
||||
try {
|
||||
const capturedImage = await generateFromWebcamImage(processor, size)
|
||||
|
||||
// Create new images array with the captured image at the specified index
|
||||
const newImages = [...images]
|
||||
newImages[index] = capturedImage
|
||||
// Create new images array with the captured image at the specified index
|
||||
const newImages = [...images]
|
||||
newImages[index] = capturedImage
|
||||
|
||||
// Filter out any null/undefined values and update the store
|
||||
const filteredImages = newImages.filter(Boolean) as GeneratedImage[]
|
||||
generatedImages.set(filteredImages)
|
||||
// Filter out any null/undefined values and update the store
|
||||
const filteredImages = newImages.filter(Boolean) as GeneratedImage[]
|
||||
generatedImages.set(filteredImages)
|
||||
|
||||
// Auto-select the newly captured image
|
||||
selectedImage.set(capturedImage)
|
||||
// Auto-select the newly captured image
|
||||
selectedImage.set(capturedImage)
|
||||
|
||||
console.log('Photo captured and added to grid at index:', index)
|
||||
} catch (error) {
|
||||
console.error('Error capturing photo:', error)
|
||||
} finally {
|
||||
setIsCapturing(false)
|
||||
setCapturingIndex(null)
|
||||
}
|
||||
}, [images, size, cameraInitialized, processor])
|
||||
} catch (error) {
|
||||
console.error('Error capturing photo:', error)
|
||||
} finally {
|
||||
setIsCapturing(false)
|
||||
setCapturingIndex(null)
|
||||
}
|
||||
},
|
||||
[images, size, cameraInitialized, processor]
|
||||
)
|
||||
|
||||
const handleSelect = useCallback((image: GeneratedImage) => {
|
||||
selectedImage.set(image)
|
||||
@ -163,9 +163,7 @@ export default function WebcamGrid({ size }: WebcamGridProps) {
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="grid grid-cols-5 gap-2">
|
||||
{gridSquares}
|
||||
</div>
|
||||
<div className="grid grid-cols-5 gap-2">{gridSquares}</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,10 +1,32 @@
|
||||
import type { GeneratedImage } from '../stores'
|
||||
|
||||
const searchTerms = [
|
||||
'painting', 'sculpture', 'drawing', 'pottery', 'portrait', 'landscape',
|
||||
'impressionist', 'modern', 'contemporary', 'abstract', 'still life',
|
||||
'figure', 'nature', 'urban', 'color', 'light', 'texture', 'pattern',
|
||||
'architecture', 'street', 'woman', 'man', 'cat', 'dog', 'flower', 'tree'
|
||||
'painting',
|
||||
'sculpture',
|
||||
'drawing',
|
||||
'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 {
|
||||
@ -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 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
|
||||
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) {
|
||||
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
|
||||
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()
|
||||
img.crossOrigin = 'anonymous'
|
||||
|
||||
@ -122,8 +149,8 @@ async function loadArtInstituteImage(size: number, index: number): Promise<Gener
|
||||
date: artwork.date_display || 'Unknown Date',
|
||||
medium: artwork.medium_display || 'Unknown Medium',
|
||||
department: artwork.department_title || 'Unknown Department',
|
||||
searchTerm
|
||||
}
|
||||
searchTerm,
|
||||
},
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('Error processing Art Institute image:', error)
|
||||
@ -150,4 +177,4 @@ async function loadArtInstituteImage(size: number, index: number): Promise<Gener
|
||||
console.error('Error creating Met Museum image:', error)
|
||||
return null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -28,12 +28,12 @@ function generateBandLayer(canvasWidth: number, canvasHeight: number): BandLayer
|
||||
const x = i * sectionWidth
|
||||
const yValue = Math.random() // 0.0 to 1.0
|
||||
// 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({
|
||||
x,
|
||||
width: sectionWidth,
|
||||
y
|
||||
y,
|
||||
})
|
||||
}
|
||||
}
|
||||
@ -41,7 +41,7 @@ function generateBandLayer(canvasWidth: number, canvasHeight: number): BandLayer
|
||||
return {
|
||||
divisions,
|
||||
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 => ({
|
||||
x: band.x,
|
||||
width: band.width,
|
||||
y: band.y
|
||||
y: band.y,
|
||||
})),
|
||||
opacity: layer.opacity
|
||||
opacity: layer.opacity,
|
||||
})),
|
||||
strokeHeight,
|
||||
size
|
||||
}
|
||||
size,
|
||||
},
|
||||
}
|
||||
|
||||
images.push(image)
|
||||
@ -113,4 +113,4 @@ export function generateBandsImages(count: number, size: number): GeneratedImage
|
||||
}
|
||||
|
||||
return images
|
||||
}
|
||||
}
|
||||
|
||||
@ -16,36 +16,50 @@ const greyLevels = [
|
||||
'#333333', // Almost black
|
||||
'#222222', // Nearly black
|
||||
'#111111', // Extremely dark
|
||||
'#0f0f0f' // Almost black (minimal amplitude)
|
||||
'#0f0f0f', // Almost black (minimal amplitude)
|
||||
]
|
||||
|
||||
interface DustParams {
|
||||
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
|
||||
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 = []
|
||||
for (let i = 0; i < count; i++) {
|
||||
points.push({
|
||||
x: Math.random() * size,
|
||||
y: Math.random() * size
|
||||
y: Math.random() * size,
|
||||
})
|
||||
}
|
||||
return points
|
||||
}
|
||||
|
||||
function generateClusteredDistribution(count: number, size: number, clusterCount: number, concentration: number): Array<{x: number, y: number}> {
|
||||
const points = []
|
||||
const clusters = []
|
||||
function generateClusteredDistribution(
|
||||
count: number,
|
||||
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
|
||||
for (let i = 0; i < clusterCount; i++) {
|
||||
clusters.push({
|
||||
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({
|
||||
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
|
||||
}
|
||||
|
||||
function generateScatteredDistribution(count: number, size: number, concentration: number): Array<{x: number, y: number}> {
|
||||
const points = []
|
||||
function generateScatteredDistribution(
|
||||
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
|
||||
|
||||
for (let i = 0; i < count; i++) {
|
||||
let attempts = 0
|
||||
let point: {x: number, y: number}
|
||||
let point: { x: number; y: number }
|
||||
|
||||
do {
|
||||
point = {
|
||||
x: Math.random() * size,
|
||||
y: Math.random() * size
|
||||
y: Math.random() * size,
|
||||
}
|
||||
attempts++
|
||||
} while (attempts < 50 && points.some(p =>
|
||||
Math.sqrt((p.x - point.x) ** 2 + (p.y - point.y) ** 2) < exclusionRadius
|
||||
))
|
||||
} while (
|
||||
attempts < 50 &&
|
||||
points.some(p => Math.sqrt((p.x - point.x) ** 2 + (p.y - point.y) ** 2) < exclusionRadius)
|
||||
)
|
||||
|
||||
points.push(point)
|
||||
}
|
||||
@ -96,7 +115,11 @@ function generateScatteredDistribution(count: number, size: number, concentratio
|
||||
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 centerX = size / 2
|
||||
const centerY = size / 2
|
||||
@ -110,14 +133,18 @@ function generateRingDistribution(count: number, size: number, concentration: nu
|
||||
|
||||
points.push({
|
||||
x: centerX + Math.cos(angle) * finalRadius,
|
||||
y: centerY + Math.sin(angle) * finalRadius
|
||||
y: centerY + Math.sin(angle) * finalRadius,
|
||||
})
|
||||
}
|
||||
|
||||
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 centerX = size / 2
|
||||
const centerY = size / 2
|
||||
@ -131,14 +158,18 @@ function generateSpiralDistribution(count: number, size: number, concentration:
|
||||
|
||||
points.push({
|
||||
x: centerX + Math.cos(t) * radius + noise,
|
||||
y: centerY + Math.sin(t) * radius + noise
|
||||
y: centerY + Math.sin(t) * radius + noise,
|
||||
})
|
||||
}
|
||||
|
||||
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 gridSize = Math.ceil(Math.sqrt(count))
|
||||
const cellSize = size / gridSize
|
||||
@ -156,15 +187,18 @@ function generateGridDistribution(count: number, size: number, concentration: nu
|
||||
|
||||
points.push({
|
||||
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
|
||||
}
|
||||
|
||||
|
||||
function generateNoiseDistribution(count: number, size: number, concentration: number): Array<{x: number, y: number}> {
|
||||
function generateNoiseDistribution(
|
||||
count: number,
|
||||
size: number,
|
||||
concentration: number
|
||||
): Array<{ x: number; y: number }> {
|
||||
const points = []
|
||||
const gridSize = 64
|
||||
const cellSize = size / gridSize
|
||||
@ -177,7 +211,7 @@ function generateNoiseDistribution(count: number, size: number, concentration: n
|
||||
|
||||
for (let i = 0; i < count; i++) {
|
||||
let attempts = 0
|
||||
let point: {x: number, y: number}
|
||||
let point: { x: number; y: number }
|
||||
|
||||
do {
|
||||
const x = Math.random() * size
|
||||
@ -204,7 +238,11 @@ function generateNoiseDistribution(count: number, size: number, concentration: n
|
||||
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 centerX = size / 2
|
||||
const centerY = size / 2
|
||||
@ -221,14 +259,18 @@ function generateRadialDistribution(count: number, size: number, concentration:
|
||||
|
||||
points.push({
|
||||
x: centerX + Math.cos(angle) * finalRadius,
|
||||
y: centerY + Math.sin(angle) * finalRadius
|
||||
y: centerY + Math.sin(angle) * finalRadius,
|
||||
})
|
||||
}
|
||||
|
||||
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 scale = 0.01 + concentration * 0.05
|
||||
|
||||
@ -243,7 +285,7 @@ function generatePerlinLikeDistribution(count: number, size: number, concentrati
|
||||
|
||||
for (let i = 0; i < count; i++) {
|
||||
let attempts = 0
|
||||
let point: {x: number, y: number}
|
||||
let point: { x: number; y: number }
|
||||
|
||||
do {
|
||||
const x = Math.random() * size
|
||||
@ -270,7 +312,17 @@ function generatePerlinLikeDistribution(count: number, size: number, concentrati
|
||||
export function generateDustImages(count: number, size: number): 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++) {
|
||||
try {
|
||||
@ -295,11 +347,11 @@ export function generateDustImages(count: number, size: number): GeneratedImage[
|
||||
pointCount,
|
||||
distributionType,
|
||||
concentration,
|
||||
clusterCount
|
||||
clusterCount,
|
||||
}
|
||||
|
||||
// Generate points based on distribution type
|
||||
let points: Array<{x: number, y: number}>
|
||||
let points: Array<{ x: number; y: number }>
|
||||
|
||||
switch (distributionType) {
|
||||
case 'uniform':
|
||||
@ -350,8 +402,8 @@ export function generateDustImages(count: number, size: number): GeneratedImage[
|
||||
...params,
|
||||
pointColor,
|
||||
actualPointCount: points.length,
|
||||
size
|
||||
}
|
||||
size,
|
||||
},
|
||||
}
|
||||
|
||||
images.push(image)
|
||||
@ -361,4 +413,4 @@ export function generateDustImages(count: number, size: number): GeneratedImage[
|
||||
}
|
||||
|
||||
return images
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,2 +1,2 @@
|
||||
export * from './types'
|
||||
export * from './processor'
|
||||
export * from './processor'
|
||||
|
||||
@ -36,7 +36,7 @@ export function processPhoto(file: File, config: PhotoProcessingConfig): Promise
|
||||
canvas,
|
||||
imageData,
|
||||
originalFile: file,
|
||||
config
|
||||
config,
|
||||
})
|
||||
} catch (error) {
|
||||
reject(error)
|
||||
@ -55,7 +55,7 @@ function calculateImageDimensions(
|
||||
imgWidth: number,
|
||||
imgHeight: number,
|
||||
targetSize: number
|
||||
): { drawWidth: number, drawHeight: number, offsetX: number, offsetY: number } {
|
||||
): { drawWidth: number; drawHeight: number; offsetX: number; offsetY: number } {
|
||||
const aspectRatio = imgWidth / imgHeight
|
||||
|
||||
let drawWidth: number
|
||||
@ -96,7 +96,7 @@ function convertToGrayscale(ctx: CanvasRenderingContext2D, size: number, enhance
|
||||
}
|
||||
|
||||
// Set R, G, B to the same grayscale value
|
||||
data[i] = final // Red
|
||||
data[i] = final // Red
|
||||
data[i + 1] = final // Green
|
||||
data[i + 2] = final // Blue
|
||||
// Alpha (data[i + 3]) remains unchanged
|
||||
@ -104,4 +104,4 @@ function convertToGrayscale(ctx: CanvasRenderingContext2D, size: number, enhance
|
||||
|
||||
// Put the processed data back
|
||||
ctx.putImageData(imageData, 0, 0)
|
||||
}
|
||||
}
|
||||
|
||||
@ -9,4 +9,4 @@ export interface PhotoResult {
|
||||
imageData: ImageData
|
||||
originalFile: File
|
||||
config: PhotoProcessingConfig
|
||||
}
|
||||
}
|
||||
|
||||
@ -6,7 +6,7 @@ export async function generateFromPhotoImage(file: File, size: number): Promise<
|
||||
const result = await processPhoto(file, {
|
||||
targetSize: size,
|
||||
contrastEnhancement: true,
|
||||
grayscaleConversion: true
|
||||
grayscaleConversion: true,
|
||||
})
|
||||
|
||||
const image: GeneratedImage = {
|
||||
@ -21,8 +21,8 @@ export async function generateFromPhotoImage(file: File, size: number): Promise<
|
||||
processedAt: new Date().toISOString(),
|
||||
size,
|
||||
contrastEnhanced: true,
|
||||
grayscaleConverted: true
|
||||
}
|
||||
grayscaleConverted: true,
|
||||
},
|
||||
}
|
||||
|
||||
return image
|
||||
@ -46,10 +46,10 @@ export async function generateFromPhotoImage(file: File, size: number): Promise<
|
||||
params: {
|
||||
fileName: file.name,
|
||||
error: error instanceof Error ? error.message : 'Unknown error',
|
||||
size
|
||||
}
|
||||
size,
|
||||
},
|
||||
}
|
||||
|
||||
return fallbackImage
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -27,7 +27,7 @@ const PATTERN_TYPES = [
|
||||
'tessellation',
|
||||
'nestedSquares',
|
||||
'mosaicSquares',
|
||||
'chevrons'
|
||||
'chevrons',
|
||||
]
|
||||
|
||||
// Grayscale base colors for variety
|
||||
@ -47,7 +47,7 @@ const GRAYSCALE_COLORS = [
|
||||
'#cccccc', // Light grey
|
||||
'#dddddd', // Very light grey
|
||||
'#eeeeee', // Almost white
|
||||
'#ffffff' // Pure white
|
||||
'#ffffff', // Pure white
|
||||
]
|
||||
|
||||
function generateRandomSeed(): string {
|
||||
@ -60,7 +60,10 @@ function generateRandomSeed(): string {
|
||||
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) => {
|
||||
const canvas = document.createElement('canvas')
|
||||
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))
|
||||
|
||||
// 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 + 2] = final // Blue
|
||||
// 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[] = []
|
||||
|
||||
// Load the geopattern library dynamically
|
||||
@ -150,7 +156,7 @@ export async function generateGeopatternImages(count: number, size: number): Pro
|
||||
// Generate the geopattern
|
||||
const pattern = geopatternLib.generate(seed, {
|
||||
generator: patternType,
|
||||
baseColor: baseColor
|
||||
baseColor: baseColor,
|
||||
})
|
||||
|
||||
// Get SVG string
|
||||
@ -169,8 +175,8 @@ export async function generateGeopatternImages(count: number, size: number): Pro
|
||||
seed,
|
||||
baseColor,
|
||||
originalSvg: svgString,
|
||||
size
|
||||
}
|
||||
size,
|
||||
},
|
||||
}
|
||||
|
||||
images.push(image)
|
||||
@ -196,8 +202,8 @@ export async function generateGeopatternImages(count: number, size: number): Pro
|
||||
seed: 'error',
|
||||
baseColor: '#000000',
|
||||
size,
|
||||
error: error instanceof Error ? error.message : 'Unknown error'
|
||||
}
|
||||
error: error instanceof Error ? error.message : 'Unknown error',
|
||||
},
|
||||
}
|
||||
|
||||
images.push(fallbackImage)
|
||||
@ -205,4 +211,4 @@ export async function generateGeopatternImages(count: number, size: number): Pro
|
||||
}
|
||||
|
||||
return images
|
||||
}
|
||||
}
|
||||
|
||||
@ -11,7 +11,7 @@ const greyLevels = [
|
||||
'#888888', // Very dark grey
|
||||
'#777777', // Darker grey
|
||||
'#666666', // Even darker grey
|
||||
'#555555' // Much darker grey
|
||||
'#555555', // Much darker grey
|
||||
]
|
||||
|
||||
interface HarmonicSeries {
|
||||
@ -54,7 +54,12 @@ function calculateHarmonicAmplitude(harmonicNumber: number, timbre: string): num
|
||||
}
|
||||
|
||||
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)]
|
||||
|
||||
// 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++) {
|
||||
// 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
|
||||
if (yPosition <= 0) break
|
||||
@ -78,14 +83,14 @@ function generateHarmonicSeries(canvasSize: number): HarmonicSeries {
|
||||
frequency: i,
|
||||
yPosition,
|
||||
amplitude,
|
||||
thickness
|
||||
thickness,
|
||||
})
|
||||
}
|
||||
|
||||
return {
|
||||
fundamental,
|
||||
harmonics,
|
||||
timbre
|
||||
timbre,
|
||||
}
|
||||
}
|
||||
|
||||
@ -137,10 +142,10 @@ export function generateHarmonicsImages(count: number, size: number): GeneratedI
|
||||
frequency: h.frequency,
|
||||
yPosition: h.yPosition,
|
||||
amplitude: h.amplitude,
|
||||
thickness: h.thickness
|
||||
thickness: h.thickness,
|
||||
})),
|
||||
size
|
||||
}
|
||||
size,
|
||||
},
|
||||
}
|
||||
|
||||
images.push(image)
|
||||
@ -150,4 +155,4 @@ export function generateHarmonicsImages(count: number, size: number): GeneratedI
|
||||
}
|
||||
|
||||
return images
|
||||
}
|
||||
}
|
||||
|
||||
@ -8,7 +8,7 @@ const greyLevels = [
|
||||
'#bbbbbb', // Medium grey
|
||||
'#aaaaaa', // Medium-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[] {
|
||||
@ -30,7 +30,7 @@ export function generatePartialsImages(count: number, size: number): GeneratedIm
|
||||
ctx.fillRect(0, 0, size, size)
|
||||
|
||||
// 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)
|
||||
|
||||
for (let j = 0; j < lineCount; j++) {
|
||||
@ -59,8 +59,8 @@ export function generatePartialsImages(count: number, size: number): GeneratedIm
|
||||
lineCount,
|
||||
linePositions: linePositions.map(l => ({ y: l.y, thickness: l.thickness })),
|
||||
lineColor,
|
||||
size
|
||||
}
|
||||
size,
|
||||
},
|
||||
}
|
||||
|
||||
images.push(image)
|
||||
@ -70,4 +70,4 @@ export function generatePartialsImages(count: number, size: number): GeneratedIm
|
||||
}
|
||||
|
||||
return images
|
||||
}
|
||||
}
|
||||
|
||||
@ -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,
|
||||
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,
|
||||
91, 92, 96, 97, 98, 99, 100, 101, 102, 103, 104, 106, 107, 108, 109, 110, 111, 112, 113, 116,
|
||||
119, 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,
|
||||
159, 160, 161, 162, 163, 164, 165, 166, 167, 168, 169, 170, 171, 172, 173, 174, 175, 176, 177, 178,
|
||||
179, 180, 181, 182, 183, 184, 185, 186, 187, 188, 189, 190, 191, 192, 193, 194, 195, 196, 197, 198,
|
||||
199, 200, 201, 202, 203, 204, 205, 206, 207, 208, 209, 210, 211, 212, 213, 214, 215, 216, 217, 218,
|
||||
219, 220, 221, 222, 223, 224, 225, 226, 227, 228, 229, 230, 231, 232, 233, 234, 235, 236, 237, 238,
|
||||
239, 240, 241, 242, 243, 244, 245, 246, 247, 248, 249, 250, 251, 252, 253, 254, 255, 256, 257, 258,
|
||||
259, 260, 261, 262, 263, 264, 265, 266, 267, 268, 269, 270, 271, 272, 273, 274, 275, 276, 277, 278,
|
||||
279, 280, 281, 282, 283, 284, 285, 286, 287, 288, 289, 290, 291, 292, 293, 294, 295, 296, 297, 298,
|
||||
299, 300, 301, 302, 303, 304, 305, 306, 307, 308, 309, 310, 311, 312, 313, 314, 315, 316, 317, 318,
|
||||
319, 320, 321, 322, 323, 324, 325, 326, 327, 328, 329, 330, 331, 332, 333, 334, 335, 336, 337, 338,
|
||||
339, 340, 341, 342, 343, 344, 345, 346, 347, 348, 349, 350, 351, 352, 353, 354, 355, 356, 357, 358,
|
||||
359, 360, 361, 362, 363, 364, 365, 366, 367, 368, 369, 370, 371, 372, 373, 374, 375, 376, 377, 378,
|
||||
379, 380, 381, 382, 383, 384, 385, 386, 387, 388, 389, 390, 391, 392, 393, 394, 395, 396, 397, 398,
|
||||
399, 400, 401, 402, 403, 404, 405, 406, 407, 408, 409, 410, 411, 412, 413, 414, 415, 416, 417, 418,
|
||||
419, 420, 421, 422, 423, 424, 425, 426, 427, 428, 429, 430, 431, 432, 433, 434, 435, 436, 437, 438,
|
||||
439, 440, 441, 442, 443, 444, 445, 446, 447, 448, 449, 450, 451, 452, 453, 454, 455, 456, 457, 458,
|
||||
459, 460, 461, 462, 463, 464, 465, 466, 467, 468, 469, 470, 471, 472, 473, 474, 475, 476, 477, 478,
|
||||
479, 480, 481, 482, 483, 484, 485, 486, 487, 488, 489, 490, 491, 492, 493, 494, 495, 496, 497, 498,
|
||||
499, 500, 501, 502, 503, 504, 505, 506, 507, 508, 509, 510, 511, 512, 513, 514, 515, 516, 517, 518,
|
||||
519, 520, 521, 522, 523, 524, 525, 526, 527, 528, 529, 530, 531, 532, 533, 534, 535, 536, 537, 538,
|
||||
539, 540, 541, 542, 543, 544, 545, 546, 547, 548, 549, 550, 551, 552, 553, 554, 555, 556, 557, 558,
|
||||
559, 560, 561, 562, 563, 564, 565, 566, 567, 568, 569, 570, 571, 572, 573, 574, 575, 576, 577, 578,
|
||||
579, 580, 581, 582, 583, 584, 585, 586, 587, 588, 589, 590, 591, 592, 593, 594, 595, 596, 597, 598,
|
||||
599, 600, 601, 602, 603, 604, 605, 606, 607, 608, 609, 610, 611, 612, 613, 614, 615, 616, 617, 618,
|
||||
619, 620, 621, 622, 623, 624, 625, 626, 627, 628, 629, 630, 631, 632, 633, 634, 635, 636, 637, 638,
|
||||
639, 640, 641, 642, 643, 644, 645, 646, 647, 648, 649, 650, 651, 652, 653, 654, 655, 656, 657, 658,
|
||||
659, 660, 661, 662, 663, 664, 665, 666, 667, 668, 669, 670, 671, 672, 673, 674, 675, 676, 677, 678,
|
||||
679, 680, 681, 682, 683, 684, 685, 686, 687, 688, 689, 690, 691, 692, 693, 694, 695, 696, 697, 698,
|
||||
699, 700, 701, 702, 703, 704, 705, 706, 707, 708, 709, 710, 711, 712, 713, 714, 715, 716, 717, 718,
|
||||
719, 720, 721, 722, 723, 724, 725, 726, 727, 728, 729, 730, 731, 732, 733, 734, 735, 736, 737, 738,
|
||||
739, 740, 741, 742, 743, 744, 745, 746, 747, 748, 749, 750, 751, 752, 753, 754, 755, 756, 757, 758,
|
||||
759, 760, 761, 762, 763, 764, 765, 766, 767, 768, 769, 770, 771, 772, 773, 774, 775, 776, 777, 778,
|
||||
779, 780, 781, 782, 783, 784, 785, 786, 787, 788, 789, 790, 791, 792, 793, 794, 795, 796, 797, 798,
|
||||
799, 800, 801, 802, 803, 804, 805, 806, 807, 808, 809, 810, 811, 812, 813, 814, 815, 816, 817, 818,
|
||||
819, 820, 821, 822, 823, 824, 825, 826, 827, 828, 829, 830, 831, 832, 833, 834, 835, 836, 837, 838,
|
||||
839, 840, 841, 842, 843, 844, 845, 846, 847, 848, 849, 850, 851, 852, 853, 854, 855, 856, 857, 858,
|
||||
859, 860, 861, 862, 863, 864, 865, 866, 867, 868, 869, 870, 871, 872, 873, 874, 875, 876, 877, 878,
|
||||
879, 880, 881, 882, 883, 884, 885, 886, 887, 888, 889, 890, 891, 892, 893, 894, 895, 896, 897, 898,
|
||||
899, 900, 901, 902, 903, 904, 905, 906, 907, 908, 909, 910, 911, 912, 913, 914, 915, 916, 917, 918,
|
||||
919, 920, 921, 922, 923, 924, 925, 926, 927, 928, 929, 930, 931, 932, 933, 934, 935, 936, 937, 938,
|
||||
939, 940, 941, 942, 943, 944, 945, 946, 947, 948, 949, 950, 951, 952, 953, 954, 955, 956, 957, 958,
|
||||
959, 960, 961, 962, 963, 964, 965, 966, 967, 968, 969, 970, 971, 972, 973, 974, 975, 976, 977, 978,
|
||||
979, 980, 981, 982, 983, 984, 985, 986, 987, 988, 989, 990, 991, 992, 993, 994, 995, 996, 997, 998,
|
||||
999, 1000, 1001, 1002, 1003, 1004, 1005, 1006, 1007, 1008, 1009, 1010, 1011, 1012, 1013, 1014, 1015, 1016,
|
||||
1017, 1018, 1019, 1020, 1021, 1022, 1023, 1024, 1025, 1026, 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
|
||||
91, 92, 96, 97, 98, 99, 100, 101, 102, 103, 104, 106, 107, 108, 109, 110, 111, 112, 113, 116, 119,
|
||||
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, 159, 160, 161, 162, 163, 164, 165, 166, 167, 168, 169, 170, 171, 172, 173, 174, 175, 176,
|
||||
177, 178, 179, 180, 181, 182, 183, 184, 185, 186, 187, 188, 189, 190, 191, 192, 193, 194, 195,
|
||||
196, 197, 198, 199, 200, 201, 202, 203, 204, 205, 206, 207, 208, 209, 210, 211, 212, 213, 214,
|
||||
215, 216, 217, 218, 219, 220, 221, 222, 223, 224, 225, 226, 227, 228, 229, 230, 231, 232, 233,
|
||||
234, 235, 236, 237, 238, 239, 240, 241, 242, 243, 244, 245, 246, 247, 248, 249, 250, 251, 252,
|
||||
253, 254, 255, 256, 257, 258, 259, 260, 261, 262, 263, 264, 265, 266, 267, 268, 269, 270, 271,
|
||||
272, 273, 274, 275, 276, 277, 278, 279, 280, 281, 282, 283, 284, 285, 286, 287, 288, 289, 290,
|
||||
291, 292, 293, 294, 295, 296, 297, 298, 299, 300, 301, 302, 303, 304, 305, 306, 307, 308, 309,
|
||||
310, 311, 312, 313, 314, 315, 316, 317, 318, 319, 320, 321, 322, 323, 324, 325, 326, 327, 328,
|
||||
329, 330, 331, 332, 333, 334, 335, 336, 337, 338, 339, 340, 341, 342, 343, 344, 345, 346, 347,
|
||||
348, 349, 350, 351, 352, 353, 354, 355, 356, 357, 358, 359, 360, 361, 362, 363, 364, 365, 366,
|
||||
367, 368, 369, 370, 371, 372, 373, 374, 375, 376, 377, 378, 379, 380, 381, 382, 383, 384, 385,
|
||||
386, 387, 388, 389, 390, 391, 392, 393, 394, 395, 396, 397, 398, 399, 400, 401, 402, 403, 404,
|
||||
405, 406, 407, 408, 409, 410, 411, 412, 413, 414, 415, 416, 417, 418, 419, 420, 421, 422, 423,
|
||||
424, 425, 426, 427, 428, 429, 430, 431, 432, 433, 434, 435, 436, 437, 438, 439, 440, 441, 442,
|
||||
443, 444, 445, 446, 447, 448, 449, 450, 451, 452, 453, 454, 455, 456, 457, 458, 459, 460, 461,
|
||||
462, 463, 464, 465, 466, 467, 468, 469, 470, 471, 472, 473, 474, 475, 476, 477, 478, 479, 480,
|
||||
481, 482, 483, 484, 485, 486, 487, 488, 489, 490, 491, 492, 493, 494, 495, 496, 497, 498, 499,
|
||||
500, 501, 502, 503, 504, 505, 506, 507, 508, 509, 510, 511, 512, 513, 514, 515, 516, 517, 518,
|
||||
519, 520, 521, 522, 523, 524, 525, 526, 527, 528, 529, 530, 531, 532, 533, 534, 535, 536, 537,
|
||||
538, 539, 540, 541, 542, 543, 544, 545, 546, 547, 548, 549, 550, 551, 552, 553, 554, 555, 556,
|
||||
557, 558, 559, 560, 561, 562, 563, 564, 565, 566, 567, 568, 569, 570, 571, 572, 573, 574, 575,
|
||||
576, 577, 578, 579, 580, 581, 582, 583, 584, 585, 586, 587, 588, 589, 590, 591, 592, 593, 594,
|
||||
595, 596, 597, 598, 599, 600, 601, 602, 603, 604, 605, 606, 607, 608, 609, 610, 611, 612, 613,
|
||||
614, 615, 616, 617, 618, 619, 620, 621, 622, 623, 624, 625, 626, 627, 628, 629, 630, 631, 632,
|
||||
633, 634, 635, 636, 637, 638, 639, 640, 641, 642, 643, 644, 645, 646, 647, 648, 649, 650, 651,
|
||||
652, 653, 654, 655, 656, 657, 658, 659, 660, 661, 662, 663, 664, 665, 666, 667, 668, 669, 670,
|
||||
671, 672, 673, 674, 675, 676, 677, 678, 679, 680, 681, 682, 683, 684, 685, 686, 687, 688, 689,
|
||||
690, 691, 692, 693, 694, 695, 696, 697, 698, 699, 700, 701, 702, 703, 704, 705, 706, 707, 708,
|
||||
709, 710, 711, 712, 713, 714, 715, 716, 717, 718, 719, 720, 721, 722, 723, 724, 725, 726, 727,
|
||||
728, 729, 730, 731, 732, 733, 734, 735, 736, 737, 738, 739, 740, 741, 742, 743, 744, 745, 746,
|
||||
747, 748, 749, 750, 751, 752, 753, 754, 755, 756, 757, 758, 759, 760, 761, 762, 763, 764, 765,
|
||||
766, 767, 768, 769, 770, 771, 772, 773, 774, 775, 776, 777, 778, 779, 780, 781, 782, 783, 784,
|
||||
785, 786, 787, 788, 789, 790, 791, 792, 793, 794, 795, 796, 797, 798, 799, 800, 801, 802, 803,
|
||||
804, 805, 806, 807, 808, 809, 810, 811, 812, 813, 814, 815, 816, 817, 818, 819, 820, 821, 822,
|
||||
823, 824, 825, 826, 827, 828, 829, 830, 831, 832, 833, 834, 835, 836, 837, 838, 839, 840, 841,
|
||||
842, 843, 844, 845, 846, 847, 848, 849, 850, 851, 852, 853, 854, 855, 856, 857, 858, 859, 860,
|
||||
861, 862, 863, 864, 865, 866, 867, 868, 869, 870, 871, 872, 873, 874, 875, 876, 877, 878, 879,
|
||||
880, 881, 882, 883, 884, 885, 886, 887, 888, 889, 890, 891, 892, 893, 894, 895, 896, 897, 898,
|
||||
899, 900, 901, 902, 903, 904, 905, 906, 907, 908, 909, 910, 911, 912, 913, 914, 915, 916, 917,
|
||||
918, 919, 920, 921, 922, 923, 924, 925, 926, 927, 928, 929, 930, 931, 932, 933, 934, 935, 936,
|
||||
937, 938, 939, 940, 941, 942, 943, 944, 945, 946, 947, 948, 949, 950, 951, 952, 953, 954, 955,
|
||||
956, 957, 958, 959, 960, 961, 962, 963, 964, 965, 966, 967, 968, 969, 970, 971, 972, 973, 974,
|
||||
975, 976, 977, 978, 979, 980, 981, 982, 983, 984, 985, 986, 987, 988, 989, 990, 991, 992, 993,
|
||||
994, 995, 996, 997, 998, 999, 1000, 1001, 1002, 1003, 1004, 1005, 1006, 1007, 1008, 1009, 1010,
|
||||
1011, 1012, 1013, 1014, 1015, 1016, 1017, 1018, 1019, 1020, 1021, 1022, 1023, 1024, 1025, 1026,
|
||||
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
|
||||
@ -97,7 +100,7 @@ async function loadPicsumImage(size: number, index: number): Promise<GeneratedIm
|
||||
const img = new Image()
|
||||
img.crossOrigin = 'anonymous'
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
return new Promise((resolve, _reject) => {
|
||||
img.onload = () => {
|
||||
try {
|
||||
const canvas = document.createElement('canvas')
|
||||
@ -114,7 +117,7 @@ async function loadPicsumImage(size: number, index: number): Promise<GeneratedIm
|
||||
canvas,
|
||||
imageData,
|
||||
generator: 'picsum',
|
||||
params: { url, imageId }
|
||||
params: { url, imageId },
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('Error processing Picsum image:', error)
|
||||
@ -133,4 +136,4 @@ async function loadPicsumImage(size: number, index: number): Promise<GeneratedIm
|
||||
console.error('Error creating Picsum image:', error)
|
||||
return null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,13 +1,32 @@
|
||||
import type { GeneratedImage } from '../stores'
|
||||
|
||||
type ShapeType = 'circle' | 'square' | 'triangle' | 'diamond' | 'pentagon' | 'hexagon' |
|
||||
'star' | 'cross' | 'ellipse' | 'rect' | 'octagon' | 'arrow' | 'ring'
|
||||
type ShapeType =
|
||||
| 'circle'
|
||||
| 'square'
|
||||
| 'triangle'
|
||||
| 'diamond'
|
||||
| 'pentagon'
|
||||
| 'hexagon'
|
||||
| 'star'
|
||||
| 'cross'
|
||||
| 'ellipse'
|
||||
| 'rect'
|
||||
| 'octagon'
|
||||
| 'arrow'
|
||||
| 'ring'
|
||||
|
||||
type FillStyle = 'solid' | 'outline'
|
||||
|
||||
const greyLevels = [
|
||||
'#ffffff', '#eeeeee', '#dddddd', '#cccccc',
|
||||
'#bbbbbb', '#aaaaaa', '#999999', '#888888', '#777777'
|
||||
'#ffffff',
|
||||
'#eeeeee',
|
||||
'#dddddd',
|
||||
'#cccccc',
|
||||
'#bbbbbb',
|
||||
'#aaaaaa',
|
||||
'#999999',
|
||||
'#888888',
|
||||
'#777777',
|
||||
]
|
||||
|
||||
interface Shape {
|
||||
@ -172,8 +191,8 @@ function drawPolygon(ctx: CanvasRenderingContext2D, shape: Shape, sides: number)
|
||||
|
||||
for (let i = 0; i < sides; i++) {
|
||||
const angle = (i * 2 * Math.PI) / sides
|
||||
const px = Math.cos(angle) * shape.size / 2
|
||||
const py = Math.sin(angle) * shape.size / 2
|
||||
const px = (Math.cos(angle) * shape.size) / 2
|
||||
const py = (Math.sin(angle) * shape.size) / 2
|
||||
|
||||
if (i === 0) {
|
||||
ctx.moveTo(px, py)
|
||||
@ -234,8 +253,19 @@ function drawShape(ctx: CanvasRenderingContext2D, shape: Shape) {
|
||||
export function generateShapesImages(count: number, size: number): GeneratedImage[] {
|
||||
const images: GeneratedImage[] = []
|
||||
const shapeTypes: ShapeType[] = [
|
||||
'circle', 'square', 'triangle', 'diamond', 'pentagon', 'hexagon',
|
||||
'star', 'cross', 'ellipse', 'rect', 'octagon', 'arrow', 'ring'
|
||||
'circle',
|
||||
'square',
|
||||
'triangle',
|
||||
'diamond',
|
||||
'pentagon',
|
||||
'hexagon',
|
||||
'star',
|
||||
'cross',
|
||||
'ellipse',
|
||||
'rect',
|
||||
'octagon',
|
||||
'arrow',
|
||||
'ring',
|
||||
]
|
||||
const fillStyles: FillStyle[] = ['solid', 'outline']
|
||||
|
||||
@ -313,7 +343,7 @@ export function generateShapesImages(count: number, size: number): GeneratedImag
|
||||
rotation: Math.random() * Math.PI * 2,
|
||||
color,
|
||||
fillStyle,
|
||||
strokeWidth: Math.floor(Math.random() * 4) + 1 // 1-4px stroke
|
||||
strokeWidth: Math.floor(Math.random() * 4) + 1, // 1-4px stroke
|
||||
}
|
||||
|
||||
shapes.push(shape)
|
||||
@ -342,10 +372,10 @@ export function generateShapesImages(count: number, size: number): GeneratedImag
|
||||
rotation: s.rotation,
|
||||
color: s.color,
|
||||
fillStyle: s.fillStyle,
|
||||
strokeWidth: s.strokeWidth
|
||||
strokeWidth: s.strokeWidth,
|
||||
})),
|
||||
size
|
||||
}
|
||||
size,
|
||||
},
|
||||
}
|
||||
|
||||
images.push(image)
|
||||
@ -355,4 +385,4 @@ export function generateShapesImages(count: number, size: number): GeneratedImag
|
||||
}
|
||||
|
||||
return images
|
||||
}
|
||||
}
|
||||
|
||||
@ -11,10 +11,18 @@ const greyLevels = [
|
||||
'#888888', // Very dark grey (lower amplitude)
|
||||
'#777777', // 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 {
|
||||
x: number
|
||||
@ -28,7 +36,12 @@ interface SlideLine {
|
||||
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 steps = width
|
||||
|
||||
@ -42,10 +55,10 @@ function generateStrokePath(startY: number, endY: number, width: number, strokeT
|
||||
y = startY + (endY - startY) * t
|
||||
break
|
||||
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
|
||||
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
|
||||
case 'cubic':
|
||||
const easedT = t * t * (3 - 2 * t) // smooth step
|
||||
@ -57,24 +70,28 @@ function generateStrokePath(startY: number, endY: number, width: number, strokeT
|
||||
break
|
||||
case 'bounce':
|
||||
let bounceT = t
|
||||
if (bounceT < 1/2.75) {
|
||||
if (bounceT < 1 / 2.75) {
|
||||
bounceT = 7.5625 * bounceT * bounceT
|
||||
} else if (bounceT < 2/2.75) {
|
||||
bounceT = 7.5625 * (bounceT - 1.5/2.75) * (bounceT - 1.5/2.75) + 0.75
|
||||
} else if (bounceT < 2.5/2.75) {
|
||||
bounceT = 7.5625 * (bounceT - 2.25/2.75) * (bounceT - 2.25/2.75) + 0.9375
|
||||
} else if (bounceT < 2 / 2.75) {
|
||||
bounceT = 7.5625 * (bounceT - 1.5 / 2.75) * (bounceT - 1.5 / 2.75) + 0.75
|
||||
} else if (bounceT < 2.5 / 2.75) {
|
||||
bounceT = 7.5625 * (bounceT - 2.25 / 2.75) * (bounceT - 2.25 / 2.75) + 0.9375
|
||||
} 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
|
||||
break
|
||||
case 'elastic':
|
||||
const elasticT = t === 0 ? 0 : t === 1 ? 1 :
|
||||
Math.pow(2, -10 * t) * Math.sin((t * 10 - 0.75) * (2 * Math.PI) / 3) + 1
|
||||
const elasticT =
|
||||
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
|
||||
break
|
||||
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
|
||||
break
|
||||
default:
|
||||
@ -107,7 +124,16 @@ export function generateSlidesImages(count: number, size: number): GeneratedImag
|
||||
|
||||
// Generate slides with more variety
|
||||
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++) {
|
||||
// More varied positioning - some can go edge to edge
|
||||
@ -153,11 +179,11 @@ export function generateSlidesImages(count: number, size: number): GeneratedImag
|
||||
startY: s.startY,
|
||||
endY: s.endY,
|
||||
thickness: s.thickness,
|
||||
strokeType: s.strokeType
|
||||
strokeType: s.strokeType,
|
||||
})),
|
||||
colors: slides.map(s => s.color),
|
||||
size
|
||||
}
|
||||
size,
|
||||
},
|
||||
}
|
||||
|
||||
images.push(image)
|
||||
@ -167,4 +193,4 @@ export function generateSlidesImages(count: number, size: number): GeneratedImag
|
||||
}
|
||||
|
||||
return images
|
||||
}
|
||||
}
|
||||
|
||||
@ -5,6 +5,7 @@ A standalone module for generating Tixy-like shader patterns in JavaScript/TypeS
|
||||
## What is Tixy?
|
||||
|
||||
Tixy is a minimalist programming language designed by Martin Kleppe for creating visual patterns using 4 variables:
|
||||
|
||||
- `t` - time
|
||||
- `i` - index (pixel index in the grid)
|
||||
- `x` - x coordinate
|
||||
@ -24,7 +25,7 @@ const result = renderTixyToCanvas(expression, {
|
||||
height: 64,
|
||||
time: 0,
|
||||
backgroundColor: '#000000',
|
||||
foregroundColor: '#ffffff'
|
||||
foregroundColor: '#ffffff',
|
||||
})
|
||||
|
||||
// Add to DOM
|
||||
@ -56,4 +57,4 @@ document.body.appendChild(result.canvas)
|
||||
- `TixyRenderOptions` - Rendering configuration
|
||||
- `TixyResult` - Canvas and ImageData result
|
||||
|
||||
Math functions like `sin`, `cos`, `sqrt` etc. are available without the `Math.` prefix.
|
||||
Math functions like `sin`, `cos`, `sqrt` etc. are available without the `Math.` prefix.
|
||||
|
||||
@ -2,9 +2,28 @@ import type { TixyFunction, TixyExpression } from './types'
|
||||
import { TixyFormulaGenerator } from './formula-generator'
|
||||
|
||||
const MATH_METHODS = [
|
||||
'abs', 'acos', 'asin', 'atan', 'atan2', 'ceil', 'cos', 'exp', 'floor',
|
||||
'log', 'max', 'min', 'pow', 'random', 'round', 'sin', 'sqrt', 'tan',
|
||||
'sinh', 'cosh', 'tanh', 'sign'
|
||||
'abs',
|
||||
'acos',
|
||||
'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 {
|
||||
@ -22,7 +41,7 @@ export function compileTixyExpression(code: string): TixyExpression {
|
||||
|
||||
return {
|
||||
code: processedCode,
|
||||
compiled
|
||||
compiled,
|
||||
}
|
||||
} catch (error) {
|
||||
throw new Error(`Failed to compile Tixy expression: ${error}`)
|
||||
@ -48,11 +67,11 @@ export function evaluateTixyExpression(
|
||||
const formulaGenerator = new TixyFormulaGenerator()
|
||||
|
||||
// Generate expressions dynamically
|
||||
export function generateTixyExpression(): { expression: string, description: string } {
|
||||
export function generateTixyExpression(): { expression: string; description: string } {
|
||||
const result = formulaGenerator.generateFormula()
|
||||
return {
|
||||
expression: result.expression,
|
||||
description: result.description
|
||||
description: result.description,
|
||||
}
|
||||
}
|
||||
|
||||
@ -61,10 +80,19 @@ export function getExampleExpressions(): Record<string, string> {
|
||||
const expressions: Record<string, string> = {}
|
||||
|
||||
// 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 => {
|
||||
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)
|
||||
expressions[result.expression] = result.description
|
||||
}
|
||||
@ -74,4 +102,4 @@ export function getExampleExpressions(): Record<string, string> {
|
||||
}
|
||||
|
||||
// Export for backward compatibility
|
||||
export const EXAMPLE_EXPRESSIONS = getExampleExpressions()
|
||||
export const EXAMPLE_EXPRESSIONS = getExampleExpressions()
|
||||
|
||||
@ -1,5 +1,18 @@
|
||||
export type Theme = 'organic' | 'geometric' | 'interference' | 'chaotic' | 'minimalist' | 'psychedelic' | 'bitwise'
|
||||
export type CoordinateSpace = 'cartesian' | 'polar' | 'log_polar' | 'hyperbolic' | 'wave_distort' | 'spiral'
|
||||
export type Theme =
|
||||
| 'organic'
|
||||
| 'geometric'
|
||||
| 'interference'
|
||||
| 'chaotic'
|
||||
| 'minimalist'
|
||||
| 'psychedelic'
|
||||
| 'bitwise'
|
||||
export type CoordinateSpace =
|
||||
| 'cartesian'
|
||||
| 'polar'
|
||||
| 'log_polar'
|
||||
| 'hyperbolic'
|
||||
| 'wave_distort'
|
||||
| 'spiral'
|
||||
|
||||
interface PatternConfig {
|
||||
weight: number
|
||||
@ -45,28 +58,28 @@ const COORDINATE_TRANSFORMS = {
|
||||
|
||||
polar: (x: string, y: string) => ({
|
||||
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) => ({
|
||||
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) => ({
|
||||
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) => ({
|
||||
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) => ({
|
||||
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
|
||||
@ -74,19 +87,27 @@ const PATTERN_GENERATORS = {
|
||||
// Wave patterns
|
||||
sine_wave: (freq: number, phase: number, axis: 'x' | 'y' | 'xy' | 'radial') => {
|
||||
switch (axis) {
|
||||
case 'x': return `sin(x*${freq.toFixed(2)}+t*${phase.toFixed(2)})`
|
||||
case 'y': 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)})`
|
||||
case 'x':
|
||||
return `sin(x*${freq.toFixed(2)}+t*${phase.toFixed(2)})`
|
||||
case 'y':
|
||||
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') => {
|
||||
switch (axis) {
|
||||
case 'x': return `cos(x*${freq.toFixed(2)}+t*${phase.toFixed(2)})`
|
||||
case 'y': 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)})`
|
||||
case 'x':
|
||||
return `cos(x*${freq.toFixed(2)}+t*${phase.toFixed(2)})`
|
||||
case 'y':
|
||||
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: (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: (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
|
||||
checkerboard: (size: number, phase: number) =>
|
||||
@ -148,9 +169,12 @@ const PATTERN_GENERATORS = {
|
||||
|
||||
modular_arithmetic: (modX: number, modY: number, operation: 'add' | 'mult' | 'xor') => {
|
||||
switch (operation) {
|
||||
case 'add': 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`
|
||||
case 'add':
|
||||
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)`,
|
||||
|
||||
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) =>
|
||||
`tanh((sin(x*${diffusion.toFixed(2)}+t)*cos(y*${diffusion.toFixed(2)}+t)-${reaction.toFixed(2)})*4)`,
|
||||
|
||||
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) =>
|
||||
`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
|
||||
@ -188,10 +212,13 @@ const THEME_CONFIGS: Record<Theme, ThemeConfig> = {
|
||||
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] } },
|
||||
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'],
|
||||
complexity: [2, 3]
|
||||
complexity: [2, 3],
|
||||
},
|
||||
|
||||
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] } },
|
||||
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] } },
|
||||
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'],
|
||||
complexity: [1, 2]
|
||||
complexity: [1, 2],
|
||||
},
|
||||
|
||||
interference: {
|
||||
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'] } },
|
||||
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'],
|
||||
complexity: [2, 3]
|
||||
complexity: [2, 3],
|
||||
},
|
||||
|
||||
chaotic: {
|
||||
@ -222,10 +255,13 @@ const THEME_CONFIGS: Record<Theme, ThemeConfig> = {
|
||||
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] } },
|
||||
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', '%', '^'],
|
||||
complexity: [2, 3]
|
||||
complexity: [2, 3],
|
||||
},
|
||||
|
||||
minimalist: {
|
||||
@ -233,44 +269,62 @@ const THEME_CONFIGS: Record<Theme, ThemeConfig> = {
|
||||
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] } },
|
||||
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', '&'],
|
||||
complexity: [1, 2]
|
||||
complexity: [1, 2],
|
||||
},
|
||||
|
||||
psychedelic: {
|
||||
patterns: {
|
||||
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] } },
|
||||
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', '^'],
|
||||
complexity: [3, 4]
|
||||
complexity: [3, 4],
|
||||
},
|
||||
|
||||
bitwise: {
|
||||
patterns: {
|
||||
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] } },
|
||||
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] } },
|
||||
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] } },
|
||||
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'],
|
||||
complexity: [1, 3]
|
||||
}
|
||||
complexity: [1, 3],
|
||||
},
|
||||
}
|
||||
|
||||
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
|
||||
const useCoordinateTransform = !varietyConfig || random() < 0.4
|
||||
const useThemeHybrid = !varietyConfig || random() < 0.3
|
||||
@ -304,15 +358,20 @@ export class TixyFormulaGenerator {
|
||||
// Coordinate transformation selection
|
||||
let coordinateSpace: CoordinateSpace = 'cartesian'
|
||||
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
|
||||
for (let i = 0; i < numPatterns; i++) {
|
||||
const patternType = weightedChoice(
|
||||
Object.fromEntries(
|
||||
Object.entries(config.patterns).map(([key, cfg]) => [key, cfg.weight])
|
||||
)
|
||||
Object.fromEntries(Object.entries(config.patterns).map(([key, cfg]) => [key, cfg.weight]))
|
||||
)
|
||||
|
||||
const patternConfig = config.patterns[patternType]
|
||||
@ -334,9 +393,10 @@ export class TixyFormulaGenerator {
|
||||
// Combine patterns with advanced techniques
|
||||
const expression = this.combinePatterns(patterns, config.combinators)
|
||||
|
||||
const themeDesc = hybridThemes.length > 0
|
||||
? `${selectedTheme}+${hybridThemes.slice(1).join('+')}`
|
||||
: selectedTheme
|
||||
const themeDesc =
|
||||
hybridThemes.length > 0
|
||||
? `${selectedTheme}+${hybridThemes.slice(1).join('+')}`
|
||||
: selectedTheme
|
||||
|
||||
const transformDesc = coordinateSpace !== 'cartesian' ? `[${coordinateSpace}]` : ''
|
||||
const couplingDesc = useParameterCoupling ? '[coupled]' : ''
|
||||
@ -344,11 +404,13 @@ export class TixyFormulaGenerator {
|
||||
return {
|
||||
expression,
|
||||
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> = {}
|
||||
|
||||
for (const [key, range] of Object.entries(paramConfig)) {
|
||||
@ -369,9 +431,19 @@ export class TixyFormulaGenerator {
|
||||
case 'cos_wave':
|
||||
return PATTERN_GENERATORS.cos_wave(params.freq, params.phase, params.axis)
|
||||
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':
|
||||
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':
|
||||
return PATTERN_GENERATORS.grid(params.sizeX, params.sizeY, params.phase)
|
||||
case 'noise':
|
||||
@ -381,7 +453,12 @@ export class TixyFormulaGenerator {
|
||||
case 'diamond':
|
||||
return PATTERN_GENERATORS.diamond(params.centerX, params.centerY, params.size, params.speed)
|
||||
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':
|
||||
return PATTERN_GENERATORS.mandelbrot_like(params.scale, params.iterations)
|
||||
case 'am_modulation':
|
||||
@ -431,7 +508,7 @@ export class TixyFormulaGenerator {
|
||||
const hybridConfig: ThemeConfig = {
|
||||
patterns: { ...primaryConfig.patterns },
|
||||
combinators: [...primaryConfig.combinators],
|
||||
complexity: primaryConfig.complexity
|
||||
complexity: primaryConfig.complexity,
|
||||
}
|
||||
|
||||
// Merge patterns from hybrid themes with reduced weights
|
||||
@ -441,7 +518,7 @@ export class TixyFormulaGenerator {
|
||||
if (!hybridConfig.patterns[patternName]) {
|
||||
hybridConfig.patterns[patternName] = {
|
||||
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
|
||||
}
|
||||
|
||||
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> = {}
|
||||
|
||||
// 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]
|
||||
|
||||
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
|
||||
return pattern
|
||||
.replace(/\bx\b/g, `(${transformedX})`)
|
||||
.replace(/\by\b/g, `(${transformedY})`)
|
||||
return pattern.replace(/\bx\b/g, `(${transformedX})`).replace(/\by\b/g, `(${transformedY})`)
|
||||
}
|
||||
|
||||
private combinePatterns(patterns: string[], combinators: string[]): string {
|
||||
@ -578,4 +656,4 @@ export class TixyFormulaGenerator {
|
||||
|
||||
return result
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -25,4 +25,4 @@ export interface TixyRenderOptions {
|
||||
export interface TixyResult {
|
||||
imageData: ImageData
|
||||
canvas: HTMLCanvasElement
|
||||
}
|
||||
}
|
||||
|
||||
@ -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 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',
|
||||
foregroundColor = '#ffffff',
|
||||
threshold = 0.3,
|
||||
pixelSize = 1
|
||||
pixelSize = 1,
|
||||
} = options
|
||||
|
||||
const canvas = document.createElement('canvas')
|
||||
@ -31,13 +31,14 @@ export function renderTixyToCanvas(
|
||||
const i = y * width + x
|
||||
const value = evaluateTixyExpression(expression, time, i, x, y)
|
||||
|
||||
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)),
|
||||
b: Math.round(fgColor.b * (Math.sign(value) > 0 ? 1 : 0.8))
|
||||
}
|
||||
: bgColor
|
||||
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)),
|
||||
b: Math.round(fgColor.b * (Math.sign(value) > 0 ? 1 : 0.8)),
|
||||
}
|
||||
: bgColor
|
||||
|
||||
for (let py = 0; py < pixelSize; py++) {
|
||||
for (let px = 0; px < pixelSize; px++) {
|
||||
@ -58,7 +59,7 @@ export function renderTixyToCanvas(
|
||||
|
||||
return {
|
||||
imageData,
|
||||
canvas
|
||||
canvas,
|
||||
}
|
||||
}
|
||||
|
||||
@ -71,9 +72,11 @@ export function renderTixyToImageData(
|
||||
|
||||
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)
|
||||
return result ? {
|
||||
r: parseInt(result[1], 16),
|
||||
g: parseInt(result[2], 16),
|
||||
b: parseInt(result[3], 16)
|
||||
} : { r: 0, g: 0, b: 0 }
|
||||
}
|
||||
return result
|
||||
? {
|
||||
r: parseInt(result[1], 16),
|
||||
g: parseInt(result[2], 16),
|
||||
b: parseInt(result[3], 16),
|
||||
}
|
||||
: { r: 0, g: 0, b: 0 }
|
||||
}
|
||||
|
||||
@ -9,7 +9,7 @@ const colorPalettes = [
|
||||
{ bg: '#1a1a1a', fg: '#ffffff' },
|
||||
{ bg: '#333333', fg: '#ffffff' },
|
||||
{ bg: '#000000', fg: '#666666' },
|
||||
{ bg: '#222222', fg: '#dddddd' }
|
||||
{ bg: '#222222', fg: '#dddddd' },
|
||||
]
|
||||
|
||||
export function generateTixyImages(count: number, size: number): GeneratedImage[] {
|
||||
@ -38,7 +38,7 @@ export function generateTixyImages(count: number, size: number): GeneratedImage[
|
||||
time,
|
||||
backgroundColor: palette.bg,
|
||||
foregroundColor: palette.fg,
|
||||
pixelSize: 4
|
||||
pixelSize: 4,
|
||||
})
|
||||
|
||||
const image = {
|
||||
@ -46,7 +46,7 @@ export function generateTixyImages(count: number, size: number): GeneratedImage[
|
||||
canvas: result.canvas,
|
||||
imageData: result.imageData,
|
||||
generator: 'tixy' as const,
|
||||
params: { expression, time, colors: palette }
|
||||
params: { expression, time, colors: palette },
|
||||
}
|
||||
|
||||
images.push(image)
|
||||
@ -56,4 +56,4 @@ export function generateTixyImages(count: number, size: number): GeneratedImage[
|
||||
}
|
||||
|
||||
return images
|
||||
}
|
||||
}
|
||||
|
||||
@ -24,7 +24,7 @@ const customWaveform = generateWaveform({
|
||||
splits: 32,
|
||||
interpolation: 'cubic',
|
||||
randomness: 'smooth',
|
||||
lineWidth: 3
|
||||
lineWidth: 3,
|
||||
})
|
||||
```
|
||||
|
||||
@ -33,27 +33,30 @@ const customWaveform = generateWaveform({
|
||||
### Main Functions
|
||||
|
||||
#### `generateWaveform(config?)`
|
||||
|
||||
Generate a waveform with optional configuration override.
|
||||
|
||||
#### `generateRandomWaveform()`
|
||||
|
||||
Generate a waveform with randomized parameters for maximum variety.
|
||||
|
||||
#### `generateWaveformBatch(count)`
|
||||
|
||||
Generate multiple random waveforms efficiently.
|
||||
|
||||
### Configuration
|
||||
|
||||
```typescript
|
||||
interface WaveformConfig {
|
||||
width: number // Canvas width (default: 256)
|
||||
height: number // Canvas height (default: 256)
|
||||
splits: number // Number of control points (8-64)
|
||||
interpolation: InterpolationType // Curve type
|
||||
randomness: RandomnessStrategy // Distribution strategy
|
||||
lineWidth: number // Stroke width (1-4)
|
||||
backgroundColor: string // Background color
|
||||
lineColor: string // Line color
|
||||
smoothness: number // Curve smoothness (0-1)
|
||||
width: number // Canvas width (default: 256)
|
||||
height: number // Canvas height (default: 256)
|
||||
splits: number // Number of control points (8-64)
|
||||
interpolation: InterpolationType // Curve type
|
||||
randomness: RandomnessStrategy // Distribution strategy
|
||||
lineWidth: number // Stroke width (1-4)
|
||||
backgroundColor: string // Background color
|
||||
lineColor: string // Line color
|
||||
smoothness: number // Curve smoothness (0-1)
|
||||
}
|
||||
```
|
||||
|
||||
@ -95,31 +98,34 @@ waveform-generator/
|
||||
## Examples
|
||||
|
||||
### Basic Waveform
|
||||
|
||||
```typescript
|
||||
const simple = generateWaveform({
|
||||
splits: 16,
|
||||
interpolation: 'linear'
|
||||
interpolation: 'linear',
|
||||
})
|
||||
```
|
||||
|
||||
### Smooth Organic Curves
|
||||
|
||||
```typescript
|
||||
const organic = generateWaveform({
|
||||
splits: 24,
|
||||
interpolation: 'cubic',
|
||||
randomness: 'smooth',
|
||||
smoothness: 0.7
|
||||
smoothness: 0.7,
|
||||
})
|
||||
```
|
||||
|
||||
### Sharp Electronic Waveform
|
||||
|
||||
```typescript
|
||||
const electronic = generateWaveform({
|
||||
splits: 32,
|
||||
interpolation: 'exponential',
|
||||
randomness: 'uniform',
|
||||
lineWidth: 1
|
||||
lineWidth: 1,
|
||||
})
|
||||
```
|
||||
|
||||
The generated waveforms are perfect for audio visualization and spectral synthesis applications.
|
||||
The generated waveforms are perfect for audio visualization and spectral synthesis applications.
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import type { ControlPoint, WaveformConfig, RandomnessStrategy } from './types'
|
||||
import type { ControlPoint, WaveformConfig, RandomnessStrategy, InterpolationType } from './types'
|
||||
import { smoothControlPoints, applyTension } from './interpolation'
|
||||
|
||||
// Generate random control points based on configuration
|
||||
@ -81,7 +81,8 @@ function generateRandomY(
|
||||
|
||||
// Generate gaussian-distributed random numbers using Box-Muller transform
|
||||
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 (v === 0) v = Math.random()
|
||||
|
||||
@ -106,7 +107,7 @@ export function generateWaveformVariation(baseConfig: WaveformConfig): WaveformC
|
||||
// Pure interpolation types (60% of patterns)
|
||||
{ type: 'pure', weight: 0.6 },
|
||||
// Blended/mixed interpolation (40% of patterns)
|
||||
{ type: 'blended', weight: 0.4 }
|
||||
{ type: 'blended', weight: 0.4 },
|
||||
]
|
||||
|
||||
const strategyType = weightedRandomChoice(strategies)
|
||||
@ -124,7 +125,7 @@ function generatePureInterpolationVariation(baseConfig: WaveformConfig): Wavefor
|
||||
{ interpolation: 'linear' as const, weight: 0.3 },
|
||||
{ interpolation: 'exponential' 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)
|
||||
@ -132,10 +133,10 @@ function generatePureInterpolationVariation(baseConfig: WaveformConfig): Wavefor
|
||||
return {
|
||||
...baseConfig,
|
||||
splits: randomChoice([8, 12, 16, 20, 24, 32]),
|
||||
interpolation: interpolationType,
|
||||
interpolation: interpolationType as InterpolationType,
|
||||
randomness: randomChoice(['uniform', 'gaussian', 'smooth'] as const),
|
||||
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,
|
||||
randomness: 'gaussian' as const,
|
||||
smoothness: 0.8,
|
||||
splits: 16
|
||||
splits: 16,
|
||||
},
|
||||
// Low smoothness + uniform = sharp chaotic lines
|
||||
{
|
||||
interpolation: 'linear' as const,
|
||||
randomness: 'uniform' as const,
|
||||
smoothness: 0.1,
|
||||
splits: 32
|
||||
splits: 32,
|
||||
},
|
||||
// Exponential + smooth = flowing acceleration curves
|
||||
{
|
||||
interpolation: 'exponential' as const,
|
||||
randomness: 'smooth' as const,
|
||||
smoothness: 0.6,
|
||||
splits: 20
|
||||
splits: 20,
|
||||
},
|
||||
// Logarithmic + gaussian = natural decay curves
|
||||
{
|
||||
interpolation: 'logarithmic' as const,
|
||||
randomness: 'gaussian' as const,
|
||||
smoothness: 0.5,
|
||||
splits: 24
|
||||
}
|
||||
splits: 24,
|
||||
},
|
||||
]
|
||||
|
||||
const config = randomChoice(blendedConfigs)
|
||||
@ -180,7 +181,7 @@ function generateBlendedInterpolationVariation(baseConfig: WaveformConfig): Wave
|
||||
return {
|
||||
...baseConfig,
|
||||
...config,
|
||||
lineWidth: 2
|
||||
lineWidth: 2,
|
||||
}
|
||||
}
|
||||
|
||||
@ -190,7 +191,7 @@ function randomChoice<T>(array: readonly T[]): T {
|
||||
}
|
||||
|
||||
// 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)
|
||||
let random = Math.random() * totalWeight
|
||||
|
||||
@ -209,5 +210,7 @@ export function generateMultipleWaveforms(
|
||||
count: number,
|
||||
baseConfig: 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,
|
||||
smoothness: number = 0.5
|
||||
): number {
|
||||
const dx = p2.x - p1.x
|
||||
const dy = p2.y - p1.y
|
||||
|
||||
switch (type) {
|
||||
@ -93,10 +92,7 @@ export function smoothControlPoints(
|
||||
}
|
||||
|
||||
// Calculate curve tension for natural-looking waveforms
|
||||
export function applyTension(
|
||||
points: ControlPoint[],
|
||||
tension: number = 0.5
|
||||
): ControlPoint[] {
|
||||
export function applyTension(points: ControlPoint[], tension: number = 0.5): ControlPoint[] {
|
||||
if (points.length <= 2) return points
|
||||
|
||||
const tensioned = [...points]
|
||||
@ -114,4 +110,4 @@ export function applyTension(
|
||||
}
|
||||
|
||||
return tensioned
|
||||
}
|
||||
}
|
||||
|
||||
@ -37,7 +37,7 @@ export const DEFAULT_WAVEFORM_CONFIG: WaveformConfig = {
|
||||
lineWidth: 0.5, // Very thin line for precise frequency definition
|
||||
backgroundColor: '#000000',
|
||||
lineColor: '#ffffff',
|
||||
smoothness: 0.5
|
||||
smoothness: 0.5,
|
||||
}
|
||||
|
||||
// Common split counts for variety
|
||||
@ -48,5 +48,5 @@ export const INTERPOLATION_WEIGHTS = {
|
||||
linear: 0.3,
|
||||
exponential: 0.25,
|
||||
logarithmic: 0.25,
|
||||
cubic: 0.2
|
||||
} as const
|
||||
cubic: 0.2,
|
||||
} as const
|
||||
|
||||
@ -6,35 +6,31 @@ export type {
|
||||
RandomnessStrategy,
|
||||
ControlPoint,
|
||||
WaveformConfig,
|
||||
WaveformResult
|
||||
WaveformResult,
|
||||
} from './core/types'
|
||||
|
||||
export {
|
||||
DEFAULT_WAVEFORM_CONFIG,
|
||||
SPLIT_COUNTS,
|
||||
INTERPOLATION_WEIGHTS
|
||||
} from './core/types'
|
||||
export { DEFAULT_WAVEFORM_CONFIG, SPLIT_COUNTS, INTERPOLATION_WEIGHTS } from './core/types'
|
||||
|
||||
// Interpolation functions
|
||||
export {
|
||||
interpolate,
|
||||
generateCurvePoints,
|
||||
smoothControlPoints,
|
||||
applyTension
|
||||
applyTension,
|
||||
} from './core/interpolation'
|
||||
|
||||
// Generation logic
|
||||
export {
|
||||
generateControlPoints,
|
||||
generateWaveformVariation,
|
||||
generateMultipleWaveforms
|
||||
generateMultipleWaveforms,
|
||||
} from './core/generator'
|
||||
|
||||
// Canvas rendering
|
||||
export {
|
||||
renderWaveformToCanvas,
|
||||
renderWaveformWithBezier,
|
||||
renderMultipleWaveforms
|
||||
renderMultipleWaveforms,
|
||||
} from './renderer/canvas'
|
||||
|
||||
// Main convenience function for generating complete waveforms
|
||||
@ -56,5 +52,7 @@ export function generateRandomWaveform(): 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,
|
||||
imageData,
|
||||
controlPoints,
|
||||
config
|
||||
config,
|
||||
}
|
||||
}
|
||||
|
||||
@ -114,7 +114,7 @@ export function renderWaveformWithBezier(
|
||||
canvas,
|
||||
imageData,
|
||||
controlPoints,
|
||||
config
|
||||
config,
|
||||
}
|
||||
}
|
||||
|
||||
@ -178,12 +178,7 @@ export function renderMultipleWaveforms(
|
||||
ctx.lineCap = 'round'
|
||||
ctx.lineJoin = 'round'
|
||||
|
||||
const curvePoints = generateCurvePoints(
|
||||
points,
|
||||
config.interpolation,
|
||||
config.smoothness,
|
||||
6
|
||||
)
|
||||
const curvePoints = generateCurvePoints(points, config.interpolation, config.smoothness, 6)
|
||||
|
||||
drawSmoothCurve(ctx, curvePoints)
|
||||
})
|
||||
@ -194,6 +189,6 @@ export function renderMultipleWaveforms(
|
||||
canvas,
|
||||
imageData,
|
||||
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'
|
||||
|
||||
// Generate multiple random waveform images
|
||||
@ -40,8 +42,8 @@ export function generateWaveformImages(count: number, size: number = 256): Gener
|
||||
randomness: waveform.config.randomness,
|
||||
smoothness: waveform.config.smoothness,
|
||||
lineWidth: waveform.config.lineWidth,
|
||||
size
|
||||
}
|
||||
size,
|
||||
},
|
||||
}
|
||||
|
||||
images.push(image)
|
||||
@ -91,8 +93,8 @@ export function generateVariedWaveforms(count: number, size: number = 256): Gene
|
||||
smoothness: waveform.config.smoothness,
|
||||
lineWidth: waveform.config.lineWidth,
|
||||
style: 'varied',
|
||||
size
|
||||
}
|
||||
size,
|
||||
},
|
||||
}
|
||||
|
||||
images.push(image)
|
||||
@ -102,4 +104,4 @@ export function generateVariedWaveforms(count: number, size: number = 256): Gene
|
||||
}
|
||||
|
||||
return images
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,2 +1,2 @@
|
||||
export * from './types'
|
||||
export * from './processor'
|
||||
export * from './processor'
|
||||
|
||||
@ -10,8 +10,8 @@ export class WebcamProcessor {
|
||||
video: {
|
||||
width: { ideal: 1280 },
|
||||
height: { ideal: 720 },
|
||||
facingMode: 'user'
|
||||
}
|
||||
facingMode: 'user',
|
||||
},
|
||||
})
|
||||
|
||||
this.video = document.createElement('video')
|
||||
@ -66,7 +66,7 @@ export class WebcamProcessor {
|
||||
canvas,
|
||||
imageData,
|
||||
capturedAt: new Date(),
|
||||
config
|
||||
config,
|
||||
}
|
||||
}
|
||||
|
||||
@ -89,7 +89,7 @@ export class WebcamProcessor {
|
||||
videoWidth: number,
|
||||
videoHeight: number,
|
||||
targetSize: number
|
||||
): { drawWidth: number, drawHeight: number, offsetX: number, offsetY: number } {
|
||||
): { drawWidth: number; drawHeight: number; offsetX: number; offsetY: number } {
|
||||
const aspectRatio = videoWidth / videoHeight
|
||||
|
||||
let drawWidth: number
|
||||
@ -114,7 +114,11 @@ export class WebcamProcessor {
|
||||
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 data = imageData.data
|
||||
|
||||
@ -133,7 +137,7 @@ export class WebcamProcessor {
|
||||
}
|
||||
|
||||
// Set R, G, B to the same grayscale value
|
||||
data[i] = final // Red
|
||||
data[i] = final // Red
|
||||
data[i + 1] = final // Green
|
||||
data[i + 2] = final // Blue
|
||||
// Alpha (data[i + 3]) remains unchanged
|
||||
@ -142,4 +146,4 @@ export class WebcamProcessor {
|
||||
// Put the processed data back
|
||||
ctx.putImageData(imageData, 0, 0)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -9,4 +9,4 @@ export interface WebcamCaptureResult {
|
||||
imageData: ImageData
|
||||
capturedAt: Date
|
||||
config: WebcamCaptureConfig
|
||||
}
|
||||
}
|
||||
|
||||
@ -9,7 +9,7 @@ export async function generateFromWebcamImage(
|
||||
const result = await processor.capturePhoto({
|
||||
targetSize: size,
|
||||
contrastEnhancement: true,
|
||||
grayscaleConversion: true
|
||||
grayscaleConversion: true,
|
||||
})
|
||||
|
||||
const image: GeneratedImage = {
|
||||
@ -21,8 +21,8 @@ export async function generateFromWebcamImage(
|
||||
capturedAt: result.capturedAt.toISOString(),
|
||||
size,
|
||||
contrastEnhanced: true,
|
||||
grayscaleConverted: true
|
||||
}
|
||||
grayscaleConverted: true,
|
||||
},
|
||||
}
|
||||
|
||||
return image
|
||||
@ -45,10 +45,10 @@ export async function generateFromWebcamImage(
|
||||
generator: 'webcam',
|
||||
params: {
|
||||
error: error instanceof Error ? error.message : 'Unknown error',
|
||||
size
|
||||
}
|
||||
size,
|
||||
},
|
||||
}
|
||||
|
||||
return fallbackImage
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1 +1 @@
|
||||
@import "tailwindcss";
|
||||
@import 'tailwindcss';
|
||||
|
||||
@ -6,5 +6,5 @@ import App from './App.tsx'
|
||||
createRoot(document.getElementById('root')!).render(
|
||||
<StrictMode>
|
||||
<App />
|
||||
</StrictMode>,
|
||||
</StrictMode>
|
||||
)
|
||||
|
||||
@ -23,7 +23,7 @@ const audioData = synthesizeFromImage(imageData, {
|
||||
duration: 10,
|
||||
minFreq: 100,
|
||||
maxFreq: 10000,
|
||||
maxPartials: 200
|
||||
maxPartials: 200,
|
||||
})
|
||||
|
||||
// Export as WAV
|
||||
@ -35,6 +35,7 @@ downloadWAV(audioData, 44100, 'my-audio.wav')
|
||||
### Main Functions
|
||||
|
||||
#### `synthesizeFromImage(imageData, params?)`
|
||||
|
||||
- **imageData**: `ImageData` - Canvas image data
|
||||
- **params**: `Partial<SynthesisParams>` - Optional parameters
|
||||
- **Returns**: `Float32Array` - Audio samples
|
||||
@ -42,16 +43,17 @@ downloadWAV(audioData, 44100, 'my-audio.wav')
|
||||
### Types
|
||||
|
||||
#### `SynthesisParams`
|
||||
|
||||
```typescript
|
||||
interface SynthesisParams {
|
||||
duration: number // Audio duration in seconds
|
||||
minFreq: number // Minimum frequency in Hz
|
||||
maxFreq: number // Maximum frequency in Hz
|
||||
sampleRate: number // Sample rate in Hz
|
||||
duration: number // Audio duration in seconds
|
||||
minFreq: number // Minimum frequency in Hz
|
||||
maxFreq: number // Maximum frequency in Hz
|
||||
sampleRate: number // Sample rate in Hz
|
||||
frequencyResolution: number // Frequency bin downsampling
|
||||
timeResolution: number // Time slice downsampling
|
||||
timeResolution: number // Time slice downsampling
|
||||
amplitudeThreshold: number // Minimum amplitude threshold
|
||||
maxPartials: number // Maximum simultaneous partials
|
||||
maxPartials: number // Maximum simultaneous partials
|
||||
}
|
||||
```
|
||||
|
||||
@ -80,6 +82,7 @@ spectral-synthesis/
|
||||
## Usage Examples
|
||||
|
||||
### Basic Synthesis
|
||||
|
||||
```typescript
|
||||
const canvas = document.createElement('canvas')
|
||||
const ctx = canvas.getContext('2d')
|
||||
@ -89,14 +92,15 @@ const audio = synthesizeFromImage(imageData)
|
||||
```
|
||||
|
||||
### Advanced Usage
|
||||
|
||||
```typescript
|
||||
import { ImageToAudioSynthesizer } from './spectral-synthesis'
|
||||
|
||||
const synthesizer = new ImageToAudioSynthesizer({
|
||||
duration: 5,
|
||||
maxPartials: 150
|
||||
maxPartials: 150,
|
||||
})
|
||||
|
||||
const result = synthesizer.synthesize(imageData)
|
||||
console.log(`Generated ${result.duration}s of audio`)
|
||||
```
|
||||
```
|
||||
|
||||
@ -25,7 +25,7 @@ export function createWAVBuffer(audioData: Float32Array, sampleRate: number): Ar
|
||||
let offset = 44
|
||||
for (let i = 0; i < length; 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
|
||||
}
|
||||
|
||||
@ -83,7 +83,9 @@ export function createAudioPlayer(audioData: Float32Array, sampleRate: number):
|
||||
gainNode.connect(audioContext.destination)
|
||||
|
||||
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) {
|
||||
// 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
|
||||
pausedAt = 0
|
||||
}
|
||||
@ -163,7 +166,7 @@ export function createAudioPlayer(audioData: Float32Array, sampleRate: number):
|
||||
|
||||
onStateChange(callback: (isPlaying: boolean) => void) {
|
||||
stateCallback = callback
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@ -174,11 +177,11 @@ export async function playAudio(audioData: Float32Array, sampleRate: number): Pr
|
||||
const player = createAudioPlayer(audioData, sampleRate)
|
||||
|
||||
return new Promise(resolve => {
|
||||
player.onStateChange((isPlaying) => {
|
||||
player.onStateChange(isPlaying => {
|
||||
if (!isPlaying) {
|
||||
resolve()
|
||||
}
|
||||
})
|
||||
player.play()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@ -9,7 +9,7 @@ import {
|
||||
generateSpectralDensity,
|
||||
mapFrequency,
|
||||
mapFrequencyLinear,
|
||||
normalizeAudioGlobal
|
||||
normalizeAudioGlobal,
|
||||
} from './utils'
|
||||
|
||||
/**
|
||||
@ -78,7 +78,7 @@ export class ImageToAudioSynthesizer {
|
||||
disableNormalization: false,
|
||||
disableContrast: false,
|
||||
exactBinMapping: false,
|
||||
...params
|
||||
...params,
|
||||
}
|
||||
}
|
||||
|
||||
@ -93,7 +93,6 @@ export class ImageToAudioSynthesizer {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Custom synthesis mode - sophisticated audio processing
|
||||
*/
|
||||
@ -112,7 +111,7 @@ export class ImageToAudioSynthesizer {
|
||||
spectralDensity,
|
||||
usePerceptualWeighting,
|
||||
frequencyMapping,
|
||||
invert = false
|
||||
invert = false,
|
||||
} = this.params
|
||||
|
||||
// Calculate synthesis parameters
|
||||
@ -136,7 +135,6 @@ export class ImageToAudioSynthesizer {
|
||||
const previousAmplitudes = new Float32Array(effectiveHeight)
|
||||
const smoothingFactor = 0.2 // Reduced for sharper transients
|
||||
|
||||
|
||||
// Process each time slice
|
||||
for (let col = 0; col < effectiveWidth; col++) {
|
||||
const sourceCol = col
|
||||
@ -160,9 +158,12 @@ export class ImageToAudioSynthesizer {
|
||||
return normalizedDb
|
||||
})
|
||||
|
||||
|
||||
// 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
|
||||
const partials: SpectralPeak[] = []
|
||||
@ -176,14 +177,21 @@ export class ImageToAudioSynthesizer {
|
||||
} else if (frequencyMapping === 'linear') {
|
||||
frequency = mapFrequencyLinear(peakRow, effectiveHeight, minFreq, maxFreq)
|
||||
} else {
|
||||
frequency = mapFrequency(peakRow, effectiveHeight, minFreq, maxFreq, frequencyMapping || 'mel')
|
||||
frequency = mapFrequency(
|
||||
peakRow,
|
||||
effectiveHeight,
|
||||
minFreq,
|
||||
maxFreq,
|
||||
frequencyMapping || 'mel'
|
||||
)
|
||||
}
|
||||
|
||||
let amplitude = processedSpectrum[peakRow]
|
||||
|
||||
// Apply temporal smoothing
|
||||
if (col > 0) {
|
||||
amplitude = smoothingFactor * previousAmplitudes[peakRow] + (1 - smoothingFactor) * amplitude
|
||||
amplitude =
|
||||
smoothingFactor * previousAmplitudes[peakRow] + (1 - smoothingFactor) * amplitude
|
||||
}
|
||||
previousAmplitudes[peakRow] = amplitude
|
||||
|
||||
@ -236,7 +244,7 @@ export class ImageToAudioSynthesizer {
|
||||
return {
|
||||
audio: normalizedAudio,
|
||||
sampleRate,
|
||||
duration
|
||||
duration,
|
||||
}
|
||||
}
|
||||
|
||||
@ -256,7 +264,7 @@ export class ImageToAudioSynthesizer {
|
||||
disableNormalization = false,
|
||||
disableContrast = false,
|
||||
exactBinMapping = true,
|
||||
invert = false
|
||||
invert = false,
|
||||
} = this.params
|
||||
|
||||
const totalSamples = Math.floor(duration * sampleRate)
|
||||
@ -282,7 +290,6 @@ export class ImageToAudioSynthesizer {
|
||||
}
|
||||
}
|
||||
// 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 {
|
||||
// Linear frequency mapping
|
||||
freqBins = []
|
||||
@ -339,16 +346,16 @@ export class ImageToAudioSynthesizer {
|
||||
const contrast = this.params.contrast || 1.0
|
||||
// Fast power optimization for common cases
|
||||
if (contrast === 1.0) {
|
||||
amplitude = intensity // No contrast
|
||||
amplitude = intensity // No contrast
|
||||
} 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) {
|
||||
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) {
|
||||
amplitude = intensity * intensity * intensity // Cube
|
||||
amplitude = intensity * intensity * intensity // Cube
|
||||
} else if (contrast === 4.0) {
|
||||
const sq = intensity * intensity
|
||||
amplitude = sq * sq // Fourth power
|
||||
amplitude = sq * sq // Fourth power
|
||||
} else {
|
||||
// Fast power approximation for arbitrary values
|
||||
// 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
|
||||
// Eliminates array lookups and multiplications in tight loop
|
||||
let phase = freqCoeff * startSample / sampleRate // Initial phase
|
||||
const phaseIncrement = freqCoeff / sampleRate // Phase per sample
|
||||
let phase = (freqCoeff * startSample) / sampleRate // Initial phase
|
||||
const phaseIncrement = freqCoeff / sampleRate // Phase per sample
|
||||
for (let i = 0; i < frameLength; i++) {
|
||||
columnSpectrum[i] += amplitude * Math.sin(phase)
|
||||
phase += phaseIncrement
|
||||
@ -415,7 +422,7 @@ export class ImageToAudioSynthesizer {
|
||||
return {
|
||||
audio,
|
||||
sampleRate,
|
||||
duration
|
||||
duration,
|
||||
}
|
||||
}
|
||||
|
||||
@ -470,7 +477,7 @@ export function createDirectParams(overrides: Partial<SynthesisParams> = {}): Sy
|
||||
disableNormalization: false,
|
||||
disableContrast: false,
|
||||
exactBinMapping: false,
|
||||
...overrides
|
||||
...overrides,
|
||||
}
|
||||
}
|
||||
|
||||
@ -498,7 +505,7 @@ export function createCustomParams(overrides: Partial<SynthesisParams> = {}): Sy
|
||||
disableNormalization: false,
|
||||
disableContrast: false,
|
||||
exactBinMapping: false,
|
||||
...overrides
|
||||
...overrides,
|
||||
}
|
||||
}
|
||||
|
||||
@ -524,4 +531,4 @@ export function synthesizeCustom(
|
||||
const customParams = createCustomParams(params)
|
||||
const synthesizer = new ImageToAudioSynthesizer(customParams)
|
||||
return synthesizer.synthesize(imageData)
|
||||
}
|
||||
}
|
||||
|
||||
@ -32,4 +32,4 @@ export interface SynthesisResult {
|
||||
audio: Float32Array
|
||||
sampleRate: number
|
||||
duration: number
|
||||
}
|
||||
}
|
||||
|
||||
@ -27,8 +27,9 @@ export function barkToHz(bark: number): number {
|
||||
let freq = 1000 // Initial guess
|
||||
for (let i = 0; i < 10; i++) {
|
||||
const barkEst = hzToBark(freq)
|
||||
const derivative = 13 * 0.00076 / (1 + Math.pow(0.00076 * freq, 2)) +
|
||||
3.5 * 2 * (freq / 7500) * (1 / 7500) / (1 + Math.pow(freq / 7500, 4))
|
||||
const derivative =
|
||||
(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
|
||||
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
|
||||
*/
|
||||
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
|
||||
|
||||
const ratio = amplitude / threshold
|
||||
@ -76,7 +81,13 @@ export function applySoftThreshold(amplitude: number, threshold: number, softnes
|
||||
/**
|
||||
* 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)
|
||||
|
||||
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
|
||||
*/
|
||||
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) {
|
||||
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
|
||||
if (peaks.length === 0) {
|
||||
for (let i = 1; i < spectrum.length - 1; i++) {
|
||||
if (spectrum[i] > spectrum[i - 1] &&
|
||||
spectrum[i] > spectrum[i + 1] &&
|
||||
spectrum[i] > 0.001) {
|
||||
if (spectrum[i] > spectrum[i - 1] && spectrum[i] > spectrum[i + 1] && spectrum[i] > 0.001) {
|
||||
peaks.push(i)
|
||||
}
|
||||
}
|
||||
@ -148,12 +161,13 @@ export function detectSmoothSpectralPeaks(spectrum: number[], threshold: number
|
||||
for (let i = 2; i < smoothedSpectrum.length - 2; i++) {
|
||||
const current = smoothedSpectrum[i]
|
||||
|
||||
if (current > threshold &&
|
||||
current > smoothedSpectrum[i - 1] &&
|
||||
current > smoothedSpectrum[i + 1] &&
|
||||
current > smoothedSpectrum[i - 2] &&
|
||||
current > smoothedSpectrum[i + 2]) {
|
||||
|
||||
if (
|
||||
current > threshold &&
|
||||
current > smoothedSpectrum[i - 1] &&
|
||||
current > smoothedSpectrum[i + 1] &&
|
||||
current > smoothedSpectrum[i - 2] &&
|
||||
current > smoothedSpectrum[i + 2]
|
||||
) {
|
||||
// Find the exact peak position with sub-bin accuracy using parabolic interpolation
|
||||
const y1 = smoothedSpectrum[i - 1]
|
||||
const y2 = smoothedSpectrum[i]
|
||||
@ -199,7 +213,11 @@ function smoothSpectrum(spectrum: number[], windowSize: number): number[] {
|
||||
let sum = 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]
|
||||
count++
|
||||
}
|
||||
@ -213,7 +231,11 @@ function smoothSpectrum(spectrum: number[], windowSize: number): number[] {
|
||||
/**
|
||||
* 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)
|
||||
const contrastedAmplitude = Math.pow(amplitude, contrast)
|
||||
|
||||
@ -223,8 +245,6 @@ export function perceptualAmplitudeWeighting(freq: number, amplitude: number, co
|
||||
return contrastedAmplitude * weight
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Generate spectral density by creating multiple tones per frequency bin
|
||||
* Inspired by LeviBorodenko's multi-tone approach
|
||||
@ -239,13 +259,13 @@ export function generateSpectralDensity(
|
||||
const toneSpacing = bandwidth / numTones
|
||||
|
||||
for (let i = 0; i < numTones; i++) {
|
||||
const freq = centerFreq + (i - numTones/2) * toneSpacing
|
||||
const toneAmplitude = amplitude * (1 - Math.abs(i - numTones/2) / numTones * 0.3) // Slight amplitude variation
|
||||
const freq = centerFreq + (i - numTones / 2) * toneSpacing
|
||||
const toneAmplitude = amplitude * (1 - (Math.abs(i - numTones / 2) / numTones) * 0.3) // Slight amplitude variation
|
||||
|
||||
peaks.push({
|
||||
frequency: freq,
|
||||
amplitude: toneAmplitude,
|
||||
phase: 0
|
||||
phase: 0,
|
||||
})
|
||||
}
|
||||
|
||||
@ -328,7 +348,8 @@ export function analyzeImageBrightness(imageData: ImageData): {
|
||||
const avgEdgeBrightness = edgePixels > 0 ? edgeBrightness / edgePixels : meanBrightness
|
||||
|
||||
// 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)
|
||||
|
||||
// Make recommendation
|
||||
@ -346,7 +367,7 @@ export function analyzeImageBrightness(imageData: ImageData): {
|
||||
medianBrightness,
|
||||
edgeBrightness: avgEdgeBrightness,
|
||||
contrast,
|
||||
recommendation
|
||||
recommendation,
|
||||
}
|
||||
}
|
||||
|
||||
@ -366,19 +387,19 @@ export function generateWindow(length: number, windowType: string): Float32Array
|
||||
switch (windowType) {
|
||||
case 'hann':
|
||||
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
|
||||
|
||||
case 'hamming':
|
||||
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
|
||||
|
||||
case 'blackman':
|
||||
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)
|
||||
}
|
||||
break
|
||||
@ -450,7 +471,12 @@ export function extractSpectrum(
|
||||
/**
|
||||
* 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)
|
||||
const normalizedRow = row / (totalRows - 1)
|
||||
return maxFreq - normalizedRow * (maxFreq - minFreq)
|
||||
@ -481,4 +507,4 @@ export function normalizeAudioGlobal(audio: Float32Array, targetLevel: number =
|
||||
}
|
||||
|
||||
return normalized
|
||||
}
|
||||
}
|
||||
|
||||
@ -5,7 +5,7 @@ export {
|
||||
createDirectParams,
|
||||
createCustomParams,
|
||||
synthesizeDirect,
|
||||
synthesizeCustom
|
||||
synthesizeCustom,
|
||||
} from './core/synthesizer'
|
||||
export type { SynthesisParams, SpectralPeak, SynthesisResult, WindowType } from './core/types'
|
||||
|
||||
@ -25,14 +25,9 @@ export {
|
||||
mapFrequency,
|
||||
mapFrequencyLinear,
|
||||
normalizeAudioGlobal,
|
||||
generateSpectralDensity
|
||||
generateSpectralDensity,
|
||||
} from './core/utils'
|
||||
|
||||
// Audio export
|
||||
export {
|
||||
createWAVBuffer,
|
||||
downloadWAV,
|
||||
playAudio,
|
||||
createAudioPlayer
|
||||
} from './audio/export'
|
||||
export type { AudioPlayer } from './audio/export'
|
||||
export { createWAVBuffer, downloadWAV, playAudio, createAudioPlayer } from './audio/export'
|
||||
export type { AudioPlayer } from './audio/export'
|
||||
|
||||
@ -1,7 +1,20 @@
|
||||
import { atom } from 'nanostores'
|
||||
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 {
|
||||
id: string
|
||||
@ -22,7 +35,7 @@ export const appSettings = atom<AppSettings>({
|
||||
selectedGenerator: 'tixy',
|
||||
gridSize: 25,
|
||||
backgroundColor: '#000000',
|
||||
foregroundColor: '#ffffff'
|
||||
foregroundColor: '#ffffff',
|
||||
})
|
||||
|
||||
export const generatedImages = atom<GeneratedImage[]>([])
|
||||
@ -54,5 +67,5 @@ export const synthesisParams = atom<SynthesisParams>({
|
||||
frameOverlap: 0.75,
|
||||
disableNormalization: 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,11 +1,8 @@
|
||||
/** @type {import('tailwindcss').Config} */
|
||||
export default {
|
||||
content: [
|
||||
"./index.html",
|
||||
"./src/**/*.{js,ts,jsx,tsx}",
|
||||
],
|
||||
content: ['./index.html', './src/**/*.{js,ts,jsx,tsx}'],
|
||||
theme: {
|
||||
extend: {},
|
||||
},
|
||||
plugins: [],
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,7 +1,4 @@
|
||||
{
|
||||
"files": [],
|
||||
"references": [
|
||||
{ "path": "./tsconfig.app.json" },
|
||||
{ "path": "./tsconfig.node.json" }
|
||||
]
|
||||
"references": [{ "path": "./tsconfig.app.json" }, { "path": "./tsconfig.node.json" }]
|
||||
}
|
||||
|
||||
@ -4,4 +4,41 @@ import react from '@vitejs/plugin-react'
|
||||
// https://vite.dev/config/
|
||||
export default defineConfig({
|
||||
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