Reset a track slider with a double-click in the Song tab
The heads of the Song tab had four rough spots. Panned down, the first track had no line above it. The M and S letters sat high in their buttons. A volume or pan slider could only be dragged back to the middle by eye. Muting a track paled its volume slider. - The first track draws a border along its top, in the lanes and in its head, like the one below every track. - M and S sit in the middle of their buttons. A label of the heads can now carry a height and is centred in its box. - A double-click on a head's volume or pan sets it back to 0: the volume to 0 dB, the pan to the middle. It is one undo step of its own after the step the first click set. - Mute only lights its own button. The volume slider keeps its colour, so a muted head no longer looks out of step. Tests: - e2e 65 on melektaus-atx: ALL PASSED, 27 checks (+1) - typecheck, ESLint, Prettier: clean (also after the mute change) - screenshot on stein2: border above track 1, M and S with 4 px free above and below the letter in a 16 px button Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
5b96d0b329
commit
5fde3e975e
2 files changed
+40
-5
No files matched your search
@@ -120,6 +120,8 @@ interface Placed {
|
||||
x: number;
|
||||
y: number;
|
||||
w: number;
|
||||
/** Given, the text sits in the middle of the box `w` by `h`. */
|
||||
h?: number;
|
||||
dark?: boolean;
|
||||
}
|
||||
|
||||
@@ -657,6 +659,17 @@ export class SongTimeline extends Widget {
|
||||
{ tracks: asset.tracks.map((t) => (t.id === track.id ? { ...t, [part]: !t[part] } : t)) },
|
||||
label,
|
||||
);
|
||||
} else if (
|
||||
(part === 'volume' || part === 'pan') &&
|
||||
pressCount(e, `song-slider:${track.id}:${part}`) === 2
|
||||
) {
|
||||
// A double-click sets it back to the middle: 0 dB, or straight ahead.
|
||||
this.commit(
|
||||
asset,
|
||||
{ tracks: asset.tracks.map((t) => (t.id === track.id ? { ...t, [part]: 0 } : t)) },
|
||||
part === 'volume' ? 'Reset track volume' : 'Reset track pan',
|
||||
);
|
||||
this.onTracksPreview(this.asset()?.tracks ?? asset.tracks);
|
||||
} else if (part === 'volume' || part === 'pan') {
|
||||
const value = this.sliderAt(part, x);
|
||||
this.gesture = { kind: 'slider', track: track.id, part, value };
|
||||
@@ -1207,6 +1220,7 @@ export class SongTimeline extends Widget {
|
||||
this.lanesG
|
||||
.rect(0, y, lw, th)
|
||||
.fill(lit ? { color: c.accent, alpha: 0.08 } : { color: 0xffffff, alpha: i % 2 ? 0.015 : 0.035 });
|
||||
if (i === 0) this.lanesG.rect(0, y, lw, 1).fill({ color: c.border, alpha: 0.8 });
|
||||
this.lanesG.rect(0, y + th - 1, lw, 1).fill({ color: c.border, alpha: 0.8 });
|
||||
});
|
||||
// Above the first track and below the last, panned to, is empty.
|
||||
@@ -1358,14 +1372,15 @@ export class SongTimeline extends Widget {
|
||||
const lit = track.id === this.pickedTrack;
|
||||
const th = this.view.trackHeight;
|
||||
this.headsG.rect(0, y, HEAD_WIDTH - 1, th).fill(lit ? { color: c.accent, alpha: 0.18 } : c.panel);
|
||||
if (i === 0) this.headsG.rect(0, y, HEAD_WIDTH - 1, 1).fill(c.border);
|
||||
this.headsG.rect(0, y + th - 1, HEAD_WIDTH - 1, 1).fill(c.border);
|
||||
const { name, mute, solo, volume, pan } = HEAD_BOXES;
|
||||
labels.push({ text: track.name, x: name.x, y: y + name.y, w: name.w });
|
||||
this.toggle(mute, y, track.mute, MUTE_COLOR);
|
||||
this.toggle(solo, y, track.solo, SOLO_COLOR);
|
||||
labels.push(
|
||||
{ text: 'M', x: mute.x + 5, y: y + mute.y, w: mute.w, dark: track.mute },
|
||||
{ text: 'S', x: solo.x + 5, y: y + solo.y, w: solo.w, dark: track.solo },
|
||||
{ text: 'M', x: mute.x, y: y + mute.y, w: mute.w, h: mute.h, dark: track.mute },
|
||||
{ text: 'S', x: solo.x, y: y + solo.y, w: solo.w, h: solo.h, dark: track.solo },
|
||||
);
|
||||
if (th < SLIDERS_MIN_HEIGHT) return;
|
||||
const share = sliderShare(track.volume, TRACK_MIN_DB, TRACK_MAX_DB);
|
||||
@@ -1375,7 +1390,7 @@ export class SongTimeline extends Widget {
|
||||
.stroke({ width: 1, color: c.border, alignment: 1 });
|
||||
this.headsG
|
||||
.rect(volume.x + 1, y + volume.y + 1, Math.max(0, (volume.w - 2) * share), volume.h - 2)
|
||||
.fill({ color: c.accent, alpha: track.mute ? 0.35 : 0.8 });
|
||||
.fill({ color: c.accent, alpha: 0.8 });
|
||||
const unity = volume.x + volume.w * sliderShare(0, TRACK_MIN_DB, TRACK_MAX_DB);
|
||||
this.headsG.rect(unity, y + volume.y - 2, 1, volume.h + 4).fill({ color: c.text, alpha: 0.5 });
|
||||
this.headsG
|
||||
@@ -1414,7 +1429,12 @@ export class SongTimeline extends Widget {
|
||||
for (let cut = label.text.length; text.width > label.w && cut > 1; cut--)
|
||||
text.text = `${label.text.slice(0, cut - 1)}…`;
|
||||
text.visible = true;
|
||||
text.position.set(label.x, label.y);
|
||||
if (label.h === undefined) text.position.set(label.x, label.y);
|
||||
else
|
||||
text.position.set(
|
||||
Math.round(label.x + (label.w - text.width) / 2),
|
||||
Math.round(label.y + (label.h - text.height) / 2),
|
||||
);
|
||||
});
|
||||
for (let i = labels.length; i < pool.length; i++) pool[i]!.visible = false;
|
||||
}
|
||||
|
||||
+16
-1
@@ -10,7 +10,7 @@ import { LANES, pickAssetType, scaffoldBehavior } from './lib/scaffold.mjs';
|
||||
* copy behind it, Ctrl+E splits it where playing starts, Delete or its menu takes it away. A click
|
||||
* on nothing picks the song, an end under the pointer lights up, a long item marks its rounds, the
|
||||
* wheel zooms tracks or time, the strip above the ruler sets the loop. A head mutes, solos and sets
|
||||
* the volume of its track, its menu adds one. Space plays the song through the scheduler:
|
||||
* the volume of its track, a double-click sets a slider back to 0, its menu adds one. Space plays the song through the scheduler:
|
||||
* the pattern round and round at the song's tempo, the sound as a clip, each track on its own bus.
|
||||
* A pattern started meanwhile takes over, and a double-click opens a pattern item in its tab.
|
||||
* Nothing is heard in CI (muted browser): the witnesses are the model, the planned notes and clips
|
||||
@@ -407,6 +407,21 @@ export async function song(t) {
|
||||
(await json('__gm.song.info()')) === '5 tracks · 2 items',
|
||||
JSON.stringify(heads.tracks),
|
||||
);
|
||||
await mutate(`
|
||||
const song = __gm.state.project.assets.find((a) => a.id === ${JSON.stringify(id)});
|
||||
song.tracks[2].pan = 0.5;`);
|
||||
await sleep(100);
|
||||
p = await json("__gm.song.headAt(2, 'volume', 0.2)");
|
||||
await doubleClick(p.x, p.y);
|
||||
p = await json("__gm.song.headAt(2, 'pan', 0.9)");
|
||||
await doubleClick(p.x, p.y);
|
||||
await sleep(150);
|
||||
const reset = (await model()).tracks[2];
|
||||
check(
|
||||
'a double-click on a slider sets it back to 0: volume to 0 dB, pan to the middle',
|
||||
reset.volume === 0 && reset.pan === 0,
|
||||
JSON.stringify(reset),
|
||||
);
|
||||
await mutate(`
|
||||
const song = __gm.state.project.assets.find((a) => a.id === ${JSON.stringify(id)});
|
||||
song.tracks[2].volume = -6;
|
||||
|
||||
Reference in new issue
Block a user