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

캐릭터 걷기 애니메이션 9종 — 키프레임 없이 계산으로 걷게 만들기

걸음은 몸이 앞으로 넘어지는 것을 다리가 번갈아 잡는 계산이다 — 02번은 접지부터 들어올림까지 다섯 국면으로 발바닥이 바닥에 붙어 있는 시간을 만들고, 03번은 보폭을 벌리는 각도로 속도를 정하며, 07번은 내리막에서 몸이 앞으로 숙여 다리가 회복할 시간을 번다.

캐릭터 걷기 애니메이션은 포즈 여러 장을 순서대로 틀어 놓는 키프레임 재생이 아니라, 몸무게 중심이 앞으로 쏠리는 힘과 다리 두 개가 번갈아 바닥을 누르는 각도를 매 프레임 계산해서 걷는 것처럼 보이게 하는 계산이다 — 캐릭터 걷기 애니메이션은 사이클 위상 하나에서 시작한다

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

아홉 장면은 같은 걸음 시계와 같은 다리 풀이를 쓰고, 장면마다 들여다보는 값 하나만 다르다. 캐릭터는 화면 한가운데 제자리에서 걷고, 바닥 눈금과 배경의 언덕·구름이 groundV 속도에 맞춰 뒤로 흘러간다.

걸음 상태는 보행 주기 계산에서만 나온다

시계는 둘이다. 걸음 시계 walkT 는 걸음 위상을 정하고, 장면 시계 sceneT 는 방향 전환이나 경사 바꾸기 같은 시간표를 돌린다. 다리 각도·발 위치·팔 흔들림은 (walkT * STEP_HZ) % 1 에서 풀려 나오고 따로 저장한 걸음 그림은 없다.

    function advance(dt) {
        sceneT += dt;
        walkT += dt;
        if (!manual) {
            while (evAt < script.length && script[evAt].t <= sceneT) {
                const ev = script[evAt];
                evAt += 1;
                ev.f();
            }
            if (sceneT >= LAP_S) {
                lap += 1;
                begin();
                return;
            }
        }
        update(dt);
    }

begin() 이 판마다 두 시계를 0 으로 되돌리므로 같은 시간표는 같은 걸음을 낸다. 9개 index.html 어디에도 Math.random·Date·performance·fetch 는 없다.

고정 걸음 1/60초로 나눠 먹는다

프레임 간격은 DT_MAX 0.05초에서 자르고 acc 에 쌓아 while (acc >= STEP_S) 로 STEP_S 1/60초씩 advance 에 넘긴다. 탭이 숨으면 루프를 멈췄다가 돌아올 때 acc 를 비우고 첫 프레임의 dt 를 버려, 몇 초가 한 프레임에 몰려 걸음이 튀지 않게 한다.

걷기 사이클 9종 — 위상 이음·접지 다섯 국면·보폭 각도·시선과 머리 흔들림·팔꿈치 반대 흔들림·몸통 요동·경사 적응·발 이동 곡선 보정·발바닥 고정

아홉 장면을 렌더해 잰 면적과 프레임

장면 누적면적 움직인 프레임 강도 평균
01 위상 이음 21.4% 23/23 78.4
02 접지 다섯 국면 20.3% 23/23 62.0
03 보폭 각도 13.5% 23/23 62.6
04 머리와 시선 14.3% 23/23 70.5
05 팔꿈치 15.6% 23/23 78.9
06 몸통 비틀림 14.9% 23/23 73.3
07 경사 적응 20.2% 23/23 63.1
08 호 보정 14.1% 23/23 64.9
09 발 고정 완성 16.1% 23/23 115.8

누적면적은 01 의 21.4% 가 가장 넓고 03 의 13.5% 가 가장 좁다. 아홉 장면 모두 23/23 프레임 움직였고, 강도 평균은 09 가 115.8 로 가장 높으며 가장 낮은 것은 02 의 62.0 다.

01끊김 없이 이어지는 사이클 위상

걷기를 두 걸음 한 바퀴로 잡고 cyclePhase() 는 (walkT * STEP_HZ) % 2 를 돌려주며, legPhase() 는 오른다리에만 반 걸음을 얹어 두 다리를 반 바퀴 어긋나게 둔다. 나머지 연산으로 위상을 자르니 마지막 포즈와 첫 포즈 사이에 걸음마다 튀는 점프가 생기지 않는다. 화면 오른쪽 패널의 「이음 점프」 는 한 프레임에 sin 값이 0.35 넘게 튄 횟수이고, 같은 식을 노드로 3.75초(225프레임) 돌린 값은 0회였다.

끊김 없이 이어지는 사이클 위상
새 창에서 열기

01 · 끊김 없이 이어지는 사이클 위상 — 측정 누적면적 21.353% · 움직인 프레임 23/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

cyclePhaselegPhaseSTEP_HZwalkT
    const GAIT = { stepHz: STEP_HZ, strideDeg: STRIDE_DEG, legPx: LEG_PX, legRatio: LEG_RATIO, contactShare: CONTACT_SHARE };
    // walkT 는 걸음 시간으로만 오르는 시계(초). phase 는 한 걸음 위상 0~1, cyclePhase 는 두 걸음 위상 0~2
    let walkT = 0;
    function stepPhase() { return (walkT * GAIT.stepHz) % 1; }
    function cyclePhase() { return (walkT * GAIT.stepHz) % 2; }
    function stepIndex() { return Math.floor(walkT * GAIT.stepHz); }
    // 다리별 한 걸음 위상 — 왼다리가 정수 걸음, 오른다리가 반 걸음 어긋난다
    function legPhase(right) { return ((walkT * GAIT.stepHz) + (right ? 0.5 : 0)) % 1; }

02접지에서 밀어내기까지 발바닥이 붙는 다섯 국면

한 걸음을 접지 12%·저항 18%·중간 15%·밀어내기 15%·들어올림 40% 로 나누고, CONTACT_SHARE 0.6 안(inStance)에서는 solveFoot 이 발목을 바닥 높이에 얼린다. 발목은 접지에서 -14도로 닿아 0도로 눕다가 0.45 걸음 뒤부터 앞꿈치를 밀며 16도까지 구른다. 한 걸음 0.63초 가운데 접지가 0.37초, 스윙이 0.25초다.

접지에서 밀어내기까지 발바닥이 붙는 다섯 국면
새 창에서 열기

02 · 접지에서 밀어내기까지 발바닥이 붙는 다섯 국면 — 측정 누적면적 20.344% · 움직인 프레임 23/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

PH5inStanceCONTACT_SHAREsolveFoot
    function drawStanceFoot(hipX, hipY, ph) {
        const fs = solveFoot(hipX, hipY, ph, plantXOf(0, hipX), 0, GROUND_Y);
        // 접지 국면: 발바닥을 바닥에 붙인 채 굴림 — ankleDeg 를 ph 로 보간(뒤꿈치 닿기 → 앞꿈치 밀기)
        const roll = ph < 0.12 ? lerp(-14, 0, ph / 0.12) : (ph > 0.45 ? lerp(0, 16, (ph - 0.45) / (GAIT.contactShare - 0.45)) : 0);
        footShape(fs.ax, fs.ay, fs.planted ? roll : fs.ankleDeg, MINT);
        return roll;
    }

03보폭 각도로 걸음 속도 정하기

STRIDE_DEG 28도에서 한 걸음이 밀어내는 거리는 2 * LEG_PX * sin(28도) 로 84.5px 이고, 여기에 STEP_HZ 1.6 을 곱한 135.2px/s 가 groundV 로 들어가 바닥 눈금이 그 속도로 흐른다. 아래쪽 어긋난 판은 몸 속도를 0.82배인 110.9px/s 로 두고 접지 발이 drift 만큼 기어가는 자취를 그려 보인다. 접지 0.37초 동안 몸이 지나가는 거리는 50.7px 다.

보폭 각도로 걸음 속도 정하기
새 창에서 열기

03 · 보폭 각도로 걸음 속도 정하기 — 측정 누적면적 13.483% · 움직인 프레임 23/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

STRIDE_DEGstepDistMISMATCHgroundV
    function build() {
        stepDist = 0;
        vOk = 0;
    }
    function update(dt) {
        if (dt <= 0) {
            return;
        }
        // 한 걸음 거리 — 앞발 도달(앞 벌림)과 뒷발 밀기(뒤 벌림)의 합
        stepDist = Math.sin(rad(GAIT.strideDeg)) * GAIT.legPx * 2;
        vOk = stepDist * GAIT.stepHz;
        groundV = vOk;
    }

04걸음마다 들리는 머리와 고정되는 시선

hipHeight 가 만드는 몸 높이 요동은 진폭 7.20px 이고, 머리는 headY 에서 그 요동의 82% 를 반대로 빼 눈 진폭이 1.3px 로 줄어 눈높이가 거의 일정해 보인다. 방향이 바뀌는 2.4초보다 0.4초 앞서 시선 gazeX 가 320 에서 120 으로 먼저 움직여 다음 방향을 알려 준다.

걸음마다 들리는 머리와 고정되는 시선
새 창에서 열기

04 · 걸음마다 들리는 머리와 고정되는 시선 — 측정 누적면적 14.278% · 움직인 프레임 23/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

hipHeightbobNowgazeXdirChangeAt
        const ph = legPhase(false);
        // 몸 요동 — hipHeight 가 이미 ph 의 사인. 머리는 반 바퀴 어긋난 위상으로 상쇄
        bobNow = hipHeight(ph);
        headY = bobNow - 0.82 * (hipHeight(ph) - GAIT.legPx * 0.93);
        // 걸음 방향 전환 — 전환 0.4초 전에 시선이 먼저 목표로 움직인다
        if (dirChangeAt > 0 && sceneT > dirChangeAt - 0.4 && sceneT < dirChangeAt) {
            const k = (sceneT - (dirChangeAt - 0.4)) / 0.4;
            gazeX = lerp(320, 120, k);
        }
        if (dirChangeAt > 0 && sceneT >= dirChangeAt) {
            gazeX = 120;
            dirChangeAt = -1;
        }

05다리와 반대로 흔들리는 팔꿈치

solveArm 은 같은 쪽 다리에서 반 걸음 어긋난 armPh 로 어깨를 STRIDE_DEG * 0.6 만큼, 곧 최대 16.8도 흔들고 팔꿈치를 70 * (0.4 + LEG_RATIO) 로 스케일해 최대 61.6도 접는다. LEG_RATIO 를 0.42 로 내리면 같은 식이 57.4도를 돌려주니, 다리 비율을 바꾸면 팔 느낌이 함께 움직인다.

다리와 반대로 흔들리는 팔꿈치
새 창에서 열기

05 · 다리와 반대로 흔들리는 팔꿈치 — 측정 누적면적 15.59% · 움직인 프레임 23/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

solveArmLEG_RATIOARM_LbendBase
    // 팔 풀이 — 어깨 각도(다리에 비례·반위상)와 팔꿈치 접힘(앞 스윙에서 최대). 접힘 스케일은 LEG_RATIO
    function solveArm(shoulderX, shoulderY, side) {
        const ph = legPhase(side === 1);
        const armPh = (ph + 0.5) % 1;
        const swing = Math.sin(armPh * TAU);
        const deg = swing * GAIT.strideDeg * 0.6;
        const bendBase = Math.max(0, Math.sin(armPh * Math.PI)) * 70;
        const bend = bendBase * (0.4 + GAIT.legRatio);
        const a = rad(deg);
        const ex = shoulderX + Math.sin(a) * ARM_L;
        const ey = shoulderY + Math.cos(a) * ARM_L;
        const fa = a + rad(bend) * (Math.sin(a) >= 0 ? 1 : 0.6);
        const hx = ex + Math.sin(fa) * FORE_L;
        const hy = ey + Math.cos(fa) * FORE_L;
        return { ex: ex, ey: ey, hx: hx, hy: hy, bend: bend, deg: deg };
    }

06걸음마다 좌우로 도는 몸통과 골반

골반은 hipTwist = sin(ph * TAU) * amp 로 돌고 어깨는 반대 방향으로 1.2배(shTwist) 돌며, amp 는 6 * (STEP_HZ / 1.6) 이라 1.6Hz 에서 골반 ±6도·어깨 ±7.2도가 나온다. 걸음 수가 2.4Hz 로 오르면 ±9도·±10.8도로 커져 빨리 걸을수록 몸통이 더 비틀린다.

걸음마다 좌우로 도는 몸통과 골반
새 창에서 열기

06 · 걸음마다 좌우로 도는 몸통과 골반 — 측정 누적면적 14.933% · 움직인 프레임 23/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

hipTwistshTwistSTEP_HZlegPhase
    function update(dt) {
        if (dt <= 0) {
            return;
        }
        // 골반·어깨 회전 — 걸음 위상 사인에 비례, 크기는 걸음 속도에 비례(1.6Hz 에서 ±6도)
        const ph = legPhase(false);
        const amp = 6 * (GAIT.stepHz / 1.6);
        hipTwist = Math.sin(ph * TAU) * amp;
        shTwist = -Math.sin(ph * TAU) * amp * 1.2;
        if (Math.floor(sceneT * 30) % 2 === 0) {
            hipHist.push(hipTwist);
            shHist.push(shTwist);
            if (hipHist.length > 90) {
                hipHist.shift();
                shHist.shift();
            }
        }
    }

07경사면에서 몸이 기우는 적응 걸음

leanDeg 는 내리막 기울기에 0.5, 오르막에 0.25 를 곱해 몸을 기울이고, 내리막 -14도에서 몸이 7도 앞으로 숙으며 baseStepHz + max(0, -lean) * 0.06 식으로 걸음 수가 1.6Hz 에서 약 2.02Hz 로 오른다. 오르막 +10도에서는 몸이 2.5도 뒤로 기울고 걸음 수는 1.6Hz 그대로다. 접지 발의 높이는 groundYAt 경사식에서 구하고 발목은 경사각만큼 꺾는다.

경사면에서 몸이 기우는 적응 걸음
새 창에서 열기

07 · 경사면에서 몸이 기우는 적응 걸음 — 측정 누적면적 20.211% · 움직인 프레임 23/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

leanDeggroundYAtsolveFootSlopeSLOPES
    function update(dt) {
        if (dt <= 0) {
            return;
        }
        slopeNow = slopeAt();
        // 내리막에서 걸음 수를 올린다 — 앞으로 기울기(-lean)가 클수록 stepHz 증가
        const lean = leanDeg(slopeNow);
        GAIT.stepHz = clamp(baseStepHz + Math.max(0, -lean) * 0.06, 0.8, 2.8);
    }

08발이 지나가는 호를 깎는 이동 보정

ankleY 의 보정 없는 판은 GROUND_Y - sin(PI * k) * LEG_PX * 0.09 의 낮은 호를 그리고, 보정판은 ARC_MIN_PX * clamp(1.6 / STEP_HZ, 0.5, 2) 인 최소높이를 k 0.85 까지 유지하다 마지막 15% 에서만 내린다. 최소높이는 1.6Hz 에서 20px, 2.4Hz 에서 13.3px, 0.8Hz 에서 40px 이다. 스윙 구간을 100점 표본으로 잡은 근사식에서 바닥 4px 안 구간의 몫이 34% 에서 3% 로 줄었고, 화면의 scuffRaw·scuffFix 는 이와 따로 프레임을 직접 센다.

발이 지나가는 호를 깎는 이동 보정
새 창에서 열기

08 · 발이 지나가는 호를 깎는 이동 보정 — 측정 누적면적 14.109% · 움직인 프레임 23/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

ARC_MIN_PXankleYswingKscuffRaw
    function swingK() {
        const ph = legPhase(true);
        if (inStance(ph)) {
            return -1;
        }
        return (ph - GAIT.contactShare) / (1 - GAIT.contactShare);
    }
    // 발목 y — raw(원호 그대로) vs corrected(최소높이)
    function ankleY(k, corrected) {
        const half = rad(GAIT.strideDeg);
        const ang = lerp(half, -half, k);
        const rawY = GROUND_Y - Math.sin(Math.PI * k) * GAIT.legPx * 0.09;
        if (!corrected) {
            return rawY;
        }
        const minH = ARC_MIN_PX * clamp(1.6 / GAIT.stepHz, 0.5, 2);
        const drop = k > 0.85 ? (k - 0.85) / 0.15 : 0;
        const lift = Math.max(minH * (1 - drop), Math.sin(Math.PI * Math.min(k / 0.85, 1)) * minH * 1.2);
        return rawY - lift;
    }

09바닥에 고정되는 발바닥과 완성 장면

세 걸음꾼은 28도·1.6Hz, 18도·2.0Hz, 40도·1.2Hz 이고 한 걸음 거리가 84.5·55.6·115.7px, 속도가 135.2·111.3·138.8px/s 가 된다. 접지 발은 위상과 고정된 몸 x 에서 푼 화면 좌표라 몸이 sin(sceneT * 3.1) * 3px 흔들려도 제자리에 있고, maxSlip 은 좌표를 다시 풀었다면 몸 속도 ±12% 요동이 한 프레임에 밀었을 최대량 0.27px 를 식으로 어림한 값이다.

바닥에 고정되는 발바닥과 완성 장면
새 창에서 열기

09 · 바닥에 고정되는 발바닥과 완성 장면 — 측정 누적면적 16.135% · 움직인 프레임 23/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

lockPlantsplantXOfWALKERSmaxSlip
    function update(dt) {
        if (dt <= 0) {
            return;
        }
        // 접지 중 발의 화면 이동량 측정 — plant 저장이면 0, 다시 풀면 요동만큼 움직인다
        maxSlip = 0;
        for (let i = 0; i < WALKERS.length; i += 1) {
            const wk = WALKERS[i];
            for (let r = 0; r < 2; r += 1) {
                const ph = ((walkT * wk.hz) + (r === 1 ? 0.5 : 0)) % 1;
                if (ph < 0.6) {
                    const noSave = Math.sin(rad(wk.stride)) * GAIT.legPx * 2 * wk.hz * Math.cos(sceneT * 3.1) * 0.12 * (1 / 60);
                    maxSlip = Math.max(maxSlip, Math.abs(noSave));
                }
            }
        }
    }

캐릭터 걷기 애니메이션을 코드로 만들면 한 걸음은 어떤 값 몇 개로 정해질까?

다섯 값이 한 걸음을 정한다. 나머지 수치는 이 다섯에서 계산으로 따라 나온다.

소스 상수 다섯 개가 전부다

상수 값 정하는 것
STEP_HZ 1.6 초당 걸음 수. 한 걸음 0.63초, 두 걸음 사이클 1.25초
STRIDE_DEG 28 다리가 앞뒤로 벌어지는 반각
LEG_PX 90 다리 길이. 고관절에서 발목까지
LEG_RATIO 0.48 정강이와 넙다리의 비율
CONTACT_SHARE 0.6 한 걸음에서 접지가 차지하는 몫

한 걸음 안의 시간 배분

CONTACT_SHARE 0.6 은 접지 0.37초와 스윙 0.25초로 갈리고, 두 다리가 반 걸음 어긋나 있어 두 발이 동시에 바닥에 있는 구간이 0.2걸음, 0.13초 생긴다. 몸 높이 hipHeight 는 LEG_PX * (0.93 + 0.04 * bob) 꼴로 80.10px 에서 87.30px 사이를 오간다.

사이클 위상을 시간으로 밀고 다리 각도를 삼각함수로 풀고 뼈마디를 화면 좌표로 옮겨 그리는 — 걷는 애니메이션 만들기 한 걸음이 처리되는 순서

위상이 먼저, 그림은 뒤따른다

advance 가 walkT 를 올리면 legPhase 가 두 다리의 위상을 내고, inStance 가 접지인지 스윙인지 가른다. 그다음에야 solveFoot 이 발 위치를 정하고 limb 이 뼈마디를 캔버스에 긋는다.

코사인 법칙으로 무릎을 푼다

다리는 넙다리 L1 60.81px 와 정강이 L2 29.19px 의 2링크이고 합은 LEG_PX 90px 다. 고관절과 발목 사이 거리 d 를 구해 L1 + L2 - 0.01 로 자른 뒤 코사인 법칙으로 무릎 각을 풀고, 굽힘이 두 갈래로 나오는 해 가운데 base - elbow 쪽 하나만 쓴다.

걷는 캐릭터를 실제 화면에 붙일 때 — 라이브러리 없이 만들기·방향 전환과 걸음 수 조절·몸 높이가 낮아지는 상황

캐릭터 애니메이션이나 게임의 이동 연출, 로딩 마스코트에 붙일 때는 groundV 를 캐릭터의 이동 속도로, STEP_HZ 를 진행 상황에 묶으면 된다. 걷는 캐릭터 gif 나 도트 걷는 애니메이션은 그려 둔 프레임을 되풀이하지만 이 코드는 STEP_HZ 를 바꾸는 즉시 다른 걸음이 나온다.

라이브러리 없이 만들기

캔버스는 내부 480×300 에 고정하고 devicePixelRatio 는 Math.min 으로 2 까지만 곱한다. 라이브러리·WebGL·CDN·외부 이미지가 없어 이 파일 하나를 정적 페이지에 붙이면 돌아간다. 캐릭터는 원과 둥근 막대로 그린 자체 도형이다.

방향 전환과 걸음 수 조절

속도는 STEP_HZ 로 바꾸고, 방향이 바뀔 때는 04 처럼 시선을 먼저 돌린다. 07 은 내리막에서 STEP_HZ 를 스스로 올리니, 지형이 걸음을 바꾸는 구조도 같은 식에서 나온다. 키 조작으로는 좌우 방향키가 STEP_HZ 를 0.4씩 0.8~2.8 사이에서 움직인다.

몸 높이가 낮아지는 상황

hipHeight 는 한 걸음 안에서 걸음 시작점의 80.10px 가 가장 낮다. 07 은 여기에 Math.cos(rad(slopeNow)) 를 곱해 경사에서 몸 높이를 더 줄인다.

이 데모 그대로 못 쓰는 곳

발은 한 평면 위 2링크 근사다. 지형 높낮이마다 발 위치를 다시 풀어야 하면 07 의 groundYAt 를 지형 함수로 바꾸고, 다리가 3링크 이상이거나 발목 관절이 있으면 코사인 법칙 한 번으로는 안 풀려 반복식이 필요하며, 걸음에서 서기로 멈추는 전환은 정속 사이클 밖이라 위상을 멈추는 규칙을 따로 정해야 한다.

커서 생물 편과 다른 점

커서 생물 편은 목표점을 쫓아 다리가 따라가고, 이 편은 걸음 시계가 위상을 밀어 다리가 정해진다. 프레임을 한 장씩 그려 넘기는 쪽이 궁금하면 플립북 편을 보면 된다.

걷기 애니메이션이 깨지는 함정 — 발바닥이 바닥에서 미끄러져 보이는 일·두 다리가 같은 위상으로 뛰는 일·경사에서 발이 땅에 박히는 일

발바닥이 바닥에서 미끄러진다

03 의 어긋난 판이 그 실물이다. 몸 속도 110.9px/s 가 식이 요구하는 135.2px/s 에 못 미치면 접지 발이 drift 만큼 기어가 바닥에 선이 그어진다. 이 소스는 접지 발을 화면 좌표에 얼리고 눈금만 groundV 로 흘리니, 월드가 움직이는 실제 장면에서는 접지 위치를 월드 좌표로 저장해 카메라로 옮겨야 한다.

두 다리가 같은 위상으로 뛴다

오른다리에 더하는 0.5 걸음이 없으면 01 의 두 곡선이 겹쳐 두 다리가 함께 접지하고 함께 뜬다. legPhase(right) 한 줄이 이 차이를 만든다.

경사에서 발이 땅에 박힌다

접지 발 높이를 평지 값으로 두면 경사 위에서 발이 땅속으로 들어가거나 떠 보인다. 07 은 접지 발의 y 를 groundYAt(x, BASE_Y, slope) 로, 발목 각을 -slopeNow 로 풀어 이 문제를 피한다.

이 글의 아홉 장면 소스 전부와 편집용 파일은 아래 상자의 zip 으로 받을 수 있고, 압축을 여는 비밀번호는 yts6py5y 이다.

걸음 속도와 보폭 각도와 다리 길이 비율을 바꾸면 느낌은 얼마나 달라질까 — STEP_HZ·STRIDE_DEG·LEG_RATIO 를 바꿔 본 차이

이름 바꾼 값 느낌
걸음을 더 빠르게 01 STEP_HZ 1.6 → 2.4 한 걸음이 0.63초에서 0.42초로, 접지가 0.37초에서 0.25초로 짧아지고 몸 이동 속도가 135.2px/s 에서 202.8px/s 로 늘어 달리는 듯한 성급함이 붙는다. 골반 비틀림은 ±6도에서 ±9도로, 호 최소높이는 20px 에서 13.3px 로 바뀐다
보폭을 더 좁게 03 STRIDE_DEG 28 → 14 한 걸음 거리가 84.5px 에서 43.5px 로 기본의 51.5% 가 되고 1.6Hz 에서 속도도 135.2px/s 에서 69.7px/s 로 내려가 종종걸음이 된다
정강이를 더 짧게 05 LEG_RATIO 0.48 → 0.42 정강이가 29.2px 에서 26.6px 로 짧아지고 넙다리가 60.8px 에서 63.4px 로 길어지며 05 의 팔꿈치 최대 접힘이 61.6도에서 57.4도로 줄어든다

한 값이 여러 장면에 번진다. STEP_HZ 는 06 의 골반 비틀림과 08 의 최소높이까지, STRIDE_DEG 는 05 의 어깨 각도와 groundV 까지, LEG_RATIO 는 무릎 위치와 05 의 팔꿈치까지 건드리니 한 번에 하나씩 바꿔 보는 편이 낫다.

움직임 줄이기에서는 걸음이 멈춘 선 자세 한 장 — 사이클 국면과 지금 걸음 수를 문장으로 읽어 주는 접근성

키보드로 걸음을 만진다

아홉 장면의 무대는 모두 role="application" 에 tabindex="0" 이다. 좌우 방향키는 STEP_HZ 를 0.4씩 0.8~2.8 로 움직이고, 03 에서는 STRIDE_DEG 를 10~44 에서 2씩, 05 에서는 위아래가 LEG_RATIO 를 0.40~0.56 에서 0.02씩, 09 에서는 좌우가 세 걸음꾼 속도를 ×1.15·×0.87 로 바꾼다. Enter·Space·P 는 일시정지, N 은 시간표의 다음 일 한 걸음, R 은 처음부터이며 손을 대면 시간표가 멈추고 계산만 흐른다.

낭독은 모드가 바뀔 때만

화면 상태 줄은 aria-live="off" 라 값이 바뀌어도 읽지 않는다. role="status" 칸에는 say() 가 손댄 직후와 장면 모드가 바뀔 때만 문장을 쓰고, 자동 시연이 흐르는 동안은 쓰지 않는다. 캔버스의 aria-label 에는 장면이 보이는 문구와 핵심 값을 적었다.

움직임 줄이기와 숨은 탭

prefers-reduced-motion 이 켜지면 루프를 돌리지 않고 showRest() 가 walkT 와 sceneT 를 REST_S 로 놓아 걸음이 멈춘 정지 한 장을 그린다. 입력 뒤에는 afterInput() 이 바로 다시 그리고, 무대에는 touch-action: none 이 걸려 있어 터치로 화면이 끌려가지 않는다.

    // 움직임 줄이기 — 루프 없이 시간표의 REST_S 초까지를 한 번에 치른 장면 한 컷. 캐릭터는 걸음 보간 없이 그 시점의 자리에 바로 선다
    function showRest() {
        stopLoop();
        lap = 0;
        begin();
        walkT = REST_S;
        while (evAt < script.length && script[evAt].t <= REST_S) {
            const ev = script[evAt];
            evAt += 1;
            sceneT = ev.t;
            ev.f();
        }
        sceneT = REST_S;
        update(0);
        showNow();
    }

걷기 애니메이션 FAQ — 걷는 애니메이션 프레임은 몇 장이 적당한가·발이 미끄러져 보일 때 고치는 값·걷기와 달리기를 가르는 기준

걷는 애니메이션 프레임은 몇 장이 적당한가요

이 편은 장수 대신 연속된 위상을 쓴다. 1.6Hz 에서 두 걸음 사이클이 1.25초라 60fps 로 75번 그려지고, 그림 몇 장으로 옮기려면 접지 12%·저항 18%·중간 15%·밀어내기 15%·들어올림 40% 라는 비율대로 한 걸음의 장수를 나누면 된다.

발이 미끄러져 보일 때 고치는 값은 무엇인가요

몸 속도를 2 * LEG_PX * sin(STRIDE_DEG) * STEP_HZ 에 맞춘다. 03 에서 135.2px/s 와 어긋난 0.82배 110.9px/s 를 나란히 놓으면 차이가 바로 보이고, 접지 구간의 발 좌표를 한 번 풀어 저장하면 몸 속도가 흔들려도 발은 제자리다.

걷기와 달리기는 무엇으로 가르나요

이 편의 CONTACT_SHARE 0.6 은 두 발이 같이 닿는 구간이 0.2걸음 남는 값이다. 두 다리 위상차가 0.5 라서 접지 몫이 0.5 아래로 내려가면 두 발이 모두 뜨는 순간이 생기는 산수가 되지만, 이 편에서는 0.6 하나만 돌려 봤고 0.5 아래는 확인하지 않았다.

출처: CanvasRenderingContext2D.lineTo·requestAnimationFrame·prefers-reduced-motion

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

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