Skip to content

Commit 521f3f0

Browse files
committed
feat: implement MusicalNote component with animation and interaction
1 parent 3c4b68a commit 521f3f0

File tree

3 files changed

+82
-18
lines changed

3 files changed

+82
-18
lines changed

web/src/modules/shared/components/layout/BlockTab.tsx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
import { IconDefinition } from '@fortawesome/free-solid-svg-icons';
22
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
3-
import Link from 'next/link';
4-
53
import { cn } from '@web/src/lib/tailwind.utils';
4+
import Link from 'next/link';
5+
import { MusicalNote } from './MusicalNote';
66

77
export const BlockTab = ({
88
href,
@@ -25,6 +25,7 @@ export const BlockTab = ({
2525
>
2626
<FontAwesomeIcon icon={icon} />
2727
<span className='hidden sm:block'>{label}</span>
28+
<MusicalNote />
2829
</Link>
2930
);
3031
};
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
@keyframes noteAnimation {
2+
0% {
3+
transform: scale(0) translate(0, 0);
4+
opacity: 0;
5+
}
6+
7+
1% {
8+
transform: scale(1) translate(0, 0);
9+
opacity: 1;
10+
}
11+
12+
50% {
13+
transform: scale(1.2) translate(0, 0);
14+
transform: scale(1) translate(0, -20px);
15+
opacity: 1;
16+
}
17+
18+
99% {
19+
transform: scale(1) translate(0, -40px);
20+
opacity: 1;
21+
}
22+
23+
100% {
24+
transform: scale(0) translate(0, -40px);
25+
opacity: 0;
26+
}
27+
}
28+
29+
.animate-note {
30+
animation: noteAnimation 0.8s ease-out;
31+
animation-fill-mode: forwards;
32+
transform-origin: 50% 50%;
33+
position: absolute;
34+
display: none;
35+
}
36+
37+
.animate-note-active {
38+
display: block;
39+
}
Lines changed: 40 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
'use client';
2-
import { useCallback, useState } from 'react';
3-
2+
import { useCallback, useEffect, useRef, useState } from 'react';
3+
import './MusicalNote.css';
44
const notesSpritesSheet = '/notes_sprites.png';
55

66
const spritesSheetSize = {
@@ -24,44 +24,68 @@ interface MusicalNoteProps {
2424
size?: number;
2525
}
2626

27-
export const MusicalNote = ({ size = 8 }: MusicalNoteProps) => {
28-
const [currentNote, setCurrentNote] = useState(0);
27+
export const MusicalNote = ({ size = 4 }: MusicalNoteProps) => {
28+
const [currentNote, setCurrentNote] = useState(
29+
Math.floor(Math.random() * totalNotes),
30+
);
2931

3032
const noteToCell = useCallback(() => {
33+
const index = Math.abs(currentNote) % totalNotes;
3134
return {
32-
x: currentNote % gridDimensions.x,
33-
y: Math.floor(currentNote / gridDimensions.x),
35+
x: index % gridDimensions.x,
36+
y: Math.floor(index / gridDimensions.x),
3437
};
3538
}, [currentNote]);
3639

37-
const nextNote = () => {
38-
setCurrentNote((currentNote + 1) % totalNotes);
39-
};
40+
const cell = noteToCell();
41+
const ref = useRef<HTMLDivElement | null>(null);
42+
43+
useEffect(() => {
44+
const handleClick = () => {
45+
if (ref.current) {
46+
ref.current.classList.remove('animate-note');
47+
ref.current.classList.add('animate-note-active');
48+
// Trigger reflow to restart the animation
49+
void ref.current.offsetWidth;
50+
ref.current.classList.add('animate-note');
4051

41-
const onNoteClick = () => {
42-
nextNote();
43-
};
52+
setTimeout(() => {
53+
ref.current?.classList.remove('animate-note-active');
54+
}, 500); // Match the duration of the animation
4455

45-
const cell = noteToCell();
56+
setCurrentNote((prev) => (prev === totalNotes - 1 ? 0 : prev + 1));
57+
}
58+
};
59+
60+
ref.current?.parentElement?.addEventListener('click', handleClick);
61+
62+
return () => {
63+
ref.current?.parentElement?.removeEventListener('click', handleClick);
64+
};
65+
}, []);
4666

4767
return (
4868
<div
69+
className='musical-note animate-note-active'
70+
ref={ref}
4971
style={{
5072
width: singleNoteSize.width * size, // Scale display size
5173
height: singleNoteSize.height * size, // Scale display size
52-
5374
backgroundImage: `url(${notesSpritesSheet})`,
5475
backgroundPosition: `-${cell.x * singleNoteSize.width * size}px -${
5576
cell.y * singleNoteSize.height * size
5677
}px`,
5778
backgroundSize: `${spritesSheetSize.width * size}px ${
5879
spritesSheetSize.height * size
5980
}px`, // Scale background
60-
81+
zIndex: 999,
6182
imageRendering: 'pixelated',
6283
cursor: 'pointer',
84+
position: 'absolute', // Ensure the parent element is positioned relatively
85+
opacity: 0,
86+
// disable pointer events to allow the parent element to handle the click event
87+
pointerEvents: 'none',
6388
}}
64-
onClick={onNoteClick}
6589
/>
6690
);
6791
};

0 commit comments

Comments
 (0)