# AI 꾸러미 — 코드 타자 + 문법 색 (사람 박자 · 오타 고치기) — Typewriter code animation (typing effect)
> 코드가 사람 박자로 한 글자씩 쳐지며 바로 색이 입혀진다 — 글자 간격이 들쭉날쭉하고 줄바꿈 앞에서 쉬며, 오타를 냈다 지우고 고친다.  
> 견본: https://ai-techstudio.web.app/#t/i554

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

## 주문서

### 만들어 줘: 코드 타자 + 문법 색 (사람 박자 · 오타 고치기) — Typewriter code animation (typing effect)

#### 1. 목표
짧은 함수 (점수 계산)을(를) 코드 편집기에서 사람이 치는 것처럼 보여 줘 — 사람 박자 · 오타 고치기 · 치는 즉시 문법 색 · 커서 깜박임. 분위기는 어두운 편집기 (보라 · 파랑 · 주황 색).

#### 2. 핵심 기술 용어
- **Typewriter code animation (typing effect)** — 코드가 한 글자씩 쳐지는 연출
- **Syntax highlighting (tokenizer)** — 낱말 종류(예약어 · 숫자 · 문자열 · 주석 · 함수)마다 색
- **Keystroke timeline (human typing rhythm)** — 글쇠마다 시각을 정한 시간표 — 지우기 포함
- **Blinking caret** — 치는 동안 켜져 있고 멈추면 깜박이는 커서

#### 3. 환경
- 플랫폼: TypeScript + HTML · CSS · SVG (DOM), 라이브러리 없이 — 움직임은 transform · opacity 로
- 화면: 브라우저 — PC 와 폰(390px 폭) 모두, 60fps 목표
- 글이 바뀐 프레임에만 innerHTML 을 다시 만든다. white-space: pre + 고정폭 글꼴.

#### 4. 조건
- 글쇠 시간표(글자 · 지우기 · 시각)를 미리 만들고, 지금 시각까지 적용해 글을 만들 것 — setInterval 로 한 글자씩 붙이지 않는다
- 간격은 일정하지 않게(30~75ms), 새 줄 앞에서 쉬기, 오타 한 번 → 멈칫 → 지우기 → 고치기
- 문법 색은 치는 동안에도 입혀질 것 (반쪽 낱말 포함). 꺾쇠 < > & 는 HTML 이스케이프
- 커서: 치는 중엔 켜짐, 멈추면 깜박. 지금 줄 바탕 강조
- 글이 바뀐 프레임에만 다시 그리기

#### 5. 완성 기준 (이게 보이면 성공)
- 글자가 일정하지 않은 박자로 쳐지고 줄이 바뀔 때 잠깐 쉰다
- 「bonus」를 치다 「bou」로 틀린 뒤 멈칫하고 지워서 바르게 고친다
- 「function」이 다 쳐지는 순간 보라색으로 바뀌고, 숫자 · 주석 · 함수 이름이 각자 색이다
- 「사람 같은 박자」를 끄면 일정한 간격에 오타 없이 쳐진다 — 둘을 비교하면 켠 쪽이 사람 같다

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

## 원리
- 시간표 만들기: 글자마다 「언제 칠지」를 미리 정한다. 기본 간격 30~75ms 를 글자마다 다르게, 띄어쓰기 · 괄호 뒤 조금 더, 새 줄 앞 0.28초(생각), 오타는 틀린 글자 → 0.38초 멈칫 → 지우기 → 바르게.
- 지금 시각까지의 글쇠를 차례로 적용해 글을 만든다 (지우기 = 끝 글자 하나 빼기). 이렇게 하면 되감기 · 속도 조절 · 영상 굽기가 그대로 된다.
- 색은 줄마다 정규식 하나로 낱말을 나눠 종류별 span. 치는 도중의 반쪽 낱말(「functio」)은 아직 이름 색이다가 다 치는 순간 예약어 색 — 진짜 편집기처럼.
- 커서는 마지막 글쇠 뒤 0.45초 동안 켜져 있고, 그 뒤 0.53초 간격 깜박. 지금 줄은 바탕을 살짝 밝게.

## 핵심 코드 — 글쇠 시간표 만들기 (사람 박자 + 오타)
(발췌: demos/demosMotionD.ts i554 의 build() 를 정리)
```ts
type Op = { ch: string | null; at: number };   // ch = null 이면 지우기
function buildOps(src: string, typoAt: number, wrong: string): Op[] {
  const ops: Op[] = [];
  let t = 0.4;
  for (let i = 0; i < src.length; i++) {
    const ch = src[i];
    let d = 0.03 + ((i * 0.618) % 1) * 0.045;      // 글자마다 다른 간격
    if (ch === ' ') d += 0.03;
    if (src[i - 1] === '\n') d += 0.28;             // 새 줄 앞에서 생각
    if ('(){};'.includes(ch)) d += 0.04;
    if (i === typoAt) {
      for (const c of wrong) ops.push({ ch: c, at: (t += 0.06) });   // 예: 'bou'
      t += 0.38;                                     // 틀린 걸 알아챔
      ops.push({ ch: null, at: (t += 0.1) });        // 지우기
      t += 0.12;
      i += wrong.length - 2;                         // 맞게 친 글자만큼 건너뛰기
      continue;
    }
    ops.push({ ch, at: (t += d) });
  }
  return ops;
}

// 지금 시각 p 까지 적용
let text = '';
for (const op of ops) { if (op.at > p) break; text = op.ch === null ? text.slice(0, -1) : text + op.ch; }
```

## 흔한 실수 · 확인 목록
- [ ] **매 프레임 innerHTML 을 다시 만들면 커서 깜박임이 끊긴다** — 글쇠 수가 바뀐 프레임에만 다시 그리고, 깜박임은 커서 요소의 opacity 로만.
- [ ] **한글은 고정폭 글꼴에서 두 칸 폭이라 줄이 어긋나 보인다** — 주석의 한글은 대체 글꼴로 그려져 칸이 맞지 않는다. 칸 맞춤이 중요한 곳(i555)은 영어만, 여기선 흐름대로 둔다.
- [ ] **꺾쇠를 이스케이프하지 않으면 「p.combo > 2」에서 화면이 깨진다** — 낱말을 span 에 넣기 전에 & < > 를 바꾼다.
- [ ] **일정한 간격은 기계 같다** — 간격을 글자마다 다르게 하고 줄 앞에서 쉬기 · 오타 한 번이면 확 사람 같아진다.

## 완성 기준 체크리스트
- [ ] 글자가 일정하지 않은 박자로 쳐지고 줄이 바뀔 때 잠깐 쉰다
- [ ] 「bonus」를 치다 「bou」로 틀린 뒤 멈칫하고 지워서 바르게 고친다
- [ ] 「function」이 다 쳐지는 순간 보라색으로 바뀌고, 숫자 · 주석 · 함수 이름이 각자 색이다
- [ ] 「사람 같은 박자」를 끄면 일정한 간격에 오타 없이 쳐진다 — 둘을 비교하면 켠 쪽이 사람 같다

## 이 기술 정보
- id: `i554` · 분류: 이펙트 · 연출 › 모션 그래픽 · 공통 · 난이도 쉬움 · 폰 부담 가벼움 (폰 OK) — 글이 바뀐 프레임에만 몇 줄의 HTML 을 다시 만든다 (초당 15~30번). 커서 깜박임은 opacity 만.
- 라이브 견본 (브라우저에서 직접 조작): https://ai-techstudio.web.app/#t/i554
- 쓰면 좋을 때: 코딩 설명 영상 · 개발 일지 · 게임 속 해커 · 컴퓨터 장면 / 「이 코드가 이렇게 생겼다」를 차근차근 보여 줄 때
- 쓰지 말 때: 긴 코드 전체 — 지루하다. 대신 핵심 몇 줄만 치고 나머지는 코드 모양 바꾸기(i555) / 읽어야 하는 설명문 — 대신 한 번에 보여 주고 강조

## 견본 실제 코드 (라이브 견본이 돌리는 코드 — three.js · TypeScript)
### i554 — `src/demos/demosMotionD.ts:292`
```ts
const i554 = {
  kind: 'dom' as const,
  caption: '글자가 사람 박자로 쳐지며 바로 색이 입혀져요 — 띄어쓰기 · 줄바꿈에서 쉬고, 오타를 냈다 지우고 고쳐요',
  make(box: HTMLElement) {
    const o = { s: 1, human: true };
    type Op = { ch: string | null; at: number };
    let ops: Op[] = [];
    let end = 0;
    const build = (): void => {
      ops = [];
      let t = 0.4;
      const typo = SRC54.indexOf('n * bonus') + 4; // 「bonus」의 b
      for (let i = 0; i < SRC54.length; i++) {
        const ch = SRC54[i]!;
        let d = 0.05;
        if (o.human) {
          d = 0.03 + ((i * 0.618) % 1) * 0.045; // 글자마다 다른 간격
          if (ch === ' ') d += 0.03;
          if (SRC54[i - 1] === '\n') d += 0.28; // 새 줄 앞에서 생각
          if ('(){};'.includes(ch)) d += 0.04;
          if (i === typo) {
            // b · o 다음 u 를 잘못 침 → 잠깐 멈칫 → 지우기 → 바르게
            for (const c of 'bou') ops.push({ ch: c, at: (t += 0.06) });
            t += 0.38;
            ops.push({ ch: null, at: (t += 0.1) });
            t += 0.12;
            i += 1;
            continue;
          }
        }
        ops.push({ ch, at: (t += d) });
      }
      end = t;
    };
    build();
    const S = stage(
      box,
      `.mc54-ed{position:absolute;left:10px;top:8px;width:260px;height:159px;border-radius:9px;background:#1e2130;box-shadow:0 6px 18px rgba(0,0,0,.35);overflow:hidden}
      .mc54-tb{position:absolute;left:0;right:0;top:0;height:18px;background:#171a26}
      .mc54-tb i{position:absolute;top:6px;width:6px;height:6px;border-radius:50%}
      .mc54-tb span{position:absolute;left:38px;top:0;height:18px;padding:0 9px;background:#1e2130;font:700 8px/18px ${F};color:#c9d1e8}
      .mc54-code{position:absolute;left:0;right:0;top:24px;font:10px/15px ${MONO};white-space:pre;color:#e6edf3}
      .mc54-l{position:relative;height:15px;padding-left:26px}
      .mc54-l.on{background:rgba(255,255,255,.06)}
      .mc54-l b{position:absolute;left:0;width:18px;text-align:right;font-weight:400;color:#56607d}
      .mc54-l.on b{color:#c9d1e8}
      .mc54-cur{display:inline-block;width:1.5px;height:12px;margin-left:.5px;vertical-align:-2px;background:#ffcc66}
      .mc54-code .k{color:#c792ea}.mc54-code .f{color:#82aaff}.mc54-code .n{color:#f78c6c}.mc54-code .s{color:#c3e88d}.mc54-code .c{color:#6b7599;font-style:italic}.mc54-code .p{color:#89ddff}.mc54-code .i{color:#e6edf3}
      .mc54-ok{position:absolute;right:10px;bottom:9px;padding:2px 8px;border-radius:9px;background:#1fbf6a;color:#fff;font:800 8px ${F};opacity:0}`,
      `<div class="mc54-ed"><div class="mc54-tb"><i style="left:8px;background:#ff5f57"></i><i style="left:17px;background:#febc2e"></i><i style="left:26px;background:#28c840"></i><span>score.ts</span></div>
      <div class="mc54-code"></div><div class="mc54-ok">✓ 저장됨</div></div>`,
      'linear-gradient(135deg,#3b4a7a,#1a2140)',
    );
    const code = S.st.querySelector<HTMLElement>('.mc54-code')!;
    const ok = S.st.querySelector<HTMLElement>('.mc54-ok')!;
    let shown = -1;
    let cur: HTMLElement | null = null;
    let tt = 0;
    return {
      controls: [
        speedCtl(o),
        { type: 'toggle', label: '사람 같은 박자 (끄면 일정한 간격)', value: true, on: (v: boolean) => { o.human = v; build(); shown = -1; } },
      ] as Control[],
      update(_t: number, dt: number) {
        tt += Math.max(0, Math.min(dt, 0.1)) * o.s;
        const T = end + 2.3;
        const p = tt % T;
        let n = 0;
        while (n < ops.length && ops[n]!.at <= p) n++;
        if (p > end + 2.0) n = 0;
        if (n !== shown) {
          // 글이 바뀐 프레임에만 다시 그린다
          shown = n;
          let text = '';
          for (let i = 0; i < n; i++) {
            const c = ops[i]!.ch;
            text = c === null ? text.slice(0, -1) : text + c;
          }
          const lines = text.split('\n');
          const at = lines.length - 1;
          code.innerHTML = lines.map((l, i) => `<div class="mc54-l${i === at ? ' on' : ''}"><b>${i + 1}</b>${hl(l)}${i === at ? '<i class="mc54-cur"></i>' : ''}</div>`).join('');
          cur = code.querySelector<HTMLElement>('.mc54-cur');
        }
        // 치는 동안은 커서가 켜져 있고, 멈추면 깜박
        const last = n > 0 ? ops[n - 1]!.at : 0;
        const idle = p - last;
        if (cur) cur.style.opacity = idle < 0.45 || Math.floor((idle - 0.45) / 0.53) % 2 ? '1' : '0';
        code.style.opacity = (1 - seg(p, end + 1.7, end + 2.0)).toFixed(3);
        ok.style.opacity = (seg(p, end + 0.3, end + 0.5) * (1 - seg(p, end + 1.5, end + 1.8))).toFixed(3);
      },
      dispose() {
        S.dispose();
      },
    };
  },
};
```

## 관련 기술
- 먼저 알면 좋은 기술: [글자 차례 등장 (키네틱 타이포)](https://ai-techstudio.web.app/ai/t/i90.md) `i90` · [암호 풀리는 글자 (scramble)](https://ai-techstudio.web.app/ai/t/i93.md) `i93`
- 다음에 해 볼 기술: [코드 모양 바꾸기 (같은 낱말 짝짓기 · Magic Move)](https://ai-techstudio.web.app/ai/t/i555.md) `i555` · [영상으로 굽기 (헤드리스 브라우저 → mp4)](https://ai-techstudio.web.app/ai/t/i82.md) `i82`
- 참고 문서: [MDN — white-space](https://developer.mozilla.org/en-US/docs/Web/CSS/white-space)
