diff --git a/example/src/demos/SpinDemo.tsx b/example/src/demos/SpinDemo.tsx
new file mode 100644
index 0000000..a8fb69b
--- /dev/null
+++ b/example/src/demos/SpinDemo.tsx
@@ -0,0 +1,75 @@
+import { useState } from 'react';
+import { View, StyleSheet } from 'react-native';
+import { EaseView } from 'react-native-ease';
+
+import { Section } from '../components/Section';
+import { Button } from '../components/Button';
+
+export function SpinDemo() {
+ const [playing, setPlaying] = useState(false);
+ const [playingWithScale, setPlayingWithScale] = useState(false);
+ return (
+ <>
+
+ {playing && (
+
+
+
+ )}
+
+
+ {playingWithScale && (
+
+
+
+ )}
+
+ >
+ );
+}
+
+const styles = StyleSheet.create({
+ box: {
+ width: 80,
+ height: 80,
+ backgroundColor: '#4a90d9',
+ borderRadius: 12,
+ alignItems: 'center',
+ justifyContent: 'flex-start',
+ paddingTop: 8,
+ },
+ indicator: {
+ width: 12,
+ height: 12,
+ borderRadius: 6,
+ backgroundColor: '#fff',
+ },
+});
diff --git a/example/src/demos/index.ts b/example/src/demos/index.ts
index b184740..4d518a2 100644
--- a/example/src/demos/index.ts
+++ b/example/src/demos/index.ts
@@ -22,6 +22,7 @@ import { StyleReRenderDemo } from './StyleReRenderDemo';
import { StyledCardDemo } from './StyledCardDemo';
import { TransformOriginDemo } from './TransformOriginDemo';
import { PerPropertyDemo } from './PerPropertyDemo';
+import { SpinDemo } from './SpinDemo';
import { UniwindDemo } from './uniwind/UniwindDemo';
interface DemoEntry {
@@ -75,6 +76,7 @@ export const demos: Record = {
title: 'Uniwind',
section: 'Style',
},
+ 'spin': { component: SpinDemo, title: 'Spin', section: 'Loop' },
'pulse': { component: PulseDemo, title: 'Pulse', section: 'Loop' },
'banner': { component: BannerDemo, title: 'Banner', section: 'Loop' },
'interrupt': {
diff --git a/ios/EaseView.mm b/ios/EaseView.mm
index ddc099c..305b23d 100644
--- a/ios/EaseView.mm
+++ b/ios/EaseView.mm
@@ -19,6 +19,13 @@ - (void)invalidateLayer;
// Animation key constants
static NSString *const kAnimKeyOpacity = @"ease_opacity";
static NSString *const kAnimKeyTransform = @"ease_transform";
+static NSString *const kAnimKeyTransformRotateZ = @"ease_transform_rotZ";
+static NSString *const kAnimKeyTransformRotateX = @"ease_transform_rotX";
+static NSString *const kAnimKeyTransformRotateY = @"ease_transform_rotY";
+static NSString *const kAnimKeyTransformScaleX = @"ease_transform_scX";
+static NSString *const kAnimKeyTransformScaleY = @"ease_transform_scY";
+static NSString *const kAnimKeyTransformTransX = @"ease_transform_trX";
+static NSString *const kAnimKeyTransformTransY = @"ease_transform_trY";
static NSString *const kAnimKeyCornerRadius = @"ease_cornerRadius";
static NSString *const kAnimKeyBackgroundColor = @"ease_backgroundColor";
@@ -373,7 +380,10 @@ - (void)applyFirstMountProps:(const EaseViewProps &)viewProps {
}
}
if (hasInitialTransform) {
- // Build mask of which transform sub-properties actually changed
+ // Build bitmask of which transform sub-properties differ between initial
+ // and target. Comparing raw prop values (not composed matrices) so that
+ // e.g. rotate 0→360 is correctly detected as a change even though the
+ // resulting CATransform3D matrices are identical.
int changedInitTransform = 0;
if (viewProps.initialAnimateTranslateX != viewProps.animateTranslateX)
changedInitTransform |= kMaskTranslateX;
@@ -395,12 +405,71 @@ - (void)applyFirstMountProps:(const EaseViewProps &)viewProps {
transitionConfigForProperty(transformName, viewProps);
self.layer.transform = targetT;
if (transformConfig.type != "none") {
- [self applyAnimationForKeyPath:@"transform"
- animationKey:kAnimKeyTransform
- fromValue:[NSValue valueWithCATransform3D:initialT]
- toValue:[NSValue valueWithCATransform3D:targetT]
- config:transformConfig
- loop:YES];
+ // Animate each changed sub-property individually using key paths.
+ // This avoids matrix interpolation which fails for cases like
+ // rotate 0→360 (identical matrices, but visually a full rotation).
+ if (changedInitTransform & kMaskTranslateX) {
+ [self applyAnimationForKeyPath:@"transform.translation.x"
+ animationKey:kAnimKeyTransformTransX
+ fromValue:@(viewProps.initialAnimateTranslateX)
+ toValue:@(viewProps.animateTranslateX)
+ config:transformConfig
+ loop:YES];
+ }
+ if (changedInitTransform & kMaskTranslateY) {
+ [self applyAnimationForKeyPath:@"transform.translation.y"
+ animationKey:kAnimKeyTransformTransY
+ fromValue:@(viewProps.initialAnimateTranslateY)
+ toValue:@(viewProps.animateTranslateY)
+ config:transformConfig
+ loop:YES];
+ }
+ if (changedInitTransform & kMaskScaleX) {
+ [self applyAnimationForKeyPath:@"transform.scale.x"
+ animationKey:kAnimKeyTransformScaleX
+ fromValue:@(viewProps.initialAnimateScaleX)
+ toValue:@(viewProps.animateScaleX)
+ config:transformConfig
+ loop:YES];
+ }
+ if (changedInitTransform & kMaskScaleY) {
+ [self applyAnimationForKeyPath:@"transform.scale.y"
+ animationKey:kAnimKeyTransformScaleY
+ fromValue:@(viewProps.initialAnimateScaleY)
+ toValue:@(viewProps.animateScaleY)
+ config:transformConfig
+ loop:YES];
+ }
+ if (changedInitTransform & kMaskRotate) {
+ [self applyAnimationForKeyPath:@"transform.rotation.z"
+ animationKey:kAnimKeyTransformRotateZ
+ fromValue:@(degreesToRadians(
+ viewProps.initialAnimateRotate))
+ toValue:@(degreesToRadians(
+ viewProps.animateRotate))
+ config:transformConfig
+ loop:YES];
+ }
+ if (changedInitTransform & kMaskRotateX) {
+ [self applyAnimationForKeyPath:@"transform.rotation.x"
+ animationKey:kAnimKeyTransformRotateX
+ fromValue:@(degreesToRadians(
+ viewProps.initialAnimateRotateX))
+ toValue:@(degreesToRadians(
+ viewProps.animateRotateX))
+ config:transformConfig
+ loop:YES];
+ }
+ if (changedInitTransform & kMaskRotateY) {
+ [self applyAnimationForKeyPath:@"transform.rotation.y"
+ animationKey:kAnimKeyTransformRotateY
+ fromValue:@(degreesToRadians(
+ viewProps.initialAnimateRotateY))
+ toValue:@(degreesToRadians(
+ viewProps.animateRotateY))
+ config:transformConfig
+ loop:YES];
+ }
}
}
if (hasInitialBorderRadius) {
diff --git a/src/__tests__/EaseView.test.tsx b/src/__tests__/EaseView.test.tsx
index 5689dd6..4798e38 100644
--- a/src/__tests__/EaseView.test.tsx
+++ b/src/__tests__/EaseView.test.tsx
@@ -624,4 +624,53 @@ describe('EaseView', () => {
expect(t.transform!.stiffness).toBe(100);
});
});
+
+ describe('rotate loop props', () => {
+ it('passes rotate 0→360 with loop repeat to native', () => {
+ render(
+ ,
+ );
+ const props = getNativeProps();
+ expect(props.initialAnimateRotate).toBe(0);
+ expect(props.animateRotate).toBe(360);
+ expect(props.transitions.defaultConfig.loop).toBe('repeat');
+ // rotate bit = 1<<5 = 32
+ // eslint-disable-next-line no-bitwise
+ expect(props.animatedProperties & 32).toBe(32);
+ });
+
+ it('passes rotate with scale for combined loop animation', () => {
+ render(
+ ,
+ );
+ const props = getNativeProps();
+ expect(props.initialAnimateRotate).toBe(0);
+ expect(props.animateRotate).toBe(360);
+ expect(props.initialAnimateScaleX).toBe(0.8);
+ expect(props.initialAnimateScaleY).toBe(0.8);
+ expect(props.animateScaleX).toBe(1.2);
+ expect(props.animateScaleY).toBe(1.2);
+ expect(props.transitions.defaultConfig.loop).toBe('reverse');
+ });
+ });
});