@drop-ai/ui-utils 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +10 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +11 -0
- package/dist/index.js.map +1 -0
- package/dist/playhead/PlayheadTracker.d.ts +45 -0
- package/dist/playhead/PlayheadTracker.d.ts.map +1 -0
- package/dist/playhead/PlayheadTracker.js +65 -0
- package/dist/playhead/PlayheadTracker.js.map +1 -0
- package/dist/ruler/RulerTicks.d.ts +41 -0
- package/dist/ruler/RulerTicks.d.ts.map +1 -0
- package/dist/ruler/RulerTicks.js +109 -0
- package/dist/ruler/RulerTicks.js.map +1 -0
- package/dist/viewport/TimelineViewport.d.ts +80 -0
- package/dist/viewport/TimelineViewport.d.ts.map +1 -0
- package/dist/viewport/TimelineViewport.js +215 -0
- package/dist/viewport/TimelineViewport.js.map +1 -0
- package/dist/viewport/TrackLayout.d.ts +41 -0
- package/dist/viewport/TrackLayout.d.ts.map +1 -0
- package/dist/viewport/TrackLayout.js +101 -0
- package/dist/viewport/TrackLayout.js.map +1 -0
- package/dist/waveform/computePeaks.d.ts +42 -0
- package/dist/waveform/computePeaks.d.ts.map +1 -0
- package/dist/waveform/computePeaks.js +74 -0
- package/dist/waveform/computePeaks.js.map +1 -0
- package/dist/waveform/renderWaveform.d.ts +53 -0
- package/dist/waveform/renderWaveform.d.ts.map +1 -0
- package/dist/waveform/renderWaveform.js +118 -0
- package/dist/waveform/renderWaveform.js.map +1 -0
- package/package.json +52 -0
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export { TimelineViewport } from './viewport/TimelineViewport';
|
|
2
|
+
export { TrackLayout } from './viewport/TrackLayout';
|
|
3
|
+
export type { TrackLayoutEntry } from './viewport/TrackLayout';
|
|
4
|
+
export { computePeaks, computePeaksFromSamples, recommendResolution } from './waveform/computePeaks';
|
|
5
|
+
export { renderWaveform, renderStereoWaveform, renderWaveformFromSamples } from './waveform/renderWaveform';
|
|
6
|
+
export type { WaveformRenderOptions } from './waveform/renderWaveform';
|
|
7
|
+
export { computeRulerTicks } from './ruler/RulerTicks';
|
|
8
|
+
export type { RulerTick, RulerTicksOptions } from './ruler/RulerTicks';
|
|
9
|
+
export { PlayheadTracker } from './playhead/PlayheadTracker';
|
|
10
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,gBAAgB,EAAE,MAAM,6BAA6B,CAAC;AAC/D,OAAO,EAAE,WAAW,EAAE,MAAM,wBAAwB,CAAC;AACrD,YAAY,EAAE,gBAAgB,EAAE,MAAM,wBAAwB,CAAC;AAG/D,OAAO,EAAE,YAAY,EAAE,uBAAuB,EAAE,mBAAmB,EAAE,MAAM,yBAAyB,CAAC;AACrG,OAAO,EAAE,cAAc,EAAE,oBAAoB,EAAE,yBAAyB,EAAE,MAAM,2BAA2B,CAAC;AAC5G,YAAY,EAAE,qBAAqB,EAAE,MAAM,2BAA2B,CAAC;AAGvE,OAAO,EAAE,iBAAiB,EAAE,MAAM,oBAAoB,CAAC;AACvD,YAAY,EAAE,SAAS,EAAE,iBAAiB,EAAE,MAAM,oBAAoB,CAAC;AAGvE,OAAO,EAAE,eAAe,EAAE,MAAM,4BAA4B,CAAC"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
// ─── Viewport ───────────────────────────────────────────────────────────────
|
|
2
|
+
export { TimelineViewport } from './viewport/TimelineViewport';
|
|
3
|
+
export { TrackLayout } from './viewport/TrackLayout';
|
|
4
|
+
// ─── Waveform ───────────────────────────────────────────────────────────────
|
|
5
|
+
export { computePeaks, computePeaksFromSamples, recommendResolution } from './waveform/computePeaks';
|
|
6
|
+
export { renderWaveform, renderStereoWaveform, renderWaveformFromSamples } from './waveform/renderWaveform';
|
|
7
|
+
// ─── Ruler ──────────────────────────────────────────────────────────────────
|
|
8
|
+
export { computeRulerTicks } from './ruler/RulerTicks';
|
|
9
|
+
// ─── Playhead ───────────────────────────────────────────────────────────────
|
|
10
|
+
export { PlayheadTracker } from './playhead/PlayheadTracker';
|
|
11
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,+EAA+E;AAC/E,OAAO,EAAE,gBAAgB,EAAE,MAAM,6BAA6B,CAAC;AAC/D,OAAO,EAAE,WAAW,EAAE,MAAM,wBAAwB,CAAC;AAGrD,+EAA+E;AAC/E,OAAO,EAAE,YAAY,EAAE,uBAAuB,EAAE,mBAAmB,EAAE,MAAM,yBAAyB,CAAC;AACrG,OAAO,EAAE,cAAc,EAAE,oBAAoB,EAAE,yBAAyB,EAAE,MAAM,2BAA2B,CAAC;AAG5G,+EAA+E;AAC/E,OAAO,EAAE,iBAAiB,EAAE,MAAM,oBAAoB,CAAC;AAGvD,+EAA+E;AAC/E,OAAO,EAAE,eAAe,EAAE,MAAM,4BAA4B,CAAC"}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import type { FrameCount } from '@drop-ai/core';
|
|
2
|
+
import { Signal } from '@drop-ai/core';
|
|
3
|
+
import type { TimelineViewport } from '../viewport/TimelineViewport';
|
|
4
|
+
/**
|
|
5
|
+
* Tracks the transport position via `requestAnimationFrame` and converts
|
|
6
|
+
* the current frame into a pixel X coordinate.
|
|
7
|
+
*
|
|
8
|
+
* Framework-agnostic — connect to the `moved` signal to drive your UI.
|
|
9
|
+
*
|
|
10
|
+
* ```ts
|
|
11
|
+
* const tracker = new PlayheadTracker(viewport, () => engine.session.transportFrame);
|
|
12
|
+
* tracker.moved.connect(({ frame, x }) => {
|
|
13
|
+
* playheadEl.style.transform = `translateX(${x}px)`;
|
|
14
|
+
* });
|
|
15
|
+
* tracker.start();
|
|
16
|
+
* ```
|
|
17
|
+
*/
|
|
18
|
+
export declare class PlayheadTracker {
|
|
19
|
+
/** Emitted on each animation frame with the current position. */
|
|
20
|
+
readonly moved: Signal<{
|
|
21
|
+
frame: FrameCount;
|
|
22
|
+
x: number;
|
|
23
|
+
}>;
|
|
24
|
+
private _viewport;
|
|
25
|
+
private _getFrame;
|
|
26
|
+
private _rafId;
|
|
27
|
+
private _followPlayhead;
|
|
28
|
+
/**
|
|
29
|
+
* @param viewport The timeline viewport for frame → pixel conversion.
|
|
30
|
+
* @param getFrame A callback that returns the current transport frame.
|
|
31
|
+
* Typically `() => engine.session.transportFrame`.
|
|
32
|
+
*/
|
|
33
|
+
constructor(viewport: TimelineViewport, getFrame: () => FrameCount);
|
|
34
|
+
get isRunning(): boolean;
|
|
35
|
+
/** When true, the viewport will auto-scroll to keep the playhead visible. */
|
|
36
|
+
get followPlayhead(): boolean;
|
|
37
|
+
set followPlayhead(value: boolean);
|
|
38
|
+
/** Start the animation loop. */
|
|
39
|
+
start(): void;
|
|
40
|
+
/** Stop the animation loop. */
|
|
41
|
+
stop(): void;
|
|
42
|
+
/** Dispose the tracker and release resources. */
|
|
43
|
+
dispose(): void;
|
|
44
|
+
}
|
|
45
|
+
//# sourceMappingURL=PlayheadTracker.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"PlayheadTracker.d.ts","sourceRoot":"","sources":["../../src/playhead/PlayheadTracker.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,eAAe,CAAC;AAChD,OAAO,EAAE,MAAM,EAAE,MAAM,eAAe,CAAC;AACvC,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,8BAA8B,CAAC;AAMrE;;;;;;;;;;;;;GAaG;AACH,qBAAa,eAAe;IACxB,iEAAiE;IACjE,SAAgB,KAAK;eAAuB,UAAU;WAAK,MAAM;OAAM;IAEvE,OAAO,CAAC,SAAS,CAAmB;IACpC,OAAO,CAAC,SAAS,CAAmB;IACpC,OAAO,CAAC,MAAM,CAAuB;IACrC,OAAO,CAAC,eAAe,CAAkB;IAEzC;;;;OAIG;gBACS,QAAQ,EAAE,gBAAgB,EAAE,QAAQ,EAAE,MAAM,UAAU;IAKlE,IAAI,SAAS,IAAI,OAAO,CAAiC;IAEzD,6EAA6E;IAC7E,IAAI,cAAc,IAAI,OAAO,CAAiC;IAC9D,IAAI,cAAc,CAAC,KAAK,EAAE,OAAO,EAAmC;IAEpE,gCAAgC;IAChC,KAAK,IAAI,IAAI;IAgBb,+BAA+B;IAC/B,IAAI,IAAI,IAAI;IAOZ,iDAAiD;IACjD,OAAO,IAAI,IAAI;CAIlB"}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { Signal } from '@drop-ai/core';
|
|
2
|
+
// ---------------------------------------------------------------------------
|
|
3
|
+
// PlayheadTracker
|
|
4
|
+
// ---------------------------------------------------------------------------
|
|
5
|
+
/**
|
|
6
|
+
* Tracks the transport position via `requestAnimationFrame` and converts
|
|
7
|
+
* the current frame into a pixel X coordinate.
|
|
8
|
+
*
|
|
9
|
+
* Framework-agnostic — connect to the `moved` signal to drive your UI.
|
|
10
|
+
*
|
|
11
|
+
* ```ts
|
|
12
|
+
* const tracker = new PlayheadTracker(viewport, () => engine.session.transportFrame);
|
|
13
|
+
* tracker.moved.connect(({ frame, x }) => {
|
|
14
|
+
* playheadEl.style.transform = `translateX(${x}px)`;
|
|
15
|
+
* });
|
|
16
|
+
* tracker.start();
|
|
17
|
+
* ```
|
|
18
|
+
*/
|
|
19
|
+
export class PlayheadTracker {
|
|
20
|
+
/**
|
|
21
|
+
* @param viewport The timeline viewport for frame → pixel conversion.
|
|
22
|
+
* @param getFrame A callback that returns the current transport frame.
|
|
23
|
+
* Typically `() => engine.session.transportFrame`.
|
|
24
|
+
*/
|
|
25
|
+
constructor(viewport, getFrame) {
|
|
26
|
+
/** Emitted on each animation frame with the current position. */
|
|
27
|
+
this.moved = new Signal();
|
|
28
|
+
this._rafId = null;
|
|
29
|
+
this._followPlayhead = false;
|
|
30
|
+
this._viewport = viewport;
|
|
31
|
+
this._getFrame = getFrame;
|
|
32
|
+
}
|
|
33
|
+
get isRunning() { return this._rafId !== null; }
|
|
34
|
+
/** When true, the viewport will auto-scroll to keep the playhead visible. */
|
|
35
|
+
get followPlayhead() { return this._followPlayhead; }
|
|
36
|
+
set followPlayhead(value) { this._followPlayhead = value; }
|
|
37
|
+
/** Start the animation loop. */
|
|
38
|
+
start() {
|
|
39
|
+
if (this._rafId !== null)
|
|
40
|
+
return;
|
|
41
|
+
const tick = () => {
|
|
42
|
+
const frame = this._getFrame();
|
|
43
|
+
const x = this._viewport.frameToViewportPixel(frame);
|
|
44
|
+
if (this._followPlayhead) {
|
|
45
|
+
this._viewport.scrollToFrame(frame);
|
|
46
|
+
}
|
|
47
|
+
this.moved.emit({ frame, x });
|
|
48
|
+
this._rafId = requestAnimationFrame(tick);
|
|
49
|
+
};
|
|
50
|
+
this._rafId = requestAnimationFrame(tick);
|
|
51
|
+
}
|
|
52
|
+
/** Stop the animation loop. */
|
|
53
|
+
stop() {
|
|
54
|
+
if (this._rafId !== null) {
|
|
55
|
+
cancelAnimationFrame(this._rafId);
|
|
56
|
+
this._rafId = null;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
/** Dispose the tracker and release resources. */
|
|
60
|
+
dispose() {
|
|
61
|
+
this.stop();
|
|
62
|
+
this.moved.clear();
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
//# sourceMappingURL=PlayheadTracker.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"PlayheadTracker.js","sourceRoot":"","sources":["../../src/playhead/PlayheadTracker.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,MAAM,EAAE,MAAM,eAAe,CAAC;AAGvC,8EAA8E;AAC9E,kBAAkB;AAClB,8EAA8E;AAE9E;;;;;;;;;;;;;GAaG;AACH,MAAM,OAAO,eAAe;IASxB;;;;OAIG;IACH,YAAY,QAA0B,EAAE,QAA0B;QAblE,iEAAiE;QACjD,UAAK,GAAG,IAAI,MAAM,EAAoC,CAAC;QAI/D,WAAM,GAAkB,IAAI,CAAC;QAC7B,oBAAe,GAAY,KAAK,CAAC;QAQrC,IAAI,CAAC,SAAS,GAAG,QAAQ,CAAC;QAC1B,IAAI,CAAC,SAAS,GAAG,QAAQ,CAAC;IAC9B,CAAC;IAED,IAAI,SAAS,KAAc,OAAO,IAAI,CAAC,MAAM,KAAK,IAAI,CAAC,CAAC,CAAC;IAEzD,6EAA6E;IAC7E,IAAI,cAAc,KAAc,OAAO,IAAI,CAAC,eAAe,CAAC,CAAC,CAAC;IAC9D,IAAI,cAAc,CAAC,KAAc,IAAI,IAAI,CAAC,eAAe,GAAG,KAAK,CAAC,CAAC,CAAC;IAEpE,gCAAgC;IAChC,KAAK;QACD,IAAI,IAAI,CAAC,MAAM,KAAK,IAAI;YAAE,OAAO;QACjC,MAAM,IAAI,GAAG,GAAG,EAAE;YACd,MAAM,KAAK,GAAG,IAAI,CAAC,SAAS,EAAE,CAAC;YAC/B,MAAM,CAAC,GAAG,IAAI,CAAC,SAAS,CAAC,oBAAoB,CAAC,KAAK,CAAC,CAAC;YAErD,IAAI,IAAI,CAAC,eAAe,EAAE,CAAC;gBACvB,IAAI,CAAC,SAAS,CAAC,aAAa,CAAC,KAAK,CAAC,CAAC;YACxC,CAAC;YAED,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,CAAC,EAAE,CAAC,CAAC;YAC9B,IAAI,CAAC,MAAM,GAAG,qBAAqB,CAAC,IAAI,CAAC,CAAC;QAC9C,CAAC,CAAC;QACF,IAAI,CAAC,MAAM,GAAG,qBAAqB,CAAC,IAAI,CAAC,CAAC;IAC9C,CAAC;IAED,+BAA+B;IAC/B,IAAI;QACA,IAAI,IAAI,CAAC,MAAM,KAAK,IAAI,EAAE,CAAC;YACvB,oBAAoB,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;YAClC,IAAI,CAAC,MAAM,GAAG,IAAI,CAAC;QACvB,CAAC;IACL,CAAC;IAED,iDAAiD;IACjD,OAAO;QACH,IAAI,CAAC,IAAI,EAAE,CAAC;QACZ,IAAI,CAAC,KAAK,CAAC,KAAK,EAAE,CAAC;IACvB,CAAC;CACJ"}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import type { FrameCount } from '@drop-ai/core';
|
|
2
|
+
import { ClockMode } from '@drop-ai/core';
|
|
3
|
+
import type { TimelineViewport } from '../viewport/TimelineViewport';
|
|
4
|
+
export interface RulerTick {
|
|
5
|
+
/** Pixel X position (absolute, not viewport-relative). */
|
|
6
|
+
x: number;
|
|
7
|
+
/** Frame position corresponding to this tick. */
|
|
8
|
+
frame: FrameCount;
|
|
9
|
+
/** Display label (e.g. "1:30.000", "25", "01:02:03:00"). */
|
|
10
|
+
label: string;
|
|
11
|
+
/** True for major ticks (e.g. minute marks, bar starts). */
|
|
12
|
+
major: boolean;
|
|
13
|
+
}
|
|
14
|
+
export interface RulerTicksOptions {
|
|
15
|
+
viewport: TimelineViewport;
|
|
16
|
+
mode: ClockMode;
|
|
17
|
+
/** BPM — required for BBT mode. */
|
|
18
|
+
bpm?: number;
|
|
19
|
+
/** Time signature numerator — required for BBT mode (default: 4). */
|
|
20
|
+
timeSigNum?: number;
|
|
21
|
+
/** Time signature denominator — required for BBT mode (default: 4). */
|
|
22
|
+
timeSigDen?: number;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Compute ruler tick positions and labels for the visible viewport.
|
|
26
|
+
*
|
|
27
|
+
* Returns only ticks that fall within the current scroll window, so
|
|
28
|
+
* the caller can render them efficiently.
|
|
29
|
+
*
|
|
30
|
+
* ```ts
|
|
31
|
+
* const ticks = computeRulerTicks({
|
|
32
|
+
* viewport,
|
|
33
|
+
* mode: ClockMode.MINSEC,
|
|
34
|
+
* });
|
|
35
|
+
* for (const tick of ticks) {
|
|
36
|
+
* ctx.fillText(tick.label, tick.x - viewport.scrollX, 18);
|
|
37
|
+
* }
|
|
38
|
+
* ```
|
|
39
|
+
*/
|
|
40
|
+
export declare function computeRulerTicks(options: RulerTicksOptions): RulerTick[];
|
|
41
|
+
//# sourceMappingURL=RulerTicks.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"RulerTicks.d.ts","sourceRoot":"","sources":["../../src/ruler/RulerTicks.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,eAAe,CAAC;AAChD,OAAO,EAAE,SAAS,EAAe,MAAM,eAAe,CAAC;AACvD,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,8BAA8B,CAAC;AAMrE,MAAM,WAAW,SAAS;IACtB,0DAA0D;IAC1D,CAAC,EAAE,MAAM,CAAC;IACV,iDAAiD;IACjD,KAAK,EAAE,UAAU,CAAC;IAClB,4DAA4D;IAC5D,KAAK,EAAE,MAAM,CAAC;IACd,4DAA4D;IAC5D,KAAK,EAAE,OAAO,CAAC;CAClB;AAED,MAAM,WAAW,iBAAiB;IAC9B,QAAQ,EAAE,gBAAgB,CAAC;IAC3B,IAAI,EAAE,SAAS,CAAC;IAChB,mCAAmC;IACnC,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,qEAAqE;IACrE,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,uEAAuE;IACvE,UAAU,CAAC,EAAE,MAAM,CAAC;CACvB;AA2BD;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,iBAAiB,GAAG,SAAS,EAAE,CAiCzE"}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { ClockMode, formatClock } from '@drop-ai/core';
|
|
2
|
+
// ---------------------------------------------------------------------------
|
|
3
|
+
// Tick interval selection
|
|
4
|
+
// ---------------------------------------------------------------------------
|
|
5
|
+
/**
|
|
6
|
+
* Choose a nice tick interval (in seconds) based on the current zoom level,
|
|
7
|
+
* so ticks are never too crowded or too sparse.
|
|
8
|
+
*/
|
|
9
|
+
function chooseTimeInterval(pixelsPerSecond) {
|
|
10
|
+
// Target roughly 80–150 px between major ticks
|
|
11
|
+
const targetPxGap = 100;
|
|
12
|
+
const rawInterval = targetPxGap / pixelsPerSecond;
|
|
13
|
+
// Snap to a "nice" interval
|
|
14
|
+
const nice = [0.01, 0.02, 0.05, 0.1, 0.2, 0.5, 1, 2, 5, 10, 15, 30, 60, 120, 300, 600];
|
|
15
|
+
for (const n of nice) {
|
|
16
|
+
if (n >= rawInterval)
|
|
17
|
+
return n;
|
|
18
|
+
}
|
|
19
|
+
return 600;
|
|
20
|
+
}
|
|
21
|
+
// ---------------------------------------------------------------------------
|
|
22
|
+
// computeRulerTicks
|
|
23
|
+
// ---------------------------------------------------------------------------
|
|
24
|
+
/**
|
|
25
|
+
* Compute ruler tick positions and labels for the visible viewport.
|
|
26
|
+
*
|
|
27
|
+
* Returns only ticks that fall within the current scroll window, so
|
|
28
|
+
* the caller can render them efficiently.
|
|
29
|
+
*
|
|
30
|
+
* ```ts
|
|
31
|
+
* const ticks = computeRulerTicks({
|
|
32
|
+
* viewport,
|
|
33
|
+
* mode: ClockMode.MINSEC,
|
|
34
|
+
* });
|
|
35
|
+
* for (const tick of ticks) {
|
|
36
|
+
* ctx.fillText(tick.label, tick.x - viewport.scrollX, 18);
|
|
37
|
+
* }
|
|
38
|
+
* ```
|
|
39
|
+
*/
|
|
40
|
+
export function computeRulerTicks(options) {
|
|
41
|
+
const { viewport, mode, bpm = 120, timeSigNum = 4, timeSigDen = 4 } = options;
|
|
42
|
+
const { sampleRate, pixelsPerSecond, scrollX, viewportWidth } = viewport;
|
|
43
|
+
const ticks = [];
|
|
44
|
+
if (mode === ClockMode.BBT) {
|
|
45
|
+
return computeBBTTicks(sampleRate, pixelsPerSecond, scrollX, viewportWidth, bpm, timeSigNum, timeSigDen);
|
|
46
|
+
}
|
|
47
|
+
// Time-based modes: MINSEC, TIMECODE, SAMPLES
|
|
48
|
+
const interval = chooseTimeInterval(pixelsPerSecond);
|
|
49
|
+
const startSec = scrollX / pixelsPerSecond;
|
|
50
|
+
const endSec = (scrollX + viewportWidth) / pixelsPerSecond;
|
|
51
|
+
// Start from the first tick at or before the visible range
|
|
52
|
+
const firstTick = Math.floor(startSec / interval) * interval;
|
|
53
|
+
for (let sec = firstTick; sec <= endSec; sec += interval) {
|
|
54
|
+
if (sec < 0)
|
|
55
|
+
continue;
|
|
56
|
+
const frame = Math.round(sec * sampleRate);
|
|
57
|
+
const x = sec * pixelsPerSecond;
|
|
58
|
+
// Determine if major (e.g. every minute for MINSEC, every 10s if interval < 10)
|
|
59
|
+
const majorInterval = interval < 1 ? 1 : interval < 10 ? 10 : interval < 60 ? 60 : 600;
|
|
60
|
+
const major = Math.abs(sec % majorInterval) < 0.001;
|
|
61
|
+
const label = formatClock(frame, sampleRate, mode, bpm, timeSigNum);
|
|
62
|
+
ticks.push({ x, frame, label, major });
|
|
63
|
+
}
|
|
64
|
+
return ticks;
|
|
65
|
+
}
|
|
66
|
+
// ---------------------------------------------------------------------------
|
|
67
|
+
// BBT ticks (bar/beat based)
|
|
68
|
+
// ---------------------------------------------------------------------------
|
|
69
|
+
function computeBBTTicks(sampleRate, pixelsPerSecond, scrollX, viewportWidth, bpm, timeSigNum, _timeSigDen) {
|
|
70
|
+
const ticks = [];
|
|
71
|
+
const beatsPerSecond = bpm / 60;
|
|
72
|
+
const secondsPerBeat = 1 / beatsPerSecond;
|
|
73
|
+
const secondsPerBar = secondsPerBeat * timeSigNum;
|
|
74
|
+
const pixelsPerBar = secondsPerBar * pixelsPerSecond;
|
|
75
|
+
// If bars are too narrow, skip some
|
|
76
|
+
const barSkip = pixelsPerBar < 20 ? Math.ceil(20 / pixelsPerBar) : 1;
|
|
77
|
+
const showBeats = pixelsPerBar > 60;
|
|
78
|
+
const startSec = scrollX / pixelsPerSecond;
|
|
79
|
+
const endSec = (scrollX + viewportWidth) / pixelsPerSecond;
|
|
80
|
+
const firstBar = Math.max(0, Math.floor(startSec / secondsPerBar));
|
|
81
|
+
const lastBar = Math.ceil(endSec / secondsPerBar);
|
|
82
|
+
for (let bar = firstBar; bar <= lastBar; bar += barSkip) {
|
|
83
|
+
const barSec = bar * secondsPerBar;
|
|
84
|
+
const barX = barSec * pixelsPerSecond;
|
|
85
|
+
const frame = Math.round(barSec * sampleRate);
|
|
86
|
+
ticks.push({
|
|
87
|
+
x: barX,
|
|
88
|
+
frame,
|
|
89
|
+
label: `${bar + 1}`,
|
|
90
|
+
major: true,
|
|
91
|
+
});
|
|
92
|
+
// Beat ticks within this bar
|
|
93
|
+
if (showBeats && barSkip === 1) {
|
|
94
|
+
for (let beat = 1; beat < timeSigNum; beat++) {
|
|
95
|
+
const beatSec = barSec + beat * secondsPerBeat;
|
|
96
|
+
const beatX = beatSec * pixelsPerSecond;
|
|
97
|
+
const beatFrame = Math.round(beatSec * sampleRate);
|
|
98
|
+
ticks.push({
|
|
99
|
+
x: beatX,
|
|
100
|
+
frame: beatFrame,
|
|
101
|
+
label: `${bar + 1}.${beat + 1}`,
|
|
102
|
+
major: false,
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
return ticks;
|
|
108
|
+
}
|
|
109
|
+
//# sourceMappingURL=RulerTicks.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"RulerTicks.js","sourceRoot":"","sources":["../../src/ruler/RulerTicks.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import type { FrameCount } from '@drop-ai/core';
|
|
2
|
+
import { Signal } from '@drop-ai/core';
|
|
3
|
+
import { ZoomFocus } from '@drop-ai/core';
|
|
4
|
+
/**
|
|
5
|
+
* Framework-agnostic timeline viewport state.
|
|
6
|
+
*
|
|
7
|
+
* Manages zoom level (`pixelsPerSecond`), horizontal scroll offset, and
|
|
8
|
+
* provides frame ↔ pixel conversion utilities needed by every DAW UI:
|
|
9
|
+
* waveform rendering, region positioning, playhead location, ruler ticks, etc.
|
|
10
|
+
*
|
|
11
|
+
* ```ts
|
|
12
|
+
* const vp = new TimelineViewport(44100);
|
|
13
|
+
* vp.changed.connect(() => render());
|
|
14
|
+
* vp.setPixelsPerSecond(100);
|
|
15
|
+
* const px = vp.frameToPixel(44100); // → 100
|
|
16
|
+
* ```
|
|
17
|
+
*/
|
|
18
|
+
export declare class TimelineViewport {
|
|
19
|
+
/** Emitted whenever pixelsPerSecond or scrollX changes. */
|
|
20
|
+
readonly changed: Signal<void>;
|
|
21
|
+
private _sampleRate;
|
|
22
|
+
private _pixelsPerSecond;
|
|
23
|
+
/** Horizontal scroll offset in pixels. */
|
|
24
|
+
private _scrollX;
|
|
25
|
+
/** Total duration of the session in seconds. */
|
|
26
|
+
private _duration;
|
|
27
|
+
/** Visible width of the viewport in pixels. */
|
|
28
|
+
private _viewportWidth;
|
|
29
|
+
constructor(sampleRate: number);
|
|
30
|
+
get sampleRate(): number;
|
|
31
|
+
get pixelsPerSecond(): number;
|
|
32
|
+
get scrollX(): number;
|
|
33
|
+
get duration(): number;
|
|
34
|
+
get viewportWidth(): number;
|
|
35
|
+
/** Frames per pixel — useful for peak resolution selection. */
|
|
36
|
+
get framesPerPixel(): number;
|
|
37
|
+
/** Total content width in pixels. */
|
|
38
|
+
get contentWidth(): number;
|
|
39
|
+
/** The first visible frame at the current scroll position. */
|
|
40
|
+
get visibleStartFrame(): FrameCount;
|
|
41
|
+
/** The last visible frame at the current scroll position. */
|
|
42
|
+
get visibleEndFrame(): FrameCount;
|
|
43
|
+
setnumber(sampleRate: number): void;
|
|
44
|
+
setDuration(seconds: number): void;
|
|
45
|
+
setViewportWidth(pixels: number): void;
|
|
46
|
+
setPixelsPerSecond(pps: number): void;
|
|
47
|
+
setScrollX(px: number): void;
|
|
48
|
+
/**
|
|
49
|
+
* Zoom in/out while keeping the given anchor stable on screen.
|
|
50
|
+
*
|
|
51
|
+
* @param direction Positive = zoom in, negative = zoom out.
|
|
52
|
+
* @param focus Where the zoom should be anchored.
|
|
53
|
+
* @param anchorPx Pixel position of the mouse (for ZoomFocus.MOUSE).
|
|
54
|
+
* @param playheadFrame Current transport frame (for ZoomFocus.PLAYHEAD).
|
|
55
|
+
*/
|
|
56
|
+
zoom(direction: number, focus?: ZoomFocus, anchorPx?: number, playheadFrame?: FrameCount): void;
|
|
57
|
+
zoomIn(focus?: ZoomFocus, anchorPx?: number, playheadFrame?: FrameCount): void;
|
|
58
|
+
zoomOut(focus?: ZoomFocus, anchorPx?: number, playheadFrame?: FrameCount): void;
|
|
59
|
+
/** Zoom to fit the entire session duration in the viewport. */
|
|
60
|
+
zoomToFit(): void;
|
|
61
|
+
/** Zoom to show the given frame range. */
|
|
62
|
+
zoomToRange(startFrame: FrameCount, endFrame: FrameCount): void;
|
|
63
|
+
/** Convert an absolute frame position to a pixel X offset (relative to content origin). */
|
|
64
|
+
frameToPixel(frame: FrameCount): number;
|
|
65
|
+
/** Convert a pixel X offset (relative to content origin) to a frame position. */
|
|
66
|
+
pixelToFrame(px: number): FrameCount;
|
|
67
|
+
/** Convert a frame to a pixel offset relative to the visible viewport. */
|
|
68
|
+
frameToViewportPixel(frame: FrameCount): number;
|
|
69
|
+
/** Convert a viewport-relative pixel to an absolute frame. */
|
|
70
|
+
viewportPixelToFrame(px: number): FrameCount;
|
|
71
|
+
/** Convert a duration in frames to a width in pixels. */
|
|
72
|
+
framesToWidth(frames: FrameCount): number;
|
|
73
|
+
/** Convert a width in pixels to a duration in frames. */
|
|
74
|
+
widthToFrames(px: number): FrameCount;
|
|
75
|
+
/** Scroll so the given frame is visible, optionally centered. */
|
|
76
|
+
scrollToFrame(frame: FrameCount, center?: boolean): void;
|
|
77
|
+
/** Returns true if the given frame is within the visible viewport. */
|
|
78
|
+
isFrameVisible(frame: FrameCount): boolean;
|
|
79
|
+
}
|
|
80
|
+
//# sourceMappingURL=TimelineViewport.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"TimelineViewport.d.ts","sourceRoot":"","sources":["../../src/viewport/TimelineViewport.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,eAAe,CAAC;AAChD,OAAO,EAAE,MAAM,EAAE,MAAM,eAAe,CAAC;AACvC,OAAO,EAAE,SAAS,EAAE,MAAM,eAAe,CAAC;AAe1C;;;;;;;;;;;;;GAaG;AACH,qBAAa,gBAAgB;IACzB,2DAA2D;IAC3D,SAAgB,OAAO,eAAsB;IAE7C,OAAO,CAAC,WAAW,CAAS;IAC5B,OAAO,CAAC,gBAAgB,CAAuB;IAC/C,0CAA0C;IAC1C,OAAO,CAAC,QAAQ,CAAa;IAC7B,gDAAgD;IAChD,OAAO,CAAC,SAAS,CAAa;IAC9B,+CAA+C;IAC/C,OAAO,CAAC,cAAc,CAAa;gBAEvB,UAAU,EAAE,MAAM;IAM9B,IAAI,UAAU,IAAI,MAAM,CAA6B;IACrD,IAAI,eAAe,IAAI,MAAM,CAAkC;IAC/D,IAAI,OAAO,IAAI,MAAM,CAA0B;IAC/C,IAAI,QAAQ,IAAI,MAAM,CAA2B;IACjD,IAAI,aAAa,IAAI,MAAM,CAAgC;IAE3D,+DAA+D;IAC/D,IAAI,cAAc,IAAI,MAAM,CAE3B;IAED,qCAAqC;IACrC,IAAI,YAAY,IAAI,MAAM,CAEzB;IAED,8DAA8D;IAC9D,IAAI,iBAAiB,IAAI,UAAU,CAElC;IAED,6DAA6D;IAC7D,IAAI,eAAe,IAAI,UAAU,CAEhC;IAID,SAAS,CAAC,UAAU,EAAE,MAAM,GAAG,IAAI;IAOnC,WAAW,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI;IAOlC,gBAAgB,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI;IAOtC,kBAAkB,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI;IAQrC,UAAU,CAAC,EAAE,EAAE,MAAM,GAAG,IAAI;IAU5B;;;;;;;OAOG;IACH,IAAI,CACA,SAAS,EAAE,MAAM,EACjB,KAAK,GAAE,SAA0B,EACjC,QAAQ,CAAC,EAAE,MAAM,EACjB,aAAa,CAAC,EAAE,UAAU,GAC3B,IAAI;IA8CP,MAAM,CAAC,KAAK,CAAC,EAAE,SAAS,EAAE,QAAQ,CAAC,EAAE,MAAM,EAAE,aAAa,CAAC,EAAE,UAAU,GAAG,IAAI;IAI9E,OAAO,CAAC,KAAK,CAAC,EAAE,SAAS,EAAE,QAAQ,CAAC,EAAE,MAAM,EAAE,aAAa,CAAC,EAAE,UAAU,GAAG,IAAI;IAI/E,+DAA+D;IAC/D,SAAS,IAAI,IAAI;IAOjB,0CAA0C;IAC1C,WAAW,CAAC,UAAU,EAAE,UAAU,EAAE,QAAQ,EAAE,UAAU,GAAG,IAAI;IAW/D,2FAA2F;IAC3F,YAAY,CAAC,KAAK,EAAE,UAAU,GAAG,MAAM;IAIvC,iFAAiF;IACjF,YAAY,CAAC,EAAE,EAAE,MAAM,GAAG,UAAU;IAIpC,0EAA0E;IAC1E,oBAAoB,CAAC,KAAK,EAAE,UAAU,GAAG,MAAM;IAI/C,8DAA8D;IAC9D,oBAAoB,CAAC,EAAE,EAAE,MAAM,GAAG,UAAU;IAI5C,yDAAyD;IACzD,aAAa,CAAC,MAAM,EAAE,UAAU,GAAG,MAAM;IAIzC,yDAAyD;IACzD,aAAa,CAAC,EAAE,EAAE,MAAM,GAAG,UAAU;IAMrC,iEAAiE;IACjE,aAAa,CAAC,KAAK,EAAE,UAAU,EAAE,MAAM,GAAE,OAAe,GAAG,IAAI;IAW/D,sEAAsE;IACtE,cAAc,CAAC,KAAK,EAAE,UAAU,GAAG,OAAO;CAI7C"}
|
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
import { Signal } from '@drop-ai/core';
|
|
2
|
+
import { ZoomFocus } from '@drop-ai/core';
|
|
3
|
+
// ---------------------------------------------------------------------------
|
|
4
|
+
// Constants
|
|
5
|
+
// ---------------------------------------------------------------------------
|
|
6
|
+
const MIN_PPS = 1;
|
|
7
|
+
const MAX_PPS = 1000;
|
|
8
|
+
const DEFAULT_PPS = 50;
|
|
9
|
+
const ZOOM_FACTOR = 1.5;
|
|
10
|
+
// ---------------------------------------------------------------------------
|
|
11
|
+
// TimelineViewport
|
|
12
|
+
// ---------------------------------------------------------------------------
|
|
13
|
+
/**
|
|
14
|
+
* Framework-agnostic timeline viewport state.
|
|
15
|
+
*
|
|
16
|
+
* Manages zoom level (`pixelsPerSecond`), horizontal scroll offset, and
|
|
17
|
+
* provides frame ↔ pixel conversion utilities needed by every DAW UI:
|
|
18
|
+
* waveform rendering, region positioning, playhead location, ruler ticks, etc.
|
|
19
|
+
*
|
|
20
|
+
* ```ts
|
|
21
|
+
* const vp = new TimelineViewport(44100);
|
|
22
|
+
* vp.changed.connect(() => render());
|
|
23
|
+
* vp.setPixelsPerSecond(100);
|
|
24
|
+
* const px = vp.frameToPixel(44100); // → 100
|
|
25
|
+
* ```
|
|
26
|
+
*/
|
|
27
|
+
export class TimelineViewport {
|
|
28
|
+
constructor(sampleRate) {
|
|
29
|
+
/** Emitted whenever pixelsPerSecond or scrollX changes. */
|
|
30
|
+
this.changed = new Signal();
|
|
31
|
+
this._pixelsPerSecond = DEFAULT_PPS;
|
|
32
|
+
/** Horizontal scroll offset in pixels. */
|
|
33
|
+
this._scrollX = 0;
|
|
34
|
+
/** Total duration of the session in seconds. */
|
|
35
|
+
this._duration = 0;
|
|
36
|
+
/** Visible width of the viewport in pixels. */
|
|
37
|
+
this._viewportWidth = 0;
|
|
38
|
+
this._sampleRate = sampleRate;
|
|
39
|
+
}
|
|
40
|
+
// ── Getters ──────────────────────────────────────────────────────────
|
|
41
|
+
get sampleRate() { return this._sampleRate; }
|
|
42
|
+
get pixelsPerSecond() { return this._pixelsPerSecond; }
|
|
43
|
+
get scrollX() { return this._scrollX; }
|
|
44
|
+
get duration() { return this._duration; }
|
|
45
|
+
get viewportWidth() { return this._viewportWidth; }
|
|
46
|
+
/** Frames per pixel — useful for peak resolution selection. */
|
|
47
|
+
get framesPerPixel() {
|
|
48
|
+
return this._sampleRate / this._pixelsPerSecond;
|
|
49
|
+
}
|
|
50
|
+
/** Total content width in pixels. */
|
|
51
|
+
get contentWidth() {
|
|
52
|
+
return this._duration * this._pixelsPerSecond;
|
|
53
|
+
}
|
|
54
|
+
/** The first visible frame at the current scroll position. */
|
|
55
|
+
get visibleStartFrame() {
|
|
56
|
+
return this.pixelToFrame(this._scrollX);
|
|
57
|
+
}
|
|
58
|
+
/** The last visible frame at the current scroll position. */
|
|
59
|
+
get visibleEndFrame() {
|
|
60
|
+
return this.pixelToFrame(this._scrollX + this._viewportWidth);
|
|
61
|
+
}
|
|
62
|
+
// ── Setters ──────────────────────────────────────────────────────────
|
|
63
|
+
setnumber(sampleRate) {
|
|
64
|
+
if (this._sampleRate !== sampleRate) {
|
|
65
|
+
this._sampleRate = sampleRate;
|
|
66
|
+
this.changed.emit();
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
setDuration(seconds) {
|
|
70
|
+
if (this._duration !== seconds) {
|
|
71
|
+
this._duration = seconds;
|
|
72
|
+
this.changed.emit();
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
setViewportWidth(pixels) {
|
|
76
|
+
if (this._viewportWidth !== pixels) {
|
|
77
|
+
this._viewportWidth = pixels;
|
|
78
|
+
this.changed.emit();
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
setPixelsPerSecond(pps) {
|
|
82
|
+
const clamped = Math.max(MIN_PPS, Math.min(pps, MAX_PPS));
|
|
83
|
+
if (this._pixelsPerSecond !== clamped) {
|
|
84
|
+
this._pixelsPerSecond = clamped;
|
|
85
|
+
this.changed.emit();
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
setScrollX(px) {
|
|
89
|
+
const clamped = Math.max(0, Math.min(px, Math.max(0, this.contentWidth - this._viewportWidth)));
|
|
90
|
+
if (this._scrollX !== clamped) {
|
|
91
|
+
this._scrollX = clamped;
|
|
92
|
+
this.changed.emit();
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
// ── Zoom ─────────────────────────────────────────────────────────────
|
|
96
|
+
/**
|
|
97
|
+
* Zoom in/out while keeping the given anchor stable on screen.
|
|
98
|
+
*
|
|
99
|
+
* @param direction Positive = zoom in, negative = zoom out.
|
|
100
|
+
* @param focus Where the zoom should be anchored.
|
|
101
|
+
* @param anchorPx Pixel position of the mouse (for ZoomFocus.MOUSE).
|
|
102
|
+
* @param playheadFrame Current transport frame (for ZoomFocus.PLAYHEAD).
|
|
103
|
+
*/
|
|
104
|
+
zoom(direction, focus = ZoomFocus.LEFT, anchorPx, playheadFrame) {
|
|
105
|
+
const factor = direction > 0 ? ZOOM_FACTOR : 1 / ZOOM_FACTOR;
|
|
106
|
+
const newPps = Math.max(MIN_PPS, Math.min(this._pixelsPerSecond * factor, MAX_PPS));
|
|
107
|
+
if (newPps === this._pixelsPerSecond)
|
|
108
|
+
return;
|
|
109
|
+
// Determine anchor point in seconds
|
|
110
|
+
let anchorSec;
|
|
111
|
+
switch (focus) {
|
|
112
|
+
case ZoomFocus.MOUSE:
|
|
113
|
+
anchorSec = (this._scrollX + (anchorPx !== null && anchorPx !== void 0 ? anchorPx : 0)) / this._pixelsPerSecond;
|
|
114
|
+
break;
|
|
115
|
+
case ZoomFocus.PLAYHEAD:
|
|
116
|
+
anchorSec = (playheadFrame !== null && playheadFrame !== void 0 ? playheadFrame : 0) / this._sampleRate;
|
|
117
|
+
break;
|
|
118
|
+
case ZoomFocus.CENTER:
|
|
119
|
+
anchorSec = (this._scrollX + this._viewportWidth / 2) / this._pixelsPerSecond;
|
|
120
|
+
break;
|
|
121
|
+
case ZoomFocus.RIGHT:
|
|
122
|
+
anchorSec = (this._scrollX + this._viewportWidth) / this._pixelsPerSecond;
|
|
123
|
+
break;
|
|
124
|
+
case ZoomFocus.EDIT_POINT:
|
|
125
|
+
case ZoomFocus.LEFT:
|
|
126
|
+
default:
|
|
127
|
+
anchorSec = this._scrollX / this._pixelsPerSecond;
|
|
128
|
+
break;
|
|
129
|
+
}
|
|
130
|
+
// Compute where the anchor would land after zoom
|
|
131
|
+
const anchorRatio = anchorPx !== undefined
|
|
132
|
+
? anchorPx / this._viewportWidth
|
|
133
|
+
: focus === ZoomFocus.CENTER ? 0.5
|
|
134
|
+
: focus === ZoomFocus.RIGHT ? 1
|
|
135
|
+
: 0;
|
|
136
|
+
const oldPps = this._pixelsPerSecond;
|
|
137
|
+
this._pixelsPerSecond = newPps;
|
|
138
|
+
// Adjust scroll to keep anchor visually stable
|
|
139
|
+
const newAnchorPx = anchorSec * newPps;
|
|
140
|
+
this._scrollX = Math.max(0, newAnchorPx - anchorRatio * this._viewportWidth);
|
|
141
|
+
if (oldPps !== this._pixelsPerSecond) {
|
|
142
|
+
this.changed.emit();
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
zoomIn(focus, anchorPx, playheadFrame) {
|
|
146
|
+
this.zoom(1, focus, anchorPx, playheadFrame);
|
|
147
|
+
}
|
|
148
|
+
zoomOut(focus, anchorPx, playheadFrame) {
|
|
149
|
+
this.zoom(-1, focus, anchorPx, playheadFrame);
|
|
150
|
+
}
|
|
151
|
+
/** Zoom to fit the entire session duration in the viewport. */
|
|
152
|
+
zoomToFit() {
|
|
153
|
+
if (this._duration <= 0 || this._viewportWidth <= 0)
|
|
154
|
+
return;
|
|
155
|
+
this._pixelsPerSecond = Math.max(MIN_PPS, Math.min(this._viewportWidth / this._duration, MAX_PPS));
|
|
156
|
+
this._scrollX = 0;
|
|
157
|
+
this.changed.emit();
|
|
158
|
+
}
|
|
159
|
+
/** Zoom to show the given frame range. */
|
|
160
|
+
zoomToRange(startFrame, endFrame) {
|
|
161
|
+
if (this._viewportWidth <= 0)
|
|
162
|
+
return;
|
|
163
|
+
const durationSec = (endFrame - startFrame) / this._sampleRate;
|
|
164
|
+
if (durationSec <= 0)
|
|
165
|
+
return;
|
|
166
|
+
this._pixelsPerSecond = Math.max(MIN_PPS, Math.min(this._viewportWidth / durationSec, MAX_PPS));
|
|
167
|
+
this._scrollX = (startFrame / this._sampleRate) * this._pixelsPerSecond;
|
|
168
|
+
this.changed.emit();
|
|
169
|
+
}
|
|
170
|
+
// ── Frame ↔ Pixel conversion ─────────────────────────────────────────
|
|
171
|
+
/** Convert an absolute frame position to a pixel X offset (relative to content origin). */
|
|
172
|
+
frameToPixel(frame) {
|
|
173
|
+
return (frame / this._sampleRate) * this._pixelsPerSecond;
|
|
174
|
+
}
|
|
175
|
+
/** Convert a pixel X offset (relative to content origin) to a frame position. */
|
|
176
|
+
pixelToFrame(px) {
|
|
177
|
+
return (px / this._pixelsPerSecond) * this._sampleRate;
|
|
178
|
+
}
|
|
179
|
+
/** Convert a frame to a pixel offset relative to the visible viewport. */
|
|
180
|
+
frameToViewportPixel(frame) {
|
|
181
|
+
return this.frameToPixel(frame) - this._scrollX;
|
|
182
|
+
}
|
|
183
|
+
/** Convert a viewport-relative pixel to an absolute frame. */
|
|
184
|
+
viewportPixelToFrame(px) {
|
|
185
|
+
return this.pixelToFrame(px + this._scrollX);
|
|
186
|
+
}
|
|
187
|
+
/** Convert a duration in frames to a width in pixels. */
|
|
188
|
+
framesToWidth(frames) {
|
|
189
|
+
return (frames / this._sampleRate) * this._pixelsPerSecond;
|
|
190
|
+
}
|
|
191
|
+
/** Convert a width in pixels to a duration in frames. */
|
|
192
|
+
widthToFrames(px) {
|
|
193
|
+
return (px / this._pixelsPerSecond) * this._sampleRate;
|
|
194
|
+
}
|
|
195
|
+
// ── Scroll helpers ───────────────────────────────────────────────────
|
|
196
|
+
/** Scroll so the given frame is visible, optionally centered. */
|
|
197
|
+
scrollToFrame(frame, center = false) {
|
|
198
|
+
const px = this.frameToPixel(frame);
|
|
199
|
+
if (center) {
|
|
200
|
+
this.setScrollX(px - this._viewportWidth / 2);
|
|
201
|
+
}
|
|
202
|
+
else if (px < this._scrollX) {
|
|
203
|
+
this.setScrollX(px);
|
|
204
|
+
}
|
|
205
|
+
else if (px > this._scrollX + this._viewportWidth) {
|
|
206
|
+
this.setScrollX(px - this._viewportWidth);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
/** Returns true if the given frame is within the visible viewport. */
|
|
210
|
+
isFrameVisible(frame) {
|
|
211
|
+
const px = this.frameToPixel(frame);
|
|
212
|
+
return px >= this._scrollX && px <= this._scrollX + this._viewportWidth;
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
//# sourceMappingURL=TimelineViewport.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"TimelineViewport.js","sourceRoot":"","sources":["../../src/viewport/TimelineViewport.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import type { TrackId } from '@drop-ai/core';
|
|
2
|
+
import { Signal } from '@drop-ai/core';
|
|
3
|
+
export interface TrackLayoutEntry {
|
|
4
|
+
trackId: TrackId;
|
|
5
|
+
y: number;
|
|
6
|
+
height: number;
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Computes vertical positions for a list of tracks.
|
|
10
|
+
*
|
|
11
|
+
* ```ts
|
|
12
|
+
* const layout = new TrackLayout();
|
|
13
|
+
* layout.setTracks(['t1', 't2', 't3']);
|
|
14
|
+
* layout.setTrackHeight('t1', 120);
|
|
15
|
+
* const entry = layout.getEntry('t1'); // { trackId: 't1', y: 0, height: 120 }
|
|
16
|
+
* const t2 = layout.getEntry('t2'); // { trackId: 't2', y: 120, height: 80 }
|
|
17
|
+
* ```
|
|
18
|
+
*/
|
|
19
|
+
export declare class TrackLayout {
|
|
20
|
+
readonly changed: Signal<void>;
|
|
21
|
+
private _trackIds;
|
|
22
|
+
private _heights;
|
|
23
|
+
private _collapsed;
|
|
24
|
+
private _collapsedHeight;
|
|
25
|
+
private _cache;
|
|
26
|
+
/** Set the ordered list of track IDs (top to bottom). */
|
|
27
|
+
setTracks(trackIds: TrackId[]): void;
|
|
28
|
+
setTrackHeight(trackId: TrackId, height: number): void;
|
|
29
|
+
getTrackHeight(trackId: TrackId): number;
|
|
30
|
+
setCollapsed(trackId: TrackId, collapsed: boolean): void;
|
|
31
|
+
isCollapsed(trackId: TrackId): boolean;
|
|
32
|
+
/** Get all layout entries (ordered top → bottom). */
|
|
33
|
+
getEntries(): TrackLayoutEntry[];
|
|
34
|
+
/** Get layout entry for a specific track. */
|
|
35
|
+
getEntry(trackId: TrackId): TrackLayoutEntry | undefined;
|
|
36
|
+
/** Total height of all tracks. */
|
|
37
|
+
get totalHeight(): number;
|
|
38
|
+
/** Find the track at a given Y coordinate. */
|
|
39
|
+
getTrackAtY(y: number): TrackLayoutEntry | undefined;
|
|
40
|
+
}
|
|
41
|
+
//# sourceMappingURL=TrackLayout.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"TrackLayout.d.ts","sourceRoot":"","sources":["../../src/viewport/TrackLayout.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,eAAe,CAAC;AAC7C,OAAO,EAAE,MAAM,EAAE,MAAM,eAAe,CAAC;AAcvC,MAAM,WAAW,gBAAgB;IAC7B,OAAO,EAAE,OAAO,CAAC;IACjB,CAAC,EAAE,MAAM,CAAC;IACV,MAAM,EAAE,MAAM,CAAC;CAClB;AAED;;;;;;;;;;GAUG;AACH,qBAAa,WAAW;IACpB,SAAgB,OAAO,eAAsB;IAE7C,OAAO,CAAC,SAAS,CAAiB;IAClC,OAAO,CAAC,QAAQ,CAAmC;IACnD,OAAO,CAAC,UAAU,CAA2B;IAC7C,OAAO,CAAC,gBAAgB,CAA4B;IAGpD,OAAO,CAAC,MAAM,CAAmC;IAIjD,yDAAyD;IACzD,SAAS,CAAC,QAAQ,EAAE,OAAO,EAAE,GAAG,IAAI;IAMpC,cAAc,CAAC,OAAO,EAAE,OAAO,EAAE,MAAM,EAAE,MAAM,GAAG,IAAI;IAStD,cAAc,CAAC,OAAO,EAAE,OAAO,GAAG,MAAM;IAKxC,YAAY,CAAC,OAAO,EAAE,OAAO,EAAE,SAAS,EAAE,OAAO,GAAG,IAAI;IASxD,WAAW,CAAC,OAAO,EAAE,OAAO,GAAG,OAAO;IAMtC,qDAAqD;IACrD,UAAU,IAAI,gBAAgB,EAAE;IAahC,6CAA6C;IAC7C,QAAQ,CAAC,OAAO,EAAE,OAAO,GAAG,gBAAgB,GAAG,SAAS;IAIxD,kCAAkC;IAClC,IAAI,WAAW,IAAI,MAAM,CAKxB;IAED,8CAA8C;IAC9C,WAAW,CAAC,CAAC,EAAE,MAAM,GAAG,gBAAgB,GAAG,SAAS;CAOvD"}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { Signal } from '@drop-ai/core';
|
|
2
|
+
// ---------------------------------------------------------------------------
|
|
3
|
+
// Constants
|
|
4
|
+
// ---------------------------------------------------------------------------
|
|
5
|
+
const DEFAULT_TRACK_HEIGHT = 80;
|
|
6
|
+
const MIN_TRACK_HEIGHT = 24;
|
|
7
|
+
const MAX_TRACK_HEIGHT = 500;
|
|
8
|
+
/**
|
|
9
|
+
* Computes vertical positions for a list of tracks.
|
|
10
|
+
*
|
|
11
|
+
* ```ts
|
|
12
|
+
* const layout = new TrackLayout();
|
|
13
|
+
* layout.setTracks(['t1', 't2', 't3']);
|
|
14
|
+
* layout.setTrackHeight('t1', 120);
|
|
15
|
+
* const entry = layout.getEntry('t1'); // { trackId: 't1', y: 0, height: 120 }
|
|
16
|
+
* const t2 = layout.getEntry('t2'); // { trackId: 't2', y: 120, height: 80 }
|
|
17
|
+
* ```
|
|
18
|
+
*/
|
|
19
|
+
export class TrackLayout {
|
|
20
|
+
constructor() {
|
|
21
|
+
this.changed = new Signal();
|
|
22
|
+
this._trackIds = [];
|
|
23
|
+
this._heights = new Map();
|
|
24
|
+
this._collapsed = new Set();
|
|
25
|
+
this._collapsedHeight = MIN_TRACK_HEIGHT;
|
|
26
|
+
// Cached layout entries, invalidated on change.
|
|
27
|
+
this._cache = null;
|
|
28
|
+
}
|
|
29
|
+
// ── Configuration ────────────────────────────────────────────────────
|
|
30
|
+
/** Set the ordered list of track IDs (top to bottom). */
|
|
31
|
+
setTracks(trackIds) {
|
|
32
|
+
this._trackIds = trackIds;
|
|
33
|
+
this._cache = null;
|
|
34
|
+
this.changed.emit();
|
|
35
|
+
}
|
|
36
|
+
setTrackHeight(trackId, height) {
|
|
37
|
+
const clamped = Math.max(MIN_TRACK_HEIGHT, Math.min(height, MAX_TRACK_HEIGHT));
|
|
38
|
+
if (this._heights.get(trackId) !== clamped) {
|
|
39
|
+
this._heights.set(trackId, clamped);
|
|
40
|
+
this._cache = null;
|
|
41
|
+
this.changed.emit();
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
getTrackHeight(trackId) {
|
|
45
|
+
var _a;
|
|
46
|
+
if (this._collapsed.has(trackId))
|
|
47
|
+
return this._collapsedHeight;
|
|
48
|
+
return (_a = this._heights.get(trackId)) !== null && _a !== void 0 ? _a : DEFAULT_TRACK_HEIGHT;
|
|
49
|
+
}
|
|
50
|
+
setCollapsed(trackId, collapsed) {
|
|
51
|
+
const changed = collapsed ? !this._collapsed.has(trackId) : this._collapsed.has(trackId);
|
|
52
|
+
if (!changed)
|
|
53
|
+
return;
|
|
54
|
+
if (collapsed)
|
|
55
|
+
this._collapsed.add(trackId);
|
|
56
|
+
else
|
|
57
|
+
this._collapsed.delete(trackId);
|
|
58
|
+
this._cache = null;
|
|
59
|
+
this.changed.emit();
|
|
60
|
+
}
|
|
61
|
+
isCollapsed(trackId) {
|
|
62
|
+
return this._collapsed.has(trackId);
|
|
63
|
+
}
|
|
64
|
+
// ── Queries ──────────────────────────────────────────────────────────
|
|
65
|
+
/** Get all layout entries (ordered top → bottom). */
|
|
66
|
+
getEntries() {
|
|
67
|
+
if (this._cache)
|
|
68
|
+
return this._cache;
|
|
69
|
+
let y = 0;
|
|
70
|
+
const entries = [];
|
|
71
|
+
for (const trackId of this._trackIds) {
|
|
72
|
+
const height = this.getTrackHeight(trackId);
|
|
73
|
+
entries.push({ trackId, y, height });
|
|
74
|
+
y += height;
|
|
75
|
+
}
|
|
76
|
+
this._cache = entries;
|
|
77
|
+
return entries;
|
|
78
|
+
}
|
|
79
|
+
/** Get layout entry for a specific track. */
|
|
80
|
+
getEntry(trackId) {
|
|
81
|
+
return this.getEntries().find(e => e.trackId === trackId);
|
|
82
|
+
}
|
|
83
|
+
/** Total height of all tracks. */
|
|
84
|
+
get totalHeight() {
|
|
85
|
+
const entries = this.getEntries();
|
|
86
|
+
if (entries.length === 0)
|
|
87
|
+
return 0;
|
|
88
|
+
const last = entries[entries.length - 1];
|
|
89
|
+
return last.y + last.height;
|
|
90
|
+
}
|
|
91
|
+
/** Find the track at a given Y coordinate. */
|
|
92
|
+
getTrackAtY(y) {
|
|
93
|
+
const entries = this.getEntries();
|
|
94
|
+
for (let i = entries.length - 1; i >= 0; i--) {
|
|
95
|
+
if (y >= entries[i].y)
|
|
96
|
+
return entries[i];
|
|
97
|
+
}
|
|
98
|
+
return undefined;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
//# sourceMappingURL=TrackLayout.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"TrackLayout.js","sourceRoot":"","sources":["../../src/viewport/TrackLayout.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import type { PeakData } from '@drop-ai/core';
|
|
2
|
+
/**
|
|
3
|
+
* Compute peak data from a raw `Float32Array` of audio samples.
|
|
4
|
+
*
|
|
5
|
+
* This produces one min/max/rms entry per `resolution` frames, suitable for
|
|
6
|
+
* waveform rendering at the matching zoom level.
|
|
7
|
+
*
|
|
8
|
+
* @param channelData Raw PCM samples for a single channel.
|
|
9
|
+
* @param resolution Number of source frames per peak entry.
|
|
10
|
+
* @returns PeakData compatible with `Source.setPeakData()`.
|
|
11
|
+
*
|
|
12
|
+
* ```ts
|
|
13
|
+
* const data = audioBuffer.getChannelData(0);
|
|
14
|
+
* const peaks = computePeaksFromSamples(data, 512);
|
|
15
|
+
* source.setPeakData(512, peaks);
|
|
16
|
+
* ```
|
|
17
|
+
*/
|
|
18
|
+
export declare function computePeaksFromSamples(channelData: Float32Array, resolution: number): PeakData;
|
|
19
|
+
/**
|
|
20
|
+
* Compute peak data from an `AudioBuffer`.
|
|
21
|
+
*
|
|
22
|
+
* Processes channel 0 by default. For stereo, call twice with different
|
|
23
|
+
* channel indices and store separately.
|
|
24
|
+
*
|
|
25
|
+
* @param audioBuffer Decoded Web Audio buffer.
|
|
26
|
+
* @param resolution Number of source frames per peak entry.
|
|
27
|
+
* @param channel Channel index (default 0).
|
|
28
|
+
*/
|
|
29
|
+
export declare function computePeaks(audioBuffer: AudioBuffer, resolution: number, channel?: number): PeakData;
|
|
30
|
+
/**
|
|
31
|
+
* Choose an appropriate peak resolution for a given zoom level.
|
|
32
|
+
*
|
|
33
|
+
* The idea: each peak entry should map to roughly 1 pixel. Since
|
|
34
|
+
* `framesPerPixel = sampleRate / pixelsPerSecond`, we pick the nearest
|
|
35
|
+
* power-of-two resolution that is ≤ framesPerPixel so we never render
|
|
36
|
+
* more than 1 peak per pixel.
|
|
37
|
+
*
|
|
38
|
+
* @param framesPerPixel Current `sampleRate / pixelsPerSecond`.
|
|
39
|
+
* @returns Recommended resolution (power of two, minimum 64).
|
|
40
|
+
*/
|
|
41
|
+
export declare function recommendResolution(framesPerPixel: number): number;
|
|
42
|
+
//# sourceMappingURL=computePeaks.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"computePeaks.d.ts","sourceRoot":"","sources":["../../src/waveform/computePeaks.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAC;AAE9C;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,uBAAuB,CACnC,WAAW,EAAE,YAAY,EACzB,UAAU,EAAE,MAAM,GACnB,QAAQ,CA4BV;AAED;;;;;;;;;GASG;AACH,wBAAgB,YAAY,CACxB,WAAW,EAAE,WAAW,EACxB,UAAU,EAAE,MAAM,EAClB,OAAO,GAAE,MAAU,GACpB,QAAQ,CAEV;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,mBAAmB,CAAC,cAAc,EAAE,MAAM,GAAG,MAAM,CAKlE"}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Compute peak data from a raw `Float32Array` of audio samples.
|
|
3
|
+
*
|
|
4
|
+
* This produces one min/max/rms entry per `resolution` frames, suitable for
|
|
5
|
+
* waveform rendering at the matching zoom level.
|
|
6
|
+
*
|
|
7
|
+
* @param channelData Raw PCM samples for a single channel.
|
|
8
|
+
* @param resolution Number of source frames per peak entry.
|
|
9
|
+
* @returns PeakData compatible with `Source.setPeakData()`.
|
|
10
|
+
*
|
|
11
|
+
* ```ts
|
|
12
|
+
* const data = audioBuffer.getChannelData(0);
|
|
13
|
+
* const peaks = computePeaksFromSamples(data, 512);
|
|
14
|
+
* source.setPeakData(512, peaks);
|
|
15
|
+
* ```
|
|
16
|
+
*/
|
|
17
|
+
export function computePeaksFromSamples(channelData, resolution) {
|
|
18
|
+
const length = Math.ceil(channelData.length / resolution);
|
|
19
|
+
const min = new Float32Array(length);
|
|
20
|
+
const max = new Float32Array(length);
|
|
21
|
+
const rms = new Float32Array(length);
|
|
22
|
+
for (let i = 0; i < length; i++) {
|
|
23
|
+
const start = i * resolution;
|
|
24
|
+
const end = Math.min(start + resolution, channelData.length);
|
|
25
|
+
let lo = Infinity;
|
|
26
|
+
let hi = -Infinity;
|
|
27
|
+
let sumSq = 0;
|
|
28
|
+
for (let j = start; j < end; j++) {
|
|
29
|
+
const v = channelData[j];
|
|
30
|
+
if (v < lo)
|
|
31
|
+
lo = v;
|
|
32
|
+
if (v > hi)
|
|
33
|
+
hi = v;
|
|
34
|
+
sumSq += v * v;
|
|
35
|
+
}
|
|
36
|
+
const count = end - start;
|
|
37
|
+
min[i] = lo === Infinity ? 0 : lo;
|
|
38
|
+
max[i] = hi === -Infinity ? 0 : hi;
|
|
39
|
+
rms[i] = count > 0 ? Math.sqrt(sumSq / count) : 0;
|
|
40
|
+
}
|
|
41
|
+
return { min, max, rms, length, resolution };
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Compute peak data from an `AudioBuffer`.
|
|
45
|
+
*
|
|
46
|
+
* Processes channel 0 by default. For stereo, call twice with different
|
|
47
|
+
* channel indices and store separately.
|
|
48
|
+
*
|
|
49
|
+
* @param audioBuffer Decoded Web Audio buffer.
|
|
50
|
+
* @param resolution Number of source frames per peak entry.
|
|
51
|
+
* @param channel Channel index (default 0).
|
|
52
|
+
*/
|
|
53
|
+
export function computePeaks(audioBuffer, resolution, channel = 0) {
|
|
54
|
+
return computePeaksFromSamples(audioBuffer.getChannelData(channel), resolution);
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Choose an appropriate peak resolution for a given zoom level.
|
|
58
|
+
*
|
|
59
|
+
* The idea: each peak entry should map to roughly 1 pixel. Since
|
|
60
|
+
* `framesPerPixel = sampleRate / pixelsPerSecond`, we pick the nearest
|
|
61
|
+
* power-of-two resolution that is ≤ framesPerPixel so we never render
|
|
62
|
+
* more than 1 peak per pixel.
|
|
63
|
+
*
|
|
64
|
+
* @param framesPerPixel Current `sampleRate / pixelsPerSecond`.
|
|
65
|
+
* @returns Recommended resolution (power of two, minimum 64).
|
|
66
|
+
*/
|
|
67
|
+
export function recommendResolution(framesPerPixel) {
|
|
68
|
+
const MIN_RESOLUTION = 64;
|
|
69
|
+
if (framesPerPixel <= MIN_RESOLUTION)
|
|
70
|
+
return MIN_RESOLUTION;
|
|
71
|
+
// Largest power of two ≤ framesPerPixel
|
|
72
|
+
return 1 << Math.floor(Math.log2(framesPerPixel));
|
|
73
|
+
}
|
|
74
|
+
//# sourceMappingURL=computePeaks.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"computePeaks.js","sourceRoot":"","sources":["../../src/waveform/computePeaks.ts"],"names":[],"mappings":"AAEA;;;;;;;;;;;;;;;GAeG;AACH,MAAM,UAAU,uBAAuB,CACnC,WAAyB,EACzB,UAAkB;IAElB,MAAM,MAAM,GAAG,IAAI,CAAC,IAAI,CAAC,WAAW,CAAC,MAAM,GAAG,UAAU,CAAC,CAAC;IAC1D,MAAM,GAAG,GAAG,IAAI,YAAY,CAAC,MAAM,CAAC,CAAC;IACrC,MAAM,GAAG,GAAG,IAAI,YAAY,CAAC,MAAM,CAAC,CAAC;IACrC,MAAM,GAAG,GAAG,IAAI,YAAY,CAAC,MAAM,CAAC,CAAC;IAErC,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,MAAM,EAAE,CAAC,EAAE,EAAE,CAAC;QAC9B,MAAM,KAAK,GAAG,CAAC,GAAG,UAAU,CAAC;QAC7B,MAAM,GAAG,GAAG,IAAI,CAAC,GAAG,CAAC,KAAK,GAAG,UAAU,EAAE,WAAW,CAAC,MAAM,CAAC,CAAC;QAE7D,IAAI,EAAE,GAAG,QAAQ,CAAC;QAClB,IAAI,EAAE,GAAG,CAAC,QAAQ,CAAC;QACnB,IAAI,KAAK,GAAG,CAAC,CAAC;QAEd,KAAK,IAAI,CAAC,GAAG,KAAK,EAAE,CAAC,GAAG,GAAG,EAAE,CAAC,EAAE,EAAE,CAAC;YAC/B,MAAM,CAAC,GAAG,WAAW,CAAC,CAAC,CAAC,CAAC;YACzB,IAAI,CAAC,GAAG,EAAE;gBAAE,EAAE,GAAG,CAAC,CAAC;YACnB,IAAI,CAAC,GAAG,EAAE;gBAAE,EAAE,GAAG,CAAC,CAAC;YACnB,KAAK,IAAI,CAAC,GAAG,CAAC,CAAC;QACnB,CAAC;QAED,MAAM,KAAK,GAAG,GAAG,GAAG,KAAK,CAAC;QAC1B,GAAG,CAAC,CAAC,CAAC,GAAG,EAAE,KAAK,QAAQ,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC;QAClC,GAAG,CAAC,CAAC,CAAC,GAAG,EAAE,KAAK,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC;QACnC,GAAG,CAAC,CAAC,CAAC,GAAG,KAAK,GAAG,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,KAAK,GAAG,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;IACtD,CAAC;IAED,OAAO,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,MAAM,EAAE,UAAU,EAAE,CAAC;AACjD,CAAC;AAED;;;;;;;;;GASG;AACH,MAAM,UAAU,YAAY,CACxB,WAAwB,EACxB,UAAkB,EAClB,UAAkB,CAAC;IAEnB,OAAO,uBAAuB,CAAC,WAAW,CAAC,cAAc,CAAC,OAAO,CAAC,EAAE,UAAU,CAAC,CAAC;AACpF,CAAC;AAED;;;;;;;;;;GAUG;AACH,MAAM,UAAU,mBAAmB,CAAC,cAAsB;IACtD,MAAM,cAAc,GAAG,EAAE,CAAC;IAC1B,IAAI,cAAc,IAAI,cAAc;QAAE,OAAO,cAAc,CAAC;IAC5D,wCAAwC;IACxC,OAAO,CAAC,IAAI,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,cAAc,CAAC,CAAC,CAAC;AACtD,CAAC"}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import type { PeakData, FrameCount } from '@drop-ai/core';
|
|
2
|
+
export interface WaveformRenderOptions {
|
|
3
|
+
/** Canvas 2D rendering context to draw into. */
|
|
4
|
+
ctx: CanvasRenderingContext2D;
|
|
5
|
+
/** Pre-computed peak data. */
|
|
6
|
+
peaks: PeakData;
|
|
7
|
+
/** Pixel width of the canvas / draw area. */
|
|
8
|
+
width: number;
|
|
9
|
+
/** Pixel height of the canvas / draw area. */
|
|
10
|
+
height: number;
|
|
11
|
+
/** Fill color for the waveform (default: '#1a1a1a'). */
|
|
12
|
+
color?: string;
|
|
13
|
+
/** Start frame offset within the source (default: 0). */
|
|
14
|
+
sourceStart?: FrameCount;
|
|
15
|
+
/** Number of frames to render (default: entire peak data). */
|
|
16
|
+
sourceLength?: FrameCount;
|
|
17
|
+
/** Y offset for drawing (for stereo: top/bottom half). */
|
|
18
|
+
yOffset?: number;
|
|
19
|
+
/** Use logarithmic dB scaling (default: false). */
|
|
20
|
+
logScale?: boolean;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Draw a waveform from pre-computed `PeakData` onto a Canvas 2D context.
|
|
24
|
+
*
|
|
25
|
+
* This is a pure rendering function — it doesn't fetch buffers or manage
|
|
26
|
+
* state. Feed it the output of `computePeaks()` and a canvas context.
|
|
27
|
+
*
|
|
28
|
+
* ```ts
|
|
29
|
+
* const peaks = computePeaks(audioBuffer, 512);
|
|
30
|
+
* const ctx = canvas.getContext('2d')!;
|
|
31
|
+
* renderWaveform({ ctx, peaks, width: 800, height: 120 });
|
|
32
|
+
* ```
|
|
33
|
+
*/
|
|
34
|
+
export declare function renderWaveform(options: WaveformRenderOptions): void;
|
|
35
|
+
/**
|
|
36
|
+
* Convenience: render a stereo waveform (L top half, R bottom half).
|
|
37
|
+
*/
|
|
38
|
+
export declare function renderStereoWaveform(ctx: CanvasRenderingContext2D, peaksL: PeakData, peaksR: PeakData, width: number, height: number, options?: Omit<WaveformRenderOptions, 'ctx' | 'peaks' | 'width' | 'height' | 'yOffset'>): void;
|
|
39
|
+
/**
|
|
40
|
+
* Render a waveform directly from raw samples (no pre-computed peaks).
|
|
41
|
+
*
|
|
42
|
+
* Convenient for small regions or one-off rendering where pre-computing
|
|
43
|
+
* peaks isn't worth it. For large buffers, prefer `computePeaks()` +
|
|
44
|
+
* `renderWaveform()`.
|
|
45
|
+
*/
|
|
46
|
+
export declare function renderWaveformFromSamples(ctx: CanvasRenderingContext2D, channelData: Float32Array, width: number, height: number, options?: {
|
|
47
|
+
color?: string;
|
|
48
|
+
sourceStart?: number;
|
|
49
|
+
sourceLength?: number;
|
|
50
|
+
yOffset?: number;
|
|
51
|
+
logScale?: boolean;
|
|
52
|
+
}): void;
|
|
53
|
+
//# sourceMappingURL=renderWaveform.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"renderWaveform.d.ts","sourceRoot":"","sources":["../../src/waveform/renderWaveform.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,UAAU,EAAE,MAAM,eAAe,CAAC;AAM1D,MAAM,WAAW,qBAAqB;IAClC,gDAAgD;IAChD,GAAG,EAAE,wBAAwB,CAAC;IAC9B,8BAA8B;IAC9B,KAAK,EAAE,QAAQ,CAAC;IAChB,6CAA6C;IAC7C,KAAK,EAAE,MAAM,CAAC;IACd,8CAA8C;IAC9C,MAAM,EAAE,MAAM,CAAC;IACf,wDAAwD;IACxD,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,yDAAyD;IACzD,WAAW,CAAC,EAAE,UAAU,CAAC;IACzB,8DAA8D;IAC9D,YAAY,CAAC,EAAE,UAAU,CAAC;IAC1B,0DAA0D;IAC1D,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,mDAAmD;IACnD,QAAQ,CAAC,EAAE,OAAO,CAAC;CACtB;AAkBD;;;;;;;;;;;GAWG;AACH,wBAAgB,cAAc,CAAC,OAAO,EAAE,qBAAqB,GAAG,IAAI,CAwDnE;AAED;;GAEG;AACH,wBAAgB,oBAAoB,CAChC,GAAG,EAAE,wBAAwB,EAC7B,MAAM,EAAE,QAAQ,EAChB,MAAM,EAAE,QAAQ,EAChB,KAAK,EAAE,MAAM,EACb,MAAM,EAAE,MAAM,EACd,OAAO,CAAC,EAAE,IAAI,CAAC,qBAAqB,EAAE,KAAK,GAAG,OAAO,GAAG,OAAO,GAAG,QAAQ,GAAG,SAAS,CAAC,GACxF,IAAI,CAaN;AAED;;;;;;GAMG;AACH,wBAAgB,yBAAyB,CACrC,GAAG,EAAE,wBAAwB,EAC7B,WAAW,EAAE,YAAY,EACzB,KAAK,EAAE,MAAM,EACb,MAAM,EAAE,MAAM,EACd,OAAO,CAAC,EAAE;IACN,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,QAAQ,CAAC,EAAE,OAAO,CAAC;CACtB,GACF,IAAI,CAqCN"}
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
// ---------------------------------------------------------------------------
|
|
2
|
+
// Helpers
|
|
3
|
+
// ---------------------------------------------------------------------------
|
|
4
|
+
function scaleAmpLog(v) {
|
|
5
|
+
if (v === 0)
|
|
6
|
+
return 0;
|
|
7
|
+
const sign = v < 0 ? -1 : 1;
|
|
8
|
+
const db = 20 * Math.log10(Math.abs(v));
|
|
9
|
+
const clamped = Math.max(-60, db);
|
|
10
|
+
return sign * ((clamped + 60) / 60);
|
|
11
|
+
}
|
|
12
|
+
// ---------------------------------------------------------------------------
|
|
13
|
+
// renderWaveform
|
|
14
|
+
// ---------------------------------------------------------------------------
|
|
15
|
+
/**
|
|
16
|
+
* Draw a waveform from pre-computed `PeakData` onto a Canvas 2D context.
|
|
17
|
+
*
|
|
18
|
+
* This is a pure rendering function — it doesn't fetch buffers or manage
|
|
19
|
+
* state. Feed it the output of `computePeaks()` and a canvas context.
|
|
20
|
+
*
|
|
21
|
+
* ```ts
|
|
22
|
+
* const peaks = computePeaks(audioBuffer, 512);
|
|
23
|
+
* const ctx = canvas.getContext('2d')!;
|
|
24
|
+
* renderWaveform({ ctx, peaks, width: 800, height: 120 });
|
|
25
|
+
* ```
|
|
26
|
+
*/
|
|
27
|
+
export function renderWaveform(options) {
|
|
28
|
+
const { ctx, peaks, width, height, color = '#1a1a1a', sourceStart = 0, sourceLength, yOffset = 0, logScale = false, } = options;
|
|
29
|
+
const scale = logScale ? scaleAmpLog : (v) => v;
|
|
30
|
+
// Determine the range of peak entries to render
|
|
31
|
+
const peakStart = Math.floor(sourceStart / peaks.resolution);
|
|
32
|
+
const totalPeakEntries = sourceLength != null
|
|
33
|
+
? Math.ceil(sourceLength / peaks.resolution)
|
|
34
|
+
: peaks.length - peakStart;
|
|
35
|
+
const peakEnd = Math.min(peakStart + totalPeakEntries, peaks.length);
|
|
36
|
+
const peakCount = peakEnd - peakStart;
|
|
37
|
+
if (peakCount <= 0)
|
|
38
|
+
return;
|
|
39
|
+
ctx.fillStyle = color;
|
|
40
|
+
const amp = height / 2;
|
|
41
|
+
// How many peak entries map to one pixel column
|
|
42
|
+
const entriesPerPixel = peakCount / width;
|
|
43
|
+
for (let px = 0; px < width; px++) {
|
|
44
|
+
const entryStart = peakStart + Math.floor(px * entriesPerPixel);
|
|
45
|
+
const entryEnd = Math.min(peakStart + Math.floor((px + 1) * entriesPerPixel), peakEnd);
|
|
46
|
+
let lo = Infinity;
|
|
47
|
+
let hi = -Infinity;
|
|
48
|
+
for (let e = entryStart; e < entryEnd; e++) {
|
|
49
|
+
if (peaks.min[e] < lo)
|
|
50
|
+
lo = peaks.min[e];
|
|
51
|
+
if (peaks.max[e] > hi)
|
|
52
|
+
hi = peaks.max[e];
|
|
53
|
+
}
|
|
54
|
+
// Fallback for very zoomed in (< 1 entry per pixel)
|
|
55
|
+
if (lo === Infinity) {
|
|
56
|
+
const idx = Math.min(entryStart, peaks.length - 1);
|
|
57
|
+
lo = peaks.min[idx];
|
|
58
|
+
hi = peaks.max[idx];
|
|
59
|
+
}
|
|
60
|
+
const sMin = scale(lo);
|
|
61
|
+
const sMax = scale(hi);
|
|
62
|
+
const y = yOffset + (1 - sMax) * amp;
|
|
63
|
+
const h = Math.max(1, (sMax - sMin) * amp);
|
|
64
|
+
ctx.fillRect(px, y, 1, h);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Convenience: render a stereo waveform (L top half, R bottom half).
|
|
69
|
+
*/
|
|
70
|
+
export function renderStereoWaveform(ctx, peaksL, peaksR, width, height, options) {
|
|
71
|
+
const halfHeight = height / 2;
|
|
72
|
+
renderWaveform(Object.assign({ ctx, peaks: peaksL, width, height: halfHeight, yOffset: 0 }, options));
|
|
73
|
+
// Draw separator
|
|
74
|
+
ctx.strokeStyle = 'rgba(255,255,255,0.15)';
|
|
75
|
+
ctx.beginPath();
|
|
76
|
+
ctx.moveTo(0, halfHeight);
|
|
77
|
+
ctx.lineTo(width, halfHeight);
|
|
78
|
+
ctx.stroke();
|
|
79
|
+
renderWaveform(Object.assign({ ctx, peaks: peaksR, width, height: halfHeight, yOffset: halfHeight }, options));
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Render a waveform directly from raw samples (no pre-computed peaks).
|
|
83
|
+
*
|
|
84
|
+
* Convenient for small regions or one-off rendering where pre-computing
|
|
85
|
+
* peaks isn't worth it. For large buffers, prefer `computePeaks()` +
|
|
86
|
+
* `renderWaveform()`.
|
|
87
|
+
*/
|
|
88
|
+
export function renderWaveformFromSamples(ctx, channelData, width, height, options) {
|
|
89
|
+
const { color = '#1a1a1a', sourceStart = 0, sourceLength, yOffset = 0, logScale = false, } = options !== null && options !== void 0 ? options : {};
|
|
90
|
+
const scale = logScale ? scaleAmpLog : (v) => v;
|
|
91
|
+
const sampleOffset = Math.max(0, Math.floor(sourceStart));
|
|
92
|
+
const sampleCount = sourceLength != null
|
|
93
|
+
? Math.min(Math.floor(sourceLength), channelData.length - sampleOffset)
|
|
94
|
+
: channelData.length - sampleOffset;
|
|
95
|
+
ctx.fillStyle = color;
|
|
96
|
+
const amp = height / 2;
|
|
97
|
+
const step = Math.ceil(sampleCount / width);
|
|
98
|
+
for (let px = 0; px < width; px++) {
|
|
99
|
+
let lo = 1.0;
|
|
100
|
+
let hi = -1.0;
|
|
101
|
+
for (let j = 0; j < step; j++) {
|
|
102
|
+
const idx = sampleOffset + px * step + j;
|
|
103
|
+
if (idx >= channelData.length)
|
|
104
|
+
break;
|
|
105
|
+
const v = channelData[idx];
|
|
106
|
+
if (v < lo)
|
|
107
|
+
lo = v;
|
|
108
|
+
if (v > hi)
|
|
109
|
+
hi = v;
|
|
110
|
+
}
|
|
111
|
+
const sMin = scale(lo);
|
|
112
|
+
const sMax = scale(hi);
|
|
113
|
+
const y = yOffset + (1 - sMax) * amp;
|
|
114
|
+
const h = Math.max(1, (sMax - sMin) * amp);
|
|
115
|
+
ctx.fillRect(px, y, 1, h);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
//# sourceMappingURL=renderWaveform.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"renderWaveform.js","sourceRoot":"","sources":["../../src/waveform/renderWaveform.ts"],"names":[],"mappings":"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"}
|
package/package.json
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@drop-ai/ui-utils",
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "Framework-agnostic UI utilities for building DAW interfaces on top of @drop-ai/core",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"main": "./dist/index.js",
|
|
8
|
+
"types": "./dist/index.d.ts",
|
|
9
|
+
"exports": {
|
|
10
|
+
".": {
|
|
11
|
+
"types": "./dist/index.d.ts",
|
|
12
|
+
"import": "./dist/index.js"
|
|
13
|
+
},
|
|
14
|
+
"./*": {
|
|
15
|
+
"types": "./dist/*.d.ts",
|
|
16
|
+
"import": "./dist/*.js"
|
|
17
|
+
}
|
|
18
|
+
},
|
|
19
|
+
"files": [
|
|
20
|
+
"dist",
|
|
21
|
+
"README.md",
|
|
22
|
+
"LICENSE"
|
|
23
|
+
],
|
|
24
|
+
"sideEffects": false,
|
|
25
|
+
"engines": {
|
|
26
|
+
"node": ">=18"
|
|
27
|
+
},
|
|
28
|
+
"dependencies": {
|
|
29
|
+
"@drop-ai/core": "0.3.0"
|
|
30
|
+
},
|
|
31
|
+
"devDependencies": {
|
|
32
|
+
"typescript": "^5.7.2"
|
|
33
|
+
},
|
|
34
|
+
"keywords": [
|
|
35
|
+
"daw",
|
|
36
|
+
"audio",
|
|
37
|
+
"ui",
|
|
38
|
+
"waveform",
|
|
39
|
+
"timeline",
|
|
40
|
+
"viewport"
|
|
41
|
+
],
|
|
42
|
+
"repository": {
|
|
43
|
+
"type": "git",
|
|
44
|
+
"url": "https://github.com/whizzkid/drop.ai",
|
|
45
|
+
"directory": "packages/ui-utils"
|
|
46
|
+
},
|
|
47
|
+
"scripts": {
|
|
48
|
+
"build": "tsc -p tsconfig.build.json",
|
|
49
|
+
"clean": "rm -rf dist",
|
|
50
|
+
"typecheck": "tsc --noEmit"
|
|
51
|
+
}
|
|
52
|
+
}
|