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

css captcha 9가지 — 왜곡 글자·회전 맞추기·노이즈 속 글자 확인

css captcha 를 사람 확인이라 부르지만 화면이 하는 일은 계산이다 — 01번은 글자 열을 사인파로 밀고, 07번은 노이즈 문턱을 올려 숨은 글자를 건져 올린다. 여기 아홉 장면은 확인 화면이 어떻게 움직이는지 보여 주는 시연이고, 보안 기능이 아니다.

아홉 가지 사람 확인 장면을 화면에서 바로 풀어 본다

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

css captcha 9가지 — 열 밀기·각도 흡착·문턱 올리기로 만드는 확인 장면

장면은 하나같이 <canvas> 2D 위에서 그려진다. 글자가 휘는 양, 타일 줄무늬의 각도, 노이즈 값처럼 화소나 좌표 하나하나가 바뀌는 것을 키프레임 몇 장으로 적을 방법이 없어서, CSS 에는 무대 비율·둥근 모서리·touch-action·초점 테두리 같은 겉틀만 남겼다.

뼈대는 아홉 파일이 같은 mount 하나를 나눠 쓴다. requestAnimationFrame 이 넘겨주는 타임스탬프를 FIXED_DT 16ms 틱으로 쪼개 장면을 진행시키고, 난수는 시드를 받는 lcg 와 hash2 에서만 나온다. Math.random·Date.now·fetch 는 아홉 소스 어디에도 없어서 시드가 같으면 같은 문제가 다시 나온다. 06 만은 lcg 를 정의해 두고도 문제를 만드는 데 시드를 쓰지 않는다.

격자에서 장면이 저절로 맞았다 틀렸다 하는 것은 녹화 영상이 아니다. 입력이 150틱(2.4초) 동안 없으면 시연 스크립트가 사람 손과 같은 입구(down·move·up·key)로 입력을 넣어 문제를 풀어 보이고, 사람이 만지는 순간 시연은 멈춘다.

그리고 한 가지 선은 분명히 긋는다. 허용 각도, 정답 칸 집합, 통과 점수, 실패 횟수 같은 판정은 이 데모 안의 시연 규칙일 뿐이고 서버·토큰·검증 요청은 한 줄도 없다. 브라우저에 들어 있는 정답은 소스를 연 사람이 그대로 읽을 수 있으므로, 실제 서비스에서 문제를 내고 답을 가리는 일은 서버 몫이고 이 장면들은 그 결과를 보여 주는 화면 자리에 들어간다.

붙일 곳으로 출발점을 고르면 빠르다. 회원가입 폼의 확인 칸이면 01 이나 06, 사진 방향을 맞추게 하는 단계는 02, 그림 고르기 판은 03, 순서대로 누르는 짧은 퍼즐은 04 가 맞다. 서명 칸 앞의 따라 긋기는 05 이고, captcha css game 처럼 점수가 뜨는 화면을 찾는다면 05 와 04 를 함께 보면 된다. 노이즈에서 글자를 골라내게 하는 화면은 07, 로그인 실패가 쌓일수록 글자를 더 일그러뜨리는 화면은 08, 유효 시간이 있는 문제 위젯은 09 가 가깝다.

captcha css animation 을 기법별 코드로 본다

조각마다 장면 파일 index.html 안에서 끊기지 않고 이어진 줄을 옮겼고, 어느 것도 25줄을 넘기지 않는다. 확인 동작은 대개 조각 속 함수 하나가 정하고, 그리는 코드는 그 함수가 돌려준 값을 받아 칠하기만 한다.

01글자 이미지가 열마다 사인파로 밀려 휘어 보이는 왜곡 글자판

240×150 작업 캔버스에 글자와 비스듬한 줄무늬를 한 번 구워 getImageData 로 받아 두고, 매 프레임 warpPixels 가 36,000화소를 돌며 세로 위치에 따른 가로 변위와 열 위치에 따른 세로 변위를 사인으로 구해 원본 화소를 끌어온다. 판 밖으로 나간 좌표는 가장자리 화소로 묶이고, 결과는 putImageData 로 쓴 뒤 480×300 으로 2배 늘려 그 위에 흔들리는 베지어 선 4개를 겹친다. 렌더에서는 아홉 장면 중 세 번째로 넓은 57.5% 를 덮었고 23/23 프레임이 전부 달랐다(강도 44.3).

글자 이미지가 열마다 사인파로 밀려 휘어 보이는 왜곡 글자판
새 창에서 열기

01 · 글자 이미지가 열마다 사인파로 밀려 휘어 보이는 왜곡 글자판 — 측정 누적면적 57.532% · 움직인 프레임 23/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

WARP_AMPWARP_FREQNOISE_LINESGLYPH_PX
    function warpPixels(src, out, lw, lh, amp, freq, ph) {
        for (let y = 0; y < lh; y += 1) {
            const dx = Math.round(amp * 0.6 * Math.sin(y * freq * 1.3 + ph * 1.2));
            for (let x = 0; x < lw; x += 1) {
                const dy = Math.round(amp * Math.sin(x * freq + ph));
                let sx = x + dx;
                let sy = y + dy;
                sx = sx < 0 ? 0 : (sx >= lw ? lw - 1 : sx);
                sy = sy < 0 ? 0 : (sy >= lh ? lh - 1 : sy);
                const si = (sy * lw + sx) * 4;
                const di = (y * lw + x) * 4;
                out[di] = src[si];
                out[di + 1] = src[si + 1];
                out[di + 2] = src[si + 2];
                out[di + 3] = 255;
            }
        }
    }

02다이얼을 돌려 기울어진 풍경을 바로 세우면 허용 각도 안에서 스프링으로 붙는 확인

다이얼 각도에 DIAL_GAIN 1.5 를 곱해 풍경을 통째로 돌리고, 손을 떼면 snapCorrection 이 각도를 −180~180 으로 접은 다음 SNAP_DEG 10° 안이면 보정량을, 밖이면 null 을 돌려준다. 보정은 springStep 이 16ms 틱마다 SPRING_K 190·DAMP 15 로 0 까지 끌고 가는데, 노드에서 8° 를 넣어 보면 8.86° 까지 넘어갔다가(오버슈트 10.7%) 512ms 에 자리를 잡는다. 이 장면은 23프레임 중 21프레임에서 변화가 잡혔고 누적면적 21.3%·강도 56.5 였다.

다이얼을 돌려 기울어진 풍경을 바로 세우면 허용 각도 안에서 스프링으로 붙는 확인
새 창에서 열기

02 · 다이얼을 돌려 기울어진 풍경을 바로 세우면 허용 각도 안에서 스프링으로 붙는 확인 — 측정 누적면적 21.267% · 움직인 프레임 21/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

SNAP_DEGDIAL_GAINSPRING_KDAMP
    // 각도를 -180 이상 180 미만으로 접는다
    function wrapDeg(d) {
        let r = (d + 180) % 360;
        if (r < 0) {
            r += 360;
        }
        return r - 180;
    }
    // 허용 각도 안이면 똑바로 서게 할 보정량(도), 밖이면 null
    function snapCorrection(theta, snapDeg) {
        const d = wrapDeg(theta);
        return Math.abs(d) <= snapDeg ? -d : null;
    }
    // 스프링 한 걸음 (반암시적 오일러) - 위치와 속도를 돌려준다
    function springStep(pos, vel, target, k, damp, dt) {
        const a = -k * (pos - target) - damp * vel;
        const v = vel + a * dt;
        return [pos + v * dt, v];
    }

033×3 타일에서 같은 무늬를 골라 누르면 타일이 뒤집혀 표시되는 선택판

tileAngles 는 시드로 기준 각도를 15° 단위 12가지 중 하나로 정하고, 정답 칸 3개에는 기준에서 ±6° 안의 각도를, 나머지 여섯 칸에는 ANGLE_TOL × 1.6 에 0~60° 를 더한 만큼 벌린 각도를 준다. 시드 1~300 을 다 돌려 보면 정답 칸의 최대 차가 5.99°, 오답 칸의 최소 차가 19.20° 라 그 틈에 허용치 12° 가 들어가고, 누른 칸은 scaleY = |cos(πp)| 로 납작해졌다가 뒷면으로 넘어간다. 움직인 프레임은 15/23 으로 아홉 중 가장 적었지만 뒤집힐 때의 강도 평균은 104.2, 누적면적은 35.2% 였다.

3×3 타일에서 같은 무늬를 골라 누르면 타일이 뒤집혀 표시되는 선택판
새 창에서 열기

03 · 3×3 타일에서 같은 무늬를 골라 누르면 타일이 뒤집혀 표시되는 선택판 — 측정 누적면적 35.173% · 움직인 프레임 15/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

TILE_NFLIP_MSMATCH_MINSHIMMER
        const angles = [];
        const match = [];
        for (let i = 0; i < total; i += 1) {
            angles.push(0);
            match.push(false);
        }
        for (let k = 0; k < total; k += 1) {
            const i = order[k];
            if (k < nMatch) {
                angles[i] = target + (r() - 0.5) * tol;
                match[i] = true;
            }
            else {
                const sign = r() < 0.5 ? -1 : 1;
                angles[i] = target + sign * (tol * 1.6 + r() * 60);
            }
        }
        return { target: target, angles: angles, match: match };
    }

04흩어진 글리프를 순서대로 누르면 선이 이어져 그려지는 확인

글리프 4개는 placeGlyphs 가 서로 92px 이상 떨어뜨려 놓고, 맨 위 안내 줄의 순서대로 누르면 선 머리가 growStep 으로 남은 거리의 16% 에 0.012 를 더하며 자라 15틱(240ms) 만에 0.99 에 닿는다. 순서를 어기면 분홍 점선이 나갔다 돌아온 뒤 이어 둔 선이 끝에서부터 틱당 0.1 씩 되감겨 첫 글리프까지 지워진다. 누적면적은 17.9% 에 그쳤지만 강도 평균은 111.4 로 아홉 장면 가운데 가장 높았고 23/23 프레임이 움직였다.

흩어진 글리프를 순서대로 누르면 선이 이어져 그려지는 확인
새 창에서 열기

04 · 흩어진 글리프를 순서대로 누르면 선이 이어져 그려지는 확인 — 측정 누적면적 17.943% · 움직인 프레임 23/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

GLYPH_NHIT_RJITTERLINE_LERP
        function click(i) {
            if (rewind || i < 0) {
                return;
            }
            if (done) {
                // 다 이은 판을 다시 누르면 새 판으로 넘어간다
                seed += 1;
                setup();
                return;
            }
            const want = order[chain.length];
            if (i === want) {
                chain.push(i);
                if (chain.length > 1) {
                    seg.push(0);
                }
                if (chain.length === GLYPH_N) {
                    done = true;
                }
            }
            else if (!chain.includes(i)) {
                att = { from: chain.length ? chain[chain.length - 1] : -1, to: i, t: 1 };
                rewind = true;
            }
        }

05구불구불한 안내선을 따라 그은 포인터 궤적이 안내선과의 평균 거리로 점수가 되는 확인

안내선은 두 사인을 더한 곡선을 가로 40~440 에서 4px 간격 101점으로 찍어 두고, 포인터가 6px 이상 움직일 때마다 표본을 남기며 nearest 로 가장 가까운 점까지의 거리를 잰다. scoreTrail 은 그 평균을 round(100 × (1 − 평균 / (2 × TOL_PX))) 로 바꾸므로, 통과 기준 60점은 평균 거리가 TOL_PX 의 0.8배인 9.6px 이하라는 뜻이다. 선이 지나간 자리는 12.9% 로 좁았고 강도 91.6 으로 23/23 프레임에서 그어지는 모습이 찍혔다.

구불구불한 안내선을 따라 그은 포인터 궤적이 안내선과의 평균 거리로 점수가 되는 확인
새 창에서 열기

05 · 구불구불한 안내선을 따라 그은 포인터 궤적이 안내선과의 평균 거리로 점수가 되는 확인 — 측정 누적면적 12.908% · 움직인 프레임 23/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

GUIDE_AMPTOL_PXSCORE_PASSSAMPLE_STEP
    function nearest(px, py, guide) {
        let best = 1e9;
        for (let i = 0; i < guide.length; i += 1) {
            const dx = guide[i][0] - px;
            const dy = guide[i][1] - py;
            const d = dx * dx + dy * dy;
            if (d < best) {
                best = d;
            }
        }
        return Math.sqrt(best);
    }
    // 궤적 표본마다 가장 가까운 안내선 거리를 평균해 점수로 바꾼다 (평균이 허용치의 두 배면 0점)
    function scoreTrail(samples, guide, tol) {
        if (samples.length === 0) {
            return { mean: 0, score: 0 };
        }
        let sum = 0;
        for (let i = 0; i < samples.length; i += 1) {
            sum += nearest(samples[i][0], samples[i][1], guide);
        }
        const mean = sum / samples.length;
        const score = Math.round(100 * Math.min(1, Math.max(0, 1 - mean / (tol * 2))));
        return { mean: mean, score: score };
    }

06체크 칸이 고리로 바뀌어 돌다가 체크 표시로 이어지는 확인 버튼

한 줄은 대기 140 → 칸에서 고리로 360 → 호 회전 560 → 멈춤 180 → 고리에서 체크로 360ms 구간을 차례로 거치고, rowShape 는 이 구간들의 누적 시각 t1~t5 와 이징 값 e 하나로 모서리 반경(12→32px)과 회전각(0→90°)을 같이 정한다. 그래서 변환이 절반인 e = 0.5 에서 반경 22px·회전 45° 가 되고, 반경이 한 변 64px 의 절반인 32px 에 이르면 사각형이 원이 된다. 덮은 면적은 12.2% 로 아홉 중 두 번째로 좁았는데도 강도는 102.6 이었고 23/23 프레임에 변화가 있었다.

체크 칸이 고리로 바뀌어 돌다가 체크 표시로 이어지는 확인 버튼
새 창에서 열기

06 · 체크 칸이 고리로 바뀌어 돌다가 체크 표시로 이어지는 확인 버튼 — 측정 누적면적 12.18% · 움직인 프레임 23/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

RING_RSPIN_MSMORPH_EASEPASS_DELAY
        else if (u < t2) {
            e = easeInOutPow((u - t1) / morphMs, ease);
            phase = 1;
        }
        else if (u < t3) {
            e = 1;
            spin = (u - t2) / spinMs;
            phase = 1;
        }
        else if (u < t4) {
            e = 1;
            phase = 1;
        }
        else if (u < t5) {
            e = 1 - easeInOutPow((u - t4) / morphMs, ease);
            phase = 1;
            check = clamp01((u - t4) / morphMs - 0.35) / 0.65;
        }
        else {
            e = 0;
            check = 1;
            phase = 2;
        }
        return { r: boxR + (ringR - boxR) * e, rot: e * Math.PI * 0.5, e: e, spin: spin, check: check, phase: phase, total: t5 };
    }

07노이즈 속에 숨은 글자가 문턱을 올릴수록 또렷하게 떠오르는 확인

160×100 칸마다 값 노이즈 3층을 합친 fbm 에 글자 마스크 × BOOST 0.42 를 더하고, litAmount 가 그 값이 문턱을 넘은 정도를 GRAIN 0.045 폭 안에서 0~1 로 부드럽게 올려 밝기로 쓴다. 노드 시험 4만 표본에서 fbm 이 0.86 을 넘는 비율은 0.14%, 0.44 를 넘는 비율은 66.4% 라서, 문턱이 0.86 까지 오르면 바깥 노이즈는 거의 꺼지고 마스크 안쪽은 3분의 2 가량이 켜진 채 남는다. 화면 넓이로는 아홉 중 가장 큰 75.7% 를 덮었고 23/23 프레임이 움직였다(강도 87.1).

노이즈 속에 숨은 글자가 문턱을 올릴수록 또렷하게 떠오르는 확인
새 창에서 열기

07 · 노이즈 속에 숨은 글자가 문턱을 올릴수록 또렷하게 떠오르는 확인 — 측정 누적면적 75.716% · 움직인 프레임 23/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

THRESHNOISE_SCALEOCTAVESGRAIN
    function fbm(x, y, seed, oct) {
        let sum = 0;
        let amp = 1;
        let norm = 0;
        let f = 1;
        for (let o = 0; o < oct; o += 1) {
            sum += amp * vnoise(x * f + o * 17.3, y * f - o * 9.1, seed + o);
            norm += amp;
            amp *= 0.5;
            f *= 2;
        }
        return sum / norm;
    }
    // 값이 문턱을 넘은 정도를 0~1 로 (grain 폭 안에서 부드럽게 올라간다)
    function litAmount(v, thresh, grain) {
        const k = (v - thresh) / grain + 0.5;
        return k <= 0 ? 0 : (k >= 1 ? 1 : k * k * (3 - 2 * k));
    }

08틀릴 때마다 왜곡 진폭과 선 수가 한 단계씩 올라가는 단계형 확인

distortionFor 는 진폭을 min(13, 1.5 + 3 × 실패), 선 수를 min(6, 1 + 실패) 로 정하고, 글자판은 4px 폭 조각 75개를 두 사인의 합만큼 위아래로 밀어 drawImage 로 다시 붙인다. 그려지는 진폭 ampNow 는 목표 쪽으로 틱마다 20% 씩 다가가서, 실패할 때마다 판이 단번에 튀지 않고 점점 더 휜다. 실패가 쌓여 가는 시연 동안 23/23 프레임이 달라졌고 면적 59.7%·강도 79.5 로 기록됐다.

틀릴 때마다 왜곡 진폭과 선 수가 한 단계씩 올라가는 단계형 확인
새 창에서 열기

08 · 틀릴 때마다 왜곡 진폭과 선 수가 한 단계씩 올라가는 단계형 확인 — 측정 누적면적 59.694% · 움직인 프레임 23/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

ESCALATE_STEPAMP_MAXLINES_MAXFAIL_LIMIT
                const d = distortionFor(fails, ESCALATE_STEP, AMP_MAX, LINES_MAX);
                const ox = shake > 0 ? Math.sin(shake * 1.7) * shake * 0.5 : 0;
                // 글자판 - 세로 4px 조각마다 사인 변위를 다르게 줘 다시 붙인다
                ctx.save();
                ctx.beginPath();
                rrect(ctx, BX + ox, BY, BW, BH, 16);
                ctx.clip();
                ctx.fillStyle = rgba(PANEL, 1);
                ctx.fillRect(BX + ox, BY, BW, BH);
                for (let x = 0; x < BW; x += 4) {
                    const dy = ampNow * Math.sin(x * 0.05 + ph) + ampNow * 0.35 * Math.sin(x * 0.13 - ph * 1.7);
                    ctx.drawImage(src, x, 0, 4, BH, BX + ox + x, BY + dy, 4, BH);
                }

09문제 유효 시간이 고리로 줄다가 끝나면 새로고침 아이콘이 돌며 새 문제로 바뀌는 확인

ringFrac 은 1 − 경과 / TTL_MS 를 0~1 로 잘라 고리 호 길이로 쓰고, warnMix 는 남은 비율이 WARN_FRAC 0.3 아래로 내려간 뒤 0 에서 1 로 올라 민트를 분홍으로 섞는다. 0 에 닿으면 새로고침 아이콘이 360ms 동안 반 바퀴 돌고 나서 시드가 하나 넘어가고 고리가 220ms 에 다시 차므로, 문제 하나가 만료되어 새 글자판으로 바뀌기까지 1300 + 360 = 1660ms 가 걸린다. 만료되면 글자판이 globalAlpha 0.28 로 어두워지고 새 글자로 바뀌지만 움직임의 대부분은 고리와 아이콘이라 면적은 가장 작은 11.8% 였고, 강도 86.4 로 23/23 프레임이 잡혔다.

문제 유효 시간이 고리로 줄다가 끝나면 새로고침 아이콘이 돌며 새 문제로 바뀌는 확인
새 창에서 열기

09 · 문제 유효 시간이 고리로 줄다가 끝나면 새로고침 아이콘이 돌며 새 문제로 바뀌는 확인 — 측정 누적면적 11.785% · 움직인 프레임 23/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

TTL_MSWARN_FRACREFRESH_SPIN_MSRING_W
    // 남은 시간 비율 - 고리 호의 길이가 된다
    function ringFrac(elapsedMs, ttlMs) {
        const f = 1 - elapsedMs / ttlMs;
        return f < 0 ? 0 : (f > 1 ? 1 : f);
    }
    // 경고 구간 안에서 0 에서 1 로 올라가는 색 섞음 비율
    function warnMix(frac, warn) {
        if (frac >= warn) {
            return 0;
        }
        const k = 1 - frac / warn;
        return k > 1 ? 1 : k;
    }

값이 확인의 난이도를 바꾸는 곳 — 진폭·허용 각도·문턱

난이도는 상수 몇 개에 몰려 있다. 먼저 렌더 측정을 적어 둔다 — 480×300 으로 찍은 24장 사이의 변화 23번을 센 값이다. 그 아래 소제목의 숫자들은 장면 함수만 떼어 노드에서 불러 얻은 것이라 이 표와 섞어 읽지 않는다.

장면 누적면적 강도 평균 움직인 프레임
01 왜곡 글자판 57.5% 44.3 23/23
02 다이얼 흡착 21.3% 56.5 21/23
03 타일 선택판 35.2% 104.2 15/23
04 순서 잇기 17.9% 111.4 23/23
05 따라 긋기 점수 12.9% 91.6 23/23
06 고리 체크 12.2% 102.6 23/23
07 노이즈 문턱 75.7% 87.1 23/23
08 단계 왜곡 59.7% 79.5 23/23
09 만료 고리 11.8% 86.4 23/23

WARP_AMP 를 3 에서 12 로 올리면 줄무늬 화소의 56.6% 가 자리를 옮긴다

폭 8화소짜리 세로 줄무늬를 240×150 에 깔고 같은 위상으로 warpPixels 를 부르면, 원래 값과 달라지는 화소가 진폭 3 에서 14.9%, 12 에서 56.6% 다. 가장 멀리 밀리는 거리도 3화소에서 12화소로 4배가 되어, 2배로 늘린 화면 기준으로는 6px 에서 24px 로 커진다. 데모에서 포인터와 방향키로 바꾸는 배율은 둘 다 0.1~2.5 로 잘려서 화면에서 직접 바꿔 볼 수 있는 진폭은 7.5 까지다.

허용 각도 10° 의 경계는 10° 에서 붙고 10.5° 에서 놓친다

snapCorrection 에 10° 를 넣으면 −10 이 돌아와 흡착되고, 10.5° 는 null 이다. 각도를 먼저 접기 때문에 350° 는 −10° 와 같은 자리로 읽혀 +10 이 나오고, −8° 는 +8 이 나온다. 시연은 짝수 바퀴에 24°(SNAP_DEG × 2.4)에서 놓아 실패를, 홀수 바퀴에 5.5° 에서 놓아 흡착을 번갈아 보인다.

TOL_PX 를 12 에서 36 으로 넓히면 ±24px 로 떨린 궤적도 통과한다

안내선에서 ±14px 흔들며 그은 궤적(평균 거리 8.2px)은 TOL_PX 12 에서 66점, 36 에서 89점이다. ±24px 로 흔든 궤적(평균 13.8px)은 12 에서 43점으로 기준 60점에 못 미치지만 36 에서는 81점이 된다. 허용치가 공식의 분모에 들어 있어서 같은 손떨림이라도 점수에서 깎이는 폭이 3분의 1로 줄어든다.

문턱 0.86 에서 글자 바깥은 0.14% 만 켜진다

07 의 문턱을 내리면 글자 바깥의 노이즈가 함께 살아난다. 4만 표본 시험에서 바깥 화소가 켜질 확률은 문턱 0.86 에서 0.14%, 0.7 에서 8.16%, 0.6 에서 25.62% 였다. 시연은 문턱을 0.36 에서 0.86 까지 72틱 동안 올리고 28틱 머문 뒤 22틱에 걸쳐 내려서, 글자가 노이즈에서 떠올랐다 다시 잠기는 과정을 한 바퀴에 담는다.

TTL_MS 1300 은 렌더 안에 만료를 넣으려고 줄인 값이다

기본값 1300 은 2초 렌더 안에서 만료가 한 번 보이도록 짧게 잡은 시연용 값이고, 폼에 붙일 때는 늘려 쓴다. 5200 으로 늘리면 1000ms 시점에 고리의 남은 비율이 0.23 에서 0.81 로 오르고, 경고색이 시작되는 시각은 TTL_MS × 0.7 이라 910ms 에서 3640ms 로 미뤄진다.

어디서 깨지는가 — 캔버스가 클 때·탭이 멈출 때·움직임 줄이기 설정

captcha css animation 을 값만 바꿔 다시 쓰다 보면 무너지는 곳은 화소 수, 판정 범위, 시간 계산 세 갈래로 모인다. 아래 수치는 소스 함수를 노드에서 직접 부른 결과이고, 브라우저 안의 프레임 시간은 따로 재지 않았다.

작업 해상도를 캔버스 크기로 올리면 왜곡 계산이 4배가 된다

01 의 warpPixels 는 틱마다 240×150 = 36,000화소를 돌며 화소마다 사인과 범위 고정을 거치고, 작업 기계의 노드에서 진폭 12 로 한 번 부른 시간은 0.55ms 였다. 작업 해상도를 480×300 으로 올리면 화소가 144,000개가 되어 같은 루프가 4배 돈다. 반대로 지금처럼 낮은 해상도를 2배 늘리면 글자 윤곽이 계단처럼 보이고, 진폭을 키우면 가장자리에서 묶인 화소가 한 줄로 번진다. 07 도 160×100 으로 계산해 3배 늘리기 때문에 글자 가장자리가 3화소 단위로 뭉개진다.

다이얼을 캔버스 밖까지 끌면 좌표가 가장자리에서 잘린다

02 는 pointerdown 에서 setPointerCapture 를 걸어 두어서 포인터가 캔버스를 벗어나도 이동 이벤트가 끊기지 않는다. 다만 point() 가 좌표를 캔버스 가장자리로 잘라 넘기므로 바깥에서 돌린 만큼이 각도에 그대로 반영되지는 않는다. 스프링도 지금 값에서 이미 10.7% 를 넘어가므로, SPRING_K 를 올리고 DAMP 를 내리면 목표를 넘나드는 시간이 길어진다.

기준 각도가 0° 나 90° 근처면 뒤집힌 타일을 알아보기 어렵다

03 의 뒷면은 앞면 줄무늬 각도의 부호를 뒤집어 그린다. 기준이 0° 근처면 부호를 바꿔도 거의 같은 각도이고, 90° 근처면 −90° 가 180° 주기로 다시 90° 와 겹쳐서 앞뒤 줄무늬가 거의 같다. 12가지 기준 중 2가지가 여기에 걸리니, 뒷면을 연한 색으로만 구분하는 지금 방식이 그 두 경우를 떠맡는다.

글리프를 7개로 늘리면 시드 300개 중 37개가 자리를 못 채운다

04 의 자리 영역은 가로 392 × 세로 152 이고, placeGlyphs 는 최소 거리 92px 를 지키며 400번까지 시도한다. 시드 300개로 세어 보면 GLYPH_N 5 까지는 모자란 시드가 없고, 6 에서 5개, 7 에서 37개가 끝내 자리를 다 채우지 못한다. 글리프 떨림은 사인 두 개의 합이라 최대 8.4px 이고 누름 판정 반지름 26px 보다 작아서, 떨리는 순간에 눌러도 판정이 흔들리지 않는다.

05 의 점수는 방향과 완주를 보지 않는다

scoreTrail 은 표본마다 가장 가까운 안내선 점까지의 거리를 평균할 뿐이라, 안내선을 거꾸로 따라가거나 중간에서 멈춰도 평균만 작으면 통과한다. 따라 긋기를 실제 화면에 쓸 생각이라면 시작점과 끝점에 닿았는지, 가로 진행이 한쪽으로 이어졌는지 같은 조건을 따로 얹어야 한다.

06 의 시간을 늘리면 마지막 줄이 끝나기 전에 바퀴가 돈다

06 한 줄의 전체 시간은 다섯 상수를 더한 1600ms 이고 시연 한 바퀴는 126틱(2016ms)이다. 마지막 줄은 틱 17 에 시작해 117틱에 끝나서 여유가 9틱뿐이라, SPIN_MS 같은 값을 키우면 그 줄이 체크에 닿기 전에 reset() 이 온다. MORPH_EASE 를 1 로 내리면 이징이 선형이 되어 칸이 고리로 부풀어 오르는 느낌이 사라진다.

08 은 다섯 번째 실패부터 단계가 더 오르지 않는다

진폭 상한 13 은 실패 4번째에, 선 수 상한 6 은 5번째에 닿고, 그 뒤의 실패는 횟수만 늘린다. 오른쪽 눈금 5칸도 실패 5 이상이면 같은 모양이라 눈금(진폭)만 보고는 다섯 번과 열 번을 구분할 수 없고, 횟수는 상태 줄의 「실패 10회」 같은 문구로 읽는다. 조각 폭을 4px 에서 1px 로 줄이면 drawImage 호출이 75번에서 300번으로 늘고, 글자판 위아래에는 진폭만큼 빈 띠가 패널 색으로 드러난다.

탭이 백그라운드에 있던 시간은 09 의 시계에 들어가지 않는다

아홉 장면의 틀은 프레임 간격을 Math.min(ts - lastTs, 160) 으로 자른다. 탭이 숨었다 돌아오면 그사이 시간은 160ms 로 줄어들어, 09 의 고리는 실제로 흐른 시간보다 늦게 만료된다. 만료를 정확히 지켜야 하는 화면이라면 문제를 낸 시각을 서버가 들고 있다가 그 기준으로 판정하고, 이 고리는 남은 시간을 보여 주는 데만 쓴다.

움직임 줄이기 설정에서는 09 의 시간이 아예 흐르지 않는다

움직임 줄이기 설정을 켜면 09 는 고리가 저절로 줄지 않고, 「다음 장면」 버튼과 새로고침 입력으로만 처음 → 경고 직전 → 새로고침 장면을 번갈아 보인다. 이 설정을 켠 사람에게 시간 제한을 그대로 걸면 남은 시간이 화면에 보이지 않는 셈이니, 실제 화면에서는 그 사람에게 시간을 넉넉히 주는 쪽을 따로 정해야 한다.

진폭이나 허용 각도를 직접 바꿔 가며 판정이 어느 값에서 무너지는지 보려면 원본 파일이 있어야 한다. 압축을 여는 비밀번호는 t8786fwz 이고, 파일 묶음에는 장면 아홉 개의 원본과 tsx 로 옮긴 리액트 포트가 같이 실려 있다.

변형 세 가지와 접근성

이름 값 느낌
왜곡 진폭 바꾸기 01 WARP_AMP 3 → 12 줄무늬 시험에서 바뀌는 화소가 14.9% 에서 56.6% 로 늘고, 가장 크게 밀리는 거리가 화면 기준 6px 에서 24px 이 된다.
허용 폭 바꾸기 05 TOL_PX 12 → 36 ±14px 로 떨린 궤적이 66점에서 89점으로, ±24px 궤적은 43점 탈락에서 81점 통과로 바뀐다.
유효 시간 바꾸기 09 TTL_MS 1300 → 5200 1000ms 시점의 남은 비율이 0.23 에서 0.81 로 오르고 경고색은 910ms 대신 3640ms 에 켜진다.

세 변형의 숫자도 노드에서 warpPixels·scoreTrail·ringFrac·warnMix 를 직접 불러 센 값이다.

확인 화면은 포인터를 못 쓰는 사람을 쉽게 밀어낸다. 그래서 아홉 캔버스 모두 tabindex="0" 으로 Tab 초점을 받고, aria-label 에 항목 이름을 달았으며, 캔버스와 버튼에 :focus-visible 테두리가 있다. 키 입력은 장면마다 다르다 — 01·07 은 방향키로 진폭이나 문턱을 조절하고, 02 는 ←/→ 5°씩 돌리고 마지막 입력에서 30틱이 지나면 저절로 놓으며, 03 은 화살표로 칸을 옮겨 Space 로 고르고 Enter 로 확인한다. 04 는 방향키로 글리프를 고르고, 05 는 Space 로 펜을 올리고 내리며 화살표로 9px 씩 긋고, 06 은 ↑/↓ 와 Enter 또는 Space, 08 은 Backspace·Escape 로 처음으로, 09 는 R 로도 새로고침한다. preventDefault 는 그 장면이 실제로 쓰는 키에만 걸어서 Tab 으로 다음 요소에 넘어가는 길은 그대로 남는다. 캔버스에는 touch-action: none 이 있어 휴대폰에서 손가락으로 끌어 움직이는 동안 페이지가 함께 스크롤되지 않는다(포인터 이벤트).

상태 문구는 aria-live 인 <p> 하나에 나온다. 시연이 혼자 도는 동안은 aria-live="off" 라 화면 읽기 프로그램이 조용하고, 사람이 만지기 시작하면 polite 로 바뀌며 문구는 44틱(약 0.7초)에 한 번만 갱신된다.

prefers-reduced-motion 이 켜져 있으면 rAF 루프를 돌리지 않는다. 시작할 때 REDUCE_TICKS 만큼 틱을 몰아 돌려 대표 장면 한 장을 그리고, 입력이 들어오면 30틱을 몰아 돌린 결과만 다시 그려서 중간 장면이 없다. 멈춤 버튼은 「다음 장면」 이 되고, 06 은 줄을 누를 때 끝 상태와 처음 상태가 번갈아 나오며, 03·04 는 통과한 판을 다시 누르면 새 판이 열려 계속 입력할 수 있다.

captcha code html css 만으로 충분한가?

화면 틀만 보면 HTML 과 CSS 로 충분하다. 무대 .cp 는 width: min(92vw, calc(160vh - 12.8vw)) 에 aspect-ratio: 480 / 300 이고 16px 둥근 모서리에 overflow: hidden 이 전부다. 캔버스는 논리 좌표 480×300 에 devicePixelRatio 를 2 까지 곱한 해상도로 잡혀서, 렌더 캡처에서는 약 419×262px, 320×200 화면에서는 약 279×174px 로 보여도 장면 계산은 같다. 하지만 화소를 옮기고 노이즈 문턱을 재고 점수를 내는 부분은 스크립트가 해야 하고, 화소 판독은 getImageData 를 01 은 글자판을, 07 은 글자 마스크를 새로 구울 때 한 번씩만 부른다.

더 큰 이유는 따로 있다. HTML·CSS·JS 어느 쪽이든 브라우저에서 도는 코드는 정답을 숨겨 둘 수 없어서, 이 편의 장면은 확인 결과를 보여 주는 겉모습으로만 쓴다. 실패 횟수에 따라 글자를 더 일그러뜨리는 08 은 로그인 실패 횟수 제한 9종 의 잠금 화면과 짝을 이루고, 폼 전체의 흐름은 로그인 UI 디자인 9종 에서, 입력하는 동안 강도를 보여 주는 칸은 비밀번호 강도 UI 9종 에서 이어 볼 수 있다. 시간 계산의 근거는 requestAnimationFrame 문서에서 확인했다.

captcha html css js 코드를 가져다 쓰기 전에 자주 묻는 질문

이 장면을 회원가입 폼에 그대로 넣으면 사람 확인이 되나?

되지 않는다. 판정 규칙과 정답이 브라우저 스크립트 안에 있어서 소스를 열면 다 보인다. 문제를 만들고 답을 검사하는 일은 서버에 두고, 이 화면은 서버가 낸 문제를 보여 주고 결과를 표시하는 자리로 쓰는 것이 맞다.

captcha css game 처럼 점수를 내는 05 는 어떤 궤적에 60점을 주나?

TOL_PX 12 에서는 표본 평균 거리가 9.6px 이하이면 60점 이상이다. ±14px 로 떨며 그은 궤적은 66점으로 통과하고, ±24px 궤적은 43점으로 떨어진다. 방향과 완주는 따지지 않으니 점수만으로 판단하지 않는다.

같은 시드를 넣으면 같은 문제가 다시 나오나?

나온다. 난수가 lcg·hash2 에서만 나오고 Math.random 이 없어서, 06 을 뺀 여덟 장면은 시드 하나가 글자·각도·자리·안내선을 정한다. 시연은 한 바퀴가 끝날 때마다 대개 시드를 1 올려 다음 문제로 넘어간다.

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

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