CSS3 및 바닐라 JavaScript로 HTML5 SVG 채우기 애니메이션

게시 됨: 2022-03-10
빠른 요약 ↬ 이 기사에서는 Awwwards 웹사이트에서 애니메이션 메모 디스플레이를 구축하는 방법을 배울 수 있습니다. HTML5 SVG circle 요소, 해당 획 속성, CSS 변수 및 Vanilla JavaScript로 애니메이션을 적용하는 방법에 대해 설명합니다.

SVG는 S calable Vector Graphics의 약자로 벡터 그래픽을 위한 표준 XML 기반 마크업 언어입니다. 2D 평면에서 점 집합을 결정하여 경로, 곡선 및 모양을 그릴 수 있습니다. 또한 해당 경로에 트위치 속성(예: 획, 색상, 두께, 채우기 등)을 추가하여 애니메이션을 생성할 수 있습니다.

2017년 4월부터 CSS 레벨 3 채우기 및 획 모듈을 사용하면 각 요소에 속성을 설정하는 대신 외부 스타일시트에서 SVG 색상 및 채우기 패턴을 설정할 수 있습니다. 이 자습서에서는 단순한 일반 16진수 색상을 사용하지만 채우기 및 획 속성도 패턴, 그라디언트 및 이미지를 값으로 허용합니다.

참고 : Awwwards 웹사이트 방문 시 애니메이션 메모 표시는 브라우저 너비가 1024px 이상으로 설정된 경우에만 볼 수 있습니다.

참고 디스플레이 프로젝트 데모
최종 결과의 데모(큰 미리보기)
  • 데모: 노트 디스플레이 프로젝트
  • 리포지토리: 노트 디스플레이 리포지토리
점프 후 더! 아래에서 계속 읽기 ↓

파일 구조

먼저 터미널에서 파일을 생성해 보겠습니다.

 mkdir note-display cd note-display touch index.html styles.css scripts.js

HTML

다음은 cssjs 파일을 모두 연결하는 초기 템플릿입니다.

 <html lang="en"> <head> <meta charset="UTF-8"> <title>Note Display</title> <link rel="stylesheet" href="./styles.css"> </head> <body> <script src="./scripts.js"></script> </body> </html>

li 요소는 circle , note 값 및 label 을 포함하는 목록 항목으로 구성됩니다.

항목 요소 및 직계 자식 나열
목록 항목 요소와 그 직계 자식: .circle , .percent.label . (큰 미리보기)

.circle_svg 는 두 개의 <circle> 요소를 래핑하는 SVG 요소입니다. 첫 번째는 채워질 경로이고 두 번째는 애니메이션할 채우기입니다.

SVG 요소
SVG 요소. SVG 래퍼 및 원 태그. (큰 미리보기)

note 는 정수와 소수로 구분되어 서로 다른 글꼴 크기를 적용할 수 있습니다. label 은 단순한 <span> 입니다. 따라서 이 모든 것을 종합하면 다음과 같습니다.

 <li class="note-display"> <div class="circle"> <svg width="84" height="84" class="circle__svg"> <circle cx="41" cy="41" r="38" class="circle__progress circle__progress--path"></circle> <circle cx="41" cy="41" r="38" class="circle__progress circle__progress--fill"></circle> </svg> <div class="percent"> <span class="percent__int">0.</span> <span class="percent__dec">00</span> </div> </div> <span class="label">Transparent</span> </li>

cxcy 속성은 원의 x축 및 y축 중심점을 정의합니다. r 속성은 반경을 정의합니다.

클래스 이름에서 밑줄/대시 패턴을 발견했을 것입니다. block , elementmodifier 나타내는 BEM입니다. 요소 이름을 보다 체계적이고 조직적이며 의미 있게 만드는 방법론입니다.

추천 자료 : BEM에 대한 설명과 BEM이 필요한 이유

템플릿 구조를 완성하기 위해 4개의 목록 항목을 순서가 지정되지 않은 목록 요소로 래핑하겠습니다.

정렬되지 않은 목록 래퍼
정렬되지 않은 목록 래퍼에는 4개의 li 자식이 있습니다(큰 미리보기).
 <ul class="display-container"> <li class="note-display"> <div class="circle"> <svg width="84" height="84" class="circle__svg"> <circle cx="41" cy="41" r="38" class="circle__progress circle__progress--path"></circle> <circle cx="41" cy="41" r="38" class="circle__progress circle__progress--fill"></circle> </svg> <div class="percent"> <span class="percent__int">0.</span> <span class="percent__dec">00</span> </div> </div> <span class="label">Transparent</span> </li> <li class="note-display"> <div class="circle"> <svg width="84" height="84" class="circle__svg"> <circle cx="41" cy="41" r="38" class="circle__progress circle__progress--path"></circle> <circle cx="41" cy="41" r="38" class="circle__progress circle__progress--fill"></circle> </svg> <div class="percent"> <span class="percent__int">0.</span> <span class="percent__dec">00</span> </div> </div> <span class="label">Reasonable</span> </li> <li class="note-display"> <div class="circle"> <svg width="84" height="84" class="circle__svg"> <circle cx="41" cy="41" r="38" class="circle__progress circle__progress--path"></circle> <circle cx="41" cy="41" r="38" class="circle__progress circle__progress--fill"></circle> </svg> <div class="percent"> <span class="percent__int">0.</span> <span class="percent__dec">00</span> </div> </div> <span class="label">Usable</span> </li> <li class="note-display"> <div class="circle"> <svg width="84" height="84" class="circle__svg"> <circle cx="41" cy="41" r="38" class="circle__progress circle__progress--path"></circle> <circle cx="41" cy="41" r="38" class="circle__progress circle__progress--fill"></circle> </svg> <div class="percent"> <span class="percent__int">0.</span> <span class="percent__dec">00</span> </div> </div> <span class="label">Exemplary</span> </li> </ul>

Transparent , Reasonable , UsableExemplary 이라는 레이블이 무엇을 의미하는지 스스로에게 물어봐야 합니다. 프로그래밍에 대해 더 많이 알게 되면 코드 작성이 응용 프로그램을 기능적으로 만드는 것뿐만 아니라 장기적으로 유지 관리하고 확장할 수 있도록 하는 것임을 깨닫게 될 것입니다. 이는 코드를 변경하기 쉬운 경우에만 가능합니다.

" TRUE 라는 약어는 여러분이 작성한 코드가 미래에 변경 사항을 수용할 수 있는지 여부를 결정하는 데 도움이 될 것입니다."

따라서 다음에는 다음과 같이 자문해 보십시오.

  • Transparent : 코드 변경 결과가 명확합니까?
  • Reasonable : 비용적 이점이 가치가 있습니까?
  • Usable : 예상치 못한 시나리오에서 재사용할 수 있습니까?
  • Exemplary : 향후 코드의 예로 고품질을 제시합니까?

참고 : Sandi Metz의 "Practical Object-Oriented Design in Ruby"는 다른 원칙과 함께 TRUE 를 설명하고 디자인 패턴을 통해 이를 달성하는 방법을 설명합니다. 아직 디자인 패턴을 공부할 시간이 없다면 이 책을 취침 시간에 읽는 것을 고려해 보십시오.

CSS

글꼴을 가져오고 모든 항목에 재설정을 적용해 보겠습니다.

 @import url('https://fonts.googleapis.com/css?family=Nixie+One|Raleway:200'); * { padding: 0; margin: 0; box-sizing: border-box; }

box-sizing: border-box 속성은 요소의 전체 너비와 높이에 패딩 및 테두리 값을 포함하므로 요소의 크기를 더 쉽게 계산할 수 있습니다.

참고 : box-sizing 에 대한 시각적 설명은 "CSS 상자 크기 조정으로 삶을 더 쉽게 만들기"를 참조하십시오.

 body { height: 100vh; color: #fff; display: flex; background: #3E423A; font-family: 'Nixie One', cursive; } .display-container { margin: auto; display: flex; }

bodydisplay: flex.display-containermargin-auto 규칙을 결합하여 자식 요소를 세로 및 가로 중앙에 배치할 수 있습니다. .display-container 요소는 flex-container 이기도 합니다. 그렇게 하면 자식이 주 축을 따라 같은 행에 배치됩니다.

.note-display 목록 항목도 flex-container 됩니다. 센터링을 위한 자식이 많기 때문에 justify-contentalign-items 속성을 통해 합시다. 모든 flex-itemscrossmain 을 따라 중앙에 배치됩니다. 이것이 무엇인지 확실하지 않은 경우 "CSS Flexbox Fundamentals Visual Guide"에서 정렬 섹션을 확인하십시오.

 .note-display { display: flex; flex-direction: column; align-items: center; margin: 0 25px; }

스트로크 라이브 끝의 스타일을 모두 지정하는 stroke-width , stroke-opacitystroke-linecap 규칙을 설정하여 원에 스트로크를 적용해 보겠습니다. 다음으로 각 원에 색상을 추가해 보겠습니다.

 .circle__progress { fill: none; stroke-width: 3; stroke-opacity: 0.3; stroke-linecap: round; } .note-display:nth-child(1) .circle__progress { stroke: #AAFF00; } .note-display:nth-child(2) .circle__progress { stroke: #FF00AA; } .note-display:nth-child(3) .circle__progress { stroke: #AA00FF; } .note-display:nth-child(4) .circle__progress { stroke: #00AAFF; }

percent 요소를 절대적으로 위치시키기 위해서는 무엇을 절대적으로 알아야 합니다. .circle 요소는 참조여야 하므로 position: relative 를 추가해 보겠습니다.

참고 : 절대 위치 지정에 대한 더 깊고 시각적인 설명은 "CSS 위치 절대를 한 번만 이해하는 방법"을 참조하십시오.

요소를 중앙에 배치하는 또 다른 방법은 top: 50% , left: 50%transform: translate(-50%, -50%); 요소의 중심을 부모의 중심에 배치합니다.

 .circle { position: relative; } .percent { width: 100%; top: 50%; left: 50%; position: absolute; font-weight: bold; text-align: center; line-height: 28px; transform: translate(-50%, -50%); } .percent__int { font-size: 28px; } .percent__dec { font-size: 12px; } .label { font-family: 'Raleway', serif; font-size: 14px; text-transform: uppercase; margin-top: 15px; }

이제 템플릿은 다음과 같아야 합니다.

완성된 초기 템플릿
완성된 템플릿 요소 및 스타일(큰 미리보기)

채우기 전환

원 애니메이션은 두 개의 원 SVG 속성인 stroke-dasharraystroke-dashoffset 을 사용하여 만들 수 있습니다.

" stroke-dasharray 는 획의 대시 간격 패턴을 정의합니다."

최대 4개의 값을 사용할 수 있습니다.

  • 유일한 정수( stroke-dasharray: 10 )로 설정하면 대시와 간격이 같은 크기를 갖습니다.
  • 두 값( stroke-dasharray: 10 5 )의 경우 첫 번째 값은 대시에 적용되고 두 번째 값은 간격에 적용됩니다.
  • 세 번째 및 네 번째 형식( stroke-dasharray: 10 5 2stroke-dasharray: 10 5 2 3 )은 다양한 크기의 대시와 간격을 생성합니다.
Stroke dasharray 속성 값
stroke-dasharray 속성 값(큰 미리보기)

왼쪽 이미지는 원 둘레 길이인 0에서 238px로 설정되는 stroke-dasharray 속성을 보여줍니다.

두 번째 이미지는 dash 배열의 시작 부분을 오프셋하는 stroke-dashoffset 속성을 나타냅니다. 또한 0에서 원주 길이까지 설정됩니다.

Stroke dasharray 및 dashoffset 속성
stroke-dasharray 스트로크 대시 오프셋 속성(큰 미리보기)

채우기 효과를 내기 위해 stroke-dasharray 를 원주 길이로 설정하여 모든 길이가 간격 없이 큰 대시로 채워지도록 합니다. 또한 동일한 값으로 상쇄하므로 "숨겨집니다". 그런 다음 stroke-dashoffset 이 해당 음표 값으로 업데이트되어 전환 지속 시간에 따라 획을 채웁니다.

속성 업데이트는 CSS 변수를 통해 스크립트에서 수행됩니다. 변수를 선언하고 속성을 설정해 보겠습니다.

 .circle__progress--fill { --initialStroke: 0; --transitionDuration: 0; stroke-opacity: 1; stroke-dasharray: var(--initialStroke); stroke-dashoffset: var(--initialStroke); transition: stroke-dashoffset var(--transitionDuration) ease; }

초기 값을 설정하고 변수를 업데이트하기 위해 document.querySelectorAll 로 모든 .note-display 요소를 선택하는 것부터 시작하겠습니다. transitionDuration900 밀리초로 설정됩니다.

그런 다음 디스플레이 배열을 반복하고 .circle__progress.circle__progress--fill 을 선택하고 HTML에 설정된 r 속성을 추출하여 원주 길이를 계산합니다. 이를 통해 초기 --dasharray--dashoffset 값을 설정할 수 있습니다.

--dashoffset 변수가 100ms setTimeout에 의해 업데이트되면 애니메이션이 발생합니다.

 const displays = document.querySelectorAll('.note-display'); const transitionDuration = 900; displays.forEach(display => { let progress = display.querySelector('.circle__progress--fill'); let radius = progress.r.baseVal.value; let circumference = 2 * Math.PI * radius; progress.style.setProperty('--transitionDuration', `${transitionDuration}ms`); progress.style.setProperty('--initialStroke', circumference); setTimeout(() => progress.style.strokeDashoffset = 50, 100); });

위에서부터 전환을 시작하려면 .circle__svg 요소를 회전해야 합니다.

 .circle__svg { transform: rotate(-90deg); } 
획 속성 전환
획 속성 전환(큰 미리보기)

이제 음표에 상대적인 dashoffset 값을 계산해 보겠습니다. 메모 값은 data-* 속성을 통해 각 li 항목에 삽입됩니다. * 는 필요에 맞는 이름으로 전환할 수 있으며 그런 다음 요소의 데이터 세트를 통해 JavaScript에서 검색할 수 있습니다: element.dataset.* .

참고 : data-* 속성에 대한 자세한 내용은 MDN Web Docs에서 읽을 수 있습니다.

우리의 속성은 " data-note "라고 부를 것입니다:

 <ul class="display-container"> + <li class="note-display" data-note="7.50"> <div class="circle"> <svg width="84" height="84" class="circle__svg"> <circle cx="41" cy="41" r="38" class="circle__progress circle__progress--path"></circle> <circle cx="41" cy="41" r="38" class="circle__progress circle__progress--fill"></circle> </svg> <div class="percent"> <span class="percent__int">0.</span> <span class="percent__dec">00</span> </div> </div> <span class="label">Transparent</span> </li> + <li class="note-display" data-note="9.27"> <div class="circle"> <svg width="84" height="84" class="circle__svg"> <circle cx="41" cy="41" r="38" class="circle__progress circle__progress--path"></circle> <circle cx="41" cy="41" r="38" class="circle__progress circle__progress--fill"></circle> </svg> <div class="percent"> <span class="percent__int">0.</span> <span class="percent__dec">00</span> </div> </div> <span class="label">Reasonable</span> </li> + <li class="note-display" data-note="6.93"> <div class="circle"> <svg width="84" height="84" class="circle__svg"> <circle cx="41" cy="41" r="38" class="circle__progress circle__progress--path"></circle> <circle cx="41" cy="41" r="38" class="circle__progress circle__progress--fill"></circle> </svg> <div class="percent"> <span class="percent__int">0.</span> <span class="percent__dec">00</span> </div> </div> <span class="label">Usable</span> </li> + <li class="note-display" data-note="8.72"> <div class="circle"> <svg width="84" height="84" class="circle__svg"> <circle cx="41" cy="41" r="38" class="circle__progress circle__progress--path"></circle> <circle cx="41" cy="41" r="38" class="circle__progress circle__progress--fill"></circle> </svg> <div class="percent"> <span class="percent__int">0.</span> <span class="percent__dec">00</span> </div> </div> <span class="label">Exemplary</span> </li> </ul>

parseFloat 메서드는 display.dataset.note 에서 반환된 문자열을 부동 소수점 숫자로 변환합니다. offset 은 최대 점수에 도달하기 위해 누락된 백분율을 나타냅니다. 따라서 7.50 음표의 경우 (10 - 7.50) / 10 = 0.25 가 됩니다. 이는 circumference 길이가 해당 값의 25% 만큼 오프셋되어야 함을 의미합니다.

 let note = parseFloat(display.dataset.note); let offset = circumference * (10 - note) / 10;

scripts.js 업데이트:

 const displays = document.querySelectorAll('.note-display'); const transitionDuration = 900; displays.forEach(display => { let progress = display.querySelector('.circle__progress--fill'); let radius = progress.r.baseVal.value; let circumference = 2 * Math.PI * radius; + let note = parseFloat(display.dataset.note); + let offset = circumference * (10 - note) / 10; progress.style.setProperty('--initialStroke', circumference); progress.style.setProperty('--transitionDuration', `${transitionDuration}ms`); + setTimeout(() => progress.style.strokeDashoffset = offset, 100); }); 
획 속성이 음표 값으로 전환됨
획 속성이 음표 값까지 전환됨(큰 미리보기)

계속 진행하기 전에 고유한 방법으로 스토크 전환을 추출해 보겠습니다.

 const displays = document.querySelectorAll('.note-display'); const transitionDuration = 900; displays.forEach(display => { - let progress = display.querySelector('.circle__progress--fill'); - let radius = progress.r.baseVal.value; - let circumference = 2 * Math.PI * radius; let note = parseFloat(display.dataset.note); - let offset = circumference * (10 - note) / 10; - progress.style.setProperty('--initialStroke', circumference); - progress.style.setProperty('--transitionDuration', `${transitionDuration}ms`); - setTimeout(() => progress.style.strokeDashoffset = offset, 100); + strokeTransition(display, note); }); + function strokeTransition(display, note) { + let progress = display.querySelector('.circle__progress--fill'); + let radius = progress.r.baseVal.value; + let circumference = 2 * Math.PI * radius; + let offset = circumference * (10 - note) / 10; + progress.style.setProperty('--initialStroke', circumference); + progress.style.setProperty('--transitionDuration', `${transitionDuration}ms`); + setTimeout(() => progress.style.strokeDashoffset = offset, 100); + }

참고 가치 증가

여전히 0.00 에서 빌드할 음표 값으로 음표 전환이 있습니다. 가장 먼저 할 일은 정수 값과 소수 값을 분리하는 것입니다. 우리는 문자열 메소드 split() 을 사용할 것입니다(문자열이 끊어질 위치를 결정하고 깨진 문자열을 모두 포함하는 배열을 반환하는 인수를 사용합니다). 그것들은 숫자로 변환되고 display 요소 및 정수인지 십진수인지 나타내는 플래그와 함께 increaseNumber() 함수에 인수로 전달됩니다.

 const displays = document.querySelectorAll('.note-display'); const transitionDuration = 900; displays.forEach(display => { let note = parseFloat(display.dataset.note); + let [int, dec] = display.dataset.note.split('.'); + [int, dec] = [Number(int), Number(dec)]; strokeTransition(display, note); + increaseNumber(display, int, 'int'); + increaseNumber(display, dec, 'dec'); });

increaseNumber() 함수에서 className 에 따라 .percent__int 또는 .percent__dec 요소를 선택하고 출력에 소수점이 포함되어야 하는지 여부도 선택합니다. transitionDuration900ms 로 설정했습니다. 이제 예를 들어 0에서 7 사이의 숫자에 애니메이션을 적용하려면 지속 시간을 음표 900 / 7 = 128.57ms 합니다. 결과는 각 증가 반복에 소요되는 시간을 나타냅니다. 이것은 우리의 setInterval128.57ms 마다 실행된다는 것을 의미합니다.

이러한 변수를 설정하고 setInterval 을 정의해 보겠습니다. counter 변수는 요소에 텍스트로 추가되고 각 반복마다 증가합니다.

 function increaseNumber(display, number, className) { let element = display.querySelector(`.percent__${className}`), decPoint = className === 'int' ? '.' : '', interval = transitionDuration / number, counter = 0; let increaseInterval = setInterval(() => { element.textContent = counter + decPoint; counter++; }, interval); } 
무한 카운터 증가
무한 카운터 증가(큰 미리보기)

멋있는! 그것은 가치를 증가시키지만 영원히 그렇게 합니다. 메모가 원하는 값에 도달하면 setInterval 을 지워야 합니다. 이는 clearInterval 함수로 수행됩니다.

 function increaseNumber(display, number, className) { let element = display.querySelector(`.percent__${className}`), decPoint = className === 'int' ? '.' : '', interval = transitionDuration / number, counter = 0; let increaseInterval = setInterval(() => { + if (counter === number) { window.clearInterval(increaseInterval); } element.textContent = counter + decPoint; counter++; }, interval); } 
완성된 노트 디스플레이 프로젝트
완성된 프로젝트(큰 미리보기)

이제 숫자가 메모 값까지 업데이트되고 clearInterval() 함수로 지워집니다.

이 튜토리얼에서는 여기까지입니다. 나는 당신이 그것을 즐겼기를 바랍니다!

좀 더 인터랙티브한 것을 만들고 싶다면 Vanilla JavaScript로 만든 메모리 게임 튜토리얼을 확인하세요. 위치 지정, 원근감, 전환, Flexbox, 이벤트 처리, 시간 제한 및 삼항과 같은 기본 HTML5, CSS3 및 JavaScript 개념을 다룹니다.

즐거운 코딩!