Show an Envelope node as a device with its ADSR curve (#388)
game-maker / gate (push) Successful in 4m24s
game-maker / suite (push) Successful in 15m50s

The Compressor, the Gain and the Chip are devices in the graph. The
Envelope was still a list of rows, and its shape was left to the
imagination of whoever read the numbers.

- The node is a plate of 520x280 in pale sea green. Its inputs are
  on the left, out and value on the right.
- A dark screen draws the curve over a note in amber. A letter sits
  above each stage, its time below it, and the sustain in percent
  above its line.
- The time on the screen is squeezed. Each stage is as wide as the
  root of its seconds, at least 3 %, and the hold has a fixed fifth.
  So 5 ms of attack still shows beside a release of 3 s.
- The falls use SETTLE, as the player does. With sustain 0 there is
  no hold and no release, and the screen says NOTE ENDS.
- Attack, Decay, Sustain and Release are chrome knobs. The times use
  taper 4, so 5 ms sits an eighth of the way and 1 s not quite half.
- From and To have no range a knob could turn through. They are
  NumberFields on the plate, typed in or scrubbed.
- The decay at sustain 1 and From/To while nothing takes value are
  faded. While a link takes value, the bezel says what it moves
  between, as VALUE 400 TO 3500. The UI font has no arrow.
- Instrument presets give an Envelope 3 columns of 240.
- Test hook __gm.envelope.panel(nodeId).

Tests:
- Unit envelope-view: 6 new tests for the plate, the knob turn, the
  idle settings and the curve. A linear decay turns two of them red.
- e2e 34 (atx): 1 new check on the Laser preset (device between its
  ports, curve ends with the decay, decay knob and scrubbed To field
  each one undo step).
- Probe pictures on stein2: Synth Brass preset, pad, pluck, organ
  and an Envelope whose value moves a Chip.
- Gate (atx): CHECK EXIT 0, 286 files, 4383 tests. The first run was
  red on the font test (#214) for an arrow in the bezel text.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
adminandClaude Opus 5.5 committed 2026-10-10 16:06:15 +02:00
1 parent ab37d3423a
commit c75e72d4ee
9 files changed
+887 -5

No files matched your search

+49
View File
@@ -4,6 +4,7 @@ import { soundLibrary } from '../../viewport/SoundLibrary';
import type { Widget } from '../../ui';
import { currentNodePalette } from '../graph/NodePalette';
import { ChipPanel } from '../soundfx/ChipPanel';
import { EnvelopePanel } from '../soundfx/EnvelopePanel';
import { CompressorPanel } from '../soundfx/CompressorPanel';
import { GainPanel } from '../soundfx/GainPanel';
import { CurvePanel } from '../soundfx/CurvePanel';
@@ -317,6 +318,54 @@ export function graphHooks({ state, editor }: HookContext) {
};
},
},
/**
* The panel an Envelope node shows in the instrument graph (#388): whether it is shown and
* where, its knobs and fields, which settings are idle, the curve's stages, what the screen says
* and the node's ports.
*/
envelope: {
panel: (nodeId: string) => {
const canvas = editor.instrumentEditor.canvas;
const panel = canvas.faceOf(nodeId)?.widget;
if (!(panel instanceof EnvelopePanel)) return null;
const node = canvas.nodeScreen(nodeId);
const ports = (keys: string[], isOutput: boolean) =>
Object.fromEntries(keys.map((key) => [key, canvas.portGlobal(nodeId, key, isOutput)]));
const trace = panel.screen.trace;
return {
shown: shownOnScreen(panel),
rect: screenRect(panel),
size: { w: panel.w, h: panel.h },
node: node?.rect ?? null,
inputs: ports(node?.inputs ?? [], false),
outputs: ports(node?.outputs ?? [], true),
knobs: Object.fromEntries(
Object.entries(panel.knobs).map(([key, knob]) => [
key,
{ value: knob.value, number: knob.shown, enabled: knob.enabled, dial: knob.dialGlobal() },
]),
),
fields: Object.fromEntries(
Object.entries(panel.fields).map(([key, field]) => [
key,
{ text: field.value, enabled: field.enabled, at: screenCentre(field) },
]),
),
idle: [...panel.idle],
/** The texts the screen shows, by name. */
screen: Object.fromEntries(panel.screen.says),
/** What the value moves between, above the screen; empty while nothing takes it. */
range: panel.range.text,
trace: trace && {
stages: trace.stages.map((s) => ({ key: s.key, from: s.from, to: s.to, seconds: s.seconds })),
sustain: trace.sustain,
ends: trace.ends,
/** The level where the curve stops. */
last: trace.points.at(-1)?.y ?? null,
},
};
},
},
instrument: {
current: () => editor.instrumentEditor.currentAssetId,
info: () => editor.instrumentEditor.shownInfo,