Cleaning the codebase

This commit is contained in:
2025-09-29 15:19:11 +02:00
parent 623082ce3b
commit 709ba3a32a
63 changed files with 1479 additions and 4207 deletions

74
.gitignore vendored
View File

@@ -1,5 +1,24 @@
# Dependencies
node_modules/
.pnp
.pnp.js
# Production builds
dist/
dist-ssr/
build/
*.tsbuildinfo
# Environment files
.env
.env.local
.env.development.local
.env.test.local
.env.production.local
*.local
# Logs # Logs
logs logs/
*.log *.log
npm-debug.log* npm-debug.log*
yarn-debug.log* yarn-debug.log*
@@ -7,18 +26,59 @@ yarn-error.log*
pnpm-debug.log* pnpm-debug.log*
lerna-debug.log* lerna-debug.log*
node_modules # Package manager files
dist package-lock.json
dist-ssr yarn.lock
*.local .yarn/
.yarnrc.yml
# Testing and coverage
coverage/
*.lcov
.nyc_output/
.coverage/
junit.xml
# Cache directories
.cache/
.parcel-cache/
.npm/
.eslintcache
.stylelintcache
# OS generated files
.DS_Store
.DS_Store?
._*
.Spotlight-V100
.Trashes
ehthumbs.db
Thumbs.db
# Editor directories and files # Editor directories and files
.vscode/* .vscode/*
!.vscode/extensions.json !.vscode/extensions.json
.idea !.vscode/settings.json
.DS_Store !.vscode/tasks.json
!.vscode/launch.json
.idea/
*.swp
*.swo
*~
*.suo *.suo
*.ntvs* *.ntvs*
*.njsproj *.njsproj
*.sln *.sln
*.sw? *.sw?
# Temporary files
*.tmp
*.temp
.temp/
.tmp/
# Runtime data
pids
*.pid
*.seed
*.pid.lock

26
.prettierignore Normal file
View 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
View 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"
}

View File

@@ -16,12 +16,15 @@ CoolSoup is a React + TypeScript + Vite application that generates visual patter
## Architecture Overview ## Architecture Overview
### Core Application Structure ### Core Application Structure
- **State Management**: Uses Nanostores for reactive state with persistent atoms - **State Management**: Uses Nanostores for reactive state with persistent atoms
- **UI Layout**: Two-panel design - main content area (generator + image grid) and collapsible audio panel - **UI Layout**: Two-panel design - main content area (generator + image grid) and collapsible audio panel
- **Generator Pattern**: Pluggable generator system where each generator implements a common interface - **Generator Pattern**: Pluggable generator system where each generator implements a common interface
### Main Store (`src/stores/index.ts`) ### Main Store (`src/stores/index.ts`)
Central state management with these key atoms: Central state management with these key atoms:
- `appSettings` - Generator selection, grid size, colors - `appSettings` - Generator selection, grid size, colors
- `generatedImages` - Array of generated images with metadata - `generatedImages` - Array of generated images with metadata
- `selectedImage` - Current image for audio synthesis - `selectedImage` - Current image for audio synthesis
@@ -29,13 +32,16 @@ Central state management with these key atoms:
- `panelOpen` - Audio panel visibility - `panelOpen` - Audio panel visibility
### Generator System ### Generator System
Four built-in generators located in `src/generators/`: Four built-in generators located in `src/generators/`:
- **Tixy** (`tixy.ts`) - Mathematical expressions using t,i,x,y variables - **Tixy** (`tixy.ts`) - Mathematical expressions using t,i,x,y variables
- **Waveform** (`waveform.ts`) - Procedural random waveforms with various interpolation curves - **Waveform** (`waveform.ts`) - Procedural random waveforms with various interpolation curves
- **Picsum** (`picsum.ts`) - External random images API - **Picsum** (`picsum.ts`) - External random images API
- **Art Institute** (`art-institute.ts`) - Art Institute of Chicago API - **Art Institute** (`art-institute.ts`) - Art Institute of Chicago API
Each generator returns `GeneratedImage[]` with: Each generator returns `GeneratedImage[]` with:
- `id` - Unique identifier - `id` - Unique identifier
- `canvas` - HTMLCanvasElement - `canvas` - HTMLCanvasElement
- `imageData` - ImageData for synthesis - `imageData` - ImageData for synthesis
@@ -43,13 +49,16 @@ Each generator returns `GeneratedImage[]` with:
- `params` - Generation parameters - `params` - Generation parameters
### Spectral Synthesis Engine (`src/spectral-synthesis/`) ### Spectral Synthesis Engine (`src/spectral-synthesis/`)
Advanced image-to-audio synthesis library: Advanced image-to-audio synthesis library:
- **Core Logic** (`core/synthesizer.ts`) - Main ImageToAudioSynthesizer class - **Core Logic** (`core/synthesizer.ts`) - Main ImageToAudioSynthesizer class
- **Types** (`core/types.ts`) - SynthesisParams and related interfaces - **Types** (`core/types.ts`) - SynthesisParams and related interfaces
- **Audio Export** (`audio/export.ts`) - WAV file generation and download - **Audio Export** (`audio/export.ts`) - WAV file generation and download
- **Utilities** (`core/utils.ts`) - Helper functions for frequency mapping and peak detection - **Utilities** (`core/utils.ts`) - Helper functions for frequency mapping and peak detection
Key features: Key features:
- Mel-scale frequency mapping for perceptual accuracy - Mel-scale frequency mapping for perceptual accuracy
- Spectral peak detection to reduce noise - Spectral peak detection to reduce noise
- Temporal smoothing for coherent audio trajectories - Temporal smoothing for coherent audio trajectories
@@ -57,7 +66,9 @@ Key features:
- Configurable synthesis parameters (duration, frequency range, resolution) - Configurable synthesis parameters (duration, frequency range, resolution)
### Audio Export System (`src/audio-export/`) ### Audio Export System (`src/audio-export/`)
Batch audio processing capabilities: Batch audio processing capabilities:
- Single image export with custom parameters - Single image export with custom parameters
- Batch export of all generated images - Batch export of all generated images
- ZIP file generation for batch downloads - ZIP file generation for batch downloads
@@ -66,6 +77,7 @@ Batch audio processing capabilities:
## Key Implementation Details ## Key Implementation Details
### Image Generation Flow ### Image Generation Flow
1. User selects generator and parameters in `GeneratorSelector` 1. User selects generator and parameters in `GeneratorSelector`
2. `App.tsx` calls appropriate generator function 2. `App.tsx` calls appropriate generator function
3. Generator returns array of `GeneratedImage` objects 3. Generator returns array of `GeneratedImage` objects
@@ -73,6 +85,7 @@ Batch audio processing capabilities:
5. User can click image to select for audio synthesis 5. User can click image to select for audio synthesis
### Audio Synthesis Flow ### Audio Synthesis Flow
1. User selects image from grid (sets `selectedImage`) 1. User selects image from grid (sets `selectedImage`)
2. `AudioPanel` provides synthesis parameter controls 2. `AudioPanel` provides synthesis parameter controls
3. Synthesis triggered via spectral-synthesis library 3. Synthesis triggered via spectral-synthesis library
@@ -80,6 +93,7 @@ Batch audio processing capabilities:
5. Batch export processes all generated images 5. Batch export processes all generated images
### Component Architecture ### Component Architecture
- **App.tsx** - Main layout and generation orchestration - **App.tsx** - Main layout and generation orchestration
- **GeneratorSelector** - Generator picker with settings - **GeneratorSelector** - Generator picker with settings
- **ImageGrid** - Grid display of generated images - **ImageGrid** - Grid display of generated images
@@ -87,15 +101,18 @@ Batch audio processing capabilities:
- **AudioControls** - Playback controls for generated audio - **AudioControls** - Playback controls for generated audio
## Generator Module Structure ## Generator Module Structure
Both tixy-generator and waveform-generator modules are designed as standalone packages: Both tixy-generator and waveform-generator modules are designed as standalone packages:
**Tixy Generator**: **Tixy Generator**:
- `README.md` - Documentation and usage examples - `README.md` - Documentation and usage examples
- `core/types.ts` - Type definitions - `core/types.ts` - Type definitions
- `index.ts` - Main exports - `index.ts` - Main exports
- Generator-specific files (evaluator, patterns, etc.) - Generator-specific files (evaluator, patterns, etc.)
**Waveform Generator**: **Waveform Generator**:
- `README.md` - Documentation and usage examples - `README.md` - Documentation and usage examples
- `core/types.ts` - Interfaces and configuration - `core/types.ts` - Interfaces and configuration
- `core/interpolation.ts` - Curve interpolation functions (linear, exponential, logarithmic, cubic) - `core/interpolation.ts` - Curve interpolation functions (linear, exponential, logarithmic, cubic)
@@ -104,6 +121,7 @@ Both tixy-generator and waveform-generator modules are designed as standalone pa
- `index.ts` - Main exports and convenience functions - `index.ts` - Main exports and convenience functions
## Development Notes ## Development Notes
- Uses Rolldown Vite for improved performance - Uses Rolldown Vite for improved performance
- Tailwind CSS for styling (no rounded corners per user preference) - Tailwind CSS for styling (no rounded corners per user preference)
- ESLint configured for React + TypeScript - ESLint configured for React + TypeScript

3197
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -6,7 +6,11 @@
"scripts": { "scripts": {
"dev": "vite", "dev": "vite",
"build": "tsc -b && vite build", "build": "tsc -b && vite build",
"build:vite": "vite build",
"typecheck": "tsc -b",
"lint": "eslint .", "lint": "eslint .",
"format": "prettier --write .",
"format:check": "prettier --check .",
"preview": "vite preview" "preview": "vite preview"
}, },
"dependencies": { "dependencies": {
@@ -31,6 +35,7 @@
"eslint-plugin-react-refresh": "^0.4.20", "eslint-plugin-react-refresh": "^0.4.20",
"globals": "^16.4.0", "globals": "^16.4.0",
"postcss": "^8.5.6", "postcss": "^8.5.6",
"prettier": "^3.6.2",
"tailwindcss": "^4.1.13", "tailwindcss": "^4.1.13",
"typescript": "~5.8.3", "typescript": "~5.8.3",
"typescript-eslint": "^8.44.0", "typescript-eslint": "^8.44.0",

10
pnpm-lock.yaml generated
View File

@@ -66,6 +66,9 @@ importers:
postcss: postcss:
specifier: ^8.5.6 specifier: ^8.5.6
version: 8.5.6 version: 8.5.6
prettier:
specifier: ^3.6.2
version: 3.6.2
tailwindcss: tailwindcss:
specifier: ^4.1.13 specifier: ^4.1.13
version: 4.1.13 version: 4.1.13
@@ -1078,6 +1081,11 @@ packages:
resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==}
engines: {node: '>= 0.8.0'} engines: {node: '>= 0.8.0'}
prettier@3.6.2:
resolution: {integrity: sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==}
engines: {node: '>=14'}
hasBin: true
process-nextick-args@2.0.1: process-nextick-args@2.0.1:
resolution: {integrity: sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==} resolution: {integrity: sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==}
@@ -2242,6 +2250,8 @@ snapshots:
prelude-ls@1.2.1: {} prelude-ls@1.2.1: {}
prettier@3.6.2: {}
process-nextick-args@2.0.1: {} process-nextick-args@2.0.1: {}
punycode@2.3.1: {} punycode@2.3.1: {}

View File

@@ -1,5 +1,5 @@
export default { export default {
plugins: { plugins: {
"@tailwindcss/postcss": {}, '@tailwindcss/postcss': {},
}, },
} }

View File

@@ -1,5 +1,6 @@
import { useStore } from '@nanostores/react' import { useStore } from '@nanostores/react'
import { appSettings, generatedImages, isGenerating, helpPopupOpen } from './stores' import { appSettings, generatedImages, isGenerating, helpPopupOpen } from './stores'
import type { GeneratedImage } from './stores'
import { generateTixyImages } from './generators/tixy' import { generateTixyImages } from './generators/tixy'
import { generatePicsumImages } from './generators/picsum' import { generatePicsumImages } from './generators/picsum'
import { generateArtInstituteImages } from './generators/art-institute' import { generateArtInstituteImages } from './generators/art-institute'
@@ -27,7 +28,7 @@ function App() {
isGenerating.set(true) isGenerating.set(true)
try { try {
let newImages let newImages: GeneratedImage[]
if (settings.selectedGenerator === 'tixy') { if (settings.selectedGenerator === 'tixy') {
newImages = generateTixyImages(settings.gridSize, 64) newImages = generateTixyImages(settings.gridSize, 64)
@@ -76,10 +77,7 @@ function App() {
)} )}
</div> </div>
<AudioPanel /> <AudioPanel />
<HelpPopup <HelpPopup isOpen={helpOpen} onClose={() => helpPopupOpen.set(false)} />
isOpen={helpOpen}
onClose={() => helpPopupOpen.set(false)}
/>
</div> </div>
) )
} }

View File

@@ -104,20 +104,20 @@ export function exportSingleAudio(
const filename = generateFilename(image.id, image.generator, { const filename = generateFilename(image.id, image.generator, {
...options, ...options,
includeGeneratorInName: true includeGeneratorInName: true,
}) })
downloadFile(blob, filename) downloadFile(blob, filename)
return { return {
success: true, success: true,
filename filename,
} }
} catch (error) { } catch (error) {
console.error('Error exporting single audio:', error) console.error('Error exporting single audio:', error)
return { return {
success: false, success: false,
error: error instanceof Error ? error.message : 'Unknown error' error: error instanceof Error ? error.message : 'Unknown error',
} }
} }
} }
@@ -146,7 +146,9 @@ export async function exportBatchAudio(
await new Promise(resolve => setTimeout(resolve, 500)) await new Promise(resolve => setTimeout(resolve, 500))
} catch (error) { } catch (error) {
errors.push(`Failed to export ${image.id}: ${error instanceof Error ? error.message : 'Unknown error'}`) errors.push(
`Failed to export ${image.id}: ${error instanceof Error ? error.message : 'Unknown error'}`
)
} }
} }
@@ -154,7 +156,7 @@ export async function exportBatchAudio(
success: errors.length === 0, success: errors.length === 0,
totalFiles: images.length, totalFiles: images.length,
successfulFiles, successfulFiles,
errors errors,
} }
} }
@@ -167,14 +169,16 @@ export async function exportBatchAudio(
const filename = generateFilename(image.id, image.generator, { const filename = generateFilename(image.id, image.generator, {
...options, ...options,
includeGeneratorInName: true includeGeneratorInName: true,
}) })
zip.file(filename, buffer) zip.file(filename, buffer)
successfulFiles++ successfulFiles++
} catch (error) { } catch (error) {
console.error(`Error processing image ${image.id}:`, error) console.error(`Error processing image ${image.id}:`, error)
errors.push(`Failed to process ${image.id}: ${error instanceof Error ? error.message : 'Unknown error'}`) errors.push(
`Failed to process ${image.id}: ${error instanceof Error ? error.message : 'Unknown error'}`
)
} }
} }
@@ -183,7 +187,7 @@ export async function exportBatchAudio(
success: false, success: false,
totalFiles: images.length, totalFiles: images.length,
successfulFiles: 0, successfulFiles: 0,
errors: [...errors, 'No files were successfully processed'] errors: [...errors, 'No files were successfully processed'],
} }
} }
@@ -198,7 +202,7 @@ export async function exportBatchAudio(
totalFiles: images.length, totalFiles: images.length,
successfulFiles, successfulFiles,
zipFilename, zipFilename,
errors errors,
} }
} catch (error) { } catch (error) {
console.error('Error creating ZIP file:', error) console.error('Error creating ZIP file:', error)
@@ -206,7 +210,10 @@ export async function exportBatchAudio(
success: false, success: false,
totalFiles: images.length, totalFiles: images.length,
successfulFiles, successfulFiles,
errors: [...errors, `Failed to create ZIP file: ${error instanceof Error ? error.message : 'Unknown error'}`] errors: [
...errors,
`Failed to create ZIP file: ${error instanceof Error ? error.message : 'Unknown error'}`,
],
} }
} }
} }
@@ -220,7 +227,7 @@ export function quickExportSingle(
): ExportResult { ): ExportResult {
return exportSingleAudio(image, synthesisParams, { return exportSingleAudio(image, synthesisParams, {
includeGeneratorInName: true, includeGeneratorInName: true,
includeTimestamp: false includeTimestamp: false,
}) })
} }
@@ -234,6 +241,6 @@ export async function quickExportBatch(
return exportBatchAudio(images, synthesisParams, { return exportBatchAudio(images, synthesisParams, {
includeGeneratorInName: true, includeGeneratorInName: true,
includeTimestamp: true, includeTimestamp: true,
createZip: true createZip: true,
}) })
} }

View File

@@ -1,4 +1,3 @@
import { useState, useRef } from 'react'
interface AudioControlsProps { interface AudioControlsProps {
isPlaying: boolean isPlaying: boolean
@@ -17,9 +16,8 @@ export default function AudioControls({
onPause, onPause,
onStop, onStop,
onVolumeChange, onVolumeChange,
disabled = false disabled = false,
}: AudioControlsProps) { }: AudioControlsProps) {
const [isDragging, setIsDragging] = useState(false)
const handleVolumeChange = (e: React.ChangeEvent<HTMLInputElement>) => { const handleVolumeChange = (e: React.ChangeEvent<HTMLInputElement>) => {
onVolumeChange(Number(e.target.value)) onVolumeChange(Number(e.target.value))
@@ -58,7 +56,13 @@ export default function AudioControls({
</div> </div>
<div className="flex items-center space-x-3 flex-1"> <div className="flex items-center space-x-3 flex-1">
<svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor" className="text-gray-400"> <svg
width="16"
height="16"
viewBox="0 0 24 24"
fill="currentColor"
className="text-gray-400"
>
<path d="M3 9v6h4l5 5V4L7 9H3zm13.5 3c0-1.77-1.02-3.29-2.5-4.03v8.05c1.48-.73 2.5-2.25 2.5-4.02zM14 3.23v2.06c2.89.86 5 3.54 5 6.71s-2.11 5.85-5 6.71v2.06c4.01-.91 7-4.49 7-8.77s-2.99-7.86-7-8.77z" /> <path d="M3 9v6h4l5 5V4L7 9H3zm13.5 3c0-1.77-1.02-3.29-2.5-4.03v8.05c1.48-.73 2.5-2.25 2.5-4.02zM14 3.23v2.06c2.89.86 5 3.54 5 6.71s-2.11 5.85-5 6.71v2.06c4.01-.91 7-4.49 7-8.77s-2.99-7.86-7-8.77z" />
</svg> </svg>
@@ -74,8 +78,9 @@ export default function AudioControls({
className="w-full h-2 bg-black border border-white appearance-none cursor-pointer volume-slider disabled:cursor-not-allowed" className="w-full h-2 bg-black border border-white appearance-none cursor-pointer volume-slider disabled:cursor-not-allowed"
title={`Volume: ${Math.round(volume * 100)}%`} title={`Volume: ${Math.round(volume * 100)}%`}
/> />
<style dangerouslySetInnerHTML={{ <style
__html: ` dangerouslySetInnerHTML={{
__html: `
.volume-slider::-webkit-slider-thumb { .volume-slider::-webkit-slider-thumb {
appearance: none; appearance: none;
width: 16px; width: 16px;
@@ -100,13 +105,12 @@ export default function AudioControls({
.volume-slider::-moz-range-thumb:hover { .volume-slider::-moz-range-thumb:hover {
background: ${disabled ? '#6b7280' : '#e5e7eb'}; background: ${disabled ? '#6b7280' : '#e5e7eb'};
} }
` `,
}} /> }}
/>
</div> </div>
<span className="text-xs text-gray-400 w-10 text-right"> <span className="text-xs text-gray-400 w-10 text-right">{Math.round(volume * 100)}%</span>
{Math.round(volume * 100)}%
</span>
</div> </div>
</div> </div>
) )

View File

@@ -1,6 +1,10 @@
import { useStore } from '@nanostores/react' import { useStore } from '@nanostores/react'
import { selectedImage, panelOpen, generatedImages, synthesisParams } from '../stores' import { selectedImage, generatedImages, synthesisParams } from '../stores'
import { synthesizeFromImage, playAudio, createAudioPlayer, type WindowType, type AudioPlayer } from '../spectral-synthesis' import {
synthesizeFromImage,
createAudioPlayer,
type AudioPlayer,
} from '../spectral-synthesis'
import { quickExportSingle, quickExportBatch } from '../audio-export' import { quickExportSingle, quickExportBatch } from '../audio-export'
import { useState, useRef, useEffect } from 'react' import { useState, useRef, useEffect } from 'react'
import AudioControls from './AudioControls' import AudioControls from './AudioControls'
@@ -14,14 +18,8 @@ export default function AudioPanel() {
const [volume, setVolume] = useState(0.7) const [volume, setVolume] = useState(0.7)
const audioPlayerRef = useRef<AudioPlayer | null>(null) const audioPlayerRef = useRef<AudioPlayer | null>(null)
const handleClearSelection = () => {
selectedImage.set(null)
if (audioPlayerRef.current) {
audioPlayerRef.current.stop()
}
}
const updateParam = <K extends keyof typeof params>(key: K, value: typeof params[K]) => { const updateParam = <K extends keyof typeof params>(key: K, value: (typeof params)[K]) => {
synthesisParams.set({ ...params, [key]: value }) synthesisParams.set({ ...params, [key]: value })
} }
@@ -82,18 +80,6 @@ export default function AudioPanel() {
} }
} }
const handlePlaySingle = async () => {
if (!selected) return
setIsProcessing(true)
try {
const audio = synthesizeFromImage(selected.imageData, params)
await playAudio(audio, params.sampleRate)
} catch (error) {
console.error('Error playing audio:', error)
}
setIsProcessing(false)
}
// Keyboard shortcuts // Keyboard shortcuts
useEffect(() => { useEffect(() => {
@@ -131,7 +117,6 @@ export default function AudioPanel() {
if (!result.success) { if (!result.success) {
console.error('Batch export failed:', result.errors) console.error('Batch export failed:', result.errors)
} else { } else {
console.log(`Exported ${result.successfulFiles}/${result.totalFiles} files to ${result.zipFilename}`)
} }
} catch (error) { } catch (error) {
console.error('Error generating all audio:', error) console.error('Error generating all audio:', error)
@@ -147,7 +132,7 @@ export default function AudioPanel() {
<div className="flex items-center space-x-3"> <div className="flex items-center space-x-3">
<div className="w-24 h-24 border border-gray-600"> <div className="w-24 h-24 border border-gray-600">
<canvas <canvas
ref={(canvas) => { ref={canvas => {
if (canvas && selected.canvas) { if (canvas && selected.canvas) {
const ctx = canvas.getContext('2d')! const ctx = canvas.getContext('2d')!
canvas.width = selected.canvas.width canvas.width = selected.canvas.width
@@ -186,9 +171,7 @@ export default function AudioPanel() {
<div className="p-6 space-y-4"> <div className="p-6 space-y-4">
{/* Synthesis Mode Selection - Top Priority */} {/* Synthesis Mode Selection - Top Priority */}
<div> <div>
<label className="block text-xs text-gray-400 mb-1"> <label className="block text-xs text-gray-400 mb-1">Synthesis Mode</label>
Synthesis Mode
</label>
<div className="grid grid-cols-2 gap-1"> <div className="grid grid-cols-2 gap-1">
<button <button
onClick={() => updateParam('synthesisMode', 'direct')} onClick={() => updateParam('synthesisMode', 'direct')}
@@ -216,265 +199,20 @@ export default function AudioPanel() {
</p> </p>
</div> </div>
<div className="space-y-3"> <div className="space-y-3">
<div>
<label className="block text-xs text-gray-400 mb-1">
Duration: {params.duration}s
</label>
<input
type="range"
min="1"
max="30"
step="1"
value={params.duration}
onChange={(e) => updateParam('duration', Number(e.target.value))}
className="w-full"
/>
</div>
{/* Custom Mode Only Parameters */}
{params.synthesisMode === 'custom' && (
<>
<div>
<label className="block text-xs text-gray-400 mb-1">
Max Partials: {params.maxPartials}
</label>
<input
type="range"
min="10"
max="500"
step="10"
value={params.maxPartials}
onChange={(e) => updateParam('maxPartials', Number(e.target.value))}
className="w-full"
/>
<p className="text-xs text-gray-500 mt-1">Controls audio complexity vs performance</p>
</div>
<div>
<label className="block text-xs text-gray-400 mb-1">
Frequency Density: {params.frequencyResolution}x
</label>
<input
type="range"
min="1"
max="10"
step="1"
value={params.frequencyResolution}
onChange={(e) => updateParam('frequencyResolution', Number(e.target.value))}
className="w-full"
/>
<p className="text-xs text-gray-500 mt-1">Higher values create broader, richer frequency bands</p>
</div>
</>
)}
{/* Direct Mode Only Parameters */}
{(params.synthesisMode || 'direct') === 'direct' && (
<>
<div className="space-y-3">
<div>
<label className="block text-xs text-gray-400 mb-1">
FFT Size: {params.fftSize || 2048}
</label>
<div className="grid grid-cols-4 gap-1">
{[1024, 2048, 4096, 8192].map(size => (
<button
key={size}
onClick={() => updateParam('fftSize', size)}
className={`px-2 py-1 text-xs border ${
(params.fftSize || 2048) === size
? 'bg-white text-black border-white'
: 'bg-black text-white border-gray-600 hover:border-white'
}`}
>
{size}
</button>
))}
</div>
<p className="text-xs text-gray-500 mt-1">Higher = better frequency resolution</p>
</div>
<div>
<label className="block text-xs text-gray-400 mb-1">
Frame Overlap: {((params.frameOverlap || 0.75) * 100).toFixed(0)}%
</label>
<input
type="range"
min="0"
max="0.9"
step="0.125"
value={params.frameOverlap || 0.75}
onChange={(e) => updateParam('frameOverlap', Number(e.target.value))}
className="w-full"
/>
<p className="text-xs text-gray-500 mt-1">Higher = smoother temporal resolution</p>
</div>
<div className="flex items-center justify-between">
<label className="text-xs text-gray-400">Disable normalization: can be very loud</label>
<button
onClick={() => updateParam('disableNormalization', !(params.disableNormalization ?? false))}
className={`px-2 py-1 text-xs border ${
params.disableNormalization === true
? 'bg-white text-black border-white'
: 'bg-black text-white border-gray-600 hover:border-white'
}`}
>
{params.disableNormalization === true ? 'ON' : 'OFF'}
</button>
</div>
</div>
</>
)}
<div>
<label className="block text-xs text-gray-400 mb-1">
Min Frequency: {params.minFreq}Hz
</label>
<input
type="range"
min="20"
max="200"
step="10"
value={params.minFreq}
onChange={(e) => updateParam('minFreq', Number(e.target.value))}
className="w-full"
/>
</div>
<div>
<label className="block text-xs text-gray-400 mb-1">
Max Frequency: {params.maxFreq}Hz
</label>
<input
type="range"
min="1000"
max="20000"
step="500"
value={params.maxFreq}
onChange={(e) => updateParam('maxFreq', Number(e.target.value))}
className="w-full"
/>
</div>
{/* Custom Mode Only Parameters */}
{params.synthesisMode === 'custom' && (
<>
<div>
<label className="block text-xs text-gray-400 mb-1">
Amplitude Threshold: {params.amplitudeThreshold}
</label>
<input
type="range"
min="0.001"
max="0.1"
step="0.001"
value={params.amplitudeThreshold}
onChange={(e) => updateParam('amplitudeThreshold', Number(e.target.value))}
className="w-full"
/>
<p className="text-xs text-gray-500 mt-1">Minimum amplitude to include</p>
</div>
<div>
<label className="block text-xs text-gray-400 mb-1">
Window Type
</label>
<div className="grid grid-cols-4 gap-1">
<button
onClick={() => updateParam('windowType', 'rectangular')}
className={`px-2 py-1 text-xs border ${
params.windowType === 'rectangular'
? 'bg-white text-black border-white'
: 'bg-black text-white border-gray-600 hover:border-white'
}`}
>
Rect
</button>
<button
onClick={() => updateParam('windowType', 'hann')}
className={`px-2 py-1 text-xs border ${
params.windowType === 'hann'
? 'bg-white text-black border-white'
: 'bg-black text-white border-gray-600 hover:border-white'
}`}
>
Hann
</button>
<button
onClick={() => updateParam('windowType', 'hamming')}
className={`px-2 py-1 text-xs border ${
params.windowType === 'hamming'
? 'bg-white text-black border-white'
: 'bg-black text-white border-gray-600 hover:border-white'
}`}
>
Hamming
</button>
<button
onClick={() => updateParam('windowType', 'blackman')}
className={`px-2 py-1 text-xs border ${
params.windowType === 'blackman'
? 'bg-white text-black border-white'
: 'bg-black text-white border-gray-600 hover:border-white'
}`}
>
Blackman
</button>
</div>
<p className="text-xs text-gray-500 mt-1">Reduces clicking/popping between time frames</p>
</div>
</>
)}
{/* Color Inversion - Available for both modes */}
<div className="border-t border-gray-700 pt-4">
<div> <div>
<label className="block text-xs text-gray-400 mb-1"> <label className="block text-xs text-gray-400 mb-1">
Color Duration: {params.duration}s
</label>
<div className="grid grid-cols-2 gap-1">
<button
onClick={() => updateParam('invert', false)}
className={`px-2 py-1 text-xs border ${
!params.invert
? 'bg-white text-black border-white'
: 'bg-black text-white border-gray-600 hover:border-white'
}`}
>
Normal
</button>
<button
onClick={() => updateParam('invert', true)}
className={`px-2 py-1 text-xs border ${
params.invert
? 'bg-white text-black border-white'
: 'bg-black text-white border-gray-600 hover:border-white'
}`}
>
Inverted
</button>
</div>
</div>
{/* Contrast - Available for both modes */}
<div>
<label className="block text-xs text-gray-400 mb-1">
Contrast: {(params.contrast || 2.2).toFixed(1)}
</label> </label>
<input <input
type="range" type="range"
min="1" min="1"
max="5" max="30"
step="0.1" step="1"
value={params.contrast || 2.2} value={params.duration}
onChange={(e) => updateParam('contrast', Number(e.target.value))} onChange={e => updateParam('duration', Number(e.target.value))}
className="w-full" className="w-full"
/> />
<p className="text-xs text-gray-500 mt-1">Power curve for brightness perception</p>
</div> </div>
{/* Custom Mode Only Parameters */} {/* Custom Mode Only Parameters */}
@@ -482,89 +220,345 @@ export default function AudioPanel() {
<> <>
<div> <div>
<label className="block text-xs text-gray-400 mb-1"> <label className="block text-xs text-gray-400 mb-1">
Frequency Mapping Max Partials: {params.maxPartials}
</label> </label>
<div className="grid grid-cols-4 gap-1"> <input
<button type="range"
onClick={() => updateParam('frequencyMapping', 'mel')} min="10"
className={`px-2 py-1 text-xs border ${ max="500"
(params.frequencyMapping || 'linear') === 'mel' step="10"
? 'bg-white text-black border-white' value={params.maxPartials}
: 'bg-black text-white border-gray-600 hover:border-white' onChange={e => updateParam('maxPartials', Number(e.target.value))}
}`} className="w-full"
> />
Mel <p className="text-xs text-gray-500 mt-1">
</button> Controls audio complexity vs performance
<button </p>
onClick={() => updateParam('frequencyMapping', 'linear')}
className={`px-2 py-1 text-xs border ${
(params.frequencyMapping || 'linear') === 'linear'
? 'bg-white text-black border-white'
: 'bg-black text-white border-gray-600 hover:border-white'
}`}
>
Linear
</button>
<button
onClick={() => updateParam('frequencyMapping', 'bark')}
className={`px-2 py-1 text-xs border ${
params.frequencyMapping === 'bark'
? 'bg-white text-black border-white'
: 'bg-black text-white border-gray-600 hover:border-white'
}`}
>
Bark
</button>
<button
onClick={() => updateParam('frequencyMapping', 'log')}
className={`px-2 py-1 text-xs border ${
params.frequencyMapping === 'log'
? 'bg-white text-black border-white'
: 'bg-black text-white border-gray-600 hover:border-white'
}`}
>
Log
</button>
</div>
<p className="text-xs text-gray-500 mt-1">How image height maps to frequency</p>
</div> </div>
<div> <div>
<label className="block text-xs text-gray-400 mb-1"> <label className="block text-xs text-gray-400 mb-1">
Spectral Density: {params.spectralDensity || 3} Frequency Density: {params.frequencyResolution}x
</label> </label>
<input <input
type="range" type="range"
min="1" min="1"
max="7" max="10"
step="1" step="1"
value={params.spectralDensity || 3} value={params.frequencyResolution}
onChange={(e) => updateParam('spectralDensity', Number(e.target.value))} onChange={e => updateParam('frequencyResolution', Number(e.target.value))}
className="w-full" className="w-full"
/> />
<p className="text-xs text-gray-500 mt-1">Tones per frequency peak (richer = higher)</p> <p className="text-xs text-gray-500 mt-1">
Higher values create broader, richer frequency bands
</p>
</div>
</>
)}
{/* Direct Mode Only Parameters */}
{(params.synthesisMode || 'direct') === 'direct' && (
<>
<div className="space-y-3">
<div>
<label className="block text-xs text-gray-400 mb-1">
FFT Size: {params.fftSize || 2048}
</label>
<div className="grid grid-cols-4 gap-1">
{[1024, 2048, 4096, 8192].map(size => (
<button
key={size}
onClick={() => updateParam('fftSize', size)}
className={`px-2 py-1 text-xs border ${
(params.fftSize || 2048) === size
? 'bg-white text-black border-white'
: 'bg-black text-white border-gray-600 hover:border-white'
}`}
>
{size}
</button>
))}
</div>
<p className="text-xs text-gray-500 mt-1">
Higher = better frequency resolution
</p>
</div>
<div>
<label className="block text-xs text-gray-400 mb-1">
Frame Overlap: {((params.frameOverlap || 0.75) * 100).toFixed(0)}%
</label>
<input
type="range"
min="0"
max="0.9"
step="0.125"
value={params.frameOverlap || 0.75}
onChange={e => updateParam('frameOverlap', Number(e.target.value))}
className="w-full"
/>
<p className="text-xs text-gray-500 mt-1">
Higher = smoother temporal resolution
</p>
</div>
<div className="flex items-center justify-between">
<label className="text-xs text-gray-400">
Disable normalization: can be very loud
</label>
<button
onClick={() =>
updateParam('disableNormalization', !(params.disableNormalization ?? false))
}
className={`px-2 py-1 text-xs border ${
params.disableNormalization === true
? 'bg-white text-black border-white'
: 'bg-black text-white border-gray-600 hover:border-white'
}`}
>
{params.disableNormalization === true ? 'ON' : 'OFF'}
</button>
</div>
</div>
</>
)}
<div>
<label className="block text-xs text-gray-400 mb-1">
Min Frequency: {params.minFreq}Hz
</label>
<input
type="range"
min="20"
max="200"
step="10"
value={params.minFreq}
onChange={e => updateParam('minFreq', Number(e.target.value))}
className="w-full"
/>
</div>
<div>
<label className="block text-xs text-gray-400 mb-1">
Max Frequency: {params.maxFreq}Hz
</label>
<input
type="range"
min="1000"
max="20000"
step="500"
value={params.maxFreq}
onChange={e => updateParam('maxFreq', Number(e.target.value))}
className="w-full"
/>
</div>
{/* Custom Mode Only Parameters */}
{params.synthesisMode === 'custom' && (
<>
<div>
<label className="block text-xs text-gray-400 mb-1">
Amplitude Threshold: {params.amplitudeThreshold}
</label>
<input
type="range"
min="0.001"
max="0.1"
step="0.001"
value={params.amplitudeThreshold}
onChange={e => updateParam('amplitudeThreshold', Number(e.target.value))}
className="w-full"
/>
<p className="text-xs text-gray-500 mt-1">Minimum amplitude to include</p>
</div> </div>
<div className="flex items-center justify-between"> <div>
<label className="text-xs text-gray-400">Perceptual RGB Weighting</label> <label className="block text-xs text-gray-400 mb-1">Window Type</label>
<div className="grid grid-cols-4 gap-1">
<button
onClick={() => updateParam('windowType', 'rectangular')}
className={`px-2 py-1 text-xs border ${
params.windowType === 'rectangular'
? 'bg-white text-black border-white'
: 'bg-black text-white border-gray-600 hover:border-white'
}`}
>
Rect
</button>
<button
onClick={() => updateParam('windowType', 'hann')}
className={`px-2 py-1 text-xs border ${
params.windowType === 'hann'
? 'bg-white text-black border-white'
: 'bg-black text-white border-gray-600 hover:border-white'
}`}
>
Hann
</button>
<button
onClick={() => updateParam('windowType', 'hamming')}
className={`px-2 py-1 text-xs border ${
params.windowType === 'hamming'
? 'bg-white text-black border-white'
: 'bg-black text-white border-gray-600 hover:border-white'
}`}
>
Hamming
</button>
<button
onClick={() => updateParam('windowType', 'blackman')}
className={`px-2 py-1 text-xs border ${
params.windowType === 'blackman'
? 'bg-white text-black border-white'
: 'bg-black text-white border-gray-600 hover:border-white'
}`}
>
Blackman
</button>
</div>
<p className="text-xs text-gray-500 mt-1">
Reduces clicking/popping between time frames
</p>
</div>
</>
)}
{/* Color Inversion - Available for both modes */}
<div className="border-t border-gray-700 pt-4">
<div>
<label className="block text-xs text-gray-400 mb-1">Color</label>
<div className="grid grid-cols-2 gap-1">
<button <button
onClick={() => updateParam('usePerceptualWeighting', !params.usePerceptualWeighting)} onClick={() => updateParam('invert', false)}
className={`px-2 py-1 text-xs border ${ className={`px-2 py-1 text-xs border ${
params.usePerceptualWeighting !== false !params.invert
? 'bg-white text-black border-white' ? 'bg-white text-black border-white'
: 'bg-black text-white border-gray-600 hover:border-white' : 'bg-black text-white border-gray-600 hover:border-white'
}`} }`}
> >
{params.usePerceptualWeighting !== false ? 'ON' : 'OFF'} Normal
</button>
<button
onClick={() => updateParam('invert', true)}
className={`px-2 py-1 text-xs border ${
params.invert
? 'bg-white text-black border-white'
: 'bg-black text-white border-gray-600 hover:border-white'
}`}
>
Inverted
</button> </button>
</div> </div>
<p className="text-xs text-gray-500 mt-1">Squared RGB sum for better brightness perception</p> </div>
</>
)}
{/* Contrast - Available for both modes */}
<div>
<label className="block text-xs text-gray-400 mb-1">
Contrast: {(params.contrast || 2.2).toFixed(1)}
</label>
<input
type="range"
min="1"
max="5"
step="0.1"
value={params.contrast || 2.2}
onChange={e => updateParam('contrast', Number(e.target.value))}
className="w-full"
/>
<p className="text-xs text-gray-500 mt-1">Power curve for brightness perception</p>
</div>
{/* Custom Mode Only Parameters */}
{params.synthesisMode === 'custom' && (
<>
<div>
<label className="block text-xs text-gray-400 mb-1">Frequency Mapping</label>
<div className="grid grid-cols-4 gap-1">
<button
onClick={() => updateParam('frequencyMapping', 'mel')}
className={`px-2 py-1 text-xs border ${
(params.frequencyMapping || 'linear') === 'mel'
? 'bg-white text-black border-white'
: 'bg-black text-white border-gray-600 hover:border-white'
}`}
>
Mel
</button>
<button
onClick={() => updateParam('frequencyMapping', 'linear')}
className={`px-2 py-1 text-xs border ${
(params.frequencyMapping || 'linear') === 'linear'
? 'bg-white text-black border-white'
: 'bg-black text-white border-gray-600 hover:border-white'
}`}
>
Linear
</button>
<button
onClick={() => updateParam('frequencyMapping', 'bark')}
className={`px-2 py-1 text-xs border ${
params.frequencyMapping === 'bark'
? 'bg-white text-black border-white'
: 'bg-black text-white border-gray-600 hover:border-white'
}`}
>
Bark
</button>
<button
onClick={() => updateParam('frequencyMapping', 'log')}
className={`px-2 py-1 text-xs border ${
params.frequencyMapping === 'log'
? 'bg-white text-black border-white'
: 'bg-black text-white border-gray-600 hover:border-white'
}`}
>
Log
</button>
</div>
<p className="text-xs text-gray-500 mt-1">How image height maps to frequency</p>
</div>
<div>
<label className="block text-xs text-gray-400 mb-1">
Spectral Density: {params.spectralDensity || 3}
</label>
<input
type="range"
min="1"
max="7"
step="1"
value={params.spectralDensity || 3}
onChange={e => updateParam('spectralDensity', Number(e.target.value))}
className="w-full"
/>
<p className="text-xs text-gray-500 mt-1">
Tones per frequency peak (richer = higher)
</p>
</div>
<div className="flex items-center justify-between">
<label className="text-xs text-gray-400">Perceptual RGB Weighting</label>
<button
onClick={() =>
updateParam('usePerceptualWeighting', !params.usePerceptualWeighting)
}
className={`px-2 py-1 text-xs border ${
params.usePerceptualWeighting !== false
? 'bg-white text-black border-white'
: 'bg-black text-white border-gray-600 hover:border-white'
}`}
>
{params.usePerceptualWeighting !== false ? 'ON' : 'OFF'}
</button>
</div>
<p className="text-xs text-gray-500 mt-1">
Squared RGB sum for better brightness perception
</p>
</>
)}
</div>
</div> </div>
</div> </div>
</div>
</div> </div>
{/* Audio Controls - Below Parameters */} {/* Audio Controls - Below Parameters */}

View File

@@ -9,13 +9,9 @@ interface GeneratorSelectorProps {
} }
export default function GeneratorSelector({ onGenerate, isGenerating }: GeneratorSelectorProps) { export default function GeneratorSelector({ onGenerate, isGenerating }: GeneratorSelectorProps) {
console.log('GeneratorSelector rendering with props:', { onGenerate, isGenerating })
const settings = useStore(appSettings) const settings = useStore(appSettings)
console.log('GeneratorSelector settings:', settings)
const handleGeneratorChange = (generator: GeneratorType) => { const handleGeneratorChange = (generator: GeneratorType) => {
console.log('Changing generator to:', generator)
appSettings.set({ ...settings, selectedGenerator: generator }) appSettings.set({ ...settings, selectedGenerator: generator })
// Clear the grid when switching to photo or webcam modes to show drag/drop zones // Clear the grid when switching to photo or webcam modes to show drag/drop zones
@@ -25,8 +21,6 @@ export default function GeneratorSelector({ onGenerate, isGenerating }: Generato
} }
const handleGenerateClick = () => { const handleGenerateClick = () => {
console.log('Generate button clicked in GeneratorSelector!')
console.log('onGenerate function:', onGenerate)
onGenerate() onGenerate()
} }
@@ -43,7 +37,7 @@ export default function GeneratorSelector({ onGenerate, isGenerating }: Generato
{ id: 'from-photo' as const, name: 'Photo', description: 'Upload your image' }, { id: 'from-photo' as const, name: 'Photo', description: 'Upload your image' },
{ id: 'webcam' as const, name: 'Webcam', description: 'Capture from camera' }, { id: 'webcam' as const, name: 'Webcam', description: 'Capture from camera' },
{ id: 'art-institute' as const, name: 'Artworks', description: 'Famous artworks' }, { id: 'art-institute' as const, name: 'Artworks', description: 'Famous artworks' },
{ id: 'picsum' as const, name: 'Picsum', description: 'Random photos' } { id: 'picsum' as const, name: 'Picsum', description: 'Random photos' },
] ]
// Automatically split generators into two rows // Automatically split generators into two rows
@@ -55,7 +49,10 @@ export default function GeneratorSelector({ onGenerate, isGenerating }: Generato
const handleKeyPress = (event: KeyboardEvent) => { const handleKeyPress = (event: KeyboardEvent) => {
if (event.key.toLowerCase() === 'g' && !isGenerating) { if (event.key.toLowerCase() === 'g' && !isGenerating) {
// Don't trigger if user is typing in an input field // Don't trigger if user is typing in an input field
if (event.target instanceof HTMLInputElement || event.target instanceof HTMLTextAreaElement) { if (
event.target instanceof HTMLInputElement ||
event.target instanceof HTMLTextAreaElement
) {
return return
} }
event.preventDefault() event.preventDefault()
@@ -81,22 +78,23 @@ export default function GeneratorSelector({ onGenerate, isGenerating }: Generato
</h1> </h1>
</div> </div>
<div className="flex items-center justify-center h-10"> <div className="flex items-center justify-center h-10">
{settings.selectedGenerator !== 'from-photo' && settings.selectedGenerator !== 'webcam' && ( {settings.selectedGenerator !== 'from-photo' &&
<button settings.selectedGenerator !== 'webcam' && (
onClick={handleGenerateClick} <button
disabled={isGenerating} onClick={handleGenerateClick}
className="bg-white text-black px-4 py-2 font-medium hover:bg-gray-200 disabled:bg-gray-600 disabled:text-gray-400 disabled:cursor-not-allowed" disabled={isGenerating}
> className="bg-white text-black px-4 py-2 font-medium hover:bg-gray-200 disabled:bg-gray-600 disabled:text-gray-400 disabled:cursor-not-allowed"
<div>{isGenerating ? 'Generating...' : 'Generate (G)'}</div> >
</button> <div>{isGenerating ? 'Generating...' : 'Generate (G)'}</div>
)} </button>
)}
</div> </div>
</div> </div>
{/* Second container: 85% width - Generator modes in two lines */} {/* Second container: 85% width - Generator modes in two lines */}
<div className="w-[85%] flex flex-col space-y-2"> <div className="w-[85%] flex flex-col space-y-2">
<div className="flex items-center space-x-4 h-10"> <div className="flex items-center space-x-4 h-10">
{firstRowGenerators.map((generator) => ( {firstRowGenerators.map(generator => (
<Tooltip key={generator.id} content={generator.description}> <Tooltip key={generator.id} content={generator.description}>
<button <button
onClick={() => handleGeneratorChange(generator.id)} onClick={() => handleGeneratorChange(generator.id)}
@@ -112,7 +110,7 @@ export default function GeneratorSelector({ onGenerate, isGenerating }: Generato
))} ))}
</div> </div>
<div className="flex items-center space-x-4 h-10"> <div className="flex items-center space-x-4 h-10">
{secondRowGenerators.map((generator) => ( {secondRowGenerators.map(generator => (
<Tooltip key={generator.id} content={generator.description}> <Tooltip key={generator.id} content={generator.description}>
<button <button
onClick={() => handleGeneratorChange(generator.id)} onClick={() => handleGeneratorChange(generator.id)}

View File

@@ -28,21 +28,21 @@ export default function HelpPopup({ isOpen, onClose }: HelpPopupProps) {
> >
<div <div
className="bg-black border border-white p-8 max-w-md w-full mx-4" className="bg-black border border-white p-8 max-w-md w-full mx-4"
onClick={(e) => e.stopPropagation()} onClick={e => e.stopPropagation()}
> >
<div className="flex justify-between items-start mb-6"> <div className="flex justify-between items-start mb-6">
<h2 className="text-xl font-bold text-white">About CoolSoup</h2> <h2 className="text-xl font-bold text-white">About CoolSoup</h2>
<button <button onClick={onClose} className="text-white hover:text-gray-300 text-xl">
onClick={onClose}
className="text-white hover:text-gray-300 text-xl"
>
× ×
</button> </button>
</div> </div>
<div className="text-white space-y-4"> <div className="text-white space-y-4">
<p> <p>
CoolSoup generates visual patterns and converts them to audio through spectral/additive synthesis. Create images using mathematical expressions, waveforms, geometric patterns, or upload your own photos, then transform them into sound by treating the image as a spectrogram. CoolSoup generates visual patterns and converts them to audio through spectral/additive
synthesis. Create images using mathematical expressions, waveforms, geometric patterns,
or upload your own photos, then transform them into sound by treating the image as a
spectrogram.
</p> </p>
<div className="pt-4 border-t border-gray-600"> <div className="pt-4 border-t border-gray-600">

View File

@@ -21,7 +21,7 @@ export default function ImageGrid() {
const rect = event.currentTarget.getBoundingClientRect() const rect = event.currentTarget.getBoundingClientRect()
setLocalMousePosition({ setLocalMousePosition({
x: (event.clientX - rect.left) / rect.width, x: (event.clientX - rect.left) / rect.width,
y: (event.clientY - rect.top) / rect.height y: (event.clientY - rect.top) / rect.height,
}) })
} }
@@ -31,7 +31,7 @@ export default function ImageGrid() {
const rect = event.currentTarget.getBoundingClientRect() const rect = event.currentTarget.getBoundingClientRect()
setLocalMousePosition({ setLocalMousePosition({
x: (event.clientX - rect.left) / rect.width, x: (event.clientX - rect.left) / rect.width,
y: (event.clientY - rect.top) / rect.height y: (event.clientY - rect.top) / rect.height,
}) })
} }
} }
@@ -47,7 +47,10 @@ export default function ImageGrid() {
<div className="text-lg mb-2">Generating images...</div> <div className="text-lg mb-2">Generating images...</div>
<div className="text-sm">Please wait while we fetch your images</div> <div className="text-sm">Please wait while we fetch your images</div>
<div className="mt-4"> <div className="mt-4">
<div className="animate-pulse h-8 w-8 bg-white mx-auto" style={{ imageRendering: 'pixelated' }}></div> <div
className="animate-pulse h-8 w-8 bg-white mx-auto"
style={{ imageRendering: 'pixelated' }}
></div>
</div> </div>
</div> </div>
</div> </div>
@@ -69,21 +72,19 @@ export default function ImageGrid() {
<div className="flex-1 overflow-hidden relative"> <div className="flex-1 overflow-hidden relative">
<div className="h-full overflow-y-auto p-6"> <div className="h-full overflow-y-auto p-6">
<div className="grid grid-cols-5 gap-3"> <div className="grid grid-cols-5 gap-3">
{images.map((image) => ( {images.map(image => (
<button <button
key={image.id} key={image.id}
onClick={() => handleImageClick(image)} onClick={() => handleImageClick(image)}
onMouseEnter={(e) => handleMouseEnter(image, e)} onMouseEnter={e => handleMouseEnter(image, e)}
onMouseMove={handleMouseMove} onMouseMove={handleMouseMove}
onMouseLeave={handleMouseLeave} onMouseLeave={handleMouseLeave}
className={`aspect-square border-2 transition-colors ${ className={`aspect-square border-2 transition-colors ${
selected?.id === image.id selected?.id === image.id ? 'border-white' : 'border-gray-700 hover:border-gray-500'
? 'border-white'
: 'border-gray-700 hover:border-gray-500'
}`} }`}
> >
<canvas <canvas
ref={(canvas) => { ref={canvas => {
if (canvas && image.canvas) { if (canvas && image.canvas) {
const ctx = canvas.getContext('2d')! const ctx = canvas.getContext('2d')!
canvas.width = image.canvas.width canvas.width = image.canvas.width
@@ -119,7 +120,7 @@ export default function ImageGrid() {
}} }}
> >
<canvas <canvas
ref={(canvas) => { ref={canvas => {
if (canvas && hoveredImage.canvas) { if (canvas && hoveredImage.canvas) {
const ctx = canvas.getContext('2d')! const ctx = canvas.getContext('2d')!
canvas.width = 200 canvas.width = 200
@@ -143,11 +144,7 @@ export default function ImageGrid() {
ctx.imageSmoothingEnabled = true ctx.imageSmoothingEnabled = true
} }
ctx.drawImage( ctx.drawImage(sourceCanvas, cropX, cropY, cropSize, cropSize, 0, 0, 200, 200)
sourceCanvas,
cropX, cropY, cropSize, cropSize,
0, 0, 200, 200
)
} }
}} }}
className="w-full h-full block" className="w-full h-full block"

View File

@@ -2,7 +2,6 @@ import { useState, useCallback } from 'react'
import { useStore } from '@nanostores/react' import { useStore } from '@nanostores/react'
import { generateFromPhotoImage } from '../generators/from-photo' import { generateFromPhotoImage } from '../generators/from-photo'
import { generatedImages, selectedImage } from '../stores' import { generatedImages, selectedImage } from '../stores'
import type { GeneratedImage } from '../stores'
interface PhotoDropZoneProps { interface PhotoDropZoneProps {
size: number size: number
@@ -13,7 +12,6 @@ export default function PhotoDropZone({ size }: PhotoDropZoneProps) {
const [isProcessing, setIsProcessing] = useState(false) const [isProcessing, setIsProcessing] = useState(false)
const [error, setError] = useState<string | null>(null) const [error, setError] = useState<string | null>(null)
const images = useStore(generatedImages) const images = useStore(generatedImages)
const selected = useStore(selectedImage)
const handleDragOver = useCallback((e: React.DragEvent) => { const handleDragOver = useCallback((e: React.DragEvent) => {
e.preventDefault() e.preventDefault()
@@ -25,71 +23,45 @@ export default function PhotoDropZone({ size }: PhotoDropZoneProps) {
setIsDragOver(false) setIsDragOver(false)
}, []) }, [])
const handleDrop = useCallback(async (e: React.DragEvent) => { const handleDrop = useCallback(
e.preventDefault() async (e: React.DragEvent) => {
setIsDragOver(false) e.preventDefault()
setError(null) setIsDragOver(false)
const files = Array.from(e.dataTransfer.files)
const imageFiles = files.filter(file => file.type.startsWith('image/'))
if (imageFiles.length === 0) {
setError('Please drop an image file (PNG, JPG, GIF, etc.)')
return
}
if (imageFiles.length > 1) {
setError('Please drop only one image at a time')
return
}
const file = imageFiles[0]
try {
setIsProcessing(true)
const processedImage = await generateFromPhotoImage(file, size)
// Set the single processed image and automatically select it
generatedImages.set([processedImage])
selectedImage.set(processedImage)
console.log('Photo processed successfully:', processedImage)
} catch (error) {
console.error('Error processing image:', error)
setError('Failed to process the image. Please try again.')
} finally {
setIsProcessing(false)
}
}, [size])
const handleFileInput = useCallback(async (e: React.ChangeEvent<HTMLInputElement>) => {
const files = e.target.files
if (!files || files.length === 0) return
const file = files[0]
if (!file.type.startsWith('image/')) {
setError('Please select an image file (PNG, JPG, GIF, etc.)')
return
}
try {
setIsProcessing(true)
setError(null) setError(null)
const processedImage = await generateFromPhotoImage(file, size)
// Set the single processed image and automatically select it const files = Array.from(e.dataTransfer.files)
generatedImages.set([processedImage]) const imageFiles = files.filter(file => file.type.startsWith('image/'))
selectedImage.set(processedImage)
if (imageFiles.length === 0) {
setError('Please drop an image file (PNG, JPG, GIF, etc.)')
return
}
if (imageFiles.length > 1) {
setError('Please drop only one image at a time')
return
}
const file = imageFiles[0]
try {
setIsProcessing(true)
const processedImage = await generateFromPhotoImage(file, size)
// Set the single processed image and automatically select it
generatedImages.set([processedImage])
selectedImage.set(processedImage)
} catch (error) {
console.error('Error processing image:', error)
setError('Failed to process the image. Please try again.')
} finally {
setIsProcessing(false)
}
},
[size]
)
console.log('Photo processed successfully:', processedImage)
} catch (error) {
console.error('Error processing image:', error)
setError('Failed to process the image. Please try again.')
} finally {
setIsProcessing(false)
}
}, [size])
const processedImage = images.length > 0 ? images[0] : null const processedImage = images.length > 0 ? images[0] : null
@@ -107,7 +79,7 @@ export default function PhotoDropZone({ size }: PhotoDropZoneProps) {
onDrop={handleDrop} onDrop={handleDrop}
> >
<canvas <canvas
ref={(canvas) => { ref={canvas => {
if (canvas && processedImage.canvas) { if (canvas && processedImage.canvas) {
const ctx = canvas.getContext('2d')! const ctx = canvas.getContext('2d')!
canvas.width = processedImage.canvas.width canvas.width = processedImage.canvas.width
@@ -131,9 +103,7 @@ export default function PhotoDropZone({ size }: PhotoDropZoneProps) {
// Show the drop zone // Show the drop zone
<div <div
className={`w-full max-w-2xl h-96 border-2 border-dashed flex flex-col items-center justify-center transition-colors ${ className={`w-full max-w-2xl h-96 border-2 border-dashed flex flex-col items-center justify-center transition-colors ${
isDragOver isDragOver ? 'border-white bg-gray-800' : 'border-gray-600 hover:border-gray-400'
? 'border-white bg-gray-800'
: 'border-gray-600 hover:border-gray-400'
} ${isProcessing ? 'opacity-50 pointer-events-none' : ''}`} } ${isProcessing ? 'opacity-50 pointer-events-none' : ''}`}
onDragOver={handleDragOver} onDragOver={handleDragOver}
onDragLeave={handleDragLeave} onDragLeave={handleDragLeave}
@@ -150,11 +120,7 @@ export default function PhotoDropZone({ size }: PhotoDropZoneProps) {
</div> </div>
)} )}
{error && ( {error && <div className="mt-4 text-red-400 text-center">{error}</div>}
<div className="mt-4 text-red-400 text-center">
{error}
</div>
)}
</div> </div>
) )
} }

View File

@@ -30,24 +30,27 @@ function GridSquare({ index, image, onDrop, onSelect, selected }: GridSquareProp
setIsDragOver(false) setIsDragOver(false)
}, []) }, [])
const handleDrop = useCallback(async (e: React.DragEvent) => { const handleDrop = useCallback(
e.preventDefault() async (e: React.DragEvent) => {
setIsDragOver(false) e.preventDefault()
setIsDragOver(false)
const files = Array.from(e.dataTransfer.files) const files = Array.from(e.dataTransfer.files)
const imageFiles = files.filter(file => file.type.startsWith('image/')) const imageFiles = files.filter(file => file.type.startsWith('image/'))
if (imageFiles.length === 0) return if (imageFiles.length === 0) return
const file = imageFiles[0] const file = imageFiles[0]
setIsProcessing(true) setIsProcessing(true)
try { try {
await onDrop(file, index) await onDrop(file, index)
} finally { } finally {
setIsProcessing(false) setIsProcessing(false)
} }
}, [index, onDrop]) },
[index, onDrop]
)
const handleClick = useCallback(() => { const handleClick = useCallback(() => {
if (image) { if (image) {
@@ -72,7 +75,7 @@ function GridSquare({ index, image, onDrop, onSelect, selected }: GridSquareProp
</div> </div>
) : image ? ( ) : image ? (
<canvas <canvas
ref={(canvas) => { ref={canvas => {
if (canvas && image.canvas) { if (canvas && image.canvas) {
const ctx = canvas.getContext('2d')! const ctx = canvas.getContext('2d')!
canvas.width = image.canvas.width canvas.width = image.canvas.width
@@ -102,26 +105,28 @@ export default function PhotoGrid({ size }: PhotoGridProps) {
const images = useStore(generatedImages) const images = useStore(generatedImages)
const selected = useStore(selectedImage) const selected = useStore(selectedImage)
const handleDrop = useCallback(async (file: File, index: number) => { const handleDrop = useCallback(
try { async (file: File, index: number) => {
const processedImage = await generateFromPhotoImage(file, size) try {
const processedImage = await generateFromPhotoImage(file, size)
// Create new images array with the processed image at the specified index // Create new images array with the processed image at the specified index
const newImages = [...images] const newImages = [...images]
newImages[index] = processedImage newImages[index] = processedImage
// Filter out any null/undefined values and update the store // Filter out any null/undefined values and update the store
const filteredImages = newImages.filter(Boolean) as GeneratedImage[] const filteredImages = newImages.filter(Boolean) as GeneratedImage[]
generatedImages.set(filteredImages) generatedImages.set(filteredImages)
// Auto-select the newly processed image // Auto-select the newly processed image
selectedImage.set(processedImage) selectedImage.set(processedImage)
console.log('Photo processed and added to grid at index:', index) } catch (error) {
} catch (error) { console.error('Error processing photo:', error)
console.error('Error processing photo:', error) }
} },
}, [images, size]) [images, size]
)
const handleSelect = useCallback((image: GeneratedImage) => { const handleSelect = useCallback((image: GeneratedImage) => {
selectedImage.set(image) selectedImage.set(image)
@@ -146,9 +151,7 @@ export default function PhotoGrid({ size }: PhotoGridProps) {
return ( return (
<div className="flex-1 overflow-auto p-6"> <div className="flex-1 overflow-auto p-6">
<div className="grid grid-cols-5 gap-2"> <div className="grid grid-cols-5 gap-2">{gridSquares}</div>
{gridSquares}
</div>
</div> </div>
) )
} }

View File

@@ -1,4 +1,5 @@
import { ReactNode, useState } from 'react' import { useState } from 'react'
import type { ReactNode } from 'react'
interface TooltipProps { interface TooltipProps {
content: string content: string
@@ -13,7 +14,7 @@ export default function Tooltip({ content, children, position = 'bottom' }: Tool
top: 'bottom-full left-1/2 transform -translate-x-1/2 mb-2', top: 'bottom-full left-1/2 transform -translate-x-1/2 mb-2',
bottom: 'top-full left-1/2 transform -translate-x-1/2 mt-2', bottom: 'top-full left-1/2 transform -translate-x-1/2 mt-2',
left: 'right-full top-1/2 transform -translate-y-1/2 mr-2', left: 'right-full top-1/2 transform -translate-y-1/2 mr-2',
right: 'left-full top-1/2 transform -translate-y-1/2 ml-2' right: 'left-full top-1/2 transform -translate-y-1/2 ml-2',
} }
return ( return (
@@ -24,7 +25,9 @@ export default function Tooltip({ content, children, position = 'bottom' }: Tool
> >
{children} {children}
{isVisible && ( {isVisible && (
<div className={`absolute z-50 px-2 py-1 bg-black text-white text-sm whitespace-nowrap border border-white ${positionClasses[position]}`}> <div
className={`absolute z-50 px-2 py-1 bg-black text-white text-sm whitespace-nowrap border border-white ${positionClasses[position]}`}
>
{content} {content}
</div> </div>
)} )}

View File

@@ -1,4 +1,4 @@
import { useState, useCallback, useRef, useEffect } from 'react' import { useState, useCallback, useEffect } from 'react'
import { useStore } from '@nanostores/react' import { useStore } from '@nanostores/react'
import { generateFromWebcamImage } from '../generators/webcam' import { generateFromWebcamImage } from '../generators/webcam'
import { WebcamProcessor } from '../generators/webcam-generator' import { WebcamProcessor } from '../generators/webcam-generator'
@@ -37,7 +37,7 @@ function GridSquare({ index, image, onCapture, onSelect, selected, isCapturing }
onClick={handleImageClick} onClick={handleImageClick}
> >
<canvas <canvas
ref={(canvas) => { ref={canvas => {
if (canvas && image.canvas) { if (canvas && image.canvas) {
const ctx = canvas.getContext('2d')! const ctx = canvas.getContext('2d')!
canvas.width = image.canvas.width canvas.width = image.canvas.width
@@ -60,9 +60,7 @@ function GridSquare({ index, image, onCapture, onSelect, selected, isCapturing }
{isCapturing ? ( {isCapturing ? (
<div className="text-xs">Capturing...</div> <div className="text-xs">Capturing...</div>
) : ( ) : (
<div className="text-xs text-center"> <div className="text-xs text-center">Capture</div>
Capture
</div>
)} )}
</button> </button>
)} )}
@@ -100,37 +98,39 @@ export default function WebcamGrid({ size }: WebcamGridProps) {
} }
}, [processor]) }, [processor])
const handleCapture = useCallback(async (index: number) => { const handleCapture = useCallback(
if (!cameraInitialized) { async (index: number) => {
console.error('Camera not initialized') if (!cameraInitialized) {
return console.error('Camera not initialized')
} return
}
setIsCapturing(true) setIsCapturing(true)
setCapturingIndex(index) setCapturingIndex(index)
try { try {
const capturedImage = await generateFromWebcamImage(processor, size) const capturedImage = await generateFromWebcamImage(processor, size)
// Create new images array with the captured image at the specified index // Create new images array with the captured image at the specified index
const newImages = [...images] const newImages = [...images]
newImages[index] = capturedImage newImages[index] = capturedImage
// Filter out any null/undefined values and update the store // Filter out any null/undefined values and update the store
const filteredImages = newImages.filter(Boolean) as GeneratedImage[] const filteredImages = newImages.filter(Boolean) as GeneratedImage[]
generatedImages.set(filteredImages) generatedImages.set(filteredImages)
// Auto-select the newly captured image // Auto-select the newly captured image
selectedImage.set(capturedImage) selectedImage.set(capturedImage)
console.log('Photo captured and added to grid at index:', index) } catch (error) {
} catch (error) { console.error('Error capturing photo:', error)
console.error('Error capturing photo:', error) } finally {
} finally { setIsCapturing(false)
setIsCapturing(false) setCapturingIndex(null)
setCapturingIndex(null) }
} },
}, [images, size, cameraInitialized, processor]) [images, size, cameraInitialized, processor]
)
const handleSelect = useCallback((image: GeneratedImage) => { const handleSelect = useCallback((image: GeneratedImage) => {
selectedImage.set(image) selectedImage.set(image)
@@ -163,9 +163,7 @@ export default function WebcamGrid({ size }: WebcamGridProps) {
</div> </div>
)} )}
<div className="grid grid-cols-5 gap-2"> <div className="grid grid-cols-5 gap-2">{gridSquares}</div>
{gridSquares}
</div>
</div> </div>
) )
} }

View File

@@ -1,10 +1,32 @@
import type { GeneratedImage } from '../stores' import type { GeneratedImage } from '../stores'
const searchTerms = [ const searchTerms = [
'painting', 'sculpture', 'drawing', 'pottery', 'portrait', 'landscape', 'painting',
'impressionist', 'modern', 'contemporary', 'abstract', 'still life', 'sculpture',
'figure', 'nature', 'urban', 'color', 'light', 'texture', 'pattern', 'drawing',
'architecture', 'street', 'woman', 'man', 'cat', 'dog', 'flower', 'tree' 'pottery',
'portrait',
'landscape',
'impressionist',
'modern',
'contemporary',
'abstract',
'still life',
'figure',
'nature',
'urban',
'color',
'light',
'texture',
'pattern',
'architecture',
'street',
'woman',
'man',
'cat',
'dog',
'flower',
'tree',
] ]
interface ArticArtwork { interface ArticArtwork {
@@ -27,7 +49,10 @@ interface ArticSearchResponse {
} }
} }
export async function generateArtInstituteImages(count: number, size: number): Promise<GeneratedImage[]> { export async function generateArtInstituteImages(
count: number,
size: number
): Promise<GeneratedImage[]> {
const images: GeneratedImage[] = [] const images: GeneratedImage[] = []
const maxRetries = count * 3 // Try 3x more objects to account for failures const maxRetries = count * 3 // Try 3x more objects to account for failures
@@ -63,7 +88,9 @@ async function loadArtInstituteImage(size: number, index: number): Promise<Gener
} }
// Filter artworks that have images // Filter artworks that have images
const artworksWithImages = searchData.data.filter(artwork => artwork.image_id && artwork.is_public_domain) const artworksWithImages = searchData.data.filter(
artwork => artwork.image_id && artwork.is_public_domain
)
if (artworksWithImages.length === 0) { if (artworksWithImages.length === 0) {
throw new Error('No artworks with images found') throw new Error('No artworks with images found')
@@ -74,7 +101,7 @@ async function loadArtInstituteImage(size: number, index: number): Promise<Gener
// Construct IIIF image URL - 512px width, auto height // Construct IIIF image URL - 512px width, auto height
const imageUrl = `https://www.artic.edu/iiif/2/${artwork.image_id}/full/512,/0/default.jpg` const imageUrl = `https://www.artic.edu/iiif/2/${artwork.image_id}/full/512,/0/default.jpg`
return new Promise((resolve) => { return new Promise(resolve => {
const img = new Image() const img = new Image()
img.crossOrigin = 'anonymous' img.crossOrigin = 'anonymous'
@@ -122,8 +149,8 @@ async function loadArtInstituteImage(size: number, index: number): Promise<Gener
date: artwork.date_display || 'Unknown Date', date: artwork.date_display || 'Unknown Date',
medium: artwork.medium_display || 'Unknown Medium', medium: artwork.medium_display || 'Unknown Medium',
department: artwork.department_title || 'Unknown Department', department: artwork.department_title || 'Unknown Department',
searchTerm searchTerm,
} },
}) })
} catch (error) { } catch (error) {
console.error('Error processing Art Institute image:', error) console.error('Error processing Art Institute image:', error)

View File

@@ -28,12 +28,12 @@ function generateBandLayer(canvasWidth: number, canvasHeight: number): BandLayer
const x = i * sectionWidth const x = i * sectionWidth
const yValue = Math.random() // 0.0 to 1.0 const yValue = Math.random() // 0.0 to 1.0
// Convert y value where 0.0 is bottom and 1.0 is top // Convert y value where 0.0 is bottom and 1.0 is top
const y = canvasHeight - (yValue * canvasHeight) const y = canvasHeight - yValue * canvasHeight
bands.push({ bands.push({
x, x,
width: sectionWidth, width: sectionWidth,
y y,
}) })
} }
} }
@@ -41,7 +41,7 @@ function generateBandLayer(canvasWidth: number, canvasHeight: number): BandLayer
return { return {
divisions, divisions,
bands, bands,
opacity: 0.8 // Slight transparency for superimposition opacity: 0.8, // Slight transparency for superimposition
} }
} }
@@ -97,13 +97,13 @@ export function generateBandsImages(count: number, size: number): GeneratedImage
bands: layer.bands.map(band => ({ bands: layer.bands.map(band => ({
x: band.x, x: band.x,
width: band.width, width: band.width,
y: band.y y: band.y,
})), })),
opacity: layer.opacity opacity: layer.opacity,
})), })),
strokeHeight, strokeHeight,
size size,
} },
} }
images.push(image) images.push(image)

View File

@@ -16,36 +16,50 @@ const greyLevels = [
'#333333', // Almost black '#333333', // Almost black
'#222222', // Nearly black '#222222', // Nearly black
'#111111', // Extremely dark '#111111', // Extremely dark
'#0f0f0f' // Almost black (minimal amplitude) '#0f0f0f', // Almost black (minimal amplitude)
] ]
interface DustParams { interface DustParams {
pointCount: number pointCount: number
distributionType: 'uniform' | 'clustered' | 'scattered' | 'ring' | 'spiral' | 'grid' | 'noise' | 'radial' | 'perlin' distributionType:
| 'uniform'
| 'clustered'
| 'scattered'
| 'ring'
| 'spiral'
| 'grid'
| 'noise'
| 'radial'
| 'perlin'
concentration: number // 0-1, how concentrated the distribution is concentration: number // 0-1, how concentrated the distribution is
clusterCount?: number // for clustered distribution clusterCount?: number // for clustered distribution
} }
function generateUniformDistribution(count: number, size: number): Array<{x: number, y: number}> { function generateUniformDistribution(count: number, size: number): Array<{ x: number; y: number }> {
const points = [] const points = []
for (let i = 0; i < count; i++) { for (let i = 0; i < count; i++) {
points.push({ points.push({
x: Math.random() * size, x: Math.random() * size,
y: Math.random() * size y: Math.random() * size,
}) })
} }
return points return points
} }
function generateClusteredDistribution(count: number, size: number, clusterCount: number, concentration: number): Array<{x: number, y: number}> { function generateClusteredDistribution(
const points = [] count: number,
const clusters = [] size: number,
clusterCount: number,
concentration: number
): Array<{ x: number; y: number }> {
const points: Array<{ x: number; y: number }> = []
const clusters: Array<{ x: number; y: number }> = []
// Generate cluster centers // Generate cluster centers
for (let i = 0; i < clusterCount; i++) { for (let i = 0; i < clusterCount; i++) {
clusters.push({ clusters.push({
x: Math.random() * size, x: Math.random() * size,
y: Math.random() * size y: Math.random() * size,
}) })
} }
@@ -64,7 +78,7 @@ function generateClusteredDistribution(count: number, size: number, clusterCount
points.push({ points.push({
x: Math.max(0, Math.min(size, cluster.x + Math.cos(angle) * distance)), x: Math.max(0, Math.min(size, cluster.x + Math.cos(angle) * distance)),
y: Math.max(0, Math.min(size, cluster.y + Math.sin(angle) * distance)) y: Math.max(0, Math.min(size, cluster.y + Math.sin(angle) * distance)),
}) })
} }
}) })
@@ -72,23 +86,28 @@ function generateClusteredDistribution(count: number, size: number, clusterCount
return points return points
} }
function generateScatteredDistribution(count: number, size: number, concentration: number): Array<{x: number, y: number}> { function generateScatteredDistribution(
const points = [] count: number,
size: number,
concentration: number
): Array<{ x: number; y: number }> {
const points: Array<{ x: number; y: number }> = []
const exclusionRadius = size * concentration * 0.1 // minimum distance between points const exclusionRadius = size * concentration * 0.1 // minimum distance between points
for (let i = 0; i < count; i++) { for (let i = 0; i < count; i++) {
let attempts = 0 let attempts = 0
let point: {x: number, y: number} let point: { x: number; y: number }
do { do {
point = { point = {
x: Math.random() * size, x: Math.random() * size,
y: Math.random() * size y: Math.random() * size,
} }
attempts++ attempts++
} while (attempts < 50 && points.some(p => } while (
Math.sqrt((p.x - point.x) ** 2 + (p.y - point.y) ** 2) < exclusionRadius attempts < 50 &&
)) points.some(p => Math.sqrt((p.x - point.x) ** 2 + (p.y - point.y) ** 2) < exclusionRadius)
)
points.push(point) points.push(point)
} }
@@ -96,7 +115,11 @@ function generateScatteredDistribution(count: number, size: number, concentratio
return points return points
} }
function generateRingDistribution(count: number, size: number, concentration: number): Array<{x: number, y: number}> { function generateRingDistribution(
count: number,
size: number,
concentration: number
): Array<{ x: number; y: number }> {
const points = [] const points = []
const centerX = size / 2 const centerX = size / 2
const centerY = size / 2 const centerY = size / 2
@@ -110,14 +133,18 @@ function generateRingDistribution(count: number, size: number, concentration: nu
points.push({ points.push({
x: centerX + Math.cos(angle) * finalRadius, x: centerX + Math.cos(angle) * finalRadius,
y: centerY + Math.sin(angle) * finalRadius y: centerY + Math.sin(angle) * finalRadius,
}) })
} }
return points return points
} }
function generateSpiralDistribution(count: number, size: number, concentration: number): Array<{x: number, y: number}> { function generateSpiralDistribution(
count: number,
size: number,
concentration: number
): Array<{ x: number; y: number }> {
const points = [] const points = []
const centerX = size / 2 const centerX = size / 2
const centerY = size / 2 const centerY = size / 2
@@ -131,14 +158,18 @@ function generateSpiralDistribution(count: number, size: number, concentration:
points.push({ points.push({
x: centerX + Math.cos(t) * radius + noise, x: centerX + Math.cos(t) * radius + noise,
y: centerY + Math.sin(t) * radius + noise y: centerY + Math.sin(t) * radius + noise,
}) })
} }
return points return points
} }
function generateGridDistribution(count: number, size: number, concentration: number): Array<{x: number, y: number}> { function generateGridDistribution(
count: number,
size: number,
concentration: number
): Array<{ x: number; y: number }> {
const points = [] const points = []
const gridSize = Math.ceil(Math.sqrt(count)) const gridSize = Math.ceil(Math.sqrt(count))
const cellSize = size / gridSize const cellSize = size / gridSize
@@ -156,15 +187,18 @@ function generateGridDistribution(count: number, size: number, concentration: nu
points.push({ points.push({
x: Math.max(0, Math.min(size, baseX + jitterX)), x: Math.max(0, Math.min(size, baseX + jitterX)),
y: Math.max(0, Math.min(size, baseY + jitterY)) y: Math.max(0, Math.min(size, baseY + jitterY)),
}) })
} }
return points return points
} }
function generateNoiseDistribution(
function generateNoiseDistribution(count: number, size: number, concentration: number): Array<{x: number, y: number}> { count: number,
size: number,
concentration: number
): Array<{ x: number; y: number }> {
const points = [] const points = []
const gridSize = 64 const gridSize = 64
const cellSize = size / gridSize const cellSize = size / gridSize
@@ -177,7 +211,7 @@ function generateNoiseDistribution(count: number, size: number, concentration: n
for (let i = 0; i < count; i++) { for (let i = 0; i < count; i++) {
let attempts = 0 let attempts = 0
let point: {x: number, y: number} let point: { x: number; y: number }
do { do {
const x = Math.random() * size const x = Math.random() * size
@@ -204,7 +238,11 @@ function generateNoiseDistribution(count: number, size: number, concentration: n
return points return points
} }
function generateRadialDistribution(count: number, size: number, concentration: number): Array<{x: number, y: number}> { function generateRadialDistribution(
count: number,
size: number,
concentration: number
): Array<{ x: number; y: number }> {
const points = [] const points = []
const centerX = size / 2 const centerX = size / 2
const centerY = size / 2 const centerY = size / 2
@@ -221,14 +259,18 @@ function generateRadialDistribution(count: number, size: number, concentration:
points.push({ points.push({
x: centerX + Math.cos(angle) * finalRadius, x: centerX + Math.cos(angle) * finalRadius,
y: centerY + Math.sin(angle) * finalRadius y: centerY + Math.sin(angle) * finalRadius,
}) })
} }
return points return points
} }
function generatePerlinLikeDistribution(count: number, size: number, concentration: number): Array<{x: number, y: number}> { function generatePerlinLikeDistribution(
count: number,
size: number,
concentration: number
): Array<{ x: number; y: number }> {
const points = [] const points = []
const scale = 0.01 + concentration * 0.05 const scale = 0.01 + concentration * 0.05
@@ -243,7 +285,7 @@ function generatePerlinLikeDistribution(count: number, size: number, concentrati
for (let i = 0; i < count; i++) { for (let i = 0; i < count; i++) {
let attempts = 0 let attempts = 0
let point: {x: number, y: number} let point: { x: number; y: number }
do { do {
const x = Math.random() * size const x = Math.random() * size
@@ -270,7 +312,17 @@ function generatePerlinLikeDistribution(count: number, size: number, concentrati
export function generateDustImages(count: number, size: number): GeneratedImage[] { export function generateDustImages(count: number, size: number): GeneratedImage[] {
const images: GeneratedImage[] = [] const images: GeneratedImage[] = []
const distributions = ['uniform', 'clustered', 'scattered', 'ring', 'spiral', 'grid', 'noise', 'radial', 'perlin'] as const const distributions = [
'uniform',
'clustered',
'scattered',
'ring',
'spiral',
'grid',
'noise',
'radial',
'perlin',
] as const
for (let i = 0; i < count; i++) { for (let i = 0; i < count; i++) {
try { try {
@@ -295,11 +347,11 @@ export function generateDustImages(count: number, size: number): GeneratedImage[
pointCount, pointCount,
distributionType, distributionType,
concentration, concentration,
clusterCount clusterCount,
} }
// Generate points based on distribution type // Generate points based on distribution type
let points: Array<{x: number, y: number}> let points: Array<{ x: number; y: number }>
switch (distributionType) { switch (distributionType) {
case 'uniform': case 'uniform':
@@ -350,8 +402,8 @@ export function generateDustImages(count: number, size: number): GeneratedImage[
...params, ...params,
pointColor, pointColor,
actualPointCount: points.length, actualPointCount: points.length,
size size,
} },
} }
images.push(image) images.push(image)

View File

@@ -36,7 +36,7 @@ export function processPhoto(file: File, config: PhotoProcessingConfig): Promise
canvas, canvas,
imageData, imageData,
originalFile: file, originalFile: file,
config config,
}) })
} catch (error) { } catch (error) {
reject(error) reject(error)
@@ -55,7 +55,7 @@ function calculateImageDimensions(
imgWidth: number, imgWidth: number,
imgHeight: number, imgHeight: number,
targetSize: number targetSize: number
): { drawWidth: number, drawHeight: number, offsetX: number, offsetY: number } { ): { drawWidth: number; drawHeight: number; offsetX: number; offsetY: number } {
const aspectRatio = imgWidth / imgHeight const aspectRatio = imgWidth / imgHeight
let drawWidth: number let drawWidth: number
@@ -96,7 +96,7 @@ function convertToGrayscale(ctx: CanvasRenderingContext2D, size: number, enhance
} }
// Set R, G, B to the same grayscale value // Set R, G, B to the same grayscale value
data[i] = final // Red data[i] = final // Red
data[i + 1] = final // Green data[i + 1] = final // Green
data[i + 2] = final // Blue data[i + 2] = final // Blue
// Alpha (data[i + 3]) remains unchanged // Alpha (data[i + 3]) remains unchanged

View File

@@ -6,7 +6,7 @@ export async function generateFromPhotoImage(file: File, size: number): Promise<
const result = await processPhoto(file, { const result = await processPhoto(file, {
targetSize: size, targetSize: size,
contrastEnhancement: true, contrastEnhancement: true,
grayscaleConversion: true grayscaleConversion: true,
}) })
const image: GeneratedImage = { const image: GeneratedImage = {
@@ -21,8 +21,8 @@ export async function generateFromPhotoImage(file: File, size: number): Promise<
processedAt: new Date().toISOString(), processedAt: new Date().toISOString(),
size, size,
contrastEnhanced: true, contrastEnhanced: true,
grayscaleConverted: true grayscaleConverted: true,
} },
} }
return image return image
@@ -46,8 +46,8 @@ export async function generateFromPhotoImage(file: File, size: number): Promise<
params: { params: {
fileName: file.name, fileName: file.name,
error: error instanceof Error ? error.message : 'Unknown error', error: error instanceof Error ? error.message : 'Unknown error',
size size,
} },
} }
return fallbackImage return fallbackImage

View File

@@ -27,7 +27,7 @@ const PATTERN_TYPES = [
'tessellation', 'tessellation',
'nestedSquares', 'nestedSquares',
'mosaicSquares', 'mosaicSquares',
'chevrons' 'chevrons',
] ]
// Grayscale base colors for variety // Grayscale base colors for variety
@@ -47,7 +47,7 @@ const GRAYSCALE_COLORS = [
'#cccccc', // Light grey '#cccccc', // Light grey
'#dddddd', // Very light grey '#dddddd', // Very light grey
'#eeeeee', // Almost white '#eeeeee', // Almost white
'#ffffff' // Pure white '#ffffff', // Pure white
] ]
function generateRandomSeed(): string { function generateRandomSeed(): string {
@@ -60,7 +60,10 @@ function generateRandomSeed(): string {
return result return result
} }
function svgToCanvas(svgString: string, size: number): Promise<{ canvas: HTMLCanvasElement, imageData: ImageData }> { function svgToCanvas(
svgString: string,
size: number
): Promise<{ canvas: HTMLCanvasElement; imageData: ImageData }> {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
const canvas = document.createElement('canvas') const canvas = document.createElement('canvas')
const ctx = canvas.getContext('2d')! const ctx = canvas.getContext('2d')!
@@ -106,7 +109,7 @@ function svgToCanvas(svgString: string, size: number): Promise<{ canvas: HTMLCan
final = Math.max(0, Math.min(255, final)) final = Math.max(0, Math.min(255, final))
// Set R, G, B to the same enhanced grayscale value // Set R, G, B to the same enhanced grayscale value
data[i] = final // Red data[i] = final // Red
data[i + 1] = final // Green data[i + 1] = final // Green
data[i + 2] = final // Blue data[i + 2] = final // Blue
// Alpha (data[i + 3]) remains unchanged // Alpha (data[i + 3]) remains unchanged
@@ -130,7 +133,10 @@ function svgToCanvas(svgString: string, size: number): Promise<{ canvas: HTMLCan
}) })
} }
export async function generateGeopatternImages(count: number, size: number): Promise<GeneratedImage[]> { export async function generateGeopatternImages(
count: number,
size: number
): Promise<GeneratedImage[]> {
const images: GeneratedImage[] = [] const images: GeneratedImage[] = []
// Load the geopattern library dynamically // Load the geopattern library dynamically
@@ -150,7 +156,7 @@ export async function generateGeopatternImages(count: number, size: number): Pro
// Generate the geopattern // Generate the geopattern
const pattern = geopatternLib.generate(seed, { const pattern = geopatternLib.generate(seed, {
generator: patternType, generator: patternType,
baseColor: baseColor baseColor: baseColor,
}) })
// Get SVG string // Get SVG string
@@ -169,8 +175,8 @@ export async function generateGeopatternImages(count: number, size: number): Pro
seed, seed,
baseColor, baseColor,
originalSvg: svgString, originalSvg: svgString,
size size,
} },
} }
images.push(image) images.push(image)
@@ -196,8 +202,8 @@ export async function generateGeopatternImages(count: number, size: number): Pro
seed: 'error', seed: 'error',
baseColor: '#000000', baseColor: '#000000',
size, size,
error: error instanceof Error ? error.message : 'Unknown error' error: error instanceof Error ? error.message : 'Unknown error',
} },
} }
images.push(fallbackImage) images.push(fallbackImage)

View File

@@ -11,7 +11,7 @@ const greyLevels = [
'#888888', // Very dark grey '#888888', // Very dark grey
'#777777', // Darker grey '#777777', // Darker grey
'#666666', // Even darker grey '#666666', // Even darker grey
'#555555' // Much darker grey '#555555', // Much darker grey
] ]
interface HarmonicSeries { interface HarmonicSeries {
@@ -54,7 +54,12 @@ function calculateHarmonicAmplitude(harmonicNumber: number, timbre: string): num
} }
function generateHarmonicSeries(canvasSize: number): HarmonicSeries { function generateHarmonicSeries(canvasSize: number): HarmonicSeries {
const timbres: Array<'bright' | 'warm' | 'dark' | 'metallic'> = ['bright', 'warm', 'dark', 'metallic'] const timbres: Array<'bright' | 'warm' | 'dark' | 'metallic'> = [
'bright',
'warm',
'dark',
'metallic',
]
const timbre = timbres[Math.floor(Math.random() * timbres.length)] const timbre = timbres[Math.floor(Math.random() * timbres.length)]
// Fundamental frequency position (in lower 2/3 of canvas for musical range) // Fundamental frequency position (in lower 2/3 of canvas for musical range)
@@ -66,7 +71,7 @@ function generateHarmonicSeries(canvasSize: number): HarmonicSeries {
for (let i = 1; i <= maxHarmonics; i++) { for (let i = 1; i <= maxHarmonics; i++) {
// Higher harmonics = higher frequencies = lower Y positions // Higher harmonics = higher frequencies = lower Y positions
const yPosition = fundamental - (fundamental * (i - 1) / maxHarmonics) const yPosition = fundamental - (fundamental * (i - 1)) / maxHarmonics
// Stop if we go above the canvas // Stop if we go above the canvas
if (yPosition <= 0) break if (yPosition <= 0) break
@@ -78,14 +83,14 @@ function generateHarmonicSeries(canvasSize: number): HarmonicSeries {
frequency: i, frequency: i,
yPosition, yPosition,
amplitude, amplitude,
thickness thickness,
}) })
} }
return { return {
fundamental, fundamental,
harmonics, harmonics,
timbre timbre,
} }
} }
@@ -137,10 +142,10 @@ export function generateHarmonicsImages(count: number, size: number): GeneratedI
frequency: h.frequency, frequency: h.frequency,
yPosition: h.yPosition, yPosition: h.yPosition,
amplitude: h.amplitude, amplitude: h.amplitude,
thickness: h.thickness thickness: h.thickness,
})), })),
size size,
} },
} }
images.push(image) images.push(image)

View File

@@ -8,7 +8,7 @@ const greyLevels = [
'#bbbbbb', // Medium grey '#bbbbbb', // Medium grey
'#aaaaaa', // Medium-dark grey '#aaaaaa', // Medium-dark grey
'#999999', // Dark grey '#999999', // Dark grey
'#888888' // Very dark grey (lower amplitude) '#888888', // Very dark grey (lower amplitude)
] ]
export function generatePartialsImages(count: number, size: number): GeneratedImage[] { export function generatePartialsImages(count: number, size: number): GeneratedImage[] {
@@ -30,7 +30,7 @@ export function generatePartialsImages(count: number, size: number): GeneratedIm
ctx.fillRect(0, 0, size, size) ctx.fillRect(0, 0, size, size)
// Generate line positions, ensuring they don't overlap // Generate line positions, ensuring they don't overlap
const linePositions: Array<{y: number, thickness: number}> = [] const linePositions: Array<{ y: number; thickness: number }> = []
const minSpacing = size / (lineCount + 1) const minSpacing = size / (lineCount + 1)
for (let j = 0; j < lineCount; j++) { for (let j = 0; j < lineCount; j++) {
@@ -59,8 +59,8 @@ export function generatePartialsImages(count: number, size: number): GeneratedIm
lineCount, lineCount,
linePositions: linePositions.map(l => ({ y: l.y, thickness: l.thickness })), linePositions: linePositions.map(l => ({ y: l.y, thickness: l.thickness })),
lineColor, lineColor,
size size,
} },
} }
images.push(image) images.push(image)

View File

@@ -22,56 +22,59 @@ const highQualityIds = [
1, 2, 3, 5, 6, 8, 9, 10, 11, 13, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 1, 2, 3, 5, 6, 8, 9, 10, 11, 13, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30,
35, 36, 37, 39, 40, 42, 43, 44, 47, 48, 49, 50, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 35, 36, 37, 39, 40, 42, 43, 44, 47, 48, 49, 50, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63,
64, 65, 67, 68, 69, 70, 72, 73, 74, 75, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 64, 65, 67, 68, 69, 70, 72, 73, 74, 75, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90,
91, 92, 96, 97, 98, 99, 100, 101, 102, 103, 104, 106, 107, 108, 109, 110, 111, 112, 113, 116, 91, 92, 96, 97, 98, 99, 100, 101, 102, 103, 104, 106, 107, 108, 109, 110, 111, 112, 113, 116, 119,
119, 120, 121, 122, 123, 124, 125, 126, 127, 128, 129, 130, 131, 132, 133, 134, 135, 136, 137, 138, 120, 121, 122, 123, 124, 125, 126, 127, 128, 129, 130, 131, 132, 133, 134, 135, 136, 137, 138,
139, 140, 141, 142, 143, 144, 145, 146, 147, 148, 149, 150, 151, 152, 153, 154, 155, 156, 157, 158, 139, 140, 141, 142, 143, 144, 145, 146, 147, 148, 149, 150, 151, 152, 153, 154, 155, 156, 157,
159, 160, 161, 162, 163, 164, 165, 166, 167, 168, 169, 170, 171, 172, 173, 174, 175, 176, 177, 178, 158, 159, 160, 161, 162, 163, 164, 165, 166, 167, 168, 169, 170, 171, 172, 173, 174, 175, 176,
179, 180, 181, 182, 183, 184, 185, 186, 187, 188, 189, 190, 191, 192, 193, 194, 195, 196, 197, 198, 177, 178, 179, 180, 181, 182, 183, 184, 185, 186, 187, 188, 189, 190, 191, 192, 193, 194, 195,
199, 200, 201, 202, 203, 204, 205, 206, 207, 208, 209, 210, 211, 212, 213, 214, 215, 216, 217, 218, 196, 197, 198, 199, 200, 201, 202, 203, 204, 205, 206, 207, 208, 209, 210, 211, 212, 213, 214,
219, 220, 221, 222, 223, 224, 225, 226, 227, 228, 229, 230, 231, 232, 233, 234, 235, 236, 237, 238, 215, 216, 217, 218, 219, 220, 221, 222, 223, 224, 225, 226, 227, 228, 229, 230, 231, 232, 233,
239, 240, 241, 242, 243, 244, 245, 246, 247, 248, 249, 250, 251, 252, 253, 254, 255, 256, 257, 258, 234, 235, 236, 237, 238, 239, 240, 241, 242, 243, 244, 245, 246, 247, 248, 249, 250, 251, 252,
259, 260, 261, 262, 263, 264, 265, 266, 267, 268, 269, 270, 271, 272, 273, 274, 275, 276, 277, 278, 253, 254, 255, 256, 257, 258, 259, 260, 261, 262, 263, 264, 265, 266, 267, 268, 269, 270, 271,
279, 280, 281, 282, 283, 284, 285, 286, 287, 288, 289, 290, 291, 292, 293, 294, 295, 296, 297, 298, 272, 273, 274, 275, 276, 277, 278, 279, 280, 281, 282, 283, 284, 285, 286, 287, 288, 289, 290,
299, 300, 301, 302, 303, 304, 305, 306, 307, 308, 309, 310, 311, 312, 313, 314, 315, 316, 317, 318, 291, 292, 293, 294, 295, 296, 297, 298, 299, 300, 301, 302, 303, 304, 305, 306, 307, 308, 309,
319, 320, 321, 322, 323, 324, 325, 326, 327, 328, 329, 330, 331, 332, 333, 334, 335, 336, 337, 338, 310, 311, 312, 313, 314, 315, 316, 317, 318, 319, 320, 321, 322, 323, 324, 325, 326, 327, 328,
339, 340, 341, 342, 343, 344, 345, 346, 347, 348, 349, 350, 351, 352, 353, 354, 355, 356, 357, 358, 329, 330, 331, 332, 333, 334, 335, 336, 337, 338, 339, 340, 341, 342, 343, 344, 345, 346, 347,
359, 360, 361, 362, 363, 364, 365, 366, 367, 368, 369, 370, 371, 372, 373, 374, 375, 376, 377, 378, 348, 349, 350, 351, 352, 353, 354, 355, 356, 357, 358, 359, 360, 361, 362, 363, 364, 365, 366,
379, 380, 381, 382, 383, 384, 385, 386, 387, 388, 389, 390, 391, 392, 393, 394, 395, 396, 397, 398, 367, 368, 369, 370, 371, 372, 373, 374, 375, 376, 377, 378, 379, 380, 381, 382, 383, 384, 385,
399, 400, 401, 402, 403, 404, 405, 406, 407, 408, 409, 410, 411, 412, 413, 414, 415, 416, 417, 418, 386, 387, 388, 389, 390, 391, 392, 393, 394, 395, 396, 397, 398, 399, 400, 401, 402, 403, 404,
419, 420, 421, 422, 423, 424, 425, 426, 427, 428, 429, 430, 431, 432, 433, 434, 435, 436, 437, 438, 405, 406, 407, 408, 409, 410, 411, 412, 413, 414, 415, 416, 417, 418, 419, 420, 421, 422, 423,
439, 440, 441, 442, 443, 444, 445, 446, 447, 448, 449, 450, 451, 452, 453, 454, 455, 456, 457, 458, 424, 425, 426, 427, 428, 429, 430, 431, 432, 433, 434, 435, 436, 437, 438, 439, 440, 441, 442,
459, 460, 461, 462, 463, 464, 465, 466, 467, 468, 469, 470, 471, 472, 473, 474, 475, 476, 477, 478, 443, 444, 445, 446, 447, 448, 449, 450, 451, 452, 453, 454, 455, 456, 457, 458, 459, 460, 461,
479, 480, 481, 482, 483, 484, 485, 486, 487, 488, 489, 490, 491, 492, 493, 494, 495, 496, 497, 498, 462, 463, 464, 465, 466, 467, 468, 469, 470, 471, 472, 473, 474, 475, 476, 477, 478, 479, 480,
499, 500, 501, 502, 503, 504, 505, 506, 507, 508, 509, 510, 511, 512, 513, 514, 515, 516, 517, 518, 481, 482, 483, 484, 485, 486, 487, 488, 489, 490, 491, 492, 493, 494, 495, 496, 497, 498, 499,
519, 520, 521, 522, 523, 524, 525, 526, 527, 528, 529, 530, 531, 532, 533, 534, 535, 536, 537, 538, 500, 501, 502, 503, 504, 505, 506, 507, 508, 509, 510, 511, 512, 513, 514, 515, 516, 517, 518,
539, 540, 541, 542, 543, 544, 545, 546, 547, 548, 549, 550, 551, 552, 553, 554, 555, 556, 557, 558, 519, 520, 521, 522, 523, 524, 525, 526, 527, 528, 529, 530, 531, 532, 533, 534, 535, 536, 537,
559, 560, 561, 562, 563, 564, 565, 566, 567, 568, 569, 570, 571, 572, 573, 574, 575, 576, 577, 578, 538, 539, 540, 541, 542, 543, 544, 545, 546, 547, 548, 549, 550, 551, 552, 553, 554, 555, 556,
579, 580, 581, 582, 583, 584, 585, 586, 587, 588, 589, 590, 591, 592, 593, 594, 595, 596, 597, 598, 557, 558, 559, 560, 561, 562, 563, 564, 565, 566, 567, 568, 569, 570, 571, 572, 573, 574, 575,
599, 600, 601, 602, 603, 604, 605, 606, 607, 608, 609, 610, 611, 612, 613, 614, 615, 616, 617, 618, 576, 577, 578, 579, 580, 581, 582, 583, 584, 585, 586, 587, 588, 589, 590, 591, 592, 593, 594,
619, 620, 621, 622, 623, 624, 625, 626, 627, 628, 629, 630, 631, 632, 633, 634, 635, 636, 637, 638, 595, 596, 597, 598, 599, 600, 601, 602, 603, 604, 605, 606, 607, 608, 609, 610, 611, 612, 613,
639, 640, 641, 642, 643, 644, 645, 646, 647, 648, 649, 650, 651, 652, 653, 654, 655, 656, 657, 658, 614, 615, 616, 617, 618, 619, 620, 621, 622, 623, 624, 625, 626, 627, 628, 629, 630, 631, 632,
659, 660, 661, 662, 663, 664, 665, 666, 667, 668, 669, 670, 671, 672, 673, 674, 675, 676, 677, 678, 633, 634, 635, 636, 637, 638, 639, 640, 641, 642, 643, 644, 645, 646, 647, 648, 649, 650, 651,
679, 680, 681, 682, 683, 684, 685, 686, 687, 688, 689, 690, 691, 692, 693, 694, 695, 696, 697, 698, 652, 653, 654, 655, 656, 657, 658, 659, 660, 661, 662, 663, 664, 665, 666, 667, 668, 669, 670,
699, 700, 701, 702, 703, 704, 705, 706, 707, 708, 709, 710, 711, 712, 713, 714, 715, 716, 717, 718, 671, 672, 673, 674, 675, 676, 677, 678, 679, 680, 681, 682, 683, 684, 685, 686, 687, 688, 689,
719, 720, 721, 722, 723, 724, 725, 726, 727, 728, 729, 730, 731, 732, 733, 734, 735, 736, 737, 738, 690, 691, 692, 693, 694, 695, 696, 697, 698, 699, 700, 701, 702, 703, 704, 705, 706, 707, 708,
739, 740, 741, 742, 743, 744, 745, 746, 747, 748, 749, 750, 751, 752, 753, 754, 755, 756, 757, 758, 709, 710, 711, 712, 713, 714, 715, 716, 717, 718, 719, 720, 721, 722, 723, 724, 725, 726, 727,
759, 760, 761, 762, 763, 764, 765, 766, 767, 768, 769, 770, 771, 772, 773, 774, 775, 776, 777, 778, 728, 729, 730, 731, 732, 733, 734, 735, 736, 737, 738, 739, 740, 741, 742, 743, 744, 745, 746,
779, 780, 781, 782, 783, 784, 785, 786, 787, 788, 789, 790, 791, 792, 793, 794, 795, 796, 797, 798, 747, 748, 749, 750, 751, 752, 753, 754, 755, 756, 757, 758, 759, 760, 761, 762, 763, 764, 765,
799, 800, 801, 802, 803, 804, 805, 806, 807, 808, 809, 810, 811, 812, 813, 814, 815, 816, 817, 818, 766, 767, 768, 769, 770, 771, 772, 773, 774, 775, 776, 777, 778, 779, 780, 781, 782, 783, 784,
819, 820, 821, 822, 823, 824, 825, 826, 827, 828, 829, 830, 831, 832, 833, 834, 835, 836, 837, 838, 785, 786, 787, 788, 789, 790, 791, 792, 793, 794, 795, 796, 797, 798, 799, 800, 801, 802, 803,
839, 840, 841, 842, 843, 844, 845, 846, 847, 848, 849, 850, 851, 852, 853, 854, 855, 856, 857, 858, 804, 805, 806, 807, 808, 809, 810, 811, 812, 813, 814, 815, 816, 817, 818, 819, 820, 821, 822,
859, 860, 861, 862, 863, 864, 865, 866, 867, 868, 869, 870, 871, 872, 873, 874, 875, 876, 877, 878, 823, 824, 825, 826, 827, 828, 829, 830, 831, 832, 833, 834, 835, 836, 837, 838, 839, 840, 841,
879, 880, 881, 882, 883, 884, 885, 886, 887, 888, 889, 890, 891, 892, 893, 894, 895, 896, 897, 898, 842, 843, 844, 845, 846, 847, 848, 849, 850, 851, 852, 853, 854, 855, 856, 857, 858, 859, 860,
899, 900, 901, 902, 903, 904, 905, 906, 907, 908, 909, 910, 911, 912, 913, 914, 915, 916, 917, 918, 861, 862, 863, 864, 865, 866, 867, 868, 869, 870, 871, 872, 873, 874, 875, 876, 877, 878, 879,
919, 920, 921, 922, 923, 924, 925, 926, 927, 928, 929, 930, 931, 932, 933, 934, 935, 936, 937, 938, 880, 881, 882, 883, 884, 885, 886, 887, 888, 889, 890, 891, 892, 893, 894, 895, 896, 897, 898,
939, 940, 941, 942, 943, 944, 945, 946, 947, 948, 949, 950, 951, 952, 953, 954, 955, 956, 957, 958, 899, 900, 901, 902, 903, 904, 905, 906, 907, 908, 909, 910, 911, 912, 913, 914, 915, 916, 917,
959, 960, 961, 962, 963, 964, 965, 966, 967, 968, 969, 970, 971, 972, 973, 974, 975, 976, 977, 978, 918, 919, 920, 921, 922, 923, 924, 925, 926, 927, 928, 929, 930, 931, 932, 933, 934, 935, 936,
979, 980, 981, 982, 983, 984, 985, 986, 987, 988, 989, 990, 991, 992, 993, 994, 995, 996, 997, 998, 937, 938, 939, 940, 941, 942, 943, 944, 945, 946, 947, 948, 949, 950, 951, 952, 953, 954, 955,
999, 1000, 1001, 1002, 1003, 1004, 1005, 1006, 1007, 1008, 1009, 1010, 1011, 1012, 1013, 1014, 1015, 1016, 956, 957, 958, 959, 960, 961, 962, 963, 964, 965, 966, 967, 968, 969, 970, 971, 972, 973, 974,
1017, 1018, 1019, 1020, 1021, 1022, 1023, 1024, 1025, 1026, 1027, 1028, 1029, 1030, 1031, 1032, 1033, 1034, 975, 976, 977, 978, 979, 980, 981, 982, 983, 984, 985, 986, 987, 988, 989, 990, 991, 992, 993,
1035, 1036, 1037, 1038, 1039, 1040, 1041, 1042, 1043, 1044, 1045, 1046, 1047, 1048, 1049, 1050, 1051, 1052, 994, 995, 996, 997, 998, 999, 1000, 1001, 1002, 1003, 1004, 1005, 1006, 1007, 1008, 1009, 1010,
1053, 1054, 1055, 1056, 1057, 1058, 1059, 1060, 1061, 1062, 1063, 1064, 1065, 1066, 1067, 1068, 1069, 1070, 1011, 1012, 1013, 1014, 1015, 1016, 1017, 1018, 1019, 1020, 1021, 1022, 1023, 1024, 1025, 1026,
1071, 1072, 1073, 1074, 1075, 1076, 1077, 1078, 1079, 1080, 1081, 1082, 1083, 1084 1027, 1028, 1029, 1030, 1031, 1032, 1033, 1034, 1035, 1036, 1037, 1038, 1039, 1040, 1041, 1042,
1043, 1044, 1045, 1046, 1047, 1048, 1049, 1050, 1051, 1052, 1053, 1054, 1055, 1056, 1057, 1058,
1059, 1060, 1061, 1062, 1063, 1064, 1065, 1066, 1067, 1068, 1069, 1070, 1071, 1072, 1073, 1074,
1075, 1076, 1077, 1078, 1079, 1080, 1081, 1082, 1083, 1084,
] ]
// Keep track of recently used IDs to avoid immediate repeats // Keep track of recently used IDs to avoid immediate repeats
@@ -97,7 +100,7 @@ async function loadPicsumImage(size: number, index: number): Promise<GeneratedIm
const img = new Image() const img = new Image()
img.crossOrigin = 'anonymous' img.crossOrigin = 'anonymous'
return new Promise((resolve, reject) => { return new Promise((resolve, _reject) => {
img.onload = () => { img.onload = () => {
try { try {
const canvas = document.createElement('canvas') const canvas = document.createElement('canvas')
@@ -114,7 +117,7 @@ async function loadPicsumImage(size: number, index: number): Promise<GeneratedIm
canvas, canvas,
imageData, imageData,
generator: 'picsum', generator: 'picsum',
params: { url, imageId } params: { url, imageId },
}) })
} catch (error) { } catch (error) {
console.error('Error processing Picsum image:', error) console.error('Error processing Picsum image:', error)

View File

@@ -1,13 +1,32 @@
import type { GeneratedImage } from '../stores' import type { GeneratedImage } from '../stores'
type ShapeType = 'circle' | 'square' | 'triangle' | 'diamond' | 'pentagon' | 'hexagon' | type ShapeType =
'star' | 'cross' | 'ellipse' | 'rect' | 'octagon' | 'arrow' | 'ring' | 'circle'
| 'square'
| 'triangle'
| 'diamond'
| 'pentagon'
| 'hexagon'
| 'star'
| 'cross'
| 'ellipse'
| 'rect'
| 'octagon'
| 'arrow'
| 'ring'
type FillStyle = 'solid' | 'outline' type FillStyle = 'solid' | 'outline'
const greyLevels = [ const greyLevels = [
'#ffffff', '#eeeeee', '#dddddd', '#cccccc', '#ffffff',
'#bbbbbb', '#aaaaaa', '#999999', '#888888', '#777777' '#eeeeee',
'#dddddd',
'#cccccc',
'#bbbbbb',
'#aaaaaa',
'#999999',
'#888888',
'#777777',
] ]
interface Shape { interface Shape {
@@ -172,8 +191,8 @@ function drawPolygon(ctx: CanvasRenderingContext2D, shape: Shape, sides: number)
for (let i = 0; i < sides; i++) { for (let i = 0; i < sides; i++) {
const angle = (i * 2 * Math.PI) / sides const angle = (i * 2 * Math.PI) / sides
const px = Math.cos(angle) * shape.size / 2 const px = (Math.cos(angle) * shape.size) / 2
const py = Math.sin(angle) * shape.size / 2 const py = (Math.sin(angle) * shape.size) / 2
if (i === 0) { if (i === 0) {
ctx.moveTo(px, py) ctx.moveTo(px, py)
@@ -234,8 +253,19 @@ function drawShape(ctx: CanvasRenderingContext2D, shape: Shape) {
export function generateShapesImages(count: number, size: number): GeneratedImage[] { export function generateShapesImages(count: number, size: number): GeneratedImage[] {
const images: GeneratedImage[] = [] const images: GeneratedImage[] = []
const shapeTypes: ShapeType[] = [ const shapeTypes: ShapeType[] = [
'circle', 'square', 'triangle', 'diamond', 'pentagon', 'hexagon', 'circle',
'star', 'cross', 'ellipse', 'rect', 'octagon', 'arrow', 'ring' 'square',
'triangle',
'diamond',
'pentagon',
'hexagon',
'star',
'cross',
'ellipse',
'rect',
'octagon',
'arrow',
'ring',
] ]
const fillStyles: FillStyle[] = ['solid', 'outline'] const fillStyles: FillStyle[] = ['solid', 'outline']
@@ -313,7 +343,7 @@ export function generateShapesImages(count: number, size: number): GeneratedImag
rotation: Math.random() * Math.PI * 2, rotation: Math.random() * Math.PI * 2,
color, color,
fillStyle, fillStyle,
strokeWidth: Math.floor(Math.random() * 4) + 1 // 1-4px stroke strokeWidth: Math.floor(Math.random() * 4) + 1, // 1-4px stroke
} }
shapes.push(shape) shapes.push(shape)
@@ -342,10 +372,10 @@ export function generateShapesImages(count: number, size: number): GeneratedImag
rotation: s.rotation, rotation: s.rotation,
color: s.color, color: s.color,
fillStyle: s.fillStyle, fillStyle: s.fillStyle,
strokeWidth: s.strokeWidth strokeWidth: s.strokeWidth,
})), })),
size size,
} },
} }
images.push(image) images.push(image)

View File

@@ -11,10 +11,18 @@ const greyLevels = [
'#888888', // Very dark grey (lower amplitude) '#888888', // Very dark grey (lower amplitude)
'#777777', // Darker grey '#777777', // Darker grey
'#666666', // Even darker grey '#666666', // Even darker grey
'#555555' // Very dark grey '#555555', // Very dark grey
] ]
type StrokeType = 'linear' | 'logarithmic' | 'exponential' | 'cubic' | 'sine' | 'bounce' | 'elastic' | 'zigzag' type StrokeType =
| 'linear'
| 'logarithmic'
| 'exponential'
| 'cubic'
| 'sine'
| 'bounce'
| 'elastic'
| 'zigzag'
interface LinePoint { interface LinePoint {
x: number x: number
@@ -28,7 +36,12 @@ interface SlideLine {
strokeType: StrokeType strokeType: StrokeType
} }
function generateStrokePath(startY: number, endY: number, width: number, strokeType: StrokeType): LinePoint[] { function generateStrokePath(
startY: number,
endY: number,
width: number,
strokeType: StrokeType
): LinePoint[] {
const points: LinePoint[] = [] const points: LinePoint[] = []
const steps = width const steps = width
@@ -42,10 +55,10 @@ function generateStrokePath(startY: number, endY: number, width: number, strokeT
y = startY + (endY - startY) * t y = startY + (endY - startY) * t
break break
case 'logarithmic': case 'logarithmic':
y = startY + (endY - startY) * Math.log(1 + t * (Math.E - 1)) / Math.log(Math.E) y = startY + ((endY - startY) * Math.log(1 + t * (Math.E - 1))) / Math.log(Math.E)
break break
case 'exponential': case 'exponential':
y = startY + (endY - startY) * (Math.exp(t) - 1) / (Math.exp(1) - 1) y = startY + ((endY - startY) * (Math.exp(t) - 1)) / (Math.exp(1) - 1)
break break
case 'cubic': case 'cubic':
const easedT = t * t * (3 - 2 * t) // smooth step const easedT = t * t * (3 - 2 * t) // smooth step
@@ -57,24 +70,28 @@ function generateStrokePath(startY: number, endY: number, width: number, strokeT
break break
case 'bounce': case 'bounce':
let bounceT = t let bounceT = t
if (bounceT < 1/2.75) { if (bounceT < 1 / 2.75) {
bounceT = 7.5625 * bounceT * bounceT bounceT = 7.5625 * bounceT * bounceT
} else if (bounceT < 2/2.75) { } else if (bounceT < 2 / 2.75) {
bounceT = 7.5625 * (bounceT - 1.5/2.75) * (bounceT - 1.5/2.75) + 0.75 bounceT = 7.5625 * (bounceT - 1.5 / 2.75) * (bounceT - 1.5 / 2.75) + 0.75
} else if (bounceT < 2.5/2.75) { } else if (bounceT < 2.5 / 2.75) {
bounceT = 7.5625 * (bounceT - 2.25/2.75) * (bounceT - 2.25/2.75) + 0.9375 bounceT = 7.5625 * (bounceT - 2.25 / 2.75) * (bounceT - 2.25 / 2.75) + 0.9375
} else { } else {
bounceT = 7.5625 * (bounceT - 2.625/2.75) * (bounceT - 2.625/2.75) + 0.984375 bounceT = 7.5625 * (bounceT - 2.625 / 2.75) * (bounceT - 2.625 / 2.75) + 0.984375
} }
y = startY + (endY - startY) * bounceT y = startY + (endY - startY) * bounceT
break break
case 'elastic': case 'elastic':
const elasticT = t === 0 ? 0 : t === 1 ? 1 : const elasticT =
Math.pow(2, -10 * t) * Math.sin((t * 10 - 0.75) * (2 * Math.PI) / 3) + 1 t === 0
? 0
: t === 1
? 1
: Math.pow(2, -10 * t) * Math.sin(((t * 10 - 0.75) * (2 * Math.PI)) / 3) + 1
y = startY + (endY - startY) * elasticT y = startY + (endY - startY) * elasticT
break break
case 'zigzag': case 'zigzag':
const zigzagT = Math.abs((t * 6) % 2 - 1) // creates zigzag pattern const zigzagT = Math.abs(((t * 6) % 2) - 1) // creates zigzag pattern
y = startY + (endY - startY) * zigzagT y = startY + (endY - startY) * zigzagT
break break
default: default:
@@ -107,7 +124,16 @@ export function generateSlidesImages(count: number, size: number): GeneratedImag
// Generate slides with more variety // Generate slides with more variety
const slides: (SlideLine & { color: string })[] = [] const slides: (SlideLine & { color: string })[] = []
const strokeTypes: StrokeType[] = ['linear', 'logarithmic', 'exponential', 'cubic', 'sine', 'bounce', 'elastic', 'zigzag'] const strokeTypes: StrokeType[] = [
'linear',
'logarithmic',
'exponential',
'cubic',
'sine',
'bounce',
'elastic',
'zigzag',
]
for (let j = 0; j < lineCount; j++) { for (let j = 0; j < lineCount; j++) {
// More varied positioning - some can go edge to edge // More varied positioning - some can go edge to edge
@@ -153,11 +179,11 @@ export function generateSlidesImages(count: number, size: number): GeneratedImag
startY: s.startY, startY: s.startY,
endY: s.endY, endY: s.endY,
thickness: s.thickness, thickness: s.thickness,
strokeType: s.strokeType strokeType: s.strokeType,
})), })),
colors: slides.map(s => s.color), colors: slides.map(s => s.color),
size size,
} },
} }
images.push(image) images.push(image)

View File

@@ -5,6 +5,7 @@ A standalone module for generating Tixy-like shader patterns in JavaScript/TypeS
## What is Tixy? ## What is Tixy?
Tixy is a minimalist programming language designed by Martin Kleppe for creating visual patterns using 4 variables: Tixy is a minimalist programming language designed by Martin Kleppe for creating visual patterns using 4 variables:
- `t` - time - `t` - time
- `i` - index (pixel index in the grid) - `i` - index (pixel index in the grid)
- `x` - x coordinate - `x` - x coordinate
@@ -24,7 +25,7 @@ const result = renderTixyToCanvas(expression, {
height: 64, height: 64,
time: 0, time: 0,
backgroundColor: '#000000', backgroundColor: '#000000',
foregroundColor: '#ffffff' foregroundColor: '#ffffff',
}) })
// Add to DOM // Add to DOM

View File

@@ -2,9 +2,28 @@ import type { TixyFunction, TixyExpression } from './types'
import { TixyFormulaGenerator } from './formula-generator' import { TixyFormulaGenerator } from './formula-generator'
const MATH_METHODS = [ const MATH_METHODS = [
'abs', 'acos', 'asin', 'atan', 'atan2', 'ceil', 'cos', 'exp', 'floor', 'abs',
'log', 'max', 'min', 'pow', 'random', 'round', 'sin', 'sqrt', 'tan', 'acos',
'sinh', 'cosh', 'tanh', 'sign' 'asin',
'atan',
'atan2',
'ceil',
'cos',
'exp',
'floor',
'log',
'max',
'min',
'pow',
'random',
'round',
'sin',
'sqrt',
'tan',
'sinh',
'cosh',
'tanh',
'sign',
] ]
export function compileTixyExpression(code: string): TixyExpression { export function compileTixyExpression(code: string): TixyExpression {
@@ -22,7 +41,7 @@ export function compileTixyExpression(code: string): TixyExpression {
return { return {
code: processedCode, code: processedCode,
compiled compiled,
} }
} catch (error) { } catch (error) {
throw new Error(`Failed to compile Tixy expression: ${error}`) throw new Error(`Failed to compile Tixy expression: ${error}`)
@@ -48,11 +67,11 @@ export function evaluateTixyExpression(
const formulaGenerator = new TixyFormulaGenerator() const formulaGenerator = new TixyFormulaGenerator()
// Generate expressions dynamically // Generate expressions dynamically
export function generateTixyExpression(): { expression: string, description: string } { export function generateTixyExpression(): { expression: string; description: string } {
const result = formulaGenerator.generateFormula() const result = formulaGenerator.generateFormula()
return { return {
expression: result.expression, expression: result.expression,
description: result.description description: result.description,
} }
} }
@@ -61,10 +80,19 @@ export function getExampleExpressions(): Record<string, string> {
const expressions: Record<string, string> = {} const expressions: Record<string, string> = {}
// Generate expressions for each theme // Generate expressions for each theme
const themes = ['organic', 'geometric', 'interference', 'chaotic', 'minimalist', 'psychedelic', 'bitwise'] as const const themes = [
'organic',
'geometric',
'interference',
'chaotic',
'minimalist',
'psychedelic',
'bitwise',
] as const
themes.forEach(theme => { themes.forEach(theme => {
for (let i = 0; i < 5; i++) { // Generate 5 expressions per theme for (let i = 0; i < 5; i++) {
// Generate 5 expressions per theme
const result = formulaGenerator.generateFormula(theme) const result = formulaGenerator.generateFormula(theme)
expressions[result.expression] = result.description expressions[result.expression] = result.description
} }

View File

@@ -1,5 +1,18 @@
export type Theme = 'organic' | 'geometric' | 'interference' | 'chaotic' | 'minimalist' | 'psychedelic' | 'bitwise' export type Theme =
export type CoordinateSpace = 'cartesian' | 'polar' | 'log_polar' | 'hyperbolic' | 'wave_distort' | 'spiral' | 'organic'
| 'geometric'
| 'interference'
| 'chaotic'
| 'minimalist'
| 'psychedelic'
| 'bitwise'
export type CoordinateSpace =
| 'cartesian'
| 'polar'
| 'log_polar'
| 'hyperbolic'
| 'wave_distort'
| 'spiral'
interface PatternConfig { interface PatternConfig {
weight: number weight: number
@@ -45,28 +58,28 @@ const COORDINATE_TRANSFORMS = {
polar: (x: string, y: string) => ({ polar: (x: string, y: string) => ({
x: `sqrt((${x}-8)**2+(${y}-8)**2)`, // r x: `sqrt((${x}-8)**2+(${y}-8)**2)`, // r
y: `atan2(${y}-8,${x}-8)` // θ y: `atan2(${y}-8,${x}-8)`, // θ
}), }),
log_polar: (x: string, y: string) => ({ log_polar: (x: string, y: string) => ({
x: `log(sqrt((${x}-8)**2+(${y}-8)**2)+1)`, // log(r) x: `log(sqrt((${x}-8)**2+(${y}-8)**2)+1)`, // log(r)
y: `atan2(${y}-8,${x}-8)` // θ y: `atan2(${y}-8,${x}-8)`, // θ
}), }),
hyperbolic: (x: string, y: string) => ({ hyperbolic: (x: string, y: string) => ({
x: `(${x}-8)/(1+((${x}-8)**2+(${y}-8)**2)/64)`, x: `(${x}-8)/(1+((${x}-8)**2+(${y}-8)**2)/64)`,
y: `(${y}-8)/(1+((${x}-8)**2+(${y}-8)**2)/64)` y: `(${y}-8)/(1+((${x}-8)**2+(${y}-8)**2)/64)`,
}), }),
wave_distort: (x: string, y: string, freq = 0.5, amp = 2) => ({ wave_distort: (x: string, y: string, freq = 0.5, amp = 2) => ({
x: `${x}+sin(${y}*${freq})*${amp}`, x: `${x}+sin(${y}*${freq})*${amp}`,
y: `${y}+cos(${x}*${freq})*${amp}` y: `${y}+cos(${x}*${freq})*${amp}`,
}), }),
spiral: (x: string, y: string, tightness = 0.3) => ({ spiral: (x: string, y: string, tightness = 0.3) => ({
x: `sqrt((${x}-8)**2+(${y}-8)**2)*cos(atan2(${y}-8,${x}-8)+sqrt((${x}-8)**2+(${y}-8)**2)*${tightness})`, x: `sqrt((${x}-8)**2+(${y}-8)**2)*cos(atan2(${y}-8,${x}-8)+sqrt((${x}-8)**2+(${y}-8)**2)*${tightness})`,
y: `sqrt((${x}-8)**2+(${y}-8)**2)*sin(atan2(${y}-8,${x}-8)+sqrt((${x}-8)**2+(${y}-8)**2)*${tightness})` y: `sqrt((${x}-8)**2+(${y}-8)**2)*sin(atan2(${y}-8,${x}-8)+sqrt((${x}-8)**2+(${y}-8)**2)*${tightness})`,
}) }),
} }
// Pattern building blocks with parameterization // Pattern building blocks with parameterization
@@ -74,19 +87,27 @@ const PATTERN_GENERATORS = {
// Wave patterns // Wave patterns
sine_wave: (freq: number, phase: number, axis: 'x' | 'y' | 'xy' | 'radial') => { sine_wave: (freq: number, phase: number, axis: 'x' | 'y' | 'xy' | 'radial') => {
switch (axis) { switch (axis) {
case 'x': return `sin(x*${freq.toFixed(2)}+t*${phase.toFixed(2)})` case 'x':
case 'y': return `sin(y*${freq.toFixed(2)}+t*${phase.toFixed(2)})` return `sin(x*${freq.toFixed(2)}+t*${phase.toFixed(2)})`
case 'xy': return `sin((x+y)*${freq.toFixed(2)}+t*${phase.toFixed(2)})` case 'y':
case 'radial': return `sin(sqrt(x*x+y*y)*${freq.toFixed(2)}+t*${phase.toFixed(2)})` return `sin(y*${freq.toFixed(2)}+t*${phase.toFixed(2)})`
case 'xy':
return `sin((x+y)*${freq.toFixed(2)}+t*${phase.toFixed(2)})`
case 'radial':
return `sin(sqrt(x*x+y*y)*${freq.toFixed(2)}+t*${phase.toFixed(2)})`
} }
}, },
cos_wave: (freq: number, phase: number, axis: 'x' | 'y' | 'xy' | 'radial') => { cos_wave: (freq: number, phase: number, axis: 'x' | 'y' | 'xy' | 'radial') => {
switch (axis) { switch (axis) {
case 'x': return `cos(x*${freq.toFixed(2)}+t*${phase.toFixed(2)})` case 'x':
case 'y': return `cos(y*${freq.toFixed(2)}+t*${phase.toFixed(2)})` return `cos(x*${freq.toFixed(2)}+t*${phase.toFixed(2)})`
case 'xy': return `cos((x+y)*${freq.toFixed(2)}+t*${phase.toFixed(2)})` case 'y':
case 'radial': return `cos(sqrt(x*x+y*y)*${freq.toFixed(2)}+t*${phase.toFixed(2)})` return `cos(y*${freq.toFixed(2)}+t*${phase.toFixed(2)})`
case 'xy':
return `cos((x+y)*${freq.toFixed(2)}+t*${phase.toFixed(2)})`
case 'radial':
return `cos(sqrt(x*x+y*y)*${freq.toFixed(2)}+t*${phase.toFixed(2)})`
} }
}, },
@@ -100,11 +121,11 @@ const PATTERN_GENERATORS = {
// Grid patterns // Grid patterns
grid: (sizeX: number, sizeY: number, phase: number) => grid: (sizeX: number, sizeY: number, phase: number) =>
`sin((x%${sizeX.toFixed(1)})*${(2*Math.PI/sizeX).toFixed(2)}+t*${phase.toFixed(2)})*cos((y%${sizeY.toFixed(1)})*${(2*Math.PI/sizeY).toFixed(2)}+t*${phase.toFixed(2)})`, `sin((x%${sizeX.toFixed(1)})*${((2 * Math.PI) / sizeX).toFixed(2)}+t*${phase.toFixed(2)})*cos((y%${sizeY.toFixed(1)})*${((2 * Math.PI) / sizeY).toFixed(2)}+t*${phase.toFixed(2)})`,
// Noise patterns // Noise patterns
noise: (scaleX: number, scaleY: number, evolution: number) => noise: (scaleX: number, scaleY: number, evolution: number) =>
`sin(x*${scaleX.toFixed(2)}+y*${scaleY.toFixed(2)}+t*${evolution.toFixed(2)})*cos(x*${(scaleX*1.618).toFixed(2)}+y*${(scaleY*0.618).toFixed(2)}+t*${(evolution*1.414).toFixed(2)})`, `sin(x*${scaleX.toFixed(2)}+y*${scaleY.toFixed(2)}+t*${evolution.toFixed(2)})*cos(x*${(scaleX * 1.618).toFixed(2)}+y*${(scaleY * 0.618).toFixed(2)}+t*${(evolution * 1.414).toFixed(2)})`,
// Geometric shapes // Geometric shapes
checkerboard: (size: number, phase: number) => checkerboard: (size: number, phase: number) =>
@@ -148,9 +169,12 @@ const PATTERN_GENERATORS = {
modular_arithmetic: (modX: number, modY: number, operation: 'add' | 'mult' | 'xor') => { modular_arithmetic: (modX: number, modY: number, operation: 'add' | 'mult' | 'xor') => {
switch (operation) { switch (operation) {
case 'add': return `((floor(x)%${Math.floor(modX)})+(floor(y)%${Math.floor(modY)}))%16/8` case 'add':
case 'mult': return `((floor(x)%${Math.floor(modX)})*(floor(y)%${Math.floor(modY)}))%16/8` return `((floor(x)%${Math.floor(modX)})+(floor(y)%${Math.floor(modY)}))%16/8`
case 'xor': return `((floor(x)%${Math.floor(modX)})^(floor(y)%${Math.floor(modY)}))%16/8` case 'mult':
return `((floor(x)%${Math.floor(modX)})*(floor(y)%${Math.floor(modY)}))%16/8`
case 'xor':
return `((floor(x)%${Math.floor(modX)})^(floor(y)%${Math.floor(modY)}))%16/8`
} }
}, },
@@ -168,16 +192,16 @@ const PATTERN_GENERATORS = {
`sin(${a.toFixed(2)}*x+${b.toFixed(2)}*y+t)*cos(${c.toFixed(2)}*x*y+t*2)`, `sin(${a.toFixed(2)}*x+${b.toFixed(2)}*y+t)*cos(${c.toFixed(2)}*x*y+t*2)`,
voronoi: (seed1: number, seed2: number, scale: number) => voronoi: (seed1: number, seed2: number, scale: number) =>
`min(sqrt((x-${seed1.toFixed(1)})**2+(y-${seed2.toFixed(1)})**2),sqrt((x-${(16-seed1).toFixed(1)})**2+(y-${(16-seed2).toFixed(1)})**2))*${scale.toFixed(2)}`, `min(sqrt((x-${seed1.toFixed(1)})**2+(y-${seed2.toFixed(1)})**2),sqrt((x-${(16 - seed1).toFixed(1)})**2+(y-${(16 - seed2).toFixed(1)})**2))*${scale.toFixed(2)}`,
reaction_diffusion: (diffusion: number, reaction: number) => reaction_diffusion: (diffusion: number, reaction: number) =>
`tanh((sin(x*${diffusion.toFixed(2)}+t)*cos(y*${diffusion.toFixed(2)}+t)-${reaction.toFixed(2)})*4)`, `tanh((sin(x*${diffusion.toFixed(2)}+t)*cos(y*${diffusion.toFixed(2)}+t)-${reaction.toFixed(2)})*4)`,
fractal_noise: (octaves: number, persistence: number) => fractal_noise: (octaves: number, persistence: number) =>
`sin(x*${octaves.toFixed(1)}+t)*${persistence.toFixed(2)}+sin(x*${(octaves*2).toFixed(1)}+t)*${(persistence*0.5).toFixed(2)}+sin(x*${(octaves*4).toFixed(1)}+t)*${(persistence*0.25).toFixed(2)}`, `sin(x*${octaves.toFixed(1)}+t)*${persistence.toFixed(2)}+sin(x*${(octaves * 2).toFixed(1)}+t)*${(persistence * 0.5).toFixed(2)}+sin(x*${(octaves * 4).toFixed(1)}+t)*${(persistence * 0.25).toFixed(2)}`,
musical_harmony: (fundamental: number, overtone: number) => musical_harmony: (fundamental: number, overtone: number) =>
`sin(x*${fundamental.toFixed(2)}+t)+sin(x*${(fundamental*overtone).toFixed(2)}+t*${PHI.toFixed(3)})*0.5`, `sin(x*${fundamental.toFixed(2)}+t)+sin(x*${(fundamental * overtone).toFixed(2)}+t*${PHI.toFixed(3)})*0.5`,
} }
// Theme configurations with weighted pattern preferences // Theme configurations with weighted pattern preferences
@@ -188,10 +212,13 @@ const THEME_CONFIGS: Record<Theme, ThemeConfig> = {
fractal_noise: { weight: 3, params: { octaves: [0.5, 2], persistence: [0.3, 0.8] } }, fractal_noise: { weight: 3, params: { octaves: [0.5, 2], persistence: [0.3, 0.8] } },
noise: { weight: 2, params: { scaleX: [0.1, 0.5], scaleY: [0.1, 0.5], evolution: [0.5, 2] } }, noise: { weight: 2, params: { scaleX: [0.1, 0.5], scaleY: [0.1, 0.5], evolution: [0.5, 2] } },
sine_wave: { weight: 2, params: { freq: [0.1, 0.4], phase: [0.2, 1], axis: ['x', 'y'] } }, sine_wave: { weight: 2, params: { freq: [0.1, 0.4], phase: [0.2, 1], axis: ['x', 'y'] } },
am_modulation: { weight: 1, params: { carrierFreq: [0.2, 0.6], modFreq: [0.1, 0.4], depth: [0.3, 0.8] } } am_modulation: {
weight: 1,
params: { carrierFreq: [0.2, 0.6], modFreq: [0.1, 0.4], depth: [0.3, 0.8] },
},
}, },
combinators: ['*', '+', 'max', 'min'], combinators: ['*', '+', 'max', 'min'],
complexity: [2, 3] complexity: [2, 3],
}, },
geometric: { geometric: {
@@ -199,21 +226,27 @@ const THEME_CONFIGS: Record<Theme, ThemeConfig> = {
voronoi: { weight: 3, params: { seed1: [2, 14], seed2: [2, 14], scale: [0.3, 1] } }, voronoi: { weight: 3, params: { seed1: [2, 14], seed2: [2, 14], scale: [0.3, 1] } },
grid: { weight: 3, params: { sizeX: [2, 8], sizeY: [2, 8], phase: [0.5, 2] } }, grid: { weight: 3, params: { sizeX: [2, 8], sizeY: [2, 8], phase: [0.5, 2] } },
checkerboard: { weight: 2, params: { size: [1, 4], phase: [0.1, 1] } }, checkerboard: { weight: 2, params: { size: [1, 4], phase: [0.1, 1] } },
diamond: { weight: 1, params: { centerX: [7, 9], centerY: [7, 9], size: [0.5, 2], speed: [0.5, 2] } } diamond: {
weight: 1,
params: { centerX: [7, 9], centerY: [7, 9], size: [0.5, 2], speed: [0.5, 2] },
},
}, },
combinators: ['*', '+', 'floor', 'sign', 'min'], combinators: ['*', '+', 'floor', 'sign', 'min'],
complexity: [1, 2] complexity: [1, 2],
}, },
interference: { interference: {
patterns: { patterns: {
interference: { weight: 4, params: { freq1: [0.2, 0.8], freq2: [0.2, 0.8], phase1: [0.5, 2], phase2: [0.5, 2] } }, interference: {
weight: 4,
params: { freq1: [0.2, 0.8], freq2: [0.2, 0.8], phase1: [0.5, 2], phase2: [0.5, 2] },
},
sine_wave: { weight: 3, params: { freq: [0.3, 1], phase: [0.5, 3], axis: ['x', 'y', 'xy'] } }, sine_wave: { weight: 3, params: { freq: [0.3, 1], phase: [0.5, 3], axis: ['x', 'y', 'xy'] } },
cos_wave: { weight: 3, params: { freq: [0.3, 1], phase: [0.5, 3], axis: ['x', 'y', 'xy'] } }, cos_wave: { weight: 3, params: { freq: [0.3, 1], phase: [0.5, 3], axis: ['x', 'y', 'xy'] } },
grid: { weight: 2, params: { sizeX: [3, 8], sizeY: [3, 8], phase: [0.5, 2] } } grid: { weight: 2, params: { sizeX: [3, 8], sizeY: [3, 8], phase: [0.5, 2] } },
}, },
combinators: ['+', '-', '*', 'max'], combinators: ['+', '-', '*', 'max'],
complexity: [2, 3] complexity: [2, 3],
}, },
chaotic: { chaotic: {
@@ -222,10 +255,13 @@ const THEME_CONFIGS: Record<Theme, ThemeConfig> = {
mandelbrot_like: { weight: 2, params: { scale: [0.1, 0.5], iterations: [2, 5] } }, mandelbrot_like: { weight: 2, params: { scale: [0.1, 0.5], iterations: [2, 5] } },
fractal_noise: { weight: 2, params: { octaves: [1, 4], persistence: [0.2, 0.8] } }, fractal_noise: { weight: 2, params: { octaves: [1, 4], persistence: [0.2, 0.8] } },
xor_pattern: { weight: 2, params: { maskX: [7, 31], maskY: [7, 31], timeShift: [1, 4] } }, xor_pattern: { weight: 2, params: { maskX: [7, 31], maskY: [7, 31], timeShift: [1, 4] } },
interference: { weight: 1, params: { freq1: [0.5, 2], freq2: [0.5, 2], phase1: [2, 6], phase2: [2, 6] } } interference: {
weight: 1,
params: { freq1: [0.5, 2], freq2: [0.5, 2], phase1: [2, 6], phase2: [2, 6] },
},
}, },
combinators: ['*', '+', 'tan', 'pow', '%', '^'], combinators: ['*', '+', 'tan', 'pow', '%', '^'],
complexity: [2, 3] complexity: [2, 3],
}, },
minimalist: { minimalist: {
@@ -233,44 +269,62 @@ const THEME_CONFIGS: Record<Theme, ThemeConfig> = {
checkerboard: { weight: 4, params: { size: [3, 8], phase: [0.1, 0.5] } }, checkerboard: { weight: 4, params: { size: [3, 8], phase: [0.1, 0.5] } },
grid: { weight: 3, params: { sizeX: [4, 12], sizeY: [4, 12], phase: [0.2, 0.8] } }, grid: { weight: 3, params: { sizeX: [4, 12], sizeY: [4, 12], phase: [0.2, 0.8] } },
binary_maze: { weight: 2, params: { complexity: [3, 7], timeEvolution: [0.1, 0.5] } }, binary_maze: { weight: 2, params: { complexity: [3, 7], timeEvolution: [0.1, 0.5] } },
sine_wave: { weight: 1, params: { freq: [0.1, 0.3], phase: [0.2, 0.8], axis: ['x', 'y'] } } sine_wave: { weight: 1, params: { freq: [0.1, 0.3], phase: [0.2, 0.8], axis: ['x', 'y'] } },
}, },
combinators: ['sign', 'floor', 'abs', '&'], combinators: ['sign', 'floor', 'abs', '&'],
complexity: [1, 2] complexity: [1, 2],
}, },
psychedelic: { psychedelic: {
patterns: { patterns: {
musical_harmony: { weight: 3, params: { fundamental: [0.5, 2], overtone: [1.5, 4] } }, musical_harmony: { weight: 3, params: { fundamental: [0.5, 2], overtone: [1.5, 4] } },
interference: { weight: 2, params: { freq1: [0.8, 2.5], freq2: [0.8, 2.5], phase1: [2, 8], phase2: [2, 8] } }, interference: {
weight: 2,
params: { freq1: [0.8, 2.5], freq2: [0.8, 2.5], phase1: [2, 8], phase2: [2, 8] },
},
bit_rotation: { weight: 2, params: { frequency: [0.8, 2], timeSpeed: [2, 6] } }, bit_rotation: { weight: 2, params: { frequency: [0.8, 2], timeSpeed: [2, 6] } },
strange_attractor: { weight: 2, params: { a: [1, 3], b: [0.5, 2], c: [0.3, 1.5] } }, strange_attractor: { weight: 2, params: { a: [1, 3], b: [0.5, 2], c: [0.3, 1.5] } },
spiral: { weight: 1, params: { centerX: [4, 12], centerY: [4, 12], tightness: [3, 12], rotation: [3, 10] } } spiral: {
weight: 1,
params: { centerX: [4, 12], centerY: [4, 12], tightness: [3, 12], rotation: [3, 10] },
},
}, },
combinators: ['*', '+', 'tan', 'cos', 'sin', '^'], combinators: ['*', '+', 'tan', 'cos', 'sin', '^'],
complexity: [3, 4] complexity: [3, 4],
}, },
bitwise: { bitwise: {
patterns: { patterns: {
xor_pattern: { weight: 3, params: { maskX: [3, 31], maskY: [3, 31], timeShift: [0.1, 2] } }, xor_pattern: { weight: 3, params: { maskX: [3, 31], maskY: [3, 31], timeShift: [0.1, 2] } },
and_pattern: { weight: 2, params: { maskX: [7, 15], maskY: [7, 15], normalizer: [0.1, 0.5] } }, and_pattern: {
weight: 2,
params: { maskX: [7, 15], maskY: [7, 15], normalizer: [0.1, 0.5] },
},
or_pattern: { weight: 2, params: { maskX: [7, 15], maskY: [7, 15], normalizer: [0.1, 0.5] } }, or_pattern: { weight: 2, params: { maskX: [7, 15], maskY: [7, 15], normalizer: [0.1, 0.5] } },
bit_shift: { weight: 2, params: { direction: ['left', 'right'], amount: [1, 4], modulator: [0.5, 2] } }, bit_shift: {
weight: 2,
params: { direction: ['left', 'right'], amount: [1, 4], modulator: [0.5, 2] },
},
binary_cellular: { weight: 2, params: { rule: [7, 31], evolution: [0.2, 1.5] } }, binary_cellular: { weight: 2, params: { rule: [7, 31], evolution: [0.2, 1.5] } },
bit_rotation: { weight: 1, params: { frequency: [0.3, 1.5], timeSpeed: [0.5, 3] } }, bit_rotation: { weight: 1, params: { frequency: [0.3, 1.5], timeSpeed: [0.5, 3] } },
modular_arithmetic: { weight: 2, params: { modX: [3, 16], modY: [3, 16], operation: ['add', 'mult', 'xor'] } }, modular_arithmetic: {
weight: 2,
params: { modX: [3, 16], modY: [3, 16], operation: ['add', 'mult', 'xor'] },
},
sierpinski: { weight: 1, params: { scale: [0.3, 1.2], timeShift: [0.1, 0.8] } }, sierpinski: { weight: 1, params: { scale: [0.3, 1.2], timeShift: [0.1, 0.8] } },
bit_noise: { weight: 1, params: { density: [0.5, 2], evolution: [0.3, 2] } }, bit_noise: { weight: 1, params: { density: [0.5, 2], evolution: [0.3, 2] } },
binary_maze: { weight: 1, params: { complexity: [3, 15], timeEvolution: [0.1, 1] } } binary_maze: { weight: 1, params: { complexity: [3, 15], timeEvolution: [0.1, 1] } },
}, },
combinators: ['^', '&', '|', '+', '*', 'sign', 'floor'], combinators: ['^', '&', '|', '+', '*', 'sign', 'floor'],
complexity: [1, 3] complexity: [1, 3],
} },
} }
export class TixyFormulaGenerator { export class TixyFormulaGenerator {
generateFormula(theme?: Theme, varietyConfig?: VarietyConfig): { expression: string, theme: Theme, description: string } { generateFormula(
theme?: Theme,
varietyConfig?: VarietyConfig
): { expression: string; theme: Theme; description: string } {
// Enhanced variety system - randomly decide enhancement features // Enhanced variety system - randomly decide enhancement features
const useCoordinateTransform = !varietyConfig || random() < 0.4 const useCoordinateTransform = !varietyConfig || random() < 0.4
const useThemeHybrid = !varietyConfig || random() < 0.3 const useThemeHybrid = !varietyConfig || random() < 0.3
@@ -304,15 +358,20 @@ export class TixyFormulaGenerator {
// Coordinate transformation selection // Coordinate transformation selection
let coordinateSpace: CoordinateSpace = 'cartesian' let coordinateSpace: CoordinateSpace = 'cartesian'
if (useCoordinateTransform) { if (useCoordinateTransform) {
coordinateSpace = choice(['cartesian', 'polar', 'log_polar', 'hyperbolic', 'wave_distort', 'spiral']) coordinateSpace = choice([
'cartesian',
'polar',
'log_polar',
'hyperbolic',
'wave_distort',
'spiral',
])
} }
// Generate base patterns with enhanced variety // Generate base patterns with enhanced variety
for (let i = 0; i < numPatterns; i++) { for (let i = 0; i < numPatterns; i++) {
const patternType = weightedChoice( const patternType = weightedChoice(
Object.fromEntries( Object.fromEntries(Object.entries(config.patterns).map(([key, cfg]) => [key, cfg.weight]))
Object.entries(config.patterns).map(([key, cfg]) => [key, cfg.weight])
)
) )
const patternConfig = config.patterns[patternType] const patternConfig = config.patterns[patternType]
@@ -334,9 +393,10 @@ export class TixyFormulaGenerator {
// Combine patterns with advanced techniques // Combine patterns with advanced techniques
const expression = this.combinePatterns(patterns, config.combinators) const expression = this.combinePatterns(patterns, config.combinators)
const themeDesc = hybridThemes.length > 0 const themeDesc =
? `${selectedTheme}+${hybridThemes.slice(1).join('+')}` hybridThemes.length > 0
: selectedTheme ? `${selectedTheme}+${hybridThemes.slice(1).join('+')}`
: selectedTheme
const transformDesc = coordinateSpace !== 'cartesian' ? `[${coordinateSpace}]` : '' const transformDesc = coordinateSpace !== 'cartesian' ? `[${coordinateSpace}]` : ''
const couplingDesc = useParameterCoupling ? '[coupled]' : '' const couplingDesc = useParameterCoupling ? '[coupled]' : ''
@@ -344,11 +404,13 @@ export class TixyFormulaGenerator {
return { return {
expression, expression,
theme: selectedTheme, theme: selectedTheme,
description: `${themeDesc}${transformDesc}${couplingDesc}: ${descriptions.join(' + ')}` description: `${themeDesc}${transformDesc}${couplingDesc}: ${descriptions.join(' + ')}`,
} }
} }
private generateParams(paramConfig: Record<string, [number, number] | string[]>): Record<string, any> { private generateParams(
paramConfig: Record<string, [number, number] | string[]>
): Record<string, any> {
const params: Record<string, any> = {} const params: Record<string, any> = {}
for (const [key, range] of Object.entries(paramConfig)) { for (const [key, range] of Object.entries(paramConfig)) {
@@ -369,9 +431,19 @@ export class TixyFormulaGenerator {
case 'cos_wave': case 'cos_wave':
return PATTERN_GENERATORS.cos_wave(params.freq, params.phase, params.axis) return PATTERN_GENERATORS.cos_wave(params.freq, params.phase, params.axis)
case 'ripple': case 'ripple':
return PATTERN_GENERATORS.ripple(params.centerX, params.centerY, params.frequency, params.speed) return PATTERN_GENERATORS.ripple(
params.centerX,
params.centerY,
params.frequency,
params.speed
)
case 'spiral': case 'spiral':
return PATTERN_GENERATORS.spiral(params.centerX, params.centerY, params.tightness, params.rotation) return PATTERN_GENERATORS.spiral(
params.centerX,
params.centerY,
params.tightness,
params.rotation
)
case 'grid': case 'grid':
return PATTERN_GENERATORS.grid(params.sizeX, params.sizeY, params.phase) return PATTERN_GENERATORS.grid(params.sizeX, params.sizeY, params.phase)
case 'noise': case 'noise':
@@ -381,7 +453,12 @@ export class TixyFormulaGenerator {
case 'diamond': case 'diamond':
return PATTERN_GENERATORS.diamond(params.centerX, params.centerY, params.size, params.speed) return PATTERN_GENERATORS.diamond(params.centerX, params.centerY, params.size, params.speed)
case 'interference': case 'interference':
return PATTERN_GENERATORS.interference(params.freq1, params.freq2, params.phase1, params.phase2) return PATTERN_GENERATORS.interference(
params.freq1,
params.freq2,
params.phase1,
params.phase2
)
case 'mandelbrot_like': case 'mandelbrot_like':
return PATTERN_GENERATORS.mandelbrot_like(params.scale, params.iterations) return PATTERN_GENERATORS.mandelbrot_like(params.scale, params.iterations)
case 'am_modulation': case 'am_modulation':
@@ -431,7 +508,7 @@ export class TixyFormulaGenerator {
const hybridConfig: ThemeConfig = { const hybridConfig: ThemeConfig = {
patterns: { ...primaryConfig.patterns }, patterns: { ...primaryConfig.patterns },
combinators: [...primaryConfig.combinators], combinators: [...primaryConfig.combinators],
complexity: primaryConfig.complexity complexity: primaryConfig.complexity,
} }
// Merge patterns from hybrid themes with reduced weights // Merge patterns from hybrid themes with reduced weights
@@ -441,7 +518,7 @@ export class TixyFormulaGenerator {
if (!hybridConfig.patterns[patternName]) { if (!hybridConfig.patterns[patternName]) {
hybridConfig.patterns[patternName] = { hybridConfig.patterns[patternName] = {
weight: Math.round(config.weight * 0.5), // Reduced weight for hybrid patterns weight: Math.round(config.weight * 0.5), // Reduced weight for hybrid patterns
params: config.params params: config.params,
} }
} }
}) })
@@ -457,11 +534,14 @@ export class TixyFormulaGenerator {
return hybridConfig return hybridConfig
} }
private generateCoupledParams(paramConfig: Record<string, [number, number] | string[]>, index: number): Record<string, any> { private generateCoupledParams(
paramConfig: Record<string, [number, number] | string[]>,
index: number
): Record<string, any> {
const params: Record<string, any> = {} const params: Record<string, any> = {}
// Use mathematical constants and relationships for coupling // Use mathematical constants and relationships for coupling
const couplingFactors = [1, PHI, PI/4, E/2, Math.sqrt(2), Math.sqrt(3)] const couplingFactors = [1, PHI, PI / 4, E / 2, Math.sqrt(2), Math.sqrt(3)]
const baseFactor = couplingFactors[index % couplingFactors.length] const baseFactor = couplingFactors[index % couplingFactors.length]
for (const [key, range] of Object.entries(paramConfig)) { for (const [key, range] of Object.entries(paramConfig)) {
@@ -508,9 +588,7 @@ export class TixyFormulaGenerator {
} }
// Replace x and y in the pattern with transformed coordinates // Replace x and y in the pattern with transformed coordinates
return pattern return pattern.replace(/\bx\b/g, `(${transformedX})`).replace(/\by\b/g, `(${transformedY})`)
.replace(/\bx\b/g, `(${transformedX})`)
.replace(/\by\b/g, `(${transformedY})`)
} }
private combinePatterns(patterns: string[], combinators: string[]): string { private combinePatterns(patterns: string[], combinators: string[]): string {

View File

@@ -1,3 +1,14 @@
export { compileTixyExpression, evaluateTixyExpression, EXAMPLE_EXPRESSIONS, generateTixyExpression } from './core/evaluator' export {
compileTixyExpression,
evaluateTixyExpression,
EXAMPLE_EXPRESSIONS,
generateTixyExpression,
} from './core/evaluator'
export { renderTixyToCanvas, renderTixyToImageData } from './renderer/canvas' export { renderTixyToCanvas, renderTixyToImageData } from './renderer/canvas'
export type { TixyParams, TixyFunction, TixyExpression, TixyRenderOptions, TixyResult } from './core/types' export type {
TixyParams,
TixyFunction,
TixyExpression,
TixyRenderOptions,
TixyResult,
} from './core/types'

View File

@@ -12,7 +12,7 @@ export function renderTixyToCanvas(
backgroundColor = '#000000', backgroundColor = '#000000',
foregroundColor = '#ffffff', foregroundColor = '#ffffff',
threshold = 0.3, threshold = 0.3,
pixelSize = 1 pixelSize = 1,
} = options } = options
const canvas = document.createElement('canvas') const canvas = document.createElement('canvas')
@@ -31,13 +31,14 @@ export function renderTixyToCanvas(
const i = y * width + x const i = y * width + x
const value = evaluateTixyExpression(expression, time, i, x, y) const value = evaluateTixyExpression(expression, time, i, x, y)
const color = Math.abs(value) > threshold const color =
? { Math.abs(value) > threshold
r: Math.round(fgColor.r * (Math.sign(value) > 0 ? 1 : 0.8)), ? {
g: Math.round(fgColor.g * (Math.sign(value) > 0 ? 1 : 0.8)), r: Math.round(fgColor.r * (Math.sign(value) > 0 ? 1 : 0.8)),
b: Math.round(fgColor.b * (Math.sign(value) > 0 ? 1 : 0.8)) g: Math.round(fgColor.g * (Math.sign(value) > 0 ? 1 : 0.8)),
} b: Math.round(fgColor.b * (Math.sign(value) > 0 ? 1 : 0.8)),
: bgColor }
: bgColor
for (let py = 0; py < pixelSize; py++) { for (let py = 0; py < pixelSize; py++) {
for (let px = 0; px < pixelSize; px++) { for (let px = 0; px < pixelSize; px++) {
@@ -58,7 +59,7 @@ export function renderTixyToCanvas(
return { return {
imageData, imageData,
canvas canvas,
} }
} }
@@ -71,9 +72,11 @@ export function renderTixyToImageData(
function hexToRgb(hex: string): { r: number; g: number; b: number } { function hexToRgb(hex: string): { r: number; g: number; b: number } {
const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex) const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex)
return result ? { return result
r: parseInt(result[1], 16), ? {
g: parseInt(result[2], 16), r: parseInt(result[1], 16),
b: parseInt(result[3], 16) g: parseInt(result[2], 16),
} : { r: 0, g: 0, b: 0 } b: parseInt(result[3], 16),
}
: { r: 0, g: 0, b: 0 }
} }

View File

@@ -9,7 +9,7 @@ const colorPalettes = [
{ bg: '#1a1a1a', fg: '#ffffff' }, { bg: '#1a1a1a', fg: '#ffffff' },
{ bg: '#333333', fg: '#ffffff' }, { bg: '#333333', fg: '#ffffff' },
{ bg: '#000000', fg: '#666666' }, { bg: '#000000', fg: '#666666' },
{ bg: '#222222', fg: '#dddddd' } { bg: '#222222', fg: '#dddddd' },
] ]
export function generateTixyImages(count: number, size: number): GeneratedImage[] { export function generateTixyImages(count: number, size: number): GeneratedImage[] {
@@ -38,7 +38,7 @@ export function generateTixyImages(count: number, size: number): GeneratedImage[
time, time,
backgroundColor: palette.bg, backgroundColor: palette.bg,
foregroundColor: palette.fg, foregroundColor: palette.fg,
pixelSize: 4 pixelSize: 4,
}) })
const image = { const image = {
@@ -46,7 +46,7 @@ export function generateTixyImages(count: number, size: number): GeneratedImage[
canvas: result.canvas, canvas: result.canvas,
imageData: result.imageData, imageData: result.imageData,
generator: 'tixy' as const, generator: 'tixy' as const,
params: { expression, time, colors: palette } params: { expression, time, colors: palette },
} }
images.push(image) images.push(image)

View File

@@ -24,7 +24,7 @@ const customWaveform = generateWaveform({
splits: 32, splits: 32,
interpolation: 'cubic', interpolation: 'cubic',
randomness: 'smooth', randomness: 'smooth',
lineWidth: 3 lineWidth: 3,
}) })
``` ```
@@ -33,27 +33,30 @@ const customWaveform = generateWaveform({
### Main Functions ### Main Functions
#### `generateWaveform(config?)` #### `generateWaveform(config?)`
Generate a waveform with optional configuration override. Generate a waveform with optional configuration override.
#### `generateRandomWaveform()` #### `generateRandomWaveform()`
Generate a waveform with randomized parameters for maximum variety. Generate a waveform with randomized parameters for maximum variety.
#### `generateWaveformBatch(count)` #### `generateWaveformBatch(count)`
Generate multiple random waveforms efficiently. Generate multiple random waveforms efficiently.
### Configuration ### Configuration
```typescript ```typescript
interface WaveformConfig { interface WaveformConfig {
width: number // Canvas width (default: 256) width: number // Canvas width (default: 256)
height: number // Canvas height (default: 256) height: number // Canvas height (default: 256)
splits: number // Number of control points (8-64) splits: number // Number of control points (8-64)
interpolation: InterpolationType // Curve type interpolation: InterpolationType // Curve type
randomness: RandomnessStrategy // Distribution strategy randomness: RandomnessStrategy // Distribution strategy
lineWidth: number // Stroke width (1-4) lineWidth: number // Stroke width (1-4)
backgroundColor: string // Background color backgroundColor: string // Background color
lineColor: string // Line color lineColor: string // Line color
smoothness: number // Curve smoothness (0-1) smoothness: number // Curve smoothness (0-1)
} }
``` ```
@@ -95,30 +98,33 @@ waveform-generator/
## Examples ## Examples
### Basic Waveform ### Basic Waveform
```typescript ```typescript
const simple = generateWaveform({ const simple = generateWaveform({
splits: 16, splits: 16,
interpolation: 'linear' interpolation: 'linear',
}) })
``` ```
### Smooth Organic Curves ### Smooth Organic Curves
```typescript ```typescript
const organic = generateWaveform({ const organic = generateWaveform({
splits: 24, splits: 24,
interpolation: 'cubic', interpolation: 'cubic',
randomness: 'smooth', randomness: 'smooth',
smoothness: 0.7 smoothness: 0.7,
}) })
``` ```
### Sharp Electronic Waveform ### Sharp Electronic Waveform
```typescript ```typescript
const electronic = generateWaveform({ const electronic = generateWaveform({
splits: 32, splits: 32,
interpolation: 'exponential', interpolation: 'exponential',
randomness: 'uniform', randomness: 'uniform',
lineWidth: 1 lineWidth: 1,
}) })
``` ```

View File

@@ -1,4 +1,4 @@
import type { ControlPoint, WaveformConfig, RandomnessStrategy } from './types' import type { ControlPoint, WaveformConfig, RandomnessStrategy, InterpolationType } from './types'
import { smoothControlPoints, applyTension } from './interpolation' import { smoothControlPoints, applyTension } from './interpolation'
// Generate random control points based on configuration // Generate random control points based on configuration
@@ -81,7 +81,8 @@ function generateRandomY(
// Generate gaussian-distributed random numbers using Box-Muller transform // Generate gaussian-distributed random numbers using Box-Muller transform
function gaussianRandom(mean: number = 0, stdDev: number = 1): number { function gaussianRandom(mean: number = 0, stdDev: number = 1): number {
let u = 0, v = 0 let u = 0,
v = 0
while (u === 0) u = Math.random() // Converting [0,1) to (0,1) while (u === 0) u = Math.random() // Converting [0,1) to (0,1)
while (v === 0) v = Math.random() while (v === 0) v = Math.random()
@@ -106,7 +107,7 @@ export function generateWaveformVariation(baseConfig: WaveformConfig): WaveformC
// Pure interpolation types (60% of patterns) // Pure interpolation types (60% of patterns)
{ type: 'pure', weight: 0.6 }, { type: 'pure', weight: 0.6 },
// Blended/mixed interpolation (40% of patterns) // Blended/mixed interpolation (40% of patterns)
{ type: 'blended', weight: 0.4 } { type: 'blended', weight: 0.4 },
] ]
const strategyType = weightedRandomChoice(strategies) const strategyType = weightedRandomChoice(strategies)
@@ -124,7 +125,7 @@ function generatePureInterpolationVariation(baseConfig: WaveformConfig): Wavefor
{ interpolation: 'linear' as const, weight: 0.3 }, { interpolation: 'linear' as const, weight: 0.3 },
{ interpolation: 'exponential' as const, weight: 0.25 }, { interpolation: 'exponential' as const, weight: 0.25 },
{ interpolation: 'logarithmic' as const, weight: 0.25 }, { interpolation: 'logarithmic' as const, weight: 0.25 },
{ interpolation: 'cubic' as const, weight: 0.2 } { interpolation: 'cubic' as const, weight: 0.2 },
] ]
const interpolationType = weightedRandomChoice(pureTypes) const interpolationType = weightedRandomChoice(pureTypes)
@@ -132,10 +133,10 @@ function generatePureInterpolationVariation(baseConfig: WaveformConfig): Wavefor
return { return {
...baseConfig, ...baseConfig,
splits: randomChoice([8, 12, 16, 20, 24, 32]), splits: randomChoice([8, 12, 16, 20, 24, 32]),
interpolation: interpolationType, interpolation: interpolationType as InterpolationType,
randomness: randomChoice(['uniform', 'gaussian', 'smooth'] as const), randomness: randomChoice(['uniform', 'gaussian', 'smooth'] as const),
smoothness: randomChoice([0.2, 0.4, 0.6, 0.8]), smoothness: randomChoice([0.2, 0.4, 0.6, 0.8]),
lineWidth: 2 lineWidth: 2,
} }
} }
@@ -150,29 +151,29 @@ function generateBlendedInterpolationVariation(baseConfig: WaveformConfig): Wave
interpolation: 'cubic' as const, interpolation: 'cubic' as const,
randomness: 'gaussian' as const, randomness: 'gaussian' as const,
smoothness: 0.8, smoothness: 0.8,
splits: 16 splits: 16,
}, },
// Low smoothness + uniform = sharp chaotic lines // Low smoothness + uniform = sharp chaotic lines
{ {
interpolation: 'linear' as const, interpolation: 'linear' as const,
randomness: 'uniform' as const, randomness: 'uniform' as const,
smoothness: 0.1, smoothness: 0.1,
splits: 32 splits: 32,
}, },
// Exponential + smooth = flowing acceleration curves // Exponential + smooth = flowing acceleration curves
{ {
interpolation: 'exponential' as const, interpolation: 'exponential' as const,
randomness: 'smooth' as const, randomness: 'smooth' as const,
smoothness: 0.6, smoothness: 0.6,
splits: 20 splits: 20,
}, },
// Logarithmic + gaussian = natural decay curves // Logarithmic + gaussian = natural decay curves
{ {
interpolation: 'logarithmic' as const, interpolation: 'logarithmic' as const,
randomness: 'gaussian' as const, randomness: 'gaussian' as const,
smoothness: 0.5, smoothness: 0.5,
splits: 24 splits: 24,
} },
] ]
const config = randomChoice(blendedConfigs) const config = randomChoice(blendedConfigs)
@@ -180,7 +181,7 @@ function generateBlendedInterpolationVariation(baseConfig: WaveformConfig): Wave
return { return {
...baseConfig, ...baseConfig,
...config, ...config,
lineWidth: 2 lineWidth: 2,
} }
} }
@@ -190,7 +191,7 @@ function randomChoice<T>(array: readonly T[]): T {
} }
// Weighted random selection utility // Weighted random selection utility
function weightedRandomChoice<T extends { weight: number }>(choices: T[]): T[keyof T] { function weightedRandomChoice<T extends { weight: number; type?: any; interpolation?: any }>(choices: T[]): any {
const totalWeight = choices.reduce((sum, choice) => sum + choice.weight, 0) const totalWeight = choices.reduce((sum, choice) => sum + choice.weight, 0)
let random = Math.random() * totalWeight let random = Math.random() * totalWeight
@@ -209,5 +210,7 @@ export function generateMultipleWaveforms(
count: number, count: number,
baseConfig: WaveformConfig baseConfig: WaveformConfig
): WaveformConfig[] { ): WaveformConfig[] {
return Array(count).fill(null).map(() => generateWaveformVariation(baseConfig)) return Array(count)
.fill(null)
.map(() => generateWaveformVariation(baseConfig))
} }

View File

@@ -8,7 +8,6 @@ export function interpolate(
type: InterpolationType, type: InterpolationType,
smoothness: number = 0.5 smoothness: number = 0.5
): number { ): number {
const dx = p2.x - p1.x
const dy = p2.y - p1.y const dy = p2.y - p1.y
switch (type) { switch (type) {
@@ -93,10 +92,7 @@ export function smoothControlPoints(
} }
// Calculate curve tension for natural-looking waveforms // Calculate curve tension for natural-looking waveforms
export function applyTension( export function applyTension(points: ControlPoint[], tension: number = 0.5): ControlPoint[] {
points: ControlPoint[],
tension: number = 0.5
): ControlPoint[] {
if (points.length <= 2) return points if (points.length <= 2) return points
const tensioned = [...points] const tensioned = [...points]

View File

@@ -37,7 +37,7 @@ export const DEFAULT_WAVEFORM_CONFIG: WaveformConfig = {
lineWidth: 0.5, // Very thin line for precise frequency definition lineWidth: 0.5, // Very thin line for precise frequency definition
backgroundColor: '#000000', backgroundColor: '#000000',
lineColor: '#ffffff', lineColor: '#ffffff',
smoothness: 0.5 smoothness: 0.5,
} }
// Common split counts for variety // Common split counts for variety
@@ -48,5 +48,5 @@ export const INTERPOLATION_WEIGHTS = {
linear: 0.3, linear: 0.3,
exponential: 0.25, exponential: 0.25,
logarithmic: 0.25, logarithmic: 0.25,
cubic: 0.2 cubic: 0.2,
} as const } as const

View File

@@ -6,35 +6,31 @@ export type {
RandomnessStrategy, RandomnessStrategy,
ControlPoint, ControlPoint,
WaveformConfig, WaveformConfig,
WaveformResult WaveformResult,
} from './core/types' } from './core/types'
export { export { DEFAULT_WAVEFORM_CONFIG, SPLIT_COUNTS, INTERPOLATION_WEIGHTS } from './core/types'
DEFAULT_WAVEFORM_CONFIG,
SPLIT_COUNTS,
INTERPOLATION_WEIGHTS
} from './core/types'
// Interpolation functions // Interpolation functions
export { export {
interpolate, interpolate,
generateCurvePoints, generateCurvePoints,
smoothControlPoints, smoothControlPoints,
applyTension applyTension,
} from './core/interpolation' } from './core/interpolation'
// Generation logic // Generation logic
export { export {
generateControlPoints, generateControlPoints,
generateWaveformVariation, generateWaveformVariation,
generateMultipleWaveforms generateMultipleWaveforms,
} from './core/generator' } from './core/generator'
// Canvas rendering // Canvas rendering
export { export {
renderWaveformToCanvas, renderWaveformToCanvas,
renderWaveformWithBezier, renderWaveformWithBezier,
renderMultipleWaveforms renderMultipleWaveforms,
} from './renderer/canvas' } from './renderer/canvas'
// Main convenience function for generating complete waveforms // Main convenience function for generating complete waveforms
@@ -56,5 +52,7 @@ export function generateRandomWaveform(): WaveformResult {
} }
export function generateWaveformBatch(count: number): WaveformResult[] { export function generateWaveformBatch(count: number): WaveformResult[] {
return Array(count).fill(null).map(() => generateRandomWaveform()) return Array(count)
.fill(null)
.map(() => generateRandomWaveform())
} }

View File

@@ -44,7 +44,7 @@ export function renderWaveformToCanvas(
canvas, canvas,
imageData, imageData,
controlPoints, controlPoints,
config config,
} }
} }
@@ -114,7 +114,7 @@ export function renderWaveformWithBezier(
canvas, canvas,
imageData, imageData,
controlPoints, controlPoints,
config config,
} }
} }
@@ -178,12 +178,7 @@ export function renderMultipleWaveforms(
ctx.lineCap = 'round' ctx.lineCap = 'round'
ctx.lineJoin = 'round' ctx.lineJoin = 'round'
const curvePoints = generateCurvePoints( const curvePoints = generateCurvePoints(points, config.interpolation, config.smoothness, 6)
points,
config.interpolation,
config.smoothness,
6
)
drawSmoothCurve(ctx, curvePoints) drawSmoothCurve(ctx, curvePoints)
}) })
@@ -194,6 +189,6 @@ export function renderMultipleWaveforms(
canvas, canvas,
imageData, imageData,
controlPoints: waveformData[0]?.points || [], controlPoints: waveformData[0]?.points || [],
config: firstConfig || waveformData[0]?.config config: firstConfig || waveformData[0]?.config,
} }
} }

View File

@@ -1,4 +1,6 @@
import { generateRandomWaveform, generateWaveformVariation, DEFAULT_WAVEFORM_CONFIG } from './waveform-generator' import {
generateRandomWaveform,
} from './waveform-generator'
import type { GeneratedImage } from '../stores' import type { GeneratedImage } from '../stores'
// Generate multiple random waveform images // Generate multiple random waveform images
@@ -40,8 +42,8 @@ export function generateWaveformImages(count: number, size: number = 256): Gener
randomness: waveform.config.randomness, randomness: waveform.config.randomness,
smoothness: waveform.config.smoothness, smoothness: waveform.config.smoothness,
lineWidth: waveform.config.lineWidth, lineWidth: waveform.config.lineWidth,
size size,
} },
} }
images.push(image) images.push(image)
@@ -91,8 +93,8 @@ export function generateVariedWaveforms(count: number, size: number = 256): Gene
smoothness: waveform.config.smoothness, smoothness: waveform.config.smoothness,
lineWidth: waveform.config.lineWidth, lineWidth: waveform.config.lineWidth,
style: 'varied', style: 'varied',
size size,
} },
} }
images.push(image) images.push(image)

View File

@@ -10,8 +10,8 @@ export class WebcamProcessor {
video: { video: {
width: { ideal: 1280 }, width: { ideal: 1280 },
height: { ideal: 720 }, height: { ideal: 720 },
facingMode: 'user' facingMode: 'user',
} },
}) })
this.video = document.createElement('video') this.video = document.createElement('video')
@@ -66,7 +66,7 @@ export class WebcamProcessor {
canvas, canvas,
imageData, imageData,
capturedAt: new Date(), capturedAt: new Date(),
config config,
} }
} }
@@ -89,7 +89,7 @@ export class WebcamProcessor {
videoWidth: number, videoWidth: number,
videoHeight: number, videoHeight: number,
targetSize: number targetSize: number
): { drawWidth: number, drawHeight: number, offsetX: number, offsetY: number } { ): { drawWidth: number; drawHeight: number; offsetX: number; offsetY: number } {
const aspectRatio = videoWidth / videoHeight const aspectRatio = videoWidth / videoHeight
let drawWidth: number let drawWidth: number
@@ -114,7 +114,11 @@ export class WebcamProcessor {
return { drawWidth, drawHeight, offsetX, offsetY } return { drawWidth, drawHeight, offsetX, offsetY }
} }
private convertToGrayscale(ctx: CanvasRenderingContext2D, size: number, enhanceContrast: boolean) { private convertToGrayscale(
ctx: CanvasRenderingContext2D,
size: number,
enhanceContrast: boolean
) {
const imageData = ctx.getImageData(0, 0, size, size) const imageData = ctx.getImageData(0, 0, size, size)
const data = imageData.data const data = imageData.data
@@ -133,7 +137,7 @@ export class WebcamProcessor {
} }
// Set R, G, B to the same grayscale value // Set R, G, B to the same grayscale value
data[i] = final // Red data[i] = final // Red
data[i + 1] = final // Green data[i + 1] = final // Green
data[i + 2] = final // Blue data[i + 2] = final // Blue
// Alpha (data[i + 3]) remains unchanged // Alpha (data[i + 3]) remains unchanged

View File

@@ -9,7 +9,7 @@ export async function generateFromWebcamImage(
const result = await processor.capturePhoto({ const result = await processor.capturePhoto({
targetSize: size, targetSize: size,
contrastEnhancement: true, contrastEnhancement: true,
grayscaleConversion: true grayscaleConversion: true,
}) })
const image: GeneratedImage = { const image: GeneratedImage = {
@@ -21,8 +21,8 @@ export async function generateFromWebcamImage(
capturedAt: result.capturedAt.toISOString(), capturedAt: result.capturedAt.toISOString(),
size, size,
contrastEnhanced: true, contrastEnhanced: true,
grayscaleConverted: true grayscaleConverted: true,
} },
} }
return image return image
@@ -45,8 +45,8 @@ export async function generateFromWebcamImage(
generator: 'webcam', generator: 'webcam',
params: { params: {
error: error instanceof Error ? error.message : 'Unknown error', error: error instanceof Error ? error.message : 'Unknown error',
size size,
} },
} }
return fallbackImage return fallbackImage

View File

@@ -1 +1 @@
@import "tailwindcss"; @import 'tailwindcss';

View File

@@ -6,5 +6,5 @@ import App from './App.tsx'
createRoot(document.getElementById('root')!).render( createRoot(document.getElementById('root')!).render(
<StrictMode> <StrictMode>
<App /> <App />
</StrictMode>, </StrictMode>
) )

View File

@@ -23,7 +23,7 @@ const audioData = synthesizeFromImage(imageData, {
duration: 10, duration: 10,
minFreq: 100, minFreq: 100,
maxFreq: 10000, maxFreq: 10000,
maxPartials: 200 maxPartials: 200,
}) })
// Export as WAV // Export as WAV
@@ -35,6 +35,7 @@ downloadWAV(audioData, 44100, 'my-audio.wav')
### Main Functions ### Main Functions
#### `synthesizeFromImage(imageData, params?)` #### `synthesizeFromImage(imageData, params?)`
- **imageData**: `ImageData` - Canvas image data - **imageData**: `ImageData` - Canvas image data
- **params**: `Partial<SynthesisParams>` - Optional parameters - **params**: `Partial<SynthesisParams>` - Optional parameters
- **Returns**: `Float32Array` - Audio samples - **Returns**: `Float32Array` - Audio samples
@@ -42,16 +43,17 @@ downloadWAV(audioData, 44100, 'my-audio.wav')
### Types ### Types
#### `SynthesisParams` #### `SynthesisParams`
```typescript ```typescript
interface SynthesisParams { interface SynthesisParams {
duration: number // Audio duration in seconds duration: number // Audio duration in seconds
minFreq: number // Minimum frequency in Hz minFreq: number // Minimum frequency in Hz
maxFreq: number // Maximum frequency in Hz maxFreq: number // Maximum frequency in Hz
sampleRate: number // Sample rate in Hz sampleRate: number // Sample rate in Hz
frequencyResolution: number // Frequency bin downsampling frequencyResolution: number // Frequency bin downsampling
timeResolution: number // Time slice downsampling timeResolution: number // Time slice downsampling
amplitudeThreshold: number // Minimum amplitude threshold amplitudeThreshold: number // Minimum amplitude threshold
maxPartials: number // Maximum simultaneous partials maxPartials: number // Maximum simultaneous partials
} }
``` ```
@@ -80,6 +82,7 @@ spectral-synthesis/
## Usage Examples ## Usage Examples
### Basic Synthesis ### Basic Synthesis
```typescript ```typescript
const canvas = document.createElement('canvas') const canvas = document.createElement('canvas')
const ctx = canvas.getContext('2d') const ctx = canvas.getContext('2d')
@@ -89,12 +92,13 @@ const audio = synthesizeFromImage(imageData)
``` ```
### Advanced Usage ### Advanced Usage
```typescript ```typescript
import { ImageToAudioSynthesizer } from './spectral-synthesis' import { ImageToAudioSynthesizer } from './spectral-synthesis'
const synthesizer = new ImageToAudioSynthesizer({ const synthesizer = new ImageToAudioSynthesizer({
duration: 5, duration: 5,
maxPartials: 150 maxPartials: 150,
}) })
const result = synthesizer.synthesize(imageData) const result = synthesizer.synthesize(imageData)

View File

@@ -25,7 +25,7 @@ export function createWAVBuffer(audioData: Float32Array, sampleRate: number): Ar
let offset = 44 let offset = 44
for (let i = 0; i < length; i++) { for (let i = 0; i < length; i++) {
const sample = Math.max(-1, Math.min(1, audioData[i])) const sample = Math.max(-1, Math.min(1, audioData[i]))
view.setInt16(offset, sample * 0x7FFF, true) view.setInt16(offset, sample * 0x7fff, true)
offset += 2 offset += 2
} }
@@ -83,7 +83,9 @@ export function createAudioPlayer(audioData: Float32Array, sampleRate: number):
gainNode.connect(audioContext.destination) gainNode.connect(audioContext.destination)
if (audioContext.sampleRate !== sampleRate) { if (audioContext.sampleRate !== sampleRate) {
console.warn(`Audio context sample rate (${audioContext.sampleRate}) differs from data sample rate (${sampleRate})`) console.warn(
`Audio context sample rate (${audioContext.sampleRate}) differs from data sample rate (${sampleRate})`
)
} }
} }
} }
@@ -102,7 +104,8 @@ export function createAudioPlayer(audioData: Float32Array, sampleRate: number):
if (isPaused) { if (isPaused) {
// Resume from pause is not supported with AudioBufferSource // Resume from pause is not supported with AudioBufferSource
// We need to restart from the beginning // We need to restart from the beginning (pausedAt was ${pausedAt}s)
void pausedAt // Track for future resume feature
isPaused = false isPaused = false
pausedAt = 0 pausedAt = 0
} }
@@ -163,7 +166,7 @@ export function createAudioPlayer(audioData: Float32Array, sampleRate: number):
onStateChange(callback: (isPlaying: boolean) => void) { onStateChange(callback: (isPlaying: boolean) => void) {
stateCallback = callback stateCallback = callback
} },
} }
} }
@@ -174,7 +177,7 @@ export async function playAudio(audioData: Float32Array, sampleRate: number): Pr
const player = createAudioPlayer(audioData, sampleRate) const player = createAudioPlayer(audioData, sampleRate)
return new Promise(resolve => { return new Promise(resolve => {
player.onStateChange((isPlaying) => { player.onStateChange(isPlaying => {
if (!isPlaying) { if (!isPlaying) {
resolve() resolve()
} }

View File

@@ -9,7 +9,7 @@ import {
generateSpectralDensity, generateSpectralDensity,
mapFrequency, mapFrequency,
mapFrequencyLinear, mapFrequencyLinear,
normalizeAudioGlobal normalizeAudioGlobal,
} from './utils' } from './utils'
/** /**
@@ -78,7 +78,7 @@ export class ImageToAudioSynthesizer {
disableNormalization: false, disableNormalization: false,
disableContrast: false, disableContrast: false,
exactBinMapping: false, exactBinMapping: false,
...params ...params,
} }
} }
@@ -93,7 +93,6 @@ export class ImageToAudioSynthesizer {
} }
} }
/** /**
* Custom synthesis mode - sophisticated audio processing * Custom synthesis mode - sophisticated audio processing
*/ */
@@ -112,7 +111,7 @@ export class ImageToAudioSynthesizer {
spectralDensity, spectralDensity,
usePerceptualWeighting, usePerceptualWeighting,
frequencyMapping, frequencyMapping,
invert = false invert = false,
} = this.params } = this.params
// Calculate synthesis parameters // Calculate synthesis parameters
@@ -136,7 +135,6 @@ export class ImageToAudioSynthesizer {
const previousAmplitudes = new Float32Array(effectiveHeight) const previousAmplitudes = new Float32Array(effectiveHeight)
const smoothingFactor = 0.2 // Reduced for sharper transients const smoothingFactor = 0.2 // Reduced for sharper transients
// Process each time slice // Process each time slice
for (let col = 0; col < effectiveWidth; col++) { for (let col = 0; col < effectiveWidth; col++) {
const sourceCol = col const sourceCol = col
@@ -160,9 +158,12 @@ export class ImageToAudioSynthesizer {
return normalizedDb return normalizedDb
}) })
// Detect spectral peaks // Detect spectral peaks
const peaks = detectSpectralPeaks(processedSpectrum, Math.min(amplitudeThreshold, 0.01), false) const peaks = detectSpectralPeaks(
processedSpectrum,
Math.min(amplitudeThreshold, 0.01),
false
)
// Generate partials from peaks with spectral density // Generate partials from peaks with spectral density
const partials: SpectralPeak[] = [] const partials: SpectralPeak[] = []
@@ -176,14 +177,21 @@ export class ImageToAudioSynthesizer {
} else if (frequencyMapping === 'linear') { } else if (frequencyMapping === 'linear') {
frequency = mapFrequencyLinear(peakRow, effectiveHeight, minFreq, maxFreq) frequency = mapFrequencyLinear(peakRow, effectiveHeight, minFreq, maxFreq)
} else { } else {
frequency = mapFrequency(peakRow, effectiveHeight, minFreq, maxFreq, frequencyMapping || 'mel') frequency = mapFrequency(
peakRow,
effectiveHeight,
minFreq,
maxFreq,
frequencyMapping || 'mel'
)
} }
let amplitude = processedSpectrum[peakRow] let amplitude = processedSpectrum[peakRow]
// Apply temporal smoothing // Apply temporal smoothing
if (col > 0) { if (col > 0) {
amplitude = smoothingFactor * previousAmplitudes[peakRow] + (1 - smoothingFactor) * amplitude amplitude =
smoothingFactor * previousAmplitudes[peakRow] + (1 - smoothingFactor) * amplitude
} }
previousAmplitudes[peakRow] = amplitude previousAmplitudes[peakRow] = amplitude
@@ -236,7 +244,7 @@ export class ImageToAudioSynthesizer {
return { return {
audio: normalizedAudio, audio: normalizedAudio,
sampleRate, sampleRate,
duration duration,
} }
} }
@@ -256,7 +264,7 @@ export class ImageToAudioSynthesizer {
disableNormalization = false, disableNormalization = false,
disableContrast = false, disableContrast = false,
exactBinMapping = true, exactBinMapping = true,
invert = false invert = false,
} = this.params } = this.params
const totalSamples = Math.floor(duration * sampleRate) const totalSamples = Math.floor(duration * sampleRate)
@@ -282,7 +290,6 @@ export class ImageToAudioSynthesizer {
} }
} }
// Map image rows to these exact bins // Map image rows to these exact bins
console.log(`Ultra-precise mode: Using ${freqBins.length} exact FFT bins from ${minFreq}Hz to ${maxFreq}Hz`)
} else { } else {
// Linear frequency mapping // Linear frequency mapping
freqBins = [] freqBins = []
@@ -339,16 +346,16 @@ export class ImageToAudioSynthesizer {
const contrast = this.params.contrast || 1.0 const contrast = this.params.contrast || 1.0
// Fast power optimization for common cases // Fast power optimization for common cases
if (contrast === 1.0) { if (contrast === 1.0) {
amplitude = intensity // No contrast amplitude = intensity // No contrast
} else if (contrast === 2.0) { } else if (contrast === 2.0) {
amplitude = intensity * intensity // Square is much faster than Math.pow amplitude = intensity * intensity // Square is much faster than Math.pow
} else if (contrast === 0.5) { } else if (contrast === 0.5) {
amplitude = Math.sqrt(intensity) // Square root is faster than Math.pow amplitude = Math.sqrt(intensity) // Square root is faster than Math.pow
} else if (contrast === 3.0) { } else if (contrast === 3.0) {
amplitude = intensity * intensity * intensity // Cube amplitude = intensity * intensity * intensity // Cube
} else if (contrast === 4.0) { } else if (contrast === 4.0) {
const sq = intensity * intensity const sq = intensity * intensity
amplitude = sq * sq // Fourth power amplitude = sq * sq // Fourth power
} else { } else {
// Fast power approximation for arbitrary values // Fast power approximation for arbitrary values
// Uses bit manipulation + lookup for ~10x speedup over Math.pow // Uses bit manipulation + lookup for ~10x speedup over Math.pow
@@ -380,8 +387,8 @@ export class ImageToAudioSynthesizer {
// Phase increment method - mathematically identical but much faster // Phase increment method - mathematically identical but much faster
// Eliminates array lookups and multiplications in tight loop // Eliminates array lookups and multiplications in tight loop
let phase = freqCoeff * startSample / sampleRate // Initial phase let phase = (freqCoeff * startSample) / sampleRate // Initial phase
const phaseIncrement = freqCoeff / sampleRate // Phase per sample const phaseIncrement = freqCoeff / sampleRate // Phase per sample
for (let i = 0; i < frameLength; i++) { for (let i = 0; i < frameLength; i++) {
columnSpectrum[i] += amplitude * Math.sin(phase) columnSpectrum[i] += amplitude * Math.sin(phase)
phase += phaseIncrement phase += phaseIncrement
@@ -415,7 +422,7 @@ export class ImageToAudioSynthesizer {
return { return {
audio, audio,
sampleRate, sampleRate,
duration duration,
} }
} }
@@ -470,7 +477,7 @@ export function createDirectParams(overrides: Partial<SynthesisParams> = {}): Sy
disableNormalization: false, disableNormalization: false,
disableContrast: false, disableContrast: false,
exactBinMapping: false, exactBinMapping: false,
...overrides ...overrides,
} }
} }
@@ -498,7 +505,7 @@ export function createCustomParams(overrides: Partial<SynthesisParams> = {}): Sy
disableNormalization: false, disableNormalization: false,
disableContrast: false, disableContrast: false,
exactBinMapping: false, exactBinMapping: false,
...overrides ...overrides,
} }
} }

View File

@@ -27,8 +27,9 @@ export function barkToHz(bark: number): number {
let freq = 1000 // Initial guess let freq = 1000 // Initial guess
for (let i = 0; i < 10; i++) { for (let i = 0; i < 10; i++) {
const barkEst = hzToBark(freq) const barkEst = hzToBark(freq)
const derivative = 13 * 0.00076 / (1 + Math.pow(0.00076 * freq, 2)) + const derivative =
3.5 * 2 * (freq / 7500) * (1 / 7500) / (1 + Math.pow(freq / 7500, 4)) (13 * 0.00076) / (1 + Math.pow(0.00076 * freq, 2)) +
(3.5 * 2 * (freq / 7500) * (1 / 7500)) / (1 + Math.pow(freq / 7500, 4))
freq = freq - (barkEst - bark) / derivative freq = freq - (barkEst - bark) / derivative
if (Math.abs(hzToBark(freq) - bark) < 0.001) break if (Math.abs(hzToBark(freq) - bark) < 0.001) break
} }
@@ -58,7 +59,11 @@ export function applyAmplitudeCurve(amplitude: number, curve: string, gamma: num
/** /**
* Apply soft thresholding using tanh function * Apply soft thresholding using tanh function
*/ */
export function applySoftThreshold(amplitude: number, threshold: number, softness: number = 0.1): number { export function applySoftThreshold(
amplitude: number,
threshold: number,
softness: number = 0.1
): number {
if (threshold <= 0) return amplitude if (threshold <= 0) return amplitude
const ratio = amplitude / threshold const ratio = amplitude / threshold
@@ -76,7 +81,13 @@ export function applySoftThreshold(amplitude: number, threshold: number, softnes
/** /**
* Map frequency using specified scale * Map frequency using specified scale
*/ */
export function mapFrequency(row: number, totalRows: number, minFreq: number, maxFreq: number, scale: string): number { export function mapFrequency(
row: number,
totalRows: number,
minFreq: number,
maxFreq: number,
scale: string
): number {
const normalizedRow = row / (totalRows - 1) const normalizedRow = row / (totalRows - 1)
switch (scale) { switch (scale) {
@@ -109,7 +120,11 @@ export function mapFrequency(row: number, totalRows: number, minFreq: number, ma
/** /**
* Detect spectral peaks in amplitude spectrum with optional smoothing * Detect spectral peaks in amplitude spectrum with optional smoothing
*/ */
export function detectSpectralPeaks(spectrum: number[], threshold: number = 0.01, useSmoothing: boolean = false): number[] { export function detectSpectralPeaks(
spectrum: number[],
threshold: number = 0.01,
useSmoothing: boolean = false
): number[] {
if (useSmoothing) { if (useSmoothing) {
return detectSmoothSpectralPeaks(spectrum, threshold) return detectSmoothSpectralPeaks(spectrum, threshold)
} }
@@ -126,9 +141,7 @@ export function detectSpectralPeaks(spectrum: number[], threshold: number = 0.01
// Fallback: use local maxima with lower threshold if no peaks found // Fallback: use local maxima with lower threshold if no peaks found
if (peaks.length === 0) { if (peaks.length === 0) {
for (let i = 1; i < spectrum.length - 1; i++) { for (let i = 1; i < spectrum.length - 1; i++) {
if (spectrum[i] > spectrum[i - 1] && if (spectrum[i] > spectrum[i - 1] && spectrum[i] > spectrum[i + 1] && spectrum[i] > 0.001) {
spectrum[i] > spectrum[i + 1] &&
spectrum[i] > 0.001) {
peaks.push(i) peaks.push(i)
} }
} }
@@ -148,12 +161,13 @@ export function detectSmoothSpectralPeaks(spectrum: number[], threshold: number
for (let i = 2; i < smoothedSpectrum.length - 2; i++) { for (let i = 2; i < smoothedSpectrum.length - 2; i++) {
const current = smoothedSpectrum[i] const current = smoothedSpectrum[i]
if (current > threshold && if (
current > smoothedSpectrum[i - 1] && current > threshold &&
current > smoothedSpectrum[i + 1] && current > smoothedSpectrum[i - 1] &&
current > smoothedSpectrum[i - 2] && current > smoothedSpectrum[i + 1] &&
current > smoothedSpectrum[i + 2]) { current > smoothedSpectrum[i - 2] &&
current > smoothedSpectrum[i + 2]
) {
// Find the exact peak position with sub-bin accuracy using parabolic interpolation // Find the exact peak position with sub-bin accuracy using parabolic interpolation
const y1 = smoothedSpectrum[i - 1] const y1 = smoothedSpectrum[i - 1]
const y2 = smoothedSpectrum[i] const y2 = smoothedSpectrum[i]
@@ -199,7 +213,11 @@ function smoothSpectrum(spectrum: number[], windowSize: number): number[] {
let sum = 0 let sum = 0
let count = 0 let count = 0
for (let j = Math.max(0, i - halfWindow); j <= Math.min(spectrum.length - 1, i + halfWindow); j++) { for (
let j = Math.max(0, i - halfWindow);
j <= Math.min(spectrum.length - 1, i + halfWindow);
j++
) {
sum += spectrum[j] sum += spectrum[j]
count++ count++
} }
@@ -213,7 +231,11 @@ function smoothSpectrum(spectrum: number[], windowSize: number): number[] {
/** /**
* Apply perceptual amplitude weighting with contrast control * Apply perceptual amplitude weighting with contrast control
*/ */
export function perceptualAmplitudeWeighting(freq: number, amplitude: number, contrast: number = 2.2): number { export function perceptualAmplitudeWeighting(
freq: number,
amplitude: number,
contrast: number = 2.2
): number {
// Apply contrast curve first (like LeviBorodenko's approach) // Apply contrast curve first (like LeviBorodenko's approach)
const contrastedAmplitude = Math.pow(amplitude, contrast) const contrastedAmplitude = Math.pow(amplitude, contrast)
@@ -223,8 +245,6 @@ export function perceptualAmplitudeWeighting(freq: number, amplitude: number, co
return contrastedAmplitude * weight return contrastedAmplitude * weight
} }
/** /**
* Generate spectral density by creating multiple tones per frequency bin * Generate spectral density by creating multiple tones per frequency bin
* Inspired by LeviBorodenko's multi-tone approach * Inspired by LeviBorodenko's multi-tone approach
@@ -239,13 +259,13 @@ export function generateSpectralDensity(
const toneSpacing = bandwidth / numTones const toneSpacing = bandwidth / numTones
for (let i = 0; i < numTones; i++) { for (let i = 0; i < numTones; i++) {
const freq = centerFreq + (i - numTones/2) * toneSpacing const freq = centerFreq + (i - numTones / 2) * toneSpacing
const toneAmplitude = amplitude * (1 - Math.abs(i - numTones/2) / numTones * 0.3) // Slight amplitude variation const toneAmplitude = amplitude * (1 - (Math.abs(i - numTones / 2) / numTones) * 0.3) // Slight amplitude variation
peaks.push({ peaks.push({
frequency: freq, frequency: freq,
amplitude: toneAmplitude, amplitude: toneAmplitude,
phase: 0 phase: 0,
}) })
} }
@@ -328,7 +348,8 @@ export function analyzeImageBrightness(imageData: ImageData): {
const avgEdgeBrightness = edgePixels > 0 ? edgeBrightness / edgePixels : meanBrightness const avgEdgeBrightness = edgePixels > 0 ? edgeBrightness / edgePixels : meanBrightness
// Calculate contrast (standard deviation) // Calculate contrast (standard deviation)
const variance = brightnesses.reduce((sum, b) => sum + Math.pow(b - meanBrightness, 2), 0) / brightnesses.length const variance =
brightnesses.reduce((sum, b) => sum + Math.pow(b - meanBrightness, 2), 0) / brightnesses.length
const contrast = Math.sqrt(variance) const contrast = Math.sqrt(variance)
// Make recommendation // Make recommendation
@@ -346,7 +367,7 @@ export function analyzeImageBrightness(imageData: ImageData): {
medianBrightness, medianBrightness,
edgeBrightness: avgEdgeBrightness, edgeBrightness: avgEdgeBrightness,
contrast, contrast,
recommendation recommendation,
} }
} }
@@ -366,19 +387,19 @@ export function generateWindow(length: number, windowType: string): Float32Array
switch (windowType) { switch (windowType) {
case 'hann': case 'hann':
for (let i = 0; i < length; i++) { for (let i = 0; i < length; i++) {
window[i] = 0.5 * (1 - Math.cos(2 * Math.PI * i / (length - 1))) window[i] = 0.5 * (1 - Math.cos((2 * Math.PI * i) / (length - 1)))
} }
break break
case 'hamming': case 'hamming':
for (let i = 0; i < length; i++) { for (let i = 0; i < length; i++) {
window[i] = 0.54 - 0.46 * Math.cos(2 * Math.PI * i / (length - 1)) window[i] = 0.54 - 0.46 * Math.cos((2 * Math.PI * i) / (length - 1))
} }
break break
case 'blackman': case 'blackman':
for (let i = 0; i < length; i++) { for (let i = 0; i < length; i++) {
const factor = 2 * Math.PI * i / (length - 1) const factor = (2 * Math.PI * i) / (length - 1)
window[i] = 0.42 - 0.5 * Math.cos(factor) + 0.08 * Math.cos(2 * factor) window[i] = 0.42 - 0.5 * Math.cos(factor) + 0.08 * Math.cos(2 * factor)
} }
break break
@@ -450,7 +471,12 @@ export function extractSpectrum(
/** /**
* Alternative linear frequency mapping inspired by alexadam's approach * Alternative linear frequency mapping inspired by alexadam's approach
*/ */
export function mapFrequencyLinear(row: number, totalRows: number, minFreq: number, maxFreq: number): number { export function mapFrequencyLinear(
row: number,
totalRows: number,
minFreq: number,
maxFreq: number
): number {
// Direct linear mapping from top to bottom (high freq at top) // Direct linear mapping from top to bottom (high freq at top)
const normalizedRow = row / (totalRows - 1) const normalizedRow = row / (totalRows - 1)
return maxFreq - normalizedRow * (maxFreq - minFreq) return maxFreq - normalizedRow * (maxFreq - minFreq)

View File

@@ -5,7 +5,7 @@ export {
createDirectParams, createDirectParams,
createCustomParams, createCustomParams,
synthesizeDirect, synthesizeDirect,
synthesizeCustom synthesizeCustom,
} from './core/synthesizer' } from './core/synthesizer'
export type { SynthesisParams, SpectralPeak, SynthesisResult, WindowType } from './core/types' export type { SynthesisParams, SpectralPeak, SynthesisResult, WindowType } from './core/types'
@@ -25,14 +25,9 @@ export {
mapFrequency, mapFrequency,
mapFrequencyLinear, mapFrequencyLinear,
normalizeAudioGlobal, normalizeAudioGlobal,
generateSpectralDensity generateSpectralDensity,
} from './core/utils' } from './core/utils'
// Audio export // Audio export
export { export { createWAVBuffer, downloadWAV, playAudio, createAudioPlayer } from './audio/export'
createWAVBuffer,
downloadWAV,
playAudio,
createAudioPlayer
} from './audio/export'
export type { AudioPlayer } from './audio/export' export type { AudioPlayer } from './audio/export'

View File

@@ -1,7 +1,20 @@
import { atom } from 'nanostores' import { atom } from 'nanostores'
import type { SynthesisParams } from '../spectral-synthesis' import type { SynthesisParams } from '../spectral-synthesis'
export type GeneratorType = 'tixy' | 'picsum' | 'art-institute' | 'waveform' | 'partials' | 'slides' | 'shapes' | 'bands' | 'dust' | 'geopattern' | 'from-photo' | 'webcam' | 'harmonics' export type GeneratorType =
| 'tixy'
| 'picsum'
| 'art-institute'
| 'waveform'
| 'partials'
| 'slides'
| 'shapes'
| 'bands'
| 'dust'
| 'geopattern'
| 'from-photo'
| 'webcam'
| 'harmonics'
export interface GeneratedImage { export interface GeneratedImage {
id: string id: string
@@ -22,7 +35,7 @@ export const appSettings = atom<AppSettings>({
selectedGenerator: 'tixy', selectedGenerator: 'tixy',
gridSize: 25, gridSize: 25,
backgroundColor: '#000000', backgroundColor: '#000000',
foregroundColor: '#ffffff' foregroundColor: '#ffffff',
}) })
export const generatedImages = atom<GeneratedImage[]>([]) export const generatedImages = atom<GeneratedImage[]>([])
@@ -54,5 +67,5 @@ export const synthesisParams = atom<SynthesisParams>({
frameOverlap: 0.75, frameOverlap: 0.75,
disableNormalization: false, disableNormalization: false,
disableContrast: false, disableContrast: false,
exactBinMapping: false exactBinMapping: false,
}) })

17
src/types/geopattern.d.ts vendored Normal file
View 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
}

View File

@@ -1,9 +1,6 @@
/** @type {import('tailwindcss').Config} */ /** @type {import('tailwindcss').Config} */
export default { export default {
content: [ content: ['./index.html', './src/**/*.{js,ts,jsx,tsx}'],
"./index.html",
"./src/**/*.{js,ts,jsx,tsx}",
],
theme: { theme: {
extend: {}, extend: {},
}, },

View File

@@ -1,7 +1,4 @@
{ {
"files": [], "files": [],
"references": [ "references": [{ "path": "./tsconfig.app.json" }, { "path": "./tsconfig.node.json" }]
{ "path": "./tsconfig.app.json" },
{ "path": "./tsconfig.node.json" }
]
} }

View File

@@ -4,4 +4,41 @@ import react from '@vitejs/plugin-react'
// https://vite.dev/config/ // https://vite.dev/config/
export default defineConfig({ export default defineConfig({
plugins: [react()], plugins: [react()],
build: {
// Generate source maps for production debugging
sourcemap: false,
// Optimize chunk size threshold
chunkSizeWarningLimit: 1000,
// Enable minification (use default for rolldown)
minify: true,
// Rollup options for advanced bundling
rollupOptions: {
output: {
// Manual chunk splitting for better caching
manualChunks: (id) => {
if (id.includes('react') || id.includes('react-dom')) {
return 'react'
}
if (id.includes('nanostores')) {
return 'stores'
}
if (id.includes('jszip') || id.includes('geopattern')) {
return 'vendor'
}
},
// Optimize chunk file names
chunkFileNames: 'assets/[name]-[hash].js',
entryFileNames: 'assets/[name]-[hash].js',
assetFileNames: 'assets/[name]-[hash].[ext]',
},
},
// Target modern browsers for better optimization
target: 'esnext',
// Enable CSS code splitting
cssCodeSplit: true,
},
// Optimize dependency pre-bundling
optimizeDeps: {
include: ['react', 'react-dom', 'nanostores', '@nanostores/react'],
},
}) })