字体文本渲染的高效展开/折叠 CSS 动画问题

问题描述 投票:0回答:1

我正在尝试使用 Google 的 this 指南 构建展开/折叠部分动画,以获得最佳性能。文章最后提到

关于此特定变体的警告:Chrome 在动画期间在低 DPI 屏幕上出现模糊文本,因为文本的比例和反比例导致舍入错误。如果您对此详细信息感兴趣,有一个已提交的错误,您可以加注星标并关注

尝试手动修复带有舍入错误的帧,但没有成功。

动画期间 Firefox 的屏幕截图(文字不流畅): text not smooth - FIrefox

动画结束后 Firefox 的屏幕截图(闪烁以平滑文本): Smooth text

一些观察

  • 在 MacBook Pro 屏幕上,Chrome 完全没有问题。
  • 在 MacBook Pro 屏幕上,Firefox - 文本在动画过程中很平滑,但最终闪烁得更粗(如果您从动画元素中省略
    will-change
    ,则会在 Chrome 中出现此问题,但显然对于 FFox 不起作用)。
  • 在较低 DPI 上的显示器行为几乎相同(动画期间不平滑),但不同之处在于,在 Chrome 上,如果我放置
    will-change
    ,则在动画结束后它会保持“不平滑”,否则将闪烁为平滑。

由于代码较长,为了您的方便,我还提供了 Codepen,也嵌入此处。

"use strict";

class Expando {
  constructor() {
    this._el = document.querySelector(".content");
    const toggleEl = document.querySelectorAll(".section-header");
    this._toggleBtn = document.querySelector(".toggle");
    this._sections = [...document.querySelectorAll(".expand-collapse-section")];
    this._isExpanded = new Array(this._sections.length).fill(false);

    this._createEaseAnimations();

    toggleEl.forEach((el) => {
      el.addEventListener("click", (e) => {
        el.querySelector(".toggle").classList.toggle("expanded");
        const section = e.target.closest(".expand-collapse-section");
        const content = section.querySelector(".content");
        const idx = this._sections.indexOf(section);
        this.toggle(content, idx);
      });
    });

    // measure single content element's margin-top (they all have the same margin in CSS)
    const rgx = /(.+)px/;
    const marginTopPx = window.getComputedStyle(this._el, null).marginTop;
    const results = rgx.exec(marginTopPx);
    this._contentTopMargin = +results[1] || 0;
  }

  expand(el, sectionIdx) {
    if (this._isExpanded[sectionIdx]) {
      return;
    }

    this._isExpanded[sectionIdx] = true;

    this._applyAnimation(el, { expand: true });
  }

  collapse(el, sectionIdx) {
    if (!this._isExpanded[sectionIdx]) {
      return;
    }

    this._isExpanded[sectionIdx] = false;

    this._applyAnimation(el, { expand: false });
  }

  toggle(el, sectionIdx) {
    const expanded = this._isExpanded[sectionIdx];

    if (expanded) {
      return this.collapse(el, sectionIdx);
    }

    this.expand(el, sectionIdx);
  }

  _applyAnimation(el, { expand } = opts) {
    function setTranslation(el, { height, start, expand } = opts) {
      const translation = start ? (expand ? -height : 0) : (expand ? 0 : -height);

      if (translation === 0) {
        el.removeAttribute("style");
      } else {
        el.style.transform = `translateY(${translation}px)`;
      }
    }

    const elInner = el.querySelector(".content-inner");
    el.classList.remove("item--expanded");
    el.classList.remove("item--collapsed");
    elInner.classList.remove("item__contents--expanded");
    elInner.classList.remove("item__contents--collapsed");

    const sectionEl = el.closest(".expand-collapse-section");
    const sectionContent = sectionEl.querySelector(".content");
    sectionContent.style.display = "block"; // block to expand, has no effect on collapse (in the end of animation it gets set to none)
    const index = this._sections.indexOf(sectionEl);
    const targetContentHeight = sectionContent.offsetHeight + this._contentTopMargin;

    for (let i = index + 1; i < this._sections.length; i++) {
      const curr = this._sections[i];
      // don't animate yet translation of adjacent sections, just set initial value for animation
      curr.classList.add("notransition"); 
      
      // setting section content to display block pushes the other items by its height as it has transform set, but it still occupies its original height
      // initial value for animation
      setTranslation(curr, { height: targetContentHeight, start: true, expand });
    }
    // the rest of the content below the expandable sections
    const lastSectionSibling = this._sections.slice(-1)[0].nextElementSibling;
    lastSectionSibling.classList.add("notransition");
    setTranslation(lastSectionSibling, { height: targetContentHeight, start: true, expand });

    requestAnimationFrame(() => {
      if (expand) {
        el.classList.add("item--expanded");
        elInner.classList.add("item__contents--expanded");
      } else {
        el.classList.add("item--collapsed");
        elInner.classList.add("item__contents--collapsed");
      }

      sectionEl.offsetHeight; // needed for Firefox on expand

      // sectionEl.offsetHeight; -> not needed in requestAnimationFrame

      for (let i = index + 1; i < this._sections.length; i++) {
        const curr = this._sections[i];

        // trigger translation animation of adjacent sections and rest of the content now
        curr.classList.remove("notransition");
        setTranslation(curr, { height: targetContentHeight, start: false, expand });
        sectionEl.offsetHeight; // needed for Firefox on expand
      }
      lastSectionSibling.classList.remove("notransition");
      setTranslation(lastSectionSibling, { height: targetContentHeight, start: false, expand });

      if (!expand) {
        sectionContent.addEventListener("animationend", () => {
          sectionContent.style.display = "none";
  
          for (let i = index + 1; i < this._sections.length; i++) {
            const curr = this._sections[i];
            // avoid unexpected animations when removing transform inline style in the end of the animation, needs reflow
            curr.classList.add("notransition"); 
            // could also be set to translateY(0)
            curr.removeAttribute("style"); 
            // should force reflow here otherwise there will be no net change in notransition class which would animate transform, which we don't want,
            // we're just removing the unnecessary style attribute
            sectionEl.offsetHeight;
            curr.classList.remove("notransition");
          }

          lastSectionSibling.classList.add("notransition");
          lastSectionSibling.removeAttribute("style");
          sectionEl.offsetHeight;
          lastSectionSibling.classList.remove("notransition");
        }, { once: true });
      }
    });
  }

  _createEaseAnimations() {
    let ease = document.querySelector(".ease");
    if (ease) {
      return ease;
    }

    ease = document.createElement("style");
    ease.classList.add("ease");

    console.log('------------- Expand animation --------------');
    const expandAnimation = [];
    const expandContentsAnimation = [];
    const collapseAnimation = [];
    const collapseContentsAnimation = [];
    for (let i = 0; i <= 100; i++) {
      const step = this._ease(i / 100);

      // Expand animation.
      this._append({
        i,
        step,
        start: 0,
        end: 1,
        outerAnimation: expandAnimation,
        innerAnimation: expandContentsAnimation,
      });

      // Collapse animation.
      this._append({
        i,
        step,
        start: 1,
        end: 0,
        outerAnimation: collapseAnimation,
        innerAnimation: collapseContentsAnimation,
      });
    }

    ease.textContent = `
      @keyframes expandAnimation {
        ${expandAnimation.join("")}
      }

      @keyframes expandContentsAnimation {
        ${expandContentsAnimation.join("")}
      }

      @keyframes collapseAnimation {
        ${collapseAnimation.join("")}
      }

      @keyframes collapseContentsAnimation {
        ${collapseContentsAnimation.join("")}
      }`;

    document.head.appendChild(ease);
    return ease;
  }

  _append({ i, step, start, end, outerAnimation, innerAnimation } = opts) {
    let scale = start + (end - start) * step;
    let invScale = scale === 0 ? 0 : 1 / scale;

    if (start === 0) {
      
      if (i === 11) {
        scale = 0.373;
        invScale = 2.680965147453083;
      }

      if (i === 23) {
        scale = 0.648;
        invScale = 1.5432098765432098;
      }

      if (i === 28) {
        scale = 0.7312;
        invScale = 1.3676148796498906;
      }

      if (i === 41) {
        scale = 0.879;
        invScale =1.1376564277588168;
      }

      if (i === 43) {
        scale = 0.894;
        invScale = 1.1185682326621924;
      }

      if (i === 44) {
        scale = 0.9;
        invScale = 1.1111111111111112;
      }

      if (i === 55) {
        scale = 0.959;
        invScale = 1.0427528675703859;
      }

      if (i === 56) {
        scale = 0.96;
        invScale = 1.0416666666666667;
      }

      if (i === 62) {
        scale = 0.97914;
        invScale = 1.0213044099924424;
      }

      if (i === 64) {
        scale = 0.983;
        invScale = 1.017293997965412;
      }

      if (i === 67) {
        scale = 0.988;
        invScale = 1.0121457489878543;
      }

      if (i === 69) {
        scale = 0.9907648;
        invScale = 1.0093212839212697;
      }

      if (i === 72) {
        scale = 0.99385;
        invScale = 1.0061880565477688;
      }

      if (i === 74) {
        scale = 0.99543;
        invScale = 1.0045909807821745;
      }

      if (i === 85) {
        scale = 0.99949;
        invScale = 1.0005102602327187;
      }

      if (i === 89) {
        scale = 0.9998536;
        invScale = 1.0001464214360982;
      }

      if (i === 90) {
        scale = 0.99995;
        invScale = 1.000050002500125;
      }

      console.log(`${i}: scale: ${scale}, inverse: ${invScale}, scale * inverse = ${scale * invScale}`);
    }

    outerAnimation.push(`
      ${i}% {
        transform: scaleY(${scale});
      }`);

    innerAnimation.push(`
      ${i}% {
        transform: scaleY(${invScale});
      }`);
  }

  _clamp(value, min, max) {
    return Math.max(min, Math.min(max, value));
  }

  _ease(v, pow = 4) {
    v = this._clamp(v, 0, 1);

    return 1 - Math.pow(1 - v, pow);
  }
}

new Expando();
* {
    box-sizing: border-box;
}

html,
body {
    padding: 0;
    font-family: Arial, Helvetica, sans-serif;
}

.expand-collapse-section,
.example-content-below-sections {
    transition: transform .7s ease-out;
    will-change: transform;
}

.section-header {
    border: 1px solid #efefef;
    border-radius: 4px;
    padding: 0.5em;
    cursor: pointer;
    display: flex;
    justify-content: space-between;
    align-items: center;
}

.toggle-wrapper {
    width: 36px;
    height: 36px;
    background: #f7f8f9;
    border-radius: 50%;
    display: flex;
    justify-content: center;
    align-items: center;
}

.toggle {
    background-image: url('data:image/svg+xml,<svg focusable="false" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path fill="%23747878" d="M16.59 8.59L12 13.17 7.41 8.59 6 10l6 6 6-6z"></path></svg>');
    display: inline-block;
    height: 24px;
    width: 24px;
    transition: transform 200ms;
}

.toggle.expanded {
    transform: rotateZ(180deg);
}

.content {
    transform: scaleY(0);
    transform-origin: top left;
    overflow: hidden;
    will-change: transform;
    margin-top: 0.5rem;
    border: 1px solid black;
    border-radius: 5px;
    /* padding: 1em; */
    display: none;
    overflow: hidden;
}

.content-inner {
    transform-origin: top left;
    overflow: hidden;
    transform: scaleY(0);
    will-change: transform;
}

/* need to be margin as with padding, transform makes it push to the top while animating collapse */
.content-inner-spacer {
    margin: 1em;
}

.item--expanded {
    animation-name: expandAnimation;
    animation-duration: 5.7s;
    animation-timing-function: linear;
    animation-fill-mode: forwards;
}

.item__contents--expanded {
    animation-name: expandContentsAnimation;
    animation-duration: 5.7s;
    animation-timing-function: linear;
    animation-fill-mode: forwards;
}

.item--collapsed {
    animation-name: collapseAnimation;
    animation-duration: .7s;
    animation-timing-function: linear;
    animation-fill-mode: forwards;
}

.item__contents--collapsed {
    animation-name: collapseContentsAnimation;
    animation-duration: .7s;
    animation-timing-function: linear;
    animation-fill-mode: forwards;
}

.notransition {
    -webkit-transition: none !important;
    -moz-transition: none !important;
    -o-transition: none !important;
    transition: none !important;
}
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width,minimum-scale=1.0" />
    <title>Document</title>
    <link rel="stylesheet" href="style.css" />
    <script src="script.js" defer></script>
  </head>
  <body>
    <section class="expand-collapse-section first">
      <div class="section-header">
        <div class="section-header-title">Expand / collapse</div>
        <div class="toggle-wrapper">
          <div class="toggle"></div>
        </div>
      </div>
      <div class="content">
        <div class="content-inner">
          <div class="content-inner-spacer">
            Lorem ipsum dolor sit, amet consectetur adipisicing elit. Neque,
          reiciendis quasi laboriosam delectus accusantium aliquam! Alias
          voluptatum, dolore debitis aliquam maxime doloribus eligendi tempora
          amet laborum quis maiores reprehenderit deleniti impedit quo quas
          eius, fugiat atque non accusamus eum esse? Explicabo quam ea
          reprehenderit minus officiis vel et reiciendis ex omnis expedita, ab
          libero veritatis! Suscipit, magni maxime deserunt eaque laborum libero
          atque nesciunt labore consequatur provident. Aut dolor necessitatibus
          sint, dicta facilis sed molestiae laudantium incidunt repellat
          consequuntur, officiis maxime quam, dolorum possimus expedita minus.
          Error quaerat, esse magni quibusdam quis corporis, et tenetur, ullam
          ipsam a ratione fugit.
          </div>
        </div>
      </div>
    </section>

    <section class="expand-collapse-section">
      <div class="section-header">
        <div class="section-header-title">Expand / collapse</div>
        <div class="toggle-wrapper">
          <div class="toggle"></div>
        </div>
      </div>
      <div class="content">
        <div class="content-inner">
          <div class="content-inner-spacer">
            Lorem, ipsum dolor sit amet consectetur adipisicing elit. Facere
            molestiae, quo ratione voluptas iure expedita dolor ad voluptate
            maxime, aspernatur error sequi hic? Harum nobis provident recusandae
            dolor, ea corporis minima animi deserunt, voluptate adipisci
            cupiditate repudiandae inventore quasi commodi dolorum odio saepe
            consequatur nulla. Fugit quo tenetur dolores veritatis!
          </div>
        </div>
      </div>
    </section>

    <section class="expand-collapse-section">
        <div class="section-header">
          <div class="section-header-title">Expand / collapse</div>
          <div class="toggle-wrapper">
            <div class="toggle"></div>
          </div>
        </div>
        <div class="content">
          <div class="content-inner">
            <div class="content-inner-spacer">
              Lorem, ipsum dolor sit amet consectetur adipisicing elit. Facere
              molestiae, quo ratione voluptas iure expedita dolor ad voluptate
              maxime, aspernatur error sequi hic? Harum nobis provident recusandae
              dolor, ea corporis minima animi deserunt, voluptate adipisci
              cupiditate repudiandae inventore quasi commodi dolorum odio saepe
              consequatur nulla. Fugit quo tenetur dolores veritatis!
            </div>
          </div>
        </div>
      </section>

      <section class="expand-collapse-section">
        <div class="section-header">
          <div class="section-header-title">Expand / collapse</div>
          <div class="toggle-wrapper">
            <div class="toggle"></div>
          </div>
        </div>
        <div class="content">
          <div class="content-inner">
            <div class="content-inner-spacer">
              Lorem, ipsum dolor sit amet consectetur adipisicing elit. Facere
              molestiae, quo ratione voluptas iure expedita dolor ad voluptate
              maxime, aspernatur error sequi hic? Harum nobis provident recusandae
              dolor, ea corporis minima animi deserunt, voluptate adipisci
              cupiditate repudiandae inventore quasi commodi dolorum odio saepe
              consequatur nulla. Fugit quo tenetur dolores veritatis!
            </div>
          </div>
        </div>
      </section>

      <section class="expand-collapse-section">
        <div class="section-header">
          <div class="section-header-title">Expand / collapse</div>
          <div class="toggle-wrapper">
            <div class="toggle"></div>
          </div>
        </div>
        <div class="content">
          <div class="content-inner">
            <div class="content-inner-spacer">
              Lorem, ipsum dolor sit amet consectetur adipisicing elit. Facere
              molestiae, quo ratione voluptas iure expedita dolor ad voluptate
              maxime, aspernatur error sequi hic? Harum nobis provident recusandae
              dolor, ea corporis minima animi deserunt, voluptate adipisci
              cupiditate repudiandae inventore quasi commodi dolorum odio saepe
              consequatur nulla. Fugit quo tenetur dolores veritatis!
            </div>
          </div>
        </div>
      </section>

    <p class="example-content-below-sections">Bottom content</p>
  </body>
</html>

如何克服这个问题?我在 Google 的搜索结果建议展开/折叠部分(稍微滚动到人们也会问)看到,他们使用变换来制作动画,但没有问题。

javascript css performance css-animations
1个回答
0
投票

我最终在我正在制作动画的元素上使用了

perspective: 1px
。尽管动画期间有轻微的文本模糊,但这对我来说是一个可行的解决方案,因为:

  • 我真的不明白谷歌在他们的搜索页面中使用了什么,但似乎不是
    perspective
  • 尽管有点模糊,但我们对最终状态感兴趣,即当所有内容都折叠并且动画完成(而不是中间状态)时内容的可读性,这很好。
  • 通常展开/折叠动画很快,模糊几乎不会被注意到。
  • 模糊非常小,而且 CSS 中的标准比例动画也存在文本模糊。

我会接受我自己的答案,但我愿意接受更好的解决方案。

© www.soinside.com 2019 - 2024. All rights reserved.