Skip to content

Commit 596b7ff

Browse files
committed
add animated rotation to demo using motion
Closes #367 Use motion's useSpring to smoothly animate rotation when clicking the rotate left/right buttons. The slider still feels direct. Export Preview is disabled during animation to ensure the exported image uses the final rotation angle.
1 parent 1eaeb35 commit 596b7ff

3 files changed

Lines changed: 87 additions & 3 deletions

File tree

packages/demo/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
"preview": "vite preview"
1010
},
1111
"dependencies": {
12+
"motion": "^12.38.0",
1213
"react": "^19.2.4",
1314
"react-avatar-editor": "workspace:*",
1415
"react-dom": "^19.2.4",

packages/demo/src/App.tsx

Lines changed: 26 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { type ChangeEvent, type MouseEventHandler, useState } from 'react'
2+
import { useMotionValue, useSpring, useMotionValueEvent } from 'motion/react'
23
import AvatarEditor, {
34
type Position,
45
useAvatarEditor,
@@ -62,6 +63,23 @@ const App = () => {
6263
const update = (patch: Partial<State>) =>
6364
setState((s) => ({ ...s, ...patch }))
6465

66+
// Animate rotation with a spring for smooth transitions
67+
const rotateMotion = useMotionValue(0)
68+
const rotateSpring = useSpring(rotateMotion, { stiffness: 200, damping: 25 })
69+
const [animatedRotate, setAnimatedRotate] = useState(0)
70+
const [isRotating, setIsRotating] = useState(false)
71+
72+
useMotionValueEvent(rotateSpring, 'change', (v) => setAnimatedRotate(v))
73+
useMotionValueEvent(rotateSpring, 'animationStart', () => setIsRotating(true))
74+
useMotionValueEvent(rotateSpring, 'animationComplete', () =>
75+
setIsRotating(false),
76+
)
77+
78+
// Sync spring target when state.rotate changes
79+
if (rotateMotion.get() !== state.rotate) {
80+
rotateMotion.set(state.rotate)
81+
}
82+
6583
const handleSave = () => {
6684
const img = editor.getImageScaledToCanvas()?.toDataURL()
6785
const rect = editor.getCroppingRect()
@@ -115,7 +133,7 @@ const App = () => {
115133
width={state.width}
116134
height={state.height}
117135
position={state.position}
118-
rotate={state.rotate}
136+
rotate={animatedRotate}
119137
borderRadius={state.width / (100 / state.borderRadius)}
120138
backgroundColor={state.backgroundColor}
121139
showGrid={state.showGrid}
@@ -168,7 +186,8 @@ const App = () => {
168186
{/* Rotation */}
169187
<div className="control-group">
170188
<div className="control-label">
171-
Rotation <span className="value">{state.rotate}°</span>
189+
Rotation{' '}
190+
<span className="value">{Math.round(animatedRotate)}°</span>
172191
</div>
173192
<div className="inline-row">
174193
<input
@@ -340,7 +359,11 @@ const App = () => {
340359
<span className="toggle-track" />
341360
<span className="toggle-text">Lock canvas rotation</span>
342361
</label>
343-
<button className="btn btn-primary" onClick={handleSave}>
362+
<button
363+
className="btn btn-primary"
364+
onClick={handleSave}
365+
disabled={isRotating}
366+
>
344367
Export Preview
345368
</button>
346369
</div>

pnpm-lock.yaml

Lines changed: 60 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)