progress on responsive

This commit is contained in:
2025-10-06 10:56:46 +02:00
parent ef50cc9918
commit a4a26333b3
8 changed files with 261 additions and 108 deletions

View File

@ -2,7 +2,7 @@
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
<meta name="description" content="Bytebeat playground" />
<meta name="author" content="Raphaël Forment" />
<link rel="icon" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'><text x='16' y='22' text-anchor='middle' font-size='20' font-family='monospace' font-weight='bold'>&amp;</text></svg>" />

View File

@ -541,50 +541,82 @@ function App() {
<div className="w-screen h-screen flex flex-col bg-black overflow-hidden">
{showWarning && <AudioContextWarning onDismiss={handleDismissWarning} />}
{showHelp && <HelpModal onClose={() => setShowHelp(false)} />}
<header className="bg-black border-b-2 border-white px-6 py-3">
<div className="flex items-center justify-between gap-6">
<header className="bg-black border-b-2 border-white px-2 md:px-6 py-2 md:py-3">
<div className="flex flex-col md:flex-row items-start md:items-center gap-2 md:gap-6">
<div className="flex items-center justify-between w-full md:w-auto gap-2">
<h1
onClick={() => setShowHelp(true)}
className="font-mono text-sm tracking-[0.3em] text-white flex-shrink-0 cursor-pointer hover:opacity-70 transition-opacity"
className="font-mono text-[10px] md:text-sm tracking-[0.3em] text-white flex-shrink-0 cursor-pointer hover:opacity-70 transition-opacity"
>
BRUITISTE
</h1>
<div className="flex gap-2 md:hidden">
<button
onClick={handleStop}
disabled={!playing}
className="px-2 py-1 bg-black text-white border-2 border-white font-mono text-[9px] tracking-[0.2em] hover:bg-white hover:text-black transition-all disabled:opacity-30 disabled:cursor-not-allowed"
>
<Square size={12} strokeWidth={2} fill="currentColor" />
</button>
<button
onClick={handleRandom}
className="px-2 py-1 bg-white text-black font-mono text-[9px] tracking-[0.2em] hover:bg-black hover:text-white border-2 border-white transition-all"
>
<Dices size={12} strokeWidth={2} />
</button>
<button
onClick={handleRandomizeAllParams}
className="px-2 py-1 bg-white text-black font-mono text-[9px] tracking-[0.2em] hover:bg-black hover:text-white border-2 border-white transition-all"
>
<Sparkles size={12} strokeWidth={2} />
</button>
<button
onClick={handleDownloadAll}
disabled={downloading}
className="px-2 py-1 bg-black text-white border-2 border-white font-mono text-[9px] tracking-[0.2em] hover:bg-white hover:text-black transition-all disabled:opacity-30 disabled:cursor-not-allowed"
>
<Archive size={12} strokeWidth={2} />
</button>
</div>
</div>
<div className="w-full md:flex-1">
<EngineControls
values={engineValues}
onChange={handleEngineChange}
onMapClick={handleParameterMapClick}
getMappedLFOs={getMappedLFOs}
/>
<div className="flex gap-4 flex-shrink-0">
</div>
<div className="hidden md:flex gap-2 md:gap-4 flex-shrink-0">
<button
onClick={handleStop}
disabled={!playing}
className="px-6 py-2 bg-black text-white border-2 border-white font-mono text-[11px] tracking-[0.2em] hover:bg-white hover:text-black transition-all disabled:opacity-30 disabled:cursor-not-allowed flex items-center gap-2"
className="px-2 md:px-6 py-1 md:py-2 bg-black text-white border-2 border-white font-mono text-[9px] md:text-[11px] tracking-[0.2em] hover:bg-white hover:text-black transition-all disabled:opacity-30 disabled:cursor-not-allowed flex items-center gap-1 md:gap-2"
>
<Square size={14} strokeWidth={2} fill="currentColor" />
STOP
<Square size={12} strokeWidth={2} fill="currentColor" className="md:w-[14px] md:h-[14px]" />
<span className="hidden sm:inline">STOP</span>
</button>
<button
onClick={handleRandom}
className="px-6 py-2 bg-white text-black font-mono text-[11px] tracking-[0.2em] hover:bg-black hover:text-white border-2 border-white transition-all flex items-center gap-2"
className="px-2 md:px-6 py-1 md:py-2 bg-white text-black font-mono text-[9px] md:text-[11px] tracking-[0.2em] hover:bg-black hover:text-white border-2 border-white transition-all flex items-center gap-1 md:gap-2"
>
<Dices size={14} strokeWidth={2} />
RANDOM
<Dices size={12} strokeWidth={2} className="md:w-[14px] md:h-[14px]" />
<span className="hidden sm:inline">RANDOM</span>
</button>
<button
onClick={handleRandomizeAllParams}
className="px-6 py-2 bg-white text-black font-mono text-[11px] tracking-[0.2em] hover:bg-black hover:text-white border-2 border-white transition-all flex items-center gap-2"
className="px-2 md:px-6 py-1 md:py-2 bg-white text-black font-mono text-[9px] md:text-[11px] tracking-[0.2em] hover:bg-black hover:text-white border-2 border-white transition-all flex items-center gap-1 md:gap-2"
>
<Sparkles size={14} strokeWidth={2} />
CHAOS
<Sparkles size={12} strokeWidth={2} className="md:w-[14px] md:h-[14px]" />
<span className="hidden sm:inline">CHAOS</span>
</button>
<button
onClick={handleDownloadAll}
disabled={downloading}
className="px-6 py-2 bg-black text-white border-2 border-white font-mono text-[11px] tracking-[0.2em] hover:bg-white hover:text-black transition-all disabled:opacity-30 disabled:cursor-not-allowed flex items-center gap-2"
className="px-2 md:px-6 py-1 md:py-2 bg-black text-white border-2 border-white font-mono text-[9px] md:text-[11px] tracking-[0.2em] hover:bg-white hover:text-black transition-all disabled:opacity-30 disabled:cursor-not-allowed flex items-center gap-1 md:gap-2"
>
<Archive size={14} strokeWidth={2} />
{downloading ? 'DOWNLOADING...' : 'PACK'}
<Archive size={12} strokeWidth={2} className="md:w-[14px] md:h-[14px]" />
<span className="hidden sm:inline">{downloading ? 'DOWNLOADING...' : 'PACK'}</span>
</button>
</div>
</div>
@ -593,8 +625,8 @@ function App() {
<LFOPanel onChange={handleLFOChange} onUpdateDepth={handleUpdateMappingDepth} onRemoveMapping={handleRemoveMapping} />
<div className="flex-1 flex flex-col overflow-auto bg-white">
<div className="grid grid-cols-4 gap-[1px] bg-white p-[1px]">
<div className="col-span-4">
<div className="grid grid-cols-1 md:grid-cols-4 gap-[1px] bg-white p-[1px]">
<div className="col-span-1 md:col-span-4">
<FormulaEditor
formula={customTile.formula}
isPlaying={playing === PLAYBACK_ID.CUSTOM}
@ -607,7 +639,7 @@ function App() {
</div>
</div>
<div className="flex-1 grid grid-cols-4 auto-rows-min gap-[1px] bg-white p-[1px]">
<div className="flex-1 grid grid-cols-1 md:grid-cols-4 auto-rows-min gap-[1px] bg-white p-[1px]">
{tiles.map((row, i) =>
row.map((tile, j) => {
const id = getTileId(i, j)

View File

@ -1,4 +1,5 @@
import { Dices } from 'lucide-react'
import { useState } from 'react'
import { Dices, ChevronDown } from 'lucide-react'
import { Slider } from './Slider'
import { Switch } from './Switch'
import { Dropdown } from './Dropdown'
@ -13,6 +14,8 @@ interface EffectsBarProps {
}
export function EffectsBar({ values, onChange, onMapClick, getMappedLFOs }: EffectsBarProps) {
const [expandedEffect, setExpandedEffect] = useState<string | null>(null)
const [activeTab, setActiveTab] = useState<string>(EFFECTS[0].id)
const randomizeEffect = (effect: typeof EFFECTS[number]) => {
effect.parameters.forEach(param => {
if (param.id.endsWith('Enable')) return
@ -31,8 +34,9 @@ export function EffectsBar({ values, onChange, onMapClick, getMappedLFOs }: Effe
}
return (
<div className="bg-black border-t-2 border-white px-6 py-4">
<div className="grid grid-cols-4 gap-4">
<div className="bg-black border-t-2 border-white px-2 md:px-6 py-3 md:py-4">
{/* Desktop: Grid layout */}
<div className="hidden md:grid md:grid-cols-4 md:gap-4">
{EFFECTS.map(effect => {
return (
<div key={effect.id} className="border-2 border-white p-3">
@ -111,6 +115,100 @@ export function EffectsBar({ values, onChange, onMapClick, getMappedLFOs }: Effe
)
})}
</div>
{/* Mobile: Tabbed layout */}
<div className="md:hidden flex flex-col">
<div className="flex border-2 border-white">
{EFFECTS.map(effect => (
<button
key={effect.id}
onClick={() => setActiveTab(effect.id)}
className={`flex-1 p-2 font-mono text-[9px] tracking-[0.15em] transition-colors ${
activeTab === effect.id
? 'bg-white text-black'
: 'bg-black text-white hover:bg-white/10'
}`}
>
{effect.name.toUpperCase()}
</button>
))}
</div>
{EFFECTS.map(effect => {
if (activeTab !== effect.id) return null
return (
<div key={effect.id} className="border-2 border-t-0 border-white p-3">
<div className="flex items-center justify-between mb-3">
<div className="flex items-center gap-2">
<button
onClick={() => randomizeEffect(effect)}
className="p-1 text-white hover:bg-white hover:text-black transition-colors"
>
<Dices size={14} strokeWidth={2} />
</button>
</div>
{effect.bypassable && (
<Switch
checked={!values[`${effect.id}Bypass`]}
onChange={(checked) => onChange(`${effect.id}Bypass`, !checked)}
label={values[`${effect.id}Bypass`] ? 'OFF' : 'ON'}
/>
)}
</div>
<div className="flex flex-col gap-3">
{effect.parameters.map(param => {
if (param.options) {
return (
<Dropdown
key={param.id}
label={param.label}
value={values[param.id] as string ?? param.default as string}
options={param.options}
onChange={(value) => onChange(param.id, value)}
/>
)
}
const isSwitch = param.min === 0 && param.max === 1 && param.step === 1
if (isSwitch) {
return (
<div key={param.id} className="flex flex-col gap-1 mt-2">
<div className="flex items-center justify-between">
<span className="font-mono text-[9px] tracking-[0.15em] text-white">
{param.label.toUpperCase()}
</span>
<Switch
checked={Boolean(values[param.id])}
onChange={(checked) => onChange(param.id, checked ? 1 : 0)}
label={values[param.id] ? 'ON' : 'OFF'}
/>
</div>
</div>
)
}
return (
<Slider
key={param.id}
label={param.label}
value={values[param.id] as number ?? param.default as number}
min={param.min}
max={param.max}
step={param.step}
unit={param.unit}
onChange={(value) => onChange(param.id, value)}
valueId={param.id}
paramId={param.id}
onMapClick={onMapClick}
mappedLFOs={getMappedLFOs ? getMappedLFOs(param.id) : []}
/>
)
})}
</div>
</div>
)
})}
</div>
</div>
)
}

View File

@ -29,7 +29,7 @@ export function EngineControls({ values, onChange, onMapClick, getMappedLFOs }:
}
return (
<div className="flex items-center gap-6">
<div className="flex flex-wrap items-center gap-2 md:gap-4 xl:gap-6">
{ENGINE_CONTROLS[0].parameters.map(param => {
const useKnob = KNOB_PARAMS.includes(param.id)
@ -46,6 +46,7 @@ export function EngineControls({ values, onChange, onMapClick, getMappedLFOs }:
onChange={(value) => onChange(param.id, value)}
formatValue={formatValue}
valueId={param.id}
size={40}
paramId={param.id}
onMapClick={onMapClick}
mappedLFOs={getMappedLFOs ? getMappedLFOs(param.id) : []}
@ -54,12 +55,12 @@ export function EngineControls({ values, onChange, onMapClick, getMappedLFOs }:
}
return (
<div key={param.id} className="flex flex-col gap-1 min-w-[100px]">
<div className="flex justify-between items-baseline">
<label className="font-mono text-[9px] tracking-[0.15em] text-white">
<div key={param.id} className="hidden md:flex flex-col gap-1 min-w-[90px] xl:min-w-[100px]">
<div className="flex justify-between items-baseline gap-1">
<label className="font-mono text-[9px] tracking-[0.15em] text-white truncate">
{param.label.toUpperCase()}
</label>
<span className="font-mono text-[9px] text-white">
<span className="font-mono text-[9px] text-white whitespace-nowrap">
{formatValue(param.id, (values[param.id] as number) ?? param.default)}
</span>
</div>

View File

@ -6,7 +6,7 @@ interface HelpModalProps {
export function HelpModal({ onClose, showStartButton = false }: HelpModalProps) {
return (
<div
className="fixed inset-0 bg-black/70 flex items-center justify-center z-50"
className="fixed inset-0 bg-black/70 flex items-center justify-center z-50 p-4 overflow-y-auto"
onClick={onClose}
onKeyDown={(e) => {
if (e.key === 'Escape' || e.key === 'Enter' || e.key === ' ') {
@ -16,85 +16,87 @@ export function HelpModal({ onClose, showStartButton = false }: HelpModalProps)
tabIndex={0}
>
<div
className="border-4 border-white bg-black p-12 max-w-4xl w-full mx-8"
className="border-2 md:border-4 border-white bg-black p-4 md:p-8 lg:p-12 max-w-4xl w-full my-auto"
onClick={(e) => e.stopPropagation()}
>
<h1 className="font-mono text-2xl tracking-[0.3em] text-white mb-6 text-center">
<h1 className="font-mono text-lg md:text-2xl tracking-[0.3em] text-white mb-4 md:mb-6 text-center">
BRUITISTE
</h1>
<p className="font-mono text-sm text-white mb-2 leading-relaxed text-center">
<p className="font-mono text-xs md:text-sm text-white mb-2 leading-relaxed text-center">
Harsh noise soundbox made as a love statement to all weird noises, hums, audio glitches and ominous textures. Be careful, lower your volume! Tweak some parameters!
</p>
<p className="font-mono text-xs text-white mb-6 opacity-70 text-center">
<p className="font-mono text-[10px] md:text-xs text-white mb-4 md:mb-6 opacity-70 text-center">
Made by Raphaël Forment (BuboBubo) <a href="https://raphaelforment.fr" target="_blank" rel="noopener noreferrer" className="underline hover:opacity-100">raphaelforment.fr</a>
</p>
<div className="font-mono text-sm text-white mb-8">
<h2 className="text-lg tracking-[0.2em] mb-4">KEYBOARD SHORTCUTS</h2>
<div className="font-mono text-xs md:text-sm text-white mb-6 md:mb-8">
<h2 className="text-sm md:text-lg tracking-[0.2em] mb-3 md:mb-4">KEYBOARD SHORTCUTS</h2>
<div className="overflow-x-auto">
<table className="w-full border-2 border-white">
<thead>
<tr className="border-b-2 border-white">
<th className="text-left p-3 bg-white text-black tracking-[0.1em]">KEY</th>
<th className="text-left p-3 bg-white text-black tracking-[0.1em]">ACTION</th>
<th className="text-left p-2 md:p-3 bg-white text-black tracking-[0.1em] text-[10px] md:text-sm">KEY</th>
<th className="text-left p-2 md:p-3 bg-white text-black tracking-[0.1em] text-[10px] md:text-sm">ACTION</th>
</tr>
</thead>
<tbody>
<tr className="border-b border-white">
<td className="p-3 border-r border-white">SPACE</td>
<td className="p-3">Play/Stop current tile</td>
<td className="p-2 md:p-3 border-r border-white">SPACE</td>
<td className="p-2 md:p-3">Play/Stop current tile</td>
</tr>
<tr className="border-b border-white">
<td className="p-3 border-r border-white">ARROWS</td>
<td className="p-3">Navigate tiles</td>
<td className="p-2 md:p-3 border-r border-white">ARROWS</td>
<td className="p-2 md:p-3">Navigate tiles</td>
</tr>
<tr className="border-b border-white">
<td className="p-3 border-r border-white">SHIFT + ARROWS</td>
<td className="p-3">Jump 10 tiles</td>
<td className="p-2 md:p-3 border-r border-white">SHIFT + ARROWS</td>
<td className="p-2 md:p-3">Jump 10 tiles</td>
</tr>
<tr className="border-b border-white">
<td className="p-3 border-r border-white">ENTER</td>
<td className="p-3">Queue tile (play after current)</td>
<td className="p-2 md:p-3 border-r border-white">ENTER</td>
<td className="p-2 md:p-3">Queue tile (play after current)</td>
</tr>
<tr className="border-b border-white">
<td className="p-3 border-r border-white">DOUBLE ENTER</td>
<td className="p-3">Play immediately</td>
<td className="p-2 md:p-3 border-r border-white">DOUBLE ENTER</td>
<td className="p-2 md:p-3">Play immediately</td>
</tr>
<tr className="border-b border-white">
<td className="p-3 border-r border-white">R</td>
<td className="p-3">Regenerate current tile</td>
<td className="p-2 md:p-3 border-r border-white">R</td>
<td className="p-2 md:p-3">Regenerate current tile</td>
</tr>
<tr className="border-b border-white">
<td className="p-3 border-r border-white">SHIFT + R</td>
<td className="p-3">Randomize all tiles</td>
<td className="p-2 md:p-3 border-r border-white">SHIFT + R</td>
<td className="p-2 md:p-3">Randomize all tiles</td>
</tr>
<tr className="border-b border-white">
<td className="p-3 border-r border-white">C</td>
<td className="p-3">Randomize current tile params</td>
<td className="p-2 md:p-3 border-r border-white">C</td>
<td className="p-2 md:p-3">Randomize current tile params</td>
</tr>
<tr className="border-b border-white">
<td className="p-3 border-r border-white">SHIFT + C</td>
<td className="p-3">Randomize all params (CHAOS)</td>
<td className="p-2 md:p-3 border-r border-white">SHIFT + C</td>
<td className="p-2 md:p-3">Randomize all params (CHAOS)</td>
</tr>
<tr>
<td className="p-3 border-r border-white">ESC</td>
<td className="p-3">Exit mapping mode</td>
<td className="p-2 md:p-3 border-r border-white">ESC</td>
<td className="p-2 md:p-3">Exit mapping mode</td>
</tr>
</tbody>
</table>
</div>
</div>
{showStartButton ? (
<button
onClick={onClose}
className="w-full px-8 py-4 bg-white text-black font-mono text-sm tracking-[0.2em] hover:bg-black hover:text-white border-2 border-white transition-all"
className="w-full px-4 md:px-8 py-3 md:py-4 bg-white text-black font-mono text-xs md:text-sm tracking-[0.2em] hover:bg-black hover:text-white border-2 border-white transition-all"
>
START
</button>
) : (
<button
onClick={onClose}
className="w-full px-8 py-4 bg-white text-black font-mono text-sm tracking-[0.2em] hover:bg-black hover:text-white border-2 border-white transition-all"
className="w-full px-4 md:px-8 py-3 md:py-4 bg-white text-black font-mono text-xs md:text-sm tracking-[0.2em] hover:bg-black hover:text-white border-2 border-white transition-all"
>
CLOSE
</button>

View File

@ -45,6 +45,8 @@ export function Knob({
const normalizedValue = (value - min) / (max - min)
const angle = -225 + normalizedValue * 270
const fontSize = size <= 32 ? 'text-[7px]' : size <= 36 ? 'text-[8px]' : 'text-[9px]'
const handleMouseDown = (e: React.MouseEvent) => {
if (isInMappingMode && paramId && mappingModeState.activeLFO !== null && onMapClick) {
onMapClick(paramId, mappingModeState.activeLFO)
@ -135,7 +137,7 @@ export function Knob({
</svg>
<div className="absolute inset-0 flex items-center justify-center pointer-events-none">
<span className={`font-mono text-[9px] tracking-[0.15em] text-white ${isInMappingMode ? 'animate-pulse' : ''}`}>
<span className={`font-mono ${fontSize} tracking-[0.15em] text-white ${isInMappingMode ? 'animate-pulse' : ''}`}>
{isDragging ? displayValue : label.toUpperCase()}
</span>
</div>

View File

@ -36,11 +36,11 @@ export function LFOPanel({ onChange, onUpdateDepth, onRemoveMapping }: LFOPanelP
return (
<div className="bg-black border-t-2 border-white">
<div className="grid grid-cols-4 divide-x-2 divide-white">
<div className="grid grid-cols-2 xl:grid-cols-4 gap-[1px] bg-white p-[1px]">
{lfoConfigs.map(({ key, index }) => {
const lfo = lfoValues[key]
return (
<div key={key} className="px-2 py-3 flex items-center">
<div key={key} className="px-2 py-2 flex items-center bg-black">
<LFOScope
lfoIndex={index}
waveform={lfo.waveform}

View File

@ -19,7 +19,6 @@ interface LFOScopeProps {
const WAVEFORMS: LFOWaveform[] = ['sine', 'triangle', 'square', 'sawtooth', 'random']
const CANVAS_WIDTH = 340
const CANVAS_HEIGHT = 60
const MIN_FREQ = 0.01
const MAX_FREQ = 20
@ -33,6 +32,7 @@ export function LFOScope({ lfoIndex, waveform, frequency, phase, mappings, onCha
const [showEditor, setShowEditor] = useState(false)
const dragStartRef = useRef<{ x: number; y: number; freq: number; phase: number; moved: boolean } | null>(null)
const mappingModeState = useStore(mappingMode)
const [canvasWidth, setCanvasWidth] = useState(340)
const getLFOValueAtPhase = useCallback((phaseVal: number): number => {
const normalizedPhase = phaseVal % 1
@ -65,6 +65,26 @@ export function LFOScope({ lfoIndex, waveform, frequency, phase, mappings, onCha
}
}, [frequency, phase, waveform])
useEffect(() => {
const canvas = canvasRef.current
if (!canvas) return
const updateCanvasSize = () => {
const rect = canvas.getBoundingClientRect()
const width = Math.floor(rect.width)
setCanvasWidth(width)
canvas.width = width
canvas.height = CANVAS_HEIGHT
}
updateCanvasSize()
window.addEventListener('resize', updateCanvasSize)
return () => {
window.removeEventListener('resize', updateCanvasSize)
}
}, [])
useEffect(() => {
const canvas = canvasRef.current
if (!canvas) return
@ -78,13 +98,13 @@ export function LFOScope({ lfoIndex, waveform, frequency, phase, mappings, onCha
if (!lfoRef.current) return
ctx.fillStyle = '#000000'
ctx.fillRect(0, 0, CANVAS_WIDTH, CANVAS_HEIGHT)
ctx.fillRect(0, 0, canvasWidth, CANVAS_HEIGHT)
ctx.strokeStyle = '#ffffff'
ctx.lineWidth = 2
ctx.beginPath()
const samples = CANVAS_WIDTH
const samples = canvasWidth
const centerY = CANVAS_HEIGHT / 2
for (let x = 0; x < samples; x++) {
@ -119,7 +139,7 @@ export function LFOScope({ lfoIndex, waveform, frequency, phase, mappings, onCha
cancelAnimationFrame(animationRef.current)
}
}
}, [frequency, waveform, phase, getLFOValueAtPhase])
}, [frequency, waveform, phase, getLFOValueAtPhase, canvasWidth])
const handleMouseDown = (e: React.MouseEvent<HTMLCanvasElement>) => {
if (e.button === 2) return
@ -185,12 +205,10 @@ export function LFOScope({ lfoIndex, waveform, frequency, phase, mappings, onCha
<div className="flex items-center gap-1 w-full relative">
<canvas
ref={canvasRef}
width={CANVAS_WIDTH}
height={CANVAS_HEIGHT}
className={`border-2 border-white cursor-move flex-1 ${
className={`border-2 border-white cursor-move w-full ${
isActive ? 'animate-pulse' : ''
}`}
style={{ maxWidth: CANVAS_WIDTH }}
onMouseDown={handleMouseDown}
onMouseMove={handleMouseMove}
onMouseUp={handleMouseUp}