react-native-ease 0.3.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/README.md +116 -0
- package/android/src/main/java/com/ease/EaseView.kt +261 -82
- package/android/src/main/java/com/ease/EaseViewManager.kt +5 -49
- package/ios/EaseView.mm +275 -79
- package/lib/module/EaseView.js +85 -28
- package/lib/module/EaseView.js.map +1 -1
- package/lib/module/EaseView.web.js +167 -26
- package/lib/module/EaseView.web.js.map +1 -1
- package/lib/module/EaseViewNativeComponent.ts +24 -16
- 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.map +1 -1
- package/lib/typescript/src/EaseViewNativeComponent.d.ts +20 -8
- 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 +17 -2
- package/lib/typescript/src/types.d.ts.map +1 -1
- package/package.json +1 -1
- package/skills/react-native-ease-refactor/SKILL.md +8 -2
- package/src/EaseView.tsx +116 -53
- package/src/EaseView.web.tsx +209 -42
- package/src/EaseViewNativeComponent.ts +24 -16
- package/src/index.tsx +2 -0
- package/src/types.ts +22 -2
package/src/EaseView.tsx
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { StyleSheet, type ViewProps, type ViewStyle } from 'react-native';
|
|
2
|
-
import NativeEaseView from './EaseViewNativeComponent';
|
|
2
|
+
import NativeEaseView, { type NativeProps } from './EaseViewNativeComponent';
|
|
3
3
|
import type {
|
|
4
4
|
AnimateProps,
|
|
5
5
|
CubicBezier,
|
|
6
|
+
SingleTransition,
|
|
6
7
|
Transition,
|
|
7
8
|
TransitionEndEvent,
|
|
8
9
|
TransformOrigin,
|
|
@@ -58,6 +59,115 @@ const EASING_PRESETS: Record<string, CubicBezier> = {
|
|
|
58
59
|
easeInOut: [0.42, 0, 0.58, 1],
|
|
59
60
|
};
|
|
60
61
|
|
|
62
|
+
/** Returns true if the transition is a SingleTransition (has a `type` field). */
|
|
63
|
+
function isSingleTransition(t: Transition): t is SingleTransition {
|
|
64
|
+
return 'type' in t;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
type NativeTransitions = NonNullable<NativeProps['transitions']>;
|
|
68
|
+
type NativeTransitionConfig = NativeTransitions['defaultConfig'];
|
|
69
|
+
|
|
70
|
+
/** Default config: timing 300ms easeInOut. */
|
|
71
|
+
const DEFAULT_CONFIG: NativeTransitionConfig = {
|
|
72
|
+
type: 'timing',
|
|
73
|
+
duration: 300,
|
|
74
|
+
easingBezier: [0.42, 0, 0.58, 1],
|
|
75
|
+
damping: 15,
|
|
76
|
+
stiffness: 120,
|
|
77
|
+
mass: 1,
|
|
78
|
+
loop: 'none',
|
|
79
|
+
delay: 0,
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
/** Resolve a SingleTransition into a native config object. */
|
|
83
|
+
function resolveSingleConfig(config: SingleTransition): NativeTransitionConfig {
|
|
84
|
+
const type = config.type as string;
|
|
85
|
+
const duration = config.type === 'timing' ? config.duration ?? 300 : 300;
|
|
86
|
+
const rawEasing =
|
|
87
|
+
config.type === 'timing' ? config.easing ?? 'easeInOut' : 'easeInOut';
|
|
88
|
+
if (__DEV__) {
|
|
89
|
+
if (Array.isArray(rawEasing)) {
|
|
90
|
+
if ((rawEasing as number[]).length !== 4) {
|
|
91
|
+
console.warn(
|
|
92
|
+
'react-native-ease: Custom easing must be a [x1, y1, x2, y2] tuple (got length ' +
|
|
93
|
+
(rawEasing as number[]).length +
|
|
94
|
+
').',
|
|
95
|
+
);
|
|
96
|
+
}
|
|
97
|
+
if (
|
|
98
|
+
rawEasing[0] < 0 ||
|
|
99
|
+
rawEasing[0] > 1 ||
|
|
100
|
+
rawEasing[2] < 0 ||
|
|
101
|
+
rawEasing[2] > 1
|
|
102
|
+
) {
|
|
103
|
+
console.warn(
|
|
104
|
+
'react-native-ease: Easing x-values (x1, x2) must be between 0 and 1.',
|
|
105
|
+
);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
const easingBezier: number[] = Array.isArray(rawEasing)
|
|
110
|
+
? rawEasing
|
|
111
|
+
: EASING_PRESETS[rawEasing]!;
|
|
112
|
+
const damping = config.type === 'spring' ? config.damping ?? 15 : 15;
|
|
113
|
+
const stiffness = config.type === 'spring' ? config.stiffness ?? 120 : 120;
|
|
114
|
+
const mass = config.type === 'spring' ? config.mass ?? 1 : 1;
|
|
115
|
+
const loop: string =
|
|
116
|
+
config.type === 'timing' ? config.loop ?? 'none' : 'none';
|
|
117
|
+
const delay =
|
|
118
|
+
config.type === 'timing' || config.type === 'spring'
|
|
119
|
+
? config.delay ?? 0
|
|
120
|
+
: 0;
|
|
121
|
+
return {
|
|
122
|
+
type,
|
|
123
|
+
duration,
|
|
124
|
+
easingBezier,
|
|
125
|
+
damping,
|
|
126
|
+
stiffness,
|
|
127
|
+
mass,
|
|
128
|
+
loop,
|
|
129
|
+
delay,
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/** Category keys that map to optional NativeTransitions fields. */
|
|
134
|
+
const CATEGORY_KEYS = [
|
|
135
|
+
'transform',
|
|
136
|
+
'opacity',
|
|
137
|
+
'borderRadius',
|
|
138
|
+
'backgroundColor',
|
|
139
|
+
] as const;
|
|
140
|
+
|
|
141
|
+
/** Resolve the transition prop into a NativeTransitions struct. */
|
|
142
|
+
function resolveTransitions(transition?: Transition): NativeTransitions {
|
|
143
|
+
// No transition: timing default for all properties
|
|
144
|
+
if (transition == null) {
|
|
145
|
+
return { defaultConfig: DEFAULT_CONFIG };
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// Single transition: set as defaultConfig only
|
|
149
|
+
if (isSingleTransition(transition)) {
|
|
150
|
+
return { defaultConfig: resolveSingleConfig(transition) };
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// TransitionMap: resolve defaultConfig + only specified category keys
|
|
154
|
+
const defaultConfig = transition.default
|
|
155
|
+
? resolveSingleConfig(transition.default)
|
|
156
|
+
: DEFAULT_CONFIG;
|
|
157
|
+
|
|
158
|
+
const result: NativeTransitions = { defaultConfig };
|
|
159
|
+
|
|
160
|
+
for (const key of CATEGORY_KEYS) {
|
|
161
|
+
const specific = transition[key];
|
|
162
|
+
if (specific != null) {
|
|
163
|
+
(result as Record<string, NativeTransitionConfig>)[key] =
|
|
164
|
+
resolveSingleConfig(specific);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
return result;
|
|
169
|
+
}
|
|
170
|
+
|
|
61
171
|
export type EaseViewProps = ViewProps & {
|
|
62
172
|
/** Target values for animated properties. */
|
|
63
173
|
animate?: AnimateProps;
|
|
@@ -86,6 +196,8 @@ export type EaseViewProps = ViewProps & {
|
|
|
86
196
|
useHardwareLayer?: boolean;
|
|
87
197
|
/** Pivot point for scale and rotation as 0–1 fractions. @default { x: 0.5, y: 0.5 } (center) */
|
|
88
198
|
transformOrigin?: TransformOrigin;
|
|
199
|
+
/** NativeWind / Tailwind CSS class string. Requires NativeWind in your project. */
|
|
200
|
+
className?: string;
|
|
89
201
|
};
|
|
90
202
|
|
|
91
203
|
export function EaseView({
|
|
@@ -179,50 +291,8 @@ export function EaseView({
|
|
|
179
291
|
}
|
|
180
292
|
}
|
|
181
293
|
|
|
182
|
-
// Resolve transition config
|
|
183
|
-
const
|
|
184
|
-
const transitionDuration =
|
|
185
|
-
transition?.type === 'timing' ? transition.duration ?? 300 : 300;
|
|
186
|
-
const rawEasing =
|
|
187
|
-
transition?.type === 'timing'
|
|
188
|
-
? transition.easing ?? 'easeInOut'
|
|
189
|
-
: 'easeInOut';
|
|
190
|
-
if (__DEV__) {
|
|
191
|
-
if (Array.isArray(rawEasing)) {
|
|
192
|
-
if ((rawEasing as number[]).length !== 4) {
|
|
193
|
-
console.warn(
|
|
194
|
-
'react-native-ease: Custom easing must be a [x1, y1, x2, y2] tuple (got length ' +
|
|
195
|
-
(rawEasing as number[]).length +
|
|
196
|
-
').',
|
|
197
|
-
);
|
|
198
|
-
}
|
|
199
|
-
if (
|
|
200
|
-
rawEasing[0] < 0 ||
|
|
201
|
-
rawEasing[0] > 1 ||
|
|
202
|
-
rawEasing[2] < 0 ||
|
|
203
|
-
rawEasing[2] > 1
|
|
204
|
-
) {
|
|
205
|
-
console.warn(
|
|
206
|
-
'react-native-ease: Easing x-values (x1, x2) must be between 0 and 1.',
|
|
207
|
-
);
|
|
208
|
-
}
|
|
209
|
-
}
|
|
210
|
-
}
|
|
211
|
-
const bezier: CubicBezier = Array.isArray(rawEasing)
|
|
212
|
-
? rawEasing
|
|
213
|
-
: EASING_PRESETS[rawEasing]!;
|
|
214
|
-
const transitionDamping =
|
|
215
|
-
transition?.type === 'spring' ? transition.damping ?? 15 : 15;
|
|
216
|
-
const transitionStiffness =
|
|
217
|
-
transition?.type === 'spring' ? transition.stiffness ?? 120 : 120;
|
|
218
|
-
const transitionMass =
|
|
219
|
-
transition?.type === 'spring' ? transition.mass ?? 1 : 1;
|
|
220
|
-
const transitionLoop =
|
|
221
|
-
transition?.type === 'timing' ? transition.loop ?? 'none' : 'none';
|
|
222
|
-
const transitionDelay =
|
|
223
|
-
transition?.type === 'timing' || transition?.type === 'spring'
|
|
224
|
-
? transition.delay ?? 0
|
|
225
|
-
: 0;
|
|
294
|
+
// Resolve transition config into a fully-populated struct
|
|
295
|
+
const transitions = resolveTransitions(transition);
|
|
226
296
|
|
|
227
297
|
const handleTransitionEnd = onTransitionEnd
|
|
228
298
|
? (event: { nativeEvent: { finished: boolean } }) =>
|
|
@@ -254,14 +324,7 @@ export function EaseView({
|
|
|
254
324
|
initialAnimateRotateY={resolvedInitial.rotateY}
|
|
255
325
|
initialAnimateBorderRadius={resolvedInitial.borderRadius}
|
|
256
326
|
initialAnimateBackgroundColor={initialBgColor}
|
|
257
|
-
|
|
258
|
-
transitionDuration={transitionDuration}
|
|
259
|
-
transitionEasingBezier={bezier}
|
|
260
|
-
transitionDamping={transitionDamping}
|
|
261
|
-
transitionStiffness={transitionStiffness}
|
|
262
|
-
transitionMass={transitionMass}
|
|
263
|
-
transitionLoop={transitionLoop}
|
|
264
|
-
transitionDelay={transitionDelay}
|
|
327
|
+
transitions={transitions}
|
|
265
328
|
useHardwareLayer={useHardwareLayer}
|
|
266
329
|
transformOriginX={transformOrigin?.x ?? 0.5}
|
|
267
330
|
transformOriginY={transformOrigin?.y ?? 0.5}
|
package/src/EaseView.web.tsx
CHANGED
|
@@ -3,6 +3,7 @@ import { View, type ViewStyle, type StyleProp } from 'react-native';
|
|
|
3
3
|
import type {
|
|
4
4
|
AnimateProps,
|
|
5
5
|
CubicBezier,
|
|
6
|
+
SingleTransition,
|
|
6
7
|
Transition,
|
|
7
8
|
TransitionEndEvent,
|
|
8
9
|
TransformOrigin,
|
|
@@ -29,6 +30,80 @@ const EASING_PRESETS: Record<string, CubicBezier> = {
|
|
|
29
30
|
easeInOut: [0.42, 0, 0.58, 1],
|
|
30
31
|
};
|
|
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
|
+
|
|
32
107
|
export type EaseViewProps = {
|
|
33
108
|
animate?: AnimateProps;
|
|
34
109
|
initialAnimate?: AnimateProps;
|
|
@@ -81,10 +156,91 @@ function buildTransform(vals: ReturnType<typeof resolveAnimateValues>): string {
|
|
|
81
156
|
return parts.length > 0 ? parts.join(' ') : 'none';
|
|
82
157
|
}
|
|
83
158
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
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' };
|
|
87
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
|
|
88
244
|
const easing = transition.easing ?? 'easeInOut';
|
|
89
245
|
const bezier: CubicBezier = Array.isArray(easing)
|
|
90
246
|
? easing
|
|
@@ -92,24 +248,17 @@ function resolveEasing(transition: Transition | undefined): string {
|
|
|
92
248
|
return `cubic-bezier(${bezier[0]}, ${bezier[1]}, ${bezier[2]}, ${bezier[3]})`;
|
|
93
249
|
}
|
|
94
250
|
|
|
95
|
-
function resolveDuration(transition:
|
|
251
|
+
function resolveDuration(transition: SingleTransition | undefined): number {
|
|
96
252
|
if (!transition) return 300;
|
|
97
253
|
if (transition.type === 'timing') return transition.duration ?? 300;
|
|
98
254
|
if (transition.type === 'none') return 0;
|
|
99
|
-
|
|
100
|
-
const
|
|
101
|
-
const
|
|
102
|
-
|
|
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;
|
|
103
260
|
}
|
|
104
261
|
|
|
105
|
-
/** CSS transition properties that we animate. */
|
|
106
|
-
const TRANSITION_PROPS = [
|
|
107
|
-
'opacity',
|
|
108
|
-
'transform',
|
|
109
|
-
'border-radius',
|
|
110
|
-
'background-color',
|
|
111
|
-
];
|
|
112
|
-
|
|
113
262
|
/** Counter for unique keyframe names. */
|
|
114
263
|
let keyframeCounter = 0;
|
|
115
264
|
|
|
@@ -146,21 +295,36 @@ export function EaseView({
|
|
|
146
295
|
const displayValues =
|
|
147
296
|
!mounted && hasInitial ? resolveAnimateValues(initialAnimate) : resolved;
|
|
148
297
|
|
|
149
|
-
const
|
|
150
|
-
|
|
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);
|
|
151
311
|
|
|
152
312
|
const originX = ((transformOrigin?.x ?? 0.5) * 100).toFixed(1);
|
|
153
313
|
const originY = ((transformOrigin?.y ?? 0.5) * 100).toFixed(1);
|
|
154
314
|
|
|
155
|
-
const transitionType = transition?.type ?? 'timing';
|
|
156
|
-
const loopMode = transition?.type === 'timing' ? transition.loop : undefined;
|
|
157
|
-
|
|
158
315
|
const transitionCss =
|
|
159
|
-
|
|
316
|
+
!mounted && hasInitial
|
|
160
317
|
? 'none'
|
|
161
|
-
:
|
|
162
|
-
|
|
163
|
-
|
|
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';
|
|
164
328
|
|
|
165
329
|
// Apply CSS transition/animation properties imperatively (not in RN style spec).
|
|
166
330
|
useEffect(() => {
|
|
@@ -168,14 +332,7 @@ export function EaseView({
|
|
|
168
332
|
if (!el) return;
|
|
169
333
|
|
|
170
334
|
if (!loopMode) {
|
|
171
|
-
|
|
172
|
-
transitionType === 'spring'
|
|
173
|
-
? TRANSITION_PROPS.map(
|
|
174
|
-
(prop) =>
|
|
175
|
-
`${prop} ${duration}ms cubic-bezier(0.25, 0.46, 0.45, 0.94)`,
|
|
176
|
-
).join(', ')
|
|
177
|
-
: null;
|
|
178
|
-
el.style.transition = springTransition ?? transitionCss;
|
|
335
|
+
el.style.transition = transitionCss;
|
|
179
336
|
}
|
|
180
337
|
el.style.transformOrigin = `${originX}% ${originY}%`;
|
|
181
338
|
});
|
|
@@ -257,27 +414,37 @@ export function EaseView({
|
|
|
257
414
|
|
|
258
415
|
const direction = loopMode === 'reverse' ? 'alternate' : 'normal';
|
|
259
416
|
el.style.transition = 'none';
|
|
260
|
-
el.style.animation = `${name} ${
|
|
417
|
+
el.style.animation = `${name} ${loopDuration}ms ${loopEasing} infinite ${direction}`;
|
|
261
418
|
|
|
262
419
|
return () => {
|
|
263
420
|
styleEl.remove();
|
|
264
421
|
el.style.animation = '';
|
|
265
422
|
animationNameRef.current = null;
|
|
266
423
|
};
|
|
267
|
-
}, [loopMode, animate, initialAnimate,
|
|
424
|
+
}, [loopMode, animate, initialAnimate, loopDuration, loopEasing, getElement]);
|
|
268
425
|
|
|
269
426
|
// Build animated style using RN transform array format.
|
|
270
427
|
// react-native-web converts these to CSS transform strings.
|
|
271
428
|
const animatedStyle: ViewStyle = {
|
|
272
429
|
opacity: displayValues.opacity,
|
|
273
430
|
transform: [
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
{
|
|
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
|
+
: []),
|
|
281
448
|
],
|
|
282
449
|
...(displayValues.borderRadius > 0
|
|
283
450
|
? { borderRadius: displayValues.borderRadius }
|
|
@@ -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,22 +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
|
-
>;
|
|
55
|
-
transitionDelay?: CodegenTypes.WithDefault<CodegenTypes.Int32, 0>;
|
|
62
|
+
// Unified transition config — one struct with per-property configs
|
|
63
|
+
transitions?: NativeTransitions;
|
|
56
64
|
|
|
57
65
|
// Transform origin (0–1 fractions, default center)
|
|
58
66
|
transformOriginX?: CodegenTypes.WithDefault<CodegenTypes.Float, 0.5>;
|
package/src/index.tsx
CHANGED
package/src/types.ts
CHANGED
|
@@ -42,8 +42,28 @@ export type NoneTransition = {
|
|
|
42
42
|
type: 'none';
|
|
43
43
|
};
|
|
44
44
|
|
|
45
|
-
/**
|
|
46
|
-
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;
|
|
47
67
|
|
|
48
68
|
/** Event fired when the animation ends. */
|
|
49
69
|
export type TransitionEndEvent = {
|