# AI 꾸러미 — 레벨 업 연출 — LEVEL UP
> 게이지 꽉 → 번쩍 → 「LEVEL UP」 회전 착지 → 빛살  
> 견본: https://ai-techstudio.web.app/#t/i212

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

## 주문서

### 만들어 줘: 레벨 업 연출 — LEVEL UP

#### 1. 목표
실력 단계에 「레벨 업 연출」 기술을 깔끔한 기본 느낌으로 넣어 줘. — 게이지 꽉 → 번쩍 → 「LEVEL UP」 회전 착지 → 빛살

#### 2. 핵심 기술 용어
- **LEVEL UP**

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

#### 4. 조건
- 화면 코드와 계산 코드를 나눠서 (계산은 순수 함수)
- 한 프레임 16ms 안 — 오래 걸리는 계산은 나눠서

#### 5. 완성 기준 (이게 보이면 성공)
- 견본처럼 「게이지 꽉 → 번쩍 → 「LEVEL UP」 회전 착지 → 빛살」 이(가) 첫 화면에서 바로 보인다
- 켬/끔(또는 전/후) 비교 단추로 이 기술이 만드는 차이를 확인할 수 있다
- 중요한 값 1~3개를 슬라이더로 바꿔 볼 수 있다
- 콘솔 오류 0, 폰 폭(390px)에서도 동작한다

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

## 원리
- 게이지 꽉 → 번쩍 → 「LEVEL UP」 회전 착지 → 빛살


## 흔한 실수 · 확인 목록
- (아직 정리된 것이 없어요)

## 완성 기준 체크리스트
- [ ] 견본처럼 「게이지 꽉 → 번쩍 → 「LEVEL UP」 회전 착지 → 빛살」 이(가) 첫 화면에서 바로 보인다
- [ ] 켬/끔(또는 전/후) 비교 단추로 이 기술이 만드는 차이를 확인할 수 있다
- [ ] 중요한 값 1~3개를 슬라이더로 바꿔 볼 수 있다
- [ ] 콘솔 오류 0, 폰 폭(390px)에서도 동작한다

## 이 기술 정보
- id: `i212` · 분류: 이펙트 · 연출 › 손맛 · 주스 · 공통 · 난이도 보통 · 폰 부담 보통 (폰 주의)
- 라이브 견본 (브라우저에서 직접 조작): https://ai-techstudio.web.app/#t/i212
- 쓰면 좋을 때: 쓰면 좋을 곳 — 실력 단계 · 월드 해금

## 견본 실제 코드 (라이브 견본이 돌리는 코드 — three.js · TypeScript)
### D212 — `src/demos/demosJuice.ts:1481`
```ts
const D212: Demo = {
  kind: '2d',
  caption: '경험치 게이지가 꽉 → 번쩍 → 「LEVEL UP」 이 돌며 내려앉고 빛살 · 꽃가루, 배지가 Lv.4 → Lv.5 로 뒤집힘',
  make() {
    let juice = true;
    let T = 0;
    let tOff = 0;
    const P = 4.4;
    const FULL = 1.05;
    return {
      draw(g, w, h, t) {
        reset(g);
        T = t;
        const u = Math.min(w / 280, h / 175);
        const ph = (T - tOff) % P;
        const a = ph - FULL;
        const fade = 1 - smooth(4.0, 4.4, ph);
        bg(g, w, h, '#162a5c', '#07102a');
        g.save();
        if (juice && a >= 0 && a < 0.5) {
          const k = Math.exp(-a * 8) * 4 * u;
          g.translate(noise1(a * 60, 3) * k, noise1(a * 60, 4) * k);
        }
        const cx = w / 2;
        const cy = h * 0.42;
        // 빛살
        if (juice && a > 0.05) {
          g.save();
          g.translate(cx, cy);
          g.rotate(T * 0.6);
          g.globalAlpha = 0.3 * smooth(0.05, 0.4, a) * fade;
          const rg = g.createRadialGradient(0, 0, 0, 0, 0, 150 * u);
          rg.addColorStop(0, '#fff3a0');
          rg.addColorStop(1, 'rgba(255,212,59,0)');
          g.fillStyle = rg;
          for (let i = 0; i < 14; i++) {
            g.rotate(TAU / 14);
            g.beginPath();
            g.moveTo(0, 0);
            g.lineTo(-12 * u, -160 * u);
            g.lineTo(12 * u, -160 * u);
            g.fill();
          }
          g.restore();
        }
        // 배지
        const bx = 42 * u;
        const by = h * 0.8;
        const flip = juice ? clamp01((a - 0.55) / 0.35) : a >= 0.5 ? 1 : 0;
        const fs = Math.abs(Math.cos(flip * Math.PI));
        const lv = flip > 0.5 ? 5 : 4;
        g.save();
        g.translate(bx, by);
        g.scale(Math.max(0.05, fs) * (juice && flip > 0.5 ? 1 + 0.25 * Math.exp(-(a - 0.9) * 6) * (a > 0.9 ? 1 : 0) : 1), 1);
        g.beginPath();
        g.arc(0, 0, 19 * u, 0, TAU);
        g.fillStyle = lv === 5 ? '#ffd43b' : '#748ffc';
        g.fill();
        g.lineWidth = 3 * u;
        g.strokeStyle = '#fff';
        g.stroke();
        otext(g, `Lv.${lv}`, 0, 1 * u, 11 * u, '#fff', lv === 5 ? '#a05a00' : '#28308a', 2.6 * u);
        g.restore();
        // 게이지
        const gx = 70 * u;
        const gw = w - gx - 16 * u;
        const gy = by - 7 * u;
        const gh = 14 * u;
        let fill: number;
        if (a < 0) fill = lerp(0.55, 1, clamp01(ph / FULL) ** 2.2);
        else if (a < 0.9) fill = 1;
        else fill = 0.18 * easeOut((a - 0.9) / 0.8);
        rr(g, gx, gy, gw, gh, gh / 2);
        g.fillStyle = '#0a1230';
        g.fill();
        const fg = g.createLinearGradient(0, gy, 0, gy + gh);
        fg.addColorStop(0, '#9ef0ff');
        fg.addColorStop(1, '#3b8cff');
        rr(g, gx, gy, gw * fill, gh, gh / 2);
        g.fillStyle = fg;
        g.fill();
        if (juice && a < 0) {
          // 흐르는 빛
          g.save();
          rr(g, gx, gy, gw * fill, gh, gh / 2);
          g.clip();
          const sx = gx + ((ph * 1.6) % 1) * gw * 1.3 - 20 * u;
          const lg = g.createLinearGradient(sx - 16 * u, 0, sx + 16 * u, 0);
          lg.addColorStop(0, 'rgba(255,255,255,0)');
          lg.addColorStop(0.5, 'rgba(255,255,255,.55)');
          lg.addColorStop(1, 'rgba(255,255,255,0)');
          g.fillStyle = lg;
          g.fillRect(sx - 16 * u, gy, 32 * u, gh);
          g.restore();
        }
        if (juice && a >= 0 && a < 0.9) {
          rr(g, gx, gy, gw, gh, gh / 2);
          g.fillStyle = `rgba(255,255,255,${0.9 * Math.exp(-a * 5)})`;
          g.fill();
        }
        txt(g, 'EXP', gx + 8 * u, gy - 7 * u, 7 * u, '#9fb4ff', 'left', 800);
        // 글씨
        if (a >= 0.1) {
          const k = (a - 0.1) / 0.4;
          g.save();
          g.globalAlpha = fade;
          if (juice) {
            const land = clamp01(k);
            const sc = lerp(3.2, 1, land * land) + (land >= 1 ? 0.12 * Math.exp(-(a - 0.5) * 8) * Math.cos((a - 0.5) * 30) : 0);
            g.translate(cx, cy);
            g.rotate(lerp(-1.1, -0.06, easeOut(land)));
            g.scale(sc, sc);
            g.globalAlpha *= clamp01(k * 3);
          } else g.translate(cx, cy);
          const tg = g.createLinearGradient(0, -16 * u, 0, 16 * u);
          tg.addColorStop(0, '#fffbe0');
          tg.addColorStop(0.5, '#ffd43b');
          tg.addColorStop(1, '#ff8a00');
          otext(g, 'LEVEL UP', 0, 0, 30 * u, juice ? tg : '#ffd43b', '#4a1d00', 6 * u);
          if (juice) {
            g.globalCompositeOperation = 'lighter';
            g.globalAlpha *= 0.35 * Math.exp(-Math.max(0, a - 0.5) * 3);
            otext(g, 'LEVEL UP', 0, 0, 30 * u, '#ffffff', 'rgba(0,0,0,0)', 0);
          }
          g.restore();
          if (juice) {
            const ia = a - 0.5;
            ringFx(g, cx, cy, ia, 0.5, 30 * u, 140 * u, '#fff3a0', 4 * u);
            sparks(g, cx, cy, ia, 24, 77, 110 * u, ['#ffffff', '#ffe066', '#74c0fc', '#ff8787'], 0.8, 30 * u, 3 * u);
            // 꽃가루
            if (ia > 0) {
              for (let i = 0; i < 26; i++) {
                const fx = (hash(i, 61) * 1.2 - 0.1) * w;
                const fy = -10 * u + (ia - hash(i, 62) * 0.5) * (60 + hash(i, 63) * 50) * u;
                if (fy < -10 * u || fy > h) continue;
                g.save();
                g.globalAlpha = fade;
                g.translate(fx + Math.sin(ia * 4 + i) * 6 * u, fy);
                g.rotate(ia * 6 + i);
                g.scale(Math.cos(ia * 8 + i), 1);
                g.fillStyle = BLK[i % 6]!;
                g.fillRect(-3 * u, -1.6 * u, 6 * u, 3.2 * u);
                g.restore();
              }
            }
          }
        }
        // 흰 번쩍
        if (juice && a >= 0 && a < 0.15) {
          g.fillStyle = `rgba(255,255,255,${0.75 * (1 - a / 0.15)})`;
          g.fillRect(-10, -10, w + 20, h + 20);
        }
        g.restore();
        reset(g);
      },
      controls: [
        tgl('주스 켜기', juice, (v) => (juice = v)),
        { type: 'button', label: '다시 보기', on: () => (tOff = T) },
      ],
    };
  },
};
```

## 관련 기술
- 이 효과를 만든 기술: [층 쌓기 (핵 · 빛무리 · 불티 · 연기)](https://ai-techstudio.web.app/ai/t/i171.md) `i171`
