본문 바로가기
css

CSS 페인트 API 탐색: 원형 모양

by code-box 2021. 10. 25.
반응형

복잡한 모양에 테두리를 추가하는 것은 골치 아픈 일이지만, 복잡한 모양의 모서리를 돌아다니는 것은 악몽입니다! 다행히 CSS 페인트 API가 구하러 왔습니다! 이 "CSS 페인트 API 탐구" 시리즈의 일부로 살펴볼 것입니다.

우리가 목표로 하는 것은 이렇다. 이 시리즈에서 살펴본 다른 모든 것과 마찬가지로 현재는 Chrome과 Edge만 이 기능을 지원합니다.

나머지 기사를 따라 했다면 패턴이 형성되는 것을 알아차렸을지도 모릅니다. 일반적으로 CSS 그림판 API로 작업할 경우:

  • 우리는 우리가 쉽게 조정할 수 있는 몇 가지 기본적인 CSS를 작성합니다.
  • 모든 복잡한 논리는 그림() 함수 안에서 장면 뒤에서 이루어집니다.
 

실제로 페인트 API 없이도 이 작업을 수행할 수 있습니다.

복잡한 모양에 둥근 모서리를 붙이는 방법은 많겠지만, 제 작품에서 사용한 세 가지 방법을 여러분과 공유하겠습니다.

네가 하는 말 이미 들었어 당신이 이미 세 가지 방법을 알고 있다면, 왜 페인트 API를 사용하고 있습니까? 좋은 질문입니다. 제가 알고 있는 3가지 방법이 어렵고, 그중 2가지 방법이 SVG와 특별히 관련되어 있기 때문에 사용하고 있습니다. SVG에 반대하지는 않지만 CSS 전용 솔루션을 사용하면 유지 관리가 더 쉬워지고 코드를 탐색할 때 누군가가 들어가서 이해하는 것도 쉬워집니다.

그 세 가지 방법 중…

If you are an SVG guru, this method is for you. the `clip-path` property accepts SVG paths. That means we can easily pass in the path for a complex rounded shape and be done. This approach is super easy if you already have the shape you want, but it’s unsuitable if you want an adjustable shape where, for example, you want to adjust the radius.
 

다음은 둥근 육각형 모양의 예입니다. 곡률 조절과 모양 조절에 행운을 빌어요! 그렇게 하려면 그 미친놈의 길을 수정해야 할 거야

 

Chris가 구성한 SVG 경로에 대한 이 도해된 가이드를 참조하실 수 있을 것 같습니다. 하지만 그 가이드를 참조하더라도 점과 곡선을 원하는 대로 그리는 것은 여전히 많은 작업이 필요할 것입니다.

저는 이 기술을 루카스 베버가 끈적끈적한 효과를 만드는 것에 대한 게시물에서 발견했습니다. 모든 기술적 세부 사항을 찾을 수 있지만, SVG 필터를 어떤 요소에도 적용하여 모서리를 둥글게 하는 것이 아이디어입니다.

 
 
We simply use `clip-path` to create the shape we want then apply the SVG filter on a parent element. To control the radius, we adjust the `stdDeviation` variable.

이 역시 좋은 기술이지만 현장에서 조정을 하기 위해서는 깊은 수준의 SVG 노하우가 필요하다.

네, 애나 튜더가 CSS만의 끈적끈적한 효과를 내는 기술을 발견했어요 복잡한 모양의 모서리를 둥글게 만드는데 사용할 수 있죠 아마 지금 기사를 쓰고 있을 거예요 그때까지는 그녀가 만든 슬라이드를 참고해서 어떻게 작동하는지 설명해주면 됩니다.

아래 데모에서는 SVG 필터를 그녀의 기술로 교체합니다.

 
 
Again, another neat trick! But as far as being easy to work with? Not so much here, either, especially if we’re considering more complex situations where we need transparency, images, etc. It’s work finding the correct combination of `filter`, `mix-blend-mode` and other properties to get things just right.

대신 CSS 그림판 API 사용

당신이 나에게서 숨기고 있는 복잡한 모양에 둥근 테두리를 둘 수 있는 킬러 CSS만의 방법이 없다면, 당신은 내가 왜 CSS Paint API를 찾기로 했는지 알 수 있을 것이다.

The logic behind this relies on the same code structure I used in the article covering the polygon border. I’m using the `--path` variable that defines our shape, the `cc()` function to convert our points, and a few other tricks we’ll cover along the way. I highly recommend reading that article to better understand what we’re doing here.

image

 
We first start with a classic rectangular element and define our shape inside the `--path` variable (shape 2 above). The `--path` variable behaves the same way as the path we define inside `clip-path: polygon()`. Use Clippy to generate it. 
.box {
    display: inline-block;
    height: 200px;
    width: 200px;

    --path: 50% 0,100% 100%,0 100%;
    --radius: 20px;
    -webkit-mask: paint(rounded-shape);
}
.box {
    display: inline-block;
    height: 200px;
    width: 200px;

    --path: 50% 0,100% 100%,0 100%;
      --radius: 20px;
    -webkit-mask: paint(rounded-shape);
}
Nothing complex so far. We apply the custom mask and we define both the `--path` and a `--radius` variable. The latter will be used to control the curvature.

image

 
In addition to the points defined by the path variable (pictured as red points above), we’re adding even more points (pictured as green points above) that are simply the midpoints of each segment of the shape. Then we use the `arcTo()` function to build the final shape (shape 4 above).
Adding the midpoints is pretty easy, but using `arcTo()` is a bit tricky because we have to understand how it works. According to MDN:

지정된 기준점과 반지름을 사용하여 현재 하위 경로에 원형 호를 추가합니다. 지정된 매개변수에 필요한 경우 호가 직선으로 경로의 최근 점에 자동으로 연결됩니다.
이 방법은 둥근 모서리를 만드는 데 일반적으로 사용됩니다.

The fact that this method requires control points is the main reason for the extra midpoints points. It also require a radius (which we are defining as a variable called `--radius`).

MDN의 설명서를 계속 읽는 경우:

 

arcTo()를 생각하는 한 가지 방법은 두 개의 직선 세그먼트를 상상하는 것이다. 하나는 시작점에서 첫 번째 제어점까지, 다른 하나는 거기서 두 번째 제어점으로. arcTo()가 없으면 이 두 세그먼트가 날카로운 모서리를 형성할 것이다: arcTo()는 이 모서리에 맞는 원형 호를 만들고 매끄럽게 한다. 즉, 호는 두 세그먼트에 모두 접선합니다.

각 아크/코너는 세 점을 사용하여 구축됩니다. 위 그림을 확인하시면 각 코너마다 빨간색 포인트 1개와 초록색 포인트 2개가 있습니다. 각각의 적색-녹색 조합은 하나의 세그먼트를 만들어 위에서 두 세그먼트를 상세하게 가져옵니다.

한 모서리를 확대하여 어떤 일이 일어나고 있는지 알아보겠습니다.

image

자, 우리가 첫 번째 그린 포인트에서 다음 그린 포인트로 가는 경로를 가지고 있다고 상상해보세요. 그 원을 따라 움직입니다. 이렇게 각 코너마다 이렇게 해서 동그란 모양을 만들어요.

 

코드 표시 방식은 다음과 같습니다.

// We first read the variables for the path and the radius.
const points = properties.get('--path').toString().split(',');
const r = parseFloat(properties.get('--radius').value);

var Ppoints = [];
var Cpoints = [];
const w = size.width;
const h = size.height;
var N = points.length;
var i;
// Then we loop through the points to create two arrays.
for (i = 0; i < N; i++) {
    var j = i-1;
    if(j<0) j=N-1;

    var p = points[i].trim().split(/(?!\(.*)\s(?![^(]*?\))/g);
    // One defines the red points (Ppoints)
    p = cc(p[0],p[1]);
    Ppoints.push([p[0],p[1]]);
    var pj = points[j].trim().split(/(?!\(.*)\s(?![^(]*?\))/g);
    pj = cc(pj[0],pj[1]);
    // The other defines the green points (Cpoints)
    Cpoints.push([p[0]-((p[0]-pj[0])/2),p[1]-((p[1]-pj[1])/2)]);
}

/* ... */

// Using the arcTo() function to create the shape
ctx.beginPath();
ctx.moveTo(Cpoints[0][0],Cpoints[0][1]);
for (i = 0; i < (Cpoints.length - 1); i++) {
    ctx.arcTo(Ppoints[i][0], Ppoints[i][1], Cpoints[i+1][0],Cpoints[i+1][1], r);
}
ctx.arcTo(Ppoints[i][0], Ppoints[i][1], Cpoints[0][0],Cpoints[0][1], r);
ctx.closePath();

/* ... */

ctx.fillStyle = '#000';
ctx.fill();
// We first read the variables for the path and the radius.
const points = properties.get('--path').toString().split(',');
const r = parseFloat(properties.get('--radius').value);

var Ppoints = [];
var Cpoints = [];
const w = size.width;
const h = size.height;
var N = points.length;
var i;
// Then we loop through the points to create two arrays.
for (i = 0; i < N; i++) {
    var j = i-1;
    if(j<0) j=N-1;

    var p = points[i].trim().split(/(?!\(.*)\s(?![^(]*?\))/g);
    // One defines the red points (Ppoints)
    p = cc(p[0],p[1]);
    Ppoints.push([p[0],p[1]]);
    var pj = points[j].trim().split(/(?!\(.*)\s(?![^(]*?\))/g);
    pj = cc(pj[0],pj[1]);
    // The other defines the green points (Cpoints)
    Cpoints.push([p[0]-((p[0]-pj[0])/2),p[1]-((p[1]-pj[1])/2)]);
}

/* ... */

// Using the arcTo() function to create the shape
ctx.beginPath();
ctx.moveTo(Cpoints[0][0],Cpoints[0][1]);
for (i = 0; i < (Cpoints.length - 1); i++) {
    ctx.arcTo(Ppoints[i][0], Ppoints[i][1], Cpoints[i+1][0],Cpoints[i+1][1], r);
}
ctx.arcTo(Ppoints[i][0], Ppoints[i][1], Cpoints[0][0],Cpoints[0][1], r);
ctx.closePath();

/* ... */

ctx.fillStyle = '#000';
ctx.fill();

마지막 단계는 단색으로 우리의 모양을 채우는 것입니다. 이제 우리는 우리의 둥근 모양을 가지고 있고 그것을 어떤 요소에도 마스크로 사용할 수 있습니다.

That’s it! Now all we have to do is to build our shape and control the radius like we want — a radius that we can animate, thanks to `@property` which will make things more interesting!
 

image

이 방법에 단점이 있나요?

Yes, there are drawbacks, and you probably noticed them in the last example. The first drawback is related to the hover-able area. Since we are using `mask`, we can still interact with the initial rectangular shape. Remember, we faced the same issue with the polygon border and we used `clip-path` to fix it. Unfortunately, `clip-path` does not help here because it also affects the rounded corner.
Let’s take the last example and add `clip-path`. Notice how we are losing the “inward” curvature.
 
 
There’s no issue with the hexagon and triangle shapes, but the others are missing some curves. It could be an interesting feature to keep only the outward curvature — thanks to `clip-path`— and at the same time we fix the hover-able area. But we cannot keep all the curvatures and reduce the hover-able area at the same time.

두 번째 호? 그것은 큰 반지름 값의 사용과 관련이 있습니다. 아래 도형 위에 마우스를 올려놓고 놀라운 결과를 확인하십시오.

 

반지름을 제어할 수 있기 때문에 사실 큰 단점은 아니지만, 너무 큰 반지름 값을 잘못 사용할 경우 그러한 상황을 피하는 것이 좋습니다. 반경 값을 최대값으로 제한하는 범위로 제한하면 이 문제를 해결할 수 있습니다. 각 모서리마다 오버플로 없이 가장 큰 호를 가질 수 있는 반지름을 계산합니다. 이면의 수학 논리를 파헤치지는 않겠지만() 다음은 반지름 값을 제한하는 마지막 코드입니다.

var angle = 
    Math.atan2(Cpoints[i+1][1] - Ppoints[i][1], Cpoints[i+1][0] - Ppoints[i][0]) -
    Math.atan2(Cpoints[i][1]   - Ppoints[i][1], Cpoints[i][0]   - Ppoints[i][0]);
if (angle < 0) {
    angle += (2*Math.PI)
}
if (angle > Math.PI) {
    angle = 2*Math.PI - angle
}
var distance = Math.min(
    Math.sqrt(
          (Cpoints[i+1][1] - Ppoints[i][1]) ** 2 + 
          (Cpoints[i+1][0] - Ppoints[i][0]) ** 2),
    Math.sqrt(
          (Cpoints[i][1] - Ppoints[i][1]) ** 2 + 
          (Cpoints[i][0] - Ppoints[i][0]) ** 2)
    );
var rr = Math.min(distance * Math.tan(angle/2),r);
 
var angle = 
    Math.atan2(Cpoints[i+1][1] - Ppoints[i][1], Cpoints[i+1][0] - Ppoints[i][0]) -
    Math.atan2(Cpoints[i][1]   - Ppoints[i][1], Cpoints[i][0]   - Ppoints[i][0]);
if (angle < 0) {
    angle += (2*Math.PI)
}
if (angle > Math.PI) {
    angle = 2*Math.PI - angle
}
var distance = Math.min(
    Math.sqrt(
          (Cpoints[i+1][1] - Ppoints[i][1]) ** 2 + 
          (Cpoints[i+1][0] - Ppoints[i][0]) ** 2),
    Math.sqrt(
          (Cpoints[i][1] - Ppoints[i][1]) ** 2 + 
          (Cpoints[i][0] - Ppoints[i][0]) ** 2)
    );
var rr = Math.min(distance * Math.tan(angle/2),r);
r` is the radius we are defining and `rr` is the radius we’re actually using. It equal either to `r` or the maximum value allowed without overflow.
 

데모에서 모양을 가리키면 더 이상 이상한 모양이 아니라 "최대 둥근 모양"(내가 방금 만든 것)이 나타납니다. 삼각형과 육각형과 같은 정다각형은 논리적으로 원을 "최대 둥근 모양"으로 가지고 있기 때문에 서로 다른 모양 사이에 시원한 전환이나 애니메이션을 가질 수 있다.

국경을 가질 수 있을까요?

 
Yes! All we have to do is to use `stroke()` instead of `fill()` inside our `paint()` function. So, instead of using:
ctx.fillStyle = '#000';
ctx.fill();
ctx.fillStyle = '#000';
ctx.fill();

…이(가) 사용됩니다.

ctx.lineWidth = b;
ctx.strokeStyle = '#000';
ctx.stroke();
 
ctx.lineWidth = b;
ctx.strokeStyle = '#000';
ctx.stroke();
This introduces another variable, `b`, that controls the border’s thickness.
 
Did you notice that we have some strange overflow? We faced the same issue in the previous article, and that due to how `stroke()` works. I quoted MDN in that article and will do it again here as well:

획은 경로의 중심에 맞춰집니다. 즉, 획의 절반은 안쪽, 절반은 바깥쪽에서 그립니다.

 
Again, it’s that “half inner side, half outer side” that’s getting us! In order to fix it, we need to hide the outer side using another mask, the first one where we use the `fill()`. First, we need to introduce a conditional variable to the `paint()` function in order to choose if we want to draw the shape or only its border.

우리가 가진 것은 다음과 같다.

if(t==0) {
    ctx.fillStyle = '#000';
    ctx.fill();
} else {
    ctx.lineWidth = 2*b;
    ctx.strokeStyle = '#000';
    ctx.stroke();
}
if(t==0) {
    ctx.fillStyle = '#000';
    ctx.fill();
} else {
    ctx.lineWidth = 2*b;
    ctx.strokeStyle = '#000';
    ctx.stroke();
}
Next, we apply the first type of mask (`t=0`) on the main element, and the second type (`t=1`) on a pseudo-element. The mask applied on the pseudo-element produces the border (the one with the overflow issue). The mask applied on the main element addresses the overflow issue by hiding the outer part of the border. And if you’re wondering, that’s why we are adding twice the border thickness to `lineWidth`.
 

image

보이지? 윤곽과 같은 완벽한 둥근 모양을 가지고 있으며 호버로 반지름을 조절할 수 있습니다. 그리고 모양에 어떤 종류의 배경도 사용할 수 있습니다.

그리고 우리는 이 모든 것을 약간의 CSS로 해냈습니다.

div {
    --radius: 5px; /* Defines the radius */
    --border: 6px; /* Defines the border thickness */
    --path: /* Define your shape here */;
    --t: 0; /* The first mask on the main element */

    -webkit-mask: paint(rounded-shape);
    transition: --radius 1s;
}
div::before {
    content: "";
     background: ..; /* Use any background you want */
    --t: 1; /* The second mask on the pseudo-element */
    -webkit-mask: paint(rounded-shape); /* Remove this if you want the full shape */
}
div[class]:hover {
    --radius: 80px; /* Transition on hover */
}
div {
    --radius: 5px; /* Defines the radius */
    --border: 6px; /* Defines the border thickness */
    --path: /* Define your shape here */;
    --t: 0; /* The first mask on the main element */

    -webkit-mask: paint(rounded-shape);
    transition: --radius 1s;
}
div::before {
    content: "";
     background: ..; /* Use any background you want */
    --t: 1; /* The second mask on the pseudo-element */
    -webkit-mask: paint(rounded-shape); /* Remove this if you want the full shape */
}
div[class]:hover {
    --radius: 80px; /* Transition on hover */
}
 
Let’s not forget that we can easily introduce dashes using `setLineDash()` the same way we did in the previous article.

image

반지름 제어

In all the examples we’ve looked at, we always consider one radius applied to all the corners of each shape. It would be interesting if we could control the radius of each corner individually, the same way the `border-radius` property takes up to four values. So let’s extend the `--path` variable to consider more parameters.
Actually, our path can be expressed as a list of `[x y]` values. We’ll make a list of `[x y r]` values where we introduce a third value for the radius. This value isn’t mandatory; if omitted, it falls back to the main radius.
 
.box {
    display: inline-block;
    height: 200px;
    width: 200px;

    --path: 50% 0 10px,100% 100% 5px,0 100%;
    --radius: 20px;
    -webkit-mask: paint(rounded-shape);
}
.box {
    display: inline-block;
    height: 200px;
    width: 200px;

    --path: 50% 0 10px,100% 100% 5px,0 100%;
      --radius: 20px;
    -webkit-mask: paint(rounded-shape);
}
Above, we have a `10px` radius for the first corner, `5px` for the second, and since we didn’t specify a value for the third corner, it inherits the `20px` defined by the `--radius` variable.

다음은 값에 대한 JavaScript입니다.

var Radius = [];
// ...
var p = points[i].trim().split(/(?!\(.*)\s(?![^(]*?\))/g);
if(p[2])
    Radius.push(parseInt(p[2]));
else
    Radius.push(r);
 
var Radius = [];
// ...
var p = points[i].trim().split(/(?!\(.*)\s(?![^(]*?\))/g);
if(p[2])
    Radius.push(parseInt(p[2]));
else
    Radius.push(r);
This defines an array that stores the radius of each corner. Then, after splitting the value of each point, we test whether we have a third value (`p[2]`). If it’s defined, we use it; if not, we use the default radius. Later on, we’re using `Radius[i]` instead of `r`.

image

이 사소한 추가는 쉐이프의 특정 모서리에 대한 반지름을 비활성화하려는 경우에 좋은 기능입니다. 사실, 다음은 몇 가지 다른 예시를 살펴보겠습니다.

더 많은 예!

 
I made a series of demos using this trick. I recommend setting the radius to 0 to better see the shape and understand how the path is created. Remember that the `--path` variable behaves the same way as the path we define inside `clip-path: polygon()`. If you’re looking for a path to play with, try using Clippy to generate one for you.

이 기술을 사용하여 많은 화려한 모양을 만들 수 있습니다. 다음은 추가 요소, 유사 요소 또는 핵-y 코드 없이 수행되는 몇 가지입니다.

 

이전 기사에서 우리는 말풍선 요소에 테두리를 추가했습니다. 이제 우리는 이 새로운 방법을 사용하여 그것을 개선하고 모서리를 돌 수 있습니다.

 
 

이 예와 원래 구현을 비교해 보면 정확히 동일한 코드가 있음을 알 수 있습니다. 저는 새로운 워크렛을 사용하기 위해 CSS를 두세 번 변경했을 뿐입니다.

아래에서 당신의 콘텐츠를 위한 멋진 프레임을 찾아보세요. 경사가 필요할 때 더 이상 골칫거리 하지 마!

 
Simply play with the `--path` variable to create your own responsive frame with any coloration your want.

SVG는 더 이상 요즘 유행하는 물결 모양의 구획 칸막이를 만들 필요가 없다.

 
 

CSS는 가볍고 비교적 간단합니다. 저는 분할기의 새로운 인스턴스를 생성하기 위해 경로만 업데이트했습니다.

다음은 클래식한 디자인 패턴으로, 많은 분들이 언젠가 마주쳤을 것입니다. 반경을 어떻게 뒤집죠? 당신은 내비게이션 디자인에서 그것을 본 적이 있을 것이다.

 

이에 대한 약간 다른 견해:

 
 

경로 값을 가지고 놀면 화려한 애니메이션을 얻을 수 있습니다.
아래에서는 경로의 한 가지 값에만 전환을 적용해도 멋진 효과를 얻을 수 있습니다.

 

이건 애나 튜더의 데모에서 영감을 받았어요

다른 애니메이션이 있는 다른 아이디어

 
 

더 복잡한 애니메이션의 또 다른 예는 다음과 같습니다.

 

튕기는 공은 어때?

 
 

큰 반지름 값을 사용하여 다른 모양들, 특히 원과 정다각형 사이에서 시원한 전환을 만들 수 있습니다.

 

테두리 애니메이션을 추가하면 "숨쉬는" 모양이 생깁니다!

 

자, 모여보자.

 
I hope you’ve enjoyed getting nerdy with the CSS Paint API. Throughout this series, we’ve applied `paint()` to a bunch of real-life examples where having the API allows us to manipulate elements in a way we’ve never been able to do with CSS — or without resorting to hacks or crazy magic numbers and whatnot. I truly believe the CSS Paint API makes seemingly complicated problems a lot easier to solve in a straightforward way and will be a feature we reach for time and again. That is, when browser support catches up to it.

이 시리즈를 계속 진행했거나 이 기사를 우연히 접한 경우 CSS Paint API에 대해 어떻게 생각하는지, 작업에서 이 API를 어떻게 사용하는지 알고 싶습니다. 물결무늬 칸막이처럼 현재 디자인 트렌드에 도움이 되는 게 있나요? 아니면 블로비 디자인? 실험하고 즐기세요!

 

이건 제 이전 글에서 따온 거예요

댓글