Use proper SVG icons in the touch control bar, center the middle cluster absolutely

Swaps the unicode glyphs (◀ ▶ ⬆) for hand-drawn stroke/fill SVG icons
(chevrons, an eject-style plunger glyph, a coin glyph, a play triangle),
and positions the coin/start/plunger cluster with absolute + translate
so it sits on the bar's true center regardless of the flipper buttons.
This commit is contained in:
2026-08-24 13:28:45 +02:00
parent 9e8f08ba16
commit 0afa61cc5c
+62 -16
View File
@@ -25,13 +25,62 @@ function isTouchDevice(): boolean {
return typeof window !== "undefined" && ("ontouchstart" in window || navigator.maxTouchPoints > 0); return typeof window !== "undefined" && ("ontouchstart" in window || navigator.maxTouchPoints > 0);
} }
function FlipperLeftIcon() {
return (
<svg viewBox="0 0 24 24" width="28" height="28" fill="none" stroke="currentColor" strokeWidth="3">
<polyline points="15 5 8 12 15 19" strokeLinecap="round" strokeLinejoin="round" />
</svg>
);
}
function FlipperRightIcon() {
return (
<svg viewBox="0 0 24 24" width="28" height="28" fill="none" stroke="currentColor" strokeWidth="3">
<polyline points="9 5 16 12 9 19" strokeLinecap="round" strokeLinejoin="round" />
</svg>
);
}
/** Eject-style glyph (triangle over a bar) for the plunger/launch control. */
function PlungerIcon() {
return (
<svg viewBox="0 0 24 24" width="20" height="20" fill="currentColor">
<path d="M12 4.5 19 15H5l7-10.5Z" />
<rect x="5" y="17.5" width="14" height="2.5" rx="1" />
</svg>
);
}
function CoinIcon() {
return (
<svg viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor" strokeWidth="1.75">
<circle cx="12" cy="12" r="8.5" />
<path
d="M14.5 9.3c-.4-.8-1.3-1.3-2.5-1.3-1.7 0-3 .9-3 2.1 0 2.9 5.5 1.3 5.5 4.1 0 1.2-1.3 2.1-3 2.1-1.2 0-2.1-.5-2.5-1.3M12 6.8v1.2M12 16v1.2"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
);
}
function PlayIcon() {
return (
<svg viewBox="0 0 24 24" width="18" height="18" fill="currentColor">
<path d="M7 4.5v15l13-7.5-13-7.5Z" />
</svg>
);
}
function ControlButton({ function ControlButton({
binding, binding,
label, label,
icon,
size, size,
}: { }: {
binding: KeyBinding; binding: KeyBinding;
label: string; label: string;
icon: React.ReactNode;
size: "lg" | "md" | "sm"; size: "lg" | "md" | "sm";
}) { }) {
const [pressed, setPressed] = useState(false); const [pressed, setPressed] = useState(false);
@@ -50,28 +99,23 @@ function ControlButton({
dispatchKey("keyup", binding); dispatchKey("keyup", binding);
}, [binding]); }, [binding]);
const sizeClasses = const sizeClasses = size === "lg" ? "h-20 w-20" : size === "md" ? "h-14 w-14" : "h-12 w-12";
size === "lg"
? "h-20 w-20 text-3xl"
: size === "md"
? "h-14 w-14 text-lg"
: "h-12 w-14 font-mono text-[11px] tracking-widest";
return ( return (
<button <button
type="button" type="button"
aria-label={binding.key === "Shift" ? `${label} flipper` : label} aria-label={label}
onPointerDown={onDown} onPointerDown={onDown}
onPointerUp={onUp} onPointerUp={onUp}
onPointerCancel={onUp} onPointerCancel={onUp}
onContextMenu={(e) => e.preventDefault()} onContextMenu={(e) => e.preventDefault()}
className={`touch-none select-none rounded-full border-2 font-display transition ${sizeClasses} ${ className={`flex touch-none select-none items-center justify-center rounded-full border-2 transition ${sizeClasses} ${
pressed pressed
? "border-marquee bg-marquee/30 text-marquee shadow-[0_0_16px_rgba(255,122,41,0.5)]" ? "border-marquee bg-marquee/30 text-marquee shadow-[0_0_16px_rgba(255,122,41,0.5)]"
: "border-chrome/40 bg-black/40 text-chrome" : "border-chrome/40 bg-black/40 text-chrome"
}`} }`}
> >
{label} {icon}
</button> </button>
); );
} }
@@ -97,17 +141,19 @@ export default function TouchControlBar({ active }: TouchControlBarProps) {
return ( return (
<div <div
data-hud data-hud
className="absolute inset-x-0 bottom-0 z-10 flex items-center justify-between gap-2 border-t border-chrome/30 bg-ash/90 px-3 pb-[max(0.75rem,env(safe-area-inset-bottom))] pt-3 backdrop-blur sm:hidden" className="absolute inset-x-0 bottom-0 z-10 flex items-center justify-between border-t border-chrome/30 bg-ash/90 px-3 pb-[max(0.75rem,env(safe-area-inset-bottom))] pt-3 backdrop-blur sm:hidden"
> >
<ControlButton binding={LEFT_FLIPPER} label="◀" size="lg" /> <ControlButton binding={LEFT_FLIPPER} label="Left flipper" icon={<FlipperLeftIcon />} size="lg" />
<div className="flex shrink-0 items-center gap-2"> {/* Positioned absolutely so it sits on the bar's true center, independent
<ControlButton binding={INSERT_COIN} label="COIN" size="sm" /> of the (equal-width, so already symmetric) flipper buttons on either side. */}
<ControlButton binding={START_GAME} label="START" size="sm" /> <div className="absolute left-1/2 top-1/2 flex -translate-x-1/2 -translate-y-1/2 items-center gap-3">
<ControlButton binding={PLUNGER} label="⬆" size="md" /> <ControlButton binding={INSERT_COIN} label="Insert coin" icon={<CoinIcon />} size="sm" />
<ControlButton binding={START_GAME} label="Start game" icon={<PlayIcon />} size="sm" />
<ControlButton binding={PLUNGER} label="Plunger" icon={<PlungerIcon />} size="md" />
</div> </div>
<ControlButton binding={RIGHT_FLIPPER} label="▶" size="lg" /> <ControlButton binding={RIGHT_FLIPPER} label="Right flipper" icon={<FlipperRightIcon />} size="lg" />
</div> </div>
); );
} }