# AI 꾸러미 — SVG 필터 (종이 결 feTurbulence) — SVG filter feTurbulence (fractalNoise)
> SVG 필터의 feTurbulence 잡음으로 가장자리를 찢고 섬유 결 · 얼룩을 입혀, 그냥 네모를 손으로 만든 종이처럼 보이게 한다.  
> 견본: https://ai-techstudio.web.app/#t/u58

이 문서를 코드 도우미(Claude Code · Cursor · ChatGPT 등)에 그대로 주면 돼요. 「## 주문서」 가 할 일, 나머지는 참고 자료예요.

## 주문서

### 만들어 줘: SVG 필터 (종이 결 feTurbulence) — SVG filter feTurbulence (fractalNoise)

#### 1. 목표
보고서 · 서류 종이 판에 SVG 필터로 종이 결을 입혀 줘 — feTurbulence 로 가장자리를 찢고 섬유 결 · 얼룩을 그림 안에만 얹는다. 종이는 누런 크림 종이.

#### 2. 핵심 기술 용어
- **SVG filter feTurbulence (fractalNoise)** — 필터 안에서 만드는 잡음 무늬
- **feDisplacementMap** — 잡음 값만큼 그림 화소를 밀어 가장자리 흔들기
- **feColorMatrix · feComposite operator="in"** — 잡음을 색으로 바꾸고, 그림 안에만 남기기
- **feDropShadow** — 종이 그림자

#### 3. 환경
- 플랫폼: TypeScript (브라우저), 라이브러리 없이 — 화면과 떨어진 순수 함수로
- 화면: 2D · 브라우저 — PC · 폰 모두

#### 4. 조건
- 필터 id 는 화면마다 겹치지 않게 (같은 id 가 둘이면 엉뚱한 필터가 걸린다)
- filter 영역을 x/y -10% · width/height 120% 로 넓혀 찢긴 가장자리가 잘리지 않게
- 섬유 · 얼룩은 feComposite operator="in" 으로 종이 안에만
- seed 와 찢김 세기(scale)를 조절할 수 있게

#### 5. 완성 기준 (이게 보이면 성공)
- 그냥 네모와 나란히 보면 오른쪽만 가장자리가 찢기고 섬유 결 · 얼룩이 보인다
- 「찢김 세기」 0 → 30 으로 가장자리 흔들림이 커진다
- seed 를 바꾸면 다른 무늬의 종이가 된다
- 종이 위 글자 · 줄은 흔들리지 않는다 (필터는 종이 사각형에만)

#### 6. 진행 방식
- 핵심 코드 위주로, 설명은 짧게. 내 프로젝트에 끼워 넣기 쉬운 함수 · 클래스로 나눠 줘.
- 처음 화면에 바로 결과가 보이게, 그리고 켬/끔(또는 전/후) 비교를 할 수 있게 만들어 줘.
- 그림 · 소리 · 모델 파일이 필요하면 코드로 만든 임시 대체물로 먼저 돌아가게 하고, 진짜 파일로 바꿀 자리를 표시해 줘.
- 마지막에 「확인 방법」(무엇을 보면 성공인지)과 「조절할 값」 목록을 짧게 정리해 줘.
- 답변과 코드 주석은 한국어로 해 줘.

## 원리
- feTurbulence 는 필터 안에서 잡음 그림을 만든다. baseFrequency 가 작으면 큰 얼룩(0.012), 크면 잔결(0.9). 가로·세로를 따로 주면(0.9 0.05) 한쪽으로 긴 섬유 결.
- feDisplacementMap 이 잡음 R · G 값만큼 원래 그림 화소를 밀어 가장자리가 찢긴 듯 흔들린다 (scale 12).
- 섬유 · 얼룩 잡음은 feColorMatrix 로 갈색 반투명으로 바꾸고, feComposite operator="in" 으로 찢긴 종이 안에만 남긴다.
- feMerge 로 「찢긴 종이 → 섬유 → 얼룩」을 겹치고, 바깥에 feDropShadow 로 그림자. seed 를 바꾸면 다른 종이가 된다.

## 핵심 코드 — 찢긴 가장자리 + 섬유 결 + 얼룩 종이 필터
(발췌: demos/demosDraw2d.ts u58 의 <filter> 를 정리)
```ts
function paperFilter(id: string, seed = 2) {
  return [
    '<filter id="' + id + '" x="-10%" y="-10%" width="120%" height="120%">',
    // 큰 잡음으로 가장자리 밀기 = 찢긴 종이
    '<feTurbulence type="fractalNoise" baseFrequency="0.035" numOctaves="3" seed="' + seed + '" result="n"/>',
    '<feDisplacementMap in="SourceGraphic" in2="n" scale="12" xChannelSelector="R" yChannelSelector="G" result="torn"/>',
    // 가로로 긴 섬유 결 → 갈색 반투명 → 종이 안에만
    '<feTurbulence type="fractalNoise" baseFrequency="0.9 0.05" numOctaves="2" seed="' + (seed + 3) + '" result="fib"/>',
    '<feColorMatrix in="fib" type="matrix" values="0 0 0 0 0.45  0 0 0 0 0.33  0 0 0 0 0.16  0 0 0 0.7 -0.18" result="fibc"/>',
    '<feComposite in="fibc" in2="torn" operator="in" result="fibIn"/>',
    // 큰 얼룩
    '<feTurbulence type="fractalNoise" baseFrequency="0.012" numOctaves="2" seed="' + (seed + 6) + '" result="blot"/>',
    '<feColorMatrix in="blot" type="matrix" values="0 0 0 0 0.6  0 0 0 0 0.45  0 0 0 0 0.2  0 0 0 0.6 -0.25" result="blotc"/>',
    '<feComposite in="blotc" in2="torn" operator="in" result="blotIn"/>',
    '<feMerge><feMergeNode in="torn"/><feMergeNode in="fibIn"/><feMergeNode in="blotIn"/></feMerge>',
    '</filter>',
    '<filter id="' + id + 's"><feDropShadow dx="2" dy="4" stdDeviation="3" flood-opacity=".45"/></filter>',
  ].join('');
}
// 쓰기: 종이 사각형에만 필터, 글자는 그 위에 따로 (흔들리지 않게)
// <defs>paperFilter('pf1')</defs>
// <g filter="url(#pf1s)"><rect width="124" height="148" fill="#f3e6c8" filter="url(#pf1)"/></g>
// <text ...>검사 보고서</text>
```

## 흔한 실수 · 확인 목록
- [ ] **필터 영역을 넓히지 않으면 찢긴 가장자리가 네모로 잘린다** — filter 에 x="-10%" y="-10%" width="120%" height="120%".
- [ ] **글자까지 같은 필터 안에 넣으면 글자가 흔들려 읽기 어렵다** — 필터는 종이 사각형에만 걸고, 글자 · 줄은 바깥 g 에 따로 그린다.
- [ ] **같은 화면에 같은 필터 id 가 둘이면 엉뚱한 필터가 걸린다** — 견본처럼 번호를 붙여(d2pf1, d2pf2 …) id 를 겹치지 않게.
- [ ] **판이 움직이거나 커지는 애니메이션에 걸면 폰에서 버벅인다** — 정지한 판에만 쓰거나, 한 번 그림으로 구워 둔다.

## 완성 기준 체크리스트
- [ ] 그냥 네모와 나란히 보면 오른쪽만 가장자리가 찢기고 섬유 결 · 얼룩이 보인다
- [ ] 「찢김 세기」 0 → 30 으로 가장자리 흔들림이 커진다
- [ ] seed 를 바꾸면 다른 무늬의 종이가 된다
- [ ] 종이 위 글자 · 줄은 흔들리지 않는다 (필터는 종이 사각형에만)

## 이 기술 정보
- id: `u58` · 분류: 2D · 화면 › 2D 그리기 · 2D · 난이도 보통 · 폰 부담 보통 (폰 주의) — 필터는 그 영역 화소마다 잡음 3장을 계산한다. 작은 판 몇 장은 괜찮지만 판이 움직이거나 커지면 폰에서 무거울 수 있다.
- 라이브 견본 (브라우저에서 직접 조작): https://ai-techstudio.web.app/#t/u58
- 쓰면 좋을 때: HTML · SVG 판을 손으로 만든 종이처럼 보이게 할 때 / 같은 판을 여러 장 다르게 보이게 (seed 만 바꿔서)
- 쓰지 말 때: 매 프레임 크기가 바뀌는 큰 판 — 필터를 계속 다시 계산해 느리다. 대신 한 번 구운 그림 / 아주 큰 화면 전체에 — 필터 영역이 클수록 비싸다

## 견본 실제 코드 (라이브 견본이 돌리는 코드 — three.js · TypeScript)
### u58 견본 항목 — `src/demos/demosDraw2d.ts:900`
```ts
  u58: {
    kind: 'dom',
    caption: '왼쪽 그냥 네모 · 오른쪽 feTurbulence 잡음으로 종이 섬유 + 찢긴 가장자리 (씨앗이 바뀌면 다른 종이)',
    make(box) {
      const id = `d2pf${++uid}`;
      const root = domRoot(
        box,
        'd2-paper',
        `.d2-paper{background:repeating-linear-gradient(90deg,#5a3a22 0 9cqmin,#4e321d 9cqmin 10cqmin,#63412a 10cqmin 18cqmin)}
         .d2-paper svg{width:100%;height:100%;display:block}
         .d2-paper .lb{position:absolute;top:3cqmin;padding:.4em .9em;border-radius:99px;font-size:5cqmin;font-weight:800;background:rgba(0,0,0,.55)}
         .d2-paper .lb.r{background:#ff6fa8}`,
        `<svg viewBox="0 0 320 200" preserveAspectRatio="xMidYMid meet">
          <defs>
            <filter id="${id}" x="-10%" y="-10%" width="120%" height="120%">
              <feTurbulence class="tb1" type="fractalNoise" baseFrequency="0.035" numOctaves="3" seed="2" result="n"/>
              <feDisplacementMap class="dm" in="SourceGraphic" in2="n" scale="12" xChannelSelector="R" yChannelSelector="G" result="torn"/>
              <feTurbulence class="tb2" type="fractalNoise" baseFrequency="0.9 0.05" numOctaves="2" seed="2" result="fib"/>
              <feColorMatrix in="fib" type="matrix" values="0 0 0 0 0.45  0 0 0 0 0.33  0 0 0 0 0.16  0 0 0 0.7 -0.18" result="fibc"/>
              <feComposite in="fibc" in2="torn" operator="in" result="fibIn"/>
              <feTurbulence class="tb3" type="fractalNoise" baseFrequency="0.012" numOctaves="2" seed="5" result="blot"/>
              <feColorMatrix in="blot" type="matrix" values="0 0 0 0 0.6  0 0 0 0 0.45  0 0 0 0 0.2  0 0 0 0.6 -0.25" result="blotc"/>
              <feComposite in="blotc" in2="torn" operator="in" result="blotIn"/>
              <feMerge><feMergeNode in="torn"/><feMergeNode class="mf" in="fibIn"/><feMergeNode in="blotIn"/></feMerge>
            </filter>
            <filter id="${id}s"><feDropShadow dx="2" dy="4" stdDeviation="3" flood-opacity=".45"/></filter>
          </defs>
          <g class="pl" filter="url(#${id}s)"><rect x="18" y="34" width="124" height="148" fill="#f3e6c8"/></g>
          <g class="pr" filter="url(#${id}s)"><rect x="178" y="34" width="124" height="148" fill="#f3e6c8" filter="url(#${id})"/></g>
          <g font-family="'Black Han Sans', sans-serif" fill="#5b3b14" text-anchor="middle">
            <text x="80" y="64" font-size="14">검사 보고서</text><text x="240" y="64" font-size="14">검사 보고서</text>
          </g>
          <g stroke="#a88a5c" stroke-width="1.2">
            ${[0, 1, 2, 3, 4].map((i) => `<line x1="32" x2="128" y1="${86 + i * 18}" y2="${86 + i * 18}"/><line x1="192" x2="288" y1="${86 + i * 18}" y2="${86 + i * 18}"/>`).join('')}
          </g>
        </svg>
        <div class="lb" style="left:12%">그냥 네모</div><div class="lb r" style="left:62%">종이 결 필터</div>`,
      );
      const tbs = qa(root, '.tb1, .tb2, .tb3');
      const dm = q(root, '.dm');
      const mf = q(root, '.mf');
      const pl = q(root, '.pl');
      const pr = q(root, '.pr');
      let seed = -1;
      let auto = true;
      return {
        update(t) {
          const s = auto ? 1 + (Math.floor(t / 1.6) % 12) : 3;
          if (s !== seed) {
            seed = s;
            tbs.forEach((e, i) => e.setAttribute('seed', String(s + i * 3)));
          }
          pl.setAttribute('transform', `rotate(${Math.sin(t * 0.8) * 2} 80 108)`);
          pr.setAttribute('transform', `rotate(${Math.sin(t * 0.8 + 1) * 2} 240 108)`);
        },
        controls: [
          { type: 'range', label: '찢김 세기', min: 0, max: 30, step: 1, value: 12, on: (v) => dm.setAttribute('scale', String(v)) },
          { type: 'toggle', label: '섬유 결', value: true, on: (v) => mf.setAttribute('in', v ? 'fibIn' : 'torn') },
          { type: 'toggle', label: '씨앗 자동으로 바꾸기', value: true, on: (v) => (auto = v) },
        ] as Control[],
      };
    },
  }
```

## 관련 기술
- 먼저 알면 좋은 기술: [코드로 그린 SVG 아이콘 · 도장](https://ai-techstudio.web.app/ai/t/u57.md) `u57`
- 다음에 해 볼 기술: [9분할 그림 (캔버스로 한 장)](https://ai-techstudio.web.app/ai/t/u56.md) `u56`
- 참고 문서: [MDN — <feTurbulence>](https://developer.mozilla.org/en-US/docs/Web/SVG/Element/feTurbulence) · [MDN — <feDisplacementMap>](https://developer.mozilla.org/en-US/docs/Web/SVG/Element/feDisplacementMap)
