GODRICH
#css애니메이션#버튼#로딩#전환효과#스크롤#대시보드#실전ui#디자인트렌드

css tic tac toe 9가지 — 판이 스스로 셈하고 줄을 긋는 삼목 게임

css tic tac toe 판에 말을 놓기 전에 컴퓨터는 남은 끝판을 세어 본다 — 03번 점수 열지도와 04번 승패 링이 그 셈을 칸 위에 그린다.

아홉 가지 삼목 판을 화면에서 바로 두어 본다

자동 재생 · 눌러서 해당 항목으로 · 전부 한 zip

아홉 판은 말 하나가 놓이는 순간에서 시작해 판 전체를 읽는 계산으로 넓어지는 순서다. 01 은 말이 획으로 그려지며 눌렸다 돌아오고, 02 는 그 판이 포인터를 따라 기울며, 03 과 04 는 칸 위에 점수와 승패 비율을 얹는다. 05 는 판이 끝나는 순간의 선이고 06 은 판 안의 판이며, 07 과 08 은 같은 국면을 접거나 나무로 펼쳐 보여 주고, 09 는 두 컴퓨터가 스스로 두는 판을 분필 질감으로 마무리한다.

css tic tac toe 9가지 — 계산이 칸 위에 그려지는 삼목 판

판은 캔버스 한 장에 그린다. 말과 칸과 점수 색을 칠하고 다음 수를 정하는 일은 index.html 의 자바스크립트가 하고, CSS 는 무대 크기와 그 위에 얹힌 상태 알약·버튼, 키보드 포커스 외곽선만 맡는다. 무대 .tt 는 가로를 min(92vw, 160vh - 12.8vw) 로 잡고 480:300 비율을 지키며, 캔버스 안쪽 해상도는 480×300 에 devicePixelRatio(최대 2)를 곱한 크기라 창이 줄어도 칸 좌표는 그대로다. 포인터 위치는 getBoundingClientRect 비율로 480×300 안의 좌표로 바꿔 쓴다.

판 진행은 아홉 판이 같은 규칙을 쓴다

사람이 X, 컴퓨터가 O 다. 사람이 칸을 누르면 말이 놓이고 420ms 뒤에 bestCell 이 전수 탐색 점수가 가장 높은 칸(동점이면 낮은 번호)에 O 를 둔다. 그 420ms 동안과 이미 찬 칸에 들어온 입력은 버려진다.

사람 입력이 2.4초(150틱) 끊기면 두 쪽 다 컴퓨터가 둔다. X 는 빈 칸이 6개를 넘거나 30% 확률일 때 시드 고정 난수로 칸을 고르고 나머지는 bestCell 이며, 05 는 줄 여덟 개를 정해 둔 순서로 한 판씩 완성하는 대본 대국이고 06 은 소판 규칙이 따로 있어 chooseMove 가점식으로 둔다. 판이 끝난 뒤 사람이 누르면 새 판이 열린다.

입력은 칸 버튼이 아니라 캔버스 위 커서다

캔버스가 tabindex="0" 과 aria-label 을 갖고, 방향키로 커서(3×3, 06 은 9×9)를 옮겨 Enter 나 Space 로 놓는다. 칸마다 <button> 을 두지 않으니 <button> 은 멈춤·재생 하나뿐이고, 커서 칸은 캔버스가 포커스일 때만 흰 2.5px 테두리로 그린다.

상태 줄 aria-live="polite" 는 X 차례·O 승 같은 문구가 바뀔 때만 갱신하며, 키보드 커서가 있으면 뒤에 · 칸 행-열 이 붙는다.

시간과 난수는 고정이다

하네스가 16ms 고정 걸음(FIXED_DT)으로 scene.step() 을 부르고 그리기는 requestAnimationFrame 한 번당 한 번이다. 난수는 시드를 고정한 선형 합동 lcg(seed) 로만 만들고(장면마다 시드가 달라 01 은 11, 03 은 lcg(31), 09 는 lcg(91) 이며 09 의 배경 번짐만 lcg(5) 를 따로 쓴다), Math.random·new Date·Date.now·fetch·localStorage 는 아홉 파일 모두 0번 나온다.

이 판들을 붙일 만한 화면

01 은 모바일 대전 앱이나 보드게임 소개 페이지, 02 는 보드게임 카페 랜딩이나 장난감 상점 히어로에 어울린다. 03 은 학습용 게임 설명이나 AI 상대 난이도 안내, 04 는 전략 게임 튜토리얼과 수 읽기 연습 페이지에 맞는다.

05 는 승리 화면과 결과 공유 카드, 06 은 보드게임 포트폴리오와 대전 모드 소개 쪽이다. 07 은 수학 교육 콘텐츠나 퍼즐 해설, 08 은 알고리즘 강의 자료와 개발 블로그 도해, 09 는 교육 서비스 랜딩과 개발자 이벤트 포스터에 넣기 좋다.

html css tic tac toe 를 기법별 코드로 본다

각 조각은 index.html 의 <script> 에서 연속한 줄을 그대로 잘라 붙였고 25줄을 넘지 않는다. 파일 하나에 공용 코드(말 그리기·하네스·진행기)가 약 520줄 앞서고 장면 코드는 528행 근처부터 시작한다. tic tac toe html css code 를 찾는 사람이 보통 기대하는 칸 클릭 몇 줄보다 한 단계 안쪽, 판이 값을 계산하고 그리는 자리를 골라 실었다.

01칸을 누르면 X·O 가 획 순서대로 그려지고 칸이 스프링으로 눌렸다 돌아오는 말 놓기

X 는 두 획을 차례로, O 는 12시 방향에서 시계 방향 한 바퀴를 setLineDash 길이를 lineDashOffset 으로 줄여 가며 긋고, 칸의 크기는 a = -SPRING_K * (pos[i] - 1) - damp * vel[i] 라는 감쇠 스프링으로 한 번 넘쳤다가 가라앉게 한다. 아래 조각은 말이 놓인 칸마다 16ms 걸음을 네 번으로 쪼개 그 식을 적분하는 자리다. 이 칸의 누적면적은 19.3%, 평균 강도는 45.1, 움직인 프레임은 21/23 이었다.

칸을 누르면 X·O 가 획 순서대로 그려지고 칸이 스프링으로 눌렸다 돌아오는 말 놓기
새 창에서 열기

01 · 칸을 누르면 X·O 가 획 순서대로 그려지고 칸이 스프링으로 눌렸다 돌아오는 말 놓기 — 측정 누적면적 19.281% · 움직인 프레임 21/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

MARK_MSOVERSHOOTSTROKE_WSPRING_K
                const dt = FIXED_DT / 1000;
                for (let i = 0; i < 9; i += 1) {
                    if (age[i] < 0) {
                        continue;
                    }
                    age[i] += FIXED_DT;
                    // 16ms 한 걸음을 네 번으로 쪼개 적분 — 한 번에 가면 식이 목표로 한 넘침보다 작게 나온다
                    for (let q = 0; q < 4; q += 1) {
                        const a = -SPRING_K * (pos[i] - 1) - damp * vel[i];
                        vel[i] += (a * dt) / 4;
                        pos[i] += (vel[i] * dt) / 4;
                    }
                }

02포인터를 따라 판이 원근으로 기울고 말이 떨어져 앉을 때 높이만큼 그림자가 길고 흐려지는 입체 판

판은 252px 정사각형이고 포인터 위치는 lerp(ax, tx, LERP_K) 로 틱마다 기울기에 옮겨져 목표의 95% 에 닿기까지 32틱, 512ms 가 걸린다. 그림자는 말의 높이에 비례해 길어지고 번지는데 번짐은 실제 블러가 아니라 굵기가 다른 네 겹의 선이라, 앉은 말은 길이 9.9px·바깥 겹 굵기 약 17.5px 이고 90px 높이에서 떨어지는 순간은 길이 59.4px·굵기 약 56px 이다. 렌더 측정은 누적면적 26.7%, 평균 강도 45.6, 움직인 프레임 23/23 이다.

포인터를 따라 판이 원근으로 기울고 말이 떨어져 앉을 때 높이만큼 그림자가 길고 흐려지는 입체 판
새 창에서 열기

02 · 포인터를 따라 판이 원근으로 기울고 말이 떨어져 앉을 때 높이만큼 그림자가 길고 흐려지는 입체 판 — 측정 누적면적 26.728% · 움직인 프레임 23/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

TILT_MAXDEPTH_PXSHADOW_LENLERP_K
                for (let i = 0; i < 9; i += 1) {
                    if (who[i] === 0) {
                        continue;
                    }
                    const hgt = zb[i] + DEPTH_PX;
                    const off = hgt * SHADOW_LEN;
                    const blur = 1 + hgt * 0.16;
                    const alpha = clamp(0.5 - hgt * 0.0016, 0.18, 0.5);
                    for (let pass = 3; pass >= 0; pass -= 1) {
                        strokeMark(ctx, who[i], i, 0.5, 'rgba(10,8,22,' + (alpha / 4).toFixed(3) + ')', 7 + blur * pass * 0.9, off * 0.5, off * 0.8);
                    }
                }

03빈 칸마다 끝까지 수를 읽은 점수가 열지도 색으로 깔리고 가장 좋은 칸이 켜지는 판

빈 칸마다 그 칸에 둔 뒤의 판을 solve 가 끝판까지 펼쳐 X 가 이기면 SCORE_WIN - ply, 지면 그 음수, 비기면 0 을 위로 올려 보낸다. 점수는 민트(양수)와 분홍(음수)의 농도로 칠하고, 빈 판은 첫 수의 점수가 모두 0 이라 숫자 없이 그 칸 뒤 끝판에서 둘 사람이 이긴 수와 진 수의 차를 끝판 수로 나눈 비율만큼 연한 색이 갈리며, 가장 좋은 칸은 700ms 뒤부터 테두리가 맥박친다. 이 판은 누적면적 37.2%, 평균 강도 30.2, 움직인 프레임 21/23 으로 잡혔다.

빈 칸마다 끝까지 수를 읽은 점수가 열지도 색으로 깔리고 가장 좋은 칸이 켜지는 판
새 창에서 열기

03 · 빈 칸마다 끝까지 수를 읽은 점수가 열지도 색으로 깔리고 가장 좋은 칸이 켜지는 판 — 측정 누적면적 37.219% · 움직인 프레임 21/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

SEARCH_DEPTHSCORE_WINHEAT_ALPHATHINK_MS
        let best = turn === 1 ? -1000 : 1000;
        let any = false;
        for (let i = 0; i < 9; i += 1) {
            if (b[i] !== 0) {
                continue;
            }
            b[i] = turn;
            const v = solve(b, 3 - turn, ply + 1, depth - 1, cnt);
            b[i] = 0;
            any = true;
            if (turn === 1 ? v > best : v < best) {
                best = v;
            }
        }
        return any ? best : 0;

04칸에 올리면 거기 놓은 뒤 가능한 끝판의 승·무·패 비율이 반투명 말 둘레에 링으로 번지는 판

가리킨 빈 칸에 말을 놓았다고 치고 enumerate 가 끝판을 깊이 우선으로 세어 X 승·무·O 승 비율을 링 호의 길이로 바꾼다. 빈 판에서 모서리 칸을 가리키면 방문 국면이 59,705개라 ENUM_CAP 20,000 을 넘어 끝판 27,732 가운데 9,074개만 세고, 그때 링 바깥에 점선 링이 생기며 상태 줄에 집계 상한까지만 이 붙는다. 렌더 값은 누적면적 22.5%, 평균 강도 115.8, 움직인 프레임 21/23 이다.

칸에 올리면 거기 놓은 뒤 가능한 끝판의 승·무·패 비율이 반투명 말 둘레에 링으로 번지는 판
새 창에서 열기

04 · 칸에 올리면 거기 놓은 뒤 가능한 끝판의 승·무·패 비율이 반투명 말 둘레에 링으로 번지는 판 — 측정 누적면적 22.503% · 움직인 프레임 21/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

ODDS_RADIUSHOVER_LERPGHOST_ALPHAENUM_CAP
    function enumerate(b, turn, t, cap) {
        if (t.capped) {
            return;
        }
        t.nodes += 1;
        if (t.nodes > cap) {
            t.capped = true;
            return;
        }
        const w = lineOf(b);
        if (w >= 0) {
            if (b[LINES[w][0]] === 1) {
                t.x += 1;
            }
            else {
                t.o += 1;
            }
            return;
        }

05세 칸이 이어지면 줄이 칸 중심을 잇듯 긋고 빛이 번졌다 가라앉는 승리 연출

여덟 가지 줄 조합을 수마다 검사해 이긴 줄을 찾으면 양 끝 칸 중심에서 칸 크기의 0.3 만큼 더 뻗은 선을 LINE_MS 420ms 동안 늘린다. 번짐은 shadowBlur 에 GLOW_BLUR × 진행을 주다 다 그은 뒤 Math.cos(fade * Math.PI / 2) 로 900ms 동안 0 까지 줄이는데, 이 값은 캔버스 변환의 배율을 받지 않아 devicePixelRatio 를 곱해 보정한다. 측정은 누적면적 11.4%, 평균 강도 97.6, 움직인 프레임 14/23 이다.

세 칸이 이어지면 줄이 칸 중심을 잇듯 긋고 빛이 번졌다 가라앉는 승리 연출
새 창에서 열기

05 · 세 칸이 이어지면 줄이 칸 중심을 잇듯 긋고 빛이 번졌다 가라앉는 승리 연출 — 측정 누적면적 11.423% · 움직인 프레임 14/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

LINE_MSGLOW_BLURWIN_LINESFADE_ALPHA
                    x0 -= ux * CELL * 0.3;
                    y0 -= uy * CELL * 0.3;
                    x1 += ux * CELL * 0.3;
                    y1 += uy * CELL * 0.3;
                    const prog = smooth(winAge / LINE_MS);
                    // 번짐 반경 — 선이 자라는 동안 커지고, 다 그어진 뒤 사인 곡선을 따라 줄어든다
                    const fade = clamp((winAge - LINE_MS) / GLOW_FADE_MS, 0, 1);
                    const glow = GLOW_BLUR * (winAge < LINE_MS ? prog : Math.cos(fade * Math.PI / 2)) * dprG;
                    ctx.save();
                    ctx.shadowColor = rgba(MINT, 1);
                    ctx.shadowBlur = glow;
                    ctx.strokeStyle = rgba(mix(MINT, CREAM, 0.35), 1);
                    ctx.lineWidth = 9;
                    ctx.lineCap = 'round';
                    ctx.beginPath();
                    ctx.moveTo(x0, y0);
                    ctx.lineTo(lerp(x0, x1, prog), lerp(y0, y1, prog));
                    ctx.stroke();
                    ctx.restore();

06판 안에 판이 들어 있어 놓은 칸이 다음 판을 지정하고 화면이 그 판으로 스프링 확대되는 겹판

놓은 칸의 번호가 다음에 둘 소판의 번호가 되고, 그 소판이 이미 났거나 가득 찼으면 아무 소판이나 고를 수 있다. goal() 이 정한 소판 중심과 ZOOM_K 2.6배를 카메라의 위치와 배율이 각각 스프링으로 따라가 배율은 정점 2.725 까지 넘쳤다가 576ms 안에 이동 거리의 2% 이내로 정착한다. 이 겹판은 누적면적 59.0%, 평균 강도 32.3, 움직인 프레임 19/23 으로 아홉 판 중 면적이 가장 넓다.

판 안에 판이 들어 있어 놓은 칸이 다음 판을 지정하고 화면이 그 판으로 스프링 확대되는 겹판
새 창에서 열기

06 · 판 안에 판이 들어 있어 놓은 칸이 다음 판을 지정하고 화면이 그 판으로 스프링 확대되는 겹판 — 측정 누적면적 59.026% · 움직인 프레임 19/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

ZOOM_KFOCUS_MSDIM_ALPHASPRING_ZETA
        // 카메라 목표 — 정해진 판이 있으면 그 판으로 줌, 없으면 전체
        function goal() {
            if (fb >= 0) {
                return [(fb % 3) * SMALL + SMALL / 2, Math.floor(fb / 3) * SMALL + SMALL / 2, ZOOM_K];
            }
            return [BIG / 2, BIG / 2, 1];
        }

07돌리고 뒤집어도 같은 국면을 한 판으로 접어 겹치는 대칭 접기 보기

회전 네 가지와 반사 두 가지를 곱한 여덟 변환을 buildSym 이 칸 옮기기 표로 만들고, 국면을 여덟 번 옮겨 문자열로 이은 가운데 가장 작은 것을 표준형으로 삼는다. 반투명 여덟 판이 가운데에서 반지름 104px 고리로 펼쳐졌다가 800ms 동안 한 자리로 접히며, 상태 줄에 서로 다른 모양 n/8 이 적힌다. 렌더 값은 누적면적 29.1%, 평균 강도 70.0, 움직인 프레임 21/23 이다.

돌리고 뒤집어도 같은 국면을 한 판으로 접어 겹치는 대칭 접기 보기
새 창에서 열기

07 · 돌리고 뒤집어도 같은 국면을 한 판으로 접어 겹치는 대칭 접기 보기 — 측정 누적면적 29.139% · 움직인 프레임 21/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

SYM_COUNTFOLD_MSSPREAD_RMERGE_ALPHA
    function buildSym() {
        const out = [];
        for (let f = 0; f < 2; f += 1) {
            for (let r = 0; r < 4; r += 1) {
                const perm = [];
                for (let i = 0; i < 9; i += 1) {
                    let row = Math.floor(i / 3);
                    let col = i % 3;
                    for (let q = 0; q < r; q += 1) {
                        const nr = col;
                        col = 2 - row;
                        row = nr;
                    }
                    if (f === 1) {
                        col = 2 - col;
                    }
                    perm.push(row * 3 + col);
                }
                out.push(perm);
            }
        }
        return out;
    }

08현재 국면에서 갈라지는 수를 가지로 펼치고 그 아래 끝판 수에 따라 가지 굵기가 달라지는 나무 보기

뿌리는 현재 국면의 54px 미니 판이고 빈 칸마다 반지름 70px 원 위에 26px 자식 판이 55ms 시차로 펼쳐진 뒤, 160ms 늦게 손자 점이 따라 나온다. 가지 굵기는 BRANCH_W 5 에 끝판 수의 로그 비율을 곱해 1 에 더한 값이고, 끝까지 둔 점수가 양수면 하늘색·음수면 분홍·0 이면 크림색이다. 이 나무는 누적면적 18.6%, 평균 강도 75.0, 움직인 프레임 16/23 으로 측정됐다.

현재 국면에서 갈라지는 수를 가지로 펼치고 그 아래 끝판 수에 따라 가지 굵기가 달라지는 나무 보기
새 창에서 열기

08 · 현재 국면에서 갈라지는 수를 가지로 펼치고 그 아래 끝판 수에 따라 가지 굵기가 달라지는 나무 보기 — 측정 누적면적 18.648% · 움직인 프레임 16/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

RING_RNODE_RBRANCH_WEXPAND_MS
        function branch(ctx, x0, y0, x1, y1, leaves, mx, prog, col, hot) {
            const w = 1 + BRANCH_W * (Math.log(1 + leaves) / Math.log(1 + mx));
            ctx.strokeStyle = rgba(col, hot ? 0.95 : 0.55);
            ctx.lineWidth = w;
            ctx.lineCap = 'round';
            ctx.beginPath();
            ctx.moveTo(x0, y0);
            ctx.lineTo(lerp(x0, x1, prog), lerp(y0, y1, prog));
            ctx.stroke();
        }

09분필 질감의 칠판 위에서 두 AI 가 스스로 두고 가루가 흩날리다 지우개로 지워지는 히어로

분필 선은 직선을 5px 마다 끊어 수직 방향으로 값 노이즈 × 1.8px 를 더해 흔들고, 가루는 풀 90개에서 말을 긋는 틱마다 2개씩 생겨 중력과 바람을 받는다. 마지막 수 400ms 뒤 지우개가 800ms 동안 x 74 에서 406 까지 훑으며 판을 비우고, 이때 띠 오른쪽만 말을 그려 지나간 자리가 비어 보인다. 이 칠판은 누적면적 45.6%, 평균 강도 85.6, 움직인 프레임 22/23 이다.

분필 질감의 칠판 위에서 두 AI 가 스스로 두고 가루가 흩날리다 지우개로 지워지는 히어로
새 창에서 열기

09 · 분필 질감의 칠판 위에서 두 AI 가 스스로 두고 가루가 흩날리다 지우개로 지워지는 히어로 — 측정 누적면적 45.573% · 움직인 프레임 22/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

CHALK_NOISEDUST_NTURN_MSERASE_MS
        function chalkLine(x0, y0, x1, y1, seed) {
            const len = Math.hypot(x1 - x0, y1 - y0);
            const n = Math.max(2, Math.ceil(len / 5));
            const nx = -(y1 - y0) / len;
            const ny = (x1 - x0) / len;
            const out = [];
            for (let k = 0; k <= n; k += 1) {
                const t = k / n;
                const off = (noise2(t * len * 0.045, seed * 7.31, 3) - 0.5) * 2 * CHALK_NOISE;
                out.push([lerp(x0, x1, t) + nx * off, lerp(y0, y1, t) + ny * off]);
            }
            return out;
        }

값이 판의 인상을 바꾸는 곳 — 튕김 세기·탐색 깊이·번짐 반경

세 값은 화면을 찍어서 잰 것이 아니라 소스에 든 함수를 노드에서 그대로 불러 센 것이다. 렌더로 잰 수치는 뒤의 표에 따로 모았다.

튕김 세기를 1.1 에서 1.6 으로 올리면 정착이 368ms 에서 1,424ms 로 늘어난다

01 의 OVERSHOOT 가 1.1 이면 감쇠비는 0.591 이고 칸은 240ms 에 정점 1.093배를 찍은 뒤 368ms 안에 2% 이내로 가라앉는다. 1.6 으로 올리면 감쇠비가 0.160 으로 내려 정점이 192ms 에 1.598배로 오르고 정착은 1,424ms 가 된다. O 의 응답이 420ms 뒤에 오니, 그 사이 다음 수가 놓여도 앞 말은 한참 출렁이는 셈이다.

탐색 깊이를 9 에서 3 으로 낮추면 열지도가 평평해진다

03 의 SEARCH_DEPTH 9 는 빈 판에서 549,945개 국면을 훑고(노드에서 cellScores 한 번에 약 13ms), 3 은 585개만 본다. 말이 1~2개 놓인 모든 국면은 81개이고 그 둘 곳이 576칸인데, 부호가 있는 점수가 나오는 칸이 깊이 9 에서는 276칸이고 깊이 3 에서는 0칸이다.

번짐 반경을 6 에서 20 으로 올리면 빛이 닿는 면적이 약 2.3배가 된다

05 의 GLOW_BLUR 6 일 때 280px 선 한 줄(굵기 9px)을 480×300 캔버스에 그리면 중심선에서 빛이 닿는 거리가 10px 이고 닿는 픽셀은 5,900개다. 20 으로 올리면 22px 와 13,348개로, 면적이 약 2.3배라 칸 사이 틈까지 빛이 번진다.

렌더한 아홉 장면의 면적·강도·프레임은 한 표에 모았다. 움직인 프레임은 항목마다 14~23 으로 갈리는데, 05 의 14/23 이 가장 낮은 까닭은 이 측정만으로는 가르지 못했다.

항목 누적면적 평균 강도 움직인 프레임
01 스프링 말 놓기 19.3% 45.1 21/23
02 입체 판 26.7% 45.6 23/23
03 점수 열지도 37.2% 30.2 21/23
04 승패 링 22.5% 115.8 21/23
05 승리선 11.4% 97.6 14/23
06 겹판 59.0% 32.3 19/23
07 대칭 접기 29.1% 70.0 21/23
08 게임 나무 18.6% 75.0 16/23
09 칠판 자동 대국 45.6% 85.6 22/23

어디서 깨지는가 — 빠르게 연타할 때·판이 끝난 뒤·움직임 줄이기 설정

연타하면 O 의 응답 420ms 동안 입력이 사라진다

아홉 판 모두 사람이 둔 뒤 420ms 동안은 turn !== 1 이라 새 입력을 버린다. 연타가 먹히지 않는 것처럼 보이는 구간이고, 이미 말이 있는 칸을 누른 입력도 같은 이유로 무시된다.

02 는 떨어지는 말과 클릭 칸이 어긋나 보일 수 있다

클릭 판정 hit 는 말의 높이가 아니라 받침면(z = 0)의 사각형으로 계산한다. 최대 기울기 0.42rad 에서 90px 높이의 말은 원근을 빼고 어림하면 90 × sin 0.42 로 약 37px 위에 보이고, 원근까지 계산하면 가운데 줄에서 약 43px(줄에 따라 31~52px) 위에 보이므로, 떨어지는 동안 그림의 말과 눌러야 하는 칸이 어긋난다.

04 의 빈 판 비율은 앞 칸 쪽에 치우친다

상한 20,000 에서 멈춘 집계는 깊이 우선이라 앞 번호 칸 쪽 가지를 먼저 센다. 말이 한 개 놓인 판부터는 상한 아래라 끝판을 끝까지 센다(가운데에 X 가 있고 첫 빈 칸을 가리킨 판의 방문 국면은 6,812개).

05 는 판이 끝난 뒤 누르면 연출이 중간에 끊긴다

승리선을 긋는 중이든 번짐이 가라앉는 중이든 사람이 누르면 새 판이라 효과가 바로 사라진다. 비기면 줄도 번짐도 없다.

06 은 전체 보기에서 칸이 작다

칸은 캔버스 좌표로 전체 보기에서 30px, 줌에서 78px 이다. 320×200 뷰포트에서는 .tt 폭이 약 279px 로 줄어 각각 약 17px·45px 가 되므로 전체 보기의 터치 목표로는 작다. 소판이 정해지지 않은 자유 선택 순간에도 카메라는 전체로 줌 아웃하지만 클릭은 칸 좌표로 계산된다.

07 은 접히는 동안 탭을 받지 않는다

접힘과 정착 단계 1,060ms 에는 입력을 무시한다. 가운데 판의 모양이 표준형으로 바뀌어 실제 칸과 어긋나기 때문이다. 도달 가능한 국면 5,478개가 표준형으로는 765개로 접히고, 모서리 X 와 변 O 국면은 변형 여덟 개가 모두 달라 서로 다른 모양이 8/8 까지 간다.

08 은 빈 판에서 굵기 차이가 거의 없다

빈 판 아래 끝판은 255,168개이고 자식별로 25,872~29,592개라 굵기가 5.9~6.0px 안에 몰린다. 말 2개 판의 자식 7개도 끝판이 441~576개라 굵기 차이가 0.2px 정도이고, 차이는 손자 층에서 크게 난다.

09 는 배경이 고해상도에서 흐리다

배경 번짐을 한 번 굽는 오프스크린 캔버스(480×300)는 devicePixelRatio 를 곱하지 않아 dpr 2 화면에서 두 배로 늘어나 흐릿하다. 가루 풀이 90개로 차면 가장 오래된 것부터 사라진다.

위의 세 값을 손으로 바꿔 보려면 아래 카드의 압축 파일을 받으면 되고, 압축을 풀 때 입력할 비밀번호는 gvrueby4 이다. 판마다 HTML·SCSS 한 쌍과 리액트 판이 함께 들어 있다.

변형 세 가지와 접근성

이름 값 느낌
튕김 세기 바꾸기 01 OVERSHOOT 1.1 → 1.6 넘침이 커진다. 감쇠비가 0.59 에서 0.16 으로 내려 정점이 1.09배에서 1.60배로 오르고, 2% 안으로 가라앉는 시간이 약 370ms 에서 약 1,420ms 로 늘어 말이 한참 출렁인다.
탐색 깊이 바꾸기 03 SEARCH_DEPTH 9 → 3 열지도가 평평해진다. 말이 1~2개 놓인 모든 국면(81개)의 둘 곳 576칸 중 부호가 있는 점수는 깊이 9 에서 276칸, 깊이 3 에서 0칸이고, 빈 판의 탐색 국면은 549,945개에서 585개로 줄어 생각 시간이 거의 사라진다.
번짐 반경 바꾸기 05 GLOW_BLUR 6 → 20 빛이 넓게 번진다. 280px 선 한 줄을 480×300 에 그렸을 때 중심선에서 빛이 닿는 거리가 10px 에서 22px, 빛이 닿는 면적이 5,900px 에서 13,348px 로 약 2.3배 커져 칸 사이까지 번진다.

세 변형의 숫자도 위 값 절과 같은 방식으로 소스 함수를 노드에서 직접 불러 센 것이다.

입력 처리는 아홉 파일이 같은 뼈대를 쓴다. 파일마다 <button> 은 하나뿐이고 캔버스는 tabindex="0" 과 aria-label 을, 상태 줄은 aria-live 를 한 번씩 갖는다. preventDefault 는 방향키 분기와 Enter·Space 분기 두 곳에서만 부르므로 Tab 은 막히지 않아 캔버스에서 버튼으로 포커스가 넘어가고, setPointerCapture 는 아홉 파일 모두 쓰지 않는다.

prefers-reduced-motion 은 자바스크립트의 matchMedia 한 곳과 CSS 한 곳에서 읽는다. 켜져 있으면 requestAnimationFrame 루프를 시작하지 않고 세 수를 놓은 장면 한 장을 그리며, 입력이 오면 show() 로 보간 없이 곧바로 다시 그리고 O 의 응답도 기다리지 않는다. 버튼은 다음 장면 으로 바뀐다. 01 은 스프링 없이 최종 크기와 완성된 획을, 02 는 기울기 0 과 떨어지는 높이 0 을, 04 는 링이 목표 비율에 바로 닿은 모습을, 06 은 카메라가 목표에 즉시 맞은 모습을 보여 준다. 07 은 접힘 없이 여덟 변형을 고리에 고정해 두고, 08 은 가지를 즉시 펼치며, 09 는 가루와 지우개가 없다.

tic tac toe html css javascript 만으로 충분한가?

아홉 파일에는 라이브러리·CDN·WebGL 이 없고 <script src 도 http 도 0번이라 파일 하나만 있으면 돈다. 점수 열지도의 탐색, 승패 링의 집계, 대칭 접기, 게임 나무가 모두 이 한 파일 안의 자바스크립트로 계산된다.

부족한 것은 서비스로 만들 때의 부분이다. 승패 기록을 저장하는 localStorage 도 효과음을 내는 AudioContext 도 없고, 두 사람이 마주 앉아 두는 모드도 없다. 공을 튕겨 벽돌을 깨는 판의 반사각은 벽돌깨기 게임 만들기 9가지 — 반사각은 맞은 위치가 정한다 에서, 길을 찾는 뱀은 css snake game 9가지 — 키보드 없이 길을 찾아 먹이를 먹는 뱀 게임 에서 이어서 볼 수 있다. 장르를 더 넓게 훑고 싶다면 CSS 게임 만들기 — 미니게임 9종 복붙 코드와 웹 미니게임 만들기 9종 — 계산기·타이머·퍼즐 복붙 코드 를 함께 열어 본다.

tic tac toe game html css js 코드를 가져다 쓰기 전에 자주 묻는 질문

html css tic tac toe 소스를 그대로 내 사이트에 붙여도 되나?

외부 주소를 부르지 않아 붙일 때 따라붙는 의존성 목록이 없다. 파일마다 index.html 하나에 스타일과 스크립트가 들어 있고 난수가 시드로 고정이라 같은 화면이 재현된다. 다만 이 글의 한국어 판 압축 파일은 화면 문구가 한국어로 들어 있어 쓰는 곳의 언어로 바꿔야 하고, 영어·일본어 판 압축 파일에는 각 언어 문구가 들어 있다.

컴퓨터의 수는 어디서 정하나?

사람이 둔 뒤 O 의 응답은 01~05·07~09 에서 bestCell 이 전수 탐색 점수가 가장 높은 칸을 고르고 동점이면 낮은 번호 칸이다. 06 만 소판이 있어 chooseMove 가 줄 완성 +100·상대 줄 막기 +40·가운데 칸 +6 같은 가점과 상대가 바로 이길 판을 넘기면 −30 감점에 lcg 잡음을 더해 정한다. 03 의 점수는 전수 탐색 값이고 04 의 승패 비율은 상한이 있는 끝판 세기라, 둘 다 판을 어떻게 읽는지 보여 줄 뿐 승패를 약속하는 값은 아니다.

매번 다른 판이 나오게 하려면 어디를 바꾸나?

자동 대국에서 X 가 무작위로 고르는 수는 lcg 시드로 정해지므로 시드 숫자를 바꾸면 다른 판이 나온다. 05 는 줄 순서가 정해진 대본이라 시드로 달라지지 않는다. 매번 무작위로 하고 싶으면 그 자리에 쓸 값을 직접 만들어 넣어야 하고, 이 편의 파일은 그런 값을 만들지 않는다.

압축 비밀번호를 입력하세요

비밀번호는 이 글 본문 안에 있습니다. 읽다 보면 나옵니다.