Skip to content

Commit d8aad23

Browse files
authored
added demo picker (#17)
1 parent d2b5926 commit d8aad23

5 files changed

Lines changed: 366 additions & 60 deletions

File tree

packages/playground/src/App.css

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,45 @@
114114
position: relative;
115115
}
116116

117+
.demo-selector {
118+
position: absolute;
119+
left: 50%;
120+
bottom: 24px;
121+
transform: translateX(-50%);
122+
display: flex;
123+
align-items: center;
124+
gap: 0;
125+
z-index: 20;
126+
pointer-events: auto;
127+
}
128+
129+
.demo-selector-button {
130+
min-width: 32px;
131+
padding: 6px 10px;
132+
font-size: 12px;
133+
}
134+
135+
.demo-selector .button {
136+
background: #2b2b2b;
137+
border-color: rgba(255, 255, 255, 0.25);
138+
color: var(--color-text-primary);
139+
}
140+
141+
.demo-selector .button:hover {
142+
background: #343434;
143+
border-color: rgba(255, 255, 255, 0.35);
144+
}
145+
146+
.demo-selector .button.playing {
147+
background: #3b2b10;
148+
border-color: var(--color-active-pause);
149+
color: var(--color-text-primary);
150+
}
151+
152+
.demo-selector .button.playing:hover {
153+
background: #463414;
154+
}
155+
117156
.canvas-overlay {
118157
position: absolute;
119158
top: 0;
@@ -210,4 +249,14 @@ body {
210249
height: 50dvh;
211250
order: -1; /* Put canvas first on mobile */
212251
}
252+
253+
.demo-selector {
254+
bottom: 16px;
255+
}
256+
257+
.demo-selector-button {
258+
min-width: 28px;
259+
padding: 6px 8px;
260+
font-size: 11px;
261+
}
213262
}

packages/playground/src/App.tsx

Lines changed: 55 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -54,8 +54,16 @@ function AppContent() {
5454
const dispatch = useAppDispatch();
5555
const { barsVisible, restoreBarsFromFullscreenMode, setBarsVisibility } = useUI();
5656
const { invertColors } = useRender();
57-
const { hasStarted, isPlaying: isDemoPlaying, stop, play: playDemo, reduceParticles } =
58-
useDemo({ canAutostart: !openedWithSessionUrl });
57+
const {
58+
hasStarted,
59+
isPlaying: isDemoPlaying,
60+
stop,
61+
play: playDemo,
62+
reduceParticles,
63+
demoCount,
64+
currentDemoIndex,
65+
selectDemo,
66+
} = useDemo({ canAutostart: !openedWithSessionUrl });
5967
const {
6068
spawnParticles,
6169
play: playEngine,
@@ -269,6 +277,32 @@ function AppContent() {
269277
resetUrlToPlay,
270278
]);
271279

280+
const handleSelectHomepageDemo = useCallback(
281+
(index: number) => {
282+
selectDemo(index, "resume");
283+
},
284+
[selectDemo]
285+
);
286+
287+
const handleSelectHomepageNext = useCallback(() => {
288+
if (demoCount === 0) return;
289+
const nextIndex = (currentDemoIndex + 1) % demoCount;
290+
selectDemo(nextIndex, "resume");
291+
}, [currentDemoIndex, demoCount, selectDemo]);
292+
293+
const handleSelectHomepagePrev = useCallback(() => {
294+
if (demoCount === 0) return;
295+
const prevIndex = (currentDemoIndex - 1 + demoCount) % demoCount;
296+
selectDemo(prevIndex, "resume");
297+
}, [currentDemoIndex, demoCount, selectDemo]);
298+
299+
const handleSelectPlaygroundDemo = useCallback(
300+
(index: number) => {
301+
selectDemo(index, "pause");
302+
},
303+
[selectDemo]
304+
);
305+
272306
// Handle fullscreen change events (when user exits via ESC or browser controls)
273307
useEffect(() => {
274308
const handleFullscreenChange = () => {
@@ -327,9 +361,28 @@ function AppContent() {
327361
>
328362
<Canvas className="canvas" isPlaying={isDemoPlaying} />
329363
<Overlay isPlaying={isDemoPlaying} />
364+
{!isHomepage && isDemoPlaying && demoCount > 0 && (
365+
<div className="demo-selector button-group" aria-label="Demo selector">
366+
{Array.from({ length: demoCount }).map((_, index) => (
367+
<button
368+
key={`demo-${index}`}
369+
className={`button demo-selector-button ${index === currentDemoIndex ? "playing" : ""}`}
370+
onClick={() => handleSelectPlaygroundDemo(index)}
371+
aria-pressed={index === currentDemoIndex}
372+
>
373+
{index + 1}
374+
</button>
375+
))}
376+
</div>
377+
)}
330378
<Homepage
331379
onPlay={handlePlay}
332380
isVisible={isHomepage}
381+
demoCount={demoCount}
382+
currentDemoIndex={currentDemoIndex}
383+
onSelectDemo={handleSelectHomepageDemo}
384+
onSwipeNextDemo={handleSelectHomepageNext}
385+
onSwipePrevDemo={handleSelectHomepagePrev}
333386
isWebGPUWarningDismissed={isWebGPUWarningDismissed}
334387
onDismissWebGPUWarning={() => setIsWebGPUWarningDismissed(true)}
335388
/>

packages/playground/src/components/Homepage.css

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,63 @@
8686
background-color: #333333;
8787
}
8888

89+
.homepage-demo-dots {
90+
position: fixed;
91+
left: 50%;
92+
bottom: calc(24px + env(safe-area-inset-bottom));
93+
transform: translateX(-50%);
94+
display: flex;
95+
align-items: center;
96+
gap: 4px;
97+
z-index: 200;
98+
pointer-events: auto;
99+
}
100+
101+
102+
.homepage-demo-dot-button {
103+
-webkit-tap-highlight-color: transparent;
104+
width: 32px;
105+
height: 32px;
106+
display: inline-flex;
107+
align-items: center;
108+
justify-content: center;
109+
border: none;
110+
background: transparent;
111+
padding: 0;
112+
cursor: pointer;
113+
border-radius: 999px;
114+
transition: background 0.2s ease;
115+
}
116+
117+
.homepage-demo-dot-button:focus,
118+
.homepage-demo-dot-button:active {
119+
background: transparent;
120+
outline: none;
121+
}
122+
123+
.homepage-demo-dot-button:focus-visible {
124+
outline: 2px solid rgba(0, 0, 0, 0.2);
125+
outline-offset: 2px;
126+
}
127+
128+
.homepage-demo-dot-button:hover .homepage-demo-dot{
129+
opacity: 0.75;
130+
}
131+
132+
.homepage-demo-dot {
133+
width: 6px;
134+
height: 6px;
135+
border-radius: 999px;
136+
background: #000000;
137+
opacity: 0.25;
138+
transition: opacity 0.2s ease, transform 0.2s ease;
139+
}
140+
141+
.homepage-demo-dot-button.active .homepage-demo-dot {
142+
opacity: 0.75;
143+
transform: scale(1.2);
144+
}
145+
89146
@media (max-width: 1024px) {
90147
.homepage-subtitle {
91148
width: 800px;
@@ -108,3 +165,19 @@
108165
}
109166
}
110167

168+
@media (max-width: 768px) {
169+
.homepage-demo-dots {
170+
bottom: calc(16px + env(safe-area-inset-bottom));
171+
gap: 6px;
172+
}
173+
174+
.homepage-demo-dot-button {
175+
width: 32px;
176+
height: 32px;
177+
}
178+
179+
.homepage-demo-dot {
180+
width: 7px;
181+
height: 7px;
182+
}
183+
}

packages/playground/src/components/Homepage.tsx

Lines changed: 71 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,13 +9,23 @@ import "./Homepage.css";
99
interface HomepageProps {
1010
onPlay: () => void;
1111
isVisible: boolean;
12+
demoCount: number;
13+
currentDemoIndex: number;
14+
onSelectDemo: (index: number) => void;
15+
onSwipeNextDemo: () => void;
16+
onSwipePrevDemo: () => void;
1217
isWebGPUWarningDismissed: boolean;
1318
onDismissWebGPUWarning: () => void;
1419
}
1520

1621
export function Homepage({
1722
onPlay,
1823
isVisible,
24+
demoCount,
25+
currentDemoIndex,
26+
onSelectDemo,
27+
onSwipeNextDemo,
28+
onSwipePrevDemo,
1929
isWebGPUWarningDismissed,
2030
onDismissWebGPUWarning,
2131
}: HomepageProps) {
@@ -24,6 +34,7 @@ export function Homepage({
2434
const { canvasRef, screenToWorld, isWebGPU, isInitialized, isInitializing } = useEngine();
2535
const { setPosition, setActive, setMode, setStrength, setRadius } = useInteraction();
2636
const isMouseDownRef = useRef(false);
37+
const touchStartRef = useRef<{ x: number; y: number; time: number } | null>(null);
2738

2839
// Mouse and touch interaction for homepage demo
2940
useEffect(() => {
@@ -70,6 +81,12 @@ export function Homepage({
7081
setActive(false);
7182
};
7283

84+
const handleTouchStart = (e: TouchEvent) => {
85+
const touch = e.touches[0];
86+
if (!touch) return;
87+
touchStartRef.current = { x: touch.clientX, y: touch.clientY, time: Date.now() };
88+
};
89+
7390
const handleTouchMove = (e: TouchEvent) => {
7491
e.preventDefault();
7592
const touch = e.touches[0];
@@ -82,10 +99,30 @@ export function Homepage({
8299
setActive(true);
83100
};
84101

85-
const handleTouchEnd = () => {
102+
const handleTouchEnd = (e: TouchEvent) => {
103+
const touch = e.changedTouches[0];
104+
const start = touchStartRef.current;
105+
touchStartRef.current = null;
106+
if (touch && start && demoCount > 0) {
107+
const deltaX = touch.clientX - start.x;
108+
const deltaY = touch.clientY - start.y;
109+
const elapsed = Date.now() - start.time;
110+
const horizontalSwipe = Math.abs(deltaX) > 60 && Math.abs(deltaX) > Math.abs(deltaY) * 1.3;
111+
const withinTime = elapsed < 600;
112+
113+
if (horizontalSwipe && withinTime) {
114+
if (deltaX < 0) {
115+
onSwipeNextDemo();
116+
} else {
117+
onSwipePrevDemo();
118+
}
119+
}
120+
}
121+
86122
setActive(false);
87123
};
88124

125+
canvas.addEventListener("touchstart", handleTouchStart, { passive: true });
89126
canvas.addEventListener("mousedown", handleMouseDown);
90127
canvas.addEventListener("mousemove", handleMouseMove);
91128
canvas.addEventListener("mouseup", handleMouseUp);
@@ -95,6 +132,7 @@ export function Homepage({
95132
canvas.addEventListener("touchcancel", handleTouchEnd);
96133

97134
return () => {
135+
canvas.removeEventListener("touchstart", handleTouchStart);
98136
canvas.removeEventListener("mousedown", handleMouseDown);
99137
canvas.removeEventListener("mousemove", handleMouseMove);
100138
canvas.removeEventListener("mouseup", handleMouseUp);
@@ -104,8 +142,24 @@ export function Homepage({
104142
canvas.removeEventListener("touchcancel", handleTouchEnd);
105143
setActive(false);
106144
isMouseDownRef.current = false;
145+
touchStartRef.current = null;
107146
};
108-
}, [isVisible, showWarning, canvasRef, screenToWorld, setPosition, setActive, setMode, setStrength, setRadius, isWebGPU, isMobile]);
147+
}, [
148+
isVisible,
149+
showWarning,
150+
canvasRef,
151+
screenToWorld,
152+
setPosition,
153+
setActive,
154+
setMode,
155+
setStrength,
156+
setRadius,
157+
isWebGPU,
158+
isMobile,
159+
demoCount,
160+
onSwipeNextDemo,
161+
onSwipePrevDemo,
162+
]);
109163

110164
if (!isVisible) return null;
111165

@@ -171,6 +225,21 @@ export function Homepage({
171225
</a>
172226
</div>
173227
</div>
228+
{demoCount > 0 && (
229+
<div className="homepage-demo-dots" aria-label="Demo selector">
230+
{Array.from({ length: demoCount }).map((_, index) => (
231+
<button
232+
key={`demo-dot-${index}`}
233+
className={`homepage-demo-dot-button ${index === currentDemoIndex ? "active" : ""}`}
234+
onClick={() => onSelectDemo(index)}
235+
aria-pressed={index === currentDemoIndex}
236+
aria-label={`Show demo ${index + 1}`}
237+
>
238+
<span className="homepage-demo-dot" />
239+
</button>
240+
))}
241+
</div>
242+
)}
174243
</>
175244
);
176245
}

0 commit comments

Comments
 (0)