03モグラたたき
3×3の穴からモグラがランダムに飛び出し、出ている間にたたくと得点が入り、制限時間が減っていく。
使いどころイベントページの景品ルーレット代わり、子どもの目を引くセクション
@keyframestranslateYpointerdown

<main class="stage">
<div class="wg is-demo" id="wg">
<header class="wg__bar">
<p class="wg__title">モグラたたき</p>
<span class="wg__best" aria-label="最高記録">
<svg class="wg__trophy" viewBox="0 0 256 256" aria-hidden="true" focusable="false"><g fill="currentColor"><path d="M200 48v63.1c0 39.7-31.75 72.6-71.45 72.9A72 72 0 0 1 56 112V48Z" opacity=".2"/><path d="M232 64h-24V48a8 8 0 0 0-8-8H56a8 8 0 0 0-8 8v16H24A16 16 0 0 0 8 80v16a40 40 0 0 0 40 40h3.65A80.13 80.13 0 0 0 120 191.61V216H96a8 8 0 0 0 0 16h64a8 8 0 0 0 0-16h-24v-24.42c31.94-3.23 58.44-25.64 68.08-55.58H208a40 40 0 0 0 40-40V80a16 16 0 0 0-16-16M48 120a24 24 0 0 1-24-24V80h24v32q0 4 .39 8Zm144-8.9c0 35.52-29 64.64-64 64.9a64 64 0 0 1-64-64V56h128ZM232 96a24 24 0 0 1-24 24h-.5a82 82 0 0 0 .5-8.9V80h24Z"/></g></svg>
<b class="wg__num" id="wgBest">0</b>
</span>
<button class="wg__restart" id="wgRestart" type="button" aria-label="もう一度">
<svg viewBox="0 0 256 256" aria-hidden="true" focusable="false"><g fill="currentColor"><path d="M216 128a88 88 0 1 1-88-88a88 88 0 0 1 88 88" opacity=".2"/><path d="M240 56v48a8 8 0 0 1-8 8h-48a8 8 0 0 1 0-16h27.4l-26.59-24.36l-.25-.24a80 80 0 1 0-1.67 114.78a8 8 0 0 1 11 11.63A95.44 95.44 0 0 1 128 224h-1.32a96 96 0 1 1 69.07-164L224 85.8V56a8 8 0 1 1 16 0"/></g></svg>
</button>
</header>
<div class="wg__meter">
<span class="wg__clock">
<svg class="wg__timer" viewBox="0 0 256 256" aria-hidden="true" focusable="false"><g fill="currentColor"><path d="M216 136a88 88 0 1 1-88-88a88 88 0 0 1 88 88" opacity=".2"/><path d="M128 40a96 96 0 1 0 96 96a96.11 96.11 0 0 0-96-96m0 176a80 80 0 1 1 80-80a80.09 80.09 0 0 1-80 80m45.66-125.66a8 8 0 0 1 0 11.32l-40 40a8 8 0 0 1-11.32-11.32l40-40a8 8 0 0 1 11.32 0M96 16a8 8 0 0 1 8-8h48a8 8 0 0 1 0 16h-48a8 8 0 0 1-8-8"/></g></svg>
<b class="wg__num" id="wgClock">20.0</b><span class="wg__unit">秒</span>
</span>
<span class="wg__track"><span class="wg__fill" id="wgFill"></span></span>
<span class="wg__score"><b class="wg__num wg__num--score" id="wgScore">0</b><span class="wg__unit">点</span></span>
</div>
<div class="wg__field" id="wgField">
<button class="wg__hole" type="button" aria-label="穴1" style="--i: 0">
<span class="wg__window"><span class="wg__mole"><svg viewBox="0 0 256 256" aria-hidden="true" focusable="false"><g fill="currentColor"><path d="M208 188a44 44 0 0 1-80 25.3a44 44 0 1 1-63.71-59.48a64 64 0 0 1 127.42 0A43.9 43.9 0 0 1 208 188" opacity=".2"/><path d="M112 164a12 12 0 1 1-12-12a12 12 0 0 1 12 12m44-12a12 12 0 1 0 12 12a12 12 0 0 0-12-12m60 36a52 52 0 0 1-88 37.52a52 52 0 1 1-71.28-75.72A71.6 71.6 0 0 1 63 129C44 91.1 32.53 40.76 45.64 19.08A22 22 0 0 1 65.06 8c14.12 0 26 11.89 36.44 36.36c6.22 14.62 10.85 31.32 14 44.74a71.8 71.8 0 0 1 25 0c3.13-13.42 7.76-30.12 14-44.74C164.9 19.89 176.82 8 190.94 8a22 22 0 0 1 19.42 11.08C223.47 40.76 212 91.1 193 129a71.6 71.6 0 0 1 6.26 20.76A51.77 51.77 0 0 1 216 188m-60.11-94.37a71.7 71.7 0 0 1 26.88 19.64a218.5 218.5 0 0 0 14.83-46.19c3.49-18.13 3.15-33-.93-39.72a6 6 0 0 0-5.73-3.36c-6.61 0-14.52 9.7-21.72 26.62c-5.93 13.94-10.35 30.12-13.33 43.01m-82.66 19.64a71.7 71.7 0 0 1 26.88-19.64c-3-12.89-7.4-29.07-13.33-43C79.58 33.7 71.67 24 65.06 24a6 6 0 0 0-5.73 3.36C55.25 34.1 54.91 49 58.4 67.08a218.5 218.5 0 0 0 14.83 46.19M200 188a35.87 35.87 0 0 0-13.34-28a8 8 0 0 1-2.92-5.45a56 56 0 0 0-111.48 0a8 8 0 0 1-2.92 5.45a36 36 0 1 0 47.28 54.21l-9.74-8.09a8 8 0 1 1 10.24-12.3L128 202.9l10.88-9.05a8 8 0 1 1 10.24 12.3l-9.74 8.09A36 36 0 0 0 200 188"/></g></svg></span></span>
<span class="wg__rim" aria-hidden="true"></span>
<span class="wg__flash" aria-hidden="true"></span>
</button>
<button class="wg__hole" type="button" aria-label="穴2" style="--i: 1">
<span class="wg__window"><span class="wg__mole"><svg viewBox="0 0 256 256" aria-hidden="true" focusable="false"><g fill="currentColor"><path d="M208 188a44 44 0 0 1-80 25.3a44 44 0 1 1-63.71-59.48a64 64 0 0 1 127.42 0A43.9 43.9 0 0 1 208 188" opacity=".2"/><path d="M112 164a12 12 0 1 1-12-12a12 12 0 0 1 12 12m44-12a12 12 0 1 0 12 12a12 12 0 0 0-12-12m60 36a52 52 0 0 1-88 37.52a52 52 0 1 1-71.28-75.72A71.6 71.6 0 0 1 63 129C44 91.1 32.53 40.76 45.64 19.08A22 22 0 0 1 65.06 8c14.12 0 26 11.89 36.44 36.36c6.22 14.62 10.85 31.32 14 44.74a71.8 71.8 0 0 1 25 0c3.13-13.42 7.76-30.12 14-44.74C164.9 19.89 176.82 8 190.94 8a22 22 0 0 1 19.42 11.08C223.47 40.76 212 91.1 193 129a71.6 71.6 0 0 1 6.26 20.76A51.77 51.77 0 0 1 216 188m-60.11-94.37a71.7 71.7 0 0 1 26.88 19.64a218.5 218.5 0 0 0 14.83-46.19c3.49-18.13 3.15-33-.93-39.72a6 6 0 0 0-5.73-3.36c-6.61 0-14.52 9.7-21.72 26.62c-5.93 13.94-10.35 30.12-13.33 43.01m-82.66 19.64a71.7 71.7 0 0 1 26.88-19.64c-3-12.89-7.4-29.07-13.33-43C79.58 33.7 71.67 24 65.06 24a6 6 0 0 0-5.73 3.36C55.25 34.1 54.91 49 58.4 67.08a218.5 218.5 0 0 0 14.83 46.19M200 188a35.87 35.87 0 0 0-13.34-28a8 8 0 0 1-2.92-5.45a56 56 0 0 0-111.48 0a8 8 0 0 1-2.92 5.45a36 36 0 1 0 47.28 54.21l-9.74-8.09a8 8 0 1 1 10.24-12.3L128 202.9l10.88-9.05a8 8 0 1 1 10.24 12.3l-9.74 8.09A36 36 0 0 0 200 188"/></g></svg></span></span>
<span class="wg__rim" aria-hidden="true"></span>
<span class="wg__flash" aria-hidden="true"></span>
</button>
<button class="wg__hole" type="button" aria-label="穴3" style="--i: 2">
<span class="wg__window"><span class="wg__mole"><svg viewBox="0 0 256 256" aria-hidden="true" focusable="false"><g fill="currentColor"><path d="M208 188a44 44 0 0 1-80 25.3a44 44 0 1 1-63.71-59.48a64 64 0 0 1 127.42 0A43.9 43.9 0 0 1 208 188" opacity=".2"/><path d="M112 164a12 12 0 1 1-12-12a12 12 0 0 1 12 12m44-12a12 12 0 1 0 12 12a12 12 0 0 0-12-12m60 36a52 52 0 0 1-88 37.52a52 52 0 1 1-71.28-75.72A71.6 71.6 0 0 1 63 129C44 91.1 32.53 40.76 45.64 19.08A22 22 0 0 1 65.06 8c14.12 0 26 11.89 36.44 36.36c6.22 14.62 10.85 31.32 14 44.74a71.8 71.8 0 0 1 25 0c3.13-13.42 7.76-30.12 14-44.74C164.9 19.89 176.82 8 190.94 8a22 22 0 0 1 19.42 11.08C223.47 40.76 212 91.1 193 129a71.6 71.6 0 0 1 6.26 20.76A51.77 51.77 0 0 1 216 188m-60.11-94.37a71.7 71.7 0 0 1 26.88 19.64a218.5 218.5 0 0 0 14.83-46.19c3.49-18.13 3.15-33-.93-39.72a6 6 0 0 0-5.73-3.36c-6.61 0-14.52 9.7-21.72 26.62c-5.93 13.94-10.35 30.12-13.33 43.01m-82.66 19.64a71.7 71.7 0 0 1 26.88-19.64c-3-12.89-7.4-29.07-13.33-43C79.58 33.7 71.67 24 65.06 24a6 6 0 0 0-5.73 3.36C55.25 34.1 54.91 49 58.4 67.08a218.5 218.5 0 0 0 14.83 46.19M200 188a35.87 35.87 0 0 0-13.34-28a8 8 0 0 1-2.92-5.45a56 56 0 0 0-111.48 0a8 8 0 0 1-2.92 5.45a36 36 0 1 0 47.28 54.21l-9.74-8.09a8 8 0 1 1 10.24-12.3L128 202.9l10.88-9.05a8 8 0 1 1 10.24 12.3l-9.74 8.09A36 36 0 0 0 200 188"/></g></svg></span></span>
<span class="wg__rim" aria-hidden="true"></span>
<span class="wg__flash" aria-hidden="true"></span>
</button>
<button class="wg__hole" type="button" aria-label="穴4" style="--i: 3">
<span class="wg__window"><span class="wg__mole"><svg viewBox="0 0 256 256" aria-hidden="true" focusable="false"><g fill="currentColor"><path d="M208 188a44 44 0 0 1-80 25.3a44 44 0 1 1-63.71-59.48a64 64 0 0 1 127.42 0A43.9 43.9 0 0 1 208 188" opacity=".2"/><path d="M112 164a12 12 0 1 1-12-12a12 12 0 0 1 12 12m44-12a12 12 0 1 0 12 12a12 12 0 0 0-12-12m60 36a52 52 0 0 1-88 37.52a52 52 0 1 1-71.28-75.72A71.6 71.6 0 0 1 63 129C44 91.1 32.53 40.76 45.64 19.08A22 22 0 0 1 65.06 8c14.12 0 26 11.89 36.44 36.36c6.22 14.62 10.85 31.32 14 44.74a71.8 71.8 0 0 1 25 0c3.13-13.42 7.76-30.12 14-44.74C164.9 19.89 176.82 8 190.94 8a22 22 0 0 1 19.42 11.08C223.47 40.76 212 91.1 193 129a71.6 71.6 0 0 1 6.26 20.76A51.77 51.77 0 0 1 216 188m-60.11-94.37a71.7 71.7 0 0 1 26.88 19.64a218.5 218.5 0 0 0 14.83-46.19c3.49-18.13 3.15-33-.93-39.72a6 6 0 0 0-5.73-3.36c-6.61 0-14.52 9.7-21.72 26.62c-5.93 13.94-10.35 30.12-13.33 43.01m-82.66 19.64a71.7 71.7 0 0 1 26.88-19.64c-3-12.89-7.4-29.07-13.33-43C79.58 33.7 71.67 24 65.06 24a6 6 0 0 0-5.73 3.36C55.25 34.1 54.91 49 58.4 67.08a218.5 218.5 0 0 0 14.83 46.19M200 188a35.87 35.87 0 0 0-13.34-28a8 8 0 0 1-2.92-5.45a56 56 0 0 0-111.48 0a8 8 0 0 1-2.92 5.45a36 36 0 1 0 47.28 54.21l-9.74-8.09a8 8 0 1 1 10.24-12.3L128 202.9l10.88-9.05a8 8 0 1 1 10.24 12.3l-9.74 8.09A36 36 0 0 0 200 188"/></g></svg></span></span>
<span class="wg__rim" aria-hidden="true"></span>
<span class="wg__flash" aria-hidden="true"></span>
</button>
<button class="wg__hole" type="button" aria-label="穴5" style="--i: 4">
<span class="wg__window"><span class="wg__mole"><svg viewBox="0 0 256 256" aria-hidden="true" focusable="false"><g fill="currentColor"><path d="M208 188a44 44 0 0 1-80 25.3a44 44 0 1 1-63.71-59.48a64 64 0 0 1 127.42 0A43.9 43.9 0 0 1 208 188" opacity=".2"/><path d="M112 164a12 12 0 1 1-12-12a12 12 0 0 1 12 12m44-12a12 12 0 1 0 12 12a12 12 0 0 0-12-12m60 36a52 52 0 0 1-88 37.52a52 52 0 1 1-71.28-75.72A71.6 71.6 0 0 1 63 129C44 91.1 32.53 40.76 45.64 19.08A22 22 0 0 1 65.06 8c14.12 0 26 11.89 36.44 36.36c6.22 14.62 10.85 31.32 14 44.74a71.8 71.8 0 0 1 25 0c3.13-13.42 7.76-30.12 14-44.74C164.9 19.89 176.82 8 190.94 8a22 22 0 0 1 19.42 11.08C223.47 40.76 212 91.1 193 129a71.6 71.6 0 0 1 6.26 20.76A51.77 51.77 0 0 1 216 188m-60.11-94.37a71.7 71.7 0 0 1 26.88 19.64a218.5 218.5 0 0 0 14.83-46.19c3.49-18.13 3.15-33-.93-39.72a6 6 0 0 0-5.73-3.36c-6.61 0-14.52 9.7-21.72 26.62c-5.93 13.94-10.35 30.12-13.33 43.01m-82.66 19.64a71.7 71.7 0 0 1 26.88-19.64c-3-12.89-7.4-29.07-13.33-43C79.58 33.7 71.67 24 65.06 24a6 6 0 0 0-5.73 3.36C55.25 34.1 54.91 49 58.4 67.08a218.5 218.5 0 0 0 14.83 46.19M200 188a35.87 35.87 0 0 0-13.34-28a8 8 0 0 1-2.92-5.45a56 56 0 0 0-111.48 0a8 8 0 0 1-2.92 5.45a36 36 0 1 0 47.28 54.21l-9.74-8.09a8 8 0 1 1 10.24-12.3L128 202.9l10.88-9.05a8 8 0 1 1 10.24 12.3l-9.74 8.09A36 36 0 0 0 200 188"/></g></svg></span></span>
<span class="wg__rim" aria-hidden="true"></span>
<span class="wg__flash" aria-hidden="true"></span>
</button>
<button class="wg__hole" type="button" aria-label="穴6" style="--i: 5">
<span class="wg__window"><span class="wg__mole"><svg viewBox="0 0 256 256" aria-hidden="true" focusable="false"><g fill="currentColor"><path d="M208 188a44 44 0 0 1-80 25.3a44 44 0 1 1-63.71-59.48a64 64 0 0 1 127.42 0A43.9 43.9 0 0 1 208 188" opacity=".2"/><path d="M112 164a12 12 0 1 1-12-12a12 12 0 0 1 12 12m44-12a12 12 0 1 0 12 12a12 12 0 0 0-12-12m60 36a52 52 0 0 1-88 37.52a52 52 0 1 1-71.28-75.72A71.6 71.6 0 0 1 63 129C44 91.1 32.53 40.76 45.64 19.08A22 22 0 0 1 65.06 8c14.12 0 26 11.89 36.44 36.36c6.22 14.62 10.85 31.32 14 44.74a71.8 71.8 0 0 1 25 0c3.13-13.42 7.76-30.12 14-44.74C164.9 19.89 176.82 8 190.94 8a22 22 0 0 1 19.42 11.08C223.47 40.76 212 91.1 193 129a71.6 71.6 0 0 1 6.26 20.76A51.77 51.77 0 0 1 216 188m-60.11-94.37a71.7 71.7 0 0 1 26.88 19.64a218.5 218.5 0 0 0 14.83-46.19c3.49-18.13 3.15-33-.93-39.72a6 6 0 0 0-5.73-3.36c-6.61 0-14.52 9.7-21.72 26.62c-5.93 13.94-10.35 30.12-13.33 43.01m-82.66 19.64a71.7 71.7 0 0 1 26.88-19.64c-3-12.89-7.4-29.07-13.33-43C79.58 33.7 71.67 24 65.06 24a6 6 0 0 0-5.73 3.36C55.25 34.1 54.91 49 58.4 67.08a218.5 218.5 0 0 0 14.83 46.19M200 188a35.87 35.87 0 0 0-13.34-28a8 8 0 0 1-2.92-5.45a56 56 0 0 0-111.48 0a8 8 0 0 1-2.92 5.45a36 36 0 1 0 47.28 54.21l-9.74-8.09a8 8 0 1 1 10.24-12.3L128 202.9l10.88-9.05a8 8 0 1 1 10.24 12.3l-9.74 8.09A36 36 0 0 0 200 188"/></g></svg></span></span>
<span class="wg__rim" aria-hidden="true"></span>
<span class="wg__flash" aria-hidden="true"></span>
</button>
<button class="wg__hole" type="button" aria-label="穴7" style="--i: 6">
<span class="wg__window"><span class="wg__mole"><svg viewBox="0 0 256 256" aria-hidden="true" focusable="false"><g fill="currentColor"><path d="M208 188a44 44 0 0 1-80 25.3a44 44 0 1 1-63.71-59.48a64 64 0 0 1 127.42 0A43.9 43.9 0 0 1 208 188" opacity=".2"/><path d="M112 164a12 12 0 1 1-12-12a12 12 0 0 1 12 12m44-12a12 12 0 1 0 12 12a12 12 0 0 0-12-12m60 36a52 52 0 0 1-88 37.52a52 52 0 1 1-71.28-75.72A71.6 71.6 0 0 1 63 129C44 91.1 32.53 40.76 45.64 19.08A22 22 0 0 1 65.06 8c14.12 0 26 11.89 36.44 36.36c6.22 14.62 10.85 31.32 14 44.74a71.8 71.8 0 0 1 25 0c3.13-13.42 7.76-30.12 14-44.74C164.9 19.89 176.82 8 190.94 8a22 22 0 0 1 19.42 11.08C223.47 40.76 212 91.1 193 129a71.6 71.6 0 0 1 6.26 20.76A51.77 51.77 0 0 1 216 188m-60.11-94.37a71.7 71.7 0 0 1 26.88 19.64a218.5 218.5 0 0 0 14.83-46.19c3.49-18.13 3.15-33-.93-39.72a6 6 0 0 0-5.73-3.36c-6.61 0-14.52 9.7-21.72 26.62c-5.93 13.94-10.35 30.12-13.33 43.01m-82.66 19.64a71.7 71.7 0 0 1 26.88-19.64c-3-12.89-7.4-29.07-13.33-43C79.58 33.7 71.67 24 65.06 24a6 6 0 0 0-5.73 3.36C55.25 34.1 54.91 49 58.4 67.08a218.5 218.5 0 0 0 14.83 46.19M200 188a35.87 35.87 0 0 0-13.34-28a8 8 0 0 1-2.92-5.45a56 56 0 0 0-111.48 0a8 8 0 0 1-2.92 5.45a36 36 0 1 0 47.28 54.21l-9.74-8.09a8 8 0 1 1 10.24-12.3L128 202.9l10.88-9.05a8 8 0 1 1 10.24 12.3l-9.74 8.09A36 36 0 0 0 200 188"/></g></svg></span></span>
<span class="wg__rim" aria-hidden="true"></span>
<span class="wg__flash" aria-hidden="true"></span>
</button>
<button class="wg__hole" type="button" aria-label="穴8" style="--i: 7">
<span class="wg__window"><span class="wg__mole"><svg viewBox="0 0 256 256" aria-hidden="true" focusable="false"><g fill="currentColor"><path d="M208 188a44 44 0 0 1-80 25.3a44 44 0 1 1-63.71-59.48a64 64 0 0 1 127.42 0A43.9 43.9 0 0 1 208 188" opacity=".2"/><path d="M112 164a12 12 0 1 1-12-12a12 12 0 0 1 12 12m44-12a12 12 0 1 0 12 12a12 12 0 0 0-12-12m60 36a52 52 0 0 1-88 37.52a52 52 0 1 1-71.28-75.72A71.6 71.6 0 0 1 63 129C44 91.1 32.53 40.76 45.64 19.08A22 22 0 0 1 65.06 8c14.12 0 26 11.89 36.44 36.36c6.22 14.62 10.85 31.32 14 44.74a71.8 71.8 0 0 1 25 0c3.13-13.42 7.76-30.12 14-44.74C164.9 19.89 176.82 8 190.94 8a22 22 0 0 1 19.42 11.08C223.47 40.76 212 91.1 193 129a71.6 71.6 0 0 1 6.26 20.76A51.77 51.77 0 0 1 216 188m-60.11-94.37a71.7 71.7 0 0 1 26.88 19.64a218.5 218.5 0 0 0 14.83-46.19c3.49-18.13 3.15-33-.93-39.72a6 6 0 0 0-5.73-3.36c-6.61 0-14.52 9.7-21.72 26.62c-5.93 13.94-10.35 30.12-13.33 43.01m-82.66 19.64a71.7 71.7 0 0 1 26.88-19.64c-3-12.89-7.4-29.07-13.33-43C79.58 33.7 71.67 24 65.06 24a6 6 0 0 0-5.73 3.36C55.25 34.1 54.91 49 58.4 67.08a218.5 218.5 0 0 0 14.83 46.19M200 188a35.87 35.87 0 0 0-13.34-28a8 8 0 0 1-2.92-5.45a56 56 0 0 0-111.48 0a8 8 0 0 1-2.92 5.45a36 36 0 1 0 47.28 54.21l-9.74-8.09a8 8 0 1 1 10.24-12.3L128 202.9l10.88-9.05a8 8 0 1 1 10.24 12.3l-9.74 8.09A36 36 0 0 0 200 188"/></g></svg></span></span>
<span class="wg__rim" aria-hidden="true"></span>
<span class="wg__flash" aria-hidden="true"></span>
</button>
<button class="wg__hole" type="button" aria-label="穴9" style="--i: 8">
<span class="wg__window"><span class="wg__mole"><svg viewBox="0 0 256 256" aria-hidden="true" focusable="false"><g fill="currentColor"><path d="M208 188a44 44 0 0 1-80 25.3a44 44 0 1 1-63.71-59.48a64 64 0 0 1 127.42 0A43.9 43.9 0 0 1 208 188" opacity=".2"/><path d="M112 164a12 12 0 1 1-12-12a12 12 0 0 1 12 12m44-12a12 12 0 1 0 12 12a12 12 0 0 0-12-12m60 36a52 52 0 0 1-88 37.52a52 52 0 1 1-71.28-75.72A71.6 71.6 0 0 1 63 129C44 91.1 32.53 40.76 45.64 19.08A22 22 0 0 1 65.06 8c14.12 0 26 11.89 36.44 36.36c6.22 14.62 10.85 31.32 14 44.74a71.8 71.8 0 0 1 25 0c3.13-13.42 7.76-30.12 14-44.74C164.9 19.89 176.82 8 190.94 8a22 22 0 0 1 19.42 11.08C223.47 40.76 212 91.1 193 129a71.6 71.6 0 0 1 6.26 20.76A51.77 51.77 0 0 1 216 188m-60.11-94.37a71.7 71.7 0 0 1 26.88 19.64a218.5 218.5 0 0 0 14.83-46.19c3.49-18.13 3.15-33-.93-39.72a6 6 0 0 0-5.73-3.36c-6.61 0-14.52 9.7-21.72 26.62c-5.93 13.94-10.35 30.12-13.33 43.01m-82.66 19.64a71.7 71.7 0 0 1 26.88-19.64c-3-12.89-7.4-29.07-13.33-43C79.58 33.7 71.67 24 65.06 24a6 6 0 0 0-5.73 3.36C55.25 34.1 54.91 49 58.4 67.08a218.5 218.5 0 0 0 14.83 46.19M200 188a35.87 35.87 0 0 0-13.34-28a8 8 0 0 1-2.92-5.45a56 56 0 0 0-111.48 0a8 8 0 0 1-2.92 5.45a36 36 0 1 0 47.28 54.21l-9.74-8.09a8 8 0 1 1 10.24-12.3L128 202.9l10.88-9.05a8 8 0 1 1 10.24 12.3l-9.74 8.09A36 36 0 0 0 200 188"/></g></svg></span></span>
<span class="wg__rim" aria-hidden="true"></span>
<span class="wg__flash" aria-hidden="true"></span>
</button>
<div class="wg__veil" id="wgVeil">
<p class="wg__veilMsg" id="wgVeilMsg" role="status" hidden><b id="wgEndMsg">ゲーム終了! スコア{n} · 最高{b}</b></p>
<button class="wg__start" id="wgStart" type="button">スタート</button>
</div>
</div>
</div>
</main>@use "../../../../site/static/demo-tokens" as *;
// 두더지 잡기 — 3×3 구멍에서 두더지가 무작위로 솟았다 들어간다.
// 실사용 경로: JS 가 구멍에 .is-up 을 붙였다 떼고, 솟고 들어가는 움직임은 transition 이 그린다.
// 격자 미리보기(.is-demo): 같은 움직임을 @keyframes 로 그리되 구멍마다 음수 animation-delay 를 달아
// 위상만 어긋뜨린다. 주기가 전부 2초라 gif 가 한 바퀴에서 매끈하게 이어진다(271 계열 함정 회피).
// 실사용 코드와 미리보기 코드가 갈라져 있는 것은 의도다 — 252-07 과 같은 계열.
// 무대가 주황이라 무대 위 글자는 전부 $subject-ink 다(5.50:1). 크림 글자는 3.11:1 로 AA 미달이라 쓰지 않고,
// 알파를 씌운 잉크(.72)도 주황 위에서는 3.77:1 이라 못 쓴다 — 보조 글자는 굵기·크기로만 낮춘다.
$duration: $dur-loop;
$easing: $ease-pop;
$color: $subject-cream;
@include stage($stage-orange);
// 구멍에는 글자가 없어 내용 높이가 0 이다 — 칸을 세로로 채우지 않으면 1fr 행이 통째로 접힌다
.stage { align-items: stretch; }
.wg {
display: flex; flex-direction: column; gap: $sp-2;
width: 100%; max-width: 300px; height: 100%;
}
// ── 머리줄 ───────────────────────────────────────────────
.wg__bar { display: flex; align-items: center; gap: $sp-2; }
.wg__title { flex: 1 1 auto; margin: 0; font-weight: 800; font-size: clamp(12px, 3vw, 15px); color: $subject-ink; }
.wg__best { display: inline-flex; align-items: center; gap: $sp-1; color: $subject-ink; }
.wg__trophy { width: clamp(12px, 3vw, 15px); height: clamp(12px, 3vw, 15px); }
.wg__restart {
display: grid; place-items: center; width: clamp(22px, 5vw, 28px); aspect-ratio: 1; padding: 0;
border: 2px solid rgba($subject-ink, .32); border-radius: $r-control;
background: none; color: $subject-ink; cursor: pointer;
}
.wg__restart svg { width: 62%; height: 62%; }
.wg__restart:focus-visible { outline: 2px solid $subject-ink; outline-offset: 2px; }
// ── 시계·점수줄 ───────────────────────────────────────────
.wg__meter { display: flex; align-items: center; gap: $sp-2; }
.wg__clock { display: inline-flex; align-items: center; gap: $sp-1; color: $subject-ink; }
.wg__timer { width: clamp(11px, 2.8vw, 14px); height: clamp(11px, 2.8vw, 14px); }
.wg__num { font-weight: 800; font-size: clamp(11px, 2.8vw, 14px); color: $subject-ink; font-variant-numeric: tabular-nums; }
.wg__num--score { font-size: clamp(13px, 3.4vw, 17px); }
.wg__unit { font-weight: 700; font-size: clamp(8px, 2.2vw, 10px); color: $subject-ink; }
.wg__score { display: inline-flex; align-items: baseline; gap: $sp-1; }
.wg__track {
flex: 1 1 auto; display: block; height: 8px; overflow: hidden;
border-radius: $r-pill; background-color: rgba($subject-ink, .24);
}
// 채움 막대는 시작 변에서 줄어든다 — 그래서 origin 이 중심이 아니다(blog-demo 규칙 8 의 "붙는 지점" 쪽)
.wg__fill {
display: block; width: 100%; height: 100%; border-radius: inherit;
background-color: $subject-ink; transform-origin: left center; transform: scaleX(1);
}
// ── 구멍판 ───────────────────────────────────────────────
.wg__field {
position: relative; flex: 1 1 auto; min-height: 0;
display: grid; grid-template: repeat(3, 1fr) / repeat(3, 1fr); gap: $sp-2;
}
.wg__hole {
position: relative; display: block; width: 100%; height: 100%; padding: 0;
border: 0; border-radius: 50%; background: none; cursor: pointer;
}
.wg__hole:focus-visible { outline: 2px solid $subject-ink; outline-offset: 2px; }
// 구멍 안쪽 — 여기서만 잘린다. 두더지는 이 상자 아래에 숨어 있다가 올라온다
.wg__window {
position: absolute; inset: 0; display: block; overflow: hidden;
border-radius: inherit; background-color: $subject-ink;
}
// 구멍 테두리(입구 턱) — 클릭을 가로채지 않는다
.wg__rim {
position: absolute; inset: 0; display: block; pointer-events: none;
border: 2px solid rgba($subject-cream, .18); border-radius: inherit;
}
// 두더지 몸통은 CSS(알약 반쪽)이고 얼굴만 Phosphor Duotone 아이콘이다
.wg__mole {
position: absolute; left: 16%; right: 16%; bottom: 0; height: 76%;
display: grid; place-items: center;
border-radius: $r-pill $r-pill 0 0;
background-color: $color; color: $subject-ink;
transform: translateY(100%);
transition: transform $dur-quick $easing;
}
.wg__mole svg { width: 66%; height: 66%; }
.wg__hole.is-up .wg__mole { transform: translateY(0); }
// 명중 플래시 — 구멍 전체를 한 번 밝히고 퍼지며 사라진다(자기 중심에서 커진다)
.wg__flash {
position: absolute; inset: 0; display: block; pointer-events: none;
border-radius: inherit; background-color: $subject-cream; opacity: 0;
}
.wg__hole.is-hit .wg__flash {
animation-name: wgFlashHit; animation-duration: 380ms; animation-timing-function: $ease-out;
animation-iteration-count: 1; animation-fill-mode: both;
}
@keyframes wgFlashHit {
from { opacity: .8; transform: scale(1); }
to { opacity: 0; transform: scale(1.22); }
}
// ── 시작·결과 덮개 ────────────────────────────────────────
.wg__veil {
position: absolute; inset: 0; z-index: 3; padding: $sp-2;
display: grid; place-items: center; align-content: center; gap: $sp-2;
border-radius: $r-card; background-color: rgba($subject-ink, .72);
}
.wg__veil[hidden] { display: none; }
.wg__veilMsg {
margin: 0; text-align: center;
font-weight: 800; font-size: clamp(10px, 2.6vw, 13px); color: $subject-cream;
}
.wg__veilMsg[hidden] { display: none; }
.wg__start {
padding: $sp-2 $sp-5; border: 0; border-radius: $r-pill;
background-color: $subject-cream; color: $subject-ink;
font-weight: 800; font-size: clamp(11px, 2.8vw, 14px); cursor: pointer;
box-shadow: $shadow-press; transition: transform $dur-instant $ease-out;
}
.wg__start:active { transform: scale(.94); }
.wg__start:focus-visible { outline: 2px solid $subject-cream; outline-offset: 2px; }
// ── 격자 미리보기 ─────────────────────────────────────────
// 구멍 아홉에 -220ms 씩 어긋난 지연을 주면 두더지가 읽는 순서대로 물결처럼 솟는다.
// 지연이 음수라 첫 프레임부터 이미 여럿이 올라와 있다 — 격자 썸네일이 빈 화면으로 시작하지 않는다.
.wg.is-demo .wg__veil { opacity: 0; }
.wg.is-demo .wg__mole,
.wg.is-demo .wg__flash {
animation-duration: $duration; animation-timing-function: $easing;
animation-iteration-count: infinite; animation-fill-mode: both;
animation-delay: calc(var(--i) * -220ms);
}
.wg.is-demo .wg__mole { animation-name: wgMoleRise; transition: none; }
.wg.is-demo .wg__flash { animation-name: wgHitLoop; }
@keyframes wgMoleRise {
0% { transform: translateY(100%); }
8% { transform: translateY(0); }
20% { transform: translateY(0); }
30% { transform: translateY(100%); }
100% { transform: translateY(100%); }
}
@keyframes wgHitLoop {
0% { opacity: 0; transform: scale(.92); }
17% { opacity: 0; transform: scale(.92); }
21% { opacity: .8; transform: scale(1); }
34% { opacity: 0; transform: scale(1.18); }
100% { opacity: 0; transform: scale(1.18); }
}
@media (max-width: 340px) {
.wg { gap: $sp-1; }
.wg__field { gap: $sp-1; }
.wg__veil { padding: $sp-1; }
.wg__track { height: 4px; }
}
// reduce 에서는 솟는 연출을 끄고 두더지를 올라온 채로 세워 둔다 — 무엇을 잡는 게임인지는 그대로 보인다.
// .is-demo 가 붙은 규칙(0,3,0)을 이기려면 !important 가 필요하다(260 실측)
@media (prefers-reduced-motion: reduce) {
.wg.is-demo .wg__mole { animation: none !important; transform: translateY(0) !important; }
.wg.is-demo .wg__flash { animation: none !important; opacity: 0 !important; }
.wg__hole.is-hit .wg__flash { animation: none !important; }
.wg__mole { transition: none !important; }
.wg__start { transition: none !important; }
}html, body {
margin: 0;
overflow: hidden;
}
.stage {
box-sizing: border-box;
min-height: 100vh;
display: grid;
place-items: center;
margin: 0;
background: #ff4d1f;
font-family: -apple-system, "Segoe UI", "Noto Sans KR", sans-serif;
padding: clamp(8px, 4vw, 24px);
}
.stage {
align-items: stretch;
}
.wg {
display: flex;
flex-direction: column;
gap: 8px;
width: 100%;
max-width: 300px;
height: 100%;
}
.wg__bar {
display: flex;
align-items: center;
gap: 8px;
}
.wg__title {
flex: 1 1 auto;
margin: 0;
font-weight: 800;
font-size: clamp(12px, 3vw, 15px);
color: #17141a;
}
.wg__best {
display: inline-flex;
align-items: center;
gap: 4px;
color: #17141a;
}
.wg__trophy {
width: clamp(12px, 3vw, 15px);
height: clamp(12px, 3vw, 15px);
}
.wg__restart {
display: grid;
place-items: center;
width: clamp(22px, 5vw, 28px);
aspect-ratio: 1;
padding: 0;
border: 2px solid rgba(23, 20, 26, 0.32);
border-radius: 12px;
background: none;
color: #17141a;
cursor: pointer;
}
.wg__restart svg {
width: 62%;
height: 62%;
}
.wg__restart:focus-visible {
outline: 2px solid #17141a;
outline-offset: 2px;
}
.wg__meter {
display: flex;
align-items: center;
gap: 8px;
}
.wg__clock {
display: inline-flex;
align-items: center;
gap: 4px;
color: #17141a;
}
.wg__timer {
width: clamp(11px, 2.8vw, 14px);
height: clamp(11px, 2.8vw, 14px);
}
.wg__num {
font-weight: 800;
font-size: clamp(11px, 2.8vw, 14px);
color: #17141a;
font-variant-numeric: tabular-nums;
}
.wg__num--score {
font-size: clamp(13px, 3.4vw, 17px);
}
.wg__unit {
font-weight: 700;
font-size: clamp(8px, 2.2vw, 10px);
color: #17141a;
}
.wg__score {
display: inline-flex;
align-items: baseline;
gap: 4px;
}
.wg__track {
flex: 1 1 auto;
display: block;
height: 8px;
overflow: hidden;
border-radius: 999px;
background-color: rgba(23, 20, 26, 0.24);
}
.wg__fill {
display: block;
width: 100%;
height: 100%;
border-radius: inherit;
background-color: #17141a;
transform-origin: left center;
transform: scaleX(1);
}
.wg__field {
position: relative;
flex: 1 1 auto;
min-height: 0;
display: grid;
grid-template: repeat(3, 1fr)/repeat(3, 1fr);
gap: 8px;
}
.wg__hole {
position: relative;
display: block;
width: 100%;
height: 100%;
padding: 0;
border: 0;
border-radius: 50%;
background: none;
cursor: pointer;
}
.wg__hole:focus-visible {
outline: 2px solid #17141a;
outline-offset: 2px;
}
.wg__window {
position: absolute;
inset: 0;
display: block;
overflow: hidden;
border-radius: inherit;
background-color: #17141a;
}
.wg__rim {
position: absolute;
inset: 0;
display: block;
pointer-events: none;
border: 2px solid rgba(255, 247, 230, 0.18);
border-radius: inherit;
}
.wg__mole {
position: absolute;
left: 16%;
right: 16%;
bottom: 0;
height: 76%;
display: grid;
place-items: center;
border-radius: 999px 999px 0 0;
background-color: #fff7e6;
color: #17141a;
transform: translateY(100%);
transition: transform 220ms cubic-bezier(0.2, 1.4, 0.4, 1);
}
.wg__mole svg {
width: 66%;
height: 66%;
}
.wg__hole.is-up .wg__mole {
transform: translateY(0);
}
.wg__flash {
position: absolute;
inset: 0;
display: block;
pointer-events: none;
border-radius: inherit;
background-color: #fff7e6;
opacity: 0;
}
.wg__hole.is-hit .wg__flash {
animation-name: wgFlashHit;
animation-duration: 380ms;
animation-timing-function: ease-out;
animation-iteration-count: 1;
animation-fill-mode: both;
}
@keyframes wgFlashHit {
from {
opacity: 0.8;
transform: scale(1);
}
to {
opacity: 0;
transform: scale(1.22);
}
}
.wg__veil {
position: absolute;
inset: 0;
z-index: 3;
padding: 8px;
display: grid;
place-items: center;
align-content: center;
gap: 8px;
border-radius: 16px;
background-color: rgba(23, 20, 26, 0.72);
}
.wg__veil[hidden] {
display: none;
}
.wg__veilMsg {
margin: 0;
text-align: center;
font-weight: 800;
font-size: clamp(10px, 2.6vw, 13px);
color: #fff7e6;
}
.wg__veilMsg[hidden] {
display: none;
}
.wg__start {
padding: 8px 20px;
border: 0;
border-radius: 999px;
background-color: #fff7e6;
color: #17141a;
font-weight: 800;
font-size: clamp(11px, 2.8vw, 14px);
cursor: pointer;
box-shadow: 0 6px 12px rgba(23, 20, 26, 0.32);
transition: transform 140ms ease-out;
}
.wg__start:active {
transform: scale(0.94);
}
.wg__start:focus-visible {
outline: 2px solid #fff7e6;
outline-offset: 2px;
}
.wg.is-demo .wg__veil {
opacity: 0;
}
.wg.is-demo .wg__mole,
.wg.is-demo .wg__flash {
animation-duration: 2s;
animation-timing-function: cubic-bezier(0.2, 1.4, 0.4, 1);
animation-iteration-count: infinite;
animation-fill-mode: both;
animation-delay: calc(var(--i) * -220ms);
}
.wg.is-demo .wg__mole {
animation-name: wgMoleRise;
transition: none;
}
.wg.is-demo .wg__flash {
animation-name: wgHitLoop;
}
@keyframes wgMoleRise {
0% {
transform: translateY(100%);
}
8% {
transform: translateY(0);
}
20% {
transform: translateY(0);
}
30% {
transform: translateY(100%);
}
100% {
transform: translateY(100%);
}
}
@keyframes wgHitLoop {
0% {
opacity: 0;
transform: scale(0.92);
}
17% {
opacity: 0;
transform: scale(0.92);
}
21% {
opacity: 0.8;
transform: scale(1);
}
34% {
opacity: 0;
transform: scale(1.18);
}
100% {
opacity: 0;
transform: scale(1.18);
}
}
@media (max-width: 340px) {
.wg {
gap: 4px;
}
.wg__field {
gap: 4px;
}
.wg__veil {
padding: 4px;
}
.wg__track {
height: 4px;
}
}
@media (prefers-reduced-motion: reduce) {
.wg.is-demo .wg__mole {
animation: none !important;
transform: translateY(0) !important;
}
.wg.is-demo .wg__flash {
animation: none !important;
opacity: 0 !important;
}
.wg__hole.is-hit .wg__flash {
animation: none !important;
}
.wg__mole {
transition: none !important;
}
.wg__start {
transition: none !important;
}
}// モグラたたき — 3×3 구멍에 무작위로(700~1000ms 간격) 두더지가 솟고, 솟아 있는 동안 pointerdown 하면 スコア.
// 제한 시간 20秒. 놓쳐도 감점은 없다. 최고 점수는 p301-03-best.
// 실제 솟기·들기는 transition(부드럽게), 격자 미리보기(.is-demo)만 @keyframes — 두 코드가 갈라진다(252-07 계열).
(function () {
var KEY = 'p301-03-best';
var TIME = 20000; // 제한 시간 20秒 (ms)
var UP_MS = 850; // 두더지가 올라와 있는 시간 (ms)
var END_TEXT = 'ゲーム終了! スコア{n} · 最高{b}';
var AGAIN = 'もう一度';
var START = 'スタート';
var root = document.getElementById('wg');
var field = document.getElementById('wgField');
var holes = [].slice.call(field.querySelectorAll('.wg__hole'));
var scoreEl = document.getElementById('wgScore');
var clockEl = document.getElementById('wgClock');
var fillEl = document.getElementById('wgFill');
var bestEl = document.getElementById('wgBest');
var veil = document.getElementById('wgVeil');
var veilMsg = document.getElementById('wgVeilMsg');
var endMsg = document.getElementById('wgEndMsg');
var startBtn = document.getElementById('wgStart');
var playing = false, score = 0, endAt = 0, upIndex = -1, lastHole = -1;
var tickId = 0, spawnId = 0, downId = 0;
function loadBest() {
try { return Number(window.localStorage.getItem(KEY)) || 0; } catch (e) { return 0; }
}
var best = loadBest();
bestEl.textContent = String(best);
function paintTime(ms) {
clockEl.textContent = (ms / 1000).toFixed(1);
fillEl.style.transform = 'scaleX(' + (ms / TIME) + ')'; // 채움 막대 — スタート 변 기준
}
function sink() {
clearTimeout(downId); downId = 0;
if (upIndex >= 0) { holes[upIndex].classList.remove('is-up'); upIndex = -1; }
}
function spawn() {
if (!playing) { return; }
sink();
var i = lastHole;
while (i === lastHole) { i = Math.floor(Math.random() * holes.length); } // 같은 구멍 연속 스폰은 피한다
lastHole = i; upIndex = i;
holes[i].classList.add('is-up');
downId = setTimeout(sink, UP_MS);
spawnId = setTimeout(spawn, 700 + Math.random() * 300);
}
function stopTimers() {
clearInterval(tickId); clearTimeout(spawnId); clearTimeout(downId);
tickId = 0; spawnId = 0; downId = 0;
}
function hit(btn) {
if (!playing) { return; }
var i = holes.indexOf(btn);
if (i !== upIndex) { return; } // 안 올라와 있는 구멍 — 중복·빈 클릭 방지
upIndex = -1; clearTimeout(downId); downId = 0;
score++; scoreEl.textContent = String(score);
btn.classList.remove('is-up');
btn.classList.add('is-hit');
setTimeout(function () { btn.classList.remove('is-hit'); }, 380);
}
function tick() {
var left = endAt - Date.now();
if (left <= 0) { finish(); return; }
paintTime(left);
}
function finish() {
playing = false; stopTimers(); sink();
paintTime(0);
if (score > best) {
best = score;
try { window.localStorage.setItem(KEY, String(best)); } catch (e) { /* 저장 건너뛰기 */ }
bestEl.textContent = String(best);
}
endMsg.textContent = END_TEXT.replace('{n}', String(score)).replace('{b}', String(best));
veilMsg.hidden = false;
startBtn.textContent = AGAIN;
veil.hidden = false;
startBtn.focus({ preventScroll: true }); // iframe 안에서 부모 스크롤 방지
}
function start() {
root.classList.remove('is-demo');
playing = true; score = 0; lastHole = -1;
scoreEl.textContent = '0';
veil.hidden = true; veilMsg.hidden = true;
endAt = Date.now() + TIME;
paintTime(TIME);
tickId = setInterval(tick, 100);
spawnId = setTimeout(spawn, 350);
}
field.addEventListener('pointerdown', function (e) {
root.classList.remove('is-demo');
var btn = e.target.closest ? e.target.closest('.wg__hole') : null;
if (btn) { hit(btn); }
});
field.addEventListener('keydown', function (e) {
if (e.key !== 'Enter' && e.key !== ' ') { return; }
var btn = e.target.closest ? e.target.closest('.wg__hole') : null;
if (btn) { e.preventDefault(); hit(btn); }
});
startBtn.addEventListener('click', start);
document.getElementById('wgRestart').addEventListener('click', function () {
root.classList.remove('is-demo');
playing = false; stopTimers(); sink();
score = 0; scoreEl.textContent = '0';
paintTime(TIME);
startBtn.textContent = START;
veilMsg.hidden = true;
veil.hidden = false;
});
paintTime(TIME);
})();// MoleWhackTimer — vanilla 판(run/301/demo/03-mole-whack-timer/index.html) 의 React 포트.
// 3×3 구멍에 무작위로(700~1000ms 간격) 두더지가 솟고, 솟아 있는 동안 pointerdown 하면 점수.
// 제한 시간 20초. 최고 점수는 p301-03-best.
// 솟고 들어가는 움직임은 CSS(transition / .is-demo 에서는 @keyframes)가 그리고 React 는 상태만 바꾼다.
import { useEffect, useRef, useState } from "react";
import type { CSSProperties } from "react";
import styles from "./MoleWhackTimer.module.scss";
export type MoleWhackTimerProps = {
duration?: number;
easing?: string;
accentColor?: string;
};
type Vars = CSSProperties & { "--duration"?: string; "--easing"?: string; "--color"?: string };
type TimerBox = { current: number }; // ambient 의 useRef 는 T|null 을 돌려주므로 단언용
type HitBox = { current: number[] };
// ambient.d.ts 가 React 이벤트 제네릭을 안 주므로 로컬 타입로 쓴다(237 계열)
type PointerEvt = { target: { closest: (s: string) => HTMLElement | null } };
type KeyEvt = { key: string; preventDefault(): void; target: { closest: (s: string) => HTMLElement | null } };
const KEY = "p301-03-best";
const TIME = 20000; // 제한 시간 20초 (ms)
const UP_MS = 850; // 두더지가 올라와 있는 시간 (ms)
const HOLES = 9;
const TITLE = "두더지 잡기";
const BEST_LABEL = "최고 기록";
const RESTART_LABEL = "다시 시작";
const HOLE_LABEL = "{n}번 구멍";
const UNIT_SEC = "초";
const UNIT_PTS = "점";
const END_TEXT = "게임 끝! 점수 {n} · 최고 {b}";
const START = "시작";
const AGAIN = "다시";
// Phosphor Duotone(무료 아이콘 세트) 경로 — [20% 채움 경로, 본 경로]
const MOLE_PATHS: [string, string] = [
"M208 188a44 44 0 0 1-80 25.3a44 44 0 1 1-63.71-59.48a64 64 0 0 1 127.42 0A43.9 43.9 0 0 1 208 188",
"M112 164a12 12 0 1 1-12-12a12 12 0 0 1 12 12m44-12a12 12 0 1 0 12 12a12 12 0 0 0-12-12m60 36a52 52 0 0 1-88 37.52a52 52 0 1 1-71.28-75.72A71.6 71.6 0 0 1 63 129C44 91.1 32.53 40.76 45.64 19.08A22 22 0 0 1 65.06 8c14.12 0 26 11.89 36.44 36.36c6.22 14.62 10.85 31.32 14 44.74a71.8 71.8 0 0 1 25 0c3.13-13.42 7.76-30.12 14-44.74C164.9 19.89 176.82 8 190.94 8a22 22 0 0 1 19.42 11.08C223.47 40.76 212 91.1 193 129a71.6 71.6 0 0 1 6.26 20.76A51.77 51.77 0 0 1 216 188m-60.11-94.37a71.7 71.7 0 0 1 26.88 19.64a218.5 218.5 0 0 0 14.83-46.19c3.49-18.13 3.15-33-.93-39.72a6 6 0 0 0-5.73-3.36c-6.61 0-14.52 9.7-21.72 26.62c-5.93 13.94-10.35 30.12-13.33 43.01m-82.66 19.64a71.7 71.7 0 0 1 26.88-19.64c-3-12.89-7.4-29.07-13.33-43C79.58 33.7 71.67 24 65.06 24a6 6 0 0 0-5.73 3.36C55.25 34.1 54.91 49 58.4 67.08a218.5 218.5 0 0 0 14.83 46.19M200 188a35.87 35.87 0 0 0-13.34-28a8 8 0 0 1-2.92-5.45a56 56 0 0 0-111.48 0a8 8 0 0 1-2.92 5.45a36 36 0 1 0 47.28 54.21l-9.74-8.09a8 8 0 1 1 10.24-12.3L128 202.9l10.88-9.05a8 8 0 1 1 10.24 12.3l-9.74 8.09A36 36 0 0 0 200 188",
];
const TROPHY_PATHS: [string, string] = [
"M200 48v63.1c0 39.7-31.75 72.6-71.45 72.9A72 72 0 0 1 56 112V48Z",
"M232 64h-24V48a8 8 0 0 0-8-8H56a8 8 0 0 0-8 8v16H24A16 16 0 0 0 8 80v16a40 40 0 0 0 40 40h3.65A80.13 80.13 0 0 0 120 191.61V216H96a8 8 0 0 0 0 16h64a8 8 0 0 0 0-16h-24v-24.42c31.94-3.23 58.44-25.64 68.08-55.58H208a40 40 0 0 0 40-40V80a16 16 0 0 0-16-16M48 120a24 24 0 0 1-24-24V80h24v32q0 4 .39 8Zm144-8.9c0 35.52-29 64.64-64 64.9a64 64 0 0 1-64-64V56h128ZM232 96a24 24 0 0 1-24 24h-.5a82 82 0 0 0 .5-8.9V80h24Z",
];
const RESTART_PATHS: [string, string] = [
"M216 128a88 88 0 1 1-88-88a88 88 0 0 1 88 88",
"M240 56v48a8 8 0 0 1-8 8h-48a8 8 0 0 1 0-16h27.4l-26.59-24.36l-.25-.24a80 80 0 1 0-1.67 114.78a8 8 0 0 1 11 11.63A95.44 95.44 0 0 1 128 224h-1.32a96 96 0 1 1 69.07-164L224 85.8V56a8 8 0 1 1 16 0",
];
const TIMER_PATHS: [string, string] = [
"M216 136a88 88 0 1 1-88-88a88 88 0 0 1 88 88",
"M128 40a96 96 0 1 0 96 96a96.11 96.11 0 0 0-96-96m0 176a80 80 0 1 1 80-80a80.09 80.09 0 0 1-80 80m45.66-125.66a8 8 0 0 1 0 11.32l-40 40a8 8 0 0 1-11.32-11.32l40-40a8 8 0 0 1 11.32 0M96 16a8 8 0 0 1 8-8h48a8 8 0 0 1 0 16h-48a8 8 0 0 1-8-8",
];
function DuotoneIcon({ paths }: { paths: [string, string] }) {
return (
<svg viewBox="0 0 256 256" aria-hidden="true" focusable="false">
<g fill="currentColor">
<path d={paths[0]} opacity=".2" />
<path d={paths[1]} />
</g>
</svg>
);
}
export default function MoleWhackTimer({
duration = 2000,
easing = "cubic-bezier(.2, 1.4, .4, 1)",
accentColor = "#fff7e6",
}: MoleWhackTimerProps) {
const [playing, setPlaying] = useState(false);
const [score, setScore] = useState(0);
const [up, setUp] = useState(-1);
const [hitIndex, setHitIndex] = useState(-1);
const [best, setBest] = useState(0);
const [demo, setDemo] = useState(true);
const [left, setLeft] = useState(TIME);
const [endText, setEndText] = useState<string | null>(null);
const [again, setAgain] = useState(false);
const playingRef = useRef(false) as { current: boolean };
const upRef = useRef(-1) as TimerBox;
const lastHoleRef = useRef(-1) as TimerBox;
const scoreRef = useRef(0) as TimerBox;
const endAtRef = useRef(0) as TimerBox;
const bestRef = useRef(0) as TimerBox;
const tickRef = useRef(0) as TimerBox;
const spawnRef = useRef(0) as TimerBox;
const downRef = useRef(0) as TimerBox;
const hitTimersRef = useRef<number[]>([]) as HitBox;
const startBtnRef = useRef<HTMLButtonElement | null>(null);
const vars: Vars = { "--duration": `${duration}ms`, "--easing": easing, "--color": accentColor };
useEffect(() => {
try {
const v = Number(window.localStorage.getItem(KEY)) || 0;
if (v) { bestRef.current = v; setBest(v); }
} catch (e) { /* 저장소가 막힌 창은 기본값 그대로 */ }
return () => { clearAll(); };
}, []);
function clearAll() {
window.clearInterval(tickRef.current);
window.clearTimeout(spawnRef.current);
window.clearTimeout(downRef.current);
hitTimersRef.current.forEach((t) => window.clearTimeout(t));
tickRef.current = 0; spawnRef.current = 0; downRef.current = 0;
hitTimersRef.current = [];
}
function sink() {
if (downRef.current) { window.clearTimeout(downRef.current); downRef.current = 0; }
if (upRef.current >= 0) { upRef.current = -1; setUp(-1); }
}
function spawn() {
if (!playingRef.current) { return; }
sink();
let i = lastHoleRef.current;
while (i === lastHoleRef.current) { i = Math.floor(Math.random() * HOLES); } // 같은 구멍 연속 스폰은 피한다
lastHoleRef.current = i; upRef.current = i; setUp(i);
downRef.current = window.setTimeout(sink, UP_MS);
spawnRef.current = window.setTimeout(spawn, 700 + Math.random() * 300);
}
function tick() {
const remain = endAtRef.current - Date.now();
if (remain <= 0) { finish(); return; }
setLeft(remain);
}
function finish() {
playingRef.current = false;
clearAll(); sink();
setLeft(0);
const s = scoreRef.current;
let b = bestRef.current;
if (s > b) {
b = s; bestRef.current = b; setBest(b);
try { window.localStorage.setItem(KEY, String(b)); } catch (e) { /* 저장 건너뛰기 */ }
}
setEndText(END_TEXT.replace("{n}", String(s)).replace("{b}", String(b)));
setAgain(true);
setPlaying(false);
if (startBtnRef.current) { startBtnRef.current.focus({ preventScroll: true }); } // iframe 안에서 부모 스크롤 방지
}
function start() {
setDemo(false);
playingRef.current = true; scoreRef.current = 0; lastHoleRef.current = -1;
setScore(0); setLeft(TIME); setEndText(null); setPlaying(true);
endAtRef.current = Date.now() + TIME;
tickRef.current = window.setInterval(tick, 100);
spawnRef.current = window.setTimeout(spawn, 350);
}
function hit(i: number) {
if (!playingRef.current || i !== upRef.current) { return; } // 안 올라와 있는 구멍 — 중복·빈 클릭 방지
upRef.current = -1;
if (downRef.current) { window.clearTimeout(downRef.current); downRef.current = 0; }
scoreRef.current += 1; setScore(scoreRef.current);
setUp(-1); setHitIndex(i);
hitTimersRef.current.push(window.setTimeout(() => setHitIndex(-1), 380));
}
function reset() {
setDemo(false);
playingRef.current = false;
clearAll(); sink();
scoreRef.current = 0; setScore(0); setLeft(TIME);
setEndText(null); setAgain(false); setPlaying(false);
}
function onFieldPointerDown(e: PointerEvt) {
setDemo(false);
const btn = e.target.closest ? e.target.closest("button") : null;
if (btn) { hit(Number(btn.getAttribute("data-i"))); }
}
function onFieldKeyDown(e: KeyEvt) {
if (e.key !== "Enter" && e.key !== " ") { return; }
const btn = e.target.closest ? e.target.closest("button") : null;
if (btn) { e.preventDefault(); hit(Number(btn.getAttribute("data-i"))); }
}
const rootCls = [styles.root, demo ? styles.isDemo : ""].filter(Boolean).join(" ");
const holeCls = (i: number) => (
[styles.hole, up === i ? styles.isUp : "", hitIndex === i ? styles.isHit : ""].filter(Boolean).join(" ")
);
return (
<main className={styles.stage}>
<div className={rootCls} style={vars}>
<header className={styles.bar}>
<p className={styles.title}>{TITLE}</p>
<span className={styles.best} aria-label={BEST_LABEL}>
<svg className={styles.trophy} viewBox="0 0 256 256" aria-hidden="true" focusable="false">
<g fill="currentColor"><path d={TROPHY_PATHS[0]} opacity=".2" /><path d={TROPHY_PATHS[1]} /></g>
</svg>
<b className={styles.num}>{best}</b>
</span>
<button className={styles.restart} type="button" aria-label={RESTART_LABEL} onClick={reset}>
<svg viewBox="0 0 256 256" aria-hidden="true" focusable="false">
<g fill="currentColor"><path d={RESTART_PATHS[0]} opacity=".2" /><path d={RESTART_PATHS[1]} /></g>
</svg>
</button>
</header>
<div className={styles.meter}>
<span className={styles.clock}>
<svg className={styles.timer} viewBox="0 0 256 256" aria-hidden="true" focusable="false">
<g fill="currentColor"><path d={TIMER_PATHS[0]} opacity=".2" /><path d={TIMER_PATHS[1]} /></g>
</svg>
<b className={styles.num}>{(left / 1000).toFixed(1)}</b><span className={styles.unit}>{UNIT_SEC}</span>
</span>
<span className={styles.track}>
<span className={styles.fill} style={{ transform: `scaleX(${left / TIME})` }}></span>
</span>
<span className={styles.score}>
<b className={[styles.num, styles.numScore].join(" ")}>{score}</b><span className={styles.unit}>{UNIT_PTS}</span>
</span>
</div>
<div className={styles.field} onPointerDown={onFieldPointerDown} onKeyDown={onFieldKeyDown}>
{Array.from({ length: HOLES }, (_, i) => (
<button className={holeCls(i)} type="button" key={i} data-i={i}
aria-label={HOLE_LABEL.replace("{n}", String(i + 1))} style={{ "--i": i } as Vars}>
<span className={styles.window}>
<span className={styles.mole}><DuotoneIcon paths={MOLE_PATHS} /></span>
</span>
<span className={styles.rim} aria-hidden="true"></span>
<span className={styles.flash} aria-hidden="true"></span>
</button>
))}
{!playing && (
<div className={styles.veil}>
{endText && <p className={styles.veilMsg} role="status"><b>{endText}</b></p>}
<button className={styles.start} type="button" ref={startBtnRef} onClick={start}>
{again ? AGAIN : START}
</button>
</div>
)}
</div>
</div>
</main>
);
}03 モグラたたき — 計測 累積面積 33.16% · 動いたフレーム 23/23GODRICH オリジナル · SCSS と React は zip に同梱 · CSS ゲーム 作成 — CSSだけで動くミニゲーム9選
このデモが属する記事
CSS ゲーム 作成 — CSSだけで動くミニゲーム9選css ゲームとは、セレクタとキーフレームと transform だけでルールと画面の両方をまかなう仕掛けのことです。この9つはゲームエンジンなしで、素の HTML・CSS・バニラ JS だけで組んであり、下のグリッドでそのまま遊べます。 この記事のデモをすべて見る


