Show a Pan node as a one-knob device in cyan (#392)
game-maker / gate (push) Successful in 4m13s
game-maker / suite (push) Failing after 15m23s

After the Gain, the Pitch and the Mix, the Pan is the next node that
is just one number. It now gets the same device in the graph instead
of a row for its pan.

- PAN_DEVICE: the Gain's 160x160 plate in cyan with one bipolar knob
  for Pan. The middle stands straight up, and the turn goes evenly to
  L and R, which are engraved at its ends like A and B on the Mix.
- PAN_TAPER in hardwareView is 1, so left and right are heard alike.
  Device and unit test read the same constant.
- PAN_ROW is exported from the core, so the knob has the node's range.
- A linked Pan greys the knob, as for the Gain.
- The test hook __gm.pan.panel returns the same shape as the Gain's.
- No instrument preset uses a Pan, so no preset columns change.

Tests:
- Unit hardware-view: 17 tests, 2 new (the turn, and L and R inside
  the plate and clear of the screws). Counter-check: PAN_TAPER 2
  turns the turn test red.
- e2e 34 (melektaus-atx): 135 checks, 1 new.
- Gate (melektaus-atx): 287 files, 4399 tests.
- Pictures looked at: Gain, Pitch, Mix and Pan in a row, Pan at -0.35.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
adminandClaude Opus 5.5 committed 2026-10-10 18:26:17 +02:00
1 parent fe4e0d9f1a
commit 389d9555fa
8 files changed
+102 -12

No files matched your search

+2 -1
View File
@@ -274,12 +274,13 @@ export function graphHooks({ state, editor }: HookContext) {
},
},
/**
* The panel a Gain node shows (#384), and a Pitch or a Mix (#390): whether it is shown and where,
* The panel a Gain node shows (#384), a Pitch or a Mix (#390), a Pan (#392): whether it is shown and where,
* its knob, the Mix's lever and the node's ports.
*/
gain: { panel: (nodeId: string) => knobPanel(nodeId) },
pitch: { panel: (nodeId: string) => knobPanel(nodeId) },
mix: { panel: (nodeId: string) => knobPanel(nodeId) },
pan: { panel: (nodeId: string) => knobPanel(nodeId) },
/**
* The panel a Chip node shows in the instrument graph (#386): whether it is shown and where, its
* knobs, which settings are idle, the picked wave, the lever, both screens and the node's ports.
+25 -2
View File
@@ -1,7 +1,7 @@
/**
* The devices with one big chrome knob, what their nodes show in the graph: the Gain (#384) on an
* orange plate, the Pitch (#390) on a green one, and the Mix (#390) on a blue one with a lever for
* its curve. All on the Compressor's plate (#379).
* orange plate, the Pitch (#390) on a green one, the Mix (#390) on a blue one with a lever for
* its curve, and the Pan (#392) on a cyan one. All on the Compressor's plate (#379).
*/
import { Graphics, type Text } from 'pixi.js';
import { Knob, Widget } from '../../ui';
@@ -9,9 +9,11 @@ import {
GAIN_ROW,
MIX_CURVE_ROW,
MIX_ROW,
PAN_ROW,
PITCH_ROW,
SoundFxGain,
SoundFxMix,
SoundFxPan,
SoundFxPitch,
} from '../../core/soundfx/nodes';
import { drawPlate, engrave, INK, Lever, placeEngraving, type PlateLook } from './CompressorPanel';
@@ -23,6 +25,7 @@ import {
knobEnds,
MIX_LAYOUT,
MIX_PANEL,
PAN_TAPER,
PITCH_TAPER,
} from './hardwareView';
@@ -114,6 +117,26 @@ export const MIX_DEVICE: KnobDevice = {
},
};
export const PAN_DEVICE: KnobDevice = {
type: SoundFxPan.type,
caption: 'PAN',
row: PAN_ROW,
taper: PAN_TAPER,
bipolar: true,
decimals: 2,
plate: {
stops: [
[0, 0x8ff0ea],
[0.5, 0x52d6cf],
[1, 0x2fb5ae],
],
rim: 0x1f8a85,
grain: 0x0e5c58,
},
arc: 0x07393a,
ends: ['L', 'R'],
};
export class KnobPanel extends Widget {
private readonly plate = new Graphics();
readonly knob: Knob;
+3
View File
@@ -96,6 +96,9 @@ export const GAIN_TAPER = 0.26;
/** The Pitch's knob (#390), ±64 semitones round 0: an octave either way takes 43 % of a half turn. */
export const PITCH_TAPER = 2;
/** The Pan's knob (#392), -1 to 1 round the middle: evenly, so left and right are heard alike. */
export const PAN_TAPER = 1;
/** How much lower the Mix's knob sits than the Gain's, for its curve lever above it. */
const MIX_LEVER_ROOM = 40;
+5 -3
View File
@@ -15,10 +15,10 @@ import { EnvelopeFace } from './EnvelopeFace';
import { EqPanel } from './EqPanel';
import { FilterPanel } from './FilterPanel';
import { KnobFace } from './KnobFace';
import { GAIN_DEVICE, MIX_DEVICE, PITCH_DEVICE } from './KnobPanel';
import { GAIN_DEVICE, MIX_DEVICE, PAN_DEVICE, PITCH_DEVICE } from './KnobPanel';
import { ReverbFace } from './ReverbFace';
/** The nodes drawn as a face of their own instead of rows (#380, #381, #384–#386, #388–#390), by type. */
/** The nodes drawn as a face of their own instead of rows (#380, #381, #384–#386, #388–#390, #392), by type. */
export function nodeFaceFor(state: EditorState, assetId: string, node: GraphNode): NodeFace | null {
if (node.type === InstrumentChip.type) return new ChipFace(state, assetId, node.id);
if (node.type === InstrumentEnvelope.type) return new EnvelopeFace(state, assetId, node.id);
@@ -26,7 +26,9 @@ export function nodeFaceFor(state: EditorState, assetId: string, node: GraphNode
if (node.type === SoundFxParametricEq.type)
return new CurveFace(new EqPanel(state, true), assetId, node.id);
if (node.type === SoundFxFilter.type) return new CurveFace(new FilterPanel(state, true), assetId, node.id);
const knobbed = [GAIN_DEVICE, PITCH_DEVICE, MIX_DEVICE].find((device) => device.type === node.type);
const knobbed = [GAIN_DEVICE, PITCH_DEVICE, MIX_DEVICE, PAN_DEVICE].find(
(device) => device.type === node.type,
);
if (knobbed) return new KnobFace(state, assetId, node.id, knobbed);
if (node.type === SoundFxReverb.type) return new ReverbFace(state, assetId, node.id);
return null;