Why is my order of transformation applied affecting the way my code behaves

I created this simple cursor for fun, it’s a circle div that follows the mouse with some bounciness (spring and damping). I also wanted it to stretch and squeeze based on the velocity of the mouse so I added that as well, and both works. Now I also added rotation based on the velocity direction of the mouse, now that works too (individually). however when I put them all together it doesn’t work as expected, for example:

if I put the final transform like so : circle.style.transform = ${translateElement} ${scaleElement} ${rotateElement};
everything technically works, but the stretch and squeeze is applied on the same axis after the rotation which means it’s always squeezing from top to bottom (visually) and stretching from right to left (again visually). So it doesn’t look like it’s being stretched towards the mouse, which was the desired effect I was going for

now if I put the transform like this : circle.style.transform = ${translateElement} ${rotateElement} ${scaleElement};

the circle div does not move or scale at all (basically the code is not working without console error), Can anyone help me figure out why?





const circle = document.querySelector('.circle');

const mousePos = { x: 0, y: 0 };
const prevMousePos = { x: 0, y: 0 };
let mouseSpeed = 0;

const circlePos = { x: 0, y: 0 };
const circleVelocity = { x: 0, y: 0 };
const spring = 0.020;
const damping = 0.845;

const maxSqueeze = 0.5;
const maxStretch = 1.5;
const maxMouseSpeed = 1.1;

let lowSpeedStartTime = 0;
let lowSpeedDuration = 0;
const lowSpeedThreshold = 0.5; // 0.5 seconds




document.addEventListener('mousemove', (e) => {
    const currentMousePos = { x: e.clientX, y: e.clientY };
    const dx = currentMousePos.x - prevMousePos.x;
    const dy = currentMousePos.y - prevMousePos.y;

    // Calculate the speed as the magnitude of the velocity vector
    mouseSpeed = Math.sqrt(dx * dx + dy * dy);

    // Update previous mouse position
    prevMousePos.x = currentMousePos.x;
    prevMousePos.y = currentMousePos.y;

    // Update the current mouse position
    mousePos.x = currentMousePos.x;
    mousePos.y = currentMousePos.y;
});

function animate() {
    const ax = (mousePos.x - circlePos.x) * spring;
    const ay = (mousePos.y - circlePos.y) * spring;

    circleVelocity.x += ax;
    circleVelocity.y += ay;
    circleVelocity.x *= damping;
    circleVelocity.y *= damping;

    circlePos.x += circleVelocity.x;
    circlePos.y += circleVelocity.y;

    // calculate rotation angle
    const angle = Math.atan2(circleVelocity.y, circleVelocity.x) * 180 / Math.PI;

    // Calculate the normalized mouse speed
    const normalizedSpeed = Math.min(mouseSpeed, maxMouseSpeed) / maxMouseSpeed;
    
    // Calculate stretch and squeeze factors
    const scaleFactor = 1 + (maxStretch - 1) * normalizedSpeed;
    const squeezeFactor = 1 - (1 - maxSqueeze) * normalizedSpeed;

    if (mouseSpeed <= 1) {
        if (lowSpeedStartTime === 0) {
            lowSpeedStartTime = performance.now();
        }
        lowSpeedDuration = (performance.now() - lowSpeedStartTime) / 1000; // in seconds
    } else {
        lowSpeedStartTime = 0;
        lowSpeedDuration = 0;
    }

    // Apply squeeze/stretch effect
    let scaleX = scaleFactor; // Stretch
    let scaleY = squeezeFactor; // Squeeze
    if (lowSpeedDuration > lowSpeedThreshold) {
        scaleX = 1.0; // Squeeze
        scaleY = 1.0; // Stretch
    }

    const translateElement = `translate(${circlePos.x}px, ${circlePos.y}px)`;
    const rotateElement = `rotate(${angle}deg`;
    const scaleElement = `scale(${scaleX}, ${scaleY})`;

    circle.style.transform = `${translateElement} ${rotateElement} ${scaleElement}`;

    requestAnimationFrame(animate);
}

animate();



document.addEventListener('mouseleave', () => {
    circle.style.opacity = '0'; // Make cursor disappear
});

document.addEventListener('mouseenter', () => {
    circle.style.opacity = '1'; // Make cursor reappear when mouse enters the document again
});

I tried changing the shape of the div to check if the rotation is actually being applied on the first order of transforms and it does work, so there is something wrong with the 2nd order of transform which is not working.

Note: the 2nd order of transform is valid in CSS, I have used it before and it works, it just does not work on this particular scenario and I don’t understand why.