Skip to content

Commit eaef074

Browse files
committed
refac: refactor UX (especially at stage select)
1 parent 237f8bb commit eaef074

File tree

5 files changed

+276
-63
lines changed

5 files changed

+276
-63
lines changed

routes/+page.svelte

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,5 @@ $effect(() => {
7474
left top,
7575
left,
7676
top;
77-
backdrop-filter: blur(10px);
7877
}
7978
</style>

routes/clear/+page.svelte

Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
<script>
2+
import { goto } from "$app/navigation";
3+
import { onMount } from "svelte";
4+
5+
let confetti = $state(false);
6+
7+
onMount(() => {
8+
// Simulate confetti animation
9+
setTimeout(() => {
10+
confetti = true;
11+
}, 100);
12+
});
13+
</script>
14+
15+
<main class="clear-screen">
16+
<div class="content">
17+
<h1 class="title">Congratulations!</h1>
18+
<h2 class="subtitle">You've completed the game!</h2>
19+
<div class="confetti">
20+
<div class="confetti-piece" style="--delay: 0s"></div>
21+
<div class="confetti-piece" style="--delay: 0.1s"></div>
22+
<div class="confetti-piece" style="--delay: 0.2s"></div>
23+
<div class="confetti-piece" style="--delay: 0.3s"></div>
24+
<div class="confetti-piece" style="--delay: 0.4s"></div>
25+
</div>
26+
<a class="restart-btn" href="/">
27+
Play Again
28+
</a>
29+
</div>
30+
</main>
31+
32+
<style>
33+
.clear-screen {
34+
width: 100vw;
35+
height: 100vh;
36+
display: flex;
37+
justify-content: center;
38+
align-items: center;
39+
background: linear-gradient(135deg, #3498db 0%, #2ecc71 100%);
40+
color: white;
41+
overflow: hidden;
42+
position: relative;
43+
}
44+
45+
.content {
46+
text-align: center;
47+
padding: 2rem;
48+
}
49+
50+
.title {
51+
font-size: 3rem;
52+
margin-bottom: 1rem;
53+
text-shadow: 2px 2px 4px rgba(0,0,0,0.2);
54+
}
55+
56+
.subtitle {
57+
font-size: 1.5rem;
58+
margin-bottom: 2rem;
59+
opacity: 0.9;
60+
}
61+
62+
.confetti {
63+
position: absolute;
64+
top: 0;
65+
left: 0;
66+
width: 100%;
67+
height: 100%;
68+
pointer-events: none;
69+
}
70+
71+
.confetti-piece {
72+
position: absolute;
73+
width: 10px;
74+
height: 10px;
75+
background: white;
76+
border-radius: 50%;
77+
animation: fall 3s ease-in-out forwards;
78+
}
79+
80+
.confetti-piece:nth-child(1) { left: 10%; }
81+
.confetti-piece:nth-child(2) { left: 30%; }
82+
.confetti-piece:nth-child(3) { left: 50%; }
83+
.confetti-piece:nth-child(4) { left: 70%; }
84+
.confetti-piece:nth-child(5) { left: 90%; }
85+
86+
@keyframes fall {
87+
from {
88+
transform: translateY(-100vh) rotate(0deg);
89+
opacity: 0;
90+
}
91+
to {
92+
transform: translateY(100vh) rotate(360deg);
93+
opacity: 1;
94+
}
95+
}
96+
97+
.restart-btn {
98+
padding: 1rem 2rem;
99+
font-size: 1.2rem;
100+
background: white;
101+
color: #3498db;
102+
border: none;
103+
border-radius: 5px;
104+
cursor: pointer;
105+
transition: all 0.3s ease;
106+
box-shadow: 0 4px 6px rgba(0,0,0,0.1);
107+
}
108+
109+
.restart-btn:hover {
110+
transform: translateY(-2px);
111+
box-shadow: 0 6px 8px rgba(0,0,0,0.2);
112+
}
113+
</style>

routes/stage-select/+page.svelte

Lines changed: 81 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -2,39 +2,23 @@
22
import Key from "@/ui-components/Key.svelte";
33
import { onMount } from "svelte";
44
import "@/ui-components/menu/menu.css";
5-
import { replaceState } from "$app/navigation";
5+
import { goto } from "$app/navigation";
6+
import { MAX_WORLD, SearchParamState } from "./params.svelte.ts";
67
7-
// starts from 1
8-
let world = $state<number | null>(null);
9-
let stage = $state<number | null>(null);
10-
const maxWorld = 4;
11-
const maxStage = 4;
8+
const search = new SearchParamState(1);
129
13-
onMount(() => {
14-
const params = new URLSearchParams(window.location.search);
15-
world = Number(params.get("w") ?? "1");
16-
stage = Number(params.get("s") ?? "1");
17-
});
10+
const blocks = $derived(
11+
new Array(search.maxStage).fill(null).map((_, idx) => ({
12+
label: `${search.world}-${idx + 1}`,
13+
link: `/game?stage=${search.world}-${idx + 1}`,
14+
thumbnail: `/assets/thumbnail${search.world}-${idx + 1}.png`,
15+
})),
16+
);
1817
1918
let lastKeyTime = 0;
2019
let lastKey: string | null = null;
2120
const KEY_REPEAT_DELAY = 180; // ms
2221
23-
function changeStage(worldNum: number, stageNum: number): void {
24-
if (worldNum < 1 || worldNum > maxWorld) {
25-
throw new Error(`World number must be between 1 and ${maxWorld}, but got ${worldNum}`);
26-
}
27-
if (stageNum < 1 || stageNum > maxStage) {
28-
throw new Error(`Stage number must be between 1 and ${maxStage}, but got ${stageNum}`);
29-
}
30-
world = worldNum;
31-
stage = stageNum;
32-
const url = new URL(window.location.href);
33-
url.searchParams.set("w", String(world));
34-
url.searchParams.set("s", String(stageNum));
35-
replaceState(url.toString(), {});
36-
}
37-
3822
function handleKey(e: KeyboardEvent): void {
3923
const now = Date.now();
4024
if (e.key === "ArrowRight" || e.key === "ArrowLeft") {
@@ -45,31 +29,52 @@ function handleKey(e: KeyboardEvent): void {
4529
lastKey = e.key;
4630
}
4731
48-
if (stage === null || world === null) {
32+
if (search.selected === null) {
4933
return;
5034
}
5135
5236
if (e.key === "ArrowRight") {
53-
if (stage === maxStage) {
54-
if (world < maxWorld) {
55-
changeStage(world + 1, 1);
37+
if (search.selected === blocks.length - 1) {
38+
if (search.world !== 4) {
39+
search.nextWorld();
40+
search.selected = 1;
5641
}
5742
} else {
58-
changeStage(world, stage + 1);
43+
search.selected += 1;
5944
}
6045
} else if (e.key === "ArrowLeft") {
61-
if (stage === 1) {
62-
if (world > 1) {
63-
changeStage(world - 1, maxStage);
46+
if (search.selected === 0) {
47+
if (search.world !== 1) {
48+
search.prevWorld();
49+
search.selected = blocks.length - 1;
6450
}
6551
} else {
66-
changeStage(world, stage - 1);
52+
search.selected -= 1;
6753
}
6854
} else if (e.key === "Enter" || e.key === " ") {
69-
window.location.href = `/game?stage=${world}-${stage}`;
55+
goto(`/game?stage=${search.world}-${search.selected}`);
7056
} else if (e.key === "Escape") {
71-
window.location.href = "/";
57+
goto("/");
7258
e.preventDefault();
59+
if (search.selected === blocks.length - 1) {
60+
if (search.world !== 4) {
61+
search.nextWorld();
62+
search.selected = 1;
63+
}
64+
} else {
65+
search.selected += 1;
66+
}
67+
} else if (e.key === "ArrowLeft") {
68+
if (search.selected === 0) {
69+
if (search.world !== 1) {
70+
search.prevWorld();
71+
search.selected = blocks.length - 1;
72+
}
73+
} else {
74+
search.selected -= 1;
75+
}
76+
} else if (e.key === "Enter" || e.key === " ") {
77+
goto(blocks[search.selected].link);
7378
}
7479
}
7580
function handleKeyUp() {
@@ -101,44 +106,48 @@ onMount(() => {
101106
<div class="text-7xl text-center flex items-center justify-center gap-8">
102107
<!-- 左矢印ボタン -->
103108
<button
104-
class="appearance-none focus:outline-none px-4 select-none cursor-pointer {Number(world) <= 1
105-
? 'invisible'
106-
: ''} hover:-translate-y-1 hover:text-gray-700 active:translate-y-0 active:text-black"
107-
aria-label="前のワールド"
108-
onclick={() => world !== null && world > 1 && changeStage(world - 1, 1)}
109-
disabled={Number(world) <= 1}
109+
class={[
110+
"appearance-none focus:outline-none px-4 select-none cursor-pointer",
111+
search.world <= 1 && 'invisible',
112+
"hover:-translate-y-1 hover:text-gray-700 active:translate-y-0 active:text-black"]}
113+
onclick={() => search.world--}
114+
disabled={search.world <= 1}
110115
>
111116
&lt;
112117
</button>
113-
<span>World {world}</span>
118+
<span>World {search.world}</span>
114119
<!-- 右矢印ボタン -->
115120
<button
116-
class="appearance-none focus:outline-none px-4 select-none cursor-pointer {Number(world) >= maxWorld
117-
? 'invisible'
118-
: ''} hover:-translate-y-1 hover:text-gray-700 active:translate-y-0 active:text-black"
119121
aria-label="次のワールド"
120-
onclick={() => world !== null && world < maxWorld && changeStage(world + 1, 1)}
121-
disabled={Number(world) >= maxWorld}
122+
onclick={() => search.world++}
123+
disabled={search.world >= MAX_WORLD}
124+
class={[
125+
"appearance-none focus:outline-none px-4 select-none cursor-pointer",
126+
search.world >= MAX_WORLD && 'invisible',
127+
"hover:-translate-y-1 hover:text-gray-700 active:translate-y-0 active:text-black"
128+
]}
122129
>
123130
&gt;
124131
</button>
125132
</div>
126133

127134
<div class="flex justify-center items-center grow-1">
128135
<div role="button" tabindex="0" class="flex outline-none items-center">
129-
{#each { length: maxStage } as _, idx}
136+
{#each blocks as block, idx}
137+
{@const stage = idx + 1}
130138
<button
131139
type="button"
132140
class={`appearance-none focus:outline-none bg-white border-6 pt-8 pb-6 pl-8 pr-6 transition-colors duration-200 text-7xl cursor-pointer ${
133-
stage === idx + 1
141+
search.selected === stage
134142
? "border-red-500 ring ring-red-500 bg-amber-100!"
135143
: "border-base"
136144
}`}
137-
onclick={() => world !== null && changeStage(world, idx + 1)}
145+
onmouseenter={() => search.selected = stage}
146+
onclick={() => goto(block.link)}
138147
>
139-
{idx + 1}
148+
{block.label}
140149
</button>
141-
{#if idx + 1 < maxStage}
150+
{#if stage < search.maxStage}
142151
<!-- 線(矢印や線) -->
143152
<div class="w-20 h-3 bg-black"></div>
144153
{/if}
@@ -150,23 +159,34 @@ onMount(() => {
150159
>
151160
<!-- 画像を中央に配置 -->
152161
<div class="h-full">
153-
{#if stage !== null}
154-
{#key world}
155-
{#each { length: 4 } as _, idx}
162+
{#if search.selected !== null}
163+
{#key search.world}
164+
{#each blocks as block, idx}
165+
{@const stage = idx + 1}
156166
<img
157-
src="/assets/thumbnail{world}-{idx + 1}.png"
167+
src={block.thumbnail}
158168
alt=""
159-
class={["h-full skeleton", idx + 1 !== stage && "hidden"]}
169+
class={["h-full skeleton", stage !== search.selected && "hidden"]}
160170
/>
161171
{/each}
162172
{/key}
163173
{/if}
164174
</div>
165175
<!-- テキストを画像の右側に配置 -->
166176
<div
167-
class="flex-none w-max max-h-full flex flex-col items-start bg-white/90 p-4 m-4 rounded-lg border-2"
177+
class="flex-none w-70 max-h-full bg-white/90 p-4 m-4 rounded-lg border-2"
168178
>
169-
Press <Key key="Enter" enabled /> or <Key key="Space" enabled /> to start
179+
<div>
180+
Click,
181+
</div>
182+
<div>
183+
Press <Key key="Enter" enabled />
184+
</div>
185+
<div>or Press <Key key="Space" enabled />
186+
</div>
187+
<div>
188+
To Start
189+
</div>
170190
</div>
171191
</div>
172192
</div>

0 commit comments

Comments
 (0)