Show Pitch and Mix nodes as one-knob devices in green and blue (#390)
The Gain has been a small device with one big knob since #384. The Pitch and the Mix are just as much a single number, so they now look and work the same way. The Mix also gets a lever for its curve, which until now was only a row in the inspector. - GainPanel and GainFace become KnobPanel and KnobFace. Each node is described by a KnobDevice: its plate, its row, how the knob turns, and an optional lever. The Gain looks as before. - Pitch: a green 160x160 plate with a bipolar knob for Semitones. 0 stands straight up and taper 2 puts an octave at 43 % of each half, so small steps near 0 are easy and the full +-64 is still there. - Mix: a blue 180x200 plate, the lever on top and the knob below. The knob has A and B engraved at the ends of its turn. The lever reads "crossfade" down and "both at center" up with its lamp lit. A flip is one undo step. - A wired Semitones or Mix greys the knob, as for the Gain. - The instrument presets give Pitch and Mix two columns. In the graph they are 281 and 261 units wide, more than one column of 240. - The test hooks __gm.pitch.panel and __gm.mix.panel return the same shape as __gm.gain.panel, plus the lever. Tests: - Unit hardware-view: 15 tests, 3 new (Pitch turn, lever above the knob and ends on the Mix plate). Counter-check: taper 1 turns the Pitch test red. - e2e 34 (melektaus-atx): 134 checks, 1 new. - Gate (melektaus-atx, together with #391): 287 files, 4397 tests. - Pictures looked at: Gain, Pitch and Mix in a row, Pitch at -24.5, Mix with both curves, the Bell preset without overlaps. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
e8021b1fd3
commit
e70954115c
12 files changed
+468
-184
No files matched your search
@@ -6,7 +6,7 @@ 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 { KnobPanel } from '../soundfx/KnobPanel';
|
||||
import { ReverbPanel } from '../soundfx/ReverbPanel';
|
||||
import { CurvePanel } from '../soundfx/CurvePanel';
|
||||
import type { EqEditor } from '../soundfx/EqEditor';
|
||||
@@ -16,6 +16,33 @@ import { centreOf, snapButton, type HookContext } from './hookHelpers';
|
||||
/** The node graphs: behaviors, state machines, particles and shaders. */
|
||||
export function graphHooks({ state, editor }: HookContext) {
|
||||
const { graphEditor } = editor;
|
||||
const knobPanel = (nodeId: string) => {
|
||||
const canvas = [editor.soundFxEditor.canvas, editor.instrumentEditor.canvas].find(
|
||||
(c) => c.faceOf(nodeId)?.widget instanceof KnobPanel,
|
||||
);
|
||||
const panel = canvas?.faceOf(nodeId)?.widget;
|
||||
if (!canvas || !(panel instanceof KnobPanel)) return null;
|
||||
const node = canvas.nodeScreen(nodeId);
|
||||
const ports = (keys: string[], isOutput: boolean) =>
|
||||
Object.fromEntries(keys.map((key) => [key, canvas.portGlobal(nodeId, key, isOutput)]));
|
||||
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),
|
||||
knob: {
|
||||
value: panel.knob.value,
|
||||
enabled: panel.knob.enabled,
|
||||
number: panel.knob.shown,
|
||||
dial: panel.knob.dialGlobal(),
|
||||
},
|
||||
lever: panel.lever
|
||||
? { on: panel.lever.lit, says: panel.lever.says.text, at: screenCentre(panel.lever) }
|
||||
: null,
|
||||
};
|
||||
};
|
||||
return {
|
||||
graph: {
|
||||
current: () => graphEditor.currentAssetId,
|
||||
@@ -247,33 +274,12 @@ export function graphHooks({ state, editor }: HookContext) {
|
||||
},
|
||||
},
|
||||
/**
|
||||
* The panel a Gain node shows in the sound FX graph (#384): whether it is shown and where, its
|
||||
* knob, and the node's ports.
|
||||
* The panel a Gain node shows (#384), and a Pitch or a Mix (#390): whether it is shown and where,
|
||||
* its knob, the Mix's lever and the node's ports.
|
||||
*/
|
||||
gain: {
|
||||
panel: (nodeId: string) => {
|
||||
const canvas = editor.soundFxEditor.canvas;
|
||||
const panel = canvas.faceOf(nodeId)?.widget;
|
||||
if (!(panel instanceof GainPanel)) return null;
|
||||
const node = canvas.nodeScreen(nodeId);
|
||||
const ports = (keys: string[], isOutput: boolean) =>
|
||||
Object.fromEntries(keys.map((key) => [key, canvas.portGlobal(nodeId, key, isOutput)]));
|
||||
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),
|
||||
knob: {
|
||||
value: panel.knob.value,
|
||||
enabled: panel.knob.enabled,
|
||||
number: panel.knob.shown,
|
||||
dial: panel.knob.dialGlobal(),
|
||||
},
|
||||
};
|
||||
},
|
||||
},
|
||||
gain: { panel: (nodeId: string) => knobPanel(nodeId) },
|
||||
pitch: { panel: (nodeId: string) => knobPanel(nodeId) },
|
||||
mix: { 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.
|
||||
|
||||
@@ -1,59 +0,0 @@
|
||||
import type { GraphNode } from '../../core/graph';
|
||||
import { findGraphAsset } from '../../core/model/Project';
|
||||
import { GAIN_ROW, SoundFxGain } from '../../core/soundfx/nodes';
|
||||
import type { NodeFace } from '../graph/nodeFace';
|
||||
import type { EditorState } from '../state';
|
||||
import { compressorWired } from './CompressorEditor';
|
||||
import { type GainHost, GainPanel } from './GainPanel';
|
||||
import { NodeParamDrag } from './nodeParamDrag';
|
||||
|
||||
/**
|
||||
* A Gain node drawn as its device (#384): one knob, turned right in the graph. It listens to
|
||||
* nothing, so it has nothing to move between changes.
|
||||
*/
|
||||
export class GainFace implements NodeFace, GainHost {
|
||||
readonly widget: GainPanel;
|
||||
private readonly drag: NodeParamDrag;
|
||||
|
||||
constructor(
|
||||
private readonly state: EditorState,
|
||||
private readonly assetId: string,
|
||||
private readonly nodeId: string,
|
||||
) {
|
||||
this.widget = new GainPanel(this);
|
||||
this.drag = new NodeParamDrag(state, 'gain-face');
|
||||
}
|
||||
|
||||
private node(): GraphNode | null {
|
||||
const node = findGraphAsset(this.state.project, this.assetId)?.graph.nodes.find(
|
||||
(n) => n.id === this.nodeId,
|
||||
);
|
||||
return node?.type === SoundFxGain.type ? node : null;
|
||||
}
|
||||
|
||||
sync(): void {
|
||||
const node = this.node();
|
||||
if (!node) return;
|
||||
const db = node.params[GAIN_ROW.key];
|
||||
this.widget.sync(
|
||||
typeof db === 'number' && Number.isFinite(db) ? db : GAIN_ROW.default,
|
||||
compressorWired(this.state.project, this.assetId, this.nodeId, GAIN_ROW.key),
|
||||
);
|
||||
}
|
||||
|
||||
tick(): boolean {
|
||||
return false;
|
||||
}
|
||||
|
||||
hide(): void {}
|
||||
|
||||
preview(value: number): void {
|
||||
const node = this.node();
|
||||
if (node) this.drag.preview(node, GAIN_ROW.key, value);
|
||||
}
|
||||
|
||||
commit(value: number): void {
|
||||
const node = this.node();
|
||||
if (node) this.drag.commit(this.assetId, node, GAIN_ROW.key, value);
|
||||
}
|
||||
}
|
||||
@@ -1,81 +0,0 @@
|
||||
/**
|
||||
* The Gain as a device (#384), what its node shows in the graph: the Compressor's plate (#379) in
|
||||
* orange, with one big chrome knob.
|
||||
*/
|
||||
import { Graphics, type Text } from 'pixi.js';
|
||||
import { Knob, Widget } from '../../ui';
|
||||
import { GAIN_ROW } from '../../core/soundfx/nodes';
|
||||
import { drawPlate, engrave, INK, placeEngraving, type PlateLook } from './CompressorPanel';
|
||||
import { GAIN_LAYOUT, GAIN_PANEL, GAIN_TAPER, KNOB_CAPTION } from './hardwareView';
|
||||
|
||||
/** What the panel asks of the node it sits in. */
|
||||
export interface GainHost {
|
||||
preview(value: number): void;
|
||||
commit(value: number): void;
|
||||
}
|
||||
|
||||
/** Brushed anodised orange. */
|
||||
const ORANGE: PlateLook = {
|
||||
stops: [
|
||||
[0, 0xffb25e],
|
||||
[0.5, 0xf39433],
|
||||
[1, 0xd8761c],
|
||||
],
|
||||
rim: 0xa85616,
|
||||
grain: 0x8a3e08,
|
||||
};
|
||||
/** The knob's value arc: the usual orange would vanish on this plate. */
|
||||
const DARK_ARC = 0x3b1a06;
|
||||
|
||||
export class GainPanel extends Widget {
|
||||
private readonly plate = new Graphics();
|
||||
readonly knob: Knob;
|
||||
private readonly caption: { ink: Text; edge: Text };
|
||||
|
||||
constructor(host: GainHost) {
|
||||
super();
|
||||
this.minWidth = GAIN_PANEL.w;
|
||||
this.minHeight = GAIN_PANEL.h;
|
||||
this.addChild(this.plate);
|
||||
this.caption = engrave(this, 'DB', 11);
|
||||
const row = GAIN_ROW;
|
||||
this.knob = new Knob('', row.default, {
|
||||
min: row.min ?? 0,
|
||||
max: row.max ?? 0,
|
||||
step: row.step ?? 1,
|
||||
taper: GAIN_TAPER,
|
||||
default: row.default,
|
||||
decimals: 1,
|
||||
size: GAIN_LAYOUT.knob.size,
|
||||
look: 'chrome',
|
||||
typed: false,
|
||||
ink: INK,
|
||||
arc: DARK_ARC,
|
||||
onInput: (value) => host.preview(value),
|
||||
onChange: (value) => host.commit(value),
|
||||
});
|
||||
this.addChild(this.knob);
|
||||
}
|
||||
|
||||
override measure(): { w: number; h: number } {
|
||||
return { ...GAIN_PANEL };
|
||||
}
|
||||
|
||||
/** The node's gain, and whether a link feeds it, which greys the knob. */
|
||||
sync(db: number, wired: boolean): void {
|
||||
this.knob.setValue(db);
|
||||
this.knob.setEnabled(!wired);
|
||||
}
|
||||
|
||||
override layout(): void {
|
||||
const spot = GAIN_LAYOUT.knob;
|
||||
const size = this.knob.measure();
|
||||
this.knob.position.set(Math.round(spot.x - size.w / 2), Math.round(spot.y - spot.size / 2));
|
||||
this.knob.resize(size.w, size.h);
|
||||
placeEngraving(this.caption, spot.x, spot.y - spot.size / 2 - KNOB_CAPTION);
|
||||
}
|
||||
|
||||
override redraw(): void {
|
||||
drawPlate(this.plate.clear(), GAIN_PANEL, ORANGE, GAIN_LAYOUT.screws);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
import type { GraphNode } from '../../core/graph';
|
||||
import { SetNodeParamCommand } from '../../core/commands/graphCommands';
|
||||
import { findGraphAsset } from '../../core/model/Project';
|
||||
import type { NodeFace } from '../graph/nodeFace';
|
||||
import type { EditorState } from '../state';
|
||||
import { compressorWired } from './CompressorEditor';
|
||||
import { type KnobDevice, type KnobHost, KnobPanel } from './KnobPanel';
|
||||
import { NodeParamDrag } from './nodeParamDrag';
|
||||
|
||||
/**
|
||||
* A node drawn as a device with one knob (#384, #390), turned right in the graph. It listens to
|
||||
* nothing, so it has nothing to move between changes.
|
||||
*/
|
||||
export class KnobFace implements NodeFace, KnobHost {
|
||||
readonly widget: KnobPanel;
|
||||
private readonly drag: NodeParamDrag;
|
||||
|
||||
constructor(
|
||||
private readonly state: EditorState,
|
||||
private readonly assetId: string,
|
||||
private readonly nodeId: string,
|
||||
private readonly device: KnobDevice,
|
||||
) {
|
||||
this.widget = new KnobPanel(this, device);
|
||||
this.drag = new NodeParamDrag(state, 'knob-face');
|
||||
}
|
||||
|
||||
private node(): GraphNode | null {
|
||||
const node = findGraphAsset(this.state.project, this.assetId)?.graph.nodes.find(
|
||||
(n) => n.id === this.nodeId,
|
||||
);
|
||||
return node?.type === this.device.type ? node : null;
|
||||
}
|
||||
|
||||
sync(): void {
|
||||
const node = this.node();
|
||||
if (!node) return;
|
||||
const lever = this.device.lever;
|
||||
const value = node.params[this.device.row.key];
|
||||
this.widget.sync(
|
||||
typeof value === 'number' && Number.isFinite(value) ? value : this.device.row.default,
|
||||
compressorWired(this.state.project, this.assetId, this.nodeId, this.device.row.key),
|
||||
lever !== undefined && node.params[lever.key] === lever.values[1],
|
||||
);
|
||||
}
|
||||
|
||||
tick(): boolean {
|
||||
return false;
|
||||
}
|
||||
|
||||
hide(): void {}
|
||||
|
||||
preview(value: number): void {
|
||||
const node = this.node();
|
||||
if (node) this.drag.preview(node, this.device.row.key, value);
|
||||
}
|
||||
|
||||
commit(value: number): void {
|
||||
const node = this.node();
|
||||
if (node) this.drag.commit(this.assetId, node, this.device.row.key, value);
|
||||
}
|
||||
|
||||
flip(on: boolean): void {
|
||||
const lever = this.device.lever;
|
||||
if (lever && this.node())
|
||||
this.state.execute(
|
||||
new SetNodeParamCommand(this.assetId, this.nodeId, lever.key, lever.values[on ? 1 : 0]),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,194 @@
|
||||
/**
|
||||
* 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).
|
||||
*/
|
||||
import { Graphics, type Text } from 'pixi.js';
|
||||
import { Knob, Widget } from '../../ui';
|
||||
import {
|
||||
GAIN_ROW,
|
||||
MIX_CURVE_ROW,
|
||||
MIX_ROW,
|
||||
PITCH_ROW,
|
||||
SoundFxGain,
|
||||
SoundFxMix,
|
||||
SoundFxPitch,
|
||||
} from '../../core/soundfx/nodes';
|
||||
import { drawPlate, engrave, INK, Lever, placeEngraving, type PlateLook } from './CompressorPanel';
|
||||
import {
|
||||
GAIN_LAYOUT,
|
||||
GAIN_PANEL,
|
||||
GAIN_TAPER,
|
||||
KNOB_CAPTION,
|
||||
knobEnds,
|
||||
MIX_LAYOUT,
|
||||
MIX_PANEL,
|
||||
PITCH_TAPER,
|
||||
} from './hardwareView';
|
||||
|
||||
/** What the panel asks of the node it sits in. */
|
||||
export interface KnobHost {
|
||||
preview(value: number): void;
|
||||
commit(value: number): void;
|
||||
/** The lever, on a device that has one. */
|
||||
flip(on: boolean): void;
|
||||
}
|
||||
|
||||
export interface KnobDevice {
|
||||
/** The node type it is drawn for. */
|
||||
type: string;
|
||||
/** Engraved above the knob. */
|
||||
caption: string;
|
||||
row: { key: string; default: number; min?: number; max?: number; step?: number };
|
||||
taper: number;
|
||||
bipolar?: boolean;
|
||||
decimals: number;
|
||||
plate: PlateLook;
|
||||
/** The knob's value arc, dark where the plate would swallow the usual orange. */
|
||||
arc: number;
|
||||
/** Engraved at the start and the end of the turn. */
|
||||
ends?: [string, string];
|
||||
/** A lever for a setting of two values, off and on, and what it `says` at each. */
|
||||
lever?: { caption: string; key: string; values: [string, string]; says: [string, string] };
|
||||
}
|
||||
|
||||
export const GAIN_DEVICE: KnobDevice = {
|
||||
type: SoundFxGain.type,
|
||||
caption: 'DB',
|
||||
row: GAIN_ROW,
|
||||
taper: GAIN_TAPER,
|
||||
decimals: 1,
|
||||
plate: {
|
||||
stops: [
|
||||
[0, 0xffb25e],
|
||||
[0.5, 0xf39433],
|
||||
[1, 0xd8761c],
|
||||
],
|
||||
rim: 0xa85616,
|
||||
grain: 0x8a3e08,
|
||||
},
|
||||
arc: 0x3b1a06,
|
||||
};
|
||||
|
||||
export const PITCH_DEVICE: KnobDevice = {
|
||||
type: SoundFxPitch.type,
|
||||
caption: 'SEMITONES',
|
||||
row: PITCH_ROW,
|
||||
taper: PITCH_TAPER,
|
||||
bipolar: true,
|
||||
decimals: 1,
|
||||
plate: {
|
||||
stops: [
|
||||
[0, 0x9be08a],
|
||||
[0.5, 0x6cc65a],
|
||||
[1, 0x4ea53f],
|
||||
],
|
||||
rim: 0x37802b,
|
||||
grain: 0x1f5a17,
|
||||
},
|
||||
arc: 0x123a0d,
|
||||
};
|
||||
|
||||
export const MIX_DEVICE: KnobDevice = {
|
||||
type: SoundFxMix.type,
|
||||
caption: 'MIX',
|
||||
row: MIX_ROW,
|
||||
taper: 1,
|
||||
decimals: 2,
|
||||
plate: {
|
||||
stops: [
|
||||
[0, 0x8ec5ff],
|
||||
[0.5, 0x5ea6f2],
|
||||
[1, 0x3f86d6],
|
||||
],
|
||||
rim: 0x2b64a8,
|
||||
grain: 0x173f75,
|
||||
},
|
||||
arc: 0x0d2748,
|
||||
ends: ['A', 'B'],
|
||||
lever: {
|
||||
caption: 'CURVE',
|
||||
key: MIX_CURVE_ROW.key,
|
||||
values: ['crossfade', 'both'],
|
||||
says: ['crossfade', 'both at center'],
|
||||
},
|
||||
};
|
||||
|
||||
export class KnobPanel extends Widget {
|
||||
private readonly plate = new Graphics();
|
||||
readonly knob: Knob;
|
||||
readonly lever: Lever | null = null;
|
||||
private readonly caption: { ink: Text; edge: Text };
|
||||
private readonly ends: { ink: Text; edge: Text }[];
|
||||
private readonly size: { w: number; h: number };
|
||||
private readonly spots: typeof GAIN_LAYOUT;
|
||||
|
||||
constructor(
|
||||
host: KnobHost,
|
||||
readonly device: KnobDevice,
|
||||
) {
|
||||
super();
|
||||
this.size = device.lever ? MIX_PANEL : GAIN_PANEL;
|
||||
this.spots = device.lever ? MIX_LAYOUT : GAIN_LAYOUT;
|
||||
this.minWidth = this.size.w;
|
||||
this.minHeight = this.size.h;
|
||||
this.addChild(this.plate);
|
||||
this.caption = engrave(this, device.caption, 11);
|
||||
this.ends = (device.ends ?? []).map((end) => engrave(this, end, 11));
|
||||
const row = device.row;
|
||||
this.knob = new Knob('', row.default, {
|
||||
min: row.min ?? 0,
|
||||
max: row.max ?? 0,
|
||||
step: row.step ?? 1,
|
||||
taper: device.taper,
|
||||
bipolar: device.bipolar,
|
||||
default: row.default,
|
||||
decimals: device.decimals,
|
||||
size: GAIN_LAYOUT.knob.size,
|
||||
look: 'chrome',
|
||||
typed: false,
|
||||
ink: INK,
|
||||
arc: device.arc,
|
||||
onInput: (value) => host.preview(value),
|
||||
onChange: (value) => host.commit(value),
|
||||
});
|
||||
this.addChild(this.knob);
|
||||
if (device.lever) {
|
||||
this.lever = new Lever(device.lever.caption, (on) => host.flip(on));
|
||||
this.addChild(this.lever);
|
||||
}
|
||||
}
|
||||
|
||||
override measure(): { w: number; h: number } {
|
||||
return { ...this.size };
|
||||
}
|
||||
|
||||
/** The node's number and whether a link feeds it, which greys the knob; and the lever's setting. */
|
||||
sync(value: number, wired: boolean, on = false): void {
|
||||
this.knob.setValue(value);
|
||||
this.knob.setEnabled(!wired);
|
||||
const lever = this.device.lever;
|
||||
if (this.lever && lever) this.lever.set(on, lever.says[on ? 1 : 0]);
|
||||
}
|
||||
|
||||
override layout(): void {
|
||||
const spot = this.spots.knob;
|
||||
const size = this.knob.measure();
|
||||
this.knob.position.set(Math.round(spot.x - size.w / 2), Math.round(spot.y - spot.size / 2));
|
||||
this.knob.resize(size.w, size.h);
|
||||
placeEngraving(this.caption, spot.x, spot.y - spot.size / 2 - KNOB_CAPTION);
|
||||
const ends = knobEnds(spot);
|
||||
const [a, b] = this.ends;
|
||||
if (a) placeEngraving(a, ends.a.x, ends.a.y - 7);
|
||||
if (b) placeEngraving(b, ends.b.x, ends.b.y - 7);
|
||||
if (this.lever) {
|
||||
const box = MIX_LAYOUT.curve;
|
||||
this.lever.position.set(box.x, box.y);
|
||||
this.lever.resize(box.w, box.h);
|
||||
}
|
||||
}
|
||||
|
||||
override redraw(): void {
|
||||
drawPlate(this.plate.clear(), this.size, this.device.plate, this.spots.screws);
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,7 @@
|
||||
* one size, how the needle of its meter reads and moves, which LEDs are lit, and the grain of the
|
||||
* brushed plate. Kept apart from the drawing so it is checked without a browser.
|
||||
*/
|
||||
import { KNOB_START, KNOB_SWEEP } from '../../ui/knob';
|
||||
import { clamp } from '../../ui/math';
|
||||
import type { Levels } from '../../viewport/spectrum';
|
||||
import { LEVEL_FLOOR_DB } from './compressorView';
|
||||
@@ -92,6 +93,32 @@ export const GAIN_LAYOUT = {
|
||||
*/
|
||||
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;
|
||||
|
||||
/** How much lower the Mix's knob sits than the Gain's, for its curve lever above it. */
|
||||
const MIX_LEVER_ROOM = 40;
|
||||
|
||||
/** The Mix (#390): the Gain's plate, a little wider and taller by its curve lever on top. */
|
||||
export const MIX_PANEL = { w: 180, h: GAIN_PANEL.h + MIX_LEVER_ROOM };
|
||||
|
||||
export const MIX_LAYOUT = {
|
||||
knob: { ...GAIN_LAYOUT.knob, x: MIX_PANEL.w / 2, y: GAIN_LAYOUT.knob.y + MIX_LEVER_ROOM },
|
||||
// Between the top screws, its lamp, lever and words in the middle.
|
||||
curve: { x: (MIX_PANEL.w - 130) / 2, y: 12, w: 130, h: 40 },
|
||||
screws: screwsOf(MIX_PANEL),
|
||||
};
|
||||
|
||||
/** Where the ends of a knob's turn are named, just outside its dial: `a` at the start, `b` at the end. */
|
||||
export function knobEnds(spot: KnobSpot): { a: { x: number; y: number }; b: { x: number; y: number } } {
|
||||
const r = spot.size / 2 + 8;
|
||||
const at = (angle: number) => ({
|
||||
x: Math.round(spot.x + Math.cos(angle) * r),
|
||||
y: Math.round(spot.y + Math.sin(angle) * r),
|
||||
});
|
||||
return { a: at(KNOB_START), b: at(KNOB_START + KNOB_SWEEP) };
|
||||
}
|
||||
|
||||
/** All a knob takes on the plate: its name above, its dial and its number below. */
|
||||
export function knobBox(spot: KnobSpot): Box {
|
||||
const w = Math.max(spot.size, KNOB_LEAST_WIDTH);
|
||||
|
||||
@@ -3,7 +3,6 @@ import { InstrumentChip, InstrumentEnvelope } from '../../core/instrument/nodes'
|
||||
import {
|
||||
SoundFxCompressor,
|
||||
SoundFxFilter,
|
||||
SoundFxGain,
|
||||
SoundFxParametricEq,
|
||||
SoundFxReverb,
|
||||
} from '../../core/soundfx/nodes';
|
||||
@@ -15,10 +14,11 @@ import { CurveFace } from './CurveFace';
|
||||
import { EnvelopeFace } from './EnvelopeFace';
|
||||
import { EqPanel } from './EqPanel';
|
||||
import { FilterPanel } from './FilterPanel';
|
||||
import { GainFace } from './GainFace';
|
||||
import { KnobFace } from './KnobFace';
|
||||
import { GAIN_DEVICE, MIX_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, #389), by type. */
|
||||
/** The nodes drawn as a face of their own instead of rows (#380, #381, #384–#386, #388–#390), 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,8 @@ 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);
|
||||
if (node.type === SoundFxGain.type) return new GainFace(state, assetId, node.id);
|
||||
const knobbed = [GAIN_DEVICE, PITCH_DEVICE, MIX_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;
|
||||
}
|
||||
Reference in new issue
Block a user