본문 바로가기
Javascript

SVG 모션 Path 그리기

by F.E.D 2020. 4. 18.

CSS 모션 경로를 사용하면 사용자 정의 사용자 정의 경로를 따라 요소에 애니메이션을 적용 할 수 있습니다. 

이러한 경로는 SVG 경로와 동일한 구조를 따릅니다. 

`offset-path`를 사용하여 요소의 경로를 정의합니다.

 

.block {
  offset-path: path('M20,20 C20,100 200,0 200,100');
}

위 요소는 상대적인 offset-path입니다. 하지만 여기서 문제점은 크기에 따라 반응하지 않는 다는 것입니다.

왜냐하면 해당 숫자들은 `px` 기반이기 때문이지요.

 

스테이지를 설정하기 위해 offset-distance 속성은 요소가 해당 경로에서 있어야하는 위치를 나타냅니다.

 

https://codepen.io/jh3y/pen/cd95c54d57891b095df99eaa02cdbd67

 

CSS Motion Path offset-distance

...

codepen.io

요소가 경로를 따라 거리를 정의 할 수있을뿐만 아니라 오프셋 회전을 사용하여 요소의 회전을 정의 할 수도 있습니다. 

기본값은 auto이며, 요소가 경로를 따라갑니다.

.block {
	offset-path: path('M20,20 C20,100 200,0 200,100');
	offset-distance: calc(var(--distance, 50) * 1%);
}

https://codepen.io/jh3y/pen/d5aa1448cb745c1affe9fd6786958053

 

CSS Motion Path offset-path + offset-rotate

...

codepen.io

여기까지가 기본적인 static 환경에서의 path 그리기 입니다.

 

이제 어떻게 반응형 path를 그릴지 알아봅시다.

 

 

SVG는 포함 된 경로와 마찬가지로 뷰포트 크기로 확장됩니다. 

반응형일 때 offset-path는 조정되지 않고 요소가 코스를 벗어납니다.

 

d3를 이용한 방법도 있습니다.

https://codepen.io/jh3y/pen/mdJMzWW?editors=0010

 

Responsive CSS Motion Path with d3.js 😎

So we all love CSS Motion Path right? ❤️ But, it would be so much better if it was responsive 😅 So here's a solution using d3.js. Compose your path us...

codepen.io

$color-1 = #ddd
$color-2 = #222
$color-3 = rgba(128,191,255,0.5)
$color-4 = #80bfff

:root
    --size 75

*
    box-sizing border-box

body
    display flex
    box-align center
    align-items center
    flex-direction column
    justify-content center
    min-height 100vh
    background $color-1

    .container
        position relative
        height calc(var(--size) * 1vmin)
        width calc(var(--size) * 1vmin)
        border 2px solid $color-2

    .element
        height 40px
        width 40px
        background $color-3
        border 2px $color-4 solid
        position absolute
        top 0%
        left 0%
        offset-path path(var(--path))
        animation travel 2s infinite alternate linear

    svg
        position absolute
        height calc(var(--size) * 1vmin)
        opacity 0.5
        width calc(var(--size) * 1vmin)

        path
            fill none
            stroke-width 4px
            stroke #222


    @media (prefers-color-scheme: dark)
        background $color-2

        .container
            border 2px solid $color-1

        svg
            path
                stroke #ddd


@keyframes travel
    from
        offset-distance 0%

    to
        offset-distance 100%
import css from './style3.styl';
window.addEventListener('DOMContentLoaded', () => {
    // d3 library
    const { d3 } = window;

    const POINTS = [
        [0, 0],
        [5, -5],
        [10, 0],
        [15, 5],
        [20, 0],
    ];

    const CONTAINER = document.querySelector('.container');
    const PADDING = 40;

    // assignPath
    const assignPath = () => {
        const { height: size } = CONTAINER.getBoundingClientRect();

        // Create an X scale
        const xScale = d3
            .scaleLinear()
            .domain([0, 20])
            .range([PADDING, size - PADDING]);

        // Create an Y scale
        const yScale = d3
            .scaleLinear()
            .domain([-5, 5])
            .range([PADDING, size - PADDING]);

        // Map the POINTS using our scales
        const SCALED_POINTS = POINTS.map((POINT) => [xScale(POINT[0]), yScale(POINT[1])]);

        //Generate PATH string with our points
        const LINE = d3.line().curve(d3.curveBasis)(SCALED_POINTS);

        d3.select('svg').attr('viewBox', `0 0 ${size} ${size}`);
        d3.select('path').attr('d', `${LINE}`);
        // Assign path to animated element
        document.querySelector('.element').style.setProperty('--path', `"${LINE}"`);
    };

    assignPath();

    window.addEventListener('resize', assignPath);
});

 

 

이것은 확실히 작동하지만 좌표 세트를 사용하여 SVG 경로를 선언하지 않을 것이므로 이상적이지 않습니다. 

우리가하고 싶은 일은 벡터 드로잉 응용 프로그램에서 바로 경로를 가져 와서 최적화하는 것입니다. 

 

그렇게하면 JavaScript 함수를 호출 할 수 있고 그렇게 하는 편이 최적의 드로잉 방법입니다.

 

자바스크립트를 이용하는 방법

이제 나머지를 처리하는 JavaScript 함수를 만들 수 있습니다. 

이전에는 일련의 데이터 포인트를 가져 와서 확장 가능한 SVG 경로로 변환하는 함수를 만들었습니다. 

그러나 이제 한 걸음 더 나아가 경로 문자열을 가져 와서 데이터 세트를 해결하려고합니다. 

이러한 방식으로 사용자는 경로를 데이터 세트로 변환하려고 시도 할 필요가 없습니다.

 

이 함수에는 한 가지주의 사항이 있습니다. 

경로 문자열 외에도 경로를 확장 할 수있는 경계가 필요합니다. 

이러한 범위는 최적화 된 SVG에서 viewBox 속성의 세 번째 및 네 번째 값일 수 있습니다.

 

const path =
"M10.362 18.996s-6.046 21.453 1.47 25.329c10.158 5.238 18.033-21.308 29.039-18.23 13.125 3.672 18.325 36.55 18.325 36.55l12.031-47.544";
const height = 79.375 // equivalent to viewbox y2
const width = 79.375 // equivalent to viewbox x2


const motionPath = new ResponsiveMotionPath({
  height,
  width,
  path,
});

 

1. path string을 dataset으로 변환합니다.

이를 가능하게하는 가장 큰 부분은 경로 세그먼트를 읽을 수 있다는 것입니다. 

SVG GeometryElement API 덕분에 완전히 가능합니다.

경로가있는 SVG 요소를 생성하고 경로 속성을 d 속성에 할당하는 것으로 시작합니다.

// To convert the path data to points, we need an SVG path element.
const svgContainer = document.createElement('div');
// To create one though, a quick way is to use innerHTML
svgContainer.innerHTML = `
  <svg xmlns="http://www.w3.org/2000/svg">
    <path d="${path}" stroke-width="${strokeWidth}"/>
  </svg>`;
const pathElement = svgContainer.querySelector('path');

그런 다음 해당 경로 요소에서 SVGGeometryElement API를 사용할 수 있습니다. 

경로의 전체 길이에 대해 반복하고 경로의 각 길이에서 점을 반환하기만 하면됩니다.

convertPathToData = path => {
  // To convert the path data to points, we need an SVG path element.
  const svgContainer = document.createElement('div');
  // To create one though, a quick way is to use innerHTML
  svgContainer.innerHTML = `<svg xmlns="http://www.w3.org/2000/svg">
                              <path d="${path}"/>
                            </svg>`;
  const pathElement = svgContainer.querySelector('path');
  // Now to gather up the path points.
  const DATA = [];
  // Iterate over the total length of the path pushing the x and y into
  // a data set for d3 to handle 👍
  for (let p = 0; p < pathElement.getTotalLength(); p++) {
    const { x, y } = pathElement.getPointAtLength(p);
    DATA.push([x, y]);
  }
  return DATA;
}

2. 크기 비율을 만듭니다.

가장 큰 x 및 y 값을 가져 오는 기능과 viewBox와 관련된 비율을 계산하는 기능이 있습니다.

getMaximums = data => {
  const X_POINTS = data.map(point => point[0])
  const Y_POINTS = data.map(point => point[1])
  return [
    Math.max(...X_POINTS), // x2
    Math.max(...Y_POINTS), // y2
  ]
}
getRatios = (maxs, width, height) => [maxs[0] / width, maxs[1] / height]

viewBox에 의해 정의 된 경계가 필요한 이유와 같습니다.

이는 컨테이너에 대한 모션 경로의 비율을 계산할 방법이 필요하기 때문입니다.

이 비율은 SVG viewBox에 대한 경로의 비율과 같습니다.

 

가장 큰 x 및 y 값을 가져 오는 기능과 viewBox와 관련된 비율을 계산하는 기능이 있습니다.

getMaximums = data => {
  const X_POINTS = data.map(point => point[0])
  const Y_POINTS = data.map(point => point[1])
  return [
    Math.max(...X_POINTS), // x2
    Math.max(...Y_POINTS), // y2
  ]
}
getRatios = (maxs, width, height) => [maxs[0] / width, maxs[1] / height]

3. path를 그립시다

D3을 사용하여 앞서 생성 한 데이터 세트로 경로 문자열을 생성합니다.

d3.line()(data); // M10.362000465393066,18.996000289916992L10.107386589050293, etc.

D3의 멋진 점은 스케일을 만들 수 있다는 것입니다. 

하나의 좌표 세트를 작성한 다음 D3가 경로를 다시 계산하도록 할 수 있습니다.

생성한 비율을 사용하여 컨테이너 크기를 기준으로이 작업을 수행 할 수 있습니다.

const xScale = d3
  .scaleLinear()
  .domain([
    0,
    maxWidth,
  ])
  .range([0, width * widthRatio]);

도메인은 0에서 가장 높은 x 값입니다. 

대부분의 경우 범위는 0에서 컨테이너 너비에 너비 비율을 곱한 값입니다.

우리의 범위가 다를 수 있으며 시간을 조정해야 할 때가 있습니다. 

컨테이너의 가로 세로 비율이 경로의 가로 세로 비율과 일치하지 않는 경우입니다. 

예를 들어, SVG에서 viewBox가 0 0 100 200 인 경로를 고려하십시오. 

이는 종횡비가 1 : 2입니다. 

그러나 높이와 너비가 20vmin 인 컨테이너에 이것을 그리면 컨테이너의 종횡비는 1 : 1입니다. 

경로를 중앙에 유지하고 종횡비를 유지하려면 너비 범위를 채워야합니다.

이 경우 우리가 할 수있는 일은 경로가 컨테이너의 중심에 오도록 오프셋을 계산하는 것입니다.

const widthRatio = (height - width) / height
const widthOffset = (ratio * containerWidth) / 2
const xScale = d3
  .scaleLinear()
  .domain([0, maxWidth])
  .range([widthOffset, containerWidth * widthRatio - widthOffset])

두 개의 스케일이 있으면 스케일을 사용하여 데이터 포인트를 매핑하고 새 선을 생성 할 수 있습니다.

const SCALED_POINTS = data.map(POINT => [
  xScale(POINT[0]),
  yScale(POINT[1]),
]);
d3.line()(SCALED_POINTS); // Scaled path string that is scaled to our container

CSS 속성을 통해 인라인으로 전달하여 해당 경로를 요소에 적용 할 수 있습니다.

ELEMENT.style.setProperty('--path', `"${newPath}"`);

그런 다음 새로운 확장 경로를 생성하고 적용 할 시점을 결정하는 것은 우리의 책임입니다. 

가능한 해결책은 다음과 같습니다.

const setPath = () => {
  const scaledPath = responsivePath.generatePath(
    CONTAINER.offsetWidth,
    CONTAINER.offsetHeight
  )
  ELEMENT.style.setProperty('--path', `"${scaledPath}"`)
}
const SizeObserver = new ResizeObserver(setPath)
SizeObserver.observe(CONTAINER)

https://codepen.io/jh3y/pen/dyoewER?editors=0010

최종본입니다.

 

 

Drag and Drop Responsive Motion Path 🤓🎉

Drag an SVG file onto the page containing a `path` and have that `path` scale with the container size 👍 Proof of concept where the JavaScript `class` ...

codepen.io

 

 

출처 : https://css-tricks.com/create-a-responsive-css-motion-path-sure-we-can/

댓글