후디니의 애니메이션 Worklet

웹 앱의 애니메이션 강화

요약: Animation Worklet을 사용하면 기기의 네이티브 프레임 속도로 실행되는 명령형 애니메이션을 작성하여 버벅거림이 없는 부드러운 부드러움TM을 처리할 수 있으며, 기본 스레드 버벅거림에 대한 복원력이 더 우수하며 시간 대신 스크롤하도록 연결할 수 있습니다. Animation Worklet은 '실험용 웹 플랫폼 기능' 플래그 뒤에 Chrome Canary에 있으며 Chrome 71용 오리진 트라이얼을 계획하고 있습니다. 지금 점진적 개선 기능으로 사용할 수 있습니다.

다른 애니메이션 API?

실제로는 그렇지 않습니다. 이미 보유하고 있는 확장 프로그램의 연장이며 그럴만한 이유가 있습니다. 처음부터 시작해 봅시다. 현재 웹에서 DOM 요소를 애니메이션화하려는 경우 두 가지 방법 중에서 선택할 수 있습니다. 간단한 A에서 B로의 전환을 위한 CSS 전환, 더 복잡한 시간 기반 애니메이션을 위한 CSS 애니메이션, 거의 임의적으로 복잡한 애니메이션을 위한 Web Animations API(WAAPI)입니다. WAAPI의 지원 매트릭스는 꽤 침울해 보이지만 점차 개선되고 있습니다. 그때까지는 polyfill이 있습니다.

이러한 모든 메서드의 공통점은 스테이트리스(Stateless)이고 시간 기반이라는 점입니다. 하지만 개발자들이 추구하는 효과 중 일부는 시간 기반이나 스테이트리스(Stateless)도 아닙니다. 예를 들어 악명 높은 시차 스크롤러는 이름에서 알 수 있듯이 스크롤 기반입니다. 오늘날 웹에서 성능 기준에 맞는 패럴랙스 스크롤러를 구현하기란 의외로 어렵습니다.

스테이트리스(Stateless)는 어떨까요? 예를 들어 Android의 Chrome 주소 표시줄을 생각해 보세요 아래로 스크롤하면 시야 밖으로 스크롤됩니다. 그러나 두 번 위로 스크롤하면 페이지의 절반 아래로 내려가도 다시 표시됩니다. 애니메이션은 스크롤 위치뿐만 아니라 이전 스크롤 방향에 따라서도 달라집니다. 스테이트풀(Stateful)입니다.

또 다른 문제는 스크롤바의 스타일 지정입니다. 스타일링이 어렵거나 적어도 스타일을 지정할 수 없는 것으로 악명 높습니다. 냥 고양이를 스크롤바로 사용하려면 어떻게 해야 하나요? 어떤 기법을 선택하든 맞춤 스크롤바를 빌드하는 것은 성능이 우수하지도 쉬운 것도 아닙니다.

요점은 이러한 모든 것이 어색하고 효율적으로 구현하는 것이 불가능하다는 것입니다. 대부분은 화면이 90fps 또는 120fps 이상에서 실행될 수 있고 귀중한 기본 스레드 프레임 예산의 일부를 사용하는 경우에도 60fps로 유지할 수 있는 이벤트 또는 requestAnimationFrame에 의존합니다.

애니메이션 Worklet은 이러한 종류의 효과를 더 쉽게 만들 수 있도록 웹 애니메이션 스택의 기능을 확장합니다. 본격적으로 시작하기 전에 애니메이션의 기본 사항을 최신 상태로 유지해야 합니다.

애니메이션 및 타임라인 입문서

WAAPI 및 Animation Worklet은 타임라인을 광범위하게 사용하여 애니메이션과 효과를 원하는 방식으로 조정할 수 있습니다. 이 섹션에서는 타임라인 및 타임라인이 애니메이션과 함께 작동하는 방식을 빠르게 복습하거나 소개합니다.

각 문서에는 document.timeline가 있습니다. 문서가 생성되면 0부터 시작하며, 문서가 존재하기 시작한 이후의 밀리초를 계산합니다. 문서의 모든 애니메이션은 이 타임라인을 기준으로 작동합니다.

좀 더 구체적으로 설명하기 위해 이 WAAPI 스니펫을 살펴보겠습니다.

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();

animation.play()를 호출하면 애니메이션은 타임라인의 currentTime를 시작 시간으로 사용합니다. 애니메이션의 지연 시간은 3,000ms입니다. 즉, 타임라인이 'startTime'에 도달하면 애니메이션이 시작 (또는 '활성' 상태가 됨)됩니다.

  • 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 thedurationoptions. 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`입니다. 요점은 애니메이션에서 타임라인을 제어하는 것입니다.

애니메이션이 마지막 키프레임에 도달하면 첫 번째 키프레임으로 돌아가서 애니메이션의 다음 반복이 시작됩니다. 이 프로세스는 iterations: 3를 설정한 이후 총 3회 반복합니다. 애니메이션이 중단되지 않도록 하려면 iterations: Number.POSITIVE_INFINITY를 작성합니다. 위 코드의 결과는 다음과 같습니다.

WAAPI는 놀라울 정도로 강력하며 이 API에는 이징, 시작 오프셋, 키프레임 가중치 부여, 채우기 동작 등 이 문서의 범위를 넓히는 다양한 기능이 포함되어 있습니다. 자세한 내용은 CSS 기법에 관한 CSS 애니메이션 도움말을 참고하세요.

애니메이션 Worklet 작성

타임라인의 개념을 살펴봤으니 이제 Animation Worklet과 이를 통해 타임라인을 조작하는 방법을 살펴보겠습니다. Animation Worklet API는 WAAPI를 기반으로 할 뿐만 아니라 확장 가능한 웹의 관점에서 WAAPI 작동 방식을 설명하는 하위 수준 프리미티브입니다. 구문 측면에서 매우 유사합니다.

애니메이션 Worklet 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();
        

차이점은 이 애니메이션을 구동하는 worklet의 이름인 첫 번째 매개변수에 있습니다.

기능 감지

Chrome은 이 기능을 제공하는 첫 번째 브라우저이므로 코드에서 AnimationWorklet가 있을 것이라고 기대해서는 안 됩니다. 따라서 Worklet을 로드하기 전에 간단한 검사를 통해 사용자의 브라우저가 AnimationWorklet를 지원하는지 감지해야 합니다.

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

Worklet 로드

Worklet은 많은 새 API를 더 쉽게 빌드하고 확장할 수 있도록 Houdini 태스크포스에서 도입한 새로운 개념입니다. Worklet에 대한 세부정보는 나중에 좀 더 다루겠지만, 지금은 편의상 Worklet과 같은 비용이 저렴하고 가벼운 스레드로 생각하면 됩니다.

애니메이션을 선언하기 전에 '패스 스루'라는 이름의 Worklet을 로드했는지 확인해야 합니다.

// 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;
    }
  }
);

문제가 무엇인가요? AnimationWorklet의 registerAnimator() 호출을 사용하여 '패스 스루'라는 이름을 지정하여 클래스를 애니메이터로 등록합니다. 위의 WorkletAnimation() 생성자에서 사용한 이름과 동일합니다. 등록이 완료되면 addModule()에서 반환된 프로미스가 결정되고 해당 Worklet을 사용하여 애니메이션 만들기를 시작할 수 있습니다.

인스턴스의 animate() 메서드는 브라우저가 렌더링하려는 모든 프레임에 대해 호출되어 애니메이션 타임라인의 currentTime와 현재 처리 중인 효과를 전달합니다. KeyframeEffect라는 하나의 효과만 있고 currentTime를 사용하여 효과의 localTime를 설정합니다. 따라서 이 애니메이터를 '패스 스루'라고 합니다. 이 Worklet에 관한 코드를 사용하면 위의 WAAPI와 AnimationWorklet은 데모에서 볼 수 있듯이 정확히 동일하게 동작합니다.

시간

animate() 메서드의 currentTime 매개변수는 WorkletAnimation() 생성자에 전달한 타임라인의 currentTime입니다. 이전 예에서는 이 시간을 효과에 전달했습니다. 하지만 이 코드는 JavaScript 코드이므로 시간을 왜곡할 수 있습니다. 💫

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)
      );
    }
  }
);

currentTimeMath.sin()를 가져와 효과가 정의된 시간 범위인 [0; 2000] 범위로 다시 매핑합니다. 이제 키프레임이나 애니메이션의 옵션을 변경하지 않고도 애니메이션이 매우 다르게 보입니다. Worklet 코드는 임의적으로 복잡할 수 있으며 어떤 효과가 어떤 순서로 어떤 순서로 재생되는지 프로그래매틱 방식으로 정의할 수 있습니다.

옵션보다 옵션

Worklet을 재사용하여 번호를 변경할 수 있습니다. 이러한 이유로 WorkletAnimation 생성자를 사용하면 옵션 객체를 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();

에서 두 애니메이션은 동일한 코드로 구동되지만 옵션은 서로 다릅니다.

거주 지역의 주를 보여 주세요!

앞서 언급했듯이 애니메이션 Worklet에서 해결하고자 하는 주요 문제 중 하나는 스테이트풀(Stateful) 애니메이션입니다. 애니메이션 Worklet은 상태를 유지할 수 있습니다. 그러나 Worklet의 핵심 기능 중 하나는 다른 스레드로 마이그레이션하거나 리소스를 저장하기 위해 제거될 수 있으며 이 경우 상태도 소멸된다는 것입니다. 상태 손실을 방지하기 위해 애니메이션 Worklet은 상태 객체를 반환하는 데 사용할 수 있는 Worklet이 소멸되기 전에 호출되는 후크를 제공합니다. 이 객체는 worklet이 다시 생성되면 생성자에 전달됩니다. 처음 만들 때 이 매개변수는 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,
      };
    }
  }
);

이 데모를 새로고침할 때마다 정사각형이 회전할 방향이 50/50입니다. 브라우저에서 Worklet을 분해하고 다른 스레드로 이전하면 생성 시 또 다른 Math.random() 호출이 발생하여 갑작스러운 방향 변경이 발생할 수 있습니다. 이러한 일이 발생하지 않도록 하기 위해 무작위로 선택된 애니메이션을 state로 반환하고 생성자가 제공된 경우 이를 사용합니다.

시공간 연속체에 연결하기: ScrollTimeline

이전 섹션에서 살펴본 것처럼 AnimationWorklet을 사용하면 타임라인을 앞당길 때 애니메이션 효과에 미치는 영향을 프로그래매틱 방식으로 정의할 수 있습니다. 하지만 지금까지 타임라인은 항상 시간을 추적하는 document.timeline였습니다.

ScrollTimeline는 새로운 가능성을 열어주며, 이를 통해 시간이 아닌 스크롤을 통해 애니메이션을 구동할 수 있습니다. 이 데모에서는 첫 번째 '패스 스루' Worklet을 재사용합니다.

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();

document.timeline를 전달하는 대신 새 ScrollTimeline를 만듭니다. 짐작하셨겠지만 ScrollTimeline는 시간을 사용하지 않지만 scrollSource의 스크롤 위치를 사용하여 Worklet에서 currentTime를 설정합니다. 상단 (또는 왼쪽)까지 스크롤하면 currentTime = 0을 의미하고, 하단 (또는 오른쪽)까지 스크롤하면 currentTimetimeRange로 설정됩니다. 이 데모에서 상자를 스크롤하면 빨간색 상자의 위치를 제어할 수 있습니다.

스크롤되지 않는 요소로 ScrollTimeline를 만들면 타임라인의 currentTimeNaN가 됩니다. 따라서 특히 반응형 디자인을 염두에 두고 currentTime으로 NaN를 항상 준비해야 합니다. 값을 기본적으로 0으로 하는 것이 합리적인 경우가 많습니다.

애니메이션을 스크롤 위치와 연결하는 것은 오랫동안 추구해 왔지만 이 수준의 충실도에서는 실제로 달성되지 못했습니다 (CSS3D를 사용한 잘못된 해결 방법 제외). Animation Worklet을 사용하면 높은 성능을 발휘하면서도 이러한 효과를 간단하게 구현할 수 있습니다. 예를 들어 이 데모와 같은 시차 스크롤 효과에서는 이제 스크롤 기반 애니메이션을 정의하는 데 몇 줄 밖에 걸리지 않음을 알 수 있습니다.

자세히 들여다보기

Worklet

Worklet은 격리된 범위와 매우 작은 API 노출 영역을 포함하는 JavaScript 컨텍스트입니다. 작은 API 노출 영역을 사용하면 특히 저사양 기기에서 더욱 적극적으로 최적화할 수 있습니다. 또한 Worklet은 특정 이벤트 루프에 바인딩되지 않지만 필요에 따라 스레드 간에 이동할 수 있습니다. 이는 AnimationWorklet에 특히 중요합니다.

합성기 NSync

특정 CSS 속성의 애니메이션 처리 속도는 빠르지만 그렇지 않은 속성도 있습니다. 일부 속성은 GPU에서 애니메이션 작업을 하기만 하면 되는 반면 브라우저에서 전체 문서를 강제로 재배치해야 하는 속성도 있습니다.

다른 많은 브라우저에서와 마찬가지로 Chrome에는 컴포지터라는 프로세스가 있습니다. 이 프로세스는 레이어와 텍스처를 정렬한 다음 GPU를 활용하여 최대한 정기적으로 (화면 업데이트 속도 (보통 60Hz) 이상적으로 화면을 업데이트하는 데 사용합니다.) 애니메이션화할 CSS 속성에 따라 브라우저는 컴포지터가 작동하도록 하고 다른 속성은 레이아웃을 실행해야 할 수 있습니다. 레이아웃은 기본 스레드만 실행할 수 있습니다. 애니메이션을 적용할 속성에 따라 애니메이션 Worklet은 기본 스레드에 결합되거나 컴포지터와 동기화되어 별도의 스레드에서 실행됩니다.

손목을 잡으세요.

GPU는 경합이 심한 리소스이므로 일반적으로 여러 탭에서 공유될 수 있는 컴포지터 프로세스는 하나만 있습니다. 컴포지터가 어떻게 차단되면 전체 브라우저가 멈추고 사용자 입력에 응답하지 않게 됩니다. 어떤 경우에도 이러한 상황을 피해야 합니다. 그렇다면 Worklet이 프레임 렌더링에 필요한 시간 내에 컴포지터에 필요한 데이터를 제공하지 못하면 어떻게 될까요?

이 경우, 사양에 따라 Worklet이 '슬립'될 수 있습니다. 이는 컴포지터 뒤에 있으며 컴포지터는 마지막 프레임의 데이터를 재사용하여 프레임 속도를 유지할 수 있습니다. 시각적으로는 버벅거림처럼 보이지만 가장 큰 차이점은 브라우저가 여전히 사용자 입력에 응답한다는 것입니다.

결론

AnimationWorklet에는 여러 가지 면과 이 속성이 웹에 제공하는 이점이 있습니다. 분명한 이점은 애니메이션을 더 세부적으로 관리하고 애니메이션을 구동하여 웹에 새로운 수준의 시각적 충실도를 제공할 수 있다는 새로운 방법입니다. 그러나 API 설계를 통해 앱의 버벅거림에 대한 복원력이 향상되고 새로운 기능에 모두 동시에 액세스할 수 있습니다.

Animation Worklet은 Canary에 있으며 Chrome 71에서는 오리진 트라이얼을 목표로 합니다. Google은 여러분의 새롭고 훌륭한 웹 환경을 기대하고 있으며 개선의 여지가 있는 부분에 대한 의견을 기다리고 있습니다. polyfill은 같은 API를 제공하지만 성능 분리는 제공하지 않습니다.

CSS 전환 및 CSS 애니메이션은 여전히 유효한 옵션이며 기본 애니메이션의 경우 훨씬 더 간단할 수 있습니다. 하지만 멋진 작업을 해야 한다면 AnimationWorklet을 사용해 보세요.