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

스플래시 화면 9종 — 로딩에 묶인 로고 와이프부터 원형 열림과 헤더 인계까지

스플래시 화면은 로딩을 어떻게 로고와 숫자로 바꿀까 — 01번은 읽어 온 비율 하나로 로고 마스크 폭을 정하고, 02번은 최소 표시 시간과 로딩 완료 시각 중 큰 값에서 퇴장하며, 06번은 가장 먼 모서리까지의 거리로 원형 열림을 끝낸다.

스플래시 화면은 로딩을 가리는 그림이 아니라 읽어 온 비율과 시간표 두 값으로 로고·숫자·열림을 모두 계산하는 장치다 — 스플래시 화면 만들기는 어느 값이 화면을 움직이는지 정하는 데서 시작한다

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

아홉 장면은 같은 모형 하나 위에서 움직인다. 화면은 480×300 캔버스 하나이고, 읽어 온 비율은 시드로 정해지는 가상 리소스 표 makeLoad 에서 나오고(02 만 표 없이 방문 두 번의 고정 로딩 시간 VISIT_LOAD 를 쓴다), 시계는 장면 시계 sceneT 하나다. 로고와 숫자와 열림은 모두 이 두 값을 받아 계산하는 화면일 뿐이라, 실제 사이트에서는 비율 한 줄만 진짜 값으로 바꾸면 된다.

순서는 로딩 표시에서 시작해 장면 전환으로 넓어진다. 01·02·03 은 로고 마스크와 최소 표시 시간과 퍼센트 숫자를, 04·05·06 은 커튼과 글자와 원형 열림을, 07·08·09 는 링과 헤더 인계와 랜딩 인트로를 다룬다.

스플래시 화면 애니메이션 9종 — 로딩에 묶인 로고 와이프·최소 표시 시간 퇴장·따라가는 퍼센트 숫자·두 장 커튼·글자 스프링 워드마크·원형 열림·자원 무게 링·헤더로 날아가는 로고·랜딩 히어로 인트로

아홉 장면의 면적과 움직임

장면 누적면적 움직인 프레임 강도 평균
01 로고 와이프 6.0% 23/23 48.6
02 최소 표시 퇴장 22.8% 23/23 117.2
03 퍼센트 카운터 8.3% 23/23 105.2
04 두 장 커튼 30.9% 16/23 67.6
05 글자 스프링 14.1% 15/23 112.0
06 원형 열림 26.8% 6/23 63.3
07 링 구간 6.9% 23/23 81.8
08 로고 날기 31.1% 9/23 65.6
09 랜딩 인트로 11.1% 23/23 61.8

화면을 크게 덮는 08 이 31.1% 로 가장 넓고 04 가 30.9% 로 뒤따르며, 로고 하나가 채워지는 01 은 6.0% 로 가장 좁다. 강도 평균은 번쩍이는 02 가 117.2 로 가장 높고 01 이 48.6 으로 가장 낮다.

움직인 프레임이 23/23 인 장면은 01·02·03·07·09 다섯 개이고 04 는 16/23, 05 는 15/23 이다. 06 은 6/23, 08 은 9/23 에 그치는데, 열림이 0.9초에 끝나고 날기와 교차가 2.75초에서 3.75초 안에 끝난 뒤에는 본문이 멈춰 있어서다.

01로딩 비율에 묶여 로고가 채워지는 와이프

읽어 온 비율 LOAD_FRAC 하나가 마스크 폭 MASK_W(f) = f × (LOGO_S 74 + EDGE_SOFT 18) 가 되어, 비율이 멈추면 와이프도 선다. 가장자리 18 px 는 알파가 줄어드는 6 조각이고, 시간만으로 채운 오른쪽 판은 3.6초에 읽은 비율 0.303 인데 0.457 을 가리켜 최대 20.8점(2.90초)까지 어긋난다. 앱이나 사이트 첫 화면에서 로딩 진행을 로고로 보여 줄 때 쓴다.

로딩 비율에 맞춰 로고가 채워지는 와이프
새 창에서 열기

01 · 로딩 비율에 맞춰 로고가 채워지는 와이프 — 측정 누적면적 6.018% · 움직인 프레임 23/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

LOAD_FRACMASK_WEDGE_SOFT
    function build() {
        makeLoad(SEED_BASE + (lap % 3), 8, LOAD_START, LOAD_SPAN);
    }
    function update(dt) { if (dt < 0) {
        return;
    } }
    function LOAD_FRAC(t) { return loadFrac(t); }
    function TIME_FRAC(t) { return clamp((t - LOAD_START) / LOAD_SPAN, 0, 1); }
    function MASK_W(f) { return maskW(LOGO_S, EDGE_SOFT, f); }

02최소 표시 시간을 지키는 퇴장 시각

퇴장 시작 EXIT_START 는 로딩 완료 시각과 MIN_HOLD 0.6초 중 큰 값이고, 덮개는 EXIT_MS 450 ms 동안 사라진다. 로딩이 0.35초 걸리는 빠른 방문에서 왼쪽은 600 ms 에 나가지만 로딩이 끝나자마자 나가는 오른쪽은 350 ms 라 FLASH_MS 500 미만의 번쩍임이 되고, 2.4초 걸리는 느린 방문에서는 둘 다 2400 ms 다. 로딩이 빠를 때 깜빡임을 막는 규칙이다.

최소 표시 시간을 지키는 퇴장 시각
새 창에서 열기

02 · 최소 표시 시간을 지키는 퇴장 시각 — 측정 누적면적 22.769% · 움직인 프레임 23/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

MIN_HOLDEXIT_STARTEXIT_MS
    function visitOf(t) { return t >= VISIT_AT[1] ? 1 : 0; }
    // 방문 시작부터 잰 시각
    function localT(t) { return t - VISIT_AT[visitOf(t)]; }
    function LOAD_DONE(t) { return VISIT_LOAD[visitOf(t)]; }
    // 퇴장 시작 시각 — 왼쪽은 최소 표시를 지키고 오른쪽은 로딩이 끝나자마자
    function EXIT_START(t, keep) { return keep ? Math.max(LOAD_DONE(t), MIN_HOLD) : LOAD_DONE(t); }
    function splashAlpha(t, keep) { return 1 - ease((localT(t) - EXIT_START(t, keep)) / (EXIT_MS / 1000)); }

03퍼센트 숫자가 부드럽게 따라가는 카운터

진행률 숫자가 튀지 않아야 하는 로딩 화면에서는 목표 비율을 리소스가 통째로 도착한 KB ÷ 전체 KB 로 두고, 왼쪽 숫자가 disp += (목표 − disp) × (1 − e^(−5·dt)) 로 남은 차의 일부씩 따라가게 한다. CATCH_RATE 5 에서 도착 한 번의 95% 까지 0.60초 걸리고 왼쪽은 목표보다 최대 26점 뒤처진다. 목표를 그대로 찍는 오른쪽은 270 KB 짜리 마지막 덩어리가 도착하면 25점이 한 번에 뛰고 도착 사이 1.46초를 멈춘 듯 보낸다.

퍼센트 숫자가 부드럽게 따라가는 카운터
새 창에서 열기

03 · 퍼센트 숫자가 부드럽게 따라가는 카운터 — 측정 누적면적 8.297% · 움직인 프레임 23/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

CATCH_RATEDISPLAY_PCTARRIVED_KB
    function ARRIVED_KB(t) { return arrivedKb(t); }
    function target(t) { return ARRIVED_KB(t) / totalKb; }
    // 남은 차 × (1 − e^(−rate·dt)) 만큼 다가간다 — 목표가 그대로인 동안은 dt 를 잘게 나눠도 한 번에 가도 같은 값
    function update(dt) {
        const tg = target(sceneT);
        if (Math.abs(tg - lastTarget) > 1e-9) {
            lastTarget = tg;
            lastChange = sceneT;
        }
        disp += (tg - disp) * (1 - Math.exp(-CATCH_RATE * dt));
        while (nextSample <= sceneT) {
            hist.push([nextSample, target(nextSample), disp]);
            nextSample += SAMPLE_S;
        }
    }

04두 장 커튼이 갈라지며 본문이 드러나는 전환

커튼은 로딩이 끝나고 HOLD_S 0.3초 뒤에 열리며, 열린 폭 HALF_W = easeInOut(p) × 103 px 이라 OPEN_S 1.1초 동안 느리게 시작해 가운데에서 가장 빠르다. 왼쪽은 clip 으로 로고를 반쪽씩 잘라 양쪽으로 밀고 본문은 1.08 배에서 1 배로 가라앉으며, 오른쪽은 한 장이 올라가 로고가 통째로 따라간다. 사이트 첫 진입에서 로고 뒤로 본문을 여는 인트로에 쓴다.

두 장 커튼이 갈라지며 본문이 드러나는 전환
새 창에서 열기

04 · 두 장 커튼이 갈라지며 본문이 드러나는 전환 — 측정 누적면적 30.906% · 움직인 프레임 16/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

HALF_WHOLD_SOPEN_S
    function openP(t) { return clamp((t - (doneT + HOLD_S)) / OPEN_S, 0, 1); }
    // 한쪽 커튼이 밀려난 거리 = 열림 곡선 × 절반 폭
    function HALF_W(t) { return easeInOut(openP(t)) * (PW / 2); }
    function drawCover(px, py, cx, f, shift, c, cl, cr) {
        // 왼쪽 반 — clip 은 반쪽 사각형. 로고는 가운데에 그려 반쪽만 보인다
        const half = PW / 2;
        ctx.save();
        ctx.beginPath();
        ctx.rect(px - shift, py, half, PH);
        ctx.clip();
        box(px - shift, py, half + 1, PH, 0, mix(c, FACE, cl), 1);
        ctx.translate(-shift, 0);
        logoWipe(cx, py + PH / 2 - 6, 64, f, 14, LAV_DEEP, 1);
        ctx.restore();
        ctx.save();
        ctx.beginPath();
        ctx.rect(px + half + shift, py, half, PH);
        ctx.clip();
        box(px + half + shift - 1, py, half + 1, PH, 0, mix(c, FACE, cr), 1);
        ctx.translate(shift, 0);
        logoWipe(cx, py + PH / 2 - 6, 64, f, 14, LAV_DEEP, 1);
        ctx.restore();
    }

05글자가 차례로 솟는 스프링 워드마크

첫 글자는 읽어 온 비율이 15% 에 닿는 timeAtFrac(0.15)(첫 판 1.07초)에 출발하고 다음 글자는 STAGGER_MS 90 씩 늦다. 감쇠비 0.465 의 스프링은 목표를 19% 넘쳤다가 0.51초 뒤 2% 안에 서서 전체가 960 ms 에 끝나고, 오른쪽은 한꺼번에 500 ms 동안 넘침 없이 올라온다. 브랜드 이름을 글자 단위로 등장시키는 워드마크에 쓴다.

글자가 차례로 솟는 스프링 워드마크
새 창에서 열기

05 · 글자가 차례로 솟는 스프링 워드마크 — 측정 누적면적 14.11% · 움직인 프레임 15/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

STAGGER_MSSPRING_KDAMPING_C
    function build() {
        makeLoad(SEED + (lap % 3), 6, 0.3, 2.5);
        t0 = timeAtFrac(START_FRAC);
        // 스프링이 목표의 2% 안에 들어와 머무는 시각 — 0.01 초 간격으로 뒤에서 찾는다
        settleS = 0;
        for (let q = 2.0; q > 0; q -= 0.01) {
            if (Math.abs(1 - springStep(q, SPRING_K, DAMPING_C)) > 0.02) {
                settleS = q;
                break;
            }
        }
    }
    function update(dt) { if (dt < 0) {
        return;
    } }
    function letterStart(i) { return t0 + (i * STAGGER_MS) / 1000; }
    // 글자 i 의 올라온 정도 — 스프링은 1 을 한 번 넘는다
    function letterRise(i, t, spring) {
        const q = t - (spring ? letterStart(i) : t0);
        return spring ? springStep(q, SPRING_K, DAMPING_C) : ease(q / FLAT_S);
    }
    // 전체 길이(ms) — 마지막 글자의 출발 지연 + 스프링이 자리 잡는 시간
    function TOTAL_MS() { return (WORD.length - 1) * STAGGER_MS + Math.round(settleS * 1000); }

06로고 중심에서 퍼지는 원형 열림

필요한 반지름 MAX_RADIUS() 는 중심에서 네 모서리까지 Math.hypot 거리의 최댓값이다. 중심이 가운데가 아니라 왼쪽 아래 쪽(FX 0.3·FY 0.62)이라 거리가 111.7·171.6·84.1·155.1 px 로 갈려 최댓값이 171.6 px 이고, 반지름을 FIXED_RADIUS() 103 px 로 못박은 오른쪽은 세 모서리(111.7·171.6·155.1)가 덮인 채 남는다. 버튼이나 로고 위치에서 화면을 여는 원형 전환에 쓴다.

로고 중심에서 퍼지는 원형 열림
새 창에서 열기

06 · 로고 중심에서 퍼지는 원형 열림 — 측정 누적면적 26.758% · 움직인 프레임 6/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

EXIT_MSMAX_RADIUSFIXED_RADIUS
    function openP(t) { return clamp((t - OPEN_AT) / (EXIT_MS / 1000), 0, 1); }
    // 중심(fx, fy)에서 사각형 네 모서리까지의 거리 — 위왼·위오른·아래왼·아래오른
    function cornerDist(fx, fy) {
        const w = PW;
        const h = PH;
        return [Math.hypot(fx, fy), Math.hypot(w - fx, fy), Math.hypot(fx, h - fy), Math.hypot(w - fx, h - fy)];
    }
    function MAX_RADIUS() {
        const d = cornerDist(FX * PW, FY * PH);
        return Math.max(d[0], d[1], d[2], d[3]);
    }
    // 고정 반지름 실패판 — 화면 너비의 절반으로 못박는다
    function FIXED_RADIUS() { return PW / 2; }
    function radiusAt(t, full) { return ease(openP(t)) * (full ? MAX_RADIUS() : FIXED_RADIUS()); }

07자원 무게로 나뉜 링 구간 로딩

리소스 여덟 개가 링을 크기 비율로 나눠 가지며 ARC_BY_KB 가 조각 크기를 KB ÷ 전체 KB × (2π − 8 × SEG_GAP) 로 정하고, 각 조각은 그 리소스가 읽힌 비율만큼 찬다. 첫 판은 가장 큰 조각(293 KB)이 86.8도, 가장 작은 조각(34 KB)이 10.1도이며 가운데 숫자는 읽은 KB 비율이다. 같은 각도로 나눈 오른쪽은 조각 평균을 읽어 실제와 최대 6.2점 어긋나고, 여러 자원을 받는 앱의 진행 링에 쓴다.

자원 무게로 나뉜 링 구간 로딩
새 창에서 열기

07 · 자원 무게로 나뉜 링 구간 로딩 — 측정 누적면적 6.937% · 움직인 프레임 23/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

ARC_BY_KBSEG_GAPRING_W
    // 조각 i 의 시작 각도와 크기 — weighted 면 KB 비율, 아니면 같은 크기
    function ARC_BY_KB(i, weighted) {
        const room = TAU - SEG_GAP * RES_N;
        let a = -PI / 2;
        for (let k = 0; k < i; k += 1) {
            a += (weighted ? (res[k].kb / totalKb) * room : room / RES_N) + SEG_GAP;
        }
        return [a, weighted ? (res[i].kb / totalKb) * room : room / RES_N];
    }
    function meanFrac(t) {
        let s = 0;
        for (let i = 0; i < res.length; i += 1) {
            s += resFrac(res[i], t);
        }
        return s / res.length;
    }

08로고가 헤더 자리로 날아가는 인계

로딩이 끝나고 0.25초 뒤 큰 로고가 FLY_S 0.8초 동안 FLY_PATH 곡선을 따라 머리띠 자리로 날고, SCALE_LERP 가 같은 진행값으로 70 px 를 22 px 로 줄인다. 본문은 날기 0.4초 뒤부터 0.6초 동안 XFADE 로 해골 판에서 실제 내용으로 바뀌고, 오른쪽은 큰 로고가 사라진 0.35초 뒤에야 머리띠 로고가 나타나 그동안 로고가 없다. 스플래시에서 실제 화면으로 이어지는 공유 요소 전환에 쓴다.

로고가 헤더 자리로 날아가는 인계
새 창에서 열기

08 · 로고가 헤더 자리로 날아가는 인계 — 측정 누적면적 31.064% · 움직인 프레임 9/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

FLY_PATHSCALE_LERPXFADE
    function flyAt() { return doneT + 0.25; }
    function flyP(t) { return clamp((t - flyAt()) / FLY_S, 0, 1); }
    // 본문 교차 페이드 — 날기 시작 XF_AT 뒤부터 XF_S 동안 해골이 사라지고 내용이 나타난다
    function XFADE(t) { return ease((t - flyAt() - XF_AT) / XF_S); }
    // 시작점·끝점·제어점 — 제어점은 시작 x, 도착 y 라 먼저 위로 솟았다가 왼쪽으로 꺾인다
    function FLY_PATH(x0, y0, x1, y1, p) {
        const e = easeInOut(p);
        const cxp = x0;
        const cyp = y1;
        const a = (1 - e) * (1 - e);
        const b = 2 * (1 - e) * e;
        const c = e * e;
        return [a * x0 + b * cxp + c * x1, a * y0 + b * cyp + c * y1];
    }
    function SCALE_LERP(p) { return lerp(LOGO_BIG, LOGO_SMALL, easeInOut(p)); }

09랜딩 히어로로 이어지는 브랜드 인트로

로딩이 끝나고 0.25초 뒤 블라인드 여섯 줄이 BLIND_STAGGER 0.07초씩 늦게 BLIND_S 0.5초에 걸쳐 걷히고(첫 판 3.05초부터 3.90초까지), 로고는 읽어 온 비율로 채워졌다가 사라진다. 뒤의 히어로에서는 두 줄 제목이 clip 으로 솟고 단추가 springStep 스프링으로 튀며 구슬 여섯 개가 타원 궤도로 돈다. 랜딩 페이지 첫 화면 인트로에 쓴다.

랜딩 히어로로 이어지는 브랜드 인트로
새 창에서 열기

09 · 랜딩 히어로로 이어지는 브랜드 인트로 — 측정 누적면적 11.057% · 움직인 프레임 23/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

BLIND_OPENBLIND_STAGGERSPRING_K
    function build() { makeLoad(SEED + (lap % 3), 8, LOAD_START, LOAD_SPAN); }
    function update(dt) { if (dt < 0) {
        return;
    } }
    function blindStart(i) { return doneT + 0.25 + i * BLIND_STAGGER; }
    // 블라인드 i 의 열림 — 줄의 높이가 이 값만큼 줄어든다
    function BLIND_OPEN(i, t) { return ease((t - blindStart(i)) / BLIND_S); }
    function heroT() { return blindStart(BLIND_N - 1) + 0.15; }

스플래시 화면 디자인을 코드로 짜면 로고 하나와 숫자 하나는 어떤 값 몇 개로 정해질까?

진행값은 가상 리소스 표 하나에서 나온다

02 를 뺀 여덟 장면은 makeLoad(seed, n, start, spanS) 로 이 표를 만든다. 02 는 이 함수를 부르지 않고 VISIT_LOAD 0.35초·2.4초와 localT·LOAD_DONE 만 쓴다. makeLoad 는 시드로 리소스 n 개의 크기(24~300 KB)와 도착 구간을 정하고 spanS 초 안에 로딩이 끝나게 맞춘다. 읽어 온 비율은 loadFrac(t) = 읽은 KB ÷ 전체 KB 이고, 01 의 첫 판은 전체 1103 KB 를 0.4초부터 7.4초까지 받는다. 시드는 판마다 시드 + lap % 3 으로 바뀌어 세 가지 로딩이 돌아가며 나온다.

    function makeLoad(seed, n, start, spanS) {
        const rnd = seeded(seed);
        const raw = [];
        let sum = 0;
        let mx = 0;
        for (let i = 0; i < n; i += 1) {
            const kb = Math.round(24 + rnd() * 276);
            const a = rnd() * 0.5;
            const b = a + 0.12 + (kb / 300) * 0.38;
            raw.push({ kb: kb, t0: a, t1: b });
            sum += kb;
            mx = Math.max(mx, b);
        }
        res = raw.map(function (r) { return { kb: r.kb, t0: start + (r.t0 / mx) * spanS, t1: start + (r.t1 / mx) * spanS }; });
        totalKb = sum;
        doneT = start + spanS;
    }
    // 리소스 하나가 시각 t 에 얼마나 읽혔는지 0 에서 1

화면 연출은 그 값을 받아 계산한다

로고 마스크 폭(01)·숫자의 목표(03)·글자 출발 시각(05)·링 조각 각도(07)·블라인드 시작(09)이 전부 같은 표에서 갈라져 나온다(02 는 표 없이 고정 로딩 시간 두 개를 쓴다). 그래서 실제 사이트에서는 loadFrac 자리에 읽은 바이트 ÷ 전체 바이트를 넣으면 나머지 소스가 그대로 쓰인다. 데모는 네트워크를 부르지 않고, 난수는 시드 고정 생성기 하나가 만든다.

시계는 sceneT 하나다

Date 와 performance 와 Math.random 호출은 아홉 소스에 0건이고, 장면 시계가 쌓이는 것은 sceneT += dt 한 곳이다. 한 프레임은 DT_MAX 0.05초를 넘지 않게 잘려서 탭이 숨었다 돌아와도 시간이 한 번에 몰리지 않는다. 같은 시드의 시연은 몇 번을 돌려도 같은 값을 그린다.

읽어 온 비율과 경과 시간을 받아 최소 표시 시간을 지키고, 숫자를 따라가게 하고, 마스크 폭과 열림 반지름으로 바꾸는 — 스플래시 한 프레임이 계산되는 순서

한 프레임의 계산 순서

requestAnimationFrame 이 주는 시각 차를 DT_MAX 로 자르고, advance(dt) 가 sceneT 를 더한 뒤 LAP_S 를 넘으면 lap 을 올리고 begin() 으로 처음부터 시작한다. 그다음 update(dt) 가 항목의 상태를 고치고 draw() 가 캔버스를 그리며, 상태 줄과 진행 막대와 완료 안내가 이어서 갱신된다. 첫 판은 START_S 만큼 앞서 시작해서 연출이 이미 진행 중인 장면에서 처음 보인다.

읽어 온 비율이 f 에 닿는 시각을 구하는 이분 탐색

읽어 온 비율은 시간에 따라 줄어들지 않으므로, 05 의 timeAtFrac(f) 는 0 에서 완료 시각 사이를 40번 반으로 줄여 처음 f 에 닿는 시각을 찾는다. 이 한 값에서 첫 글자의 출발이 정해지고, 다음 글자는 STAGGER_MS 씩 늦게 이어진다.

    function timeAtFrac(f) {
        let lo = 0;
        let hi = doneT;
        for (let i = 0; i < 40; i += 1) {
            const mid = (lo + hi) / 2;
            if (loadFrac(mid) < f) {
                lo = mid;
            }
            else {
                hi = mid;
            }
        }
        return hi;
    }

스플래시 화면 ui 를 실제 사이트에 붙일 때 — 라이브러리 없이 만들기·로딩이 너무 빨라도 너무 느려도 어색한 이유·본문으로 넘기는 시점

앱 첫 화면과 웹앱 초기 로딩 — 어디에 붙이나

모바일 웹앱이 첫 화면을 받아 오는 동안에는 01 의 마스크 와이프와 03 의 퍼센트 카운터가 맞고, 이미지·글꼴·데이터를 따로 받는 화면에서는 07 의 링이 자원 크기만큼 조각을 나눈다. 브랜드 랜딩 페이지는 09 의 블라인드가 걷히며 히어로로 이어지는 인트로가 어울리고, 앱 설치 화면처럼 로고가 헤더로 올라가는 구조는 08 이 맞다. 스플래시 화면 만들기는 어느 화면에서 어느 장면이 필요한지부터 고르면 절반이 정해진다.

로딩이 너무 빨라도 너무 느려도 어색한 이유

로딩이 0.35초 만에 끝나면 로고가 350 ms 만 보이고 사라져 번쩍임이 되므로 02 처럼 최소 표시 시간을 둔다. 반대로 로딩이 느리면 03 의 오른쪽 판처럼 숫자가 1.46초 동안 서 있어 멈춘 듯 보이니, 목표를 따라가는 숫자로 바꿔 움직임을 이어 준다. 스플래시 화면 디자인은 이 두 극단 사이에서 로고가 머무는 시간을 정하는 일이다.

본문으로 넘기는 시점

스플래시를 끝내는 순간은 로딩 완료 시각에 0.25~0.3초의 여유를 더한 값이 기준이다. 04 는 HOLD_S 0.3초, 08 과 09 는 0.25초를 두고 나서 열리거나 날아가고, 06 은 OPEN_AT 3.2초에 열림을 시작한다. 페이지 전환 전체의 로딩 상태는 페이지 전환과 로딩 상태 에서 따로 다룬다.

라이브러리 없이 만들기

캔버스 2D 만으로 그려지고 아홉 소스에 라이브러리·WebGL·CDN 이 0건이라 복사한 파일 하나로 돈다. 로고 자체는 둥근 타일에 구멍 두 개를 낸 도형이라 logoPath 한 함수에 들어 있어 다른 도형으로 바꿔 끼울 수 있다. 로고 선 하나가 그려지는 쪽은 로고 로딩 애니메이션 이, 스피너와 막대 쪽은 CSS 로딩 화면 디자인 이, CSS 오프닝은 CSS 인트로 오프닝 애니메이션 이 이웃한다.

스플래시 화면이 깨지는 순간 — 로딩이 빨라 로고가 번쩍이고 사라지는 함정·숫자가 건너뛰어 멈춘 듯 보이는 진행률·고정 반지름이 모서리를 남기는 원형 열림

로딩이 빨라 로고가 번쩍이고 사라진다

02 의 빠른 방문은 로딩이 0.35초에 끝나서, 로딩이 끝나자마자 나가는 판은 350 ms 에 사라져 FLASH_MS 500 미만이 된다. 최소 표시 시간을 지키는 판은 600 ms 를 채워서 로고가 읽힌다. 같은 코드로 느린 방문(2.4초)에서는 두 판이 똑같이 2400 ms 라, 이 함정은 빠른 방문에서만 생긴다.

숫자가 건너뛰어 멈춘 듯 보이는 진행률

리소스가 통째로 도착할 때만 오르는 목표를 그대로 찍으면 03 의 오른쪽 판처럼 270 KB 덩어리 하나가 25점을 한 번에 올린다. 도착과 도착 사이는 가장 길게 1.46초(3.54에서 5.00초)를 같은 숫자로 서 있다. 왼쪽 판은 CATCH_RATE 5 로 따라가서 한 번 도착의 95% 에 0.60초 만에 닿는다.

고정 반지름이 모서리를 남기는 원형 열림

06 은 반지름을 화면 너비의 절반 103 px 로 못박으면 열림이 끝나도 세 모서리(111.7·171.6·155.1 px)가 덮인 채 남는다. 모서리가 둘이 아니라 셋인 까닭은 중심이 왼쪽 아래 쪽이라 거리가 네 갈래로 갈리고 103 px 이 가장 가까운 84.1 px 한 곳만 넘기 때문이다. 해결은 MAX_RADIUS() 로 가장 먼 모서리까지의 거리 171.6 px 를 쓰는 것이다.

세 가지 로딩 시드로 도는 아홉 장면의 HTML 과 React 소스는 아래 zip 에 묶여 있으며 압축은 du9w8544 으로 푼다.

최소 표시 시간과 글자 간격과 열림 속도를 바꾸면 느낌은 얼마나 달라질까 — MIN_HOLD·STAGGER_MS·EXIT_MS 를 바꿔 본 차이

이름 바꾼 값 느낌
최소 표시 시간을 늘리기 02 MIN_HOLD 0.6 → 1.4 빠른 방문에서 왼쪽 퇴장 시작이 600 ms 에서 1400 ms 로 늦어져 로고가 800 ms 더 머물고, 오른쪽의 350 ms 번쩍임은 그대로다. 로딩이 2.4초 걸리는 느린 방문은 2400 ms 로 같아 값이 바뀌지 않는다.
글자 간격을 좁히기 05 STAGGER_MS 90 → 30 전체 길이가 960 ms 에서 660 ms 로 줄지만(마지막 글자 출발 450 ms 에서 150 ms), 스프링이 자리 잡는 510 ms 는 그대로라 글자가 거의 겹쳐 한 덩어리로 솟는다.
열림을 더 빠르게 06 EXIT_MS 900 → 450 반지름이 171.6 px 까지 자라는 평균 속도가 191 px/s 에서 381 px/s 로(최고 속도는 286 에서 572 px/s) 두 배가 되어 열림이 짧아지지만 본문이 드러나는 순간을 눈이 따라가기 어렵다.

값을 바꾸기 전에 볼 것

이 세 값은 각각 02·05·06 의 상수라서 장면 하나에서 값 하나만 바꿔야 비교가 맞는다. 02 는 빠른 방문의 600 ms 와 350 ms 를, 05 는 첫 판의 960 ms 를, 06 은 첫 판의 171.6 px 를 기준으로 본다. lap 이 달라지면 시드가 바뀌어 로딩 시간표도 달라지므로 첫 판에서 비교해야 위 수치와 맞는다.

움직임 줄이기에서는 멈춘 한 장의 로고 화면 — 장면 이름과 현재 진행률을 문장으로 읽어 주는 접근성

Enter·Space·R 로 다시 시작하고 P·N 으로 다룬다

무대 상자는 아홉 장면 모두 role="application" 이고 Enter·Space·R·클릭이 처음부터 다시 시작, P 가 일시정지, N 이 한 걸음이다. 아홉 장면의 시간표가 비어 있어 N 도 처음부터 다시 시작하는 동작이 된다. 시연은 조작 없이 끝까지 자동으로 돈다.

진행 막대와 상태 줄과 완료 안내

진행값은 role="progressbar" 요소의 aria-valuenow 로 0 에서 100 까지 읽히고, 값이 바뀔 때만 갱신된다. 상태 줄 data-role="status" 의 문장은 항목마다 달라서 02 는 퇴장 ms, 03 은 목표와 표시 퍼센트, 06 은 반지름과 필요 px, 07 은 읽은 KB 를 읽어 준다. 이 줄은 시연 중 aria-live="off" 라 매 프레임이 읽히지 않으며 사람이 처음 손대면 polite 로 바뀐다. 완료 안내는 data-role="done"(role="status") 에 RESULT_S 초가 되면 한 번만 쓴다.

움직임 줄이기와 숨은 탭

prefers-reduced-motion 이 켜져 있으면 루프를 돌리지 않고 REST_S 초 지점의 멈춘 한 장을 showRest() 로 그리며, 입력해도 바로 그 장을 다시 그린다. 02·04·06·08·09 는 연출이 끝난 본문 화면이고 01·03·05·07 은 로딩이 끝나 값이 다 찬 화면이다. 아홉 장면 모두 visibilitychange 로 탭이 숨으면 루프를 멈추고 돌아오면 이어 간다.

스플래시 화면 FAQ — 라이브러리 없이 만들기·로딩이 너무 빠를 때 어떻게 할지·웹에 올릴 때 필요한 것

스플래시 화면은 라이브러리 없이 만들 수 있나요

만들 수 있다. 캔버스 2D 와 requestAnimationFrame 만으로 아홉 장면이 돌아가고 소스에 라이브러리·WebGL·CDN 이 0건이다. 애니메이션 쪽 설명은 MDN 의 requestAnimationFrame 문서와 반지름으로 자르는 clip-path 문서를 본다.

로딩이 너무 빠르면 스플래시를 어떻게 하나요

02 처럼 최소 표시 시간을 둔다. 퇴장 시작을 로딩 완료와 MIN_HOLD 중 큰 값으로 하면 빠른 방문에서도 로고가 600 ms 머물고, 느린 방문은 기다린 2400 ms 그대로 간다. 값을 1.4초로 올리면 빠른 방문은 1400 ms 까지 늘어나지만 느린 방문은 변하지 않는다.

웹에 올릴 때 필요한 것은 무엇인가요

진짜 읽은 바이트와 전체 바이트에서 loadFrac 자리의 값을 만들고, 로딩이 끝나는 시점을 알려 주는 한 줄을 연결하면 된다. 움직임 줄이기와 탭 숨김 처리는 소스에 이미 들어 있어 그대로 두면 되고, 탭이 숨는 동작은 Page Visibility API 문서가, 움직임 설정은 prefers-reduced-motion 문서가 설명한다.

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

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