From 7dce180879236c0a85f2bf8136d212c9f1b8e0eb Mon Sep 17 00:00:00 2001
From: skalthoff <32023561+skalthoff@users.noreply.github.com>
Date: Wed, 10 Dec 2025 12:57:15 -0800
Subject: [PATCH] feat: Reimplement expressive play button using layered petal
shapes with rotation animation instead of border radius morphing.
---
.../components/expressive-play-button.tsx | 168 ++++++++----------
1 file changed, 72 insertions(+), 96 deletions(-)
diff --git a/src/components/Player/components/expressive-play-button.tsx b/src/components/Player/components/expressive-play-button.tsx
index 0f2bc5c7..6f3ce685 100644
--- a/src/components/Player/components/expressive-play-button.tsx
+++ b/src/components/Player/components/expressive-play-button.tsx
@@ -16,35 +16,15 @@ import MaterialDesignIcons from '@react-native-vector-icons/material-design-icon
import useHapticFeedback from '../../../hooks/use-haptic-feedback'
/**
- * M3 Expressive Play Button
+ * M3 Expressive Play Button - Flower/Petal Shape
*
- * Features:
- * - Dramatically larger (80px) than other controls
- * - Organic "petal" shape with asymmetric border radii
- * - Shape morphing animation on state change
- * - Filled background with primary color
- * - Scale animation on press
+ * Creates an organic blob by layering multiple rotated rounded rectangles
+ * This creates a flower-petal / organic blob effect that's distinctly
+ * different from a simple circle
*/
-const BUTTON_SIZE = 84
-const ICON_SIZE = 40
-
-// Dramatic organic blob shape - very asymmetric for expressive feel
-// Think: rounded square with one corner much rounder than others
-const PETAL_SHAPE_PLAYING = {
- borderTopLeftRadius: 50, // Very round
- borderTopRightRadius: 28, // Sharper
- borderBottomLeftRadius: 28, // Sharper
- borderBottomRightRadius: 50, // Very round
-}
-
-// Inverted asymmetry when paused - creates noticeable morph
-const PETAL_SHAPE_PAUSED = {
- borderTopLeftRadius: 28, // Sharper
- borderTopRightRadius: 50, // Very round
- borderBottomLeftRadius: 50, // Very round
- borderBottomRightRadius: 28, // Sharper
-}
+const BUTTON_SIZE = 88
+const ICON_SIZE = 42
export default function ExpressivePlayButton(): React.JSX.Element {
const togglePlayback = useTogglePlayback()
@@ -57,54 +37,28 @@ export default function ExpressivePlayButton(): React.JSX.Element {
// Animation values
const scale = useSharedValue(1)
- const morphProgress = useSharedValue(isPlaying ? 1 : 0)
+ const rotation = useSharedValue(0)
- // Update morph progress when state changes
+ // Rotate shape slightly when playing for visual feedback
React.useEffect(() => {
- morphProgress.value = withSpring(isPlaying ? 1 : 0, {
+ rotation.value = withSpring(isPlaying ? 22.5 : 0, {
damping: 15,
- stiffness: 150,
+ stiffness: 100,
})
- }, [isPlaying, morphProgress])
+ }, [isPlaying, rotation])
- const animatedContainerStyle = useAnimatedStyle(() => {
- return {
- transform: [{ scale: scale.value }],
- borderTopLeftRadius: interpolate(
- morphProgress.value,
- [0, 1],
- [PETAL_SHAPE_PAUSED.borderTopLeftRadius, PETAL_SHAPE_PLAYING.borderTopLeftRadius],
- Extrapolation.CLAMP,
- ),
- borderTopRightRadius: interpolate(
- morphProgress.value,
- [0, 1],
- [PETAL_SHAPE_PAUSED.borderTopRightRadius, PETAL_SHAPE_PLAYING.borderTopRightRadius],
- Extrapolation.CLAMP,
- ),
- borderBottomLeftRadius: interpolate(
- morphProgress.value,
- [0, 1],
- [
- PETAL_SHAPE_PAUSED.borderBottomLeftRadius,
- PETAL_SHAPE_PLAYING.borderBottomLeftRadius,
- ],
- Extrapolation.CLAMP,
- ),
- borderBottomRightRadius: interpolate(
- morphProgress.value,
- [0, 1],
- [
- PETAL_SHAPE_PAUSED.borderBottomRightRadius,
- PETAL_SHAPE_PLAYING.borderBottomRightRadius,
- ],
- Extrapolation.CLAMP,
- ),
- }
- })
+ // Animated style for scale and rotation
+ const animatedContainerStyle = useAnimatedStyle(() => ({
+ transform: [{ scale: scale.value }, { rotate: `${rotation.value}deg` }],
+ }))
+
+ // Counter-rotate icon so it stays upright
+ const animatedIconStyle = useAnimatedStyle(() => ({
+ transform: [{ rotate: `${-rotation.value}deg` }],
+ }))
const handlePressIn = () => {
- scale.value = withSpring(0.92, { damping: 15, stiffness: 400 })
+ scale.value = withSpring(0.9, { damping: 15, stiffness: 400 })
trigger('impactMedium')
}
@@ -117,17 +71,32 @@ export default function ExpressivePlayButton(): React.JSX.Element {
await togglePlayback()
}
+ const renderShape = (children: React.ReactNode) => (
+
+ {/* Base layer - rotated rounded rectangle */}
+
+ {/* Second layer - rotated 45° for flower effect */}
+
+ {/* Content overlay */}
+ {children}
+
+ )
+
if (isLoading) {
return (
-
-
-
+
+
+ {renderShape()}
)
@@ -141,19 +110,17 @@ export default function ExpressivePlayButton(): React.JSX.Element {
onPress={handlePress}
testID={isPlaying ? 'pause-button-test-id' : 'play-button-test-id'}
>
-
-
+
+ {renderShape(
+
+
+ ,
+ )}
@@ -165,24 +132,33 @@ const styles = StyleSheet.create({
alignItems: 'center',
justifyContent: 'center',
},
- loadingContainer: {
- alignItems: 'center',
- justifyContent: 'center',
- },
- button: {
+ shapeContainer: {
width: BUTTON_SIZE,
height: BUTTON_SIZE,
alignItems: 'center',
justifyContent: 'center',
+ },
+ petal: {
+ position: 'absolute',
+ width: BUTTON_SIZE * 0.85,
+ height: BUTTON_SIZE * 0.85,
+ borderRadius: BUTTON_SIZE * 0.28, // Creates rounded rectangle
// Shadow for depth
shadowColor: '#000',
shadowOffset: { width: 0, height: 4 },
- shadowOpacity: 0.25,
+ shadowOpacity: 0.3,
shadowRadius: 8,
elevation: 8,
},
+ contentOverlay: {
+ position: 'absolute',
+ width: BUTTON_SIZE,
+ height: BUTTON_SIZE,
+ alignItems: 'center',
+ justifyContent: 'center',
+ },
playIconOffset: {
// Play icon is visually off-center, nudge it right
- marginLeft: 4,
+ marginLeft: 5,
},
})