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

매직아이 9종 — 깊이 지도를 주사선 밀기 한 겹으로 짜는 스테레오그램 캔버스

픽셀의 밝기를 깊이로 읽어 같은 줄 안에서 몇 픽셀 밀어 붙이면 평면 무늬 속에 입체가 숨습니다. 라이브러리 없이 복붙으로 가져가는 숨은그림 엔진입니다.

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

데모

아홉 판 각각은 html 파일 하나로 자립해 열자마자 움직이고, 화면의 모든 변화는 CSS 애니메이션이 아니라 깊이 지도를 다시 계산해 행별 복사 시프트를 다시 실행한 결과입니다. 24프레임 녹화 구간에서 아홉 판 전부 23프레임이 움직였습니다. 05 는 휠과 가로 드래그로 지형을 직접 여행하고 07 은 포인터로 깊이를 파고 쌓는 판이니 지나가기 전에 멈춰 보시기를 권합니다.

24프레임 실측 한눈에

항목 초점 누적면적(%) 강도 평균
01 랜덤닷 기본 숨쉬는 반구가 떠오르는 최소 구현 31.427 69.7
02 패턴 타일 이산 시차와 패턴 주기의 정밀 대조 52.806 155.2
03 글자 숨기기 fillText 래스터 밝기를 깊이로 넘기기 31.934 85.8
04 사진 부조 합성 사진의 밝기를 지형 높이로 읽기 50.087 71.8
05 스크롤 지형 3옥타브 사인 산맥을 여행하며 드러내기 51.918 71.8
06 보는법 가이드 단서 두 점과 풀이 지도로 입문시키기 52.87 66.0
07 포인터 조각 보이지 않는 진흙을 끌어올리고 누르기 34.71 76.0
08 살아있는 돌출 파장 130px 사인 파동이 표면을 숨쉬게 하기 31.176 71.1
09 로고 히어로 씬 워드마크와 비네팅으로 짜는 대문 장면 52.539 69.9

수치는 성격으로 읽는다

누적면적이 전 항목 31% 이상인 것은 매직아이 특유의 화면 구성 탓입니다. 노이즈 밑칠이 캔버스 전면을 덮는 깊이 있는 화면이라 기본값부터 높고, 항목 사이의 차이는 같은 프레임 안에서 얼마나 다시 엮이는가를 재는 프레임별 최대 면적과 강도평균으로 벌어집니다. 06 보는법 가이드는 풀이 지도가 겹쳐지는 점등 순간 한 프레임 면적 52.87% 로 정점을 찍고, 02 패턴 타일은 강도평균 155.2 로 아홉 판 중 가장 밝습니다. 반대로 01 랜덤닷 기본(31.427%)·03 글자 숨기기(31.934%)·08 살아있는 돌출(31.176%)은 깊이가 있는 영역 자체가 절제된 판이라 낮게 나온 것이지 결함이 아닙니다.

반구 수식에서 포인터 조각까지, 매직아이 깊이를 넓힌 순서

순서는 화려함의 서열이 아니라 깊이의 원천을 몇 단계로 넓혀 가는가의 서열입니다. 먼저 수식으로 잡은 반구 하나로 최소 구현을 얹고(01), 노이즈 대신 반복 패턴을 깔아 이산 시차를 무늬로 읽게 한 뒤(02), 깊이 소스를 수식에서 그림으로 바꿉니다(03). 넷째와 다섯째는 그림의 범위를 넓히는 두 걸음 — 합성한 사진의 밝기를 통째로 높이로 쓰고(04), 사인 세 개를 합한 지형을 여행 축으로 펼칩니다(05). 여섯째는 관점의 전환으로, 그림이 아니라 보는 사람을 바꾸는 접근성 판이고(06), 일곱째부터는 깊이를 손으로 만집니다 — 포인터로 조각하고(07), 시간을 흘려 표면을 숨쉬게 하며(08), 마지막에 워드마크와 비네팅까지 얹어 서비스의 첫 화면으로 조립합니다(09). 픽셀 버퍼를 직접 굽는 계열로는 SDF 거리장을 한 걸음씩 굽는 레이마칭 캔버스와 같은 계보이고, 미적분 한 걸음으로 나비를 쌓는 로렌츠 어트랙터·발산 횟수로 색을 매기는 만델브로 줌과도 이웃하는, 계산으로 그림을 얻는 편입니다.

항목별 코드

01랜덤닷 기본 — 원 하나가 소리 없이 떠오르는 첫 화면

숨은그림 입문 도해나 놀이 위젯에 그대로 얹을 수 있는, 매직아이의 모든 것이 시작되는 최소 구현입니다. 배경 노이즈를 깔고 원판 영역의 픽셀만 한 줄 안에서 몇 칸 밀면 눈을 평행으로 늦추는 순간 원이 공으로 떠오릅니다.

랜덤닷 기본 — 원 하나가 소리 없이 떠오르는 첫 화면
새 창에서 열기

01 · 랜덤닷 기본 — 원 하나가 소리 없이 떠오르는 첫 화면 — 측정 누적면적 31.427% · 움직인 프레임 23/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

ImageDataUint8ClampedArrayputImageData

깊이 지도를 주사선으로 미는 한 겹

  function draw(t) {
    var d = img.data, x, y, di;
    var col = new Int32Array(W);   // 이 행의 낱알 팔레트 인덱스
    for (y = 0; y < H; y++) {
      for (x = 0; x < W; x++) {
        var s = Math.round(depthAt(x, y, t) * MAXSHIFT);
        if (s <= 0) {
          col[x] = baseIdx(x, y);                    // 배경 — 밑칠 낱알 그대로
        } else {
          var from = x - s;
          col[x] = from >= 0 ? col[from] : baseIdx(from + W, y);  // 왼쪽에서 복사(화면 밖은 되감아)
        }
      }
      for (x = 0; x < W; x++) {
        var c = PAL[col[x]];
        di = (y * W + x) * 4;
        d[di] = c[0]; d[di + 1] = c[1]; d[di + 2] = c[2]; d[di + 3] = 255;
      }
    }
    ctx.putImageData(img, 0, 0);
  }

02패턴 타일 — 반복 무늬 폭이 곧 눈의 모으는 각도다

노이즈 대신 폭 100px 사다리꼴 스트라이프 타일을 깔고 깊이를 4·8·12·16px 네 계단으로 둔, 파라미터 실험과 교육 도해에 맞는 정밀 판입니다. 시프트가 타일 주기의 배수일 때 이음매가 맞고 아니면 어긋나 도드라지는데, 어긋남의 띠는 주사 위상이 2초 주기로 흐르게 하고 타일 자체는 28px/초로 위로 흘러 흐름량을 정수 픽셀로 내림해 결을 유지합니다.

패턴 타일 — 반복 무늬 폭이 곧 눈의 모으는 각도다
새 창에서 열기

02 · 패턴 타일 — 반복 무늬 폭이 곧 눈의 모으는 각도다 — 측정 누적면적 52.806% · 움직인 프레임 23/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

ImageDatarequestAnimationFrame

계단 넷과 주사 위상

  function depthAt(x, y, t) {
    var band = (y * 4 + Math.floor(x / (W / 4)));      // 대각 계단 인덱스
    var scan = (t / CYCLE) % 1;                        // 0..1 주사 위상
    var appear = ((band % 8) + 8) % 8 / 8;             // 단마다 등장 시각을 엇갈리게
    var lift = Math.max(0, Math.sin((scan - appear) * Math.PI * 2 - Math.PI / 2) * .5 + .5);
    return lift * lift;
  }

  function shiftFor(x, y, t) {
    var band = Math.floor(x / (W / 4)) % 4;
    var d = depthAt(x, y, t);
    return Math.round(STEPS[band] * d);
  }

03글자 숨기기 — fillText 로 찍은 글자를 깊이 지도로 쓴다

이벤트 이스터에그나 놀이 위젯에 넣을 문구 숨기기입니다. 오프스크린 캔버스에 굵은 글자를 흑백으로 찍어 밝기를 0(배경)..1(글자 살) 깊이로 넘기면 노이즈 속에 단어가 통째로 잠기는데, 좌우 1px 평균으로 가장자리 낙차를 무디게 하는 것이 요령이고 살이 1~2px 인 가는 글자는 시프트 뒤 형태가 녹기 때문에 몸통이 굵은 짧은 단어(SOLID)가 잘 뜹니다.

글자 숨기기 — fillText 로 찍은 글자를 깊이 지도로 쓴다
새 창에서 열기

03 · 글자 숨기기 — fillText 로 찍은 글자를 깊이 지도로 쓴다 — 측정 누적면적 31.934% · 움직인 프레임 23/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

fillTextgetImageDataImageData

래스터 밝기를 깊이 버퍼로 바꾸기

    srcCv.width = W; srcCv.height = H;
    srcCtx.fillStyle = '#000';
    srcCtx.fillRect(0, 0, W, H);
    srcCtx.fillStyle = '#fff';
    srcCtx.textAlign = 'center';
    srcCtx.textBaseline = 'middle';
    var fs = Math.min(FSIZE, W * 0.46);
    srcCtx.font = '900 ' + fs + 'px "Arial Black", "Segoe UI", sans-serif';
    srcCtx.fillText(WORD, W * 0.5, H * 0.58);
    var sd = srcCtx.getImageData(0, 0, W, H).data;

    // 밝기 → 깊이. 좌우 1px 평균으로 가장자리 낙차를 무디게 한다(꺾여 보이지 않게)
    depthBuf = new Float32Array(W * H);
    var x, y, v, sum, n;
    for (y = 0; y < H; y++) {
      for (x = 0; x < W; x++) {
        v = sd[(y * W + x) * 4] / 255;
        sum = v; n = 1;
        if (x > 0)     { sum += sd[(y * W + x - 1) * 4] / 255; n++; }
        if (x < W - 1) { sum += sd[(y * W + x + 1) * 4] / 255; n++; }
        depthBuf[y * W + x] = sum / n;
      }
    }
  }

04사진 부조 — 밝기를 지형의 높이로 읽어 들어올린다

어두운 사진 한 장의 밝기를 그대로 깊이로 쓰면 얼굴과 사물이 노이즈 언덕 위로 부조처럼 떠오르는, 초상 아트와 SNS 공유용 놀이에 맞는 변환기입니다. 언덕마다 광원과의 가로 거리로 밝기를 정하는데 광원이 2초 주기로 좌우로 돌면 음영이 바뀌고 음영이 곧 높이라 부조 전체가 살아 움직이고, draw 는 매 프레임 래스터를 다시 읽어 밝기 0..255 를 MAXSHIFT 12px 에 곱한 칸수만큼 밀어 붙입니다.

사진 부조 — 밝기를 지형의 높이로 읽어 들어올린다
새 창에서 열기

04 · 사진 부조 — 밝기를 지형의 높이로 읽어 들어올린다 — 측정 누적면적 50.087% · 움직인 프레임 23/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

ImagegetImageDataImageData

광원 쪽이 밝은 언덕 음영

    // 언덕 세 개 — 포물선 낙차. 광원 쪽이 밝다(높이 음영)
    function hill(cx2, baseY, halfW, lift, hue) {
      var g2 = phCtx.createLinearGradient(cx2 - halfW, 0, cx2 + halfW, 0);
      var lit = Math.max(0, Math.min(1, 0.5 + (lx - cx2) / W));
      var r = Math.round(hue[0] + (255 - hue[0]) * lit * lift);
      var g3 = Math.round(hue[1] + (255 - hue[1]) * lit * lift);
      var b2 = Math.round(hue[2] + (255 - hue[2]) * lit * lift);
      var dk = 0.82;
      g2.addColorStop(0, 'rgb(' + Math.round(r * dk) + ',' + Math.round(g3 * dk) + ',' + Math.round(b2 * dk) + ')');
      g2.addColorStop(0.5, 'rgb(' + r + ',' + g3 + ',' + b2 + ')');
      g2.addColorStop(1, 'rgb(' + Math.round(r * dk) + ',' + Math.round(g3 * dk) + ',' + Math.round(b2 * dk) + ')');
      phCtx.fillStyle = g2;
      phCtx.beginPath();
      phCtx.moveTo(cx2 - halfW, H);
      for (var sx = -halfW; sx <= halfW; sx += 4) {
        phCtx.lineTo(cx2 + sx, baseY - Math.cos(sx / halfW * Math.PI / 2) * lift * H * 0.3);
      }
      phCtx.lineTo(cx2 + halfW, H);
      phCtx.closePath();
      phCtx.fill();
    }

05스크롤 지형 — 굴리는 만큼 산맥이 드러난다

스크롤 스토리나 탐험형 랜딩에 쓸 대표 장면입니다. 진폭 0.45·0.30·0.18 사인 세 개에 느린 코사인 한 개를 더해 무빙 램프를 만들고 화면 하단 곧 카메라에 가까운 쪽을 제곱으로 높여 앞쪽 산이 더 솟게 하는 것이 원근감의 전부인데, 행별 시프트에서 terrain(x + travel, y) 처럼 여행 오프셋을 더해 샘플하는 것이 지형을 흐르게 하는 전부이고 휠은 델타의 0.6배, 가로 드래그는 이동량의 1.2배를 travel 에서 뺍니다.

스크롤 지형 — 굴리는 만큼 산맥이 드러난다
새 창에서 열기

05 · 스크롤 지형 — 굴리는 만큼 산맥이 드러난다 — 측정 누적면적 51.918% · 움직인 프레임 23/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

scrollImageDatarequestAnimationFrame

사인 세 개를 합한 3옥타브 지형

  // 지형 — 3옥타브 무빙 램프. wx = 세계 좌표(여행 오프셋 더한 x)
  function terrain(wx, y) {
    var v = 0;
    v += Math.sin(wx * 0.011 + y * 0.004) * 0.45;
    v += Math.sin(wx * 0.031 + 1.7 + y * 0.010) * 0.30;
    v += Math.sin(wx * 0.074 + 4.2) * 0.18;
    v += (Math.cos(wx * 0.005 + y * 0.006) * 0.5 + 0.5) * 0.35;
    var front = y / H;                       // 화면 하단(앞쪽)이 더 높게 솟는다
    var h01 = Math.max(0, Math.min(1, v * 0.5 + 0.5)) * (0.35 + 0.65 * front * front);
    return h01;
  }

06보는법 가이드 — 초점 유도 점과 셀프 풀이 오버레이

매직아이가 안 되는 사람을 위한 접근성 판입니다. 판 위에 스테레오 시차(100px)만큼 벌어진 같은 점 두 개를 띄워 겹칠 때까지 눈을 늦추게 연습시키는데, 벌림이 눈이 늦춰야 할 시차와 같아야 점이 겹치는 순간 판이 합쳐지므로 모든 움직임을 rAF 캔버스로 그려 캡처 시계와 어긋나지 않게 합니다.

보는법 가이드 — 초점 유도 점과 셀프 풀이 오버레이
새 창에서 열기

06 · 보는법 가이드 — 초점 유도 점과 셀프 풀이 오버레이 — 측정 누적면적 52.87% · 움직인 프레임 23/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

requestAnimationFramefillRectMath.sin

구멍이 낮은 도넛의 깊이

  function depthAt(x, y, t) {
    var ph = t / CYCLE * Math.PI * 2;
    var cy = H * 0.54 + Math.sin(ph) * H * 0.08;              // 수직 bob
    var cx = W * 0.5;
    var rad = Math.min(W, H) * 0.36;
    var dx = (x - cx) / rad, dy = (y - cy) / rad;
    var d2 = dx * dx + dy * dy;
    if (d2 >= 1) return 0;
    var ring = Math.sqrt(d2);
    var hole = ring < 0.36 ? 0.22 : 1;   // 안쪽 구멍은 낮게 — 입체로 읽으면 도넛

07포인터 조각 — 갈고리로 깊이를 파고 쌓는 판

포인터가 지나간 자리마다 깊이 지도에 흔적을 더하고 빼는, 창작 도구 성격의 놀이 판입니다. 브러시 지름 26px 의 원뿔 falloff 로 가장자리는 얕게 스며들게 하고 포인터 이동을 선분으로 잡아 그 사이를 빈틈없이 채우는데, 왼쪽 끌기는 끌어올림(+0.8)·오른쪽 끌기는 누르기(−0.5) 로 한 판에서 지형을 파고 쌓고 스친 자리는 매 프레임 exp(−dt/DECAY) 를 곱해 6초에 걸쳐 스며들듯 사라집니다.

포인터 조각 — 갈고리로 깊이를 누르고 끌어올린다
새 창에서 열기

07 · 포인터 조각 — 갈고리로 깊이를 누르고 끌어올린다 — 측정 누적면적 34.71% · 움직인 프레임 23/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

pointermoveImageDatarequestAnimationFrame

선분을 따라 찍는 원뿔 브러시

  function brush(x0, y0, x1, y1, amount, r) {
    r = r || BRUSH / 2;
    var dist = Math.hypot(x1 - x0, y1 - y0);
    var steps = Math.max(1, Math.ceil(dist / (r * 0.4)));
    for (var i = 0; i <= steps; i++) {
      var bx = x0 + (x1 - x0) * (i / steps);
      var by = y0 + (y1 - y0) * (i / steps);
      var x0i = Math.max(0, Math.floor(bx - r)), x1i = Math.min(W - 1, Math.ceil(bx + r));
      var y0i = Math.max(0, Math.floor(by - r)), y1i = Math.min(H - 1, Math.ceil(by + r));
      for (var yy = y0i; yy <= y1i; yy++) {
        for (var xx = x0i; xx <= x1i; xx++) {
          var dx = xx - bx, dy = yy - by;
          var dd = Math.sqrt(dx * dx + dy * dy);
          if (dd >= r) continue;
          var fall = 1 - dd / r;               // 원뿔 falloff — 가장자리는 얕게
          var idx = yy * W + xx;
          var v = sculpt[idx] + amount * fall;
          sculpt[idx] = v < 0 ? 0 : (v > 1 ? 1 : v);
        }
      }
    }
  }

08살아있는 돌출 — 깊이 파동으로 숨쉬는 표면

히어로 배경이나 전시형 놀이에 맞는 애니메이션 판입니다. 기저 부풀음(중앙 돔) 위로 파장 130px 사인 파동이 대각선으로 지나가 봉우리가 진폭 12px 최대 시프트를 만들고 골은 0 으로 눌러 산과 구멍이 서로 바뀌며 표면이 숨쉬는데, 시프트가 있는 칸과 없는 칸이 행마다 번갈아 흐르는 것이 이 판의 결입니다.

살아있는 돌출 — 깊이 파동으로 숨쉬는 표면
새 창에서 열기

08 · 살아있는 돌출 — 깊이 파동으로 숨쉬는 표면 — 측정 누적면적 31.176% · 움직인 프레임 23/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

ImageDataMath.sinrequestAnimationFrame

돔에 사인 두 겹을 얹은 깊이

  function depthAt(x, y, t) {
    var ph = t / CYCLE * Math.PI * 2;
    // 기저 — 중앙 돔(화면 가장자리는 0). 중심이 천천히 수직으로 bob 한다.
    var cx = W * 0.5, cy = H * 0.5 + Math.sin(ph) * H * 0.06;
    var rad = Math.min(W, H) * 0.74;
    var dx = (x - cx) / rad, dy = (y - cy) / rad;
    var d2 = dx * dx + dy * dy;
    if (d2 >= 1) return 0;
    var dome = Math.sqrt(1 - d2) * 0.60;
    // 파동 — 대각선 사인 + 직교 사인(파동면이 비틀린다), 파장 WAVELEN
    var wpos = (x + y * 0.6) / WAVELEN * Math.PI * 2;
    var wpos2 = (x * 0.4 - y) / (WAVELEN * 1.4) * Math.PI * 2;
    var wave = Math.sin(wpos - ph) * 0.5 + 0.5;
    var wave2 = Math.sin(wpos2 - ph * 0.7 + 2.1) * 0.5 + 0.5;
    var v = dome * (0.28 + 0.44 * wave + 0.28 * wave2);

09로고 히어로 씬 — 브랜드 글자를 숨긴 대문 장면

대문 히어로나 스플래시에 바로 쓸 조립 예시입니다. 오프스크린 캔버스에 찍은 워드마크(GODRICH, 96px)의 밝기를 깊이로 넘기고 그 위로 느린 사인 파동을 흘려 글자가 부풀었다 가라앉으며 숨쉬게 하고, 노이즈 판 위에 createLinearGradient 로 위아래 가장자리를 어둡게 얹어 비네팅 18% 의 대문 씬을 완성합니다.

로고 히어로 씬 — 브랜드 글자를 숨긴 대문 장면
새 창에서 열기

09 · 로고 히어로 씬 — 브랜드 글자를 숨긴 대문 장면 — 측정 누적면적 52.539% · 움직인 프레임 23/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

createLinearGradientImageDatarequestAnimationFrame

바닥 지형과 마크를 겹쳐 주는 깊이

  function depthAt(x, y, t) {
    if (!markMap) return 0;
    var ph = t / CYCLE * Math.PI * 2;
    var breathe = 0.55 + 0.45 * Math.sin(ph + 0.8);
    // 바닥 지면 — 판 전체에 아주 얕은 원거리 지형(1~2px)을 깐다. 마크 뒤 안개처럼
    // 숨쉬며, 노이즈 배경 전체가 느리게 재엮인다(마크만으로는 면적이 좁다).
    var fx = (x / W) * Math.PI * 2;
    var fw = 0.5 + 0.5 * Math.sin(fx * 1.5 - ph * 0.5);
    var fw2 = 0.5 + 0.5 * Math.sin(fx * 0.8 + y * 0.012 - ph * 0.34 + 2.0);
    var floorD = (0.085 * fw + 0.075 * fw2) * breathe;
    // 수직 bob — 마크를 천천히 떠오르게 한다(행마다 샘플이 흐른다)
    var bob = Math.round(Math.sin(ph) * H * 0.08);
    var sy = y - bob;
    if (sy < 0 || sy >= H) sy = y;
    var m = markMap[sy * W + x];
    if (!m || m <= 0) return floorD;
    // 느린 파동 — 마크를 대각선으로 가로지르는 사인이 글자를 부풀렸다 가라앉힌다
    var wpos = ((x + y * 0.5) / WAVELEN) * Math.PI * 2;
    var wave = Math.sin(wpos - ph) * 0.5 + 0.5;
    var wave2 = Math.sin(((x * 0.4 - y) / (WAVELEN * 1.3)) * Math.PI * 2 - ph * 0.6 + 1.4) * 0.5 + 0.5;
    var markD = m * (0.22 + 0.42 * wave + 0.36 * wave2) * breathe;
    return markD > floorD ? markD : floorD;
  }

MAXSHIFT 를 너무 키우면 무늬가 찢겨 이중으로 보이는 함정

아홉 판을 만들며 실제로 걸린 지점은 시프트 값 하나로 좁혀졌습니다 — 값을 늘려 입체감을 세게 뽑고 싶은 마음과 눈이 대응을 놓치지 않는 한계 사이의 줄다리기입니다. 이 한계를 넘어서면 어떤 판이든 같은 증상으로 깨집니다. MAXSHIFT 를 크게 잡으면 무늬가 찢겨 이중으로 보이는 것이 가장 자주 만나는 깨짐입니다. 이웃 낱알과의 대응이 무너지는 지점이 대략 18px 이상이라 이 편의 아홉 판은 10~16px 의 안정권에 있고, 22px 는 변형으로만 쓰는 값입니다. 노이즈 낱알을 매 프레임 새로 뽑으면 입체가 죽고, 세로 시프트를 섞는 순간 대응 자체가 성립하지 않습니다. 02 패턴 타일은 시프트 단과 타일 폭 100px 의 주기 관계를 맞대지 않으면 어긋남이 띠로 읽히지 않고, 03 글자 숨기기는 가는 글씨를 숨기면 시프트 뒤 형태가 녹습니다. 07 포인터 조각의 흔적을 즉시 반영하면 스친 자리가 번쩍 떠서 눈의 평행이 깨지므로 6초 감쇠를 지켜야 합니다. 이 편의 아홉 판은 노이즈 밑칠부터 시프트 루프까지 통째로 index.html 하나씩에 들어 있는데, 그 원본을 그대로 묶은 암호 zip 의 비밀번호는 9kzb6vjj 이고 풀면 본문의 코드 조각과 줄 단위로 나란히 대조해 보실 수 있습니다.

세 변형 중 하나는 시작부터 접근성 대응을 겸한다

값 하나를 바꾼 세 변형

이름 바꾼 값 느낌
돌출 깊이 01 원판 MAXSHIFT 10px → 22px 공이 코앞까지 튀어나와 보는 법을 몰라도 보이는 반면, 너무 올리면 무늬가 찢겨 이중으로 보인다
패턴 폭 02 타일 폭 100px → 140px 폭이 넓어질수록 눈을 더 멀리 늦춰야 하고 위화감이 커지며, 좁히면 반대로 아주 쉬워진다
파동 속도 08 사인 주기 2초 → 1초 숨쉬듯 부풀던 표면이 맥박처럼 뛰며 입체의 윤곽이 빠르게 달라진다

동작을 줄인 사용자에게 주는 장면

각 판은 시작할 때 prefers-reduced-motion: reduce 일치 여부를 검사해, 켜져 있으면 애니메이션 루프를 돌리지 않고 대표 장면 한 번을 그려 고정한 뒤 .is-still 클래스를 붙입니다. 정지 사용자에게도 빈 캔버스가 아니라 입체 한 장이 전달되는 것이 목적입니다.

손으로 만지는 판과 화면 낭독

05 와 07 이 wheel·pointermove 로 입력을 받고 나머지 판은 자동 재생이며, 모든 실험판에는 사람이 알 수 있는 aria-label 과 tabindex 가 붙고 깊이 px · 주기 초 같은 측정 메타가 화면 아래에 함께 표시됩니다.

이런 화면에서 쓴다 — 브랜드 대문과 제품 소개 배경

09 는 반복 무늬 속에 로고 글자를 깊이로만 숨긴 장면이라 랜딩 첫 화면에 그대로 얹으면 방문자가 초점을 맞추는 몇 초 동안 시선이 머뭅니다. 처음부터 글자가 보이지 않고 스스로 찾아야 뜨는 구조라 스크롤을 재촉하는 안내 문구 없이도 화면에 머무는 시간을 늘릴 수 있습니다.

04 는 사진의 밝기를 부조 높이로 바꿔 숨기므로 제품 소개나 회사 연혁 페이지의 배경 장식으로 쓰기에 맞고, 05 는 스크롤한 만큼 산맥이 드러나는 구조라 스크롤텔링 페이지의 중간 전환 구간에 끼워 넣기 좋습니다.

세 판 모두 노이즈 밑칠과 시프트 루프는 그대로 두고 깊이 지도를 만드는 부분만 로고·사진·지형 데이터로 바꿔 넣으면 됩니다.

매직아이 데모를 처음 붙일 때 자주 나오는 질문 FAQ

매직아이가 안 되는 사람도 이 데모에서 볼 수 있나요?

가능합니다. 06 보는법 가이드가 정확히 그 사람을 위한 판입니다. 시차 100px 만큼 벌어진 단서 두 점을 천천히 벌였다 모으는 연습으로 눈의 평행 감각을 익히고, 그래도 어려우면 버튼으로 풀이 지도를 회색 농담으로 겹쳐 어느 자리에 무엇이 숨었는지 확인할 수 있습니다.

매직아이 만들기를 이 코드로 처음해도 따라할 수 있나요?

가능합니다. 핵심은 매 행을 왼쪽에서 오른쪽으로 훑며 깊이가 있는 칸만 왼쪽에서 몇 픽셀 당겨 복사하는 한 겹인데, 01 판의 20여 줄이 그 전부입니다. zip 안에는 아홉 판을 같은 조건으로 다시 짜라고 시키는 지시 문서가 들어 있어 코드 도구에 붙여 넣으면 같은 화면부터 시작합니다. 그다음은 상수 블록의 MAXSHIFT · 주기 값을 하나씩 바꿔 보는 것이 가장 빠른 순서입니다.

왜 굵고 짧은 단어만 제대로 뜨나요?

시프트는 픽셀 단위의 정수라 글자의 살이 1~2px 로 가늘면 시프트 뒤 형태가 녹아 버리기 때문입니다. 몸통이 굵은 짧은 단어(SOLID · GODRICH)가 잘 뜨고, 가는 서체를 쓰고 싶으면 03 처럼 좌우 1px 평균으로 가장자리 낙차를 무디게 하거나 글자 크기를 올려 살을 두껍게 해야 합니다.

다시 보면 — 왜 평면 무늬에서 입체가 떠오르는가

두 눈은 조금 다른 자리에서 같은 장면을 봅니다. 스테레오그램 만들기의 출발점도 이 대응인데, 화면 뒤 편안히 초점을 두고 두 눈의 시선을 평행하게 늦추면 반복 무늬의 이웃한 낱알들이 각각 양쪽 눈에 들어가 대응이 맞물립니다. 이때 두 낱알 사이의 가로 거리가 벌어질수록 뇌는 그 무늬를 화면보다 가까운 쪽에 놓는데, 이 가로 거리가 시차이고 깊이 지도의 값을 행 안에서 몇 픽셀 밀어 붙이는 시프트로 직결됩니다. 같은 깊이면 같은 시프트, 깊은 곳은 큰 시프트 — 이 규칙 하나로 눈이 수평 방향의 밀기만 읽어 들이면 평면의 노이즈 속에서 부피가 솟습니다. 세로 방향으로는 아무 대응도 만들지 않는 것이 핵심이라, 시프트는 언제나 행마다 독립으로 왼쪽에서 오른쪽 한 방향으로만 일어납니다.

낱알이 프레임 사이에 같아야 하는 까닭

두 눈이 무늬의 대응을 기억하는 순간 입체가 뜹니다. 그래서 밑칠 해시는 좌표만 받아 시간을 모르고, 매 프레임 ImageData 를 다시 써도 같은 자리는 같은 낱알로 시작합니다.

더 읽을거리

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

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