progress on responsive
This commit is contained in:
@ -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'>&</text></svg>" />
|
||||
|
||||
92
src/App.tsx
92
src/App.tsx
@ -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">
|
||||
<h1
|
||||
onClick={() => setShowHelp(true)}
|
||||
className="font-mono text-sm tracking-[0.3em] text-white flex-shrink-0 cursor-pointer hover:opacity-70 transition-opacity"
|
||||
>
|
||||
BRUITISTE
|
||||
</h1>
|
||||
<EngineControls
|
||||
values={engineValues}
|
||||
onChange={handleEngineChange}
|
||||
onMapClick={handleParameterMapClick}
|
||||
getMappedLFOs={getMappedLFOs}
|
||||
/>
|
||||
<div className="flex gap-4 flex-shrink-0">
|
||||
<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-[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>
|
||||
<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)
|
||||
|
||||
@ -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>
|
||||
)
|
||||
}
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
|
||||
<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>
|
||||
</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>
|
||||
</tr>
|
||||
<tr className="border-b border-white">
|
||||
<td className="p-3 border-r border-white">ARROWS</td>
|
||||
<td className="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>
|
||||
</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>
|
||||
</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>
|
||||
</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>
|
||||
</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>
|
||||
</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>
|
||||
</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>
|
||||
</tr>
|
||||
<tr>
|
||||
<td className="p-3 border-r border-white">ESC</td>
|
||||
<td className="p-3">Exit mapping mode</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<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-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-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-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-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-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-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-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-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-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-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-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>
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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}
|
||||
|
||||
@ -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}
|
||||
|
||||
Reference in New Issue
Block a user