refactor(manifold): one applyCurve, one GROUP_COLOR — and make the knob honest
Phase 3 (L38, ST4). L38 is a REAL, VISIBLE behaviour change, and it is a bug fix rather than a wash. There were two divergent applyCurve implementations: model.ts's (`e = 0.25 + c*1.75`, linear at c≈0.43) drove the on-screen bars, while mapping.ts's spec-anchored version (0.5 = exactly linear, symmetric in log-exponent space) drove `mapOutput` — i.e. the value actually sent to MIDI/OSC/ VCV. So the display disagreed with the signal at every curve setting except the shared endpoint. mapping.ts's survives; model.ts's is deleted and re-exported. Displayed values now move visibly, and they now match what is transmitted. That unification exposed a third copy the finding did not mention: CurvePad.tsx inlined the same deleted formula to draw the curve-knob preview and its numeric readout. Left alone it would have matched NEITHER implementation — the knob would have drawn one response while the bars and the output used another. CurvePad now calls the same applyCurve, so knob, bars and transmitted value are one thing. The readout shows the actual exponent. ST4: GROUP_COLOR was duplicated across four files (Phase 1's ReadoutStrip deletion removed a fifth). All four now import the single map in model.ts — including OutputStage.tsx's OUT_GROUP_COLOR, which was outside the sweep's scope and would have been the last survivor. NOT done, deliberately: ST4 also invited replacing the fixed map with a hash-to-palette function, because its keys (formant/pitch/amp/filter/fx/mod) are JSX-era placeholders that do not match the real schema group strings (verb/sequencer/general/filterbank/delays/eq/midi/...), so most groups currently fall through to the accent colour. That is a visual redesign, not a deduplication, and it should be one deliberate change now that every consumer reads one map. Gates: typecheck clean, 17/17 unit tests, 33 e2e specs.
This commit is contained in:
parent
87daac4f0d
commit
ae2657a01b
5 changed files with 16 additions and 43 deletions
|
|
@ -22,6 +22,7 @@ import { VirtualJoystick, XYPad } from '../primitives';
|
||||||
import { Manifold } from './Manifold';
|
import { Manifold } from './Manifold';
|
||||||
import { OutputStage } from './OutputStage';
|
import { OutputStage } from './OutputStage';
|
||||||
import { MiniMeters } from './shared-ui';
|
import { MiniMeters } from './shared-ui';
|
||||||
|
import { GROUP_COLOR } from './model';
|
||||||
import type { MFMode, MFParam } from './model';
|
import type { MFMode, MFParam } from './model';
|
||||||
import type { FeedbackMarker, Pin } from './types';
|
import type { FeedbackMarker, Pin } from './types';
|
||||||
|
|
||||||
|
|
@ -29,15 +30,6 @@ const SNAPS = [0.14, 0.33, 0.5, 0.66, 0.86];
|
||||||
const MAGNET = 0.026;
|
const MAGNET = 0.026;
|
||||||
const SHUT = 0.1;
|
const SHUT = 0.1;
|
||||||
|
|
||||||
const GC: Record<string, string> = {
|
|
||||||
formant: '--accent',
|
|
||||||
pitch: '--accent-2',
|
|
||||||
amp: '--good',
|
|
||||||
filter: '--warn',
|
|
||||||
fx: '--info',
|
|
||||||
mod: '--accent-3',
|
|
||||||
};
|
|
||||||
|
|
||||||
const CORNERS: Record<string, CSSProperties> = {
|
const CORNERS: Record<string, CSSProperties> = {
|
||||||
tl: { top: 62, left: 14 },
|
tl: { top: 62, left: 14 },
|
||||||
tr: { top: 62, right: 14 },
|
tr: { top: 62, right: 14 },
|
||||||
|
|
@ -261,7 +253,7 @@ export function CompositeStage({
|
||||||
>
|
>
|
||||||
{params.map((p, i) => {
|
{params.map((p, i) => {
|
||||||
const eff = values[i] ?? 0;
|
const eff = values[i] ?? 0;
|
||||||
const gc = `var(${GC[p.group] || '--accent'})`;
|
const gc = `var(${GROUP_COLOR[p.group] || '--accent'})`;
|
||||||
const dim = p.status === 'off';
|
const dim = p.status === 'off';
|
||||||
const set = (cx: number, el: HTMLDivElement) => {
|
const set = (cx: number, el: HTMLDivElement) => {
|
||||||
const r = el.getBoundingClientRect();
|
const r = el.getBoundingClientRect();
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,14 @@
|
||||||
/**
|
/**
|
||||||
* CurvePad — a square response-curve plot. Vertical drag reshapes the curve.
|
* CurvePad — a square response-curve plot. Vertical drag reshapes the curve.
|
||||||
* `curve` is 0..1 where ~0.43 reads as linear; mirrors the engine's applyCurve.
|
* `curve` is 0..1 where 0.5 is exactly linear.
|
||||||
|
*
|
||||||
|
* The preview draws the SAME `applyCurve` the output path uses
|
||||||
|
* (`backends/mapping.ts`) rather than re-deriving an exponent, so the knob, the
|
||||||
|
* on-screen bars and the value actually sent to a backend can never disagree.
|
||||||
* Ported from `CurvePad.jsx`.
|
* Ported from `CurvePad.jsx`.
|
||||||
*/
|
*/
|
||||||
import { useEffect, useRef } from 'react';
|
import { useEffect, useRef } from 'react';
|
||||||
|
import { applyCurve } from '../backends/mapping';
|
||||||
import type { PointerEvent as ReactPointerEvent } from 'react';
|
import type { PointerEvent as ReactPointerEvent } from 'react';
|
||||||
|
|
||||||
export interface CurvePadProps {
|
export interface CurvePadProps {
|
||||||
|
|
@ -48,14 +53,13 @@ export function CurvePad({ curve = 0.5, onChange, size = 116 }: CurvePadProps) {
|
||||||
ctx.setLineDash([]);
|
ctx.setLineDash([]);
|
||||||
const css = getComputedStyle(cv);
|
const css = getComputedStyle(cv);
|
||||||
const accent = css.getPropertyValue('--accent').trim() || '#ff6a00';
|
const accent = css.getPropertyValue('--accent').trim() || '#ff6a00';
|
||||||
const e = 0.25 + curve * 1.75;
|
|
||||||
ctx.strokeStyle = accent;
|
ctx.strokeStyle = accent;
|
||||||
ctx.lineWidth = 2;
|
ctx.lineWidth = 2;
|
||||||
ctx.beginPath();
|
ctx.beginPath();
|
||||||
const pad = 3;
|
const pad = 3;
|
||||||
for (let p = 0; p <= 80; p++) {
|
for (let p = 0; p <= 80; p++) {
|
||||||
const xv = p / 80;
|
const xv = p / 80;
|
||||||
const yv = Math.pow(xv, e);
|
const yv = applyCurve(xv, curve);
|
||||||
const px = pad + xv * (size - 2 * pad);
|
const px = pad + xv * (size - 2 * pad);
|
||||||
const py = size - pad - yv * (size - 2 * pad);
|
const py = size - pad - yv * (size - 2 * pad);
|
||||||
if (p === 0) ctx.moveTo(px, py);
|
if (p === 0) ctx.moveTo(px, py);
|
||||||
|
|
@ -95,7 +99,7 @@ export function CurvePad({ curve = 0.5, onChange, size = 116 }: CurvePadProps) {
|
||||||
<span
|
<span
|
||||||
style={{ fontSize: 10, color: 'var(--fg-dim)', fontVariantNumeric: 'tabular-nums' }}
|
style={{ fontSize: 10, color: 'var(--fg-dim)', fontVariantNumeric: 'tabular-nums' }}
|
||||||
>
|
>
|
||||||
{(0.25 + curve * 1.75).toFixed(2)}
|
{Math.pow(2, 4 * (curve - 0.5)).toFixed(2)}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<canvas
|
<canvas
|
||||||
|
|
|
||||||
|
|
@ -6,16 +6,9 @@
|
||||||
import { useRef, useState } from 'react';
|
import { useRef, useState } from 'react';
|
||||||
import type { PointerEvent as ReactPointerEvent } from 'react';
|
import type { PointerEvent as ReactPointerEvent } from 'react';
|
||||||
import type { MFParam, ParamStatus } from './model';
|
import type { MFParam, ParamStatus } from './model';
|
||||||
|
import { GROUP_COLOR } from './model';
|
||||||
import { OutputEditor } from './OutputEditor';
|
import { OutputEditor } from './OutputEditor';
|
||||||
|
|
||||||
const OUT_GROUP_COLOR: Record<string, string> = {
|
|
||||||
formant: '--accent',
|
|
||||||
pitch: '--accent-2',
|
|
||||||
amp: '--good',
|
|
||||||
filter: '--warn',
|
|
||||||
fx: '--info',
|
|
||||||
mod: '--accent-3',
|
|
||||||
};
|
|
||||||
const OUT_NEXT: Record<ParamStatus, ParamStatus> = { off: 'fixed', fixed: 'live', live: 'off' };
|
const OUT_NEXT: Record<ParamStatus, ParamStatus> = { off: 'fixed', fixed: 'live', live: 'off' };
|
||||||
|
|
||||||
export interface OutputStageProps {
|
export interface OutputStageProps {
|
||||||
|
|
@ -94,7 +87,7 @@ export function OutputStage({ params, values, onChange, compact = false }: Outpu
|
||||||
>
|
>
|
||||||
{params.map((p, i) => {
|
{params.map((p, i) => {
|
||||||
const eff = values[i] ?? 0;
|
const eff = values[i] ?? 0;
|
||||||
const gc = `var(${OUT_GROUP_COLOR[p.group] || '--accent'})`;
|
const gc = `var(${GROUP_COLOR[p.group] || '--accent'})`;
|
||||||
const dim = p.status === 'off';
|
const dim = p.status === 'off';
|
||||||
const placeRight = i > params.length - 4;
|
const placeRight = i > params.length - 4;
|
||||||
return (
|
return (
|
||||||
|
|
@ -119,7 +112,7 @@ export function OutputStage({ params, values, onChange, compact = false }: Outpu
|
||||||
color:
|
color:
|
||||||
p.status === 'live'
|
p.status === 'live'
|
||||||
? 'var(--fg-mute)'
|
? 'var(--fg-mute)'
|
||||||
: `var(${OUT_GROUP_COLOR[p.group] || '--accent'})`,
|
: `var(${GROUP_COLOR[p.group] || '--accent'})`,
|
||||||
overflow: 'hidden',
|
overflow: 'hidden',
|
||||||
whiteSpace: 'nowrap',
|
whiteSpace: 'nowrap',
|
||||||
textOverflow: 'ellipsis',
|
textOverflow: 'ellipsis',
|
||||||
|
|
|
||||||
|
|
@ -6,17 +6,9 @@
|
||||||
* Manifold ships a single "composite" altitude). MiniMeters survives — it is
|
* Manifold ships a single "composite" altitude). MiniMeters survives — it is
|
||||||
* the live glanceable output readout used by CompositeStage's minimap.
|
* the live glanceable output readout used by CompositeStage's minimap.
|
||||||
*/
|
*/
|
||||||
|
import { GROUP_COLOR } from './model';
|
||||||
import type { MFParam } from './model';
|
import type { MFParam } from './model';
|
||||||
|
|
||||||
const MM_GROUP_COLOR: Record<string, string> = {
|
|
||||||
formant: '--accent',
|
|
||||||
pitch: '--accent-2',
|
|
||||||
amp: '--good',
|
|
||||||
filter: '--warn',
|
|
||||||
fx: '--info',
|
|
||||||
mod: '--accent-3',
|
|
||||||
};
|
|
||||||
|
|
||||||
/** MiniMeters — glanceable read-only output bars (no interaction). */
|
/** MiniMeters — glanceable read-only output bars (no interaction). */
|
||||||
export function MiniMeters({ params, values }: { params: MFParam[]; values: number[] }) {
|
export function MiniMeters({ params, values }: { params: MFParam[]; values: number[] }) {
|
||||||
return (
|
return (
|
||||||
|
|
@ -41,7 +33,7 @@ export function MiniMeters({ params, values }: { params: MFParam[]; values: numb
|
||||||
right: 0,
|
right: 0,
|
||||||
bottom: 0,
|
bottom: 0,
|
||||||
height: `${v * 100}%`,
|
height: `${v * 100}%`,
|
||||||
background: `var(${MM_GROUP_COLOR[params[i]?.group] || '--accent'})`,
|
background: `var(${GROUP_COLOR[params[i]?.group] || '--accent'})`,
|
||||||
opacity: 0.3 + v * 0.6,
|
opacity: 0.3 + v * 0.6,
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@
|
||||||
*/
|
*/
|
||||||
import { useRef } from 'react';
|
import { useRef } from 'react';
|
||||||
import type { CSSProperties, PointerEvent as ReactPointerEvent } from 'react';
|
import type { CSSProperties, PointerEvent as ReactPointerEvent } from 'react';
|
||||||
|
import { GROUP_COLOR } from '../console/model';
|
||||||
import type { MFParam, ParamStatus } from '../console/model';
|
import type { MFParam, ParamStatus } from '../console/model';
|
||||||
import { CurvePad } from '../console/CurvePad';
|
import { CurvePad } from '../console/CurvePad';
|
||||||
|
|
||||||
|
|
@ -18,15 +19,6 @@ const STATE_META: { v: ParamStatus; label: string; color: string }[] = [
|
||||||
{ v: 'live', label: 'live', color: 'var(--accent)' },
|
{ v: 'live', label: 'live', color: 'var(--accent)' },
|
||||||
];
|
];
|
||||||
|
|
||||||
const GROUP_COLOR: Record<string, string> = {
|
|
||||||
formant: '--accent',
|
|
||||||
pitch: '--accent-2',
|
|
||||||
amp: '--good',
|
|
||||||
filter: '--warn',
|
|
||||||
fx: '--info',
|
|
||||||
mod: '--accent-3',
|
|
||||||
};
|
|
||||||
|
|
||||||
/** A compact dual-thumb min/max range (min blue, max orange — dock-spec §3.1). */
|
/** A compact dual-thumb min/max range (min blue, max orange — dock-spec §3.1). */
|
||||||
function DualRange({
|
function DualRange({
|
||||||
min,
|
min,
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue