@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,461 @@
|
|
|
1
|
+
import React, { useCallback } from 'react';
|
|
2
|
+
import './LoopZoneOverlay.css';
|
|
3
|
+
|
|
4
|
+
// ─────────────────────────────────────────────
|
|
5
|
+
// Public Types
|
|
6
|
+
// ─────────────────────────────────────────────
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Visual and behavioral configuration for the {@link LoopZoneOverlay} component.
|
|
10
|
+
* All properties are optional — defaults produce a green-themed loop zone.
|
|
11
|
+
*
|
|
12
|
+
* @example
|
|
13
|
+
* ```tsx
|
|
14
|
+
* const loopConfig: LoopZoneConfig = {
|
|
15
|
+
* bandColor: '#a855f7',
|
|
16
|
+
* bandOpacity: 0.1,
|
|
17
|
+
* handleColor: '#c084fc',
|
|
18
|
+
* showBoundaryLines: true,
|
|
19
|
+
* };
|
|
20
|
+
* <LoopZoneOverlay config={loopConfig} ... />
|
|
21
|
+
* ```
|
|
22
|
+
*/
|
|
23
|
+
export interface LoopZoneConfig {
|
|
24
|
+
/**
|
|
25
|
+
* CSS color of the loop region shaded band.
|
|
26
|
+
* @default '#10b981'
|
|
27
|
+
*/
|
|
28
|
+
bandColor?: string;
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Opacity of the loop region band (0–1).
|
|
32
|
+
* Keep this low (< 0.15) so underlying blocks remain visible.
|
|
33
|
+
* @default 0.07
|
|
34
|
+
*/
|
|
35
|
+
bandOpacity?: number;
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Border/stripe color used on the band's edges and stripe pattern.
|
|
39
|
+
* Falls back to `bandColor` when not set.
|
|
40
|
+
*/
|
|
41
|
+
bandBorderColor?: string;
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* CSS color of the default drag handle grip pills.
|
|
45
|
+
* Falls back to `bandColor` when not set.
|
|
46
|
+
* Has no effect when `renderHandle` is provided.
|
|
47
|
+
*/
|
|
48
|
+
handleColor?: string;
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Whether to render the dashed vertical boundary lines
|
|
52
|
+
* extending through the edit rows below the time ruler.
|
|
53
|
+
* @default true
|
|
54
|
+
*/
|
|
55
|
+
showBoundaryLines?: boolean;
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* CSS color of the dashed boundary lines.
|
|
59
|
+
* @default 'rgba(16, 185, 129, 0.4)'
|
|
60
|
+
*/
|
|
61
|
+
boundaryLineColor?: string;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Props passed into a custom handle renderer supplied via `LoopZoneOverlayProps.renderHandle`.
|
|
66
|
+
*
|
|
67
|
+
* Attach `onMouseDown` to your draggable element to enable the drag interaction.
|
|
68
|
+
* The overlay provides all pixel math — your renderer only needs to call `onMouseDown`.
|
|
69
|
+
*
|
|
70
|
+
* @example
|
|
71
|
+
* ```tsx
|
|
72
|
+
* renderHandle={({ type, time, onMouseDown }) => (
|
|
73
|
+
* <div
|
|
74
|
+
* onMouseDown={onMouseDown}
|
|
75
|
+
* style={{ cursor: 'ew-resize', background: 'purple' }}
|
|
76
|
+
* title={`${type === 'start' ? 'Loop start' : 'Loop end'}: ${time.toFixed(2)}s`}
|
|
77
|
+
* >
|
|
78
|
+
* {type === 'start' ? '⟨' : '⟩'}
|
|
79
|
+
* </div>
|
|
80
|
+
* )}
|
|
81
|
+
* ```
|
|
82
|
+
*/
|
|
83
|
+
export interface LoopHandleRenderProps {
|
|
84
|
+
/**
|
|
85
|
+
* Which boundary this handle controls.
|
|
86
|
+
* Use to differentiate styling between the two handles.
|
|
87
|
+
*/
|
|
88
|
+
type: 'start' | 'end';
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* The current time value (in seconds) for this boundary.
|
|
92
|
+
* Useful for displaying a tooltip or label.
|
|
93
|
+
*/
|
|
94
|
+
time: number;
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Attach this to your draggable element's `onMouseDown` to activate dragging.
|
|
98
|
+
* The LoopZoneOverlay hooks into document `mousemove`/`mouseup` internally —
|
|
99
|
+
* you do not need to manage drag events yourself.
|
|
100
|
+
*/
|
|
101
|
+
onMouseDown: (e: React.MouseEvent) => void;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Props for the {@link LoopZoneOverlay} component.
|
|
106
|
+
*
|
|
107
|
+
* Mount this absolutely inside the same container as your `<Timeline>` to render
|
|
108
|
+
* a draggable loop region on top of the timeline ruler and edit area.
|
|
109
|
+
*
|
|
110
|
+
* @example
|
|
111
|
+
* ```tsx
|
|
112
|
+
* const [loopStart, setLoopStart] = useState(1);
|
|
113
|
+
* const [loopEnd, setLoopEnd] = useState(3);
|
|
114
|
+
* const [scrollLeft, setScrollLeft] = useState(0);
|
|
115
|
+
*
|
|
116
|
+
* // Inside your container:
|
|
117
|
+
* <div style={{ position: 'relative' }}>
|
|
118
|
+
* <Timeline
|
|
119
|
+
* ref={timelineRef}
|
|
120
|
+
* scale={1}
|
|
121
|
+
* scaleWidth={160}
|
|
122
|
+
* startLeft={20}
|
|
123
|
+
* onScroll={(p) => setScrollLeft(p.scrollLeft)}
|
|
124
|
+
* ...
|
|
125
|
+
* />
|
|
126
|
+
* {loopEnabled && (
|
|
127
|
+
* <LoopZoneOverlay
|
|
128
|
+
* scale={1}
|
|
129
|
+
* scaleWidth={160}
|
|
130
|
+
* startLeft={20}
|
|
131
|
+
* scrollLeft={scrollLeft}
|
|
132
|
+
* loopStart={loopStart}
|
|
133
|
+
* loopEnd={loopEnd}
|
|
134
|
+
* onLoopStartChange={setLoopStart}
|
|
135
|
+
* onLoopEndChange={setLoopEnd}
|
|
136
|
+
* />
|
|
137
|
+
* )}
|
|
138
|
+
* </div>
|
|
139
|
+
* ```
|
|
140
|
+
*/
|
|
141
|
+
export interface LoopZoneOverlayProps {
|
|
142
|
+
// ── Timeline geometry (must match <Timeline> props) ────────────────────────
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Duration represented by one scale unit (seconds).
|
|
146
|
+
* Must match the `scale` prop on `<Timeline>`.
|
|
147
|
+
* @default 1
|
|
148
|
+
*/
|
|
149
|
+
scale: number;
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Width in pixels of one scale unit.
|
|
153
|
+
* Must match the `scaleWidth` prop on `<Timeline>`.
|
|
154
|
+
* @default 160
|
|
155
|
+
*/
|
|
156
|
+
scaleWidth: number;
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Left inset in pixels before the first scale mark begins.
|
|
160
|
+
* Must match the `startLeft` prop on `<Timeline>`.
|
|
161
|
+
* @default 20
|
|
162
|
+
*/
|
|
163
|
+
startLeft: number;
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Current horizontal scroll offset of the timeline.
|
|
167
|
+
* Track this via the `onScroll` callback on `<Timeline>`:
|
|
168
|
+
* ```tsx
|
|
169
|
+
* onScroll={(params) => setScrollLeft(params.scrollLeft)}
|
|
170
|
+
* ```
|
|
171
|
+
*/
|
|
172
|
+
scrollLeft: number;
|
|
173
|
+
|
|
174
|
+
// ── Controlled loop state ──────────────────────────────────────────────────
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Loop region start time in seconds.
|
|
178
|
+
* This is a **controlled** prop — manage it in parent state.
|
|
179
|
+
*/
|
|
180
|
+
loopStart: number;
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Loop region end time in seconds.
|
|
184
|
+
* Must be greater than `loopStart`.
|
|
185
|
+
* This is a **controlled** prop — manage it in parent state.
|
|
186
|
+
*/
|
|
187
|
+
loopEnd: number;
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Called when the user drags the start handle to a new time value.
|
|
191
|
+
* Update your `loopStart` state here.
|
|
192
|
+
*
|
|
193
|
+
* @param time - New loop start time in seconds.
|
|
194
|
+
*/
|
|
195
|
+
onLoopStartChange: (time: number) => void;
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Called when the user drags the end handle to a new time value.
|
|
199
|
+
* Update your `loopEnd` state here.
|
|
200
|
+
*
|
|
201
|
+
* @param time - New loop end time in seconds.
|
|
202
|
+
*/
|
|
203
|
+
onLoopEndChange: (time: number) => void;
|
|
204
|
+
|
|
205
|
+
// ── Customization ──────────────────────────────────────────────────────────
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* Fine-grained control over the band color, opacity, handle color, and
|
|
209
|
+
* boundary line appearance. All properties are optional and fall back
|
|
210
|
+
* to green-themed defaults.
|
|
211
|
+
*
|
|
212
|
+
* @see {@link LoopZoneConfig}
|
|
213
|
+
*/
|
|
214
|
+
config?: LoopZoneConfig;
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* Custom render function for the drag handles.
|
|
218
|
+
*
|
|
219
|
+
* When provided, the default SVG grip pill is replaced by whatever
|
|
220
|
+
* your function returns. The function is called once for each handle
|
|
221
|
+
* (`'start'` and `'end'`).
|
|
222
|
+
*
|
|
223
|
+
* You **must** attach the provided `onMouseDown` to your draggable
|
|
224
|
+
* element to preserve the drag interaction.
|
|
225
|
+
*
|
|
226
|
+
* @see {@link LoopHandleRenderProps}
|
|
227
|
+
*
|
|
228
|
+
* @example
|
|
229
|
+
* ```tsx
|
|
230
|
+
* renderHandle={({ type, time, onMouseDown }) => (
|
|
231
|
+
* <MyHandle side={type} label={`${time.toFixed(2)}s`} onMouseDown={onMouseDown} />
|
|
232
|
+
* )}
|
|
233
|
+
* ```
|
|
234
|
+
*/
|
|
235
|
+
renderHandle?: (props: LoopHandleRenderProps) => React.ReactNode;
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* Additional CSS class name appended to the overlay root element.
|
|
239
|
+
* Use to apply custom styles alongside or instead of `config`.
|
|
240
|
+
*/
|
|
241
|
+
className?: string;
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* Inline styles applied to the overlay root element.
|
|
245
|
+
*/
|
|
246
|
+
style?: React.CSSProperties;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// ─────────────────────────────────────────────
|
|
250
|
+
// Defaults
|
|
251
|
+
// ─────────────────────────────────────────────
|
|
252
|
+
|
|
253
|
+
const DEFAULT_CONFIG: Required<LoopZoneConfig> = {
|
|
254
|
+
bandColor: '#10b981',
|
|
255
|
+
bandOpacity: 0.07,
|
|
256
|
+
bandBorderColor: '#10b981',
|
|
257
|
+
handleColor: '#10b981',
|
|
258
|
+
showBoundaryLines: true,
|
|
259
|
+
boundaryLineColor: 'rgba(16, 185, 129, 0.4)',
|
|
260
|
+
};
|
|
261
|
+
|
|
262
|
+
// ─────────────────────────────────────────────
|
|
263
|
+
// Default handle
|
|
264
|
+
// ─────────────────────────────────────────────
|
|
265
|
+
|
|
266
|
+
/** Three-bar SVG grip icon — the default handle rendering. */
|
|
267
|
+
const GripIcon: React.FC<{ color: string }> = ({ color }) => (
|
|
268
|
+
<svg width="8" height="12" viewBox="0 0 8 12" fill={color} aria-hidden>
|
|
269
|
+
<rect x="0" y="1" width="8" height="1.5" rx="0.75" />
|
|
270
|
+
<rect x="0" y="5.25" width="8" height="1.5" rx="0.75" />
|
|
271
|
+
<rect x="0" y="9.5" width="8" height="1.5" rx="0.75" />
|
|
272
|
+
</svg>
|
|
273
|
+
);
|
|
274
|
+
|
|
275
|
+
interface DefaultHandleProps {
|
|
276
|
+
color: string;
|
|
277
|
+
onMouseDown: (e: React.MouseEvent) => void;
|
|
278
|
+
title: string;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
const DefaultHandle: React.FC<DefaultHandleProps> = ({ color, onMouseDown, title }) => (
|
|
282
|
+
<div
|
|
283
|
+
className="loop-zone-handle__grip"
|
|
284
|
+
onMouseDown={onMouseDown}
|
|
285
|
+
title={title}
|
|
286
|
+
style={{
|
|
287
|
+
background: color,
|
|
288
|
+
boxShadow: `0 2px 10px ${color}8c, 0 0 0 1px ${color}4d`,
|
|
289
|
+
}}
|
|
290
|
+
>
|
|
291
|
+
<GripIcon color="rgba(255,255,255,0.9)" />
|
|
292
|
+
</div>
|
|
293
|
+
);
|
|
294
|
+
|
|
295
|
+
// ─────────────────────────────────────────────
|
|
296
|
+
// Component
|
|
297
|
+
// ─────────────────────────────────────────────
|
|
298
|
+
|
|
299
|
+
/**
|
|
300
|
+
* `LoopZoneOverlay` renders a draggable loop/repeat region on top of a `<Timeline>`.
|
|
301
|
+
*
|
|
302
|
+
* When mounted inside the same positioned container as `<Timeline>`, it draws:
|
|
303
|
+
* - A **semi-transparent band** spanning the ruler and edit rows between `loopStart` and `loopEnd`
|
|
304
|
+
* - Two **draggable handles** at the boundaries (or custom handles via `renderHandle`)
|
|
305
|
+
* - Optional **dashed boundary lines** extending through the edit rows
|
|
306
|
+
*
|
|
307
|
+
* The component is **fully controlled** — it calls `onLoopStartChange` /
|
|
308
|
+
* `onLoopEndChange` as the user drags and does not hold its own loop state.
|
|
309
|
+
*
|
|
310
|
+
* @example
|
|
311
|
+
* ```tsx
|
|
312
|
+
* <LoopZoneOverlay
|
|
313
|
+
* scale={1}
|
|
314
|
+
* scaleWidth={160}
|
|
315
|
+
* startLeft={20}
|
|
316
|
+
* scrollLeft={scrollLeft}
|
|
317
|
+
* loopStart={loopStart}
|
|
318
|
+
* loopEnd={loopEnd}
|
|
319
|
+
* onLoopStartChange={setLoopStart}
|
|
320
|
+
* onLoopEndChange={setLoopEnd}
|
|
321
|
+
* config={{ bandColor: '#6366f1', bandOpacity: 0.1 }}
|
|
322
|
+
* />
|
|
323
|
+
* ```
|
|
324
|
+
*/
|
|
325
|
+
const LoopZoneOverlay: React.FC<LoopZoneOverlayProps> = ({
|
|
326
|
+
scale,
|
|
327
|
+
scaleWidth,
|
|
328
|
+
startLeft,
|
|
329
|
+
scrollLeft,
|
|
330
|
+
loopStart,
|
|
331
|
+
loopEnd,
|
|
332
|
+
onLoopStartChange,
|
|
333
|
+
onLoopEndChange,
|
|
334
|
+
config,
|
|
335
|
+
renderHandle,
|
|
336
|
+
className,
|
|
337
|
+
style,
|
|
338
|
+
}) => {
|
|
339
|
+
// Merge user config with defaults
|
|
340
|
+
const cfg: Required<LoopZoneConfig> = {
|
|
341
|
+
...DEFAULT_CONFIG,
|
|
342
|
+
...config,
|
|
343
|
+
handleColor: config?.handleColor ?? config?.bandColor ?? DEFAULT_CONFIG.handleColor,
|
|
344
|
+
bandBorderColor: config?.bandBorderColor ?? config?.bandColor ?? DEFAULT_CONFIG.bandBorderColor,
|
|
345
|
+
boundaryLineColor: config?.boundaryLineColor ?? `${config?.bandColor ?? DEFAULT_CONFIG.bandColor}66`,
|
|
346
|
+
};
|
|
347
|
+
|
|
348
|
+
/** Convert a time (seconds) to a pixel x-position relative to the overlay. */
|
|
349
|
+
const t2px = useCallback(
|
|
350
|
+
(t: number) => startLeft + (t / scale) * scaleWidth - scrollLeft,
|
|
351
|
+
[startLeft, scale, scaleWidth, scrollLeft],
|
|
352
|
+
);
|
|
353
|
+
|
|
354
|
+
const leftPx = t2px(loopStart);
|
|
355
|
+
const rightPx = t2px(loopEnd);
|
|
356
|
+
const bandWidth = Math.max(0, rightPx - leftPx);
|
|
357
|
+
|
|
358
|
+
/**
|
|
359
|
+
* Returns a mousedown handler that wires up document-level drag tracking
|
|
360
|
+
* for the given handle type.
|
|
361
|
+
*/
|
|
362
|
+
const makeDragHandler = useCallback(
|
|
363
|
+
(which: 'start' | 'end') => (e: React.MouseEvent) => {
|
|
364
|
+
e.preventDefault();
|
|
365
|
+
const startClientX = e.clientX;
|
|
366
|
+
const initTime = which === 'start' ? loopStart : loopEnd;
|
|
367
|
+
|
|
368
|
+
const onMove = (me: MouseEvent) => {
|
|
369
|
+
const dt = ((me.clientX - startClientX) / scaleWidth) * scale;
|
|
370
|
+
const newTime = Math.max(0, parseFloat((initTime + dt).toFixed(3)));
|
|
371
|
+
if (which === 'start') {
|
|
372
|
+
const clamped = Math.min(newTime, loopEnd - 0.05);
|
|
373
|
+
onLoopStartChange(clamped);
|
|
374
|
+
} else {
|
|
375
|
+
const clamped = Math.max(newTime, loopStart + 0.05);
|
|
376
|
+
onLoopEndChange(clamped);
|
|
377
|
+
}
|
|
378
|
+
};
|
|
379
|
+
|
|
380
|
+
const onUp = () => {
|
|
381
|
+
document.removeEventListener('mousemove', onMove);
|
|
382
|
+
document.removeEventListener('mouseup', onUp);
|
|
383
|
+
};
|
|
384
|
+
|
|
385
|
+
document.addEventListener('mousemove', onMove);
|
|
386
|
+
document.addEventListener('mouseup', onUp);
|
|
387
|
+
},
|
|
388
|
+
[loopStart, loopEnd, scale, scaleWidth, onLoopStartChange, onLoopEndChange],
|
|
389
|
+
);
|
|
390
|
+
|
|
391
|
+
const startDrag = makeDragHandler('start');
|
|
392
|
+
const endDrag = makeDragHandler('end');
|
|
393
|
+
|
|
394
|
+
const renderHandleNode = (type: 'start' | 'end') => {
|
|
395
|
+
const time = type === 'start' ? loopStart : loopEnd;
|
|
396
|
+
const onMouseDown = type === 'start' ? startDrag : endDrag;
|
|
397
|
+
|
|
398
|
+
if (renderHandle) {
|
|
399
|
+
return renderHandle({ type, time, onMouseDown });
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
return (
|
|
403
|
+
<DefaultHandle
|
|
404
|
+
color={cfg.handleColor}
|
|
405
|
+
onMouseDown={onMouseDown}
|
|
406
|
+
title={`Drag to move loop ${type} (${time.toFixed(2)}s)`}
|
|
407
|
+
/>
|
|
408
|
+
);
|
|
409
|
+
};
|
|
410
|
+
|
|
411
|
+
return (
|
|
412
|
+
<div
|
|
413
|
+
className={`loop-zone-overlay${className ? ` ${className}` : ''}`}
|
|
414
|
+
style={{ pointerEvents: 'none', ...style }}
|
|
415
|
+
>
|
|
416
|
+
{/* Shaded band */}
|
|
417
|
+
{bandWidth > 0 && (
|
|
418
|
+
<div
|
|
419
|
+
className="loop-zone-band"
|
|
420
|
+
style={{
|
|
421
|
+
left: leftPx,
|
|
422
|
+
width: bandWidth,
|
|
423
|
+
background: cfg.bandColor,
|
|
424
|
+
opacity: cfg.bandOpacity,
|
|
425
|
+
borderColor: cfg.bandBorderColor,
|
|
426
|
+
}}
|
|
427
|
+
/>
|
|
428
|
+
)}
|
|
429
|
+
|
|
430
|
+
{/* Start handle */}
|
|
431
|
+
<div
|
|
432
|
+
className="loop-zone-handle"
|
|
433
|
+
style={{ left: leftPx }}
|
|
434
|
+
>
|
|
435
|
+
{cfg.showBoundaryLines && (
|
|
436
|
+
<div
|
|
437
|
+
className="loop-zone-handle__line"
|
|
438
|
+
style={{ borderColor: cfg.boundaryLineColor }}
|
|
439
|
+
/>
|
|
440
|
+
)}
|
|
441
|
+
{renderHandleNode('start')}
|
|
442
|
+
</div>
|
|
443
|
+
|
|
444
|
+
{/* End handle */}
|
|
445
|
+
<div
|
|
446
|
+
className="loop-zone-handle"
|
|
447
|
+
style={{ left: rightPx }}
|
|
448
|
+
>
|
|
449
|
+
{cfg.showBoundaryLines && (
|
|
450
|
+
<div
|
|
451
|
+
className="loop-zone-handle__line"
|
|
452
|
+
style={{ borderColor: cfg.boundaryLineColor }}
|
|
453
|
+
/>
|
|
454
|
+
)}
|
|
455
|
+
{renderHandleNode('end')}
|
|
456
|
+
</div>
|
|
457
|
+
</div>
|
|
458
|
+
);
|
|
459
|
+
};
|
|
460
|
+
|
|
461
|
+
export default LoopZoneOverlay;
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { DragEvent, ResizeEvent } from '@interactjs/types/index';
|
|
2
|
+
import { useRef } from 'react';
|
|
3
|
+
|
|
4
|
+
const DEFAULT_SPEED = 1;
|
|
5
|
+
const MAX_SPEED = 3;
|
|
6
|
+
const CRITICAL_SIZE = 10;
|
|
7
|
+
|
|
8
|
+
export function useAutoScroll(target: React.RefObject<HTMLDivElement>) {
|
|
9
|
+
const leftBoundRef = useRef(Number.MIN_SAFE_INTEGER);
|
|
10
|
+
const rightBoundRef = useRef(Number.MAX_SAFE_INTEGER);
|
|
11
|
+
|
|
12
|
+
const speed = useRef(DEFAULT_SPEED);
|
|
13
|
+
const frame = useRef<number>();
|
|
14
|
+
|
|
15
|
+
const initAutoScroll = () => {
|
|
16
|
+
if (target?.current) {
|
|
17
|
+
const { left, width } = target.current.getBoundingClientRect();
|
|
18
|
+
leftBoundRef.current = left;
|
|
19
|
+
rightBoundRef.current = left + width;
|
|
20
|
+
}
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
const dealDragAutoScroll = (e: DragEvent, deltaScroll?: (delta: number) => void) => {
|
|
24
|
+
// Out of bounds
|
|
25
|
+
if (e.clientX >= rightBoundRef.current || e.clientX <= leftBoundRef.current) {
|
|
26
|
+
frame.current && cancelAnimationFrame(frame.current);
|
|
27
|
+
const over = Math.abs(e.clientX >= rightBoundRef.current ? e.clientX - rightBoundRef.current : e.clientX - leftBoundRef.current);
|
|
28
|
+
speed.current = Math.min(Number((over / CRITICAL_SIZE).toFixed(0)) * DEFAULT_SPEED, MAX_SPEED);
|
|
29
|
+
|
|
30
|
+
const dir = e.clientX >= rightBoundRef.current ? 1 : -1;
|
|
31
|
+
const delta = dir * speed.current;
|
|
32
|
+
const loop = () => {
|
|
33
|
+
deltaScroll && deltaScroll(delta);
|
|
34
|
+
frame.current = requestAnimationFrame(loop);
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
frame.current = requestAnimationFrame(loop);
|
|
38
|
+
return false;
|
|
39
|
+
} else {
|
|
40
|
+
frame.current && cancelAnimationFrame(frame.current);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return true;
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
const dealResizeAutoScroll = (e: ResizeEvent, dir: 'left' | 'right', deltaScroll?: (delta: number) => void) => {
|
|
47
|
+
if (e.clientX >= rightBoundRef.current || e.clientX < leftBoundRef.current) {
|
|
48
|
+
frame.current && cancelAnimationFrame(frame.current);
|
|
49
|
+
const over = Math.abs(e.clientX >= rightBoundRef.current ? e.clientX - rightBoundRef.current : e.clientX - leftBoundRef.current);
|
|
50
|
+
speed.current = Math.min(Number((over / CRITICAL_SIZE).toFixed(0)) * DEFAULT_SPEED, MAX_SPEED);
|
|
51
|
+
|
|
52
|
+
const direction = e.clientX >= rightBoundRef.current ? 1 : -1;
|
|
53
|
+
const delta = direction * speed.current;
|
|
54
|
+
const loop = () => {
|
|
55
|
+
deltaScroll && deltaScroll(delta);
|
|
56
|
+
frame.current = requestAnimationFrame(loop);
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
frame.current = requestAnimationFrame(loop);
|
|
60
|
+
|
|
61
|
+
return false;
|
|
62
|
+
} else {
|
|
63
|
+
frame.current && cancelAnimationFrame(frame.current);
|
|
64
|
+
}
|
|
65
|
+
return true;
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
const stopAutoScroll = () => {
|
|
69
|
+
leftBoundRef.current = Number.MIN_SAFE_INTEGER;
|
|
70
|
+
rightBoundRef.current = Number.MAX_SAFE_INTEGER;
|
|
71
|
+
speed.current = DEFAULT_SPEED;
|
|
72
|
+
frame.current && cancelAnimationFrame(frame.current);
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
return {
|
|
76
|
+
initAutoScroll,
|
|
77
|
+
dealDragAutoScroll,
|
|
78
|
+
dealResizeAutoScroll,
|
|
79
|
+
stopAutoScroll,
|
|
80
|
+
};
|
|
81
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { DraggableOptions } from '@interactjs/actions/drag/plugin';
|
|
2
|
+
import { ResizableOptions } from '@interactjs/actions/resize/plugin';
|
|
3
|
+
import { DragEvent, Interactable } from '@interactjs/types';
|
|
4
|
+
import interact from 'interactjs';
|
|
5
|
+
import { cloneElement, FC, ReactElement, useEffect, useRef } from 'react';
|
|
6
|
+
|
|
7
|
+
interface InteractCompProps {
|
|
8
|
+
children: ReactElement;
|
|
9
|
+
interactRef: React.MutableRefObject<Interactable | null>;
|
|
10
|
+
draggable: boolean;
|
|
11
|
+
draggableOptions: DraggableOptions;
|
|
12
|
+
resizable: boolean;
|
|
13
|
+
resizableOptions: ResizableOptions;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export const InteractComp: FC<InteractCompProps> = ({ children, interactRef, draggable, resizable, draggableOptions, resizableOptions }) => {
|
|
17
|
+
const nodeRef = useRef<HTMLElement>(null);
|
|
18
|
+
const interactable = useRef<Interactable | null>();
|
|
19
|
+
const draggableOptionsRef = useRef<DraggableOptions>();
|
|
20
|
+
const resizableOptionsRef = useRef<ResizableOptions>();
|
|
21
|
+
|
|
22
|
+
useEffect(() => {
|
|
23
|
+
draggableOptionsRef.current = { ...draggableOptions };
|
|
24
|
+
resizableOptionsRef.current = { ...resizableOptions };
|
|
25
|
+
}, [draggableOptions, resizableOptions]);
|
|
26
|
+
|
|
27
|
+
useEffect(() => {
|
|
28
|
+
interactable.current && interactable.current.unset();
|
|
29
|
+
interactable.current = interact(nodeRef.current as HTMLElement);
|
|
30
|
+
interactRef.current = interactable.current;
|
|
31
|
+
setInteractions();
|
|
32
|
+
}, [draggable, resizable]);
|
|
33
|
+
|
|
34
|
+
const setInteractions = () => {
|
|
35
|
+
if (draggable)
|
|
36
|
+
interactable.current?.draggable({
|
|
37
|
+
...draggableOptionsRef.current,
|
|
38
|
+
onstart: (e) => draggableOptionsRef.current?.onstart && (draggableOptionsRef.current.onstart as (e: DragEvent) => any)(e),
|
|
39
|
+
onmove: (e) => draggableOptionsRef.current?.onmove && (draggableOptionsRef.current.onmove as (e: DragEvent) => any)(e),
|
|
40
|
+
onend: (e) => draggableOptionsRef.current?.onend && (draggableOptionsRef.current.onend as (e: DragEvent) => any)(e),
|
|
41
|
+
});
|
|
42
|
+
if (resizable)
|
|
43
|
+
interactable.current?.resizable({
|
|
44
|
+
...resizableOptionsRef.current,
|
|
45
|
+
onstart: (e) => resizableOptionsRef.current?.onstart && (resizableOptionsRef.current.onstart as (e: DragEvent) => any)(e),
|
|
46
|
+
onmove: (e) => resizableOptionsRef.current?.onmove && (resizableOptionsRef.current.onmove as (e: DragEvent) => any)(e),
|
|
47
|
+
onend: (e) => resizableOptionsRef.current?.onend && (resizableOptionsRef.current.onend as (e: DragEvent) => any)(e),
|
|
48
|
+
});
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
return cloneElement(children as ReactElement, {
|
|
52
|
+
ref: nodeRef,
|
|
53
|
+
draggable: false,
|
|
54
|
+
});
|
|
55
|
+
};
|