@open-motion/core 0.0.1-alpha.0 → 0.0.2

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,38 @@
1
+ # @open-motion/core
2
+
3
+ Core React primitives, hooks, and player for **OpenMotion** — the open-source programmatic video engine.
4
+
5
+ ## ✨ Features
6
+
7
+ - ⚛️ **React Components**: Use `Composition`, `Sequence`, `Video`, and more to build your video.
8
+ - 🎣 **Powerful Hooks**: Access `useCurrentFrame`, `useVideoConfig`, and `getInputProps` anywhere.
9
+ - ⏱️ **Animation Utilities**: High-performance `spring` animations and multi-segment `interpolate` functions.
10
+ - 🎞️ **Interactive Player**: Real-time preview and scrubbing during development.
11
+
12
+ ## 🚀 Installation
13
+
14
+ ```bash
15
+ pnpm add @open-motion/core
16
+ # or
17
+ npm install @open-motion/core
18
+ ```
19
+
20
+ ## 📖 Usage
21
+
22
+ ```tsx
23
+ import { Composition, Sequence, spring, useCurrentFrame, useVideoConfig } from "@open-motion/core";
24
+
25
+ const MyVideo = () => {
26
+ const frame = useCurrentFrame();
27
+ const { fps } = useVideoConfig();
28
+ const scale = spring({ frame, fps });
29
+
30
+ return (
31
+ <div style={{ flex: 1, backgroundColor: "white", display: "flex", justifyContent: "center", alignItems: "center" }}>
32
+ <h1 style={{ transform: `scale(${scale})` }}>Hello OpenMotion</h1>
33
+ </div>
34
+ );
35
+ };
36
+ ```
37
+
38
+ Learn more at the [main OpenMotion repository](https://github.com/jsongo/open-motion).
package/dist/Player.js CHANGED
@@ -7,8 +7,23 @@ const index_1 = require("./index");
7
7
  const Player = ({ component: Component, config, inputProps = {}, controls = true, autoPlay = false, loop = false, }) => {
8
8
  const [frame, setFrame] = (0, react_1.useState)(0);
9
9
  const [isPlaying, setIsPlaying] = (0, react_1.useState)(autoPlay);
10
+ const [scale, setScale] = (0, react_1.useState)(1);
11
+ const containerRef = (0, react_1.useRef)(null);
10
12
  const requestRef = (0, react_1.useRef)();
11
13
  const lastTimeRef = (0, react_1.useRef)();
14
+ (0, react_1.useEffect)(() => {
15
+ const handleResize = () => {
16
+ if (containerRef.current) {
17
+ const parentWidth = containerRef.current.parentElement?.clientWidth || window.innerWidth;
18
+ const s = Math.min(1, (parentWidth - 40) / config.width);
19
+ if (s > 0)
20
+ setScale(s);
21
+ }
22
+ };
23
+ handleResize();
24
+ window.addEventListener('resize', handleResize);
25
+ return () => window.removeEventListener('resize', handleResize);
26
+ }, [config.width]);
12
27
  const animate = (0, react_1.useCallback)((time) => {
13
28
  if (lastTimeRef.current !== undefined) {
14
29
  const deltaTime = time - lastTimeRef.current;
@@ -51,12 +66,19 @@ const Player = ({ component: Component, config, inputProps = {}, controls = true
51
66
  setFrame(Number(e.target.value));
52
67
  setIsPlaying(false);
53
68
  };
54
- return ((0, jsx_runtime_1.jsxs)("div", { style: { display: 'inline-block', border: '1px solid #ccc', borderRadius: '4px', overflow: 'hidden' }, children: [(0, jsx_runtime_1.jsx)("div", { style: {
69
+ return ((0, jsx_runtime_1.jsxs)("div", { ref: containerRef, style: {
70
+ display: 'inline-block',
71
+ border: '1px solid #ccc',
72
+ borderRadius: '4px',
73
+ overflow: 'hidden',
74
+ background: '#f0f0f0',
75
+ width: config.width * scale,
76
+ }, children: [(0, jsx_runtime_1.jsx)("div", { style: {
55
77
  width: config.width,
56
78
  height: config.height,
57
- position: 'relative',
58
- background: '#000',
59
- overflow: 'hidden'
60
- }, children: (0, jsx_runtime_1.jsx)(index_1.CompositionProvider, { config: config, frame: Math.floor(frame), inputProps: inputProps, children: (0, jsx_runtime_1.jsx)(Component, {}) }) }), controls && ((0, jsx_runtime_1.jsxs)("div", { style: { padding: '10px', background: '#f0f0f0', display: 'flex', alignItems: 'center', gap: '10px' }, children: [(0, jsx_runtime_1.jsx)("button", { onClick: togglePlay, children: isPlaying ? 'Pause' : 'Play' }), (0, jsx_runtime_1.jsx)("input", { type: "range", min: "0", max: config.durationInFrames - 1, step: "1", value: Math.floor(frame), onChange: handleSeek, style: { flex: 1 } }), (0, jsx_runtime_1.jsxs)("div", { style: { minWidth: '80px', fontSize: '12px', textAlign: 'right' }, children: [Math.floor(frame), " / ", config.durationInFrames] })] }))] }));
79
+ transform: `scale(${scale})`,
80
+ transformOrigin: 'top left',
81
+ background: '#fff'
82
+ }, children: (0, jsx_runtime_1.jsx)(index_1.CompositionProvider, { config: config, frame: frame, inputProps: inputProps, children: (0, jsx_runtime_1.jsx)(Component, {}) }) }), controls && ((0, jsx_runtime_1.jsxs)("div", { style: { padding: '10px', background: '#f0f0f0', display: 'flex', alignItems: 'center', gap: '10px', position: 'relative', zIndex: 10 }, children: [(0, jsx_runtime_1.jsx)("button", { onClick: togglePlay, children: isPlaying ? 'Pause' : 'Play' }), (0, jsx_runtime_1.jsx)("input", { type: "range", min: "0", max: config.durationInFrames - 1, step: "1", value: Math.floor(frame), onChange: handleSeek, style: { flex: 1 } }), (0, jsx_runtime_1.jsxs)("div", { style: { minWidth: '80px', fontSize: '12px', textAlign: 'right', color: '#000' }, children: [Math.floor(frame), " / ", config.durationInFrames] })] }))] }));
61
83
  };
62
84
  exports.Player = Player;
package/dist/index.d.ts CHANGED
@@ -34,13 +34,27 @@ export declare const delayRender: (label?: string) => number;
34
34
  * continueRender: Signal that an async resource has finished loading.
35
35
  */
36
36
  export declare const continueRender: (handle: number) => void;
37
+ /**
38
+ * Easing functions
39
+ */
40
+ export type EasingFunction = (t: number) => number;
41
+ export declare const Easing: {
42
+ linear: (t: number) => number;
43
+ ease: (t: number) => number;
44
+ in: (t: number) => number;
45
+ out: (t: number) => number;
46
+ inOut: (t: number) => number;
47
+ bezier: (_x1: number, y1: number, _x2: number, y2: number) => (t: number) => number;
48
+ step: (t: number) => 0 | 1;
49
+ };
37
50
  /**
38
51
  * interpolate function: maps a value from one range to another.
39
52
  * Compatible with Remotion's interpolate.
40
53
  */
41
- export declare const interpolate: (input: number, inputRange: [number, number], outputRange: [number, number], options?: {
54
+ export declare const interpolate: (input: number, inputRange: number[], outputRange: number[], options?: {
42
55
  extrapolateLeft?: "extrapolate" | "clamp";
43
56
  extrapolateRight?: "extrapolate" | "clamp";
57
+ easing?: EasingFunction;
44
58
  }) => number;
45
59
  /**
46
60
  * Time Hijacking Bridge Script
@@ -74,4 +88,17 @@ export declare const Audio: React.FC<{
74
88
  startFrom?: number;
75
89
  volume?: number;
76
90
  }>;
91
+ /**
92
+ * Video Component
93
+ * Supports frame-accurate seeking and synchronization with the engine.
94
+ */
95
+ export declare const Video: React.FC<{
96
+ src: string;
97
+ startFrom?: number;
98
+ endAt?: number;
99
+ playbackRate?: number;
100
+ muted?: boolean;
101
+ volume?: number;
102
+ style?: React.CSSProperties;
103
+ }>;
77
104
  export * from './Player';
package/dist/index.js CHANGED
@@ -10,25 +10,76 @@ var __createBinding = (this && this.__createBinding) || (Object.create ? (functi
10
10
  if (k2 === undefined) k2 = k;
11
11
  o[k2] = m[k];
12
12
  }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
13
35
  var __exportStar = (this && this.__exportStar) || function(m, exports) {
14
36
  for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
15
37
  };
16
38
  Object.defineProperty(exports, "__esModule", { value: true });
17
- exports.Audio = exports.spring = exports.Sequence = exports.getTimeHijackScript = exports.interpolate = exports.continueRender = exports.delayRender = exports.Composition = exports.getCompositionById = exports.getCompositions = exports.registerComposition = exports.getInputProps = exports.useAbsoluteFrame = exports.useCurrentFrame = exports.useVideoConfig = exports.CompositionProvider = void 0;
39
+ exports.Video = exports.Audio = exports.spring = exports.Sequence = exports.getTimeHijackScript = exports.interpolate = exports.Easing = exports.continueRender = exports.delayRender = exports.Composition = exports.getCompositionById = exports.getCompositions = exports.registerComposition = exports.getInputProps = exports.useAbsoluteFrame = exports.useCurrentFrame = exports.useVideoConfig = exports.CompositionProvider = void 0;
18
40
  const jsx_runtime_1 = require("react/jsx-runtime");
19
- const react_1 = require("react");
41
+ const react_1 = __importStar(require("react"));
20
42
  const VideoConfigContext = (0, react_1.createContext)(null);
21
43
  const FrameContext = (0, react_1.createContext)(0);
22
44
  const AbsoluteFrameContext = (0, react_1.createContext)(0);
23
45
  const InputPropsContext = (0, react_1.createContext)({});
24
46
  const CompositionProvider = ({ config, frame, inputProps = {}, children }) => {
25
- return ((0, jsx_runtime_1.jsx)(VideoConfigContext.Provider, { value: config, children: (0, jsx_runtime_1.jsx)(InputPropsContext.Provider, { value: inputProps, children: (0, jsx_runtime_1.jsx)(AbsoluteFrameContext.Provider, { value: frame, children: (0, jsx_runtime_1.jsx)(FrameContext.Provider, { value: frame, children: children }) }) }) }));
47
+ const [currentFrame, setCurrentFrame] = react_1.default.useState(frame);
48
+ react_1.default.useEffect(() => {
49
+ if (typeof window !== 'undefined') {
50
+ window.__OPEN_MOTION_READY__ = true;
51
+ }
52
+ }, [currentFrame]);
53
+ react_1.default.useEffect(() => {
54
+ setCurrentFrame(frame);
55
+ }, [frame]);
56
+ react_1.default.useEffect(() => {
57
+ if (typeof window !== 'undefined') {
58
+ const handler = (e) => {
59
+ if (typeof e.detail?.frame === 'number') {
60
+ setCurrentFrame(e.detail.frame);
61
+ }
62
+ };
63
+ window.addEventListener('open-motion-frame-update', handler);
64
+ return () => window.removeEventListener('open-motion-frame-update', handler);
65
+ }
66
+ }, []);
67
+ return ((0, jsx_runtime_1.jsx)(VideoConfigContext.Provider, { value: config, children: (0, jsx_runtime_1.jsx)(InputPropsContext.Provider, { value: inputProps, children: (0, jsx_runtime_1.jsx)(AbsoluteFrameContext.Provider, { value: currentFrame, children: (0, jsx_runtime_1.jsx)(FrameContext.Provider, { value: currentFrame, children: (0, jsx_runtime_1.jsx)("div", { style: {
68
+ width: config.width,
69
+ height: config.height,
70
+ backgroundColor: 'white',
71
+ display: 'flex',
72
+ flexDirection: 'column',
73
+ position: 'relative',
74
+ overflow: 'hidden',
75
+ }, children: children }) }) }) }) }));
26
76
  };
27
77
  exports.CompositionProvider = CompositionProvider;
28
78
  const useVideoConfig = () => {
29
79
  const context = (0, react_1.useContext)(VideoConfigContext);
30
- if (!context)
31
- throw new Error('useVideoConfig must be used within CompositionProvider');
80
+ if (!context) {
81
+ return { width: 1920, height: 1080, fps: 30, durationInFrames: 100 };
82
+ }
32
83
  return context;
33
84
  };
34
85
  exports.useVideoConfig = useVideoConfig;
@@ -71,9 +122,9 @@ let delayRenderCounter = 0;
71
122
  */
72
123
  const delayRender = (label) => {
73
124
  const handle = delayRenderCounter++;
74
- console.debug(`OpenMotion: delayRender[${handle}] ${label || ''}`);
75
125
  if (typeof window !== 'undefined') {
76
126
  window.__OPEN_MOTION_DELAY_RENDER_COUNT__ = (window.__OPEN_MOTION_DELAY_RENDER_COUNT__ || 0) + 1;
127
+ console.debug(`[OpenMotion] delayRender: ${label || handle}, count: ${window.__OPEN_MOTION_DELAY_RENDER_COUNT__}`);
77
128
  }
78
129
  return handle;
79
130
  };
@@ -82,25 +133,81 @@ exports.delayRender = delayRender;
82
133
  * continueRender: Signal that an async resource has finished loading.
83
134
  */
84
135
  const continueRender = (handle) => {
85
- console.debug(`OpenMotion: continueRender[${handle}]`);
86
136
  if (typeof window !== 'undefined') {
87
137
  window.__OPEN_MOTION_DELAY_RENDER_COUNT__ = Math.max(0, (window.__OPEN_MOTION_DELAY_RENDER_COUNT__ || 0) - 1);
138
+ console.debug(`[OpenMotion] continueRender: ${handle}, count: ${window.__OPEN_MOTION_DELAY_RENDER_COUNT__}`);
88
139
  }
89
140
  };
90
141
  exports.continueRender = continueRender;
142
+ exports.Easing = {
143
+ linear: (t) => t,
144
+ ease: (t) => t * t * (3 - 2 * t),
145
+ in: (t) => t * t,
146
+ out: (t) => t * (2 - t),
147
+ inOut: (t) => (t < 0.5 ? 2 * t * t : -1 + (4 - 2 * t) * t),
148
+ bezier: (_x1, y1, _x2, y2) => {
149
+ // Simple cubic bezier approximation (y-only for now)
150
+ return (t) => {
151
+ const u = 1 - t;
152
+ const tt = t * t;
153
+ const uu = u * u;
154
+ const ttt = tt * t;
155
+ // P0=0, P3=1
156
+ return 3 * uu * t * y1 + 3 * u * tt * y2 + ttt;
157
+ };
158
+ },
159
+ step: (t) => (t < 0.5 ? 0 : 1),
160
+ };
91
161
  /**
92
162
  * interpolate function: maps a value from one range to another.
93
163
  * Compatible with Remotion's interpolate.
94
164
  */
95
165
  const interpolate = (input, inputRange, outputRange, options) => {
96
- const [minInput, maxInput] = inputRange;
97
- const [minOutput, maxOutput] = outputRange;
98
- let result = minOutput + ((input - minInput) / (maxInput - minInput)) * (maxOutput - minOutput);
99
- if (options?.extrapolateLeft === 'clamp' && input < minInput)
100
- return minOutput;
101
- if (options?.extrapolateRight === 'clamp' && input > maxInput)
102
- return maxOutput;
103
- return result;
166
+ if (inputRange.length < 2)
167
+ return outputRange[0];
168
+ // Simple linear interpolation between multiple segments
169
+ for (let i = 0; i < inputRange.length - 1; i++) {
170
+ const minInput = inputRange[i];
171
+ const maxInput = inputRange[i + 1];
172
+ const minOutput = outputRange[i];
173
+ const maxOutput = outputRange[i + 1];
174
+ if (input >= minInput && input <= maxInput) {
175
+ let progress = (input - minInput) / (maxInput - minInput);
176
+ if (options?.easing) {
177
+ progress = options.easing(progress);
178
+ }
179
+ return minOutput + progress * (maxOutput - minOutput);
180
+ }
181
+ }
182
+ const firstInput = inputRange[0];
183
+ const lastInput = inputRange[inputRange.length - 1];
184
+ const firstOutput = outputRange[0];
185
+ const lastOutput = outputRange[outputRange.length - 1];
186
+ if (input < firstInput) {
187
+ if (options?.extrapolateLeft === 'clamp')
188
+ return firstOutput;
189
+ // Extrapolate using first segment
190
+ const nextInput = inputRange[1];
191
+ const nextOutput = outputRange[1];
192
+ let progress = (input - firstInput) / (nextInput - firstInput);
193
+ if (options?.easing) {
194
+ progress = options.easing(progress);
195
+ }
196
+ return firstOutput + progress * (nextOutput - firstOutput);
197
+ }
198
+ if (input > lastInput) {
199
+ if (options?.extrapolateRight === 'clamp')
200
+ return lastOutput;
201
+ // Extrapolate using last segment
202
+ const prevInput = inputRange[inputRange.length - 2];
203
+ const prevOutput = outputRange[outputRange.length - 2];
204
+ let progress = (input - lastInput) / (lastInput - prevInput);
205
+ if (options?.easing) {
206
+ progress = options.easing(progress);
207
+ }
208
+ return lastOutput + progress * (lastOutput - prevOutput);
209
+ }
210
+ return firstOutput;
104
211
  };
105
212
  exports.interpolate = interpolate;
106
213
  /**
@@ -131,8 +238,6 @@ const getTimeHijackScript = (frame, fps) => {
131
238
  return setTimeout(() => callback(timeMs), 0);
132
239
  };
133
240
  window.cancelAnimationFrame = (id) => clearTimeout(id);
134
-
135
- console.debug('OpenMotion: Time hijacked for frame ' + frame + ' at ' + timeMs + 'ms');
136
241
  })();
137
242
  `;
138
243
  };
@@ -171,13 +276,54 @@ exports.spring = spring;
171
276
  * Audio Component
172
277
  */
173
278
  const Audio = (props) => {
279
+ const startFrame = (0, exports.useAbsoluteFrame)();
174
280
  if (typeof window !== 'undefined') {
175
281
  window.__OPEN_MOTION_AUDIO_ASSETS__ = window.__OPEN_MOTION_AUDIO_ASSETS__ || [];
176
- if (!window.__OPEN_MOTION_AUDIO_ASSETS__.find((a) => a.src === props.src)) {
177
- window.__OPEN_MOTION_AUDIO_ASSETS__.push(props);
282
+ const exists = window.__OPEN_MOTION_AUDIO_ASSETS__.find((a) => a.src === props.src &&
283
+ (a.startFrom || 0) === (props.startFrom || 0) &&
284
+ (a.volume || 1) === (props.volume || 1) &&
285
+ a.startFrame === startFrame);
286
+ if (!exists) {
287
+ window.__OPEN_MOTION_AUDIO_ASSETS__.push({
288
+ ...props,
289
+ startFrame,
290
+ });
178
291
  }
179
292
  }
180
293
  return null;
181
294
  };
182
295
  exports.Audio = Audio;
296
+ /**
297
+ * Video Component
298
+ * Supports frame-accurate seeking and synchronization with the engine.
299
+ */
300
+ const Video = ({ src, startFrom = 0, endAt, playbackRate = 1, muted = true, volume = 1, style }) => {
301
+ const frame = (0, exports.useCurrentFrame)();
302
+ const { fps } = (0, exports.useVideoConfig)();
303
+ const videoRef = react_1.default.useRef(null);
304
+ const isVisible = endAt === undefined || frame < endAt;
305
+ react_1.default.useEffect(() => {
306
+ const video = videoRef.current;
307
+ if (!video || !isVisible)
308
+ return;
309
+ const targetTime = (frame * playbackRate + startFrom) / fps;
310
+ // Only seek if the difference is significant to avoid jitter
311
+ if (Math.abs(video.currentTime - targetTime) > 0.001) {
312
+ video.currentTime = targetTime;
313
+ }
314
+ }, [frame, fps, startFrom, playbackRate, isVisible]);
315
+ if (!isVisible)
316
+ return null;
317
+ return ((0, jsx_runtime_1.jsx)("video", { ref: videoRef, src: src, muted: muted, playsInline: true, style: {
318
+ display: 'block',
319
+ objectFit: 'cover',
320
+ ...style,
321
+ }, onLoadedMetadata: (e) => {
322
+ const video = e.currentTarget;
323
+ video.volume = volume;
324
+ // Ensure it's paused so we can control it via currentTime
325
+ video.pause();
326
+ } }));
327
+ };
328
+ exports.Video = Video;
183
329
  __exportStar(require("./Player"), exports);
package/package.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@open-motion/core",
3
- "version": "0.0.1-alpha.0",
3
+ "version": "0.0.2",
4
4
  "main": "dist/index.js",
5
5
  "types": "dist/index.d.ts",
6
6
  "license": "MIT",
7
7
  "repository": {
8
8
  "type": "git",
9
- "url": "https://github.com/open-motion/open-motion.git",
9
+ "url": "https://github.com/jsongo/open-motion.git",
10
10
  "directory": "packages/core"
11
11
  },
12
12
  "publishConfig": {
@@ -17,7 +17,8 @@
17
17
  ],
18
18
  "dependencies": {},
19
19
  "peerDependencies": {
20
- "react": "^18.0.0"
20
+ "react": "^18.0.0",
21
+ "react-dom": "^18.0.0"
21
22
  },
22
23
  "scripts": {
23
24
  "build": "tsc"