react-native-ease 0.2.0 → 0.4.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/.claude-plugin/marketplace.json +21 -0
- package/.claude-plugin/plugin.json +5 -0
- package/README.md +218 -74
- package/android/src/main/java/com/ease/EaseView.kt +275 -78
- package/android/src/main/java/com/ease/EaseViewManager.kt +5 -44
- package/ios/EaseView.mm +277 -76
- package/lib/module/EaseView.js +85 -26
- package/lib/module/EaseView.js.map +1 -1
- package/lib/module/EaseView.web.js +351 -0
- package/lib/module/EaseView.web.js.map +1 -0
- package/lib/module/EaseViewNativeComponent.ts +24 -15
- package/lib/module/index.js +1 -1
- package/lib/module/index.js.map +1 -1
- package/lib/typescript/src/EaseView.d.ts +2 -0
- package/lib/typescript/src/EaseView.d.ts.map +1 -1
- package/lib/typescript/src/EaseView.web.d.ts +16 -0
- package/lib/typescript/src/EaseView.web.d.ts.map +1 -0
- package/lib/typescript/src/EaseViewNativeComponent.d.ts +20 -7
- package/lib/typescript/src/EaseViewNativeComponent.d.ts.map +1 -1
- package/lib/typescript/src/index.d.ts +1 -1
- package/lib/typescript/src/index.d.ts.map +1 -1
- package/lib/typescript/src/types.d.ts +21 -2
- package/lib/typescript/src/types.d.ts.map +1 -1
- package/package.json +7 -5
- package/skills/react-native-ease-refactor/SKILL.md +405 -0
- package/src/EaseView.tsx +116 -48
- package/src/EaseView.web.tsx +462 -0
- package/src/EaseViewNativeComponent.ts +24 -15
- package/src/index.tsx +2 -0
- package/src/types.ts +26 -2
|
@@ -0,0 +1,462 @@
|
|
|
1
|
+
import React, { useEffect, useRef, useState, useCallback } from 'react';
|
|
2
|
+
import { View, type ViewStyle, type StyleProp } from 'react-native';
|
|
3
|
+
import type {
|
|
4
|
+
AnimateProps,
|
|
5
|
+
CubicBezier,
|
|
6
|
+
SingleTransition,
|
|
7
|
+
Transition,
|
|
8
|
+
TransitionEndEvent,
|
|
9
|
+
TransformOrigin,
|
|
10
|
+
} from './types';
|
|
11
|
+
|
|
12
|
+
/** Identity values used as defaults for animate/initialAnimate. */
|
|
13
|
+
const IDENTITY: Required<Omit<AnimateProps, 'scale' | 'backgroundColor'>> = {
|
|
14
|
+
opacity: 1,
|
|
15
|
+
translateX: 0,
|
|
16
|
+
translateY: 0,
|
|
17
|
+
scaleX: 1,
|
|
18
|
+
scaleY: 1,
|
|
19
|
+
rotate: 0,
|
|
20
|
+
rotateX: 0,
|
|
21
|
+
rotateY: 0,
|
|
22
|
+
borderRadius: 0,
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
/** Preset easing curves as cubic bezier control points. */
|
|
26
|
+
const EASING_PRESETS: Record<string, CubicBezier> = {
|
|
27
|
+
linear: [0, 0, 1, 1],
|
|
28
|
+
easeIn: [0.42, 0, 1, 1],
|
|
29
|
+
easeOut: [0, 0, 0.58, 1],
|
|
30
|
+
easeInOut: [0.42, 0, 0.58, 1],
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
// ---------------------------------------------------------------------------
|
|
34
|
+
// Spring simulation → CSS linear() easing
|
|
35
|
+
// ---------------------------------------------------------------------------
|
|
36
|
+
|
|
37
|
+
/** Simulate a damped spring from 0 → 1 and return settling duration + sample points. */
|
|
38
|
+
function simulateSpring(
|
|
39
|
+
damping: number,
|
|
40
|
+
stiffness: number,
|
|
41
|
+
mass: number,
|
|
42
|
+
): { durationMs: number; points: number[] } {
|
|
43
|
+
const dt = 1 / 120; // 120 Hz simulation
|
|
44
|
+
const maxTime = 10; // 10s safety cap
|
|
45
|
+
let x = 0;
|
|
46
|
+
let v = 0;
|
|
47
|
+
const samples: number[] = [0];
|
|
48
|
+
let step = 0;
|
|
49
|
+
|
|
50
|
+
while (step * dt < maxTime) {
|
|
51
|
+
const a = (-stiffness * (x - 1) - damping * v) / mass;
|
|
52
|
+
v += a * dt;
|
|
53
|
+
x += v * dt;
|
|
54
|
+
step++;
|
|
55
|
+
// Downsample to ~60 fps (every 2nd sample)
|
|
56
|
+
if (step % 2 === 0) {
|
|
57
|
+
samples.push(Math.round(x * 10000) / 10000);
|
|
58
|
+
}
|
|
59
|
+
// Settled?
|
|
60
|
+
if (Math.abs(x - 1) < 0.001 && Math.abs(v) < 0.001) break;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Ensure last point is exactly 1
|
|
64
|
+
samples[samples.length - 1] = 1;
|
|
65
|
+
|
|
66
|
+
return {
|
|
67
|
+
durationMs: Math.round(step * dt * 1000),
|
|
68
|
+
points: samples,
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** Cache for computed spring easing strings (keyed by damping-stiffness-mass). */
|
|
73
|
+
const springCache = new Map<string, { duration: number; easing: string }>();
|
|
74
|
+
|
|
75
|
+
function getSpringEasing(
|
|
76
|
+
damping: number,
|
|
77
|
+
stiffness: number,
|
|
78
|
+
mass: number,
|
|
79
|
+
): { duration: number; easing: string } {
|
|
80
|
+
const key = `${damping}-${stiffness}-${mass}`;
|
|
81
|
+
let cached = springCache.get(key);
|
|
82
|
+
if (cached) return cached;
|
|
83
|
+
|
|
84
|
+
const { durationMs, points } = simulateSpring(damping, stiffness, mass);
|
|
85
|
+
const easing = `linear(${points.join(', ')})`;
|
|
86
|
+
cached = { duration: durationMs, easing };
|
|
87
|
+
springCache.set(key, cached);
|
|
88
|
+
return cached;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** Detect CSS linear() support (lazy, cached). */
|
|
92
|
+
let linearSupported: boolean | null = null;
|
|
93
|
+
function supportsLinearEasing(): boolean {
|
|
94
|
+
if (linearSupported != null) return linearSupported;
|
|
95
|
+
try {
|
|
96
|
+
const el = document.createElement('div');
|
|
97
|
+
el.style.transitionTimingFunction = 'linear(0, 1)';
|
|
98
|
+
linearSupported = el.style.transitionTimingFunction !== '';
|
|
99
|
+
} catch {
|
|
100
|
+
linearSupported = false;
|
|
101
|
+
}
|
|
102
|
+
return linearSupported;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const SPRING_FALLBACK_EASING = 'cubic-bezier(0.25, 0.46, 0.45, 0.94)';
|
|
106
|
+
|
|
107
|
+
export type EaseViewProps = {
|
|
108
|
+
animate?: AnimateProps;
|
|
109
|
+
initialAnimate?: AnimateProps;
|
|
110
|
+
transition?: Transition;
|
|
111
|
+
onTransitionEnd?: (event: TransitionEndEvent) => void;
|
|
112
|
+
/** No-op on web. */
|
|
113
|
+
useHardwareLayer?: boolean;
|
|
114
|
+
transformOrigin?: TransformOrigin;
|
|
115
|
+
style?: StyleProp<ViewStyle>;
|
|
116
|
+
children?: React.ReactNode;
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
function resolveAnimateValues(props: AnimateProps | undefined): Required<
|
|
120
|
+
Omit<AnimateProps, 'scale' | 'backgroundColor'>
|
|
121
|
+
> & {
|
|
122
|
+
backgroundColor?: string;
|
|
123
|
+
} {
|
|
124
|
+
return {
|
|
125
|
+
...IDENTITY,
|
|
126
|
+
...props,
|
|
127
|
+
scaleX: props?.scaleX ?? props?.scale ?? IDENTITY.scaleX,
|
|
128
|
+
scaleY: props?.scaleY ?? props?.scale ?? IDENTITY.scaleY,
|
|
129
|
+
rotateX: props?.rotateX ?? IDENTITY.rotateX,
|
|
130
|
+
rotateY: props?.rotateY ?? IDENTITY.rotateY,
|
|
131
|
+
backgroundColor: props?.backgroundColor as string | undefined,
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function buildTransform(vals: ReturnType<typeof resolveAnimateValues>): string {
|
|
136
|
+
const parts: string[] = [];
|
|
137
|
+
if (vals.translateX !== 0 || vals.translateY !== 0) {
|
|
138
|
+
parts.push(`translate(${vals.translateX}px, ${vals.translateY}px)`);
|
|
139
|
+
}
|
|
140
|
+
if (vals.scaleX !== 1 || vals.scaleY !== 1) {
|
|
141
|
+
parts.push(
|
|
142
|
+
vals.scaleX === vals.scaleY
|
|
143
|
+
? `scale(${vals.scaleX})`
|
|
144
|
+
: `scale(${vals.scaleX}, ${vals.scaleY})`,
|
|
145
|
+
);
|
|
146
|
+
}
|
|
147
|
+
if (vals.rotate !== 0) {
|
|
148
|
+
parts.push(`rotate(${vals.rotate}deg)`);
|
|
149
|
+
}
|
|
150
|
+
if (vals.rotateX !== 0) {
|
|
151
|
+
parts.push(`rotateX(${vals.rotateX}deg)`);
|
|
152
|
+
}
|
|
153
|
+
if (vals.rotateY !== 0) {
|
|
154
|
+
parts.push(`rotateY(${vals.rotateY}deg)`);
|
|
155
|
+
}
|
|
156
|
+
return parts.length > 0 ? parts.join(' ') : 'none';
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/** Returns true if the transition is a SingleTransition (has a `type` field). */
|
|
160
|
+
function isSingleTransition(t: Transition): t is SingleTransition {
|
|
161
|
+
return 'type' in t;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/** Resolve a single config into CSS-ready duration/easing. */
|
|
165
|
+
function resolveConfigForCss(config: SingleTransition | undefined): {
|
|
166
|
+
duration: number;
|
|
167
|
+
easing: string;
|
|
168
|
+
type: string;
|
|
169
|
+
} {
|
|
170
|
+
if (!config || config.type === 'none') {
|
|
171
|
+
return { duration: 0, easing: 'linear', type: config?.type ?? 'timing' };
|
|
172
|
+
}
|
|
173
|
+
return {
|
|
174
|
+
duration: resolveDuration(config),
|
|
175
|
+
easing: resolveEasing(config),
|
|
176
|
+
type: config.type,
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/** CSS property names for each category. */
|
|
181
|
+
const CSS_PROP_MAP = {
|
|
182
|
+
opacity: 'opacity',
|
|
183
|
+
transform: 'transform',
|
|
184
|
+
borderRadius: 'border-radius',
|
|
185
|
+
backgroundColor: 'background-color',
|
|
186
|
+
} as const;
|
|
187
|
+
|
|
188
|
+
type CategoryKey = keyof typeof CSS_PROP_MAP;
|
|
189
|
+
|
|
190
|
+
/** Resolve transition prop into per-category CSS configs. */
|
|
191
|
+
function resolvePerCategoryConfigs(
|
|
192
|
+
transition: Transition | undefined,
|
|
193
|
+
): Record<CategoryKey, { duration: number; easing: string; type: string }> {
|
|
194
|
+
if (!transition) {
|
|
195
|
+
const def = resolveConfigForCss(undefined);
|
|
196
|
+
return {
|
|
197
|
+
opacity: def,
|
|
198
|
+
transform: def,
|
|
199
|
+
borderRadius: def,
|
|
200
|
+
backgroundColor: def,
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
if (isSingleTransition(transition)) {
|
|
204
|
+
const def = resolveConfigForCss(transition);
|
|
205
|
+
return {
|
|
206
|
+
opacity: def,
|
|
207
|
+
transform: def,
|
|
208
|
+
borderRadius: def,
|
|
209
|
+
backgroundColor: def,
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
// TransitionMap
|
|
213
|
+
const defaultConfig = resolveConfigForCss(transition.default);
|
|
214
|
+
return {
|
|
215
|
+
opacity: transition.opacity
|
|
216
|
+
? resolveConfigForCss(transition.opacity)
|
|
217
|
+
: defaultConfig,
|
|
218
|
+
transform: transition.transform
|
|
219
|
+
? resolveConfigForCss(transition.transform)
|
|
220
|
+
: defaultConfig,
|
|
221
|
+
borderRadius: transition.borderRadius
|
|
222
|
+
? resolveConfigForCss(transition.borderRadius)
|
|
223
|
+
: defaultConfig,
|
|
224
|
+
backgroundColor: transition.backgroundColor
|
|
225
|
+
? resolveConfigForCss(transition.backgroundColor)
|
|
226
|
+
: defaultConfig,
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
function resolveEasing(transition: SingleTransition | undefined): string {
|
|
231
|
+
if (!transition || transition.type === 'none') {
|
|
232
|
+
return 'linear';
|
|
233
|
+
}
|
|
234
|
+
if (transition.type === 'spring') {
|
|
235
|
+
const d = transition.damping ?? 15;
|
|
236
|
+
const s = transition.stiffness ?? 120;
|
|
237
|
+
const m = transition.mass ?? 1;
|
|
238
|
+
if (supportsLinearEasing()) {
|
|
239
|
+
return getSpringEasing(d, s, m).easing;
|
|
240
|
+
}
|
|
241
|
+
return SPRING_FALLBACK_EASING;
|
|
242
|
+
}
|
|
243
|
+
// timing
|
|
244
|
+
const easing = transition.easing ?? 'easeInOut';
|
|
245
|
+
const bezier: CubicBezier = Array.isArray(easing)
|
|
246
|
+
? easing
|
|
247
|
+
: EASING_PRESETS[easing]!;
|
|
248
|
+
return `cubic-bezier(${bezier[0]}, ${bezier[1]}, ${bezier[2]}, ${bezier[3]})`;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
function resolveDuration(transition: SingleTransition | undefined): number {
|
|
252
|
+
if (!transition) return 300;
|
|
253
|
+
if (transition.type === 'timing') return transition.duration ?? 300;
|
|
254
|
+
if (transition.type === 'none') return 0;
|
|
255
|
+
// Spring: use simulation-derived duration (incorporates stiffness)
|
|
256
|
+
const d = transition.damping ?? 15;
|
|
257
|
+
const s = transition.stiffness ?? 120;
|
|
258
|
+
const m = transition.mass ?? 1;
|
|
259
|
+
return getSpringEasing(d, s, m).duration;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/** Counter for unique keyframe names. */
|
|
263
|
+
let keyframeCounter = 0;
|
|
264
|
+
|
|
265
|
+
export function EaseView({
|
|
266
|
+
animate,
|
|
267
|
+
initialAnimate,
|
|
268
|
+
transition,
|
|
269
|
+
onTransitionEnd,
|
|
270
|
+
useHardwareLayer: _useHardwareLayer,
|
|
271
|
+
transformOrigin,
|
|
272
|
+
style,
|
|
273
|
+
children,
|
|
274
|
+
}: EaseViewProps) {
|
|
275
|
+
const resolved = resolveAnimateValues(animate);
|
|
276
|
+
const hasInitial = initialAnimate != null;
|
|
277
|
+
const [mounted, setMounted] = useState(!hasInitial);
|
|
278
|
+
// On web, View ref gives us the underlying DOM element.
|
|
279
|
+
const viewRef = useRef<React.ComponentRef<typeof View>>(null);
|
|
280
|
+
const animationNameRef = useRef<string | null>(null);
|
|
281
|
+
|
|
282
|
+
const getElement = useCallback(
|
|
283
|
+
() => viewRef.current as unknown as HTMLElement | null,
|
|
284
|
+
[],
|
|
285
|
+
);
|
|
286
|
+
|
|
287
|
+
// For initialAnimate: render initial values first, then animate on mount.
|
|
288
|
+
useEffect(() => {
|
|
289
|
+
if (hasInitial) {
|
|
290
|
+
getElement()?.getBoundingClientRect();
|
|
291
|
+
setMounted(true);
|
|
292
|
+
}
|
|
293
|
+
}, []); // eslint-disable-line react-hooks/exhaustive-deps
|
|
294
|
+
|
|
295
|
+
const displayValues =
|
|
296
|
+
!mounted && hasInitial ? resolveAnimateValues(initialAnimate) : resolved;
|
|
297
|
+
|
|
298
|
+
const categoryConfigs = resolvePerCategoryConfigs(transition);
|
|
299
|
+
|
|
300
|
+
// For loop mode, use the default/single transition config
|
|
301
|
+
const singleTransition =
|
|
302
|
+
transition && isSingleTransition(transition)
|
|
303
|
+
? transition
|
|
304
|
+
: transition && !isSingleTransition(transition)
|
|
305
|
+
? transition.default
|
|
306
|
+
: undefined;
|
|
307
|
+
const loopMode =
|
|
308
|
+
singleTransition?.type === 'timing' ? singleTransition.loop : undefined;
|
|
309
|
+
const loopDuration = resolveDuration(singleTransition);
|
|
310
|
+
const loopEasing = resolveEasing(singleTransition);
|
|
311
|
+
|
|
312
|
+
const originX = ((transformOrigin?.x ?? 0.5) * 100).toFixed(1);
|
|
313
|
+
const originY = ((transformOrigin?.y ?? 0.5) * 100).toFixed(1);
|
|
314
|
+
|
|
315
|
+
const transitionCss =
|
|
316
|
+
!mounted && hasInitial
|
|
317
|
+
? 'none'
|
|
318
|
+
: (Object.keys(CSS_PROP_MAP) as CategoryKey[])
|
|
319
|
+
.filter((key) => {
|
|
320
|
+
const cfg = categoryConfigs[key];
|
|
321
|
+
return cfg.type !== 'none' && cfg.duration > 0;
|
|
322
|
+
})
|
|
323
|
+
.map((key) => {
|
|
324
|
+
const cfg = categoryConfigs[key];
|
|
325
|
+
return `${CSS_PROP_MAP[key]} ${cfg.duration}ms ${cfg.easing}`;
|
|
326
|
+
})
|
|
327
|
+
.join(', ') || 'none';
|
|
328
|
+
|
|
329
|
+
// Apply CSS transition/animation properties imperatively (not in RN style spec).
|
|
330
|
+
useEffect(() => {
|
|
331
|
+
const el = getElement();
|
|
332
|
+
if (!el) return;
|
|
333
|
+
|
|
334
|
+
if (!loopMode) {
|
|
335
|
+
el.style.transition = transitionCss;
|
|
336
|
+
}
|
|
337
|
+
el.style.transformOrigin = `${originX}% ${originY}%`;
|
|
338
|
+
});
|
|
339
|
+
|
|
340
|
+
// Handle transitionend event via DOM listener.
|
|
341
|
+
useEffect(() => {
|
|
342
|
+
const el = getElement();
|
|
343
|
+
if (!el || !onTransitionEnd) return;
|
|
344
|
+
|
|
345
|
+
const handler = (e: TransitionEvent) => {
|
|
346
|
+
if (e.target !== e.currentTarget) return;
|
|
347
|
+
if (e.propertyName !== 'opacity' && e.propertyName !== 'transform')
|
|
348
|
+
return;
|
|
349
|
+
onTransitionEnd({ finished: true });
|
|
350
|
+
};
|
|
351
|
+
|
|
352
|
+
el.addEventListener('transitionend', handler);
|
|
353
|
+
return () => el.removeEventListener('transitionend', handler);
|
|
354
|
+
}, [onTransitionEnd, getElement]);
|
|
355
|
+
|
|
356
|
+
// Handle loop animations via CSS @keyframes.
|
|
357
|
+
useEffect(() => {
|
|
358
|
+
const el = getElement();
|
|
359
|
+
if (!loopMode || !el) {
|
|
360
|
+
if (animationNameRef.current) {
|
|
361
|
+
const cleanEl = getElement();
|
|
362
|
+
if (cleanEl) cleanEl.style.animation = '';
|
|
363
|
+
animationNameRef.current = null;
|
|
364
|
+
}
|
|
365
|
+
return;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
const fromValues = initialAnimate
|
|
369
|
+
? resolveAnimateValues(initialAnimate)
|
|
370
|
+
: resolveAnimateValues(undefined);
|
|
371
|
+
const toValues = resolveAnimateValues(animate);
|
|
372
|
+
|
|
373
|
+
const fromTransform = buildTransform(fromValues);
|
|
374
|
+
const toTransform = buildTransform(toValues);
|
|
375
|
+
|
|
376
|
+
const name = `ease-loop-${++keyframeCounter}`;
|
|
377
|
+
animationNameRef.current = name;
|
|
378
|
+
|
|
379
|
+
// Only include border-radius/background-color in keyframes when explicitly
|
|
380
|
+
// set by the user, to avoid overriding values from the style prop.
|
|
381
|
+
const hasBorderRadius =
|
|
382
|
+
initialAnimate?.borderRadius != null || animate?.borderRadius != null;
|
|
383
|
+
const hasBgColor =
|
|
384
|
+
initialAnimate?.backgroundColor != null ||
|
|
385
|
+
animate?.backgroundColor != null;
|
|
386
|
+
|
|
387
|
+
const fromBlock = [
|
|
388
|
+
`opacity: ${fromValues.opacity}`,
|
|
389
|
+
`transform: ${fromTransform}`,
|
|
390
|
+
hasBorderRadius ? `border-radius: ${fromValues.borderRadius}px` : '',
|
|
391
|
+
hasBgColor && fromValues.backgroundColor
|
|
392
|
+
? `background-color: ${fromValues.backgroundColor}`
|
|
393
|
+
: '',
|
|
394
|
+
]
|
|
395
|
+
.filter(Boolean)
|
|
396
|
+
.join('; ');
|
|
397
|
+
|
|
398
|
+
const toBlock = [
|
|
399
|
+
`opacity: ${toValues.opacity}`,
|
|
400
|
+
`transform: ${toTransform}`,
|
|
401
|
+
hasBorderRadius ? `border-radius: ${toValues.borderRadius}px` : '',
|
|
402
|
+
hasBgColor && toValues.backgroundColor
|
|
403
|
+
? `background-color: ${toValues.backgroundColor}`
|
|
404
|
+
: '',
|
|
405
|
+
]
|
|
406
|
+
.filter(Boolean)
|
|
407
|
+
.join('; ');
|
|
408
|
+
|
|
409
|
+
const keyframes = `@keyframes ${name} { from { ${fromBlock} } to { ${toBlock} } }`;
|
|
410
|
+
|
|
411
|
+
const styleEl = document.createElement('style');
|
|
412
|
+
styleEl.textContent = keyframes;
|
|
413
|
+
document.head.appendChild(styleEl);
|
|
414
|
+
|
|
415
|
+
const direction = loopMode === 'reverse' ? 'alternate' : 'normal';
|
|
416
|
+
el.style.transition = 'none';
|
|
417
|
+
el.style.animation = `${name} ${loopDuration}ms ${loopEasing} infinite ${direction}`;
|
|
418
|
+
|
|
419
|
+
return () => {
|
|
420
|
+
styleEl.remove();
|
|
421
|
+
el.style.animation = '';
|
|
422
|
+
animationNameRef.current = null;
|
|
423
|
+
};
|
|
424
|
+
}, [loopMode, animate, initialAnimate, loopDuration, loopEasing, getElement]);
|
|
425
|
+
|
|
426
|
+
// Build animated style using RN transform array format.
|
|
427
|
+
// react-native-web converts these to CSS transform strings.
|
|
428
|
+
const animatedStyle: ViewStyle = {
|
|
429
|
+
opacity: displayValues.opacity,
|
|
430
|
+
transform: [
|
|
431
|
+
...(displayValues.translateX !== 0
|
|
432
|
+
? [{ translateX: displayValues.translateX }]
|
|
433
|
+
: []),
|
|
434
|
+
...(displayValues.translateY !== 0
|
|
435
|
+
? [{ translateY: displayValues.translateY }]
|
|
436
|
+
: []),
|
|
437
|
+
...(displayValues.scaleX !== 1 ? [{ scaleX: displayValues.scaleX }] : []),
|
|
438
|
+
...(displayValues.scaleY !== 1 ? [{ scaleY: displayValues.scaleY }] : []),
|
|
439
|
+
...(displayValues.rotate !== 0
|
|
440
|
+
? [{ rotate: `${displayValues.rotate}deg` }]
|
|
441
|
+
: []),
|
|
442
|
+
...(displayValues.rotateX !== 0
|
|
443
|
+
? [{ rotateX: `${displayValues.rotateX}deg` }]
|
|
444
|
+
: []),
|
|
445
|
+
...(displayValues.rotateY !== 0
|
|
446
|
+
? [{ rotateY: `${displayValues.rotateY}deg` }]
|
|
447
|
+
: []),
|
|
448
|
+
],
|
|
449
|
+
...(displayValues.borderRadius > 0
|
|
450
|
+
? { borderRadius: displayValues.borderRadius }
|
|
451
|
+
: {}),
|
|
452
|
+
...(displayValues.backgroundColor
|
|
453
|
+
? { backgroundColor: displayValues.backgroundColor }
|
|
454
|
+
: {}),
|
|
455
|
+
};
|
|
456
|
+
|
|
457
|
+
return (
|
|
458
|
+
<View ref={viewRef} style={[style, animatedStyle]}>
|
|
459
|
+
{children}
|
|
460
|
+
</View>
|
|
461
|
+
);
|
|
462
|
+
}
|
|
@@ -6,6 +6,28 @@ import {
|
|
|
6
6
|
type ColorValue,
|
|
7
7
|
} from 'react-native';
|
|
8
8
|
|
|
9
|
+
type Float = CodegenTypes.Float;
|
|
10
|
+
type Int32 = CodegenTypes.Int32;
|
|
11
|
+
|
|
12
|
+
type NativeTransitionConfig = Readonly<{
|
|
13
|
+
type: string;
|
|
14
|
+
duration: Int32;
|
|
15
|
+
easingBezier: ReadonlyArray<Float>;
|
|
16
|
+
damping: Float;
|
|
17
|
+
stiffness: Float;
|
|
18
|
+
mass: Float;
|
|
19
|
+
loop: string;
|
|
20
|
+
delay: Int32;
|
|
21
|
+
}>;
|
|
22
|
+
|
|
23
|
+
type NativeTransitions = Readonly<{
|
|
24
|
+
defaultConfig: NativeTransitionConfig;
|
|
25
|
+
transform?: NativeTransitionConfig;
|
|
26
|
+
opacity?: NativeTransitionConfig;
|
|
27
|
+
borderRadius?: NativeTransitionConfig;
|
|
28
|
+
backgroundColor?: NativeTransitionConfig;
|
|
29
|
+
}>;
|
|
30
|
+
|
|
9
31
|
export interface NativeProps extends ViewProps {
|
|
10
32
|
// Bitmask of which properties are animated (0 = none, let style handle all)
|
|
11
33
|
animatedProperties?: CodegenTypes.WithDefault<CodegenTypes.Int32, 0>;
|
|
@@ -37,21 +59,8 @@ export interface NativeProps extends ViewProps {
|
|
|
37
59
|
>;
|
|
38
60
|
initialAnimateBackgroundColor?: ColorValue;
|
|
39
61
|
|
|
40
|
-
//
|
|
41
|
-
|
|
42
|
-
'timing' | 'spring' | 'none',
|
|
43
|
-
'timing'
|
|
44
|
-
>;
|
|
45
|
-
transitionDuration?: CodegenTypes.WithDefault<CodegenTypes.Int32, 300>;
|
|
46
|
-
// Easing cubic bezier control points [x1, y1, x2, y2] (default: easeInOut)
|
|
47
|
-
transitionEasingBezier?: ReadonlyArray<CodegenTypes.Float>;
|
|
48
|
-
transitionDamping?: CodegenTypes.WithDefault<CodegenTypes.Float, 15.0>;
|
|
49
|
-
transitionStiffness?: CodegenTypes.WithDefault<CodegenTypes.Float, 120.0>;
|
|
50
|
-
transitionMass?: CodegenTypes.WithDefault<CodegenTypes.Float, 1.0>;
|
|
51
|
-
transitionLoop?: CodegenTypes.WithDefault<
|
|
52
|
-
'none' | 'repeat' | 'reverse',
|
|
53
|
-
'none'
|
|
54
|
-
>;
|
|
62
|
+
// Unified transition config — one struct with per-property configs
|
|
63
|
+
transitions?: NativeTransitions;
|
|
55
64
|
|
|
56
65
|
// Transform origin (0–1 fractions, default center)
|
|
57
66
|
transformOriginX?: CodegenTypes.WithDefault<CodegenTypes.Float, 0.5>;
|
package/src/index.tsx
CHANGED
package/src/types.ts
CHANGED
|
@@ -20,6 +20,8 @@ export type TimingTransition = {
|
|
|
20
20
|
easing?: EasingType;
|
|
21
21
|
/** Loop mode — 'repeat' restarts from the beginning, 'reverse' alternates direction. */
|
|
22
22
|
loop?: 'repeat' | 'reverse';
|
|
23
|
+
/** Delay in milliseconds before the animation starts. @default 0 */
|
|
24
|
+
delay?: number;
|
|
23
25
|
};
|
|
24
26
|
|
|
25
27
|
/** Physics-based spring transition. */
|
|
@@ -31,6 +33,8 @@ export type SpringTransition = {
|
|
|
31
33
|
stiffness?: number;
|
|
32
34
|
/** Mass of the object — higher values mean slower, more momentum. @default 1 */
|
|
33
35
|
mass?: number;
|
|
36
|
+
/** Delay in milliseconds before the animation starts. @default 0 */
|
|
37
|
+
delay?: number;
|
|
34
38
|
};
|
|
35
39
|
|
|
36
40
|
/** No transition — values are applied immediately without animation. */
|
|
@@ -38,8 +42,28 @@ export type NoneTransition = {
|
|
|
38
42
|
type: 'none';
|
|
39
43
|
};
|
|
40
44
|
|
|
41
|
-
/**
|
|
42
|
-
export type
|
|
45
|
+
/** A single animation transition configuration. */
|
|
46
|
+
export type SingleTransition =
|
|
47
|
+
| TimingTransition
|
|
48
|
+
| SpringTransition
|
|
49
|
+
| NoneTransition;
|
|
50
|
+
|
|
51
|
+
/** Per-property transition map with category-based keys. */
|
|
52
|
+
export type TransitionMap = {
|
|
53
|
+
/** Fallback config for properties not explicitly listed. */
|
|
54
|
+
default?: SingleTransition;
|
|
55
|
+
/** Config for all transform properties (translateX/Y, scaleX/Y, rotate, rotateX/Y). */
|
|
56
|
+
transform?: SingleTransition;
|
|
57
|
+
/** Config for opacity. */
|
|
58
|
+
opacity?: SingleTransition;
|
|
59
|
+
/** Config for borderRadius. */
|
|
60
|
+
borderRadius?: SingleTransition;
|
|
61
|
+
/** Config for backgroundColor. */
|
|
62
|
+
backgroundColor?: SingleTransition;
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
/** Animation transition configuration — either a single config or a per-property map. */
|
|
66
|
+
export type Transition = SingleTransition | TransitionMap;
|
|
43
67
|
|
|
44
68
|
/** Event fired when the animation ends. */
|
|
45
69
|
export type TransitionEndEvent = {
|