reactcut-napi 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/.turbo/turbo-build.log +5 -0
- package/Cargo.toml +19 -0
- package/build.rs +5 -0
- package/index.d.ts +37 -0
- package/index.js +318 -0
- package/package.json +19 -0
- package/reactcut.darwin-arm64.node +0 -0
- package/reactcut.linux-x64-gnu.node +0 -0
- package/src/lib.rs +114 -0
package/Cargo.toml
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
[package]
|
|
2
|
+
name = "reactcut-napi"
|
|
3
|
+
description = "Node.js native bindings for ReactCut (via napi-rs)"
|
|
4
|
+
version.workspace = true
|
|
5
|
+
edition.workspace = true
|
|
6
|
+
license.workspace = true
|
|
7
|
+
|
|
8
|
+
[lib]
|
|
9
|
+
crate-type = ["cdylib"]
|
|
10
|
+
|
|
11
|
+
[dependencies]
|
|
12
|
+
reactcut-core = { path = "../reactcut-core" }
|
|
13
|
+
napi = { version = "2", features = ["async", "serde-json"] }
|
|
14
|
+
napi-derive = "2"
|
|
15
|
+
serde = { workspace = true }
|
|
16
|
+
serde_json = { workspace = true }
|
|
17
|
+
|
|
18
|
+
[build-dependencies]
|
|
19
|
+
napi-build = "2"
|
package/build.rs
ADDED
package/index.d.ts
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/* tslint:disable */
|
|
2
|
+
/* eslint-disable */
|
|
3
|
+
|
|
4
|
+
/* auto-generated by NAPI-RS */
|
|
5
|
+
|
|
6
|
+
/** Structured validation error for JavaScript. */
|
|
7
|
+
export interface ValidationError {
|
|
8
|
+
code: string
|
|
9
|
+
message: string
|
|
10
|
+
path?: string
|
|
11
|
+
suggestion?: string
|
|
12
|
+
}
|
|
13
|
+
/** Validation result returned to JavaScript. */
|
|
14
|
+
export interface ValidationResult {
|
|
15
|
+
valid: boolean
|
|
16
|
+
errors: Array<ValidationError>
|
|
17
|
+
warnings: Array<ValidationError>
|
|
18
|
+
}
|
|
19
|
+
/** Validate a ReactCut project JSON string. */
|
|
20
|
+
export declare function validateProject(json: string): ValidationResult
|
|
21
|
+
/** Normalize a ReactCut project JSON string. */
|
|
22
|
+
export declare function normalizeProject(json: string): string
|
|
23
|
+
/** Computed animation style at a specific frame. */
|
|
24
|
+
export interface ComputedStyleResult {
|
|
25
|
+
x: number
|
|
26
|
+
y: number
|
|
27
|
+
width: number
|
|
28
|
+
height: number
|
|
29
|
+
opacity: number
|
|
30
|
+
rotation: number
|
|
31
|
+
scale: number
|
|
32
|
+
borderRadius: number
|
|
33
|
+
}
|
|
34
|
+
/** Get the computed animation style for a clip at a specific frame. */
|
|
35
|
+
export declare function getAnimationState(clipJson: string, frame: number): ComputedStyleResult
|
|
36
|
+
/** Interpolate between two values with easing. */
|
|
37
|
+
export declare function interpolate(from: number, to: number, progress: number, easing: string): number
|
package/index.js
ADDED
|
@@ -0,0 +1,318 @@
|
|
|
1
|
+
/* tslint:disable */
|
|
2
|
+
/* eslint-disable */
|
|
3
|
+
/* prettier-ignore */
|
|
4
|
+
|
|
5
|
+
/* auto-generated by NAPI-RS */
|
|
6
|
+
|
|
7
|
+
const { existsSync, readFileSync } = require('fs')
|
|
8
|
+
const { join } = require('path')
|
|
9
|
+
|
|
10
|
+
const { platform, arch } = process
|
|
11
|
+
|
|
12
|
+
let nativeBinding = null
|
|
13
|
+
let localFileExisted = false
|
|
14
|
+
let loadError = null
|
|
15
|
+
|
|
16
|
+
function isMusl() {
|
|
17
|
+
// For Node 10
|
|
18
|
+
if (!process.report || typeof process.report.getReport !== 'function') {
|
|
19
|
+
try {
|
|
20
|
+
const lddPath = require('child_process').execSync('which ldd').toString().trim()
|
|
21
|
+
return readFileSync(lddPath, 'utf8').includes('musl')
|
|
22
|
+
} catch (e) {
|
|
23
|
+
return true
|
|
24
|
+
}
|
|
25
|
+
} else {
|
|
26
|
+
const { glibcVersionRuntime } = process.report.getReport().header
|
|
27
|
+
return !glibcVersionRuntime
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
switch (platform) {
|
|
32
|
+
case 'android':
|
|
33
|
+
switch (arch) {
|
|
34
|
+
case 'arm64':
|
|
35
|
+
localFileExisted = existsSync(join(__dirname, 'reactcut.android-arm64.node'))
|
|
36
|
+
try {
|
|
37
|
+
if (localFileExisted) {
|
|
38
|
+
nativeBinding = require('./reactcut.android-arm64.node')
|
|
39
|
+
} else {
|
|
40
|
+
nativeBinding = require('reactcut-napi-android-arm64')
|
|
41
|
+
}
|
|
42
|
+
} catch (e) {
|
|
43
|
+
loadError = e
|
|
44
|
+
}
|
|
45
|
+
break
|
|
46
|
+
case 'arm':
|
|
47
|
+
localFileExisted = existsSync(join(__dirname, 'reactcut.android-arm-eabi.node'))
|
|
48
|
+
try {
|
|
49
|
+
if (localFileExisted) {
|
|
50
|
+
nativeBinding = require('./reactcut.android-arm-eabi.node')
|
|
51
|
+
} else {
|
|
52
|
+
nativeBinding = require('reactcut-napi-android-arm-eabi')
|
|
53
|
+
}
|
|
54
|
+
} catch (e) {
|
|
55
|
+
loadError = e
|
|
56
|
+
}
|
|
57
|
+
break
|
|
58
|
+
default:
|
|
59
|
+
throw new Error(`Unsupported architecture on Android ${arch}`)
|
|
60
|
+
}
|
|
61
|
+
break
|
|
62
|
+
case 'win32':
|
|
63
|
+
switch (arch) {
|
|
64
|
+
case 'x64':
|
|
65
|
+
localFileExisted = existsSync(
|
|
66
|
+
join(__dirname, 'reactcut.win32-x64-msvc.node')
|
|
67
|
+
)
|
|
68
|
+
try {
|
|
69
|
+
if (localFileExisted) {
|
|
70
|
+
nativeBinding = require('./reactcut.win32-x64-msvc.node')
|
|
71
|
+
} else {
|
|
72
|
+
nativeBinding = require('reactcut-napi-win32-x64-msvc')
|
|
73
|
+
}
|
|
74
|
+
} catch (e) {
|
|
75
|
+
loadError = e
|
|
76
|
+
}
|
|
77
|
+
break
|
|
78
|
+
case 'ia32':
|
|
79
|
+
localFileExisted = existsSync(
|
|
80
|
+
join(__dirname, 'reactcut.win32-ia32-msvc.node')
|
|
81
|
+
)
|
|
82
|
+
try {
|
|
83
|
+
if (localFileExisted) {
|
|
84
|
+
nativeBinding = require('./reactcut.win32-ia32-msvc.node')
|
|
85
|
+
} else {
|
|
86
|
+
nativeBinding = require('reactcut-napi-win32-ia32-msvc')
|
|
87
|
+
}
|
|
88
|
+
} catch (e) {
|
|
89
|
+
loadError = e
|
|
90
|
+
}
|
|
91
|
+
break
|
|
92
|
+
case 'arm64':
|
|
93
|
+
localFileExisted = existsSync(
|
|
94
|
+
join(__dirname, 'reactcut.win32-arm64-msvc.node')
|
|
95
|
+
)
|
|
96
|
+
try {
|
|
97
|
+
if (localFileExisted) {
|
|
98
|
+
nativeBinding = require('./reactcut.win32-arm64-msvc.node')
|
|
99
|
+
} else {
|
|
100
|
+
nativeBinding = require('reactcut-napi-win32-arm64-msvc')
|
|
101
|
+
}
|
|
102
|
+
} catch (e) {
|
|
103
|
+
loadError = e
|
|
104
|
+
}
|
|
105
|
+
break
|
|
106
|
+
default:
|
|
107
|
+
throw new Error(`Unsupported architecture on Windows: ${arch}`)
|
|
108
|
+
}
|
|
109
|
+
break
|
|
110
|
+
case 'darwin':
|
|
111
|
+
localFileExisted = existsSync(join(__dirname, 'reactcut.darwin-universal.node'))
|
|
112
|
+
try {
|
|
113
|
+
if (localFileExisted) {
|
|
114
|
+
nativeBinding = require('./reactcut.darwin-universal.node')
|
|
115
|
+
} else {
|
|
116
|
+
nativeBinding = require('reactcut-napi-darwin-universal')
|
|
117
|
+
}
|
|
118
|
+
break
|
|
119
|
+
} catch {}
|
|
120
|
+
switch (arch) {
|
|
121
|
+
case 'x64':
|
|
122
|
+
localFileExisted = existsSync(join(__dirname, 'reactcut.darwin-x64.node'))
|
|
123
|
+
try {
|
|
124
|
+
if (localFileExisted) {
|
|
125
|
+
nativeBinding = require('./reactcut.darwin-x64.node')
|
|
126
|
+
} else {
|
|
127
|
+
nativeBinding = require('reactcut-napi-darwin-x64')
|
|
128
|
+
}
|
|
129
|
+
} catch (e) {
|
|
130
|
+
loadError = e
|
|
131
|
+
}
|
|
132
|
+
break
|
|
133
|
+
case 'arm64':
|
|
134
|
+
localFileExisted = existsSync(
|
|
135
|
+
join(__dirname, 'reactcut.darwin-arm64.node')
|
|
136
|
+
)
|
|
137
|
+
try {
|
|
138
|
+
if (localFileExisted) {
|
|
139
|
+
nativeBinding = require('./reactcut.darwin-arm64.node')
|
|
140
|
+
} else {
|
|
141
|
+
nativeBinding = require('reactcut-napi-darwin-arm64')
|
|
142
|
+
}
|
|
143
|
+
} catch (e) {
|
|
144
|
+
loadError = e
|
|
145
|
+
}
|
|
146
|
+
break
|
|
147
|
+
default:
|
|
148
|
+
throw new Error(`Unsupported architecture on macOS: ${arch}`)
|
|
149
|
+
}
|
|
150
|
+
break
|
|
151
|
+
case 'freebsd':
|
|
152
|
+
if (arch !== 'x64') {
|
|
153
|
+
throw new Error(`Unsupported architecture on FreeBSD: ${arch}`)
|
|
154
|
+
}
|
|
155
|
+
localFileExisted = existsSync(join(__dirname, 'reactcut.freebsd-x64.node'))
|
|
156
|
+
try {
|
|
157
|
+
if (localFileExisted) {
|
|
158
|
+
nativeBinding = require('./reactcut.freebsd-x64.node')
|
|
159
|
+
} else {
|
|
160
|
+
nativeBinding = require('reactcut-napi-freebsd-x64')
|
|
161
|
+
}
|
|
162
|
+
} catch (e) {
|
|
163
|
+
loadError = e
|
|
164
|
+
}
|
|
165
|
+
break
|
|
166
|
+
case 'linux':
|
|
167
|
+
switch (arch) {
|
|
168
|
+
case 'x64':
|
|
169
|
+
if (isMusl()) {
|
|
170
|
+
localFileExisted = existsSync(
|
|
171
|
+
join(__dirname, 'reactcut.linux-x64-musl.node')
|
|
172
|
+
)
|
|
173
|
+
try {
|
|
174
|
+
if (localFileExisted) {
|
|
175
|
+
nativeBinding = require('./reactcut.linux-x64-musl.node')
|
|
176
|
+
} else {
|
|
177
|
+
nativeBinding = require('reactcut-napi-linux-x64-musl')
|
|
178
|
+
}
|
|
179
|
+
} catch (e) {
|
|
180
|
+
loadError = e
|
|
181
|
+
}
|
|
182
|
+
} else {
|
|
183
|
+
localFileExisted = existsSync(
|
|
184
|
+
join(__dirname, 'reactcut.linux-x64-gnu.node')
|
|
185
|
+
)
|
|
186
|
+
try {
|
|
187
|
+
if (localFileExisted) {
|
|
188
|
+
nativeBinding = require('./reactcut.linux-x64-gnu.node')
|
|
189
|
+
} else {
|
|
190
|
+
nativeBinding = require('reactcut-napi-linux-x64-gnu')
|
|
191
|
+
}
|
|
192
|
+
} catch (e) {
|
|
193
|
+
loadError = e
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
break
|
|
197
|
+
case 'arm64':
|
|
198
|
+
if (isMusl()) {
|
|
199
|
+
localFileExisted = existsSync(
|
|
200
|
+
join(__dirname, 'reactcut.linux-arm64-musl.node')
|
|
201
|
+
)
|
|
202
|
+
try {
|
|
203
|
+
if (localFileExisted) {
|
|
204
|
+
nativeBinding = require('./reactcut.linux-arm64-musl.node')
|
|
205
|
+
} else {
|
|
206
|
+
nativeBinding = require('reactcut-napi-linux-arm64-musl')
|
|
207
|
+
}
|
|
208
|
+
} catch (e) {
|
|
209
|
+
loadError = e
|
|
210
|
+
}
|
|
211
|
+
} else {
|
|
212
|
+
localFileExisted = existsSync(
|
|
213
|
+
join(__dirname, 'reactcut.linux-arm64-gnu.node')
|
|
214
|
+
)
|
|
215
|
+
try {
|
|
216
|
+
if (localFileExisted) {
|
|
217
|
+
nativeBinding = require('./reactcut.linux-arm64-gnu.node')
|
|
218
|
+
} else {
|
|
219
|
+
nativeBinding = require('reactcut-napi-linux-arm64-gnu')
|
|
220
|
+
}
|
|
221
|
+
} catch (e) {
|
|
222
|
+
loadError = e
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
break
|
|
226
|
+
case 'arm':
|
|
227
|
+
if (isMusl()) {
|
|
228
|
+
localFileExisted = existsSync(
|
|
229
|
+
join(__dirname, 'reactcut.linux-arm-musleabihf.node')
|
|
230
|
+
)
|
|
231
|
+
try {
|
|
232
|
+
if (localFileExisted) {
|
|
233
|
+
nativeBinding = require('./reactcut.linux-arm-musleabihf.node')
|
|
234
|
+
} else {
|
|
235
|
+
nativeBinding = require('reactcut-napi-linux-arm-musleabihf')
|
|
236
|
+
}
|
|
237
|
+
} catch (e) {
|
|
238
|
+
loadError = e
|
|
239
|
+
}
|
|
240
|
+
} else {
|
|
241
|
+
localFileExisted = existsSync(
|
|
242
|
+
join(__dirname, 'reactcut.linux-arm-gnueabihf.node')
|
|
243
|
+
)
|
|
244
|
+
try {
|
|
245
|
+
if (localFileExisted) {
|
|
246
|
+
nativeBinding = require('./reactcut.linux-arm-gnueabihf.node')
|
|
247
|
+
} else {
|
|
248
|
+
nativeBinding = require('reactcut-napi-linux-arm-gnueabihf')
|
|
249
|
+
}
|
|
250
|
+
} catch (e) {
|
|
251
|
+
loadError = e
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
break
|
|
255
|
+
case 'riscv64':
|
|
256
|
+
if (isMusl()) {
|
|
257
|
+
localFileExisted = existsSync(
|
|
258
|
+
join(__dirname, 'reactcut.linux-riscv64-musl.node')
|
|
259
|
+
)
|
|
260
|
+
try {
|
|
261
|
+
if (localFileExisted) {
|
|
262
|
+
nativeBinding = require('./reactcut.linux-riscv64-musl.node')
|
|
263
|
+
} else {
|
|
264
|
+
nativeBinding = require('reactcut-napi-linux-riscv64-musl')
|
|
265
|
+
}
|
|
266
|
+
} catch (e) {
|
|
267
|
+
loadError = e
|
|
268
|
+
}
|
|
269
|
+
} else {
|
|
270
|
+
localFileExisted = existsSync(
|
|
271
|
+
join(__dirname, 'reactcut.linux-riscv64-gnu.node')
|
|
272
|
+
)
|
|
273
|
+
try {
|
|
274
|
+
if (localFileExisted) {
|
|
275
|
+
nativeBinding = require('./reactcut.linux-riscv64-gnu.node')
|
|
276
|
+
} else {
|
|
277
|
+
nativeBinding = require('reactcut-napi-linux-riscv64-gnu')
|
|
278
|
+
}
|
|
279
|
+
} catch (e) {
|
|
280
|
+
loadError = e
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
break
|
|
284
|
+
case 's390x':
|
|
285
|
+
localFileExisted = existsSync(
|
|
286
|
+
join(__dirname, 'reactcut.linux-s390x-gnu.node')
|
|
287
|
+
)
|
|
288
|
+
try {
|
|
289
|
+
if (localFileExisted) {
|
|
290
|
+
nativeBinding = require('./reactcut.linux-s390x-gnu.node')
|
|
291
|
+
} else {
|
|
292
|
+
nativeBinding = require('reactcut-napi-linux-s390x-gnu')
|
|
293
|
+
}
|
|
294
|
+
} catch (e) {
|
|
295
|
+
loadError = e
|
|
296
|
+
}
|
|
297
|
+
break
|
|
298
|
+
default:
|
|
299
|
+
throw new Error(`Unsupported architecture on Linux: ${arch}`)
|
|
300
|
+
}
|
|
301
|
+
break
|
|
302
|
+
default:
|
|
303
|
+
throw new Error(`Unsupported OS: ${platform}, architecture: ${arch}`)
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
if (!nativeBinding) {
|
|
307
|
+
if (loadError) {
|
|
308
|
+
throw loadError
|
|
309
|
+
}
|
|
310
|
+
throw new Error(`Failed to load native binding`)
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
const { validateProject, normalizeProject, getAnimationState, interpolate } = nativeBinding
|
|
314
|
+
|
|
315
|
+
module.exports.validateProject = validateProject
|
|
316
|
+
module.exports.normalizeProject = normalizeProject
|
|
317
|
+
module.exports.getAnimationState = getAnimationState
|
|
318
|
+
module.exports.interpolate = interpolate
|
package/package.json
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "reactcut-napi",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"main": "index.js",
|
|
5
|
+
"types": "index.d.ts",
|
|
6
|
+
"napi": {
|
|
7
|
+
"name": "reactcut",
|
|
8
|
+
"package": {
|
|
9
|
+
"name": "reactcut-napi"
|
|
10
|
+
}
|
|
11
|
+
},
|
|
12
|
+
"scripts": {
|
|
13
|
+
"build": "napi build --platform --release",
|
|
14
|
+
"build:debug": "napi build --platform"
|
|
15
|
+
},
|
|
16
|
+
"devDependencies": {
|
|
17
|
+
"@napi-rs/cli": "^2.18.0"
|
|
18
|
+
}
|
|
19
|
+
}
|
|
Binary file
|
|
Binary file
|
package/src/lib.rs
ADDED
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
//! Node.js native bindings for ReactCut (via napi-rs).
|
|
2
|
+
//!
|
|
3
|
+
//! These functions are callable from Node.js/TypeScript on the server side.
|
|
4
|
+
//! Used primarily for video export (FFmpeg pipeline).
|
|
5
|
+
|
|
6
|
+
use napi::bindgen_prelude::*;
|
|
7
|
+
use napi_derive::napi;
|
|
8
|
+
use reactcut_core::timeline::{schema::ReactCutProject, validate, normalize};
|
|
9
|
+
use reactcut_core::animation::interpolate::compute_style_at_frame;
|
|
10
|
+
|
|
11
|
+
/// Structured validation error for JavaScript.
|
|
12
|
+
#[napi(object)]
|
|
13
|
+
pub struct ValidationError {
|
|
14
|
+
pub code: String,
|
|
15
|
+
pub message: String,
|
|
16
|
+
pub path: Option<String>,
|
|
17
|
+
pub suggestion: Option<String>,
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/// Validation result returned to JavaScript.
|
|
21
|
+
#[napi(object)]
|
|
22
|
+
pub struct ValidationResult {
|
|
23
|
+
pub valid: bool,
|
|
24
|
+
pub errors: Vec<ValidationError>,
|
|
25
|
+
pub warnings: Vec<ValidationError>,
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/// Validate a ReactCut project JSON string.
|
|
29
|
+
#[napi]
|
|
30
|
+
pub fn validate_project(json: String) -> Result<ValidationResult> {
|
|
31
|
+
let project: ReactCutProject = serde_json::from_str(&json)
|
|
32
|
+
.map_err(|e| Error::from_reason(format!("Invalid JSON: {}", e)))?;
|
|
33
|
+
let result = validate::validate_project(&project);
|
|
34
|
+
|
|
35
|
+
let errors = result.errors.into_iter().map(|e| ValidationError {
|
|
36
|
+
code: e.code,
|
|
37
|
+
message: e.message,
|
|
38
|
+
path: e.path,
|
|
39
|
+
suggestion: e.suggestion,
|
|
40
|
+
}).collect();
|
|
41
|
+
|
|
42
|
+
let warnings = result.warnings.into_iter().map(|w| ValidationError {
|
|
43
|
+
code: w.code,
|
|
44
|
+
message: w.message,
|
|
45
|
+
path: w.path,
|
|
46
|
+
suggestion: w.suggestion,
|
|
47
|
+
}).collect();
|
|
48
|
+
|
|
49
|
+
Ok(ValidationResult {
|
|
50
|
+
valid: result.valid,
|
|
51
|
+
errors,
|
|
52
|
+
warnings,
|
|
53
|
+
})
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/// Normalize a ReactCut project JSON string.
|
|
57
|
+
#[napi]
|
|
58
|
+
pub fn normalize_project(json: String) -> Result<String> {
|
|
59
|
+
let mut project: ReactCutProject = serde_json::from_str(&json)
|
|
60
|
+
.map_err(|e| Error::from_reason(format!("Invalid JSON: {}", e)))?;
|
|
61
|
+
normalize::normalize_project(&mut project);
|
|
62
|
+
serde_json::to_string_pretty(&project)
|
|
63
|
+
.map_err(|e| Error::from_reason(format!("Serialization error: {}", e)))
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/// Computed animation style at a specific frame.
|
|
67
|
+
#[napi(object)]
|
|
68
|
+
pub struct ComputedStyleResult {
|
|
69
|
+
pub x: f64,
|
|
70
|
+
pub y: f64,
|
|
71
|
+
pub width: f64,
|
|
72
|
+
pub height: f64,
|
|
73
|
+
pub opacity: f64,
|
|
74
|
+
pub rotation: f64,
|
|
75
|
+
pub scale: f64,
|
|
76
|
+
pub border_radius: f64,
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/// Get the computed animation style for a clip at a specific frame.
|
|
80
|
+
#[napi]
|
|
81
|
+
pub fn get_animation_state(clip_json: String, frame: u32) -> Result<ComputedStyleResult> {
|
|
82
|
+
let clip: reactcut_core::Clip = serde_json::from_str(&clip_json)
|
|
83
|
+
.map_err(|e| Error::from_reason(format!("Invalid clip JSON: {}", e)))?;
|
|
84
|
+
let style = compute_style_at_frame(&clip, frame);
|
|
85
|
+
Ok(ComputedStyleResult {
|
|
86
|
+
x: style.x,
|
|
87
|
+
y: style.y,
|
|
88
|
+
width: style.width,
|
|
89
|
+
height: style.height,
|
|
90
|
+
opacity: style.opacity,
|
|
91
|
+
rotation: style.rotation,
|
|
92
|
+
scale: style.scale,
|
|
93
|
+
border_radius: style.border_radius,
|
|
94
|
+
})
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/// Interpolate between two values with easing.
|
|
98
|
+
#[napi]
|
|
99
|
+
pub fn interpolate(from: f64, to: f64, progress: f64, easing: String) -> f64 {
|
|
100
|
+
use reactcut_core::animation::easing::apply_easing;
|
|
101
|
+
use reactcut_core::timeline::schema::EasingType;
|
|
102
|
+
|
|
103
|
+
let easing_type = match easing.as_str() {
|
|
104
|
+
"linear" => EasingType::Linear,
|
|
105
|
+
"easeIn" => EasingType::EaseIn,
|
|
106
|
+
"easeOut" => EasingType::EaseOut,
|
|
107
|
+
"easeInOut" => EasingType::EaseInOut,
|
|
108
|
+
"spring" => EasingType::Spring,
|
|
109
|
+
_ => EasingType::EaseInOut,
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
let eased = apply_easing(easing_type, progress);
|
|
113
|
+
reactcut_core::animation::interpolate::lerp(from, to, eased)
|
|
114
|
+
}
|