# AI 꾸러미 — 홀로 포일 카드 — Holographic foil card effect
> 수집 카드 위에 무지개 띠 · 반짝이 층을 color-dodge 로 겹치고 기울기에 따라 옮겨서, 기울일 때마다 빛 띠가 미끄러지는 홀로 포일을 만든다.  
> 견본: https://ai-techstudio.web.app/#t/i220

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

## 주문서

### 만들어 줘: 홀로 포일 카드 — Holographic foil card effect

#### 1. 목표
수학자 수집 카드를 홀로 포일 카드로 만들어 줘 — 마우스(폰은 저절로)로 기울이면 무지개 빛 띠가 미끄러지고 반짝이가 빛 쪽에서만 반짝이게. 분위기는 남색 바탕 금테.

#### 2. 핵심 기술 용어
- **Holographic foil card effect** — 홀로그램 포일 카드
- **CSS mix-blend-mode: color-dodge** — 아래 색을 밝게 태우는 섞기
- **repeating-linear-gradient + background-position** — 무지개 줄을 크게 깔고 위치만 옮기기
- **CSS 3D tilt (perspective · rotateX/Y)** — 마우스 쪽으로 카드 기울이기

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

#### 4. 조건
- 무지개 · 반짝이 층은 mix-blend-mode: color-dodge, 빛 반사 층은 overlay
- 카드 자체에 isolation: isolate — 섞기가 카드 밖 배경까지 번지지 않게
- 움직이는 건 background-position 과 transform 뿐 — 그러데이션을 매 프레임 새로 만들지 않는다 (빛 위치 radial 은 예외)
- 마우스가 없으면(폰 · 1.2초 손 뗌) 저절로 천천히 기울어진다
- 「홀로 포일」 켬/끔 · 「빛 세기」 조절

#### 5. 완성 기준 (이게 보이면 성공)
- 마우스를 카드 위에서 움직이면 카드가 그쪽으로 기울고 무지개 띠가 반대쪽으로 미끄러진다
- 반짝이 점은 빛이 닿는 쪽에서만 보이고 반대쪽은 어둡다
- 「홀로 포일」을 끄면 빛 반사 하나만 남은 평범한 카드가 된다
- 폰(마우스 없음)에서도 저절로 흔들리며 빛이 움직인다

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

## 원리
- 카드 위에 층을 셋 겹친다: 무지개 줄(holo) · 반짝이 점(spark) · 빛 반사(glare). 모두 pointer-events: none.
- 무지개 층은 115° 무지개 repeating-linear-gradient 를 300% 크기로 깔고, 기울기 nx · ny 에 따라 background-position 만 옮긴다 — 그래서 띠가 미끄러져 보인다.
- color-dodge 로 섞으면 어두운 곳은 거의 그대로, 밝은 곳은 무지갯빛으로 탄다. 그림 칸에는 반대로 움직이는 둘째 무지개 층을 하나 더.
- 반짝이 층은 빛이 닿는 자리를 가운데로 한 radial-gradient 마스크로 가려, 빛 쪽에서만 반짝인다.
- 카드는 perspective 안에서 rotateX(−ny·13°) rotateY(nx·17°). 기울기는 1 − e^(−9dt) 로 부드럽게 따라가고, 1.2초 손을 떼면 저절로 흔들린다.

## 핵심 코드 — 기울기 → 층 위치 · 마스크 · 카드 회전
(발췌: demos/demosJuice.ts D220 update() · tiltRig() 를 정리)
```ts
// CSS (요지)
// .card  { isolation:isolate; overflow:hidden; }
// .holo  { mix-blend-mode:color-dodge; background-size:300% 300%;
//          background-image:repeating-linear-gradient(115deg,#ff5fa2 0%,#ffcf5c 7%,#6dffb0 14%,#5cc8ff 21%,#b18cff 28%,#ff5fa2 35%); }
// .spark { mix-blend-mode:color-dodge; background-image:radial-gradient(circle,#fff 0 .35cqmin,transparent .8cqmin); background-size:7cqmin 7cqmin; }
// .glare { mix-blend-mode:overlay; }

let nx = 0, ny = 0;              // 부드럽게 따라가는 기울기 (−1 ~ 1)
function update(t: number, dt: number, mx: number, my: number, idle: boolean, power: number): void {
  const gx = idle ? Math.sin(t * 1.25) * 0.85 : mx;     // 손 떼면 저절로
  const gy = idle ? Math.sin(t * 1.7 + 1.2) * 0.65 : my;
  const k = 1 - Math.exp(-Math.min(dt, 0.05) * 9);
  nx += (gx - nx) * k;
  ny += (gy - ny) * k;
  stage.style.transform = 'translate(-50%,-50%) rotateX(' + (-ny * 13).toFixed(2) + 'deg) rotateY(' + (nx * 17).toFixed(2) + 'deg)';
  const lx = 50 + nx * 50, ly = 50 + ny * 50;            // 빛이 닿는 곳 (%)
  const mag = Math.min(1, Math.hypot(nx, ny));
  holo.style.opacity = String((0.28 + 0.3 * mag) * power);
  holo.style.backgroundPosition = (50 + nx * 60).toFixed(1) + '% ' + (50 + ny * 60).toFixed(1) + '%';
  const m = 'radial-gradient(circle at ' + lx.toFixed(1) + '% ' + ly.toFixed(1) + '%, #000 0%, rgba(0,0,0,.35) 30%, transparent 55%)';
  spark.style.maskImage = m;            // 빛 쪽만 반짝
  spark.style.webkitMaskImage = m;
  spark.style.opacity = String((0.6 + 0.4 * Math.sin(t * 9)) * power);
  glare.style.background = 'radial-gradient(circle at ' + lx.toFixed(1) + '% ' + ly.toFixed(1) + '%, rgba(255,255,255,.8), rgba(255,255,255,.1) 35%, rgba(0,0,0,.3) 90%)';
}
```

## 흔한 실수 · 확인 목록
- [ ] **isolation 없이 섞으면 무지개가 카드 밖 배경까지 태운다** — 카드 상자에 isolation: isolate (또는 자체 쌓임 맥락)를 준다.
- [ ] **그러데이션 각도를 매 프레임 바꾸면 버벅인다** — 큰 그러데이션을 한 번 깔고 background-position 만 옮긴다.
- [ ] **-webkit-mask 를 빼먹으면 사파리에서 반짝이가 전부 보인다** — maskImage 와 webkitMaskImage 를 함께 넣는다.
- [ ] **폰에서는 마우스가 없어 그냥 정지 카드가 된다** — 마지막 입력 뒤 1.2초가 지나면 sin 으로 저절로 기울인다.

## 완성 기준 체크리스트
- [ ] 마우스를 카드 위에서 움직이면 카드가 그쪽으로 기울고 무지개 띠가 반대쪽으로 미끄러진다
- [ ] 반짝이 점은 빛이 닿는 쪽에서만 보이고 반대쪽은 어둡다
- [ ] 「홀로 포일」을 끄면 빛 반사 하나만 남은 평범한 카드가 된다
- [ ] 폰(마우스 없음)에서도 저절로 흔들리며 빛이 움직인다

## 이 기술 정보
- id: `i220` · 분류: 이펙트 · 연출 › 손맛 · 주스 · 공통 · 난이도 보통 · 폰 부담 가벼움 (폰 OK) — CSS 층 4장과 transform 만 바꾼다. 카드 한두 장은 폰에서도 가볍지만, 수십 장을 동시에 섞으면 GPU 합성이 무거워진다.
- 라이브 견본 (브라우저에서 직접 조작): https://ai-techstudio.web.app/#t/i220
- 쓰면 좋을 때: 보상 카드 · 배지처럼 「갖고 싶은」 느낌이 필요할 때 / 등급(★★★)이 높은 것만 특별해 보여야 할 때
- 쓰지 말 때: 글씨를 많이 읽어야 하는 카드 — color-dodge 가 글씨를 태운다. 글씨 칸은 층 아래가 아니라 위에 두거나 세기를 낮춘다 / 한 화면에 카드 수십 장 — 섞기 층이 많아 느려진다. 고른 한 장만 포일

## 견본 실제 코드 (라이브 견본이 돌리는 코드 — three.js · TypeScript)
### D220 — `src/demos/demosJuice.ts:2630`
```ts
const D220: Demo = {
  kind: 'dom',
  caption: '수학자 수집 카드 — 기울이면 무지개 빛 띠가 미끄러지고 반짝이가 빛 쪽에서만 반짝 (color-dodge 겹치기)',
  make(box) {
    const root = domRoot(
      box,
      'jx-holo',
      `.jx-holo{background:radial-gradient(110% 90% at 50% 10%,#3a2b6e,#0b0820);perspective:110cqmin}
       .jx-holo .stage{position:absolute;left:50%;top:50%;width:62cqmin;height:86cqmin;transform:translate(-50%,-50%)}
       .jx-holo .card{position:absolute;inset:0;border-radius:5cqmin;isolation:isolate;overflow:hidden;background:linear-gradient(160deg,#2a2f6e,#141640);box-shadow:0 3cqmin 6cqmin rgba(0,0,0,.6)}
       .jx-holo .frame{position:absolute;inset:2.4cqmin;border-radius:3.4cqmin;border:.8cqmin solid #c9a64a;box-shadow:inset 0 0 0 .5cqmin #fff3c4}
       .jx-holo .art{position:absolute;left:7%;right:7%;top:13%;height:50%;border-radius:2.4cqmin;background:linear-gradient(#1d2b6b,#3a4fb0);overflow:hidden}
       .jx-holo .art svg{width:100%;height:100%}
       .jx-holo .nm{position:absolute;left:8%;top:4%;font-size:5.6cqmin;font-weight:900;color:#fff3c4}
       .jx-holo .rk{position:absolute;right:8%;top:4.4%;font-size:4.6cqmin;color:#ffd43b;letter-spacing:.3cqmin}
       .jx-holo .eq{position:absolute;left:0;right:0;top:67%;text-align:center;font-size:7cqmin;font-weight:900;color:#fff}
       .jx-holo .tx{position:absolute;left:10%;right:10%;top:79%;text-align:center;font-size:3.9cqmin;line-height:1.35;color:#c7cbf2}
       .jx-holo .holo,.jx-holo .holo2,.jx-holo .spark,.jx-holo .glare{position:absolute;inset:0;pointer-events:none}
       .jx-holo .holo{mix-blend-mode:color-dodge;background-image:repeating-linear-gradient(115deg,#ff5fa2 0%,#ffcf5c 7%,#6dffb0 14%,#5cc8ff 21%,#b18cff 28%,#ff5fa2 35%),repeating-linear-gradient(25deg,rgba(255,255,255,.0) 0 1.2cqmin,rgba(255,255,255,.12) 1.2cqmin 1.6cqmin);background-size:300% 300%,100% 100%}
       .jx-holo .holo2{left:7%;right:7%;top:13%;height:50%;inset:auto;border-radius:2.4cqmin;mix-blend-mode:color-dodge;background-image:repeating-linear-gradient(115deg,#ff5fa2 0%,#ffcf5c 6%,#6dffb0 12%,#5cc8ff 18%,#b18cff 24%,#ff5fa2 30%);background-size:260% 260%}
       .jx-holo .spark{mix-blend-mode:color-dodge;background-image:radial-gradient(circle,#fff 0 .35cqmin,transparent .8cqmin),radial-gradient(circle,#fff 0 .25cqmin,transparent .6cqmin);background-size:7cqmin 7cqmin,4.6cqmin 4.6cqmin}
       .jx-holo .glare{mix-blend-mode:overlay}`,
      `<div class="stage"><div class="card">
         <div class="art"><svg viewBox="0 0 100 70">
           <g fill="none" stroke="rgba(255,255,255,.12)" stroke-width=".5">${Array.from({ length: 12 }, (_, i) => `<line x1="${i * 9}" y1="0" x2="${i * 9}" y2="70"/>`).join('')}</g>
           <polygon points="38,48 62,48 38,30" fill="#fff" stroke="#1d2b6b" stroke-width="1.2"/>
           <rect x="38" y="48" width="24" height="18" fill="#ff6b8a" stroke="#fff" stroke-width="1.2"/>
           <rect x="20" y="30" width="18" height="18" fill="#69db7c" stroke="#fff" stroke-width="1.2"/>
           <polygon points="38,30 62,48 80,24 56,6" fill="#ffd43b" stroke="#fff" stroke-width="1.2"/>
           <text x="50" y="60" font-size="7" font-weight="900" text-anchor="middle" fill="#fff" font-family="${FCSS}">a²</text>
           <text x="29" y="42" font-size="7" font-weight="900" text-anchor="middle" fill="#fff" font-family="${FCSS}">b²</text>
           <text x="59" y="30" font-size="8" font-weight="900" text-anchor="middle" fill="#7a4b00" font-family="${FCSS}">c²</text>
         </svg></div>
         <div class="holo2"></div>
         <div class="frame"></div>
         <div class="nm">피타고라스</div><div class="rk">★★★</div>
         <div class="eq">a² + b² = c²</div>
         <div class="tx">직각삼각형의 두 변 위 정사각형을 더하면 빗변 위 정사각형</div>
         <div class="holo"></div><div class="spark"></div><div class="glare"></div>
       </div></div>`,
    );
    const stage = q(root, '.stage');
    const holo = q(root, '.holo');
    const holo2 = q(root, '.holo2');
    const spark = q(root, '.spark');
    const glare = q(root, '.glare');
    const rig = tiltRig(root);
    let on = true;
    let power = 1;
    return {
      update(t, dt) {
        rig.step(t, dt);
        const { nx, ny } = rig;
        stage.style.transform = `translate(-50%,-50%) rotateX(${(-ny * 13).toFixed(2)}deg) rotateY(${(nx * 17).toFixed(2)}deg)`;
        const lx = 50 + nx * 50;
        const ly = 50 + ny * 50;
        const mag = Math.min(1, Math.hypot(nx, ny));
        if (!on) {
          holo.style.opacity = holo2.style.opacity = spark.style.opacity = '0';
          glare.style.background = `radial-gradient(circle at ${lx}% ${ly}%, rgba(255,255,255,.35), transparent 60%)`;
          return;
        }
        holo.style.opacity = String((0.28 + 0.3 * mag) * power);
        holo2.style.opacity = String((0.45 + 0.4 * mag) * power);
        holo.style.backgroundPosition = `${(50 + nx * 60).toFixed(1)}% ${(50 + ny * 60).toFixed(1)}%, 0 0`;
        holo2.style.backgroundPosition = `${(50 - nx * 80).toFixed(1)}% ${(50 - ny * 80).toFixed(1)}%`;
        const m = `radial-gradient(circle at ${lx.toFixed(1)}% ${ly.toFixed(1)}%, #000 0%, rgba(0,0,0,.35) 30%, transparent 55%)`;
        spark.style.maskImage = m;
        spark.style.webkitMaskImage = m;
        spark.style.backgroundPosition = `${(nx * 3).toFixed(2)}cqmin ${(ny * 3).toFixed(2)}cqmin, ${(-nx * 2).toFixed(2)}cqmin ${(-ny * 2).toFixed(2)}cqmin`;
        spark.style.opacity = String((0.6 + 0.4 * Math.sin(t * 9)) * power);
        glare.style.background = `radial-gradient(circle at ${lx.toFixed(1)}% ${ly.toFixed(1)}%, rgba(255,255,255,.8), rgba(255,255,255,.1) 35%, rgba(0,0,0,.3) 90%)`;
      },
      controls: [tgl('홀로 포일', on, (v) => (on = v)), rng('빛 세기', 0.2, 1.8, 0.05, power, (v) => (power = v))],
      dispose() {
        rig.dispose();
      },
    };
  },
};
```

## 관련 기술
- 같은 방법으로 만든 효과 예시: [마우스 따라 3D 카드 기울기](https://ai-techstudio.web.app/ai/t/i219.md) `i219`
- 먼저 알면 좋은 기술: [만화 연출 층 (DOM)](https://ai-techstudio.web.app/ai/t/u38.md) `u38`
- 다음에 해 볼 기술: [홀로그램 · 주사선 셰이더](https://ai-techstudio.web.app/ai/t/i221.md) `i221`
- 참고 문서: [MDN — mix-blend-mode](https://developer.mozilla.org/en-US/docs/Web/CSS/mix-blend-mode)
