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

라인 드로잉 9종 — 점 순서와 선 길이를 계산해 한 줄로 그리는 캔버스

라인 드로잉은 펜을 한 번도 떼지 않은 선 하나로 얼굴·꽃·글자를 그려 내는 그림이다. 가장 가까운 점부터 이은 선은 제 몸을 몇 번이고 가로지른다.

사진을 한 줄 그림으로 바꾸는 라인 드로잉 변환은 무엇을 계산하는가?

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

그림 한 장을 선 하나로 옮기려면 세 가지를 차례로 정해야 한다. 선을 어디에 많이 보낼지, 그 자리들을 어떤 순서로 지날지, 그 순서를 얼마나 빨리 그을지다. 아홉 장면은 이 셋 가운데 하나를 깊게 파거나 여럿을 이어 붙인다.

정하는 것 맡은 장면 하는 계산 이 편에서 잰 값
어디에 01·02·03 칸의 어둡기 0~1 만큼 흔들림 폭·꺾임 수·점 수를 늘린다 01 흔들림 최대 2.43px · 02 줄마다 꺾임 10~61번 · 03 점 1,800개
어떤 순서로 04·07·08·09 가장 가까운 점부터 잇고 엇갈림을 뒤집거나, 꼭짓점의 선 수를 센다 04 4,080 → 3,864px · 07 봉투는 홀수 점 4개라 불가
얼마나 빨리 05·06·08·09 누적 길이 표로 같은 빠르기를 내거나, 꺾임으로 빠르기와 굵기를 정한다 05 초당 240px · 06 초당 70~492px · 08 초당 620px

원본 그림은 사진이 아니라 코드로 만든 도형

01 의 얼굴, 02 의 김 나는 커피잔, 03 의 고양이 얼굴, 09 의 꽃은 모두 타원·선분·삼각형까지의 거리를 겹쳐 칸마다 어둡기 0~1 을 돌려주는 함수다. 외부 사진이나 이미지 파일은 하나도 쓰지 않고, 08 만 글자를 화면 밖 캔버스에 그려 그 픽셀을 읽는다. 01·03 이 무게로 쓰는 값은 밝기가 아니라 어둡기라서, 검은 칸일수록 흔들림이 커지고 점이 몰린다. 자기 사진을 쓰려면 이 어둡기 함수 자리에 픽셀을 읽는 함수를 넣어야 하는데, 그 길은 아래 FAQ 에서 다룬다.

이런 화면에 붙인다

포트폴리오 첫 화면의 자기소개 칸에는 01 의 나선 얼굴이나 09 의 꽃이, 브랜드 첫 화면 아래 대표 서명 자리에는 05 의 같은 빠르기 펜이 맞다. 청첩장이나 초대장 첫 장에는 06 의 두 겹 하트를, 배송 경로를 고객에게 설명하는 페이지에는 04 의 풀려 가는 경로를 놓을 수 있다. 완성된 그림 파일을 모아 둔 라인 드로잉 사이트와 달리, 이 편의 장면은 그림이 그려지는 과정 자체를 화면에 보여 준다.

펜을 한 번도 떼지 않은 그림이 눈길을 붙잡는 이유 — 점의 순서와 선의 길이가 만드는 윤곽

선이 하나뿐이면 점을 지나는 순서가 곧 그림이다. 같은 점 420개라도 순서가 나쁘면 선이 그림을 가로질러 엉키고, 순서를 고치면 같은 점에서 윤곽이 드러난다. 다 그려진 라인 드로잉 일러스트 한 장보다 그려지는 장면이 눈을 오래 붙잡는 것도, 보는 사람이 펜끝을 따라가며 다음 획이 어디로 갈지 기다리게 되기 때문으로 보인다.

가장 가까운 점부터 이으면 엇갈리는 까닭

04 는 창고를 포함한 배송지 420곳을 출발점에서 가장 가까운 곳부터 차례로 잇는다. 이 방식은 87,990번 거리를 비교해 4,080px 짜리 첫 줄을 만들지만, 그 줄에는 서로 엇갈린 구간 쌍이 34개 있다. 가까운 곳만 고르다 보면 빠뜨린 곳이 뒤에 남고, 나중에 그곳으로 돌아가는 길이 이미 그은 선을 가로지르는 것으로 보인다. 엇갈린 쌍을 32번 뒤집고 나면 길이는 3,864px(−5.3%)로 줄고 엇갈림은 0 이 된다. 09 의 꽃도 같은 계산으로 엇갈림 74개를 61번 뒤집어 4,094px 를 3,826px(−6.6%)로 줄인다.

선 길이가 곧 그리는 시간

한 줄 그림은 길이를 시간으로 바꾸는 순간 애니메이션이 된다. 01 의 나선은 11,160px 를 4.6초에, 02 의 지그재그는 13,688px 를 5초에 그어 초당 2,400px 를 넘는 빠르기로 화면을 채운다. 반대로 05 의 서명은 1,312.6px 를 5.47초에, 06 의 하트는 1,742.1px 를 7.45초에 그어 펜끝을 눈으로 따라갈 수 있다. 빠른 펜은 그림을 채우는 쪽, 느린 펜은 손으로 쓰는 느낌을 내는 쪽에 쓰인다.

라인 드로잉 9종 — 밝기 나선·지그재그 음영·고르게 펴는 점·엉킨 줄 풀기·같은 빠르기 펜·손 떨림 굵기·한붓그리기 도안 판정·한 줄 필기체·한 줄로 그려지는 첫 화면

01·02 는 정해 둔 길(나선·가로줄) 위에서 선을 흔들어 음영을 낸다. 03·04 는 점을 놓는 자리와 지나는 순서를, 05·06 은 펜의 빠르기와 굵기를 맡고, 07·08 은 도형과 글자를 한 번에 그릴 수 있는지 따진다. 09 는 03·04·05·06 의 계산을 꽃 한 송이에 이어 붙인 첫 화면이다. 아홉 장면 모두 캔버스 2D 하나에 그리고, 외부 라이브러리·WebGL·이미지 파일이 없으며, 시드를 고정한 난수라 열 때마다 같은 그림이 나온다. 사람이 누르는 것은 일시정지 버튼 하나뿐이고 그림은 저절로 그려진다.

480×300 판을 2초 동안 찍어 잰 선의 움직임

24장을 찍어 이웃한 장끼리 비교한 23번 가운데 화면이 바뀐 횟수와, 한 번이라도 바뀐 면적의 몫이다. 색면이 번지는 09 가 35.0% 로 가장 넓고, 펜끝을 따라 선 한 줄과 그 아래 띠만 늘어나는 05·08 은 4~6% 에 머문다.

장면 누적면적 움직인 프레임 강도 평균
01 밝기 나선 23.1% 23/23 92.9
02 지그재그 음영 21.4% 21/23 123.7
03 점 찍기 12.4% 14/23 40.1
04 배송 경로 풀기 18.4% 20/23 74.4
05 같은 빠르기 펜 4.4% 23/23 73.1
06 볼펜 하트 23.4% 23/23 84.1
07 한붓그리기 판정 11.5% 23/23 84.9
08 한 줄 필기체 5.7% 19/23 81.5
09 꽃 첫 화면 35.0% 19/23 79.4

03 이 14/23 에 그친 것은 0.22초마다 한 번 옮긴 점이 다음 옮기기 전에 새 자리에 멈춰 서는 구간 때문으로 보인다. 04 의 20/23 과 08·09 의 19/23 도 단계가 바뀌는 사이 판이 잠깐 멈추는 몫으로 보인다.

이웃 편과 겹치지 않는 부분

로고 윤곽을 따라 선을 긋는 장면은 로고 로딩 애니메이션이, 어둡기를 표본으로 실을 거는 계산은 스트링 아트가, 글자 픽셀을 점으로 읽는 법은 글자 픽셀 샘플링이, 손으로 쓴 서명의 빠르기와 굵기는 서명 캔버스가 다룬다. 이 편은 그 위에서 점의 순서를 고치고 선 길이를 시간으로 바꾸는 쪽에 무게를 둔다. 09 처럼 선 아래로 색이 번지는 표현을 더 깊이 보려면 수채화 효과로 이어진다.

01어두운 만큼 흔들려 그림이 떠오르는 한 줄 나선

가운데에서 6px 간격으로 감기는 나선을 1.2px 걸음으로 따라가며, 지나는 자리가 어두울수록 나선에 수직인 쪽으로 크게 흔들고 물결도 촘촘하게(한 물결이 밝은 곳 62.83px, 가장 어두운 곳 5.71px) 바꿔 얼굴이 떠오르게 한다. 24.33바퀴·꼭짓점 9,301개·길이 11,160px 의 나선을 초당 2,426.1px 로 4.6초에 다 긋고, 가장 어두운 곳에서는 흔들림 2.43px 두 겹과 선 굵기 2px 가 간격 6px 보다 0.86px 넘쳐 이웃 바퀴가 맞닿아 검게 뭉친다. 포트폴리오 첫 화면에서 프로필 사진 자리에 놓을 얼굴 그림으로 쓸 수 있다.

어두운 만큼 흔들려 그림이 떠오르는 한 줄 나선
새 창에서 열기

01 · 어두운 만큼 흔들려 그림이 떠오르는 한 줄 나선 — 측정 누적면적 23.066% · 움직인 프레임 23/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

SPIRAL_GAPWOBBLE_AMPWOBBLE_FREQ_DARK
        while (a * theta < SPIRAL_RADIUS) {
            const r = a * theta;
            const c = Math.cos(theta);
            const s = Math.sin(theta);
            const bx = CX + r * c;
            const by = CY + r * s;
            // 접선 = d/dθ (r cosθ, r sinθ) = (a cosθ − r sinθ, a sinθ + r cosθ), 법선은 접선을 90° 돌린 것
            const tx = a * c - r * s;
            const ty = a * s + r * c;
            const tl = Math.sqrt(tx * tx + ty * ty);
            const nx = -ty / tl;
            const ny = tx / tl;
            dSmooth = lerp(dSmooth, dark(bx, by), SAMPLE_STEP / DARK_SMOOTH);
            phase += lerp(WOBBLE_FREQ_LIGHT, WOBBLE_FREQ_DARK, dSmooth) * SAMPLE_STEP;
            const off = WOBBLE_AMP * dSmooth * Math.sin(phase);
            maxOffset = Math.max(maxOffset, Math.abs(off));
            xs.push(bx + nx * off);
            ys.push(by + ny * off);
            // 호 길이 ds = √(r² + a²)·dθ 이므로 같은 걸음을 가려면 dθ = ds / √(r² + a²)
            theta += SAMPLE_STEP / Math.sqrt(r * r + a * a);
        }
        return { xs: Float32Array.from(xs), ys: Float32Array.from(ys), turns: theta / TAU, maxOffset: maxOffset };

02어두운 곳에서 촘촘해지는 지그재그 한 줄 음영

가로줄 27개를 10px 간격으로 왼쪽·오른쪽 번갈아 1px 씩 지나며, 지나는 자리가 어두울수록 위아래 폭(최대 4.4px)과 꺾이는 횟수를 늘리고, 줄 끝은 반지름 5px 반원으로 이어 펜을 떼지 않는다. 꼭짓점 12,137개·길이 13,688px 의 한 줄을 초당 2,738px 로 5초에 칠하는데 줄마다 꺾임이 10~61번(모두 806번)이고, 그림이 없는 줄도 바탕 어둡기 0.03 때문에 10번은 꺾여 완전히 곧지는 않다. 카페 소개 페이지 머리에 김 나는 커피잔을 판화처럼 새겨 둘 때 어울린다.

어두운 곳에서 촘촘해지는 지그재그 한 줄 음영
새 창에서 열기

02 · 어두운 곳에서 촘촘해지는 지그재그 한 줄 음영 — 측정 누적면적 21.386% · 움직인 프레임 21/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

ROW_GAPZIG_AMPZIG_FREQ_DARK
        for (let ry = ROW_TOP; ry <= H - ROW_TOP; ry += ROW_GAP) {
            const dir = rows % 2 === 0 ? 1 : -1;
            let phase = 0;
            for (let k = 0; k <= ROW_RIGHT - ROW_LEFT; k += 1) {
                const x = dir > 0 ? ROW_LEFT + k : ROW_RIGHT - k;
                const d = dark(x, ry);
                phase += lerp(ZIG_FREQ_LIGHT, ZIG_FREQ_DARK, d);
                xs.push(x);
                ys.push(ry + ZIG_AMP * d * triangleWave(phase));
            }
            // 꺾임 수 — 삼각파는 위상 π 마다 한 번 방향을 바꾼다
            turnsPerRow.push(Math.floor(phase / Math.PI));
            rows += 1;
            if (ry + ROW_GAP > H - ROW_TOP) {
                break;
            }
            // 줄 끝 반원 — 지금 줄 끝에서 다음 줄 끝까지, 바깥쪽으로 부푼 반지름 ROW_GAP/2
            const ex = dir > 0 ? ROW_RIGHT : ROW_LEFT;
            const cy = ry + ROW_GAP / 2;
            for (let j = 1; j < TURN_STEPS; j += 1) {
                const a = -Math.PI / 2 + Math.PI * j / TURN_STEPS;
                xs.push(ex + dir * Math.cos(a) * ROW_GAP / 2);
                ys.push(cy + Math.sin(a) * ROW_GAP / 2);
            }
        }

03어둡기 무게중심으로 옮겨 고르게 펴지는 점 찍기

어두운 칸일수록 받아들일 확률을 높여(칸 무게의 1.4제곱) 점 1,800개를 뿌린 뒤, 0.25초부터 0.22초마다 14번 점마다 자기가 가장 가까운 점인 칸들을 모아 어둡기를 무게로 한 중심으로 옮기고, 초당 30px 넘게 미끄러지는 점은 산호색으로 칠한다. 가장 가까운 이웃까지 거리는 평균 1.88px·흩어짐(표준편차 ÷ 평균) 0.68 에서 2.88px·0.50 으로 고르게 펴지고, 한 번 옮길 때 거리 비교는 칸마다 점 전부를 훑는 5,731,200번 대신 12px 바구니 덕에 218,776번이다. 반려동물 용품 가게 소개 페이지의 고양이 얼굴처럼 점묘 판화 느낌이 필요한 곳에 쓴다.

어둡기 무게중심으로 옮겨 고르게 펴지는 점 찍기
새 창에서 열기

03 · 어둡기 무게중심으로 옮겨 고르게 펴지는 점 찍기 — 측정 누적면적 12.4% · 움직인 프레임 14/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

STIPPLE_COUNTRELAX_ROUNDSMAX_DOTS
        let moved = 0;
        for (let i = 0; i < dots.n; i += 1) {
            // 맡은 칸이 없는 점은 제자리 — 0 으로 나누지 않는다
            toX[i] = sumW[i] > 0 ? sumX[i] / sumW[i] : dots.xs[i];
            toY[i] = sumW[i] > 0 ? sumY[i] / sumW[i] : dots.ys[i];
            moved += dist2(toX[i], toY[i], dots.xs[i], dots.ys[i]);
        }
        return moved / Math.max(1, dots.n);

04엇갈린 두 구간을 뒤집어 풀어 가는 한 줄 경로

창고를 포함한 배송지 420곳을 가장 가까운 곳부터 1.4초 동안 이어 긋고, 1.55초부터 프레임마다 구간 쌍 1,200개씩 견주어 엇갈린 두 구간이 나오면 그 사이 순서를 거꾸로 뒤집으며 뒤집힌 자리를 산호색 띠로 번쩍인다. 엇갈린 쌍 34개를 32번 뒤집어 엇갈림 0 이 되기까지 261,459쌍을 견주고, 60Hz 에서 218프레임 뒤인 5.18초에 상태 줄 앞머리가 「엇갈림 푸는 중」 에서 「다 풀림」 으로 바뀐다. 꽃 배달이나 정기 배송 업체가 하루 도는 순서를 고객에게 설명하는 페이지에 맞는다.

엇갈린 두 구간을 뒤집어 풀어 가는 한 줄 경로
새 창에서 열기

04 · 엇갈린 두 구간을 뒤집어 풀어 가는 한 줄 경로 — 측정 누적면적 18.442% · 움직인 프레임 20/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

STOP_COUNTTRIES_PER_FRAMEGREEDY_TIME
    function segmentsCross(ax, ay, bx, by, cx, cy, dx, dy) {
        const d1 = cross3(ax, ay, bx, by, cx, cy);
        const d2 = cross3(ax, ay, bx, by, dx, dy);
        const d3 = cross3(cx, cy, dx, dy, ax, ay);
        const d4 = cross3(cx, cy, dx, dy, bx, by);
        return ((d1 > 0 && d2 < 0) || (d1 < 0 && d2 > 0)) && ((d3 > 0 && d4 < 0) || (d3 < 0 && d4 > 0));
    }
            if (segmentsCross(xs[a], ys[a], xs[b], ys[b], xs[c], ys[c], xs[d], ys[d])) {
                // i+1 부터 j 까지 순서를 거꾸로 — a→c, b→d 로 다시 이어져 엇갈림이 사라지고 길이가 줄어든다
                let lo = u.i + 1;
                let hi = u.j;
                u.lastLo = lo;
                u.lastHi = hi;
                while (lo < hi) {
                    const tmp = o[lo];
                    o[lo] = o[hi];
                    o[hi] = tmp;
                    lo += 1;
                    hi -= 1;
                }
                u.flips += 1;
                u.flipsThisPass += 1;
                flipped += 1;
            }

05굽은 곳에서도 같은 빠르기로 그려 나가는 펜

서명 점 39개(고리 넷과 밑줄)를 앞뒤 이웃 점으로 기울기를 정한 3차 곡선 38조각으로 잇고, 조각마다 16마디로 쪼개 누적 길이 609칸 표를 만든 뒤 「지난 시간 × 초당 240px」 길이에 해당하는 자리를 찾아 1,312.6px 를 5.47초에 같은 빠르기로 쓴다. 0.12초마다 찍는 라벤더 점은 호 길이로 28.8px 마다 45개 놓이는데 곧은 거리로는 3.4~28.8px 라 고리 꼭대기에서 좁아지고, 선 아래 굵은 띠는 길이 표 없이 조각마다 같은 시간을 줬다면 느렸을 곳을 하늘색, 빨랐을 곳을 산호색으로 칠한다. 브랜드 첫 화면 아래 대표 서명이 써지는 장면처럼 펜이 들쭉날쭉하면 바로 티가 나는 곳에 맞다.

굽은 곳에서도 같은 빠르기로 그려 나가는 펜
새 창에서 열기

05 · 굽은 곳에서도 같은 빠르기로 그려 나가는 펜 — 측정 누적면적 4.36% · 움직인 프레임 23/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

PEN_SPEEDTICK_EVERYCURVE_STEPS
    function indexAtLength(c, s) {
        if (s <= 0) {
            return 0;
        }
        if (s >= c.total) {
            return c.lens.length - 1;
        }
        let lo = 0;
        let hi = c.lens.length - 1;
        while (hi - lo > 1) {
            const mid = (lo + hi) >> 1;
            if (c.lens[mid] < s) {
                lo = mid;
            }
            else {
                hi = mid;
            }
        }
        return lo + (s - c.lens[lo]) / Math.max(1e-9, c.lens[hi] - c.lens[lo]);
    }

06손 떨림이 섞이고 빠르기로 굵기가 바뀌는 볼펜 선

하트 40점과 그 점들을 안쪽으로 7px 민 두 번째 하트를 한 걸음으로 이어 한 줄을 만들고, 곡선이 꺾이는 정도로 펜 빠르기를 초당 70~492px 사이에서 정해 빠른 곳은 가늘게, 모서리는 굵게(2.42~4.96px) 칠하며 법선 쪽으로 두 겹 노이즈 떨림(세기 1.2)을 섞는다. 길이 1,742.1px 를 7.45초에 그리는 동안 하트 안 분홍 펠트가 불투명도 0.55 까지 차오르고, 펜이 모서리에서 초당 70px 까지 느려지므로 같은 빠르기로 가는 05 와 달리 굽은 곳에서 잉크가 고인 듯 굵어진다. 청첩장이나 돌잔치 초대장 첫 장에서 바느질로 박은 듯한 두 겹 하트로 쓸 수 있다.

손 떨림이 섞이고 빠르기로 굵기가 바뀌는 볼펜 선
새 창에서 열기

06 · 손 떨림이 섞이고 빠르기로 굵기가 바뀌는 볼펜 선 — 측정 누적면적 23.39% · 움직인 프레임 23/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

TREMOR_AMPWIDTH_KCORNER_SLOW
            let peak = 0;
            for (let j = lo; j < n && c.lens[j] - c.lens[i] <= CORNER_LOOK; j += 1) {
                peak = Math.max(peak, bend[j]);
            }
            const v = Math.max(PEN_SLOWEST, PEN_FAST / (1 + CORNER_SLOW * peak));
            speeds[i] = v;
            widths[i] = PEN_WIDTH / (1 + WIDTH_K * v);
            if (i > 0) {
                times[i] = times[i - 1] + (c.lens[i] - c.lens[i - 1]) / ((v + speeds[i - 1]) / 2);
            }

07홀수 갈래를 세어 한 번에 그릴 순서를 찾는 한붓그리기 도안

도안 넷을 차례로 넘기며 0.45초 동안 꼭짓점마다 이어진 선 수를 띄우고 홀수인 점에 산호색 원을 두른 뒤, 홀수 점이 0개나 2개면 찾은 순서대로 초당 520px 로 파스텔 띠를 긋고 선마다 순서 번호를 남긴다. 집(홀수 2개)은 0번에서 출발해 1번에서 끝나는 열 획을 2.35초에, 나비 리본(홀수 0개)은 고리 하나를 끼워 넣은 여섯 획을 1.79초에, 별은 열 획을 3.27초에 긋고, 봉투는 홀수 점이 넷이라 1.8초 동안 엑스 도장을 찍는다. 어린이 학습지나 두뇌 퍼즐 페이지에서 그릴 수 있는지와 그 순서를 정답처럼 보여 줄 때 쓴다.

홀수 갈래를 세어 한 번에 그릴 순서를 찾는 한붓그리기 도안
새 창에서 열기

07 · 홀수 갈래를 세어 한 번에 그릴 순서를 찾는 한붓그리기 도안 — 측정 누적면적 11.544% · 움직인 프레임 23/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

COUNT_TIMETRACE_SPEEDIMPOSSIBLE_TIME
        const deg = degrees(f);
        const odd = [];
        for (let i = 0; i < deg.length; i += 1) {
            if (deg[i] % 2 === 1) {
                odd.push(i);
            }
        }
        if (odd.length > 2) {
            return { possible: false, odd: odd, order: [], splices: 0 };
        }
            if (next >= 0) {
                // 되돌아오다가 다시 걷기 시작했다 = 남은 고리 하나를 끼워 넣는다
                if (backing) {
                    splices += 1;
                    backing = false;
                }
                stack.push(next);
            }
            else {
                out.push(stack.pop());
                backing = true;
            }

08글자 사이를 이어 한 줄로 써지는 필기체 이름

단어 atelier 를 기울인 굵은 104px serif 로 글자마다 화면 밖 캔버스에 그려 3px 칸의 알파를 읽고, 바깥과 맞닿은 안쪽 칸만 윤곽 점으로 남긴 뒤 글자마다 왼쪽 아래 점에서 가장 가까운 점으로 이어 붙인다. 글자 사이에는 앞 글자 끝에서 6px 오른쪽, 다음 글자 첫 점에서 6px 왼쪽에 기준선보다 14px 내려간 두 점을 끼워 이음 획 6개를 만들고, 점 280개를 지나는 2,299px 곡선을 초당 620px 로 3.71초에 쓰는데, 윤곽 칸 535개는 렌더한 기기의 serif 글꼴에서 잰 값이라 글꼴이 다른 기기에서는 달라진다. 공방이나 작업실 이름을 첫 화면에 손으로 써 내려가듯 보여 줄 때 단어만 바꿔 쓴다.

글자 사이를 이어 한 줄로 써지는 필기체 이름
새 창에서 열기

08 · 글자 사이를 이어 한 줄로 써지는 필기체 이름 — 측정 누적면적 5.701% · 움직인 프레임 19/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

GLYPH_CELLBASELINE_DIPWRITE_SPEED
        for (let k = 0; k < WORD.length; k += 1) {
            const x = left + m.measureText(WORD.slice(0, k)).width;
            lefts.push(x);
            const g = glyphOutline(WORD[k], x);
            cells += g.cells;
            const path = glyphPath(g);
            if (path[0].length === 0) {
                continue;
            }
            if (px.length > 0) {
                // 앞 글자 끝 → 기준선 아래로 내려가 → 다음 글자 첫 점 아래 → 첫 점. 필기체의 이음 획
                px.push(px[px.length - 1] + 6, path[0][0] - 6);
                py.push(BASE_Y + BASELINE_DIP, BASE_Y + BASELINE_DIP);
                joins += 1;
            }
            starts.push(px.length * CURVE_STEPS);
            for (let i = 0; i < path[0].length; i += 1) {
                px.push(path[0][i]);
                py.push(path[1][i]);
            }
        }

09한 줄로 그려진 뒤 색이 번져 드는 꽃 첫 화면

코드로 만든 꽃 그림에 점 700개를 뿌려 여섯 번 고르게 펴고(흩어짐 0.67 → 0.38), 줄기 맨 아래에서 가장 가까운 점부터 이은 순서의 엇갈림을 다 푼 뒤, 모서리에서 느려지는 떨리는 펜이 3,988px 잉크 선을 3.4초에 맞춰 그린다. 화면의 풀기 단계는 프레임마다 14,000쌍씩 70프레임이 걸려 60Hz 에서는 펜이 출발하는 2.84초보다 먼저 끝나지만, 펜은 처음에 끝까지 돌려 둔 순서를 그리므로 풀기가 늦어도 같은 꽃이 나오고, 6.24초부터 1.4초 동안 꽃잎·잎·꽃술 뒤로 색면 10덩이가 번져 든다. 꽃집이나 플라워 클래스 첫 화면에서 선 하나로 그려진 꽃이 색을 입는 장면으로 쓸 수 있다.

한 줄로 그려진 뒤 색이 번져 드는 꽃 첫 화면
새 창에서 열기

09 · 한 줄로 그려진 뒤 색이 번져 드는 꽃 첫 화면 — 측정 누적면적 35.003% · 움직인 프레임 19/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

FLOWER_DOTSTRIES_09PEN_TIME_09
    const greedy09 = greedyOrder(final09.xs, final09.ys, final09.n, start09);
    const greedy09Length = pathLength(final09.xs, final09.ys, greedy09, final09.n);
    // 다 푼 순서 — 같은 계산을 끝까지 돌려 둔다. 화면의 풀기 단계가 시간 안에 못 끝내도 펜은 이 순서를 그린다
    const solved09 = makeUntangle(Int32Array.from(greedy09));
    while (!solved09.done) {
        untangleStep(solved09, final09.xs, final09.ys, TRIES_09);
    }
    const solved09Length = pathLength(final09.xs, final09.ys, solved09.order, final09.n);
    function orderedCurve(o) {
        const px = [];
        const py = [];
        for (let k = 0; k < o.length; k += 1) {
            px.push(final09.xs[o[k]]);
            py.push(final09.ys[o[k]]);
        }
        return curveThrough(px, py);
    }
    const ink09 = inkLine(orderedCurve(solved09.order), TREMOR_09);

점은 밝기로 뿌리고, 순서는 엇갈림을 풀어서, 빠르기는 누적 길이로 — 선 하나를 세우는 계산

어둡기를 확률로 바꾸는 뽑기

03 은 16,000칸(3px 칸, 160×100)마다 어둡기를 한 번 재어 무게표를 만들고, 09 도 같은 함수로 꽃의 점을 뿌린다. 그다음 아무 자리나 뽑고, 그 칸 무게의 1.4제곱 확률로 받아들인다. 어둡기 0.5 인 칸은 0.5^1.4 ≈ 0.38 의 확률로, 어둡기 0.9 인 칸은 0.9^1.4 ≈ 0.86 의 확률로 점을 받으니, 두 칸의 차이가 0.9 ÷ 0.5 = 1.8배에서 약 2.3배로 벌어진다. 제곱을 올린 만큼 밝은 바탕에 점이 덜 흩어지고 어두운 윤곽에 점이 몰린다.

엇갈림 판정을 손으로 풀어 보면

segmentsCross 는 외적 네 번으로 두 선분이 엇갈리는지 본다. a(0,0)–b(10,10) 와 c(0,10)–d(10,0) 를 넣으면 a·b 쪽에서 본 c 는 +100, d 는 −100 이고, c·d 쪽에서 본 a 는 −100, b 는 +100 이라 양쪽 모두 부호가 갈려 엇갈림이다. 두 구간 길이의 합은 14.14 + 14.14 = 28.28 인데, 사이 순서를 뒤집어 a→c, b→d 로 다시 이으면 10 + 10 = 20 이 된다. 엇갈린 두 선분을 엇갈리지 않게 다시 이으면 길이가 늘지 않으니, 뒤집기를 되풀이하면 전체 길이는 줄어들기만 한다. 외적이 정확히 0 인 경우(세 점이 한 줄)는 엇갈림으로 치지 않는데, 길이가 줄지 않는 뒤집기가 같은 자리에서 오가는 것을 막는 쪽으로 보인다.

길이를 시간으로, 꺾임을 빠르기로

05 의 펜은 「지난 시간 × 240」 을 길이 s 로 바꾼 뒤 indexAtLength 로 609칸 누적 길이 표를 반씩 좁혀 s 가 든 마디를 찾는다. 608칸 폭이 한 번에 절반씩 줄어 2^10 = 1,024 가 608 을 넘으니 많아야 10번이면 마디가 정해진다. 06 은 반대로 빠르기를 꺾임에서 낸다. 꺾임이 0 인 곧은 곳은 v = 560 이고 굵기는 6 ÷ (1 + 0.003 × 560) ≈ 2.24px, 꺾임 0.1 rad/px 인 곳은 v = 560 ÷ (1 + 60 × 0.1) = 80 이고 굵기는 6 ÷ 1.24 ≈ 4.84px 다. 하트에서는 가장 곧은 곳도 초당 492px 까지만 올라가 실제 굵기는 2.42px 부터 시작한다.

한 줄 그림이 깨지는 때 — 점이 만 개로 늘어 멈추는 순서 정리·이음매에서 꺾이는 곡선·홀수 갈래가 넷인 도안

점이 만 개면 순서 정리가 화면을 붙잡는다

untangleStep 의 한 바퀴는 (N − 3)(N − 2) ÷ 2 쌍을 견준다. 420점이면 87,153쌍이라 프레임당 1,200쌍으로 73프레임이면 한 바퀴를 돌지만, 점이 10,000개면 한 바퀴가 49,975,003쌍이 되어 같은 속도로 41,646프레임(60Hz 에서 11.6분)이 걸린다. 상한 없이 한 프레임에 다 돌리면 그 프레임 하나가 5천만 쌍을 붙잡고, 가장 가까운 점부터 잇는 첫 단계만 해도 비교가 49,995,000번이다. 프레임당 상한을 두면 이번에는 풀리는 시간이 화면 주사율을 따른다. 04 의 218프레임은 60Hz 에서 3.63초지만 30Hz 에서는 7.27초라 그리기 단계 5.2초 안에 「다 풀림」 까지 가지 못하고, 120Hz 에서는 1.82초에 끝난다. 03 은 점 수를 10,000으로 불러도 MAX_DOTS 에서 2,400개로 멈추고, 칸마다 점 전부를 훑으면 5,731,200번일 비교를 12px 바구니로 218,776번(26.2분의 1)까지 줄인다.

조각마다 따로 정한 접선은 이음매에서 꺾인다

05 의 서명 점 39개를 조각마다 자기 구간의 현을 양 끝 접선으로 써서 이으면, 앞 조각이 끝나는 방향과 뒤 조각이 시작하는 방향이 달라진다. 같은 점으로 재면 이음매 37곳에서 꺾임이 최대 158.8°, 평균 44.6° 이고 20° 를 넘는 곳이 34곳이다. 앞뒤 이웃 점으로 정한 접선은 이음매 양쪽 조각이 같은 값을 쓰므로 0° 다. 여기에 길이 표 없이 조각마다 같은 시간을 주면 조각 길이가 곧 빠르기가 되어 펜이 초당 76~769px 사이를 10.17배로 오간다. 05 폴더의 index.html 에서 buildCurve 안의 cubicPiece 에 넘기는 두 접선을 그 구간의 현으로 바꿔 이음매가 꺾이는 모습을 직접 보려면, 내려받은 압축 파일에 걸린 비밀번호 hqaux5m7 으로 풀면 된다.

꼭짓점 넷이 모두 선 셋이면 한 번에 못 그린다

07 의 봉투는 네모에 가운데에서 만나는 대각선 넷을 더한 도안이라 차수가 [3, 3, 3, 3, 4] 이고 홀수 꼭짓점이 4개다. planStroke 는 순서를 찾지 않고 그 자리에서 불가를 돌려준다. 집은 차수 [3, 3, 4, 4, 2, 4] 로 홀수 점이 2개라 그 하나에서 출발해 다른 하나에서 끝나고, 나비 리본과 별은 홀수 점이 0개라 출발점으로 돌아온다. 도안을 직접 만들 때 선을 하나 더하거나 빼면 양 끝 두 꼭짓점의 홀짝이 함께 바뀌므로, 홀수 점이 넷인 도안은 선 하나로 둘을 이어 주면 그릴 수 있는 도안이 된다.

점을 성기게, 떨림을 크게, 펜을 빠르게 — 값 하나로 달라지는 선의 손맛

변형 바꾼 값 달라지는 것
점을 성기게 03 STIPPLE_COUNT 1800 → 700 이웃 거리 평균 2.88 → 5.09px, 흩어짐 0.50 → 0.42 로 더 고르지만 두 눈 자리 점이 204 → 73개라 눈매가 성긴 얼룩이 된다
떨림을 크게 06 TREMOR_AMP 1.2 → 3 두 겹 하트 사이 가장 좁은 여유가 2.16 → −0.13px 로 음수가 되어 맞닿는 곳이 0 → 2곳
펜을 빠르게 05 PEN_SPEED 240 → 520 그리는 시간 5.47 → 2.52초, 순환 8.47 → 5.52초, 0.12초 점 간격 28.8 → 62.4px

점을 줄이면 한 번 옮길 때 거리 비교도 218,776번에서 84,284번으로 줄지만 순환 시간은 옮기는 횟수가 정하므로 6.38초 그대로다. 떨림은 선의 자리만 밀기 때문에 06 의 길이·시간·굵기는 바뀌지 않고, 321곳을 검사한 바깥 줄 가운데 두 곳에서만 안쪽 줄과 붙는다. 펜을 빠르게 하면 점이 45개에서 21개로 줄어 고리 하나를 도는 동안 펜 자리를 눈으로 따라가기 어려워진다.

다 그려진 한 장으로 멈추게 — 움직임 줄이기·일시정지 버튼·그림 설명 문장의 접근성

움직임 줄이기에서 남는 그림

아홉 장면 모두 prefers-reduced-motion: reduce 를 읽고, 켜져 있으면 루프를 돌리지 않은 채 끝 시각까지 1/60초 걸음으로 계산만 해 다 그려진 한 장을 보여 준다. 설정을 도중에 바꿔도 change 를 듣고 같은 완성 판으로 넘어간다. 07 의 완성 판은 집 도안을 다 그은 자리다. 처음 열 때 01·02·05·06·08·09 는 첫 순환만 그림이 어느 정도 그려진 자리(01 1.5초·09 4.6초 등)에서 시작해 빈 종이로 열리지 않고, 03·04·07 은 0초에서 시작한다.

버튼과 화면 읽기 도구

캔버스 아홉 개는 모두 role="img" 와 그림을 설명하는 aria-label 을 갖고, 오른쪽 아래 일시정지 버튼은 aria-pressed 로 눌린 상태를 알리며 키보드 초점에 3px 잉크색 외곽선이 생긴다. 상태 줄이 있는 04·07 은 시연 중에는 aria-live 를 꺼 두었다가 처음 버튼을 누른 뒤에만 바뀐 문장을 읽어 준다. 탭이 숨으면 루프를 멈추고, 돌아오면 시계를 다시 맞춰 그리던 자리부터 잇는다. 기준은 MDN prefers-reduced-motion 과 MDN requestAnimationFrame 이다.

웹 페이지에 한 줄 그림을 넣기 전에 자주 묻는 질문

내 사진으로 이런 그림을 만들려면 어디를 바꾸나요?

01·02·03·09 는 faceDark 같은 어둡기 함수가 칸마다 0~1 을 돌려주므로, 그 자리에 사진을 화면 밖 캔버스에 그린 뒤 getImageData 로 읽은 밝기를 1 에서 뺀 값을 돌려주는 함수를 넣으면 된다. 다른 출처의 그림을 그린 캔버스는 이 읽기에서 보안 오류를 던지므로, 08 처럼 읽는 줄을 try 로 감싸고 사진은 같은 사이트에 두어야 한다.

점이 수천 개인 그림도 휴대폰에서 버벅이지 않나요?

04 는 프레임마다 1,200쌍, 09 는 14,000쌍까지만 견주고 돌아오며, 03·09 는 점을 2,400개 넘게 뿌리지 않는다. 캔버스 화소 배율도 2 에서 멈춘다. 점을 더 늘리려면 위 함정 절의 계산처럼 순서 정리 시간이 점 수의 제곱으로 늘어나는 것을 먼저 견줘 보는 편이 좋다.

한붓그리기 도안을 직접 만들어 07 에 넣으려면 무엇을 적나요?

07 의 FIGURES 는 도안마다 꼭짓점 좌표를 x·y 번갈아 늘어놓은 pts 와, 이어진 두 꼭짓점 번호를 짝지어 늘어놓은 edges 두 배열이다. 새 도안을 이 꼴로 하나 더하면 차수 세기·홀수 점 표시·순서 찾기·불가 표시가 그대로 돈다. 홀수 점이 2개를 넘으면 불가 도장이 찍히는데, 홀수 점 둘을 잇는 선을 하나 더할 때마다 홀수 점이 2개씩 줄어든다.

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

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