merge main
This commit is contained in:
@@ -16,7 +16,7 @@ import useColorScheme from '../hooks/colorScheme';
|
||||
type BigActionButtonProps = {
|
||||
title: string;
|
||||
subtitle: string;
|
||||
image: string;
|
||||
image?: string;
|
||||
style?: StyleProp<ViewStyle>;
|
||||
iconName?: string;
|
||||
// It is not possible to recover the type, the `Icon` parameter is `any` as well.
|
||||
|
||||
@@ -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,8 +10,6 @@ type PartitionCoordProps = {
|
||||
onEndReached: () => void;
|
||||
onResume: () => void;
|
||||
onPause: () => void;
|
||||
// Timestamp of the play session, in milisecond
|
||||
timestamp: number;
|
||||
};
|
||||
|
||||
const PartitionCoord = ({
|
||||
@@ -20,10 +18,9 @@ const PartitionCoord = ({
|
||||
onEndReached,
|
||||
onPause,
|
||||
onResume,
|
||||
timestamp,
|
||||
}: PartitionCoordProps) => {
|
||||
const [partitionData, setPartitionData] = React.useState<
|
||||
[string, PianoCursorPosition[]] | null
|
||||
[[number, number], string, PianoCursorPosition[]] | null
|
||||
>(null);
|
||||
|
||||
return (
|
||||
@@ -31,21 +28,21 @@ const PartitionCoord = ({
|
||||
{!partitionData && (
|
||||
<PartitionView
|
||||
file={file}
|
||||
onPartitionReady={(base64data, a) => {
|
||||
setPartitionData([base64data, a]);
|
||||
onPartitionReady={(dims, base64data, a) => {
|
||||
setPartitionData([dims, base64data, a]);
|
||||
onPartitionReady();
|
||||
}}
|
||||
onEndReached={() => {
|
||||
console.log('osmd end reached');
|
||||
}}
|
||||
timestamp={timestamp}
|
||||
timestamp={0}
|
||||
/>
|
||||
)}
|
||||
{partitionData && (
|
||||
<PhaserCanvas
|
||||
partitionB64={partitionData?.[0]}
|
||||
cursorPositions={partitionData?.[1]}
|
||||
timestamp={timestamp}
|
||||
partitionDims={partitionData?.[0]}
|
||||
partitionB64={partitionData?.[1]}
|
||||
cursorPositions={partitionData?.[2]}
|
||||
onPause={onPause}
|
||||
onResume={onResume}
|
||||
onEndReached={() => {
|
||||
|
||||
@@ -10,12 +10,15 @@ 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;
|
||||
onPartitionReady: (base64data: string, cursorInfos: PianoCursorPosition[]) => void;
|
||||
onPartitionReady: (
|
||||
dims: [number, number],
|
||||
base64data: string,
|
||||
cursorInfos: PianoCursorPosition[]
|
||||
) => void;
|
||||
onEndReached: () => void;
|
||||
// Timestamp of the play session, in milisecond
|
||||
timestamp: number;
|
||||
@@ -97,7 +100,6 @@ const PartitionView = (props: PartitionViewProps) => {
|
||||
});
|
||||
_osmd.cursor.next();
|
||||
}
|
||||
// console.log('curPos', curPos);
|
||||
_osmd.cursor.reset();
|
||||
_osmd.cursor.hide();
|
||||
// console.log('timestamp cursor', _osmd.cursor.iterator.CurrentSourceTimestamp);
|
||||
@@ -110,12 +112,18 @@ const PartitionView = (props: PartitionViewProps) => {
|
||||
if (!osmdCanvas) {
|
||||
throw new Error('No canvas found');
|
||||
}
|
||||
let scale = osmdCanvas.width / parseFloat(osmdCanvas.style.width);
|
||||
if (Number.isNaN(scale)) {
|
||||
console.error('Scale is NaN setting it to 1');
|
||||
scale = 1;
|
||||
}
|
||||
// Ty https://github.com/jimutt/osmd-audio-player/blob/ec205a6e46ee50002c1fa8f5999389447bba7bbf/src/PlaybackEngine.ts#LL77C12-L77C63
|
||||
props.onPartitionReady(
|
||||
[osmdCanvas.width, osmdCanvas.height],
|
||||
osmdCanvas.toDataURL(),
|
||||
curPos.map((pos) => {
|
||||
return {
|
||||
x: pos.offset,
|
||||
x: pos.offset * scale,
|
||||
timing: pos.sNinfos.sNL,
|
||||
timestamp: pos.sNinfos.ts,
|
||||
notes: pos.notes,
|
||||
|
||||
@@ -1,22 +1,31 @@
|
||||
// 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 { Dimensions } from 'react-native';
|
||||
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<number, number> = new Map();
|
||||
// the messages are consummed from the end and new messages should be added at the end
|
||||
let globalMessages: Array<PianoCanvasMsg> = [];
|
||||
const globalStatus: 'playing' | 'paused' | 'stopped' = 'playing';
|
||||
|
||||
const isValidSoundPlayer = (soundPlayer: SplendidGrandPiano | undefined) => {
|
||||
return soundPlayer && soundPlayer.loaded;
|
||||
};
|
||||
|
||||
const min = (a: number, b: number) => (a < b ? a : b);
|
||||
const max = (a: number, b: number) => (a > b ? a : b);
|
||||
|
||||
const myFindLast = <T,>(a: T[], p: (_: T, _2: number) => boolean) => {
|
||||
for (let i = a.length - 1; i >= 0; i--) {
|
||||
if (p(a[i]!, i)) {
|
||||
@@ -48,17 +57,42 @@ const getPianoScene = (
|
||||
private cursorPositionsIdx = -1;
|
||||
private partition!: Phaser.GameObjects.Image;
|
||||
private cursor!: Phaser.GameObjects.Rectangle;
|
||||
private emitter!: Phaser.GameObjects.Particles.ParticleEmitter;
|
||||
private nbTextureToload!: number;
|
||||
create() {
|
||||
this.textures.addBase64(
|
||||
'star',
|
||||
'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEQAAAApCAYAAACMeY82AAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAyZpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuNi1jMDE0IDc5LjE1Njc5NywgMjAxNC8wOC8yMC0wOTo1MzowMiAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvIiB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL21tLyIgeG1sbnM6c3RSZWY9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZVJlZiMiIHhtcDpDcmVhdG9yVG9vbD0iQWRvYmUgUGhvdG9zaG9wIENDIDIwMTQgKFdpbmRvd3MpIiB4bXBNTTpJbnN0YW5jZUlEPSJ4bXAuaWlkOjYyNzNEOEU4NzUxMzExRTRCN0ZCODQ1QUJCREFFQzA4IiB4bXBNTTpEb2N1bWVudElEPSJ4bXAuZGlkOjYyNzNEOEU5NzUxMzExRTRCN0ZCODQ1QUJCREFFQzA4Ij4gPHhtcE1NOkRlcml2ZWRGcm9tIHN0UmVmOmluc3RhbmNlSUQ9InhtcC5paWQ6NjI3M0Q4RTY3NTEzMTFFNEI3RkI4NDVBQkJEQUVDMDgiIHN0UmVmOmRvY3VtZW50SUQ9InhtcC5kaWQ6NjI3M0Q4RTc3NTEzMTFFNEI3RkI4NDVBQkJEQUVDMDgiLz4gPC9yZGY6RGVzY3JpcHRpb24+IDwvcmRmOlJERj4gPC94OnhtcG1ldGE+IDw/eHBhY2tldCBlbmQ9InIiPz4vDiTDAAAB4UlEQVR42uxagY2DMAw0VRdghayQFVghs3SF/1XaEdoVGIGswAj5OK0rkwfalx5wiY2smAik+OrYFxcAAWLABFQJazmAykCOEhZRx0sBYdLHS0VFk6omVU2qOwRktS1jwYY5QKSAslqEtNBWM0lVt4xUQERUmdrWSV+VZi27xVYZU1OimYyOmJTRDB58VVyE5BUJQYhJGZYGYzMH87nuqwuoRSRVdLyB5hcoWIZxjs9P2buT3LkI0PP+BKcQriEp2jjnwO0XjDFwUDFRouNXF5HoQlK0iwKDVw10/GzPdzBIoo1zBApF1prb57iUw/zQxkdkpY1rwNhoOQMDkhpt62wqw+ZisMTiOwNQqLu2VMWpxhggOcBbe/zwlTvKqTfZxDyJYyAAfEyPTTF2/1AcWj8Ye39fU98+geHlebBuvv4xX8Zal5WoCEGnvn1y/na5JQdz55aOkM3knRyS5xwpbcZ/BSG//2uVWTrB6uFOAjHjoT9GzIojZzlYdJaRQNcPW0cMby3OtRl32w950PbU2yAAiGPk22qL0rp4hOSlEkFAfvGi6RwenCXsLkLGfuUcDGKf/J2tIkTGv/9t/xaQxQDCzyPFJdU1AYns5kO3jKAPZhQQPctohHweIJK+D/kRYAAaWClvtE6otAAAAABJRU5ErkJggg=='
|
||||
);
|
||||
this.textures.addBase64('partition', partitionB64);
|
||||
this.cursorPositionsIdx = -1;
|
||||
// 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.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);
|
||||
|
||||
this.cursor = this.add.rectangle(0, 0, 30, 350, 0x31ef8c, 0.5).setOrigin(0, 0);
|
||||
const dims = this.partition.getBounds();
|
||||
// base ref normal cursor is 276px by 30px
|
||||
this.cursor = this.add
|
||||
.rectangle(0, 0, (dims.height * 30) / 276, dims.height, 0x31ef8c, 0.5)
|
||||
.setOrigin(0, 0);
|
||||
this.cameras.main.startFollow(this.cursor, true, 0.05, 0.05);
|
||||
|
||||
this.emitter = this.add.particles(0, 0, 'star', {
|
||||
lifespan: 700,
|
||||
duration: 100,
|
||||
follow: this.cursor,
|
||||
speed: { min: 10, max: 20 },
|
||||
scale: { start: 0, end: 0.4 },
|
||||
emitZone: { type: 'edge', source: this.cursor.getBounds(), quantity: 50 },
|
||||
|
||||
emitting: false,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -76,6 +110,16 @@ const getPianoScene = (
|
||||
this.cursorPositionsIdx = idx;
|
||||
return true;
|
||||
}
|
||||
if (globalPressedKeys.size > 0) {
|
||||
this.cursor.fillAlpha = 0.9;
|
||||
} else if (this.cursor) {
|
||||
this.cursor.fillAlpha = 0.5;
|
||||
}
|
||||
|
||||
if (globalMessages.length > 0) {
|
||||
handlePianoGameMsg(globalMessages, this.emitter, undefined);
|
||||
}
|
||||
|
||||
return false;
|
||||
});
|
||||
if (cP) {
|
||||
@@ -100,47 +144,30 @@ 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 = {
|
||||
partitionDims: [number, number];
|
||||
partitionB64: string;
|
||||
cursorPositions: PianoCursorPosition[];
|
||||
onEndReached: () => void;
|
||||
onPause: () => void;
|
||||
onResume: () => void;
|
||||
// Timestamp of the play session, in milisecond
|
||||
timestamp: number;
|
||||
};
|
||||
|
||||
const PhaserCanvas = ({
|
||||
partitionDims,
|
||||
partitionB64,
|
||||
cursorPositions,
|
||||
onEndReached,
|
||||
timestamp,
|
||||
}: PhaserCanvasProps) => {
|
||||
const colorScheme = useColorScheme();
|
||||
const dispatch = useDispatch();
|
||||
const pianoCC = useContext(PianoCC);
|
||||
const soundPlayer = useSelector((state: RootState) => state.soundPlayer.soundPlayer);
|
||||
const [game, setGame] = React.useState<Phaser.Game | null>(null);
|
||||
|
||||
globalTimestamp = timestamp;
|
||||
globalTimestamp = pianoCC.timestamp;
|
||||
globalPressedKeys = pianoCC.pressedKeys;
|
||||
globalMessages = pianoCC.messages;
|
||||
|
||||
useEffect(() => {
|
||||
if (isValidSoundPlayer(soundPlayer)) {
|
||||
@@ -164,13 +191,43 @@ const PhaserCanvas = ({
|
||||
soundPlayer,
|
||||
colorScheme
|
||||
);
|
||||
const { width, height } = Dimensions.get('window');
|
||||
|
||||
class UIScene extends Phaser.Scene {
|
||||
private statusTextValue: string;
|
||||
private statusText!: Phaser.GameObjects.Text;
|
||||
constructor() {
|
||||
super({ key: 'UIScene', active: true });
|
||||
|
||||
this.statusTextValue = 'Score: 0 Streak: 0';
|
||||
}
|
||||
|
||||
create() {
|
||||
this.statusText = this.add.text(
|
||||
this.cameras.main.width - 300,
|
||||
10,
|
||||
this.statusTextValue,
|
||||
{
|
||||
fontFamily: 'Arial',
|
||||
fontSize: 25,
|
||||
color: '#3A784B',
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
override update() {
|
||||
if (globalMessages.length > 0) {
|
||||
handlePianoGameMsg(globalMessages, undefined, this.statusText);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const config = {
|
||||
type: Phaser.AUTO,
|
||||
parent: 'phaser-canvas',
|
||||
width: 1000,
|
||||
height: 400,
|
||||
scene: pianoScene,
|
||||
width: max(width * 0.9, 850),
|
||||
height: min(max(height * 0.7, 400), partitionDims[1]),
|
||||
scene: [pianoScene, UIScene],
|
||||
scale: {
|
||||
mode: Phaser.Scale.FIT,
|
||||
autoCenter: Phaser.Scale.CENTER_HORIZONTALLY,
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
import { NoteTiming, PianoCanvasMsg, PianoScoreInfo } from '../../models/PianoGame';
|
||||
|
||||
const handleNoteTimingMsg = (
|
||||
noteTiming: NoteTiming,
|
||||
emitter: Phaser.GameObjects.Particles.ParticleEmitter
|
||||
) => {
|
||||
if (noteTiming === NoteTiming.Perfect) {
|
||||
// gold
|
||||
emitter.particleTint = 0xffd700;
|
||||
emitter.start(20);
|
||||
} else if (noteTiming === NoteTiming.Great) {
|
||||
emitter.particleTint = 0x00ffff;
|
||||
emitter.start(10);
|
||||
} else if (noteTiming === NoteTiming.Good) {
|
||||
// orange/brown
|
||||
emitter.particleTint = 0xffa500;
|
||||
emitter.start(5);
|
||||
} else if (noteTiming === NoteTiming.Missed) {
|
||||
emitter.particleTint = 0xff0000;
|
||||
emitter.start(5);
|
||||
} else if (noteTiming === NoteTiming.Wrong) {
|
||||
// maybe add some other effect
|
||||
}
|
||||
};
|
||||
|
||||
const handleScoreMsg = (score: PianoScoreInfo, statusText: Phaser.GameObjects.Text) => {
|
||||
statusText.setText(`Score: ${score.score} Streak: ${score.streak}`);
|
||||
};
|
||||
|
||||
const findAndRemove = <T>(arr: Array<T>, predicate: (el: T) => boolean): T | undefined => {
|
||||
const idx = arr.findIndex(predicate);
|
||||
if (idx === -1) {
|
||||
return undefined;
|
||||
}
|
||||
return arr.splice(idx, 1)[0];
|
||||
};
|
||||
|
||||
export const handlePianoGameMsg = (
|
||||
msgs: Array<PianoCanvasMsg>,
|
||||
emitter: Phaser.GameObjects.Particles.ParticleEmitter | undefined,
|
||||
statusText: Phaser.GameObjects.Text | undefined
|
||||
) => {
|
||||
// this is temporary way of hanlding messages it works ok but is laggy when
|
||||
// pressing a lot of keys in a short time I will be using phaser events in the future I think
|
||||
const msg = findAndRemove(msgs, (msg) => {
|
||||
if (emitter && msg.type === 'noteTiming') {
|
||||
return true;
|
||||
} else if (statusText && msg.type === 'scoreInfo') {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
if (msg) {
|
||||
if (msg.type === 'noteTiming') {
|
||||
handleNoteTimingMsg(msg.data as NoteTiming, emitter!);
|
||||
} else if (msg.type === 'scoreInfo') {
|
||||
handleScoreMsg(msg.data as PianoScoreInfo, statusText!);
|
||||
}
|
||||
}
|
||||
};
|
||||
Reference in New Issue
Block a user