01로고 퍼즐 맞추기
네 개의 기하 로고 중 진열 카드와 같은 모양을 고르는 퀴즈판입니다. 고르면 정오가 표시되고 맞은 수가 쌓이며 다음 문제로 넘어갑니다.
쓰임이벤트 페이지 아이스브레이킹, 학습 퀴즈 첫 화면
clip-pathconic-gradientaria-live

<main class="stage">
<div class="lp is-demo" id="lp">
<header class="lp__bar">
<p class="lp__q"><span class="lp__qLabel">문제</span><b class="lp__qNum" id="lpQNum">1</b><span class="lp__qOf">/ 5</span></p>
<div class="lp__scoreWrap">
<p class="lp__score" id="lpScore" aria-live="polite"><span class="lp__scoreLabel">맞은 수</span><b class="lp__scoreNum" id="lpScoreNum">0</b></p>
<span class="lp__plus" aria-hidden="true">+1</span>
</div>
</header>
<div class="lp__body">
<div class="lp__card">
<span class="lp__mark lp__mark--big lp__mark--cut1" id="lpTarget" data-shape="hex" aria-hidden="true"></span>
<span class="lp__mark lp__mark--big lp__mark--cut2" data-shape="wedge" aria-hidden="true"></span>
<span class="lp__mark lp__mark--big lp__mark--cut3" data-shape="ring" aria-hidden="true"></span>
</div>
<div class="lp__tiles" id="lpTiles">
<button class="lp__tile" type="button" aria-label="보기 1" aria-keyshortcuts="1"><span class="lp__mark" data-shape="wedge" aria-hidden="true"></span><span class="lp__key" aria-hidden="true">1</span></button>
<button class="lp__tile" type="button" aria-label="보기 2" aria-keyshortcuts="2"><span class="lp__mark" data-shape="hex" aria-hidden="true"></span><span class="lp__key" aria-hidden="true">2</span></button>
<button class="lp__tile" type="button" aria-label="보기 3" aria-keyshortcuts="3"><span class="lp__mark" data-shape="chevron" aria-hidden="true"></span><span class="lp__key" aria-hidden="true">3</span></button>
<button class="lp__tile" type="button" aria-label="보기 4" aria-keyshortcuts="4"><span class="lp__mark" data-shape="ring" aria-hidden="true"></span><span class="lp__key" aria-hidden="true">4</span></button>
<span class="lp__ring" aria-hidden="true"></span>
</div>
</div>
<p class="lp__done" id="lpDone" role="status" hidden><b class="lp__doneMsg" id="lpDoneMsg">5문제 중 {n}개 정답</b><button class="lp__again" type="button" id="lpAgain">새 문제</button></p>
</div>
</main>@use "../../../../site/static/demo-tokens" as *;
// 로고 퍼즐 맞추기 — 진열 카드의 기하 마크와 같은 모양을 보기 넷에서 고른다.
// 마크는 전부 CSS 도형이다: clip-path polygon(육각·마름모·삼각·갈매기) · conic-gradient 부채꼴 · 링 두 개 겹침.
// 실제 브랜드 로고는 쓰지 않는다(상표). 정적 상태는 1번 문제가 그려진 완결된 판이고,
// 격자 미리보기(.is-demo)는 그 위에 하이라이트 링 순회 · 진열 마크 컷 교체 · 점수 칩 팝만 덧씌운다.
$duration: $dur-loop;
$easing: $ease-pop;
$color: $subject-blue-deep;
@include stage($stage-ink);
.lp { position: relative; display: flex; flex-direction: column; gap: $sp-3; width: 100%; max-width: 420px; height: 100%; }
.lp__bar { display: flex; align-items: center; justify-content: space-between; gap: $sp-2; }
.lp__q { display: inline-flex; align-items: baseline; gap: $sp-1; margin: 0; }
.lp__qLabel, .lp__qOf { font-weight: 500; font-size: clamp(9px, 2.3vw, 11px); color: rgba($subject-cream, .72); }
.lp__qNum { font-weight: 800; font-size: clamp(11px, 2.8vw, 14px); color: $subject-cream; }
.lp__scoreWrap { position: relative; display: inline-flex; }
.lp__score {
display: inline-flex; align-items: baseline; gap: $sp-1; margin: 0;
padding: $sp-1 $sp-3; border-radius: $r-pill;
background-color: $color; color: $subject-cream; // 진파랑 면 위 크림 글자
}
.lp__scoreLabel { font-weight: 500; font-size: clamp(9px, 2.3vw, 11px); }
.lp__scoreNum { font-weight: 800; font-size: clamp(11px, 2.8vw, 14px); }
.lp__plus {
position: absolute; top: 0; left: 50%; opacity: 0;
font-weight: 800; font-size: clamp(10px, 2.6vw, 12px); color: $tick-up; // 잉크 무대 위 상승색
}
.lp__body { flex: 1 1 auto; min-height: 0; display: grid; grid-template-columns: minmax(0, 118px) minmax(0, 1fr); gap: $sp-3; }
.lp__card {
position: relative; display: grid; place-items: center;
border-radius: $r-card; background-color: $stage-paper; color: $color;
}
.lp__tiles {
position: relative; display: grid;
grid-template-columns: repeat(2, 1fr); grid-template-rows: repeat(2, 1fr);
gap: $sp-2;
--lp-space: #{$sp-2};
}
.lp__tile {
position: relative; display: grid; place-items: center; width: 100%; height: 100%; padding: 0;
border: 0; border-radius: $r-control; background-color: $subject-cream; color: $color; cursor: pointer;
}
.lp__tile:focus-visible { outline: 2px solid $tick-up; outline-offset: 2px; }
.lp__key {
position: absolute; top: $sp-1; left: $sp-2;
font-weight: 700; font-size: clamp(8px, 2.2vw, 10px); color: rgba($subject-ink, .72); // 크림 면 위 흐린 잉크
}
.lp__tile.is-correct { box-shadow: 0 0 0 3px $tick-up; }
.lp__tile.is-wrong { box-shadow: 0 0 0 3px $tick-down; }
// ── 기하 마크 ────────────────────────────────────────────────
.lp__mark { display: block; width: clamp(28px, 8vw, 44px); height: clamp(28px, 8vw, 44px); background-color: currentColor; }
.lp__mark--big { width: clamp(56px, 17vw, 82px); height: clamp(56px, 17vw, 82px); }
.lp__mark[data-shape="hex"] { clip-path: polygon(50% 0, 94% 25%, 94% 75%, 50% 100%, 6% 75%, 6% 25%); }
.lp__mark[data-shape="diamond"] { clip-path: polygon(50% 2%, 98% 50%, 50% 98%, 2% 50%); }
.lp__mark[data-shape="tri"] { clip-path: polygon(50% 4%, 96% 92%, 4% 92%); }
.lp__mark[data-shape="chevron"] { clip-path: polygon(16% 0, 100% 50%, 16% 100%, 0 100%, 84% 50%, 0 0); }
.lp__mark[data-shape="wedge"] {
background-color: transparent; border-radius: 50%;
background-image: conic-gradient(from 205deg, currentColor 0deg 125deg, transparent 125deg 360deg);
}
.lp__mark[data-shape="ring"] { position: relative; background-color: transparent; }
.lp__mark[data-shape="ring"]::before,
.lp__mark[data-shape="ring"]::after {
content: ""; position: absolute; top: 50%; width: 68%; height: 68%; margin-top: -34%;
border-radius: 50%; box-shadow: inset 0 0 0 4px currentColor;
}
.lp__mark[data-shape="ring"]::before { left: 0; }
.lp__mark[data-shape="ring"]::after { right: 0; }
// ── 결과 ────────────────────────────────────────────────────
.lp__done {
position: absolute; inset: 0; z-index: 6; margin: 0;
display: flex; flex-direction: column; align-items: center; justify-content: center; gap: $sp-3;
background-color: rgba($subject-ink, .78);
}
.lp__done[hidden] { display: none; }
.lp__doneMsg {
padding: $sp-2 $sp-4; border-radius: $r-pill;
background-color: $color; color: $subject-cream;
font-weight: 800; font-size: clamp(11px, 2.8vw, 14px);
}
.lp__again {
padding: $sp-2 $sp-4; border: 0; border-radius: $r-pill;
background-color: $subject-cream; color: $subject-ink;
font-weight: 700; font-size: clamp(10px, 2.6vw, 12px); cursor: pointer;
}
.lp__again:focus-visible { outline: 2px solid $tick-up; outline-offset: 2px; }
// ── 격자 미리보기 ────────────────────────────────────────────
// 진열 마크 세 장을 겹쳐 두고 steps(1, end) 로 컷 교체한다 — 크로스페이드는 유령 프레임이 된다(237 교훈).
.lp__mark--cut2, .lp__mark--cut3 { display: none; position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); }
.lp.is-demo .lp__mark--cut2, .lp.is-demo .lp__mark--cut3 { display: block; }
.lp.is-demo .lp__mark--cut1 { animation-name: lpCut1; }
.lp.is-demo .lp__mark--cut2 { animation-name: lpCut2; }
.lp.is-demo .lp__mark--cut3 { animation-name: lpCut3; }
.lp.is-demo .lp__mark--cut1,
.lp.is-demo .lp__mark--cut2,
.lp.is-demo .lp__mark--cut3 {
animation-duration: $duration; animation-timing-function: steps(1, end);
animation-iteration-count: infinite; animation-fill-mode: backwards;
}
@keyframes lpCut1 { 0% { opacity: 1; } 30% { opacity: 0; } 60% { opacity: 0; } 88% { opacity: 1; } 100% { opacity: 1; } }
@keyframes lpCut2 { 0% { opacity: 0; } 30% { opacity: 1; } 60% { opacity: 0; } 100% { opacity: 0; } }
@keyframes lpCut3 { 0% { opacity: 0; } 60% { opacity: 1; } 88% { opacity: 0; } 100% { opacity: 0; } }
// 하이라이트 링 — 보기 타일 넷을 순회한다. 이동 거리는 자기 폭(= 타일 폭) 100% + 격자 간격
.lp__ring {
display: none; position: absolute; top: 0; left: 0;
width: calc(50% - var(--lp-space) / 2); height: calc(50% - var(--lp-space) / 2);
box-sizing: border-box; border: 3px solid $tick-up; border-radius: $r-control; pointer-events: none;
}
.lp.is-demo .lp__ring {
display: block;
animation-name: lpRingHop; animation-duration: $duration; animation-timing-function: $easing;
animation-iteration-count: infinite;
}
@keyframes lpRingHop {
0%, 8% { transform: translate(0, 0); }
22%, 30% { transform: translate(calc(100% + var(--lp-space)), 0); }
44%, 52% { transform: translate(calc(100% + var(--lp-space)), calc(100% + var(--lp-space))); }
66%, 74% { transform: translate(0, calc(100% + var(--lp-space))); }
88%, 100% { transform: translate(0, 0); }
}
// 점수 칩 팝 — 자기 중심에서 커진다
.lp.is-demo .lp__score {
animation-name: lpChipPop; animation-duration: $duration; animation-timing-function: $easing;
animation-iteration-count: infinite;
}
@keyframes lpChipPop {
0%, 26% { transform: scale(1); }
33% { transform: scale(1.14); }
42%, 56% { transform: scale(1); }
63% { transform: scale(1.14); }
72%, 100% { transform: scale(1); }
}
.lp.is-demo .lp__plus {
animation-name: lpPlusRise; animation-duration: $duration; animation-timing-function: $ease-out;
animation-iteration-count: infinite;
}
@keyframes lpPlusRise {
0%, 28% { opacity: 0; transform: translate(-50%, 0); }
34% { opacity: 1; transform: translate(-50%, -13px); }
46% { opacity: 0; transform: translate(-50%, -22px); }
58% { opacity: 0; transform: translate(-50%, 0); }
64% { opacity: 1; transform: translate(-50%, -13px); }
76%, 100% { opacity: 0; transform: translate(-50%, -22px); }
}
@media (max-width: 340px) {
.lp { gap: $sp-1; }
.lp__body { grid-template-columns: minmax(0, 84px) minmax(0, 1fr); gap: $sp-1; }
.lp__tiles { gap: $sp-1; --lp-space: #{$sp-1}; }
.lp__score { padding: 0 $sp-2; }
.lp__done { gap: $sp-2; }
.lp__doneMsg, .lp__again { padding: $sp-1 $sp-3; }
}
@media (prefers-reduced-motion: reduce) {
.lp.is-demo .lp__mark--cut1,
.lp.is-demo .lp__mark--cut2,
.lp.is-demo .lp__mark--cut3,
.lp.is-demo .lp__ring,
.lp.is-demo .lp__score,
.lp.is-demo .lp__plus { animation: none !important; }
.lp.is-demo .lp__mark--cut2,
.lp.is-demo .lp__mark--cut3 { display: none !important; }
}html, body {
margin: 0;
overflow: hidden;
}
.stage {
box-sizing: border-box;
min-height: 100vh;
display: grid;
place-items: center;
margin: 0;
background: #17141a;
font-family: -apple-system, "Segoe UI", "Noto Sans KR", sans-serif;
padding: clamp(8px, 4vw, 24px);
}
.lp {
position: relative;
display: flex;
flex-direction: column;
gap: 12px;
width: 100%;
max-width: 420px;
height: 100%;
}
.lp__bar {
display: flex;
align-items: center;
justify-content: space-between;
gap: 8px;
}
.lp__q {
display: inline-flex;
align-items: baseline;
gap: 4px;
margin: 0;
}
.lp__qLabel, .lp__qOf {
font-weight: 500;
font-size: clamp(9px, 2.3vw, 11px);
color: rgba(255, 247, 230, 0.72);
}
.lp__qNum {
font-weight: 800;
font-size: clamp(11px, 2.8vw, 14px);
color: #fff7e6;
}
.lp__scoreWrap {
position: relative;
display: inline-flex;
}
.lp__score {
display: inline-flex;
align-items: baseline;
gap: 4px;
margin: 0;
padding: 4px 12px;
border-radius: 999px;
background-color: #0a4fe8;
color: #fff7e6;
}
.lp__scoreLabel {
font-weight: 500;
font-size: clamp(9px, 2.3vw, 11px);
}
.lp__scoreNum {
font-weight: 800;
font-size: clamp(11px, 2.8vw, 14px);
}
.lp__plus {
position: absolute;
top: 0;
left: 50%;
opacity: 0;
font-weight: 800;
font-size: clamp(10px, 2.6vw, 12px);
color: #3ddc97;
}
.lp__body {
flex: 1 1 auto;
min-height: 0;
display: grid;
grid-template-columns: minmax(0, 118px) minmax(0, 1fr);
gap: 12px;
}
.lp__card {
position: relative;
display: grid;
place-items: center;
border-radius: 16px;
background-color: #ffffff;
color: #0a4fe8;
}
.lp__tiles {
position: relative;
display: grid;
grid-template-columns: repeat(2, 1fr);
grid-template-rows: repeat(2, 1fr);
gap: 8px;
--lp-space: 8px;
}
.lp__tile {
position: relative;
display: grid;
place-items: center;
width: 100%;
height: 100%;
padding: 0;
border: 0;
border-radius: 12px;
background-color: #fff7e6;
color: #0a4fe8;
cursor: pointer;
}
.lp__tile:focus-visible {
outline: 2px solid #3ddc97;
outline-offset: 2px;
}
.lp__key {
position: absolute;
top: 4px;
left: 8px;
font-weight: 700;
font-size: clamp(8px, 2.2vw, 10px);
color: rgba(23, 20, 26, 0.72);
}
.lp__tile.is-correct {
box-shadow: 0 0 0 3px #3ddc97;
}
.lp__tile.is-wrong {
box-shadow: 0 0 0 3px #ff7a5e;
}
.lp__mark {
display: block;
width: clamp(28px, 8vw, 44px);
height: clamp(28px, 8vw, 44px);
background-color: currentColor;
}
.lp__mark--big {
width: clamp(56px, 17vw, 82px);
height: clamp(56px, 17vw, 82px);
}
.lp__mark[data-shape=hex] {
clip-path: polygon(50% 0, 94% 25%, 94% 75%, 50% 100%, 6% 75%, 6% 25%);
}
.lp__mark[data-shape=diamond] {
clip-path: polygon(50% 2%, 98% 50%, 50% 98%, 2% 50%);
}
.lp__mark[data-shape=tri] {
clip-path: polygon(50% 4%, 96% 92%, 4% 92%);
}
.lp__mark[data-shape=chevron] {
clip-path: polygon(16% 0, 100% 50%, 16% 100%, 0 100%, 84% 50%, 0 0);
}
.lp__mark[data-shape=wedge] {
background-color: transparent;
border-radius: 50%;
background-image: conic-gradient(from 205deg, currentColor 0deg 125deg, transparent 125deg 360deg);
}
.lp__mark[data-shape=ring] {
position: relative;
background-color: transparent;
}
.lp__mark[data-shape=ring]::before,
.lp__mark[data-shape=ring]::after {
content: "";
position: absolute;
top: 50%;
width: 68%;
height: 68%;
margin-top: -34%;
border-radius: 50%;
box-shadow: inset 0 0 0 4px currentColor;
}
.lp__mark[data-shape=ring]::before {
left: 0;
}
.lp__mark[data-shape=ring]::after {
right: 0;
}
.lp__done {
position: absolute;
inset: 0;
z-index: 6;
margin: 0;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 12px;
background-color: rgba(23, 20, 26, 0.78);
}
.lp__done[hidden] {
display: none;
}
.lp__doneMsg {
padding: 8px 16px;
border-radius: 999px;
background-color: #0a4fe8;
color: #fff7e6;
font-weight: 800;
font-size: clamp(11px, 2.8vw, 14px);
}
.lp__again {
padding: 8px 16px;
border: 0;
border-radius: 999px;
background-color: #fff7e6;
color: #17141a;
font-weight: 700;
font-size: clamp(10px, 2.6vw, 12px);
cursor: pointer;
}
.lp__again:focus-visible {
outline: 2px solid #3ddc97;
outline-offset: 2px;
}
.lp__mark--cut2, .lp__mark--cut3 {
display: none;
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}
.lp.is-demo .lp__mark--cut2, .lp.is-demo .lp__mark--cut3 {
display: block;
}
.lp.is-demo .lp__mark--cut1 {
animation-name: lpCut1;
}
.lp.is-demo .lp__mark--cut2 {
animation-name: lpCut2;
}
.lp.is-demo .lp__mark--cut3 {
animation-name: lpCut3;
}
.lp.is-demo .lp__mark--cut1,
.lp.is-demo .lp__mark--cut2,
.lp.is-demo .lp__mark--cut3 {
animation-duration: 2s;
animation-timing-function: steps(1, end);
animation-iteration-count: infinite;
animation-fill-mode: backwards;
}
@keyframes lpCut1 {
0% {
opacity: 1;
}
30% {
opacity: 0;
}
60% {
opacity: 0;
}
88% {
opacity: 1;
}
100% {
opacity: 1;
}
}
@keyframes lpCut2 {
0% {
opacity: 0;
}
30% {
opacity: 1;
}
60% {
opacity: 0;
}
100% {
opacity: 0;
}
}
@keyframes lpCut3 {
0% {
opacity: 0;
}
60% {
opacity: 1;
}
88% {
opacity: 0;
}
100% {
opacity: 0;
}
}
.lp__ring {
display: none;
position: absolute;
top: 0;
left: 0;
width: calc(50% - var(--lp-space) / 2);
height: calc(50% - var(--lp-space) / 2);
box-sizing: border-box;
border: 3px solid #3ddc97;
border-radius: 12px;
pointer-events: none;
}
.lp.is-demo .lp__ring {
display: block;
animation-name: lpRingHop;
animation-duration: 2s;
animation-timing-function: cubic-bezier(0.2, 1.4, 0.4, 1);
animation-iteration-count: infinite;
}
@keyframes lpRingHop {
0%, 8% {
transform: translate(0, 0);
}
22%, 30% {
transform: translate(calc(100% + var(--lp-space)), 0);
}
44%, 52% {
transform: translate(calc(100% + var(--lp-space)), calc(100% + var(--lp-space)));
}
66%, 74% {
transform: translate(0, calc(100% + var(--lp-space)));
}
88%, 100% {
transform: translate(0, 0);
}
}
.lp.is-demo .lp__score {
animation-name: lpChipPop;
animation-duration: 2s;
animation-timing-function: cubic-bezier(0.2, 1.4, 0.4, 1);
animation-iteration-count: infinite;
}
@keyframes lpChipPop {
0%, 26% {
transform: scale(1);
}
33% {
transform: scale(1.14);
}
42%, 56% {
transform: scale(1);
}
63% {
transform: scale(1.14);
}
72%, 100% {
transform: scale(1);
}
}
.lp.is-demo .lp__plus {
animation-name: lpPlusRise;
animation-duration: 2s;
animation-timing-function: ease-out;
animation-iteration-count: infinite;
}
@keyframes lpPlusRise {
0%, 28% {
opacity: 0;
transform: translate(-50%, 0);
}
34% {
opacity: 1;
transform: translate(-50%, -13px);
}
46% {
opacity: 0;
transform: translate(-50%, -22px);
}
58% {
opacity: 0;
transform: translate(-50%, 0);
}
64% {
opacity: 1;
transform: translate(-50%, -13px);
}
76%, 100% {
opacity: 0;
transform: translate(-50%, -22px);
}
}
@media (max-width: 340px) {
.lp {
gap: 4px;
}
.lp__body {
grid-template-columns: minmax(0, 84px) minmax(0, 1fr);
gap: 4px;
}
.lp__tiles {
gap: 4px;
--lp-space: 4px;
}
.lp__score {
padding: 0 8px;
}
.lp__done {
gap: 8px;
}
.lp__doneMsg, .lp__again {
padding: 4px 12px;
}
}
@media (prefers-reduced-motion: reduce) {
.lp.is-demo .lp__mark--cut1,
.lp.is-demo .lp__mark--cut2,
.lp.is-demo .lp__mark--cut3,
.lp.is-demo .lp__ring,
.lp.is-demo .lp__score,
.lp.is-demo .lp__plus {
animation: none !important;
}
.lp.is-demo .lp__mark--cut2,
.lp.is-demo .lp__mark--cut3 {
display: none !important;
}
}// 로고 퍼즐 — 진열 카드의 기하 마크와 같은 모양을 보기 넷에서 고른다.
// 마크는 전부 CSS 도형(clip-path polygon · conic-gradient 부채꼴 · 링 겹침)이라 이미지가 0이다.
// 맞으면 초록 테두리와 점수, 틀리면 빨간 테두리와 정답 공개. 0.8초 뒤 다음 문제.
(function () {
var NEXT_MS = 800;
var DONE_TEXT = '5문제 중 {n}개 정답';
var QUESTIONS = [
{ answer: 'hex', tiles: ['wedge', 'hex', 'chevron', 'ring'] },
{ answer: 'chevron', tiles: ['chevron', 'diamond', 'ring', 'tri'] },
{ answer: 'wedge', tiles: ['tri', 'ring', 'wedge', 'hex'] },
{ answer: 'diamond', tiles: ['hex', 'chevron', 'tri', 'diamond'] },
{ answer: 'ring', tiles: ['diamond', 'wedge', 'ring', 'chevron'] }
];
var root = document.getElementById('lp');
var target = document.getElementById('lpTarget');
var qNumEl = document.getElementById('lpQNum');
var scoreEl = document.getElementById('lpScoreNum');
var doneEl = document.getElementById('lpDone');
var doneMsgEl = document.getElementById('lpDoneMsg');
var tiles = [].slice.call(document.querySelectorAll('.lp__tile'));
var qIndex = 0, score = 0, lock = false;
function render() {
var q = QUESTIONS[qIndex];
target.setAttribute('data-shape', q.answer);
qNumEl.textContent = String(qIndex + 1);
for (var i = 0; i < tiles.length; i++) {
tiles[i].classList.remove('is-correct', 'is-wrong');
tiles[i].firstElementChild.setAttribute('data-shape', q.tiles[i]);
}
}
function next() {
lock = false;
if (qIndex + 1 >= QUESTIONS.length) {
doneMsgEl.textContent = DONE_TEXT.replace('{n}', String(score));
doneEl.hidden = false;
return;
}
qIndex++;
render();
}
function pick(i) {
if (lock || !doneEl.hidden) { return; }
lock = true;
root.classList.remove('is-demo');
var answer = QUESTIONS[qIndex].answer;
var ok = tiles[i].firstElementChild.getAttribute('data-shape') === answer;
tiles[i].classList.add(ok ? 'is-correct' : 'is-wrong');
if (ok) { score++; scoreEl.textContent = String(score); }
else {
for (var k = 0; k < tiles.length; k++) {
if (tiles[k].firstElementChild.getAttribute('data-shape') === answer) { tiles[k].classList.add('is-correct'); }
}
}
window.setTimeout(next, NEXT_MS);
}
for (var t = 0; t < tiles.length; t++) {
(function (i) { tiles[i].addEventListener('click', function () { pick(i); }); })(t);
}
document.addEventListener('keydown', function (e) {
var n = '1234'.indexOf(e.key);
if (n >= 0) { e.preventDefault(); pick(n); }
});
document.getElementById('lpAgain').addEventListener('click', function () {
qIndex = 0; score = 0; lock = false;
scoreEl.textContent = '0';
doneEl.hidden = true;
render();
});
render();
})();// LogoMatchPuzzle — vanilla 판(run/080/demo/01-logo-match-puzzle/index.html) 의 React 포트.
// 진열 카드의 기하 마크와 같은 모양을 보기 넷에서 고른다. 마크는 전부 CSS 도형이라 이미지가 0이다.
// 상태는 훅이 들고(문제 번호·점수·고른 칸·끝남), 도형과 연출은 CSS 가 그린다.
import { useEffect, useRef, useState } from "react";
import type { CSSProperties } from "react";
import styles from "./LogoMatchPuzzle.module.scss";
export type LogoMatchPuzzleProps = {
duration?: number;
easing?: string;
accentColor?: string;
};
type Vars = CSSProperties & { "--duration"?: string; "--easing"?: string; "--color"?: string };
type ShapeKey = "hex" | "wedge" | "chevron" | "ring" | "diamond" | "tri";
type Question = { answer: ShapeKey; tiles: ShapeKey[] };
type KeyEvt = { key: string; preventDefault(): void };
type TimerBox = { current: number }; // ambient 의 useRef 는 T|null 을 돌려주므로 단언용
const NEXT_MS = 800;
const Q_LABEL = "문제";
const Q_OF = "/ 5";
const SCORE_LABEL = "맞은 수";
const TILE_LABEL = "보기 {n}";
const AGAIN_LABEL = "새 문제";
const DONE_TEXT = "5문제 중 {n}개 정답";
const QUESTIONS: Question[] = [
{ answer: "hex", tiles: ["wedge", "hex", "chevron", "ring"] },
{ answer: "chevron", tiles: ["chevron", "diamond", "ring", "tri"] },
{ answer: "wedge", tiles: ["tri", "ring", "wedge", "hex"] },
{ answer: "diamond", tiles: ["hex", "chevron", "tri", "diamond"] },
{ answer: "ring", tiles: ["diamond", "wedge", "ring", "chevron"] },
];
export default function LogoMatchPuzzle({
duration = 2000,
easing = "cubic-bezier(.2, 1.4, .4, 1)",
accentColor = "#0a4fe8",
}: LogoMatchPuzzleProps) {
const [qIndex, setQIndex] = useState(0);
const [score, setScore] = useState(0);
const [picked, setPicked] = useState(-1);
const [done, setDone] = useState(false);
const [demo, setDemo] = useState(true);
const timer = useRef<number>(0) as TimerBox;
const question = QUESTIONS[qIndex];
function pick(i: number) {
if (picked >= 0 || done) { return; }
setDemo(false);
setPicked(i);
if (question.tiles[i] === question.answer) { setScore((s) => s + 1); }
timer.current = window.setTimeout(() => {
setPicked(-1);
if (qIndex + 1 >= QUESTIONS.length) { setDone(true); }
else { setQIndex((n) => n + 1); }
}, NEXT_MS);
}
function again() {
setQIndex(0);
setScore(0);
setPicked(-1);
setDone(false);
}
useEffect(() => {
const onKey = (e: KeyEvt) => {
const n = "1234".indexOf(e.key);
if (n >= 0) { e.preventDefault(); pick(n); }
};
window.addEventListener("keydown", onKey);
return () => window.removeEventListener("keydown", onKey);
});
useEffect(() => () => window.clearTimeout(timer.current), []);
const vars: Vars = { "--duration": `${duration}ms`, "--easing": easing, "--color": accentColor };
const rootCls = [styles.root, demo ? styles.isDemo : ""].filter(Boolean).join(" ");
function tileCls(i: number) {
if (picked < 0) { return styles.tile; }
const isAnswer = question.tiles[i] === question.answer;
if (isAnswer) { return `${styles.tile} ${styles.isCorrect}`; }
return i === picked ? `${styles.tile} ${styles.isWrong}` : styles.tile;
}
return (
<main className={styles.stage}>
<div className={rootCls} style={vars}>
<header className={styles.bar}>
<p className={styles.q}>
<span className={styles.qLabel}>{Q_LABEL}</span>
<b className={styles.qNum}>{qIndex + 1}</b>
<span className={styles.qOf}>{Q_OF}</span>
</p>
<div className={styles.scoreWrap}>
<p className={styles.score} aria-live="polite">
<span className={styles.scoreLabel}>{SCORE_LABEL}</span>
<b className={styles.scoreNum}>{score}</b>
</p>
<span className={styles.plus} aria-hidden="true">+1</span>
</div>
</header>
<div className={styles.body}>
<div className={styles.card}>
<span className={`${styles.mark} ${styles.markBig} ${styles.cut1}`} data-shape={question.answer} aria-hidden="true" />
<span className={`${styles.mark} ${styles.markBig} ${styles.cut2}`} data-shape="wedge" aria-hidden="true" />
<span className={`${styles.mark} ${styles.markBig} ${styles.cut3}`} data-shape="ring" aria-hidden="true" />
</div>
<div className={styles.tiles}>
{question.tiles.map((shape, i) => (
<button
key={`${qIndex}-${i}`}
className={tileCls(i)}
type="button"
aria-label={TILE_LABEL.replace("{n}", String(i + 1))}
aria-keyshortcuts={String(i + 1)}
onClick={() => pick(i)}
>
<span className={styles.mark} data-shape={shape} aria-hidden="true" />
<span className={styles.tileKey} aria-hidden="true">{i + 1}</span>
</button>
))}
<span className={styles.ring} aria-hidden="true" />
</div>
</div>
{done ? (
<p className={styles.done} role="status">
<b className={styles.doneMsg}>{DONE_TEXT.replace("{n}", String(score))}</b>
<button className={styles.again} type="button" onClick={again}>{AGAIN_LABEL}</button>
</p>
) : null}
</div>
</main>
);
}01 로고 퍼즐 맞추기 — 측정 누적면적 17.351% · 움직인 프레임 19/23GODRICH 원작 · zip 에 SCSS·React 포함 · 웹 미니게임 만들기 9종 — 계산기·타이머·퍼즐 복붙 코드
이 데모가 속한 글
웹 미니게임 만들기 9종 — 계산기·타이머·퍼즐 복붙 코드웹 미니게임 만들기는 브라우저에서 바로 손이 가는 작은 놀잇감과 업무 도구를 HTML·CSS·JS 로 짜는 일입니다. 라이브러리 없이 파일 하나로 도는 아홉 개를 아래 격자에 늘어놓았고, 지금 그대로 움직입니다. 이 글의 데모 전부 보기


