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/
*.log
npm-debug.log*
yarn-debug.log*
@@ -7,18 +26,59 @@ yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
dist
dist-ssr
*.local
# Package manager files
package-lock.json
yarn.lock
.yarn/
.yarnrc.yml
# Testing and coverage
coverage/
*.lcov
.nyc_output/
.coverage/
junit.xml
# Cache directories
.cache/
.parcel-cache/
.npm/
.eslintcache
.stylelintcache
# OS generated files
.DS_Store
.DS_Store?
._*
.Spotlight-V100
.Trashes
ehthumbs.db
Thumbs.db
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
!.vscode/settings.json
!.vscode/tasks.json
!.vscode/launch.json
.idea/
*.swp
*.swo
*~
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
# Temporary files
*.tmp
*.temp
.temp/
.tmp/
# Runtime data
pids
*.pid
*.seed
*.pid.lock

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

3197
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

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

10
pnpm-lock.yaml generated
View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,6 +1,10 @@
import { useStore } from '@nanostores/react'
import { selectedImage, panelOpen, generatedImages, synthesisParams } from '../stores'
import { synthesizeFromImage, playAudio, createAudioPlayer, type WindowType, type AudioPlayer } from '../spectral-synthesis'
import { selectedImage, generatedImages, synthesisParams } from '../stores'
import {
synthesizeFromImage,
createAudioPlayer,
type AudioPlayer,
} from '../spectral-synthesis'
import { quickExportSingle, quickExportBatch } from '../audio-export'
import { useState, useRef, useEffect } from 'react'
import AudioControls from './AudioControls'
@@ -14,14 +18,8 @@ export default function AudioPanel() {
const [volume, setVolume] = useState(0.7)
const audioPlayerRef = useRef<AudioPlayer | null>(null)
const handleClearSelection = () => {
selectedImage.set(null)
if (audioPlayerRef.current) {
audioPlayerRef.current.stop()
}
}
const updateParam = <K extends keyof typeof params>(key: K, value: typeof params[K]) => {
const updateParam = <K extends keyof typeof params>(key: K, value: (typeof params)[K]) => {
synthesisParams.set({ ...params, [key]: value })
}
@@ -82,18 +80,6 @@ export default function AudioPanel() {
}
}
const handlePlaySingle = async () => {
if (!selected) return
setIsProcessing(true)
try {
const audio = synthesizeFromImage(selected.imageData, params)
await playAudio(audio, params.sampleRate)
} catch (error) {
console.error('Error playing audio:', error)
}
setIsProcessing(false)
}
// Keyboard shortcuts
useEffect(() => {
@@ -131,7 +117,6 @@ export default function AudioPanel() {
if (!result.success) {
console.error('Batch export failed:', result.errors)
} else {
console.log(`Exported ${result.successfulFiles}/${result.totalFiles} files to ${result.zipFilename}`)
}
} catch (error) {
console.error('Error generating all audio:', error)
@@ -147,7 +132,7 @@ export default function AudioPanel() {
<div className="flex items-center space-x-3">
<div className="w-24 h-24 border border-gray-600">
<canvas
ref={(canvas) => {
ref={canvas => {
if (canvas && selected.canvas) {
const ctx = canvas.getContext('2d')!
canvas.width = selected.canvas.width
@@ -186,9 +171,7 @@ export default function AudioPanel() {
<div className="p-6 space-y-4">
{/* Synthesis Mode Selection - Top Priority */}
<div>
<label className="block text-xs text-gray-400 mb-1">
Synthesis Mode
</label>
<label className="block text-xs text-gray-400 mb-1">Synthesis Mode</label>
<div className="grid grid-cols-2 gap-1">
<button
onClick={() => updateParam('synthesisMode', 'direct')}
@@ -227,7 +210,7 @@ export default function AudioPanel() {
max="30"
step="1"
value={params.duration}
onChange={(e) => updateParam('duration', Number(e.target.value))}
onChange={e => updateParam('duration', Number(e.target.value))}
className="w-full"
/>
</div>
@@ -245,10 +228,12 @@ export default function AudioPanel() {
max="500"
step="10"
value={params.maxPartials}
onChange={(e) => updateParam('maxPartials', Number(e.target.value))}
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>
<p className="text-xs text-gray-500 mt-1">
Controls audio complexity vs performance
</p>
</div>
<div>
@@ -261,10 +246,12 @@ export default function AudioPanel() {
max="10"
step="1"
value={params.frequencyResolution}
onChange={(e) => updateParam('frequencyResolution', Number(e.target.value))}
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>
<p className="text-xs text-gray-500 mt-1">
Higher values create broader, richer frequency bands
</p>
</div>
</>
)}
@@ -273,7 +260,6 @@ export default function AudioPanel() {
{(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}
@@ -293,7 +279,9 @@ export default function AudioPanel() {
</button>
))}
</div>
<p className="text-xs text-gray-500 mt-1">Higher = better frequency resolution</p>
<p className="text-xs text-gray-500 mt-1">
Higher = better frequency resolution
</p>
</div>
<div>
@@ -306,16 +294,22 @@ export default function AudioPanel() {
max="0.9"
step="0.125"
value={params.frameOverlap || 0.75}
onChange={(e) => updateParam('frameOverlap', Number(e.target.value))}
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>
<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>
<label className="text-xs text-gray-400">
Disable normalization: can be very loud
</label>
<button
onClick={() => updateParam('disableNormalization', !(params.disableNormalization ?? false))}
onClick={() =>
updateParam('disableNormalization', !(params.disableNormalization ?? false))
}
className={`px-2 py-1 text-xs border ${
params.disableNormalization === true
? 'bg-white text-black border-white'
@@ -329,7 +323,6 @@ export default function AudioPanel() {
</>
)}
<div>
<label className="block text-xs text-gray-400 mb-1">
Min Frequency: {params.minFreq}Hz
@@ -340,7 +333,7 @@ export default function AudioPanel() {
max="200"
step="10"
value={params.minFreq}
onChange={(e) => updateParam('minFreq', Number(e.target.value))}
onChange={e => updateParam('minFreq', Number(e.target.value))}
className="w-full"
/>
</div>
@@ -355,7 +348,7 @@ export default function AudioPanel() {
max="20000"
step="500"
value={params.maxFreq}
onChange={(e) => updateParam('maxFreq', Number(e.target.value))}
onChange={e => updateParam('maxFreq', Number(e.target.value))}
className="w-full"
/>
</div>
@@ -373,16 +366,14 @@ export default function AudioPanel() {
max="0.1"
step="0.001"
value={params.amplitudeThreshold}
onChange={(e) => updateParam('amplitudeThreshold', Number(e.target.value))}
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>
<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')}
@@ -425,7 +416,9 @@ export default function AudioPanel() {
Blackman
</button>
</div>
<p className="text-xs text-gray-500 mt-1">Reduces clicking/popping between time frames</p>
<p className="text-xs text-gray-500 mt-1">
Reduces clicking/popping between time frames
</p>
</div>
</>
)}
@@ -433,9 +426,7 @@ export default function AudioPanel() {
{/* 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>
<label className="block text-xs text-gray-400 mb-1">Color</label>
<div className="grid grid-cols-2 gap-1">
<button
onClick={() => updateParam('invert', false)}
@@ -471,7 +462,7 @@ export default function AudioPanel() {
max="5"
step="0.1"
value={params.contrast || 2.2}
onChange={(e) => updateParam('contrast', Number(e.target.value))}
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>
@@ -481,9 +472,7 @@ export default function AudioPanel() {
{params.synthesisMode === 'custom' && (
<>
<div>
<label className="block text-xs text-gray-400 mb-1">
Frequency Mapping
</label>
<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')}
@@ -539,16 +528,20 @@ export default function AudioPanel() {
max="7"
step="1"
value={params.spectralDensity || 3}
onChange={(e) => updateParam('spectralDensity', Number(e.target.value))}
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>
<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)}
onClick={() =>
updateParam('usePerceptualWeighting', !params.usePerceptualWeighting)
}
className={`px-2 py-1 text-xs border ${
params.usePerceptualWeighting !== false
? 'bg-white text-black border-white'
@@ -558,10 +551,11 @@ export default function AudioPanel() {
{params.usePerceptualWeighting !== false ? 'ON' : 'OFF'}
</button>
</div>
<p className="text-xs text-gray-500 mt-1">Squared RGB sum for better brightness perception</p>
<p className="text-xs text-gray-500 mt-1">
Squared RGB sum for better brightness perception
</p>
</>
)}
</div>
</div>
</div>

View File

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

View File

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

View File

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

View File

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

View File

@@ -30,7 +30,8 @@ function GridSquare({ index, image, onDrop, onSelect, selected }: GridSquareProp
setIsDragOver(false)
}, [])
const handleDrop = useCallback(async (e: React.DragEvent) => {
const handleDrop = useCallback(
async (e: React.DragEvent) => {
e.preventDefault()
setIsDragOver(false)
@@ -47,7 +48,9 @@ function GridSquare({ index, image, onDrop, onSelect, selected }: GridSquareProp
} finally {
setIsProcessing(false)
}
}, [index, onDrop])
},
[index, onDrop]
)
const handleClick = useCallback(() => {
if (image) {
@@ -72,7 +75,7 @@ function GridSquare({ index, image, onDrop, onSelect, selected }: GridSquareProp
</div>
) : image ? (
<canvas
ref={(canvas) => {
ref={canvas => {
if (canvas && image.canvas) {
const ctx = canvas.getContext('2d')!
canvas.width = image.canvas.width
@@ -102,7 +105,8 @@ export default function PhotoGrid({ size }: PhotoGridProps) {
const images = useStore(generatedImages)
const selected = useStore(selectedImage)
const handleDrop = useCallback(async (file: File, index: number) => {
const handleDrop = useCallback(
async (file: File, index: number) => {
try {
const processedImage = await generateFromPhotoImage(file, size)
@@ -117,11 +121,12 @@ export default function PhotoGrid({ size }: PhotoGridProps) {
// Auto-select the newly processed image
selectedImage.set(processedImage)
console.log('Photo processed and added to grid at index:', index)
} catch (error) {
console.error('Error processing photo:', error)
}
}, [images, size])
},
[images, size]
)
const handleSelect = useCallback((image: GeneratedImage) => {
selectedImage.set(image)
@@ -146,9 +151,7 @@ export default function PhotoGrid({ size }: PhotoGridProps) {
return (
<div className="flex-1 overflow-auto p-6">
<div className="grid grid-cols-5 gap-2">
{gridSquares}
</div>
<div className="grid grid-cols-5 gap-2">{gridSquares}</div>
</div>
)
}

View File

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

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 { generateFromWebcamImage } from '../generators/webcam'
import { WebcamProcessor } from '../generators/webcam-generator'
@@ -37,7 +37,7 @@ function GridSquare({ index, image, onCapture, onSelect, selected, isCapturing }
onClick={handleImageClick}
>
<canvas
ref={(canvas) => {
ref={canvas => {
if (canvas && image.canvas) {
const ctx = canvas.getContext('2d')!
canvas.width = image.canvas.width
@@ -60,9 +60,7 @@ function GridSquare({ index, image, onCapture, onSelect, selected, isCapturing }
{isCapturing ? (
<div className="text-xs">Capturing...</div>
) : (
<div className="text-xs text-center">
Capture
</div>
<div className="text-xs text-center">Capture</div>
)}
</button>
)}
@@ -100,7 +98,8 @@ export default function WebcamGrid({ size }: WebcamGridProps) {
}
}, [processor])
const handleCapture = useCallback(async (index: number) => {
const handleCapture = useCallback(
async (index: number) => {
if (!cameraInitialized) {
console.error('Camera not initialized')
return
@@ -123,14 +122,15 @@ export default function WebcamGrid({ size }: WebcamGridProps) {
// Auto-select the newly captured image
selectedImage.set(capturedImage)
console.log('Photo captured and added to grid at index:', index)
} catch (error) {
console.error('Error capturing photo:', error)
} finally {
setIsCapturing(false)
setCapturingIndex(null)
}
}, [images, size, cameraInitialized, processor])
},
[images, size, cameraInitialized, processor]
)
const handleSelect = useCallback((image: GeneratedImage) => {
selectedImage.set(image)
@@ -163,9 +163,7 @@ export default function WebcamGrid({ size }: WebcamGridProps) {
</div>
)}
<div className="grid grid-cols-5 gap-2">
{gridSquares}
</div>
<div className="grid grid-cols-5 gap-2">{gridSquares}</div>
</div>
)
}

View File

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

View File

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

View File

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

View File

@@ -36,7 +36,7 @@ export function processPhoto(file: File, config: PhotoProcessingConfig): Promise
canvas,
imageData,
originalFile: file,
config
config,
})
} catch (error) {
reject(error)
@@ -55,7 +55,7 @@ function calculateImageDimensions(
imgWidth: number,
imgHeight: number,
targetSize: number
): { drawWidth: number, drawHeight: number, offsetX: number, offsetY: number } {
): { drawWidth: number; drawHeight: number; offsetX: number; offsetY: number } {
const aspectRatio = imgWidth / imgHeight
let drawWidth: number

View File

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

View File

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

View File

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

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

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 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',
foregroundColor = '#ffffff',
threshold = 0.3,
pixelSize = 1
pixelSize = 1,
} = options
const canvas = document.createElement('canvas')
@@ -31,11 +31,12 @@ export function renderTixyToCanvas(
const i = y * width + x
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)),
b: Math.round(fgColor.b * (Math.sign(value) > 0 ? 1 : 0.8))
b: Math.round(fgColor.b * (Math.sign(value) > 0 ? 1 : 0.8)),
}
: bgColor
@@ -58,7 +59,7 @@ export function renderTixyToCanvas(
return {
imageData,
canvas
canvas,
}
}
@@ -71,9 +72,11 @@ export function renderTixyToImageData(
function hexToRgb(hex: string): { r: number; g: number; b: number } {
const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex)
return result ? {
return result
? {
r: parseInt(result[1], 16),
g: parseInt(result[2], 16),
b: parseInt(result[3], 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: '#333333', fg: '#ffffff' },
{ bg: '#000000', fg: '#666666' },
{ bg: '#222222', fg: '#dddddd' }
{ bg: '#222222', fg: '#dddddd' },
]
export function generateTixyImages(count: number, size: number): GeneratedImage[] {
@@ -38,7 +38,7 @@ export function generateTixyImages(count: number, size: number): GeneratedImage[
time,
backgroundColor: palette.bg,
foregroundColor: palette.fg,
pixelSize: 4
pixelSize: 4,
})
const image = {
@@ -46,7 +46,7 @@ export function generateTixyImages(count: number, size: number): GeneratedImage[
canvas: result.canvas,
imageData: result.imageData,
generator: 'tixy' as const,
params: { expression, time, colors: palette }
params: { expression, time, colors: palette },
}
images.push(image)

View File

@@ -24,7 +24,7 @@ const customWaveform = generateWaveform({
splits: 32,
interpolation: 'cubic',
randomness: 'smooth',
lineWidth: 3
lineWidth: 3,
})
```
@@ -33,12 +33,15 @@ const customWaveform = generateWaveform({
### Main Functions
#### `generateWaveform(config?)`
Generate a waveform with optional configuration override.
#### `generateRandomWaveform()`
Generate a waveform with randomized parameters for maximum variety.
#### `generateWaveformBatch(count)`
Generate multiple random waveforms efficiently.
### Configuration
@@ -95,30 +98,33 @@ waveform-generator/
## Examples
### Basic Waveform
```typescript
const simple = generateWaveform({
splits: 16,
interpolation: 'linear'
interpolation: 'linear',
})
```
### Smooth Organic Curves
```typescript
const organic = generateWaveform({
splits: 24,
interpolation: 'cubic',
randomness: 'smooth',
smoothness: 0.7
smoothness: 0.7,
})
```
### Sharp Electronic Waveform
```typescript
const electronic = generateWaveform({
splits: 32,
interpolation: 'exponential',
randomness: 'uniform',
lineWidth: 1
lineWidth: 1,
})
```

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

View File

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

View File

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

View File

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

View File

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

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'
// Generate multiple random waveform images
@@ -40,8 +42,8 @@ export function generateWaveformImages(count: number, size: number = 256): Gener
randomness: waveform.config.randomness,
smoothness: waveform.config.smoothness,
lineWidth: waveform.config.lineWidth,
size
}
size,
},
}
images.push(image)
@@ -91,8 +93,8 @@ export function generateVariedWaveforms(count: number, size: number = 256): Gene
smoothness: waveform.config.smoothness,
lineWidth: waveform.config.lineWidth,
style: 'varied',
size
}
size,
},
}
images.push(image)

View File

@@ -10,8 +10,8 @@ export class WebcamProcessor {
video: {
width: { ideal: 1280 },
height: { ideal: 720 },
facingMode: 'user'
}
facingMode: 'user',
},
})
this.video = document.createElement('video')
@@ -66,7 +66,7 @@ export class WebcamProcessor {
canvas,
imageData,
capturedAt: new Date(),
config
config,
}
}
@@ -89,7 +89,7 @@ export class WebcamProcessor {
videoWidth: number,
videoHeight: number,
targetSize: number
): { drawWidth: number, drawHeight: number, offsetX: number, offsetY: number } {
): { drawWidth: number; drawHeight: number; offsetX: number; offsetY: number } {
const aspectRatio = videoWidth / videoHeight
let drawWidth: number
@@ -114,7 +114,11 @@ export class WebcamProcessor {
return { drawWidth, drawHeight, offsetX, offsetY }
}
private convertToGrayscale(ctx: CanvasRenderingContext2D, size: number, enhanceContrast: boolean) {
private convertToGrayscale(
ctx: CanvasRenderingContext2D,
size: number,
enhanceContrast: boolean
) {
const imageData = ctx.getImageData(0, 0, size, size)
const data = imageData.data

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

17
src/types/geopattern.d.ts vendored Normal file
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} */
export default {
content: [
"./index.html",
"./src/**/*.{js,ts,jsx,tsx}",
],
content: ['./index.html', './src/**/*.{js,ts,jsx,tsx}'],
theme: {
extend: {},
},

View File

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

View File

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