ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • Pure CSS + SVG 애니메이션 적용 실패기
    프로그래밍/Web 2022. 4. 13. 14:58

    때는 2022년 2월 21일.

    비디오 플레이어 디자인을 고민할 때였다.

    기본, 1줄 프로토타입, 2줄 프로토타입

     

    이왕 만드는 것, 재생-일시정지 버튼에 무언가 포인트를 넣고 싶었다.

     

    아이디어 원천은 LG 음악앱.

    폰은 구렸지만 음악앱 하나는 기똥차게 좋았던 LG..ㅠ

    처음 써봤을때 재생, 리플레이 인터렉션보고 감동받았다. (투박한 유튜브 따위와 비교가 안됨)

     

    대략 이런 느낌 [Dribble: Play -> Pause, Micro Interactions]

     

    먼저 움직이는 SVG를 만들어봤다.

    일시정지 사각형 두개가 중앙에서 만나 깔끔하게 옆으로 솟아나는 재생버튼을 만들기란 어지간히 어려운 일이 아니었다.

    똥손이라 단순한 크기조정과 이동 정도를 구현하면 몰라도, 자체적으로 그래픽 리소스를 만들 자신은 없었다.

    Pure CSS + SVG 조합으로 가장 좋은 방법은 SVG 필름스트립(개념, 구현툴링, lottie-web issue)이지만 애프터 이펙트를 쓸 줄 모르니ㅠㅠ

    필름스트립 기법 자체는 리소스가 부족하던 고전 게임에서도 많이 사용되었다.

    대신 간단한 조정으로 깔끔해 보이면서도 대안이 될만한 애니메이션을 만들어냈다.

     

    1. 우측 사각형이 작아지며 왼쪽으로 이동
    2. 좌측 사각형에 닿게 되면 사다리꼴 모양이 만들어짐
    3. 좌측 사각형은 작아지며 우측으로 조금 이동하다 사라짐
      이때 모양을 연속적으로 관측하면 크기가 작기 때문에 재생버튼과 상당히 유사해짐
    4. 우측 사각형은 작은 재생 버튼으로 교체 후 원래 크기로 복원
      자연스러운 모핑과 눈에띄는 강조 표현..ㅎ

     

    실제 SVG 파일 (눌러보세요)

     

    원래 존재하던 파일 2개 속의 요소만을 이용한것을 볼 수 있다. (왼쪽 사각형, 오른쪽 사각형, 재생버튼)

    <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16" fill="context-fill" fill-opacity="context-fill-opacity">
      <style type="text/css">
        #pauseBar {
          animation: hideBar 2s ease infinite;
          transform-origin: center;
          opacity: 0;
        }
        @keyframes hideBar {
          0% {
            opacity: 1;
            transform: translate(0px);
          }
          75% {
            opacity: 1;
            transform: translate(0px) scale(1);
          }
          80% {
            opacity: 1;
            transform: translate(1px) scale(0.9);
          }
          100% {
            transform: scale(0.8);
          }
        }
    
        #pauseMove {
          animation: move 1s ease-in-out alternate infinite;
          transform-origin: center;
        }
        @keyframes move {
          from {
            transform: translate(-1px) scale(0.95);
          }
          25% {
            transform: translate(-1px) scale(0.8);
          }
          35% {
            d: path("m2.992 13.498 0-10.996a1.5 1.5 0 0 1 2.245-1.303l9.621 5.498a1.5 1.5 0 0 1 0 2.605L5.237 14.8a1.5 1.5 0 0 1-2.245-1.302z");
            transform: translate(-1px) scale(0.6);
          }
    
          36% {
            d: path("m4.5 14-1 0A1.5 1.5 0 0 1 2 12.5l0-9A1.5 1.5 0 0 1 3.5 2l1 0A1.5 1.5 0 0 1 6 3.5l0 9A1.5 1.5 0 0 1 4.5 14z");
            transform: scale(0.75);
          }
          50% {
            transform: scale(0.8);
          }
          to {
            d: path("m11.5 14-1 0A1.5 1.5 0 0 1 9 12.5l0-9A1.5 1.5 0 0 1 10.5 2l1 0A1.5 1.5 0 0 1 13 3.5l0 9a1.5 1.5 0 0 1-1.5 1.5z");
            transform: scale(1);
          }
        }
      </style>
      <path d="m4.5 14-1 0A1.5 1.5 0 0 1 2 12.5l0-9A1.5 1.5 0 0 1 3.5 2l1 0A1.5 1.5 0 0 1 6 3.5l0 9A1.5 1.5 0 0 1 4.5 14z" id="pauseBar" />
      <path d="m2.992 13.498 0-10.996a1.5 1.5 0 0 1 2.245-1.303l9.621 5.498a1.5 1.5 0 0 1 0 2.605L5.237 14.8a1.5 1.5 0 0 1-2.245-1.302z" id="pauseMove"/>
    </svg>

     

    아ㅋㅋ 이제

    • 일시정지 ->  재생:  순방향
    • 재생 -> 일시정지: 역방향

    으로만 만들면 되니 간단하겠네~

     

    싶었으나.. 간과한것.

    DOM 수정은 일체 불가하며, 단 한줄의 Javascript도 사용할 수 없는 상황이었다.

    유일하게 동적인건 [paused]라는 어트리뷰트를 사용할 수 있다는 점..

     

    갑자기 생각난 꼼수로 정방향/역방향으로 계속 움직이는 이미지를 상태가 보였을때만 끊어서 보여주면 어떻게든 되지 않을까 싶었으나 ㅋ

    과연 엄밀하게 틱이 맞아질거란 걱정은 있었다.

    (참고로 animation이 infinite가 아니면 최초 한번밖에 실행이 안되기에 계속 움직이지 않는  의미가 없었다)

    .playButton {
      background-image: url(chrome://global/skin/media/pause-fill.svg);
    }
    .playButton:not([paused]){
      animation: playPlay 1s;
    }
    .playButton[paused] {
      background-image: url(chrome://global/skin/media/play-fill.svg);
      animation: playPaused 1s;
    }
    
    @keyframes playPlay {
      from {
        background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' width='16' height='16' fill='context-fill' fill-opacity='context-fill-opacity'%3E%3Cstyle type='text/css'%3E %23pauseBar %7B animation: hideBar 1s ease reverse infinite; transform-origin: center; opacity: 0; %7D @keyframes hideBar %7B 0%25 %7B opacity: 1; transform: translate(0px); %7D 50%25 %7B opacity: 1; transform: translate(0px) scale(1); %7D 60%25 %7B opacity: 1; transform: translate(1px) scale(0.9); %7D 100%25 %7B transform: scale(0.8); %7D %7D %23pauseMove %7B animation: move 1s ease-in-out infinite; transform-origin: center; %7D @keyframes move %7B from %7B transform: translate(-1px) scale(0.95); %7D 25%25 %7B transform: translate(-1px) scale(0.8); %7D 35%25 %7B d: path('m2.992 13.498 0-10.996a1.5 1.5 0 0 1 2.245-1.303l9.621 5.498a1.5 1.5 0 0 1 0 2.605L5.237 14.8a1.5 1.5 0 0 1-2.245-1.302z'); transform: translate(-1px) scale(0.6); %7D 36%25 %7B d: path('m4.5 14-1 0A1.5 1.5 0 0 1 2 12.5l0-9A1.5 1.5 0 0 1 3.5 2l1 0A1.5 1.5 0 0 1 6 3.5l0 9A1.5 1.5 0 0 1 4.5 14z'); transform: scale(0.75); %7D 50%25 %7B transform: scale(0.8); %7D to %7B d: path('m11.5 14-1 0A1.5 1.5 0 0 1 9 12.5l0-9A1.5 1.5 0 0 1 10.5 2l1 0A1.5 1.5 0 0 1 13 3.5l0 9a1.5 1.5 0 0 1-1.5 1.5z'); transform: scale(1); %7D %7D %3C/style%3E%3Cpath d='m4.5 14-1 0A1.5 1.5 0 0 1 2 12.5l0-9A1.5 1.5 0 0 1 3.5 2l1 0A1.5 1.5 0 0 1 6 3.5l0 9A1.5 1.5 0 0 1 4.5 14z' id='pauseBar' /%3E%3Cpath d='m2.992 13.498 0-10.996a1.5 1.5 0 0 1 2.245-1.303l9.621 5.498a1.5 1.5 0 0 1 0 2.605L5.237 14.8a1.5 1.5 0 0 1-2.245-1.302z' id='pauseMove'/%3E%3C/svg%3E");
      }
      to {
        background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' width='16' height='16' fill='context-fill' fill-opacity='context-fill-opacity'%3E%3Cstyle type='text/css'%3E %23pauseBar %7B animation: hideBar 1s ease reverse infinite; transform-origin: center; opacity: 0; %7D @keyframes hideBar %7B 0%25 %7B opacity: 1; transform: translate(0px); %7D 50%25 %7B opacity: 1; transform: translate(0px) scale(1); %7D 60%25 %7B opacity: 1; transform: translate(1px) scale(0.9); %7D 100%25 %7B transform: scale(0.8); %7D %7D %23pauseMove %7B animation: move 1s ease-in-out infinite; transform-origin: center; %7D @keyframes move %7B from %7B transform: translate(-1px) scale(0.95); %7D 25%25 %7B transform: translate(-1px) scale(0.8); %7D 35%25 %7B d: path('m2.992 13.498 0-10.996a1.5 1.5 0 0 1 2.245-1.303l9.621 5.498a1.5 1.5 0 0 1 0 2.605L5.237 14.8a1.5 1.5 0 0 1-2.245-1.302z'); transform: translate(-1px) scale(0.6); %7D 36%25 %7B d: path('m4.5 14-1 0A1.5 1.5 0 0 1 2 12.5l0-9A1.5 1.5 0 0 1 3.5 2l1 0A1.5 1.5 0 0 1 6 3.5l0 9A1.5 1.5 0 0 1 4.5 14z'); transform: scale(0.75); %7D 50%25 %7B transform: scale(0.8); %7D to %7B d: path('m11.5 14-1 0A1.5 1.5 0 0 1 9 12.5l0-9A1.5 1.5 0 0 1 10.5 2l1 0A1.5 1.5 0 0 1 13 3.5l0 9a1.5 1.5 0 0 1-1.5 1.5z'); transform: scale(1); %7D %7D %3C/style%3E%3Cpath d='m4.5 14-1 0A1.5 1.5 0 0 1 2 12.5l0-9A1.5 1.5 0 0 1 3.5 2l1 0A1.5 1.5 0 0 1 6 3.5l0 9A1.5 1.5 0 0 1 4.5 14z' id='pauseBar' /%3E%3Cpath d='m2.992 13.498 0-10.996a1.5 1.5 0 0 1 2.245-1.303l9.621 5.498a1.5 1.5 0 0 1 0 2.605L5.237 14.8a1.5 1.5 0 0 1-2.245-1.302z' id='pauseMove'/%3E%3C/svg%3E");
      }
    }
    @keyframes playPaused {
      from {
        background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' width='16' height='16' fill='context-fill' fill-opacity='context-fill-opacity'%3E%3Cstyle type='text/css'%3E %23pauseBar %7B animation: hideBar 1s ease infinite; transform-origin: center; opacity: 0; %7D @keyframes hideBar %7B 0%25 %7B opacity: 1; transform: translate(0px); %7D 50%25 %7B opacity: 1; transform: translate(0px) scale(1); %7D 60%25 %7B opacity: 1; transform: translate(1px) scale(0.9); %7D 100%25 %7B transform: scale(0.8); %7D %7D %23pauseMove %7B animation: move 1s ease-in-out reverse infinite; transform-origin: center; %7D @keyframes move %7B from %7B transform: translate(-1px) scale(0.95); %7D 25%25 %7B transform: translate(-1px) scale(0.8); %7D 35%25 %7B d: path('m2.992 13.498 0-10.996a1.5 1.5 0 0 1 2.245-1.303l9.621 5.498a1.5 1.5 0 0 1 0 2.605L5.237 14.8a1.5 1.5 0 0 1-2.245-1.302z'); transform: translate(-1px) scale(0.6); %7D 36%25 %7B d: path('m4.5 14-1 0A1.5 1.5 0 0 1 2 12.5l0-9A1.5 1.5 0 0 1 3.5 2l1 0A1.5 1.5 0 0 1 6 3.5l0 9A1.5 1.5 0 0 1 4.5 14z'); transform: scale(0.75); %7D 50%25 %7B transform: scale(0.8); %7D to %7B d: path('m11.5 14-1 0A1.5 1.5 0 0 1 9 12.5l0-9A1.5 1.5 0 0 1 10.5 2l1 0A1.5 1.5 0 0 1 13 3.5l0 9a1.5 1.5 0 0 1-1.5 1.5z'); transform: scale(1); %7D %7D %3C/style%3E%3Cpath d='m4.5 14-1 0A1.5 1.5 0 0 1 2 12.5l0-9A1.5 1.5 0 0 1 3.5 2l1 0A1.5 1.5 0 0 1 6 3.5l0 9A1.5 1.5 0 0 1 4.5 14z' id='pauseBar' /%3E%3Cpath d='m2.992 13.498 0-10.996a1.5 1.5 0 0 1 2.245-1.303l9.621 5.498a1.5 1.5 0 0 1 0 2.605L5.237 14.8a1.5 1.5 0 0 1-2.245-1.302z' id='pauseMove'/%3E%3C/svg%3E");
      }
      to {
        background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' width='16' height='16' fill='context-fill' fill-opacity='context-fill-opacity'%3E%3Cstyle type='text/css'%3E %23pauseBar %7B animation: hideBar 1s ease infinite; transform-origin: center; opacity: 0; %7D @keyframes hideBar %7B 0%25 %7B opacity: 1; transform: translate(0px); %7D 50%25 %7B opacity: 1; transform: translate(0px) scale(1); %7D 60%25 %7B opacity: 1; transform: translate(1px) scale(0.9); %7D 100%25 %7B transform: scale(0.8); %7D %7D %23pauseMove %7B animation: move 1s ease-in-out reverse infinite; transform-origin: center; %7D @keyframes move %7B from %7B transform: translate(-1px) scale(0.95); %7D 25%25 %7B transform: translate(-1px) scale(0.8); %7D 35%25 %7B d: path('m2.992 13.498 0-10.996a1.5 1.5 0 0 1 2.245-1.303l9.621 5.498a1.5 1.5 0 0 1 0 2.605L5.237 14.8a1.5 1.5 0 0 1-2.245-1.302z'); transform: translate(-1px) scale(0.6); %7D 36%25 %7B d: path('m4.5 14-1 0A1.5 1.5 0 0 1 2 12.5l0-9A1.5 1.5 0 0 1 3.5 2l1 0A1.5 1.5 0 0 1 6 3.5l0 9A1.5 1.5 0 0 1 4.5 14z'); transform: scale(0.75); %7D 50%25 %7B transform: scale(0.8); %7D to %7B d: path('m11.5 14-1 0A1.5 1.5 0 0 1 9 12.5l0-9A1.5 1.5 0 0 1 10.5 2l1 0A1.5 1.5 0 0 1 13 3.5l0 9a1.5 1.5 0 0 1-1.5 1.5z'); transform: scale(1); %7D %7D %3C/style%3E%3Cpath d='m4.5 14-1 0A1.5 1.5 0 0 1 2 12.5l0-9A1.5 1.5 0 0 1 3.5 2l1 0A1.5 1.5 0 0 1 6 3.5l0 9A1.5 1.5 0 0 1 4.5 14z' id='pauseBar' /%3E%3Cpath d='m2.992 13.498 0-10.996a1.5 1.5 0 0 1 2.245-1.303l9.621 5.498a1.5 1.5 0 0 1 0 2.605L5.237 14.8a1.5 1.5 0 0 1-2.245-1.302z' id='pauseMove'/%3E%3C/svg%3E");
      }
    }

    역시 불안한 예상은 벗어나지 않구, 처음과 고장난 시계처럼만 맞아떨어졌다.

    뒤틀리는 싱크 ㅠㅠㅠ

     

    다음 시도는 약간 더 복잡해질수 밖에.

    • 재생->일시정지까지만
    • 일시정지->재생까지만

    으로 딱 정해진 일만 하게하자라는 아이디어였다.

     

    그런데 아까 구현을 보았다시피 진행속도는 ease를 따른다.

    cubic-bezier(.25, .1, .25, 1)

    전체가 2초라지만 1초가 절반이 아니며, 가속이 그대로 이어가도록 나누기가 어렵다는 의미다.

     

    베지어 곡선을 나눠서 진행하기 위해 스택오버플로우 답변을 바탕으로 간단하게 코딩해봤다.

     

    GitHub - black7375/SplitCubicalBezier

    Contribute to black7375/SplitCubicalBezier development by creating an account on GitHub.

    github.com

    다음처럼 나눠진 값이 출력된다.

    import { presets, createCubicBezier, getSplitCubicBezier } from "splitCubicalBezier";
    
    const linear1 = presets.linear;                    // Same as linear2
    const linear2 = createCubicBezier(x1, y1, x2, y2); // { xs: [ 0, 0, 1, 1 ], ys: [ 0, 0, 1, 1 ] }
    const results = getSplitCubicBezier(linear2, 0.5); // { left: [ 0, 0, 0.5, 0.5 ], right: [ 0.5, 0.5, 1, 1 ] }

     

    우리가 나눌 구간은 @keyframe hidebar 75%로 먼저 키프레임 각 구간의 시간을 정해주고,

    각각에 해당하는 가속곡선을 구해줬다.

    위와 같은 식? (예제라 실제값이랑 다를수도? 당시에 기록용으로 남겨놓기 위한 짤)

     

     pause 애니메이션,

    <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16" fill="context-fill" fill-opacity="context-fill-opacity">
      <style type="text/css">
        #pauseBar {
          animation: hideBar 0.65s cubic-bezier(0, 0, 0.51, 0.74) alternate;
          transform-origin: center;
        }
        @keyframes hideBar {
          from {
            transform: translate(0.97px) scale(0.85);
          }
          10% {
            transform: translate(0px) scale(1);
          }
          to {
            transform: translate(0px);
          }
        }
    
        #pauseMove {
          animation: move 0.65s cubic-bezier(0.31, 0.41, 0.57, 1) alternate;
          transform-origin: center;
        }
        @keyframes move {
          from {
            d: path("m4.5 14-1 0A1.5 1.5 0 0 1 2 12.5l0-9A1.5 1.5 0 0 1 3.5 2l1 0A1.5 1.5 0 0 1 6 3.5l0 9A1.5 1.5 0 0 1 4.5 14z");
            transform: scale(0.75);
          }
          23% {
            transform: scale(0.8);
          }
          to {
            d: path("m11.5 14-1 0A1.5 1.5 0 0 1 9 12.5l0-9A1.5 1.5 0 0 1 10.5 2l1 0A1.5 1.5 0 0 1 13 3.5l0 9a1.5 1.5 0 0 1-1.5 1.5z");
            transform: scale(1);
          }
        }
      </style>
      <path d="m4.5 14-1 0A1.5 1.5 0 0 1 2 12.5l0-9A1.5 1.5 0 0 1 3.5 2l1 0A1.5 1.5 0 0 1 6 3.5l0 9A1.5 1.5 0 0 1 4.5 14z" id="pauseBar" />
      <path d="m11.5 14-1 0A1.5 1.5 0 0 1 9 12.5l0-9A1.5 1.5 0 0 1 10.5 2l1 0A1.5 1.5 0 0 1 13 3.5l0 9a1.5 1.5 0 0 1-1.5 1.5z" id="pauseMove" />
    </svg>

    play 애니메이션

    <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16" fill="context-fill" fill-opacity="context-fill-opacity">
      <style type="text/css">
        #pauseBar {
          animation: hideBar 0.35s cubic-bezier(0.34, 0.62, 0.68, 1) alternate;
          transform-origin: center;
          opacity: 0;
        }
        @keyframes hideBar {
          33% {
            opacity: 1;
            transform: translate(0.97px) scale(0.85);
          }
          47% {
            opacity: 1;
            transform: translate(0.97px) scale(0.85);
          }
          to {
            transform: scale(0.8);
          }
        }
    
        #playMove {
          animation: move 0.35s cubic-bezier(0.43, 0, 0.67, 0.66) alternate;
          transform-origin: center;
        }
        @keyframes move {
          from {
            d: path("m2.992 13.498 0-10.996a1.5 1.5 0 0 1 2.245-1.303l9.621 5.498a1.5 1.5 0 0 1 0 2.605L5.237 14.8a1.5 1.5 0 0 1-2.245-1.302z");
            transform: translate(-1px) scale(0.6);
          }
          29% {
            transform: translate(-1px) scale(0.8);
          }
          to {
            transform: translate(-1px) scale(0.95);
          }
        }
      </style>
      <path d="m4.5 14-1 0A1.5 1.5 0 0 1 2 12.5l0-9A1.5 1.5 0 0 1 3.5 2l1 0A1.5 1.5 0 0 1 6 3.5l0 9A1.5 1.5 0 0 1 4.5 14z" id="pauseBar" />
      <path d="m2.992 13.498 0-10.996a1.5 1.5 0 0 1 2.245-1.303l9.621 5.498a1.5 1.5 0 0 1 0 2.605L5.237 14.8a1.5 1.5 0 0 1-2.245-1.302z" id="playMove"/>
    </svg>

    근데 역시 적용해보니 마음처럼 안되드란.

     

    이렇게 뻘짓을 하다가 구현 실패는 하고 시간도 날라갔다..ㅠㅠㅠ

    좋은 아이디어 있으면 댓글 주세요.

     

    한줄요약:  예능은 예능으로.

     


    지금보니 키프레임 구간을 나누는 코드가 깃허브에 반영 안되어 있네요.

    집에가서 있나 찾아보겠습니다.

    댓글

Designed by black7375.