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,8 +121,9 @@ Both tixy-generator and waveform-generator modules are designed as standalone pa
- `index.ts` - Main exports and convenience functions
## Development Notes
- Uses Rolldown Vite for improved performance
- Tailwind CSS for styling (no rounded corners per user preference)
- ESLint configured for React + TypeScript
- All generators support time-based animation parameters
- Image synthesis supports real-time parameter adjustment
- Image synthesis supports real-time parameter adjustment

3197
package-lock.json generated

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,8 +78,9 @@ export default function AudioControls({
className="w-full h-2 bg-black border border-white appearance-none cursor-pointer volume-slider disabled:cursor-not-allowed"
title={`Volume: ${Math.round(volume * 100)}%`}
/>
<style dangerouslySetInnerHTML={{
__html: `
<style
dangerouslySetInnerHTML={{
__html: `
.volume-slider::-webkit-slider-thumb {
appearance: none;
width: 16px;
@ -100,14 +105,13 @@ export default function AudioControls({
.volume-slider::-moz-range-thumb:hover {
background: ${disabled ? '#6b7280' : '#e5e7eb'};
}
`
}} />
`,
}}
/>
</div>
<span className="text-xs text-gray-400 w-10 text-right">
{Math.round(volume * 100)}%
</span>
<span className="text-xs text-gray-400 w-10 text-right">{Math.round(volume * 100)}%</span>
</div>
</div>
)
}
}

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')}
@ -216,265 +199,20 @@ export default function AudioPanel() {
</p>
</div>
<div className="space-y-3">
<div>
<label className="block text-xs text-gray-400 mb-1">
Duration: {params.duration}s
</label>
<input
type="range"
min="1"
max="30"
step="1"
value={params.duration}
onChange={(e) => updateParam('duration', Number(e.target.value))}
className="w-full"
/>
</div>
{/* Custom Mode Only Parameters */}
{params.synthesisMode === 'custom' && (
<>
<div>
<label className="block text-xs text-gray-400 mb-1">
Max Partials: {params.maxPartials}
</label>
<input
type="range"
min="10"
max="500"
step="10"
value={params.maxPartials}
onChange={(e) => updateParam('maxPartials', Number(e.target.value))}
className="w-full"
/>
<p className="text-xs text-gray-500 mt-1">Controls audio complexity vs performance</p>
</div>
<div>
<label className="block text-xs text-gray-400 mb-1">
Frequency Density: {params.frequencyResolution}x
</label>
<input
type="range"
min="1"
max="10"
step="1"
value={params.frequencyResolution}
onChange={(e) => updateParam('frequencyResolution', Number(e.target.value))}
className="w-full"
/>
<p className="text-xs text-gray-500 mt-1">Higher values create broader, richer frequency bands</p>
</div>
</>
)}
{/* Direct Mode Only Parameters */}
{(params.synthesisMode || 'direct') === 'direct' && (
<>
<div className="space-y-3">
<div>
<label className="block text-xs text-gray-400 mb-1">
FFT Size: {params.fftSize || 2048}
</label>
<div className="grid grid-cols-4 gap-1">
{[1024, 2048, 4096, 8192].map(size => (
<button
key={size}
onClick={() => updateParam('fftSize', size)}
className={`px-2 py-1 text-xs border ${
(params.fftSize || 2048) === size
? 'bg-white text-black border-white'
: 'bg-black text-white border-gray-600 hover:border-white'
}`}
>
{size}
</button>
))}
</div>
<p className="text-xs text-gray-500 mt-1">Higher = better frequency resolution</p>
</div>
<div>
<label className="block text-xs text-gray-400 mb-1">
Frame Overlap: {((params.frameOverlap || 0.75) * 100).toFixed(0)}%
</label>
<input
type="range"
min="0"
max="0.9"
step="0.125"
value={params.frameOverlap || 0.75}
onChange={(e) => updateParam('frameOverlap', Number(e.target.value))}
className="w-full"
/>
<p className="text-xs text-gray-500 mt-1">Higher = smoother temporal resolution</p>
</div>
<div className="flex items-center justify-between">
<label className="text-xs text-gray-400">Disable normalization: can be very loud</label>
<button
onClick={() => updateParam('disableNormalization', !(params.disableNormalization ?? false))}
className={`px-2 py-1 text-xs border ${
params.disableNormalization === true
? 'bg-white text-black border-white'
: 'bg-black text-white border-gray-600 hover:border-white'
}`}
>
{params.disableNormalization === true ? 'ON' : 'OFF'}
</button>
</div>
</div>
</>
)}
<div>
<label className="block text-xs text-gray-400 mb-1">
Min Frequency: {params.minFreq}Hz
</label>
<input
type="range"
min="20"
max="200"
step="10"
value={params.minFreq}
onChange={(e) => updateParam('minFreq', Number(e.target.value))}
className="w-full"
/>
</div>
<div>
<label className="block text-xs text-gray-400 mb-1">
Max Frequency: {params.maxFreq}Hz
</label>
<input
type="range"
min="1000"
max="20000"
step="500"
value={params.maxFreq}
onChange={(e) => updateParam('maxFreq', Number(e.target.value))}
className="w-full"
/>
</div>
{/* Custom Mode Only Parameters */}
{params.synthesisMode === 'custom' && (
<>
<div>
<label className="block text-xs text-gray-400 mb-1">
Amplitude Threshold: {params.amplitudeThreshold}
</label>
<input
type="range"
min="0.001"
max="0.1"
step="0.001"
value={params.amplitudeThreshold}
onChange={(e) => updateParam('amplitudeThreshold', Number(e.target.value))}
className="w-full"
/>
<p className="text-xs text-gray-500 mt-1">Minimum amplitude to include</p>
</div>
<div>
<label className="block text-xs text-gray-400 mb-1">
Window Type
</label>
<div className="grid grid-cols-4 gap-1">
<button
onClick={() => updateParam('windowType', 'rectangular')}
className={`px-2 py-1 text-xs border ${
params.windowType === 'rectangular'
? 'bg-white text-black border-white'
: 'bg-black text-white border-gray-600 hover:border-white'
}`}
>
Rect
</button>
<button
onClick={() => updateParam('windowType', 'hann')}
className={`px-2 py-1 text-xs border ${
params.windowType === 'hann'
? 'bg-white text-black border-white'
: 'bg-black text-white border-gray-600 hover:border-white'
}`}
>
Hann
</button>
<button
onClick={() => updateParam('windowType', 'hamming')}
className={`px-2 py-1 text-xs border ${
params.windowType === 'hamming'
? 'bg-white text-black border-white'
: 'bg-black text-white border-gray-600 hover:border-white'
}`}
>
Hamming
</button>
<button
onClick={() => updateParam('windowType', 'blackman')}
className={`px-2 py-1 text-xs border ${
params.windowType === 'blackman'
? 'bg-white text-black border-white'
: 'bg-black text-white border-gray-600 hover:border-white'
}`}
>
Blackman
</button>
</div>
<p className="text-xs text-gray-500 mt-1">Reduces clicking/popping between time frames</p>
</div>
</>
)}
{/* Color Inversion - Available for both modes */}
<div className="border-t border-gray-700 pt-4">
<div className="space-y-3">
<div>
<label className="block text-xs text-gray-400 mb-1">
Color
</label>
<div className="grid grid-cols-2 gap-1">
<button
onClick={() => updateParam('invert', false)}
className={`px-2 py-1 text-xs border ${
!params.invert
? 'bg-white text-black border-white'
: 'bg-black text-white border-gray-600 hover:border-white'
}`}
>
Normal
</button>
<button
onClick={() => updateParam('invert', true)}
className={`px-2 py-1 text-xs border ${
params.invert
? 'bg-white text-black border-white'
: 'bg-black text-white border-gray-600 hover:border-white'
}`}
>
Inverted
</button>
</div>
</div>
{/* Contrast - Available for both modes */}
<div>
<label className="block text-xs text-gray-400 mb-1">
Contrast: {(params.contrast || 2.2).toFixed(1)}
Duration: {params.duration}s
</label>
<input
type="range"
min="1"
max="5"
step="0.1"
value={params.contrast || 2.2}
onChange={(e) => updateParam('contrast', Number(e.target.value))}
max="30"
step="1"
value={params.duration}
onChange={e => updateParam('duration', Number(e.target.value))}
className="w-full"
/>
<p className="text-xs text-gray-500 mt-1">Power curve for brightness perception</p>
</div>
{/* Custom Mode Only Parameters */}
@ -482,89 +220,345 @@ export default function AudioPanel() {
<>
<div>
<label className="block text-xs text-gray-400 mb-1">
Frequency Mapping
Max Partials: {params.maxPartials}
</label>
<div className="grid grid-cols-4 gap-1">
<button
onClick={() => updateParam('frequencyMapping', 'mel')}
className={`px-2 py-1 text-xs border ${
(params.frequencyMapping || 'linear') === 'mel'
? 'bg-white text-black border-white'
: 'bg-black text-white border-gray-600 hover:border-white'
}`}
>
Mel
</button>
<button
onClick={() => updateParam('frequencyMapping', 'linear')}
className={`px-2 py-1 text-xs border ${
(params.frequencyMapping || 'linear') === 'linear'
? 'bg-white text-black border-white'
: 'bg-black text-white border-gray-600 hover:border-white'
}`}
>
Linear
</button>
<button
onClick={() => updateParam('frequencyMapping', 'bark')}
className={`px-2 py-1 text-xs border ${
params.frequencyMapping === 'bark'
? 'bg-white text-black border-white'
: 'bg-black text-white border-gray-600 hover:border-white'
}`}
>
Bark
</button>
<button
onClick={() => updateParam('frequencyMapping', 'log')}
className={`px-2 py-1 text-xs border ${
params.frequencyMapping === 'log'
? 'bg-white text-black border-white'
: 'bg-black text-white border-gray-600 hover:border-white'
}`}
>
Log
</button>
</div>
<p className="text-xs text-gray-500 mt-1">How image height maps to frequency</p>
<input
type="range"
min="10"
max="500"
step="10"
value={params.maxPartials}
onChange={e => updateParam('maxPartials', Number(e.target.value))}
className="w-full"
/>
<p className="text-xs text-gray-500 mt-1">
Controls audio complexity vs performance
</p>
</div>
<div>
<label className="block text-xs text-gray-400 mb-1">
Spectral Density: {params.spectralDensity || 3}
Frequency Density: {params.frequencyResolution}x
</label>
<input
type="range"
min="1"
max="7"
max="10"
step="1"
value={params.spectralDensity || 3}
onChange={(e) => updateParam('spectralDensity', Number(e.target.value))}
value={params.frequencyResolution}
onChange={e => updateParam('frequencyResolution', Number(e.target.value))}
className="w-full"
/>
<p className="text-xs text-gray-500 mt-1">Tones per frequency peak (richer = higher)</p>
<p className="text-xs text-gray-500 mt-1">
Higher values create broader, richer frequency bands
</p>
</div>
</>
)}
{/* Direct Mode Only Parameters */}
{(params.synthesisMode || 'direct') === 'direct' && (
<>
<div className="space-y-3">
<div>
<label className="block text-xs text-gray-400 mb-1">
FFT Size: {params.fftSize || 2048}
</label>
<div className="grid grid-cols-4 gap-1">
{[1024, 2048, 4096, 8192].map(size => (
<button
key={size}
onClick={() => updateParam('fftSize', size)}
className={`px-2 py-1 text-xs border ${
(params.fftSize || 2048) === size
? 'bg-white text-black border-white'
: 'bg-black text-white border-gray-600 hover:border-white'
}`}
>
{size}
</button>
))}
</div>
<p className="text-xs text-gray-500 mt-1">
Higher = better frequency resolution
</p>
</div>
<div>
<label className="block text-xs text-gray-400 mb-1">
Frame Overlap: {((params.frameOverlap || 0.75) * 100).toFixed(0)}%
</label>
<input
type="range"
min="0"
max="0.9"
step="0.125"
value={params.frameOverlap || 0.75}
onChange={e => updateParam('frameOverlap', Number(e.target.value))}
className="w-full"
/>
<p className="text-xs text-gray-500 mt-1">
Higher = smoother temporal resolution
</p>
</div>
<div className="flex items-center justify-between">
<label className="text-xs text-gray-400">
Disable normalization: can be very loud
</label>
<button
onClick={() =>
updateParam('disableNormalization', !(params.disableNormalization ?? false))
}
className={`px-2 py-1 text-xs border ${
params.disableNormalization === true
? 'bg-white text-black border-white'
: 'bg-black text-white border-gray-600 hover:border-white'
}`}
>
{params.disableNormalization === true ? 'ON' : 'OFF'}
</button>
</div>
</div>
</>
)}
<div>
<label className="block text-xs text-gray-400 mb-1">
Min Frequency: {params.minFreq}Hz
</label>
<input
type="range"
min="20"
max="200"
step="10"
value={params.minFreq}
onChange={e => updateParam('minFreq', Number(e.target.value))}
className="w-full"
/>
</div>
<div>
<label className="block text-xs text-gray-400 mb-1">
Max Frequency: {params.maxFreq}Hz
</label>
<input
type="range"
min="1000"
max="20000"
step="500"
value={params.maxFreq}
onChange={e => updateParam('maxFreq', Number(e.target.value))}
className="w-full"
/>
</div>
{/* Custom Mode Only Parameters */}
{params.synthesisMode === 'custom' && (
<>
<div>
<label className="block text-xs text-gray-400 mb-1">
Amplitude Threshold: {params.amplitudeThreshold}
</label>
<input
type="range"
min="0.001"
max="0.1"
step="0.001"
value={params.amplitudeThreshold}
onChange={e => updateParam('amplitudeThreshold', Number(e.target.value))}
className="w-full"
/>
<p className="text-xs text-gray-500 mt-1">Minimum amplitude to include</p>
</div>
<div className="flex items-center justify-between">
<label className="text-xs text-gray-400">Perceptual RGB Weighting</label>
<div>
<label className="block text-xs text-gray-400 mb-1">Window Type</label>
<div className="grid grid-cols-4 gap-1">
<button
onClick={() => updateParam('windowType', 'rectangular')}
className={`px-2 py-1 text-xs border ${
params.windowType === 'rectangular'
? 'bg-white text-black border-white'
: 'bg-black text-white border-gray-600 hover:border-white'
}`}
>
Rect
</button>
<button
onClick={() => updateParam('windowType', 'hann')}
className={`px-2 py-1 text-xs border ${
params.windowType === 'hann'
? 'bg-white text-black border-white'
: 'bg-black text-white border-gray-600 hover:border-white'
}`}
>
Hann
</button>
<button
onClick={() => updateParam('windowType', 'hamming')}
className={`px-2 py-1 text-xs border ${
params.windowType === 'hamming'
? 'bg-white text-black border-white'
: 'bg-black text-white border-gray-600 hover:border-white'
}`}
>
Hamming
</button>
<button
onClick={() => updateParam('windowType', 'blackman')}
className={`px-2 py-1 text-xs border ${
params.windowType === 'blackman'
? 'bg-white text-black border-white'
: 'bg-black text-white border-gray-600 hover:border-white'
}`}
>
Blackman
</button>
</div>
<p className="text-xs text-gray-500 mt-1">
Reduces clicking/popping between time frames
</p>
</div>
</>
)}
{/* Color Inversion - Available for both modes */}
<div className="border-t border-gray-700 pt-4">
<div>
<label className="block text-xs text-gray-400 mb-1">Color</label>
<div className="grid grid-cols-2 gap-1">
<button
onClick={() => updateParam('usePerceptualWeighting', !params.usePerceptualWeighting)}
onClick={() => updateParam('invert', false)}
className={`px-2 py-1 text-xs border ${
params.usePerceptualWeighting !== false
!params.invert
? 'bg-white text-black border-white'
: 'bg-black text-white border-gray-600 hover:border-white'
}`}
>
{params.usePerceptualWeighting !== false ? 'ON' : 'OFF'}
Normal
</button>
<button
onClick={() => updateParam('invert', true)}
className={`px-2 py-1 text-xs border ${
params.invert
? 'bg-white text-black border-white'
: 'bg-black text-white border-gray-600 hover:border-white'
}`}
>
Inverted
</button>
</div>
<p className="text-xs text-gray-500 mt-1">Squared RGB sum for better brightness perception</p>
</>
)}
</div>
{/* Contrast - Available for both modes */}
<div>
<label className="block text-xs text-gray-400 mb-1">
Contrast: {(params.contrast || 2.2).toFixed(1)}
</label>
<input
type="range"
min="1"
max="5"
step="0.1"
value={params.contrast || 2.2}
onChange={e => updateParam('contrast', Number(e.target.value))}
className="w-full"
/>
<p className="text-xs text-gray-500 mt-1">Power curve for brightness perception</p>
</div>
{/* Custom Mode Only Parameters */}
{params.synthesisMode === 'custom' && (
<>
<div>
<label className="block text-xs text-gray-400 mb-1">Frequency Mapping</label>
<div className="grid grid-cols-4 gap-1">
<button
onClick={() => updateParam('frequencyMapping', 'mel')}
className={`px-2 py-1 text-xs border ${
(params.frequencyMapping || 'linear') === 'mel'
? 'bg-white text-black border-white'
: 'bg-black text-white border-gray-600 hover:border-white'
}`}
>
Mel
</button>
<button
onClick={() => updateParam('frequencyMapping', 'linear')}
className={`px-2 py-1 text-xs border ${
(params.frequencyMapping || 'linear') === 'linear'
? 'bg-white text-black border-white'
: 'bg-black text-white border-gray-600 hover:border-white'
}`}
>
Linear
</button>
<button
onClick={() => updateParam('frequencyMapping', 'bark')}
className={`px-2 py-1 text-xs border ${
params.frequencyMapping === 'bark'
? 'bg-white text-black border-white'
: 'bg-black text-white border-gray-600 hover:border-white'
}`}
>
Bark
</button>
<button
onClick={() => updateParam('frequencyMapping', 'log')}
className={`px-2 py-1 text-xs border ${
params.frequencyMapping === 'log'
? 'bg-white text-black border-white'
: 'bg-black text-white border-gray-600 hover:border-white'
}`}
>
Log
</button>
</div>
<p className="text-xs text-gray-500 mt-1">How image height maps to frequency</p>
</div>
<div>
<label className="block text-xs text-gray-400 mb-1">
Spectral Density: {params.spectralDensity || 3}
</label>
<input
type="range"
min="1"
max="7"
step="1"
value={params.spectralDensity || 3}
onChange={e => updateParam('spectralDensity', Number(e.target.value))}
className="w-full"
/>
<p className="text-xs text-gray-500 mt-1">
Tones per frequency peak (richer = higher)
</p>
</div>
<div className="flex items-center justify-between">
<label className="text-xs text-gray-400">Perceptual RGB Weighting</label>
<button
onClick={() =>
updateParam('usePerceptualWeighting', !params.usePerceptualWeighting)
}
className={`px-2 py-1 text-xs border ${
params.usePerceptualWeighting !== false
? 'bg-white text-black border-white'
: 'bg-black text-white border-gray-600 hover:border-white'
}`}
>
{params.usePerceptualWeighting !== false ? 'ON' : 'OFF'}
</button>
</div>
<p className="text-xs text-gray-500 mt-1">
Squared RGB sum for better brightness perception
</p>
</>
)}
</div>
</div>
</div>
</div>
</div>
{/* Audio Controls - Below Parameters */}
@ -603,4 +597,4 @@ export default function AudioPanel() {
</div>
</div>
)
}
}

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,22 +78,23 @@ export default function GeneratorSelector({ onGenerate, isGenerating }: Generato
</h1>
</div>
<div className="flex items-center justify-center h-10">
{settings.selectedGenerator !== 'from-photo' && settings.selectedGenerator !== 'webcam' && (
<button
onClick={handleGenerateClick}
disabled={isGenerating}
className="bg-white text-black px-4 py-2 font-medium hover:bg-gray-200 disabled:bg-gray-600 disabled:text-gray-400 disabled:cursor-not-allowed"
>
<div>{isGenerating ? 'Generating...' : 'Generate (G)'}</div>
</button>
)}
{settings.selectedGenerator !== 'from-photo' &&
settings.selectedGenerator !== 'webcam' && (
<button
onClick={handleGenerateClick}
disabled={isGenerating}
className="bg-white text-black px-4 py-2 font-medium hover:bg-gray-200 disabled:bg-gray-600 disabled:text-gray-400 disabled:cursor-not-allowed"
>
<div>{isGenerating ? 'Generating...' : 'Generate (G)'}</div>
</button>
)}
</div>
</div>
{/* Second container: 85% width - Generator modes in two lines */}
<div className="w-[85%] flex flex-col space-y-2">
<div className="flex items-center space-x-4 h-10">
{firstRowGenerators.map((generator) => (
{firstRowGenerators.map(generator => (
<Tooltip key={generator.id} content={generator.description}>
<button
onClick={() => handleGeneratorChange(generator.id)}
@ -112,7 +110,7 @@ export default function GeneratorSelector({ onGenerate, isGenerating }: Generato
))}
</div>
<div className="flex items-center space-x-4 h-10">
{secondRowGenerators.map((generator) => (
{secondRowGenerators.map(generator => (
<Tooltip key={generator.id} content={generator.description}>
<button
onClick={() => handleGeneratorChange(generator.id)}
@ -131,4 +129,4 @@ export default function GeneratorSelector({ onGenerate, isGenerating }: Generato
</div>
</div>
)
}
}

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">
@ -63,4 +63,4 @@ export default function HelpPopup({ isOpen, onClose }: HelpPopupProps) {
</div>
</div>
)
}
}

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"
@ -157,4 +154,4 @@ export default function ImageGrid() {
)}
</div>
)
}
}

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

View File

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

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,10 +25,12 @@ export default function Tooltip({ content, children, position = 'bottom' }: Tool
>
{children}
{isVisible && (
<div className={`absolute z-50 px-2 py-1 bg-black text-white text-sm whitespace-nowrap border border-white ${positionClasses[position]}`}>
<div
className={`absolute z-50 px-2 py-1 bg-black text-white text-sm whitespace-nowrap border border-white ${positionClasses[position]}`}
>
{content}
</div>
)}
</div>
)
}
}

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,37 +98,39 @@ export default function WebcamGrid({ size }: WebcamGridProps) {
}
}, [processor])
const handleCapture = useCallback(async (index: number) => {
if (!cameraInitialized) {
console.error('Camera not initialized')
return
}
const handleCapture = useCallback(
async (index: number) => {
if (!cameraInitialized) {
console.error('Camera not initialized')
return
}
setIsCapturing(true)
setCapturingIndex(index)
setIsCapturing(true)
setCapturingIndex(index)
try {
const capturedImage = await generateFromWebcamImage(processor, size)
try {
const capturedImage = await generateFromWebcamImage(processor, size)
// Create new images array with the captured image at the specified index
const newImages = [...images]
newImages[index] = capturedImage
// Create new images array with the captured image at the specified index
const newImages = [...images]
newImages[index] = capturedImage
// Filter out any null/undefined values and update the store
const filteredImages = newImages.filter(Boolean) as GeneratedImage[]
generatedImages.set(filteredImages)
// Filter out any null/undefined values and update the store
const filteredImages = newImages.filter(Boolean) as GeneratedImage[]
generatedImages.set(filteredImages)
// Auto-select the newly captured image
selectedImage.set(capturedImage)
// Auto-select the newly captured image
selectedImage.set(capturedImage)
console.log('Photo captured and added to grid at index:', index)
} catch (error) {
console.error('Error capturing photo:', error)
} finally {
setIsCapturing(false)
setCapturingIndex(null)
}
}, [images, size, cameraInitialized, processor])
} catch (error) {
console.error('Error capturing photo:', error)
} finally {
setIsCapturing(false)
setCapturingIndex(null)
}
},
[images, size, cameraInitialized, processor]
)
const handleSelect = useCallback((image: GeneratedImage) => {
selectedImage.set(image)
@ -163,9 +163,7 @@ export default function WebcamGrid({ size }: WebcamGridProps) {
</div>
)}
<div className="grid grid-cols-5 gap-2">
{gridSquares}
</div>
<div className="grid grid-cols-5 gap-2">{gridSquares}</div>
</div>
)
}
}

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)
@ -150,4 +177,4 @@ async function loadArtInstituteImage(size: number, index: number): Promise<Gener
console.error('Error creating Met Museum image:', error)
return null
}
}
}

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)
@ -113,4 +113,4 @@ export function generateBandsImages(count: number, size: number): GeneratedImage
}
return images
}
}

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)
@ -361,4 +413,4 @@ export function generateDustImages(count: number, size: number): GeneratedImage[
}
return images
}
}

View File

@ -1,2 +1,2 @@
export * from './types'
export * from './processor'
export * from './processor'

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
@ -96,7 +96,7 @@ function convertToGrayscale(ctx: CanvasRenderingContext2D, size: number, enhance
}
// Set R, G, B to the same grayscale value
data[i] = final // Red
data[i] = final // Red
data[i + 1] = final // Green
data[i + 2] = final // Blue
// Alpha (data[i + 3]) remains unchanged
@ -104,4 +104,4 @@ function convertToGrayscale(ctx: CanvasRenderingContext2D, size: number, enhance
// Put the processed data back
ctx.putImageData(imageData, 0, 0)
}
}

View File

@ -9,4 +9,4 @@ export interface PhotoResult {
imageData: ImageData
originalFile: File
config: PhotoProcessingConfig
}
}

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,10 +46,10 @@ export async function generateFromPhotoImage(file: File, size: number): Promise<
params: {
fileName: file.name,
error: error instanceof Error ? error.message : 'Unknown error',
size
}
size,
},
}
return fallbackImage
}
}
}

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')!
@ -106,7 +109,7 @@ function svgToCanvas(svgString: string, size: number): Promise<{ canvas: HTMLCan
final = Math.max(0, Math.min(255, final))
// Set R, G, B to the same enhanced grayscale value
data[i] = final // Red
data[i] = final // Red
data[i + 1] = final // Green
data[i + 2] = final // Blue
// Alpha (data[i + 3]) remains unchanged
@ -130,7 +133,10 @@ function svgToCanvas(svgString: string, size: number): Promise<{ canvas: HTMLCan
})
}
export async function generateGeopatternImages(count: number, size: number): Promise<GeneratedImage[]> {
export async function generateGeopatternImages(
count: number,
size: number
): Promise<GeneratedImage[]> {
const images: GeneratedImage[] = []
// Load the geopattern library dynamically
@ -150,7 +156,7 @@ export async function generateGeopatternImages(count: number, size: number): Pro
// Generate the geopattern
const pattern = geopatternLib.generate(seed, {
generator: patternType,
baseColor: baseColor
baseColor: baseColor,
})
// Get SVG string
@ -169,8 +175,8 @@ export async function generateGeopatternImages(count: number, size: number): Pro
seed,
baseColor,
originalSvg: svgString,
size
}
size,
},
}
images.push(image)
@ -196,8 +202,8 @@ export async function generateGeopatternImages(count: number, size: number): Pro
seed: 'error',
baseColor: '#000000',
size,
error: error instanceof Error ? error.message : 'Unknown error'
}
error: error instanceof Error ? error.message : 'Unknown error',
},
}
images.push(fallbackImage)
@ -205,4 +211,4 @@ export async function generateGeopatternImages(count: number, size: number): Pro
}
return images
}
}

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)
@ -150,4 +155,4 @@ export function generateHarmonicsImages(count: number, size: number): GeneratedI
}
return images
}
}

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)
@ -70,4 +70,4 @@ export function generatePartialsImages(count: number, size: number): GeneratedIm
}
return images
}
}

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)
@ -133,4 +136,4 @@ async function loadPicsumImage(size: number, index: number): Promise<GeneratedIm
console.error('Error creating Picsum image:', error)
return null
}
}
}

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)
@ -355,4 +385,4 @@ export function generateShapesImages(count: number, size: number): GeneratedImag
}
return images
}
}

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
@ -57,24 +70,28 @@ function generateStrokePath(startY: number, endY: number, width: number, strokeT
break
case 'bounce':
let bounceT = t
if (bounceT < 1/2.75) {
if (bounceT < 1 / 2.75) {
bounceT = 7.5625 * bounceT * bounceT
} else if (bounceT < 2/2.75) {
bounceT = 7.5625 * (bounceT - 1.5/2.75) * (bounceT - 1.5/2.75) + 0.75
} else if (bounceT < 2.5/2.75) {
bounceT = 7.5625 * (bounceT - 2.25/2.75) * (bounceT - 2.25/2.75) + 0.9375
} else if (bounceT < 2 / 2.75) {
bounceT = 7.5625 * (bounceT - 1.5 / 2.75) * (bounceT - 1.5 / 2.75) + 0.75
} else if (bounceT < 2.5 / 2.75) {
bounceT = 7.5625 * (bounceT - 2.25 / 2.75) * (bounceT - 2.25 / 2.75) + 0.9375
} else {
bounceT = 7.5625 * (bounceT - 2.625/2.75) * (bounceT - 2.625/2.75) + 0.984375
bounceT = 7.5625 * (bounceT - 2.625 / 2.75) * (bounceT - 2.625 / 2.75) + 0.984375
}
y = startY + (endY - startY) * bounceT
break
case 'elastic':
const elasticT = t === 0 ? 0 : t === 1 ? 1 :
Math.pow(2, -10 * t) * Math.sin((t * 10 - 0.75) * (2 * Math.PI) / 3) + 1
const elasticT =
t === 0
? 0
: t === 1
? 1
: Math.pow(2, -10 * t) * Math.sin(((t * 10 - 0.75) * (2 * Math.PI)) / 3) + 1
y = startY + (endY - startY) * elasticT
break
case 'zigzag':
const zigzagT = Math.abs((t * 6) % 2 - 1) // creates zigzag pattern
const zigzagT = Math.abs(((t * 6) % 2) - 1) // creates zigzag pattern
y = startY + (endY - startY) * zigzagT
break
default:
@ -107,7 +124,16 @@ export function generateSlidesImages(count: number, size: number): GeneratedImag
// Generate slides with more variety
const slides: (SlideLine & { color: string })[] = []
const strokeTypes: StrokeType[] = ['linear', 'logarithmic', 'exponential', 'cubic', 'sine', 'bounce', 'elastic', 'zigzag']
const strokeTypes: StrokeType[] = [
'linear',
'logarithmic',
'exponential',
'cubic',
'sine',
'bounce',
'elastic',
'zigzag',
]
for (let j = 0; j < lineCount; j++) {
// More varied positioning - some can go edge to edge
@ -153,11 +179,11 @@ export function generateSlidesImages(count: number, size: number): GeneratedImag
startY: s.startY,
endY: s.endY,
thickness: s.thickness,
strokeType: s.strokeType
strokeType: s.strokeType,
})),
colors: slides.map(s => s.color),
size
}
size,
},
}
images.push(image)
@ -167,4 +193,4 @@ export function generateSlidesImages(count: number, size: number): GeneratedImag
}
return images
}
}

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
@ -56,4 +57,4 @@ document.body.appendChild(result.canvas)
- `TixyRenderOptions` - Rendering configuration
- `TixyResult` - Canvas and ImageData result
Math functions like `sin`, `cos`, `sqrt` etc. are available without the `Math.` prefix.
Math functions like `sin`, `cos`, `sqrt` etc. are available without the `Math.` prefix.

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
}
@ -74,4 +102,4 @@ export function getExampleExpressions(): Record<string, string> {
}
// Export for backward compatibility
export const EXAMPLE_EXPRESSIONS = getExampleExpressions()
export const EXAMPLE_EXPRESSIONS = getExampleExpressions()

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

View File

@ -25,4 +25,4 @@ export interface TixyRenderOptions {
export interface TixyResult {
imageData: ImageData
canvas: HTMLCanvasElement
}
}

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

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)
@ -56,4 +56,4 @@ export function generateTixyImages(count: number, size: number): GeneratedImage[
}
return images
}
}

View File

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

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]
@ -114,4 +110,4 @@ export function applyTension(
}
return tensioned
}
}

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
} as const
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)
@ -102,4 +104,4 @@ export function generateVariedWaveforms(count: number, size: number = 256): Gene
}
return images
}
}

View File

@ -1,2 +1,2 @@
export * from './types'
export * from './processor'
export * from './processor'

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
@ -133,7 +137,7 @@ export class WebcamProcessor {
}
// Set R, G, B to the same grayscale value
data[i] = final // Red
data[i] = final // Red
data[i + 1] = final // Green
data[i + 2] = final // Blue
// Alpha (data[i + 3]) remains unchanged
@ -142,4 +146,4 @@ export class WebcamProcessor {
// Put the processed data back
ctx.putImageData(imageData, 0, 0)
}
}
}

View File

@ -9,4 +9,4 @@ export interface WebcamCaptureResult {
imageData: ImageData
capturedAt: Date
config: WebcamCaptureConfig
}
}

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,10 +45,10 @@ 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,16 +43,17 @@ downloadWAV(audioData, 44100, 'my-audio.wav')
### Types
#### `SynthesisParams`
```typescript
interface SynthesisParams {
duration: number // Audio duration in seconds
minFreq: number // Minimum frequency in Hz
maxFreq: number // Maximum frequency in Hz
sampleRate: number // Sample rate in Hz
duration: number // Audio duration in seconds
minFreq: number // Minimum frequency in Hz
maxFreq: number // Maximum frequency in Hz
sampleRate: number // Sample rate in Hz
frequencyResolution: number // Frequency bin downsampling
timeResolution: number // Time slice downsampling
timeResolution: number // Time slice downsampling
amplitudeThreshold: number // Minimum amplitude threshold
maxPartials: number // Maximum simultaneous partials
maxPartials: number // Maximum simultaneous partials
}
```
@ -80,6 +82,7 @@ spectral-synthesis/
## Usage Examples
### Basic Synthesis
```typescript
const canvas = document.createElement('canvas')
const ctx = canvas.getContext('2d')
@ -89,14 +92,15 @@ const audio = synthesizeFromImage(imageData)
```
### Advanced Usage
```typescript
import { ImageToAudioSynthesizer } from './spectral-synthesis'
const synthesizer = new ImageToAudioSynthesizer({
duration: 5,
maxPartials: 150
maxPartials: 150,
})
const result = synthesizer.synthesize(imageData)
console.log(`Generated ${result.duration}s of audio`)
```
```

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,11 +177,11 @@ export async function playAudio(audioData: Float32Array, sampleRate: number): Pr
const player = createAudioPlayer(audioData, sampleRate)
return new Promise(resolve => {
player.onStateChange((isPlaying) => {
player.onStateChange(isPlaying => {
if (!isPlaying) {
resolve()
}
})
player.play()
})
}
}

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 = []
@ -339,16 +346,16 @@ export class ImageToAudioSynthesizer {
const contrast = this.params.contrast || 1.0
// Fast power optimization for common cases
if (contrast === 1.0) {
amplitude = intensity // No contrast
amplitude = intensity // No contrast
} else if (contrast === 2.0) {
amplitude = intensity * intensity // Square is much faster than Math.pow
amplitude = intensity * intensity // Square is much faster than Math.pow
} else if (contrast === 0.5) {
amplitude = Math.sqrt(intensity) // Square root is faster than Math.pow
amplitude = Math.sqrt(intensity) // Square root is faster than Math.pow
} else if (contrast === 3.0) {
amplitude = intensity * intensity * intensity // Cube
amplitude = intensity * intensity * intensity // Cube
} else if (contrast === 4.0) {
const sq = intensity * intensity
amplitude = sq * sq // Fourth power
amplitude = sq * sq // Fourth power
} else {
// Fast power approximation for arbitrary values
// Uses bit manipulation + lookup for ~10x speedup over Math.pow
@ -380,8 +387,8 @@ export class ImageToAudioSynthesizer {
// Phase increment method - mathematically identical but much faster
// Eliminates array lookups and multiplications in tight loop
let phase = freqCoeff * startSample / sampleRate // Initial phase
const phaseIncrement = freqCoeff / sampleRate // Phase per sample
let phase = (freqCoeff * startSample) / sampleRate // Initial phase
const phaseIncrement = freqCoeff / sampleRate // Phase per sample
for (let i = 0; i < frameLength; i++) {
columnSpectrum[i] += amplitude * Math.sin(phase)
phase += phaseIncrement
@ -415,7 +422,7 @@ export class ImageToAudioSynthesizer {
return {
audio,
sampleRate,
duration
duration,
}
}
@ -470,7 +477,7 @@ export function createDirectParams(overrides: Partial<SynthesisParams> = {}): Sy
disableNormalization: false,
disableContrast: false,
exactBinMapping: false,
...overrides
...overrides,
}
}
@ -498,7 +505,7 @@ export function createCustomParams(overrides: Partial<SynthesisParams> = {}): Sy
disableNormalization: false,
disableContrast: false,
exactBinMapping: false,
...overrides
...overrides,
}
}
@ -524,4 +531,4 @@ export function synthesizeCustom(
const customParams = createCustomParams(params)
const synthesizer = new ImageToAudioSynthesizer(customParams)
return synthesizer.synthesize(imageData)
}
}

View File

@ -32,4 +32,4 @@ export interface SynthesisResult {
audio: Float32Array
sampleRate: number
duration: number
}
}

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 &&
current > smoothedSpectrum[i - 1] &&
current > smoothedSpectrum[i + 1] &&
current > smoothedSpectrum[i - 2] &&
current > smoothedSpectrum[i + 2]) {
if (
current > threshold &&
current > smoothedSpectrum[i - 1] &&
current > smoothedSpectrum[i + 1] &&
current > smoothedSpectrum[i - 2] &&
current > smoothedSpectrum[i + 2]
) {
// Find the exact peak position with sub-bin accuracy using parabolic interpolation
const y1 = smoothedSpectrum[i - 1]
const y2 = smoothedSpectrum[i]
@ -199,7 +213,11 @@ function smoothSpectrum(spectrum: number[], windowSize: number): number[] {
let sum = 0
let count = 0
for (let j = Math.max(0, i - halfWindow); j <= Math.min(spectrum.length - 1, i + halfWindow); j++) {
for (
let j = Math.max(0, i - halfWindow);
j <= Math.min(spectrum.length - 1, i + halfWindow);
j++
) {
sum += spectrum[j]
count++
}
@ -213,7 +231,11 @@ function smoothSpectrum(spectrum: number[], windowSize: number): number[] {
/**
* Apply perceptual amplitude weighting with contrast control
*/
export function perceptualAmplitudeWeighting(freq: number, amplitude: number, contrast: number = 2.2): number {
export function perceptualAmplitudeWeighting(
freq: number,
amplitude: number,
contrast: number = 2.2
): number {
// Apply contrast curve first (like LeviBorodenko's approach)
const contrastedAmplitude = Math.pow(amplitude, contrast)
@ -223,8 +245,6 @@ export function perceptualAmplitudeWeighting(freq: number, amplitude: number, co
return contrastedAmplitude * weight
}
/**
* Generate spectral density by creating multiple tones per frequency bin
* Inspired by LeviBorodenko's multi-tone approach
@ -239,13 +259,13 @@ export function generateSpectralDensity(
const toneSpacing = bandwidth / numTones
for (let i = 0; i < numTones; i++) {
const freq = centerFreq + (i - numTones/2) * toneSpacing
const toneAmplitude = amplitude * (1 - Math.abs(i - numTones/2) / numTones * 0.3) // Slight amplitude variation
const freq = centerFreq + (i - numTones / 2) * toneSpacing
const toneAmplitude = amplitude * (1 - (Math.abs(i - numTones / 2) / numTones) * 0.3) // Slight amplitude variation
peaks.push({
frequency: freq,
amplitude: toneAmplitude,
phase: 0
phase: 0,
})
}
@ -328,7 +348,8 @@ export function analyzeImageBrightness(imageData: ImageData): {
const avgEdgeBrightness = edgePixels > 0 ? edgeBrightness / edgePixels : meanBrightness
// Calculate contrast (standard deviation)
const variance = brightnesses.reduce((sum, b) => sum + Math.pow(b - meanBrightness, 2), 0) / brightnesses.length
const variance =
brightnesses.reduce((sum, b) => sum + Math.pow(b - meanBrightness, 2), 0) / brightnesses.length
const contrast = Math.sqrt(variance)
// Make recommendation
@ -346,7 +367,7 @@ export function analyzeImageBrightness(imageData: ImageData): {
medianBrightness,
edgeBrightness: avgEdgeBrightness,
contrast,
recommendation
recommendation,
}
}
@ -366,19 +387,19 @@ export function generateWindow(length: number, windowType: string): Float32Array
switch (windowType) {
case 'hann':
for (let i = 0; i < length; i++) {
window[i] = 0.5 * (1 - Math.cos(2 * Math.PI * i / (length - 1)))
window[i] = 0.5 * (1 - Math.cos((2 * Math.PI * i) / (length - 1)))
}
break
case 'hamming':
for (let i = 0; i < length; i++) {
window[i] = 0.54 - 0.46 * Math.cos(2 * Math.PI * i / (length - 1))
window[i] = 0.54 - 0.46 * Math.cos((2 * Math.PI * i) / (length - 1))
}
break
case 'blackman':
for (let i = 0; i < length; i++) {
const factor = 2 * Math.PI * i / (length - 1)
const factor = (2 * Math.PI * i) / (length - 1)
window[i] = 0.42 - 0.5 * Math.cos(factor) + 0.08 * Math.cos(2 * factor)
}
break
@ -450,7 +471,12 @@ export function extractSpectrum(
/**
* Alternative linear frequency mapping inspired by alexadam's approach
*/
export function mapFrequencyLinear(row: number, totalRows: number, minFreq: number, maxFreq: number): number {
export function mapFrequencyLinear(
row: number,
totalRows: number,
minFreq: number,
maxFreq: number
): number {
// Direct linear mapping from top to bottom (high freq at top)
const normalizedRow = row / (totalRows - 1)
return maxFreq - normalizedRow * (maxFreq - minFreq)
@ -481,4 +507,4 @@ export function normalizeAudioGlobal(audio: Float32Array, targetLevel: number =
}
return normalized
}
}

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 type { AudioPlayer } 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,11 +1,8 @@
/** @type {import('tailwindcss').Config} */
export default {
content: [
"./index.html",
"./src/**/*.{js,ts,jsx,tsx}",
],
content: ['./index.html', './src/**/*.{js,ts,jsx,tsx}'],
theme: {
extend: {},
},
plugins: [],
}
}

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'],
},
})