feat: chip sound effects
This commit is contained in:
@@ -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 />
|
||||||
|
|||||||
@@ -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
4
connect4-ui/lib/sfx.ts
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
export const CHIP_DROP_SOUND_PATHS = Array.from(
|
||||||
|
{ length: 7 },
|
||||||
|
(_, index) => `/sfx/chip_collide_${index + 1}.ogg`,
|
||||||
|
);
|
||||||
BIN
connect4-ui/public/sfx/chip_collide_1.ogg
Normal file
BIN
connect4-ui/public/sfx/chip_collide_1.ogg
Normal file
Binary file not shown.
BIN
connect4-ui/public/sfx/chip_collide_2.ogg
Normal file
BIN
connect4-ui/public/sfx/chip_collide_2.ogg
Normal file
Binary file not shown.
BIN
connect4-ui/public/sfx/chip_collide_3.ogg
Normal file
BIN
connect4-ui/public/sfx/chip_collide_3.ogg
Normal file
Binary file not shown.
BIN
connect4-ui/public/sfx/chip_collide_4.ogg
Normal file
BIN
connect4-ui/public/sfx/chip_collide_4.ogg
Normal file
Binary file not shown.
BIN
connect4-ui/public/sfx/chip_collide_5.ogg
Normal file
BIN
connect4-ui/public/sfx/chip_collide_5.ogg
Normal file
Binary file not shown.
BIN
connect4-ui/public/sfx/chip_collide_6.ogg
Normal file
BIN
connect4-ui/public/sfx/chip_collide_6.ogg
Normal file
Binary file not shown.
BIN
connect4-ui/public/sfx/chip_collide_7.ogg
Normal file
BIN
connect4-ui/public/sfx/chip_collide_7.ogg
Normal file
Binary file not shown.
Reference in New Issue
Block a user