똑똑, 문을 눌러 주세요

ZUNA'S DEN 0

STAND BY · 곧 시작할게요

MADOROME
ZUNA

마도로메 주나의 개집에 오신 걸 환영해요. 쓰다듬고, 뼈다귀 던지고, 스티커 하나 뽑아 가세요.

0번 쓰다듬음

01 · ACRYLIC STAND

아크릴 스탠디 선반

마우스를 올리면 빛이 돌고, 툭 치면 흔들려요. 끌어서 순서를 바꿀 수 있어요.

좌우 화살표 키로 자리를 바꿔요.

02 · POSTER WALL

포스터 벽

포스터는 끌어서 옮기고, 누르면 크게 봐요. 잠긴 그림은 뼈다귀를 모으면 열려요.

03 · FETCH!

뼈다귀 던지기

뼈다귀를 잡아당겼다 놓으면 날아가요. 주나가 받으면 1개 적립.

뼈다귀를 기다리는 주나

당겨서 던지기

    04 · ON AIR

    방송 화면

    방송에서 쓰는 화면들이에요. 탭을 눌러 바꿔 보세요.

    곧 시작할게요 대기 화면

    05 · GACHA

    스티커 뽑기

    손잡이를 돌려 캡슐을 뽑고, 모은 스티커는 아래 판에 붙여요.

    아직 뽑은 스티커가 없어요

      스티커를 누르면 여기 붙어요

      06 · ARCADE

      오락실

      PC용으로 만든 Stealth Arcade를 옮겨 왔어요. 규칙은 원작 그대로, 판을 누르면 키보드로 조작해요.

      뿌요뿌요 · 테트리스 · 스도쿠 · 지뢰찾기

      PC에서는 Esc 한 번에 사라지는 원본도 있어요. Windows용 받기 (24.9MB)

      07 · DESK

      작업대

      계산기, 스톱워치, 디데이. 진짜로 쓰라고 둔 거예요. 값은 이 브라우저에 남아요.

      계산기

      스톱워치

      00:00.00

        디데이

        D-?

        며칠 뒤는 언제?

        부터 일 뒤

        인사하는 주나

        또 놀러 와!

        08 · COLOPHON

        만든 법

        이 방에 들어간 기술을 엔지니어용으로 정리했어요. 프레임워크도 빌드도 없이 손으로 짰어요.

        $ cat den.toml
        framework = "none"          # vanilla JS, native ES modules
        build     = "none"          # 쓴 파일이 그대로 배포됨
        deps      = 0
        modules   = { core = 15, lazy = 4 }   # 게임 4종은 import()
        loc       = { js = 5201, css = 1451, html = 352 }
        art       = { files = 36, format = "webp", total = "2.7MB" }
        state     = "localStorage (den:*)"   # 서버 없음
        fonts     = ["Bebas Neue", "Jua", "Gowun Dodum"]
        host      = "Vercel static"

        runtime

        • top-level await art.json 그림 목록을 받은 뒤에 방마다 init*(). 없는 그림은 원본 스탠디로 대체돼요 (pick()).main.js · art.js
        • import() 오락실 게임은 탭을 처음 열 때 JS와 CSS를 Promise.all로 같이 받아요. 스타일 없는 판이 번쩍이지 않아요.arcade.js
        • pre-paint script <head> 인라인 스크립트가 첫 페인트 전에 html.intro-on을 정해서, 문 뒤의 방이 비치지 않아요.index.html
        • safe storage localStorage를 try/catch로 감싼 load/save. 시크릿 창에서도 페이지는 그대로 돌아가요.store.js
        • media gates (pointer: fine), (prefers-reduced-motion)를 한 번 읽어 모든 모듈이 공유해요.store.js

        motion & physics

        • damped spring 아크릴 스탠디 흔들림. a = −kx − cv를 semi-implicit Euler로 적분, dt는 1/30s로 자름.physics.js
        • ballistic solve 뼈다귀 착지점을 y(t) = ground 2차방정식으로 미리 풀고, 벽 반사는 삼각파로 접어 x를 구해요. 주나는 ±50px 틀리게 예측해서 가끔 놓쳐요.fetch.js
        • FLIP 스탠디 순서를 바꿀 때 DOM을 먼저 옮기고, 이웃은 이전 자리에서 미끄러져 와요. 드래그 중엔 rect를 먼저 다 읽고 한 번만 써요.shelf.js
        • Pointer Events setPointerCapture + 6px 데드존으로 탭과 드래그를 구분하는 공용 drag().physics.js
        • WAAPI 하트·뼈다귀·발바닥 파티클은 el.animate(), .finished에서 노드를 지워요.fx.js
        • paw trail 커서가 64px 갈 때마다 진행 방향의 법선으로 좌우 발을 번갈아 찍어요.fx.js
        • parallax 레이어별 data-depth, lerp 0.08로 따라가다 수렴하면 rAF를 멈춰요. 폰에선 deviceorientation.room.js
        • holo acrylic 포인터 위치를 CSS 변수 --rx/--ry/--hx/--hy로 넘기고, perspective + preserve-3d + color-dodge 광택.shelf.js · den.css

        image pipeline prep_den_art.py · Pillow + NumPy + OpenCV

        1. source VTuberStudio 아트 폴더의 PNG. 로고·포스터·방송 화면·방 배경, 포즈 9종, 표정 7종, 일러스트 7종.
        2. chroma key 마젠타(#FF00FF) 배경 스탠디. α = 1 − clip((min(r,b) − g − 60) / 120), 반투명 가장자리에서 마젠타 spill을 60% 빼요.
        3. flood-fill cutout 흰·회색 배경 포즈. 테두리 픽셀의 중앙값을 배경색으로 잡고 가장자리에서만 cv2.floodFill. 어두운 선(<110)을 5×5로 팽창시켜 벽으로 쓰니까, 선 안쪽의 하얀 머리카락은 안 지워져요.
        4. feather 알파를 1px 깎고(erode) 3×3 Gaussian으로 경계를 부드럽게.
        5. crop · resize getbbox()로 여백 자르기, 표정 컷은 상반신 50%만. 긴 변 512–1920px로 Lanczos 축소.
        6. encode WebP quality=82, method=6.
        7. manifest data/art.json에 {name: [w, h]}. 페이지가 이걸로 width/height를 채워 레이아웃 밀림이 없어요.

        그림 파일이 아닌 장식은 전부 코드예요. 뼈다귀·발바닥·하트는 인라인 SVG <symbol> 스프라이트, 할로윈 박쥐는 SVG path, 포스터 벽 하프톤과 선반 점무늬는 radial-gradient, 모니터 받침은 clip-path.

        arcade & desk

        • tetris SRS 킥 테이블, 7-bag, hold, ghost, 180° 회전, lock delay 0.5s / 15 resets, 3-corner T-spin, B2B, combo, perfect clear.arcade/tetris.js
        • puyo 원작 상태 기계(play → drop → settle → pop → garbage), 연쇄·색·그룹 보너스 표, margin time, wall/floor kick, quick turn.arcade/puyo.js
        • sudoku 원작 Python 생성기로 미리 만든 72문제(6단계 × 12). 난이도는 풀이기가 쓴 기법(X-Wing … forcing chain)으로 매겼어요.data/sudoku.json
        • minesweeper winmine식 첫 클릭 보정, 깃발 → 물음표, chording, 420ms 롱프레스.arcade/mine.js
        • lifecycle 화면 밖이거나 탭이 숨으면 일시정지(IntersectionObserver + visibilitychange), 판 크기는 ResizeObserver.
        • desk 계산기는 토큰 + 연산자 우선순위, 스톱워치는 시작 시각 저장이라 새로고침해도 이어져요. 디데이는 Date.UTC로 일수를 세서 서머타임과 무관.tools.js

        a11y & perf

        • inert 문이 열려 있는 동안 뒤 페이지는 Tab과 스크린리더에서 빠져요. Esc로 건너뛰기.
        • roving tabindex 방송·오락실 탭은 ←/→, Home/End. 스탠디는 화살표로 순서 바꾸기.
        • ARIA grid 스도쿠·지뢰찾기 판은 aria-rowindex/colindex, 결과는 aria-live로 읽어 줘요.
        • <dialog> 라이트박스는 showModal() + ::backdrop blur. 포커스 가두기는 브라우저 몫.
        • reduced motion 반복 애니메이션은 멈추고, 한 번짜리 피드백은 0.2s 페이드로 바꿔요. 통째로 끄지 않아요.
        • IntersectionObserver 지금 보이는 방을 내비에 표시(rootMargin −40% / −55%), 가짜 채팅과 패럴랙스는 화면 밖에서 쉬어요.
        • loading 첫 화면 그림만 fetchpriority="high", 나머지는 lazy. 한국어는 word-break: keep-all + text-wrap: pretty.