반짝이는 포켓몬 카드, 어떻게 만들까
simeydotme(Simon Goellner)의 pokemon-cards-css를 보면, 실제 포켓몬 홀로 카드의 무지갯빛 반사가 순수 CSS로 재현돼 있다. 마우스를 움직이면 각도에 따라 빛이 흐르고, 반짝이는 하이라이트가 커서를 따라온다.
이 글은 그 원본 프로젝트(라이브 데모)의 기법을 뜯어보고 단순화해 재현한 것이다. 아래 데모들의 기법 출처는 simeydotme/pokemon-cards-css(MIT) 이고, 카드 이미지는 Pokémon TCG 자료(images.pokemontcg.io) 다. 포켓몬 관련 저작권은 The Pokémon Company / Nintendo에 있으며, 여기서는 학습·소개 목적으로만 사용한다.
비결은 이미지 한 장이 아니라, 여러 레이어를 겹치고 블렌드 모드로 섞는 것 이다. 아래 데모에서 레이어를 하나씩 켜고 끄며 어떻게 쌓이는지 직접 보자. 카드 위에서 커서를 움직이면(모바일은 터치로 문지르면) 빛이 반응한다.
- 홀로 (shine) - 무지개+스캔라인(color-dodge), 그 위에 바 패턴(hard-light)과 방사 하이라이트(luminosity)가 겹친다.
- 광원 (glare) - 포인터를 따라오는 radial-gradient 하이라이트를 overlay 로 얹는다.
- 마스크 (clip-path) - clip-path: inset(...) 으로 홀로를 카드의 일러스트 창에만 가둔다.
* 커서를 움직이거나(모바일은 터치로 문질러) 보세요. 기법: simeydotme/pokemon-cards-css(regular-holo) · 이미지: Pokémon TCG.
큰 그림: 포인터 → CSS 변수 → 레이어
효과의 뼈대는 단순하다. 포인터 위치를 CSS 변수로 만들고, 모든 레이어가 그 변수를 참조 한다.
card.addEventListener("pointermove", (e) => {
const r = card.getBoundingClientRect();
const px = (e.clientX - r.left) / r.width; // 0~1
const py = (e.clientY - r.top) / r.height; // 0~1
card.style.setProperty("--px", `${px * 100}%`);
card.style.setProperty("--py", `${py * 100}%`);
card.style.setProperty("--rx", `${(py - 0.5) * -22}deg`); // 틸트
card.style.setProperty("--ry", `${(px - 0.5) * 22}deg`);
});JS가 하는 일은 여기까지다. 빛의 계산과 색 섞기는 전부 CSS가 한다. JS는 좌표만 넘기고, 시각 효과는 선언적으로 CSS에 맡기는 구조가 이 기법의 핵심이다.
레이어 1 - 3D 틸트
먼저 카드를 실제 물체처럼 기울인다. perspective 안에서 포인터 편차만큼 rotateX/rotateY 를 준다.
.stage { perspective: 900px; }
.card {
transform: rotateX(var(--rx)) rotateY(var(--ry));
transform-style: preserve-3d;
}커서가 위로 가면 카드 윗면이 뒤로 젖혀지도록 rotateX 에 음수를 곱한다. 이 틸트가 있어야 뒤이어 흐르는 빛이 입체적으로 느껴진다.
레이어 2 - 홀로 무늬 (color-dodge)
홀로그램의 정체는 무지개 반복 그라디언트 + 스캔라인 이다. 여기에 블렌드 모드 color-dodge 를 걸면, 아래 레이어의 밝은 부분에서 색이 터지듯 발광 한다.
.shine {
background:
repeating-linear-gradient(110deg,
#ff7773, #ffed99, #8affc1, #7ff3ff, #c39fff, #ff7773),
repeating-linear-gradient(90deg,
rgba(0,0,0,.14) 0 1px, transparent 1px 4px); /* 스캔라인 */
background-blend-mode: overlay;
background-size: 300% 300%;
background-position: var(--bgx) var(--bgy); /* 틸트 따라 이동 */
mix-blend-mode: color-dodge;
filter: brightness(1.05) contrast(1.15) saturate(1.5);
}두 가지가 물리를 흉내 낸다.
background-position이동 - 틸트할 때 그라디언트를--bgx/--bgy로 밀어, 각도에 따라 다른 색이 보이게 한다. 실제 포일의 회절(각도마다 색이 바뀜)을 근사한다.color-dodge- 단순히 색을 얹는 게 아니라 아래층 밝기에 비례해 증폭한다. 그래서 밝은 부분만 강하게 빛나고 어두운 곳은 잠잠한, 진짜 금속 포일 같은 느낌이 난다.
블렌드 모드가 곧 광학 모델 이다. color-dodge 는 빛의 증폭을, overlay 는 대비 강조를 맡는다. 포토샵 레이어 모드와 같은 공식을 브라우저가 픽셀마다 계산해 준다.
레이어 3 - 광원 (포인터를 따라오는 반사점)
카드에 닿는 조명은 포인터 위치를 중심으로 한 radial-gradient 다. 커서를 따라 밝은 점이 미끄러진다.
.glare {
background: radial-gradient(circle at var(--px) var(--py),
rgba(255,255,255,.8) 0%, transparent 55%);
mix-blend-mode: overlay;
filter: brightness(0.9) contrast(1.4);
}at var(--px) var(--py) 한 줄이 핵심이다. 광원의 중심을 포인터로 묶어두면, 마우스가 곧 가상의 조명 이 된다. 이 하이라이트가 홀로 무늬 위에 겹치면서 "빛이 닿는 지점만 더 반짝이는" 자연스러운 반사가 완성된다.
레이어 4 - 마스크 (포일을 아트 영역에만 가두기)
실제 카드는 그림 부분만 홀로 처리돼 있고 테두리는 평범하다. 이걸 mask-image 로 재현한다. 홀로 레이어를 특정 모양(예: 안쪽 사각형)으로 오려내서 그 밖으로는 반짝임이 새어 나가지 않게 한다.
.shine {
/* content-box 안쪽만 남기고 바깥 테두리는 마스크로 제외 */
padding: 18px;
mask:
linear-gradient(#000 0 0) content-box,
linear-gradient(#000 0 0);
mask-composite: exclude;
}원본 프로젝트는 여기서 한발 더 나가, 카드 종류(일반/트레이너/스테이지)마다 다른 clip-path 로 홀로 영역을 다르게 잡는다. 위 데모에서 마스크 토글을 꺼 보면 반짝임이 카드 전체로 번지는 걸 볼 수 있다.
정리 - 겹침의 미학
| 레이어 | 기법 | 역할 |
|---|---|---|
| 아트 | 베이스 그라디언트/이미지 | 카드 그림 |
| 틸트 | perspective + rotateX/Y | 입체감 |
| 홀로 | 무지개 repeating-linear-gradient + color-dodge | 각도별 무지갯빛 |
| 광원 | radial-gradient at 포인터 + overlay | 따라오는 반사점 |
| 마스크 | mask-image / clip-path | 포일을 아트에 한정 |
이미지 한 장으로는 각도에 반응하는 빛을 만들 수 없다. 대신 반투명 레이어를 쌓고 블렌드 모드로 빛의 물리를 흉내 내면, 포인터 좌표 하나만으로 살아 있는 홀로그램이 된다.
실습 1 - 블렌드 모드 바꿔보기
데모의 홀로 레이어에서 mix-blend-mode 를 바꾸면 느낌이 어떻게 달라질지 예상해 보자. 아래에서 직접 전환해 보면 차이가 확 보인다.
mix-blend-mode: color-dodge
원본 값. 밝은 곳이 폭발적으로 발광하는 진짜 포일 느낌.
color-dodge → 밝은 곳이 강하게 발광(포일 느낌). screen → 전체적으로 부드럽게 밝아짐. overlay → 대비가 세짐. normal → 그냥 반투명 색이 얹혀 홀로 느낌이 사라진다. 블렌드 모드 하나가 "금속 포일 vs 셀로판" 을 가른다.
color-dodge(기본값):결과 = 아래색 ÷ (1 - 위색). 위 레이어가 밝을수록 아래층을 폭발적으로 밝힌다. 어두운 아트 위에서 무지개의 밝은 부분만 확 튀어, 금속 포일이 빛을 튕겨내는 느낌이 난다.screen:결과 = 1 - (1-아래)(1-위). 항상 밝아지지만 증폭은 없어, 전체가 균일하게 뿌옇게 밝아진다. 발광의 날카로움이 사라진다.overlay: 아래층이 밝으면screen, 어두우면multiply. 대비만 강해지고 무지개색은 탁해진다.normal: 그냥 반투명 색이 위에 얹힌다. 아래층과 상호작용이 없으니 "빛"이 아니라 "색 필름"처럼 보이고 홀로 느낌이 사라진다.
핵심은 홀로 느낌이 색이 아니라 "아래층과의 곱·나눗셈" 에서 나온다는 것. color-dodge 의 나눗셈이 있어야 밝은 지점만 과포화되며 반짝인다.
실습 2 - 광원을 포인터에서 떼어보기
radial-gradient(circle at var(--px) var(--py), ...) 에서 var(--px) var(--py) 를 고정값(예: 50% 30%)으로 바꾸면 어떻게 될까? 아래에서 두 방식을 번갈아 켜 보자.
광원이 포인터를 따라오니, 마우스가 곧 가상의 조명이 되어 진짜 반사처럼 느껴진다.
광원이 고정되면 카드를 아무리 기울여도 하이라이트가 그 자리에 붙어 있어, 조명이 카드에 붙박인 스티커처럼 느껴진다. 반사점이 포인터를 따라올 때만 뇌가 "저기 조명이 있고 카드가 그 아래에서 움직인다"고 해석한다. 상호작용의 사실감은 이 한 줄의 연동에서 나온다.
50% 30% 로 고정하면 하이라이트가 카드 좌우 중앙·위쪽 30% 지점에 못 박힌다. 카드를 기울여도 반사점이 안 움직이니, 빛이 카드 표면에 인쇄된 무늬처럼 보인다.
우리 눈은 "물체는 움직이는데 반사점은 제자리"인 상황을 진짜 반사로 받아들이지 않는다. 실제 광택 표면에서는 시점·각도가 바뀌면 하이라이트도 미끄러지기 때문이다. 그래서 at var(--px) var(--py) 로 반사점을 포인터(가상 광원)에 묶어야, 뇌가 비로소 3D 표면의 스페큘러 하이라이트로 해석한다.
정리하면 이 데모의 사실감은 두 연동에서 나온다 — 틸트(--rx/--ry)로 면의 각도를, 광원(--px/--py)으로 반사점 위치를 동시에 포인터에 묶는 것. 둘 중 하나만 고정해도 착시가 깨진다.
레퍼런스
- simeydotme/pokemon-cards-css (GitHub) - 원본 프로젝트와 전체 소스
- Pokémon Cards CSS 데모 - 라이브 데모
- Holographic Trading Card Effect - CSS-Tricks - 기법 소개
- MDN - mix-blend-mode · mask-image - 블렌드/마스크 레퍼런스