diff --git a/src/App.tsx b/src/App.tsx index be07130f..42aededb 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -3,20 +3,22 @@ import { useStore } from '@nanostores/react' import { Square, Archive, Dices } from 'lucide-react' import { PlaybackManager } from './services/PlaybackManager' import { DownloadService } from './services/DownloadService' -import { generateFormulaGrid, generateRandomFormula } from './utils/bytebeatFormulas' +import { generateTileGrid, generateRandomFormula } from './utils/bytebeatFormulas' import { BytebeatTile } from './components/BytebeatTile' import { EffectsBar } from './components/EffectsBar' import { EngineControls } from './components/EngineControls' import { getSampleRateFromIndex } from './config/effects' import { engineSettings, effectSettings } from './stores/settings' import { useKeyboardShortcuts } from './hooks/useKeyboardShortcuts' +import type { TileState } from './types/tiles' +import { createTileStateFromCurrent, loadTileParams, saveTileParams } from './utils/tileState' function App() { const engineValues = useStore(engineSettings) const effectValues = useStore(effectSettings) - const [formulas, setFormulas] = useState(() => - generateFormulaGrid(100, 2, engineValues.complexity) + const [tiles, setTiles] = useState(() => + generateTileGrid(100, 2, engineValues.complexity) ) const [playing, setPlaying] = useState(null) const [queued, setQueued] = useState(null) @@ -27,18 +29,18 @@ function App() { const playbackManagerRef = useRef(null) const downloadServiceRef = useRef(new DownloadService()) const animationFrameRef = useRef(null) - const formulasRef = useRef(formulas) + const tilesRef = useRef(tiles) useEffect(() => { - formulasRef.current = formulas - }, [formulas]) + tilesRef.current = tiles + }, [tiles]) useEffect(() => { effectSettings.setKey('masterVolume', engineValues.masterVolume) }, [engineValues.masterVolume]) const handleRandom = () => { - setFormulas(generateFormulaGrid(100, 2, engineValues.complexity)) + setTiles(generateTileGrid(100, 2, engineValues.complexity)) setQueued(null) } @@ -82,8 +84,20 @@ function App() { updatePosition() } - const handleTileClick = (formula: string, row: number, col: number, isDoubleClick: boolean = false) => { + const handleTileClick = (_formula: string, row: number, col: number, isDoubleClick: boolean = false) => { const id = `${row}-${col}` + const tile = tiles[row]?.[col] + + if (!tile) return + + if (focusedTile.row !== row || focusedTile.col !== col) { + const currentTile = tiles[focusedTile.row]?.[focusedTile.col] + if (currentTile) { + saveTileParams(currentTile) + } + loadTileParams(tile) + } + setFocusedTile({ row, col }) if (playing === id) { @@ -98,14 +112,15 @@ function App() { } if (isDoubleClick || playing === null) { - playFormula(formula, id) + playFormula(tile.formula, id) } else { setQueued(id) if (playbackManagerRef.current) { playbackManagerRef.current.scheduleNextTrack(() => { - const queuedFormula = formulasRef.current.flat()[parseInt(id.split('-')[0]) * 2 + parseInt(id.split('-')[1])] - if (queuedFormula) { - playFormula(queuedFormula, id) + const queuedTile = tilesRef.current[row]?.[col] + if (queuedTile) { + loadTileParams(queuedTile) + playFormula(queuedTile.formula, id) } }) } @@ -120,6 +135,11 @@ function App() { const handleEngineChange = (parameterId: string, value: number) => { engineSettings.setKey(parameterId as keyof typeof engineValues, value) + const currentTile = tiles[focusedTile.row]?.[focusedTile.col] + if (currentTile) { + saveTileParams(currentTile) + } + if (parameterId === 'masterVolume' && playbackManagerRef.current) { playbackManagerRef.current.setEffects({ ...effectValues, masterVolume: value }) } @@ -131,6 +151,12 @@ function App() { const handleEffectChange = (parameterId: string, value: number | boolean | string) => { effectSettings.setKey(parameterId as any, value as any) + + const currentTile = tiles[focusedTile.row]?.[focusedTile.col] + if (currentTile) { + saveTileParams(currentTile) + } + if (playbackManagerRef.current) { playbackManagerRef.current.setEffects(effectValues) } @@ -138,6 +164,7 @@ function App() { const handleDownloadAll = async () => { setDownloading(true) + const formulas = tiles.map(row => row.map(tile => tile.formula)) await downloadServiceRef.current.downloadAll(formulas, { duration: 10, bitDepth: 8 }) setDownloading(false) } @@ -149,25 +176,27 @@ function App() { const handleRegenerate = (row: number, col: number) => { const id = `${row}-${col}` const newFormula = generateRandomFormula(engineValues.complexity) + const newTile = createTileStateFromCurrent(newFormula) if (playing === id && playbackManagerRef.current) { setRegenerating(id) playbackManagerRef.current.scheduleNextTrack(() => { - setFormulas(prevFormulas => { - const newFormulas = [...prevFormulas] - newFormulas[row] = [...newFormulas[row]] - newFormulas[row][col] = newFormula - return newFormulas + setTiles(prevTiles => { + const newTiles = [...prevTiles] + newTiles[row] = [...newTiles[row]] + newTiles[row][col] = newTile + return newTiles }) - playFormula(newFormula, id) + loadTileParams(newTile) + playFormula(newTile.formula, id) setRegenerating(null) }) } else { - setFormulas(prevFormulas => { - const newFormulas = [...prevFormulas] - newFormulas[row] = [...newFormulas[row]] - newFormulas[row][col] = newFormula - return newFormulas + setTiles(prevTiles => { + const newTiles = [...prevTiles] + newTiles[row] = [...newTiles[row]] + newTiles[row][col] = newTile + return newTiles }) } } @@ -183,10 +212,15 @@ function App() { } const moveFocus = (direction: 'up' | 'down' | 'left' | 'right', step: number = 1) => { + const currentTile = tiles[focusedTile.row]?.[focusedTile.col] + if (currentTile) { + saveTileParams(currentTile) + } + setFocusedTile(prev => { let { row, col } = prev - const maxRow = formulas.length - 1 - const maxCol = (formulas[row]?.length || 1) - 1 + const maxRow = tiles.length - 1 + const maxCol = (tiles[row]?.length || 1) - 1 switch (direction) { case 'up': @@ -202,6 +236,12 @@ function App() { col = Math.min(maxCol, col + step) break } + + const newTile = tiles[row]?.[col] + if (newTile) { + loadTileParams(newTile) + } + return { row, col } }) } @@ -210,24 +250,24 @@ function App() { if (playing) { handleStop() } else { - const formula = formulas[focusedTile.row]?.[focusedTile.col] - if (formula) { - handleTileClick(formula, focusedTile.row, focusedTile.col, true) + const tile = tiles[focusedTile.row]?.[focusedTile.col] + if (tile) { + handleTileClick(tile.formula, focusedTile.row, focusedTile.col, true) } } } const handleKeyboardEnter = () => { - const formula = formulas[focusedTile.row]?.[focusedTile.col] - if (formula) { - handleTileClick(formula, focusedTile.row, focusedTile.col, false) + const tile = tiles[focusedTile.row]?.[focusedTile.col] + if (tile) { + handleTileClick(tile.formula, focusedTile.row, focusedTile.col, false) } } const handleKeyboardDoubleEnter = () => { - const formula = formulas[focusedTile.row]?.[focusedTile.col] - if (formula) { - handleTileClick(formula, focusedTile.row, focusedTile.col, true) + const tile = tiles[focusedTile.row]?.[focusedTile.col] + if (tile) { + handleTileClick(tile.formula, focusedTile.row, focusedTile.col, true) } } @@ -293,13 +333,13 @@ function App() {
- {formulas.map((row, i) => - row.map((formula, j) => { + {tiles.map((row, i) => + row.map((tile, j) => { const id = `${i}-${j}` return ( { + effect.parameters.forEach(param => { + if (param.id.endsWith('Enable')) return + + if (param.options) { + const randomOption = param.options[Math.floor(Math.random() * param.options.length)] + onChange(param.id, randomOption.value) + } else { + const range = param.max - param.min + const steps = Math.floor(range / param.step) + const randomStep = Math.floor(Math.random() * (steps + 1)) + const randomValue = param.min + (randomStep * param.step) + onChange(param.id, randomValue) + } + }) + } + const renderFilterEffect = (effect: typeof EFFECTS[number]) => { const filterGroups = [ { prefix: 'hp', label: 'HP' }, @@ -19,9 +37,17 @@ export function EffectsBar({ values, onChange }: EffectsBarProps) { return (
-

- {effect.name.toUpperCase()} -

+
+

+ {effect.name.toUpperCase()} +

+ +
{filterGroups.map(group => { const enableParam = effect.parameters.find(p => p.id === `${group.prefix}Enable`) @@ -32,11 +58,14 @@ export function EffectsBar({ values, onChange }: EffectsBarProps) { return (
- onChange(enableParam.id, checked ? 1 : 0)} - vertical - /> +
-

- {effect.name.toUpperCase()} -

+
+

+ {effect.name.toUpperCase()} +

+ +
{effect.bypassable && ( + effectParams: Record +} diff --git a/src/utils/bytebeatFormulas.ts b/src/utils/bytebeatFormulas.ts index b200c9f0..80f10413 100644 --- a/src/utils/bytebeatFormulas.ts +++ b/src/utils/bytebeatFormulas.ts @@ -1,3 +1,6 @@ +import type { TileState } from '../types/tiles' +import { createTileState } from './tileState' + interface Template { pattern: string weight: number @@ -216,4 +219,17 @@ export function generateFormulaGrid(rows: number, cols: number, complexity: numb grid.push(row) } return grid +} + +export function generateTileGrid(rows: number, cols: number, complexity: number = 1): TileState[][] { + const grid: TileState[][] = [] + for (let i = 0; i < rows; i++) { + const row: TileState[] = [] + for (let j = 0; j < cols; j++) { + const formula = generateRandomFormula(complexity) + row.push(createTileState(formula)) + } + grid.push(row) + } + return grid } \ No newline at end of file diff --git a/src/utils/tileState.ts b/src/utils/tileState.ts new file mode 100644 index 00000000..106c4d81 --- /dev/null +++ b/src/utils/tileState.ts @@ -0,0 +1,46 @@ +import type { TileState } from '../types/tiles' +import { engineSettings, effectSettings } from '../stores/settings' +import { getDefaultEngineValues, getDefaultEffectValues } from '../config/effects' + +export function createTileState( + formula: string, + engineParams?: Record, + effectParams?: Record +): TileState { + return { + formula, + engineParams: engineParams ?? { ...getDefaultEngineValues() }, + effectParams: effectParams ?? { ...getDefaultEffectValues(), masterVolume: 75 } + } +} + +export function createTileStateFromCurrent(formula: string): TileState { + return { + formula, + engineParams: { ...engineSettings.get() }, + effectParams: { ...effectSettings.get() } + } +} + +export function loadTileParams(tile: TileState): void { + Object.entries(tile.engineParams).forEach(([key, value]) => { + engineSettings.setKey(key as any, value) + }) + + Object.entries(tile.effectParams).forEach(([key, value]) => { + effectSettings.setKey(key as any, value as any) + }) +} + +export function saveTileParams(tile: TileState): void { + tile.engineParams = { ...engineSettings.get() } + tile.effectParams = { ...effectSettings.get() } +} + +export function cloneTileState(tile: TileState): TileState { + return { + formula: tile.formula, + engineParams: { ...tile.engineParams }, + effectParams: { ...tile.effectParams } + } +}