# AI 꾸러미 — 이징 곡선 모음 (트윈 비교) — Easing functions (tweening)
> 같은 거리 · 같은 시간의 이동을 linear · easeInOut · easeOut · back · elastic · bounce 로 나란히 돌리고 곡선 그래프를 붙여, 움직임 맛을 비교해 고르게 한다.  
> 견본: https://ai-techstudio.web.app/#t/i292

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

## 주문서

### 만들어 줘: 이징 곡선 모음 (트윈 비교) — Easing functions (tweening)

#### 1. 목표
단추 · 카드 · 창 움직임에 이징 곡선을 골라 넣어 줘 — 같은 이동을 linear · easeOut · back · elastic · bounce 로 비교하고 곡선 그래프도 함께. 분위기는 통통 튀는 귀여움.

#### 2. 핵심 기술 용어
- **Easing functions (tweening)** — 이징 — 0→1 시간을 0→1 위치로 바꾸는 곡선
- **easeOutCubic · easeInOutCubic** — 감속 · 살살-빠르게-살살
- **easeOutBack · easeOutElastic · easeOutBounce** — 넘쳤다 돌아옴 · 고무줄 · 튀며 멈춤
- **Lerp** — 선형 보간 — 시작 + (끝 − 시작) × 값

#### 3. 환경
- 플랫폼: HTML Canvas 2D (TypeScript), requestAnimationFrame 루프, 라이브러리 없이
- 화면: 2D · 브라우저 — PC 와 폰(390px 폭) 모두, devicePixelRatio 맞춰 또렷하게, 60fps 목표

#### 4. 조건
- 이징 함수는 p 를 0 ~ 1 로 받아 f(0) = 0, f(1) = 1 이 되게 (back · elastic 은 중간에 1 을 넘어도 됨)
- p 는 반드시 0 ~ 1 로 자르고 넣기
- 같은 거리 · 같은 시간으로 나란히 비교 + 곡선 그래프(시간 → 위치)를 옆에
- 크기에 back · elastic 을 쓸 때는 음수 크기가 되지 않게 막기 (견본은 −0.3 ~ 1.4 로 자름)

#### 5. 완성 기준 (이게 보이면 성공)
- 여섯 줄이 같은 순간 출발해 같은 순간 도착하는데 움직임 맛이 다르다
- 「같은 시간 간격 점」을 켜면 easeOut 은 끝쪽 점이 촘촘하다
- 걸리는 시간 슬라이더로 모든 줄이 함께 빨라지고 느려진다
- 「크기에도 적용」을 켜면 back · elastic 이 크기로도 통통 튄다

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

## 원리
- 진행률 p = 지난 시간 / 걸리는 시간 (0 → 1). 이징 함수 f 는 p 를 「얼마나 왔나」로 바꾼다.
- 위치 = lerp(시작, 끝, f(p)). 거리 · 시간은 같고 f 만 다르다.
- back 은 1 을 살짝 넘었다 돌아오고(c1 = 1.70158), elastic 은 2^(−10p) × sin 으로 출렁, bounce 는 포물선 4토막으로 튄다.
- 같은 시간 간격 점(1/12 마다)을 찍으면 점이 촘촘한 곳 = 느린 곳, 성긴 곳 = 빠른 곳이 보인다.

## 핵심 코드 — 이징 여섯 가지와 쓰는 법
(발췌: demos/demos2dMove.ts EASE 표 · i292 draw() 를 정리)
```ts
export const EASE: Record<string, (x: number) => number> = {
  linear: (x) => x,
  easeInOut: (x) => (x < 0.5 ? 4 * x * x * x : 1 - Math.pow(-2 * x + 2, 3) / 2),
  easeOut: (x) => 1 - Math.pow(1 - x, 3),
  back: (x) => {
    const c1 = 1.70158, c3 = c1 + 1;               // 살짝 넘쳤다 돌아옴
    return 1 + c3 * Math.pow(x - 1, 3) + c1 * Math.pow(x - 1, 2);
  },
  elastic: (x) => (x === 0 ? 0 : x === 1 ? 1 : Math.pow(2, -10 * x) * Math.sin((x * 10 - 0.75) * ((2 * Math.PI) / 3)) + 1),
  bounce: (x) => {
    const n1 = 7.5625, d1 = 2.75;                   // 포물선 4토막
    if (x < 1 / d1) return n1 * x * x;
    if (x < 2 / d1) return n1 * (x -= 1.5 / d1) * x + 0.75;
    if (x < 2.5 / d1) return n1 * (x -= 2.25 / d1) * x + 0.9375;
    return n1 * (x -= 2.625 / d1) * x + 0.984375;
  },
};
const lerp = (a: number, b: number, k: number) => a + (b - a) * k;

/** 시작한 지 elapsed 초 — dur 초 동안 x0 → x1 */
function tweenX(x0: number, x1: number, elapsed: number, dur: number, ease = EASE.easeOut) {
  const p = Math.max(0, Math.min(1, elapsed / dur));  // 0 ~ 1 로 자르기
  return lerp(x0, x1, ease(p));
}
```

## 흔한 실수 · 확인 목록
- [ ] **p 를 자르지 않으면 끝난 뒤에도 back · elastic 이 계속 움직인다** — p 를 0 ~ 1 로 자르고, 끝나면 정확히 끝 값에 놓는다.
- [ ] **back · elastic 을 크기에 쓰면 순간 음수가 되어 뒤집힌다** — 크기용은 범위를 잘라 쓴다 (견본: −0.3 ~ 1.4 로 자른 뒤 0.6 + 0.6 × 값).
- [ ] **목표가 움직이는 중에 트윈을 새로 시작하면 덜컥 끊긴다** — 따라가기는 스프링(i293)으로 — 지금 속도를 이어받는다.

## 완성 기준 체크리스트
- [ ] 여섯 줄이 같은 순간 출발해 같은 순간 도착하는데 움직임 맛이 다르다
- [ ] 「같은 시간 간격 점」을 켜면 easeOut 은 끝쪽 점이 촘촘하다
- [ ] 걸리는 시간 슬라이더로 모든 줄이 함께 빨라지고 느려진다
- [ ] 「크기에도 적용」을 켜면 back · elastic 이 크기로도 통통 튄다

## 이 기술 정보
- id: `i292` · 분류: 2D · 화면 › 2D 움직임 · 충돌 · 2D · 난이도 쉬움 · 폰 부담 가벼움 (폰 OK) — 함수 한 번 계산 — 비용 없음.
- 라이브 견본 (브라우저에서 직접 조작): https://ai-techstudio.web.app/#t/i292
- 쓰면 좋을 때: 단추 · 카드 · 창이 나타나고 사라질 때 — 거의 모든 UI 움직임 / 어떤 맛이 좋은지 여럿을 나란히 비교해 고를 때
- 쓰지 말 때: 목표가 계속 바뀌는 따라가기 — 끊긴다. 대신 스프링(i293) / 물리로 움직여야 하는 것 — 이징은 정해진 길만 간다

## 견본 실제 코드 (라이브 견본이 돌리는 코드 — three.js · TypeScript)
### i292 — `src/demos/demos2dMove.ts:3852`
```ts
const i292: Demo2D = {
  kind: '2d',
  caption: '같은 거리 · 같은 시간인데 움직임 맛이 다르다 — 왼쪽 그래프(시간 → 위치)와 같은 간격 점으로 비교',
  make() {
    let dur = 1.2;
    let dots = true;
    let scale = false;
    return {
      controls: [
        { type: 'range', label: '걸리는 시간(초)', min: 0.4, max: 3, step: 0.1, value: 1.2, on: (v) => (dur = v) },
        { type: 'toggle', label: '같은 시간 간격 점', value: true, on: (v) => (dots = v) },
        { type: 'toggle', label: '크기에도 적용', value: false, on: (v) => (scale = v) },
      ] as Control[],
      draw(g, w, h, t) {
        const v = viewOf(w, h);
        const cyc = 2 * (dur + 0.45);
        const tt = t % cyc;
        let p: number;
        let back = false;
        if (tt < dur) p = tt / dur;
        else if (tt < dur + 0.45) p = 1;
        else if (tt < 2 * dur + 0.45) {
          p = (tt - dur - 0.45) / dur;
          back = true;
        } else {
          p = 1;
          back = true;
        }
        fillBg(g, w, h, '#141833', '#1f2650');
        enter(g, v);
        txt(g, '시간 → 위치', 25, 8, 6, 'rgba(255,255,255,.5)', 'center', 700);
        txt(g, '같은 거리를 같은 시간에', 185, 8, 6, 'rgba(255,255,255,.5)', 'center', 700);
        const X0 = 104;
        const X1 = 266;
        EASE.forEach((e, i) => {
          const cy = 26 + i * 25.4;
          // 줄 바탕
          rr(g, 3, cy - 11.5, 274, 23, 5);
          g.fillStyle = i % 2 ? 'rgba(255,255,255,.03)' : 'rgba(255,255,255,.055)';
          g.fill();
          // 그래프
          const gx = 7;
          const gw = 36;
          const gy0 = cy + 8;
          const gh = 15;
          const Y = (val: number): number => gy0 - val * gh;
          g.strokeStyle = 'rgba(255,255,255,.15)';
          g.lineWidth = 0.5;
          g.strokeRect(gx, Y(1), gw, gh);
          g.strokeStyle = e.c;
          g.lineWidth = 1.2;
          g.beginPath();
          for (let k = 0; k <= 40; k++) {
            const x = k / 40;
            const yy = Y(e.f(x));
            if (k) g.lineTo(gx + x * gw, yy);
            else g.moveTo(gx + x * gw, yy);
          }
          g.stroke();
          const ev = e.f(p);
          g.fillStyle = '#fff';
          g.beginPath();
          g.arc(gx + p * gw, Y(ev), 1.6, 0, TAU);
          g.fill();
          txt(g, e.name, 48, cy - 4, 7, e.c, 'left', 800);
          txt(g, e.sub, 48, cy + 5, 5.5, 'rgba(255,255,255,.6)', 'left', 600);
          // 길
          g.strokeStyle = 'rgba(255,255,255,.18)';
          g.lineWidth = 1;
          g.setLineDash([1.5, 2]);
          g.beginPath();
          g.moveTo(X0, cy);
          g.lineTo(X1, cy);
          g.stroke();
          g.setLineDash([]);
          for (const xx of [X0, X1]) {
            g.fillStyle = 'rgba(255,255,255,.3)';
            g.fillRect(xx - 0.5, cy - 5, 1, 10);
          }
          if (dots) {
            for (let k = 0; k <= 12; k++) {
              const q = e.f(k / 12);
              g.fillStyle = e.c;
              g.globalAlpha = 0.35;
              g.beginPath();
              g.arc(lerp(X0, X1, q), cy + 7.5, 1.1, 0, TAU);
              g.fill();
            }
            g.globalAlpha = 1;
          }
          const val = back ? 1 - ev : ev;
          const x = lerp(X0, X1, val);
          // 잔상
          for (let k = 1; k <= 4; k++) {
            const pp = clamp01(p - k * 0.025);
            const vv = back ? 1 - e.f(pp) : e.f(pp);
            g.fillStyle = e.c;
            g.globalAlpha = 0.12 * (5 - k) * (p > 0 && p < 1 ? 1 : 0);
            g.beginPath();
            g.arc(lerp(X0, X1, vv), cy - 1, 5, 0, TAU);
            g.fill();
          }
          g.globalAlpha = 1;
          const s = scale ? 0.6 + 0.6 * clamp(back ? 1 - ev : ev, -0.3, 1.4) : 1;
          const bg2 = g.createRadialGradient(x - 1.6, cy - 3, 0.5, x, cy - 1, 6 * s);
          bg2.addColorStop(0, '#fff');
          bg2.addColorStop(1, e.c);
          g.fillStyle = bg2;
          g.beginPath();
          g.arc(x, cy - 1, 5.5 * s, 0, TAU);
          g.fill();
          g.fillStyle = '#1a1433';
          g.beginPath();
          g.arc(x - 1.6 * s, cy - 1.8, 0.8 * s, 0, TAU);
          g.arc(x + 1.6 * s, cy - 1.8, 0.8 * s, 0, TAU);
          g.fill();
        });
        g.restore();
      },
    };
  },
};
```

## 관련 기술
- 먼저 알면 좋은 기술: [탄막 무늬 (극좌표 · 나선)](https://ai-techstudio.web.app/ai/t/i291.md) `i291`
- 다음에 해 볼 기술: [스프링 · 감쇠 움직임 (임계 감쇠)](https://ai-techstudio.web.app/ai/t/i293.md) `i293`
- 참고 문서: [easings.net — 이징 함수 모음](https://easings.net/)
