# AI 꾸러미 — 암호 풀리는 글자 (scramble) — Text scramble / decode effect
> 칸마다 무작위 글자를 빠르게 바꾸다가 왼쪽부터 하나씩 정답으로 딸깍 고정해, 암호가 풀리는 해독 연출을 만든다.  
> 견본: https://ai-techstudio.web.app/#t/i93

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

## 주문서

### 만들어 줘: 암호 풀리는 글자 (scramble) — Text scramble / decode effect

#### 1. 목표
비밀번호 · 숫자 4자리 해독에 암호 풀리는 글자 연출을 넣어 줘 — 무작위 글자가 돌다가 왼쪽부터 하나씩 정답으로 고정되고, 고정되는 순간 번쩍. 분위기는 초록 해커 화면.

#### 2. 핵심 기술 용어
- **Text scramble / decode effect** — 무작위 글자가 정답으로 풀리는 연출
- **Staggered lock-in** — 칸마다 차례로 고정
- **Deterministic hash random** — 프레임 번호로 정하는 무작위 — 다시 그려도 같은 글자

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

#### 4. 조건
- 무작위는 Math.random 대신 프레임 번호 해시로 (화면이 다시 그려져도 같은 글자)
- 글자 바뀜은 초당 15번 정도 — 매 프레임 바꾸면 너무 어지럽다
- 칸마다 고정 시각을 차례로 늦추기 (견본 0.5초 간격)
- 풀리는 중인 글자는 흐리게 · 살짝 떨리게, 고정된 글자는 또렷하게

#### 5. 완성 기준 (이게 보이면 성공)
- 네 칸의 숫자가 빠르게 바뀌다가 왼쪽부터 4 → 7 → 2 → 9 로 하나씩 고정된다
- 고정되는 칸이 초록빛으로 번쩍하고 테두리가 진해진다
- 아래 문장도 무작위 한글이 돌다가 「모순을 찾았다!」 로 풀린다
- 다 풀리면 「해독 완료 ✓」 가 톡 나온다

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

## 원리
- 칸 i 의 고정 시각 = 0.6 + i × 0.5초. 그 전에는 무작위 글자, 그 뒤에는 정답 글자.
- 무작위 글자는 「초당 15번」 바뀌는 tick 과 칸 번호로 해시해서 고른다 — 같은 프레임은 늘 같은 글자.
- 고정되는 순간 번쩍(빛 번짐 e^(−5t))과 테두리 색으로 「딸깍」 을 보여 준다.
- 숫자 칸은 기호 집합(0~9 # $ % …), 한글 문장은 한글 집합에서 골라 글자 모양이 어울리게.

## 핵심 코드 — 칸마다 고정 시각 · 해시 무작위 · 고정 번쩍
(발췌: demos/demosMotionA.ts i93 의 draw 를 정리)
```ts
const SYM = '0123456789#$%&?@*';
const code = ['4', '7', '2', '9'];
const hsh = (n: number) => { const s = Math.sin(n * 12.9898 + 78.233) * 43758.5453; return s - Math.floor(s); };

const p = tt % 5.4;                    // 한 주기
const cyc = Math.floor(tt / 5.4);      // 주기마다 다른 무작위
const tick = Math.floor(tt * 15);      // 글자는 초당 15번 바뀐다
code.forEach((c, i) => {
  const lockAt = 0.6 + i * 0.5;        // 왼쪽부터 차례로 고정
  const locked = p >= lockAt && p < 4.6;
  const flash = locked ? Math.exp(-(p - lockAt) * 5) : 0;
  const ch = locked ? c : SYM[Math.floor(hsh(tick * 7 + i * 13 + cyc) * SYM.length)];
  const x = x0 + i * 50;
  g.save();
  if (flash > 0) { g.shadowColor = '#6dffb4'; g.shadowBlur = 14 * flash; } // 딸깍 번쩍
  g.font = '800 30px Consolas, monospace';
  g.fillStyle = locked ? '#eafff4' : 'rgba(150,255,200,.55)';
  const jitter = locked ? 0 : (hsh(tick + i) - 0.5) * 3; // 풀리는 중엔 살짝 떨림
  g.fillText(ch, x + 21, 65 + jitter);
  g.restore();
});
```

## 흔한 실수 · 확인 목록
- [ ] **Math.random 으로 고르면 같은 프레임을 다시 그릴 때 글자가 바뀐다** — (tick, 칸 번호, 주기) 해시로 고른다. 카드 · 큰 화면 두 번 그려도 같다.
- [ ] **매 프레임 글자를 바꾸면 눈이 아프다** — tick = floor(시간 × 15) 로 초당 15번만 바꾼다.
- [ ] **고정 순간이 밋밋하면 「풀렸다」 가 안 느껴진다** — 빛 번짐 · 테두리 색을 지수로 빠지게(e^(−5t)) 주고, 효과음을 붙이면 더 좋다.

## 완성 기준 체크리스트
- [ ] 네 칸의 숫자가 빠르게 바뀌다가 왼쪽부터 4 → 7 → 2 → 9 로 하나씩 고정된다
- [ ] 고정되는 칸이 초록빛으로 번쩍하고 테두리가 진해진다
- [ ] 아래 문장도 무작위 한글이 돌다가 「모순을 찾았다!」 로 풀린다
- [ ] 다 풀리면 「해독 완료 ✓」 가 톡 나온다

## 이 기술 정보
- id: `i93` · 분류: 이펙트 · 연출 › 모션 그래픽 · 2D · 난이도 쉬움 · 폰 부담 가벼움 (폰 OK) — 칸 몇 개의 글자를 매 프레임 그리는 정도. 빛 번짐(shadowBlur)은 고정 순간에만.
- 라이브 견본 (브라우저에서 직접 조작): https://ai-techstudio.web.app/#t/i93
- 쓰면 좋을 때: 답 · 비밀을 한 번에 보이지 않고 긴장감 있게 공개할 때 / 진행 정도(■■□□)를 함께 보여 주고 싶을 때
- 쓰지 말 때: 곧바로 읽어야 하는 안내 — 기다리게 만든다. 대신 바로 보이기 / 아주 긴 문장 — 칸이 많으면 오래 걸린다

## 견본 실제 코드 (라이브 견본이 돌리는 코드 — three.js · TypeScript)
### i93 — `src/demos/demosMotionA.ts:439`
```ts
const i93 = {
  kind: '2d' as const,
  caption: '무작위 글자가 돌다가 왼쪽부터 하나씩 정답으로 딸깍 고정 — 해독 연출',
  make() {
    const o = { s: 1 };
    let tt = 0;
    const SYM = '0123456789#$%&?@*';
    const HAN = '가나다라마바사아자차카타파하수학모순검문';
    const code = ['4', '7', '2', '9'];
    const line = [...'모순을 찾았다!'];
    return {
      controls: [speedCtl(o)],
      draw(g: G, w: number, h: number, _t: number, dt: number) {
        tt += Math.max(0, dt) * o.s;
        const T = 5.4;
        const p = tt % T;
        const cyc = Math.floor(tt / T);
        bg(g, w, h, '#0f2a2a', '#06120f');
        // 스캔 줄
        g.fillStyle = 'rgba(80,255,170,.04)';
        for (let y = 0; y < h; y += 3) g.fillRect(0, y, w, 1);
        view(g, w, h);
        txt(g, '비밀번호 해독', VW / 2, 18, 11, '#6dffb4', 'center', F, 800);
        const tick = Math.floor(tt * 15);
        const out = seg(p, 4.6, 5.1);
        const BW = 42;
        const x0 = VW / 2 - (BW * 4 + 8 * 3) / 2;
        code.forEach((c, i) => {
          const lockAt = 0.6 + i * 0.5;
          const unlockAt = 4.6 + (3 - i) * 0.12;
          const locked = p >= lockAt && p < unlockAt;
          const flash = locked ? Math.exp(-(p - lockAt) * 5) : 0;
          const x = x0 + i * (BW + 8);
          rr(g, x, 38, BW, 52, 8);
          g.fillStyle = locked ? `rgba(40,${120 + flash * 120},90,.55)` : 'rgba(255,255,255,.07)';
          g.fill();
          g.strokeStyle = locked ? '#6dffb4' : 'rgba(109,255,180,.35)';
          g.lineWidth = locked ? 2 : 1;
          g.stroke();
          const ch = locked ? c : SYM[Math.floor(hsh(tick * 7 + i * 13 + cyc) * SYM.length)]!;
          g.save();
          if (flash > 0) {
            g.shadowColor = '#6dffb4';
            g.shadowBlur = 14 * flash;
          }
          txt(g, ch, x + BW / 2, 65 + (locked ? 0 : (hsh(tick + i) - 0.5) * 3), 30, locked ? '#eafff4' : 'rgba(150,255,200,.55)', 'center', '"Consolas", monospace', 800);
          g.restore();
        });
        // 문장
        g.font = `800 15px ${F}`;
        const ws = line.map((c) => (c === ' ' ? 6 : 16));
        const tot = ws.reduce((a, b) => a + b, 0);
        let x = VW / 2 - tot / 2;
        line.forEach((c, i) => {
          const cw = ws[i]!;
          const lx = x + cw / 2;
          x += cw;
          if (c === ' ') return;
          const lock = p >= 2.6 + i * 0.13 && p < 4.6 + i * 0.05;
          const ch = lock ? c : HAN[Math.floor(hsh(tick * 3 + i * 31 + cyc) * HAN.length)]!;
          txt(g, ch, lx, 115, 15, lock ? '#ffffff' : 'rgba(109,255,180,.45)', 'center', F, 800);
        });
        const done = seg(p, 3.6, 3.9) * (1 - out);
        if (done > 0) {
          g.save();
          g.globalAlpha = done;
          g.translate(VW / 2, 147);
          const s = outBack(done);
          g.scale(s, s);
          pill(g, '해독 완료 ✓', 0, 0, 11, '#21c47a');
          g.restore();
        } else {
          const prog = code.filter((_, i) => p >= 0.6 + i * 0.5 && p < 4.6).length;
          txt(g, `${'■'.repeat(prog)}${'□'.repeat(4 - prog)}  풀어 내는 중…`, VW / 2, 147, 9, 'rgba(109,255,180,.7)', 'center', F, 700);
        }
        g.restore();
      },
    };
  },
};
```

## 관련 기술
- 먼저 알면 좋은 기술: [글자 차례 등장 (키네틱 타이포)](https://ai-techstudio.web.app/ai/t/i90.md) `i90`
- 다음에 해 볼 기술: [숫자 굴러가기 (계기판)](https://ai-techstudio.web.app/ai/t/i94.md) `i94` · [글리치 · 주사선 · CRT](https://ai-techstudio.web.app/ai/t/i112.md) `i112`
