Skip to content

Commit

Permalink
feat: improve user experience
Browse files Browse the repository at this point in the history
  • Loading branch information
micooz committed Mar 30, 2024
1 parent fc0be4e commit e2d999e
Show file tree
Hide file tree
Showing 10 changed files with 165 additions and 99 deletions.
19 changes: 1 addition & 18 deletions next.config.mjs
Original file line number Diff line number Diff line change
@@ -1,21 +1,4 @@
/** @type {import('next').NextConfig} */
const nextConfig = {
/**
* Enable static exports for the App Router.
*
* @see https://nextjs.org/docs/app/building-your-application/deploying/static-exports
*/
output: "export",

/**
* Disable server-based image optimization. Next.js does not support
* dynamic features with static exports.
*
* @see https://nextjs.org/docs/app/api-reference/components/image#unoptimized
*/
images: {
unoptimized: true,
},
};
const nextConfig = {};

export default nextConfig;
Binary file modified screenshot.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
9 changes: 7 additions & 2 deletions src/app/components/ChordTable/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -56,11 +56,16 @@ export function ChordTable(props: ChordTableProps) {

// methods

function onPlayScale() {
async function onPlayScale() {
if (!pianoRef.current) {
return;
}
pianoRef.current.play("loop-once", modeKeys);

await pianoRef.current.attackOneByOne([...modeKeys], 200);

const reversedModeKeys = [...modeKeys].reverse();
reversedModeKeys.shift();
await pianoRef.current.attackOneByOne(reversedModeKeys, 200);
}

function onAddChord(chord: Chord) {
Expand Down
2 changes: 1 addition & 1 deletion src/app/components/ModeList/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,7 @@ export function ModeList(props: ModeListProps) {
<ReactSortable
list={state.list}
setList={(newList) => (state.list = newList)}
onSort={() => onSort()}
onSort={onSort}
animation={200}
handle=".dragHandle"
className={responsiveClassNames}
Expand Down
5 changes: 4 additions & 1 deletion src/app/page.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { ScrollTop } from "primereact/scrolltop";

import { ToneLoader } from "@/components/ToneLoader";

import { ModeList } from "./components/ModeList";
Expand All @@ -8,9 +10,10 @@ export default function Home() {
return (
<main className="">
<Header />
<ToneLoader />
<ProgressionDesigner className="mb-4 px-4" />
<ModeList className="px-4" />
<ToneLoader />
<ScrollTop />
</main>
);
}
9 changes: 5 additions & 4 deletions src/components/PercussionPad/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ export const PercussionPad = React.forwardRef<
disabled,
notes,
active = false,
activeBgColor = "#3a7bd0",
activeBgColor = "#3576cb",
activeFgColor = "#ffffff",
children,
} = props;
Expand All @@ -51,11 +51,11 @@ export const PercussionPad = React.forwardRef<
// highlight,
}));

function highlight() {
function onMouseEnter() {
if (disabled) {
return;
}
pianoRef.current?.highlight(notes);
pianoRef.current?.mouseHover(notes);
}

function onAttack() {
Expand All @@ -71,6 +71,7 @@ export const PercussionPad = React.forwardRef<
return;
}
pianoRef.current?.release();
pianoRef.current?.mouseLeave();
state.pressed = false;
}

Expand All @@ -82,7 +83,7 @@ export const PercussionPad = React.forwardRef<
background: state.pressed || active ? activeBgColor : undefined,
color: state.pressed || active ? activeFgColor : undefined,
}}
onMouseEnter={highlight}
onMouseEnter={onMouseEnter}
onTouchStart={onAttack}
onTouchEnd={onRelease}
onMouseLeave={onRelease}
Expand Down
40 changes: 32 additions & 8 deletions src/components/PianoKeyboard/index.scss
Original file line number Diff line number Diff line change
@@ -1,20 +1,44 @@
.pianoKeyboard {
.whiteKey {
&.disabled {
background: #ccc;
&.dim {
background: #cae6fc;
color: #fff;
}
&.pressed {
background: #6aafff;
&.playing,
&.hover {
background: #3576cb;
color: #fff;
}
&.disabled {
background: #ccc
repeating-linear-gradient(
30deg,
rgba(0, 0, 0, 0.3),
rgba(0, 0, 0, 0.3) 10px,
transparent 0,
transparent 20px
);
}
}
.blackKey {
&.disabled {
background: #666;
&.dim {
background: #6e96b7;
color: #fff;
}
&.pressed {
background: #095ab7;
&.playing,
&.hover {
background: #3576cb;
color: #fff;
}
&.disabled {
background: #666
repeating-linear-gradient(
30deg,
rgba(0, 0, 0, 0.3),
rgba(0, 0, 0, 0.3) 8px,
transparent 0,
transparent 16px
);
}
}
}
122 changes: 64 additions & 58 deletions src/components/PianoKeyboard/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,17 +19,17 @@ export interface PianoKeyboardProps {
endNote?: string;
showLabelFor?: ("c" | "blacks" | "whites")[];
showHighlightNotesHint?: boolean;
clearHighlightAfterRelease?: boolean;
enabledKeys?: string[];
dottedNotes?: string[];
}

export interface PianoKeyboardRef {
highlight: (keys: string[]) => void;
mouseHover: (keys: string[]) => void;
mouseLeave: () => void;
attack: (keys: string[]) => Promise<void>;
attackRelease: (keys: string[], duration: number) => Promise<void>;
attackOneByOne: (keys: string[], interval: number) => Promise<void>;
release: () => Promise<void>;
play: (mode: "loop-once", keys: string[]) => Promise<void>;
isPlaying: () => boolean;
}

Expand All @@ -43,16 +43,18 @@ export const PianoKeyboard = React.forwardRef<
endNote = "B5",
showLabelFor = ["c"],
showHighlightNotesHint = false,
clearHighlightAfterRelease = false,
enabledKeys = [],
dottedNotes = [],
} = props;

const boxRef = useRef<HTMLDivElement>(null);
const pianoPlayingRef = useRef(false);
const lastPlayedNotesRef = useRef<string[]>([]);

const state = useReactive({
highlightNotes: [] as string[],
hoverNotes: [] as string[],
playingNotes: [] as string[],
lastPlayedNotes: [] as string[],
});

const collection = useMemo(
Expand All @@ -72,90 +74,77 @@ export const PianoKeyboard = React.forwardRef<
}, [dottedNotes]);

useImperativeHandle(ref, () => ({
highlight,
mouseHover,
mouseLeave,
attack,
attackRelease,
attackOneByOne,
release,
play,
isPlaying,
}));

// methods

function highlight(keys: string[]) {
if (pianoPlayingRef.current) {
return;
}
state.highlightNotes = keys;
function mouseHover(keys: string[]) {
state.hoverNotes = keys;
}

function mouseLeave() {
state.hoverNotes = [];
}

async function attack(keys: string[]) {
if (pianoPlayingRef.current) {
return;
}
await toneUtil.triggerAttack(keys);
state.highlightNotes = keys;
state.playingNotes = keys;
lastPlayedNotesRef.current = keys;
}

async function attackRelease(keys: string[], duration: number) {
if (pianoPlayingRef.current) {
return;
}
await toneUtil.triggerAttackRelease(keys, duration / 1000);
state.highlightNotes = keys;
state.playingNotes = keys;
lastPlayedNotesRef.current = keys;

await sleep(duration);

if (clearHighlightAfterRelease) {
state.highlightNotes = [];
}
state.lastPlayedNotes = lastPlayedNotesRef.current;
state.playingNotes = [];
}

async function release() {
async function attackOneByOne(keys: string[], interval: number) {
if (pianoPlayingRef.current) {
return;
}
await toneUtil.releaseAll();

if (clearHighlightAfterRelease) {
state.highlightNotes = [];
}
}

function isPlaying() {
return pianoPlayingRef.current;
}

async function play(mode: "loop-once", keys: string[]) {
if (mode === "loop-once") {
await playOneByOne([...keys]);
pianoPlayingRef.current = true;
state.lastPlayedNotes = [];

const reverse = [...keys].reverse();
reverse.shift();
await playOneByOne(reverse);
return;
for (const note of keys) {
toneUtil.triggerAttackRelease(note, interval / 1000);
state.playingNotes = [note];
await sleep(interval);
}

// if (mode === "simultaneously") {
// await playTogether(keys);
// }
state.playingNotes = [];
pianoPlayingRef.current = false;
}

async function playOneByOne(keys: string[]) {
async function release() {
if (pianoPlayingRef.current) {
return;
}
await toneUtil.releaseAll();

pianoPlayingRef.current = true;

for (const note of keys) {
toneUtil.triggerAttackRelease(note, 0.2);
state.highlightNotes = [note];
await sleep(200);
}
state.lastPlayedNotes = lastPlayedNotesRef.current;
state.playingNotes = [];
}

state.highlightNotes = [];
pianoPlayingRef.current = false;
function isPlaying() {
return pianoPlayingRef.current;
}

// events
Expand Down Expand Up @@ -191,7 +180,7 @@ export const PianoKeyboard = React.forwardRef<
// components

function HighlightHint() {
const notes = state.highlightNotes.map((note) => Note.from(note));
const notes = state.playingNotes.map((note) => Note.from(note));
const hint = notes
.map((note) => note.nameWithGroup({ transformAccidental: true }))
.join(", ");
Expand Down Expand Up @@ -221,7 +210,11 @@ export const PianoKeyboard = React.forwardRef<
key.is(note, { checkAccidental: true })
);

const pressed = state.highlightNotes.includes(note.nameWithGroup());
const noteWithGroup = note.nameWithGroup();

const playing = state.playingNotes.includes(noteWithGroup);
const hover = state.hoverNotes.includes(noteWithGroup);
const dim = state.lastPlayedNotes.includes(noteWithGroup);

const dotted = dottedKeys.find((key) =>
key.is(note, { checkAccidental: true })
Expand All @@ -248,9 +241,10 @@ export const PianoKeyboard = React.forwardRef<
"relative",
"flex items-end flex-1 border-[2px] border-black -ml-[2px]",
"text-black bg-white",
"shadow-[inset_0_0_4px_2px_#ddd]",
disabled && "disabled",
pressed && "pressed"
playing && "playing",
hover && "hover",
dim && "dim"
)}
onTouchStart={() => {
if (disabled) {
Expand Down Expand Up @@ -284,9 +278,20 @@ export const PianoKeyboard = React.forwardRef<
key.is(nextB, { checkAccidental: true })
);

const pressed =
state.highlightNotes.includes(nextA.nameWithGroup()) ||
state.highlightNotes.includes(nextB.nameWithGroup());
const nextAWithGroup = nextA.nameWithGroup();
const nextBWithGroup = nextB.nameWithGroup();

const playing =
state.playingNotes.includes(nextAWithGroup) ||
state.playingNotes.includes(nextBWithGroup);

const hover =
state.hoverNotes.includes(nextAWithGroup) ||
state.hoverNotes.includes(nextBWithGroup);

const dim =
state.lastPlayedNotes.includes(nextAWithGroup) ||
state.lastPlayedNotes.includes(nextBWithGroup);

const dotted = dottedKeys.find(
(key) =>
Expand All @@ -308,9 +313,10 @@ export const PianoKeyboard = React.forwardRef<
"flex items-end justify-center flex-1",
"bg-black text-white",
"rounded-bl-[4px] rounded-br-[4px]",
"shadow-[0_2px_4px_2px_rgba(221,221,221,0.5)]",
disabled && "disabled",
pressed && "pressed"
playing && "playing",
hover && "hover",
dim && "dim"
)}
onTouchStart={() => {
if (disabled) {
Expand Down
Loading

0 comments on commit e2d999e

Please sign in to comment.