CSS3 및 바닐라 JavaScript로 HTML5 SVG 채우기 애니메이션
게시 됨: 2022-03-10SVG는 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
다음은 css
및 js
파일을 모두 연결하는 초기 템플릿입니다.
<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_svg
는 두 개의 <circle> 요소를 래핑하는 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>
cx
및 cy
속성은 원의 x축 및 y축 중심점을 정의합니다. r
속성은 반경을 정의합니다.
클래스 이름에서 밑줄/대시 패턴을 발견했을 것입니다. block
, element
및 modifier
나타내는 BEM입니다. 요소 이름을 보다 체계적이고 조직적이며 의미 있게 만드는 방법론입니다.
추천 자료 : BEM에 대한 설명과 BEM이 필요한 이유
템플릿 구조를 완성하기 위해 4개의 목록 항목을 순서가 지정되지 않은 목록 요소로 래핑하겠습니다.
<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
, Usable
및 Exemplary
이라는 레이블이 무엇을 의미하는지 스스로에게 물어봐야 합니다. 프로그래밍에 대해 더 많이 알게 되면 코드 작성이 응용 프로그램을 기능적으로 만드는 것뿐만 아니라 장기적으로 유지 관리하고 확장할 수 있도록 하는 것임을 깨닫게 될 것입니다. 이는 코드를 변경하기 쉬운 경우에만 가능합니다.
" 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; }
body
의 display: flex
와 .display-container
의 margin-auto
규칙을 결합하여 자식 요소를 세로 및 가로 중앙에 배치할 수 있습니다. .display-container
요소는 flex-container
이기도 합니다. 그렇게 하면 자식이 주 축을 따라 같은 행에 배치됩니다.
.note-display
목록 항목도 flex-container
됩니다. 센터링을 위한 자식이 많기 때문에 justify-content
및 align-items
속성을 통해 합시다. 모든 flex-items
은 cross
및 main
을 따라 중앙에 배치됩니다. 이것이 무엇인지 확실하지 않은 경우 "CSS Flexbox Fundamentals Visual Guide"에서 정렬 섹션을 확인하십시오.
.note-display { display: flex; flex-direction: column; align-items: center; margin: 0 25px; }
스트로크 라이브 끝의 스타일을 모두 지정하는 stroke-width
, stroke-opacity
및 stroke-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-dasharray
및 stroke-dashoffset
을 사용하여 만들 수 있습니다.
" stroke-dasharray
는 획의 대시 간격 패턴을 정의합니다."
최대 4개의 값을 사용할 수 있습니다.
- 유일한 정수(
stroke-dasharray: 10
)로 설정하면 대시와 간격이 같은 크기를 갖습니다. - 두 값(
stroke-dasharray: 10 5
)의 경우 첫 번째 값은 대시에 적용되고 두 번째 값은 간격에 적용됩니다. - 세 번째 및 네 번째 형식(
stroke-dasharray: 10 5 2
및stroke-dasharray: 10 5 2 3
)은 다양한 크기의 대시와 간격을 생성합니다.
왼쪽 이미지는 원 둘레 길이인 0에서 238px로 설정되는 stroke-dasharray
속성을 보여줍니다.
두 번째 이미지는 dash 배열의 시작 부분을 오프셋하는 stroke-dashoffset
속성을 나타냅니다. 또한 0에서 원주 길이까지 설정됩니다.
채우기 효과를 내기 위해 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
요소를 선택하는 것부터 시작하겠습니다. transitionDuration
은 900
밀리초로 설정됩니다.
그런 다음 디스플레이 배열을 반복하고 .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
요소를 선택하고 출력에 소수점이 포함되어야 하는지 여부도 선택합니다. transitionDuration
을 900ms
로 설정했습니다. 이제 예를 들어 0에서 7 사이의 숫자에 애니메이션을 적용하려면 지속 시간을 음표 900 / 7 = 128.57ms
합니다. 결과는 각 증가 반복에 소요되는 시간을 나타냅니다. 이것은 우리의 setInterval
이 128.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 개념을 다룹니다.
즐거운 코딩!