From 4706199329ada71032b9ba8d6072817c8d29bf2e Mon Sep 17 00:00:00 2001 From: APRIASZ Date: Thu, 11 Sep 2025 10:16:22 +0200 Subject: [PATCH 1/2] added comments and tests for FAQ and About subpage --- .../creatingNoteEvents.test.ts | 22 ++++----- TESTS/OTHER/About.test.tsx | 19 ++++++++ src/Pages/About/About.tsx | 7 ++- src/Pages/About/Description/Description.tsx | 7 ++- src/Pages/About/FAQ/FAQ.tsx | 45 +++++++------------ src/Pages/About/Support/Support.tsx | 5 +++ src/Pages/Play/Play.tsx | 1 - src/Utils/FAQ_questions.tsx | 27 +++++++++++ 8 files changed, 91 insertions(+), 42 deletions(-) create mode 100644 TESTS/OTHER/About.test.tsx create mode 100644 src/Utils/FAQ_questions.tsx diff --git a/TESTS/MIDI_management/creatingNoteEvents.test.ts b/TESTS/MIDI_management/creatingNoteEvents.test.ts index 9c2fe56..a7fb2be 100644 --- a/TESTS/MIDI_management/creatingNoteEvents.test.ts +++ b/TESTS/MIDI_management/creatingNoteEvents.test.ts @@ -61,14 +61,14 @@ test("Testing: createNoteEvents", () =>{ // DELETED DURING DEVELOPMENT CAUSE TAKES TO MUCH TIME // -describe("Testing: AnimationFrameMidiPlayer", () =>{ - const onEvent = (data) =>{ - } - const note_events = createNoteEvents(MIDI_mock,timeSignatures(MIDI_mock)) - const player = new AnimationFrameMidiPlayer(note_events, onEvent); - player.pausePlay() - it("resolve in 10m seconds",async () =>{ - const result = await player.__for_testing() - expect(result).toBe(true) - }, player.MidiLength + 1000) -}) \ No newline at end of file +// describe("Testing: AnimationFrameMidiPlayer", () =>{ +// const onEvent = (data) =>{ +// } +// const note_events = createNoteEvents(MIDI_mock,timeSignatures(MIDI_mock)) +// const player = new AnimationFrameMidiPlayer(note_events, onEvent); +// player.pausePlay() +// it("resolve in 10m seconds",async () =>{ +// const result = await player.__for_testing() +// expect(result).toBe(true) +// }, player.MidiLength + 1000) +// }) \ No newline at end of file diff --git a/TESTS/OTHER/About.test.tsx b/TESTS/OTHER/About.test.tsx new file mode 100644 index 0000000..a358562 --- /dev/null +++ b/TESTS/OTHER/About.test.tsx @@ -0,0 +1,19 @@ +import { expect, it, describe} from "vitest"; +import { render, screen } from '@testing-library/react'; +import userEvent from "@testing-library/user-event"; + +import FAQ_component from "../../src/Pages/About/FAQ/FAQ.tsx"; +import questions from "../../src/Utils/FAQ_questions"; + +describe("FAQ questions rendering",()=>{ + it("should render exact number of questions", () =>{ + render(); + expect(screen.getByTestId("FAQ_container_for_questions").children.length).toEqual(questions.length); + }); + it("should change class on click", async () =>{ + render(); + const element = screen.getByTestId("FAQ_container_for_questions").children[0] + await userEvent.click(element); + expect(element.classList).toContain("FAQ_open"); + }) +}) \ No newline at end of file diff --git a/src/Pages/About/About.tsx b/src/Pages/About/About.tsx index ae38192..7db7ecc 100644 --- a/src/Pages/About/About.tsx +++ b/src/Pages/About/About.tsx @@ -8,13 +8,18 @@ import "./About_res_560.scss"; import Description from './Description/Description'; import FAQ from './FAQ/FAQ'; import Support from './Support/Support'; +import FAQ_Questions from "../../Utils/FAQ_questions"; +/** + * Page of the react app, About subpage + * @returns React Element + */ export default function About():React.ReactElement { return (
- +

Want to contribute to the project, or You have any questions? Contact: tymsonekjelonek@gmail.com, IG: @tymsonekjelonek, LinkedIn: Tymoteusz Apriasz .

diff --git a/src/Pages/About/Description/Description.tsx b/src/Pages/About/Description/Description.tsx index bcfd9d7..95ee187 100644 --- a/src/Pages/About/Description/Description.tsx +++ b/src/Pages/About/Description/Description.tsx @@ -36,7 +36,12 @@ const articles:Article[] = [ ]; -export default function Description() { +/** + * Component has a fields for description of the app, + * created to lesser the mess in the text editor + * @returns React Element + */ +export default function Description(): React.ReactElement { const render_articles = ():React.ReactElement[] | React.ReactElement =>{ return articles.map((article, index) =>
diff --git a/src/Pages/About/FAQ/FAQ.tsx b/src/Pages/About/FAQ/FAQ.tsx index db08ee2..8b2c063 100644 --- a/src/Pages/About/FAQ/FAQ.tsx +++ b/src/Pages/About/FAQ/FAQ.tsx @@ -1,36 +1,20 @@ import React, { ReactElement } from 'react' - -interface FAQ_question{ - title:string, - answer: string | React.ReactElement -} +import { FAQ_Question_type } from '../../../Utils/FAQ_questions' interface Question_props{ title:string, answer: string | React.ReactElement, } -const questions:FAQ_question[] = [ - { - title:"Are there any cookies, or following robots on this website? If so why are they like this and not like that", - answer: <>No, Piano-Blocks-App does not use any cookies or tracking devices. The only used localizing script is Vercel Analitics - , which is used to collect following data: country origin of a request, subpages visited (like /Play, /Docs etc.), and number of requests. No target advertising data is collected - - }, - { - title:"The app lags and performes poorly during playing, what can I do?", - answer:<>Easiest way to improve performance is to use "Black-n-White" preset in configurations. If you need your own configuration of visuals, set the Effects to None, - Block-Shadow-Radius to 0, and switch off sound. Also, faster speed helps to improve performance. If the app still lags through playthrough, try playing it once, - then replaying it, without refreshing the page. The replay should be in a much better performance. - - }, - { - title:"I have some feature requests for the app, how can I propose them?", - answer:<>Please go to the Github Repository of this project, and request a feature. If you believe you can implement the feature yourself, you can contact - the app developer(s) through instagram, email, or linkedin. Contact data is displayed at the bottom of the About subpage - } -]; +interface FAQ_props{ + questions:Array +} +/** + * Question component, uses state to open & close on click + * @param props for question + * @returns React Element + */ function Question({title,answer}:Question_props):ReactElement{ const [open,setOpen] = React.useState(false); @@ -38,7 +22,7 @@ function Question({title,answer}:Question_props):ReactElement{ return(
{setOpen(curr => !curr)}}>
-
+

{title}

{answer}

@@ -46,7 +30,12 @@ function Question({title,answer}:Question_props):ReactElement{ ); } -export default function FAQ():ReactElement { +/** + * FAQ element renders a set of questions, defined by given prop "questions" + * @param questions Array of questions + * @returns React Element + */ +export default function FAQ({questions}:FAQ_props):ReactElement { const renderQuestions = ():ReactElement[] | ReactElement =>{ return questions.map((question,index) => @@ -57,7 +46,7 @@ export default function FAQ():ReactElement { return (

FAQ - Frequently Asked Questions

-
+
{renderQuestions()}
diff --git a/src/Pages/About/Support/Support.tsx b/src/Pages/About/Support/Support.tsx index e933887..096089b 100644 --- a/src/Pages/About/Support/Support.tsx +++ b/src/Pages/About/Support/Support.tsx @@ -2,6 +2,11 @@ import React from 'react' import DonateImg from "../../../Assets/Donate_Img.png" +/** + * Support Block for the About Page, + * Static, Stateless component, separated just to clear out the code + * @returns React Element + */ export default function Support():React.ReactElement { return (
diff --git a/src/Pages/Play/Play.tsx b/src/Pages/Play/Play.tsx index 66bbccf..2508478 100644 --- a/src/Pages/Play/Play.tsx +++ b/src/Pages/Play/Play.tsx @@ -66,7 +66,6 @@ export default function Play():React.ReactElement{ } },[player]) - //Page Renders the piano, if the width is to small, it renders "Screen Width to small" //Rendering the components to handle playing management, and component to handle the piano return (
diff --git a/src/Utils/FAQ_questions.tsx b/src/Utils/FAQ_questions.tsx new file mode 100644 index 0000000..e27d2de --- /dev/null +++ b/src/Utils/FAQ_questions.tsx @@ -0,0 +1,27 @@ +interface FAQ_question{ + title:string, + answer: string | React.ReactElement +} + +const questions:FAQ_question[] = [ + { + title:"Are there any cookies, or following robots on this website? If so why are they like this and not like that", + answer: <>No, Piano-Blocks-App does not use any cookies or tracking devices. The only used localizing script is Vercel Analitics + , which is used to collect following data: country origin of a request, subpages visited (like /Play, /Docs etc.), and number of requests. No target advertising data is collected + + }, + { + title:"The app lags and performes poorly during playing, what can I do?", + answer:<>Easiest way to improve performance is to use "Black-n-White" preset in configurations. If you need your own configuration of visuals, set the Effects to None, + Block-Shadow-Radius to 0, and switch off sound. Also, faster speed helps to improve performance. If the app still lags through playthrough, try playing it once, + then replaying it, without refreshing the page. The replay should be in a much better performance. + + }, + { + title:"I have some feature requests for the app, how can I propose them?", + answer:<>Please go to the Github Repository of this project, and request a feature. If you believe you can implement the feature yourself, you can contact + the app developer(s) through instagram, email, or linkedin. Contact data is displayed at the bottom of the About subpage + } +]; +export type {FAQ_question as FAQ_Question_type}; +export default questions; From 49ce910ef98f397f3d6ab4a828ac2d483a142572 Mon Sep 17 00:00:00 2001 From: APRIASZ Date: Fri, 12 Sep 2025 15:03:04 +0200 Subject: [PATCH 2/2] Adding comments and fixing minor issues --- src/App.tsx | 5 +++ .../DonationPrompt/DonationPrompt.tsx | 6 ++- .../DrawPiano/LoadingScreen/LoadingScreen.tsx | 3 +- src/Components/DrawPiano/UpdatedDrawPiano.tsx | 36 +++++++--------- .../UpdatedPlayingManagement.tsx | 27 +++++++----- src/Components/Preview/Preview.tsx | 12 +++--- src/Components/Tracks/index.ts | 2 + src/Components/Tracks/updatedTracks.tsx | 23 +++++++---- src/Helpers/Blocks/pianoInteraction.ts | 37 ++++++++++++++++- src/Helpers/Blocks/updatedBlocks.ts | 6 +-- src/Helpers/CanvasEffects/EmptyEffect.ts | 3 ++ src/Helpers/CanvasEffects/Fireworks.ts | 3 ++ src/Helpers/CanvasEffects/index.ts | 6 ++- src/Helpers/Effects/EffectsManager.ts | 41 +++++++++++++++++-- src/Helpers/Effects/_Effect.ts | 7 ++++ .../MidiReader/AnimationFrameMidiPlayer.ts | 21 +++++++++- src/Helpers/MidiReader/MidiToSingleTrack.ts | 5 ++- src/Helpers/MidiReader/createNoteEvents.ts | 10 ++--- .../timeSignatureValuesFromMidiFile.ts | 1 + src/Helpers/ReadMidiFile.ts | 3 +- src/Helpers/soundManager.ts | 26 +++++++++++- 21 files changed, 212 insertions(+), 71 deletions(-) diff --git a/src/App.tsx b/src/App.tsx index 17348d2..078c771 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -11,6 +11,11 @@ import PlayLive from './Pages/PlayLive/PlayLive'; import {Routes as Switch, Route} from 'react-router-dom'; import './App.css'; +/** + * Main App function with router. + * Here add new routes + * @returns + */ function App() { return ( diff --git a/src/Components/DonationPrompt/DonationPrompt.tsx b/src/Components/DonationPrompt/DonationPrompt.tsx index 01e6ffb..b28fac0 100644 --- a/src/Components/DonationPrompt/DonationPrompt.tsx +++ b/src/Components/DonationPrompt/DonationPrompt.tsx @@ -1,8 +1,11 @@ import React,{useEffect,useState} from 'react' -import DonateButton from '../DonateButton/DonateButton'; import Hi from '../../Assets/hi.png'; import './DonationPrompt.scss' +/** + * @deprecated + * @returns + */ export default function DonationPrompt() { const [canPrompt,setCanPrompt] = useState(false); @@ -25,7 +28,6 @@ export default function DonationPrompt() { If you like using this app consider donating It really helps run this project for free -

Thank You!

diff --git a/src/Components/DrawPiano/LoadingScreen/LoadingScreen.tsx b/src/Components/DrawPiano/LoadingScreen/LoadingScreen.tsx index fc2b58a..3fd2dcf 100644 --- a/src/Components/DrawPiano/LoadingScreen/LoadingScreen.tsx +++ b/src/Components/DrawPiano/LoadingScreen/LoadingScreen.tsx @@ -8,6 +8,7 @@ import React,{ReactElement,useState,useEffect} from 'react' import './LoadingScreen.scss'; +import PBA_Icon from "../../../Assets/PBA_logo.png" interface LoadingScreenProps{ Finished:boolean } @@ -34,7 +35,7 @@ export default function LoadingScreen({Finished}:LoadingScreenProps):ReactElemen return ( <> {render_screen &&
- Loading + Loading

Reading the MIDI file, please wait...

diff --git a/src/Components/DrawPiano/UpdatedDrawPiano.tsx b/src/Components/DrawPiano/UpdatedDrawPiano.tsx index 24542bf..83fca46 100644 --- a/src/Components/DrawPiano/UpdatedDrawPiano.tsx +++ b/src/Components/DrawPiano/UpdatedDrawPiano.tsx @@ -1,15 +1,13 @@ /** * Component Created during new ver creation for AVANT - * Last Update: 07/29/2025 + * Last Update: 12/09/2025 * - Component is a refreshed version of "DrawPiano", which can handle new, not obesolete components * - Component now should be able to handle different height and width, not only fixed to window Size * - Moved Loading Screen Here - * - Moved Watermark render Here */ import React, {useEffect, useState } from "react"; import AnimationFrameMidiPlayer from "../../Helpers/MidiReader/AnimationFrameMidiPlayer"; -import { TrackNoteEvent } from "../../Utils/TypesForMidi"; import LoadingScreen from "./LoadingScreen/LoadingScreen"; import soundManagerClass from "../../Helpers/soundManager"; import { useSelector } from 'react-redux'; @@ -36,51 +34,45 @@ export default function UpdatedDrawPiano({width,height,Player,piano_keys_height const nr_of_white_keys = total_nr_of_keys === 25 ? 15 : total_nr_of_keys === 49 ? 28 : total_nr_of_keys === 61 ? 36 : total_nr_of_keys === 76 ? 44 : 52; //I don't believe I had to write this... const [is_loading,set_is_loading] = useState(true); - const [soundManager,setSoundManager] = useState(); + const [soundManager,setSoundManager] = useState(null); const options = useSelector((state:{options:OptionsType}) => state.options); + //If player and sounds are ready, then set the loading screen to dissapear useEffect(()=>{ - if(Player && soundManager){ - soundManager.load_sounds().then(e => set_is_loading(false)); + if(Player){ + if(options.soundOn === true){ + if(soundManager)soundManager.load_sounds().then(e => set_is_loading(false)); + return; + } + //else + set_is_loading(false); } },[Player, soundManager]) + //When player is ready and maxvelocity can be deducted, create sound manager useEffect(()=>{ - if(Player){ + if(Player && options.soundOn === true){ setSoundManager(new soundManagerClass(Player.MidiMaxVelocity * 50)) } },[Player]) + //Function renders the tracks only when player is defined, and sounds are defined (if sounds are on) const renderTracks = ():React.ReactElement =>{ - if(Player !== undefined && soundManager !== undefined){ + if(Player !== undefined && (soundManager !== undefined || options.soundOn === false)){ return } return <> } - const renderWatermark = ():React.ReactElement => { - if(options.watermark){ - return ( -
-

Some watermark

-
) - } - return <> - } - - return
{/*Loading Screen will automatically dissapear after it's work is done, so this will become an empty component*/} {renderTracks()} - {renderWatermark()}
} \ No newline at end of file diff --git a/src/Components/PlayingManagement/UpdatedPlayingManagement.tsx b/src/Components/PlayingManagement/UpdatedPlayingManagement.tsx index 90c3314..688f739 100644 --- a/src/Components/PlayingManagement/UpdatedPlayingManagement.tsx +++ b/src/Components/PlayingManagement/UpdatedPlayingManagement.tsx @@ -15,28 +15,30 @@ interface UPM_props{ /** * Component handles the bar with "pause,play,stop,reset,move" etc. * Important for user interaction and control of what is goind on with the playing - * @param param0 - * @returns + * @param Player AnimationFrameMidiPlayer needed to run the component, as all the buttons correspond with it */ export default function UpdatedPlayingManagement({Player}:UPM_props):React.ReactElement { - const timeout_ref = useRef(null); - const [dot_left,set_dot_left] = useState(0); - const [bt_display,set_bt_display] = useState(Button_Play); - const [timing, set_timing] = useState<{curr:number,length:number}>({curr:0,length:0}); - const [active, setActive] = useState(false); - const navi = useNavigate(); + const timeout_ref = useRef(null); //auto hide the element + const [dot_left,set_dot_left] = useState(0); //for playing bar, how much % of was played + const [bt_display,set_bt_display] = useState(Button_Play); //paused or played button to display + const [timing, set_timing] = useState<{curr:number,length:number}>({curr:0,length:0}); //time of playing + const [active, setActive] = useState(false); //is bar visible + const navi = useNavigate(); //navigate to go back to main page + //Handle pause play... const handlePausePlay = ():void =>{ Player.pausePlay(); set_bt_display(curr => curr === Button_Play ? Button_Pause : Button_Play) } + //Handle Reset of the playing... const handleResetButton = ():void =>{ Player.restart(); - set_bt_display(Button_Play) + set_bt_display(Button_Play); } + //When Player is ready, set function to handle update of the timer every 100ms useEffect(()=>{ if(Player){ const handle_timer_update = ():void =>{ @@ -51,7 +53,8 @@ export default function UpdatedPlayingManagement({Player}:UPM_props):React.React } },[Player]) - const make_timer_string = () =>{ + //Function to create the timer of how much of the track was played + const make_timer_string = ():string =>{ let curr_str, length_str const mins = Math.floor(timing.curr/60) const secs = Math.floor(timing.curr % 60); @@ -62,13 +65,14 @@ export default function UpdatedPlayingManagement({Player}:UPM_props):React.React return curr_str + "/" + length_str } + //handle clicking on the bar const click_bar_handler = (ev:MouseEvent) =>{ const target_data = ev.currentTarget.getBoundingClientRect() const percent = Math.floor((ev.clientX - target_data.x) *100 /target_data.width); Player.moveTo(percent + 1); } - + //set player active on mouse move const set_player_active = ():void =>{ if(active === false){ setActive(true); @@ -79,6 +83,7 @@ export default function UpdatedPlayingManagement({Player}:UPM_props):React.React },2000); } + useEffect(()=>{ window.addEventListener('mousemove',set_player_active); return () => {window.removeEventListener('mousemove',set_player_active)} diff --git a/src/Components/Preview/Preview.tsx b/src/Components/Preview/Preview.tsx index 14699fc..0c5b4f2 100644 --- a/src/Components/Preview/Preview.tsx +++ b/src/Components/Preview/Preview.tsx @@ -2,9 +2,7 @@ import React, {useState, useEffect, useCallback, useRef} from 'react' import './Preview.scss' import UpdatedTracks from '../Tracks/updatedTracks' -import LoadingScreen from '../DrawPiano/LoadingScreen/LoadingScreen' import AnimationFrameMidiPlayer from '../../Helpers/MidiReader/AnimationFrameMidiPlayer' -import { TrackNoteEvent } from '../../Utils/TypesForMidi' import { Options as OptionsType } from '../../Utils/TypesForOptions'; import { useSelector } from 'react-redux'; @@ -14,12 +12,12 @@ interface PrevProps{ /** * Preview component creates a small piano, to preview the changes done in options + * LAST EDIT: 12/09/2025 * @param active As preview is designed for OptionsTab, which can be either open, or closed, this parameter defines if preview should be rendered * @returns */ export default function Preview({active}:PrevProps):React.ReactElement { - const [events, setEvents] = useState([]); const [player, setPlayer] = useState(); const [key, addKey] = useState(0); const [ready,setReady] = useState(false); @@ -29,12 +27,12 @@ export default function Preview({active}:PrevProps):React.ReactElement { const timeout_ref = useRef(0); /** - * Load the component + * Load the component, loaded the player when it is undefined. */ useEffect(()=>{ if(player === undefined && width_ref.current !== null){ const props = width_ref.current.getBoundingClientRect(); - setPlayer(new AnimationFrameMidiPlayer([],setEvents)) + setPlayer(new AnimationFrameMidiPlayer([])) //Set width and height initially set_width_height({ width: props.width, @@ -85,6 +83,7 @@ export default function Preview({active}:PrevProps):React.ReactElement { } },[active]) + //Set new width and height on resize const listener = useCallback(()=>{ if(width_ref.current === null)return; const props = width_ref.current.getBoundingClientRect(); @@ -95,6 +94,7 @@ export default function Preview({active}:PrevProps):React.ReactElement { },[width_ref.current]) + //Add listeners for resize useEffect(()=>{ window.addEventListener('resize',listener) @@ -112,11 +112,9 @@ export default function Preview({active}:PrevProps):React.ReactElement { Player={player} height={width_height.height + 200} width={width_height.width} - number_of_keys={width_height.width > 760 ? 76 : 25} number_of_white_keys={width_height.width > 760 ? 44 : 15} options={options} sound={null} - white_key_height={400} />}
}
diff --git a/src/Components/Tracks/index.ts b/src/Components/Tracks/index.ts index 865c8ba..0456ffb 100644 --- a/src/Components/Tracks/index.ts +++ b/src/Components/Tracks/index.ts @@ -1,3 +1,5 @@ +//!! FILE DEPRECATED + import TracksAnimationFrame from './Tracks'; import TracksInterval from './TracksIntervalMethod'; diff --git a/src/Components/Tracks/updatedTracks.tsx b/src/Components/Tracks/updatedTracks.tsx index eea6381..d35993d 100644 --- a/src/Components/Tracks/updatedTracks.tsx +++ b/src/Components/Tracks/updatedTracks.tsx @@ -1,9 +1,8 @@ /** * Component created during new version preparing for AVANT in replacement of (/src/Components/Tracks/Tracks.tsx) and (/src/Components/Tracks/TracksIntervalMethod.tsx) - * LAST UPDATE: 07/29/2025 + * LAST UPDATE: 12/09/2025 * Main changes from obesolete components: * - width and height is nowehwere from static window.inner values - * - */ import AnimationFrameMidiPlayer from "../../Helpers/MidiReader/AnimationFrameMidiPlayer"; @@ -19,13 +18,11 @@ import './Tracks.styles.css' interface UT_props{ number_of_white_keys:number, - white_key_height:number, width:number, height:number, Player:AnimationFrameMidiPlayer options:OptionsType, sound:soundManager | null, - number_of_keys:number } /** @@ -33,7 +30,7 @@ interface UT_props{ * Performs main visualizations and playing of Midi file, can be called a heart of app * @returns */ -const UpdatedTracks = ({width,height,Player,options,sound,number_of_white_keys,white_key_height, number_of_keys,}:UT_props):React.ReactElement =>{ +const UpdatedTracks = ({width,height,Player,options,sound,number_of_white_keys}:UT_props):React.ReactElement =>{ const [blocks,setBlocks] = useState(undefined); const animation_frame = useRef(0); const mainCtx = useRef(null); @@ -42,14 +39,18 @@ const UpdatedTracks = ({width,height,Player,options,sound,number_of_white_keys,w const gradCtx = useRef(null); const EffectCtx = useRef(null); + /** + * Function handles adding the event. + * !usecallback does not work as this function is copied. + */ const add_event = useCallback((ev: TrackNoteEvent[]) =>{ if(blocks){ blocks.add_blocks(ev); } },[blocks]) + /** Cancel animation frame when component is deleted */ useEffect(()=>{ - return () => { if(animation_frame.current !== 0){ window.cancelAnimationFrame(animation_frame.current); @@ -57,14 +58,17 @@ const UpdatedTracks = ({width,height,Player,options,sound,number_of_white_keys,w } },[]) + /** + * main animation frame, renders blocks. + */ const main_animation_frame = useCallback(():any =>{ if(blocks){ blocks.render(); - Player.setEventHandler(add_event) } animation_frame.current = window.requestAnimationFrame(main_animation_frame); },[blocks]) + /** Handles the resizing of the page */ useEffect(()=>{ if(blocks){ blocks.handle_resize(width,height,width/number_of_white_keys); @@ -73,6 +77,7 @@ const UpdatedTracks = ({width,height,Player,options,sound,number_of_white_keys,w },[width,height]); + //Function checks if every canvas is loaded and sets it up in blocks useEffect(()=>{ if(blocks === undefined && pianoWhite.current && mainCtx.current && pianoBlack.current && gradCtx.current && EffectCtx.current){ const context = mainCtx.current.getContext('2d') @@ -92,10 +97,12 @@ const UpdatedTracks = ({width,height,Player,options,sound,number_of_white_keys,w } },[mainCtx.current, blocks, pianoBlack.current, pianoWhite.current, EffectCtx.current]) + //Here set up all functions/handlers which require blocks to exist useEffect(()=>{ if(blocks !== undefined){ + Player.setEventHandler(add_event) Player.set_pause_move_handlers(blocks.pause_playing, blocks.impel_blocks_in_places, blocks.reset); - if(options.soundOn && sound !== null){ + if(sound !== null){ blocks.set_sound_manager(sound); } main_animation_frame(); diff --git a/src/Helpers/Blocks/pianoInteraction.ts b/src/Helpers/Blocks/pianoInteraction.ts index f44c0f9..b022a5d 100644 --- a/src/Helpers/Blocks/pianoInteraction.ts +++ b/src/Helpers/Blocks/pianoInteraction.ts @@ -1,19 +1,45 @@ +//TODO: Add option for Key Gradient to be like fire: https://codepen.io/Capse/pen/aNOeee +// Current RadialGradient is very basic and unatractive +// Also, that radial gradient could be prerendered and reused +//TODO: Combine white_ctx and black_ctx into one single canvas (goal - reduce .clearRect function as much as possible) +//TODO: Combine radial gradient with piano lighting up - generate offsets for canvas... (goal - reduce .clearRect function as much as possible) + import { keyInfo } from "../../Utils/TypesForMidi"; import { CanvasRoundRect, addShadow } from '../../Utils/CanvasFuntions'; import { Options } from "../../Utils/TypesForOptions"; import { KeyGradient } from "../CanvasEffects"; +/** + * Class is used to light-up the keys on the piano + * LAST UPDATE: 12/09/2025 + */ export default class pianoInteraction{ private keys_to_light:Array = []; private black_key_height:number private white_key_height:number + /** + * Class used to light up the keys on the piano when the block reaches it (or when the method is executed...) + * @param black_ctx - ctx for black keys + * @param white_ctx - ctx for white keys + * @param gradient_ctx - ctx for gradient keys + * @param width -width + * @param height -height + * @param cnavas_offSet -- the offset on how bigger the light key should be than the normal key + * @param options - options + */ constructor(private black_ctx:CanvasRenderingContext2D,private white_ctx:CanvasRenderingContext2D, private gradient_ctx: CanvasRenderingContext2D,private width:number,private height:number, private cnavas_offSet:number, private options:Options){ this.black_key_height = (height-cnavas_offSet) / 1.5 + 5; this.white_key_height = (height-cnavas_offSet) + 2; this.gradient_ctx.globalCompositeOperation = 'source-over'; } + /** + * Method adds tke key to be lighten up + * @param key information about the key + * @param color color which the key will be lighten up + * @param gradient_color and color of the gradient + */ public handle_block_key(key:keyInfo, color:string, gradient_color:string):void { this.keys_to_light.push({ width: key.width, @@ -25,6 +51,11 @@ export default class pianoInteraction{ }) } + /** + * Method handles the resize of the canvas + * @param width new width + * @param height new height + */ public handle_resize(width:number, height:number):void { this.black_key_height = (height-this.cnavas_offSet) / 1.5 + 5; this.white_key_height = (height-this.cnavas_offSet) + 2; @@ -32,10 +63,14 @@ export default class pianoInteraction{ this.height = height; } + /** + * Method renders the lighten-up keys and the radial gradient for the keys + */ public render():void { + //!! TO MANY CLEAR RECTS this.white_ctx.clearRect(0,0,this.width,this.height); this.black_ctx.clearRect(0,0,this.width,this.height); - this.gradient_ctx.clearRect(0,0,this.width,this.height * 3); + this.gradient_ctx.clearRect(0,0,this.width,this.height * 3); this.keys_to_light.map(key =>{ const height = key.type === 'BLACK' ? this.black_key_height : this.white_key_height if(key.type === "BLACK"){ diff --git a/src/Helpers/Blocks/updatedBlocks.ts b/src/Helpers/Blocks/updatedBlocks.ts index 38e5c7f..ce44aef 100644 --- a/src/Helpers/Blocks/updatedBlocks.ts +++ b/src/Helpers/Blocks/updatedBlocks.ts @@ -1,6 +1,6 @@ /** * This Class was created for new version on AVANT update, replacing old /src/Helpers/Blocks/Blocks.ts - * LAST UPDATE: 07/29/2025 + * LAST UPDATE: 12/09/2025 * Key changes: * - Code refactoring * - Deletion of old "game" code @@ -273,7 +273,7 @@ class Blocks{ }; /** - * Function creates the blocks from the waiting list + * Method creates the blocks from the waiting list */ private __add_blocks_from_waiting_list(current_time:number){ if(this.notes_to_add.length <= 0)return; @@ -318,7 +318,7 @@ class Blocks{ } /** - * Function creates a map of key positions + * Method creates a map of key positions */ private __create_key_position_map(width:number,nr_of_keys:number, WhiteKeyWidth:number):keyInfo[]{ let Returning:Array = []; diff --git a/src/Helpers/CanvasEffects/EmptyEffect.ts b/src/Helpers/CanvasEffects/EmptyEffect.ts index a3144c8..f91b868 100644 --- a/src/Helpers/CanvasEffects/EmptyEffect.ts +++ b/src/Helpers/CanvasEffects/EmptyEffect.ts @@ -1,4 +1,7 @@ +/** + * @deprecated + */ class EmptyEffect{ public render(){ diff --git a/src/Helpers/CanvasEffects/Fireworks.ts b/src/Helpers/CanvasEffects/Fireworks.ts index 48385fc..65a1357 100644 --- a/src/Helpers/CanvasEffects/Fireworks.ts +++ b/src/Helpers/CanvasEffects/Fireworks.ts @@ -48,6 +48,9 @@ class Trail_Element{ } } +/** + * @deprecated + */ export default class Firework{ private ctx:CanvasRenderingContext2D private width:number diff --git a/src/Helpers/CanvasEffects/index.ts b/src/Helpers/CanvasEffects/index.ts index 62412f6..4f4d5c8 100644 --- a/src/Helpers/CanvasEffects/index.ts +++ b/src/Helpers/CanvasEffects/index.ts @@ -9,7 +9,11 @@ import DNA from "./DNA"; import hexToRgba from "hex-rgba"; -const KeyGradient = (ctx:CanvasRenderingContext2D,pos_x:number,block_width:number,height:number,color:string, radius:number = 80) =>{ +/** + * Function creates a radial gradient in the specified pos_x of the canvas + * Should be soon replaced by a better optimized function + */ +const KeyGradient = (ctx:CanvasRenderingContext2D,pos_x:number,block_width:number,height:number,color:string, radius:number = 80):void =>{ const radialGradient = ctx.createRadialGradient(pos_x + block_width/2, height, 0, pos_x + block_width/2, height, Math.random() * radius/15 + (radius - radius/15)); radialGradient.addColorStop(0.0, hexToRgba(color,95)); radialGradient.addColorStop(0.35, hexToRgba(color,40)); diff --git a/src/Helpers/Effects/EffectsManager.ts b/src/Helpers/Effects/EffectsManager.ts index 5bac777..1ff26a0 100644 --- a/src/Helpers/Effects/EffectsManager.ts +++ b/src/Helpers/Effects/EffectsManager.ts @@ -1,4 +1,5 @@ import Effect from './_Effect' +import { Options as OptionsType } from '../../Utils/TypesForOptions'; /** * Import Effects @@ -6,12 +7,26 @@ import Effect from './_Effect' import Sparks from './DesignedEffects/Sparks'; import Squared from './DesignedEffects/Squared'; import Blank from './DesignedEffects/Blank'; -import { Options } from '../../Utils/TypesForOptions'; +/** + * Effects Manager handles the choice of effects, and just fires the correct effects + * It is created mainly for clearer code, and easier effects to update when new ones are created + * LAST UPDATE: 12/09/2025 + */ export default class EffectsManager{ - private effects:Effect; - constructor(private ctx:CanvasRenderingContext2D, width:number, private height:number, private key_width:number,private effect_type:string,private options:Options){ + + /** + * Effects Manager handles the choice of effects, and just fires the correct effects + * It is created mainly for clearer code, and easier effects to update when new ones are created + * @param ctx canvas for effects + * @param width width of the + * @param height height of the canvas + * @param key_width width of the key - should be also changed with resizing + * @param effect_type type of effect + * @param options options + */ + constructor(ctx:CanvasRenderingContext2D, width:number, private height:number, private key_width:number, effect_type:string, options:OptionsType){ switch(effect_type){ case 'Sparks': this.effects = new Sparks(ctx, width, height, 1, 6,); @@ -28,20 +43,38 @@ export default class EffectsManager{ } } - public handle_resize(width:number, height: number){ + /** + * Method handles the resize of the page/canvas + * @param width width of the canvas to change + * @param height height of the canvas to change + */ + public handle_resize(width:number, height: number):void{ this.height = height; this.effects.handle_resze(width, height); } + /** + * Method generates the effect + * Should be executed when the block is touching the piano + * but dependent on situation can be executed in other situations + * @param pos_x pos_x of the key + */ public generate_effect(pos_x:number):void{ this.effects.create_effect(pos_x, this.height, this.key_width); } + /** + * Method renders the effects, + * should be executed inside some kind of AnimationFrame method/function + */ public render_effect():void{ this.effects.update_effect(); this.effects.render_effect(); } + /** + * Method clears the effects canvas + */ public clear_effect():void{ this.effects.clear_ctx(); } diff --git a/src/Helpers/Effects/_Effect.ts b/src/Helpers/Effects/_Effect.ts index 18ebf20..f834ef7 100644 --- a/src/Helpers/Effects/_Effect.ts +++ b/src/Helpers/Effects/_Effect.ts @@ -1,8 +1,15 @@ /** * A pattern for class effect, * Every effect must follow the same methods + * LAST UPDATE: 12/09/2025 */ export default abstract class Effect{ + /** + * Constructor for abstract class of Effect + * @param ctx canvas ctx + * @param width width of ctx + * @param height height of ctx + */ constructor(protected ctx:CanvasRenderingContext2D, protected width:number, protected height:number){} /** diff --git a/src/Helpers/MidiReader/AnimationFrameMidiPlayer.ts b/src/Helpers/MidiReader/AnimationFrameMidiPlayer.ts index aae5934..4baa153 100644 --- a/src/Helpers/MidiReader/AnimationFrameMidiPlayer.ts +++ b/src/Helpers/MidiReader/AnimationFrameMidiPlayer.ts @@ -1,6 +1,6 @@ /** * Class Created during new version update for AVANT as replacement for /src/Helpers/MidiPlayer - * Last Update: 07/28/2025 + * Last Update: 12/09/2025 * Key new differences * - Now midi is played using window.requestAnimationFrame, not with interval (as interval can lag often) * - Now argument required is array of TrackNoteEvents, so the whole Midi processing and loading process must be done before Player object is created @@ -8,7 +8,7 @@ * - Other functionalities should be preserved as in the obesolete class MidiPlayer */ -import { IMidiFile, TrackNoteEvent } from "../../Utils/TypesForMidi"; +import { TrackNoteEvent } from "../../Utils/TypesForMidi"; /** * Class consists of Methods which work with Blocks file (/src/Helpers/Blocks/Blocks.ts) @@ -58,6 +58,11 @@ class AnimationFrameMidiPlayer{ this.restart() } + /** + * Method sets the event handler for the events to add + * Can be set by this function, or in constructor, but MUST be set, otherwise not good + * @param onEvent function which accepts event as argument + */ public setEventHandler(onEvent: (ev:any) => any):void{ this.onEvent = onEvent; } @@ -192,6 +197,10 @@ class AnimationFrameMidiPlayer{ } } + /** + * Method plays the midi file, sets up animation frame for itself, rerenders itself and awiat events + * @returns + */ private playMidi(): void{ const elapsed_time = Date.now() * 1000 - this.timer - this.pauseTime //microseconds //now start from the current index of midi file @@ -202,6 +211,10 @@ class AnimationFrameMidiPlayer{ this.isFinished = true return } + //As long as there is event which occured between current and last animation frame, it is added to the list + //EXPLENATION - yes then there can be delay in the real playback, however, the delay is no longer than 17ms + //And there is no point in changing this, as blocks render in 60FPS, having one frame per 16.7ms, therefore + //Even if this was changed there still can be a delay when the blocks hit the piano... (as they hit it with 16.7ms delay) while(this.currentNotesIndex < this.notes.length && this.notes[this.currentNotesIndex].Delta <= elapsed_time){ noteEvents.push(this.notes[this.currentNotesIndex++]) } @@ -255,6 +268,10 @@ class AnimationFrameMidiPlayer{ this.animationFrame = window.requestAnimationFrame(() =>{this.playRandom(range)}); } + /** + * Function created for testing purposes of the midi player + * @returns Promise of completion of playing + */ public async __for_testing():Promise{ return new Promise((res,ret) =>{ setInterval(()=>{ diff --git a/src/Helpers/MidiReader/MidiToSingleTrack.ts b/src/Helpers/MidiReader/MidiToSingleTrack.ts index 1b7dcb9..0e4cda8 100644 --- a/src/Helpers/MidiReader/MidiToSingleTrack.ts +++ b/src/Helpers/MidiReader/MidiToSingleTrack.ts @@ -1,10 +1,11 @@ -import { IMidiFile, MidiEventType } from "../../Utils/TypesForMidi" +import { IMidiFile, UpdatedMidiEventType } from "../../Utils/TypesForMidi" /** * Function which converts a IMidiFile to have one single track * Every event in track receives 2 additional objects: * "controlTrack" --> boolean stating if an event is from track which only had controlEvents and no any noteOn, noteOff events * "trackNumber" --> number field stating from which track it came. If the controlTrack flag is true, this number will be -1 + * LAST CHANGE: 12/09/2025 * @param file IMidiFile * @returns an updated IMidiFile with only one track, also the track has additional 2 objects */ @@ -37,7 +38,7 @@ const convertToSingleTrack = (file:IMidiFile): IMidiFile =>{ let smallest_delta_index = 0, smallest_value = Number.MAX_SAFE_INTEGER;//Look for the smallest delta new_file.tracks.forEach((track,index) =>{ if(track.length === 0)return; - const first_event = track[0] as MidiEventType; + const first_event = track[0] as UpdatedMidiEventType; if(first_event.delta < smallest_value){ smallest_delta_index = index; smallest_value = first_event.delta diff --git a/src/Helpers/MidiReader/createNoteEvents.ts b/src/Helpers/MidiReader/createNoteEvents.ts index 2cc492c..508e50d 100644 --- a/src/Helpers/MidiReader/createNoteEvents.ts +++ b/src/Helpers/MidiReader/createNoteEvents.ts @@ -1,7 +1,3 @@ -/* - Function after creation will be converted to work as web worker, as it is computationally expensive -*/ - import { IMidiFile, TrackNoteEvent, UpdatedMidiEventType, timeSignatureDataProps } from "../../Utils/TypesForMidi"; import MidiToSingleTrack from './MidiToSingleTrack' @@ -17,6 +13,7 @@ interface awaiting_event{ /** * Function Converts IMidiFile to NoteEvents array. It uses TrackNoteEvent, which states if the event occured on different track * Also, it converts all the tracks to one single track + * LAST CHANGE: 28/07/2025 * @param src_file source file * @param timeControl file with time controll * @param focus_track Optional parameter - as the tracks are merged, sometimes the noteEvent can occure for same note number. This parameter specifies the priority. first_event means unless noteOff event happens, all other events are ignored. end_last means the previous note will be ended. @@ -36,6 +33,7 @@ const CreateMidiNoteEventsArray = (src_file:IMidiFile, timeControl:timeSignature tickTime = midiEvent.setTempo.microsecondsPerQuarter / timeControl.division; } if("noteOn" in midiEvent){ + //On event being noteOn, the note is added to "waitingEvents" array, and it wait there until noteOff event happens on the same key WaitingEvents.push({ delta_start:time_passed, delta_end:-1, @@ -46,6 +44,8 @@ const CreateMidiNoteEventsArray = (src_file:IMidiFile, timeControl:timeSignature }) } if("noteOff" in midiEvent){ + //on NoteOff event the note is found in waitingEvents array, and it's length etc. is calucalted. + //If sustain is on, note is kept in array until pedal is off, to calculate duration of the sound const waiting_event = WaitingEvents.find(el => el.note_number === (midiEvent.noteOff.noteNumber) && el.sustain_on_end === false) //Finding el.sustain_on_end === false took me 9 hours... if(waiting_event === undefined){ throw new Error("Error during parsing of MIDI file --> found noteOff event without previous noteOn event !!!") @@ -76,7 +76,7 @@ const CreateMidiNoteEventsArray = (src_file:IMidiFile, timeControl:timeSignature const new_waiting_events:Array = []; WaitingEvents.forEach(ev =>{ if(ev.delta_end != -1 && ev.sustain_on_end === true){ - //These events will now end + //These events will now end, as sustain pedal is of NoteEvents.push({ Delta:ev.delta_start, Duration:ev.delta_end, diff --git a/src/Helpers/MidiReader/timeSignatureValuesFromMidiFile.ts b/src/Helpers/MidiReader/timeSignatureValuesFromMidiFile.ts index 00eb175..f68f94d 100644 --- a/src/Helpers/MidiReader/timeSignatureValuesFromMidiFile.ts +++ b/src/Helpers/MidiReader/timeSignatureValuesFromMidiFile.ts @@ -2,6 +2,7 @@ import { TimeSignature, IMidiFile, timeSignatureDataProps } from "../../Utils/Ty /** * Function extracts the timeSignature values with their corresponding deltas + * LAST CHANGE: 28/07/2025 * @param file an IMidiFile * @returns returns array of denominator/nominator/metronome/thirtyseconds with their delta when they change */ diff --git a/src/Helpers/ReadMidiFile.ts b/src/Helpers/ReadMidiFile.ts index 9525258..c9f94a2 100644 --- a/src/Helpers/ReadMidiFile.ts +++ b/src/Helpers/ReadMidiFile.ts @@ -3,6 +3,7 @@ // into Object(JSON) // It returns a Promise of an Object, which Object // Will be converted to JSON +// LAST CHANGE: 12/09/2025 import { parseArrayBuffer } from 'midi-json-parser'; import { IMidiFile } from '../Utils/TypesForMidi'; @@ -13,7 +14,7 @@ import { IMidiFile } from '../Utils/TypesForMidi'; * Function reads a midiFile, returning a JSON with IMidiFile * @param file a file * @param type Specify if File is a reference object or an ArrayBuffer - * @returns + * @returns Promise with IMidiFile inside */ const ReadMidiFile = (file:any,type:'ref' | 'ArrayBuffer'): Promise =>{ diff --git a/src/Helpers/soundManager.ts b/src/Helpers/soundManager.ts index 2f05180..a97affa 100644 --- a/src/Helpers/soundManager.ts +++ b/src/Helpers/soundManager.ts @@ -1,3 +1,8 @@ +/** + * File manages the sounds during playing + * Implements only one function - play - which plays the sound for certain amount of time + * LAST CHANGE: 12/09/2025 + */ import key_1 from '../Assets/piano_sounds/at0.ogg'; import key_2 from '../Assets/piano_sounds/ao0.ogg'; import key_3 from '../Assets/piano_sounds/b0.ogg'; @@ -18,8 +23,14 @@ class soundManager{ this.additional_sounds = [{audio:new Audio(key_1),id:0},{audio:new Audio(key_2),id:1},{audio:new Audio(key_3),id:2},{audio:new Audio(key_88),id:87}]; } + /** + * Method load the piano sounds, loading each after each + * @tip This method should be revritten for faster load + * @returns Promise with boolean stating compleated (true) + */ public load_sounds():Promise { + //Function handles the load element for audio, checking if it can be loaded const loadAudioElement = (audio:HTMLAudioElement):Promise => { audio.load() return new Promise(res =>{ @@ -76,6 +87,12 @@ class soundManager{ }) } + /** + * Method fades the audio, to smoothen the effect of audio fading away, and simulate how it works normally on the piano + * @param audio audio element which needs to be faded + * @param time time of fading + * @param key key parameter to reset the current element + */ private audio_fade(audio:HTMLAudioElement, time:number, key:sound_object):void{ const initial_volume = audio.volume; const inter = setInterval(()=>{ @@ -92,7 +109,14 @@ class soundManager{ } - public play_key(key:number,time:number = 0.1,velocity:number=0.1){ + /** + * Method plays the key on the certain note number, for given time and with given velocity (volume) + * @param key number of key on the piano to play + * @param time Time for which the sound needs to be played + * @param velocity volume of the sound + * @returns Nothing + */ + public play_key(key:number,time:number = 0.1,velocity:number=0.1):void{ const okey_key = this.current_sounds[this.current_sounds.findIndex(e => e.id===key)]; const additional_key = this.additional_sounds[this.additional_sounds.findIndex(e => e.id===key)]; if(okey_key.time_started !== 0){