Skip to content

Commit be095d9

Browse files
committed
feat(Rotate): implement rotateAtom for single-axis rotation and update Rotate component to use new parameters
1 parent f0c388b commit be095d9

File tree

5 files changed

+118
-133
lines changed

5 files changed

+118
-133
lines changed
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
import { AtomMotion, PresenceDirection, motionTokens } from '@fluentui/react-motion';
2+
3+
export type Axis3D = 'x' | 'y' | 'z';
4+
5+
interface RotateAtomParams {
6+
direction: PresenceDirection;
7+
duration: number;
8+
easing?: string;
9+
axis?: Axis3D;
10+
enterAngle?: number;
11+
exitAngle?: number;
12+
}
13+
14+
const createRotateValue = (axis: Axis3D, angle: number): string => {
15+
return `${axis} ${angle}deg`;
16+
};
17+
18+
/**
19+
* Generates a motion atom object for a rotation around a single axis.
20+
* @param direction - The functional direction of the motion: 'enter' or 'exit'.
21+
* @param duration - The duration of the motion in milliseconds.
22+
* @param easing - The easing curve for the motion. Defaults to `motionTokens.curveLinear`.
23+
* @param axis - The axis of rotation: 'X', 'Y', or 'Z'. Defaults to 'Y'.
24+
* @param enterAngle - The starting rotation angle in degrees. Defaults to -90.
25+
* @param exitAngle - The ending rotation angle in degrees. Defaults to the negation of `enterAngle`.
26+
* @returns A motion atom object with rotate keyframes and the supplied duration and easing.
27+
*/
28+
export const rotateAtom = ({
29+
direction,
30+
duration,
31+
easing = motionTokens.curveLinear,
32+
axis = 'y',
33+
enterAngle = -90,
34+
exitAngle = -enterAngle,
35+
}: RotateAtomParams): AtomMotion => {
36+
let fromAngle = enterAngle;
37+
let toAngle = 0;
38+
39+
if (direction === 'exit') {
40+
fromAngle = 0;
41+
toAngle = exitAngle;
42+
}
43+
const keyframes = [{ rotate: createRotateValue(axis, fromAngle) }, { rotate: createRotateValue(axis, toAngle) }];
44+
45+
return {
46+
keyframes,
47+
duration,
48+
easing,
49+
};
50+
};
Lines changed: 25 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { AtomMotion, createPresenceComponent, motionTokens, PresenceMotionFn } from '@fluentui/react-motion';
22
import { fadeAtom } from '../../atoms/fade-atom';
3+
import { rotateAtom, Axis3D } from '../../atoms/rotate-atom';
34

45
/**
56
* Parameters for configuring the Rotate motion.
@@ -30,40 +31,22 @@ export type RotateParams = {
3031
exitEasing?: string;
3132

3233
/**
33-
* The starting X-axis rotation angle, in degrees.
34-
* Defaults to 0.
34+
* The axis of rotation: 'X', 'Y', or 'Z'.
35+
* Defaults to 'Y'.
3536
*/
36-
fromX?: number;
37+
axis?: Axis3D;
3738

3839
/**
39-
* The starting Y-axis rotation angle, in degrees.
40+
* The starting rotation angle in degrees.
4041
* Defaults to -90.
4142
*/
42-
fromY?: number;
43+
enterAngle?: number;
4344

4445
/**
45-
* The starting Z-axis rotation angle, in degrees.
46-
* Defaults to 0.
46+
* The ending rotation angle in degrees.
47+
* Defaults to the negation of `enterAngle`.
4748
*/
48-
fromZ?: number;
49-
50-
/**
51-
* The ending X-axis rotation angle, in degrees.
52-
* Defaults to the negation of `fromX`.
53-
*/
54-
toX?: number;
55-
56-
/**
57-
* The ending Y-axis rotation angle, in degrees.
58-
* Defaults to the negation of `fromY`.
59-
*/
60-
toY?: number;
61-
62-
/**
63-
* The ending Z-axis rotation angle, in degrees.
64-
* Defaults to the negation of `fromZ`.
65-
*/
66-
toZ?: number;
49+
exitAngle?: number;
6750

6851
/**
6952
* Whether to animate the opacity during the rotation.
@@ -72,50 +55,36 @@ export type RotateParams = {
7255
animateOpacity?: boolean;
7356
};
7457

75-
const createRotateValue = (x: number, y: number, z: number): string => {
76-
const rotations = [];
77-
78-
if (x !== 0) {
79-
rotations.push(`x ${x}deg`);
80-
}
81-
if (y !== 0) {
82-
rotations.push(`y ${y}deg`);
83-
}
84-
if (z !== 0) {
85-
rotations.push(`z ${z}deg`);
86-
}
87-
88-
return rotations.length > 0 ? rotations.join(' ') : '0deg';
89-
};
90-
9158
const rotatePresenceFn: PresenceMotionFn<RotateParams> = ({
92-
// element,
93-
fromX = 0,
94-
fromY = -90,
95-
fromZ = 0,
96-
toX = -fromX,
97-
toY = -fromY,
98-
toZ = -fromZ,
59+
axis = 'y',
60+
enterAngle = -90,
61+
exitAngle = -enterAngle,
9962
duration = motionTokens.durationGentle,
10063
exitDuration = duration,
10164
easing = motionTokens.curveDecelerateMax,
10265
exitEasing = motionTokens.curveAccelerateMax,
10366
animateOpacity = true,
10467
}: RotateParams) => {
10568
const enterAtoms: AtomMotion[] = [
106-
{
107-
keyframes: [{ rotate: createRotateValue(fromX, fromY, fromZ) }, { rotate: createRotateValue(0, 0, 0) }],
69+
rotateAtom({
70+
direction: 'enter',
10871
duration,
10972
easing,
110-
},
73+
axis,
74+
enterAngle,
75+
exitAngle,
76+
}),
11177
];
11278

11379
const exitAtoms: AtomMotion[] = [
114-
{
115-
keyframes: [{ rotate: createRotateValue(0, 0, 0) }, { rotate: createRotateValue(toX, toY, toZ) }],
80+
rotateAtom({
81+
direction: 'exit',
11682
duration: exitDuration,
11783
easing: exitEasing,
118-
},
84+
axis,
85+
enterAngle,
86+
exitAngle,
87+
}),
11988
];
12089

12190
if (animateOpacity) {
@@ -129,5 +98,5 @@ const rotatePresenceFn: PresenceMotionFn<RotateParams> = ({
12998
};
13099
};
131100

132-
// Create a presence motion component to rotate an element around the X, Y, and/or Z axes.
101+
// Create a presence motion component to rotate an element around a single axis (X, Y, or Z).
133102
export const Rotate = createPresenceComponent(rotatePresenceFn);

packages/react-components/react-motion-components-preview/stories/src/Rotate/RotateCardFlip.stories.tsx

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -157,8 +157,9 @@ export const CardFlip = () => {
157157
<div key={card.id} className={classes.cardContainer}>
158158
<Rotate
159159
visible={!flippedCards.has(card.id)}
160-
fromY={0}
161-
toY={180}
160+
axis="Y"
161+
enterAngle={0}
162+
exitAngle={180}
162163
duration={600}
163164
easing="cubic-bezier(0.4, 0, 0.2, 1)"
164165
>
@@ -172,8 +173,9 @@ export const CardFlip = () => {
172173

173174
<Rotate
174175
visible={flippedCards.has(card.id)}
175-
fromY={-180}
176-
toY={0}
176+
axis="Y"
177+
enterAngle={-180}
178+
exitAngle={0}
177179
duration={600}
178180
easing="cubic-bezier(0.4, 0, 0.2, 1)"
179181
>

packages/react-components/react-motion-components-preview/stories/src/Rotate/RotateCommonPatterns.stories.tsx

Lines changed: 15 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,8 @@ const patterns = [
6464
description: 'Classic Y-axis rotation',
6565
icon: '↔️',
6666
color: tokens.colorPaletteBlueForeground2,
67-
rotation: { fromY: 180 },
67+
axis: 'Y' as const,
68+
enterAngle: 180,
6869
easing: 'ease-out',
6970
duration: 600,
7071
},
@@ -74,7 +75,8 @@ const patterns = [
7475
description: 'X-axis rotation',
7576
icon: '↕️',
7677
color: tokens.colorPaletteGreenForeground2,
77-
rotation: { fromX: 180 },
78+
axis: 'X' as const,
79+
enterAngle: 180,
7880
easing: 'ease-out',
7981
duration: 600,
8082
},
@@ -84,27 +86,19 @@ const patterns = [
8486
description: 'Z-axis rotation',
8587
icon: '🔄',
8688
color: tokens.colorPaletteRedForeground2,
87-
rotation: { fromZ: 360 },
89+
axis: 'Z' as const,
90+
enterAngle: 360,
8891
easing: 'ease-in-out',
8992
duration: 800,
9093
},
91-
{
92-
id: 'tumble',
93-
name: 'Tumble',
94-
description: 'Multi-axis rotation',
95-
icon: '🎲',
96-
color: tokens.colorPalettePurpleForeground2,
97-
rotation: { fromX: 180, fromY: 180 },
98-
easing: 'cubic-bezier(0.68, -0.55, 0.265, 1.55)',
99-
duration: 1000,
100-
},
10194
{
10295
id: 'wobble',
10396
name: 'Wobble',
10497
description: 'Gentle X-axis tilt',
10598
icon: '〰️',
10699
color: tokens.colorPaletteYellowForeground2,
107-
rotation: { fromX: 15 },
100+
axis: 'X' as const,
101+
enterAngle: 15,
108102
easing: 'cubic-bezier(0.25, 0.46, 0.45, 0.94)',
109103
duration: 400,
110104
},
@@ -114,7 +108,8 @@ const patterns = [
114108
description: 'Slight Y-axis reveal',
115109
icon: '👀',
116110
color: tokens.colorPaletteTealForeground2,
117-
rotation: { fromY: -15 },
111+
axis: 'Y' as const,
112+
enterAngle: -15,
118113
easing: 'cubic-bezier(0.25, 0.46, 0.45, 0.94)',
119114
duration: 400,
120115
},
@@ -124,20 +119,11 @@ const patterns = [
124119
description: 'Accordion-style fold',
125120
icon: '📄',
126121
color: tokens.colorPaletteDarkOrangeForeground2,
127-
rotation: { fromX: -90 },
122+
axis: 'X' as const,
123+
enterAngle: -90,
128124
easing: 'cubic-bezier(0.4, 0, 0.2, 1)',
129125
duration: 500,
130126
},
131-
{
132-
id: 'twist',
133-
name: 'Twist',
134-
description: 'Diagonal rotation',
135-
icon: '🌪️',
136-
color: tokens.colorPalettePinkForeground2,
137-
rotation: { fromX: 45, fromZ: 45 },
138-
easing: 'cubic-bezier(0.175, 0.885, 0.32, 1.275)',
139-
duration: 700,
140-
},
141127
];
142128

143129
export const CommonPatterns = () => {
@@ -199,9 +185,8 @@ export const CommonPatterns = () => {
199185
<div key={pattern.id}>
200186
<Rotate
201187
visible={activePatterns.has(pattern.id)}
202-
fromX={pattern.rotation.fromX || 0}
203-
fromY={pattern.rotation.fromY || 0}
204-
fromZ={pattern.rotation.fromZ || 0}
188+
axis={pattern.axis}
189+
enterAngle={pattern.enterAngle}
205190
duration={pattern.duration}
206191
easing={pattern.easing}
207192
>
@@ -228,7 +213,7 @@ CommonPatterns.parameters = {
228213
docs: {
229214
description: {
230215
story:
231-
'A collection of common rotation patterns that you can use as starting points for your own animations. Each pattern demonstrates different combinations of rotation axes and easing curves.',
216+
'A collection of common single-axis rotation patterns that you can use as starting points for your own animations. Each pattern demonstrates rotation around a specific axis (X, Y, or Z) with different easing curves.',
232217
},
233218
},
234219
};

0 commit comments

Comments
 (0)