Second custom animation breaks first

Following on from this question, I have created an interaction similar to Pinegrow’s rotating card example, but extending it to multiple divs.

To do so, I utilised a custom JavaScript function to calculate the rotation amount in X and Y directions.

While the animation on each axis works individually, when both are active, only the second animation activates (apart from occasional “flashes” where the X direction responds for a brief moment).

The interesting diagnostic is when I am editing the interaction in Pinegrow, and moving my cursor left and right over the container div. The timeline playhead moves as expected, but the adjustments aren’t reflected on screen. However, when I drag the playhead directly, the items on the screen do respond.

Am I doing something wrong, or does it sound like only 1 custom function can be active at a time?

In case it’s relevant, here’s the JS code.

function lookAtX(element, progress)
{
    let elementXCentre = element.offsetLeft + element.clientWidth / 2;
    let container = element.parentElement;
    let cursorXPosition = container.clientWidth * progress;
    let xDisplacement = cursorXPosition - elementXCentre;

    const cursorDistance = 500;
    let angle = Math.atan2(xDisplacement, cursorDistance);
    element.style.transform = "rotateY(" + angle + "rad)";
}

function lookAtY(element, progress)
{
    let elementYCentre = element.offsetTop + element.clientHeight / 2;
    let container = element.parentElement;
    let cursorYPosition = container.clientHeight * progress;
    let yDisplacement = cursorYPosition - elementYCentre;

    const cursorDistance = 500;
    let angle = -Math.atan2(yDisplacement, cursorDistance);
    element.style.transform = "rotateX(" + angle + "rad)";
}

Hi @reflex

I think I understand the issue you’re facing with your rotating card animations. The problem is in how you’re applying the transform property in your JavaScript functions.

When you set element.style.transform in both functions, each one is overwriting the other’s transformation. CSS transforms don’t stack when applied separately, the last one applied wins.

Here’s how you can fix it:

// Global variables to store current angles
let currentXAngle = 0;
let currentYAngle = 0;

function lookAtX(element, progress) {
    let elementXCentre = element.offsetLeft + element.clientWidth / 2;
    let container = element.parentElement;
    let cursorXPosition = container.clientWidth * progress;
    let xDisplacement = cursorXPosition - elementXCentre;

    const cursorDistance = 500;
    currentYAngle = Math.atan2(xDisplacement, cursorDistance);
    
    // Apply both rotations together
    applyTransforms(element);
}

function lookAtY(element, progress) {
    let elementYCentre = element.offsetTop + element.clientHeight / 2;
    let container = element.parentElement;
    let cursorYPosition = container.clientHeight * progress;
    let yDisplacement = cursorYPosition - elementYCentre;

    const cursorDistance = 500;
    currentXAngle = -Math.atan2(yDisplacement, cursorDistance);
    
    // Apply both rotations together
    applyTransforms(element);
}

function applyTransforms(element) {
    element.style.transform = `rotateX(${currentXAngle}rad) rotateY(${currentYAngle}rad)`;
}

This approach stores both rotation angles and applies them together in a single transform statement. The behavior you’re seeing in Pinegrow (timeline playhead moving but animation not showing) happens because each function is overwriting the other’s transform. CSS constraint.

Hope that helps :crossed_fingers:

Many thanks, @Blep. That indeed makes sense. Originally I had been using vars but thought that may cause an issue with scope conflict. Was a red herring.

Your suggested solution comes close. It solves the conflict between X & Y (plus thanks for pointing out the ${} usage).

Unfortunately the issue now is there are 6 cards in the main container. It appears (understandably) the first animation (lookAtX) applies to each card, then the second animation (lookAtY). This means the 6th Y rotation (looking at X) is then applied to each of the cards during the X calculations.

I have some ideas how to address this. It will be easier now that I’m confident it’s restricted to an issue of logic, not something out of my control.

1 Like

For your specific case with 6 cards, we need to track rotation state per element. Here’s a solution that should work better:

// Store rotations per element using WeakMap
const rotations = new WeakMap();

function lookAtX(element, progress) {
    // Initialize if needed
    if (!rotations.has(element)) {
        rotations.set(element, { x: 0, y: 0 });
    }
    
    let elementXCentre = element.offsetLeft + element.clientWidth / 2;
    let container = element.parentElement;
    let cursorXPosition = container.clientWidth * progress;
    let xDisplacement = cursorXPosition - elementXCentre;

    const cursorDistance = 500;
    const angle = Math.atan2(xDisplacement, cursorDistance);
    
    // Update just the Y rotation (around Y axis)
    const currentRotation = rotations.get(element);
    currentRotation.y = angle;
    
    // Apply both rotations
    element.style.transform = `rotateX(${currentRotation.x}rad) rotateY(${currentRotation.y}rad)`;
}

function lookAtY(element, progress) {
    // Initialize if needed
    if (!rotations.has(element)) {
        rotations.set(element, { x: 0, y: 0 });
    }
    
    let elementYCentre = element.offsetTop + element.clientHeight / 2;
    let container = element.parentElement;
    let cursorYPosition = container.clientHeight * progress;
    let yDisplacement = cursorYPosition - elementYCentre;

    const cursorDistance = 500;
    const angle = -Math.atan2(yDisplacement, cursorDistance);
    
    // Update just the X rotation (around X axis)
    const currentRotation = rotations.get(element);
    currentRotation.x = angle;
    
    // Apply both rotations
    element.style.transform = `rotateX(${currentRotation.x}rad) rotateY(${currentRotation.y}rad)`;
}

This approach stores rotation state individually for each card element, so they won’t interfere with each other.

I participate in forums like this because questions often make me consider things I wouldn’t necessarily think about otherwise, so it’s win-win when I can help. This challenge is particularly interesting. It’s worth seeing if this approach can work easily for you.

Hope that helps again :crossed_fingers: and :four_leaf_clover: just in case

1 Like

I had contemplated using an array. Went in a different direction – storing the value in the element itself. Here’s the working version:

function lookAtX(element, progress)
{
    let elementXCentre = element.offsetLeft + element.clientWidth / 2;
    let container = element.parentElement;
    let cursorXPosition = container.clientWidth * progress;
    let xDisplacement = cursorXPosition - elementXCentre;

    const cursorDistance = 500;
    let currentXAngle = element.getAttribute("data-currentXAngle") || 0;
    let currentYAngle = Math.atan2(xDisplacement, cursorDistance);
    
    element.style.transform = "rotateX(${currentXAngle}rad) rotateY(${currentYAngle}rad)";
    element.setAttribute("data-currentYAngle", currentYAngle)
}

function lookAtY(element, progress)
{
    let elementYCentre = element.offsetTop + element.clientHeight / 2;
    let container = element.parentElement;
    let cursorYPosition = container.clientHeight * progress;
    let yDisplacement = cursorYPosition - elementYCentre;

    const cursorDistance = 500;
    let currentXAngle = -Math.atan2(yDisplacement, cursorDistance);
    let currentYAngle = element.getAttribute("data-currentYAngle") || 0;
    
    element.style.transform = "rotateX(${currentXAngle}rad) rotateY(${currentYAngle}rad)";
    element.setAttribute("data-currentXAngle", currentXAngle)
}

While it works – and may be the version I go with – I might tidy up the maths a little bit to make the physics look a little more realistic.

Thank you for digging me out of the holes I fell in.

Great work on solving the rotation issue. Your approach using data attributes is really clever and elegant. I’m glad you found a solution that works for your cards.

Just a small note, I noticed you’re using regular quotes instead of backticks for your template literals. To make the string interpolation work properly, you’ll need:

element.style.transform = `rotateX(${currentXAngle}rad) rotateY(${currentYAngle}rad)`;

I’m amazed you picked that up. I actually am using backticks in the file, but it interfered with the formatting when I was posting the code here, so I made a quick edit, not being aware that it did have special significance.

Anyone wishing to utilise that code for their own work: note that you’ll want to make that (@Blep’s) correction.

1 Like