Reset a track slider with a double-click in the Song tab
game-maker / gate (push) Successful in 3m30s
game-maker / suite (push) Successful in 16m37s

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:
adminandClaude Opus 5.5 committed 2026-10-06 19:36:07 +02:00
1 parent 5b96d0b329
commit 5fde3e975e
2 files changed
+40 -5

No files matched your search

+24 -4
View File
@@ -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;
}