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

아스키 아트 9종 — 밝기를 글자로 바꾸는 램프부터 가장자리 기호까지

아스키 아트는 그림을 어떻게 글자로 바꿀까 — 01번은 칸의 평균 밝기를 글자 농도 줄에서 골라 찍고, 05번은 밝기가 변하는 방향에 맞춰 슬래시와 세로선을 고르며, 06번은 움직이는 무늬를 프레임마다 글자로 다시 읽는다.

아스키 아트는 글자를 그리는 일이 아니라 칸마다 밝기를 읽어 글자 하나로 답하는 계산이다 — 아스키 아트 만들기는 그림을 격자로 나누는 데서 시작한다

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

아홉 장면은 같은 뼈대를 쓴다. 장면은 화면에 붙이지 않는 두 번째 캔버스에 그리고, 화면에 찍히는 글자는 그 화소를 칸마다 평균 낸 값에서만 나온다. 라이브러리·WebGL·CDN 은 쓰지 않고 난수도 없으며, 장면 시계에서 나온 위상으로만 움직여 LAP_S 10초가 지나면 이음매 없이 되풀이된다.

장면은 숨은 캔버스에 그리고 글자는 읽은 값으로만 찍는다

setSrc 가 원본 캔버스의 크기와 배율을 정하고, readGrid 가 getImageData 를 한 번 불러 칸마다 원본 화소의 평균 r·g·b 와 밝기를 구한다. 밝기는 (0.2126 r + 0.7152 g + 0.0722 b) / 255 이고, 글자 판은 칸마다 fillText 를 부르지 않고 줄마다 같은 색이 이어지는 조각을 한 번에 찍는다. 글꼴은 고정폭 시스템 글꼴 묶음이고 글자 폭은 100px 글꼴에서 M 의 폭을 재서 비율로 둔다.

한 판은 시간표로 흘러간다

시연은 장면 시계 0.6초에서 시작하고 프레임 간격은 0.05초로 잘라 탭이 숨었다 돌아와도 한꺼번에 튀지 않는다. 글자 판은 캔버스에 그려지므로 아스키 아트 복사는 복사 단추나 C 키로 줄 끝 공백을 지운 평문을 클립보드에 넣는 방식이고, 그렇게 얻은 아스키 아트 텍스트는 고정폭 글꼴에 붙여 넣어야 칸이 맞는다. 한 판이 끝나기 0.6초 전에는 role="status" 문단에 상태 문장이 한 번만 쓰인다.

장면마다 얹은 계산 한 겹

속성 하나를 트윈하는 대신 계산을 한 겹씩 얹었다. 01 은 밝기를 램프 번호로, 02 는 칸의 가로세로 비를, 03 은 감마와 대비 곡선을, 04 는 규칙 격자 문턱을, 05 는 밝기 기울기의 방향을, 06 은 사인 네 개의 합을, 07 은 확산 음영을, 08 은 평균 색과 팔레트의 거리를, 09 는 줄마다 어긋난 위상을 계산한다. CSS 디더 편은 CSS 로 화소를 찍고 제너러티브 아트 편은 선과 타일을 그리며 떨어지는 글자 편은 글자의 물리를 다루는데, 이 편은 그림의 화소를 읽어 글자를 고르는 계산이 주제다.

아스키 아트 9종 — 밝기를 글자 농도로 옮기는 램프·글자 칸의 가로세로 비 보정·감마와 대비 곡선·두 글자만 쓰는 순서 디더·밝기 방향을 읽는 가장자리 기호·움직이는 플라즈마 글자 판·구 표면 음영 글자·글자마다 색을 입히는 팔레트·파도치는 글자 배너 히어로

아홉 장면을 렌더해서 얻은 면적·프레임·강도

장면 누적면적 움직인 프레임 강도 평균
01 램프 18.8% 23/23 78.5
02 칸 비율 10.9% 23/23 67.1
03 감마 곡선 32.5% 23/23 65.5
04 순서 디더 13.0% 23/23 103.3
05 방향 기호 15.0% 23/23 75.2
06 플라즈마 39.8% 23/23 60.9
07 구 음영 15.6% 23/23 73.9
08 색 틴트 45.1% 23/23 61.2
09 파도 배너 28.8% 23/23 72.4

08 이 45.1% 로 가장 넓고 02 가 10.9% 로 가장 좁으며, 두 글자만 쓰는 04 는 면적이 13.0% 인데도 강도 평균이 103.3 으로 가장 높다. 아홉 장면이 매 프레임 움직여 23/23 이라, 정지 화면으로 읽히는 장면은 없다.

01칸의 밝기를 글자 농도 줄에서 골라 찍는 램프

이미지를 글자로 바꾸는 변환 화면의 기본 틀로, readGrid 가 38×20 칸마다 평균 밝기를 구하고 glyphIndex 가 그 밝기를 램프 번호로 바꿔 글자 하나를 고른다. 왼쪽은 10단계 램프, 오른쪽은 rampOf(2) 로 공백과 @ 만 남겨 중간 밝기가 사라지는 대비판이다. 램프를 5단계로 줄이면 한 장면에서 평균 5종, 10단계는 10종, 16단계는 15종의 글자가 쓰인다.

칸의 밝기를 글자 농도 줄에서 골라 찍는 램프
새 창에서 열기

01 · 칸의 밝기를 글자 농도 줄에서 골라 찍는 램프 — 측정 누적면적 18.842% · 움직인 프레임 23/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

RAMP_LENreadGridglyphIndex
        const cw = cellW(FONT_PX);
        const ch = cellH(FONT_PX);
        const cols = Math.floor(PW2 / cw);
        const rows = Math.floor(PH / ch);
        const g = readGrid(cols, rows, cw, ch);
        const c1 = [];
        const c2 = [];
        for (let i = 0; i < g.lum.length; i += 1) {
            c1.push(RAMP.charAt(glyphIndex(g.lum[i], RAMP_LEN)));
            c2.push(RAMP_2.charAt(glyphIndex(g.lum[i], 2)));
        }
        lastKinds = kinds(usage(c1, RAMP));
        lastKinds2 = kinds(usage(c2, RAMP_2));
        lastCols = cols;
        lastRows = rows;
        plainText = plainOf(g, c1);

02글자 칸이 세로로 긴 비율을 맞춰 원형을 지키는 보정

비율이 맞아야 하는 로고·얼굴·원형 도안을 글자로 바꿀 때 먼저 걸리는 것이 글자 칸의 모양이다. 칸은 가로 5.9px 세로 12.0px 로 세로가 약 2.05배 길어서 읽는 칸을 정사각형으로 잡으면 그림이 세로로 늘어나고, 보정 판은 읽는 칸의 세로를 GLYPH_RATIO 배로 잡아 반지름 40px 원을 80px × 80px 로 찍는다. 오른쪽 실패판은 같은 줄 수 20 로 원본의 위쪽 절반만 읽어 같은 원이 80px × 약 164px 타원이 된다.

글자 칸이 세로로 긴 비율을 맞춰 원형을 지키는 보정
새 창에서 열기

02 · 글자 칸이 세로로 긴 비율을 맞춰 원형을 지키는 보정 — 측정 누적면적 10.906% · 움직인 프레임 23/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

GLYPH_RATIOSAMPLE_HMEASURE_W
        MEASURE_W = cellW(FONT_PX);
        SAMPLE_H = cellH(FONT_PX);
        GLYPH_RATIO = SAMPLE_H / MEASURE_W;
        setSrc(PW2, PH, 1);
        paintScene(PW2, PH, a);
        const cols = Math.floor(PW2 / MEASURE_W);
        const rows = Math.floor(PH / SAMPLE_H);
        // 보정: 읽는 칸의 세로를 줄 높이만큼(가로의 GLYPH_RATIO 배) 잡는다. 보정 없음: 읽는 칸을 정사각형(가로 = 세로)으로 잡는다
        const gA = readGrid(cols, rows, MEASURE_W, SAMPLE_H);
        const gB = readGrid(cols, rows, MEASURE_W, MEASURE_W);
        const cA = [];
        const cB = [];
        for (let i = 0; i < gA.lum.length; i += 1) {
            cA.push(RAMP.charAt(glyphIndex(gA.lum[i], RAMP_LEN)));
            cB.push(RAMP.charAt(glyphIndex(gB.lum[i], RAMP_LEN)));
        }

03감마와 대비 곡선으로 글자 분포를 고르게 펴는 조정

사진 노출이 한쪽으로 치우쳐도 글자 그림이 뭉치지 않게 하는 전처리다. curve 는 밝기를 PIVOT 0.72 둘레로 CONTRAST 2.6 배 늘린 뒤 GAMMA 1.2 제곱으로 구부려서, 0.5 에서 0.95 사이 좁은 띠에 몰린 안개 장면을 램프 전체로 펼친다. 곡선이 있으면 평균 10종이 쓰이고 없으면 4.9종이며, 가장 많이 쓰인 글자의 몫은 21% 와 42% 로 갈린다.

감마와 대비 곡선으로 글자 분포를 고르게 펴는 조정
새 창에서 열기

03 · 감마와 대비 곡선으로 글자 분포를 고르게 펴는 조정 — 측정 누적면적 32.474% · 움직인 프레임 23/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

GAMMACONTRASTPIVOT
        const g = readGrid(cols, rows, cw, ch);
        const cA = [];
        const cB = [];
        for (let i = 0; i < g.lum.length; i += 1) {
            cA.push(RAMP.charAt(glyphIndex(curve(g.lum[i]), RAMP_LEN)));
            cB.push(RAMP.charAt(glyphIndex(g.lum[i], RAMP_LEN)));
        }
        useA = usage(cA, RAMP);
        useB = usage(cB, RAMP);

04두 글자만으로 명암을 표현하는 순서 디더

글자 종류를 늘릴 수 없는 환경에서 흑백 글자 그림을 만들 때 쓴다. 칸 밝기를 칸 위치 (r % 4, c % 4) 로 고른 ORDER_4 문턱 (값 + 0.5) / 16 과 견줘 @ 와 . 둘 중 하나만 찍고, 오른쪽 판은 문턱 THRESHOLD 0.5 하나로 자른다. 짙은 글자 @ 의 몫은 규칙 격자가 평균 45.8%, 문턱 하나가 40.4% 다.

두 글자만으로 명암을 표현하는 순서 디더
새 창에서 열기

04 · 두 글자만으로 명암을 표현하는 순서 디더 — 측정 누적면적 13.044% · 움직인 프레임 23/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

ORDER_NTHRESHOLDTWO_GLYPH
        for (let r = 0; r < rows; r += 1) {
            for (let c = 0; c < cols; c += 1) {
                const v = g.lum[r * cols + c];
                const t = (ORDER_4[(r % ORDER_N) * ORDER_N + (c % ORDER_N)] + 0.5) / (ORDER_N * ORDER_N);
                if (v > t) {
                    cA.push(TWO_GLYPH.charAt(1));
                    nA += 1;
                }
                else {
                    cA.push(TWO_GLYPH.charAt(0));
                }
                if (v > THRESHOLD) {
                    cB.push(TWO_GLYPH.charAt(1));
                    nB += 1;
                }
                else {
                    cB.push(TWO_GLYPH.charAt(0));
                }
            }
        }

05밝기가 변하는 방향을 읽어 슬래시와 세로선을 고르는 가장자리

윤곽선이 또렷해야 하는 로고·도형을 글자로 바꿀 때 쓴다. gradientAt 이 좌우 칸의 밝기 차와 위아래 칸의 밝기 차로 기울기를 구하고, 세기가 EDGE_MIN 0.012 를 넘는 칸은 접선 각을 4구간으로 나눠 가로선·역슬래시·세로선·슬래시 중 하나를 찍는다. 칸 760개 가운데 경계 기호가 평균 275칸이고 기호 4종이 모두 나오며, 오른쪽 판은 밝기 램프만 읽어 윤곽이 번진다.

밝기가 변하는 방향을 읽어 슬래시와 세로선을 고르는 가장자리
새 창에서 열기

05 · 밝기가 변하는 방향을 읽어 슬래시와 세로선을 고르는 가장자리 — 측정 누적면적 15.004% · 움직인 프레임 23/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

gradientAtEDGE_MINANGLE_BIN
        for (let r = 0; r < rows; r += 1) {
            for (let c = 0; c < cols; c += 1) {
                const v = g.lum[r * cols + c];
                const gr = gradientAt(g, c, r, ch);
                const mag = Math.hypot(gr[0], gr[1]);
                if (mag > EDGE_MIN) {
                    // 접선 = 기울기 방향 + 90°. 0 에서 180° 로 접어 구간 번호를 구한다
                    let phi = Math.atan2(gr[1], gr[0]) + Math.PI / 2;
                    phi = ((phi % Math.PI) + Math.PI) % Math.PI;
                    const bin = Math.floor((phi + Math.PI / (2 * ANGLE_BIN)) / (Math.PI / ANGLE_BIN)) % ANGLE_BIN;
                    cA.push(EDGE_SYMBOLS.charAt(bin));
                    seen[bin] = true;
                    edges += 1;
                }
                else {
                    cA.push(FILL_RAMP.charAt(glyphIndex(v, FILL_RAMP.length)));
                }
                cB.push(RAMP.charAt(glyphIndex(v, RAMP.length)));
            }
        }

06움직이는 플라즈마를 프레임마다 글자로 다시 읽는 글자 판

터미널 느낌의 랜딩 배경이나 로딩 화면에 쓴다. sineSum 이 가로·세로·대각 사인과 움직이는 중심에서 퍼지는 고리 사인을 더한 값을 절반 해상도 원본에 쓰고, readGrid 가 그것을 다시 읽어 79×20 칸의 12단계 글자로 바꾼다. 12단계 램프의 글자가 평균 12종 모두 쓰였고 한 프레임 draw 는 이 PC 의 Chrome 에서 평균 약 2.1ms 걸렸다.

움직이는 플라즈마를 프레임마다 글자로 다시 읽는 글자 판
새 창에서 열기

06 · 움직이는 플라즈마를 프레임마다 글자로 다시 읽는 글자 판 — 측정 누적면적 39.769% · 움직인 프레임 23/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

sineSumSINE_TERMSFIELD_SS
    function sineSum(x, y, a) {
        const s1 = Math.sin(x * 0.04 + a);
        const s2 = Math.sin(y * 0.065 - a);
        const s3 = Math.sin((x + y) * 0.028 + 2 * a);
        const s4 = Math.sin(Math.hypot(x - 232 + 120 * Math.cos(a), y - 126 + 60 * Math.sin(a)) * 0.05 - 2 * a);
        return clamp(((s1 + s2 + s3 + s4) / SINE_TERMS) * 0.5 * 1.7 + 0.5, 0, 1);
    }

07구 표면의 빛과 그림자를 글자 농도로 옮기는 음영

입체감 있는 글자 로고나 히어로 오브젝트에 쓴다. 화소마다 구 위의 한 점으로 보고 normalDot 이 법선과 빛 방향의 내적이 양수일 때만 밝기를 주며, 줄무늬 구는 한 바퀴에 2번 자전하고 달은 mz + nz > zBest 일 때만 구 앞에 그려져 구 뒤로 돌면 가려진다. 12단계 램프에서 평균 11.6종이 쓰이고 한 프레임은 이 PC 의 Chrome 에서 평균 약 1.6ms 다.

구 표면의 빛과 그림자를 글자 농도로 옮기는 음영
새 창에서 열기

07 · 구 표면의 빛과 그림자를 글자 농도로 옮기는 음영 — 측정 누적면적 15.562% · 움직인 프레임 23/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

normalDotLIGHT_ANGLERAMP_SHADE
                if (r2 < SPHERE_R * SPHERE_R) {
                    const nz = Math.sqrt(SPHERE_R * SPHERE_R - r2);
                    const nx = dx / SPHERE_R;
                    const ny = dy / SPHERE_R;
                    const nzn = nz / SPHERE_R;
                    // 자전 — 법선을 세로축 둘레로 돌려 위도·경도를 구하고 줄무늬 무늬를 입힌다
                    const rx = nx * Math.cos(rot) + nzn * Math.sin(rot);
                    const rz = -nx * Math.sin(rot) + nzn * Math.cos(rot);
                    const lon = Math.atan2(rx, rz);
                    const lat = Math.asin(clamp(ny, -1, 1));
                    const tex = 0.5 + 0.5 * Math.sin(5 * lon + 3 * Math.sin(lat * 3));
                    const alb = 0.42 + 0.58 * smooth(0.35, 0.65, tex);
                    v = 0.05 + 0.95 * normalDot(nx, ny, nzn, lx, ly, lz) * alb;
                    zBest = nz;
                }

08칸의 평균 색을 글자마다 입히는 팔레트 틴트

글자 그림에 색을 더한 포스터나 프로필 이미지에 쓴다. chroma 로 칸 평균 색에서 밝기를 덜어 낸 색 방향을 만들고 nearestColor 가 파스텔 여섯 색 PALETTE 중 가장 가까운 번호를 골라서, 빛 덩어리가 겹쳐 섞인 색도 여섯 색 안으로 떨어진다. 평균 5.6색이 쓰이고, 예를 들어 한 시각에는 민트 169·분홍 153·버터 0·하늘 341·라벤더 533·회백 384칸이 찍혔다.

칸의 평균 색을 글자마다 입히는 팔레트 틴트
새 창에서 열기

08 · 칸의 평균 색을 글자마다 입히는 팔레트 틴트 — 측정 누적면적 45.06% · 움직인 프레임 23/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

PALETTE_NnearestColorchroma
    // 색 방향 — 가장 큰 채널이 255 가 되게 맞춘 색
    function chroma(r, g, b) {
        const m = Math.max(r, g, b, 1);
        return [r * 255 / m, g * 255 / m, b * 255 / m];
    }
    const PAL_DIR = PALETTE.map(function (c) { return chroma(c[0], c[1], c[2]); });
    function nearestColor(r, g, b) {
        const c = chroma(r, g, b);
        let best = 0;
        let bd = 1e9;
        for (let i = 0; i < PALETTE_N; i += 1) {
            const dr = c[0] - PAL_DIR[i][0];
            const dg = c[1] - PAL_DIR[i][1];
            const db = c[2] - PAL_DIR[i][2];
            const dd = dr * dr + dg * dg + db * db;
            if (dd < bd) {
                bd = dd;
                best = i;
            }
        }
        return best;
    }

09파도치는 글자 배너로 이어지는 히어로

소개 화면이나 행사 안내 상단에 놓는 글자 배너에 쓴다. 줄마다 위상이 ROW_PHASE 0.5 씩 어긋난 사인에 WAVE_COLS 4칸을 곱해 반올림한 만큼 가로로 밀어 읽어 파도가 치고, 원본 양쪽에 PAD_COLS 5칸을 덧대 밀린 줄이 가장자리에서 끊기지 않는다. 20줄 판에서 한 바퀴 loopCols 68칸이 10초에 흘러 driftX 는 초당 6.8칸이다.

파도치는 글자 배너로 이어지는 히어로
새 창에서 열기

09 · 파도치는 글자 배너로 이어지는 히어로 — 측정 누적면적 28.763% · 움직인 프레임 23/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

ROW_PHASEloopColsdriftX
        const sCols = cols + 2 * PAD_COLS;
        setSrc(sCols * cw, PH, 1);
        paintScene(sCols * cw, PH, ph);
        const g = readGrid(sCols, rows, cw, ch);
        const cA = [];
        for (let r = 0; r < rows; r += 1) {
            // 줄마다 위상이 ROW_PHASE 씩 어긋난 사인으로 가로로 밀린다(칸 단위로 반올림)
            const off = Math.round(WAVE_COLS * Math.sin(ROW_PHASE * r - a));
            for (let c = 0; c < cols; c += 1) {
                cA.push(RAMP.charAt(glyphIndex(g.lum[r * sCols + c + PAD_COLS + off], RAMP.length)));
            }
        }

아스키 아트 변환을 코드로 짜면 글자 하나는 어떤 값 몇 개로 정해질까?

글자 하나는 칸의 평균 밝기 하나와 램프 줄 하나로 정해지고, 나머지 계산은 이 밝기를 글자 번호로 옮기기 전에 고치는 일이다. 그래서 아홉 장면은 glyphIndex 한 함수를 공유하고 장면마다 그 앞의 값만 다르게 만든다.

칸이 가진 값과 하는 일

값 만들어지는 곳 하는 일
lum readGrid 칸 원본 화소의 평균 밝기 0 에서 1
RAMP_FULL 소스 상수 성긴 글자에서 짙은 글자까지 16자
glyphIndex 공용 함수 밝기를 램프 번호로 내림
GLYPH_RATIO 02 글자 칸 세로 ÷ 가로 비 약 2.05
curve 03 밝기를 늘리고 구부려 번호를 고르게 함

램프 줄은 RAMP_FULL 열여섯 글자이고 rampOf(10) 은 그 가운데 열 개를 고르게 뽑아 공백과 , : - = + o # % @ 순서가 된다.

터미널 느낌의 랜딩 배경과 로딩 장면

06 과 09 는 글자 판이 계속 흘러서 터미널 화면 같은 배경이나 로딩 장면에 그대로 얹을 수 있다. 06 은 한 줄 79칸에 20줄이라 가로로 긴 영역에, 09 는 줄 20개가 배너 글자를 파도로 밀어 소개 문구 영역에 맞는다. 글자 판 아래에 문구를 올릴 때는 판 바탕 #221e38 위 기본 글자 대비가 12.0:1 이라 겹쳐 두어도 읽힌다.

이미지를 글자로 바꾸는 변환 화면

변환 화면은 01 의 램프, 02 의 비율 보정, 03 의 곡선이 한 줄로 이어진 것이다. 사진을 올리면 비율이 틀어지지 않도록 02 를 먼저 맞추고, 노출이 한쪽으로 몰린 사진은 03 의 곡선을 거쳐 글자를 고른 뒤, 04 로 두 글자만 쓰거나 05 로 윤곽만 따로 찍는 선택지를 두면 된다.

그림을 칸으로 나누고, 칸의 평균 밝기를 구하고, 곡선으로 밝기를 고치고, 글자 줄에서 한 글자를 골라 찍기까지 — 아스키 아트 한 프레임이 계산되는 순서

한 프레임이 지나가는 다섯 걸음

  1. 장면을 원본 캔버스에 그린다. 06·07 은 화소를 직접 써서 절반 해상도(FIELD_SS 0.5)로 계산한다.
  2. cellW·cellH 로 글자 칸 크기를 잰 뒤 칸 수를 정한다. 판 하나는 79×20, 판 둘은 38×20 칸이다.
  3. readGrid 가 칸마다 평균 밝기와 평균 색을 읽는다.
  4. 03 의 곡선이나 04 의 문턱, 05 의 기울기처럼 장면이 얹은 계산을 거친다.
  5. glyphIndex 로 글자를 고르고 drawGlyphs 가 줄마다 같은 색 조각을 한 번에 찍는다.

판이 둘인 장면은 두 판을 나란히 비교한다

01~05 는 왼쪽 판과 오른쪽 대비판이 나란히 놓인다. 01·03·04·05 는 readGrid 한 번의 결과를 두 판이 나눠 쓰고 글자 고르는 방식만 다르며, 02 는 칸의 읽는 모양이 달라 readGrid 를 두 번 부른다. 두 판의 차이가 곧 그 장면이 얹은 계산의 효과라서 값을 바꿔 볼 때도 한 화면에서 비교된다.

아스키 아트가 어긋나는 순간 — 글자 칸이 세로로 길어 세로로 늘어나 보이는 그림·밝기 곡선이 없어 한쪽으로 몰린 글자·칸이 너무 커서 윤곽이 뭉개진 가장자리

정사각 칸으로 읽으면 세로로 늘어나는 그림

02 의 오른쪽 판이 그 사례다. 글자 칸 하나가 5.9px × 12.0px 이라 정사각 칸으로 읽으면 같은 20줄이 원본의 위쪽 절반만 덮고, 원은 80px 폭에 세로 약 164px 로 길어진다.

곡선 없이 읽으면 몇 글자만 남는다

03 의 곡선 없는 판은 밝기가 좁은 띠에 몰려 글자가 평균 4.9종만 쓰이고, 가장 많이 쓰인 글자 하나가 칸의 42% 를 차지한다. 곡선을 거치면 같은 장면이 평균 10종으로 펼쳐지고 그 몫은 21% 로 줄어든다.

칸이 크면 윤곽 기호가 닿는 칸이 적다

05 의 칸 높이 12px 에서는 칸 760개 가운데 경계 기호가 평균 275칸이다. 칸을 6px 로 줄이면 3,157칸 가운데 645칸이 경계 기호로 바뀌어 같은 도형의 선이 더 또렷하게 이어진다.

이 편의 코드를 파일로 가져가려면 아래 zip 을 내려받아 amzukyh6 으로 열면 되고, 안에는 아홉 장면의 완성 소스가 들어 있다.

칸 크기와 글자 단계 수와 감마를 바꾸면 모양과 읽힘은 얼마나 달라질까 — 칸 크기·단계 수·감마를 바꿔 본 차이

이름 바꾼 값 느낌
글자 단계를 더 촘촘히 01 RAMP_LEN 10 → 16 같은 장면에서 쓰이는 글자가 평균 10종에서 15종으로 늘어 음영이 더 잘게 나뉘지만 이웃 단계끼리 농도 차가 작아 작은 칸에서는 구분이 어렵다. 칸 수는 38×20 = 760 그대로다
감마를 더 세게 03 GAMMA 1.2 → 2.2 곡선을 거친 칸 가운데 가장 어두운 다섯 글자의 몫이 평균 55%에서 77%로 늘어 중간 밝기가 어두운 글자로 내려가고 그림자가 짙어진다
칸을 더 작게 05 CELL_PX 12 → 6 칸 수가 760개에서 3,157개로 약 4배가 되고 경계 기호 칸도 평균 275개에서 645개로 늘어 선이 또렷해지며, 한 프레임 draw 는 이 PC 의 Chrome 에서 평균 0.64ms 에서 1.04ms 로 늘었다

값을 바꾸기 전에 볼 것

램프 단계를 늘릴수록 좋아지는 것은 아니다. 16단계에서 쓰이는 글자는 15종이라 한 글자는 이 장면에서 한 번도 안 쓰이고, 인접 단계가 닮아 작은 칸에서 구분이 흐려진다.

감마는 0.6 에서 17.7%, 1.2 에서 약 55%, 1.8 에서 70.4%, 2.2 에서 약 77% 로 어두운 다섯 글자의 몫이 올라가 숫자만으로 판을 고를 수 있다. 칸을 6px 로 줄여도 계산 시간은 4배가 아니라 1.6배쯤 늘었는데, 이 값은 이 PC 의 헤드리스 Chrome 에서 20프레임을 평균한 것이라 기기마다 다르다.

움직임 줄이기에서는 한 장으로 멈춘 글자 판 — 장면 설명과 상태 줄의 수치를 문장으로 읽어 주고 복사 단추로 글자 판을 텍스트로 가져가는 접근성

키보드로 멈추고 한 걸음씩 보고 복사한다

무대는 role="application" 이고 포커스를 주면 Enter·Space·P 가 일시정지, N 이 장면 시계 0.4초 앞으로 한 걸음, R 이 처음부터, C 가 글자 판 복사다. 단추는 일시정지(aria-pressed)·한 걸음·복사 셋이고 무대를 누르면 일시정지가 토글된다. 복사는 navigator 를 쓰지 않고 임시 textarea 와 execCommand('copy') 로 하며, 성공하면 상태 줄에 1.6초 동안 알린다.

글자 판은 낭독기에 읽히지 않으므로 수치를 문장으로 준다

캔버스는 role="img" 에 장면 설명 aria-label 이 있지만 글자 판의 글자는 그림이라 낭독기에 읽히지 않는다. 그래서 상태 줄이 칸 수와 글자 종 수를 문장으로 알리며, 시연 중에는 aria-live="off" 이고 사용자가 손대면 polite 로 바뀐다. 판 바탕 #221e38 위 기본 글자 대비는 12.0:1 이고 06 의 가장 어두운 글자 단계는 5.4:1, 08 팔레트는 8.8 에서 13.6:1 이다.

움직임 줄이기와 숨은 탭

prefers-reduced-motion 이 켜져 있으면 루프 없이 showRest 가 REST_S 초 지점의 장면 한 장만 그리고, 설정이 바뀌면 change 리스너가 다시 읽는다. 탭이 숨으면 루프를 멈추고 돌아오면 첫 프레임 간격을 버리며, 이 경우에도 일시정지·한 걸음·복사·R 은 화면에 반영된다.

    document.addEventListener('visibilitychange', function () {
        if (document.hidden) {
            stopLoop();
        }
        else if (!paused && !still) {
            startLoop();
        }
    }, { signal: off.signal });
    // 설정이 바뀌면 — 켜지면 정지 장면 한 장, 꺼지면 다시 움직인다
    reduceQuery.addEventListener('change', function () {
        still = reduceQuery.matches;
        if (still) {
            showRest();
        }
        else {
            sceneT = START_S;
            showNow();
            if (!paused) {
                startLoop();
            }
        }
    }, { signal: off.signal });

아스키 아트 FAQ — 라이브러리 없이 만들기·글자 폭이 다른 글꼴에서 깨지는 이유·한글이나 이모지를 램프에 넣어도 되는지

아스키 아트 변환에 라이브러리가 필요한가요

필요 없다. 외부 코드 없이 브라우저 캔버스 2D 만 쓰고, 화소를 읽는 getImageData 와 글자를 찍는 fillText 두 호출이 일을 나눠 맡는다.

글자 폭이 다른 글꼴에서는 왜 그림이 깨지나요

칸 격자는 글자 하나의 폭이 같다는 가정으로 짠 것이라 비례 글꼴에서는 줄마다 글자 폭이 달라 열이 어긋난다. 이 편은 고정폭 글꼴 묶음을 쓰고 100px 에서 M 의 폭을 재서 비율로 두며, 칸 높이는 글자 크기의 1.2배인 LINE_K 로 잡는다.

한글이나 이모지를 램프에 넣어도 되나요

이 편은 ASCII 16자만 썼고 한글이나 이모지는 돌려 보지 않았다. 글자 하나가 영문 한 칸보다 넓으면 격자와 어긋나고, 이모지는 charAt 으로 뽑으면 반쪽이 나오므로 쓰려면 rampOf 를 글자 단위 배열로 바꾸고 폭을 다시 재야 한다.

MDN 의 fillText 문서와 measureText 문서, requestAnimationFrame 문서, 움직임 줄이기 설정은 prefers-reduced-motion 문서가 기준이다. 캔버스로 지형을 만드는 값 노이즈는 캔버스 노이즈 지형 편을 이어서 보면 된다.

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

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