7P by GN⁺ | ★ favorite | 댓글 1개
  • CSS 수학 함수 지원이 늘면서 keyframes 대신 시간 tick 변수 --t 로 애니메이션 상태를 계산하는 접근이 다시 실용화됨
  • @property --t86400000ms 길이의 루트 애니메이션을 조합하면 밀리초 단위 시간이 증가하고, 이를 rotate, translate 같은 속성 계산에 연결할 수 있음
  • step()을 쓰면 8fps, 24fps, 60fps처럼 프레임 레이트를 제한할 수 있고, 브라우저 최적화 덕분에 60fps보다 높은 갱신 빈도도 보통 문제되지 않음
  • min(), mod(), sin(), pow(), round()로 시간 값을 제한·반복·진동·완화 처리해 커스텀 easing과 위치 이동을 keyframes 없이 표현함
  • css-doodle@t, @T, @TS 같은 축약 표현을 제공해 수학식이 많은 데모를 더 짧게 작성하고 빠르게 실험할 수 있음

시간 tick을 CSS 변수로 만들기

  • 과거 접근은 CSS에서 복잡한 수학 계산을 하기 어려워 적용 범위가 제한적이었음
  • 현재 CSS는 mod(), round(), 삼각함수 등 필요한 수학 함수를 지원해 시간 기반 애니메이션을 다시 시도할 수 있음
  • 데모 확인에는 Experimental feature flags 활성화가 필요할 수 있음

기본 동작 방식

  • JavaScript처럼 CSS가 직접 타이머를 시작할 수는 없지만, CSS Houdini API로 커스텀 변수 --t를 정의해 밀리초 단위 시간을 추적할 수 있음
  • 기본 예시는 @property --t<integer>로 선언하고, @keyframes tick에서 0부터 86400000까지 변화시키며, :root86400000ms linear infinite 애니메이션을 적용함
  • --t는 1밀리초마다 1씩 증가하며 1초에는 1000이 됨
  • counter()를 사용하면 현재 --t 값을 화면에 표시할 수 있음
  • rotate: calc(var(--t) * .001turn)처럼 --t 기반 계산을 속성에 연결하면 시간이 흐르며 값이 바뀌고 애니메이션으로 이어짐

프레임 레이트 제어

  • 부드러운 애니메이션에는 60fps 갱신 빈도로 충분함
  • 브라우저 렌더링 최적화가 있어 갱신 빈도가 60fps보다 높아도 보통 문제가 되지 않음
  • 필요하면 animation-timing-functionstep() 을 사용해 프레임 레이트를 직접 제한할 수 있음
    • 8fps: step(calc(86400000/(1000/8)))
    • 24fps: step(calc(86400000/(1000/24)))
    • 60fps: step(calc(86400000/(1000/60)))

증가하는 시간 값을 애니메이션 값으로 바꾸기

  • --t 값은 한 방향으로 계속 증가함
  • angle 값은 360deg를 넘어도 괜찮지만, 모든 CSS 속성이 값을 순환적으로 처리하지는 않음
  • translate: calc(var(--t) * .001px)는 박스를 계속 오른쪽으로 이동시킴
  • min()으로 멈추기

    • 특정 offset에 도달하자마자 멈추게 하려면 min() 을 사용할 수 있음
    • translate: min(270px, calc(var(--t) * .5px))는 이동 값을 270px로 제한함
    • 지속 시간을 정확히 제어하려면 --t 자체를 제한할 수 있음
    • translate: calc(min(3000, var(--t)) * (270px / 3000))
    • 이 계산은 3초 동안 270px 이동하게 만듦
  • mod()로 반복하기

    • 박스가 오른쪽으로 이동한 뒤 처음부터 다시 시작하려면 mod() 를 사용함
    • translate: calc(mod(var(--t)/4, 270) * 1px)로 offset을 반복시킬 수 있음
  • sin()으로 왕복 이동 만들기

    • 왕복 이동은 sin() 으로 표현할 수 있음
    • translate: calc(sin(mod(var(--t)/135, 270)) * 135px)처럼 시간 값을 삼각함수에 연결함

커스텀 easing 함수

  • CSS 수학 함수와 --t 변수로 cubic-bezier()로는 달성하기 어려울 수 있는 커스텀 easing을 만들 수 있음
  • 먼저 --t01 사이 값으로 제한함
    • --t01: calc(min(1000, var(--t)) / 1000)는 1초 동안 0에서 1로 변화함
  • ease-out-cubic

    • ease-out-cubic1 - pow(1 - t, 3) 계산식으로 구성됨
    • --ease-out-cubic 값을 만든 뒤 translate: calc(var(--ease-out-cubic) * 270px)에 연결함
  • ease-out-elastic

    • ease-out-elasticpow(2, -10t) * sin((10t - .75) * 2/3 * PI) + 1 계산식으로 구성됨
    • 같은 방식으로 --ease-out-elastic 값을 만들고 translate에 곱해 270px 이동에 적용함

css-doodle로 수식 줄이기

  • 수식이 복잡해질수록 var()calc()가 코드를 읽기 어렵게 만듦
  • css-doodle 최신 버전은 --t를 표현하는 @t 함수를 추가했고, 인자 안에서 간단한 수학식을 직접 받을 수 있음
  • rotate 표현은 다음처럼 줄어듦
    • 기존: rotate: calc(mod(var(--t) / 1000, 10) * 5deg)
    • css-doodle: rotate: @t(/1000, %10, *5deg)
  • keyframes를 작성하지 않아도 짧은 코드로 수학 기반 애니메이션을 만들 수 있음
  • 매개변수를 바꾸면 새로운 결과를 빠르게 실험할 수 있음
  • @T와 @TS

    • @t 외에 대문자 @T는 하루 시작부터의 time ticks를 나타냄
    • @TS@t(/1000)의 shorthand이며 시간을 초 단위로 추적함
    • 시계 예시는 CodePen에서 볼 수 있음
    • 초침: rotate: @TS(*6, %360deg)
    • 분침: rotate: @TS(/60, *6, %360deg)
    • 시침: rotate: @TS(/60, /12, *6, %360deg)
  • round()로 초침 점프 만들기

    • 초침을 점프하듯 움직이게 하려면 round() 를 사용할 수 있음
    • round()의 세 번째 매개변수는 반올림 간격을 지정함
    • 시계에서는 한 단계가 360 / 60 = 6deg이므로 rotate: round(down, @TS(*6, %360deg), 6deg)를 사용함

데모와 적용 포인트

  • 색상과 위치를 함께 애니메이션하는 예시는 CodePen에서 볼 수 있음
  • 텍스트 색상 애니메이션 예시는 CodePen에 있음
  • keyframes가 더 직관적으로 보일 수 있지만, 수학 계산과 입력 변수가 많은 데모 장면에서는 시간을 변수로 사용하는 방식이 더 다양한 결과를 만들기 쉬움
  • 2024년 5월 17일 업데이트로 텍스트 색상 애니메이션 데모가 추가됨

댓글과 토론

Hacker News 의견들
  • JS로 진행도를 제어하고 싶다면 음수 animation-delay 값을 쓰는 것도 추천함
    예를 들어 animation-delay: -1500ms는 애니메이션을 즉시 시작하되 1.5초 지점으로 건너뜀
    이 값을 JS로 제어하면 CSS 애니메이션을 JS에서 사실상 스크러빙할 수 있어, 모든 애니메이션을 게임 엔진식 compute-update-render 틱 루프와 맞출 수 있음
    https://developer.mozilla.org/en-US/docs/Web/CSS/animation-d...

    • 그러면 iOS 스타일의 스크롤 오프셋 기반 애니메이션도 가능해지는 것 아닌가?
  • 단순한 이징 함수나 한두 채널의 기본 키프레임보다 조금만 복잡해지면 이 접근의 한계에 금방 부딪힘
    지난 몇 년간 Theatre.js를 써왔고 꽤 만족함
    키프레임과 베지어 곡선을 타임라인에서 편집하는 스튜디오 UI와, 그 키프레임을 타임라인 기준으로 보간하는 런타임으로 나뉜 라이브러리임
    여러 애니메이션을 맞춰 움직여야 한다면 써볼 만함
    https://www.theatrejs.com/

  • 매우 인상적이지만, 어쩐지 수십 년 전 Flash로 가능했던 것에 이제야 가까워진 느낌도 듦
    Flash가 돌아오길 바라는 건 아니지만, CSS 애니메이션을 만들기 위한 더 사용자 친화적인 도구가 있었으면 함

  • CSS는 이제 mod(), round(), 삼각함수 같은 수학 함수 지원이 충분해졌음
    CSS는 JavaScript처럼 타이머를 시작할 수는 없지만, 요즘은 CSS Houdini API로 밀리초 단위 시간을 추적하는 사용자 정의 변수를 정의할 수 있음
    그런데 JavaScript가 있는데 왜 이 모든 게 필요할까?
    그리기 계층은 그리기 기본 요소에 집중해야 하지 않나? 왜 더 많은 상위 계층 기능을 거기에 넣는 걸까?

    • CSS 애니메이션은 하드웨어 가속된다는 점을 잊으면 안 됨
      애니메이션을 위해 JS에서 삼각함수를 돌린다면, 아마 매 밀리초마다 UI를 갱신하려고 DOM도 조작하고 있을 텐데 그 비용이 매우 큼
    • CSS는 그리기 계층이 아님
      그리기는 HTML로 구성되거나 JS로 갱신되는 DOM이 주도함
      CSS는 DOM에 대해 무엇을 할지 페인팅 엔진에 매개변수를 넘기며 기본값을 덮어쓰는 식으로 얹혀 있을 뿐임
      DOM 블록을 추가할 수도 있고, 복잡한 알고리즘으로 위치 지정 동작을 바꿀 수도 있으며, 마진조차 사실 그렇게 단순하지 않음
      훌륭한 해법이라고 보진 않지만, DOM 렌더링 중 색상을 바꾸기 위해 HTML 코드를 갱신하지 않아도 되도록 하려다 시작된 것으로 보임
      아마 HTML 코드 갱신이 전혀 즐겁지 않았기 때문일 텐데, 애초에 잘못된 문제를 풀었다고 봄
      그때가 “웹마스터” 시대였다는 점도 있긴 함
    • 떠오르는 장점 하나는 애플리케이션 로직과 표현의 분리
      fetch 로직 안에 로더 DOM 요소의 기하 진행 로직, 예를 들어 회전 각도 계산과 인라인 스타일 적용 같은 것이 섞이지 않으면 읽기 쉬움
      요즘은 초콜릿과 땅콩버터를 따로 두는 방식이 인기 없다는 건 알지만, 이런 관례는 코드가 어디에 있을지, 어디에 넣어야 할지 추론하기 쉽게 경계를 그어줌
    • JS는 기본적으로 단일 스레드라서, 물론 워커가 있긴 하지만, UI를 안정적인 프레임률로 유지하려면 다른 곳으로 작업을 넘길 수 있는 건 무엇이든 좋음
    • CSS에 WebSocket이 들어갈 때까지 기다려 보자!
      농담은 제쳐두고, CSS는 선언적이며 JS와 CSS로 풀 수 있는 문제가 겹치는 건 자연스러운 일이라고 봄
      최근 CSS에 추가되는 새 기능들이 정말 기대되고, 해석되는 코드가 줄어드는 건 좋은 일임
  • 글에서 사용한 사용자 정의 CSS @property는 훌륭하고, 브라우저 지원도 88% 임 [1]
    다만 대체 initial-value를 설정할 때 브라우저별 동작 차이를 조심해야 함
    Chrome은 CSS 변수가 정의되지 않았거나 잘못된 값으로 설정된 경우 initial-value를 사용함
    Firefox는 변수가 정의되지 않았을 때만 initial-value를 사용함
    대부분의 프로젝트에서는 문제가 안 되겠지만, 최근 프로젝트에서는 브라우저 구현 차이를 맞추려고 Firefox에서 기본값을 JavaScript로 설정해야 했음
    [1] https://caniuse.com/?search=%40property

    • Firefox에서 데모를 시험해보고 싶다면 about:config로 가서 layout.css.properties-and-values.enabled를 켜면 됨
    • 필요할 때 JavaScript에서 속성값을 설정하는 방법은 이렇음
      :root{}에 정의된 속성값 가져오기:
      getComputedStyle(document.documentElement).getPropertyValue("--some-custom-css-var");
      속성값 설정하기:
      document.documentElement.style.setProperty("--some-custom-css-var", "some value");
      아직 CSS @property 지원이 켜지지 않은 브라우저에서는 폴리필처럼 쓸 수 있음
  • 최신 Android Chrome에서는 흥미롭게도 Experimental feature flags 없이도 mod() 이전 데모들은 동작하지만, mod()sin()은 아무 일도 하지 않음

  • 한동안 이걸 원했던 이유가 box-breathing 같은 걸 만들고 싶어서였음
    들이쉬기-멈추기-내쉬기-멈추기 단계를 몸에 맞게, 원하는 효과에 맞게 모두 조정할 수 있게 하고 싶었음
    이 페이지의 방식 같은 것이 없이는 JS 없이, 혹은 말도 안 되는 복잡도 없이 구현할 방법을 찾지 못했음
    세상에 스타일시트라니! 이 페이지 데모들이 정말 창의적임

    • 그건 그냥 CSS의 애니메이션 곡선 아닌가?
  • 글은 정말 잘 쓰였음
    Chrome은 아직 CSS mod() 지원을 정식 릴리스하지 않아서, Chrome 미리보기 릴리스를 쓰지 않으면 페이지의 예제 대부분이 애니메이션되지 않음
    틱 제어가 필요 없고 모든 브라우저에서 부드러운 CSS 애니메이션만 원한다면 FLIP 기법이 유용함
    https://medium.com/outsystems-experts/flip-your-60-fps-anima...

  • 페이지 하단의 애니메이션이 매우 인상적임
    개별 요소라기보다 하나의 영상처럼 보임

  • https://RTCode.io는 새로고침 없이, 상태 초기화 없이 동작하는 실시간 웹 플레이그라운드라서 CSS 애니메이션이나 전환을 다루는 사람에게 큰 도움이 될 수 있음
    데모는 https://RTCode.io에 있음
    Pen을 가져오려면 URL의 호스트명을 xcodepen.io로 바꾸면 됨

    • RTCode를 써본 사람 있나? 흥미로워 보이지만, 홈페이지를 보면 실시간 지원이 있는 CodePen 대체재인지, 아니면 프로덕션에도 쓸 수 있는지 조금 헷갈림