Refactor zoom in/out, add test coverage
continuous-integration/drone/push Build is passing
Details
continuous-integration/drone/push Build is passing
Details
This commit is contained in:
parent
aa4d235c0c
commit
d136e00c59
|
@ -17,6 +17,7 @@ import './App.css';
|
|||
import { firstValueFrom, from, Observable } from 'rxjs';
|
||||
import { first, map } from 'rxjs/operators';
|
||||
import millisFromDuration from './helpers/millisFromDuration';
|
||||
import { zoomViewportIn, zoomViewportOut } from './helpers/zoom';
|
||||
|
||||
// ported from backend, where should they live?
|
||||
const thumbnailWidth = 177;
|
||||
|
@ -24,6 +25,8 @@ const thumbnailHeight = 100;
|
|||
|
||||
const initialViewportCanvasPixels = 100;
|
||||
|
||||
const zoomFactor = 2;
|
||||
|
||||
const apiURL = process.env.REACT_APP_API_URL || 'http://localhost:8888';
|
||||
|
||||
// Frames represents a range of audio frames.
|
||||
|
@ -309,30 +312,32 @@ function App(): JSX.Element {
|
|||
if (mediaSet == null) {
|
||||
return;
|
||||
}
|
||||
if (viewport.start == viewport.end) {
|
||||
return;
|
||||
}
|
||||
setViewport({
|
||||
...viewport,
|
||||
end: viewport.end - Math.round((viewport.end - viewport.start) / 2),
|
||||
});
|
||||
|
||||
const newViewport = zoomViewportIn(
|
||||
viewport,
|
||||
mediaSet.audioFrames,
|
||||
selection,
|
||||
currentTimeToFrame(positionRef.current.currentTime),
|
||||
zoomFactor
|
||||
);
|
||||
|
||||
setViewport(newViewport);
|
||||
};
|
||||
|
||||
const handleZoomOut = () => {
|
||||
if (mediaSet == null) {
|
||||
return;
|
||||
}
|
||||
if (viewport.start == viewport.end) {
|
||||
return;
|
||||
}
|
||||
let end = viewport.end + Math.round(viewport.end - viewport.start);
|
||||
if (end > mediaSet.audioFrames) {
|
||||
end = mediaSet.audioFrames;
|
||||
}
|
||||
setViewport({
|
||||
...viewport,
|
||||
end: end,
|
||||
});
|
||||
|
||||
const newViewport = zoomViewportOut(
|
||||
viewport,
|
||||
mediaSet.audioFrames,
|
||||
selection,
|
||||
currentTimeToFrame(positionRef.current.currentTime),
|
||||
zoomFactor
|
||||
);
|
||||
|
||||
setViewport(newViewport);
|
||||
};
|
||||
|
||||
const setPositionFromFrame = useCallback(
|
||||
|
|
|
@ -0,0 +1,222 @@
|
|||
import { zoomViewportIn, zoomViewportOut } from './zoom';
|
||||
|
||||
// zf is the zoom factor.
|
||||
const zf = 2;
|
||||
const emptySelection = { start: 0, end: 0 };
|
||||
|
||||
describe('zoomViewportIn', () => {
|
||||
describe('when viewport start and end is equal', () => {
|
||||
it('returns the same viewport', () => {
|
||||
const newViewport = zoomViewportIn(
|
||||
{ start: 100, end: 100 },
|
||||
500,
|
||||
emptySelection,
|
||||
0,
|
||||
zf
|
||||
);
|
||||
|
||||
expect(newViewport).toEqual({ start: 100, end: 100 });
|
||||
});
|
||||
});
|
||||
|
||||
describe('with nothing selected', () => {
|
||||
it('centres the zoom on the playback position if possible', () => {
|
||||
const newViewport = zoomViewportIn(
|
||||
{ start: 100_000, end: 200_000 },
|
||||
500_000,
|
||||
emptySelection,
|
||||
50_000,
|
||||
zf
|
||||
);
|
||||
expect(newViewport).toEqual({ start: 25_000, end: 75_000 });
|
||||
});
|
||||
|
||||
it('offsets the new viewport if it overlaps the viewport minimum', () => {
|
||||
const newViewport = zoomViewportIn(
|
||||
{ start: 100_000, end: 200_000 },
|
||||
500_000,
|
||||
emptySelection,
|
||||
0,
|
||||
zf
|
||||
);
|
||||
expect(newViewport).toEqual({ start: 0, end: 50_000 });
|
||||
});
|
||||
|
||||
it('offsets the new viewport if it overlaps the viewport maximum', () => {
|
||||
const newViewport = zoomViewportIn(
|
||||
{ start: 100_000, end: 200_000 },
|
||||
500_000,
|
||||
emptySelection,
|
||||
490_000,
|
||||
zf
|
||||
);
|
||||
expect(newViewport).toEqual({ start: 450_000, end: 500_000 });
|
||||
});
|
||||
});
|
||||
|
||||
describe('with an active selection', () => {
|
||||
it('centres the new viewport on the selection if possible', () => {
|
||||
const newViewport = zoomViewportIn(
|
||||
{ start: 100_000, end: 200_000 },
|
||||
500_000,
|
||||
{ start: 120_000, end: 140_000 },
|
||||
0,
|
||||
zf
|
||||
);
|
||||
|
||||
expect(newViewport).toEqual({ start: 105_000, end: 155_000 });
|
||||
});
|
||||
|
||||
it('offsets the new viewport if it overlaps the viewport minimum', () => {
|
||||
const newViewport = zoomViewportIn(
|
||||
{ start: 100_000, end: 200_000 },
|
||||
500_000,
|
||||
{ start: 10_000, end: 20_000 },
|
||||
0,
|
||||
zf
|
||||
);
|
||||
|
||||
expect(newViewport).toEqual({ start: 0, end: 50_000 });
|
||||
});
|
||||
|
||||
it('offsets the new viewport if it overlaps the viewport maximum', () => {
|
||||
const newViewport = zoomViewportIn(
|
||||
{ start: 100_000, end: 200_000 },
|
||||
500_000,
|
||||
{ start: 480_000, end: 490_000 },
|
||||
0,
|
||||
zf
|
||||
);
|
||||
|
||||
expect(newViewport).toEqual({ start: 450_000, end: 500_000 });
|
||||
});
|
||||
|
||||
describe('when zooming beyond the selection', () => {
|
||||
it('disallows the zoom', () => {
|
||||
const newViewport = zoomViewportIn(
|
||||
{ start: 100_000, end: 200_000 },
|
||||
500_000,
|
||||
{ start: 110_000, end: 190_000 },
|
||||
0,
|
||||
zf
|
||||
);
|
||||
|
||||
expect(newViewport).toEqual({ start: 100_000, end: 200_000 });
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('zoomViewportOut', () => {
|
||||
describe('when viewport start and end is equal', () => {
|
||||
it('returns the same viewport', () => {
|
||||
const newViewport = zoomViewportOut(
|
||||
{ start: 100, end: 100 },
|
||||
500,
|
||||
emptySelection,
|
||||
0,
|
||||
zf
|
||||
);
|
||||
|
||||
expect(newViewport).toEqual({ start: 100, end: 100 });
|
||||
});
|
||||
});
|
||||
|
||||
describe('with nothing selected', () => {
|
||||
it('centres the zoom on the playback position if possible', () => {
|
||||
const newViewport = zoomViewportOut(
|
||||
{ start: 190_000, end: 210_000 },
|
||||
500_000,
|
||||
emptySelection,
|
||||
170_000,
|
||||
zf
|
||||
);
|
||||
expect(newViewport).toEqual({ start: 150_000, end: 190_000 });
|
||||
});
|
||||
|
||||
it('offsets the new viewport if it overlaps the viewport minimum', () => {
|
||||
const newViewport = zoomViewportOut(
|
||||
{ start: 190_000, end: 210_000 },
|
||||
500_000,
|
||||
emptySelection,
|
||||
10_000,
|
||||
zf
|
||||
);
|
||||
expect(newViewport).toEqual({ start: 0, end: 40_000 });
|
||||
});
|
||||
|
||||
it('offsets the new viewport if it overlaps the viewport maximum', () => {
|
||||
const newViewport = zoomViewportOut(
|
||||
{ start: 190_000, end: 210_000 },
|
||||
500_000,
|
||||
emptySelection,
|
||||
485_000,
|
||||
zf
|
||||
);
|
||||
|
||||
expect(newViewport).toEqual({ start: 460_000, end: 500_000 });
|
||||
});
|
||||
|
||||
it('refuses to zoom out beyond the available limits', () => {
|
||||
const newViewport = zoomViewportOut(
|
||||
{ start: 10_000, end: 490_000 },
|
||||
500_000,
|
||||
emptySelection,
|
||||
200_000,
|
||||
zf
|
||||
);
|
||||
|
||||
expect(newViewport).toEqual({ start: 0, end: 500_000 });
|
||||
});
|
||||
});
|
||||
|
||||
describe('with an active selection', () => {
|
||||
it('centres the new viewport on the selection if possible', () => {
|
||||
const newViewport = zoomViewportOut(
|
||||
{ start: 150_000, end: 170_000 },
|
||||
500_000,
|
||||
{ start: 120_000, end: 140_000 },
|
||||
0,
|
||||
zf
|
||||
);
|
||||
|
||||
expect(newViewport).toEqual({ start: 110_000, end: 150_000 });
|
||||
});
|
||||
|
||||
it('offsets the new viewport if it overlaps the viewport minimum', () => {
|
||||
const newViewport = zoomViewportOut(
|
||||
{ start: 190_000, end: 210_000 },
|
||||
500_000,
|
||||
{ start: 10_000, end: 20_000 },
|
||||
10_000,
|
||||
zf
|
||||
);
|
||||
|
||||
expect(newViewport).toEqual({ start: 0, end: 40_000 });
|
||||
});
|
||||
|
||||
it('offsets the new viewport if it overlaps the viewport minimum', () => {
|
||||
const newViewport = zoomViewportOut(
|
||||
{ start: 190_000, end: 210_000 },
|
||||
500_000,
|
||||
{ start: 495_000, end: 500_000 },
|
||||
0,
|
||||
zf
|
||||
);
|
||||
|
||||
expect(newViewport).toEqual({ start: 460_000, end: 500_000 });
|
||||
});
|
||||
|
||||
it('refuses to zoom out beyond the available limits', () => {
|
||||
const newViewport = zoomViewportOut(
|
||||
{ start: 10_000, end: 490_000 },
|
||||
500_000,
|
||||
{ start: 20_000, end: 480_000 },
|
||||
0,
|
||||
zf
|
||||
);
|
||||
|
||||
expect(newViewport).toEqual({ start: 0, end: 500_000 });
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,76 @@
|
|||
import { Frames } from '../App';
|
||||
|
||||
export function zoomViewportIn(
|
||||
viewport: Frames,
|
||||
numFrames: number,
|
||||
selection: Frames,
|
||||
position: number,
|
||||
factor: number
|
||||
): Frames {
|
||||
if (viewport.start == viewport.end) {
|
||||
return viewport;
|
||||
}
|
||||
|
||||
return zoom(
|
||||
Math.round((viewport.end - viewport.start) / factor),
|
||||
viewport,
|
||||
numFrames,
|
||||
selection,
|
||||
position
|
||||
);
|
||||
}
|
||||
|
||||
export function zoomViewportOut(
|
||||
viewport: Frames,
|
||||
numFrames: number,
|
||||
selection: Frames,
|
||||
position: number,
|
||||
factor: number
|
||||
): Frames {
|
||||
if (viewport.start == viewport.end) {
|
||||
return viewport;
|
||||
}
|
||||
|
||||
return zoom(
|
||||
Math.round((viewport.end - viewport.start) * factor),
|
||||
viewport,
|
||||
numFrames,
|
||||
selection,
|
||||
position
|
||||
);
|
||||
}
|
||||
|
||||
function zoom(
|
||||
newWidth: number,
|
||||
viewport: Frames,
|
||||
numFrames: number,
|
||||
selection: Frames,
|
||||
position: number
|
||||
): Frames {
|
||||
let newStart;
|
||||
|
||||
if (selection.start != selection.end) {
|
||||
const selectionWidth = selection.end - selection.start;
|
||||
|
||||
// disallow zooming beyond the selection:
|
||||
if (newWidth < selectionWidth) {
|
||||
return viewport;
|
||||
}
|
||||
|
||||
const selectionMidpoint = selection.end - selectionWidth / 2;
|
||||
newStart = selectionMidpoint - Math.round(newWidth / 2);
|
||||
} else {
|
||||
newStart = position - newWidth / 2;
|
||||
}
|
||||
|
||||
if (newStart < 0) {
|
||||
newStart = 0;
|
||||
}
|
||||
let newEnd = newStart + newWidth;
|
||||
if (newEnd > numFrames) {
|
||||
newEnd = numFrames;
|
||||
newStart = Math.max(0, newEnd - newWidth);
|
||||
}
|
||||
|
||||
return { start: newStart, end: newEnd };
|
||||
}
|
Loading…
Reference in New Issue