Skip to content

Instantly share code, notes, and snippets.

@marcelosanchez
Last active May 12, 2025 14:42
Show Gist options
  • Save marcelosanchez/d3d7b2cd0c8b5eb9714fdcd986e9f9ba to your computer and use it in GitHub Desktop.
Save marcelosanchez/d3d7b2cd0c8b5eb9714fdcd986e9f9ba to your computer and use it in GitHub Desktop.
React Native Reanimated: Hook to apply custom pivot rotation transform
import { useSharedValue, useAnimatedStyle, withTiming, useDerivedValue, Easing } from 'react-native-reanimated';
// Matrix functions
const createTranslationMatrix = (dx: number, dy: number): number[] => {
'worklet';
return [1, 0, 0, 0, 1, 0, dx, dy, 1];
};
const createRotationMatrix = (radians: number): number[] => {
'worklet';
const cos = Math.cos(radians);
const sin = Math.sin(radians);
return [cos, -sin, 0, sin, cos, 0, 0, 0, 1];
};
const multiplyMatrices = (A: number[], B: number[]): number[] => {
'worklet';
const C = new Array(9).fill(0);
for (let i = 0; i < 3; i++) {
for (let j = 0; j < 3; j++) {
for (let k = 0; k < 3; k++) {
C[i * 3 + j] += A[i * 3 + k] * B[k * 3 + j];
}
}
}
return C;
};
const invertMatrix = (m: number[]): number[] => {
'worklet';
const det = m[0] * (m[4] * m[8] - m[5] * m[7])
- m[1] * (m[3] * m[8] - m[5] * m[6])
+ m[2] * (m[3] * m[7] - m[4] * m[6]);
if (det === 0) {
console.warn('invertMatrix: The matrix is not invertible (det = 0). Returning original matrix');
return [...m];
}
const invDet = 1 / det;
return [
(m[4] * m[8] - m[5] * m[7]) * invDet,
(m[2] * m[7] - m[1] * m[8]) * invDet,
(m[1] * m[5] - m[2] * m[4]) * invDet,
(m[5] * m[6] - m[3] * m[8]) * invDet,
(m[0] * m[8] - m[2] * m[6]) * invDet,
(m[2] * m[3] - m[0] * m[5]) * invDet,
(m[3] * m[7] - m[4] * m[6]) * invDet,
(m[1] * m[6] - m[0] * m[7]) * invDet,
(m[0] * m[4] - m[1] * m[3]) * invDet,
];
};
const applyMatrix = (m: number[], x: number, y: number): { x: number; y: number } => {
'worklet';
return {
x: m[0] * x + m[1] * y + m[2] * 1,
y: m[3] * x + m[4] * y + m[5] * 1,
};
};
// Custom hook for pivot transformation
const usePivotTransform = (
width: number,
height: number,
initialPivotX: number,
initialPivotY: number,
centerX: number,
centerY: number,
) => {
const rotation = useSharedValue(0);
const boxX = useSharedValue(centerX);
const boxY = useSharedValue(centerY);
const pivotX = useSharedValue(initialPivotX);
const pivotY = useSharedValue(initialPivotY);
// Estilo animado para el View
const animatedStyle = useAnimatedStyle(() => {
'worklet';
const boxXVal = boxX.value;
const boxYVal = boxY.value;
const pivotXVal = pivotX.value;
const pivotYVal = pivotY.value;
const rotationVal = rotation.value;
const rotate = `${rotationVal}deg`;
return {
transform: [
{ translateX: boxXVal - width / 2 },
{ translateY: boxYVal - height / 2 },
{ translateX: pivotXVal - boxXVal },
{ translateY: pivotYVal - boxYVal },
{ rotate },
{ translateX: -(pivotXVal - boxXVal) },
{ translateY: -(pivotYVal - boxYVal) },
],
};
});
const pivotLocal = useDerivedValue(() => {
'worklet';
const rotationVal = rotation.value;
const pivotXVal = pivotX.value;
const pivotYVal = pivotY.value;
const boxXVal = boxX.value;
const boxYVal = boxY.value;
const rotationRadians = (rotationVal * Math.PI) / 180;
const T1 = createTranslationMatrix(boxXVal - width / 2, boxYVal - height / 2);
const T2 = createTranslationMatrix(pivotXVal - boxXVal, pivotYVal - boxYVal);
const R = createRotationMatrix(rotationRadians);
const T3 = createTranslationMatrix(-(pivotXVal - boxXVal), -(pivotYVal - boxYVal));
let M = multiplyMatrices(T1, T2);
M = multiplyMatrices(M, R);
M = multiplyMatrices(M, T3);
const invM = invertMatrix(M);
return applyMatrix(invM, pivotXVal, pivotYVal);
});
const pivotMarkerStyle = useAnimatedStyle(() => {
'worklet';
const pivotLocalValue = pivotLocal.value;
return {
position: 'absolute',
left: pivotLocalValue.x,
top: pivotLocalValue.y,
};
});
const setRotation = (newRotation: number, duration: number = 1000) => {
rotation.value = withTiming(newRotation, { duration, easing: Easing.inOut(Easing.ease) });
};
const move = (dx: number, dy: number, duration: number = 1000) => {
boxX.value = withTiming(boxX.value + dx, { duration });
boxY.value = withTiming(boxY.value + dy, { duration });
};
const updatePivot = (newPivotX: number, newPivotY: number) => {
pivotX.value = newPivotX;
pivotY.value = newPivotY;
};
return {
rotation,
animatedStyle,
pivotMarkerStyle,
pivotLocal,
rotate: setRotation,
move,
updatePivot,
};
};
export default usePivotTransform;
@marcelosanchez
Copy link
Author

Example usage of usePivotTransform hook

This example demonstrates how to use the hook to rotate a View around a custom pivot point using Reanimated 2+.

const RotatingBox = () => {
  const { animatedStyle, rotate, updatePivot } = usePivotTransform(
    100, // width
    100, // height
    150, // pivotX
    150, // pivotY
    200, // centerX
    200  // centerY
  );

  useEffect(() => {
    rotate(90); // Rotate 90 degrees around the custom pivot
  }, []);

  return (
    <Animated.View style={[styles.box, animatedStyle]} />
  );
};

const styles = StyleSheet.create({
  box: {
    width: 100,
    height: 100,
    backgroundColor: 'tomato',
    position: 'absolute',
  },
});

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment