@keplar-404/react-timeline-editor 1.0.6
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/components/control_area/index.d.ts +0 -0
- package/dist/components/cursor/cursor.d.ts +20 -0
- package/dist/components/cut-overlay/CutOverlay.d.ts +202 -0
- package/dist/components/edit_area/cross_row_drag.d.ts +50 -0
- package/dist/components/edit_area/drag_lines.d.ts +11 -0
- package/dist/components/edit_area/drag_preview.d.ts +14 -0
- package/dist/components/edit_area/drag_utils.d.ts +39 -0
- package/dist/components/edit_area/edit_action.d.ts +19 -0
- package/dist/components/edit_area/edit_area.d.ts +56 -0
- package/dist/components/edit_area/edit_row.d.ts +27 -0
- package/dist/components/edit_area/hooks/use_drag_line.d.ts +33 -0
- package/dist/components/edit_area/insertion_line.d.ts +12 -0
- package/dist/components/loop-zone/LoopZoneOverlay.d.ts +243 -0
- package/dist/components/row_rnd/hooks/useAutoScroll.d.ts +7 -0
- package/dist/components/row_rnd/interactable.d.ts +14 -0
- package/dist/components/row_rnd/row_rnd.d.ts +3 -0
- package/dist/components/row_rnd/row_rnd_interface.d.ts +47 -0
- package/dist/components/time_area/time_area.d.ts +17 -0
- package/dist/components/timeline.d.ts +3 -0
- package/dist/components/transport/TransportBar.d.ts +132 -0
- package/dist/components/transport/useTimelinePlayer.d.ts +164 -0
- package/dist/index.cjs.js +13 -0
- package/dist/index.d.ts +12 -0
- package/dist/index.es.js +10102 -0
- package/dist/index.umd.js +13 -0
- package/dist/interface/common_prop.d.ts +12 -0
- package/dist/interface/const.d.ts +28 -0
- package/dist/interface/timeline.d.ts +342 -0
- package/dist/react-timeline-editor.css +1 -0
- package/dist/utils/check_props.d.ts +2 -0
- package/dist/utils/deal_class_prefix.d.ts +1 -0
- package/dist/utils/deal_data.d.ts +58 -0
- package/dist/utils/logger.d.ts +132 -0
- package/package.json +70 -0
- package/src/components/control_area/index.tsx +1 -0
- package/src/components/cursor/cursor.css +26 -0
- package/src/components/cursor/cursor.tsx +105 -0
- package/src/components/cut-overlay/CutOverlay.css +68 -0
- package/src/components/cut-overlay/CutOverlay.tsx +491 -0
- package/src/components/edit_area/cross_row_drag.tsx +174 -0
- package/src/components/edit_area/drag_lines.css +13 -0
- package/src/components/edit_area/drag_lines.tsx +31 -0
- package/src/components/edit_area/drag_preview.tsx +50 -0
- package/src/components/edit_area/drag_utils.ts +77 -0
- package/src/components/edit_area/edit_action.css +56 -0
- package/src/components/edit_area/edit_action.tsx +362 -0
- package/src/components/edit_area/edit_area.css +24 -0
- package/src/components/edit_area/edit_area.tsx +606 -0
- package/src/components/edit_area/edit_row.css +78 -0
- package/src/components/edit_area/edit_row.tsx +128 -0
- package/src/components/edit_area/hooks/use_drag_line.ts +93 -0
- package/src/components/edit_area/insertion_line.tsx +39 -0
- package/src/components/loop-zone/LoopZoneOverlay.css +65 -0
- package/src/components/loop-zone/LoopZoneOverlay.tsx +461 -0
- package/src/components/row_rnd/hooks/useAutoScroll.ts +81 -0
- package/src/components/row_rnd/interactable.tsx +55 -0
- package/src/components/row_rnd/row_rnd.tsx +365 -0
- package/src/components/row_rnd/row_rnd_interface.ts +59 -0
- package/src/components/time_area/time_area.css +35 -0
- package/src/components/time_area/time_area.tsx +93 -0
- package/src/components/timeline.css +12 -0
- package/src/components/timeline.tsx +227 -0
- package/src/components/transport/TransportBar.css +171 -0
- package/src/components/transport/TransportBar.tsx +322 -0
- package/src/components/transport/useTimelinePlayer.ts +319 -0
- package/src/index.tsx +17 -0
- package/src/interface/common_prop.ts +13 -0
- package/src/interface/const.ts +32 -0
- package/src/interface/timeline.ts +329 -0
- package/src/utils/check_props.ts +77 -0
- package/src/utils/deal_class_prefix.ts +6 -0
- package/src/utils/deal_data.ts +159 -0
- package/src/utils/logger.ts +239 -0
|
@@ -0,0 +1,322 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { TimelinePlayerState, formatTime } from './useTimelinePlayer';
|
|
3
|
+
import './TransportBar.css';
|
|
4
|
+
|
|
5
|
+
// ─────────────────────────────────────────────
|
|
6
|
+
// Public Types
|
|
7
|
+
// ─────────────────────────────────────────────
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Loop control props for the {@link TransportBar} component.
|
|
11
|
+
* Supply this to enable the loop toggle button and start/end inputs.
|
|
12
|
+
*/
|
|
13
|
+
export interface TransportBarLoopProps {
|
|
14
|
+
/**
|
|
15
|
+
* Whether the loop is currently active.
|
|
16
|
+
* Controls the visual state of the loop button (lit vs. dim).
|
|
17
|
+
*/
|
|
18
|
+
enabled: boolean;
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Loop region start time in seconds.
|
|
22
|
+
*/
|
|
23
|
+
start: number;
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Loop region end time in seconds.
|
|
27
|
+
*/
|
|
28
|
+
end: number;
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Called when the user clicks the loop toggle button.
|
|
32
|
+
*/
|
|
33
|
+
onToggle: () => void;
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Called when the user edits the loop start input.
|
|
37
|
+
* @param time - New start time in seconds.
|
|
38
|
+
*/
|
|
39
|
+
onStartChange: (time: number) => void;
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Called when the user edits the loop end input.
|
|
43
|
+
* @param time - New end time in seconds.
|
|
44
|
+
*/
|
|
45
|
+
onEndChange: (time: number) => void;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Props for the {@link TransportBar} component.
|
|
50
|
+
*
|
|
51
|
+
* @example
|
|
52
|
+
* ```tsx
|
|
53
|
+
* const player = useTimelinePlayer(timelineRef, { loop: { enabled, start, end } });
|
|
54
|
+
*
|
|
55
|
+
* <TransportBar
|
|
56
|
+
* player={player}
|
|
57
|
+
* loop={{ enabled, start, end, onToggle, onStartChange, onEndChange }}
|
|
58
|
+
* />
|
|
59
|
+
* ```
|
|
60
|
+
*/
|
|
61
|
+
export interface TransportBarProps {
|
|
62
|
+
/**
|
|
63
|
+
* The return value of `useTimelinePlayer()`.
|
|
64
|
+
* Provides all reactive state and control functions.
|
|
65
|
+
*
|
|
66
|
+
* @see {@link TimelinePlayerState}
|
|
67
|
+
*/
|
|
68
|
+
player: TimelinePlayerState;
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Optional loop controls.
|
|
72
|
+
* When provided, a loop toggle button (🔁) appears after the stop button,
|
|
73
|
+
* and start/end time inputs appear when the loop is active.
|
|
74
|
+
*
|
|
75
|
+
* @see {@link TransportBarLoopProps}
|
|
76
|
+
*/
|
|
77
|
+
loop?: TransportBarLoopProps;
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Playback rate options displayed as speed buttons.
|
|
81
|
+
* @default [0.25, 0.5, 1, 2, 4]
|
|
82
|
+
*/
|
|
83
|
+
playRates?: number[];
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Custom time format function.
|
|
87
|
+
* Receives the current time in seconds, should return a display string.
|
|
88
|
+
*
|
|
89
|
+
* @default Built-in `formatTime` (produces `'M:SS.mm'` format)
|
|
90
|
+
*
|
|
91
|
+
* @example
|
|
92
|
+
* ```tsx
|
|
93
|
+
* formatDisplayTime={(t) => `${(t * 1000).toFixed(0)} ms`}
|
|
94
|
+
* ```
|
|
95
|
+
*/
|
|
96
|
+
formatDisplayTime?: (seconds: number) => string;
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Additional CSS class name for the transport bar root element.
|
|
100
|
+
*/
|
|
101
|
+
className?: string;
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Inline styles for the transport bar root element.
|
|
105
|
+
*/
|
|
106
|
+
style?: React.CSSProperties;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// ─────────────────────────────────────────────
|
|
110
|
+
// Default values
|
|
111
|
+
// ─────────────────────────────────────────────
|
|
112
|
+
|
|
113
|
+
const DEFAULT_PLAY_RATES = [0.25, 0.5, 1, 2, 4];
|
|
114
|
+
|
|
115
|
+
// ─────────────────────────────────────────────
|
|
116
|
+
// Component
|
|
117
|
+
// ─────────────────────────────────────────────
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* `TransportBar` is a pre-built playback control bar for use with `<Timeline>`.
|
|
121
|
+
*
|
|
122
|
+
* It renders transport controls (to-start, rewind, play/pause, forward, stop),
|
|
123
|
+
* a time display, playback rate buttons, and optionally a loop toggle with
|
|
124
|
+
* start/end inputs.
|
|
125
|
+
*
|
|
126
|
+
* Pair it with {@link useTimelinePlayer} to wire it to a `<Timeline>` ref:
|
|
127
|
+
*
|
|
128
|
+
* @example
|
|
129
|
+
* ```tsx
|
|
130
|
+
* const timelineRef = useRef<TimelineState>(null);
|
|
131
|
+
* const [loopOn, setLoopOn] = useState(false);
|
|
132
|
+
* const [loopStart, setLoopStart] = useState(1);
|
|
133
|
+
* const [loopEnd, setLoopEnd] = useState(3);
|
|
134
|
+
*
|
|
135
|
+
* const player = useTimelinePlayer(timelineRef, {
|
|
136
|
+
* loop: { enabled: loopOn, start: loopStart, end: loopEnd },
|
|
137
|
+
* });
|
|
138
|
+
*
|
|
139
|
+
* return (
|
|
140
|
+
* <>
|
|
141
|
+
* <TransportBar
|
|
142
|
+
* player={player}
|
|
143
|
+
* loop={{
|
|
144
|
+
* enabled: loopOn, start: loopStart, end: loopEnd,
|
|
145
|
+
* onToggle: () => setLoopOn((v) => !v),
|
|
146
|
+
* onStartChange: setLoopStart,
|
|
147
|
+
* onEndChange: setLoopEnd,
|
|
148
|
+
* }}
|
|
149
|
+
* />
|
|
150
|
+
* <Timeline ref={timelineRef} ... />
|
|
151
|
+
* </>
|
|
152
|
+
* );
|
|
153
|
+
* ```
|
|
154
|
+
*
|
|
155
|
+
* **Building your own UI?**
|
|
156
|
+
* Skip this component entirely and use just `useTimelinePlayer()` to get the state
|
|
157
|
+
* and handlers, then wire them to your own custom controls.
|
|
158
|
+
*/
|
|
159
|
+
const TransportBar: React.FC<TransportBarProps> = ({
|
|
160
|
+
player,
|
|
161
|
+
loop,
|
|
162
|
+
playRates = DEFAULT_PLAY_RATES,
|
|
163
|
+
formatDisplayTime = formatTime,
|
|
164
|
+
className,
|
|
165
|
+
style,
|
|
166
|
+
}) => {
|
|
167
|
+
const {
|
|
168
|
+
isPlaying, currentTime, playRate,
|
|
169
|
+
play, pause, stop, toStart, rewind, forward, setPlayRate,
|
|
170
|
+
} = player;
|
|
171
|
+
|
|
172
|
+
return (
|
|
173
|
+
<div
|
|
174
|
+
className={`timeline-transport-bar${className ? ` ${className}` : ''}`}
|
|
175
|
+
style={style}
|
|
176
|
+
>
|
|
177
|
+
{/* ── Left: transport controls ── */}
|
|
178
|
+
<div className="timeline-transport-controls">
|
|
179
|
+
{/* To start */}
|
|
180
|
+
<button
|
|
181
|
+
className="timeline-transport-btn"
|
|
182
|
+
title="Return to start"
|
|
183
|
+
onClick={toStart}
|
|
184
|
+
>
|
|
185
|
+
<svg width="14" height="14" viewBox="0 0 14 14" fill="currentColor">
|
|
186
|
+
<rect x="1" y="1" width="2" height="12" rx="1" />
|
|
187
|
+
<path d="M13 2.27L5.5 7 13 11.73V2.27z" />
|
|
188
|
+
</svg>
|
|
189
|
+
</button>
|
|
190
|
+
|
|
191
|
+
{/* Rewind */}
|
|
192
|
+
<button
|
|
193
|
+
className="timeline-transport-btn"
|
|
194
|
+
title="Rewind"
|
|
195
|
+
onClick={rewind}
|
|
196
|
+
>
|
|
197
|
+
<svg width="14" height="14" viewBox="0 0 14 14" fill="currentColor">
|
|
198
|
+
<path d="M7.5 2.27L0 7l7.5 4.73V8.96L2.92 7l4.58-5zM14 2.27L6.5 7 14 11.73V2.27z" />
|
|
199
|
+
</svg>
|
|
200
|
+
</button>
|
|
201
|
+
|
|
202
|
+
{/* Play / Pause */}
|
|
203
|
+
{isPlaying ? (
|
|
204
|
+
<button
|
|
205
|
+
className="timeline-transport-btn timeline-transport-btn--primary"
|
|
206
|
+
title="Pause"
|
|
207
|
+
onClick={pause}
|
|
208
|
+
>
|
|
209
|
+
<svg width="14" height="14" viewBox="0 0 14 14" fill="currentColor">
|
|
210
|
+
<rect x="2" y="1" width="4" height="12" rx="1" />
|
|
211
|
+
<rect x="8" y="1" width="4" height="12" rx="1" />
|
|
212
|
+
</svg>
|
|
213
|
+
</button>
|
|
214
|
+
) : (
|
|
215
|
+
<button
|
|
216
|
+
className="timeline-transport-btn timeline-transport-btn--primary"
|
|
217
|
+
title="Play"
|
|
218
|
+
onClick={play}
|
|
219
|
+
>
|
|
220
|
+
<svg width="14" height="14" viewBox="0 0 14 14" fill="currentColor">
|
|
221
|
+
<path d="M2 1.5l11 5.5-11 5.5z" />
|
|
222
|
+
</svg>
|
|
223
|
+
</button>
|
|
224
|
+
)}
|
|
225
|
+
|
|
226
|
+
{/* Forward */}
|
|
227
|
+
<button
|
|
228
|
+
className="timeline-transport-btn"
|
|
229
|
+
title="Forward"
|
|
230
|
+
onClick={forward}
|
|
231
|
+
>
|
|
232
|
+
<svg width="14" height="14" viewBox="0 0 14 14" fill="currentColor">
|
|
233
|
+
<path d="M6.5 2.27V5.04L11.08 7 6.5 8.96v2.77L14 7 6.5 2.27zM0 2.27L7.5 7 0 11.73V2.27z" />
|
|
234
|
+
</svg>
|
|
235
|
+
</button>
|
|
236
|
+
|
|
237
|
+
{/* Stop */}
|
|
238
|
+
<button
|
|
239
|
+
className="timeline-transport-btn timeline-transport-btn--stop"
|
|
240
|
+
title="Stop"
|
|
241
|
+
onClick={stop}
|
|
242
|
+
>
|
|
243
|
+
<svg width="14" height="14" viewBox="0 0 14 14" fill="currentColor">
|
|
244
|
+
<rect x="1" y="1" width="12" height="12" rx="2" />
|
|
245
|
+
</svg>
|
|
246
|
+
</button>
|
|
247
|
+
|
|
248
|
+
{/* ── Loop controls (optional) ── */}
|
|
249
|
+
{loop && (
|
|
250
|
+
<>
|
|
251
|
+
<div className="timeline-transport-divider" />
|
|
252
|
+
|
|
253
|
+
<button
|
|
254
|
+
className={`timeline-transport-btn timeline-transport-btn--loop${loop.enabled ? ' timeline-transport-btn--loop-active' : ''}`}
|
|
255
|
+
title={loop.enabled ? 'Disable loop' : 'Enable loop'}
|
|
256
|
+
onClick={loop.onToggle}
|
|
257
|
+
>
|
|
258
|
+
🔁
|
|
259
|
+
</button>
|
|
260
|
+
|
|
261
|
+
{loop.enabled && (
|
|
262
|
+
<div className="timeline-transport-loop-inputs">
|
|
263
|
+
<input
|
|
264
|
+
className="timeline-transport-loop-input"
|
|
265
|
+
type="number"
|
|
266
|
+
step="0.1"
|
|
267
|
+
min="0"
|
|
268
|
+
value={loop.start}
|
|
269
|
+
title="Loop start (seconds)"
|
|
270
|
+
onChange={(e) => {
|
|
271
|
+
const v = parseFloat(e.target.value);
|
|
272
|
+
if (!isNaN(v) && v >= 0 && v < loop.end) loop.onStartChange(v);
|
|
273
|
+
}}
|
|
274
|
+
/>
|
|
275
|
+
<span className="timeline-transport-loop-arrow">→</span>
|
|
276
|
+
<input
|
|
277
|
+
className="timeline-transport-loop-input"
|
|
278
|
+
type="number"
|
|
279
|
+
step="0.1"
|
|
280
|
+
min="0"
|
|
281
|
+
value={loop.end}
|
|
282
|
+
title="Loop end (seconds)"
|
|
283
|
+
onChange={(e) => {
|
|
284
|
+
const v = parseFloat(e.target.value);
|
|
285
|
+
if (!isNaN(v) && v > loop.start) loop.onEndChange(v);
|
|
286
|
+
}}
|
|
287
|
+
/>
|
|
288
|
+
<span className="timeline-transport-loop-unit">s</span>
|
|
289
|
+
</div>
|
|
290
|
+
)}
|
|
291
|
+
</>
|
|
292
|
+
)}
|
|
293
|
+
</div>
|
|
294
|
+
|
|
295
|
+
{/* ── Centre: time display ── */}
|
|
296
|
+
<div className="timeline-transport-time">
|
|
297
|
+
<span className="timeline-transport-time-value">
|
|
298
|
+
{formatDisplayTime(currentTime)}
|
|
299
|
+
</span>
|
|
300
|
+
<span className="timeline-transport-time-label">TIME</span>
|
|
301
|
+
</div>
|
|
302
|
+
|
|
303
|
+
{/* ── Right: playback rate ── */}
|
|
304
|
+
<div className="timeline-transport-rate">
|
|
305
|
+
<span className="timeline-transport-rate-label">SPEED</span>
|
|
306
|
+
<div className="timeline-transport-rate-buttons">
|
|
307
|
+
{playRates.map((r) => (
|
|
308
|
+
<button
|
|
309
|
+
key={r}
|
|
310
|
+
className={`timeline-transport-rate-btn${playRate === r ? ' timeline-transport-rate-btn--active' : ''}`}
|
|
311
|
+
onClick={() => setPlayRate(r)}
|
|
312
|
+
>
|
|
313
|
+
{r}×
|
|
314
|
+
</button>
|
|
315
|
+
))}
|
|
316
|
+
</div>
|
|
317
|
+
</div>
|
|
318
|
+
</div>
|
|
319
|
+
);
|
|
320
|
+
};
|
|
321
|
+
|
|
322
|
+
export default TransportBar;
|
|
@@ -0,0 +1,319 @@
|
|
|
1
|
+
import { useCallback, useEffect, useRef, useState } from 'react';
|
|
2
|
+
import type { TimelineState } from '../../interface/timeline';
|
|
3
|
+
|
|
4
|
+
// ─────────────────────────────────────────────
|
|
5
|
+
// Utility
|
|
6
|
+
// ─────────────────────────────────────────────
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Formats a time value in seconds to a `M:SS.mm` display string.
|
|
10
|
+
*
|
|
11
|
+
* @param seconds - Time in seconds (e.g. `75.4` → `'1:15.40'`).
|
|
12
|
+
* @returns Formatted time string.
|
|
13
|
+
*
|
|
14
|
+
* @example
|
|
15
|
+
* ```ts
|
|
16
|
+
* formatTime(0); // '0:00.00'
|
|
17
|
+
* formatTime(75.4); // '1:15.40'
|
|
18
|
+
* formatTime(3661.1); // '61:01.10'
|
|
19
|
+
* ```
|
|
20
|
+
*/
|
|
21
|
+
export function formatTime(seconds: number): string {
|
|
22
|
+
const m = Math.floor(seconds / 60);
|
|
23
|
+
const s = Math.floor(seconds % 60);
|
|
24
|
+
const ms = Math.floor((seconds % 1) * 100);
|
|
25
|
+
return `${m}:${String(s).padStart(2, '0')}.${String(ms).padStart(2, '0')}`;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// ─────────────────────────────────────────────
|
|
29
|
+
// Public Types
|
|
30
|
+
// ─────────────────────────────────────────────
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Loop configuration passed to {@link useTimelinePlayer}.
|
|
34
|
+
*
|
|
35
|
+
* When `enabled` is `true`, the hook automatically resets playback to `start`
|
|
36
|
+
* whenever the current time reaches `end`. The check uses React refs internally,
|
|
37
|
+
* so it never suffers from stale-closure issues even as values change.
|
|
38
|
+
*/
|
|
39
|
+
export interface UseTimelinePlayerLoop {
|
|
40
|
+
/**
|
|
41
|
+
* Whether the loop is active. Safe to toggle while the timeline is playing.
|
|
42
|
+
*/
|
|
43
|
+
enabled: boolean;
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Loop region start time in seconds.
|
|
47
|
+
* Must be less than `end`.
|
|
48
|
+
*/
|
|
49
|
+
start: number;
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Loop region end time in seconds.
|
|
53
|
+
* Must be greater than `start`.
|
|
54
|
+
*/
|
|
55
|
+
end: number;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Options for the {@link useTimelinePlayer} hook.
|
|
60
|
+
*/
|
|
61
|
+
export interface UseTimelinePlayerOptions {
|
|
62
|
+
/**
|
|
63
|
+
* Number of seconds to jump when calling `rewind()` or `forward()`.
|
|
64
|
+
* @default 5
|
|
65
|
+
*/
|
|
66
|
+
seekStep?: number;
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Optional loop configuration.
|
|
70
|
+
* When provided, the hook handles the loop clock internally — no extra code needed.
|
|
71
|
+
*
|
|
72
|
+
* @see {@link UseTimelinePlayerLoop}
|
|
73
|
+
*
|
|
74
|
+
* @example
|
|
75
|
+
* ```tsx
|
|
76
|
+
* const player = useTimelinePlayer(ref, {
|
|
77
|
+
* loop: { enabled: loopOn, start: 1, end: 3 },
|
|
78
|
+
* });
|
|
79
|
+
* ```
|
|
80
|
+
*/
|
|
81
|
+
loop?: UseTimelinePlayerLoop;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* State and controls returned by {@link useTimelinePlayer}.
|
|
86
|
+
*
|
|
87
|
+
* Destructure what you need — use it with the built-in `<TransportBar>` component
|
|
88
|
+
* or wire it to your own custom controls.
|
|
89
|
+
*
|
|
90
|
+
* @example
|
|
91
|
+
* ```tsx
|
|
92
|
+
* const { isPlaying, currentTime, play, pause, stop } = useTimelinePlayer(ref);
|
|
93
|
+
*
|
|
94
|
+
* <button onClick={isPlaying ? pause : play}>
|
|
95
|
+
* {isPlaying ? '⏸' : '▶'}
|
|
96
|
+
* </button>
|
|
97
|
+
* <span>{formatTime(currentTime)}</span>
|
|
98
|
+
* ```
|
|
99
|
+
*/
|
|
100
|
+
export interface TimelinePlayerState {
|
|
101
|
+
// ── Reactive state ─────────────────────────────────────────────────────────
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Whether the timeline is currently playing.
|
|
105
|
+
* Updated automatically from engine events — no polling required.
|
|
106
|
+
*/
|
|
107
|
+
isPlaying: boolean;
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Current playback position in seconds.
|
|
111
|
+
* Updated on every engine tick — suitable for display.
|
|
112
|
+
*/
|
|
113
|
+
currentTime: number;
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Current playback rate multiplier (e.g. `1` = 1×, `2` = 2×).
|
|
117
|
+
* Updated when `setPlayRate` is called.
|
|
118
|
+
*/
|
|
119
|
+
playRate: number;
|
|
120
|
+
|
|
121
|
+
// ── Playback controls ──────────────────────────────────────────────────────
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Start playback from the current position.
|
|
125
|
+
* Plays to the end of the timeline and then stops.
|
|
126
|
+
*/
|
|
127
|
+
play: () => void;
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Pause playback at the current position.
|
|
131
|
+
*/
|
|
132
|
+
pause: () => void;
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Stop playback and reset the cursor to time `0`.
|
|
136
|
+
*/
|
|
137
|
+
stop: () => void;
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Pause and jump the cursor back to time `0`.
|
|
141
|
+
* Differs from `stop` only in semantics — both do the same thing currently.
|
|
142
|
+
*/
|
|
143
|
+
toStart: () => void;
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Jump backwards by `seekStep` seconds (clamped to 0).
|
|
147
|
+
*/
|
|
148
|
+
rewind: () => void;
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Jump forwards by `seekStep` seconds.
|
|
152
|
+
*/
|
|
153
|
+
forward: () => void;
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Jump to a specific time value (seconds).
|
|
157
|
+
*
|
|
158
|
+
* @param time - Target time in seconds.
|
|
159
|
+
*/
|
|
160
|
+
setTime: (time: number) => void;
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Change the playback rate multiplier.
|
|
164
|
+
*
|
|
165
|
+
* @param rate - Multiplier value (e.g. `0.5`, `1`, `2`, `4`).
|
|
166
|
+
*/
|
|
167
|
+
setPlayRate: (rate: number) => void;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// ─────────────────────────────────────────────
|
|
171
|
+
// Hook
|
|
172
|
+
// ─────────────────────────────────────────────
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* `useTimelinePlayer` manages playback state and controls for a `<Timeline>` component.
|
|
176
|
+
*
|
|
177
|
+
* It attaches engine event listeners (`play`, `paused`, `setTimeByTick`) on mount,
|
|
178
|
+
* exposes reactive state (`isPlaying`, `currentTime`, `playRate`) and all control
|
|
179
|
+
* functions. Optionally handles loop playback internally via the `loop` option.
|
|
180
|
+
*
|
|
181
|
+
* @param ref - A React ref pointing to the `<Timeline>` component's imperative handle.
|
|
182
|
+
* @param options - Optional configuration (`seekStep`, `loop`).
|
|
183
|
+
* @returns {@link TimelinePlayerState} — reactive state + control functions.
|
|
184
|
+
*
|
|
185
|
+
* @example
|
|
186
|
+
* ```tsx
|
|
187
|
+
* const timelineRef = useRef<TimelineState>(null);
|
|
188
|
+
*
|
|
189
|
+
* const player = useTimelinePlayer(timelineRef, {
|
|
190
|
+
* seekStep: 5,
|
|
191
|
+
* loop: { enabled: loopOn, start: loopStart, end: loopEnd },
|
|
192
|
+
* });
|
|
193
|
+
*
|
|
194
|
+
* // Option A: use the pre-built TransportBar
|
|
195
|
+
* <TransportBar player={player} />
|
|
196
|
+
*
|
|
197
|
+
* // Option B: build your own UI
|
|
198
|
+
* <button onClick={player.isPlaying ? player.pause : player.play}>
|
|
199
|
+
* {player.isPlaying ? '⏸' : '▶'}
|
|
200
|
+
* </button>
|
|
201
|
+
* <span>{formatTime(player.currentTime)}</span>
|
|
202
|
+
* ```
|
|
203
|
+
*/
|
|
204
|
+
export function useTimelinePlayer(
|
|
205
|
+
ref: React.RefObject<TimelineState>,
|
|
206
|
+
options: UseTimelinePlayerOptions = {},
|
|
207
|
+
): TimelinePlayerState {
|
|
208
|
+
const { seekStep = 5, loop } = options;
|
|
209
|
+
|
|
210
|
+
const [isPlaying, setIsPlaying] = useState(false);
|
|
211
|
+
const [currentTime, setCurrentTime] = useState(0);
|
|
212
|
+
const [playRate, setPlayRateState] = useState(1);
|
|
213
|
+
|
|
214
|
+
// ── Refs for loop values ──
|
|
215
|
+
// Using refs instead of state in the tick callback avoids stale closures;
|
|
216
|
+
// we never need to re-create the listener when loop params change.
|
|
217
|
+
const loopEnabledRef = useRef(false);
|
|
218
|
+
const loopStartRef = useRef(0);
|
|
219
|
+
const loopEndRef = useRef(0);
|
|
220
|
+
|
|
221
|
+
// Keep refs in sync with options synchronously on every render
|
|
222
|
+
loopEnabledRef.current = loop?.enabled ?? false;
|
|
223
|
+
loopStartRef.current = loop?.start ?? 0;
|
|
224
|
+
loopEndRef.current = loop?.end ?? 0;
|
|
225
|
+
|
|
226
|
+
// ── Engine event listeners ──
|
|
227
|
+
useEffect(() => {
|
|
228
|
+
const timeline = ref.current;
|
|
229
|
+
if (!timeline) return;
|
|
230
|
+
|
|
231
|
+
const handlePlay = () => setIsPlaying(true);
|
|
232
|
+
const handlePaused = () => setIsPlaying(false);
|
|
233
|
+
|
|
234
|
+
const handleTick = ({ time }: { time: number }) => {
|
|
235
|
+
setCurrentTime(time);
|
|
236
|
+
// Loop: if loop is enabled and we've passed the end, snap back to start
|
|
237
|
+
if (loopEnabledRef.current && time >= loopEndRef.current) {
|
|
238
|
+
ref.current?.setTime(loopStartRef.current);
|
|
239
|
+
}
|
|
240
|
+
};
|
|
241
|
+
|
|
242
|
+
timeline.listener.on('play', handlePlay);
|
|
243
|
+
timeline.listener.on('paused', handlePaused);
|
|
244
|
+
timeline.listener.on('setTimeByTick', handleTick);
|
|
245
|
+
|
|
246
|
+
return () => {
|
|
247
|
+
timeline.listener.off('play', handlePlay);
|
|
248
|
+
timeline.listener.off('paused', handlePaused);
|
|
249
|
+
timeline.listener.off('setTimeByTick', handleTick);
|
|
250
|
+
};
|
|
251
|
+
}, [ref]); // ref object is stable across renders — runs once on mount
|
|
252
|
+
|
|
253
|
+
// ── Control callbacks ──
|
|
254
|
+
const play = useCallback(() => {
|
|
255
|
+
// When loop is enabled, playback must always start inside the loop zone.
|
|
256
|
+
// If the cursor is sitting before loopStart, snap it there first so the
|
|
257
|
+
// engine starts from the correct position (the tick handler already handles
|
|
258
|
+
// the case where the cursor is past loopEnd).
|
|
259
|
+
if (loopEnabledRef.current) {
|
|
260
|
+
const currentTime = ref.current?.getTime() ?? 0;
|
|
261
|
+
if (currentTime < loopStartRef.current) {
|
|
262
|
+
ref.current?.setTime(loopStartRef.current);
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
ref.current?.play({ autoEnd: true });
|
|
266
|
+
}, [ref]);
|
|
267
|
+
|
|
268
|
+
const pause = useCallback(() => {
|
|
269
|
+
ref.current?.pause();
|
|
270
|
+
}, [ref]);
|
|
271
|
+
|
|
272
|
+
const stop = useCallback(() => {
|
|
273
|
+
ref.current?.pause();
|
|
274
|
+
ref.current?.setTime(0);
|
|
275
|
+
setCurrentTime(0);
|
|
276
|
+
}, [ref]);
|
|
277
|
+
|
|
278
|
+
const toStart = useCallback(() => {
|
|
279
|
+
ref.current?.pause();
|
|
280
|
+
ref.current?.setTime(0);
|
|
281
|
+
setCurrentTime(0);
|
|
282
|
+
}, [ref]);
|
|
283
|
+
|
|
284
|
+
const rewind = useCallback(() => {
|
|
285
|
+
const cur = ref.current?.getTime() ?? 0;
|
|
286
|
+
const next = Math.max(0, cur - seekStep);
|
|
287
|
+
ref.current?.setTime(next);
|
|
288
|
+
setCurrentTime(next);
|
|
289
|
+
}, [ref, seekStep]);
|
|
290
|
+
|
|
291
|
+
const forward = useCallback(() => {
|
|
292
|
+
const cur = ref.current?.getTime() ?? 0;
|
|
293
|
+
ref.current?.setTime(cur + seekStep);
|
|
294
|
+
}, [ref, seekStep]);
|
|
295
|
+
|
|
296
|
+
const setTime = useCallback((time: number) => {
|
|
297
|
+
ref.current?.setTime(time);
|
|
298
|
+
setCurrentTime(time);
|
|
299
|
+
}, [ref]);
|
|
300
|
+
|
|
301
|
+
const setPlayRate = useCallback((rate: number) => {
|
|
302
|
+
ref.current?.setPlayRate(rate);
|
|
303
|
+
setPlayRateState(rate);
|
|
304
|
+
}, [ref]);
|
|
305
|
+
|
|
306
|
+
return {
|
|
307
|
+
isPlaying,
|
|
308
|
+
currentTime,
|
|
309
|
+
playRate,
|
|
310
|
+
play,
|
|
311
|
+
pause,
|
|
312
|
+
stop,
|
|
313
|
+
toStart,
|
|
314
|
+
rewind,
|
|
315
|
+
forward,
|
|
316
|
+
setTime,
|
|
317
|
+
setPlayRate,
|
|
318
|
+
};
|
|
319
|
+
}
|
package/src/index.tsx
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export * from './components/timeline';
|
|
2
|
+
export { default as CutOverlay } from './components/cut-overlay/CutOverlay';
|
|
3
|
+
export * from './components/cut-overlay/CutOverlay';
|
|
4
|
+
export { default as LoopZoneOverlay } from './components/loop-zone/LoopZoneOverlay';
|
|
5
|
+
export * from './components/loop-zone/LoopZoneOverlay';
|
|
6
|
+
export { default as TransportBar } from './components/transport/TransportBar';
|
|
7
|
+
export * from './components/transport/TransportBar';
|
|
8
|
+
export { useTimelinePlayer, formatTime } from './components/transport/useTimelinePlayer';
|
|
9
|
+
export type {
|
|
10
|
+
UseTimelinePlayerOptions,
|
|
11
|
+
UseTimelinePlayerLoop,
|
|
12
|
+
TimelinePlayerState,
|
|
13
|
+
} from './components/transport/useTimelinePlayer';
|
|
14
|
+
export { splitActionInRow } from './utils/deal_data';
|
|
15
|
+
export * from './interface/timeline';
|
|
16
|
+
export * from '@keplar-404/timeline-engine';
|
|
17
|
+
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { EditData, RequiredEditData } from "./timeline";
|
|
2
|
+
|
|
3
|
+
/** Common component parameters */
|
|
4
|
+
export interface CommonProp extends RequiredEditData {
|
|
5
|
+
/** Scale count */
|
|
6
|
+
scaleCount: number;
|
|
7
|
+
/** Set scale count */
|
|
8
|
+
setScaleCount: (scaleCount: number) => void;
|
|
9
|
+
/** Cursor time */
|
|
10
|
+
cursorTime: number;
|
|
11
|
+
/** Current timeline width */
|
|
12
|
+
timelineWidth: number;
|
|
13
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
export const PREFIX = `timeline-editor`;
|
|
2
|
+
|
|
3
|
+
/** Cursor time at start */
|
|
4
|
+
export const START_CURSOR_TIME = 0;
|
|
5
|
+
/** Default scale */
|
|
6
|
+
export const DEFAULT_SCALE = 1;
|
|
7
|
+
/** Default scale split count */
|
|
8
|
+
export const DEFAULT_SCALE_SPLIT_COUNT = 10;
|
|
9
|
+
|
|
10
|
+
/** Default scale display width */
|
|
11
|
+
export const DEFAULT_SCALE_WIDTH = 160;
|
|
12
|
+
/** Default scale start distance from left */
|
|
13
|
+
export const DEFAULT_START_LEFT = 20;
|
|
14
|
+
/** Default minimum movement in pixels */
|
|
15
|
+
export const DEFAULT_MOVE_GRID = 1;
|
|
16
|
+
/** Default adsorption distance in pixels */
|
|
17
|
+
export const DEFAULT_ADSORPTION_DISTANCE = 8;
|
|
18
|
+
/** Default action row height */
|
|
19
|
+
export const DEFAULT_ROW_HEIGHT = 32;
|
|
20
|
+
|
|
21
|
+
/** Minimum scale count */
|
|
22
|
+
export const MIN_SCALE_COUNT = 20;
|
|
23
|
+
/** Maximum scale count */
|
|
24
|
+
export const MAX_SCALE_COUNT = Infinity;
|
|
25
|
+
/** Number of scale marks to add each time */
|
|
26
|
+
export const ADD_SCALE_COUNT = 5;
|
|
27
|
+
|
|
28
|
+
/** Error messages */
|
|
29
|
+
export const ERROR = {
|
|
30
|
+
START_TIME_LESS_THEN_ZERO: 'Action start time cannot be less than 0!',
|
|
31
|
+
END_TIME_LESS_THEN_START_TIME: 'Action end time cannot be less then start time!',
|
|
32
|
+
}
|