diff --git a/front/components/PartitionCoord.tsx b/front/components/PartitionCoord.tsx index 15cdcdd..b4b0d26 100644 --- a/front/components/PartitionCoord.tsx +++ b/front/components/PartitionCoord.tsx @@ -1,7 +1,7 @@ import * as React from 'react'; import PartitionView from './PartitionView'; import PhaserCanvas from './PartitionVisualizer/PhaserCanvas'; -import { PianoCursorPosition } from './PartitionVisualizer/PhaserCanvas'; +import { PianoCursorPosition } from '../models/PianoGame'; type PartitionCoordProps = { // The Buffer of the MusicXML file retreived from the API @@ -10,9 +10,6 @@ type PartitionCoordProps = { onEndReached: () => void; onResume: () => void; onPause: () => void; - // Timestamp of the play session, in milisecond - timestamp: number; - pressedKeys: Map; }; const PartitionCoord = ({ @@ -21,8 +18,6 @@ const PartitionCoord = ({ onEndReached, onPause, onResume, - timestamp, - pressedKeys, }: PartitionCoordProps) => { const [partitionData, setPartitionData] = React.useState< [string, PianoCursorPosition[]] | null @@ -40,17 +35,15 @@ const PartitionCoord = ({ onEndReached={() => { console.log('osmd end reached'); }} - timestamp={timestamp} + timestamp={0} /> )} {partitionData && ( { onEndReached(); }} diff --git a/front/components/PartitionView.tsx b/front/components/PartitionView.tsx index e9a6ed1..659a158 100644 --- a/front/components/PartitionView.tsx +++ b/front/components/PartitionView.tsx @@ -10,8 +10,7 @@ import { Note, } from 'opensheetmusicdisplay'; import useColorScheme from '../hooks/colorScheme'; -import { PianoCursorPosition } from './PartitionVisualizer/PhaserCanvas'; - +import { PianoCursorPosition } from '../models/PianoGame'; type PartitionViewProps = { // The Buffer of the MusicXML file retreived from the API file: string; diff --git a/front/components/PartitionVisualizer/PhaserCanvas.tsx b/front/components/PartitionVisualizer/PhaserCanvas.tsx index 6a63da9..b3ddc8e 100644 --- a/front/components/PartitionVisualizer/PhaserCanvas.tsx +++ b/front/components/PartitionVisualizer/PhaserCanvas.tsx @@ -1,17 +1,21 @@ // create a simple phaser effect with a canvas that can be easily imported as a react component import * as React from 'react'; -import { useEffect } from 'react'; +import { useEffect, useContext } from 'react'; import Phaser from 'phaser'; import useColorScheme from '../../hooks/colorScheme'; import { RootState, useSelector } from '../../state/Store'; import { setSoundPlayer as setSPStore } from '../../state/SoundPlayerSlice'; import { useDispatch } from 'react-redux'; import { SplendidGrandPiano, CacheStorage } from 'smplr'; -import { Note } from 'opensheetmusicdisplay'; +import { handlePianoGameMsg } from './PianoGameUpdateFunctions'; +import { PianoCC } from '../../views/PlayView'; +import { PianoCanvasMsg, PianoCursorNote, PianoCursorPosition } from '../../models/PianoGame'; let globalTimestamp = 0; let globalPressedKeys: Map = new Map(); +// the messages are consummed from the end and new messages should be added at the end +let globalMessages: Array = []; const globalStatus: 'playing' | 'paused' | 'stopped' = 'playing'; const isValidSoundPlayer = (soundPlayer: SplendidGrandPiano | undefined) => { @@ -50,8 +54,7 @@ const getPianoScene = ( private partition!: Phaser.GameObjects.Image; private cursor!: Phaser.GameObjects.Rectangle; private emitter!: Phaser.GameObjects.Particles.ParticleEmitter; - private emitzone!: Phaser.GameObjects.Particles.Zones.EdgeZone; - private nbTextureTolad!: number; + private nbTextureToload!: number; create() { this.textures.addBase64( 'star', @@ -59,12 +62,13 @@ const getPianoScene = ( ); this.textures.addBase64('partition', partitionB64); this.cursorPositionsIdx = -1; - this.nbTextureTolad = 2; + // this is to prevent multiple initialisation of the scene + this.nbTextureToload = 2; this.cameras.main.setBackgroundColor(colorScheme === 'light' ? '#FFFFFF' : '#000000'); this.textures.on('onload', () => { - this.nbTextureTolad--; - if (this.nbTextureTolad > 0) return; + this.nbTextureToload--; + if (this.nbTextureToload > 0) return; this.partition = this.add.image(0, 0, 'partition').setOrigin(0, 0); this.cameras.main.setBounds(0, 0, this.partition.width, this.partition.height); @@ -103,13 +107,15 @@ const getPianoScene = ( return true; } if (globalPressedKeys.size > 0) { - // add particles at the position of the cursor - this.emitter.start(1); this.cursor.fillAlpha = 0.9; } else if (this.cursor) { this.cursor.fillAlpha = 0.5; } + if (globalMessages.length > 0) { + handlePianoGameMsg(globalMessages, this.emitter); + } + return false; }); if (cP) { @@ -134,50 +140,28 @@ const getPianoScene = ( return PianoScene; }; -type PianoCursorNote = { - note: Note; - duration: number; -}; - -export type PianoCursorPosition = { - // offset in pixels - x: number; - // timestamp in ms - timing: number; - timestamp: number; - notes: PianoCursorNote[]; -}; - -export type UpdateInfo = { - currentTimestamp: number; - status: 'playing' | 'paused' | 'stopped'; -}; - export type PhaserCanvasProps = { partitionB64: string; cursorPositions: PianoCursorPosition[]; onEndReached: () => void; onPause: () => void; onResume: () => void; - // Timestamp of the play session, in milisecond - timestamp: number; - pressedKeys: Map; }; const PhaserCanvas = ({ partitionB64, cursorPositions, onEndReached, - timestamp, - pressedKeys, }: PhaserCanvasProps) => { const colorScheme = useColorScheme(); const dispatch = useDispatch(); + const pianoCC = useContext(PianoCC); const soundPlayer = useSelector((state: RootState) => state.soundPlayer.soundPlayer); const [game, setGame] = React.useState(null); - globalTimestamp = timestamp; - globalPressedKeys = pressedKeys; + globalTimestamp = pianoCC.timestamp; + globalPressedKeys = pianoCC.pressedKeys; + globalMessages = pianoCC.messages; useEffect(() => { if (isValidSoundPlayer(soundPlayer)) { diff --git a/front/components/PartitionVisualizer/PianoGameUpdateFunctions.ts b/front/components/PartitionVisualizer/PianoGameUpdateFunctions.ts new file mode 100644 index 0000000..f714935 --- /dev/null +++ b/front/components/PartitionVisualizer/PianoGameUpdateFunctions.ts @@ -0,0 +1,34 @@ +import { NoteTiming, PianoCanvasMsg } from '../../models/PianoGame'; + +const handleNoteTimingMsg = ( + noteTiming: NoteTiming, + emitter: Phaser.GameObjects.Particles.ParticleEmitter +) => { + if (noteTiming === NoteTiming.Perfect) { + emitter.particleTint = 0x00ff00; + emitter.start(10); + } else if (noteTiming === NoteTiming.Great) { + emitter.particleTint = 0x00ffff; + emitter.start(5); + } else if (noteTiming === NoteTiming.Good) { + emitter.particleTint = 0xffff00; + emitter.start(3); + } else if (noteTiming === NoteTiming.Missed) { + emitter.particleTint = 0xff0000; + emitter.start(1); + } else if (noteTiming === NoteTiming.Wrong) { + // maybe add some other effect + } +}; + +export const handlePianoGameMsg = ( + msgs: Array, + emitter: Phaser.GameObjects.Particles.ParticleEmitter +) => { + const msg = msgs.shift(); + if (msg) { + if (msg.type === 'noteTiming') { + handleNoteTimingMsg(msg.data as NoteTiming, emitter); + } + } +}; diff --git a/front/models/PianoGame.ts b/front/models/PianoGame.ts new file mode 100644 index 0000000..544570c --- /dev/null +++ b/front/models/PianoGame.ts @@ -0,0 +1,40 @@ +import { Note } from 'opensheetmusicdisplay'; + +export type PianoCursorNote = { + note: Note; + duration: number; +}; + +export type PianoCursorPosition = { + // offset in pixels + x: number; + // timestamp in ms + timing: number; + timestamp: number; + notes: PianoCursorNote[]; +}; + +export type UpdateInfo = { + currentTimestamp: number; + status: 'playing' | 'paused' | 'stopped'; +}; + +export enum NoteTiming { + Perfect = 'Perfect', + Great = 'Great', + Good = 'Good', + Missed = 'Missed', + Wrong = 'Wrong', +} + +export type PianoCanvasMsg = { + type: 'noteTiming' | 'score' | 'gameUpdate'; + data: UpdateInfo | NoteTiming | number; +}; + +export type PianoCanvasContext = { + messages: Array; + // Timestamp of the play session, in miliseconds + timestamp: number; + pressedKeys: Map; +}; \ No newline at end of file diff --git a/front/views/PlayView.tsx b/front/views/PlayView.tsx index aaec2b4..08325fd 100644 --- a/front/views/PlayView.tsx +++ b/front/views/PlayView.tsx @@ -1,6 +1,6 @@ /* eslint-disable no-mixed-spaces-and-tabs */ import { StackActions } from '@react-navigation/native'; -import React, { useEffect, useRef, useState } from 'react'; +import React, { useEffect, useRef, useState, createContext, useReducer } from 'react'; import { SafeAreaView, Platform, Animated } from 'react-native'; import * as ScreenOrientation from 'expo-screen-orientation'; import { @@ -32,6 +32,7 @@ import TextButton from '../components/TextButton'; import { MIDIAccess, MIDIMessageEvent, requestMIDIAccess } from '@motiz88/react-native-midi'; import * as Linking from 'expo-linking'; import url from 'url'; +import { PianoCanvasContext, PianoCanvasMsg, NoteTiming } from '../models/PianoGame'; type PlayViewProps = { songId: number; @@ -68,6 +69,13 @@ function parseMidiMessage(message: MIDIMessageEvent) { }; } +//create a context with an array of number +export const PianoCC = createContext({ + pressedKeys: new Map(), + timestamp: 0, + messages: [], +}); + const PlayView = ({ songId, type, route }: RouteProps) => { const accessToken = useSelector((state: RootState) => state.user.accessToken); const navigation = useNavigation(); @@ -89,6 +97,13 @@ const PlayView = ({ songId, type, route }: RouteProps) => { const [midiKeyboardFound, setMidiKeyboardFound] = useState(); // first number is the note, the other is the time when pressed on release the key is removed const [pressedKeys, setPressedKeys] = useState>(new Map()); // [note, time] + const [pianoMsgs, setPianoMsgs] = useReducer( + (state: PianoCanvasMsg[], action: PianoCanvasMsg) => { + state.push(action); + return state; + }, + [] + ); const onPause = () => { stopwatch.pause(); @@ -182,25 +197,45 @@ const PlayView = ({ songId, type, route }: RouteProps) => { if (data.type == 'miss') { formattedMessage = translate('missed'); + setPianoMsgs({ + type: 'noteTiming', + data: NoteTiming.Missed, + }); messageColor = 'black'; } else if (data.type == 'timing' || data.type == 'duration') { formattedMessage = translate(data[data.type]); switch (data[data.type]) { case 'perfect': messageColor = 'green'; + setPianoMsgs({ + type: 'noteTiming', + data: NoteTiming.Perfect, + }); break; case 'great': messageColor = 'blue'; + setPianoMsgs({ + type: 'noteTiming', + data: NoteTiming.Great, + }); break; case 'short': case 'long': case 'good': messageColor = 'lightBlue'; + setPianoMsgs({ + type: 'noteTiming', + data: NoteTiming.Good, + }); break; case 'too short': case 'too long': case 'wrong': messageColor = 'trueGray'; + setPianoMsgs({ + type: 'noteTiming', + data: NoteTiming.Wrong, + }); break; default: break; @@ -302,15 +337,21 @@ const PlayView = ({ songId, type, route }: RouteProps) => { - setPartitionRendered(true)} - /> + + setPartitionRendered(true)} + /> + {!partitionRendered && }