@hyperframes/studio 0.6.86 → 0.6.88
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/assets/index-B9_ctmee.js +143 -0
- package/dist/assets/index-CGlIm_-E.css +1 -0
- package/dist/index.html +2 -2
- package/package.json +4 -4
- package/src/App.tsx +159 -6
- package/src/components/StudioHeader.tsx +20 -7
- package/src/components/StudioPreviewArea.tsx +6 -1
- package/src/components/StudioRightPanel.tsx +13 -0
- package/src/components/StudioToast.tsx +47 -7
- package/src/components/TimelineToolbar.tsx +12 -122
- package/src/components/editor/AnimationCard.tsx +64 -10
- package/src/components/editor/ArcPathControls.tsx +131 -0
- package/src/components/editor/BorderRadiusEditor.tsx +209 -0
- package/src/components/editor/DomEditOverlay.tsx +70 -11
- package/src/components/editor/DopesheetStrip.tsx +141 -0
- package/src/components/editor/EaseCurveSection.tsx +82 -7
- package/src/components/editor/GestureTrailOverlay.tsx +132 -0
- package/src/components/editor/GsapAnimationSection.tsx +14 -1
- package/src/components/editor/KeyframeDiamond.tsx +27 -12
- package/src/components/editor/LayersPanel.tsx +14 -12
- package/src/components/editor/MotionPathOverlay.tsx +146 -0
- package/src/components/editor/PropertyPanel.tsx +196 -66
- package/src/components/editor/SourceEditor.tsx +0 -1
- package/src/components/editor/StaggerControls.tsx +61 -0
- package/src/components/editor/domEditOverlayGeometry.test.ts +13 -0
- package/src/components/editor/domEditOverlayGeometry.ts +2 -1
- package/src/components/editor/domEditing.test.ts +43 -0
- package/src/components/editor/domEditing.ts +2 -0
- package/src/components/editor/domEditingElement.ts +25 -2
- package/src/components/editor/domEditingLayers.test.ts +78 -0
- package/src/components/editor/domEditingLayers.ts +33 -13
- package/src/components/editor/domEditingTypes.ts +1 -0
- package/src/components/editor/manualEditingAvailability.ts +1 -1
- package/src/components/editor/manualEdits.ts +3 -0
- package/src/components/editor/manualEditsDom.ts +23 -5
- package/src/components/editor/manualOffsetDrag.ts +59 -0
- package/src/components/editor/panelTokens.ts +10 -0
- package/src/components/editor/propertyPanelColor.tsx +2 -2
- package/src/components/editor/propertyPanelFill.tsx +1 -1
- package/src/components/editor/propertyPanelHelpers.ts +18 -2
- package/src/components/editor/propertyPanelMediaSection.tsx +1 -1
- package/src/components/editor/propertyPanelPrimitives.tsx +38 -25
- package/src/components/editor/propertyPanelSections.tsx +4 -6
- package/src/components/editor/propertyPanelStyleSections.tsx +30 -8
- package/src/components/editor/useDomEditOverlayRects.ts +46 -2
- package/src/components/renders/RenderQueue.tsx +121 -100
- package/src/components/renders/RenderQueueItem.tsx +13 -13
- package/src/contexts/DomEditContext.tsx +12 -0
- package/src/contexts/FileManagerContext.tsx +3 -0
- package/src/contexts/StudioContext.tsx +0 -4
- package/src/hooks/gsapKeyframeCommit.ts +92 -0
- package/src/hooks/gsapRuntimeBridge.ts +147 -85
- package/src/hooks/gsapRuntimeKeyframes.ts +75 -24
- package/src/hooks/gsapRuntimePreview.ts +19 -0
- package/src/hooks/useAppHotkeys.ts +18 -0
- package/src/hooks/useAskAgentModal.ts +2 -4
- package/src/hooks/useDomEditCommits.ts +11 -17
- package/src/hooks/useDomEditSession.ts +47 -4
- package/src/hooks/useEnableKeyframes.ts +171 -0
- package/src/hooks/useFileManager.ts +7 -0
- package/src/hooks/useGestureRecording.ts +340 -0
- package/src/hooks/useGsapScriptCommits.ts +171 -35
- package/src/hooks/useGsapSelectionHandlers.ts +27 -8
- package/src/hooks/useGsapTweenCache.ts +169 -11
- package/src/hooks/useKeyframeKeyboard.ts +103 -0
- package/src/hooks/useStudioContextValue.ts +5 -4
- package/src/hooks/useStudioUrlState.ts +1 -2
- package/src/hooks/useTimelineEditing.ts +50 -3
- package/src/hooks/useToast.ts +6 -1
- package/src/player/components/ShortcutsPanel.tsx +40 -0
- package/src/player/components/TimelineClipDiamonds.tsx +3 -3
- package/src/player/components/TimelinePropertyRows.tsx +120 -0
- package/src/player/lib/timelineDOM.test.ts +55 -0
- package/src/player/lib/timelineDOM.ts +13 -0
- package/src/player/lib/timelineIframeHelpers.test.ts +51 -0
- package/src/player/lib/timelineIframeHelpers.ts +1 -0
- package/src/player/store/playerStore.ts +43 -0
- package/src/utils/audioBeatDetection.ts +58 -0
- package/src/utils/globalTimeCompiler.test.ts +169 -0
- package/src/utils/globalTimeCompiler.ts +77 -0
- package/src/utils/gsapSoftReload.ts +30 -10
- package/src/utils/keyframeSnapping.test.ts +74 -0
- package/src/utils/keyframeSnapping.ts +63 -0
- package/src/utils/rdpSimplify.ts +183 -0
- package/src/utils/sourcePatcher.ts +2 -0
- package/dist/assets/index-BT9VHgSy.js +0 -140
- package/dist/assets/index-DHcptK1_.css +0 -1
|
@@ -0,0 +1,340 @@
|
|
|
1
|
+
import { useCallback, useEffect, useRef, useState } from "react";
|
|
2
|
+
import { usePlayerStore, liveTime } from "../player/store/playerStore";
|
|
3
|
+
|
|
4
|
+
export interface GestureSample {
|
|
5
|
+
time: number;
|
|
6
|
+
properties: Record<string, number>;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
interface Modifiers {
|
|
10
|
+
shift: boolean;
|
|
11
|
+
alt: boolean;
|
|
12
|
+
meta: boolean;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
interface AccumulatedState {
|
|
16
|
+
opacity: number;
|
|
17
|
+
scale: number;
|
|
18
|
+
z: number;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function resolveGestureProperties(
|
|
22
|
+
dx: number,
|
|
23
|
+
dy: number,
|
|
24
|
+
scrollDelta: number,
|
|
25
|
+
modifiers: Modifiers,
|
|
26
|
+
accumulatedState: AccumulatedState,
|
|
27
|
+
): {
|
|
28
|
+
properties: Record<string, number>;
|
|
29
|
+
nextState: AccumulatedState;
|
|
30
|
+
} {
|
|
31
|
+
const properties: Record<string, number> = {};
|
|
32
|
+
let nextOpacity = accumulatedState.opacity;
|
|
33
|
+
let nextScale = accumulatedState.scale;
|
|
34
|
+
let nextZ = accumulatedState.z;
|
|
35
|
+
|
|
36
|
+
if (modifiers.meta) {
|
|
37
|
+
// Opacity derived from total vertical displacement (absolute, not accumulated).
|
|
38
|
+
// Dragging down reduces opacity; dragging back up restores it.
|
|
39
|
+
nextOpacity = Math.max(0, Math.min(1, 1 - dy * 0.005));
|
|
40
|
+
properties.opacity = nextOpacity;
|
|
41
|
+
if (scrollDelta !== 0) {
|
|
42
|
+
nextScale = Math.max(0.01, accumulatedState.scale + scrollDelta * 0.01);
|
|
43
|
+
properties.scale = nextScale;
|
|
44
|
+
}
|
|
45
|
+
} else if (modifiers.shift) {
|
|
46
|
+
properties.rotationX = dy * 0.5;
|
|
47
|
+
properties.rotationY = dx * 0.5;
|
|
48
|
+
} else if (modifiers.alt) {
|
|
49
|
+
properties.rotation = dx * 0.5;
|
|
50
|
+
} else {
|
|
51
|
+
properties.x = dx;
|
|
52
|
+
properties.y = dy;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
if (!modifiers.meta && scrollDelta !== 0) {
|
|
56
|
+
nextZ = accumulatedState.z + scrollDelta;
|
|
57
|
+
properties.z = nextZ;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return {
|
|
61
|
+
properties,
|
|
62
|
+
nextState: { opacity: nextOpacity, scale: nextScale, z: nextZ },
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function useGestureRecording() {
|
|
67
|
+
const [isRecording, setIsRecording] = useState(false);
|
|
68
|
+
const [recordingDuration, setRecordingDuration] = useState(0);
|
|
69
|
+
|
|
70
|
+
// Synchronous guard — immune to React's async state batching.
|
|
71
|
+
// startRecording and stopRecording check this ref, not the useState value.
|
|
72
|
+
const isRecordingRef = useRef(false);
|
|
73
|
+
|
|
74
|
+
const pointerRef = useRef({ x: 0, y: 0 });
|
|
75
|
+
const startPointerRef = useRef({ x: 0, y: 0 });
|
|
76
|
+
const scrollDeltaRef = useRef(0);
|
|
77
|
+
const modifiersRef = useRef<Modifiers>({ shift: false, alt: false, meta: false });
|
|
78
|
+
const accumulatedRef = useRef<AccumulatedState>({ opacity: 1, scale: 1, z: 0 });
|
|
79
|
+
const basePositionRef = useRef({ x: 0, y: 0 });
|
|
80
|
+
const scaleRef = useRef(1);
|
|
81
|
+
const hasMovedRef = useRef(false);
|
|
82
|
+
const pointerElementOffsetRef = useRef({ x: 0, y: 0 });
|
|
83
|
+
const runtimeRef = useRef<{
|
|
84
|
+
seek: (t: number) => void;
|
|
85
|
+
set: (target: string, vars: Record<string, number>) => void;
|
|
86
|
+
selector: string;
|
|
87
|
+
element: HTMLElement;
|
|
88
|
+
startTime: number;
|
|
89
|
+
maxSeekTime: number;
|
|
90
|
+
} | null>(null);
|
|
91
|
+
|
|
92
|
+
const rafIdRef = useRef(0);
|
|
93
|
+
const samplesRef = useRef<GestureSample[]>([]);
|
|
94
|
+
const trailRef = useRef<Array<{ x: number; y: number }>>([]);
|
|
95
|
+
const cleanupRef = useRef<(() => void) | null>(null);
|
|
96
|
+
|
|
97
|
+
// Unmount safety: cancel RAF + remove listeners if component tears down mid-recording.
|
|
98
|
+
useEffect(() => {
|
|
99
|
+
return () => {
|
|
100
|
+
cleanupRef.current?.();
|
|
101
|
+
cleanupRef.current = null;
|
|
102
|
+
isRecordingRef.current = false;
|
|
103
|
+
};
|
|
104
|
+
}, []);
|
|
105
|
+
|
|
106
|
+
const startRecording = useCallback(
|
|
107
|
+
(element: HTMLElement, iframeEl: HTMLIFrameElement, elementEndTime?: number) => {
|
|
108
|
+
if (isRecordingRef.current) return;
|
|
109
|
+
isRecordingRef.current = true;
|
|
110
|
+
|
|
111
|
+
samplesRef.current = [];
|
|
112
|
+
trailRef.current = [];
|
|
113
|
+
hasMovedRef.current = false;
|
|
114
|
+
setRecordingDuration(0);
|
|
115
|
+
scrollDeltaRef.current = 0;
|
|
116
|
+
|
|
117
|
+
let baseOpacity = 1;
|
|
118
|
+
let baseScaleVal = 1;
|
|
119
|
+
let baseX = 0;
|
|
120
|
+
let baseY = 0;
|
|
121
|
+
try {
|
|
122
|
+
const gsap = (
|
|
123
|
+
iframeEl.contentWindow as Window & {
|
|
124
|
+
gsap?: { getProperty: (el: Element, prop: string) => number };
|
|
125
|
+
}
|
|
126
|
+
).gsap;
|
|
127
|
+
if (gsap?.getProperty) {
|
|
128
|
+
baseOpacity = Number(gsap.getProperty(element, "opacity")) || 1;
|
|
129
|
+
baseScaleVal = Number(gsap.getProperty(element, "scaleX")) || 1;
|
|
130
|
+
baseX = Number(gsap.getProperty(element, "x")) || 0;
|
|
131
|
+
baseY = Number(gsap.getProperty(element, "y")) || 0;
|
|
132
|
+
}
|
|
133
|
+
} catch {
|
|
134
|
+
/* cross-origin guard */
|
|
135
|
+
}
|
|
136
|
+
// When reapplyPathOffsets has run (translate restored to var-based),
|
|
137
|
+
// GSAP's cache was stripped — gsapX is 0 but the element is visually
|
|
138
|
+
// at CSSLeft + translate(offset). gsap.set wipes translate, so we need
|
|
139
|
+
// baseX to include the offset. When translate is "none" (GSAP owns it),
|
|
140
|
+
// gsapX already includes the baked offset — don't add.
|
|
141
|
+
const translateVal = element.style.translate ?? "";
|
|
142
|
+
if (translateVal.includes("var(")) {
|
|
143
|
+
const offX = Number.parseFloat(element.style.getPropertyValue("--hf-studio-offset-x")) || 0;
|
|
144
|
+
const offY = Number.parseFloat(element.style.getPropertyValue("--hf-studio-offset-y")) || 0;
|
|
145
|
+
baseX += offX;
|
|
146
|
+
baseY += offY;
|
|
147
|
+
}
|
|
148
|
+
accumulatedRef.current = { opacity: baseOpacity, scale: baseScaleVal, z: 0 };
|
|
149
|
+
basePositionRef.current = { x: baseX, y: baseY };
|
|
150
|
+
|
|
151
|
+
const selector = element.id ? `#${element.id}` : null;
|
|
152
|
+
try {
|
|
153
|
+
const win = iframeEl.contentWindow as Window & {
|
|
154
|
+
gsap?: { set: (t: string, v: Record<string, number>) => void };
|
|
155
|
+
__timelines?: Record<string, { seek: (t: number) => void; duration: () => number }>;
|
|
156
|
+
__player?: { getTime: () => number };
|
|
157
|
+
};
|
|
158
|
+
const tl = win?.__timelines ? Object.values(win.__timelines)[0] : null;
|
|
159
|
+
if (win?.gsap?.set && tl?.seek && selector) {
|
|
160
|
+
const tlDuration = tl.duration();
|
|
161
|
+
runtimeRef.current = {
|
|
162
|
+
seek: tl.seek.bind(tl),
|
|
163
|
+
set: win.gsap.set.bind(win.gsap),
|
|
164
|
+
selector,
|
|
165
|
+
element,
|
|
166
|
+
startTime: win.__player?.getTime() ?? 0,
|
|
167
|
+
maxSeekTime:
|
|
168
|
+
elementEndTime != null && elementEndTime < tlDuration ? elementEndTime : tlDuration,
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
} catch {
|
|
172
|
+
runtimeRef.current = null;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
const iframeRect = iframeEl.getBoundingClientRect();
|
|
176
|
+
const doc = iframeEl.contentDocument;
|
|
177
|
+
const root = doc?.querySelector<HTMLElement>("[data-composition-id]") ?? doc?.documentElement;
|
|
178
|
+
const declaredWidth = Number(root?.getAttribute("data-width")) || 1920;
|
|
179
|
+
scaleRef.current = declaredWidth > 0 ? iframeRect.width / declaredWidth : 1;
|
|
180
|
+
|
|
181
|
+
// Compute the offset between the element's visual center and the pointer
|
|
182
|
+
// so the element tracks the pointer exactly during recording (no jump).
|
|
183
|
+
const elRect = element.getBoundingClientRect();
|
|
184
|
+
const elCenterViewport = {
|
|
185
|
+
x: elRect.left + elRect.width / 2,
|
|
186
|
+
y: elRect.top + elRect.height / 2,
|
|
187
|
+
};
|
|
188
|
+
pointerElementOffsetRef.current = { x: 0, y: 0 }; // reset; set on first move
|
|
189
|
+
|
|
190
|
+
const handlePointerMove = (e: PointerEvent) => {
|
|
191
|
+
pointerRef.current = { x: e.clientX, y: e.clientY };
|
|
192
|
+
modifiersRef.current = {
|
|
193
|
+
shift: e.shiftKey,
|
|
194
|
+
alt: e.altKey,
|
|
195
|
+
meta: e.metaKey || e.ctrlKey,
|
|
196
|
+
};
|
|
197
|
+
};
|
|
198
|
+
|
|
199
|
+
const handleWheel = (e: WheelEvent) => {
|
|
200
|
+
scrollDeltaRef.current += e.deltaY;
|
|
201
|
+
modifiersRef.current = {
|
|
202
|
+
shift: e.shiftKey,
|
|
203
|
+
alt: e.altKey,
|
|
204
|
+
meta: e.metaKey || e.ctrlKey,
|
|
205
|
+
};
|
|
206
|
+
};
|
|
207
|
+
|
|
208
|
+
const handleKeyChange = (e: KeyboardEvent) => {
|
|
209
|
+
modifiersRef.current = {
|
|
210
|
+
shift: e.shiftKey,
|
|
211
|
+
alt: e.altKey,
|
|
212
|
+
meta: e.metaKey || e.ctrlKey,
|
|
213
|
+
};
|
|
214
|
+
};
|
|
215
|
+
|
|
216
|
+
document.addEventListener("pointermove", handlePointerMove, { passive: true });
|
|
217
|
+
document.addEventListener("wheel", handleWheel, { passive: true });
|
|
218
|
+
document.addEventListener("keydown", handleKeyChange, { passive: true });
|
|
219
|
+
document.addEventListener("keyup", handleKeyChange, { passive: true });
|
|
220
|
+
|
|
221
|
+
startPointerRef.current = { ...pointerRef.current };
|
|
222
|
+
const startMs = performance.now();
|
|
223
|
+
|
|
224
|
+
let startCaptured = false;
|
|
225
|
+
const captureStart = (e: PointerEvent) => {
|
|
226
|
+
if (!startCaptured) {
|
|
227
|
+
startPointerRef.current = { x: e.clientX, y: e.clientY };
|
|
228
|
+
// Compute the offset between the pointer and the element center
|
|
229
|
+
// so the element follows the pointer without jumping.
|
|
230
|
+
pointerElementOffsetRef.current = {
|
|
231
|
+
x: e.clientX - elCenterViewport.x,
|
|
232
|
+
y: e.clientY - elCenterViewport.y,
|
|
233
|
+
};
|
|
234
|
+
startCaptured = true;
|
|
235
|
+
hasMovedRef.current = true;
|
|
236
|
+
}
|
|
237
|
+
};
|
|
238
|
+
document.addEventListener("pointermove", captureStart, { passive: true, once: true });
|
|
239
|
+
|
|
240
|
+
const tick = () => {
|
|
241
|
+
if (!isRecordingRef.current) return;
|
|
242
|
+
const now = performance.now();
|
|
243
|
+
const time = (now - startMs) / 1000;
|
|
244
|
+
const scale = scaleRef.current || 1;
|
|
245
|
+
const dx = (pointerRef.current.x - startPointerRef.current.x) / scale;
|
|
246
|
+
const dy = (pointerRef.current.y - startPointerRef.current.y) / scale;
|
|
247
|
+
const scrollDelta = scrollDeltaRef.current;
|
|
248
|
+
|
|
249
|
+
// Skip zero-displacement samples before the pointer has moved.
|
|
250
|
+
if (!hasMovedRef.current && dx === 0 && dy === 0 && scrollDelta === 0) {
|
|
251
|
+
rafIdRef.current = requestAnimationFrame(tick);
|
|
252
|
+
return;
|
|
253
|
+
}
|
|
254
|
+
hasMovedRef.current = true;
|
|
255
|
+
|
|
256
|
+
const { properties, nextState } = resolveGestureProperties(
|
|
257
|
+
dx,
|
|
258
|
+
dy,
|
|
259
|
+
scrollDelta,
|
|
260
|
+
modifiersRef.current,
|
|
261
|
+
accumulatedRef.current,
|
|
262
|
+
);
|
|
263
|
+
if ("x" in properties) properties.x = Math.round(basePositionRef.current.x + properties.x);
|
|
264
|
+
if ("y" in properties) properties.y = Math.round(basePositionRef.current.y + properties.y);
|
|
265
|
+
|
|
266
|
+
accumulatedRef.current = nextState;
|
|
267
|
+
scrollDeltaRef.current = 0;
|
|
268
|
+
|
|
269
|
+
// Manual seek on the raw GSAP timeline (not the Studio player wrapper,
|
|
270
|
+
// which triggers React state updates). After seek renders all elements
|
|
271
|
+
// at the correct time, gsap.set overrides the recorded element so it
|
|
272
|
+
// follows the pointer. The browser paints the set values on this frame;
|
|
273
|
+
// next tick's seek will overwrite, but we re-apply immediately.
|
|
274
|
+
if (runtimeRef.current) {
|
|
275
|
+
try {
|
|
276
|
+
const seekTime = Math.min(
|
|
277
|
+
runtimeRef.current.startTime + time,
|
|
278
|
+
runtimeRef.current.maxSeekTime,
|
|
279
|
+
);
|
|
280
|
+
runtimeRef.current.seek(seekTime);
|
|
281
|
+
runtimeRef.current.set(runtimeRef.current.selector, { ...properties });
|
|
282
|
+
runtimeRef.current.element.style.visibility = "visible";
|
|
283
|
+
liveTime.notify(seekTime);
|
|
284
|
+
usePlayerStore.getState().setCurrentTime(seekTime);
|
|
285
|
+
} catch {
|
|
286
|
+
runtimeRef.current = null;
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
samplesRef.current.push({ time, properties });
|
|
291
|
+
trailRef.current.push({ x: pointerRef.current.x, y: pointerRef.current.y });
|
|
292
|
+
setRecordingDuration(time);
|
|
293
|
+
rafIdRef.current = requestAnimationFrame(tick);
|
|
294
|
+
};
|
|
295
|
+
|
|
296
|
+
setIsRecording(true);
|
|
297
|
+
rafIdRef.current = requestAnimationFrame(tick);
|
|
298
|
+
|
|
299
|
+
cleanupRef.current = () => {
|
|
300
|
+
cancelAnimationFrame(rafIdRef.current);
|
|
301
|
+
document.removeEventListener("pointermove", handlePointerMove);
|
|
302
|
+
document.removeEventListener("wheel", handleWheel);
|
|
303
|
+
document.removeEventListener("keydown", handleKeyChange);
|
|
304
|
+
document.removeEventListener("keyup", handleKeyChange);
|
|
305
|
+
document.removeEventListener("pointermove", captureStart);
|
|
306
|
+
};
|
|
307
|
+
},
|
|
308
|
+
[], // No deps — uses refs only for all mutable state
|
|
309
|
+
);
|
|
310
|
+
|
|
311
|
+
const stopRecording = useCallback((): GestureSample[] => {
|
|
312
|
+
if (!isRecordingRef.current) return [];
|
|
313
|
+
isRecordingRef.current = false;
|
|
314
|
+
runtimeRef.current = null;
|
|
315
|
+
cleanupRef.current?.();
|
|
316
|
+
cleanupRef.current = null;
|
|
317
|
+
const frozen = samplesRef.current.slice();
|
|
318
|
+
setRecordingDuration(frozen.length > 0 ? frozen[frozen.length - 1]!.time : 0);
|
|
319
|
+
setIsRecording(false);
|
|
320
|
+
return frozen;
|
|
321
|
+
}, []); // No deps — uses refs only
|
|
322
|
+
|
|
323
|
+
const clearSamples = useCallback(() => {
|
|
324
|
+
samplesRef.current = [];
|
|
325
|
+
trailRef.current = [];
|
|
326
|
+
setRecordingDuration(0);
|
|
327
|
+
accumulatedRef.current = { opacity: 1, scale: 1, z: 0 };
|
|
328
|
+
scrollDeltaRef.current = 0;
|
|
329
|
+
}, []);
|
|
330
|
+
|
|
331
|
+
return {
|
|
332
|
+
startRecording,
|
|
333
|
+
stopRecording,
|
|
334
|
+
isRecording,
|
|
335
|
+
samplesRef,
|
|
336
|
+
trailRef,
|
|
337
|
+
recordingDuration,
|
|
338
|
+
clearSamples,
|
|
339
|
+
};
|
|
340
|
+
}
|