You've already forked torvalds-GuitarPedal
mirror of
https://github.com/torvalds/GuitarPedal.git
synced 2026-08-19 05:33:46 +00:00
'Software' was the directory everything that was not KiCad ended up in, which stopped describing anything a while ago - Validation and the web app are software too. Worse, it put the shared parts inside the firmware, where they read as the firmware's own. They are not. Effects/ has three consumers built from it: the firmware, Validation's bench, and the web app's controls, all generated from the same POT: comments by gen_effects.py. Audio/ has two - the bench compiles the same biquads, the same envelope followers and the same single_sample(), which is the whole reason a measurement on a workstation says anything about the pedal. Neither belongs under Firmware/, so neither is under it any more: Effects/ one file per effect Audio/ the DSP they are built from, and the audio loop Firmware/ the rest of what runs on the pedal, and the submodules WebMIDI/ the web app scripts/ what the build runs Validation/ unchanged Hardware/, Documentation/, Images/ CMakeLists.txt and the wrapper Makefile move to the top with them, because the build now consumes four of those directories and generates into a fifth. board.local and build/ come along; MIDI_CC_MAP.md is generated into Documentation/ rather than into the old Software/ root. scripts/ goes with the build rather than staying under the firmware, because six of the ten had nothing to do with the firmware: gen_effects.py reads Effects/ and writes to three different places, pow2/log2/quarter_sine generate Audio/'s tables, check-readme.py compares Effects/ against the README, and server.py serves the web app. Four of them are invoked from Validation, which was reaching into Firmware/ for tooling - the same burying this commit is undoing. The four that really are about the firmware are ELF checks the top-level build drives anyway, and a second scripts directory would only be a second place to look. C includes say "Audio/foo.h" and the generated map says "Effects/bar.h", with the repository root on the include path for both the firmware and the bench. Spelling the directory out rather than relying on a bare name is what keeps Audio/cycles.h shimmable: a quoted include searches the including file's own directory first. The submodules are renamed as well as moved. git mv updates their paths but leaves the section names, and 'Software/pico-sdk' surviving in .gitmodules would be the word this commit removes, still load-bearing. That meant the nested modules under pico-sdk too - six .git files pointing into .git/modules/Software - which is why 'git submodule update --init --recursive' is worth running once after pulling this. Verified rather than assumed: a clean configure and build, make check (failing only on the missing-eeprom case it already failed on), check-effects, all four analysis pages reproducing every series and drawing every chart, and a flash to the board that still measures a routed reverb where it did before. One latent bug fell out of it. bench/coeff declared only quarter_sine.h of the three generated math tables, and Audio/util.h includes pow2.h and log2.h as well - so building that target with an empty gen/ could never have worked. 'make bench' builds bench/bench first, which generates all three, so it stayed hidden until this rebuilt everything from nothing. Signed-off-by: Linus Torvalds <torvalds@linux-foundation.org>
1412 lines
31 KiB
CSS
1412 lines
31 KiB
CSS
:root {
|
|
--bg-color: #0f1115;
|
|
--panel-bg: rgba(255, 255, 255, 0.03);
|
|
--panel-border: rgba(255, 255, 255, 0.05);
|
|
--text-main: #e2e8f0;
|
|
--text-muted: #94a3b8;
|
|
--accent: #3b82f6;
|
|
--accent-hover: #60a5fa;
|
|
--danger: #ef4444;
|
|
--success: #10b981;
|
|
/* A locally served copy, in place of --success/--text-muted */
|
|
--dev: #f59e0b;
|
|
--dev-muted: #8a7040;
|
|
}
|
|
|
|
* {
|
|
box-sizing: border-box;
|
|
margin: 0;
|
|
padding: 0;
|
|
}
|
|
|
|
body {
|
|
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
|
|
background-color: var(--bg-color);
|
|
color: var(--text-main);
|
|
line-height: 1.5;
|
|
padding: 20px;
|
|
}
|
|
|
|
.app-container {
|
|
max-width: 800px;
|
|
margin: 0 auto;
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 20px;
|
|
}
|
|
|
|
.glass-panel {
|
|
background: var(--panel-bg);
|
|
border: 1px solid var(--panel-border);
|
|
border-radius: 12px;
|
|
padding: 20px;
|
|
backdrop-filter: blur(10px);
|
|
-webkit-backdrop-filter: blur(10px);
|
|
}
|
|
|
|
header {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
}
|
|
|
|
h1 {
|
|
font-size: 1.5rem;
|
|
font-weight: 600;
|
|
}
|
|
|
|
h2 {
|
|
font-size: 1.2rem;
|
|
margin-bottom: 15px;
|
|
color: var(--text-muted);
|
|
}
|
|
|
|
/*
|
|
* Two rows: what the header does, then what it says.
|
|
*
|
|
* It was one row, and it did not fit on a phone - the title alone was
|
|
* most of a 360px screen, and the controls did not fit in what was left
|
|
* even without it. Widening the page to hold it all is the one outcome
|
|
* worth ruling out, since it costs you every card below as well.
|
|
*
|
|
* So the controls get the row, at the size a thumb needs, and the title
|
|
* and the fault chips go underneath in small text. Nothing was lost:
|
|
* the title is a device name that does not change while you look at it,
|
|
* the chips are absent nearly always, and what you actually read in the
|
|
* moment is the colour - which a small word carries exactly as well as
|
|
* a large one.
|
|
*/
|
|
.app-header {
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: stretch;
|
|
gap: 10px;
|
|
/*
|
|
* The menu hangs out of the bottom of this, and has to be able to
|
|
* cover the cards below. It can't do that on its own z-index:
|
|
* .glass-panel sets backdrop-filter, which creates a stacking
|
|
* context, so the menu's z-index only ranks it inside this header.
|
|
* The cards are .glass-panel too and come later in the document, so
|
|
* without this they paint over the menu - and being backdrop
|
|
* filters, they blur it while they're at it.
|
|
*
|
|
* Stays well below the backdrop (900) and the panels above it.
|
|
*/
|
|
position: relative;
|
|
z-index: 20;
|
|
}
|
|
|
|
/*
|
|
* The heading stays first in the document and second on the screen.
|
|
* Ordering it here rather than in the markup keeps the page's one <h1>
|
|
* ahead of a row of buttons for anything reading the document instead
|
|
* of looking at it.
|
|
*/
|
|
.header-controls {
|
|
order: 1;
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 15px;
|
|
}
|
|
|
|
|
|
.header-status {
|
|
order: 2;
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 8px;
|
|
min-width: 0;
|
|
}
|
|
|
|
/*
|
|
* The name gives way and the faults do not. A truncated device name is
|
|
* still the same colour and still says what it needs to; a truncated
|
|
* CLIP is a fault you were not told about.
|
|
*/
|
|
#app-title {
|
|
font-size: 0.8rem;
|
|
font-weight: 500;
|
|
min-width: 0;
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
white-space: nowrap;
|
|
transition: color 0.3s;
|
|
}
|
|
|
|
|
|
/*
|
|
* The title is the pedal-state light, and it carries where the page
|
|
* came from as well: hue says which copy this is, brightness says
|
|
* whether the pedal is answering. Two signals, one word, no badge.
|
|
*/
|
|
.title-connected {
|
|
color: var(--success);
|
|
}
|
|
|
|
.title-disconnected {
|
|
color: var(--text-muted);
|
|
}
|
|
|
|
body.local-dev .title-connected {
|
|
color: var(--dev);
|
|
}
|
|
|
|
body.local-dev .title-disconnected {
|
|
color: var(--dev-muted);
|
|
}
|
|
|
|
.control-group label {
|
|
font-size: 0.9rem;
|
|
color: var(--text-muted);
|
|
}
|
|
|
|
select {
|
|
background-color: rgba(0, 0, 0, 0.2);
|
|
border: 1px solid var(--panel-border);
|
|
color: var(--text-main);
|
|
padding: 10px 15px;
|
|
border-radius: 8px;
|
|
font-size: 1rem;
|
|
outline: none;
|
|
cursor: pointer;
|
|
appearance: none;
|
|
}
|
|
|
|
select:focus {
|
|
border-color: var(--accent);
|
|
}
|
|
|
|
option {
|
|
background-color: var(--bg-color);
|
|
color: var(--text-main);
|
|
}
|
|
|
|
/* Toggle Switch */
|
|
.switch {
|
|
position: relative;
|
|
display: inline-block;
|
|
width: 50px;
|
|
height: 28px;
|
|
}
|
|
|
|
.switch input {
|
|
opacity: 0;
|
|
width: 0;
|
|
height: 0;
|
|
}
|
|
|
|
.slider {
|
|
position: absolute;
|
|
cursor: pointer;
|
|
top: 0; left: 0; right: 0; bottom: 0;
|
|
background-color: rgba(255, 255, 255, 0.1);
|
|
transition: .3s;
|
|
}
|
|
|
|
.slider:before {
|
|
position: absolute;
|
|
content: "";
|
|
height: 20px;
|
|
width: 20px;
|
|
left: 4px;
|
|
bottom: 4px;
|
|
background-color: white;
|
|
transition: .3s;
|
|
}
|
|
|
|
input:checked + .slider {
|
|
background-color: var(--accent);
|
|
}
|
|
|
|
input:checked + .slider:before {
|
|
transform: translateX(22px);
|
|
}
|
|
|
|
.slider.round {
|
|
border-radius: 34px;
|
|
}
|
|
|
|
.slider.round:before {
|
|
border-radius: 50%;
|
|
}
|
|
|
|
.enable-group {
|
|
display: flex;
|
|
flex-direction: row;
|
|
align-items: center;
|
|
gap: 8px;
|
|
}
|
|
|
|
.action-btn {
|
|
background: rgba(255, 255, 255, 0.05);
|
|
border: 1px solid var(--panel-border);
|
|
color: var(--text-muted);
|
|
font-size: 1.1rem;
|
|
width: 32px;
|
|
height: 32px;
|
|
border-radius: 50%;
|
|
cursor: pointer;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
transition: all 0.2s;
|
|
padding-bottom: 2px;
|
|
}
|
|
|
|
.action-btn:hover {
|
|
background: rgba(255, 255, 255, 0.15);
|
|
color: var(--text-main);
|
|
transform: scale(1.1);
|
|
}
|
|
|
|
.action-btn:active {
|
|
transform: scale(0.95);
|
|
}
|
|
|
|
/* Pots */
|
|
.pots-section {
|
|
display: grid;
|
|
grid-template-columns: repeat(auto-fill, minmax(120px, 1fr));
|
|
gap: 20px;
|
|
}
|
|
|
|
.pot-control {
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: center;
|
|
gap: 10px;
|
|
}
|
|
|
|
.pot-label {
|
|
font-size: 0.9rem;
|
|
color: var(--text-muted);
|
|
text-align: center;
|
|
}
|
|
|
|
/*
|
|
* A pot that can explain itself says so, faintly. Dotted rather than
|
|
* a cursor change, because half of these are used on a touchscreen
|
|
* where there is no hover and no pointer to change.
|
|
*/
|
|
.pot-control.has-info > .pot-label {
|
|
text-decoration: underline dotted;
|
|
text-underline-offset: 0.2em;
|
|
}
|
|
|
|
.pot-value {
|
|
font-size: 1.1rem;
|
|
font-weight: 500;
|
|
}
|
|
|
|
input[type=range] {
|
|
-webkit-appearance: none;
|
|
width: 100%;
|
|
background: transparent;
|
|
}
|
|
|
|
input[type=range]::-webkit-slider-thumb {
|
|
-webkit-appearance: none;
|
|
height: 16px;
|
|
width: 16px;
|
|
border-radius: 50%;
|
|
background: var(--accent);
|
|
cursor: pointer;
|
|
margin-top: -6px;
|
|
}
|
|
|
|
input[type=range]::-webkit-slider-runnable-track {
|
|
width: 100%;
|
|
height: 4px;
|
|
cursor: pointer;
|
|
background: rgba(255, 255, 255, 0.1);
|
|
border-radius: 2px;
|
|
}
|
|
|
|
/*
|
|
* Focus still needs to be visible - it's the only cue a keyboard user
|
|
* gets - but the default outline looks wrong on a round thumb, so put a
|
|
* ring on the thumb instead.
|
|
*/
|
|
input[type=range]:focus {
|
|
outline: none;
|
|
}
|
|
|
|
input[type=range]:focus::-webkit-slider-thumb {
|
|
box-shadow: 0 0 0 5px rgba(59, 130, 246, 0.35);
|
|
}
|
|
|
|
input[type=range]:focus::-moz-range-thumb {
|
|
box-shadow: 0 0 0 5px rgba(59, 130, 246, 0.35);
|
|
}
|
|
|
|
/* Diagnostics */
|
|
.diag-grid {
|
|
display: flex;
|
|
gap: 30px;
|
|
}
|
|
|
|
.diag-item {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 10px;
|
|
}
|
|
|
|
.diag-label {
|
|
font-size: 0.9rem;
|
|
color: var(--text-muted);
|
|
}
|
|
|
|
.indicator {
|
|
width: 12px;
|
|
height: 12px;
|
|
border-radius: 50%;
|
|
background-color: rgba(255, 255, 255, 0.1);
|
|
}
|
|
|
|
.indicator.active {
|
|
background-color: var(--danger);
|
|
box-shadow: 0 0 10px var(--danger);
|
|
}
|
|
|
|
.value {
|
|
font-family: monospace;
|
|
font-size: 1.1rem;
|
|
background: rgba(0, 0, 0, 0.2);
|
|
padding: 4px 8px;
|
|
border-radius: 4px;
|
|
}
|
|
|
|
/* Effect Cards */
|
|
#effects-container {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 20px;
|
|
}
|
|
|
|
.effect-card {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 15px;
|
|
}
|
|
|
|
.effect-header {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
border-bottom: 1px solid var(--panel-border);
|
|
padding-bottom: 10px;
|
|
/*
|
|
* The header is what you grab to reorder, so none of it may behave
|
|
* like text. A long press on text is the browser's select gesture,
|
|
* which on a phone is what you got instead of a drag, and iOS puts
|
|
* its callout menu up on top of that. This has to be here rather
|
|
* than switched on once a drag is recognised: by then the selection
|
|
* has already started.
|
|
*/
|
|
user-select: none;
|
|
-webkit-user-select: none;
|
|
-webkit-touch-callout: none;
|
|
}
|
|
|
|
/*
|
|
* A mouse grabs the whole header. A finger grabs the handle, and only
|
|
* the handle - see cardDragStart().
|
|
*
|
|
* The header used to be the grip for both, on the reasoning that a
|
|
* header is finger-sized and a ~20px glyph is not. That is true and it
|
|
* was still the wrong trade: touch-action has to be on whatever the
|
|
* touch lands on, so making the whole header the grip made the whole
|
|
* header a place the page could not be scrolled from - and a card
|
|
* header is a wide bar that a thumb lands on constantly on the way past.
|
|
* Reaching for a scroll and picking a card up instead is a worse failure
|
|
* than a grip that has to be aimed at, because you did not ask for it.
|
|
*
|
|
* A mouse has no scroll-by-dragging gesture to be confused with, so it
|
|
* loses nothing by keeping the big grip.
|
|
*/
|
|
/*
|
|
* pan-y, not none: the page still scrolls vertically from a card header,
|
|
* which is the point of narrowing the grip. Sideways is ours, and the
|
|
* page has nowhere to go sideways, so claiming it costs nothing - that
|
|
* is the room the flick-to-unroute gesture lives in. See cardSwipeStart().
|
|
*/
|
|
.effect-header.draggable {
|
|
cursor: grab;
|
|
touch-action: pan-y;
|
|
}
|
|
|
|
/* Only while it springs back - a card following a finger must not lag */
|
|
.effect-card.swipe-return {
|
|
transition: transform 180ms ease-out, opacity 180ms ease-out;
|
|
}
|
|
|
|
/*
|
|
* The right answer to a 20px glyph was always a 44px target around it,
|
|
* which is what this is. touch-action lives here now: this is the one
|
|
* place in a card that a drag can start, so it is the one place that
|
|
* does not scroll.
|
|
*/
|
|
.drag-handle {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
min-width: 44px;
|
|
min-height: 44px;
|
|
margin-right: 4px;
|
|
font-size: 1.4em;
|
|
opacity: 0.7;
|
|
cursor: grab;
|
|
touch-action: none;
|
|
}
|
|
|
|
.effect-title {
|
|
font-size: 1.2rem;
|
|
font-weight: 600;
|
|
color: var(--accent);
|
|
}
|
|
|
|
.effect-controls {
|
|
display: grid;
|
|
grid-template-columns: repeat(auto-fill, minmax(120px, 1fr));
|
|
gap: 20px;
|
|
}
|
|
|
|
/* Graphic EQ specific styles */
|
|
.effect-controls.eq-container {
|
|
display: block;
|
|
position: relative;
|
|
padding-bottom: 20px;
|
|
}
|
|
|
|
.eq-curve-wrapper {
|
|
position: relative;
|
|
width: 100%;
|
|
height: 300px;
|
|
margin-top: 20px;
|
|
z-index: 1;
|
|
pointer-events: auto;
|
|
cursor: crosshair;
|
|
}
|
|
|
|
|
|
/*
|
|
* The canvas is a drag surface in both directions - a node carries a
|
|
* frequency sideways and a gain vertically - so there is no axis left to
|
|
* give the browser, and it gets none.
|
|
*
|
|
* This is not new behaviour. The old touchstart handler called
|
|
* preventDefault() unconditionally, which claimed the gesture the same
|
|
* way; saying it in touch-action is what pointer events need instead,
|
|
* and it says it where it can be seen.
|
|
*/
|
|
.eq-canvas {
|
|
width: 100%;
|
|
height: 100%;
|
|
display: block;
|
|
touch-action: none;
|
|
}
|
|
|
|
/*
|
|
* The sliders behind the curve.
|
|
*
|
|
* Every graphed pot still has one, because the slider is what holds the
|
|
* value and what the pedal's updates arrive on - the node just moves it.
|
|
* None of them are ever seen, so this is the whole of their styling.
|
|
*/
|
|
.eq-sliders {
|
|
display: none;
|
|
}
|
|
|
|
.eq-curve-svg {
|
|
width: 100%;
|
|
height: 100%;
|
|
overflow: visible;
|
|
}
|
|
|
|
.eq-path {
|
|
fill: rgba(59, 130, 246, 0.15); /* var(--accent) transparent */
|
|
stroke: var(--accent);
|
|
stroke-width: 3px;
|
|
vector-effect: non-scaling-stroke;
|
|
}
|
|
|
|
/*
|
|
* The row under the curve: the Mix control, and the switches for how
|
|
* the graph behaves.
|
|
*
|
|
* Mix takes the width it needs and the switches go to the far end, so
|
|
* that the two are read as the different things they are - one is a
|
|
* parameter the pedal stores, the others are how this screen behaves
|
|
* and go no further than this browser.
|
|
*
|
|
* It wraps, because on a narrow screen a row of both is neither.
|
|
*/
|
|
.eq-footer {
|
|
display: flex;
|
|
align-items: center;
|
|
flex-wrap: wrap;
|
|
gap: 10px 24px;
|
|
margin-top: 10px;
|
|
}
|
|
|
|
.eq-footer .pot-control {
|
|
flex: 1 1 180px;
|
|
}
|
|
|
|
.eq-options {
|
|
margin-left: auto;
|
|
display: flex;
|
|
flex-wrap: wrap;
|
|
gap: 4px 20px;
|
|
}
|
|
|
|
/* The label is part of the target, which is what gets it to 44px */
|
|
.eq-option {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 8px;
|
|
min-height: 44px;
|
|
font-size: 0.9rem;
|
|
color: var(--text-muted);
|
|
cursor: pointer;
|
|
user-select: none;
|
|
-webkit-user-select: none;
|
|
}
|
|
|
|
.eq-option input {
|
|
width: 18px;
|
|
height: 18px;
|
|
accent-color: var(--accent);
|
|
cursor: pointer;
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* Bottom Panels (Generic) */
|
|
/* Panel Backdrop */
|
|
#panel-backdrop {
|
|
position: fixed;
|
|
top: 0; left: 0; right: 0; bottom: 0;
|
|
background: rgba(0, 0, 0, 0.5);
|
|
backdrop-filter: blur(2px);
|
|
-webkit-backdrop-filter: blur(2px);
|
|
z-index: 900;
|
|
transition: opacity 0.3s;
|
|
}
|
|
|
|
#panel-backdrop.hidden {
|
|
display: none !important;
|
|
}
|
|
|
|
.bottom-panel {
|
|
position: fixed;
|
|
bottom: 0;
|
|
left: 0;
|
|
right: 0;
|
|
margin: 0;
|
|
border-radius: 20px 20px 0 0;
|
|
border-bottom: none;
|
|
z-index: 1000;
|
|
padding: 20px 30px;
|
|
background: rgba(15, 17, 21, 0.95);
|
|
box-shadow: 0 -10px 40px rgba(0, 0, 0, 0.8);
|
|
transform: translateY(110%);
|
|
transition: transform 0.3s cubic-bezier(0.175, 0.885, 0.32, 1.275);
|
|
}
|
|
|
|
.bottom-panel:not(.hidden) {
|
|
transform: translateY(0);
|
|
}
|
|
|
|
.panel-header {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
margin-bottom: 25px;
|
|
}
|
|
|
|
.panel-header h2 {
|
|
margin: 0;
|
|
color: var(--text-main);
|
|
}
|
|
|
|
/*
|
|
* The menu proper: a dropdown hung off the burger, holding actions and
|
|
* nothing else. It deliberately does not dim the page behind it - it
|
|
* isn't modal, and a menu that darkens the whole app to offer four
|
|
* items reads as heavier than it is.
|
|
*/
|
|
.burger-wrap {
|
|
position: relative;
|
|
}
|
|
|
|
|
|
.menu-dropdown {
|
|
position: absolute;
|
|
z-index: 1000;
|
|
top: calc(100% + 8px);
|
|
/*
|
|
* Anchored to the left edge of the button, because that is the edge
|
|
* the button is now against. Anchored right, a menu wider than the
|
|
* burger grows leftwards - which was correct while it sat in the top
|
|
* right corner, and walks straight off the screen from the top left.
|
|
*/
|
|
left: 0;
|
|
/*
|
|
* Sized to whatever the longest item happens to be, rather than to a
|
|
* number that was right until someone added a word. "Reboot to
|
|
* Programming Mode" wrapped at 260px, and the scene items change
|
|
* width as you pick different scenes, so nothing here has a length
|
|
* worth hard-coding. The floor keeps a short menu from looking
|
|
* mean; the ceiling keeps a long one on the screen.
|
|
*/
|
|
width: max-content;
|
|
min-width: 260px;
|
|
max-width: calc(100vw - 32px);
|
|
padding: 6px;
|
|
background: rgba(20, 23, 29, 0.98);
|
|
border: 1px solid var(--panel-border);
|
|
border-radius: 12px;
|
|
box-shadow: 0 14px 40px rgba(0, 0, 0, 0.65);
|
|
}
|
|
|
|
/* The bottom sheet hid itself by sliding off the bottom edge, so there
|
|
has never been a general .hidden rule to inherit. */
|
|
.menu-dropdown.hidden,
|
|
.dialog-panel.hidden {
|
|
display: none;
|
|
}
|
|
|
|
.menu-dropdown button {
|
|
display: block;
|
|
width: 100%;
|
|
text-align: left;
|
|
background: none;
|
|
border: 0;
|
|
color: var(--text-main);
|
|
padding: 11px 14px;
|
|
border-radius: 8px;
|
|
cursor: pointer;
|
|
font: inherit;
|
|
font-size: 0.98rem;
|
|
white-space: nowrap;
|
|
}
|
|
|
|
.menu-dropdown button:hover {
|
|
background: rgba(255, 255, 255, 0.08);
|
|
}
|
|
|
|
.menu-dropdown button.danger-item {
|
|
color: var(--danger);
|
|
}
|
|
|
|
.menu-dropdown button.danger-item:hover {
|
|
background: rgba(239, 68, 68, 0.12);
|
|
}
|
|
|
|
.menu-dropdown hr {
|
|
border: 0;
|
|
border-top: 1px solid var(--panel-border);
|
|
margin: 6px 8px;
|
|
}
|
|
|
|
/* Which scene you're on, promoted out of the menu into the header.
|
|
Saving and loading stayed behind the menu - see index.html. */
|
|
/*
|
|
* The gap in the row, and so what splits it in two: the menu and the
|
|
* scene on the left, because those are about editing, and the tuner and
|
|
* the bypass on the right, because those are what the pedal is doing
|
|
* right now.
|
|
*
|
|
* One auto margin rather than space-between, which would have spread all
|
|
* four evenly across a desktop's worth of empty header.
|
|
*/
|
|
.header-scenes {
|
|
display: flex;
|
|
align-items: center;
|
|
margin-right: auto;
|
|
}
|
|
|
|
.header-scenes select {
|
|
background: rgba(255, 255, 255, 0.06);
|
|
color: var(--text-main);
|
|
border: 1px solid var(--panel-border);
|
|
border-radius: 8px;
|
|
padding: 6px 10px;
|
|
font: inherit;
|
|
font-size: 0.88rem;
|
|
}
|
|
|
|
/*
|
|
* Settings, which is a dialog rather than a sheet: it is centred and
|
|
* sized to its content instead of anchored to the bottom edge, because
|
|
* nothing about it is phone-shaped any more.
|
|
*/
|
|
.dialog-panel {
|
|
position: fixed;
|
|
z-index: 1000;
|
|
top: 50%;
|
|
left: 50%;
|
|
transform: translate(-50%, -50%);
|
|
width: min(460px, calc(100vw - 40px));
|
|
max-height: calc(100vh - 40px);
|
|
overflow-y: auto;
|
|
padding: 0;
|
|
background: rgba(15, 17, 21, 0.97);
|
|
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.7);
|
|
}
|
|
|
|
.dialog-panel .panel-header {
|
|
margin: 0;
|
|
padding: 18px 22px;
|
|
border-bottom: 1px solid var(--panel-border);
|
|
position: sticky;
|
|
top: 0;
|
|
background: rgba(15, 17, 21, 0.97);
|
|
}
|
|
|
|
.dialog-body {
|
|
padding: 6px 22px 22px;
|
|
}
|
|
|
|
.menu-section {
|
|
margin-top: 22px;
|
|
}
|
|
|
|
.menu-section > h3 {
|
|
font-size: 0.72rem;
|
|
font-weight: 700;
|
|
letter-spacing: 0.09em;
|
|
text-transform: uppercase;
|
|
color: var(--text-muted);
|
|
margin: 0 0 10px;
|
|
padding-bottom: 6px;
|
|
border-bottom: 1px solid var(--panel-border);
|
|
}
|
|
|
|
.menu-field {
|
|
margin-bottom: 10px;
|
|
}
|
|
|
|
.menu-field > label {
|
|
display: block;
|
|
font-size: 0.82rem;
|
|
color: var(--text-muted);
|
|
margin-bottom: 4px;
|
|
}
|
|
|
|
.menu-hint {
|
|
font-size: 0.8rem;
|
|
color: var(--text-muted);
|
|
margin-top: 6px;
|
|
}
|
|
|
|
.menu-select {
|
|
width: 100%;
|
|
background: #1a1d23;
|
|
color: var(--text-main);
|
|
padding: 9px 10px;
|
|
border-radius: 8px;
|
|
border: 1px solid var(--panel-border);
|
|
font: inherit;
|
|
font-size: 0.95rem;
|
|
}
|
|
|
|
/*
|
|
* showButtonSuccess()/showButtonError() swap a button's label for a
|
|
* moment and add one of these. They aren't tied to a particular kind
|
|
* of button, because the buttons that report back like this are now
|
|
* both menu items and header controls - hence !important, which beats
|
|
* the two-selector rules those live under without having to enumerate
|
|
* every context a reporting button might turn up in.
|
|
*/
|
|
.success {
|
|
background: rgba(16, 185, 129, 0.2) !important;
|
|
color: var(--success) !important;
|
|
border-color: var(--success) !important;
|
|
}
|
|
|
|
.error {
|
|
background: rgba(239, 68, 68, 0.2) !important;
|
|
color: var(--danger) !important;
|
|
border-color: var(--danger) !important;
|
|
}
|
|
|
|
.active-pot-info {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 15px;
|
|
}
|
|
|
|
#active-pot-title {
|
|
font-size: 1.2rem;
|
|
font-weight: 600;
|
|
color: var(--text-main);
|
|
}
|
|
|
|
#active-pot-value {
|
|
font-size: 1.3rem;
|
|
color: var(--accent);
|
|
background: rgba(0, 0, 0, 0.3);
|
|
padding: 4px 12px;
|
|
border-radius: 8px;
|
|
}
|
|
|
|
.active-pot-slider-container {
|
|
padding-bottom: 20px;
|
|
}
|
|
|
|
/* The knob shortcut, and what it is about to take the knob away from. */
|
|
.active-pot-knob {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 12px;
|
|
padding-bottom: 18px;
|
|
}
|
|
|
|
.active-pot-knob > .menu-hint {
|
|
margin-top: 0;
|
|
}
|
|
|
|
#assign-knob-btn {
|
|
flex: none;
|
|
width: auto;
|
|
padding: 6px 12px;
|
|
font-size: 0.85rem;
|
|
white-space: nowrap;
|
|
}
|
|
|
|
#assign-knob-btn:disabled {
|
|
opacity: 0.45;
|
|
cursor: default;
|
|
}
|
|
|
|
/* One gesture, and everything it does. */
|
|
.rule-group {
|
|
margin-bottom: 20px;
|
|
}
|
|
|
|
/*
|
|
* The gesture's name and its add button, laid out like a rule row so
|
|
* that the + lands in the same column as the x below it - which is
|
|
* also what makes it obvious which gesture it adds to.
|
|
*
|
|
* Both keep .action-btn's own size. They are circles, and a circle
|
|
* told to be any other shape stops being one.
|
|
*/
|
|
.rule-head {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 8px;
|
|
margin-bottom: 6px;
|
|
}
|
|
|
|
.rule-head > label {
|
|
flex: 1;
|
|
min-width: 0;
|
|
font-size: 0.82rem;
|
|
color: var(--text-muted);
|
|
}
|
|
|
|
.rule-add {
|
|
flex: none;
|
|
opacity: 0.5;
|
|
}
|
|
|
|
.rule-add:hover {
|
|
opacity: 1;
|
|
}
|
|
|
|
.rule-add:disabled {
|
|
opacity: 0.2;
|
|
cursor: default;
|
|
}
|
|
|
|
/*
|
|
* A rule reads left to right as a sentence - what it does, what it does
|
|
* it to, and the values it does it with - wrapping onto a second line
|
|
* when there are values. The remove button is outside that, beside the
|
|
* whole rule, because it belongs to the rule rather than to any line of
|
|
* it. It also has to keep .action-btn's fixed size: it is a circle,
|
|
* and a circle stretched to fill a grid cell is an oval.
|
|
*/
|
|
.rule-row {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 8px;
|
|
margin-bottom: 8px;
|
|
}
|
|
|
|
.rule-body {
|
|
flex: 1;
|
|
min-width: 0;
|
|
display: grid;
|
|
grid-template-columns: 1fr 1fr;
|
|
gap: 8px;
|
|
align-items: center;
|
|
}
|
|
|
|
/* An action with nothing to point at gets the line to itself. */
|
|
.rule-body:not(.has-target) > .menu-select {
|
|
grid-column: 1 / -1;
|
|
}
|
|
|
|
.rule-remove {
|
|
flex: none;
|
|
font-size: 0.95rem;
|
|
opacity: 0.5;
|
|
}
|
|
|
|
.rule-remove:hover {
|
|
opacity: 1;
|
|
}
|
|
|
|
|
|
/*
|
|
* A value is the pot's own control in miniature - the slider and the
|
|
* readout it would have on its card, in the units it would use there.
|
|
*/
|
|
.binding-value {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 8px;
|
|
min-width: 0;
|
|
}
|
|
|
|
.binding-value > input[type="range"] {
|
|
flex: 1;
|
|
min-width: 0;
|
|
}
|
|
|
|
.binding-value > .pot-value {
|
|
flex: none;
|
|
font-size: 0.8rem;
|
|
color: var(--text-muted);
|
|
white-space: nowrap;
|
|
}
|
|
|
|
/* Larger slider for fine adjustments */
|
|
input[type=range].large-slider {
|
|
height: 40px;
|
|
}
|
|
|
|
input[type=range].large-slider::-webkit-slider-thumb {
|
|
height: 32px;
|
|
width: 32px;
|
|
margin-top: -14px;
|
|
box-shadow: 0 0 10px rgba(0, 0, 0, 0.5);
|
|
}
|
|
|
|
input[type=range].large-slider::-webkit-slider-runnable-track {
|
|
height: 8px;
|
|
border-radius: 4px;
|
|
}
|
|
|
|
/* Make pot-controls clickable */
|
|
.pot-control, .pot-control * {
|
|
cursor: pointer;
|
|
}
|
|
|
|
/* Tuner Overlay */
|
|
#tuner-panel {
|
|
position: fixed;
|
|
top: 0; left: 0; right: 0; bottom: 0;
|
|
background: var(--bg-color);
|
|
z-index: 2000;
|
|
display: flex;
|
|
flex-direction: column;
|
|
padding: 30px;
|
|
border-radius: 0;
|
|
border: none;
|
|
justify-content: center;
|
|
gap: 30px;
|
|
}
|
|
|
|
#tuner-panel.hidden {
|
|
display: none !important;
|
|
}
|
|
|
|
.tuner-header {
|
|
position: absolute;
|
|
top: 30px;
|
|
left: 30px;
|
|
right: 30px;
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
}
|
|
|
|
.tuner-chromatic-display {
|
|
display: flex;
|
|
justify-content: center;
|
|
align-items: baseline;
|
|
gap: 15px;
|
|
margin-top: 50px;
|
|
}
|
|
|
|
.tuner-note {
|
|
font-size: 5rem;
|
|
font-weight: bold;
|
|
color: var(--accent);
|
|
text-align: right;
|
|
font-variant-numeric: tabular-nums;
|
|
line-height: 1;
|
|
}
|
|
|
|
.tuner-cents {
|
|
font-size: 2.5rem;
|
|
color: var(--text-muted);
|
|
font-variant-numeric: tabular-nums;
|
|
line-height: 1;
|
|
}
|
|
|
|
.tuner-vol {
|
|
font-size: 1.2rem;
|
|
color: var(--text-muted);
|
|
font-variant-numeric: tabular-nums;
|
|
line-height: 1;
|
|
opacity: 0.7;
|
|
}
|
|
|
|
.tuner-meter-container {
|
|
position: relative;
|
|
height: 60px;
|
|
background: rgba(0, 0, 0, 0.3);
|
|
border-radius: 12px;
|
|
overflow: hidden;
|
|
margin-top: 10px;
|
|
}
|
|
|
|
.tuner-meter-center {
|
|
position: absolute;
|
|
left: 50%;
|
|
top: 0;
|
|
bottom: 0;
|
|
width: 2px;
|
|
background-color: rgba(255, 255, 255, 0.2);
|
|
transform: translateX(-50%);
|
|
}
|
|
|
|
.tuner-needle {
|
|
position: absolute;
|
|
top: 0;
|
|
bottom: 0;
|
|
width: 6px;
|
|
background-color: var(--success);
|
|
left: 50%;
|
|
transform: translateX(-50%);
|
|
transition: left 0.1s linear, background-color 0.2s;
|
|
border-radius: 3px;
|
|
}
|
|
|
|
.tuner-strings {
|
|
display: flex;
|
|
justify-content: space-around;
|
|
margin-top: 40px;
|
|
}
|
|
|
|
.tuner-string {
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: center;
|
|
gap: 15px;
|
|
width: 60px;
|
|
}
|
|
|
|
.tuner-string-name {
|
|
font-weight: bold;
|
|
font-size: 1.5rem;
|
|
color: var(--text-muted);
|
|
}
|
|
|
|
.tuner-string-name.active {
|
|
color: var(--text-main);
|
|
}
|
|
|
|
.tuner-string-arrow {
|
|
width: 0;
|
|
height: 0;
|
|
border-left: 15px solid transparent;
|
|
border-right: 15px solid transparent;
|
|
transition: all 0.1s;
|
|
}
|
|
|
|
.tuner-string-arrow.up {
|
|
border-bottom: 20px solid var(--danger);
|
|
border-top: none;
|
|
background: transparent;
|
|
width: 0; height: 0; border-radius: 0;
|
|
}
|
|
|
|
.tuner-string-arrow.down {
|
|
border-top: 20px solid var(--danger);
|
|
border-bottom: none;
|
|
background: transparent;
|
|
width: 0; height: 0; border-radius: 0;
|
|
}
|
|
|
|
.tuner-string-arrow.perfect {
|
|
border: none;
|
|
width: 24px;
|
|
height: 24px;
|
|
border-radius: 50%;
|
|
background-color: var(--success);
|
|
}
|
|
|
|
.tuner-string-arrow.inactive {
|
|
border: none;
|
|
width: 15px;
|
|
height: 15px;
|
|
border-radius: 50%;
|
|
background-color: rgba(255, 255, 255, 0.1);
|
|
}
|
|
|
|
.tuner-string-vol {
|
|
font-size: 0.9rem;
|
|
color: var(--text-muted);
|
|
font-variant-numeric: tabular-nums;
|
|
opacity: 0.7;
|
|
}
|
|
|
|
#tuner-btn.active {
|
|
background: rgba(59, 130, 246, 0.5);
|
|
color: white;
|
|
border-color: var(--accent);
|
|
}
|
|
|
|
/*
|
|
* The pool of effects that are not in the chain.
|
|
*
|
|
* These used to be full cards below a divider, greyed out and collapsed,
|
|
* which is a lot of screen for a list of things the pedal is not running.
|
|
* A chip carries everything true about an unrouted effect - its name -
|
|
* and the whole pool now costs about what one card used to.
|
|
*
|
|
* No general .hidden rule to inherit, so declare it.
|
|
*/
|
|
.effect-pool {
|
|
margin-top: 1.5rem;
|
|
}
|
|
|
|
.effect-pool.hidden {
|
|
display: none;
|
|
}
|
|
|
|
.pool-label {
|
|
color: var(--text-muted);
|
|
font-size: 0.9rem;
|
|
margin-bottom: 0.75rem;
|
|
}
|
|
|
|
.pool-chips {
|
|
display: flex;
|
|
flex-wrap: wrap;
|
|
gap: 10px;
|
|
}
|
|
|
|
/*
|
|
* Finger-sized from the start. 44px is the smallest target worth
|
|
* offering on a touchscreen, and a chip has no reason to be smaller -
|
|
* unlike the controls it sits under, which still need that pass.
|
|
*/
|
|
.effect-chip {
|
|
min-height: 44px;
|
|
padding: 0 16px;
|
|
border: 1px solid var(--panel-border);
|
|
border-radius: 22px;
|
|
background: rgba(255, 255, 255, 0.05);
|
|
color: var(--text-main);
|
|
font-size: 1rem;
|
|
font-family: inherit;
|
|
cursor: pointer;
|
|
transition: background 0.2s, border-color 0.2s, transform 0.1s;
|
|
}
|
|
|
|
.effect-chip:hover:not(:disabled) {
|
|
background: rgba(255, 255, 255, 0.12);
|
|
border-color: var(--accent);
|
|
}
|
|
|
|
.effect-chip:active:not(:disabled) {
|
|
transform: scale(0.96);
|
|
}
|
|
|
|
.effect-chip:disabled {
|
|
opacity: 0.4;
|
|
cursor: default;
|
|
}
|
|
|
|
/*
|
|
* Parked: routed effects are the ones on screen, but the card of an
|
|
* unrouted one stays in the document because the pot elements inside it
|
|
* are what the pedal's PARAM_UPDATE messages address. See applyRouting().
|
|
*/
|
|
.effect-card.parked {
|
|
display: none;
|
|
}
|
|
|
|
/*
|
|
* What the pedal is up to.
|
|
*
|
|
* Two different things with two different visual languages, because they
|
|
* are two different kinds of news. A fault - the output clipping, the
|
|
* audio core missing its deadline - is a warning and belongs in the
|
|
* header. An effect asking for attention is that effect *working*, and
|
|
* belongs on that effect.
|
|
*
|
|
* No general .hidden rule to inherit, so declare it (see above).
|
|
*/
|
|
#pedal-status.hidden,
|
|
#status-clip.hidden,
|
|
#status-drop.hidden {
|
|
display: none;
|
|
}
|
|
|
|
.pedal-status {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 8px;
|
|
font-size: 0.7rem;
|
|
font-weight: 700;
|
|
letter-spacing: 0.06em;
|
|
/* The title next to this is what shrinks - see #app-title */
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
.status-clip,
|
|
.status-drop {
|
|
padding: 2px 7px;
|
|
border-radius: 4px;
|
|
white-space: nowrap;
|
|
}
|
|
|
|
/* Clipping is your fault and fixable by playing softer or trimming. */
|
|
.status-clip {
|
|
color: var(--danger);
|
|
border: 1px solid var(--danger);
|
|
}
|
|
|
|
/* Missing the deadline is the pedal's, and means the chain is too much. */
|
|
.status-drop {
|
|
color: var(--dev);
|
|
border: 1px solid var(--dev);
|
|
}
|
|
|
|
/*
|
|
* An effect wants attention: the gate has closed, the compressor is
|
|
* compressing, the boost is folding.
|
|
*
|
|
* The asymmetric transition is the whole trick, and it is the same one
|
|
* the pedal's LED gets for free from being updated slowly. Switching on
|
|
* is instant and switching off takes a moment, so a single event is
|
|
* visible at all rather than a 40ms flicker nobody catches, while
|
|
* something happening constantly reads as solid instead of a strobe.
|
|
*/
|
|
.effect-card {
|
|
transition: box-shadow 400ms ease-out;
|
|
}
|
|
|
|
.effect-card.attention {
|
|
box-shadow: 0 0 0 1px var(--accent), 0 0 12px -2px var(--accent);
|
|
transition: box-shadow 0ms;
|
|
}
|
|
|
|
/*
|
|
* The pedal is not the hardware this firmware was built for - an early
|
|
* board answering on i2c, or the other eeprom. Filled rather than
|
|
* outlined like the other two, because it is not a transient to glance
|
|
* at: it will not clear itself, and the symptoms are quiet ones. Hover
|
|
* for which, or open MIDI Connection.
|
|
*/
|
|
#status-board.hidden {
|
|
display: none;
|
|
}
|
|
|
|
.status-board {
|
|
padding: 2px 7px;
|
|
border-radius: 4px;
|
|
white-space: nowrap;
|
|
color: var(--bg-color);
|
|
background: var(--danger);
|
|
}
|
|
|
|
/*
|
|
* An early board: information rather than a fault.
|
|
*
|
|
* Those still play - the pedal sets the TAC5112 up over i2c, and the
|
|
* eeprom geometry is worked out rather than compiled in, so a 2kbit part
|
|
* gets its one scene. Mono, because they never routed the second channel.
|
|
* Worth knowing which board is on the bench; not worth the red one.
|
|
*/
|
|
#status-early.hidden {
|
|
display: none;
|
|
}
|
|
|
|
.status-early {
|
|
padding: 2px 7px;
|
|
border-radius: 4px;
|
|
white-space: nowrap;
|
|
color: var(--text-muted);
|
|
border: 1px solid var(--text-muted);
|
|
}
|
|
|
|
/*
|
|
* The Signal Chain's meters.
|
|
*
|
|
* Deliberately plain text rather than bars. The number is what you act on
|
|
* - Level is set in dB and the floor is reported in dB, so the useful
|
|
* gesture is comparing two numbers, and a bar would make that harder
|
|
* rather than easier.
|
|
*
|
|
* Tabular figures so the digits do not shuffle sideways five times a
|
|
* second while you watch them.
|
|
*/
|
|
.meters {
|
|
font-size: 0.75rem;
|
|
color: var(--text-muted);
|
|
border-top: 1px solid var(--panel-border);
|
|
padding-top: 10px;
|
|
font-variant-numeric: tabular-nums;
|
|
font-feature-settings: "tnum";
|
|
}
|
|
|
|
/*
|
|
* A phone, or something near enough.
|
|
*
|
|
* Last in the file deliberately: a media query adds no specificity, so
|
|
* anything it overrides has to have been declared already or the plain
|
|
* rule wins and this looks like it does nothing.
|
|
*
|
|
* 20px of body padding either side of 20px of panel padding is 80px of
|
|
* nothing, which is a fifth of a 360px screen and reads as generous only
|
|
* on a desktop. This gives the content back the width it was spending
|
|
* on margins. It deliberately does not shrink any control: that is the
|
|
* direction the rest of this work is trying to go the other way.
|
|
*/
|
|
@media (max-width: 600px) {
|
|
body {
|
|
padding: 10px;
|
|
}
|
|
|
|
.glass-panel {
|
|
padding: 14px;
|
|
}
|
|
|
|
/* Horizontal only - the height of a select is its tap target */
|
|
select {
|
|
padding: 10px 12px;
|
|
}
|
|
}
|
|
|
|
/*
|
|
* A rule this scene did not set, shown because the control still does
|
|
* it - from the pedal-wide table or from the built-in defaults. Faded
|
|
* rather than hidden: "nothing here" and "nothing of its own here" are
|
|
* different, and only one of them means the footswitch does nothing.
|
|
*/
|
|
.rule-row.inherited {
|
|
opacity: 0.55;
|
|
}
|