kamituel
kamituel

Reputation: 35950

Why is this animation incorrect?

I'm trying to implement a table (based on CSS-grid at the moment) with a row that is hidden. Upon clicking somewhere, that row should smoothly expand. I want contents of this row to be visible and preserve their correct size during the animation.

This is very similar to this demo, but I'm not implementing a menu. But the fact that in that demo, contents ("Menu item 1", "Menu item 2", ...) have constant size during animation is something I want.

I want to implement this using a FLIP technique as described here to achieve high framerate with ease.

I've added scaleY(0.01) on the row, and scaleY(100) on the inner wrapper, in a hope they would cancel out and thus maintain scale. I also added transform-origin: 0 0 hoping the top edge of the animated row will stay in the same place during the animation.

However, what happens is that the contents are initially way too tall. They also appear to be moving along the Y axis, even when I set transform-origin: 0 0 (but that might be just the effect of the incorrect height).

I tried using Element.animate() as well as manual element.style.transform = ... approaches, to no avail.

My question: why the hidden row isn't animated properly (the height of its contents isn't constant and they appear to move along Y axis)? How to fix this?

let collapsed = Array.from(document.querySelectorAll(".collapsed"));

collapsed.forEach((row, idx) => {
  row.dataset["collapsedIdx"] = idx;
  document.querySelector("label").addEventListener("click", () => {
    let collapsedSelf = row.getBoundingClientRect();
    let rowsBelow = Array.from(
      row.parentElement.querySelectorAll(`[data-collapsed-idx='${idx}'] ~ *`)
    );

    row.classList.remove("collapsed");
    let expandedSelf = row.getBoundingClientRect();
    let diffY = expandedSelf.height - collapsedSelf.height;

    let animationTiming = {
      duration: 2000,
      easing: "linear"
    };

    let wrapper = row.querySelector(":scope > *");

    row.animate(
      [{ transform: `scaleY(0.01)` }, { transform: `scaleY(1)` }],
      animationTiming
    );

    wrapper.animate(
      [{ transform: `scaleY(100) ` },
       { transform: `scaleY(1) ` }],
      animationTiming
    );

    rowsBelow.forEach((rowBelow) => {
      rowBelow.animate([
        { transform: `translateY(-${diffY}px)` },
        { transform: `translateY(0)` }
      ], animationTiming);
    });
  });
});
* {
  box-sizing: border-box;
}

section {
  display: grid;
  grid-template-columns: max-content 1fr;
}

.subsection {
  grid-column: span 2;
}

label, p {
  display: block;
  margin: 0;
}

.collapsible,
.collapsible > * {
  transform-origin: 0 0;
  will-change: transform;
  contain: content;
}

.collapsed {
  height: 0;
  overflow: hidden;
}

/* .collapsed {
  transform: scaleY(.2);
}

.collapsed > * {
  transform: scaleY(5)
}

*:nth-child(8),
*:nth-child(9),
*:nth-child(10),
*:nth-child(11),
*:nth-child(12) { transform: translateY(-35px); }

  */

/* .animate-on-transforms {
  transition: transform 2000ms linear;
} */
<section>
    <label><strong>CLICK ME!!!</strong></label>
    <p>Value 1</p>

    <label>Row 2</label>
    <p>Value 2</p>

    <label>Row 3</label>
    <p>Value 3</p>
    <div class="subsection collapsible collapsed">
      <section>
        <label>Subrow 1</label>
        <p>Subvalue 1</p>

        <label>Subrow 2</label>
        <p>Subvalue 2</p>

        <label>Subrow 3</label>
        <p>Subvalue 3</p>

        <label>Subrow 4</label>
        <p>Subvalue 4</p>

      </section>
    </div>

    <label>Row 4</label>
    <p>Value 4</p>

    <label>Row 5</label>
    <p>Value 5</p>

</section>

Upvotes: 0

Views: 84

Answers (1)

Temani Afif
Temani Afif

Reputation: 272723

I think the issue here is the interpolation. You are assuming that at each step of the animation we will always have scale(x) and scale(1/x) where x within [0,1] but due to rounding I don't think you will have this.

Here is a basic example:

.box,
p{
 transform-origin:0 0;
 transition:1s linear;
}
.box:hover {
  transform:scale(0.1);
}
.box:hover p{
  transform:scale(10);
}
<div class="box">
<p>
Lorem Ipsume<br>
Lorem Ipsume<br>
Lorem Ipsume<br>
Lorem Ipsume<br>
Lorem Ipsume<br>
</p>
</div>

Logically we may think that the text will stay the same but no. It will grow then shrink again. So yes the scales will get cancelled but not along the animation.

Upvotes: 1

Related Questions