Shrink a clip with the wave and drop it with a click beside (#357)
Zoomed out, the wave left room above and below it, but the bright file, the picked clip and its handles still took the whole height. And a click in the wave beside the picked clip kept it picked, so only the strip could take it back to the sound. - fileReach in waveView.ts is how far up and down the file reaches: full scale and its inset, never past the area. At level 1 it is the whole height, zoomed in too. - The bright file, the shade outside the clip and the two handles are drawn within that reach. - A click over the wave beside the picked clip picks the sound. Inside the clip or on a handle nothing changes, and a drag still spans the clip anew. Tests: - Unit wave-view: 11 green (1 new), a counter-check red - e2e 34 (melektaus-atx): ALL PASSED, 91 checks (1 new) - Gate (melektaus-atx): CHECK EXIT 0, 271 files, 4195 tests Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
42a3759d9d
commit
852dce5142
4 files changed
+53
-14
No files matched your search
@@ -21,6 +21,7 @@ import {
|
||||
type CutSpan,
|
||||
draggedSpan,
|
||||
edgeAt,
|
||||
fileReach,
|
||||
fitWave,
|
||||
levelMarks,
|
||||
levelOffset,
|
||||
@@ -65,7 +66,8 @@ const RAMP_STEPS = 32;
|
||||
|
||||
/**
|
||||
* A drag that spans: an edge of the clip `clipId`, that clip anew, or with `clipId` null a new clip.
|
||||
* `picks` is what a press in the strip selects when it does not move: a clip, or null for the sound.
|
||||
* `picks` is what a press selects when it does not move: in the strip a clip or null for the sound,
|
||||
* over the wave null beside the picked clip.
|
||||
*/
|
||||
type Gesture =
|
||||
| { kind: 'pan'; pointerX: number; panX: number }
|
||||
@@ -356,9 +358,13 @@ export class WaveCanvas extends Widget {
|
||||
};
|
||||
return;
|
||||
}
|
||||
// Over the wave a click beside the picked clip picks the sound again.
|
||||
const clip = this.pickedClip();
|
||||
const span = clip && this.clipSpan(clip, wave);
|
||||
this.gesture = { ...start, clipId: clip?.id ?? null, edge: span ? edgeAt(this.view, span, p.x) : null };
|
||||
const edge = span ? edgeAt(this.view, span, p.x) : null;
|
||||
const beside =
|
||||
span && !edge && (p.x < secondsToX(this.view, span.start) || p.x > secondsToX(this.view, span.end));
|
||||
this.gesture = { ...start, clipId: clip?.id ?? null, edge, picks: beside ? null : undefined };
|
||||
}
|
||||
|
||||
private onPointerMove(e: FederatedPointerEvent): void {
|
||||
@@ -487,16 +493,18 @@ export class WaveCanvas extends Widget {
|
||||
const mid = WAVE_TOP + wh / 2;
|
||||
const half = this.level * this.fullHalf;
|
||||
const room = wh / 2;
|
||||
// The file itself a shade lighter than what lies before and after it.
|
||||
// The file itself a shade lighter than what lies around it, as high as the wave is zoomed out.
|
||||
const reach = fileReach(this.level, this.fullHalf, WAVE_INSET, room);
|
||||
const left = Math.max(0, x0);
|
||||
const right = Math.min(w, x1);
|
||||
if (right > left) this.waveG.rect(left, WAVE_TOP, right - left, wh).fill({ color: c.panel, alpha: 0.5 });
|
||||
if (right > left)
|
||||
this.waveG.rect(left, mid - reach, right - left, 2 * reach).fill({ color: c.panel, alpha: 0.5 });
|
||||
if (right > left) this.waveG.rect(left, mid, right - left, 1).fill({ color: c.border });
|
||||
this.drawWave(wave, left, right, mid, half, room);
|
||||
this.drawRuler(left, right);
|
||||
this.drawStrip(asset, wave);
|
||||
const span = this.shownClipSpan();
|
||||
if (span) this.drawCut(wave, span, mid, half, room);
|
||||
if (span) this.drawCut(wave, span, mid, half, room, reach);
|
||||
this.drawScale(mid, room);
|
||||
if (this.playhead !== null) {
|
||||
const x = secondsToX(this.view, this.playhead);
|
||||
@@ -582,16 +590,19 @@ export class WaveCanvas extends Widget {
|
||||
this.syncStrip(placed);
|
||||
}
|
||||
|
||||
/** Dark outside the stretch, a handle at each edge, and the ramp at each edge inside the file. */
|
||||
private drawCut(wave: Wave, span: CutSpan, mid: number, half: number, room: number): void {
|
||||
/**
|
||||
* Dark outside the stretch, a handle at each edge, and the ramp at each edge inside the file – all
|
||||
* as far up and down from the middle as the file reaches.
|
||||
*/
|
||||
private drawCut(wave: Wave, span: CutSpan, mid: number, half: number, room: number, reach: number): void {
|
||||
const c = Theme.colors;
|
||||
const { h } = this;
|
||||
const w = this.areaWidth;
|
||||
const top = mid - reach;
|
||||
const a = secondsToX(this.view, span.start);
|
||||
const b = secondsToX(this.view, span.end);
|
||||
const shade = { color: 0x000000, alpha: 0.55 };
|
||||
if (a > 0) this.overlay.rect(0, WAVE_TOP, a, h - WAVE_TOP).fill(shade);
|
||||
if (b < w) this.overlay.rect(b, WAVE_TOP, w - b, h - WAVE_TOP).fill(shade);
|
||||
if (a > 0) this.overlay.rect(0, top, a, 2 * reach).fill(shade);
|
||||
if (b < w) this.overlay.rect(b, top, w - b, 2 * reach).fill(shade);
|
||||
const cut = cutOf(this.pickedClip()?.props ?? {});
|
||||
const range = cutRange(wave.channels[0]!.length, wave.rate, { ...cut, ...span });
|
||||
const ramp = (from: number, to: number, rising: boolean, curve: FadeCurve) => {
|
||||
@@ -620,12 +631,12 @@ export class WaveCanvas extends Widget {
|
||||
[b, 'end'],
|
||||
] as const) {
|
||||
const held = this.gesture?.kind === 'span' && this.gesture.edge === edge;
|
||||
this.overlay.rect(x - 1, WAVE_TOP, 2, h - WAVE_TOP).fill(held ? c.hot : c.accent);
|
||||
this.overlay.rect(x - 1, top, 2, 2 * reach).fill(held ? c.hot : c.accent);
|
||||
const toward = edge === 'start' ? 1 : -1;
|
||||
this.overlay
|
||||
.moveTo(x, WAVE_TOP)
|
||||
.lineTo(x + toward * 8, WAVE_TOP)
|
||||
.lineTo(x, WAVE_TOP + 10)
|
||||
.moveTo(x, top)
|
||||
.lineTo(x + toward * 8, top)
|
||||
.lineTo(x, top + 10)
|
||||
.closePath()
|
||||
.fill(held ? c.hot : c.accent);
|
||||
}
|
||||
|
||||
@@ -66,6 +66,14 @@ export function levelOffset(amplitude: number, level: number, half: number): num
|
||||
return amplitude * level * half;
|
||||
}
|
||||
|
||||
/**
|
||||
* Pixels from the middle the file and its clip reach up and down: full scale and `inset` around it,
|
||||
* zoomed out short of the area, zoomed in never past its `room` (#357).
|
||||
*/
|
||||
export function fileReach(level: number, half: number, inset: number, room: number): number {
|
||||
return Math.min(room, levelOffset(1, level, half) + inset);
|
||||
}
|
||||
|
||||
/**
|
||||
* The marks of the level scale: every 6 dB down from 0, as far from the middle as they are drawn.
|
||||
* Only those within `room` of the middle and at least `minPx` from it – 6 dB halve the distance,
|
||||
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
clampWave,
|
||||
draggedSpan,
|
||||
edgeAt,
|
||||
fileReach,
|
||||
fitWave,
|
||||
levelMarks,
|
||||
MAX_LEVEL,
|
||||
@@ -43,6 +44,13 @@ describe('the height of the wave (#357)', () => {
|
||||
expect(loud[0]).toBe(-24);
|
||||
expect(loud).not.toContain(0);
|
||||
});
|
||||
|
||||
it('lets the file and its clip reach as high as full scale zoomed out, never past the area', () => {
|
||||
// Full scale 100 px from the middle at level 1, 6 px inset, 106 px of room.
|
||||
expect(fileReach(1, 100, 6, 106)).toBe(106);
|
||||
expect(fileReach(0.25, 100, 6, 106)).toBe(31);
|
||||
expect(fileReach(4, 100, 6, 106)).toBe(106);
|
||||
});
|
||||
});
|
||||
|
||||
describe('the wave view', () => {
|
||||
|
||||
@@ -2016,6 +2016,18 @@ export async function sound(t) {
|
||||
(await picked()) === clipId,
|
||||
JSON.stringify({ undoneClip, redoneClip, pickedBeside, picked: await picked() }),
|
||||
);
|
||||
const inClip = await json('__gm.soundTab.at(0.2)');
|
||||
await click(inClip.x, inClip.y);
|
||||
const pickedInClip = await picked();
|
||||
const besideClip = await json('__gm.soundTab.at(0.6)');
|
||||
await click(besideClip.x, besideClip.y);
|
||||
const pickedBesideClip = await picked();
|
||||
await click(onBand.x, onBand.y);
|
||||
check(
|
||||
'a click over the wave inside the picked clip keeps it, beside it picks the sound',
|
||||
pickedInClip === clipId && pickedBesideClip === null && (await picked()) === clipId,
|
||||
JSON.stringify({ pickedInClip, pickedBesideClip, picked: await picked() }),
|
||||
);
|
||||
dragFrom = await json('__gm.soundTab.at(0.3)');
|
||||
dragTo = await json('__gm.soundTab.at(0.5)');
|
||||
await t.mouse('mouseMoved', dragFrom.x - 1, dragFrom.y);
|
||||
|
||||
Reference in new issue
Block a user