
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 awaitart.json그림 목록을 받은 뒤에 방마다init*(). 없는 그림은 원본 스탠디로 대체돼요 (pick()).main.js · art.jsimport()오락실 게임은 탭을 처음 열 때 JS와 CSS를Promise.all로 같이 받아요. 스타일 없는 판이 번쩍이지 않아요.arcade.jspre-paint script<head>인라인 스크립트가 첫 페인트 전에html.intro-on을 정해서, 문 뒤의 방이 비치지 않아요.index.htmlsafe storagelocalStorage를try/catch로 감싼load/save. 시크릿 창에서도 페이지는 그대로 돌아가요.store.jsmedia gates(pointer: fine),(prefers-reduced-motion)를 한 번 읽어 모든 모듈이 공유해요.store.js
motion & physics
damped spring아크릴 스탠디 흔들림.a = −kx − cv를 semi-implicit Euler로 적분,dt는 1/30s로 자름.physics.jsballistic solve뼈다귀 착지점을y(t) = ground2차방정식으로 미리 풀고, 벽 반사는 삼각파로 접어 x를 구해요. 주나는 ±50px 틀리게 예측해서 가끔 놓쳐요.fetch.jsFLIP스탠디 순서를 바꿀 때 DOM을 먼저 옮기고, 이웃은 이전 자리에서 미끄러져 와요. 드래그 중엔 rect를 먼저 다 읽고 한 번만 써요.shelf.jsPointer EventssetPointerCapture+ 6px 데드존으로 탭과 드래그를 구분하는 공용drag().physics.jsWAAPI하트·뼈다귀·발바닥 파티클은el.animate(),.finished에서 노드를 지워요.fx.jspaw trail커서가 64px 갈 때마다 진행 방향의 법선으로 좌우 발을 번갈아 찍어요.fx.jsparallax레이어별data-depth, lerp 0.08로 따라가다 수렴하면 rAF를 멈춰요. 폰에선deviceorientation.room.jsholo acrylic포인터 위치를 CSS 변수--rx/--ry/--hx/--hy로 넘기고,perspective+preserve-3d+color-dodge광택.shelf.js · den.css
image pipeline prep_den_art.py · Pillow + NumPy + OpenCV
- source VTuberStudio 아트 폴더의 PNG. 로고·포스터·방송 화면·방 배경, 포즈 9종, 표정 7종, 일러스트 7종.
- chroma key 마젠타(#FF00FF) 배경 스탠디.
α = 1 − clip((min(r,b) − g − 60) / 120), 반투명 가장자리에서 마젠타 spill을 60% 빼요. - flood-fill cutout 흰·회색 배경 포즈. 테두리 픽셀의 중앙값을 배경색으로 잡고 가장자리에서만
cv2.floodFill. 어두운 선(<110)을 5×5로 팽창시켜 벽으로 쓰니까, 선 안쪽의 하얀 머리카락은 안 지워져요. - feather 알파를 1px 깎고(
erode) 3×3 Gaussian으로 경계를 부드럽게. - crop · resize
getbbox()로 여백 자르기, 표정 컷은 상반신 50%만. 긴 변 512–1920px로 Lanczos 축소. - encode WebP
quality=82, method=6. - manifest
data/art.json에{name: [w, h]}. 페이지가 이걸로width/height를 채워 레이아웃 밀림이 없어요.
그림 파일이 아닌 장식은 전부 코드예요. 뼈다귀·발바닥·하트는 인라인 SVG <symbol> 스프라이트, 할로윈 박쥐는 SVG path, 포스터 벽 하프톤과 선반 점무늬는 radial-gradient, 모니터 받침은 clip-path.
arcade & desk
tetrisSRS 킥 테이블, 7-bag, hold, ghost, 180° 회전, lock delay 0.5s / 15 resets, 3-corner T-spin, B2B, combo, perfect clear.arcade/tetris.jspuyo원작 상태 기계(play → drop → settle → pop → garbage), 연쇄·색·그룹 보너스 표, margin time, wall/floor kick, quick turn.arcade/puyo.jssudoku원작 Python 생성기로 미리 만든 72문제(6단계 × 12). 난이도는 풀이기가 쓴 기법(X-Wing … forcing chain)으로 매겼어요.data/sudoku.jsonminesweeperwinmine식 첫 클릭 보정, 깃발 → 물음표, chording, 420ms 롱프레스.arcade/mine.jslifecycle화면 밖이거나 탭이 숨으면 일시정지(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()+::backdropblur. 포커스 가두기는 브라우저 몫.reduced motion반복 애니메이션은 멈추고, 한 번짜리 피드백은 0.2s 페이드로 바꿔요. 통째로 끄지 않아요.IntersectionObserver지금 보이는 방을 내비에 표시(rootMargin −40% / −55%), 가짜 채팅과 패럴랙스는 화면 밖에서 쉬어요.loading첫 화면 그림만fetchpriority="high", 나머지는lazy. 한국어는word-break: keep-all+text-wrap: pretty.