feat: chip sound effects

This commit is contained in:
2026-04-15 14:37:42 -04:00
Unverified
parent 9c25f1464c
commit 1f7a482e75
10 changed files with 53 additions and 2 deletions

View File

@@ -3,6 +3,7 @@ import "./globals.css";
import Celebration from "@/components/Celebration"; import Celebration from "@/components/Celebration";
import Nav from "@/components/Nav"; import Nav from "@/components/Nav";
import { ConnectionProvider } from "@/lib/connection"; import { ConnectionProvider } from "@/lib/connection";
import { CHIP_DROP_SOUND_PATHS } from "@/lib/sfx";
export const metadata: Metadata = { export const metadata: Metadata = {
title: "Connect4 Moderator", title: "Connect4 Moderator",
@@ -16,6 +17,17 @@ export default function RootLayout({
}) { }) {
return ( return (
<html lang="en"> <html lang="en">
<head>
{CHIP_DROP_SOUND_PATHS.map((path) => (
<link
key={path}
rel="preload"
href={path}
as="audio"
type="audio/ogg"
/>
))}
</head>
<body className="min-h-screen bg-gray-950 text-gray-100"> <body className="min-h-screen bg-gray-950 text-gray-100">
<ConnectionProvider> <ConnectionProvider>
<Celebration /> <Celebration />

View File

@@ -3,6 +3,8 @@
import type { CSSProperties } from "react"; import type { CSSProperties } from "react";
import { useEffect, useRef, useState } from "react"; import { useEffect, useRef, useState } from "react";
import type { BoardState } from "@/lib/protocol"; import type { BoardState } from "@/lib/protocol";
import { CHIP_DROP_SOUND_PATHS } from "@/lib/sfx";
const CHIP_DROP_ANIMATION_MS = 450;
interface BoardProps { interface BoardProps {
board: BoardState; board: BoardState;
@@ -30,10 +32,27 @@ export default function Board({
} | null>(null); } | null>(null);
const isFirstRender = useRef(true); const isFirstRender = useRef(true);
const animationTimeoutRef = useRef<number | null>(null); const animationTimeoutRef = useRef<number | null>(null);
const chipDropSoundsRef = useRef<HTMLAudioElement[]>([]);
const canInteract = !disabled && !!onColumnClick; const canInteract = !disabled && !!onColumnClick;
const lastMoveColumn = lastMove?.column ?? null; const lastMoveColumn = lastMove?.column ?? null;
const lastMoveRow = lastMove?.row ?? null; const lastMoveRow = lastMove?.row ?? null;
useEffect(() => {
chipDropSoundsRef.current = CHIP_DROP_SOUND_PATHS.map((path) => {
const sound = new Audio(path);
sound.preload = "auto";
return sound;
});
return () => {
chipDropSoundsRef.current.forEach((sound) => {
sound.pause();
sound.src = "";
});
chipDropSoundsRef.current = [];
};
}, []);
useEffect(() => { useEffect(() => {
if (isFirstRender.current) { if (isFirstRender.current) {
isFirstRender.current = false; isFirstRender.current = false;
@@ -49,11 +68,26 @@ export default function Board({
return; return;
} }
const prefersReducedMotion = window.matchMedia(
"(prefers-reduced-motion: reduce)",
).matches;
const dropDurationMs = prefersReducedMotion ? 0 : CHIP_DROP_ANIMATION_MS;
setAnimatingMove({ column: lastMoveColumn, row: lastMoveRow }); setAnimatingMove({ column: lastMoveColumn, row: lastMoveRow });
animationTimeoutRef.current = window.setTimeout(() => { animationTimeoutRef.current = window.setTimeout(() => {
const sounds = chipDropSoundsRef.current;
const sound = sounds[Math.floor(Math.random() * sounds.length)];
if (sound) {
sound.currentTime = 0;
void sound.play().catch(() => {
// Ignore autoplay failures until the user has interacted.
});
}
setAnimatingMove(null); setAnimatingMove(null);
animationTimeoutRef.current = null; animationTimeoutRef.current = null;
}, 450); }, dropDurationMs);
return () => { return () => {
if (animationTimeoutRef.current !== null) { if (animationTimeoutRef.current !== null) {
@@ -192,7 +226,8 @@ export default function Board({
<style jsx>{` <style jsx>{`
.chip-drop { .chip-drop {
animation: chip-drop 450ms cubic-bezier(0.22, 1, 0.36, 1) forwards; animation: chip-drop ${CHIP_DROP_ANIMATION_MS}ms cubic-bezier(0.22, 1, 0.36, 1)
forwards;
will-change: transform; will-change: transform;
} }

4
connect4-ui/lib/sfx.ts Normal file
View File

@@ -0,0 +1,4 @@
export const CHIP_DROP_SOUND_PATHS = Array.from(
{ length: 7 },
(_, index) => `/sfx/chip_collide_${index + 1}.ogg`,
);

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.