@remotion/gsap 4.0.517

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 ADDED
@@ -0,0 +1,18 @@
1
+ # @remotion/gsap
2
+
3
+ Use GSAP timelines in Remotion
4
+
5
+ [![NPM Downloads](https://img.shields.io/npm/dm/@remotion/gsap.svg?style=flat&color=black&label=Downloads)](https://npmcharts.com/compare/@remotion/gsap?minimal=true)
6
+
7
+ ## Installation
8
+
9
+ ```bash
10
+ npm install @remotion/gsap --save-exact
11
+ ```
12
+
13
+ When installing a Remotion package, make sure to align the version of all `remotion` and `@remotion/*` packages to the same version.
14
+ Remove the `^` character from the version number to use the exact version.
15
+
16
+ ## Usage
17
+
18
+ See the [documentation](https://www.remotion.dev/docs/gsap) for more information.
@@ -0,0 +1,390 @@
1
+ // src/use-gsap-timeline.ts
2
+ import { gsap } from "gsap";
3
+ import { useLayoutEffect, useRef, useState } from "react";
4
+ import { useCurrentFrame, useVideoConfig } from "remotion";
5
+
6
+ // src/frame-time.ts
7
+ var frameToSeconds = (frame, fps) => {
8
+ if (!Number.isFinite(frame)) {
9
+ throw new TypeError(`frame must be finite, received ${String(frame)}`);
10
+ }
11
+ if (!Number.isFinite(fps) || fps <= 0) {
12
+ throw new RangeError(`fps must be a positive finite number, received ${String(fps)}`);
13
+ }
14
+ return Math.max(0, frame) / fps;
15
+ };
16
+
17
+ // src/use-gsap-timeline.ts
18
+ var EMPTY_DEPENDENCIES = [];
19
+ var CALLBACK_KEYS = [
20
+ "onStart",
21
+ "onUpdate",
22
+ "onComplete",
23
+ "onRepeat",
24
+ "onReverseComplete",
25
+ "onInterrupt"
26
+ ];
27
+ var getTimelineAnimations = (timeline) => [
28
+ timeline,
29
+ ...timeline.getChildren(true, true, true)
30
+ ];
31
+ var animationVars = (animation) => animation.vars;
32
+ var isWalkable = (value) => {
33
+ if (typeof value !== "object" || value === null)
34
+ return false;
35
+ if (Array.isArray(value))
36
+ return true;
37
+ const proto = Object.getPrototypeOf(value);
38
+ return proto === Object.prototype || proto === null;
39
+ };
40
+ var findCallbacksInValue = (value, seen = new Set) => {
41
+ if (!isWalkable(value) || seen.has(value)) {
42
+ return [];
43
+ }
44
+ seen.add(value);
45
+ const entries = Array.isArray(value) ? value.map((entry, index) => [String(index), entry]) : Object.entries(value);
46
+ const found = [];
47
+ for (const [key, nestedValue] of entries) {
48
+ if (CALLBACK_KEYS.includes(key) && typeof nestedValue === "function") {
49
+ found.push(key);
50
+ }
51
+ found.push(...findCallbacksInValue(nestedValue, seen));
52
+ }
53
+ return [...new Set(found)];
54
+ };
55
+ var findTimelineCallbacks = (timeline) => {
56
+ const animations = getTimelineAnimations(timeline);
57
+ return [
58
+ ...new Set(animations.flatMap((animation) => findCallbacksInValue(animationVars(animation))))
59
+ ];
60
+ };
61
+ var isElementTarget = (target) => typeof target === "object" && target !== null && target.nodeType === 1;
62
+ var describeTarget = (target) => {
63
+ if (target === null) {
64
+ return "null";
65
+ }
66
+ if (typeof target !== "object") {
67
+ return typeof target;
68
+ }
69
+ const keys = Object.keys(target).filter((key) => key !== "_gsap");
70
+ const preview = keys.slice(0, 3).join(", ");
71
+ return `plain object {${preview}${keys.length > 3 ? ", …" : ""}}`;
72
+ };
73
+ var findNonElementTargets = (timeline) => {
74
+ const found = [];
75
+ for (const animation of getTimelineAnimations(timeline)) {
76
+ const targets = animation.targets?.();
77
+ if (!targets) {
78
+ continue;
79
+ }
80
+ for (const target of targets) {
81
+ if (!isElementTarget(target)) {
82
+ found.push(describeTarget(target));
83
+ }
84
+ }
85
+ }
86
+ return [...new Set(found)];
87
+ };
88
+ var GSAP_RANDOM_TOKEN = /random\(\s*(-?[\d.]|\[)/;
89
+ var findNondeterministicVars = (value, seen = new Set) => {
90
+ if (typeof value === "string") {
91
+ return GSAP_RANDOM_TOKEN.test(value) ? [`"${value.slice(0, 120)}"`] : [];
92
+ }
93
+ if (!isWalkable(value) || seen.has(value)) {
94
+ return [];
95
+ }
96
+ seen.add(value);
97
+ const found = [];
98
+ const record = value;
99
+ if (!Array.isArray(value)) {
100
+ if (record.repeatRefresh === true) {
101
+ found.push("repeatRefresh: true");
102
+ }
103
+ const { stagger } = record;
104
+ if (typeof stagger === "object" && stagger !== null && stagger.from === "random") {
105
+ found.push("stagger: {from: 'random'}");
106
+ }
107
+ }
108
+ for (const nested of Array.isArray(value) ? value : Object.values(record)) {
109
+ found.push(...findNondeterministicVars(nested, seen));
110
+ }
111
+ return [...new Set(found)];
112
+ };
113
+ var findTimelineNondeterminism = (timeline) => {
114
+ const animations = getTimelineAnimations(timeline);
115
+ return [
116
+ ...new Set(animations.flatMap((animation) => findNondeterministicVars(animationVars(animation))))
117
+ ];
118
+ };
119
+ var findStrayAnimations = (timeline, preexisting) => {
120
+ const owned = new Set(getTimelineAnimations(timeline));
121
+ const problems = [];
122
+ for (const animation of gsap.globalTimeline.getChildren(true, true, true)) {
123
+ if (owned.has(animation) || preexisting.has(animation)) {
124
+ continue;
125
+ }
126
+ const vars = animationVars(animation);
127
+ const callbacks = findCallbacksInValue(vars);
128
+ const delay = typeof vars.delay === "number" ? vars.delay : 0;
129
+ if (callbacks.length > 0) {
130
+ problems.push(`a wall-clock animation with callbacks (${callbacks.join(", ")})`);
131
+ } else if (animation.totalDuration() > 0 || delay > 0) {
132
+ problems.push("a wall-clock animation that is not attached to the provided timeline");
133
+ }
134
+ }
135
+ return problems;
136
+ };
137
+ var assertNoCallbacks = (value) => {
138
+ const callbacks = findCallbacksInValue(value);
139
+ if (callbacks.length > 0) {
140
+ throw new Error(`useGsapTimeline does not allow timeline callbacks (${callbacks.join(", ")}). Derive visual state from the timeline instead of side effects.`);
141
+ }
142
+ };
143
+ var guardTimelineMethods = (timeline) => {
144
+ const originalPaused = timeline.paused.bind(timeline);
145
+ const originalTotalTime = timeline.totalTime.bind(timeline);
146
+ const originalKill = timeline.kill.bind(timeline);
147
+ const playbackError = () => {
148
+ throw new Error("useGsapTimeline builders must not start playback. Remove play(), resume(), restart(), reverse(), or paused(false).");
149
+ };
150
+ timeline.play = playbackError;
151
+ timeline.resume = playbackError;
152
+ timeline.restart = playbackError;
153
+ timeline.reverse = playbackError;
154
+ timeline.pause = () => {
155
+ throw new Error("useGsapTimeline owns timeline pause state. Do not call timeline.pause() inside the builder.");
156
+ };
157
+ timeline.paused = (value) => {
158
+ if (value === false) {
159
+ return playbackError();
160
+ }
161
+ if (value !== undefined) {
162
+ throw new Error("useGsapTimeline owns timeline pause state. Read timeline.paused() if needed, but do not set it.");
163
+ }
164
+ return originalPaused();
165
+ };
166
+ const seekingError = () => {
167
+ throw new Error("useGsapTimeline owns timeline seeking. Remove seek(), time(), totalTime(), progress(), totalProgress(), tweenTo(), or tweenFromTo() from the builder.");
168
+ };
169
+ for (const method of ["timeScale", "duration", "totalDuration"]) {
170
+ const original = timeline[method].bind(timeline);
171
+ timeline[method] = (...args) => {
172
+ if (args.length > 0) {
173
+ throw new Error(`useGsapTimeline does not support timeline.${method}(value). The Remotion frame drives the timeline at a fixed 1:1 time scale; author tween durations directly.`);
174
+ }
175
+ return original();
176
+ };
177
+ }
178
+ timeline.seek = seekingError;
179
+ timeline.time = seekingError;
180
+ timeline.totalTime = seekingError;
181
+ timeline.progress = seekingError;
182
+ timeline.totalProgress = seekingError;
183
+ timeline.tweenTo = seekingError;
184
+ timeline.tweenFromTo = seekingError;
185
+ timeline.kill = () => {
186
+ throw new Error("useGsapTimeline owns timeline cleanup. Do not call timeline.kill() inside the builder.");
187
+ };
188
+ const originalEventCallback = timeline.eventCallback.bind(timeline);
189
+ timeline.eventCallback = (...args) => {
190
+ if (args.length > 1 && typeof args[1] === "function") {
191
+ throw new Error("useGsapTimeline does not allow timeline.eventCallback().");
192
+ }
193
+ return originalEventCallback(...args);
194
+ };
195
+ timeline.then = () => {
196
+ throw new Error("useGsapTimeline does not allow timeline.then() callbacks.");
197
+ };
198
+ const originalTo = timeline.to.bind(timeline);
199
+ timeline.to = (...args) => {
200
+ assertNoCallbacks(args[1]);
201
+ return originalTo(...args);
202
+ };
203
+ const originalFrom = timeline.from.bind(timeline);
204
+ timeline.from = (...args) => {
205
+ assertNoCallbacks(args[1]);
206
+ return originalFrom(...args);
207
+ };
208
+ const originalFromTo = timeline.fromTo.bind(timeline);
209
+ timeline.fromTo = (...args) => {
210
+ assertNoCallbacks(args[1]);
211
+ assertNoCallbacks(args[2]);
212
+ return originalFromTo(...args);
213
+ };
214
+ const originalSet = timeline.set.bind(timeline);
215
+ timeline.set = (...args) => {
216
+ assertNoCallbacks(args[1]);
217
+ return originalSet(...args);
218
+ };
219
+ const originalAdd = timeline.add.bind(timeline);
220
+ timeline.add = (...args) => {
221
+ if (typeof args[0] === "function") {
222
+ throw new Error("useGsapTimeline does not allow callback functions in timeline.add().");
223
+ }
224
+ return originalAdd(...args);
225
+ };
226
+ timeline.call = () => {
227
+ throw new Error("useGsapTimeline does not allow timeline.call().");
228
+ };
229
+ return {
230
+ isPaused: () => originalPaused(),
231
+ pause: () => {
232
+ originalPaused(true);
233
+ },
234
+ renderAt: (seconds) => {
235
+ originalTotalTime(0, true);
236
+ originalTotalTime(seconds, true);
237
+ },
238
+ restoreForContextRevert: () => {
239
+ for (const method of [
240
+ "play",
241
+ "resume",
242
+ "restart",
243
+ "reverse",
244
+ "pause",
245
+ "paused",
246
+ "seek",
247
+ "time",
248
+ "totalTime",
249
+ "timeScale",
250
+ "duration",
251
+ "totalDuration",
252
+ "progress",
253
+ "totalProgress",
254
+ "tweenTo",
255
+ "tweenFromTo",
256
+ "kill",
257
+ "eventCallback",
258
+ "then",
259
+ "to",
260
+ "from",
261
+ "fromTo",
262
+ "set",
263
+ "add",
264
+ "call"
265
+ ]) {
266
+ delete timeline[method];
267
+ }
268
+ },
269
+ kill: () => {
270
+ originalKill();
271
+ }
272
+ };
273
+ };
274
+ var useGsapTimeline = (build, { dependencies = EMPTY_DEPENDENCIES } = {}) => {
275
+ const frame = useCurrentFrame();
276
+ const { fps } = useVideoConfig();
277
+ const scopeRef = useRef(null);
278
+ const controlsRef = useRef(null);
279
+ const builtForRef = useRef(null);
280
+ const buildRef = useRef(build);
281
+ const frameRef = useRef(frame);
282
+ const fpsRef = useRef(fps);
283
+ const [remountNonce, setRemountNonce] = useState(0);
284
+ buildRef.current = build;
285
+ frameRef.current = frame;
286
+ fpsRef.current = fps;
287
+ useLayoutEffect(() => {
288
+ const scope = scopeRef.current;
289
+ builtForRef.current = scope;
290
+ if (!scope) {
291
+ return;
292
+ }
293
+ const state = { timeline: null, controls: null };
294
+ const context = gsap.context(() => {
295
+ return;
296
+ }, scope);
297
+ try {
298
+ context.add(() => {
299
+ state.timeline = gsap.timeline({ paused: true });
300
+ state.controls = guardTimelineMethods(state.timeline);
301
+ const selector = gsap.utils.selector(scope);
302
+ const preexisting = new Set(gsap.globalTimeline.getChildren(true, true, true));
303
+ const originalTickerAdd = gsap.ticker.add.bind(gsap.ticker);
304
+ gsap.ticker.add = () => {
305
+ throw new Error("useGsapTimeline builders must not register gsap.ticker callbacks. The Remotion frame is the only clock.");
306
+ };
307
+ let result;
308
+ try {
309
+ result = buildRef.current({
310
+ timeline: state.timeline,
311
+ scope,
312
+ selector
313
+ });
314
+ } finally {
315
+ gsap.ticker.add = originalTickerAdd;
316
+ }
317
+ if (!state.controls.isPaused()) {
318
+ throw new Error("useGsapTimeline builders must not start playback. Remove play(), resume(), restart(), or reverse().");
319
+ }
320
+ if (result instanceof Promise) {
321
+ throw new TypeError("useGsapTimeline builders must be synchronous. Load data before building the timeline.");
322
+ }
323
+ const callbacks = findTimelineCallbacks(state.timeline);
324
+ if (callbacks.length > 0) {
325
+ throw new Error(`useGsapTimeline does not allow timeline callbacks (${callbacks.join(", ")}). Derive visual state from the timeline instead of side effects.`);
326
+ }
327
+ const nonElementTargets = findNonElementTargets(state.timeline);
328
+ if (nonElementTargets.length > 0) {
329
+ throw new Error(`useGsapTimeline tweens must target DOM or SVG elements, but found: ${nonElementTargets.join("; ")}. Tweening plain objects animates in the Player but freezes in stills and renders, because nothing re-reads the object after a frame seek. Animate elements through the scoped selector or refs, or derive numeric values with Remotion's useCurrentFrame() and interpolate().`);
330
+ }
331
+ const nondeterminism = findTimelineNondeterminism(state.timeline);
332
+ if (nondeterminism.length > 0) {
333
+ throw new Error(`useGsapTimeline rejects nondeterministic tween configuration (${nondeterminism.join("; ")}). GSAP resolves these with unseeded Math.random, so every mount and every render process disagrees. Derive stable values from data or Remotion's seeded random().`);
334
+ }
335
+ const strays = findStrayAnimations(state.timeline, preexisting);
336
+ if (strays.length > 0) {
337
+ throw new Error(`useGsapTimeline builders created ${strays.join("; ")}. Animations that are not children of the provided timeline advance on the wall clock instead of the Remotion frame. Attach them to the timeline, or use a plain zero-duration gsap.set() for static state.`);
338
+ }
339
+ state.controls.pause();
340
+ const ownedTimeline = state.timeline;
341
+ const startTimeOnOwnedClock = (child) => {
342
+ let time = child.startTime();
343
+ let parent = child.parent;
344
+ while (parent && parent !== ownedTimeline) {
345
+ time = parent.startTime() + time / (parent.timeScale() || 1);
346
+ parent = parent.parent;
347
+ }
348
+ return time;
349
+ };
350
+ const startTimes = [
351
+ ...new Set(state.timeline.getChildren(true, true, true).map((child) => startTimeOnOwnedClock(child)).filter((startTime) => Number.isFinite(startTime)).sort((a, b) => a - b))
352
+ ];
353
+ const totalDuration = state.timeline.totalDuration();
354
+ const cycleDuration = state.timeline.duration();
355
+ const primeEnd = Number.isFinite(totalDuration) ? totalDuration : Number.isFinite(cycleDuration) ? cycleDuration : 3600;
356
+ for (const startTime of [...startTimes, primeEnd]) {
357
+ state.controls.renderAt(startTime);
358
+ }
359
+ state.controls.renderAt(frameToSeconds(frameRef.current, fpsRef.current));
360
+ });
361
+ controlsRef.current = state.controls;
362
+ } catch (error) {
363
+ state.controls?.restoreForContextRevert();
364
+ context.revert();
365
+ state.controls?.kill();
366
+ throw error;
367
+ }
368
+ return () => {
369
+ controlsRef.current = null;
370
+ state.controls?.restoreForContextRevert();
371
+ context.revert();
372
+ state.controls?.kill();
373
+ };
374
+ }, [...dependencies, remountNonce]);
375
+ useLayoutEffect(() => {
376
+ const controls = controlsRef.current;
377
+ if (controls) {
378
+ controls.renderAt(frameToSeconds(frame, fps));
379
+ }
380
+ }, [fps, frame]);
381
+ useLayoutEffect(() => {
382
+ if (scopeRef.current !== builtForRef.current) {
383
+ setRemountNonce((nonce) => nonce + 1);
384
+ }
385
+ });
386
+ return scopeRef;
387
+ };
388
+ export {
389
+ useGsapTimeline
390
+ };
@@ -0,0 +1 @@
1
+ export declare const frameToSeconds: (frame: number, fps: number) => number;
@@ -0,0 +1,3 @@
1
+ /// <reference types="gsap" />
2
+ export { useGsapTimeline } from './use-gsap-timeline';
3
+ export type { GsapTimelineBuildContext, GsapTimelineBuilder, GsapScopeRef, UseGsapTimelineOptions, } from './use-gsap-timeline';
@@ -0,0 +1,24 @@
1
+ /// <reference types="gsap" />
2
+ import { type DependencyList } from 'react';
3
+ export type GsapTimelineBuildContext<T extends Element> = {
4
+ /** A package-owned timeline that is already paused at time zero. */
5
+ timeline: gsap.core.Timeline;
6
+ /** The mounted element attached to the returned ref. */
7
+ scope: T;
8
+ /** A GSAP selector restricted to descendants of the scope element. */
9
+ selector: gsap.utils.SelectorFunc;
10
+ };
11
+ export type GsapTimelineBuilder<T extends Element> = (context: GsapTimelineBuildContext<T>) => unknown;
12
+ export type UseGsapTimelineOptions = {
13
+ /** Rebuild the timeline when one of these values changes. */
14
+ dependencies?: DependencyList;
15
+ };
16
+ /** A React 18/19-compatible object ref for the scoped HTML or SVG root. */
17
+ export type GsapScopeRef<T extends Element> = {
18
+ current: T | null;
19
+ };
20
+ /**
21
+ * Builds one scoped, paused GSAP timeline and seeks it from Remotion's frame.
22
+ * The hook never plays the timeline on GSAP's ticker.
23
+ */
24
+ export declare const useGsapTimeline: <T extends Element>(build: GsapTimelineBuilder<T>, { dependencies }?: UseGsapTimelineOptions) => GsapScopeRef<T>;
package/package.json ADDED
@@ -0,0 +1,64 @@
1
+ {
2
+ "name": "@remotion/gsap",
3
+ "version": "4.0.517",
4
+ "description": "Use GSAP timelines in Remotion",
5
+ "main": "dist/index.js",
6
+ "types": "dist/index.d.ts",
7
+ "module": "dist/esm/index.mjs",
8
+ "repository": {
9
+ "url": "https://github.com/remotion-dev/remotion/tree/main/packages/gsap"
10
+ },
11
+ "type": "module",
12
+ "scripts": {
13
+ "test": "bun test src/test",
14
+ "formatting": "oxfmt src --check",
15
+ "format": "oxfmt src",
16
+ "lint": "eslint src",
17
+ "watch": "tsgo -w",
18
+ "make": "tsgo && bun --env-file=../.env.bundle bundle.ts"
19
+ },
20
+ "author": "Brayden Blackwell",
21
+ "contributors": [],
22
+ "license": "MIT",
23
+ "bugs": {
24
+ "url": "https://github.com/remotion-dev/remotion/issues"
25
+ },
26
+ "dependencies": {
27
+ "remotion": "4.0.517"
28
+ },
29
+ "peerDependencies": {
30
+ "gsap": ">=3.12.0 <4",
31
+ "react": ">=16.8.0",
32
+ "react-dom": ">=16.8.0"
33
+ },
34
+ "exports": {
35
+ ".": {
36
+ "types": "./dist/index.d.ts",
37
+ "module": "./dist/esm/index.mjs",
38
+ "import": "./dist/esm/index.mjs"
39
+ },
40
+ "./package.json": "./package.json"
41
+ },
42
+ "devDependencies": {
43
+ "react": "19.2.3",
44
+ "react-dom": "19.2.3",
45
+ "@types/react": "19.2.7",
46
+ "@types/react-dom": "19.2.3",
47
+ "@remotion/eslint-config-internal": "4.0.517",
48
+ "@happy-dom/global-registrator": "14.5.1",
49
+ "eslint": "9.19.0",
50
+ "gsap": "3.15.0",
51
+ "@typescript/native-preview": "7.0.0-dev.20260217.1"
52
+ },
53
+ "keywords": [
54
+ "remotion",
55
+ "gsap",
56
+ "animation",
57
+ "video",
58
+ "react"
59
+ ],
60
+ "publishConfig": {
61
+ "access": "public"
62
+ },
63
+ "homepage": "https://www.remotion.dev/docs/gsap"
64
+ }