Hoạt hình Worklet của Houdini

Tăng cường hiệu ứng cho ảnh động trong ứng dụng web

TL;DR: Worklet ảnh động cho phép bạn viết ảnh động bắt buộc chạy ở tốc độ khung hình gốc của thiết bị để đảm bảo độ mượt mà không bị giậtTM, giúp ảnh động của bạn đàn hồi tốt hơn trước tình trạng giật của luồng chính và có thể liên kết để cuộn thay vì thời gian. Worklet ảnh động nằm trong Chrome Canary (phía sau cờ "Các tính năng của Nền tảng web thử nghiệm") và chúng tôi đang lên kế hoạch Bản dùng thử theo nguyên gốc cho Chrome 71. Bạn có thể bắt đầu sử dụng tính năng này dưới dạng một tính năng nâng cao tăng dần ngay hôm nay.

API ảnh động khác?

Thực ra là không, đó là phần mở rộng của những gì chúng tôi đã có và có lý do chính đáng! Hãy bắt đầu từ đầu. Nếu muốn tạo ảnh động cho bất kỳ phần tử DOM nào trên web hiện nay, bạn có 2 1⁄2 lựa chọn: Chuyển đổi CSS để chuyển đổi A sang B đơn giản, Ảnh động CSS cho ảnh động có chu kỳ, phức tạp hơn và API Ảnh động trên web (WAAPI) cho các ảnh động gần như phức tạp. Ma trận hỗ trợ của WAAPI có vẻ khá khó khăn, nhưng nó đang trong quá trình phát triển. Cho đến lúc đó, sẽ có polyfill.

Điểm chung của tất cả các phương thức này là chúng không có trạng thái và theo thời gian. Tuy nhiên, một số hiệu ứng mà nhà phát triển đang thử không phải dựa trên thời gian cũng như không có trạng thái. Ví dụ như công cụ cuộn thị sai khét tiếng, đúng như tên gọi của nó, được cuộn theo hướng cuộn. Ngày nay, việc triển khai công cụ cuộn thị sai có hiệu suất cao trên web thật sự khó khăn.

Vậy còn tính không trạng thái thì sao? Chẳng hạn, hãy nghĩ về thanh địa chỉ của Chrome trên Android. Nếu bạn cuộn xuống, nút đó sẽ ra khỏi chế độ xem. Nhưng khi bạn cuộn lên, thông báo sẽ xuất hiện trở lại, ngay cả khi bạn đã di chuyển xuống nửa dưới của trang đó. Ảnh động không chỉ phụ thuộc vào vị trí cuộn mà còn phụ thuộc vào hướng cuộn trước đó. Đây là nội dung có tính trạng thái.

Một vấn đề khác là định kiểu cho thanh cuộn. Chúng nổi tiếng là không hợp thời – hoặc ít nhất là không đủ kiểu. Nếu tôi muốn dùng một chú mèo màu xanh lơ làm thanh cuộn thì sao? Bất kể bạn chọn kỹ thuật nào, việc xây dựng thanh cuộn tuỳ chỉnh không hiệu quả hay dễ dàng.

Vấn đề là tất cả những điều này đều khó hiểu và khó triển khai một cách hiệu quả. Hầu hết các sự kiện và/hoặc requestAnimationFrame có thể duy trì tốc độ ở tốc độ 60 khung hình/giây, ngay cả khi màn hình của bạn có thể chạy ở tốc độ 90 khung hình/giây, 120 khung hình/giây trở lên và sử dụng một phần ngân sách lớn cho khung hình luồng chính.

Worklet ảnh động mở rộng khả năng của ngăn xếp ảnh động trên web để giúp các loại hiệu ứng này dễ dàng hơn. Trước khi tìm hiểu kỹ hơn, hãy đảm bảo rằng chúng ta đã nắm được kiến thức cơ bản về ảnh động.

Sơ lược về ảnh động và dòng thời gian

WAAPI và Animation Worklet sử dụng rộng rãi dòng thời gian để cho phép bạn sắp xếp ảnh động và hiệu ứng theo cách mình muốn. Phần này cung cấp thông tin ôn tập hoặc giới thiệu nhanh về dòng thời gian và cách xử lý ảnh động.

Mỗi tài liệu có document.timeline. Thời điểm bắt đầu từ 0 khi tài liệu được tạo và tính số mili giây kể từ khi tài liệu bắt đầu tồn tại. Tất cả ảnh động của tài liệu đều hoạt động tương ứng với dòng thời gian này.

Để cụ thể hơn một chút, hãy xem đoạn mã WAAPI này

const animation = new Animation(
  new KeyframeEffect(
    document.querySelector('#a'),
    [
      {
        transform: 'translateX(0)',
      },
      {
        transform: 'translateX(500px)',
      },
      {
        transform: 'translateY(500px)',
      },
    ],
    {
      delay: 3000,
      duration: 2000,
      iterations: 3,
    }
  ),
  document.timeline
);

animation.play();

Khi gọi animation.play(), ảnh động sẽ sử dụng currentTime của dòng thời gian làm thời gian bắt đầu. Ảnh động của chúng ta có độ trễ 3000 mili giây, nghĩa là ảnh động sẽ bắt đầu (hoặc "đang hoạt động") khi tiến trình đạt đến "startTime" (thời gian bắt đầu)

  • 3000. After that time, the animation engine will animate the given element from the first keyframe (translateX(0)), through all intermediate keyframes (translateX(500px)) all the way to the last keyframe (translateY(500px)) in exactly 2000ms, as prescribed by thethời lượngoptions. Since we have a duration of 2000ms, we will reach the middle keyframe when the timeline'scurrentTimeisstartTime + 3000 + 1000and the last keyframe atstartTime + 3000 + 2000. Vấn đề là ở đây, dòng thời gian sẽ kiểm soát vị trí của chúng ta trong ảnh động!

Sau khi đạt đến khung hình chính cuối cùng, ảnh động sẽ quay lại khung hình chính đầu tiên và bắt đầu vòng lặp tiếp theo của ảnh động. Quá trình này lặp lại tổng cộng 3 lần kể từ khi chúng ta đặt iterations: 3. Nếu muốn ảnh động không bao giờ dừng, chúng ta sẽ viết iterations: Number.POSITIVE_INFINITY. Dưới đây là kết quả của mã ở trên.

WAAPI vô cùng mạnh mẽ và còn có nhiều tính năng khác trong API này như tốc độ, độ lệch bắt đầu, trọng số khung hình chính và hành vi lấp đầy sẽ vượt qua phạm vi của bài viết này. Nếu muốn biết thêm, bạn nên đọc bài viết này về Ảnh động CSS trên các thủ thuật CSS.

Viết Worklet ảnh động

Bây giờ, khi đã có khái niệm về dòng thời gian, chúng ta có thể bắt đầu tìm hiểu về Worklet ảnh động và cách thức công cụ này giúp bạn chỉnh sửa dòng thời gian! API Worklet ảnh động không chỉ dựa trên WAAPI, mà còn theo nghĩa của web có thể mở rộng, là một dữ liệu nguyên gốc cấp thấp hơn giúp giải thích cách WAAPI hoạt động. Về cú pháp, chúng cực kỳ giống nhau:

Worklet ảnh động WAAPI (API WAAPI)
new WorkletAnimation(
  'passthrough',
  new KeyframeEffect(
    document.querySelector('#a'),
    [
      {
        transform: 'translateX(0)'
      },
      {
        transform: 'translateX(500px)'
      }
    ],
    {
      duration: 2000,
      iterations: Number.POSITIVE_INFINITY
    }
  ),
  document.timeline
).play();
      
        new Animation(

        new KeyframeEffect(
        document.querySelector('#a'),
        [
        {
        transform: 'translateX(0)'
        },
        {
        transform: 'translateX(500px)'
        }
        ],
        {
        duration: 2000,
        iterations: Number.POSITIVE_INFINITY
        }
        ),
        document.timeline
        ).play();
        

Sự khác biệt nằm ở tham số đầu tiên, là tên của worklet điều khiển ảnh động này.

Phát hiện tính năng

Chrome là trình duyệt đầu tiên có tính năng này, vì vậy, bạn cần đảm bảo mã của mình không chỉ mong đợi AnimationWorklet xuất hiện ở đó. Vì vậy, trước khi tải worklet, chúng ta nên xác định xem trình duyệt của người dùng có hỗ trợ AnimationWorklet hay không bằng một cách kiểm tra đơn giản:

if ('animationWorklet' in CSS) {
  // AnimationWorklet is supported!
}

Tải worklet

Worklet là một khái niệm mới do nhóm chuyên trách Houdini đưa ra để giúp việc xây dựng và mở rộng nhiều API mới trở nên dễ dàng hơn. Chúng tôi sẽ trình bày chi tiết về worklet sau này, nhưng để đơn giản, hiện tại bạn có thể coi chúng là các luồng rẻ và nhẹ (chẳng hạn như worker).

Chúng ta cần đảm bảo đã tải một worklet có tên "truyền qua" trước khi khai báo ảnh động:

// index.html
await CSS.animationWorklet.addModule('passthrough-aw.js');
// ... WorkletAnimation initialization from above ...

// passthrough-aw.js
registerAnimator(
  'passthrough',
  class {
    animate(currentTime, effect) {
      effect.localTime = currentTime;
    }
  }
);

Điều gì đang xảy ra ở đây? Chúng ta đang đăng ký một lớp dưới dạng trình tạo ảnh động bằng cách sử dụng lệnh gọi registerAnimator() củaAnimationWorklet, đặt tên cho lớp này là "truyền qua". Tên này cũng giống với tên chúng ta dùng trong hàm khởi tạo WorkletAnimation() ở trên. Sau khi đăng ký xong, lời hứa do addModule() trả về sẽ được giải quyết và chúng ta có thể bắt đầu tạo ảnh động bằng cách sử dụng worklet đó.

Phương thức animate() của thực thể sẽ được gọi cho mọi khung mà trình duyệt muốn kết xuất, truyền currentTime tiến trình của ảnh động cũng như hiệu ứng hiện đang được xử lý. Chúng ta chỉ có một hiệu ứng là KeyframeEffect và chúng ta đang sử dụng currentTime để đặt localTime của hiệu ứng đó. Do đó, tại sao trình tạo ảnh động này được gọi là "truyền qua". Với mã này cho worklet, WAAPI và AnimationWorklet ở trên hoạt động giống hệt nhau, như bạn có thể thấy trong bản minh hoạ.

Thời gian

Tham số currentTime của phương thức animate()currentTime của tiến trình mà chúng tôi đã truyền đến hàm khởi tạo WorkletAnimation(). Trong ví dụ trước, chúng ta vừa chuyển thời gian đó đến hiệu ứng. Nhưng vì đây là mã JavaScript và chúng ta có thể bóp méo thời gian 💫

function remap(minIn, maxIn, minOut, maxOut, v) {
  return ((v - minIn) / (maxIn - minIn)) * (maxOut - minOut) + minOut;
}
registerAnimator(
  'sin',
  class {
    animate(currentTime, effect) {
      effect.localTime = remap(
        -1,
        1,
        0,
        2000,
        Math.sin((currentTime * 2 * Math.PI) / 2000)
      );
    }
  }
);

Chúng ta đang lấy Math.sin() của currentTime và ánh xạ lại giá trị đó thành phạm vi [0; 2000]. Đây là khoảng thời gian mà hiệu ứng của chúng ta được xác định. Ảnh động trông rất khác mà không cần thay đổi khung hình chính hoặc các tuỳ chọn của ảnh động. Mã worklet có thể phức tạp tuỳ ý và cho phép bạn xác định hiệu ứng nào được phát theo thứ tự và mức độ phát.

Tuỳ chọn đối với Tuỳ chọn

Bạn nên sử dụng lại một worklet và thay đổi các số của công việc đó. Vì lý do này, hàm khởi tạo WorkletAnimation cho phép bạn truyền đối tượng tuỳ chọn đến worklet:

registerAnimator(
  'factor',
  class {
    constructor(options = {}) {
      this.factor = options.factor || 1;
    }
    animate(currentTime, effect) {
      effect.localTime = currentTime * this.factor;
    }
  }
);

new WorkletAnimation(
  'factor',
  new KeyframeEffect(
    document.querySelector('#b'),
    [
      /* ... same keyframes as before ... */
    ],
    {
      duration: 2000,
      iterations: Number.POSITIVE_INFINITY,
    }
  ),
  document.timeline,
  {factor: 0.5}
).play();

Trong ví dụ này, cả hai ảnh động đều được điều khiển bằng cùng một mã nhưng với các tuỳ chọn khác nhau.

Hãy cung cấp thông tin về tiểu bang tại địa phương của bạn!

Như tôi gợi ý ở phần trước, một trong những vấn đề chính về worklet ảnh động cần giải quyết là ảnh động có trạng thái. Cho phép các worklet ảnh động duy trì trạng thái. Tuy nhiên, một trong những đặc điểm cốt lõi của worklet là có thể di chuyển sang một luồng khác hoặc thậm chí là bị huỷ để tiết kiệm tài nguyên. Việc này cũng sẽ phá huỷ trạng thái của chúng. Để ngăn chặn tình trạng mất trạng thái, worklet ảnh động cung cấp một hook được gọi là trước khi huỷ một worklet mà bạn có thể dùng để trả về một đối tượng trạng thái. Đối tượng đó sẽ được truyền đến hàm khởi tạo khi công việc được tạo lại. Khi được tạo ban đầu, tham số đó sẽ là undefined.

registerAnimator(
  'randomspin',
  class {
    constructor(options = {}, state = {}) {
      this.direction = state.direction || (Math.random() > 0.5 ? 1 : -1);
    }
    animate(currentTime, effect) {
      // Some math to make sure that `localTime` is always > 0.
      effect.localTime = 2000 + this.direction * (currentTime % 2000);
    }
    destroy() {
      return {
        direction: this.direction,
      };
    }
  }
);

Mỗi lần bạn làm mới bản minh hoạ này, bạn sẽ có tỷ lệ 50/50 về hướng hình vuông sẽ xoay. Nếu trình duyệt chia nhỏ công việc và di chuyển sang một luồng khác, thì sẽ có một lệnh gọi Math.random() khác khi tạo. Điều này có thể gây ra sự thay đổi hướng đột ngột. Để đảm bảo việc này không xảy ra, chúng tôi trả về các ảnh động được chọn ngẫu nhiên dưới dạng state và sử dụng hướng đó trong hàm khởi tạo nếu được cung cấp.

Bám sát liên tục không gian-thời gian: ScrollTimeline

Như phần trước đã hiển thị, AnimationWorklet cho phép chúng ta xác định bằng cách lập trình mức độ ảnh hưởng của việc triển khai tiến trình đến hiệu ứng của ảnh động. Nhưng cho đến nay, dòng thời gian của chúng tôi vẫn luôn là document.timeline và theo dõi thời gian.

ScrollTimeline mở ra những khả năng mới và cho phép bạn điều khiển ảnh động bằng thao tác cuộn thay vì thời gian. Chúng ta sẽ sử dụng lại worklet "truyền qua" đầu tiên cho bản minh hoạ này:

new WorkletAnimation(
  'passthrough',
  new KeyframeEffect(
    document.querySelector('#a'),
    [
      {
        transform: 'translateX(0)',
      },
      {
        transform: 'translateX(500px)',
      },
    ],
    {
      duration: 2000,
      fill: 'both',
    }
  ),
  new ScrollTimeline({
    scrollSource: document.querySelector('main'),
    orientation: 'vertical', // "horizontal" or "vertical".
    timeRange: 2000,
  })
).play();

Thay vì truyền document.timeline, chúng ta sẽ tạo một ScrollTimeline mới. Bạn có thể đoán được, ScrollTimeline không sử dụng thời gian nhưng vị trí cuộn của scrollSource để đặt currentTime trong worklet. Việc cuộn hoàn toàn đến phần trên cùng (hoặc bên trái) có nghĩa là currentTime = 0, trong khi được cuộn đến tận cùng phía dưới cùng (hoặc bên phải) sẽ đặt currentTime thành timeRange. Nếu cuộn hộp trong bản minh hoạ này, bạn có thể kiểm soát vị trí của hộp màu đỏ.

Nếu bạn tạo một ScrollTimeline với một phần tử không cuộn, thì currentTime của dòng thời gian sẽ là NaN. Vì vậy, đặc biệt là với thiết kế thích ứng, bạn phải luôn chuẩn bị sẵn sàng cho NaN dưới dạng currentTime. Thông thường, bạn có thể đặt giá trị mặc định là 0.

Việc liên kết ảnh động với vị trí cuộn là điều mà nhiều người mong muốn từ lâu, nhưng chưa bao giờ thực sự đạt được ở mức độ trung thực này (ngoài các giải pháp kiểu tin tặc với CSS3D). Worklet ảnh động cho phép triển khai các hiệu ứng này theo cách đơn giản và mang lại hiệu suất cao. Ví dụ: hiệu ứng cuộn thị sai như bản minh hoạ này cho thấy rằng hiện tại, hệ thống chỉ cần một vài dòng để xác định ảnh động theo hướng cuộn.

Tìm hiểu sâu

Worklet

Worklet là ngữ cảnh JavaScript có phạm vi tách biệt và giao diện API rất nhỏ. Giao diện API nhỏ cho phép tối ưu hoá linh hoạt hơn từ trình duyệt, đặc biệt là trên các thiết bị cấp thấp. Ngoài ra, các worklet không liên kết với một vòng lặp sự kiện cụ thể nhưng có thể di chuyển giữa các luồng khi cần. Điều này đặc biệt quan trọng đối với AnimationWorklet.

Bộ tổng hợp NSync

Bạn có thể biết rằng một số thuộc tính CSS nhất định có tốc độ tạo ảnh động nhanh, trong khi một số thuộc tính khác thì không. Một số thuộc tính chỉ cần một số thao tác trên GPU để tạo ảnh động, trong khi một số thuộc tính khác buộc trình duyệt phải bố cục lại toàn bộ tài liệu.

Trong Chrome (như trên nhiều trình duyệt khác), chúng tôi có một quy trình tên là trình tổng hợp. Công việc của chúng tôi là sắp xếp các lớp và kết cấu, sau đó sử dụng GPU để cập nhật màn hình thường xuyên nhất có thể, tốt nhất là nhanh nhất là màn hình có thể cập nhật (thường là 60Hz). Tuỳ thuộc vào việc thuộc tính CSS nào đang được tạo ảnh động, trình duyệt có thể chỉ cần trình tổng hợp để thực hiện việc này, trong khi các thuộc tính khác cần chạy bố cục, đây là thao tác mà chỉ luồng chính mới có thể thực hiện. Tuỳ thuộc vào thuộc tính bạn đang định tạo ảnh động, công việc ảnh động của bạn sẽ được liên kết với luồng chính hoặc chạy trong một luồng riêng để đồng bộ hoá với trình tổng hợp.

Vỗ tay vào cổ tay

Thường chỉ có một quy trình tổng hợp có thể được chia sẻ trên nhiều thẻ, vì GPU là tài nguyên có tính cạnh tranh cao. Nếu trình tổng hợp bị chặn bằng cách nào đó, toàn bộ trình duyệt sẽ tạm dừng và không phản hồi với hoạt động đầu vào của người dùng. Bạn cần phải tránh điều này bằng mọi giá. Vậy điều gì sẽ xảy ra nếu trình tổng hợp không thể phân phối dữ liệu mà trình tổng hợp cần kịp thời để kết xuất khung hình ?

Nếu điều này xảy ra, worklet sẽ được cho phép "tách" (theo thông số kỹ thuật). Khung hình này nằm phía sau trình tổng hợp và trình tổng hợp được phép sử dụng lại dữ liệu của khung hình gần đây nhất để giữ tốc độ khung hình ở mức cao. Rõ ràng, giao diện này sẽ trông giống như hiện tượng giật, nhưng có một điểm khác biệt lớn là trình duyệt vẫn phản hồi với hoạt động đầu vào của người dùng.

Kết luận

Có nhiều khía cạnh trong AnimationWorklet và những lợi ích mà AnimationWorklet mang lại cho web. Lợi ích rõ ràng là khả năng kiểm soát tốt hơn đối với ảnh động và các cách mới để điều khiển ảnh động nhằm mang lại độ chân thực mới về hình ảnh cho web. Tuy nhiên, thiết kế API cũng giúp bạn tăng khả năng chống chọi với hiện tượng giật trong khi vẫn truy cập được vào mọi tính năng mới.

Worklet ảnh động có trong Canary và chúng tôi đang nhắm đến Bản dùng thử gốc với Chrome 71. Chúng tôi đang chờ đợi những trải nghiệm web mới tuyệt vời của bạn và lắng nghe về những điều chúng tôi có thể cải thiện. Ngoài ra, bạn cũng có thể dùng polyfill để cung cấp cho bạn cùng một API nhưng không cung cấp khả năng tách biệt hiệu suất.

Lưu ý rằng hiệu ứng chuyển đổi CSS và ảnh động CSS vẫn là các tuỳ chọn hợp lệ và có thể đơn giản hơn nhiều đối với ảnh động cơ bản. Nhưng nếu bạn cần thú vị, AnimationWorklet sẽ hỗ trợ bạn!