# AI 꾸러미 — 계기 바늘 물리
> 바늘이 목표를 지나쳤다 떨며 멈춤 — 감쇠 스프링  
> 견본: https://ai-techstudio.web.app/#t/i218

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

## 주문서

### 만들어 줘: 계기 바늘 물리

#### 1. 목표
저울에 「계기 바늘 물리」 기술을 깔끔한 기본 느낌으로 넣어 줘. — 바늘이 목표를 지나쳤다 떨며 멈춤 — 감쇠 스프링

#### 2. 핵심 기술 용어
- 계기 바늘 물리 — 바늘이 목표를 지나쳤다 떨며 멈춤 — 감쇠 스프링

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

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

#### 5. 완성 기준 (이게 보이면 성공)
- 견본처럼 「바늘이 목표를 지나쳤다 떨며 멈춤 — 감쇠 스프링」 이(가) 첫 화면에서 바로 보인다
- 켬/끔(또는 전/후) 비교 단추로 이 기술이 만드는 차이를 확인할 수 있다
- 중요한 값 1~3개를 슬라이더로 바꿔 볼 수 있다
- 콘솔 오류 0, 폰 폭(390px)에서도 동작한다

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

## 원리
- 바늘이 목표를 지나쳤다 떨며 멈춤 — 감쇠 스프링


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

## 완성 기준 체크리스트
- [ ] 견본처럼 「바늘이 목표를 지나쳤다 떨며 멈춤 — 감쇠 스프링」 이(가) 첫 화면에서 바로 보인다
- [ ] 켬/끔(또는 전/후) 비교 단추로 이 기술이 만드는 차이를 확인할 수 있다
- [ ] 중요한 값 1~3개를 슬라이더로 바꿔 볼 수 있다
- [ ] 콘솔 오류 0, 폰 폭(390px)에서도 동작한다

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

## 견본 실제 코드 (라이브 견본이 돌리는 코드 — three.js · TypeScript)
### D218 — `src/demos/demosJuice.ts:2144`
```ts
const D218: Demo = {
  kind: '2d',
  caption: '같은 무게를 올려도 — 왼쪽 바늘은 미끄러지듯 멈추고, 오른쪽은 목표를 지나쳤다 바르르 떨며 멈춘다 (감쇠 스프링)',
  make() {
    let k = 120;
    let c = 7;
    const TG = [3, 8.5, 5, 9.6, 1.5, 6.5];
    const STEP = 1.7;
    let a1 = 0;
    let a2 = 0;
    let v2 = 0;
    const dial = (g: G, cx: number, cy: number, r: number, val: number, title: string, u: number, col: string, tgt: number): void => {
      // 몸통
      g.beginPath();
      g.arc(cx, cy + 4 * u, r + 9 * u, 0, TAU);
      g.fillStyle = '#0a0d22';
      g.fill();
      const bg2 = g.createRadialGradient(cx, cy - r * 0.4, r * 0.1, cx, cy, r + 8 * u);
      bg2.addColorStop(0, '#fffdf5');
      bg2.addColorStop(1, '#e9dfc9');
      g.beginPath();
      g.arc(cx, cy, r + 8 * u, 0, TAU);
      g.fillStyle = col;
      g.fill();
      g.beginPath();
      g.arc(cx, cy, r + 3 * u, 0, TAU);
      g.fillStyle = bg2;
      g.fill();
      const A0 = Math.PI * 0.8;
      const A1 = Math.PI * 2.2;
      const ang = (v: number): number => lerp(A0, A1, v / 10);
      // 위험 구간
      g.beginPath();
      g.arc(cx, cy, r - 4 * u, ang(8), ang(10));
      g.strokeStyle = 'rgba(255,80,100,.5)';
      g.lineWidth = 5 * u;
      g.stroke();
      for (let i = 0; i <= 20; i++) {
        const a = ang(i / 2);
        const big = i % 2 === 0;
        const r0 = r - (big ? 10 : 6) * u;
        g.strokeStyle = '#3a2f4f';
        g.lineWidth = (big ? 1.8 : 1) * u;
        g.beginPath();
        g.moveTo(cx + Math.cos(a) * r0, cy + Math.sin(a) * r0);
        g.lineTo(cx + Math.cos(a) * (r - 1 * u), cy + Math.sin(a) * (r - 1 * u));
        g.stroke();
        if (big) txt(g, String(i / 2), cx + Math.cos(a) * (r - 17 * u), cy + Math.sin(a) * (r - 17 * u), 7 * u, '#3a2f4f', 'center', 800);
      }
      txt(g, 'kg', cx, cy + r * 0.45, 7.5 * u, '#8a7f9f', 'center', 800);
      // 목표 표시
      const ta = ang(tgt);
      g.fillStyle = '#20c997';
      g.beginPath();
      g.moveTo(cx + Math.cos(ta) * (r + 2 * u), cy + Math.sin(ta) * (r + 2 * u));
      g.lineTo(cx + Math.cos(ta - 0.08) * (r + 9 * u), cy + Math.sin(ta - 0.08) * (r + 9 * u));
      g.lineTo(cx + Math.cos(ta + 0.08) * (r + 9 * u), cy + Math.sin(ta + 0.08) * (r + 9 * u));
      g.fill();
      // 바늘
      const na = ang(clamp(val, -0.4, 10.4));
      g.save();
      g.translate(cx + 1.5 * u, cy + 2 * u);
      g.rotate(na);
      g.fillStyle = 'rgba(0,0,0,.18)';
      g.beginPath();
      g.moveTo(-8 * u, -2.4 * u);
      g.lineTo(r - 8 * u, 0);
      g.lineTo(-8 * u, 2.4 * u);
      g.fill();
      g.restore();
      g.save();
      g.translate(cx, cy);
      g.rotate(na);
      g.fillStyle = '#e8364a';
      g.beginPath();
      g.moveTo(-9 * u, -2.6 * u);
      g.lineTo(r - 8 * u, 0);
      g.lineTo(-9 * u, 2.6 * u);
      g.closePath();
      g.fill();
      g.restore();
      g.beginPath();
      g.arc(cx, cy, 5 * u, 0, TAU);
      g.fillStyle = '#3a2f4f';
      g.fill();
      g.beginPath();
      g.arc(cx - 1.2 * u, cy - 1.2 * u, 1.8 * u, 0, TAU);
      g.fillStyle = 'rgba(255,255,255,.6)';
      g.fill();
      // 유리 빛
      g.save();
      g.beginPath();
      g.arc(cx, cy, r + 3 * u, 0, TAU);
      g.clip();
      const gl = g.createLinearGradient(cx - r, cy - r, cx + r * 0.2, cy);
      gl.addColorStop(0, 'rgba(255,255,255,.45)');
      gl.addColorStop(1, 'rgba(255,255,255,0)');
      g.fillStyle = gl;
      g.beginPath();
      g.ellipse(cx - r * 0.3, cy - r * 0.45, r * 0.9, r * 0.5, -0.5, 0, TAU);
      g.fill();
      g.restore();
      txt(g, title, cx, cy + r + 20 * u, 8.5 * u, '#dfe3ff', 'center', 800);
      txt(g, val.toFixed(1), cx, cy + r * 0.72, 9 * u, '#3a2f4f', 'center', 900);
    };
    return {
      draw(g, w, h, t, dt0) {
        reset(g);
        const dt = Math.min(dt0, 0.04);
        const u = Math.min(w / 280, h / 175);
        const tgt = TG[Math.floor(t / STEP) % TG.length]!;
        a1 = lerp(a1, tgt, 1 - Math.exp(-dt * 6));
        // 반 걸음씩 두 번 (안정)
        for (let s = 0; s < 2; s++) {
          const h2 = dt / 2;
          v2 += ((tgt - a2) * k - v2 * c) * h2;
          a2 += v2 * h2;
        }
        bg(g, w, h, '#26215a', '#0c0a24');
        const r = Math.min(52 * u, h * 0.3);
        dial(g, w * 0.27, h * 0.44, r, a1, '그냥 따라가기', u, '#5c7cfa', tgt);
        dial(g, w * 0.73, h * 0.44, r, a2, '감쇠 스프링', u, '#ffa94d', tgt);
        pill(g, `목표 ${tgt} kg`, w / 2, 12 * u, 8 * u, '#20c997', '#062b20');
        reset(g);
      },
      controls: [rng('스프링 세기 k', 20, 400, 5, k, (v) => (k = v)), rng('감쇠 c', 1, 40, 0.5, c, (v) => (c = v))],
    };
  },
};
```

## 관련 기술
- 이 효과를 만든 기술: [스프링 · 감쇠 움직임 (임계 감쇠)](https://ai-techstudio.web.app/ai/t/i293.md) `i293`
