# AI 꾸러미 — 접근성 · 움직임 줄이기 — prefers-reduced-motion
> 「움직임 줄이기」를 켠 기기에서는 튀고 도는 연출을 살짝 나타나기로 바꾸고, 그림 단추에는 aria-label 을 달아 화면 읽기가 이름을 소리로 읽게 한다.  
> 견본: https://ai-techstudio.web.app/#t/u86

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

## 주문서

### 만들어 줘: 접근성 · 움직임 줄이기 — prefers-reduced-motion

#### 1. 목표
게임 메뉴 · 단추에 접근성을 넣어 줘 — prefers-reduced-motion: reduce 이면 통통 · 빙글 · 흔들 연출을 흐려졌다 또렷해지는 정도로 줄이고, 글씨 없는 단추에 aria-label, 키보드 초점 테두리를 또렷하게. CSS 한 곳에서 한꺼번에.

#### 2. 핵심 기술 용어
- **prefers-reduced-motion** — 기기 설정 「움직임 줄이기」를 읽는 미디어 쿼리
- **aria-label** — 화면 읽기용 이름 — 글씨 없는 단추에
- **Focus indicator (:focus-visible)** — 키보드로 고른 단추 테두리
- **matchMedia** — JS 에서 미디어 쿼리 읽기

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

#### 4. 조건
- CSS: @media (prefers-reduced-motion: reduce) 안에서 animation-duration · transition-duration 0.01ms !important
- JS 연출: matchMedia('(prefers-reduced-motion: reduce)').matches 면 튀기 · 돌기 · 흔들기 대신 투명도만
- 글씨 없는 단추 · 아이콘에 aria-label (번역되는 글로)
- :focus-visible 테두리를 없애지 않기
- 설정이 바뀌면(change 이벤트) 바로 따라가기

#### 5. 완성 기준 (이게 보이면 성공)
- 보통 기기는 공이 통통 · 별이 빙글 · 카드가 흔들, 움직임 줄이기 기기는 셋 다 살짝 흐려졌다 또렷해지기만
- 키보드 Tab 으로 단추를 고르면 주황 테두리가 보인다
- 화면 읽기가 단추를 「시작하기, 단추」 · 「게임 방법 보기, 단추」처럼 읽는다

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

## 원리
- 어지럼을 느끼는 사람은 기기 설정에서 「움직임 줄이기」를 켠다 — 브라우저는 prefers-reduced-motion: reduce 로 알려 준다.
- CSS 는 한 곳에서: 그 미디어 쿼리 안에서 animation · transition 시간을 0.01ms 로 (이 사이트 base.css).
- 캔버스 · 3D 연출은 JS 에서 matchMedia 로 읽어 — 튀기 · 돌기 대신 투명도만 살짝 바꾼다.
- 그림 · 아이콘 단추는 aria-label 로 이름을 준다 — 화면 읽기가 「시작하기, 단추」처럼 읽는다.
- 키보드로 고를 수 있게 초점 테두리를 지우지 않는다.

## 핵심 코드 — CSS 한꺼번에 줄이기 + JS 연출 갈래 + aria-label
(발췌: src/styles/base.css 의 미디어 쿼리 + 새로 씀 (JS 갈래 · aria-label 은 같은 방식))
```ts
// CSS (base.css) — 사이트 전체 애니메이션을 한꺼번에
const css = [
  '@media (prefers-reduced-motion: reduce) {',
  '  *, *::before, *::after {',
  '    animation-duration: 0.01ms !important;',
  '    animation-iteration-count: 1 !important;',
  '    transition-duration: 0.01ms !important;',
  '  }',
  '}',
].join('\n');

// 캔버스 · 3D 연출 — 튀기 대신 투명도만
const mq = matchMedia('(prefers-reduced-motion: reduce)');
let reduce = mq.matches;
mq.addEventListener('change', (e) => (reduce = e.matches));

function drawBall(g: CanvasRenderingContext2D, x: number, y: number, t: number): void {
  const by = reduce ? y : y - Math.abs(Math.sin(t * 4)) * 22;            // 보통: 통통
  g.globalAlpha = reduce ? 0.5 + 0.5 * Math.abs(Math.sin(t * 1.05)) : 1;  // 줄이기: 살짝 흐려졌다 또렷
  g.beginPath();
  g.arc(x, by, 13, 0, Math.PI * 2);
  g.fill();
  g.globalAlpha = 1;
}

// 글씨 없는 단추 — 화면 읽기용 이름 (번역되는 글로)
soundBtn.setAttribute('aria-label', t('소리 켜기'));
```

## 흔한 실수 · 확인 목록
- [ ] **「움직임 줄이기」에서 연출을 아예 없애면 무엇이 일어났는지 모른다** — 움직임만 줄이고 결과는 투명도 · 색 변화로 남긴다.
- [ ] **CSS 만 고치면 캔버스 · 3D 연출은 그대로 흔들린다** — JS 에서도 matchMedia 로 읽어 갈래를 둔다.
- [ ] **aria-label 을 한국어로 박아 두면 다른 언어 사용자의 화면 읽기가 한국어를 읽는다** — 번역 함수(t)를 거친 글로 넣는다 — 이 사이트는 한국어가 번역 키.

## 완성 기준 체크리스트
- [ ] 보통 기기는 공이 통통 · 별이 빙글 · 카드가 흔들, 움직임 줄이기 기기는 셋 다 살짝 흐려졌다 또렷해지기만
- [ ] 키보드 Tab 으로 단추를 고르면 주황 테두리가 보인다
- [ ] 화면 읽기가 단추를 「시작하기, 단추」 · 「게임 방법 보기, 단추」처럼 읽는다

## 이 기술 정보
- id: `u86` · 분류: 게임 시스템 · AI › 플랫폼 · 성능 · 공통 · 난이도 쉬움 · 폰 부담 가벼움 (폰 OK) — 비용 없음. 연출 코드에 갈래 하나.
- 라이브 견본 (브라우저에서 직접 조작): https://ai-techstudio.web.app/#t/u86
- 쓰면 좋을 때: 모든 공개 화면 — 기본으로 / 흔들림 · 번쩍 · 빠른 회전이 있는 성공 연출
- 쓰지 말 때: 움직임 자체가 정보인 것(공이 굴러가는 물리 체험)까지 멈추기 — 정보는 남기고 장식만 줄이기 / 번쩍이는 화면 전체 깜빡임 — 줄이기 설정이 없어도 피한다

## 견본 실제 코드 (라이브 견본이 돌리는 코드 — three.js · TypeScript)
### u86 견본 항목 — `src/demos/demosSystem.ts:1484`
```ts
  u86: {
    kind: '2d',
    caption: '왼쪽 보통 · 오른쪽 「움직임 줄이기」 켠 기기 — 튀고 도는 대신 살짝 나타나기만, 단추 이름은 화면 읽기가 소리로',
    make() {
      return {
        draw(g, w, h, t) {
          stage(g, w, h, SKY);
          const half = (ox: number, reduce: boolean): void => {
            box(g, ox, 10, 148, 134, 12, '#fff', reduce ? '#2bb673' : '#ff9e3d', 2);
            txt(g, reduce ? '움직임 줄이기' : '보통', ox + 74, 24, 10, reduce ? '#1a7a4a' : '#c0600a', 'center', 800, TF);
            // 공
            const by = reduce ? 66 : 66 - Math.abs(Math.sin(t * 4)) * 22;
            const ba = reduce ? 0.5 + 0.5 * pingpong(t, 3) : 1;
            g.globalAlpha = ba;
            circle(g, ox + 34, by, 13, '#ff7a6b');
            face(g, ox + 34, by, 9);
            g.globalAlpha = 1;
            // 별
            star(g, ox + 76, 60, 13, '#ffd23f', reduce ? 0 : t * 3, '#d9a010');
            // 카드
            const cx = reduce ? 0 : Math.sin(t * 2.5) * 10;
            g.globalAlpha = reduce ? 0.6 + 0.4 * pingpong(t + 1, 3) : 1;
            box(g, ox + 104 + cx, 46, 30, 38, 5, '#9ac4ff', '#5b6cff', 1.5);
            g.globalAlpha = 1;
            txt(g, reduce ? '살짝 흐려졌다 또렷하게' : '통통 · 빙글 · 흔들', ox + 74, 98, 8.5, '#5a6080', 'center', 700);
            // 단추 셋 + 초점
            const fi = Math.floor(t / 1.2) % 3;
            ['시작', '규칙', '소리'].forEach((s, i) => {
              const bx = ox + 10 + i * 45;
              box(g, bx, 110, 40, 22, 8, '#5b6cff');
              txt(g, s, bx + 20, 121.5, 9, '#fff', 'center', 800);
              if (i === fi) box(g, bx - 3, 107, 46, 28, 10, null, '#ffb22e', 2.5);
            });
          };
          half(8, false);
          half(164, true);
          // 화면 읽기
          const fi = Math.floor(t / 1.2) % 3;
          const names = ['시작하기', '게임 방법 보기', '소리 켜기'];
          box(g, 40, 152, 240, 34, 17, '#22305e');
          speaker(g, 62, 169, 6, '#ffd23f', 2, t);
          txt(g, `화면 읽기: 「${names[fi]}, 단추」`, 172, 169.5, 10, '#fff', 'center', 700);
          txt(g, 'aria-label', 244, 147, 7.5, '#3a4f8c', 'center', 700, MONO);
        },
      };
    },
  }
```

## 관련 기술
- 먼저 알면 좋은 기술: [바뀔 때만 그리기 (멈춘 화면은 쉬기)](https://ai-techstudio.web.app/ai/t/u81.md) `u81`
- 다음에 해 볼 기술: [오프라인 (서비스 워커)](https://ai-techstudio.web.app/ai/t/u84.md) `u84`
- 참고 문서: [MDN — prefers-reduced-motion](https://developer.mozilla.org/en-US/docs/Web/CSS/@media/prefers-reduced-motion) · [MDN — aria-label](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Attributes/aria-label)
