reanimated-pause-state 0.1.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/README.md +276 -0
- package/dist/babel.d.ts +21 -0
- package/dist/babel.d.ts.map +1 -0
- package/dist/babel.js +315 -0
- package/dist/babel.js.map +1 -0
- package/dist/index.d.ts +69 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +454 -0
- package/dist/index.js.map +1 -0
- package/dist/types.d.ts +144 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +3 -0
- package/dist/types.js.map +1 -0
- package/package.json +54 -0
- package/src/babel.ts +486 -0
- package/src/global.d.ts +11 -0
- package/src/index.ts +553 -0
- package/src/types.ts +156 -0
package/src/index.ts
ADDED
|
@@ -0,0 +1,553 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* reanimated-pause-plugin
|
|
3
|
+
*
|
|
4
|
+
* Pause and resume Reanimated animations with structured state snapshots.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type {
|
|
8
|
+
AnimationSnapshot,
|
|
9
|
+
AnimationRegistration,
|
|
10
|
+
PauseSnapshot,
|
|
11
|
+
AnimationType,
|
|
12
|
+
SharedValueRef,
|
|
13
|
+
SnapshotOptions,
|
|
14
|
+
} from './types';
|
|
15
|
+
|
|
16
|
+
export type { AnimationSnapshot, PauseSnapshot, AnimationType, SnapshotOptions } from './types';
|
|
17
|
+
|
|
18
|
+
// Type declarations for globals
|
|
19
|
+
declare global {
|
|
20
|
+
var __RPP__: {
|
|
21
|
+
paused: boolean;
|
|
22
|
+
pausedAt: number | null;
|
|
23
|
+
snapshot: PauseSnapshot | null;
|
|
24
|
+
} | undefined;
|
|
25
|
+
var __RPP_PAUSED__: boolean | undefined;
|
|
26
|
+
var __RPP_INSTALLED__: { installed: boolean; version: string } | undefined;
|
|
27
|
+
var __RPP_ORIGINAL_RAF__: ((callback: (timestamp: number) => void) => number) | undefined;
|
|
28
|
+
var __RPP_PENDING_CALLBACKS__: Array<(timestamp: number) => void> | undefined;
|
|
29
|
+
var __RPP_PAUSED_AT__: number | undefined;
|
|
30
|
+
var __RPP_ACCUMULATED_PAUSE__: number | undefined;
|
|
31
|
+
var __RPP_REGISTRY__: {
|
|
32
|
+
animations: Map<string, AnimationRegistration>;
|
|
33
|
+
register: (meta: any) => void;
|
|
34
|
+
unregister: (id: string) => void;
|
|
35
|
+
getAll: () => AnimationRegistration[];
|
|
36
|
+
} | undefined;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const VERSION = '0.1.0';
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Initialize JS-side state and registry
|
|
43
|
+
*/
|
|
44
|
+
function ensureInitialized(): void {
|
|
45
|
+
if (typeof globalThis.__RPP__ === 'undefined') {
|
|
46
|
+
globalThis.__RPP__ = {
|
|
47
|
+
paused: false,
|
|
48
|
+
pausedAt: null,
|
|
49
|
+
snapshot: null,
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if (typeof globalThis.__RPP_REGISTRY__ === 'undefined') {
|
|
54
|
+
const animations = new Map<string, AnimationRegistration>();
|
|
55
|
+
|
|
56
|
+
globalThis.__RPP_REGISTRY__ = {
|
|
57
|
+
animations,
|
|
58
|
+
register: (meta: any) => {
|
|
59
|
+
// Skip registration in production - zero overhead
|
|
60
|
+
if (typeof __DEV__ !== 'undefined' && !__DEV__) {
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const config = meta.__rpp_config || {};
|
|
65
|
+
|
|
66
|
+
// Extract toValue and duration from config
|
|
67
|
+
const toValue = config.toValue ?? 0;
|
|
68
|
+
const duration = config.duration;
|
|
69
|
+
|
|
70
|
+
// Capture SharedValue reference if available
|
|
71
|
+
const sharedValue: SharedValueRef | undefined = meta.__rpp_sharedValue;
|
|
72
|
+
|
|
73
|
+
// Read the current value from SharedValue as the fromValue
|
|
74
|
+
// This works because Reanimated syncs SharedValue.value to JS thread
|
|
75
|
+
const fromValue = sharedValue?.value ?? 0;
|
|
76
|
+
|
|
77
|
+
const registration: AnimationRegistration = {
|
|
78
|
+
id: String(meta.__rpp_id),
|
|
79
|
+
type: meta.__rpp_type || 'unknown',
|
|
80
|
+
callsite: {
|
|
81
|
+
file: meta.__rpp_file || 'unknown',
|
|
82
|
+
line: meta.__rpp_line || 0,
|
|
83
|
+
column: meta.__rpp_column,
|
|
84
|
+
},
|
|
85
|
+
config,
|
|
86
|
+
startTime: Date.now(),
|
|
87
|
+
fromValue,
|
|
88
|
+
toValue,
|
|
89
|
+
duration,
|
|
90
|
+
sharedValue, // Store reference for reading current value at pause time
|
|
91
|
+
sharedValueName: meta.__rpp_sharedValueName,
|
|
92
|
+
};
|
|
93
|
+
animations.set(registration.id, registration);
|
|
94
|
+
|
|
95
|
+
// Don't auto-cleanup - animations may repeat or user may pause at any time
|
|
96
|
+
// Cleanup will happen on app reload or explicit clear
|
|
97
|
+
},
|
|
98
|
+
unregister: (id: string) => {
|
|
99
|
+
animations.delete(id);
|
|
100
|
+
},
|
|
101
|
+
getAll: () => Array.from(animations.values()),
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Check if the Babel plugin is installed
|
|
108
|
+
*/
|
|
109
|
+
export function isInstalled(): boolean {
|
|
110
|
+
return globalThis.__RPP_INSTALLED__?.installed === true;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Check if animations are paused
|
|
115
|
+
*/
|
|
116
|
+
export function isPaused(): boolean {
|
|
117
|
+
ensureInitialized();
|
|
118
|
+
return globalThis.__RPP__!.paused;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Get the last pause snapshot (or null if not paused/no snapshot)
|
|
123
|
+
*/
|
|
124
|
+
export function getSnapshot(): PauseSnapshot | null {
|
|
125
|
+
ensureInitialized();
|
|
126
|
+
return globalThis.__RPP__!.snapshot;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Take a snapshot now with optional filtering
|
|
131
|
+
* This is the main API for getting animation state
|
|
132
|
+
*/
|
|
133
|
+
export function snapshotNow(opts?: SnapshotOptions): PauseSnapshot {
|
|
134
|
+
// No-op in production
|
|
135
|
+
if (typeof __DEV__ !== 'undefined' && !__DEV__) {
|
|
136
|
+
return { timestamp: 0, timestampISO: '', animations: [], totalCount: 0, summary: 'disabled in production' };
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
ensureInitialized();
|
|
140
|
+
return captureSnapshot(opts);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Get snapshot as JSON string (for clipboard/export)
|
|
145
|
+
*/
|
|
146
|
+
export function getSnapshotJSON(opts?: SnapshotOptions): string {
|
|
147
|
+
const snapshot = snapshotNow(opts);
|
|
148
|
+
return JSON.stringify(snapshot, null, 2);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Get snapshot as prompt-ready markdown
|
|
153
|
+
* Optimized for pasting into Claude/LLM context
|
|
154
|
+
*/
|
|
155
|
+
export function getSnapshotMarkdown(opts?: SnapshotOptions): string {
|
|
156
|
+
const snapshot = snapshotNow(opts);
|
|
157
|
+
if (!snapshot || snapshot.animations.length === 0) {
|
|
158
|
+
return '**Animation State:** No active animations captured.';
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
let md = `## Animation State at Pause\n\n`;
|
|
162
|
+
md += `> Captured: ${snapshot.timestampISO}\n\n`;
|
|
163
|
+
|
|
164
|
+
for (const anim of snapshot.animations) {
|
|
165
|
+
// Use SharedValue name as heading if available
|
|
166
|
+
const heading = anim.sharedValueName
|
|
167
|
+
? `\`${anim.sharedValueName}\` (${anim.type})`
|
|
168
|
+
: `${anim.type} animation`;
|
|
169
|
+
md += `### ${heading}\n\n`;
|
|
170
|
+
|
|
171
|
+
// Current value is the most important for debugging
|
|
172
|
+
md += `| Property | Value |\n`;
|
|
173
|
+
md += `|----------|-------|\n`;
|
|
174
|
+
md += `| **Current** | \`${anim.values.current.toFixed(2)}\` |\n`;
|
|
175
|
+
md += `| From → To | ${anim.values.from} → ${anim.values.to} |\n`;
|
|
176
|
+
|
|
177
|
+
if (anim.timing.duration) {
|
|
178
|
+
const progress = anim.timing.progress?.toFixed(1) || '0';
|
|
179
|
+
md += `| Progress | ${progress}% (${anim.timing.elapsed}ms / ${anim.timing.duration}ms) |\n`;
|
|
180
|
+
} else {
|
|
181
|
+
md += `| Elapsed | ${anim.timing.elapsed}ms |\n`;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
md += `| Location | \`${anim.callsite.file}:${anim.callsite.line}\` |\n`;
|
|
185
|
+
|
|
186
|
+
// Show relevant config
|
|
187
|
+
const cfg = anim.config.config as Record<string, unknown>;
|
|
188
|
+
if (anim.type === 'spring' && cfg.damping) {
|
|
189
|
+
md += `| Spring | damping=${cfg.damping}`;
|
|
190
|
+
if (cfg.mass) md += `, mass=${cfg.mass}`;
|
|
191
|
+
if (cfg.stiffness) md += `, stiffness=${cfg.stiffness}`;
|
|
192
|
+
md += ` |\n`;
|
|
193
|
+
}
|
|
194
|
+
if (anim.type === 'repeat') {
|
|
195
|
+
const reps = cfg.numberOfReps as number | undefined;
|
|
196
|
+
md += `| Repeat | ${reps === -1 ? 'infinite' : reps || 'unknown'}`;
|
|
197
|
+
if (cfg.reverse) md += ' (reversing)';
|
|
198
|
+
md += ` |\n`;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
md += `\n`;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
return md;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* Install the frame interceptor on UI thread
|
|
209
|
+
* Only installs in __DEV__ mode - zero cost in production
|
|
210
|
+
*/
|
|
211
|
+
function installFrameInterceptor(): void {
|
|
212
|
+
// Skip in production - this is a dev/design review tool
|
|
213
|
+
if (typeof __DEV__ !== 'undefined' && !__DEV__) {
|
|
214
|
+
return;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
try {
|
|
218
|
+
const { runOnUI } = require('react-native-reanimated');
|
|
219
|
+
|
|
220
|
+
runOnUI(() => {
|
|
221
|
+
'worklet';
|
|
222
|
+
|
|
223
|
+
if (globalThis.__RPP_ORIGINAL_RAF__) {
|
|
224
|
+
return;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
const originalRAF = globalThis.requestAnimationFrame;
|
|
228
|
+
if (!originalRAF) {
|
|
229
|
+
console.warn('[rpp] requestAnimationFrame not found on UI thread');
|
|
230
|
+
return;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
globalThis.__RPP_ORIGINAL_RAF__ = originalRAF;
|
|
234
|
+
globalThis.__RPP_PENDING_CALLBACKS__ = [];
|
|
235
|
+
globalThis.__RPP_PAUSED__ = false;
|
|
236
|
+
globalThis.__RPP_PAUSED_AT__ = 0;
|
|
237
|
+
globalThis.__RPP_ACCUMULATED_PAUSE__ = 0;
|
|
238
|
+
|
|
239
|
+
(globalThis as any).requestAnimationFrame = function(
|
|
240
|
+
callback: (timestamp: number) => void
|
|
241
|
+
): number {
|
|
242
|
+
if (globalThis.__RPP_PAUSED__) {
|
|
243
|
+
globalThis.__RPP_PENDING_CALLBACKS__!.push(callback);
|
|
244
|
+
return -1;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
return globalThis.__RPP_ORIGINAL_RAF__!((timestamp: number) => {
|
|
248
|
+
const adjustedTimestamp =
|
|
249
|
+
timestamp - (globalThis.__RPP_ACCUMULATED_PAUSE__ || 0);
|
|
250
|
+
callback(adjustedTimestamp);
|
|
251
|
+
});
|
|
252
|
+
};
|
|
253
|
+
|
|
254
|
+
})();
|
|
255
|
+
} catch (e) {
|
|
256
|
+
console.warn('[rpp] Failed to install frame interceptor:', e);
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/**
|
|
261
|
+
* Flush pending callbacks after resume
|
|
262
|
+
*/
|
|
263
|
+
function flushPendingCallbacks(): void {
|
|
264
|
+
try {
|
|
265
|
+
const { runOnUI } = require('react-native-reanimated');
|
|
266
|
+
|
|
267
|
+
runOnUI(() => {
|
|
268
|
+
'worklet';
|
|
269
|
+
|
|
270
|
+
const pending = globalThis.__RPP_PENDING_CALLBACKS__ || [];
|
|
271
|
+
globalThis.__RPP_PENDING_CALLBACKS__ = [];
|
|
272
|
+
|
|
273
|
+
if (pending.length === 0) return;
|
|
274
|
+
|
|
275
|
+
const originalRAF = globalThis.__RPP_ORIGINAL_RAF__;
|
|
276
|
+
if (originalRAF) {
|
|
277
|
+
originalRAF((timestamp: number) => {
|
|
278
|
+
const adjusted =
|
|
279
|
+
timestamp - (globalThis.__RPP_ACCUMULATED_PAUSE__ || 0);
|
|
280
|
+
for (const cb of pending) {
|
|
281
|
+
try {
|
|
282
|
+
cb(adjusted);
|
|
283
|
+
} catch (e) {
|
|
284
|
+
// Ignore errors in callbacks
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
});
|
|
288
|
+
}
|
|
289
|
+
})();
|
|
290
|
+
} catch (e) {
|
|
291
|
+
console.warn('[rpp] Failed to flush callbacks:', e);
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
/**
|
|
296
|
+
* Create a snapshot of current animation state with optional filtering
|
|
297
|
+
*/
|
|
298
|
+
function captureSnapshot(opts?: SnapshotOptions): PauseSnapshot {
|
|
299
|
+
ensureInitialized();
|
|
300
|
+
|
|
301
|
+
const now = Date.now();
|
|
302
|
+
let registrations = globalThis.__RPP_REGISTRY__?.getAll() || [];
|
|
303
|
+
|
|
304
|
+
// Tier 1: Filter by file
|
|
305
|
+
if (opts?.filterFile) {
|
|
306
|
+
registrations = registrations.filter((reg) =>
|
|
307
|
+
reg.callsite.file.includes(opts.filterFile!)
|
|
308
|
+
);
|
|
309
|
+
|
|
310
|
+
// Tier 2: Filter by line proximity (requires filterFile)
|
|
311
|
+
if (opts.filterLine !== undefined) {
|
|
312
|
+
const threshold = opts.proximityThreshold ?? 200;
|
|
313
|
+
registrations = registrations.filter(
|
|
314
|
+
(reg) => Math.abs(reg.callsite.line - opts.filterLine!) < threshold
|
|
315
|
+
);
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
// Apply max limit
|
|
320
|
+
const maxAnimations = opts?.maxAnimations ?? 20;
|
|
321
|
+
if (registrations.length > maxAnimations) {
|
|
322
|
+
registrations = registrations.slice(0, maxAnimations);
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
const animations: AnimationSnapshot[] = registrations.map((reg) => {
|
|
326
|
+
const elapsed = now - reg.startTime;
|
|
327
|
+
const progress = reg.duration ? (elapsed / reg.duration) * 100 : undefined;
|
|
328
|
+
|
|
329
|
+
// Read the current value from the SharedValue if available
|
|
330
|
+
// Reanimated syncs SharedValue.value to JS thread automatically
|
|
331
|
+
let currentValue = reg.fromValue;
|
|
332
|
+
if (reg.sharedValue && typeof reg.sharedValue.value === 'number') {
|
|
333
|
+
currentValue = reg.sharedValue.value;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
const snapshot: AnimationSnapshot = {
|
|
337
|
+
id: reg.id,
|
|
338
|
+
type: reg.type as AnimationType,
|
|
339
|
+
sharedValueName: reg.sharedValueName,
|
|
340
|
+
callsite: reg.callsite,
|
|
341
|
+
config: {
|
|
342
|
+
type: reg.type as any,
|
|
343
|
+
config: reg.config as any,
|
|
344
|
+
},
|
|
345
|
+
timing: {
|
|
346
|
+
startTime: reg.startTime,
|
|
347
|
+
elapsed,
|
|
348
|
+
duration: reg.duration,
|
|
349
|
+
progress: progress ? Math.min(progress, 100) : undefined,
|
|
350
|
+
},
|
|
351
|
+
values: {
|
|
352
|
+
from: reg.fromValue,
|
|
353
|
+
to: reg.toValue,
|
|
354
|
+
current: currentValue,
|
|
355
|
+
},
|
|
356
|
+
description: formatDescription(reg, elapsed, currentValue),
|
|
357
|
+
};
|
|
358
|
+
|
|
359
|
+
return snapshot;
|
|
360
|
+
});
|
|
361
|
+
|
|
362
|
+
const snapshot: PauseSnapshot = {
|
|
363
|
+
timestamp: now,
|
|
364
|
+
timestampISO: new Date(now).toISOString(),
|
|
365
|
+
animations,
|
|
366
|
+
totalCount: animations.length,
|
|
367
|
+
summary: `${animations.length} animation(s) captured`,
|
|
368
|
+
};
|
|
369
|
+
|
|
370
|
+
return snapshot;
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
/**
|
|
374
|
+
* Format a human-readable description optimized for prompt context
|
|
375
|
+
*/
|
|
376
|
+
function formatDescription(
|
|
377
|
+
reg: AnimationRegistration,
|
|
378
|
+
elapsed: number,
|
|
379
|
+
currentValue?: number
|
|
380
|
+
): string {
|
|
381
|
+
const file = reg.callsite.file.split('/').pop() || reg.callsite.file;
|
|
382
|
+
const location = `${file}:${reg.callsite.line}`;
|
|
383
|
+
const config = reg.config as Record<string, unknown>;
|
|
384
|
+
|
|
385
|
+
// Start with SharedValue name if available (most useful for prompts)
|
|
386
|
+
let desc = reg.sharedValueName ? `${reg.sharedValueName}: ` : '';
|
|
387
|
+
|
|
388
|
+
desc += `${reg.type} `;
|
|
389
|
+
|
|
390
|
+
// Add config-specific details
|
|
391
|
+
switch (reg.type) {
|
|
392
|
+
case 'timing': {
|
|
393
|
+
if (reg.toValue !== 0) {
|
|
394
|
+
desc += `to ${reg.toValue} `;
|
|
395
|
+
}
|
|
396
|
+
if (reg.duration) {
|
|
397
|
+
desc += `(${reg.duration}ms) `;
|
|
398
|
+
}
|
|
399
|
+
break;
|
|
400
|
+
}
|
|
401
|
+
case 'spring': {
|
|
402
|
+
if (reg.toValue !== 0) {
|
|
403
|
+
desc += `to ${reg.toValue} `;
|
|
404
|
+
}
|
|
405
|
+
if (config.damping) {
|
|
406
|
+
desc += `damping=${config.damping} `;
|
|
407
|
+
}
|
|
408
|
+
break;
|
|
409
|
+
}
|
|
410
|
+
case 'repeat': {
|
|
411
|
+
const reps = config.numberOfReps as number | undefined;
|
|
412
|
+
if (reps === -1) {
|
|
413
|
+
desc += '(infinite) ';
|
|
414
|
+
} else if (reps) {
|
|
415
|
+
desc += `(${reps}x) `;
|
|
416
|
+
}
|
|
417
|
+
if (config.reverse) {
|
|
418
|
+
desc += 'reverse ';
|
|
419
|
+
}
|
|
420
|
+
break;
|
|
421
|
+
}
|
|
422
|
+
case 'delay': {
|
|
423
|
+
if (config.delayMs) {
|
|
424
|
+
desc += `${config.delayMs}ms `;
|
|
425
|
+
}
|
|
426
|
+
break;
|
|
427
|
+
}
|
|
428
|
+
case 'decay': {
|
|
429
|
+
if (config.velocity) {
|
|
430
|
+
desc += `velocity=${config.velocity} `;
|
|
431
|
+
}
|
|
432
|
+
break;
|
|
433
|
+
}
|
|
434
|
+
case 'sequence': {
|
|
435
|
+
if (config.count) {
|
|
436
|
+
desc += `(${config.count} animations) `;
|
|
437
|
+
}
|
|
438
|
+
break;
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
// Current value is the most important info for debugging
|
|
443
|
+
if (currentValue !== undefined) {
|
|
444
|
+
desc += `| current=${currentValue.toFixed(2)} `;
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
desc += `| ${elapsed}ms elapsed`;
|
|
448
|
+
if (reg.duration) {
|
|
449
|
+
const progress = Math.min((elapsed / reg.duration) * 100, 100).toFixed(0);
|
|
450
|
+
desc += ` (${progress}%)`;
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
desc += ` | ${location}`;
|
|
454
|
+
|
|
455
|
+
return desc;
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
/**
|
|
459
|
+
* Pause all animations and capture snapshot
|
|
460
|
+
* No-op in production builds
|
|
461
|
+
*/
|
|
462
|
+
export function pause(): PauseSnapshot {
|
|
463
|
+
// No-op in production
|
|
464
|
+
if (typeof __DEV__ !== 'undefined' && !__DEV__) {
|
|
465
|
+
return { timestamp: 0, timestampISO: '', animations: [], totalCount: 0, summary: 'disabled in production' };
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
ensureInitialized();
|
|
469
|
+
|
|
470
|
+
if (globalThis.__RPP__!.paused) {
|
|
471
|
+
return globalThis.__RPP__!.snapshot!;
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
installFrameInterceptor();
|
|
475
|
+
|
|
476
|
+
// Capture snapshot before pausing
|
|
477
|
+
const snapshot = captureSnapshot();
|
|
478
|
+
globalThis.__RPP__!.snapshot = snapshot;
|
|
479
|
+
globalThis.__RPP__!.paused = true;
|
|
480
|
+
globalThis.__RPP__!.pausedAt = Date.now();
|
|
481
|
+
|
|
482
|
+
try {
|
|
483
|
+
const { runOnUI } = require('react-native-reanimated');
|
|
484
|
+
|
|
485
|
+
runOnUI(() => {
|
|
486
|
+
'worklet';
|
|
487
|
+
globalThis.__RPP_PAUSED__ = true;
|
|
488
|
+
globalThis.__RPP_PAUSED_AT__ =
|
|
489
|
+
(globalThis as any)._getAnimationTimestamp?.() || Date.now();
|
|
490
|
+
})();
|
|
491
|
+
} catch (e) {
|
|
492
|
+
console.warn('[rpp] Failed to pause on UI thread:', e);
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
return snapshot;
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
/**
|
|
499
|
+
* Resume all animations
|
|
500
|
+
*/
|
|
501
|
+
export function resume(): void {
|
|
502
|
+
ensureInitialized();
|
|
503
|
+
|
|
504
|
+
if (!globalThis.__RPP__!.paused) {
|
|
505
|
+
return;
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
globalThis.__RPP__!.paused = false;
|
|
509
|
+
|
|
510
|
+
try {
|
|
511
|
+
const { runOnUI } = require('react-native-reanimated');
|
|
512
|
+
|
|
513
|
+
runOnUI(() => {
|
|
514
|
+
'worklet';
|
|
515
|
+
|
|
516
|
+
const pausedAt = globalThis.__RPP_PAUSED_AT__ || 0;
|
|
517
|
+
const now = (globalThis as any)._getAnimationTimestamp?.() || Date.now();
|
|
518
|
+
const pauseDuration = now - pausedAt;
|
|
519
|
+
|
|
520
|
+
globalThis.__RPP_ACCUMULATED_PAUSE__ =
|
|
521
|
+
(globalThis.__RPP_ACCUMULATED_PAUSE__ || 0) + pauseDuration;
|
|
522
|
+
|
|
523
|
+
globalThis.__RPP_PAUSED__ = false;
|
|
524
|
+
})();
|
|
525
|
+
|
|
526
|
+
flushPendingCallbacks();
|
|
527
|
+
} catch (e) {
|
|
528
|
+
console.warn('[rpp] Failed to resume:', e);
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
/**
|
|
534
|
+
* Toggle pause state
|
|
535
|
+
*/
|
|
536
|
+
export function toggle(): PauseSnapshot | null {
|
|
537
|
+
if (isPaused()) {
|
|
538
|
+
resume();
|
|
539
|
+
return null;
|
|
540
|
+
} else {
|
|
541
|
+
return pause();
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
// Auto-initialize registry immediately so Babel-injected trackers work
|
|
546
|
+
ensureInitialized();
|
|
547
|
+
|
|
548
|
+
// Auto-install interceptor in dev mode (delayed to ensure Reanimated is ready)
|
|
549
|
+
if (typeof __DEV__ !== 'undefined' && __DEV__) {
|
|
550
|
+
setTimeout(() => {
|
|
551
|
+
installFrameInterceptor();
|
|
552
|
+
}, 100);
|
|
553
|
+
}
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Animation snapshot captured at pause time
|
|
3
|
+
*/
|
|
4
|
+
export interface AnimationSnapshot {
|
|
5
|
+
/** Unique ID for this animation instance */
|
|
6
|
+
id: string;
|
|
7
|
+
|
|
8
|
+
/** Animation type: 'timing' | 'spring' | 'decay' | 'repeat' | 'sequence' | 'delay' */
|
|
9
|
+
type: AnimationType;
|
|
10
|
+
|
|
11
|
+
/** Variable name of the animated SharedValue (e.g., "translateX") */
|
|
12
|
+
sharedValueName?: string;
|
|
13
|
+
|
|
14
|
+
/** Source location where the animation was created */
|
|
15
|
+
callsite: {
|
|
16
|
+
file: string;
|
|
17
|
+
line: number;
|
|
18
|
+
column?: number;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
/** Animation configuration */
|
|
22
|
+
config: AnimationConfig;
|
|
23
|
+
|
|
24
|
+
/** Timing information */
|
|
25
|
+
timing: {
|
|
26
|
+
/** When the animation started (ms since epoch) */
|
|
27
|
+
startTime: number;
|
|
28
|
+
/** How long the animation has been running (ms) */
|
|
29
|
+
elapsed: number;
|
|
30
|
+
/** Expected duration (for timing animations) */
|
|
31
|
+
duration?: number;
|
|
32
|
+
/** Progress as percentage 0-100 (for timing animations) */
|
|
33
|
+
progress?: number;
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
/** Value information */
|
|
37
|
+
values: {
|
|
38
|
+
/** Starting value */
|
|
39
|
+
from: number;
|
|
40
|
+
/** Target value */
|
|
41
|
+
to: number;
|
|
42
|
+
/** Current value at pause */
|
|
43
|
+
current: number;
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
/** Human-readable description */
|
|
47
|
+
description: string;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export type AnimationType =
|
|
51
|
+
| 'timing'
|
|
52
|
+
| 'spring'
|
|
53
|
+
| 'decay'
|
|
54
|
+
| 'repeat'
|
|
55
|
+
| 'sequence'
|
|
56
|
+
| 'delay'
|
|
57
|
+
| 'unknown';
|
|
58
|
+
|
|
59
|
+
export interface TimingConfig {
|
|
60
|
+
duration: number;
|
|
61
|
+
easing?: string;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export interface SpringConfig {
|
|
65
|
+
damping?: number;
|
|
66
|
+
mass?: number;
|
|
67
|
+
stiffness?: number;
|
|
68
|
+
overshootClamping?: boolean;
|
|
69
|
+
restDisplacementThreshold?: number;
|
|
70
|
+
restSpeedThreshold?: number;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export interface DecayConfig {
|
|
74
|
+
velocity?: number;
|
|
75
|
+
deceleration?: number;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export interface RepeatConfig {
|
|
79
|
+
numberOfReps?: number;
|
|
80
|
+
reverse?: boolean;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export type AnimationConfig =
|
|
84
|
+
| { type: 'timing'; config: TimingConfig }
|
|
85
|
+
| { type: 'spring'; config: SpringConfig }
|
|
86
|
+
| { type: 'decay'; config: DecayConfig }
|
|
87
|
+
| { type: 'repeat'; config: RepeatConfig }
|
|
88
|
+
| { type: 'sequence'; config: { count: number } }
|
|
89
|
+
| { type: 'delay'; config: { delayMs: number } }
|
|
90
|
+
| { type: 'unknown'; config: Record<string, unknown> };
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Full pause state snapshot
|
|
94
|
+
*/
|
|
95
|
+
export interface PauseSnapshot {
|
|
96
|
+
/** When the snapshot was taken */
|
|
97
|
+
timestamp: number;
|
|
98
|
+
|
|
99
|
+
/** ISO timestamp string */
|
|
100
|
+
timestampISO: string;
|
|
101
|
+
|
|
102
|
+
/** All active animations at pause time */
|
|
103
|
+
animations: AnimationSnapshot[];
|
|
104
|
+
|
|
105
|
+
/** Total count of tracked animations */
|
|
106
|
+
totalCount: number;
|
|
107
|
+
|
|
108
|
+
/** Summary for quick reference */
|
|
109
|
+
summary: string;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Options for filtering snapshots
|
|
114
|
+
*/
|
|
115
|
+
export interface SnapshotOptions {
|
|
116
|
+
/** Filter animations by file path (partial match) */
|
|
117
|
+
filterFile?: string;
|
|
118
|
+
|
|
119
|
+
/** Filter animations by line proximity (requires filterFile) */
|
|
120
|
+
filterLine?: number;
|
|
121
|
+
|
|
122
|
+
/** Max distance in lines from filterLine (default: 200) */
|
|
123
|
+
proximityThreshold?: number;
|
|
124
|
+
|
|
125
|
+
/** Max animations to return (default: 20) */
|
|
126
|
+
maxAnimations?: number;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* SharedValue type from Reanimated (minimal interface for our needs)
|
|
131
|
+
*/
|
|
132
|
+
export interface SharedValueRef {
|
|
133
|
+
value: number;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Registration data for tracking an animation
|
|
138
|
+
*/
|
|
139
|
+
export interface AnimationRegistration {
|
|
140
|
+
id: string;
|
|
141
|
+
type: AnimationType;
|
|
142
|
+
callsite: {
|
|
143
|
+
file: string;
|
|
144
|
+
line: number;
|
|
145
|
+
column?: number;
|
|
146
|
+
};
|
|
147
|
+
config: Record<string, unknown>;
|
|
148
|
+
startTime: number;
|
|
149
|
+
fromValue: number;
|
|
150
|
+
toValue: number;
|
|
151
|
+
duration?: number;
|
|
152
|
+
/** Reference to the SharedValue being animated */
|
|
153
|
+
sharedValue?: SharedValueRef;
|
|
154
|
+
/** Variable name of the SharedValue (e.g., "translateX", "scale") */
|
|
155
|
+
sharedValueName?: string;
|
|
156
|
+
}
|