SVG "Filter" 에 대한 정리 - 1

조경석·2023년 1월 29일
1


출처 : https://codepen.io/lbebber/pen/KwGEQv
위 예제의 지글거리는 텍스트는 편집 가능한(contenteditable 특성이 지정, 상속된) div, p 등의 텍스트 요소이다.

JS 애니메이션 라이브러리 없이, div의 텍스트 요소와 별도로 마크업되어 있는 SVG의 <filter> 요소를 CSS 애니메이션으로 키프레임과 타이밍만 지정해두었다.

이렇게 HTML 문서 요소에도 자유롭고 복합적인 후처리 효과를 지정할 수 있는 SVG 필터 요소에 대해 알아보자.

들어가기 전에

실제로 <filter> 요소 내부에 포함되는 Filter Effect 요소(=filter primitive)에 대한 설명 이전에, 공통적으로 적용되는 정보를 먼저 정리해보자.

SVG의 쌓임 순서

SVG는 CSS의 z-index와 같은 레이어 순서를 위한 특성이 없으며, CSS를 통해 직접 z-index를 지정하더라도 적용되지 않는다.
반드시 요소가 선언된 순서대로 밑에서부터 쌓아 올려지고, 이는 필터 요소에도 동일하게 적용된다.

<defs>

보통 SVG의 그라디언트나 clip-path, 필터 등은 <defs>요소 내부에 선언한다고 알고 있을 것이다.
실제 <defs>의 역할은 나중에 참조해서 사용할 개체 정의들(definitions)를 저장하기 위한 요소로, 이 요소 내에 정의된 요소는 렌더링되지 않는 특징을 가진다.
보통 특성값으로 지정되는 url()함수나 <use> 요소를 통해 참조, 복제하여 사용하기 위해 미리 선언해두기 위한 요소인 것이다.

거꾸로 말하면, <filter>와 같이 그 자체로 렌더링되지 않는 요소는 반드시 <defs> 내부에 포함해야 하는 것은 아니며, 이미 렌더링한 도형을 참조나 복제하려는 경우 역시 마찬가지다.
하지만, 계층적 구조를 가지는 XML인 SVG 특성상 접근성과 이해 가능한 문서 구조를 위해 <defs> 내부에 선언하는걸 권장한다.

in, in2, result

대부분의 Filter Effect 요소의 필수 특성으로 사용되는 in과, 그를 참조하는 result에 대해 알아보자.
쉽게 말하면 적용될 대상이 필요한 모든 Filter Effect 요소에 적용되는 바로 그 대상의 이름이며, XML에서 일반적으로 사용되는 id가 아닌 result로 명명된 이름을 사용한다.
id는 문서 전체에서 특정 요소를 찾기 위한 이름이고, in과 result는 대상 요소의 부모 <filter> 내에서만 적용되는 이름이라고 이해할 수 있겠다.

당연히 그래서는 안되지만, 중복되어 선언된 경우 DOM이나 내부 참조에서 먼저 선언된 항목을 참조하는 id와 다르게, result가 중복되어 선언되는 경우 in은 더 나중에 선언된 요소를 참조한다.
첫번째 항목인 경우와 생략되는 경우 SourceGraphic이나 바로 전에 선언된 Filter Effect 요소를 참조하는 in의 트리 순서를 따지는 성질 때문인 것으로 보인다.

filter region

일반적으로 '필터'라 함은 필터가 적용되는 대상 이미지의 영역 전체에 적용된다고 생각하기 쉽지만, SVG의 <filter>는 대상 이미지와 별개의 영역을 지정할 수도 있고, 하위 세부 필터 요소에 대한 영역을 개별적으로 지정할 수도 있다.

필터 요소 전체에 대한 단위 영역을 지정할 수 있는 filterUnits과, 아래에서 설명할 단위 filter primitive에서 사용되는 길이값의 단위를 지정할 수 있는 primitiveUnits 특성이 있으며, 둘 모두 <filter> 요소에 적용하여 사용한다.
<pattern>, <marker> 등에서도 patternUnits, markerUnits가 있고, 적용되는 방식은 유사하지만 자식 요소인 filter primitive의 특성상 primitiveUnits은 그 성질이 독특한 편이다.

filterUnits와 primitiveUnits 모두 영역을 지정하기 위한 값으로 userSpaceOnUseobjectBoundingBox가 있다.
filterUnits에서는 위 값에 의해 <filter>에 적용된 x, y, width, height 값이 해석되는 방식이 정해지며,
primitiveUnits에서는 내부의 filter primitive에서 사용하는 길이 값의 기준이 결정된다.

userSpaceOnUse
primitiveUnits에서 별도로 특성을 지정하지 않았을 경우 지정되는 기본값으로, 상위 요소(보통 SVG)에 대한 상대 좌표값으로 결정된다.

objectBoundingBox
filterUnits에서 별도로 특성을 지정하지 않았을 경우 지정되는 기본값으로, 기준 영역(필터가 적용된 대상 요소)을 기준으로 백분율 값으로 결정된다.

라고 써놨는데, 아무리 레퍼런스 문서를 찾아봐도 도저히 이해가 가질 않아, 비교표를 만들기 위한 테스트 SVG를 만들었다.

<svg xmlns="http://www.w3.org/2000/svg" width="200" height="200" viewBox="0 0 200 200">
   <defs>
      <!-- width는 백분율, height는 상수로 지정해둔 filter #test -->
      <filter id="test" filterUnits="objectBoundingBox" primitiveUnits="userSpaceOnUse" x="0" y="30%" width="90%" height="100">
         <!-- width, height를 상수로 지정한 flood1 -->
         <feFlood x="0" y="60%" width="70" height="60" flood-color="red" flood-opacity="0.5" result="flood1"/>
         <!-- width, height를 백분율로 지정한 flood2 -->
         <feFlood x="0" y="60%" width="40%" height="30%" flood-color="green" flood-opacity="0.5" result="flood2"/>
         <!-- 모든 feFlood를 표현하기 위한 feMerge -->
         <feMerge>
            <feMergeNode in="flood1"/>
            <feMergeNode in="flood2"/>
         </feMerge>
      </filter>
   </defs>
   <!-- SVG 영역을 표시하는 청록색 배경 -->
   <rect width="100%" height="100%" fill="cyan"/>
   <!-- 필터가 적용된 50% 너비, 높이의 중앙에 배치된 사각형 -->
   <rect filter="url(#test)" stroke-width="1" stroke="black" fill="transparent" x="25%" y="25%" width="50%" height="50%"/>
   <!-- 비교를 위해 원래 도형 영역을 표시하는 검은 사각형 -->
   <rect stroke-width="1" stroke="black" fill="transparent" x="25%" y="25%" width="50%" height="50%"/>
</svg>

feFlood와 feMerge에 대한 자세한 설명은 일단 다음으로 미루고, filterUnitsprimitiveUnits의 값과 그로 인해 그려지는 렌더링 결과에만 집중해보자.
필터 영역은 노란색 사각형, flood1, flood2 영역은 각각 빨간색, 초록색으로 표시해두었다.

filterUnits="objectBoundingBox" primitiveUnits="userSpaceOnUse" (기본값)

필터 영역인 노란색 영역은 필터가 적용된 사각형의 좌상단을 기준으로 하고, 높이값인 100을 백분율 값으로 해석해 10000%가 되어 SVG 영역을 아예 초과해버린 것을 볼 수 있다.

여기서 볼 수 있듯이, objectBoundingBox는 길이값을 반드시 백분율 값으로 해석한다.

userSpaceOnUse로 지정된 primitiveUnits을 따르는 flood1과 flood2인 빨간색, 초록색 사각형은 픽셀값인 60과 백분율 값인 30%가 동일하게 적용된 것을 볼 수 있다.

여기서 볼 수 있듯이, userSpaceOnUse는 반드시 상위 요소인 SVG의 전체 영역을 기준으로 하는 상대값과 절대값 전부를 사용한다.

filterUnits="userSpaceOnUse" primitiveUnits="objectBoundingBox"

비교를 위해, 위 예제와 길이값은 모두 동일한데 filterUnits와 primitiveUnits 값을 뒤바꾼 버전을 확인해보자.

필터 영역인 노란색 영역은 상위 요소인 SVG 영역의 좌상단을 기준으로 하고, 높이 픽셀값인 100과 너비 상대값인 90%가 적용된 것을 볼 수 있고,
반대로 절대값을 적용했던 flood1의 너비, 높이가 백분율로 해석되어 영역을 초과했고, flood2 사각형은 백분율 값인 너비, 높이가 필터가 적용된 사각형을 기준으로 적용된 것을 볼 수 있다.

요약하자면,

  1. filterUnits와 primitiveUnits의 값은 서로에게 영향을 주지 않는다.
  2. objectBoundingBox는 모든 값을 필터가 적용된 도형을 기준으로 상대값으로 해석한다.
  3. userSpaceOnUse는 모든 값을 상위 요소인 SVG의 상대값과 절대값으로 해석한다.

암만 사용해봐도 직관적으로 이해가 가질 않았는데, 사각형으로 각 영역을 그려 가며 해석해 보니 꽤 알아보기 쉽게 이해가 되었다.

쉬어가기

필터에 적용되는 개념과 필터 영역에 대한 정리만으로도 분량이 너무 길어져, 개별 Filter Primitive에 대해서는 다음 글에서 정리할 예정이다.

0개의 댓글