diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml deleted file mode 100644 index d69af06..0000000 --- a/.github/FUNDING.yml +++ /dev/null @@ -1,2 +0,0 @@ -patreon: hlorenzi -ko_fi: hlorenzi \ No newline at end of file diff --git a/.github/workflows/build_ghpages.yml b/.github/workflows/build_ghpages.yml deleted file mode 100644 index 38ae039..0000000 --- a/.github/workflows/build_ghpages.yml +++ /dev/null @@ -1,36 +0,0 @@ -name: Build GitHub Pages - -on: - push: - branches: ["main"] - -jobs: - build: - - runs-on: ubuntu-latest - - steps: - - name: Checkout code - uses: actions/checkout@master - with: - fetch-depth: 0 - - - name: Use Node.js 15.x - uses: actions/setup-node@v1 - with: - node-version: 15.x - - - run: git branch -D ghpages - continue-on-error: true - - - run: git checkout -b ghpages - - run: rm .gitignore - - run: mv .gitignore.ghpages .gitignore - - run: npm ci - - run: npm run build - - run: git config user.name github-actions - - run: git config user.email github-actions@github.com - - run: git describe --tags --match v* > build/build_version.txt - - run: git add -A - - run: git commit -m "build GitHub Pages" - - run: git push -f origin ghpages \ No newline at end of file diff --git a/.gitignore b/.gitignore index d150aeb..a47f7e0 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1 @@ -/node_modules/ -/build/ -/library/ \ No newline at end of file +/temp/ \ No newline at end of file diff --git a/.gitignore.ghpages b/.gitignore.ghpages deleted file mode 100644 index 6db2b0f..0000000 --- a/.gitignore.ghpages +++ /dev/null @@ -1,2 +0,0 @@ -/node_modules/ -/library/ \ No newline at end of file diff --git a/README.md b/README.md deleted file mode 100644 index b4fe2b3..0000000 --- a/README.md +++ /dev/null @@ -1,63 +0,0 @@ -# TheoryTracker - -[đŸŽŧ Try it right now on your browser!](https://hlorenzi.github.io/theorytracker/) - -[![Join the Discord server!][badge-discord-img]][badge-discord-url] - -[badge-discord-img]: https://img.shields.io/discord/394999035540275222?label=Join%20the%20Discord%20server!&logo=discord -[badge-discord-url]: https://discord.com/invite/pXeDXGD - -This is a multi-track piano-roll-style song editor app with an emphasis on music theory, -where notes and chords are color-coded according to the key, and some editing tools -work diatonically. - -The app can currently import `.mid` and its own `.ttproj` project files, -which are in plain JSON format. -It can currently export `.ttproj` files and render `.wav` files. - -Several soundfonts are readily available from an online repository. - -## How to use - -### General usage: - -* Use the middle or right mouse buttons to pan. -* Hold A to draw elements with the mouse. -* Right-click on an element to change its properties. - * Do a long right-click for a context menu with more commands. -* Double-click on a note block to edit its notes. - * Click on "Project Root" on the breadcrumb bar to exit note editing mode. - -* Press Space to toggle playback starting from the cursor. -* Press Esc to rewind. -* Use Backspace to delete incrementally. -* Use Ctrl + Z to undo, and Ctrl + Y -or Ctrl + Shift + Z to redo. - -* Also use 1234567 -to create notes and chords. - -### When elements are selected: - -* Use ←→ to move. - * Combine with Ctrl for faster movement. -* Use ↑↓ to change the pitch of notes, and the root of chords. - * Combine with Ctrl to change pitch by octaves. - * Combine with Shift to change pitch chromatically. -* Also use ,. to change pitch chromatically. -* Use Shift + ←→ to stretch. -* Hold Alt then drag with the mouse to duplicate. - -* Press Enter to unselect all. -* Press Delete to delete. - -* Use Ctrl + X, C, or V for the usual -cut/copy/paste commands. - -## Building from source - -Install npm dependencies with `npm install`, then perform a build -with `npm run build`. You can also run `npm run watch` to work locally. - -Then, run `npm start` or any other simple HTTP web server from -the repository folder, and navigate to `http://127.0.0.1`. \ No newline at end of file diff --git a/audio/piano/a1.mp3 b/audio/piano/a1.mp3 deleted file mode 100644 index 4fff8ad..0000000 Binary files a/audio/piano/a1.mp3 and /dev/null differ diff --git a/audio/piano/a2.mp3 b/audio/piano/a2.mp3 deleted file mode 100644 index c9b8a87..0000000 Binary files a/audio/piano/a2.mp3 and /dev/null differ diff --git a/audio/piano/a3.mp3 b/audio/piano/a3.mp3 deleted file mode 100644 index 3332353..0000000 Binary files a/audio/piano/a3.mp3 and /dev/null differ diff --git a/audio/piano/a4.mp3 b/audio/piano/a4.mp3 deleted file mode 100644 index 1c2866e..0000000 Binary files a/audio/piano/a4.mp3 and /dev/null differ diff --git a/audio/piano/a5.mp3 b/audio/piano/a5.mp3 deleted file mode 100644 index 36eccf0..0000000 Binary files a/audio/piano/a5.mp3 and /dev/null differ diff --git a/audio/piano/a6.mp3 b/audio/piano/a6.mp3 deleted file mode 100644 index 45d8cbb..0000000 Binary files a/audio/piano/a6.mp3 and /dev/null differ diff --git a/audio/piano/a7.mp3 b/audio/piano/a7.mp3 deleted file mode 100644 index 9ea6f7b..0000000 Binary files a/audio/piano/a7.mp3 and /dev/null differ diff --git a/build-ghpages.bat b/build-ghpages.bat deleted file mode 100644 index 411c58b..0000000 --- a/build-ghpages.bat +++ /dev/null @@ -1,10 +0,0 @@ -git branch -D ghpages -git checkout -b ghpages -del .gitignore -move .gitignore.ghpages .gitignore -cmd /C npm run build -git describe --tags --match v* > build/build_version.txt -git add -A -git commit -m "build GitHub Pages" -git push -f origin ghpages -git checkout main \ No newline at end of file diff --git a/examples/adventure.json b/examples/adventure.json deleted file mode 100644 index 5b33967..0000000 --- a/examples/adventure.json +++ /dev/null @@ -1,109 +0,0 @@ -{ -"version": 3, -"bpm": 160, - -"tracks": -[ -{ -"notes": -[ -[[0,0,1],[0,3,8],61], -[[0,3,8],[0,3,8],64], -[[0,3,4],[1,0,1],59], -[[1,3,4],[0,1,8],57], -[[1,7,8],[0,1,8],59], -[[2,0,1],[0,3,8],61], -[[2,3,8],[0,1,4],64], -[[2,5,8],[0,1,8],61], -[[2,3,4],[0,1,8],71], -[[2,7,8],[0,7,8],69], -[[3,3,4],[0,1,8],69], -[[3,7,8],[0,1,8],71], -[[4,0,1],[0,3,8],73], -[[4,3,8],[0,1,4],71], -[[4,5,8],[0,1,8],69], -[[4,3,4],[1,0,1],71], -[[5,3,4],[0,1,8],69], -[[5,7,8],[0,1,8],71], -[[6,0,1],[0,1,8],73], -[[6,1,8],[0,1,8],71], -[[6,1,4],[0,1,8],64], -[[6,3,8],[0,1,4],69], -[[6,5,8],[0,1,16],68], -[[6,11,16],[0,1,16],69], -[[6,3,4],[0,1,16],68], -[[6,13,16],[0,1,16],64], -[[6,7,8],[0,3,8],59], -[[7,1,4],[0,1,8],69], -[[7,3,8],[0,1,8],71], -[[7,1,2],[0,1,8],76], -[[7,5,8],[0,1,8],71], -[[7,3,4],[0,1,8],69], -[[7,7,8],[0,1,8],68], -[[8,0,1],[0,3,8],61], -[[8,3,8],[0,3,8],64], -[[8,3,4],[1,0,1],59], -[[9,3,4],[0,1,8],57], -[[9,7,8],[0,1,8],59], -[[10,0,1],[0,3,8],61], -[[10,3,8],[0,1,4],64], -[[10,5,8],[0,1,8],61], -[[10,3,4],[0,1,8],71], -[[10,7,8],[0,7,8],69], -[[11,3,4],[0,1,8],69], -[[11,7,8],[0,1,8],71], -[[12,0,1],[0,3,8],73], -[[12,3,8],[0,1,4],71], -[[12,5,8],[0,1,8],69], -[[12,3,4],[1,0,1],71], -[[13,3,4],[0,1,8],69], -[[13,7,8],[0,1,8],71], -[[14,0,1],[0,1,8],73], -[[14,1,8],[0,1,8],71], -[[14,1,4],[0,1,8],64], -[[14,3,8],[0,1,4],69], -[[14,5,8],[0,1,16],68], -[[14,11,16],[0,1,16],69], -[[14,3,4],[0,1,16],68], -[[14,13,16],[0,1,16],64], -[[14,7,8],[0,3,8],59], -[[15,1,4],[0,1,8],69], -[[15,3,8],[0,1,8],71], -[[15,1,2],[0,1,8],76], -[[15,5,8],[0,1,8],71], -[[15,3,4],[0,1,8],69], -[[15,7,8],[0,1,8],68] -] -} -], - -"chords": -[ -[[0,0,1],[0,3,4],9,0,"M",[],0], -[[0,3,4],[1,1,4],4,0,"M",[],0], -[[2,0,1],[0,3,4],6,0,"m",[],0], -[[2,3,4],[1,1,4],7,0,"M",[],0], -[[4,0,1],[0,3,4],2,0,"M",[],0], -[[4,3,4],[1,1,4],2,0,"m",[],0], -[[6,0,1],[0,3,4],1,0,"m7",[],0], -[[6,3,4],[1,1,4],4,0,"M",[],0], -[[8,0,1],[0,3,4],9,0,"M",[],0], -[[8,3,4],[1,1,4],4,0,"M",[],0], -[[10,0,1],[0,3,4],6,0,"m",[],0], -[[10,3,4],[1,1,4],7,0,"M",[],0], -[[12,0,1],[0,3,4],2,0,"M",[],0], -[[12,3,4],[1,1,4],2,0,"m",[],0], -[[14,0,1],[0,3,4],1,0,"m7",[],0], -[[14,3,4],[1,1,4],4,0,"M",[],0] -], - -"keyChanges": -[ -[[0,0,1],9,0,[0,2,4,5,7,9,10]] -], - -"meterChanges": -[ -[[0,0,1],4,4] -] -} \ No newline at end of file diff --git a/examples/chronotrigger.json b/examples/chronotrigger.json deleted file mode 100644 index fb0418c..0000000 --- a/examples/chronotrigger.json +++ /dev/null @@ -1,326 +0,0 @@ -{ -"version": 3, -"bpm": 100, - -"tracks": -[ -{ -"notes": -[ -[[0,0,1],[0,1,8],48], -[[0,0,1],[1,1,4],87], -[[0,1,8],[0,1,8],55], -[[0,1,4],[0,1,8],56], -[[0,3,8],[0,1,8],62], -[[0,1,2],[0,1,8],63], -[[0,5,8],[0,1,8],48], -[[0,3,4],[0,1,8],55], -[[0,7,8],[0,1,8],56], -[[1,0,1],[0,1,8],62], -[[1,0,1],[0,1,8],79], -[[1,1,8],[0,1,8],63], -[[1,1,8],[0,1,8],80], -[[1,1,4],[0,1,8],48], -[[1,1,4],[1,1,4],86], -[[1,3,8],[0,1,8],55], -[[1,1,2],[0,1,8],56], -[[1,5,8],[0,1,8],60], -[[1,3,4],[0,1,8],62], -[[1,7,8],[0,1,8],48], -[[2,0,1],[0,1,8],55], -[[2,1,8],[0,1,8],56], -[[2,1,4],[0,1,8],60], -[[2,1,4],[0,1,8],79], -[[2,3,8],[0,1,8],80], -[[2,3,8],[0,1,8],62], -[[2,1,2],[1,1,4],87], -[[2,1,2],[0,1,8],48], -[[2,5,8],[0,1,8],55], -[[2,3,4],[0,1,8],56], -[[2,7,8],[0,1,8],62], -[[3,0,1],[0,1,8],63], -[[3,1,8],[0,1,8],48], -[[3,1,4],[0,1,8],55], -[[3,3,8],[0,1,8],56], -[[3,1,2],[0,1,8],62], -[[3,5,8],[0,1,8],63], -[[3,3,4],[0,1,8],48], -[[3,7,8],[0,1,8],55], -[[4,0,1],[0,1,8],56], -[[4,1,8],[0,1,8],62], -[[4,1,4],[0,1,8],63], -[[4,3,8],[0,1,8],48], -[[4,1,2],[0,1,8],55], -[[4,5,8],[0,1,8],56], -[[4,3,4],[0,1,8],62], -[[4,3,4],[0,1,8],79], -[[4,7,8],[0,1,8],80], -[[4,7,8],[0,1,8],63], -[[5,0,1],[0,1,8],48], -[[5,0,1],[1,1,4],87], -[[5,1,8],[0,1,8],55], -[[5,1,4],[0,1,8],56], -[[5,3,8],[0,1,8],62], -[[5,1,2],[0,1,8],63], -[[5,5,8],[0,1,8],48], -[[5,3,4],[0,1,8],55], -[[5,7,8],[0,1,8],56], -[[6,0,1],[0,1,8],62], -[[6,0,1],[0,1,8],79], -[[6,1,8],[0,1,8],80], -[[6,1,8],[0,1,8],63], -[[6,1,4],[1,1,4],86], -[[6,1,4],[0,1,8],48], -[[6,3,8],[0,1,8],55], -[[6,1,2],[0,1,8],56], -[[6,5,8],[0,1,8],60], -[[6,3,4],[0,1,8],62], -[[6,7,8],[0,1,8],48], -[[7,0,1],[0,1,8],55], -[[7,1,8],[0,1,8],56], -[[7,1,4],[0,1,8],79], -[[7,1,4],[0,1,8],60], -[[7,3,8],[0,1,8],62], -[[7,3,8],[0,1,8],80], -[[7,1,2],[1,1,4],89], -[[7,1,2],[0,1,8],55], -[[7,5,8],[0,1,8],57], -[[7,3,4],[0,1,8],58], -[[7,7,8],[0,1,8],62], -[[8,0,1],[0,1,8],65], -[[8,1,8],[0,1,8],55], -[[8,1,4],[0,1,8],57], -[[8,3,8],[0,1,8],58], -[[8,1,2],[0,1,8],79], -[[8,1,2],[0,1,8],62], -[[8,5,8],[0,1,8],65], -[[8,5,8],[0,1,8],80], -[[8,3,4],[1,1,4],87], -[[8,3,4],[0,1,8],51], -[[8,7,8],[0,1,8],56], -[[9,0,1],[0,1,8],58], -[[9,1,8],[0,1,8],60], -[[9,1,4],[0,1,8],63], -[[9,3,8],[0,1,8],51], -[[9,1,2],[0,1,8],56], -[[9,5,8],[0,1,8],58], -[[9,3,4],[0,1,8],60], -[[9,7,8],[0,1,8],63], -[[10,0,1],[1,1,4],86], -[[10,0,1],[0,1,8],48], -[[10,1,8],[0,1,8],55], -[[10,1,4],[0,1,8],56], -[[10,3,8],[0,1,8],62], -[[10,1,2],[0,1,8],63], -[[10,5,8],[0,1,8],48], -[[10,3,4],[0,1,8],55], -[[10,7,8],[0,1,8],56], -[[11,0,1],[0,1,8],62], -[[11,1,8],[0,1,8],63], -[[11,1,4],[1,1,4],84], -[[11,1,4],[0,1,8],48], -[[11,3,8],[0,1,8],55], -[[11,1,2],[0,1,8],56], -[[11,5,8],[0,1,8],62], -[[11,3,4],[0,1,8],63], -[[11,7,8],[0,1,8],48], -[[12,0,1],[0,1,8],55], -[[12,1,8],[0,1,8],56], -[[12,1,4],[0,1,8],62], -[[12,3,8],[0,1,8],63], -[[12,5,8],[0,1,8],53], -[[12,5,8],[0,1,8],77], -[[12,3,4],[0,1,8],79], -[[12,3,4],[0,1,8],58], -[[12,7,8],[0,1,8],80], -[[12,7,8],[0,1,8],60], -[[13,0,1],[0,1,8],61], -[[13,0,1],[0,1,8],79], -[[13,1,8],[0,1,8],65], -[[13,1,8],[0,1,8],77], -[[13,1,4],[0,1,4],84], -[[13,3,8],[0,1,8],53], -[[13,1,2],[0,1,8],58], -[[13,1,2],[0,1,8],82], -[[13,5,8],[0,1,4],80], -[[13,5,8],[0,1,8],60], -[[13,3,4],[0,1,8],61], -[[13,7,8],[0,1,8],65], -[[13,7,8],[0,1,8],82], -[[14,0,1],[1,1,2],77], -[[14,1,8],[0,1,8],53], -[[14,1,4],[0,1,8],58], -[[14,3,8],[0,1,8],60], -[[14,1,2],[0,1,8],61], -[[14,5,8],[0,1,8],65], -[[14,7,8],[0,1,8],53], -[[15,0,1],[0,1,8],58], -[[15,1,8],[0,1,8],60], -[[15,1,4],[0,1,8],61], -[[15,3,8],[0,1,8],65], -[[15,5,8],[0,1,8],80], -[[15,5,8],[0,1,8],56], -[[15,3,4],[0,1,8],59], -[[15,3,4],[0,1,8],82], -[[15,7,8],[0,1,8],83], -[[15,7,8],[0,1,8],61], -[[16,0,1],[0,1,8],82], -[[16,0,1],[0,1,8],64], -[[16,1,8],[0,1,8],80], -[[16,1,8],[0,1,8],68], -[[16,1,4],[0,1,4],87], -[[16,3,8],[0,1,8],56], -[[16,1,2],[0,1,8],85], -[[16,1,2],[0,1,8],59], -[[16,5,8],[0,1,4],83], -[[16,5,8],[0,1,8],61], -[[16,3,4],[0,1,8],64], -[[16,7,8],[0,1,8],68], -[[16,7,8],[0,1,8],85], -[[17,0,1],[1,1,8],80], -[[17,1,8],[0,1,8],56], -[[17,1,4],[0,1,8],59], -[[17,3,8],[0,1,8],61], -[[17,1,2],[0,1,8],64], -[[17,5,8],[0,1,8],68], -[[17,7,8],[0,1,8],56], -[[18,0,1],[0,1,8],59], -[[18,1,8],[0,1,4],80], -[[18,1,8],[0,1,8],61], -[[18,1,4],[0,1,8],64], -[[18,3,8],[0,1,8],68], -[[18,3,8],[0,1,8],82], -[[18,1,2],[0,1,8],53], -[[18,1,2],[1,1,8],84], -[[18,5,8],[0,1,8],60], -[[18,3,4],[0,1,8],67], -[[18,7,8],[0,1,8],68], -[[19,0,1],[0,1,8],72], -[[19,1,8],[0,1,8],75], -[[19,1,4],[0,1,8],53], -[[19,3,8],[0,1,8],60], -[[19,1,2],[0,1,8],67], -[[19,5,8],[0,1,4],80], -[[19,5,8],[0,1,8],68], -[[19,3,4],[0,1,8],72], -[[19,7,8],[0,1,8],75], -[[19,7,8],[0,1,8],82], -[[20,0,1],[1,1,8],83], -[[20,0,1],[0,1,8],56], -[[20,1,8],[0,1,8],63], -[[20,1,4],[0,1,8],70], -[[20,3,8],[0,1,8],71], -[[20,1,2],[0,1,8],75], -[[20,5,8],[0,1,8],77], -[[20,3,4],[0,1,8],56], -[[20,7,8],[0,1,8],63], -[[21,0,1],[0,1,8],70], -[[21,1,8],[0,1,4],83], -[[21,1,8],[0,1,8],71], -[[21,1,4],[0,1,8],75], -[[21,3,8],[0,1,8],85], -[[21,3,8],[0,1,8],77], -[[21,1,2],[1,1,8],86], -[[21,1,2],[0,1,8],59], -[[21,5,8],[0,1,8],66], -[[21,3,4],[0,1,8],73], -[[21,7,8],[0,1,8],74], -[[22,0,1],[0,1,8],78], -[[22,1,8],[0,1,8],81], -[[22,1,4],[0,1,8],59], -[[22,3,8],[0,1,8],66], -[[22,1,2],[0,1,8],73], -[[22,5,8],[0,1,4],81], -[[22,5,8],[0,1,8],74], -[[22,3,4],[0,1,8],78], -[[22,7,8],[0,1,8],83], -[[23,0,1],[0,1,8],57], -[[23,0,1],[1,1,2],85], -[[23,1,8],[0,1,8],64], -[[23,1,4],[0,1,8],68], -[[23,3,8],[0,1,8],69], -[[23,1,2],[0,1,8],73], -[[23,5,8],[0,1,8],76], -[[23,3,4],[0,1,8],57], -[[23,7,8],[0,1,8],64], -[[24,0,1],[0,1,8],68], -[[24,1,8],[0,1,8],69], -[[24,1,4],[0,1,8],73], -[[24,3,8],[0,1,8],76], -[[24,1,2],[0,1,8],53], -[[24,1,2],[3,0,1],84], -[[24,5,8],[0,1,8],60], -[[24,3,4],[0,1,8],67], -[[24,7,8],[0,1,8],68], -[[25,0,1],[0,1,8],72], -[[25,1,8],[0,1,8],74], -[[25,1,4],[0,1,8],53], -[[25,3,8],[0,1,8],60], -[[25,1,2],[0,1,8],67], -[[25,5,8],[0,1,8],68], -[[25,3,4],[0,1,8],72], -[[25,7,8],[0,1,8],74], -[[26,0,1],[0,1,8],53], -[[26,1,8],[0,1,8],60], -[[26,1,4],[0,1,8],67], -[[26,3,8],[0,1,8],68], -[[26,1,2],[0,1,8],72], -[[26,5,8],[0,1,8],74], -[[26,3,4],[0,1,8],53], -[[26,7,8],[0,1,8],60], -[[27,0,1],[0,1,8],67], -[[27,1,8],[0,1,8],68], -[[27,1,4],[0,1,8],72], -[[27,1,4],[0,1,8],79], -[[27,3,8],[0,1,8],74], -[[27,3,8],[0,1,8],80] -] -} -], - -"chords": -[ -[[0,0,1],[1,1,4],0,0,"m",[],0], -[[1,1,4],[1,1,4],5,0,"m",[],0], -[[2,1,2],[1,1,4],0,0,"m",[],0], -[[3,3,4],[1,1,4],0,0,"m",[],0], -[[5,0,1],[1,1,4],0,0,"m",[],0], -[[6,1,4],[1,1,4],5,0,"m",[],0], -[[7,1,2],[1,1,4],7,0,"m7",[],0], -[[8,3,4],[1,1,4],8,0,"M",[],0], -[[10,0,1],[1,1,4],0,0,"m",[],0], -[[11,1,4],[1,1,4],0,0,"m",[],0], -[[12,1,2],[1,1,8],10,0,"m",[],0], -[[13,5,8],[0,3,8],5,0,"5",[],0], -[[14,0,1],[1,1,8],10,0,"m",[],0], -[[15,1,8],[0,3,8],5,0,"5",[],0], -[[15,1,2],[1,1,8],1,0,"m7",[],0], -[[16,5,8],[0,3,8],8,0,"m7",[],0], -[[17,0,1],[1,1,8],1,0,"m7",[],0], -[[18,1,8],[0,3,8],8,0,"m7",[],0], -[[18,1,2],[1,1,2],5,0,"m",[],0], -[[20,0,1],[1,1,2],8,0,"m",[],0], -[[21,1,2],[1,1,2],11,0,"m",[],0], -[[23,0,1],[1,1,2],9,0,"M",[],0], -[[24,1,2],[1,1,2],5,0,"m",[],0], -[[26,0,1],[1,1,2],5,0,"m",[],0] -], - -"keyChanges": -[ -[[0,0,1],0,0,[0,2,3,5,7,8,10]], -[[12,1,2],11,-1,[0,2,3,5,7,9,10]], -[[15,1,2],2,-1,[0,2,3,5,7,9,10]], -[[18,1,2],5,0,[0,2,3,5,7,8,10]], -[[20,0,1],7,1,[0,2,3,5,7,9,10]], -[[21,1,2],11,0,[0,2,3,5,7,9,10]], -[[23,0,1],9,0,[0,2,4,5,7,9,11]], -[[24,1,2],5,0,[0,2,3,5,7,9,10]] -], - -"meterChanges": -[ -[[0,0,1],5,4], -[[12,1,2],6,4] -] -} \ No newline at end of file diff --git a/examples/dontstarve.json b/examples/dontstarve.json deleted file mode 100644 index 72ac512..0000000 --- a/examples/dontstarve.json +++ /dev/null @@ -1,74 +0,0 @@ -{ -"version": 3, -"bpm": 120, - -"tracks": -[ -{ -"notes": -[ -[[0,0,1],[0,1,4],55], -[[0,1,4],[0,1,4],58], -[[0,1,2],[0,1,4],55], -[[0,3,4],[0,1,4],62], -[[1,0,1],[0,1,4],63], -[[1,1,4],[0,1,4],65], -[[1,1,2],[0,1,2],63], -[[2,0,1],[0,1,4],62], -[[2,1,4],[0,1,2],57], -[[2,3,4],[0,1,4],58], -[[3,0,1],[0,1,4],58], -[[3,1,4],[0,1,2],60], -[[3,3,4],[0,1,2],61], -[[4,1,4],[0,1,4],58], -[[4,1,2],[0,3,4],57], -[[5,1,4],[0,1,2],45], -[[5,3,4],[0,1,4],46], -[[6,0,1],[0,1,2],58], -[[6,1,2],[0,1,4],57], -[[6,3,4],[0,1,2],54], -[[7,1,4],[0,1,4],55], -[[7,1,2],[0,5,8],63], -[[8,1,8],[0,1,8],62], -[[8,1,4],[0,5,8],63], -[[8,7,8],[0,1,8],62], -[[9,0,1],[0,5,8],61], -[[9,5,8],[0,1,8],62], -[[9,3,4],[0,1,2],63], -[[10,1,4],[0,1,4],65], -[[10,1,2],[0,3,4],66], -[[11,1,4],[0,3,4],69] -] -} -], - -"chords": -[ -[[0,0,1],[0,3,4],7,0,"m",[],0], -[[0,3,4],[0,3,4],2,0,"M",[],0], -[[1,1,2],[0,3,4],7,0,"m",[],0], -[[2,1,4],[0,3,4],2,0,"M",[],0], -[[3,0,1],[0,3,4],5,-1,"o",[],0], -[[3,3,4],[0,3,4],10,-1,"M",[],0], -[[4,1,2],[0,3,4],5,-1,"o",[],0], -[[5,1,4],[0,3,4],10,-1,"M",[],0], -[[6,0,1],[0,3,4],0,0,"m",[],0], -[[6,3,4],[0,3,4],7,-1,"+",[],0], -[[7,1,2],[0,3,4],3,0,"+",[],0], -[[8,1,4],[0,3,4],7,0,"+",[],0], -[[9,0,1],[0,3,4],10,-1,"M",[],0], -[[9,3,4],[0,3,4],3,0,"+",[],0], -[[10,1,2],[0,3,4],7,-1,"+",[],0], -[[11,1,4],[0,3,4],9,0,"+",[],0] -], - -"keyChanges": -[ -[[0,0,1],7,0,[0,2,3,5,7,8,10]] -], - -"meterChanges": -[ -[[0,0,1],3,4] -] -} \ No newline at end of file diff --git a/examples/isaac.json b/examples/isaac.json deleted file mode 100644 index bee1143..0000000 --- a/examples/isaac.json +++ /dev/null @@ -1,134 +0,0 @@ -{ -"version": 3, -"bpm": 166, - -"tracks": -[ -{ -"notes": -[ -[[0,0,1],[0,1,4],71], -[[0,1,4],[0,3,8],72], -[[0,5,8],[0,1,8],68], -[[0,3,4],[0,1,4],69], -[[1,0,1],[0,1,4],71], -[[1,1,4],[0,1,8],67], -[[1,3,8],[0,1,8],69], -[[1,1,2],[0,1,8],67], -[[1,5,8],[0,1,8],66], -[[1,3,4],[0,1,2],64], -[[2,1,4],[0,1,4],59], -[[2,1,2],[0,3,8],67], -[[2,7,8],[0,1,8],63], -[[3,0,1],[0,1,4],64], -[[3,1,4],[0,3,8],71], -[[3,5,8],[0,1,8],64], -[[3,3,4],[0,1,4],66], -[[4,0,1],[0,1,4],67], -[[4,1,4],[0,1,8],69], -[[4,3,8],[0,1,8],71], -[[4,1,2],[0,1,4],76], -[[4,3,4],[0,1,4],71], -[[5,0,1],[0,1,4],69], -[[5,1,4],[0,1,4],67], -[[5,1,2],[0,1,8],69], -[[5,5,8],[0,1,8],71], -[[5,3,4],[0,1,8],69], -[[5,7,8],[0,1,8],67], -[[6,0,1],[0,1,4],66], -[[6,1,4],[0,1,4],63], -[[6,1,2],[0,1,4],64], -[[6,3,4],[0,1,4],66], -[[7,0,1],[0,1,4],67], -[[7,1,4],[0,1,4],69], -[[7,1,2],[0,1,4],63], -[[7,3,4],[0,1,4],64], -[[8,0,1],[0,1,4],66], -[[8,1,4],[0,1,4],59], -[[8,1,2],[0,1,4],63], -[[8,3,4],[0,1,4],66], -[[9,0,1],[0,1,8],64], -[[9,1,8],[0,1,8],59], -[[9,1,4],[0,1,8],64], -[[9,3,8],[0,1,8],67], -[[9,1,2],[0,1,8],64], -[[9,5,8],[0,1,8],59], -[[9,3,4],[0,1,8],64], -[[9,7,8],[0,1,8],59], -[[10,0,1],[0,1,8],64], -[[10,1,8],[0,1,8],67], -[[10,1,4],[0,1,8],64], -[[10,3,8],[0,1,8],59], -[[10,1,2],[0,1,2],64], -[[11,1,8],[0,1,16],66], -[[11,3,16],[0,1,16],64], -[[11,1,4],[0,1,4],63], -[[11,1,2],[0,1,4],64], -[[11,3,4],[0,1,4],66], -[[12,0,1],[0,1,2],71], -[[12,1,2],[0,1,4],66], -[[12,3,4],[0,1,4],69], -[[13,0,1],[0,1,4],67], -[[13,1,4],[0,1,4],66], -[[13,1,2],[0,3,8],64], -[[13,7,8],[0,1,8],66], -[[14,0,1],[0,1,4],64], -[[14,1,4],[0,1,2],63], -[[14,3,4],[0,1,4],59], -[[15,0,1],[0,1,2],60], -[[15,1,2],[0,1,4],59], -[[15,3,4],[0,1,4],57], -[[16,0,1],[0,1,4],59], -[[16,1,4],[0,1,4],57], -[[16,1,2],[0,1,4],55], -[[16,3,4],[0,1,4],57], -[[17,0,1],[0,1,4],55], -[[17,1,4],[0,1,4],54], -[[17,1,2],[0,1,8],51], -[[17,5,8],[0,1,8],59], -[[17,3,4],[0,1,8],63], -[[17,7,8],[0,1,8],66], -[[18,0,1],[0,1,4],64] -] -} -], - -"chords": -[ -[[0,0,1],[0,3,4],4,0,"m",[],0], -[[0,3,4],[0,3,4],2,0,"M",[],0], -[[1,1,2],[0,3,4],0,0,"M",[],0], -[[2,1,4],[0,3,4],4,0,"m",[],0], -[[3,0,1],[0,3,4],7,0,"M",[],0], -[[3,3,4],[0,3,4],2,0,"M",[],0], -[[4,1,2],[0,3,4],7,0,"M",[],0], -[[5,1,4],[0,3,4],4,0,"m",[],0], -[[6,0,1],[0,3,4],2,1,"o",[],0], -[[6,3,4],[0,3,4],11,0,"M",[],0], -[[7,1,2],[0,3,4],2,1,"o",[],0], -[[8,1,4],[0,3,4],11,0,"M",[],0], -[[9,0,1],[0,3,4],4,0,"m",[],0], -[[9,3,4],[0,3,4],2,0,"M",[],0], -[[10,1,2],[0,3,4],0,0,"M",[],0], -[[11,1,4],[0,3,4],11,0,"M",[],0], -[[12,0,1],[0,3,4],2,1,"+",[],0], -[[12,3,4],[0,3,4],6,0,"o",[],0], -[[13,1,2],[0,3,4],11,0,"M",[],0], -[[14,1,4],[0,3,4],2,1,"o",[],0], -[[15,0,1],[0,3,4],6,0,"o",[],0], -[[15,3,4],[0,3,4],2,1,"+",[],0], -[[16,1,2],[0,3,4],11,0,"M",[],0], -[[17,1,4],[0,3,4],6,0,"o",[],0], -[[18,0,1],[0,3,4],4,0,"m",[],0] -], - -"keyChanges": -[ -[[0,0,1],4,0,[0,2,3,5,7,8,10]] -], - -"meterChanges": -[ -[[0,0,1],3,4] -] -} \ No newline at end of file diff --git a/examples/jimmyneutron.json b/examples/jimmyneutron.json deleted file mode 100644 index 63f7736..0000000 --- a/examples/jimmyneutron.json +++ /dev/null @@ -1,91 +0,0 @@ -{ -"version": 3, -"bpm": 110, - -"tracks": -[ -{ -"notes": -[ -[[-1,13,16],[0,1,16],60], -[[-1,7,8],[0,1,16],60], -[[-1,15,16],[0,1,16],60], -[[0,0,1],[0,1,8],63], -[[0,1,8],[0,1,16],63], -[[0,3,16],[0,3,16],63], -[[0,3,8],[0,1,8],67], -[[0,1,2],[0,1,8],65], -[[0,5,8],[0,1,16],65], -[[0,11,16],[0,3,16],65], -[[0,7,8],[0,1,8],67], -[[1,0,1],[0,13,16],64], -[[1,13,16],[0,1,16],60], -[[1,7,8],[0,1,16],60], -[[1,15,16],[0,1,16],60], -[[2,0,1],[0,1,8],63], -[[2,1,8],[0,1,16],63], -[[2,3,16],[0,3,16],63], -[[2,3,8],[0,1,8],67], -[[2,1,2],[0,1,8],65], -[[2,5,8],[0,1,16],65], -[[2,11,16],[0,3,16],65], -[[2,7,8],[0,1,8],65], -[[3,1,8],[0,3,4],62], -[[3,7,8],[0,1,8],66], -[[4,0,1],[0,1,8],67], -[[4,1,8],[0,1,16],67], -[[4,3,16],[0,3,16],67], -[[4,3,8],[0,1,8],67], -[[4,1,2],[0,1,8],65], -[[4,5,8],[0,1,16],65], -[[4,11,16],[0,5,16],65], -[[5,0,1],[0,1,8],69], -[[5,1,8],[0,1,16],69], -[[5,3,16],[0,1,8],69], -[[5,5,16],[0,1,8],69], -[[5,7,16],[0,1,8],69], -[[5,9,16],[0,1,8],67], -[[5,11,16],[0,3,16],67], -[[5,7,8],[0,1,8],67], -[[6,0,1],[0,1,4],70], -[[6,1,4],[0,1,4],69], -[[6,1,2],[0,1,4],70], -[[6,3,4],[0,1,4],70], -[[7,0,1],[0,1,2],69], -[[7,1,2],[0,3,16],65], -[[7,11,16],[0,3,16],65], -[[7,7,8],[0,1,8],64], -[[8,0,1],[0,3,16],67], -[[8,3,16],[0,13,16],60] -] -} -], - -"chords": -[ -[[0,0,1],[0,1,2],3,0,"M",[],0], -[[0,1,2],[0,1,2],5,0,"M",[],0], -[[1,0,1],[1,0,1],0,0,"M",[],0], -[[2,0,1],[0,1,2],3,0,"M",[],0], -[[2,1,2],[0,1,2],5,0,"M",[],0], -[[3,1,8],[0,7,8],2,0,"M",[],0], -[[4,0,1],[0,1,2],3,0,"M",[],0], -[[4,1,2],[0,1,2],10,0,"M",[],0], -[[5,0,1],[0,1,2],5,0,"M",[],0], -[[5,1,2],[0,1,2],0,0,"M",[],0], -[[6,0,1],[1,0,1],3,0,"M",[],0], -[[7,0,1],[1,0,1],5,0,"M",[],0], -[[8,0,1],[1,0,1],0,0,"M",[],0] -], - -"keyChanges": -[ -[[-1,3,4],0,0,[0,2,4,5,7,9,11]] -], - -"meterChanges": -[ -[[-1,3,4],4,4], -[[0,0,1],4,4] -] -} \ No newline at end of file diff --git a/examples/letitgo.json b/examples/letitgo.json deleted file mode 100644 index c03ded6..0000000 --- a/examples/letitgo.json +++ /dev/null @@ -1,67 +0,0 @@ -{ -"version": 3, -"bpm": 120, - -"tracks": -[ -{ -"notes": -[ -[[-1,5,8],[0,1,8],66], -[[-1,3,4],[0,1,8],68], -[[-1,7,8],[0,11,16],69], -[[0,5,8],[0,1,8],64], -[[0,3,4],[0,1,8],64], -[[0,7,8],[0,7,8],71], -[[1,3,4],[0,1,8],69], -[[1,7,8],[0,1,8],68], -[[2,0,1],[0,1,8],66], -[[2,1,8],[0,1,8],66], -[[2,1,4],[0,15,64],66], -[[2,1,2],[0,1,8],66], -[[2,5,8],[0,15,64],68], -[[2,7,8],[0,3,4],69], -[[3,5,8],[0,1,8],66], -[[3,3,4],[0,1,8],68], -[[3,7,8],[0,3,4],69], -[[4,5,8],[0,1,8],64], -[[4,3,4],[0,1,8],64], -[[4,7,8],[0,1,4],73], -[[5,1,8],[0,1,2],71], -[[5,5,8],[0,1,8],69], -[[5,3,4],[0,1,8],71], -[[5,7,8],[0,1,4],73], -[[6,1,8],[0,1,4],73], -[[6,3,8],[0,1,8],74], -[[6,1,2],[0,3,8],73], -[[6,7,8],[0,1,16],71], -[[6,15,16],[0,1,16],69], -[[7,0,1],[0,1,16],71], -[[7,1,16],[0,15,16],69] -] -} -], - -"chords": -[ -[[0,0,1],[1,0,1],9,0,"M",[],0], -[[1,0,1],[1,0,1],4,0,"M",[],0], -[[2,0,1],[1,0,1],6,0,"m",[],0], -[[3,0,1],[1,0,1],2,0,"M",[],0], -[[4,0,1],[1,0,1],9,0,"M",[],0], -[[5,0,1],[1,0,1],4,0,"M",[],0], -[[6,0,1],[1,0,1],6,0,"m",[],0], -[[7,0,1],[1,0,1],2,0,"M",[],0] -], - -"keyChanges": -[ -[[-1,5,8],9,0,[0,2,4,5,7,9,11]] -], - -"meterChanges": -[ -[[-1,5,8],4,4], -[[0,0,1],4,4] -] -} \ No newline at end of file diff --git a/examples/mozartk453.json b/examples/mozartk453.json deleted file mode 100644 index e490839..0000000 --- a/examples/mozartk453.json +++ /dev/null @@ -1,276 +0,0 @@ -{ -"version": 3, -"bpm": 120, - -"tracks": -[ -{ -"notes": -[ -[[0,0,1],[0,1,8],57], -[[0,1,8],[0,1,8],57], -[[0,1,4],[0,1,8],59], -[[0,1,4],[0,1,8],57], -[[0,3,8],[0,1,8],61], -[[0,3,8],[0,1,8],55], -[[0,1,2],[0,1,8],62], -[[0,1,2],[0,1,8],59], -[[0,1,2],[0,3,8],57], -[[0,1,2],[0,1,8],54], -[[0,5,8],[0,1,8],64], -[[0,5,8],[0,1,8],61], -[[0,5,8],[0,1,8],55], -[[0,3,4],[0,1,8],66], -[[0,3,4],[0,1,8],62], -[[0,3,4],[0,1,8],54], -[[0,7,8],[0,1,8],67], -[[0,7,8],[0,1,8],64], -[[0,7,8],[0,1,8],55], -[[0,7,8],[0,1,8],52], -[[1,0,1],[0,3,8],81], -[[1,0,1],[0,1,4],69], -[[1,0,1],[0,1,4],66], -[[1,0,1],[0,1,4],54], -[[1,0,1],[0,1,4],50], -[[1,1,8],[0,1,8],62], -[[1,1,4],[0,1,8],66], -[[1,3,8],[0,1,8],78], -[[1,3,8],[0,1,8],69], -[[1,1,2],[0,3,8],83], -[[1,5,8],[0,1,8],63], -[[1,3,4],[0,1,8],66], -[[1,7,8],[0,1,8],78], -[[1,7,8],[0,1,8],71], -[[2,0,1],[0,1,16],79], -[[2,0,1],[0,1,16],64], -[[2,1,8],[0,1,16],76], -[[2,1,8],[0,1,16],67], -[[2,1,4],[0,1,4],81], -[[2,1,4],[0,1,4],61], -[[2,5,8],[0,1,32],76], -[[2,21,32],[0,1,32],78], -[[2,11,16],[0,1,32],76], -[[2,23,32],[0,1,32],74], -[[2,3,4],[0,1,32],76], -[[2,3,4],[0,1,4],62], -[[2,3,4],[0,1,4],60], -[[2,25,32],[0,7,64],78], -[[2,57,64],[0,7,64],74], -[[3,0,1],[0,1,16],79], -[[3,0,1],[0,1,4],59], -[[3,0,1],[0,1,4],62], -[[3,1,8],[0,1,64],81], -[[3,9,64],[0,3,64],79], -[[3,13,64],[0,3,64],78], -[[3,1,4],[0,1,8],79], -[[3,3,8],[0,1,16],81], -[[3,1,2],[0,1,16],83], -[[3,5,8],[0,1,8],71], -[[3,3,4],[0,1,8],78], -[[3,3,4],[0,1,4],59], -[[3,3,4],[0,1,4],56], -[[3,7,8],[0,1,8],76], -[[4,0,1],[0,1,4],74], -[[4,0,1],[0,1,4],57], -[[4,0,1],[0,1,2],64], -[[4,1,4],[0,1,32],73], -[[4,1,4],[0,1,4],55], -[[4,9,32],[0,1,32],74], -[[4,5,16],[0,1,32],73], -[[4,11,32],[0,1,32],74], -[[4,3,8],[0,1,16],71], -[[4,7,16],[0,1,16],73], -[[4,1,2],[0,1,4],74], -[[4,1,2],[0,1,4],57], -[[4,1,2],[0,1,4],54], -[[4,3,4],[0,1,4],76], -[[4,3,4],[0,1,4],57], -[[4,3,4],[0,1,4],64], -[[5,0,1],[0,1,8],78], -[[5,0,1],[0,1,4],66], -[[5,0,1],[0,1,4],62], -[[5,1,8],[0,1,8],81], -[[5,1,4],[0,1,8],79], -[[5,1,4],[0,1,4],64], -[[5,1,4],[0,1,4],57], -[[5,3,8],[0,1,8],76], -[[5,1,2],[0,1,4],74], -[[5,1,2],[0,1,4],62], -[[5,1,2],[0,1,4],54], -[[5,3,4],[0,1,4],76], -[[5,3,4],[0,1,4],64], -[[5,3,4],[0,1,4],57], -[[6,0,1],[0,1,8],78], -[[6,0,1],[0,1,4],62], -[[6,0,1],[0,1,4],66], -[[6,1,8],[0,1,8],81], -[[6,1,4],[0,1,8],79], -[[6,1,4],[0,1,4],64], -[[6,1,4],[0,1,4],57], -[[6,3,8],[0,1,8],76], -[[6,1,2],[0,1,4],74], -[[6,1,2],[0,1,4],62], -[[6,1,2],[0,1,4],54], -[[6,3,4],[0,1,4],76], -[[6,3,4],[0,1,4],64], -[[6,3,4],[0,1,4],57], -[[7,0,1],[0,1,32],78], -[[7,0,1],[0,1,4],66], -[[7,0,1],[0,1,4],62], -[[7,5,64],[0,3,64],79], -[[7,1,6],[0,1,32],81], -[[7,1,4],[0,1,32],83], -[[7,21,64],[0,3,64],84], -[[7,5,12],[0,1,32],86], -[[7,1,2],[0,1,32],74], -[[7,1,2],[0,1,4],67], -[[7,1,2],[0,1,4],62], -[[7,1,2],[0,1,4],59], -[[7,37,64],[0,3,64],76], -[[7,2,3],[0,1,32],78], -[[7,3,4],[0,1,32],79], -[[7,53,64],[0,3,64],81], -[[7,11,12],[0,1,32],83], -[[8,0,1],[0,3,8],78], -[[8,0,1],[0,1,2],62], -[[8,0,1],[0,1,2],57], -[[8,3,8],[0,1,16],79], -[[8,7,16],[0,1,16],78], -[[8,1,2],[0,1,8],76], -[[8,1,2],[0,1,4],61], -[[8,1,2],[0,1,4],57], -[[8,5,8],[0,1,16],78], -[[8,3,4],[0,1,16],79], -[[8,7,8],[0,1,16],80], -[[9,0,1],[0,1,16],81], -[[9,0,1],[0,1,4],69], -[[9,0,1],[0,1,4],66], -[[9,0,1],[0,1,4],50], -[[9,0,1],[0,1,8],62], -[[9,1,8],[0,1,8],62], -[[9,1,8],[0,1,4],81], -[[9,1,4],[0,1,8],66], -[[9,3,8],[0,1,8],78], -[[9,3,8],[0,1,8],69], -[[9,1,2],[0,1,16],83], -[[9,1,2],[0,1,4],71], -[[9,1,2],[0,1,4],66], -[[9,1,2],[0,1,4],63], -[[9,1,2],[0,1,4],47], -[[9,5,8],[0,1,8],62], -[[9,5,8],[0,1,4],83], -[[9,3,4],[0,1,8],66], -[[9,7,8],[0,1,8],71], -[[9,7,8],[0,1,8],78], -[[10,0,1],[0,1,16],79], -[[10,0,1],[0,1,4],64], -[[10,0,1],[0,1,4],59], -[[10,0,1],[0,1,4],52], -[[10,0,1],[0,1,8],67], -[[10,1,8],[0,1,8],76], -[[10,1,8],[0,1,8],67], -[[10,1,4],[0,1,4],81], -[[10,1,4],[0,1,8],61], -[[10,1,4],[0,1,4],69], -[[10,1,4],[0,1,4],64], -[[10,1,4],[0,1,4],57], -[[10,1,4],[0,1,4],49], -[[10,5,8],[0,1,32],76], -[[10,21,32],[0,1,32],78], -[[10,11,16],[0,1,32],76], -[[10,23,32],[0,1,32],74], -[[10,3,4],[0,1,32],76], -[[10,3,4],[0,1,4],60], -[[10,3,4],[0,1,4],66], -[[10,3,4],[0,1,4],62], -[[10,3,4],[0,1,4],57], -[[10,3,4],[0,1,4],48], -[[10,25,32],[0,1,32],78], -[[10,7,8],[0,1,8],74], -[[11,0,1],[0,1,16],79], -[[11,0,1],[0,1,4],67], -[[11,0,1],[0,1,4],62], -[[11,0,1],[0,1,4],59], -[[11,0,1],[0,1,4],47], -[[11,1,8],[0,1,32],81], -[[11,5,32],[0,1,32],79], -[[11,3,16],[0,1,16],78], -[[11,1,4],[0,1,8],79], -[[11,3,8],[0,1,16],81], -[[11,1,2],[0,1,16],83], -[[11,5,8],[0,1,8],71], -[[11,3,4],[0,1,8],78], -[[11,3,4],[0,1,4],71], -[[11,3,4],[0,1,4],59], -[[11,3,4],[0,1,4],56], -[[11,7,8],[0,1,8],76], -[[12,0,1],[0,1,4],74], -[[12,0,1],[0,1,2],69], -[[12,0,1],[0,1,2],64], -[[12,0,1],[0,1,4],57], -[[12,1,4],[0,1,4],55], -[[12,1,4],[0,1,32],73], -[[12,9,32],[0,1,32],74], -[[12,5,16],[0,1,32],73], -[[12,11,32],[0,1,32],74], -[[12,11,32],[0,3,32],73], -[[12,3,8],[0,1,32],71], -[[12,7,16],[0,1,16],73], -[[12,1,2],[0,1,4],57], -[[12,1,2],[0,1,4],74], -[[12,1,2],[0,1,4],69], -[[12,1,2],[0,1,4],54], -[[12,1,2],[0,1,4],62] -] -} -], - -"chords": -[ -[[1,0,1],[0,1,2],2,0,"M",[],0], -[[1,1,2],[0,1,2],11,0,"M",[],0], -[[2,0,1],[0,1,4],4,0,"m",[],0], -[[2,1,4],[0,1,4],9,0,"M",[],0], -[[2,3,4],[0,1,4],2,0,"7",[],0], -[[3,0,1],[0,1,4],7,0,"M",[],0], -[[3,1,2],[0,1,4],4,0,"m",[],0], -[[3,3,4],[0,1,4],4,0,"7",[],0], -[[4,0,1],[0,1,4],9,0,"M",[],0], -[[4,1,4],[0,1,4],1,0,"o",[],0], -[[4,1,2],[0,1,4],2,0,"M",[],0], -[[4,3,4],[0,1,4],9,0,"M",[],0], -[[5,0,1],[0,1,4],2,0,"M",[],0], -[[5,1,4],[0,1,4],9,0,"M",[],0], -[[5,1,2],[0,1,4],2,0,"M",[],0], -[[5,3,4],[0,1,4],9,0,"M",[],0], -[[6,0,1],[0,1,4],2,0,"M",[],0], -[[6,1,4],[0,1,4],9,0,"M",[],0], -[[6,1,2],[0,1,4],2,0,"M",[],0], -[[6,3,4],[0,1,4],9,0,"M",[],0], -[[7,0,1],[0,1,4],2,0,"M",[],0], -[[7,1,2],[0,1,4],7,0,"M",[],0], -[[8,0,1],[0,1,2],2,0,"M",[],0], -[[8,1,2],[0,1,2],9,0,"M",[],0], -[[9,0,1],[0,1,2],2,0,"M",[],0], -[[9,1,2],[0,1,2],11,0,"M",[],0], -[[10,0,1],[0,1,4],4,0,"m",[],0], -[[10,1,4],[0,1,4],9,0,"M",[],0], -[[10,3,4],[0,1,4],2,0,"7",[],0], -[[11,0,1],[0,1,4],7,0,"M",[],0], -[[11,1,2],[0,1,4],4,0,"m",[],0], -[[11,3,4],[0,1,4],4,0,"7",[],0], -[[12,0,1],[0,1,4],9,0,"M",[],0], -[[12,1,4],[0,1,4],1,0,"o",[],0], -[[12,1,2],[0,1,4],2,0,"M",[],0] -], - -"keyChanges": -[ -[[0,0,1],2,0,[0,2,4,5,7,9,11]] -], - -"meterChanges": -[ -[[0,0,1],4,4] -] -} \ No newline at end of file diff --git a/examples/rollercoaster.json b/examples/rollercoaster.json deleted file mode 100644 index 4cf1c5b..0000000 --- a/examples/rollercoaster.json +++ /dev/null @@ -1,255 +0,0 @@ -{ -"version": 3, -"bpm": 180, - -"tracks": -[ -{ -"notes": -[ -[[0,0,1],[0,1,2],64], -[[0,3,4],[0,1,4],64], -[[1,0,1],[0,1,4],66], -[[1,1,4],[0,1,4],68], -[[1,1,2],[0,1,2],66], -[[2,1,4],[0,1,4],66], -[[2,1,2],[0,1,4],64], -[[2,3,4],[0,1,4],63], -[[3,0,1],[0,1,2],64], -[[3,3,4],[0,3,8],64], -[[4,1,8],[0,1,8],63], -[[4,1,4],[0,1,4],61], -[[4,1,2],[0,1,2],60], -[[5,0,1],[0,1,4],61], -[[5,1,4],[0,3,8],63], -[[5,5,8],[0,1,8],61], -[[5,3,4],[0,1,4],60], -[[6,0,1],[0,1,2],66], -[[6,3,4],[0,1,4],66], -[[7,0,1],[0,1,4],68], -[[7,1,4],[0,1,4],69], -[[7,1,2],[0,1,2],68], -[[8,1,4],[0,1,4],73], -[[8,1,2],[0,1,4],75], -[[8,3,4],[0,1,4],76], -[[9,0,1],[0,1,2],75], -[[9,3,4],[0,1,4],75], -[[10,0,1],[0,1,4],73], -[[10,1,4],[0,1,4],72], -[[10,1,2],[0,3,4],75], -[[11,1,4],[0,3,4],68], -[[12,0,1],[0,1,8],60], -[[12,1,16],[0,1,8],61], -[[12,1,8],[0,1,8],63], -[[12,3,16],[0,1,8],64], -[[12,1,4],[0,1,8],66], -[[12,5,16],[0,1,8],67], -[[12,3,8],[0,1,8],68], -[[12,7,16],[0,1,8],69], -[[12,1,2],[0,1,8],71], -[[12,9,16],[0,1,8],73], -[[12,5,8],[0,1,8],75], -[[12,3,4],[0,3,4],76], -[[13,1,2],[0,1,2],75], -[[14,0,1],[0,1,4],73], -[[14,1,4],[0,3,4],71], -[[15,1,4],[0,1,4],71], -[[15,1,2],[0,1,4],73], -[[15,3,4],[0,3,4],74], -[[16,1,2],[0,1,2],73], -[[17,0,1],[0,1,4],71], -[[17,1,4],[0,3,4],69], -[[18,1,4],[0,1,4],69], -[[18,1,2],[0,1,4],71], -[[18,3,4],[0,3,4],72], -[[19,1,2],[0,1,2],71], -[[20,0,1],[0,1,4],69], -[[20,1,4],[0,3,4],68], -[[21,0,1],[0,1,2],73], -[[21,1,2],[0,1,8],73], -[[21,5,8],[0,1,8],71], -[[21,3,4],[0,3,4],69], -[[22,1,2],[0,3,4],66], -[[23,1,4],[0,1,4],76], -[[23,1,2],[0,1,4],76], -[[23,3,4],[0,1,4],76], -[[24,0,1],[0,1,4],76], -[[24,1,4],[0,1,4],68], -[[24,1,2],[0,1,4],71], -[[24,3,4],[0,3,4],76], -[[25,1,2],[0,1,2],75], -[[26,0,1],[0,1,4],73], -[[26,1,4],[0,3,4],71], -[[27,1,4],[0,1,4],71], -[[27,1,2],[0,1,4],73], -[[27,3,4],[0,3,4],74], -[[28,1,2],[0,1,2],73], -[[29,0,1],[0,1,4],71], -[[29,1,4],[0,3,4],69], -[[30,1,4],[0,1,4],69], -[[30,1,2],[0,1,4],71], -[[30,3,4],[0,3,4],72], -[[31,1,2],[0,1,2],71], -[[32,0,1],[0,1,4],69], -[[32,1,4],[0,3,4],68], -[[33,0,1],[0,1,2],73], -[[33,1,2],[0,1,8],73], -[[33,5,8],[0,1,8],71], -[[33,3,4],[0,3,4],69], -[[34,1,2],[0,3,4],66], -[[35,1,4],[0,1,4],76], -[[35,1,2],[0,1,4],76], -[[35,3,4],[0,1,4],76], -[[36,0,1],[0,1,4],76], -[[36,1,4],[0,1,4],68], -[[36,1,2],[0,1,4],71], -[[36,3,4],[0,3,4],72], -[[37,0,1],[0,1,8],76], -[[37,1,8],[0,1,8],78], -[[37,1,4],[0,1,8],76], -[[37,3,8],[0,1,8],78], -[[37,1,2],[0,1,2],71], -[[37,1,2],[0,1,8],76], -[[37,5,8],[0,1,8],72], -[[37,3,4],[0,1,8],76], -[[37,7,8],[0,1,8],78], -[[38,0,1],[0,1,4],69], -[[38,0,1],[0,1,8],76], -[[38,1,8],[0,1,8],78], -[[38,1,4],[0,1,8],76], -[[38,1,4],[0,3,4],68], -[[38,3,8],[0,1,8],78], -[[38,1,2],[0,1,8],76], -[[38,5,8],[0,1,8],78], -[[38,3,4],[0,1,8],76], -[[38,7,8],[0,1,8],78], -[[39,0,1],[0,1,8],76], -[[39,1,8],[0,1,8],71], -[[39,1,4],[0,1,4],69], -[[39,1,2],[0,1,4],71], -[[39,3,4],[0,3,4],72], -[[40,0,1],[0,1,8],76], -[[40,1,8],[0,1,8],78], -[[40,1,4],[0,1,8],76], -[[40,3,8],[0,1,8],78], -[[40,1,2],[0,1,2],71], -[[40,1,2],[0,1,8],76], -[[40,5,8],[0,1,8],72], -[[40,3,4],[0,1,8],74], -[[40,7,8],[0,1,8],76], -[[41,0,1],[0,1,8],74], -[[41,0,1],[0,1,4],69], -[[41,1,8],[0,1,8],78], -[[41,1,4],[0,7,16],79], -[[41,11,16],[0,5,16],83], -[[42,1,4],[0,1,4],69], -[[42,1,2],[0,1,4],71], -[[42,3,4],[0,3,4],72], -[[43,1,2],[0,1,2],71], -[[44,0,1],[0,1,4],72], -[[44,1,4],[0,3,4],73], -[[45,0,1],[0,1,2],72], -[[45,1,2],[0,1,4],73], -[[45,3,4],[0,3,4],74], -[[46,1,2],[0,1,2],73], -[[47,0,1],[0,1,4],74], -[[47,1,4],[0,1,4],76], -[[47,1,2],[0,1,4],76], -[[47,3,4],[0,1,4],76], -[[48,0,1],[0,1,4],76], -[[48,1,4],[0,1,8],73], -[[48,3,8],[0,1,8],71], -[[48,1,2],[0,1,8],69], -[[48,5,8],[0,1,8],68], -[[48,3,4],[1,0,1],76], -[[49,3,4],[0,1,8],73], -[[49,7,8],[0,1,8],71], -[[50,0,1],[0,1,8],69], -[[50,1,8],[0,1,8],68], -[[50,1,4],[0,1,4],76], -[[50,1,2],[0,1,4],76], -[[50,3,4],[0,1,4],76], -[[51,0,1],[0,1,4],76] -] -} -], - -"chords": -[ -[[0,0,1],[1,1,2],1,0,"m",[],0], -[[1,1,2],[0,3,4],1,-1,"o",[],0], -[[2,1,4],[0,3,4],8,0,"7",[],0], -[[3,0,1],[0,3,4],11,-1,"o",[],0], -[[3,3,4],[0,3,4],8,-1,"o7",[],0], -[[4,1,2],[0,3,4],8,0,"7",[],0], -[[5,1,4],[0,3,4],8,0,"7",[],0], -[[6,0,1],[0,3,4],9,0,"o7",[],0], -[[6,3,4],[0,3,4],6,0,"o7",[],0], -[[7,1,2],[0,3,4],1,0,"m",[],0], -[[8,1,4],[0,3,4],9,0,"M",[],0], -[[9,0,1],[0,3,4],1,-1,"o",[],0], -[[9,3,4],[0,3,4],1,-1,"o",[],0], -[[10,1,2],[0,3,4],1,-1,"o",[],0], -[[11,1,4],[0,1,4],8,0,"M",[],0], -[[12,3,4],[1,1,2],4,0,"M",[],0], -[[14,1,4],[0,3,4],3,0,"o",[],0], -[[15,0,1],[0,3,4],11,0,"M",[],0], -[[15,3,4],[0,3,4],3,-1,"M",[],0], -[[16,1,2],[0,3,4],11,0,"m",[],0], -[[17,1,4],[0,3,4],6,0,"m",[],0], -[[18,0,1],[0,3,4],9,0,"M",[],0], -[[18,3,4],[0,3,4],1,-1,"M",[],0], -[[19,1,2],[0,3,4],6,0,"o",[],0], -[[20,1,4],[0,3,4],4,0,"M",[],0], -[[21,0,1],[0,3,4],1,0,"m",[],0], -[[21,3,4],[0,3,4],6,0,"m",[],0], -[[22,1,2],[0,3,4],11,0,"M",[],0], -[[23,1,4],[0,3,4],4,0,"M",[],0], -[[24,0,1],[0,3,4],4,0,"M",[],0], -[[24,3,4],[1,1,2],4,0,"M",[],0], -[[26,1,4],[0,3,4],3,0,"o",[],0], -[[27,0,1],[0,3,4],11,0,"M",[],0], -[[27,3,4],[0,3,4],3,-1,"M",[],0], -[[28,1,2],[0,3,4],11,0,"m",[],0], -[[29,1,4],[0,3,4],6,0,"m",[],0], -[[30,0,1],[0,3,4],9,0,"M",[],0], -[[30,3,4],[0,3,4],1,-1,"M",[],0], -[[31,1,2],[0,3,4],6,0,"o",[],0], -[[32,1,4],[0,3,4],4,0,"M",[],0], -[[33,0,1],[0,3,4],1,0,"m",[],0], -[[33,3,4],[0,3,4],6,0,"m",[],0], -[[34,1,2],[0,3,4],11,0,"M",[],0], -[[35,1,4],[0,3,4],4,0,"M",[],0], -[[36,0,1],[0,1,4],4,0,"M",[],0], -[[36,3,4],[0,3,4],1,-1,"M",[],0], -[[37,1,2],[0,3,4],6,0,"o",[],0], -[[38,1,4],[0,3,4],4,0,"M",[],0], -[[39,0,1],[0,3,4],4,0,"M",[],0], -[[39,3,4],[0,3,4],1,-1,"M",[],0], -[[40,1,2],[0,3,4],6,0,"o",[],0], -[[41,1,4],[0,3,4],4,0,"m",[],0], -[[42,0,1],[0,3,4],8,-1,"M",[],0], -[[42,3,4],[0,3,4],1,-1,"M",[],0], -[[43,1,2],[0,3,4],1,-1,"M",[],0], -[[44,1,4],[1,1,2],1,0,"M",[],0], -[[45,3,4],[0,3,4],3,-1,"+",[],0], -[[46,1,2],[0,3,4],3,-1,"+",[],0], -[[47,1,4],[0,3,4],4,0,"M",[],0], -[[48,0,1],[0,3,4],4,0,"M",[],0], -[[48,3,4],[0,3,4],1,-1,"M",[],0], -[[49,1,2],[0,3,4],6,-1,"M",[],0], -[[50,1,4],[0,3,4],4,0,"M",[],0], -[[51,0,1],[0,1,4],4,0,"M",[],0] -], - -"keyChanges": -[ -[[0,0,1],0,1,[0,2,3,5,7,8,10]], -[[12,3,4],4,0,[0,2,4,5,7,9,11]] -], - -"meterChanges": -[ -[[0,0,1],3,4] -] -} \ No newline at end of file diff --git a/examples/whensomebodylovedme.json b/examples/whensomebodylovedme.json deleted file mode 100644 index cd841d6..0000000 --- a/examples/whensomebodylovedme.json +++ /dev/null @@ -1,100 +0,0 @@ -{ -"version": 3, -"bpm": 80, - -"tracks": -[ -{ -"notes": -[ -[[0,0,1],[0,1,8],60], -[[0,0,1],[0,1,4],41], -[[0,0,1],[0,1,4],53], -[[0,0,1],[0,1,4],57], -[[0,1,8],[0,1,8],65], -[[0,1,4],[0,1,8],65], -[[0,1,4],[0,1,4],55], -[[0,1,4],[0,1,4],43], -[[0,1,4],[0,1,4],58], -[[0,3,8],[0,433,6930],65], -[[0,12127,27720],[0,1733,27720],64], -[[0,1,2],[0,1,8],65], -[[0,1,2],[0,1,4],45], -[[0,1,2],[0,1,4],53], -[[0,1,2],[0,1,8],60], -[[0,5,8],[0,1,8],60], -[[1,0,1],[0,1,8],62], -[[1,0,1],[0,1,4],46], -[[1,0,1],[0,1,4],53], -[[1,1,8],[0,1,8],65], -[[1,1,4],[0,1,8],65], -[[1,1,4],[0,1,4],47], -[[1,1,4],[0,1,4],55], -[[1,3,8],[0,1,8],69], -[[1,1,2],[0,433,6930],67], -[[1,1,2],[0,1,2],48], -[[1,1,2],[0,1,2],55], -[[1,1949,3465],[0,1733,27720],65], -[[1,5,8],[0,1,8],67], -[[2,0,1],[0,1,8],64], -[[2,0,1],[0,1,4],52], -[[2,0,1],[0,1,4],58], -[[2,0,1],[0,1,4],62], -[[2,1,8],[0,1,8],67], -[[2,1,4],[0,5197,27720],69], -[[2,1,4],[0,1,4],49], -[[2,1,4],[0,1,4],57], -[[2,1,4],[0,1,4],64], -[[2,12127,27720],[0,1733,27720],67], -[[2,1,2],[0,1,8],65], -[[2,1,2],[0,1,4],50], -[[2,1,2],[0,1,4],57], -[[2,1,2],[0,1,4],62], -[[2,5,8],[0,433,6930],67], -[[2,19057,27720],[0,1,8],69], -[[2,19057,27720],[0,1,4],60], -[[2,19057,27720],[0,1,4],65], -[[2,19057,27720],[0,1,4],67], -[[2,19057,27720],[0,1,4],48], -[[2,11261,13860],[0,2599,13860],72], -[[3,0,1],[0,5197,27720],62], -[[3,0,1],[0,1,8],46], -[[3,1,8],[0,1,8],53], -[[3,5197,27720],[0,1733,27720],60], -[[3,1,4],[0,1,8],62], -[[3,1,4],[0,1,4],58], -[[3,3,8],[0,1,8],65], -[[3,1,2],[0,1,4],67], -[[3,1,2],[0,1,8],60], -[[3,5,8],[0,1,8],55], -[[3,3,4],[0,1,4],48] -] -} -], - -"chords": -[ -[[0,0,1],[0,1,4],5,0,"M",[],0], -[[0,1,4],[0,1,4],7,0,"m",[],0], -[[0,1,2],[0,1,4],5,0,"M",[],0], -[[1,0,1],[0,1,4],10,0,"M",[],0], -[[1,1,4],[0,1,4],7,0,"M",[],0], -[[1,1,2],[0,1,4],0,0,"M",[],0], -[[2,0,1],[0,1,4],10,0,"M",[],0], -[[2,1,4],[0,1,4],9,0,"M",[],0], -[[2,1,2],[0,3,16],2,0,"m",[],0], -[[2,11,16],[0,5,16],5,0,"M",["add9"],0], -[[3,0,1],[0,1,2],10,0,"M",[],0], -[[3,1,2],[0,1,2],0,0,"M",[],0] -], - -"keyChanges": -[ -[[0,0,1],5,0,[0,2,4,5,7,9,11]] -], - -"meterChanges": -[ -[[0,0,1],4,4] -] -} \ No newline at end of file diff --git a/index.html b/index.html index a891ab1..ade0fac 100644 --- a/index.html +++ b/index.html @@ -1,50 +1,44 @@ - - - TheoryTracker - + Music Analysis + - - - -
- - - - - + +
+ +
+ - + + + + + + + + + + + + + + + + + + + + + + + + + + + - \ No newline at end of file + diff --git a/main.js b/main.js new file mode 100644 index 0000000..27c6601 --- /dev/null +++ b/main.js @@ -0,0 +1,35 @@ +var mainTimeline; + + +function main() +{ + var div = document.getElementById("divTimeline"); + var canvas = document.getElementById("canvasTimeline"); + canvas.width = div.clientWidth; + + var song = new Song(); + song.keyAdd(new Key(0, 0, theory.C)); + //song.keyAdd(new Key(960, 0, theory.D)); + //song.keyAdd(new Key(1920, 0, theory.Fs)); + song.meterAdd(new Meter(0, 4, 4)); + //song.meterAdd(new Meter(720, 3, 4)); + //song.noteAdd(new Note(new TimeRange( 0, 960), new Pitch(5 * 12 + 0))); + //song.noteAdd(new Note(new TimeRange( 0, 240), new Pitch(5 * 12 + 1))); + //song.noteAdd(new Note(new TimeRange(240, 480), new Pitch(5 * 12 + 2))); + //song.noteAdd(new Note(new TimeRange(480, 720), new Pitch(5 * 12 + 3))); + //song.noteAdd(new Note(new TimeRange(720, 960), new Pitch(5 * 12 + 4))); + song.chordAdd(new Chord(new TimeRange(960 * 0, 960 * 1), 0, theory.C)); + song.chordAdd(new Chord(new TimeRange(960 * 1, 960 * 2), 0, theory.F)); + song.chordAdd(new Chord(new TimeRange(960 * 2, 960 * 3), 0, theory.G)); + song.chordAdd(new Chord(new TimeRange(960 * 3, 960 * 4), 0, theory.C)); + + var synth = new Synth(); + setInterval(function() { synth.process(1 / 60); }, 1000 / 60); + + var timeline = new Timeline(canvas, synth); + timeline.relayout(); + timeline.setSong(song); + timeline.redraw(); + + mainTimeline = timeline; +} \ No newline at end of file diff --git a/package-lock.json b/package-lock.json deleted file mode 100644 index 8490349..0000000 --- a/package-lock.json +++ /dev/null @@ -1,6628 +0,0 @@ -{ - "name": "theorytracker", - "version": "0.1.0", - "lockfileVersion": 2, - "requires": true, - "packages": { - "": { - "name": "theorytracker", - "version": "0.1.0", - "dependencies": { - "@babel/core": "^7.22.5", - "@babel/plugin-proposal-class-properties": "^7.18.6", - "@babel/preset-env": "^7.22.5", - "@babel/preset-react": "^7.22.5", - "@types/node": "^20.2.5", - "@types/react": "^18.2.9", - "@types/react-dom": "^18.2.4", - "@types/styled-components": "^5.1.26", - "@types/wicg-file-system-access": "^2020.9.6", - "babel-loader": "^9.1.2", - "core-js": "^3.30.2", - "immutable": "^4.3.0", - "pako": "^2.1.0", - "react": "^18.2.0", - "react-dom": "^18.2.0", - "regenerator-runtime": "^0.13.11", - "source-map-loader": "^4.0.1", - "styled-components": "^5.3.11", - "ts-loader": "^9.4.3", - "typescript": "^5.1.3", - "webpack": "^5.86.0", - "webpack-cli": "^5.1.4" - } - }, - "node_modules/@ampproject/remapping": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.2.0.tgz", - "integrity": "sha512-qRmjj8nj9qmLTQXXmaR1cck3UXSRMPrbsLJAasZpF+t3riI71BXed5ebIOYwQntykeZuhjsdweEc9BxH5Jc26w==", - "dependencies": { - "@jridgewell/gen-mapping": "^0.1.0", - "@jridgewell/trace-mapping": "^0.3.9" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@babel/code-frame": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.22.5.tgz", - "integrity": "sha512-Xmwn266vad+6DAqEB2A6V/CcZVp62BbwVmcOJc2RPuwih1kw02TjQvWVWlcKGbBPd+8/0V5DEkOcizRGYsspYQ==", - "dependencies": { - "@babel/highlight": "^7.22.5" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/compat-data": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.22.5.tgz", - "integrity": "sha512-4Jc/YuIaYqKnDDz892kPIledykKg12Aw1PYX5i/TY28anJtacvM1Rrr8wbieB9GfEJwlzqT0hUEao0CxEebiDA==", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/core": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.22.5.tgz", - "integrity": "sha512-SBuTAjg91A3eKOvD+bPEz3LlhHZRNu1nFOVts9lzDJTXshHTjII0BAtDS3Y2DAkdZdDKWVZGVwkDfc4Clxn1dg==", - "dependencies": { - "@ampproject/remapping": "^2.2.0", - "@babel/code-frame": "^7.22.5", - "@babel/generator": "^7.22.5", - "@babel/helper-compilation-targets": "^7.22.5", - "@babel/helper-module-transforms": "^7.22.5", - "@babel/helpers": "^7.22.5", - "@babel/parser": "^7.22.5", - "@babel/template": "^7.22.5", - "@babel/traverse": "^7.22.5", - "@babel/types": "^7.22.5", - "convert-source-map": "^1.7.0", - "debug": "^4.1.0", - "gensync": "^1.0.0-beta.2", - "json5": "^2.2.2", - "semver": "^6.3.0" - }, - "engines": { - "node": ">=6.9.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/babel" - } - }, - "node_modules/@babel/generator": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.22.5.tgz", - "integrity": "sha512-+lcUbnTRhd0jOewtFSedLyiPsD5tswKkbgcezOqqWFUVNEwoUTlpPOBmvhG7OXWLR4jMdv0czPGH5XbflnD1EA==", - "dependencies": { - "@babel/types": "^7.22.5", - "@jridgewell/gen-mapping": "^0.3.2", - "@jridgewell/trace-mapping": "^0.3.17", - "jsesc": "^2.5.1" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/generator/node_modules/@jridgewell/gen-mapping": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.2.tgz", - "integrity": "sha512-mh65xKQAzI6iBcFzwv28KVWSmCkdRBWoOh+bYQGW3+6OZvbbN3TqMGo5hqYxQniRcH9F2VZIoJCm4pa3BPDK/A==", - "dependencies": { - "@jridgewell/set-array": "^1.0.1", - "@jridgewell/sourcemap-codec": "^1.4.10", - "@jridgewell/trace-mapping": "^0.3.9" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@babel/helper-annotate-as-pure": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.22.5.tgz", - "integrity": "sha512-LvBTxu8bQSQkcyKOU+a1btnNFQ1dMAd0R6PyW3arXes06F6QLWLIrd681bxRPIXlrMGR3XYnW9JyML7dP3qgxg==", - "dependencies": { - "@babel/types": "^7.22.5" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-builder-binary-assignment-operator-visitor": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helper-builder-binary-assignment-operator-visitor/-/helper-builder-binary-assignment-operator-visitor-7.22.5.tgz", - "integrity": "sha512-m1EP3lVOPptR+2DwD125gziZNcmoNSHGmJROKoy87loWUQyJaVXDgpmruWqDARZSmtYQ+Dl25okU8+qhVzuykw==", - "dependencies": { - "@babel/types": "^7.22.5" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-compilation-targets": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.22.5.tgz", - "integrity": "sha512-Ji+ywpHeuqxB8WDxraCiqR0xfhYjiDE/e6k7FuIaANnoOFxAHskHChz4vA1mJC9Lbm01s1PVAGhQY4FUKSkGZw==", - "dependencies": { - "@babel/compat-data": "^7.22.5", - "@babel/helper-validator-option": "^7.22.5", - "browserslist": "^4.21.3", - "lru-cache": "^5.1.1", - "semver": "^6.3.0" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/@babel/helper-compilation-targets/node_modules/lru-cache": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", - "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", - "dependencies": { - "yallist": "^3.0.2" - } - }, - "node_modules/@babel/helper-compilation-targets/node_modules/yallist": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", - "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==" - }, - "node_modules/@babel/helper-create-class-features-plugin": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.22.5.tgz", - "integrity": "sha512-xkb58MyOYIslxu3gKmVXmjTtUPvBU4odYzbiIQbWwLKIHCsx6UGZGX6F1IznMFVnDdirseUZopzN+ZRt8Xb33Q==", - "dependencies": { - "@babel/helper-annotate-as-pure": "^7.22.5", - "@babel/helper-environment-visitor": "^7.22.5", - "@babel/helper-function-name": "^7.22.5", - "@babel/helper-member-expression-to-functions": "^7.22.5", - "@babel/helper-optimise-call-expression": "^7.22.5", - "@babel/helper-replace-supers": "^7.22.5", - "@babel/helper-skip-transparent-expression-wrappers": "^7.22.5", - "@babel/helper-split-export-declaration": "^7.22.5", - "semver": "^6.3.0" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/@babel/helper-create-regexp-features-plugin": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.22.5.tgz", - "integrity": "sha512-1VpEFOIbMRaXyDeUwUfmTIxExLwQ+zkW+Bh5zXpApA3oQedBx9v/updixWxnx/bZpKw7u8VxWjb/qWpIcmPq8A==", - "dependencies": { - "@babel/helper-annotate-as-pure": "^7.22.5", - "regexpu-core": "^5.3.1", - "semver": "^6.3.0" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/@babel/helper-define-polyfill-provider": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.4.0.tgz", - "integrity": "sha512-RnanLx5ETe6aybRi1cO/edaRH+bNYWaryCEmjDDYyNr4wnSzyOp8T0dWipmqVHKEY3AbVKUom50AKSlj1zmKbg==", - "dependencies": { - "@babel/helper-compilation-targets": "^7.17.7", - "@babel/helper-plugin-utils": "^7.16.7", - "debug": "^4.1.1", - "lodash.debounce": "^4.0.8", - "resolve": "^1.14.2", - "semver": "^6.1.2" - }, - "peerDependencies": { - "@babel/core": "^7.4.0-0" - } - }, - "node_modules/@babel/helper-environment-visitor": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.22.5.tgz", - "integrity": "sha512-XGmhECfVA/5sAt+H+xpSg0mfrHq6FzNr9Oxh7PSEBBRUb/mL7Kz3NICXb194rCqAEdxkhPT1a88teizAFyvk8Q==", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-function-name": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.22.5.tgz", - "integrity": "sha512-wtHSq6jMRE3uF2otvfuD3DIvVhOsSNshQl0Qrd7qC9oQJzHvOL4qQXlQn2916+CXGywIjpGuIkoyZRRxHPiNQQ==", - "dependencies": { - "@babel/template": "^7.22.5", - "@babel/types": "^7.22.5" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-hoist-variables": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.22.5.tgz", - "integrity": "sha512-wGjk9QZVzvknA6yKIUURb8zY3grXCcOZt+/7Wcy8O2uctxhplmUPkOdlgoNhmdVee2c92JXbf1xpMtVNbfoxRw==", - "dependencies": { - "@babel/types": "^7.22.5" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-member-expression-to-functions": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.22.5.tgz", - "integrity": "sha512-aBiH1NKMG0H2cGZqspNvsaBe6wNGjbJjuLy29aU+eDZjSbbN53BaxlpB02xm9v34pLTZ1nIQPFYn2qMZoa5BQQ==", - "dependencies": { - "@babel/types": "^7.22.5" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-module-imports": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.22.5.tgz", - "integrity": "sha512-8Dl6+HD/cKifutF5qGd/8ZJi84QeAKh+CEe1sBzz8UayBBGg1dAIJrdHOcOM5b2MpzWL2yuotJTtGjETq0qjXg==", - "dependencies": { - "@babel/types": "^7.22.5" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-module-transforms": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.22.5.tgz", - "integrity": "sha512-+hGKDt/Ze8GFExiVHno/2dvG5IdstpzCq0y4Qc9OJ25D4q3pKfiIP/4Vp3/JvhDkLKsDK2api3q3fpIgiIF5bw==", - "dependencies": { - "@babel/helper-environment-visitor": "^7.22.5", - "@babel/helper-module-imports": "^7.22.5", - "@babel/helper-simple-access": "^7.22.5", - "@babel/helper-split-export-declaration": "^7.22.5", - "@babel/helper-validator-identifier": "^7.22.5", - "@babel/template": "^7.22.5", - "@babel/traverse": "^7.22.5", - "@babel/types": "^7.22.5" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-optimise-call-expression": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.22.5.tgz", - "integrity": "sha512-HBwaojN0xFRx4yIvpwGqxiV2tUfl7401jlok564NgB9EHS1y6QT17FmKWm4ztqjeVdXLuC4fSvHc5ePpQjoTbw==", - "dependencies": { - "@babel/types": "^7.22.5" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-plugin-utils": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.22.5.tgz", - "integrity": "sha512-uLls06UVKgFG9QD4OeFYLEGteMIAa5kpTPcFL28yuCIIzsf6ZyKZMllKVOCZFhiZ5ptnwX4mtKdWCBE/uT4amg==", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-remap-async-to-generator": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.22.5.tgz", - "integrity": "sha512-cU0Sq1Rf4Z55fgz7haOakIyM7+x/uCFwXpLPaeRzfoUtAEAuUZjZvFPjL/rk5rW693dIgn2hng1W7xbT7lWT4g==", - "dependencies": { - "@babel/helper-annotate-as-pure": "^7.22.5", - "@babel/helper-environment-visitor": "^7.22.5", - "@babel/helper-wrap-function": "^7.22.5", - "@babel/types": "^7.22.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/@babel/helper-replace-supers": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.22.5.tgz", - "integrity": "sha512-aLdNM5I3kdI/V9xGNyKSF3X/gTyMUBohTZ+/3QdQKAA9vxIiy12E+8E2HoOP1/DjeqU+g6as35QHJNMDDYpuCg==", - "dependencies": { - "@babel/helper-environment-visitor": "^7.22.5", - "@babel/helper-member-expression-to-functions": "^7.22.5", - "@babel/helper-optimise-call-expression": "^7.22.5", - "@babel/template": "^7.22.5", - "@babel/traverse": "^7.22.5", - "@babel/types": "^7.22.5" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-simple-access": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.22.5.tgz", - "integrity": "sha512-n0H99E/K+Bika3++WNL17POvo4rKWZ7lZEp1Q+fStVbUi8nxPQEBOlTmCOxW/0JsS56SKKQ+ojAe2pHKJHN35w==", - "dependencies": { - "@babel/types": "^7.22.5" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-skip-transparent-expression-wrappers": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.22.5.tgz", - "integrity": "sha512-tK14r66JZKiC43p8Ki33yLBVJKlQDFoA8GYN67lWCDCqoL6EMMSuM9b+Iff2jHaM/RRFYl7K+iiru7hbRqNx8Q==", - "dependencies": { - "@babel/types": "^7.22.5" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-split-export-declaration": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.22.5.tgz", - "integrity": "sha512-thqK5QFghPKWLhAV321lxF95yCg2K3Ob5yw+M3VHWfdia0IkPXUtoLH8x/6Fh486QUvzhb8YOWHChTVen2/PoQ==", - "dependencies": { - "@babel/types": "^7.22.5" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-string-parser": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.22.5.tgz", - "integrity": "sha512-mM4COjgZox8U+JcXQwPijIZLElkgEpO5rsERVDJTc2qfCDfERyob6k5WegS14SX18IIjv+XD+GrqNumY5JRCDw==", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-validator-identifier": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.5.tgz", - "integrity": "sha512-aJXu+6lErq8ltp+JhkJUfk1MTGyuA4v7f3pA+BJ5HLfNC6nAQ0Cpi9uOquUj8Hehg0aUiHzWQbOVJGao6ztBAQ==", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-validator-option": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.22.5.tgz", - "integrity": "sha512-R3oB6xlIVKUnxNUxbmgq7pKjxpru24zlimpE8WK47fACIlM0II/Hm1RS8IaOI7NgCr6LNS+jl5l75m20npAziw==", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-wrap-function": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helper-wrap-function/-/helper-wrap-function-7.22.5.tgz", - "integrity": "sha512-bYqLIBSEshYcYQyfks8ewYA8S30yaGSeRslcvKMvoUk6HHPySbxHq9YRi6ghhzEU+yhQv9bP/jXnygkStOcqZw==", - "dependencies": { - "@babel/helper-function-name": "^7.22.5", - "@babel/template": "^7.22.5", - "@babel/traverse": "^7.22.5", - "@babel/types": "^7.22.5" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helpers": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.22.5.tgz", - "integrity": "sha512-pSXRmfE1vzcUIDFQcSGA5Mr+GxBV9oiRKDuDxXvWQQBCh8HoIjs/2DlDB7H8smac1IVrB9/xdXj2N3Wol9Cr+Q==", - "dependencies": { - "@babel/template": "^7.22.5", - "@babel/traverse": "^7.22.5", - "@babel/types": "^7.22.5" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/highlight": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.22.5.tgz", - "integrity": "sha512-BSKlD1hgnedS5XRnGOljZawtag7H1yPfQp0tdNJCHoH6AZ+Pcm9VvkrK59/Yy593Ypg0zMxH2BxD1VPYUQ7UIw==", - "dependencies": { - "@babel/helper-validator-identifier": "^7.22.5", - "chalk": "^2.0.0", - "js-tokens": "^4.0.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/parser": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.22.5.tgz", - "integrity": "sha512-DFZMC9LJUG9PLOclRC32G63UXwzqS2koQC8dkx+PLdmt1xSePYpbT/NbsrJy8Q/muXz7o/h/d4A7Fuyixm559Q==", - "bin": { - "parser": "bin/babel-parser.js" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression/-/plugin-bugfix-safari-id-destructuring-collision-in-function-expression-7.22.5.tgz", - "integrity": "sha512-NP1M5Rf+u2Gw9qfSO4ihjcTGW5zXTi36ITLd4/EoAcEhIZ0yjMqmftDNl3QC19CX7olhrjpyU454g/2W7X0jvQ==", - "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining/-/plugin-bugfix-v8-spread-parameters-in-optional-chaining-7.22.5.tgz", - "integrity": "sha512-31Bb65aZaUwqCbWMnZPduIZxCBngHFlzyN6Dq6KAJjtx+lx6ohKHubc61OomYi7XwVD4Ol0XCVz4h+pYFR048g==", - "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5", - "@babel/helper-skip-transparent-expression-wrappers": "^7.22.5", - "@babel/plugin-transform-optional-chaining": "^7.22.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.13.0" - } - }, - "node_modules/@babel/plugin-proposal-class-properties": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-class-properties/-/plugin-proposal-class-properties-7.18.6.tgz", - "integrity": "sha512-cumfXOF0+nzZrrN8Rf0t7M+tF6sZc7vhQwYQck9q1/5w2OExlD+b4v4RpMJFaV1Z7WcDRgO6FqvxqxGlwo+RHQ==", - "dependencies": { - "@babel/helper-create-class-features-plugin": "^7.18.6", - "@babel/helper-plugin-utils": "^7.18.6" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-proposal-private-property-in-object": { - "version": "7.21.0-placeholder-for-preset-env.2", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-private-property-in-object/-/plugin-proposal-private-property-in-object-7.21.0-placeholder-for-preset-env.2.tgz", - "integrity": "sha512-SOSkfJDddaM7mak6cPEpswyTRnuRltl429hMraQEglW+OkovnCzsiszTmsrlY//qLFjCpQDFRvjdm2wA5pPm9w==", - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-proposal-unicode-property-regex": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-unicode-property-regex/-/plugin-proposal-unicode-property-regex-7.18.6.tgz", - "integrity": "sha512-2BShG/d5yoZyXZfVePH91urL5wTG6ASZU9M4o03lKK8u8UW1y08OMttBSOADTcJrnPMpvDXRG3G8fyLh4ovs8w==", - "dependencies": { - "@babel/helper-create-regexp-features-plugin": "^7.18.6", - "@babel/helper-plugin-utils": "^7.18.6" - }, - "engines": { - "node": ">=4" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-async-generators": { - "version": "7.8.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz", - "integrity": "sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==", - "dependencies": { - "@babel/helper-plugin-utils": "^7.8.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-class-properties": { - "version": "7.12.13", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.12.13.tgz", - "integrity": "sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA==", - "dependencies": { - "@babel/helper-plugin-utils": "^7.12.13" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-class-static-block": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-static-block/-/plugin-syntax-class-static-block-7.14.5.tgz", - "integrity": "sha512-b+YyPmr6ldyNnM6sqYeMWE+bgJcJpO6yS4QD7ymxgH34GBPNDM/THBh8iunyvKIZztiwLH4CJZ0RxTk9emgpjw==", - "dependencies": { - "@babel/helper-plugin-utils": "^7.14.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-dynamic-import": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-dynamic-import/-/plugin-syntax-dynamic-import-7.8.3.tgz", - "integrity": "sha512-5gdGbFon+PszYzqs83S3E5mpi7/y/8M9eC90MRTZfduQOYW76ig6SOSPNe41IG5LoP3FGBn2N0RjVDSQiS94kQ==", - "dependencies": { - "@babel/helper-plugin-utils": "^7.8.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-export-namespace-from": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-export-namespace-from/-/plugin-syntax-export-namespace-from-7.8.3.tgz", - "integrity": "sha512-MXf5laXo6c1IbEbegDmzGPwGNTsHZmEy6QGznu5Sh2UCWvueywb2ee+CCE4zQiZstxU9BMoQO9i6zUFSY0Kj0Q==", - "dependencies": { - "@babel/helper-plugin-utils": "^7.8.3" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-import-assertions": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-assertions/-/plugin-syntax-import-assertions-7.22.5.tgz", - "integrity": "sha512-rdV97N7KqsRzeNGoWUOK6yUsWarLjE5Su/Snk9IYPU9CwkWHs4t+rTGOvffTR8XGkJMTAdLfO0xVnXm8wugIJg==", - "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-import-attributes": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.22.5.tgz", - "integrity": "sha512-KwvoWDeNKPETmozyFE0P2rOLqh39EoQHNjqizrI5B8Vt0ZNS7M56s7dAiAqbYfiAYOuIzIh96z3iR2ktgu3tEg==", - "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-import-meta": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-meta/-/plugin-syntax-import-meta-7.10.4.tgz", - "integrity": "sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==", - "dependencies": { - "@babel/helper-plugin-utils": "^7.10.4" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-json-strings": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.8.3.tgz", - "integrity": "sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==", - "dependencies": { - "@babel/helper-plugin-utils": "^7.8.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-jsx": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.22.5.tgz", - "integrity": "sha512-gvyP4hZrgrs/wWMaocvxZ44Hw0b3W8Pe+cMxc8V1ULQ07oh8VNbIRaoD1LRZVTvD+0nieDKjfgKg89sD7rrKrg==", - "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-logical-assignment-operators": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz", - "integrity": "sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==", - "dependencies": { - "@babel/helper-plugin-utils": "^7.10.4" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-nullish-coalescing-operator": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.8.3.tgz", - "integrity": "sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==", - "dependencies": { - "@babel/helper-plugin-utils": "^7.8.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-numeric-separator": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.10.4.tgz", - "integrity": "sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==", - "dependencies": { - "@babel/helper-plugin-utils": "^7.10.4" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-object-rest-spread": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz", - "integrity": "sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==", - "dependencies": { - "@babel/helper-plugin-utils": "^7.8.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-optional-catch-binding": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.8.3.tgz", - "integrity": "sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==", - "dependencies": { - "@babel/helper-plugin-utils": "^7.8.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-optional-chaining": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.8.3.tgz", - "integrity": "sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==", - "dependencies": { - "@babel/helper-plugin-utils": "^7.8.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-private-property-in-object": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-private-property-in-object/-/plugin-syntax-private-property-in-object-7.14.5.tgz", - "integrity": "sha512-0wVnp9dxJ72ZUJDV27ZfbSj6iHLoytYZmh3rFcxNnvsJF3ktkzLDZPy/mA17HGsaQT3/DQsWYX1f1QGWkCoVUg==", - "dependencies": { - "@babel/helper-plugin-utils": "^7.14.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-top-level-await": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.14.5.tgz", - "integrity": "sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw==", - "dependencies": { - "@babel/helper-plugin-utils": "^7.14.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-unicode-sets-regex": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-unicode-sets-regex/-/plugin-syntax-unicode-sets-regex-7.18.6.tgz", - "integrity": "sha512-727YkEAPwSIQTv5im8QHz3upqp92JTWhidIC81Tdx4VJYIte/VndKf1qKrfnnhPLiPghStWfvC/iFaMCQu7Nqg==", - "dependencies": { - "@babel/helper-create-regexp-features-plugin": "^7.18.6", - "@babel/helper-plugin-utils": "^7.18.6" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/@babel/plugin-transform-arrow-functions": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.22.5.tgz", - "integrity": "sha512-26lTNXoVRdAnsaDXPpvCNUq+OVWEVC6bx7Vvz9rC53F2bagUWW4u4ii2+h8Fejfh7RYqPxn+libeFBBck9muEw==", - "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-async-generator-functions": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-generator-functions/-/plugin-transform-async-generator-functions-7.22.5.tgz", - "integrity": "sha512-gGOEvFzm3fWoyD5uZq7vVTD57pPJ3PczPUD/xCFGjzBpUosnklmXyKnGQbbbGs1NPNPskFex0j93yKbHt0cHyg==", - "dependencies": { - "@babel/helper-environment-visitor": "^7.22.5", - "@babel/helper-plugin-utils": "^7.22.5", - "@babel/helper-remap-async-to-generator": "^7.22.5", - "@babel/plugin-syntax-async-generators": "^7.8.4" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-async-to-generator": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.22.5.tgz", - "integrity": "sha512-b1A8D8ZzE/VhNDoV1MSJTnpKkCG5bJo+19R4o4oy03zM7ws8yEMK755j61Dc3EyvdysbqH5BOOTquJ7ZX9C6vQ==", - "dependencies": { - "@babel/helper-module-imports": "^7.22.5", - "@babel/helper-plugin-utils": "^7.22.5", - "@babel/helper-remap-async-to-generator": "^7.22.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-block-scoped-functions": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.22.5.tgz", - "integrity": "sha512-tdXZ2UdknEKQWKJP1KMNmuF5Lx3MymtMN/pvA+p/VEkhK8jVcQ1fzSy8KM9qRYhAf2/lV33hoMPKI/xaI9sADA==", - "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-block-scoping": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.22.5.tgz", - "integrity": "sha512-EcACl1i5fSQ6bt+YGuU/XGCeZKStLmyVGytWkpyhCLeQVA0eu6Wtiw92V+I1T/hnezUv7j74dA/Ro69gWcU+hg==", - "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-class-properties": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-properties/-/plugin-transform-class-properties-7.22.5.tgz", - "integrity": "sha512-nDkQ0NfkOhPTq8YCLiWNxp1+f9fCobEjCb0n8WdbNUBc4IB5V7P1QnX9IjpSoquKrXF5SKojHleVNs2vGeHCHQ==", - "dependencies": { - "@babel/helper-create-class-features-plugin": "^7.22.5", - "@babel/helper-plugin-utils": "^7.22.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-class-static-block": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-static-block/-/plugin-transform-class-static-block-7.22.5.tgz", - "integrity": "sha512-SPToJ5eYZLxlnp1UzdARpOGeC2GbHvr9d/UV0EukuVx8atktg194oe+C5BqQ8jRTkgLRVOPYeXRSBg1IlMoVRA==", - "dependencies": { - "@babel/helper-create-class-features-plugin": "^7.22.5", - "@babel/helper-plugin-utils": "^7.22.5", - "@babel/plugin-syntax-class-static-block": "^7.14.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.12.0" - } - }, - "node_modules/@babel/plugin-transform-classes": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.22.5.tgz", - "integrity": "sha512-2edQhLfibpWpsVBx2n/GKOz6JdGQvLruZQfGr9l1qes2KQaWswjBzhQF7UDUZMNaMMQeYnQzxwOMPsbYF7wqPQ==", - "dependencies": { - "@babel/helper-annotate-as-pure": "^7.22.5", - "@babel/helper-compilation-targets": "^7.22.5", - "@babel/helper-environment-visitor": "^7.22.5", - "@babel/helper-function-name": "^7.22.5", - "@babel/helper-optimise-call-expression": "^7.22.5", - "@babel/helper-plugin-utils": "^7.22.5", - "@babel/helper-replace-supers": "^7.22.5", - "@babel/helper-split-export-declaration": "^7.22.5", - "globals": "^11.1.0" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-computed-properties": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.22.5.tgz", - "integrity": "sha512-4GHWBgRf0krxPX+AaPtgBAlTgTeZmqDynokHOX7aqqAB4tHs3U2Y02zH6ETFdLZGcg9UQSD1WCmkVrE9ErHeOg==", - "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5", - "@babel/template": "^7.22.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-destructuring": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.22.5.tgz", - "integrity": "sha512-GfqcFuGW8vnEqTUBM7UtPd5A4q797LTvvwKxXTgRsFjoqaJiEg9deBG6kWeQYkVEL569NpnmpC0Pkr/8BLKGnQ==", - "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-dotall-regex": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.22.5.tgz", - "integrity": "sha512-5/Yk9QxCQCl+sOIB1WelKnVRxTJDSAIxtJLL2/pqL14ZVlbH0fUQUZa/T5/UnQtBNgghR7mfB8ERBKyKPCi7Vw==", - "dependencies": { - "@babel/helper-create-regexp-features-plugin": "^7.22.5", - "@babel/helper-plugin-utils": "^7.22.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-duplicate-keys": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.22.5.tgz", - "integrity": "sha512-dEnYD+9BBgld5VBXHnF/DbYGp3fqGMsyxKbtD1mDyIA7AkTSpKXFhCVuj/oQVOoALfBs77DudA0BE4d5mcpmqw==", - "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-dynamic-import": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dynamic-import/-/plugin-transform-dynamic-import-7.22.5.tgz", - "integrity": "sha512-0MC3ppTB1AMxd8fXjSrbPa7LT9hrImt+/fcj+Pg5YMD7UQyWp/02+JWpdnCymmsXwIx5Z+sYn1bwCn4ZJNvhqQ==", - "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5", - "@babel/plugin-syntax-dynamic-import": "^7.8.3" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-exponentiation-operator": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.22.5.tgz", - "integrity": "sha512-vIpJFNM/FjZ4rh1myqIya9jXwrwwgFRHPjT3DkUA9ZLHuzox8jiXkOLvwm1H+PQIP3CqfC++WPKeuDi0Sjdj1g==", - "dependencies": { - "@babel/helper-builder-binary-assignment-operator-visitor": "^7.22.5", - "@babel/helper-plugin-utils": "^7.22.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-export-namespace-from": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-export-namespace-from/-/plugin-transform-export-namespace-from-7.22.5.tgz", - "integrity": "sha512-X4hhm7FRnPgd4nDA4b/5V280xCx6oL7Oob5+9qVS5C13Zq4bh1qq7LU0GgRU6b5dBWBvhGaXYVB4AcN6+ol6vg==", - "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5", - "@babel/plugin-syntax-export-namespace-from": "^7.8.3" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-for-of": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.22.5.tgz", - "integrity": "sha512-3kxQjX1dU9uudwSshyLeEipvrLjBCVthCgeTp6CzE/9JYrlAIaeekVxRpCWsDDfYTfRZRoCeZatCQvwo+wvK8A==", - "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-function-name": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.22.5.tgz", - "integrity": "sha512-UIzQNMS0p0HHiQm3oelztj+ECwFnj+ZRV4KnguvlsD2of1whUeM6o7wGNj6oLwcDoAXQ8gEqfgC24D+VdIcevg==", - "dependencies": { - "@babel/helper-compilation-targets": "^7.22.5", - "@babel/helper-function-name": "^7.22.5", - "@babel/helper-plugin-utils": "^7.22.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-json-strings": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-json-strings/-/plugin-transform-json-strings-7.22.5.tgz", - "integrity": "sha512-DuCRB7fu8MyTLbEQd1ew3R85nx/88yMoqo2uPSjevMj3yoN7CDM8jkgrY0wmVxfJZyJ/B9fE1iq7EQppWQmR5A==", - "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5", - "@babel/plugin-syntax-json-strings": "^7.8.3" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-literals": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-literals/-/plugin-transform-literals-7.22.5.tgz", - "integrity": "sha512-fTLj4D79M+mepcw3dgFBTIDYpbcB9Sm0bpm4ppXPaO+U+PKFFyV9MGRvS0gvGw62sd10kT5lRMKXAADb9pWy8g==", - "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-logical-assignment-operators": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-logical-assignment-operators/-/plugin-transform-logical-assignment-operators-7.22.5.tgz", - "integrity": "sha512-MQQOUW1KL8X0cDWfbwYP+TbVbZm16QmQXJQ+vndPtH/BoO0lOKpVoEDMI7+PskYxH+IiE0tS8xZye0qr1lGzSA==", - "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5", - "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-member-expression-literals": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-7.22.5.tgz", - "integrity": "sha512-RZEdkNtzzYCFl9SE9ATaUMTj2hqMb4StarOJLrZRbqqU4HSBE7UlBw9WBWQiDzrJZJdUWiMTVDI6Gv/8DPvfew==", - "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-modules-amd": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.22.5.tgz", - "integrity": "sha512-R+PTfLTcYEmb1+kK7FNkhQ1gP4KgjpSO6HfH9+f8/yfp2Nt3ggBjiVpRwmwTlfqZLafYKJACy36yDXlEmI9HjQ==", - "dependencies": { - "@babel/helper-module-transforms": "^7.22.5", - "@babel/helper-plugin-utils": "^7.22.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-modules-commonjs": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.22.5.tgz", - "integrity": "sha512-B4pzOXj+ONRmuaQTg05b3y/4DuFz3WcCNAXPLb2Q0GT0TrGKGxNKV4jwsXts+StaM0LQczZbOpj8o1DLPDJIiA==", - "dependencies": { - "@babel/helper-module-transforms": "^7.22.5", - "@babel/helper-plugin-utils": "^7.22.5", - "@babel/helper-simple-access": "^7.22.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-modules-systemjs": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.22.5.tgz", - "integrity": "sha512-emtEpoaTMsOs6Tzz+nbmcePl6AKVtS1yC4YNAeMun9U8YCsgadPNxnOPQ8GhHFB2qdx+LZu9LgoC0Lthuu05DQ==", - "dependencies": { - "@babel/helper-hoist-variables": "^7.22.5", - "@babel/helper-module-transforms": "^7.22.5", - "@babel/helper-plugin-utils": "^7.22.5", - "@babel/helper-validator-identifier": "^7.22.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-modules-umd": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.22.5.tgz", - "integrity": "sha512-+S6kzefN/E1vkSsKx8kmQuqeQsvCKCd1fraCM7zXm4SFoggI099Tr4G8U81+5gtMdUeMQ4ipdQffbKLX0/7dBQ==", - "dependencies": { - "@babel/helper-module-transforms": "^7.22.5", - "@babel/helper-plugin-utils": "^7.22.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-named-capturing-groups-regex": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.22.5.tgz", - "integrity": "sha512-YgLLKmS3aUBhHaxp5hi1WJTgOUb/NCuDHzGT9z9WTt3YG+CPRhJs6nprbStx6DnWM4dh6gt7SU3sZodbZ08adQ==", - "dependencies": { - "@babel/helper-create-regexp-features-plugin": "^7.22.5", - "@babel/helper-plugin-utils": "^7.22.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/@babel/plugin-transform-new-target": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.22.5.tgz", - "integrity": "sha512-AsF7K0Fx/cNKVyk3a+DW0JLo+Ua598/NxMRvxDnkpCIGFh43+h/v2xyhRUYf6oD8gE4QtL83C7zZVghMjHd+iw==", - "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-nullish-coalescing-operator": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-nullish-coalescing-operator/-/plugin-transform-nullish-coalescing-operator-7.22.5.tgz", - "integrity": "sha512-6CF8g6z1dNYZ/VXok5uYkkBBICHZPiGEl7oDnAx2Mt1hlHVHOSIKWJaXHjQJA5VB43KZnXZDIexMchY4y2PGdA==", - "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5", - "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-numeric-separator": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-numeric-separator/-/plugin-transform-numeric-separator-7.22.5.tgz", - "integrity": "sha512-NbslED1/6M+sXiwwtcAB/nieypGw02Ejf4KtDeMkCEpP6gWFMX1wI9WKYua+4oBneCCEmulOkRpwywypVZzs/g==", - "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5", - "@babel/plugin-syntax-numeric-separator": "^7.10.4" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-object-rest-spread": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-rest-spread/-/plugin-transform-object-rest-spread-7.22.5.tgz", - "integrity": "sha512-Kk3lyDmEslH9DnvCDA1s1kkd3YWQITiBOHngOtDL9Pt6BZjzqb6hiOlb8VfjiiQJ2unmegBqZu0rx5RxJb5vmQ==", - "dependencies": { - "@babel/compat-data": "^7.22.5", - "@babel/helper-compilation-targets": "^7.22.5", - "@babel/helper-plugin-utils": "^7.22.5", - "@babel/plugin-syntax-object-rest-spread": "^7.8.3", - "@babel/plugin-transform-parameters": "^7.22.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-object-super": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.22.5.tgz", - "integrity": "sha512-klXqyaT9trSjIUrcsYIfETAzmOEZL3cBYqOYLJxBHfMFFggmXOv+NYSX/Jbs9mzMVESw/WycLFPRx8ba/b2Ipw==", - "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5", - "@babel/helper-replace-supers": "^7.22.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-optional-catch-binding": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-catch-binding/-/plugin-transform-optional-catch-binding-7.22.5.tgz", - "integrity": "sha512-pH8orJahy+hzZje5b8e2QIlBWQvGpelS76C63Z+jhZKsmzfNaPQ+LaW6dcJ9bxTpo1mtXbgHwy765Ro3jftmUg==", - "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5", - "@babel/plugin-syntax-optional-catch-binding": "^7.8.3" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-optional-chaining": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-chaining/-/plugin-transform-optional-chaining-7.22.5.tgz", - "integrity": "sha512-AconbMKOMkyG+xCng2JogMCDcqW8wedQAqpVIL4cOSescZ7+iW8utC6YDZLMCSUIReEA733gzRSaOSXMAt/4WQ==", - "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5", - "@babel/helper-skip-transparent-expression-wrappers": "^7.22.5", - "@babel/plugin-syntax-optional-chaining": "^7.8.3" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-parameters": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.22.5.tgz", - "integrity": "sha512-AVkFUBurORBREOmHRKo06FjHYgjrabpdqRSwq6+C7R5iTCZOsM4QbcB27St0a4U6fffyAOqh3s/qEfybAhfivg==", - "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-private-methods": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-methods/-/plugin-transform-private-methods-7.22.5.tgz", - "integrity": "sha512-PPjh4gyrQnGe97JTalgRGMuU4icsZFnWkzicB/fUtzlKUqvsWBKEpPPfr5a2JiyirZkHxnAqkQMO5Z5B2kK3fA==", - "dependencies": { - "@babel/helper-create-class-features-plugin": "^7.22.5", - "@babel/helper-plugin-utils": "^7.22.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-private-property-in-object": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-property-in-object/-/plugin-transform-private-property-in-object-7.22.5.tgz", - "integrity": "sha512-/9xnaTTJcVoBtSSmrVyhtSvO3kbqS2ODoh2juEU72c3aYonNF0OMGiaz2gjukyKM2wBBYJP38S4JiE0Wfb5VMQ==", - "dependencies": { - "@babel/helper-annotate-as-pure": "^7.22.5", - "@babel/helper-create-class-features-plugin": "^7.22.5", - "@babel/helper-plugin-utils": "^7.22.5", - "@babel/plugin-syntax-private-property-in-object": "^7.14.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-property-literals": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.22.5.tgz", - "integrity": "sha512-TiOArgddK3mK/x1Qwf5hay2pxI6wCZnvQqrFSqbtg1GLl2JcNMitVH/YnqjP+M31pLUeTfzY1HAXFDnUBV30rQ==", - "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-react-display-name": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-display-name/-/plugin-transform-react-display-name-7.22.5.tgz", - "integrity": "sha512-PVk3WPYudRF5z4GKMEYUrLjPl38fJSKNaEOkFuoprioowGuWN6w2RKznuFNSlJx7pzzXXStPUnNSOEO0jL5EVw==", - "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-react-jsx": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx/-/plugin-transform-react-jsx-7.22.5.tgz", - "integrity": "sha512-rog5gZaVbUip5iWDMTYbVM15XQq+RkUKhET/IHR6oizR+JEoN6CAfTTuHcK4vwUyzca30qqHqEpzBOnaRMWYMA==", - "dependencies": { - "@babel/helper-annotate-as-pure": "^7.22.5", - "@babel/helper-module-imports": "^7.22.5", - "@babel/helper-plugin-utils": "^7.22.5", - "@babel/plugin-syntax-jsx": "^7.22.5", - "@babel/types": "^7.22.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-react-jsx-development": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-development/-/plugin-transform-react-jsx-development-7.22.5.tgz", - "integrity": "sha512-bDhuzwWMuInwCYeDeMzyi7TaBgRQei6DqxhbyniL7/VG4RSS7HtSL2QbY4eESy1KJqlWt8g3xeEBGPuo+XqC8A==", - "dependencies": { - "@babel/plugin-transform-react-jsx": "^7.22.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-react-pure-annotations": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-pure-annotations/-/plugin-transform-react-pure-annotations-7.22.5.tgz", - "integrity": "sha512-gP4k85wx09q+brArVinTXhWiyzLl9UpmGva0+mWyKxk6JZequ05x3eUcIUE+FyttPKJFRRVtAvQaJ6YF9h1ZpA==", - "dependencies": { - "@babel/helper-annotate-as-pure": "^7.22.5", - "@babel/helper-plugin-utils": "^7.22.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-regenerator": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.22.5.tgz", - "integrity": "sha512-rR7KePOE7gfEtNTh9Qw+iO3Q/e4DEsoQ+hdvM6QUDH7JRJ5qxq5AA52ZzBWbI5i9lfNuvySgOGP8ZN7LAmaiPw==", - "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5", - "regenerator-transform": "^0.15.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-reserved-words": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-reserved-words/-/plugin-transform-reserved-words-7.22.5.tgz", - "integrity": "sha512-DTtGKFRQUDm8svigJzZHzb/2xatPc6TzNvAIJ5GqOKDsGFYgAskjRulbR/vGsPKq3OPqtexnz327qYpP57RFyA==", - "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-shorthand-properties": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.22.5.tgz", - "integrity": "sha512-vM4fq9IXHscXVKzDv5itkO1X52SmdFBFcMIBZ2FRn2nqVYqw6dBexUgMvAjHW+KXpPPViD/Yo3GrDEBaRC0QYA==", - "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-spread": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-spread/-/plugin-transform-spread-7.22.5.tgz", - "integrity": "sha512-5ZzDQIGyvN4w8+dMmpohL6MBo+l2G7tfC/O2Dg7/hjpgeWvUx8FzfeOKxGog9IimPa4YekaQ9PlDqTLOljkcxg==", - "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5", - "@babel/helper-skip-transparent-expression-wrappers": "^7.22.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-sticky-regex": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.22.5.tgz", - "integrity": "sha512-zf7LuNpHG0iEeiyCNwX4j3gDg1jgt1k3ZdXBKbZSoA3BbGQGvMiSvfbZRR3Dr3aeJe3ooWFZxOOG3IRStYp2Bw==", - "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-template-literals": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.22.5.tgz", - "integrity": "sha512-5ciOehRNf+EyUeewo8NkbQiUs4d6ZxiHo6BcBcnFlgiJfu16q0bQUw9Jvo0b0gBKFG1SMhDSjeKXSYuJLeFSMA==", - "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-typeof-symbol": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.22.5.tgz", - "integrity": "sha512-bYkI5lMzL4kPii4HHEEChkD0rkc+nvnlR6+o/qdqR6zrm0Sv/nodmyLhlq2DO0YKLUNd2VePmPRjJXSBh9OIdA==", - "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-unicode-escapes": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-escapes/-/plugin-transform-unicode-escapes-7.22.5.tgz", - "integrity": "sha512-biEmVg1IYB/raUO5wT1tgfacCef15Fbzhkx493D3urBI++6hpJ+RFG4SrWMn0NEZLfvilqKf3QDrRVZHo08FYg==", - "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-unicode-property-regex": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-property-regex/-/plugin-transform-unicode-property-regex-7.22.5.tgz", - "integrity": "sha512-HCCIb+CbJIAE6sXn5CjFQXMwkCClcOfPCzTlilJ8cUatfzwHlWQkbtV0zD338u9dZskwvuOYTuuaMaA8J5EI5A==", - "dependencies": { - "@babel/helper-create-regexp-features-plugin": "^7.22.5", - "@babel/helper-plugin-utils": "^7.22.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-unicode-regex": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.22.5.tgz", - "integrity": "sha512-028laaOKptN5vHJf9/Arr/HiJekMd41hOEZYvNsrsXqJ7YPYuX2bQxh31fkZzGmq3YqHRJzYFFAVYvKfMPKqyg==", - "dependencies": { - "@babel/helper-create-regexp-features-plugin": "^7.22.5", - "@babel/helper-plugin-utils": "^7.22.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-unicode-sets-regex": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-sets-regex/-/plugin-transform-unicode-sets-regex-7.22.5.tgz", - "integrity": "sha512-lhMfi4FC15j13eKrh3DnYHjpGj6UKQHtNKTbtc1igvAhRy4+kLhV07OpLcsN0VgDEw/MjAvJO4BdMJsHwMhzCg==", - "dependencies": { - "@babel/helper-create-regexp-features-plugin": "^7.22.5", - "@babel/helper-plugin-utils": "^7.22.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/@babel/preset-env": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.22.5.tgz", - "integrity": "sha512-fj06hw89dpiZzGZtxn+QybifF07nNiZjZ7sazs2aVDcysAZVGjW7+7iFYxg6GLNM47R/thYfLdrXc+2f11Vi9A==", - "dependencies": { - "@babel/compat-data": "^7.22.5", - "@babel/helper-compilation-targets": "^7.22.5", - "@babel/helper-plugin-utils": "^7.22.5", - "@babel/helper-validator-option": "^7.22.5", - "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": "^7.22.5", - "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": "^7.22.5", - "@babel/plugin-proposal-private-property-in-object": "7.21.0-placeholder-for-preset-env.2", - "@babel/plugin-syntax-async-generators": "^7.8.4", - "@babel/plugin-syntax-class-properties": "^7.12.13", - "@babel/plugin-syntax-class-static-block": "^7.14.5", - "@babel/plugin-syntax-dynamic-import": "^7.8.3", - "@babel/plugin-syntax-export-namespace-from": "^7.8.3", - "@babel/plugin-syntax-import-assertions": "^7.22.5", - "@babel/plugin-syntax-import-attributes": "^7.22.5", - "@babel/plugin-syntax-import-meta": "^7.10.4", - "@babel/plugin-syntax-json-strings": "^7.8.3", - "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4", - "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3", - "@babel/plugin-syntax-numeric-separator": "^7.10.4", - "@babel/plugin-syntax-object-rest-spread": "^7.8.3", - "@babel/plugin-syntax-optional-catch-binding": "^7.8.3", - "@babel/plugin-syntax-optional-chaining": "^7.8.3", - "@babel/plugin-syntax-private-property-in-object": "^7.14.5", - "@babel/plugin-syntax-top-level-await": "^7.14.5", - "@babel/plugin-syntax-unicode-sets-regex": "^7.18.6", - "@babel/plugin-transform-arrow-functions": "^7.22.5", - "@babel/plugin-transform-async-generator-functions": "^7.22.5", - "@babel/plugin-transform-async-to-generator": "^7.22.5", - "@babel/plugin-transform-block-scoped-functions": "^7.22.5", - "@babel/plugin-transform-block-scoping": "^7.22.5", - "@babel/plugin-transform-class-properties": "^7.22.5", - "@babel/plugin-transform-class-static-block": "^7.22.5", - "@babel/plugin-transform-classes": "^7.22.5", - "@babel/plugin-transform-computed-properties": "^7.22.5", - "@babel/plugin-transform-destructuring": "^7.22.5", - "@babel/plugin-transform-dotall-regex": "^7.22.5", - "@babel/plugin-transform-duplicate-keys": "^7.22.5", - "@babel/plugin-transform-dynamic-import": "^7.22.5", - "@babel/plugin-transform-exponentiation-operator": "^7.22.5", - "@babel/plugin-transform-export-namespace-from": "^7.22.5", - "@babel/plugin-transform-for-of": "^7.22.5", - "@babel/plugin-transform-function-name": "^7.22.5", - "@babel/plugin-transform-json-strings": "^7.22.5", - "@babel/plugin-transform-literals": "^7.22.5", - "@babel/plugin-transform-logical-assignment-operators": "^7.22.5", - "@babel/plugin-transform-member-expression-literals": "^7.22.5", - "@babel/plugin-transform-modules-amd": "^7.22.5", - "@babel/plugin-transform-modules-commonjs": "^7.22.5", - "@babel/plugin-transform-modules-systemjs": "^7.22.5", - "@babel/plugin-transform-modules-umd": "^7.22.5", - "@babel/plugin-transform-named-capturing-groups-regex": "^7.22.5", - "@babel/plugin-transform-new-target": "^7.22.5", - "@babel/plugin-transform-nullish-coalescing-operator": "^7.22.5", - "@babel/plugin-transform-numeric-separator": "^7.22.5", - "@babel/plugin-transform-object-rest-spread": "^7.22.5", - "@babel/plugin-transform-object-super": "^7.22.5", - "@babel/plugin-transform-optional-catch-binding": "^7.22.5", - "@babel/plugin-transform-optional-chaining": "^7.22.5", - "@babel/plugin-transform-parameters": "^7.22.5", - "@babel/plugin-transform-private-methods": "^7.22.5", - "@babel/plugin-transform-private-property-in-object": "^7.22.5", - "@babel/plugin-transform-property-literals": "^7.22.5", - "@babel/plugin-transform-regenerator": "^7.22.5", - "@babel/plugin-transform-reserved-words": "^7.22.5", - "@babel/plugin-transform-shorthand-properties": "^7.22.5", - "@babel/plugin-transform-spread": "^7.22.5", - "@babel/plugin-transform-sticky-regex": "^7.22.5", - "@babel/plugin-transform-template-literals": "^7.22.5", - "@babel/plugin-transform-typeof-symbol": "^7.22.5", - "@babel/plugin-transform-unicode-escapes": "^7.22.5", - "@babel/plugin-transform-unicode-property-regex": "^7.22.5", - "@babel/plugin-transform-unicode-regex": "^7.22.5", - "@babel/plugin-transform-unicode-sets-regex": "^7.22.5", - "@babel/preset-modules": "^0.1.5", - "@babel/types": "^7.22.5", - "babel-plugin-polyfill-corejs2": "^0.4.3", - "babel-plugin-polyfill-corejs3": "^0.8.1", - "babel-plugin-polyfill-regenerator": "^0.5.0", - "core-js-compat": "^3.30.2", - "semver": "^6.3.0" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/preset-modules": { - "version": "0.1.5", - "resolved": "https://registry.npmjs.org/@babel/preset-modules/-/preset-modules-0.1.5.tgz", - "integrity": "sha512-A57th6YRG7oR3cq/yt/Y84MvGgE0eJG2F1JLhKuyG+jFxEgrd/HAMJatiFtmOiZurz+0DkrvbheCLaV5f2JfjA==", - "dependencies": { - "@babel/helper-plugin-utils": "^7.0.0", - "@babel/plugin-proposal-unicode-property-regex": "^7.4.4", - "@babel/plugin-transform-dotall-regex": "^7.4.4", - "@babel/types": "^7.4.4", - "esutils": "^2.0.2" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/preset-react": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/preset-react/-/preset-react-7.22.5.tgz", - "integrity": "sha512-M+Is3WikOpEJHgR385HbuCITPTaPRaNkibTEa9oiofmJvIsrceb4yp9RL9Kb+TE8LznmeyZqpP+Lopwcx59xPQ==", - "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5", - "@babel/helper-validator-option": "^7.22.5", - "@babel/plugin-transform-react-display-name": "^7.22.5", - "@babel/plugin-transform-react-jsx": "^7.22.5", - "@babel/plugin-transform-react-jsx-development": "^7.22.5", - "@babel/plugin-transform-react-pure-annotations": "^7.22.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/regjsgen": { - "version": "0.8.0", - "resolved": "https://registry.npmjs.org/@babel/regjsgen/-/regjsgen-0.8.0.tgz", - "integrity": "sha512-x/rqGMdzj+fWZvCOYForTghzbtqPDZ5gPwaoNGHdgDfF2QA/XZbCBp4Moo5scrkAMPhB7z26XM/AaHuIJdgauA==" - }, - "node_modules/@babel/runtime": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.22.5.tgz", - "integrity": "sha512-ecjvYlnAaZ/KVneE/OdKYBYfgXV3Ptu6zQWmgEF7vwKhQnvVS6bjMD2XYgj+SNvQ1GfK/pjgokfPkC/2CO8CuA==", - "dependencies": { - "regenerator-runtime": "^0.13.11" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/template": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.22.5.tgz", - "integrity": "sha512-X7yV7eiwAxdj9k94NEylvbVHLiVG1nvzCV2EAowhxLTwODV1jl9UzZ48leOC0sH7OnuHrIkllaBgneUykIcZaw==", - "dependencies": { - "@babel/code-frame": "^7.22.5", - "@babel/parser": "^7.22.5", - "@babel/types": "^7.22.5" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/traverse": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.22.5.tgz", - "integrity": "sha512-7DuIjPgERaNo6r+PZwItpjCZEa5vyw4eJGufeLxrPdBXBoLcCJCIasvK6pK/9DVNrLZTLFhUGqaC6X/PA007TQ==", - "dependencies": { - "@babel/code-frame": "^7.22.5", - "@babel/generator": "^7.22.5", - "@babel/helper-environment-visitor": "^7.22.5", - "@babel/helper-function-name": "^7.22.5", - "@babel/helper-hoist-variables": "^7.22.5", - "@babel/helper-split-export-declaration": "^7.22.5", - "@babel/parser": "^7.22.5", - "@babel/types": "^7.22.5", - "debug": "^4.1.0", - "globals": "^11.1.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/types": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.22.5.tgz", - "integrity": "sha512-zo3MIHGOkPOfoRXitsgHLjEXmlDaD/5KU1Uzuc9GNiZPhSqVxVRtxuPaSBZDsYZ9qV88AjtMtWW7ww98loJ9KA==", - "dependencies": { - "@babel/helper-string-parser": "^7.22.5", - "@babel/helper-validator-identifier": "^7.22.5", - "to-fast-properties": "^2.0.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@discoveryjs/json-ext": { - "version": "0.5.3", - "resolved": "https://registry.npmjs.org/@discoveryjs/json-ext/-/json-ext-0.5.3.tgz", - "integrity": "sha512-Fxt+AfXgjMoin2maPIYzFZnQjAXjAL0PHscM5pRTtatFqB+vZxAM9tLp2Optnuw3QOQC40jTNeGYFOMvyf7v9g==", - "engines": { - "node": ">=10.0.0" - } - }, - "node_modules/@emotion/is-prop-valid": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@emotion/is-prop-valid/-/is-prop-valid-1.2.0.tgz", - "integrity": "sha512-3aDpDprjM0AwaxGE09bOPkNxHpBd+kA6jty3RnaEXdweX1DF1U3VQpPYb0g1IStAuK7SVQ1cy+bNBBKp4W3Fjg==", - "dependencies": { - "@emotion/memoize": "^0.8.0" - } - }, - "node_modules/@emotion/memoize": { - "version": "0.8.0", - "resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.8.0.tgz", - "integrity": "sha512-G/YwXTkv7Den9mXDO7AhLWkE3q+I92B+VqAE+dYG4NGPaHZGvt3G8Q0p9vmE+sq7rTGphUbAvmQ9YpbfMQGGlA==" - }, - "node_modules/@emotion/stylis": { - "version": "0.8.5", - "resolved": "https://registry.npmjs.org/@emotion/stylis/-/stylis-0.8.5.tgz", - "integrity": "sha512-h6KtPihKFn3T9fuIrwvXXUOwlx3rfUvfZIcP5a6rh8Y7zjE3O06hT5Ss4S/YI1AYhuZ1kjaE/5EaOOI2NqSylQ==" - }, - "node_modules/@emotion/unitless": { - "version": "0.7.5", - "resolved": "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.7.5.tgz", - "integrity": "sha512-OWORNpfjMsSSUBVrRBVGECkhWcULOAJz9ZW8uK9qgxD+87M7jHRcvh/A96XXNhXTLmKcoYSQtBEX7lHMO7YRwg==" - }, - "node_modules/@jridgewell/gen-mapping": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.1.1.tgz", - "integrity": "sha512-sQXCasFk+U8lWYEe66WxRDOE9PjVz4vSM51fTu3Hw+ClTpUSQb718772vH3pyS5pShp6lvQM7SxgIDXXXmOX7w==", - "dependencies": { - "@jridgewell/set-array": "^1.0.0", - "@jridgewell/sourcemap-codec": "^1.4.10" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@jridgewell/resolve-uri": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.0.tgz", - "integrity": "sha512-F2msla3tad+Mfht5cJq7LSXcdudKTWCVYUgw6pLFOOHSTtZlj6SWNYAp+AhuqLmWdBO2X5hPrLcu8cVP8fy28w==", - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@jridgewell/set-array": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.1.2.tgz", - "integrity": "sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==", - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@jridgewell/source-map": { - "version": "0.3.3", - "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.3.tgz", - "integrity": "sha512-b+fsZXeLYi9fEULmfBrhxn4IrPlINf8fiNarzTof004v3lFdntdwa9PF7vFJqm3mg7s+ScJMxXaE3Acp1irZcg==", - "dependencies": { - "@jridgewell/gen-mapping": "^0.3.0", - "@jridgewell/trace-mapping": "^0.3.9" - } - }, - "node_modules/@jridgewell/source-map/node_modules/@jridgewell/gen-mapping": { - "version": "0.3.3", - "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.3.tgz", - "integrity": "sha512-HLhSWOLRi875zjjMG/r+Nv0oCW8umGb0BgEhyX3dDX3egwZtB8PqLnjz3yedt8R5StBrzcg4aBpnh8UA9D1BoQ==", - "dependencies": { - "@jridgewell/set-array": "^1.0.1", - "@jridgewell/sourcemap-codec": "^1.4.10", - "@jridgewell/trace-mapping": "^0.3.9" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@jridgewell/sourcemap-codec": { - "version": "1.4.14", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.14.tgz", - "integrity": "sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw==" - }, - "node_modules/@jridgewell/trace-mapping": { - "version": "0.3.18", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.18.tgz", - "integrity": "sha512-w+niJYzMHdd7USdiH2U6869nqhD2nbfZXND5Yp93qIbEmnDNk7PD48o+YchRVpzMU7M6jVCbenTR7PA1FLQ9pA==", - "dependencies": { - "@jridgewell/resolve-uri": "3.1.0", - "@jridgewell/sourcemap-codec": "1.4.14" - } - }, - "node_modules/@types/eslint": { - "version": "8.4.6", - "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-8.4.6.tgz", - "integrity": "sha512-/fqTbjxyFUaYNO7VcW5g+4npmqVACz1bB7RTHYuLj+PRjw9hrCwrUXVQFpChUS0JsyEFvMZ7U/PfmvWgxJhI9g==", - "dependencies": { - "@types/estree": "*", - "@types/json-schema": "*" - } - }, - "node_modules/@types/eslint-scope": { - "version": "3.7.4", - "resolved": "https://registry.npmjs.org/@types/eslint-scope/-/eslint-scope-3.7.4.tgz", - "integrity": "sha512-9K4zoImiZc3HlIp6AVUDE4CWYx22a+lhSZMYNpbjW04+YF0KWj4pJXnEMjdnFTiQibFFmElcsasJXDbdI/EPhA==", - "dependencies": { - "@types/eslint": "*", - "@types/estree": "*" - } - }, - "node_modules/@types/estree": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.1.tgz", - "integrity": "sha512-LG4opVs2ANWZ1TJoKc937iMmNstM/d0ae1vNbnBvBhqCSezgVUOzcLCqbI5elV8Vy6WKwKjaqR+zO9VKirBBCA==" - }, - "node_modules/@types/hoist-non-react-statics": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.1.tgz", - "integrity": "sha512-iMIqiko6ooLrTh1joXodJK5X9xeEALT1kM5G3ZLhD3hszxBdIEd5C75U834D9mLcINgD4OyZf5uQXjkuYydWvA==", - "dependencies": { - "@types/react": "*", - "hoist-non-react-statics": "^3.3.0" - } - }, - "node_modules/@types/json-schema": { - "version": "7.0.11", - "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.11.tgz", - "integrity": "sha512-wOuvG1SN4Us4rez+tylwwwCV1psiNVOkJeM3AUWUNWg/jDQY2+HE/444y5gc+jBmRqASOm2Oeh5c1axHobwRKQ==" - }, - "node_modules/@types/node": { - "version": "20.2.5", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.2.5.tgz", - "integrity": "sha512-JJulVEQXmiY9Px5axXHeYGLSjhkZEnD+MDPDGbCbIAbMslkKwmygtZFy1X6s/075Yo94sf8GuSlFfPzysQrWZQ==" - }, - "node_modules/@types/prop-types": { - "version": "15.7.3", - "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.3.tgz", - "integrity": "sha512-KfRL3PuHmqQLOG+2tGpRO26Ctg+Cq1E01D2DMriKEATHgWLfeNDmq9e29Q9WIky0dQ3NPkd1mzYH8Lm936Z9qw==" - }, - "node_modules/@types/react": { - "version": "18.2.9", - "resolved": "https://registry.npmjs.org/@types/react/-/react-18.2.9.tgz", - "integrity": "sha512-pL3JAesUkF7PEQGxh5XOwdXGV907te6m1/Qe1ERJLgomojS6Ne790QiA7GUl434JEkFA2aAaB6qJ5z4e1zJn/w==", - "dependencies": { - "@types/prop-types": "*", - "@types/scheduler": "*", - "csstype": "^3.0.2" - } - }, - "node_modules/@types/react-dom": { - "version": "18.2.4", - "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.2.4.tgz", - "integrity": "sha512-G2mHoTMTL4yoydITgOGwWdWMVd8sNgyEP85xVmMKAPUBwQWm9wBPQUmvbeF4V3WBY1P7mmL4BkjQ0SqUpf1snw==", - "dependencies": { - "@types/react": "*" - } - }, - "node_modules/@types/scheduler": { - "version": "0.16.1", - "resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.1.tgz", - "integrity": "sha512-EaCxbanVeyxDRTQBkdLb3Bvl/HK7PBK6UJjsSixB0iHKoWxE5uu2Q/DgtpOhPIojN0Zl1whvOd7PoHs2P0s5eA==" - }, - "node_modules/@types/styled-components": { - "version": "5.1.26", - "resolved": "https://registry.npmjs.org/@types/styled-components/-/styled-components-5.1.26.tgz", - "integrity": "sha512-KuKJ9Z6xb93uJiIyxo/+ksS7yLjS1KzG6iv5i78dhVg/X3u5t1H7juRWqVmodIdz6wGVaIApo1u01kmFRdJHVw==", - "dependencies": { - "@types/hoist-non-react-statics": "*", - "@types/react": "*", - "csstype": "^3.0.2" - } - }, - "node_modules/@types/wicg-file-system-access": { - "version": "2020.9.6", - "resolved": "https://registry.npmjs.org/@types/wicg-file-system-access/-/wicg-file-system-access-2020.9.6.tgz", - "integrity": "sha512-6hogE75Hl2Ov/jgp8ZhDaGmIF/q3J07GtXf8nCJCwKTHq7971po5+DId7grft09zG7plBwpF6ZU0yx9Du4/e1A==" - }, - "node_modules/@webassemblyjs/ast": { - "version": "1.11.6", - "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.11.6.tgz", - "integrity": "sha512-IN1xI7PwOvLPgjcf180gC1bqn3q/QaOCwYUahIOhbYUu8KA/3tw2RT/T0Gidi1l7Hhj5D/INhJxiICObqpMu4Q==", - "dependencies": { - "@webassemblyjs/helper-numbers": "1.11.6", - "@webassemblyjs/helper-wasm-bytecode": "1.11.6" - } - }, - "node_modules/@webassemblyjs/floating-point-hex-parser": { - "version": "1.11.6", - "resolved": "https://registry.npmjs.org/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.11.6.tgz", - "integrity": "sha512-ejAj9hfRJ2XMsNHk/v6Fu2dGS+i4UaXBXGemOfQ/JfQ6mdQg/WXtwleQRLLS4OvfDhv8rYnVwH27YJLMyYsxhw==" - }, - "node_modules/@webassemblyjs/helper-api-error": { - "version": "1.11.6", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-api-error/-/helper-api-error-1.11.6.tgz", - "integrity": "sha512-o0YkoP4pVu4rN8aTJgAyj9hC2Sv5UlkzCHhxqWj8butaLvnpdc2jOwh4ewE6CX0txSfLn/UYaV/pheS2Txg//Q==" - }, - "node_modules/@webassemblyjs/helper-buffer": { - "version": "1.11.6", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.11.6.tgz", - "integrity": "sha512-z3nFzdcp1mb8nEOFFk8DrYLpHvhKC3grJD2ardfKOzmbmJvEf/tPIqCY+sNcwZIY8ZD7IkB2l7/pqhUhqm7hLA==" - }, - "node_modules/@webassemblyjs/helper-numbers": { - "version": "1.11.6", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-numbers/-/helper-numbers-1.11.6.tgz", - "integrity": "sha512-vUIhZ8LZoIWHBohiEObxVm6hwP034jwmc9kuq5GdHZH0wiLVLIPcMCdpJzG4C11cHoQ25TFIQj9kaVADVX7N3g==", - "dependencies": { - "@webassemblyjs/floating-point-hex-parser": "1.11.6", - "@webassemblyjs/helper-api-error": "1.11.6", - "@xtuc/long": "4.2.2" - } - }, - "node_modules/@webassemblyjs/helper-wasm-bytecode": { - "version": "1.11.6", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.11.6.tgz", - "integrity": "sha512-sFFHKwcmBprO9e7Icf0+gddyWYDViL8bpPjJJl0WHxCdETktXdmtWLGVzoHbqUcY4Be1LkNfwTmXOJUFZYSJdA==" - }, - "node_modules/@webassemblyjs/helper-wasm-section": { - "version": "1.11.6", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.11.6.tgz", - "integrity": "sha512-LPpZbSOwTpEC2cgn4hTydySy1Ke+XEu+ETXuoyvuyezHO3Kjdu90KK95Sh9xTbmjrCsUwvWwCOQQNta37VrS9g==", - "dependencies": { - "@webassemblyjs/ast": "1.11.6", - "@webassemblyjs/helper-buffer": "1.11.6", - "@webassemblyjs/helper-wasm-bytecode": "1.11.6", - "@webassemblyjs/wasm-gen": "1.11.6" - } - }, - "node_modules/@webassemblyjs/ieee754": { - "version": "1.11.6", - "resolved": "https://registry.npmjs.org/@webassemblyjs/ieee754/-/ieee754-1.11.6.tgz", - "integrity": "sha512-LM4p2csPNvbij6U1f19v6WR56QZ8JcHg3QIJTlSwzFcmx6WSORicYj6I63f9yU1kEUtrpG+kjkiIAkevHpDXrg==", - "dependencies": { - "@xtuc/ieee754": "^1.2.0" - } - }, - "node_modules/@webassemblyjs/leb128": { - "version": "1.11.6", - "resolved": "https://registry.npmjs.org/@webassemblyjs/leb128/-/leb128-1.11.6.tgz", - "integrity": "sha512-m7a0FhE67DQXgouf1tbN5XQcdWoNgaAuoULHIfGFIEVKA6tu/edls6XnIlkmS6FrXAquJRPni3ZZKjw6FSPjPQ==", - "dependencies": { - "@xtuc/long": "4.2.2" - } - }, - "node_modules/@webassemblyjs/utf8": { - "version": "1.11.6", - "resolved": "https://registry.npmjs.org/@webassemblyjs/utf8/-/utf8-1.11.6.tgz", - "integrity": "sha512-vtXf2wTQ3+up9Zsg8sa2yWiQpzSsMyXj0qViVP6xKGCUT8p8YJ6HqI7l5eCnWx1T/FYdsv07HQs2wTFbbof/RA==" - }, - "node_modules/@webassemblyjs/wasm-edit": { - "version": "1.11.6", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.11.6.tgz", - "integrity": "sha512-Ybn2I6fnfIGuCR+Faaz7YcvtBKxvoLV3Lebn1tM4o/IAJzmi9AWYIPWpyBfU8cC+JxAO57bk4+zdsTjJR+VTOw==", - "dependencies": { - "@webassemblyjs/ast": "1.11.6", - "@webassemblyjs/helper-buffer": "1.11.6", - "@webassemblyjs/helper-wasm-bytecode": "1.11.6", - "@webassemblyjs/helper-wasm-section": "1.11.6", - "@webassemblyjs/wasm-gen": "1.11.6", - "@webassemblyjs/wasm-opt": "1.11.6", - "@webassemblyjs/wasm-parser": "1.11.6", - "@webassemblyjs/wast-printer": "1.11.6" - } - }, - "node_modules/@webassemblyjs/wasm-gen": { - "version": "1.11.6", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.11.6.tgz", - "integrity": "sha512-3XOqkZP/y6B4F0PBAXvI1/bky7GryoogUtfwExeP/v7Nzwo1QLcq5oQmpKlftZLbT+ERUOAZVQjuNVak6UXjPA==", - "dependencies": { - "@webassemblyjs/ast": "1.11.6", - "@webassemblyjs/helper-wasm-bytecode": "1.11.6", - "@webassemblyjs/ieee754": "1.11.6", - "@webassemblyjs/leb128": "1.11.6", - "@webassemblyjs/utf8": "1.11.6" - } - }, - "node_modules/@webassemblyjs/wasm-opt": { - "version": "1.11.6", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.11.6.tgz", - "integrity": "sha512-cOrKuLRE7PCe6AsOVl7WasYf3wbSo4CeOk6PkrjS7g57MFfVUF9u6ysQBBODX0LdgSvQqRiGz3CXvIDKcPNy4g==", - "dependencies": { - "@webassemblyjs/ast": "1.11.6", - "@webassemblyjs/helper-buffer": "1.11.6", - "@webassemblyjs/wasm-gen": "1.11.6", - "@webassemblyjs/wasm-parser": "1.11.6" - } - }, - "node_modules/@webassemblyjs/wasm-parser": { - "version": "1.11.6", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.11.6.tgz", - "integrity": "sha512-6ZwPeGzMJM3Dqp3hCsLgESxBGtT/OeCvCZ4TA1JUPYgmhAx38tTPR9JaKy0S5H3evQpO/h2uWs2j6Yc/fjkpTQ==", - "dependencies": { - "@webassemblyjs/ast": "1.11.6", - "@webassemblyjs/helper-api-error": "1.11.6", - "@webassemblyjs/helper-wasm-bytecode": "1.11.6", - "@webassemblyjs/ieee754": "1.11.6", - "@webassemblyjs/leb128": "1.11.6", - "@webassemblyjs/utf8": "1.11.6" - } - }, - "node_modules/@webassemblyjs/wast-printer": { - "version": "1.11.6", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.11.6.tgz", - "integrity": "sha512-JM7AhRcE+yW2GWYaKeHL5vt4xqee5N2WcezptmgyhNS+ScggqcT1OtXykhAb13Sn5Yas0j2uv9tHgrjwvzAP4A==", - "dependencies": { - "@webassemblyjs/ast": "1.11.6", - "@xtuc/long": "4.2.2" - } - }, - "node_modules/@webpack-cli/configtest": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/@webpack-cli/configtest/-/configtest-2.1.1.tgz", - "integrity": "sha512-wy0mglZpDSiSS0XHrVR+BAdId2+yxPSoJW8fsna3ZpYSlufjvxnP4YbKTCBZnNIcGN4r6ZPXV55X4mYExOfLmw==", - "engines": { - "node": ">=14.15.0" - }, - "peerDependencies": { - "webpack": "5.x.x", - "webpack-cli": "5.x.x" - } - }, - "node_modules/@webpack-cli/info": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/@webpack-cli/info/-/info-2.0.2.tgz", - "integrity": "sha512-zLHQdI/Qs1UyT5UBdWNqsARasIA+AaF8t+4u2aS2nEpBQh2mWIVb8qAklq0eUENnC5mOItrIB4LiS9xMtph18A==", - "engines": { - "node": ">=14.15.0" - }, - "peerDependencies": { - "webpack": "5.x.x", - "webpack-cli": "5.x.x" - } - }, - "node_modules/@webpack-cli/serve": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/@webpack-cli/serve/-/serve-2.0.5.tgz", - "integrity": "sha512-lqaoKnRYBdo1UgDX8uF24AfGMifWK19TxPmM5FHc2vAGxrJ/qtyUyFBWoY1tISZdelsQ5fBcOusifo5o5wSJxQ==", - "engines": { - "node": ">=14.15.0" - }, - "peerDependencies": { - "webpack": "5.x.x", - "webpack-cli": "5.x.x" - }, - "peerDependenciesMeta": { - "webpack-dev-server": { - "optional": true - } - } - }, - "node_modules/@xtuc/ieee754": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz", - "integrity": "sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==" - }, - "node_modules/@xtuc/long": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/@xtuc/long/-/long-4.2.2.tgz", - "integrity": "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==" - }, - "node_modules/abab": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/abab/-/abab-2.0.6.tgz", - "integrity": "sha512-j2afSsaIENvHZN2B8GOpF566vZ5WVk5opAiMTvWgaQT8DkbOqsTfvNAvHoRGU2zzP8cPoqys+xHTRDWW8L+/BA==" - }, - "node_modules/acorn": { - "version": "8.8.2", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.8.2.tgz", - "integrity": "sha512-xjIYgE8HBrkpd/sJqOGNspf8uHG+NOHGOw6a/Urj8taM2EXfdNAH2oFcPeIFfsv3+kz/mJrS5VuMqbNLjCa2vw==", - "bin": { - "acorn": "bin/acorn" - }, - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/acorn-import-assertions": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/acorn-import-assertions/-/acorn-import-assertions-1.9.0.tgz", - "integrity": "sha512-cmMwop9x+8KFhxvKrKfPYmN6/pKTYYHBqLa0DfvVZcKMJWNyWLnaqND7dx/qn66R7ewM1UX5XMaDVP5wlVTaVA==", - "peerDependencies": { - "acorn": "^8" - } - }, - "node_modules/ajv": { - "version": "8.12.0", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.12.0.tgz", - "integrity": "sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA==", - "dependencies": { - "fast-deep-equal": "^3.1.1", - "json-schema-traverse": "^1.0.0", - "require-from-string": "^2.0.2", - "uri-js": "^4.2.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, - "node_modules/ajv-formats": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.1.tgz", - "integrity": "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==", - "dependencies": { - "ajv": "^8.0.0" - }, - "peerDependencies": { - "ajv": "^8.0.0" - }, - "peerDependenciesMeta": { - "ajv": { - "optional": true - } - } - }, - "node_modules/ajv-keywords": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-5.1.0.tgz", - "integrity": "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==", - "dependencies": { - "fast-deep-equal": "^3.1.3" - }, - "peerDependencies": { - "ajv": "^8.8.2" - } - }, - "node_modules/ansi-styles": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", - "dependencies": { - "color-convert": "^1.9.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/babel-loader": { - "version": "9.1.2", - "resolved": "https://registry.npmjs.org/babel-loader/-/babel-loader-9.1.2.tgz", - "integrity": "sha512-mN14niXW43tddohGl8HPu5yfQq70iUThvFL/4QzESA7GcZoC0eVOhvWdQ8+3UlSjaDE9MVtsW9mxDY07W7VpVA==", - "dependencies": { - "find-cache-dir": "^3.3.2", - "schema-utils": "^4.0.0" - }, - "engines": { - "node": ">= 14.15.0" - }, - "peerDependencies": { - "@babel/core": "^7.12.0", - "webpack": ">=5" - } - }, - "node_modules/babel-plugin-polyfill-corejs2": { - "version": "0.4.3", - "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.4.3.tgz", - "integrity": "sha512-bM3gHc337Dta490gg+/AseNB9L4YLHxq1nGKZZSHbhXv4aTYU2MD2cjza1Ru4S6975YLTaL1K8uJf6ukJhhmtw==", - "dependencies": { - "@babel/compat-data": "^7.17.7", - "@babel/helper-define-polyfill-provider": "^0.4.0", - "semver": "^6.1.1" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/babel-plugin-polyfill-corejs3": { - "version": "0.8.1", - "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.8.1.tgz", - "integrity": "sha512-ikFrZITKg1xH6pLND8zT14UPgjKHiGLqex7rGEZCH2EvhsneJaJPemmpQaIZV5AL03II+lXylw3UmddDK8RU5Q==", - "dependencies": { - "@babel/helper-define-polyfill-provider": "^0.4.0", - "core-js-compat": "^3.30.1" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/babel-plugin-polyfill-regenerator": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.5.0.tgz", - "integrity": "sha512-hDJtKjMLVa7Z+LwnTCxoDLQj6wdc+B8dun7ayF2fYieI6OzfuvcLMB32ihJZ4UhCBwNYGl5bg/x/P9cMdnkc2g==", - "dependencies": { - "@babel/helper-define-polyfill-provider": "^0.4.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/babel-plugin-styled-components": { - "version": "2.0.7", - "resolved": "https://registry.npmjs.org/babel-plugin-styled-components/-/babel-plugin-styled-components-2.0.7.tgz", - "integrity": "sha512-i7YhvPgVqRKfoQ66toiZ06jPNA3p6ierpfUuEWxNF+fV27Uv5gxBkf8KZLHUCc1nFA9j6+80pYoIpqCeyW3/bA==", - "dependencies": { - "@babel/helper-annotate-as-pure": "^7.16.0", - "@babel/helper-module-imports": "^7.16.0", - "babel-plugin-syntax-jsx": "^6.18.0", - "lodash": "^4.17.11", - "picomatch": "^2.3.0" - }, - "peerDependencies": { - "styled-components": ">= 2" - } - }, - "node_modules/babel-plugin-syntax-jsx": { - "version": "6.18.0", - "resolved": "https://registry.npmjs.org/babel-plugin-syntax-jsx/-/babel-plugin-syntax-jsx-6.18.0.tgz", - "integrity": "sha512-qrPaCSo9c8RHNRHIotaufGbuOBN8rtdC4QrrFFc43vyWCCz7Kl7GL1PGaXtMGQZUXrkCjNEgxDfmAuAabr/rlw==" - }, - "node_modules/braces": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", - "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", - "dependencies": { - "fill-range": "^7.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/browserslist": { - "version": "4.21.7", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.21.7.tgz", - "integrity": "sha512-BauCXrQ7I2ftSqd2mvKHGo85XR0u7Ru3C/Hxsy/0TkfCtjrmAbPdzLGasmoiBxplpDXlPvdjX9u7srIMfgasNA==", - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/browserslist" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "dependencies": { - "caniuse-lite": "^1.0.30001489", - "electron-to-chromium": "^1.4.411", - "node-releases": "^2.0.12", - "update-browserslist-db": "^1.0.11" - }, - "bin": { - "browserslist": "cli.js" - }, - "engines": { - "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" - } - }, - "node_modules/buffer-from": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", - "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==" - }, - "node_modules/camelize": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/camelize/-/camelize-1.0.0.tgz", - "integrity": "sha1-FkpUg+Yw+kMh5a8HAg5TGDGyYJs=" - }, - "node_modules/caniuse-lite": { - "version": "1.0.30001497", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001497.tgz", - "integrity": "sha512-I4/duVK4wL6rAK/aKZl3HXB4g+lIZvaT4VLAn2rCgJ38jVLb0lv2Xug6QuqmxXFVRJMF74SPPWPJ/1Sdm3vCzw==", - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/caniuse-lite" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ] - }, - "node_modules/chalk": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "dependencies": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/chrome-trace-event": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.3.tgz", - "integrity": "sha512-p3KULyQg4S7NIHixdwbGX+nFHkoBiA4YQmyWtjb8XngSKV124nJmRysgAeujbUVb15vh+RvFUfCPqU7rXk+hZg==", - "engines": { - "node": ">=6.0" - } - }, - "node_modules/clone-deep": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/clone-deep/-/clone-deep-4.0.1.tgz", - "integrity": "sha512-neHB9xuzh/wk0dIHweyAXv2aPGZIVk3pLMe+/RNzINf17fe0OG96QroktYAUm7SM1PBnzTabaLboqqxDyMU+SQ==", - "dependencies": { - "is-plain-object": "^2.0.4", - "kind-of": "^6.0.2", - "shallow-clone": "^3.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/color-convert": { - "version": "1.9.3", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", - "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", - "dependencies": { - "color-name": "1.1.3" - } - }, - "node_modules/color-name": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==" - }, - "node_modules/colorette": { - "version": "2.0.19", - "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.19.tgz", - "integrity": "sha512-3tlv/dIP7FWvj3BsbHrGLJ6l/oKh1O3TcgBqMn+yyCagOxc23fyzDS6HypQbgxWbkpDnf52p1LuR4eWDQ/K9WQ==" - }, - "node_modules/commander": { - "version": "2.20.3", - "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", - "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==" - }, - "node_modules/commondir": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz", - "integrity": "sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg==" - }, - "node_modules/convert-source-map": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.7.0.tgz", - "integrity": "sha512-4FJkXzKXEDB1snCFZlLP4gpC3JILicCpGbzG9f9G7tGqGCzETQ2hWPrcinA9oU4wtf2biUaEH5065UnMeR33oA==", - "dependencies": { - "safe-buffer": "~5.1.1" - } - }, - "node_modules/core-js": { - "version": "3.30.2", - "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.30.2.tgz", - "integrity": "sha512-uBJiDmwqsbJCWHAwjrx3cvjbMXP7xD72Dmsn5LOJpiRmE3WbBbN5rCqQ2Qh6Ek6/eOrjlWngEynBWo4VxerQhg==", - "hasInstallScript": true, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/core-js" - } - }, - "node_modules/core-js-compat": { - "version": "3.30.2", - "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.30.2.tgz", - "integrity": "sha512-nriW1nuJjUgvkEjIot1Spwakz52V9YkYHZAQG6A1eCgC8AA1p0zngrQEP9R0+V6hji5XilWKG1Bd0YRppmGimA==", - "dependencies": { - "browserslist": "^4.21.5" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/core-js" - } - }, - "node_modules/cross-spawn": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", - "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", - "dependencies": { - "path-key": "^3.1.0", - "shebang-command": "^2.0.0", - "which": "^2.0.1" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/css-color-keywords": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/css-color-keywords/-/css-color-keywords-1.0.0.tgz", - "integrity": "sha1-/qJhbcZ2spYmhrOvjb2+GAskTgU=", - "engines": { - "node": ">=4" - } - }, - "node_modules/css-to-react-native": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/css-to-react-native/-/css-to-react-native-3.0.0.tgz", - "integrity": "sha512-Ro1yETZA813eoyUp2GDBhG2j+YggidUmzO1/v9eYBKR2EHVEniE2MI/NqpTQ954BMpTPZFsGNPm46qFB9dpaPQ==", - "dependencies": { - "camelize": "^1.0.0", - "css-color-keywords": "^1.0.0", - "postcss-value-parser": "^4.0.2" - } - }, - "node_modules/csstype": { - "version": "3.0.8", - "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.0.8.tgz", - "integrity": "sha512-jXKhWqXPmlUeoQnF/EhTtTl4C9SnrxSH/jZUih3jmO6lBKr99rP3/+FmrMj4EFpOXzMtXHAZkd3x0E6h6Fgflw==" - }, - "node_modules/debug": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz", - "integrity": "sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==", - "dependencies": { - "ms": "2.1.2" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/electron-to-chromium": { - "version": "1.4.427", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.427.tgz", - "integrity": "sha512-HK3r9l+Jm8dYAm1ctXEWIC+hV60zfcjS9UA5BDlYvnI5S7PU/yytjpvSrTNrSSRRkuu3tDyZhdkwIczh+0DWaw==" - }, - "node_modules/enhanced-resolve": { - "version": "5.14.1", - "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.14.1.tgz", - "integrity": "sha512-Vklwq2vDKtl0y/vtwjSesgJ5MYS7Etuk5txS8VdKL4AOS1aUlD96zqIfsOSLQsdv3xgMRbtkWM8eG9XDfKUPow==", - "dependencies": { - "graceful-fs": "^4.2.4", - "tapable": "^2.2.0" - }, - "engines": { - "node": ">=10.13.0" - } - }, - "node_modules/envinfo": { - "version": "7.8.1", - "resolved": "https://registry.npmjs.org/envinfo/-/envinfo-7.8.1.tgz", - "integrity": "sha512-/o+BXHmB7ocbHEAs6F2EnG0ogybVVUdkRunTT2glZU9XAaGmhqskrvKwqXuDfNjEO0LZKWdejEEpnq8aM0tOaw==", - "bin": { - "envinfo": "dist/cli.js" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/es-module-lexer": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.2.1.tgz", - "integrity": "sha512-9978wrXM50Y4rTMmW5kXIC09ZdXQZqkE4mxhwkd8VbzsGkXGPgV4zWuqQJgCEzYngdo2dYDa0l8xhX4fkSwJSg==" - }, - "node_modules/escalade": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", - "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==", - "engines": { - "node": ">=6" - } - }, - "node_modules/escape-string-regexp": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", - "engines": { - "node": ">=0.8.0" - } - }, - "node_modules/eslint-scope": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", - "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", - "dependencies": { - "esrecurse": "^4.3.0", - "estraverse": "^4.1.1" - }, - "engines": { - "node": ">=8.0.0" - } - }, - "node_modules/esrecurse": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", - "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", - "dependencies": { - "estraverse": "^5.2.0" - }, - "engines": { - "node": ">=4.0" - } - }, - "node_modules/esrecurse/node_modules/estraverse": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.2.0.tgz", - "integrity": "sha512-BxbNGGNm0RyRYvUdHpIwv9IWzeM9XClbOxwoATuFdOE7ZE6wHL+HQ5T8hoPM+zHvmKzzsEqhgy0GrQ5X13afiQ==", - "engines": { - "node": ">=4.0" - } - }, - "node_modules/estraverse": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", - "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", - "engines": { - "node": ">=4.0" - } - }, - "node_modules/esutils": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", - "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/events": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", - "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", - "engines": { - "node": ">=0.8.x" - } - }, - "node_modules/fast-deep-equal": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", - "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==" - }, - "node_modules/fast-json-stable-stringify": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", - "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==" - }, - "node_modules/fastest-levenshtein": { - "version": "1.0.12", - "resolved": "https://registry.npmjs.org/fastest-levenshtein/-/fastest-levenshtein-1.0.12.tgz", - "integrity": "sha512-On2N+BpYJ15xIC974QNVuYGMOlEVt4s0EOI3wwMqOmK1fdDY+FN/zltPV8vosq4ad4c/gJ1KHScUn/6AWIgiow==" - }, - "node_modules/fill-range": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", - "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", - "dependencies": { - "to-regex-range": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/find-cache-dir": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-3.3.2.tgz", - "integrity": "sha512-wXZV5emFEjrridIgED11OoUKLxiYjAcqot/NJdAkOhlJ+vGzwhOAfcG5OX1jP+S0PcjEn8bdMJv+g2jwQ3Onig==", - "dependencies": { - "commondir": "^1.0.1", - "make-dir": "^3.0.2", - "pkg-dir": "^4.1.0" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/avajs/find-cache-dir?sponsor=1" - } - }, - "node_modules/find-up": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", - "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", - "dependencies": { - "locate-path": "^5.0.0", - "path-exists": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/function-bind": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", - "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==" - }, - "node_modules/gensync": { - "version": "1.0.0-beta.2", - "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", - "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/glob-to-regexp": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz", - "integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==" - }, - "node_modules/globals": { - "version": "11.12.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", - "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", - "engines": { - "node": ">=4" - } - }, - "node_modules/graceful-fs": { - "version": "4.2.10", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.10.tgz", - "integrity": "sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA==" - }, - "node_modules/has": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", - "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", - "dependencies": { - "function-bind": "^1.1.1" - }, - "engines": { - "node": ">= 0.4.0" - } - }, - "node_modules/has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", - "engines": { - "node": ">=4" - } - }, - "node_modules/hoist-non-react-statics": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz", - "integrity": "sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==", - "dependencies": { - "react-is": "^16.7.0" - } - }, - "node_modules/hoist-non-react-statics/node_modules/react-is": { - "version": "16.13.1", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", - "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==" - }, - "node_modules/iconv-lite": { - "version": "0.6.3", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", - "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", - "dependencies": { - "safer-buffer": ">= 2.1.2 < 3.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/immutable": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/immutable/-/immutable-4.3.0.tgz", - "integrity": "sha512-0AOCmOip+xgJwEVTQj1EfiDDOkPmuyllDuTuEX+DDXUgapLAsBIfkg3sxCYyCEA8mQqZrrxPUGjcOQ2JS3WLkg==" - }, - "node_modules/import-local": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/import-local/-/import-local-3.0.2.tgz", - "integrity": "sha512-vjL3+w0oulAVZ0hBHnxa/Nm5TAurf9YLQJDhqRZyqb+VKGOB6LU8t9H1Nr5CIo16vh9XfJTOoHwU0B71S557gA==", - "dependencies": { - "pkg-dir": "^4.2.0", - "resolve-cwd": "^3.0.0" - }, - "bin": { - "import-local-fixture": "fixtures/cli.js" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/interpret": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/interpret/-/interpret-3.1.1.tgz", - "integrity": "sha512-6xwYfHbajpoF0xLW+iwLkhwgvLoZDfjYfoFNu8ftMoXINzwuymNLd9u/KmwtdT2GbR+/Cz66otEGEVVUHX9QLQ==", - "engines": { - "node": ">=10.13.0" - } - }, - "node_modules/is-core-module": { - "version": "2.12.1", - "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.12.1.tgz", - "integrity": "sha512-Q4ZuBAe2FUsKtyQJoQHlvP8OvBERxO3jEmy1I7hcRXcJBGGHFh/aJBswbXuS9sgrDH2QUO8ilkwNPHvHMd8clg==", - "dependencies": { - "has": "^1.0.3" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-number": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", - "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", - "engines": { - "node": ">=0.12.0" - } - }, - "node_modules/is-plain-object": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", - "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==", - "dependencies": { - "isobject": "^3.0.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/isexe": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==" - }, - "node_modules/isobject": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", - "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/jest-worker": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-27.5.1.tgz", - "integrity": "sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==", - "dependencies": { - "@types/node": "*", - "merge-stream": "^2.0.0", - "supports-color": "^8.0.0" - }, - "engines": { - "node": ">= 10.13.0" - } - }, - "node_modules/jest-worker/node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "engines": { - "node": ">=8" - } - }, - "node_modules/jest-worker/node_modules/supports-color": { - "version": "8.1.1", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", - "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/supports-color?sponsor=1" - } - }, - "node_modules/js-tokens": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", - "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==" - }, - "node_modules/jsesc": { - "version": "2.5.2", - "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", - "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==", - "bin": { - "jsesc": "bin/jsesc" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/json-parse-even-better-errors": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", - "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==" - }, - "node_modules/json-schema-traverse": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", - "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==" - }, - "node_modules/json5": { - "version": "2.2.3", - "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", - "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", - "bin": { - "json5": "lib/cli.js" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/kind-of": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", - "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/loader-runner": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.2.0.tgz", - "integrity": "sha512-92+huvxMvYlMzMt0iIOukcwYBFpkYJdpl2xsZ7LrlayO7E8SOv+JJUEK17B/dJIHAOLMfh2dZZ/Y18WgmGtYNw==", - "engines": { - "node": ">=6.11.5" - } - }, - "node_modules/locate-path": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", - "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", - "dependencies": { - "p-locate": "^4.1.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/lodash": { - "version": "4.17.21", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", - "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" - }, - "node_modules/lodash.debounce": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz", - "integrity": "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==" - }, - "node_modules/loose-envify": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", - "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", - "dependencies": { - "js-tokens": "^3.0.0 || ^4.0.0" - }, - "bin": { - "loose-envify": "cli.js" - } - }, - "node_modules/lru-cache": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", - "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/make-dir": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", - "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", - "dependencies": { - "semver": "^6.0.0" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/merge-stream": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", - "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==" - }, - "node_modules/micromatch": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.4.tgz", - "integrity": "sha512-pRmzw/XUcwXGpD9aI9q/0XOwLNygjETJ8y0ao0wdqprrzDa4YnxLcz7fQRZr8voh8V10kGhABbNcHVk5wHgWwg==", - "dependencies": { - "braces": "^3.0.1", - "picomatch": "^2.2.3" - }, - "engines": { - "node": ">=8.6" - } - }, - "node_modules/mime-db": { - "version": "1.48.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.48.0.tgz", - "integrity": "sha512-FM3QwxV+TnZYQ2aRqhlKBMHxk10lTbMt3bBkMAp54ddrNeVSfcQYOOKuGuy3Ddrm38I04If834fOUSq1yzslJQ==", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/mime-types": { - "version": "2.1.31", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.31.tgz", - "integrity": "sha512-XGZnNzm3QvgKxa8dpzyhFTHmpP3l5YNusmne07VUOXxou9CqUqYa/HBy124RqtVh/O2pECas/MOcsDgpilPOPg==", - "dependencies": { - "mime-db": "1.48.0" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" - }, - "node_modules/neo-async": { - "version": "2.6.2", - "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", - "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==" - }, - "node_modules/node-releases": { - "version": "2.0.12", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.12.tgz", - "integrity": "sha512-QzsYKWhXTWx8h1kIvqfnC++o0pEmpRQA/aenALsL2F4pqNVr7YzcdMlDij5WBnwftRbJCNJL/O7zdKaxKPHqgQ==" - }, - "node_modules/p-limit": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", - "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", - "dependencies": { - "p-try": "^2.0.0" - }, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/p-locate": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", - "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", - "dependencies": { - "p-limit": "^2.2.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/p-try": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", - "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", - "engines": { - "node": ">=6" - } - }, - "node_modules/pako": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/pako/-/pako-2.1.0.tgz", - "integrity": "sha512-w+eufiZ1WuJYgPXbV/PO3NCMEc3xqylkKHzp8bxp1uW4qaSNQUkwmLLEc3kKsfz8lpV1F8Ht3U1Cm+9Srog2ug==" - }, - "node_modules/path-exists": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", - "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", - "engines": { - "node": ">=8" - } - }, - "node_modules/path-key": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", - "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", - "engines": { - "node": ">=8" - } - }, - "node_modules/path-parse": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", - "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==" - }, - "node_modules/picocolors": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", - "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==" - }, - "node_modules/picomatch": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.0.tgz", - "integrity": "sha512-lY1Q/PiJGC2zOv/z391WOTD+Z02bCgsFfvxoXXf6h7kv9o+WmsmzYqrAwY63sNgOxE4xEdq0WyUnXfKeBrSvYw==", - "engines": { - "node": ">=8.6" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, - "node_modules/pkg-dir": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", - "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", - "dependencies": { - "find-up": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/postcss-value-parser": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.1.0.tgz", - "integrity": "sha512-97DXOFbQJhk71ne5/Mt6cOu6yxsSfM0QGQyl0L25Gca4yGWEGJaig7l7gbCX623VqTBNGLRLaVUCnNkcedlRSQ==" - }, - "node_modules/punycode": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.0.tgz", - "integrity": "sha512-rRV+zQD8tVFys26lAGR9WUuS4iUAngJScM+ZRSKtvl5tKeZ2t5bvdNFdNHBW9FWR4guGHlgmsZ1G7BSm2wTbuA==", - "engines": { - "node": ">=6" - } - }, - "node_modules/randombytes": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", - "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", - "dependencies": { - "safe-buffer": "^5.1.0" - } - }, - "node_modules/react": { - "version": "18.2.0", - "resolved": "https://registry.npmjs.org/react/-/react-18.2.0.tgz", - "integrity": "sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ==", - "dependencies": { - "loose-envify": "^1.1.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/react-dom": { - "version": "18.2.0", - "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.2.0.tgz", - "integrity": "sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g==", - "dependencies": { - "loose-envify": "^1.1.0", - "scheduler": "^0.23.0" - }, - "peerDependencies": { - "react": "^18.2.0" - } - }, - "node_modules/react-is": { - "version": "17.0.2", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", - "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==", - "peer": true - }, - "node_modules/rechoir": { - "version": "0.8.0", - "resolved": "https://registry.npmjs.org/rechoir/-/rechoir-0.8.0.tgz", - "integrity": "sha512-/vxpCXddiX8NGfGO/mTafwjq4aFa/71pvamip0++IQk3zG8cbCj0fifNPrjjF1XMXUne91jL9OoxmdykoEtifQ==", - "dependencies": { - "resolve": "^1.20.0" - }, - "engines": { - "node": ">= 10.13.0" - } - }, - "node_modules/regenerate": { - "version": "1.4.2", - "resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.4.2.tgz", - "integrity": "sha512-zrceR/XhGYU/d/opr2EKO7aRHUeiBI8qjtfHqADTwZd6Szfy16la6kqD0MIUs5z5hx6AaKa+PixpPrR289+I0A==" - }, - "node_modules/regenerate-unicode-properties": { - "version": "10.1.0", - "resolved": "https://registry.npmjs.org/regenerate-unicode-properties/-/regenerate-unicode-properties-10.1.0.tgz", - "integrity": "sha512-d1VudCLoIGitcU/hEg2QqvyGZQmdC0Lf8BqdOMXGFSvJP4bNV1+XqbPQeHHLD51Jh4QJJ225dlIFvY4Ly6MXmQ==", - "dependencies": { - "regenerate": "^1.4.2" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/regenerator-runtime": { - "version": "0.13.11", - "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz", - "integrity": "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==" - }, - "node_modules/regenerator-transform": { - "version": "0.15.1", - "resolved": "https://registry.npmjs.org/regenerator-transform/-/regenerator-transform-0.15.1.tgz", - "integrity": "sha512-knzmNAcuyxV+gQCufkYcvOqX/qIIfHLv0u5x79kRxuGojfYVky1f15TzZEu2Avte8QGepvUNTnLskf8E6X6Vyg==", - "dependencies": { - "@babel/runtime": "^7.8.4" - } - }, - "node_modules/regexpu-core": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/regexpu-core/-/regexpu-core-5.3.2.tgz", - "integrity": "sha512-RAM5FlZz+Lhmo7db9L298p2vHP5ZywrVXmVXpmAD9GuL5MPH6t9ROw1iA/wfHkQ76Qe7AaPF0nGuim96/IrQMQ==", - "dependencies": { - "@babel/regjsgen": "^0.8.0", - "regenerate": "^1.4.2", - "regenerate-unicode-properties": "^10.1.0", - "regjsparser": "^0.9.1", - "unicode-match-property-ecmascript": "^2.0.0", - "unicode-match-property-value-ecmascript": "^2.1.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/regjsparser": { - "version": "0.9.1", - "resolved": "https://registry.npmjs.org/regjsparser/-/regjsparser-0.9.1.tgz", - "integrity": "sha512-dQUtn90WanSNl+7mQKcXAgZxvUe7Z0SqXlgzv0za4LwiUhyzBC58yQO3liFoUgu8GiJVInAhJjkj1N0EtQ5nkQ==", - "dependencies": { - "jsesc": "~0.5.0" - }, - "bin": { - "regjsparser": "bin/parser" - } - }, - "node_modules/regjsparser/node_modules/jsesc": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-0.5.0.tgz", - "integrity": "sha512-uZz5UnB7u4T9LvwmFqXii7pZSouaRPorGs5who1Ip7VO0wxanFvBL7GkM6dTHlgX+jhBApRetaWpnDabOeTcnA==", - "bin": { - "jsesc": "bin/jsesc" - } - }, - "node_modules/require-from-string": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", - "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/resolve": { - "version": "1.22.2", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.2.tgz", - "integrity": "sha512-Sb+mjNHOULsBv818T40qSPeRiuWLyaGMa5ewydRLFimneixmVy2zdivRl+AF6jaYPC8ERxGDmFSiqui6SfPd+g==", - "dependencies": { - "is-core-module": "^2.11.0", - "path-parse": "^1.0.7", - "supports-preserve-symlinks-flag": "^1.0.0" - }, - "bin": { - "resolve": "bin/resolve" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/resolve-cwd": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-3.0.0.tgz", - "integrity": "sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==", - "dependencies": { - "resolve-from": "^5.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/resolve-from": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", - "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", - "engines": { - "node": ">=8" - } - }, - "node_modules/safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" - }, - "node_modules/safer-buffer": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", - "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" - }, - "node_modules/scheduler": { - "version": "0.23.0", - "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.0.tgz", - "integrity": "sha512-CtuThmgHNg7zIZWAXi3AsyIzA3n4xx7aNyjwC2VJldO2LMVDhFK+63xGqq6CsJH4rTAt6/M+N4GhZiDYPx9eUw==", - "dependencies": { - "loose-envify": "^1.1.0" - } - }, - "node_modules/schema-utils": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.1.0.tgz", - "integrity": "sha512-Jw+GZVbP5IggB2WAn6UHI02LBwGmsIeYN/lNbSMZyDziQ7jmtAUrqKqDja+W89YHVs+KL/3IkIMltAklqB1vAw==", - "dependencies": { - "@types/json-schema": "^7.0.9", - "ajv": "^8.9.0", - "ajv-formats": "^2.1.1", - "ajv-keywords": "^5.1.0" - }, - "engines": { - "node": ">= 12.13.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - } - }, - "node_modules/semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", - "bin": { - "semver": "bin/semver.js" - } - }, - "node_modules/serialize-javascript": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.1.tgz", - "integrity": "sha512-owoXEFjWRllis8/M1Q+Cw5k8ZH40e3zhp/ovX+Xr/vi1qj6QesbyXXViFbpNvWvPNAD62SutwEXavefrLJWj7w==", - "dependencies": { - "randombytes": "^2.1.0" - } - }, - "node_modules/shallow-clone": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/shallow-clone/-/shallow-clone-3.0.1.tgz", - "integrity": "sha512-/6KqX+GVUdqPuPPd2LxDDxzX6CAbjJehAAOKlNpqqUpAqPM6HeL8f+o3a+JsyGjn2lv0WY8UsTgUJjU9Ok55NA==", - "dependencies": { - "kind-of": "^6.0.2" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/shallowequal": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/shallowequal/-/shallowequal-1.1.0.tgz", - "integrity": "sha512-y0m1JoUZSlPAjXVtPPW70aZWfIL/dSP7AFkRnniLCrK/8MDKog3TySTBmckD+RObVxH0v4Tox67+F14PdED2oQ==" - }, - "node_modules/shebang-command": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", - "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", - "dependencies": { - "shebang-regex": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/shebang-regex": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", - "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", - "engines": { - "node": ">=8" - } - }, - "node_modules/source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/source-map-js": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz", - "integrity": "sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/source-map-loader": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/source-map-loader/-/source-map-loader-4.0.1.tgz", - "integrity": "sha512-oqXpzDIByKONVY8g1NUPOTQhe0UTU5bWUl32GSkqK2LjJj0HmwTMVKxcUip0RgAYhY1mqgOxjbQM48a0mmeNfA==", - "dependencies": { - "abab": "^2.0.6", - "iconv-lite": "^0.6.3", - "source-map-js": "^1.0.2" - }, - "engines": { - "node": ">= 14.15.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - }, - "peerDependencies": { - "webpack": "^5.72.1" - } - }, - "node_modules/source-map-support": { - "version": "0.5.21", - "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", - "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", - "dependencies": { - "buffer-from": "^1.0.0", - "source-map": "^0.6.0" - } - }, - "node_modules/styled-components": { - "version": "5.3.11", - "resolved": "https://registry.npmjs.org/styled-components/-/styled-components-5.3.11.tgz", - "integrity": "sha512-uuzIIfnVkagcVHv9nE0VPlHPSCmXIUGKfJ42LNjxCCTDTL5sgnJ8Z7GZBq0EnLYGln77tPpEpExt2+qa+cZqSw==", - "dependencies": { - "@babel/helper-module-imports": "^7.0.0", - "@babel/traverse": "^7.4.5", - "@emotion/is-prop-valid": "^1.1.0", - "@emotion/stylis": "^0.8.4", - "@emotion/unitless": "^0.7.4", - "babel-plugin-styled-components": ">= 1.12.0", - "css-to-react-native": "^3.0.0", - "hoist-non-react-statics": "^3.0.0", - "shallowequal": "^1.1.0", - "supports-color": "^5.5.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/styled-components" - }, - "peerDependencies": { - "react": ">= 16.8.0", - "react-dom": ">= 16.8.0", - "react-is": ">= 16.8.0" - } - }, - "node_modules/supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "dependencies": { - "has-flag": "^3.0.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/supports-preserve-symlinks-flag": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", - "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/tapable": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.1.tgz", - "integrity": "sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==", - "engines": { - "node": ">=6" - } - }, - "node_modules/terser": { - "version": "5.17.7", - "resolved": "https://registry.npmjs.org/terser/-/terser-5.17.7.tgz", - "integrity": "sha512-/bi0Zm2C6VAexlGgLlVxA0P2lru/sdLyfCVaRMfKVo9nWxbmz7f/sD8VPybPeSUJaJcwmCJis9pBIhcVcG1QcQ==", - "dependencies": { - "@jridgewell/source-map": "^0.3.3", - "acorn": "^8.8.2", - "commander": "^2.20.0", - "source-map-support": "~0.5.20" - }, - "bin": { - "terser": "bin/terser" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/terser-webpack-plugin": { - "version": "5.3.9", - "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.9.tgz", - "integrity": "sha512-ZuXsqE07EcggTWQjXUj+Aot/OMcD0bMKGgF63f7UxYcu5/AJF53aIpK1YoP5xR9l6s/Hy2b+t1AM0bLNPRuhwA==", - "dependencies": { - "@jridgewell/trace-mapping": "^0.3.17", - "jest-worker": "^27.4.5", - "schema-utils": "^3.1.1", - "serialize-javascript": "^6.0.1", - "terser": "^5.16.8" - }, - "engines": { - "node": ">= 10.13.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - }, - "peerDependencies": { - "webpack": "^5.1.0" - }, - "peerDependenciesMeta": { - "@swc/core": { - "optional": true - }, - "esbuild": { - "optional": true - }, - "uglify-js": { - "optional": true - } - } - }, - "node_modules/terser-webpack-plugin/node_modules/ajv": { - "version": "6.12.6", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", - "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", - "dependencies": { - "fast-deep-equal": "^3.1.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, - "node_modules/terser-webpack-plugin/node_modules/ajv-keywords": { - "version": "3.5.2", - "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", - "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==", - "peerDependencies": { - "ajv": "^6.9.1" - } - }, - "node_modules/terser-webpack-plugin/node_modules/json-schema-traverse": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==" - }, - "node_modules/terser-webpack-plugin/node_modules/schema-utils": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.2.0.tgz", - "integrity": "sha512-0zTyLGyDJYd/MBxG1AhJkKa6fpEBds4OQO2ut0w7OYG+ZGhGea09lijvzsqegYSik88zc7cUtIlnnO+/BvD6gQ==", - "dependencies": { - "@types/json-schema": "^7.0.8", - "ajv": "^6.12.5", - "ajv-keywords": "^3.5.2" - }, - "engines": { - "node": ">= 10.13.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - } - }, - "node_modules/to-fast-properties": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", - "integrity": "sha1-3F5pjL0HkmW8c+A3doGk5Og/YW4=", - "engines": { - "node": ">=4" - } - }, - "node_modules/to-regex-range": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", - "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", - "dependencies": { - "is-number": "^7.0.0" - }, - "engines": { - "node": ">=8.0" - } - }, - "node_modules/ts-loader": { - "version": "9.4.3", - "resolved": "https://registry.npmjs.org/ts-loader/-/ts-loader-9.4.3.tgz", - "integrity": "sha512-n3hBnm6ozJYzwiwt5YRiJZkzktftRpMiBApHaJPoWLA+qetQBAXkHqCLM6nwSdRDimqVtA5ocIkcTRLMTt7yzA==", - "dependencies": { - "chalk": "^4.1.0", - "enhanced-resolve": "^5.0.0", - "micromatch": "^4.0.0", - "semver": "^7.3.4" - }, - "engines": { - "node": ">=12.0.0" - }, - "peerDependencies": { - "typescript": "*", - "webpack": "^5.0.0" - } - }, - "node_modules/ts-loader/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/ts-loader/node_modules/chalk": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.1.tgz", - "integrity": "sha512-diHzdDKxcU+bAsUboHLPEDQiw0qEe0qd7SYUn3HgcFlWgbDcfLGswOHYeGrHKzG9z6UYf01d9VFMfZxPM1xZSg==", - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/ts-loader/node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/ts-loader/node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" - }, - "node_modules/ts-loader/node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "engines": { - "node": ">=8" - } - }, - "node_modules/ts-loader/node_modules/semver": { - "version": "7.3.5", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.5.tgz", - "integrity": "sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ==", - "dependencies": { - "lru-cache": "^6.0.0" - }, - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/ts-loader/node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/typescript": { - "version": "5.1.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.1.3.tgz", - "integrity": "sha512-XH627E9vkeqhlZFQuL+UsyAXEnibT0kWR2FWONlr4sTjvxyJYnyefgrkyECLzM5NenmKzRAy2rR/OlYLA1HkZw==", - "bin": { - "tsc": "bin/tsc", - "tsserver": "bin/tsserver" - }, - "engines": { - "node": ">=14.17" - } - }, - "node_modules/unicode-canonical-property-names-ecmascript": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-2.0.0.tgz", - "integrity": "sha512-yY5PpDlfVIU5+y/BSCxAJRBIS1Zc2dDG3Ujq+sR0U+JjUevW2JhocOF+soROYDSaAezOzOKuyyixhD6mBknSmQ==", - "engines": { - "node": ">=4" - } - }, - "node_modules/unicode-match-property-ecmascript": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/unicode-match-property-ecmascript/-/unicode-match-property-ecmascript-2.0.0.tgz", - "integrity": "sha512-5kaZCrbp5mmbz5ulBkDkbY0SsPOjKqVS35VpL9ulMPfSl0J0Xsm+9Evphv9CoIZFwre7aJoa94AY6seMKGVN5Q==", - "dependencies": { - "unicode-canonical-property-names-ecmascript": "^2.0.0", - "unicode-property-aliases-ecmascript": "^2.0.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/unicode-match-property-value-ecmascript": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/unicode-match-property-value-ecmascript/-/unicode-match-property-value-ecmascript-2.1.0.tgz", - "integrity": "sha512-qxkjQt6qjg/mYscYMC0XKRn3Rh0wFPlfxB0xkt9CfyTvpX1Ra0+rAmdX2QyAobptSEvuy4RtpPRui6XkV+8wjA==", - "engines": { - "node": ">=4" - } - }, - "node_modules/unicode-property-aliases-ecmascript": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/unicode-property-aliases-ecmascript/-/unicode-property-aliases-ecmascript-2.1.0.tgz", - "integrity": "sha512-6t3foTQI9qne+OZoVQB/8x8rk2k1eVy1gRXhV3oFQ5T6R1dqQ1xtin3XqSlx3+ATBkliTaR/hHyJBm+LVPNM8w==", - "engines": { - "node": ">=4" - } - }, - "node_modules/update-browserslist-db": { - "version": "1.0.11", - "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.11.tgz", - "integrity": "sha512-dCwEFf0/oT85M1fHBg4F0jtLwJrutGoHSQXCh7u4o2t1drG+c0a9Flnqww6XUKSfQMPpJBRjU8d4RXB09qtvaA==", - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/browserslist" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "dependencies": { - "escalade": "^3.1.1", - "picocolors": "^1.0.0" - }, - "bin": { - "update-browserslist-db": "cli.js" - }, - "peerDependencies": { - "browserslist": ">= 4.21.0" - } - }, - "node_modules/uri-js": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", - "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", - "dependencies": { - "punycode": "^2.1.0" - } - }, - "node_modules/watchpack": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.0.tgz", - "integrity": "sha512-Lcvm7MGST/4fup+ifyKi2hjyIAwcdI4HRgtvTpIUxBRhB+RFtUh8XtDOxUfctVCnhVi+QQj49i91OyvzkJl6cg==", - "dependencies": { - "glob-to-regexp": "^0.4.1", - "graceful-fs": "^4.1.2" - }, - "engines": { - "node": ">=10.13.0" - } - }, - "node_modules/webpack": { - "version": "5.86.0", - "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.86.0.tgz", - "integrity": "sha512-3BOvworZ8SO/D4GVP+GoRC3fVeg5MO4vzmq8TJJEkdmopxyazGDxN8ClqN12uzrZW9Tv8EED8v5VSb6Sqyi0pg==", - "dependencies": { - "@types/eslint-scope": "^3.7.3", - "@types/estree": "^1.0.0", - "@webassemblyjs/ast": "^1.11.5", - "@webassemblyjs/wasm-edit": "^1.11.5", - "@webassemblyjs/wasm-parser": "^1.11.5", - "acorn": "^8.7.1", - "acorn-import-assertions": "^1.9.0", - "browserslist": "^4.14.5", - "chrome-trace-event": "^1.0.2", - "enhanced-resolve": "^5.14.1", - "es-module-lexer": "^1.2.1", - "eslint-scope": "5.1.1", - "events": "^3.2.0", - "glob-to-regexp": "^0.4.1", - "graceful-fs": "^4.2.9", - "json-parse-even-better-errors": "^2.3.1", - "loader-runner": "^4.2.0", - "mime-types": "^2.1.27", - "neo-async": "^2.6.2", - "schema-utils": "^3.1.2", - "tapable": "^2.1.1", - "terser-webpack-plugin": "^5.3.7", - "watchpack": "^2.4.0", - "webpack-sources": "^3.2.3" - }, - "bin": { - "webpack": "bin/webpack.js" - }, - "engines": { - "node": ">=10.13.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - }, - "peerDependenciesMeta": { - "webpack-cli": { - "optional": true - } - } - }, - "node_modules/webpack-cli": { - "version": "5.1.4", - "resolved": "https://registry.npmjs.org/webpack-cli/-/webpack-cli-5.1.4.tgz", - "integrity": "sha512-pIDJHIEI9LR0yxHXQ+Qh95k2EvXpWzZ5l+d+jIo+RdSm9MiHfzazIxwwni/p7+x4eJZuvG1AJwgC4TNQ7NRgsg==", - "dependencies": { - "@discoveryjs/json-ext": "^0.5.0", - "@webpack-cli/configtest": "^2.1.1", - "@webpack-cli/info": "^2.0.2", - "@webpack-cli/serve": "^2.0.5", - "colorette": "^2.0.14", - "commander": "^10.0.1", - "cross-spawn": "^7.0.3", - "envinfo": "^7.7.3", - "fastest-levenshtein": "^1.0.12", - "import-local": "^3.0.2", - "interpret": "^3.1.1", - "rechoir": "^0.8.0", - "webpack-merge": "^5.7.3" - }, - "bin": { - "webpack-cli": "bin/cli.js" - }, - "engines": { - "node": ">=14.15.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - }, - "peerDependencies": { - "webpack": "5.x.x" - }, - "peerDependenciesMeta": { - "@webpack-cli/generators": { - "optional": true - }, - "webpack-bundle-analyzer": { - "optional": true - }, - "webpack-dev-server": { - "optional": true - } - } - }, - "node_modules/webpack-cli/node_modules/commander": { - "version": "10.0.1", - "resolved": "https://registry.npmjs.org/commander/-/commander-10.0.1.tgz", - "integrity": "sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==", - "engines": { - "node": ">=14" - } - }, - "node_modules/webpack-merge": { - "version": "5.7.3", - "resolved": "https://registry.npmjs.org/webpack-merge/-/webpack-merge-5.7.3.tgz", - "integrity": "sha512-6/JUQv0ELQ1igjGDzHkXbVDRxkfA57Zw7PfiupdLFJYrgFqY5ZP8xxbpp2lU3EPwYx89ht5Z/aDkD40hFCm5AA==", - "dependencies": { - "clone-deep": "^4.0.1", - "wildcard": "^2.0.0" - }, - "engines": { - "node": ">=10.0.0" - } - }, - "node_modules/webpack-sources": { - "version": "3.2.3", - "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.2.3.tgz", - "integrity": "sha512-/DyMEOrDgLKKIG0fmvtz+4dUX/3Ghozwgm6iPp8KRhvn+eQf9+Q7GWxVNMk3+uCPWfdXYC4ExGBckIXdFEfH1w==", - "engines": { - "node": ">=10.13.0" - } - }, - "node_modules/webpack/node_modules/ajv": { - "version": "6.12.6", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", - "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", - "dependencies": { - "fast-deep-equal": "^3.1.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, - "node_modules/webpack/node_modules/ajv-keywords": { - "version": "3.5.2", - "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", - "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==", - "peerDependencies": { - "ajv": "^6.9.1" - } - }, - "node_modules/webpack/node_modules/json-schema-traverse": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==" - }, - "node_modules/webpack/node_modules/schema-utils": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.2.0.tgz", - "integrity": "sha512-0zTyLGyDJYd/MBxG1AhJkKa6fpEBds4OQO2ut0w7OYG+ZGhGea09lijvzsqegYSik88zc7cUtIlnnO+/BvD6gQ==", - "dependencies": { - "@types/json-schema": "^7.0.8", - "ajv": "^6.12.5", - "ajv-keywords": "^3.5.2" - }, - "engines": { - "node": ">= 10.13.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - } - }, - "node_modules/which": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", - "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", - "dependencies": { - "isexe": "^2.0.0" - }, - "bin": { - "node-which": "bin/node-which" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/wildcard": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/wildcard/-/wildcard-2.0.0.tgz", - "integrity": "sha512-JcKqAHLPxcdb9KM49dufGXn2x3ssnfjbcaQdLlfZsL9rH9wgDQjUtDxbo8NE0F6SFvydeu1VhZe7hZuHsB2/pw==" - }, - "node_modules/yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" - } - }, - "dependencies": { - "@ampproject/remapping": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.2.0.tgz", - "integrity": "sha512-qRmjj8nj9qmLTQXXmaR1cck3UXSRMPrbsLJAasZpF+t3riI71BXed5ebIOYwQntykeZuhjsdweEc9BxH5Jc26w==", - "requires": { - "@jridgewell/gen-mapping": "^0.1.0", - "@jridgewell/trace-mapping": "^0.3.9" - } - }, - "@babel/code-frame": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.22.5.tgz", - "integrity": "sha512-Xmwn266vad+6DAqEB2A6V/CcZVp62BbwVmcOJc2RPuwih1kw02TjQvWVWlcKGbBPd+8/0V5DEkOcizRGYsspYQ==", - "requires": { - "@babel/highlight": "^7.22.5" - } - }, - "@babel/compat-data": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.22.5.tgz", - "integrity": "sha512-4Jc/YuIaYqKnDDz892kPIledykKg12Aw1PYX5i/TY28anJtacvM1Rrr8wbieB9GfEJwlzqT0hUEao0CxEebiDA==" - }, - "@babel/core": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.22.5.tgz", - "integrity": "sha512-SBuTAjg91A3eKOvD+bPEz3LlhHZRNu1nFOVts9lzDJTXshHTjII0BAtDS3Y2DAkdZdDKWVZGVwkDfc4Clxn1dg==", - "requires": { - "@ampproject/remapping": "^2.2.0", - "@babel/code-frame": "^7.22.5", - "@babel/generator": "^7.22.5", - "@babel/helper-compilation-targets": "^7.22.5", - "@babel/helper-module-transforms": "^7.22.5", - "@babel/helpers": "^7.22.5", - "@babel/parser": "^7.22.5", - "@babel/template": "^7.22.5", - "@babel/traverse": "^7.22.5", - "@babel/types": "^7.22.5", - "convert-source-map": "^1.7.0", - "debug": "^4.1.0", - "gensync": "^1.0.0-beta.2", - "json5": "^2.2.2", - "semver": "^6.3.0" - } - }, - "@babel/generator": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.22.5.tgz", - "integrity": "sha512-+lcUbnTRhd0jOewtFSedLyiPsD5tswKkbgcezOqqWFUVNEwoUTlpPOBmvhG7OXWLR4jMdv0czPGH5XbflnD1EA==", - "requires": { - "@babel/types": "^7.22.5", - "@jridgewell/gen-mapping": "^0.3.2", - "@jridgewell/trace-mapping": "^0.3.17", - "jsesc": "^2.5.1" - }, - "dependencies": { - "@jridgewell/gen-mapping": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.2.tgz", - "integrity": "sha512-mh65xKQAzI6iBcFzwv28KVWSmCkdRBWoOh+bYQGW3+6OZvbbN3TqMGo5hqYxQniRcH9F2VZIoJCm4pa3BPDK/A==", - "requires": { - "@jridgewell/set-array": "^1.0.1", - "@jridgewell/sourcemap-codec": "^1.4.10", - "@jridgewell/trace-mapping": "^0.3.9" - } - } - } - }, - "@babel/helper-annotate-as-pure": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.22.5.tgz", - "integrity": "sha512-LvBTxu8bQSQkcyKOU+a1btnNFQ1dMAd0R6PyW3arXes06F6QLWLIrd681bxRPIXlrMGR3XYnW9JyML7dP3qgxg==", - "requires": { - "@babel/types": "^7.22.5" - } - }, - "@babel/helper-builder-binary-assignment-operator-visitor": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helper-builder-binary-assignment-operator-visitor/-/helper-builder-binary-assignment-operator-visitor-7.22.5.tgz", - "integrity": "sha512-m1EP3lVOPptR+2DwD125gziZNcmoNSHGmJROKoy87loWUQyJaVXDgpmruWqDARZSmtYQ+Dl25okU8+qhVzuykw==", - "requires": { - "@babel/types": "^7.22.5" - } - }, - "@babel/helper-compilation-targets": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.22.5.tgz", - "integrity": "sha512-Ji+ywpHeuqxB8WDxraCiqR0xfhYjiDE/e6k7FuIaANnoOFxAHskHChz4vA1mJC9Lbm01s1PVAGhQY4FUKSkGZw==", - "requires": { - "@babel/compat-data": "^7.22.5", - "@babel/helper-validator-option": "^7.22.5", - "browserslist": "^4.21.3", - "lru-cache": "^5.1.1", - "semver": "^6.3.0" - }, - "dependencies": { - "lru-cache": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", - "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", - "requires": { - "yallist": "^3.0.2" - } - }, - "yallist": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", - "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==" - } - } - }, - "@babel/helper-create-class-features-plugin": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.22.5.tgz", - "integrity": "sha512-xkb58MyOYIslxu3gKmVXmjTtUPvBU4odYzbiIQbWwLKIHCsx6UGZGX6F1IznMFVnDdirseUZopzN+ZRt8Xb33Q==", - "requires": { - "@babel/helper-annotate-as-pure": "^7.22.5", - "@babel/helper-environment-visitor": "^7.22.5", - "@babel/helper-function-name": "^7.22.5", - "@babel/helper-member-expression-to-functions": "^7.22.5", - "@babel/helper-optimise-call-expression": "^7.22.5", - "@babel/helper-replace-supers": "^7.22.5", - "@babel/helper-skip-transparent-expression-wrappers": "^7.22.5", - "@babel/helper-split-export-declaration": "^7.22.5", - "semver": "^6.3.0" - } - }, - "@babel/helper-create-regexp-features-plugin": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.22.5.tgz", - "integrity": "sha512-1VpEFOIbMRaXyDeUwUfmTIxExLwQ+zkW+Bh5zXpApA3oQedBx9v/updixWxnx/bZpKw7u8VxWjb/qWpIcmPq8A==", - "requires": { - "@babel/helper-annotate-as-pure": "^7.22.5", - "regexpu-core": "^5.3.1", - "semver": "^6.3.0" - } - }, - "@babel/helper-define-polyfill-provider": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.4.0.tgz", - "integrity": "sha512-RnanLx5ETe6aybRi1cO/edaRH+bNYWaryCEmjDDYyNr4wnSzyOp8T0dWipmqVHKEY3AbVKUom50AKSlj1zmKbg==", - "requires": { - "@babel/helper-compilation-targets": "^7.17.7", - "@babel/helper-plugin-utils": "^7.16.7", - "debug": "^4.1.1", - "lodash.debounce": "^4.0.8", - "resolve": "^1.14.2", - "semver": "^6.1.2" - } - }, - "@babel/helper-environment-visitor": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.22.5.tgz", - "integrity": "sha512-XGmhECfVA/5sAt+H+xpSg0mfrHq6FzNr9Oxh7PSEBBRUb/mL7Kz3NICXb194rCqAEdxkhPT1a88teizAFyvk8Q==" - }, - "@babel/helper-function-name": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.22.5.tgz", - "integrity": "sha512-wtHSq6jMRE3uF2otvfuD3DIvVhOsSNshQl0Qrd7qC9oQJzHvOL4qQXlQn2916+CXGywIjpGuIkoyZRRxHPiNQQ==", - "requires": { - "@babel/template": "^7.22.5", - "@babel/types": "^7.22.5" - } - }, - "@babel/helper-hoist-variables": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.22.5.tgz", - "integrity": "sha512-wGjk9QZVzvknA6yKIUURb8zY3grXCcOZt+/7Wcy8O2uctxhplmUPkOdlgoNhmdVee2c92JXbf1xpMtVNbfoxRw==", - "requires": { - "@babel/types": "^7.22.5" - } - }, - "@babel/helper-member-expression-to-functions": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.22.5.tgz", - "integrity": "sha512-aBiH1NKMG0H2cGZqspNvsaBe6wNGjbJjuLy29aU+eDZjSbbN53BaxlpB02xm9v34pLTZ1nIQPFYn2qMZoa5BQQ==", - "requires": { - "@babel/types": "^7.22.5" - } - }, - "@babel/helper-module-imports": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.22.5.tgz", - "integrity": "sha512-8Dl6+HD/cKifutF5qGd/8ZJi84QeAKh+CEe1sBzz8UayBBGg1dAIJrdHOcOM5b2MpzWL2yuotJTtGjETq0qjXg==", - "requires": { - "@babel/types": "^7.22.5" - } - }, - "@babel/helper-module-transforms": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.22.5.tgz", - "integrity": "sha512-+hGKDt/Ze8GFExiVHno/2dvG5IdstpzCq0y4Qc9OJ25D4q3pKfiIP/4Vp3/JvhDkLKsDK2api3q3fpIgiIF5bw==", - "requires": { - "@babel/helper-environment-visitor": "^7.22.5", - "@babel/helper-module-imports": "^7.22.5", - "@babel/helper-simple-access": "^7.22.5", - "@babel/helper-split-export-declaration": "^7.22.5", - "@babel/helper-validator-identifier": "^7.22.5", - "@babel/template": "^7.22.5", - "@babel/traverse": "^7.22.5", - "@babel/types": "^7.22.5" - } - }, - "@babel/helper-optimise-call-expression": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.22.5.tgz", - "integrity": "sha512-HBwaojN0xFRx4yIvpwGqxiV2tUfl7401jlok564NgB9EHS1y6QT17FmKWm4ztqjeVdXLuC4fSvHc5ePpQjoTbw==", - "requires": { - "@babel/types": "^7.22.5" - } - }, - "@babel/helper-plugin-utils": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.22.5.tgz", - "integrity": "sha512-uLls06UVKgFG9QD4OeFYLEGteMIAa5kpTPcFL28yuCIIzsf6ZyKZMllKVOCZFhiZ5ptnwX4mtKdWCBE/uT4amg==" - }, - "@babel/helper-remap-async-to-generator": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.22.5.tgz", - "integrity": "sha512-cU0Sq1Rf4Z55fgz7haOakIyM7+x/uCFwXpLPaeRzfoUtAEAuUZjZvFPjL/rk5rW693dIgn2hng1W7xbT7lWT4g==", - "requires": { - "@babel/helper-annotate-as-pure": "^7.22.5", - "@babel/helper-environment-visitor": "^7.22.5", - "@babel/helper-wrap-function": "^7.22.5", - "@babel/types": "^7.22.5" - } - }, - "@babel/helper-replace-supers": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.22.5.tgz", - "integrity": "sha512-aLdNM5I3kdI/V9xGNyKSF3X/gTyMUBohTZ+/3QdQKAA9vxIiy12E+8E2HoOP1/DjeqU+g6as35QHJNMDDYpuCg==", - "requires": { - "@babel/helper-environment-visitor": "^7.22.5", - "@babel/helper-member-expression-to-functions": "^7.22.5", - "@babel/helper-optimise-call-expression": "^7.22.5", - "@babel/template": "^7.22.5", - "@babel/traverse": "^7.22.5", - "@babel/types": "^7.22.5" - } - }, - "@babel/helper-simple-access": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.22.5.tgz", - "integrity": "sha512-n0H99E/K+Bika3++WNL17POvo4rKWZ7lZEp1Q+fStVbUi8nxPQEBOlTmCOxW/0JsS56SKKQ+ojAe2pHKJHN35w==", - "requires": { - "@babel/types": "^7.22.5" - } - }, - "@babel/helper-skip-transparent-expression-wrappers": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.22.5.tgz", - "integrity": "sha512-tK14r66JZKiC43p8Ki33yLBVJKlQDFoA8GYN67lWCDCqoL6EMMSuM9b+Iff2jHaM/RRFYl7K+iiru7hbRqNx8Q==", - "requires": { - "@babel/types": "^7.22.5" - } - }, - "@babel/helper-split-export-declaration": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.22.5.tgz", - "integrity": "sha512-thqK5QFghPKWLhAV321lxF95yCg2K3Ob5yw+M3VHWfdia0IkPXUtoLH8x/6Fh486QUvzhb8YOWHChTVen2/PoQ==", - "requires": { - "@babel/types": "^7.22.5" - } - }, - "@babel/helper-string-parser": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.22.5.tgz", - "integrity": "sha512-mM4COjgZox8U+JcXQwPijIZLElkgEpO5rsERVDJTc2qfCDfERyob6k5WegS14SX18IIjv+XD+GrqNumY5JRCDw==" - }, - "@babel/helper-validator-identifier": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.5.tgz", - "integrity": "sha512-aJXu+6lErq8ltp+JhkJUfk1MTGyuA4v7f3pA+BJ5HLfNC6nAQ0Cpi9uOquUj8Hehg0aUiHzWQbOVJGao6ztBAQ==" - }, - "@babel/helper-validator-option": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.22.5.tgz", - "integrity": "sha512-R3oB6xlIVKUnxNUxbmgq7pKjxpru24zlimpE8WK47fACIlM0II/Hm1RS8IaOI7NgCr6LNS+jl5l75m20npAziw==" - }, - "@babel/helper-wrap-function": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helper-wrap-function/-/helper-wrap-function-7.22.5.tgz", - "integrity": "sha512-bYqLIBSEshYcYQyfks8ewYA8S30yaGSeRslcvKMvoUk6HHPySbxHq9YRi6ghhzEU+yhQv9bP/jXnygkStOcqZw==", - "requires": { - "@babel/helper-function-name": "^7.22.5", - "@babel/template": "^7.22.5", - "@babel/traverse": "^7.22.5", - "@babel/types": "^7.22.5" - } - }, - "@babel/helpers": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.22.5.tgz", - "integrity": "sha512-pSXRmfE1vzcUIDFQcSGA5Mr+GxBV9oiRKDuDxXvWQQBCh8HoIjs/2DlDB7H8smac1IVrB9/xdXj2N3Wol9Cr+Q==", - "requires": { - "@babel/template": "^7.22.5", - "@babel/traverse": "^7.22.5", - "@babel/types": "^7.22.5" - } - }, - "@babel/highlight": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.22.5.tgz", - "integrity": "sha512-BSKlD1hgnedS5XRnGOljZawtag7H1yPfQp0tdNJCHoH6AZ+Pcm9VvkrK59/Yy593Ypg0zMxH2BxD1VPYUQ7UIw==", - "requires": { - "@babel/helper-validator-identifier": "^7.22.5", - "chalk": "^2.0.0", - "js-tokens": "^4.0.0" - } - }, - "@babel/parser": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.22.5.tgz", - "integrity": "sha512-DFZMC9LJUG9PLOclRC32G63UXwzqS2koQC8dkx+PLdmt1xSePYpbT/NbsrJy8Q/muXz7o/h/d4A7Fuyixm559Q==" - }, - "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression/-/plugin-bugfix-safari-id-destructuring-collision-in-function-expression-7.22.5.tgz", - "integrity": "sha512-NP1M5Rf+u2Gw9qfSO4ihjcTGW5zXTi36ITLd4/EoAcEhIZ0yjMqmftDNl3QC19CX7olhrjpyU454g/2W7X0jvQ==", - "requires": { - "@babel/helper-plugin-utils": "^7.22.5" - } - }, - "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining/-/plugin-bugfix-v8-spread-parameters-in-optional-chaining-7.22.5.tgz", - "integrity": "sha512-31Bb65aZaUwqCbWMnZPduIZxCBngHFlzyN6Dq6KAJjtx+lx6ohKHubc61OomYi7XwVD4Ol0XCVz4h+pYFR048g==", - "requires": { - "@babel/helper-plugin-utils": "^7.22.5", - "@babel/helper-skip-transparent-expression-wrappers": "^7.22.5", - "@babel/plugin-transform-optional-chaining": "^7.22.5" - } - }, - "@babel/plugin-proposal-class-properties": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-class-properties/-/plugin-proposal-class-properties-7.18.6.tgz", - "integrity": "sha512-cumfXOF0+nzZrrN8Rf0t7M+tF6sZc7vhQwYQck9q1/5w2OExlD+b4v4RpMJFaV1Z7WcDRgO6FqvxqxGlwo+RHQ==", - "requires": { - "@babel/helper-create-class-features-plugin": "^7.18.6", - "@babel/helper-plugin-utils": "^7.18.6" - } - }, - "@babel/plugin-proposal-private-property-in-object": { - "version": "7.21.0-placeholder-for-preset-env.2", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-private-property-in-object/-/plugin-proposal-private-property-in-object-7.21.0-placeholder-for-preset-env.2.tgz", - "integrity": "sha512-SOSkfJDddaM7mak6cPEpswyTRnuRltl429hMraQEglW+OkovnCzsiszTmsrlY//qLFjCpQDFRvjdm2wA5pPm9w==", - "requires": {} - }, - "@babel/plugin-proposal-unicode-property-regex": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-unicode-property-regex/-/plugin-proposal-unicode-property-regex-7.18.6.tgz", - "integrity": "sha512-2BShG/d5yoZyXZfVePH91urL5wTG6ASZU9M4o03lKK8u8UW1y08OMttBSOADTcJrnPMpvDXRG3G8fyLh4ovs8w==", - "requires": { - "@babel/helper-create-regexp-features-plugin": "^7.18.6", - "@babel/helper-plugin-utils": "^7.18.6" - } - }, - "@babel/plugin-syntax-async-generators": { - "version": "7.8.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz", - "integrity": "sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==", - "requires": { - "@babel/helper-plugin-utils": "^7.8.0" - } - }, - "@babel/plugin-syntax-class-properties": { - "version": "7.12.13", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.12.13.tgz", - "integrity": "sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA==", - "requires": { - "@babel/helper-plugin-utils": "^7.12.13" - } - }, - "@babel/plugin-syntax-class-static-block": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-static-block/-/plugin-syntax-class-static-block-7.14.5.tgz", - "integrity": "sha512-b+YyPmr6ldyNnM6sqYeMWE+bgJcJpO6yS4QD7ymxgH34GBPNDM/THBh8iunyvKIZztiwLH4CJZ0RxTk9emgpjw==", - "requires": { - "@babel/helper-plugin-utils": "^7.14.5" - } - }, - "@babel/plugin-syntax-dynamic-import": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-dynamic-import/-/plugin-syntax-dynamic-import-7.8.3.tgz", - "integrity": "sha512-5gdGbFon+PszYzqs83S3E5mpi7/y/8M9eC90MRTZfduQOYW76ig6SOSPNe41IG5LoP3FGBn2N0RjVDSQiS94kQ==", - "requires": { - "@babel/helper-plugin-utils": "^7.8.0" - } - }, - "@babel/plugin-syntax-export-namespace-from": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-export-namespace-from/-/plugin-syntax-export-namespace-from-7.8.3.tgz", - "integrity": "sha512-MXf5laXo6c1IbEbegDmzGPwGNTsHZmEy6QGznu5Sh2UCWvueywb2ee+CCE4zQiZstxU9BMoQO9i6zUFSY0Kj0Q==", - "requires": { - "@babel/helper-plugin-utils": "^7.8.3" - } - }, - "@babel/plugin-syntax-import-assertions": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-assertions/-/plugin-syntax-import-assertions-7.22.5.tgz", - "integrity": "sha512-rdV97N7KqsRzeNGoWUOK6yUsWarLjE5Su/Snk9IYPU9CwkWHs4t+rTGOvffTR8XGkJMTAdLfO0xVnXm8wugIJg==", - "requires": { - "@babel/helper-plugin-utils": "^7.22.5" - } - }, - "@babel/plugin-syntax-import-attributes": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.22.5.tgz", - "integrity": "sha512-KwvoWDeNKPETmozyFE0P2rOLqh39EoQHNjqizrI5B8Vt0ZNS7M56s7dAiAqbYfiAYOuIzIh96z3iR2ktgu3tEg==", - "requires": { - "@babel/helper-plugin-utils": "^7.22.5" - } - }, - "@babel/plugin-syntax-import-meta": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-meta/-/plugin-syntax-import-meta-7.10.4.tgz", - "integrity": "sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==", - "requires": { - "@babel/helper-plugin-utils": "^7.10.4" - } - }, - "@babel/plugin-syntax-json-strings": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.8.3.tgz", - "integrity": "sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==", - "requires": { - "@babel/helper-plugin-utils": "^7.8.0" - } - }, - "@babel/plugin-syntax-jsx": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.22.5.tgz", - "integrity": "sha512-gvyP4hZrgrs/wWMaocvxZ44Hw0b3W8Pe+cMxc8V1ULQ07oh8VNbIRaoD1LRZVTvD+0nieDKjfgKg89sD7rrKrg==", - "requires": { - "@babel/helper-plugin-utils": "^7.22.5" - } - }, - "@babel/plugin-syntax-logical-assignment-operators": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz", - "integrity": "sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==", - "requires": { - "@babel/helper-plugin-utils": "^7.10.4" - } - }, - "@babel/plugin-syntax-nullish-coalescing-operator": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.8.3.tgz", - "integrity": "sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==", - "requires": { - "@babel/helper-plugin-utils": "^7.8.0" - } - }, - "@babel/plugin-syntax-numeric-separator": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.10.4.tgz", - "integrity": "sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==", - "requires": { - "@babel/helper-plugin-utils": "^7.10.4" - } - }, - "@babel/plugin-syntax-object-rest-spread": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz", - "integrity": "sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==", - "requires": { - "@babel/helper-plugin-utils": "^7.8.0" - } - }, - "@babel/plugin-syntax-optional-catch-binding": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.8.3.tgz", - "integrity": "sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==", - "requires": { - "@babel/helper-plugin-utils": "^7.8.0" - } - }, - "@babel/plugin-syntax-optional-chaining": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.8.3.tgz", - "integrity": "sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==", - "requires": { - "@babel/helper-plugin-utils": "^7.8.0" - } - }, - "@babel/plugin-syntax-private-property-in-object": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-private-property-in-object/-/plugin-syntax-private-property-in-object-7.14.5.tgz", - "integrity": "sha512-0wVnp9dxJ72ZUJDV27ZfbSj6iHLoytYZmh3rFcxNnvsJF3ktkzLDZPy/mA17HGsaQT3/DQsWYX1f1QGWkCoVUg==", - "requires": { - "@babel/helper-plugin-utils": "^7.14.5" - } - }, - "@babel/plugin-syntax-top-level-await": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.14.5.tgz", - "integrity": "sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw==", - "requires": { - "@babel/helper-plugin-utils": "^7.14.5" - } - }, - "@babel/plugin-syntax-unicode-sets-regex": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-unicode-sets-regex/-/plugin-syntax-unicode-sets-regex-7.18.6.tgz", - "integrity": "sha512-727YkEAPwSIQTv5im8QHz3upqp92JTWhidIC81Tdx4VJYIte/VndKf1qKrfnnhPLiPghStWfvC/iFaMCQu7Nqg==", - "requires": { - "@babel/helper-create-regexp-features-plugin": "^7.18.6", - "@babel/helper-plugin-utils": "^7.18.6" - } - }, - "@babel/plugin-transform-arrow-functions": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.22.5.tgz", - "integrity": "sha512-26lTNXoVRdAnsaDXPpvCNUq+OVWEVC6bx7Vvz9rC53F2bagUWW4u4ii2+h8Fejfh7RYqPxn+libeFBBck9muEw==", - "requires": { - "@babel/helper-plugin-utils": "^7.22.5" - } - }, - "@babel/plugin-transform-async-generator-functions": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-generator-functions/-/plugin-transform-async-generator-functions-7.22.5.tgz", - "integrity": "sha512-gGOEvFzm3fWoyD5uZq7vVTD57pPJ3PczPUD/xCFGjzBpUosnklmXyKnGQbbbGs1NPNPskFex0j93yKbHt0cHyg==", - "requires": { - "@babel/helper-environment-visitor": "^7.22.5", - "@babel/helper-plugin-utils": "^7.22.5", - "@babel/helper-remap-async-to-generator": "^7.22.5", - "@babel/plugin-syntax-async-generators": "^7.8.4" - } - }, - "@babel/plugin-transform-async-to-generator": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.22.5.tgz", - "integrity": "sha512-b1A8D8ZzE/VhNDoV1MSJTnpKkCG5bJo+19R4o4oy03zM7ws8yEMK755j61Dc3EyvdysbqH5BOOTquJ7ZX9C6vQ==", - "requires": { - "@babel/helper-module-imports": "^7.22.5", - "@babel/helper-plugin-utils": "^7.22.5", - "@babel/helper-remap-async-to-generator": "^7.22.5" - } - }, - "@babel/plugin-transform-block-scoped-functions": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.22.5.tgz", - "integrity": "sha512-tdXZ2UdknEKQWKJP1KMNmuF5Lx3MymtMN/pvA+p/VEkhK8jVcQ1fzSy8KM9qRYhAf2/lV33hoMPKI/xaI9sADA==", - "requires": { - "@babel/helper-plugin-utils": "^7.22.5" - } - }, - "@babel/plugin-transform-block-scoping": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.22.5.tgz", - "integrity": "sha512-EcACl1i5fSQ6bt+YGuU/XGCeZKStLmyVGytWkpyhCLeQVA0eu6Wtiw92V+I1T/hnezUv7j74dA/Ro69gWcU+hg==", - "requires": { - "@babel/helper-plugin-utils": "^7.22.5" - } - }, - "@babel/plugin-transform-class-properties": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-properties/-/plugin-transform-class-properties-7.22.5.tgz", - "integrity": "sha512-nDkQ0NfkOhPTq8YCLiWNxp1+f9fCobEjCb0n8WdbNUBc4IB5V7P1QnX9IjpSoquKrXF5SKojHleVNs2vGeHCHQ==", - "requires": { - "@babel/helper-create-class-features-plugin": "^7.22.5", - "@babel/helper-plugin-utils": "^7.22.5" - } - }, - "@babel/plugin-transform-class-static-block": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-static-block/-/plugin-transform-class-static-block-7.22.5.tgz", - "integrity": "sha512-SPToJ5eYZLxlnp1UzdARpOGeC2GbHvr9d/UV0EukuVx8atktg194oe+C5BqQ8jRTkgLRVOPYeXRSBg1IlMoVRA==", - "requires": { - "@babel/helper-create-class-features-plugin": "^7.22.5", - "@babel/helper-plugin-utils": "^7.22.5", - "@babel/plugin-syntax-class-static-block": "^7.14.5" - } - }, - "@babel/plugin-transform-classes": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.22.5.tgz", - "integrity": "sha512-2edQhLfibpWpsVBx2n/GKOz6JdGQvLruZQfGr9l1qes2KQaWswjBzhQF7UDUZMNaMMQeYnQzxwOMPsbYF7wqPQ==", - "requires": { - "@babel/helper-annotate-as-pure": "^7.22.5", - "@babel/helper-compilation-targets": "^7.22.5", - "@babel/helper-environment-visitor": "^7.22.5", - "@babel/helper-function-name": "^7.22.5", - "@babel/helper-optimise-call-expression": "^7.22.5", - "@babel/helper-plugin-utils": "^7.22.5", - "@babel/helper-replace-supers": "^7.22.5", - "@babel/helper-split-export-declaration": "^7.22.5", - "globals": "^11.1.0" - } - }, - "@babel/plugin-transform-computed-properties": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.22.5.tgz", - "integrity": "sha512-4GHWBgRf0krxPX+AaPtgBAlTgTeZmqDynokHOX7aqqAB4tHs3U2Y02zH6ETFdLZGcg9UQSD1WCmkVrE9ErHeOg==", - "requires": { - "@babel/helper-plugin-utils": "^7.22.5", - "@babel/template": "^7.22.5" - } - }, - "@babel/plugin-transform-destructuring": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.22.5.tgz", - "integrity": "sha512-GfqcFuGW8vnEqTUBM7UtPd5A4q797LTvvwKxXTgRsFjoqaJiEg9deBG6kWeQYkVEL569NpnmpC0Pkr/8BLKGnQ==", - "requires": { - "@babel/helper-plugin-utils": "^7.22.5" - } - }, - "@babel/plugin-transform-dotall-regex": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.22.5.tgz", - "integrity": "sha512-5/Yk9QxCQCl+sOIB1WelKnVRxTJDSAIxtJLL2/pqL14ZVlbH0fUQUZa/T5/UnQtBNgghR7mfB8ERBKyKPCi7Vw==", - "requires": { - "@babel/helper-create-regexp-features-plugin": "^7.22.5", - "@babel/helper-plugin-utils": "^7.22.5" - } - }, - "@babel/plugin-transform-duplicate-keys": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.22.5.tgz", - "integrity": "sha512-dEnYD+9BBgld5VBXHnF/DbYGp3fqGMsyxKbtD1mDyIA7AkTSpKXFhCVuj/oQVOoALfBs77DudA0BE4d5mcpmqw==", - "requires": { - "@babel/helper-plugin-utils": "^7.22.5" - } - }, - "@babel/plugin-transform-dynamic-import": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dynamic-import/-/plugin-transform-dynamic-import-7.22.5.tgz", - "integrity": "sha512-0MC3ppTB1AMxd8fXjSrbPa7LT9hrImt+/fcj+Pg5YMD7UQyWp/02+JWpdnCymmsXwIx5Z+sYn1bwCn4ZJNvhqQ==", - "requires": { - "@babel/helper-plugin-utils": "^7.22.5", - "@babel/plugin-syntax-dynamic-import": "^7.8.3" - } - }, - "@babel/plugin-transform-exponentiation-operator": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.22.5.tgz", - "integrity": "sha512-vIpJFNM/FjZ4rh1myqIya9jXwrwwgFRHPjT3DkUA9ZLHuzox8jiXkOLvwm1H+PQIP3CqfC++WPKeuDi0Sjdj1g==", - "requires": { - "@babel/helper-builder-binary-assignment-operator-visitor": "^7.22.5", - "@babel/helper-plugin-utils": "^7.22.5" - } - }, - "@babel/plugin-transform-export-namespace-from": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-export-namespace-from/-/plugin-transform-export-namespace-from-7.22.5.tgz", - "integrity": "sha512-X4hhm7FRnPgd4nDA4b/5V280xCx6oL7Oob5+9qVS5C13Zq4bh1qq7LU0GgRU6b5dBWBvhGaXYVB4AcN6+ol6vg==", - "requires": { - "@babel/helper-plugin-utils": "^7.22.5", - "@babel/plugin-syntax-export-namespace-from": "^7.8.3" - } - }, - "@babel/plugin-transform-for-of": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.22.5.tgz", - "integrity": "sha512-3kxQjX1dU9uudwSshyLeEipvrLjBCVthCgeTp6CzE/9JYrlAIaeekVxRpCWsDDfYTfRZRoCeZatCQvwo+wvK8A==", - "requires": { - "@babel/helper-plugin-utils": "^7.22.5" - } - }, - "@babel/plugin-transform-function-name": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.22.5.tgz", - "integrity": "sha512-UIzQNMS0p0HHiQm3oelztj+ECwFnj+ZRV4KnguvlsD2of1whUeM6o7wGNj6oLwcDoAXQ8gEqfgC24D+VdIcevg==", - "requires": { - "@babel/helper-compilation-targets": "^7.22.5", - "@babel/helper-function-name": "^7.22.5", - "@babel/helper-plugin-utils": "^7.22.5" - } - }, - "@babel/plugin-transform-json-strings": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-json-strings/-/plugin-transform-json-strings-7.22.5.tgz", - "integrity": "sha512-DuCRB7fu8MyTLbEQd1ew3R85nx/88yMoqo2uPSjevMj3yoN7CDM8jkgrY0wmVxfJZyJ/B9fE1iq7EQppWQmR5A==", - "requires": { - "@babel/helper-plugin-utils": "^7.22.5", - "@babel/plugin-syntax-json-strings": "^7.8.3" - } - }, - "@babel/plugin-transform-literals": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-literals/-/plugin-transform-literals-7.22.5.tgz", - "integrity": "sha512-fTLj4D79M+mepcw3dgFBTIDYpbcB9Sm0bpm4ppXPaO+U+PKFFyV9MGRvS0gvGw62sd10kT5lRMKXAADb9pWy8g==", - "requires": { - "@babel/helper-plugin-utils": "^7.22.5" - } - }, - "@babel/plugin-transform-logical-assignment-operators": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-logical-assignment-operators/-/plugin-transform-logical-assignment-operators-7.22.5.tgz", - "integrity": "sha512-MQQOUW1KL8X0cDWfbwYP+TbVbZm16QmQXJQ+vndPtH/BoO0lOKpVoEDMI7+PskYxH+IiE0tS8xZye0qr1lGzSA==", - "requires": { - "@babel/helper-plugin-utils": "^7.22.5", - "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4" - } - }, - "@babel/plugin-transform-member-expression-literals": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-7.22.5.tgz", - "integrity": "sha512-RZEdkNtzzYCFl9SE9ATaUMTj2hqMb4StarOJLrZRbqqU4HSBE7UlBw9WBWQiDzrJZJdUWiMTVDI6Gv/8DPvfew==", - "requires": { - "@babel/helper-plugin-utils": "^7.22.5" - } - }, - "@babel/plugin-transform-modules-amd": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.22.5.tgz", - "integrity": "sha512-R+PTfLTcYEmb1+kK7FNkhQ1gP4KgjpSO6HfH9+f8/yfp2Nt3ggBjiVpRwmwTlfqZLafYKJACy36yDXlEmI9HjQ==", - "requires": { - "@babel/helper-module-transforms": "^7.22.5", - "@babel/helper-plugin-utils": "^7.22.5" - } - }, - "@babel/plugin-transform-modules-commonjs": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.22.5.tgz", - "integrity": "sha512-B4pzOXj+ONRmuaQTg05b3y/4DuFz3WcCNAXPLb2Q0GT0TrGKGxNKV4jwsXts+StaM0LQczZbOpj8o1DLPDJIiA==", - "requires": { - "@babel/helper-module-transforms": "^7.22.5", - "@babel/helper-plugin-utils": "^7.22.5", - "@babel/helper-simple-access": "^7.22.5" - } - }, - "@babel/plugin-transform-modules-systemjs": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.22.5.tgz", - "integrity": "sha512-emtEpoaTMsOs6Tzz+nbmcePl6AKVtS1yC4YNAeMun9U8YCsgadPNxnOPQ8GhHFB2qdx+LZu9LgoC0Lthuu05DQ==", - "requires": { - "@babel/helper-hoist-variables": "^7.22.5", - "@babel/helper-module-transforms": "^7.22.5", - "@babel/helper-plugin-utils": "^7.22.5", - "@babel/helper-validator-identifier": "^7.22.5" - } - }, - "@babel/plugin-transform-modules-umd": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.22.5.tgz", - "integrity": "sha512-+S6kzefN/E1vkSsKx8kmQuqeQsvCKCd1fraCM7zXm4SFoggI099Tr4G8U81+5gtMdUeMQ4ipdQffbKLX0/7dBQ==", - "requires": { - "@babel/helper-module-transforms": "^7.22.5", - "@babel/helper-plugin-utils": "^7.22.5" - } - }, - "@babel/plugin-transform-named-capturing-groups-regex": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.22.5.tgz", - "integrity": "sha512-YgLLKmS3aUBhHaxp5hi1WJTgOUb/NCuDHzGT9z9WTt3YG+CPRhJs6nprbStx6DnWM4dh6gt7SU3sZodbZ08adQ==", - "requires": { - "@babel/helper-create-regexp-features-plugin": "^7.22.5", - "@babel/helper-plugin-utils": "^7.22.5" - } - }, - "@babel/plugin-transform-new-target": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.22.5.tgz", - "integrity": "sha512-AsF7K0Fx/cNKVyk3a+DW0JLo+Ua598/NxMRvxDnkpCIGFh43+h/v2xyhRUYf6oD8gE4QtL83C7zZVghMjHd+iw==", - "requires": { - "@babel/helper-plugin-utils": "^7.22.5" - } - }, - "@babel/plugin-transform-nullish-coalescing-operator": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-nullish-coalescing-operator/-/plugin-transform-nullish-coalescing-operator-7.22.5.tgz", - "integrity": "sha512-6CF8g6z1dNYZ/VXok5uYkkBBICHZPiGEl7oDnAx2Mt1hlHVHOSIKWJaXHjQJA5VB43KZnXZDIexMchY4y2PGdA==", - "requires": { - "@babel/helper-plugin-utils": "^7.22.5", - "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3" - } - }, - "@babel/plugin-transform-numeric-separator": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-numeric-separator/-/plugin-transform-numeric-separator-7.22.5.tgz", - "integrity": "sha512-NbslED1/6M+sXiwwtcAB/nieypGw02Ejf4KtDeMkCEpP6gWFMX1wI9WKYua+4oBneCCEmulOkRpwywypVZzs/g==", - "requires": { - "@babel/helper-plugin-utils": "^7.22.5", - "@babel/plugin-syntax-numeric-separator": "^7.10.4" - } - }, - "@babel/plugin-transform-object-rest-spread": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-rest-spread/-/plugin-transform-object-rest-spread-7.22.5.tgz", - "integrity": "sha512-Kk3lyDmEslH9DnvCDA1s1kkd3YWQITiBOHngOtDL9Pt6BZjzqb6hiOlb8VfjiiQJ2unmegBqZu0rx5RxJb5vmQ==", - "requires": { - "@babel/compat-data": "^7.22.5", - "@babel/helper-compilation-targets": "^7.22.5", - "@babel/helper-plugin-utils": "^7.22.5", - "@babel/plugin-syntax-object-rest-spread": "^7.8.3", - "@babel/plugin-transform-parameters": "^7.22.5" - } - }, - "@babel/plugin-transform-object-super": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.22.5.tgz", - "integrity": "sha512-klXqyaT9trSjIUrcsYIfETAzmOEZL3cBYqOYLJxBHfMFFggmXOv+NYSX/Jbs9mzMVESw/WycLFPRx8ba/b2Ipw==", - "requires": { - "@babel/helper-plugin-utils": "^7.22.5", - "@babel/helper-replace-supers": "^7.22.5" - } - }, - "@babel/plugin-transform-optional-catch-binding": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-catch-binding/-/plugin-transform-optional-catch-binding-7.22.5.tgz", - "integrity": "sha512-pH8orJahy+hzZje5b8e2QIlBWQvGpelS76C63Z+jhZKsmzfNaPQ+LaW6dcJ9bxTpo1mtXbgHwy765Ro3jftmUg==", - "requires": { - "@babel/helper-plugin-utils": "^7.22.5", - "@babel/plugin-syntax-optional-catch-binding": "^7.8.3" - } - }, - "@babel/plugin-transform-optional-chaining": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-chaining/-/plugin-transform-optional-chaining-7.22.5.tgz", - "integrity": "sha512-AconbMKOMkyG+xCng2JogMCDcqW8wedQAqpVIL4cOSescZ7+iW8utC6YDZLMCSUIReEA733gzRSaOSXMAt/4WQ==", - "requires": { - "@babel/helper-plugin-utils": "^7.22.5", - "@babel/helper-skip-transparent-expression-wrappers": "^7.22.5", - "@babel/plugin-syntax-optional-chaining": "^7.8.3" - } - }, - "@babel/plugin-transform-parameters": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.22.5.tgz", - "integrity": "sha512-AVkFUBurORBREOmHRKo06FjHYgjrabpdqRSwq6+C7R5iTCZOsM4QbcB27St0a4U6fffyAOqh3s/qEfybAhfivg==", - "requires": { - "@babel/helper-plugin-utils": "^7.22.5" - } - }, - "@babel/plugin-transform-private-methods": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-methods/-/plugin-transform-private-methods-7.22.5.tgz", - "integrity": "sha512-PPjh4gyrQnGe97JTalgRGMuU4icsZFnWkzicB/fUtzlKUqvsWBKEpPPfr5a2JiyirZkHxnAqkQMO5Z5B2kK3fA==", - "requires": { - "@babel/helper-create-class-features-plugin": "^7.22.5", - "@babel/helper-plugin-utils": "^7.22.5" - } - }, - "@babel/plugin-transform-private-property-in-object": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-property-in-object/-/plugin-transform-private-property-in-object-7.22.5.tgz", - "integrity": "sha512-/9xnaTTJcVoBtSSmrVyhtSvO3kbqS2ODoh2juEU72c3aYonNF0OMGiaz2gjukyKM2wBBYJP38S4JiE0Wfb5VMQ==", - "requires": { - "@babel/helper-annotate-as-pure": "^7.22.5", - "@babel/helper-create-class-features-plugin": "^7.22.5", - "@babel/helper-plugin-utils": "^7.22.5", - "@babel/plugin-syntax-private-property-in-object": "^7.14.5" - } - }, - "@babel/plugin-transform-property-literals": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.22.5.tgz", - "integrity": "sha512-TiOArgddK3mK/x1Qwf5hay2pxI6wCZnvQqrFSqbtg1GLl2JcNMitVH/YnqjP+M31pLUeTfzY1HAXFDnUBV30rQ==", - "requires": { - "@babel/helper-plugin-utils": "^7.22.5" - } - }, - "@babel/plugin-transform-react-display-name": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-display-name/-/plugin-transform-react-display-name-7.22.5.tgz", - "integrity": "sha512-PVk3WPYudRF5z4GKMEYUrLjPl38fJSKNaEOkFuoprioowGuWN6w2RKznuFNSlJx7pzzXXStPUnNSOEO0jL5EVw==", - "requires": { - "@babel/helper-plugin-utils": "^7.22.5" - } - }, - "@babel/plugin-transform-react-jsx": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx/-/plugin-transform-react-jsx-7.22.5.tgz", - "integrity": "sha512-rog5gZaVbUip5iWDMTYbVM15XQq+RkUKhET/IHR6oizR+JEoN6CAfTTuHcK4vwUyzca30qqHqEpzBOnaRMWYMA==", - "requires": { - "@babel/helper-annotate-as-pure": "^7.22.5", - "@babel/helper-module-imports": "^7.22.5", - "@babel/helper-plugin-utils": "^7.22.5", - "@babel/plugin-syntax-jsx": "^7.22.5", - "@babel/types": "^7.22.5" - } - }, - "@babel/plugin-transform-react-jsx-development": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-development/-/plugin-transform-react-jsx-development-7.22.5.tgz", - "integrity": "sha512-bDhuzwWMuInwCYeDeMzyi7TaBgRQei6DqxhbyniL7/VG4RSS7HtSL2QbY4eESy1KJqlWt8g3xeEBGPuo+XqC8A==", - "requires": { - "@babel/plugin-transform-react-jsx": "^7.22.5" - } - }, - "@babel/plugin-transform-react-pure-annotations": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-pure-annotations/-/plugin-transform-react-pure-annotations-7.22.5.tgz", - "integrity": "sha512-gP4k85wx09q+brArVinTXhWiyzLl9UpmGva0+mWyKxk6JZequ05x3eUcIUE+FyttPKJFRRVtAvQaJ6YF9h1ZpA==", - "requires": { - "@babel/helper-annotate-as-pure": "^7.22.5", - "@babel/helper-plugin-utils": "^7.22.5" - } - }, - "@babel/plugin-transform-regenerator": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.22.5.tgz", - "integrity": "sha512-rR7KePOE7gfEtNTh9Qw+iO3Q/e4DEsoQ+hdvM6QUDH7JRJ5qxq5AA52ZzBWbI5i9lfNuvySgOGP8ZN7LAmaiPw==", - "requires": { - "@babel/helper-plugin-utils": "^7.22.5", - "regenerator-transform": "^0.15.1" - } - }, - "@babel/plugin-transform-reserved-words": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-reserved-words/-/plugin-transform-reserved-words-7.22.5.tgz", - "integrity": "sha512-DTtGKFRQUDm8svigJzZHzb/2xatPc6TzNvAIJ5GqOKDsGFYgAskjRulbR/vGsPKq3OPqtexnz327qYpP57RFyA==", - "requires": { - "@babel/helper-plugin-utils": "^7.22.5" - } - }, - "@babel/plugin-transform-shorthand-properties": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.22.5.tgz", - "integrity": "sha512-vM4fq9IXHscXVKzDv5itkO1X52SmdFBFcMIBZ2FRn2nqVYqw6dBexUgMvAjHW+KXpPPViD/Yo3GrDEBaRC0QYA==", - "requires": { - "@babel/helper-plugin-utils": "^7.22.5" - } - }, - "@babel/plugin-transform-spread": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-spread/-/plugin-transform-spread-7.22.5.tgz", - "integrity": "sha512-5ZzDQIGyvN4w8+dMmpohL6MBo+l2G7tfC/O2Dg7/hjpgeWvUx8FzfeOKxGog9IimPa4YekaQ9PlDqTLOljkcxg==", - "requires": { - "@babel/helper-plugin-utils": "^7.22.5", - "@babel/helper-skip-transparent-expression-wrappers": "^7.22.5" - } - }, - "@babel/plugin-transform-sticky-regex": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.22.5.tgz", - "integrity": "sha512-zf7LuNpHG0iEeiyCNwX4j3gDg1jgt1k3ZdXBKbZSoA3BbGQGvMiSvfbZRR3Dr3aeJe3ooWFZxOOG3IRStYp2Bw==", - "requires": { - "@babel/helper-plugin-utils": "^7.22.5" - } - }, - "@babel/plugin-transform-template-literals": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.22.5.tgz", - "integrity": "sha512-5ciOehRNf+EyUeewo8NkbQiUs4d6ZxiHo6BcBcnFlgiJfu16q0bQUw9Jvo0b0gBKFG1SMhDSjeKXSYuJLeFSMA==", - "requires": { - "@babel/helper-plugin-utils": "^7.22.5" - } - }, - "@babel/plugin-transform-typeof-symbol": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.22.5.tgz", - "integrity": "sha512-bYkI5lMzL4kPii4HHEEChkD0rkc+nvnlR6+o/qdqR6zrm0Sv/nodmyLhlq2DO0YKLUNd2VePmPRjJXSBh9OIdA==", - "requires": { - "@babel/helper-plugin-utils": "^7.22.5" - } - }, - "@babel/plugin-transform-unicode-escapes": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-escapes/-/plugin-transform-unicode-escapes-7.22.5.tgz", - "integrity": "sha512-biEmVg1IYB/raUO5wT1tgfacCef15Fbzhkx493D3urBI++6hpJ+RFG4SrWMn0NEZLfvilqKf3QDrRVZHo08FYg==", - "requires": { - "@babel/helper-plugin-utils": "^7.22.5" - } - }, - "@babel/plugin-transform-unicode-property-regex": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-property-regex/-/plugin-transform-unicode-property-regex-7.22.5.tgz", - "integrity": "sha512-HCCIb+CbJIAE6sXn5CjFQXMwkCClcOfPCzTlilJ8cUatfzwHlWQkbtV0zD338u9dZskwvuOYTuuaMaA8J5EI5A==", - "requires": { - "@babel/helper-create-regexp-features-plugin": "^7.22.5", - "@babel/helper-plugin-utils": "^7.22.5" - } - }, - "@babel/plugin-transform-unicode-regex": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.22.5.tgz", - "integrity": "sha512-028laaOKptN5vHJf9/Arr/HiJekMd41hOEZYvNsrsXqJ7YPYuX2bQxh31fkZzGmq3YqHRJzYFFAVYvKfMPKqyg==", - "requires": { - "@babel/helper-create-regexp-features-plugin": "^7.22.5", - "@babel/helper-plugin-utils": "^7.22.5" - } - }, - "@babel/plugin-transform-unicode-sets-regex": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-sets-regex/-/plugin-transform-unicode-sets-regex-7.22.5.tgz", - "integrity": "sha512-lhMfi4FC15j13eKrh3DnYHjpGj6UKQHtNKTbtc1igvAhRy4+kLhV07OpLcsN0VgDEw/MjAvJO4BdMJsHwMhzCg==", - "requires": { - "@babel/helper-create-regexp-features-plugin": "^7.22.5", - "@babel/helper-plugin-utils": "^7.22.5" - } - }, - "@babel/preset-env": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.22.5.tgz", - "integrity": "sha512-fj06hw89dpiZzGZtxn+QybifF07nNiZjZ7sazs2aVDcysAZVGjW7+7iFYxg6GLNM47R/thYfLdrXc+2f11Vi9A==", - "requires": { - "@babel/compat-data": "^7.22.5", - "@babel/helper-compilation-targets": "^7.22.5", - "@babel/helper-plugin-utils": "^7.22.5", - "@babel/helper-validator-option": "^7.22.5", - "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": "^7.22.5", - "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": "^7.22.5", - "@babel/plugin-proposal-private-property-in-object": "7.21.0-placeholder-for-preset-env.2", - "@babel/plugin-syntax-async-generators": "^7.8.4", - "@babel/plugin-syntax-class-properties": "^7.12.13", - "@babel/plugin-syntax-class-static-block": "^7.14.5", - "@babel/plugin-syntax-dynamic-import": "^7.8.3", - "@babel/plugin-syntax-export-namespace-from": "^7.8.3", - "@babel/plugin-syntax-import-assertions": "^7.22.5", - "@babel/plugin-syntax-import-attributes": "^7.22.5", - "@babel/plugin-syntax-import-meta": "^7.10.4", - "@babel/plugin-syntax-json-strings": "^7.8.3", - "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4", - "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3", - "@babel/plugin-syntax-numeric-separator": "^7.10.4", - "@babel/plugin-syntax-object-rest-spread": "^7.8.3", - "@babel/plugin-syntax-optional-catch-binding": "^7.8.3", - "@babel/plugin-syntax-optional-chaining": "^7.8.3", - "@babel/plugin-syntax-private-property-in-object": "^7.14.5", - "@babel/plugin-syntax-top-level-await": "^7.14.5", - "@babel/plugin-syntax-unicode-sets-regex": "^7.18.6", - "@babel/plugin-transform-arrow-functions": "^7.22.5", - "@babel/plugin-transform-async-generator-functions": "^7.22.5", - "@babel/plugin-transform-async-to-generator": "^7.22.5", - "@babel/plugin-transform-block-scoped-functions": "^7.22.5", - "@babel/plugin-transform-block-scoping": "^7.22.5", - "@babel/plugin-transform-class-properties": "^7.22.5", - "@babel/plugin-transform-class-static-block": "^7.22.5", - "@babel/plugin-transform-classes": "^7.22.5", - "@babel/plugin-transform-computed-properties": "^7.22.5", - "@babel/plugin-transform-destructuring": "^7.22.5", - "@babel/plugin-transform-dotall-regex": "^7.22.5", - "@babel/plugin-transform-duplicate-keys": "^7.22.5", - "@babel/plugin-transform-dynamic-import": "^7.22.5", - "@babel/plugin-transform-exponentiation-operator": "^7.22.5", - "@babel/plugin-transform-export-namespace-from": "^7.22.5", - "@babel/plugin-transform-for-of": "^7.22.5", - "@babel/plugin-transform-function-name": "^7.22.5", - "@babel/plugin-transform-json-strings": "^7.22.5", - "@babel/plugin-transform-literals": "^7.22.5", - "@babel/plugin-transform-logical-assignment-operators": "^7.22.5", - "@babel/plugin-transform-member-expression-literals": "^7.22.5", - "@babel/plugin-transform-modules-amd": "^7.22.5", - "@babel/plugin-transform-modules-commonjs": "^7.22.5", - "@babel/plugin-transform-modules-systemjs": "^7.22.5", - "@babel/plugin-transform-modules-umd": "^7.22.5", - "@babel/plugin-transform-named-capturing-groups-regex": "^7.22.5", - "@babel/plugin-transform-new-target": "^7.22.5", - "@babel/plugin-transform-nullish-coalescing-operator": "^7.22.5", - "@babel/plugin-transform-numeric-separator": "^7.22.5", - "@babel/plugin-transform-object-rest-spread": "^7.22.5", - "@babel/plugin-transform-object-super": "^7.22.5", - "@babel/plugin-transform-optional-catch-binding": "^7.22.5", - "@babel/plugin-transform-optional-chaining": "^7.22.5", - "@babel/plugin-transform-parameters": "^7.22.5", - "@babel/plugin-transform-private-methods": "^7.22.5", - "@babel/plugin-transform-private-property-in-object": "^7.22.5", - "@babel/plugin-transform-property-literals": "^7.22.5", - "@babel/plugin-transform-regenerator": "^7.22.5", - "@babel/plugin-transform-reserved-words": "^7.22.5", - "@babel/plugin-transform-shorthand-properties": "^7.22.5", - "@babel/plugin-transform-spread": "^7.22.5", - "@babel/plugin-transform-sticky-regex": "^7.22.5", - "@babel/plugin-transform-template-literals": "^7.22.5", - "@babel/plugin-transform-typeof-symbol": "^7.22.5", - "@babel/plugin-transform-unicode-escapes": "^7.22.5", - "@babel/plugin-transform-unicode-property-regex": "^7.22.5", - "@babel/plugin-transform-unicode-regex": "^7.22.5", - "@babel/plugin-transform-unicode-sets-regex": "^7.22.5", - "@babel/preset-modules": "^0.1.5", - "@babel/types": "^7.22.5", - "babel-plugin-polyfill-corejs2": "^0.4.3", - "babel-plugin-polyfill-corejs3": "^0.8.1", - "babel-plugin-polyfill-regenerator": "^0.5.0", - "core-js-compat": "^3.30.2", - "semver": "^6.3.0" - } - }, - "@babel/preset-modules": { - "version": "0.1.5", - "resolved": "https://registry.npmjs.org/@babel/preset-modules/-/preset-modules-0.1.5.tgz", - "integrity": "sha512-A57th6YRG7oR3cq/yt/Y84MvGgE0eJG2F1JLhKuyG+jFxEgrd/HAMJatiFtmOiZurz+0DkrvbheCLaV5f2JfjA==", - "requires": { - "@babel/helper-plugin-utils": "^7.0.0", - "@babel/plugin-proposal-unicode-property-regex": "^7.4.4", - "@babel/plugin-transform-dotall-regex": "^7.4.4", - "@babel/types": "^7.4.4", - "esutils": "^2.0.2" - } - }, - "@babel/preset-react": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/preset-react/-/preset-react-7.22.5.tgz", - "integrity": "sha512-M+Is3WikOpEJHgR385HbuCITPTaPRaNkibTEa9oiofmJvIsrceb4yp9RL9Kb+TE8LznmeyZqpP+Lopwcx59xPQ==", - "requires": { - "@babel/helper-plugin-utils": "^7.22.5", - "@babel/helper-validator-option": "^7.22.5", - "@babel/plugin-transform-react-display-name": "^7.22.5", - "@babel/plugin-transform-react-jsx": "^7.22.5", - "@babel/plugin-transform-react-jsx-development": "^7.22.5", - "@babel/plugin-transform-react-pure-annotations": "^7.22.5" - } - }, - "@babel/regjsgen": { - "version": "0.8.0", - "resolved": "https://registry.npmjs.org/@babel/regjsgen/-/regjsgen-0.8.0.tgz", - "integrity": "sha512-x/rqGMdzj+fWZvCOYForTghzbtqPDZ5gPwaoNGHdgDfF2QA/XZbCBp4Moo5scrkAMPhB7z26XM/AaHuIJdgauA==" - }, - "@babel/runtime": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.22.5.tgz", - "integrity": "sha512-ecjvYlnAaZ/KVneE/OdKYBYfgXV3Ptu6zQWmgEF7vwKhQnvVS6bjMD2XYgj+SNvQ1GfK/pjgokfPkC/2CO8CuA==", - "requires": { - "regenerator-runtime": "^0.13.11" - } - }, - "@babel/template": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.22.5.tgz", - "integrity": "sha512-X7yV7eiwAxdj9k94NEylvbVHLiVG1nvzCV2EAowhxLTwODV1jl9UzZ48leOC0sH7OnuHrIkllaBgneUykIcZaw==", - "requires": { - "@babel/code-frame": "^7.22.5", - "@babel/parser": "^7.22.5", - "@babel/types": "^7.22.5" - } - }, - "@babel/traverse": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.22.5.tgz", - "integrity": "sha512-7DuIjPgERaNo6r+PZwItpjCZEa5vyw4eJGufeLxrPdBXBoLcCJCIasvK6pK/9DVNrLZTLFhUGqaC6X/PA007TQ==", - "requires": { - "@babel/code-frame": "^7.22.5", - "@babel/generator": "^7.22.5", - "@babel/helper-environment-visitor": "^7.22.5", - "@babel/helper-function-name": "^7.22.5", - "@babel/helper-hoist-variables": "^7.22.5", - "@babel/helper-split-export-declaration": "^7.22.5", - "@babel/parser": "^7.22.5", - "@babel/types": "^7.22.5", - "debug": "^4.1.0", - "globals": "^11.1.0" - } - }, - "@babel/types": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.22.5.tgz", - "integrity": "sha512-zo3MIHGOkPOfoRXitsgHLjEXmlDaD/5KU1Uzuc9GNiZPhSqVxVRtxuPaSBZDsYZ9qV88AjtMtWW7ww98loJ9KA==", - "requires": { - "@babel/helper-string-parser": "^7.22.5", - "@babel/helper-validator-identifier": "^7.22.5", - "to-fast-properties": "^2.0.0" - } - }, - "@discoveryjs/json-ext": { - "version": "0.5.3", - "resolved": "https://registry.npmjs.org/@discoveryjs/json-ext/-/json-ext-0.5.3.tgz", - "integrity": "sha512-Fxt+AfXgjMoin2maPIYzFZnQjAXjAL0PHscM5pRTtatFqB+vZxAM9tLp2Optnuw3QOQC40jTNeGYFOMvyf7v9g==" - }, - "@emotion/is-prop-valid": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@emotion/is-prop-valid/-/is-prop-valid-1.2.0.tgz", - "integrity": "sha512-3aDpDprjM0AwaxGE09bOPkNxHpBd+kA6jty3RnaEXdweX1DF1U3VQpPYb0g1IStAuK7SVQ1cy+bNBBKp4W3Fjg==", - "requires": { - "@emotion/memoize": "^0.8.0" - } - }, - "@emotion/memoize": { - "version": "0.8.0", - "resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.8.0.tgz", - "integrity": "sha512-G/YwXTkv7Den9mXDO7AhLWkE3q+I92B+VqAE+dYG4NGPaHZGvt3G8Q0p9vmE+sq7rTGphUbAvmQ9YpbfMQGGlA==" - }, - "@emotion/stylis": { - "version": "0.8.5", - "resolved": "https://registry.npmjs.org/@emotion/stylis/-/stylis-0.8.5.tgz", - "integrity": "sha512-h6KtPihKFn3T9fuIrwvXXUOwlx3rfUvfZIcP5a6rh8Y7zjE3O06hT5Ss4S/YI1AYhuZ1kjaE/5EaOOI2NqSylQ==" - }, - "@emotion/unitless": { - "version": "0.7.5", - "resolved": "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.7.5.tgz", - "integrity": "sha512-OWORNpfjMsSSUBVrRBVGECkhWcULOAJz9ZW8uK9qgxD+87M7jHRcvh/A96XXNhXTLmKcoYSQtBEX7lHMO7YRwg==" - }, - "@jridgewell/gen-mapping": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.1.1.tgz", - "integrity": "sha512-sQXCasFk+U8lWYEe66WxRDOE9PjVz4vSM51fTu3Hw+ClTpUSQb718772vH3pyS5pShp6lvQM7SxgIDXXXmOX7w==", - "requires": { - "@jridgewell/set-array": "^1.0.0", - "@jridgewell/sourcemap-codec": "^1.4.10" - } - }, - "@jridgewell/resolve-uri": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.0.tgz", - "integrity": "sha512-F2msla3tad+Mfht5cJq7LSXcdudKTWCVYUgw6pLFOOHSTtZlj6SWNYAp+AhuqLmWdBO2X5hPrLcu8cVP8fy28w==" - }, - "@jridgewell/set-array": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.1.2.tgz", - "integrity": "sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==" - }, - "@jridgewell/source-map": { - "version": "0.3.3", - "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.3.tgz", - "integrity": "sha512-b+fsZXeLYi9fEULmfBrhxn4IrPlINf8fiNarzTof004v3lFdntdwa9PF7vFJqm3mg7s+ScJMxXaE3Acp1irZcg==", - "requires": { - "@jridgewell/gen-mapping": "^0.3.0", - "@jridgewell/trace-mapping": "^0.3.9" - }, - "dependencies": { - "@jridgewell/gen-mapping": { - "version": "0.3.3", - "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.3.tgz", - "integrity": "sha512-HLhSWOLRi875zjjMG/r+Nv0oCW8umGb0BgEhyX3dDX3egwZtB8PqLnjz3yedt8R5StBrzcg4aBpnh8UA9D1BoQ==", - "requires": { - "@jridgewell/set-array": "^1.0.1", - "@jridgewell/sourcemap-codec": "^1.4.10", - "@jridgewell/trace-mapping": "^0.3.9" - } - } - } - }, - "@jridgewell/sourcemap-codec": { - "version": "1.4.14", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.14.tgz", - "integrity": "sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw==" - }, - "@jridgewell/trace-mapping": { - "version": "0.3.18", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.18.tgz", - "integrity": "sha512-w+niJYzMHdd7USdiH2U6869nqhD2nbfZXND5Yp93qIbEmnDNk7PD48o+YchRVpzMU7M6jVCbenTR7PA1FLQ9pA==", - "requires": { - "@jridgewell/resolve-uri": "3.1.0", - "@jridgewell/sourcemap-codec": "1.4.14" - } - }, - "@types/eslint": { - "version": "8.4.6", - "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-8.4.6.tgz", - "integrity": "sha512-/fqTbjxyFUaYNO7VcW5g+4npmqVACz1bB7RTHYuLj+PRjw9hrCwrUXVQFpChUS0JsyEFvMZ7U/PfmvWgxJhI9g==", - "requires": { - "@types/estree": "*", - "@types/json-schema": "*" - } - }, - "@types/eslint-scope": { - "version": "3.7.4", - "resolved": "https://registry.npmjs.org/@types/eslint-scope/-/eslint-scope-3.7.4.tgz", - "integrity": "sha512-9K4zoImiZc3HlIp6AVUDE4CWYx22a+lhSZMYNpbjW04+YF0KWj4pJXnEMjdnFTiQibFFmElcsasJXDbdI/EPhA==", - "requires": { - "@types/eslint": "*", - "@types/estree": "*" - } - }, - "@types/estree": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.1.tgz", - "integrity": "sha512-LG4opVs2ANWZ1TJoKc937iMmNstM/d0ae1vNbnBvBhqCSezgVUOzcLCqbI5elV8Vy6WKwKjaqR+zO9VKirBBCA==" - }, - "@types/hoist-non-react-statics": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.1.tgz", - "integrity": "sha512-iMIqiko6ooLrTh1joXodJK5X9xeEALT1kM5G3ZLhD3hszxBdIEd5C75U834D9mLcINgD4OyZf5uQXjkuYydWvA==", - "requires": { - "@types/react": "*", - "hoist-non-react-statics": "^3.3.0" - } - }, - "@types/json-schema": { - "version": "7.0.11", - "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.11.tgz", - "integrity": "sha512-wOuvG1SN4Us4rez+tylwwwCV1psiNVOkJeM3AUWUNWg/jDQY2+HE/444y5gc+jBmRqASOm2Oeh5c1axHobwRKQ==" - }, - "@types/node": { - "version": "20.2.5", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.2.5.tgz", - "integrity": "sha512-JJulVEQXmiY9Px5axXHeYGLSjhkZEnD+MDPDGbCbIAbMslkKwmygtZFy1X6s/075Yo94sf8GuSlFfPzysQrWZQ==" - }, - "@types/prop-types": { - "version": "15.7.3", - "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.3.tgz", - "integrity": "sha512-KfRL3PuHmqQLOG+2tGpRO26Ctg+Cq1E01D2DMriKEATHgWLfeNDmq9e29Q9WIky0dQ3NPkd1mzYH8Lm936Z9qw==" - }, - "@types/react": { - "version": "18.2.9", - "resolved": "https://registry.npmjs.org/@types/react/-/react-18.2.9.tgz", - "integrity": "sha512-pL3JAesUkF7PEQGxh5XOwdXGV907te6m1/Qe1ERJLgomojS6Ne790QiA7GUl434JEkFA2aAaB6qJ5z4e1zJn/w==", - "requires": { - "@types/prop-types": "*", - "@types/scheduler": "*", - "csstype": "^3.0.2" - } - }, - "@types/react-dom": { - "version": "18.2.4", - "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.2.4.tgz", - "integrity": "sha512-G2mHoTMTL4yoydITgOGwWdWMVd8sNgyEP85xVmMKAPUBwQWm9wBPQUmvbeF4V3WBY1P7mmL4BkjQ0SqUpf1snw==", - "requires": { - "@types/react": "*" - } - }, - "@types/scheduler": { - "version": "0.16.1", - "resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.1.tgz", - "integrity": "sha512-EaCxbanVeyxDRTQBkdLb3Bvl/HK7PBK6UJjsSixB0iHKoWxE5uu2Q/DgtpOhPIojN0Zl1whvOd7PoHs2P0s5eA==" - }, - "@types/styled-components": { - "version": "5.1.26", - "resolved": "https://registry.npmjs.org/@types/styled-components/-/styled-components-5.1.26.tgz", - "integrity": "sha512-KuKJ9Z6xb93uJiIyxo/+ksS7yLjS1KzG6iv5i78dhVg/X3u5t1H7juRWqVmodIdz6wGVaIApo1u01kmFRdJHVw==", - "requires": { - "@types/hoist-non-react-statics": "*", - "@types/react": "*", - "csstype": "^3.0.2" - } - }, - "@types/wicg-file-system-access": { - "version": "2020.9.6", - "resolved": "https://registry.npmjs.org/@types/wicg-file-system-access/-/wicg-file-system-access-2020.9.6.tgz", - "integrity": "sha512-6hogE75Hl2Ov/jgp8ZhDaGmIF/q3J07GtXf8nCJCwKTHq7971po5+DId7grft09zG7plBwpF6ZU0yx9Du4/e1A==" - }, - "@webassemblyjs/ast": { - "version": "1.11.6", - "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.11.6.tgz", - "integrity": "sha512-IN1xI7PwOvLPgjcf180gC1bqn3q/QaOCwYUahIOhbYUu8KA/3tw2RT/T0Gidi1l7Hhj5D/INhJxiICObqpMu4Q==", - "requires": { - "@webassemblyjs/helper-numbers": "1.11.6", - "@webassemblyjs/helper-wasm-bytecode": "1.11.6" - } - }, - "@webassemblyjs/floating-point-hex-parser": { - "version": "1.11.6", - "resolved": "https://registry.npmjs.org/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.11.6.tgz", - "integrity": "sha512-ejAj9hfRJ2XMsNHk/v6Fu2dGS+i4UaXBXGemOfQ/JfQ6mdQg/WXtwleQRLLS4OvfDhv8rYnVwH27YJLMyYsxhw==" - }, - "@webassemblyjs/helper-api-error": { - "version": "1.11.6", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-api-error/-/helper-api-error-1.11.6.tgz", - "integrity": "sha512-o0YkoP4pVu4rN8aTJgAyj9hC2Sv5UlkzCHhxqWj8butaLvnpdc2jOwh4ewE6CX0txSfLn/UYaV/pheS2Txg//Q==" - }, - "@webassemblyjs/helper-buffer": { - "version": "1.11.6", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.11.6.tgz", - "integrity": "sha512-z3nFzdcp1mb8nEOFFk8DrYLpHvhKC3grJD2ardfKOzmbmJvEf/tPIqCY+sNcwZIY8ZD7IkB2l7/pqhUhqm7hLA==" - }, - "@webassemblyjs/helper-numbers": { - "version": "1.11.6", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-numbers/-/helper-numbers-1.11.6.tgz", - "integrity": "sha512-vUIhZ8LZoIWHBohiEObxVm6hwP034jwmc9kuq5GdHZH0wiLVLIPcMCdpJzG4C11cHoQ25TFIQj9kaVADVX7N3g==", - "requires": { - "@webassemblyjs/floating-point-hex-parser": "1.11.6", - "@webassemblyjs/helper-api-error": "1.11.6", - "@xtuc/long": "4.2.2" - } - }, - "@webassemblyjs/helper-wasm-bytecode": { - "version": "1.11.6", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.11.6.tgz", - "integrity": "sha512-sFFHKwcmBprO9e7Icf0+gddyWYDViL8bpPjJJl0WHxCdETktXdmtWLGVzoHbqUcY4Be1LkNfwTmXOJUFZYSJdA==" - }, - "@webassemblyjs/helper-wasm-section": { - "version": "1.11.6", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.11.6.tgz", - "integrity": "sha512-LPpZbSOwTpEC2cgn4hTydySy1Ke+XEu+ETXuoyvuyezHO3Kjdu90KK95Sh9xTbmjrCsUwvWwCOQQNta37VrS9g==", - "requires": { - "@webassemblyjs/ast": "1.11.6", - "@webassemblyjs/helper-buffer": "1.11.6", - "@webassemblyjs/helper-wasm-bytecode": "1.11.6", - "@webassemblyjs/wasm-gen": "1.11.6" - } - }, - "@webassemblyjs/ieee754": { - "version": "1.11.6", - "resolved": "https://registry.npmjs.org/@webassemblyjs/ieee754/-/ieee754-1.11.6.tgz", - "integrity": "sha512-LM4p2csPNvbij6U1f19v6WR56QZ8JcHg3QIJTlSwzFcmx6WSORicYj6I63f9yU1kEUtrpG+kjkiIAkevHpDXrg==", - "requires": { - "@xtuc/ieee754": "^1.2.0" - } - }, - "@webassemblyjs/leb128": { - "version": "1.11.6", - "resolved": "https://registry.npmjs.org/@webassemblyjs/leb128/-/leb128-1.11.6.tgz", - "integrity": "sha512-m7a0FhE67DQXgouf1tbN5XQcdWoNgaAuoULHIfGFIEVKA6tu/edls6XnIlkmS6FrXAquJRPni3ZZKjw6FSPjPQ==", - "requires": { - "@xtuc/long": "4.2.2" - } - }, - "@webassemblyjs/utf8": { - "version": "1.11.6", - "resolved": "https://registry.npmjs.org/@webassemblyjs/utf8/-/utf8-1.11.6.tgz", - "integrity": "sha512-vtXf2wTQ3+up9Zsg8sa2yWiQpzSsMyXj0qViVP6xKGCUT8p8YJ6HqI7l5eCnWx1T/FYdsv07HQs2wTFbbof/RA==" - }, - "@webassemblyjs/wasm-edit": { - "version": "1.11.6", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.11.6.tgz", - "integrity": "sha512-Ybn2I6fnfIGuCR+Faaz7YcvtBKxvoLV3Lebn1tM4o/IAJzmi9AWYIPWpyBfU8cC+JxAO57bk4+zdsTjJR+VTOw==", - "requires": { - "@webassemblyjs/ast": "1.11.6", - "@webassemblyjs/helper-buffer": "1.11.6", - "@webassemblyjs/helper-wasm-bytecode": "1.11.6", - "@webassemblyjs/helper-wasm-section": "1.11.6", - "@webassemblyjs/wasm-gen": "1.11.6", - "@webassemblyjs/wasm-opt": "1.11.6", - "@webassemblyjs/wasm-parser": "1.11.6", - "@webassemblyjs/wast-printer": "1.11.6" - } - }, - "@webassemblyjs/wasm-gen": { - "version": "1.11.6", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.11.6.tgz", - "integrity": "sha512-3XOqkZP/y6B4F0PBAXvI1/bky7GryoogUtfwExeP/v7Nzwo1QLcq5oQmpKlftZLbT+ERUOAZVQjuNVak6UXjPA==", - "requires": { - "@webassemblyjs/ast": "1.11.6", - "@webassemblyjs/helper-wasm-bytecode": "1.11.6", - "@webassemblyjs/ieee754": "1.11.6", - "@webassemblyjs/leb128": "1.11.6", - "@webassemblyjs/utf8": "1.11.6" - } - }, - "@webassemblyjs/wasm-opt": { - "version": "1.11.6", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.11.6.tgz", - "integrity": "sha512-cOrKuLRE7PCe6AsOVl7WasYf3wbSo4CeOk6PkrjS7g57MFfVUF9u6ysQBBODX0LdgSvQqRiGz3CXvIDKcPNy4g==", - "requires": { - "@webassemblyjs/ast": "1.11.6", - "@webassemblyjs/helper-buffer": "1.11.6", - "@webassemblyjs/wasm-gen": "1.11.6", - "@webassemblyjs/wasm-parser": "1.11.6" - } - }, - "@webassemblyjs/wasm-parser": { - "version": "1.11.6", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.11.6.tgz", - "integrity": "sha512-6ZwPeGzMJM3Dqp3hCsLgESxBGtT/OeCvCZ4TA1JUPYgmhAx38tTPR9JaKy0S5H3evQpO/h2uWs2j6Yc/fjkpTQ==", - "requires": { - "@webassemblyjs/ast": "1.11.6", - "@webassemblyjs/helper-api-error": "1.11.6", - "@webassemblyjs/helper-wasm-bytecode": "1.11.6", - "@webassemblyjs/ieee754": "1.11.6", - "@webassemblyjs/leb128": "1.11.6", - "@webassemblyjs/utf8": "1.11.6" - } - }, - "@webassemblyjs/wast-printer": { - "version": "1.11.6", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.11.6.tgz", - "integrity": "sha512-JM7AhRcE+yW2GWYaKeHL5vt4xqee5N2WcezptmgyhNS+ScggqcT1OtXykhAb13Sn5Yas0j2uv9tHgrjwvzAP4A==", - "requires": { - "@webassemblyjs/ast": "1.11.6", - "@xtuc/long": "4.2.2" - } - }, - "@webpack-cli/configtest": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/@webpack-cli/configtest/-/configtest-2.1.1.tgz", - "integrity": "sha512-wy0mglZpDSiSS0XHrVR+BAdId2+yxPSoJW8fsna3ZpYSlufjvxnP4YbKTCBZnNIcGN4r6ZPXV55X4mYExOfLmw==", - "requires": {} - }, - "@webpack-cli/info": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/@webpack-cli/info/-/info-2.0.2.tgz", - "integrity": "sha512-zLHQdI/Qs1UyT5UBdWNqsARasIA+AaF8t+4u2aS2nEpBQh2mWIVb8qAklq0eUENnC5mOItrIB4LiS9xMtph18A==", - "requires": {} - }, - "@webpack-cli/serve": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/@webpack-cli/serve/-/serve-2.0.5.tgz", - "integrity": "sha512-lqaoKnRYBdo1UgDX8uF24AfGMifWK19TxPmM5FHc2vAGxrJ/qtyUyFBWoY1tISZdelsQ5fBcOusifo5o5wSJxQ==", - "requires": {} - }, - "@xtuc/ieee754": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz", - "integrity": "sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==" - }, - "@xtuc/long": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/@xtuc/long/-/long-4.2.2.tgz", - "integrity": "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==" - }, - "abab": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/abab/-/abab-2.0.6.tgz", - "integrity": "sha512-j2afSsaIENvHZN2B8GOpF566vZ5WVk5opAiMTvWgaQT8DkbOqsTfvNAvHoRGU2zzP8cPoqys+xHTRDWW8L+/BA==" - }, - "acorn": { - "version": "8.8.2", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.8.2.tgz", - "integrity": "sha512-xjIYgE8HBrkpd/sJqOGNspf8uHG+NOHGOw6a/Urj8taM2EXfdNAH2oFcPeIFfsv3+kz/mJrS5VuMqbNLjCa2vw==" - }, - "acorn-import-assertions": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/acorn-import-assertions/-/acorn-import-assertions-1.9.0.tgz", - "integrity": "sha512-cmMwop9x+8KFhxvKrKfPYmN6/pKTYYHBqLa0DfvVZcKMJWNyWLnaqND7dx/qn66R7ewM1UX5XMaDVP5wlVTaVA==", - "requires": {} - }, - "ajv": { - "version": "8.12.0", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.12.0.tgz", - "integrity": "sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA==", - "requires": { - "fast-deep-equal": "^3.1.1", - "json-schema-traverse": "^1.0.0", - "require-from-string": "^2.0.2", - "uri-js": "^4.2.2" - } - }, - "ajv-formats": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.1.tgz", - "integrity": "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==", - "requires": { - "ajv": "^8.0.0" - } - }, - "ajv-keywords": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-5.1.0.tgz", - "integrity": "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==", - "requires": { - "fast-deep-equal": "^3.1.3" - } - }, - "ansi-styles": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", - "requires": { - "color-convert": "^1.9.0" - } - }, - "babel-loader": { - "version": "9.1.2", - "resolved": "https://registry.npmjs.org/babel-loader/-/babel-loader-9.1.2.tgz", - "integrity": "sha512-mN14niXW43tddohGl8HPu5yfQq70iUThvFL/4QzESA7GcZoC0eVOhvWdQ8+3UlSjaDE9MVtsW9mxDY07W7VpVA==", - "requires": { - "find-cache-dir": "^3.3.2", - "schema-utils": "^4.0.0" - } - }, - "babel-plugin-polyfill-corejs2": { - "version": "0.4.3", - "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.4.3.tgz", - "integrity": "sha512-bM3gHc337Dta490gg+/AseNB9L4YLHxq1nGKZZSHbhXv4aTYU2MD2cjza1Ru4S6975YLTaL1K8uJf6ukJhhmtw==", - "requires": { - "@babel/compat-data": "^7.17.7", - "@babel/helper-define-polyfill-provider": "^0.4.0", - "semver": "^6.1.1" - } - }, - "babel-plugin-polyfill-corejs3": { - "version": "0.8.1", - "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.8.1.tgz", - "integrity": "sha512-ikFrZITKg1xH6pLND8zT14UPgjKHiGLqex7rGEZCH2EvhsneJaJPemmpQaIZV5AL03II+lXylw3UmddDK8RU5Q==", - "requires": { - "@babel/helper-define-polyfill-provider": "^0.4.0", - "core-js-compat": "^3.30.1" - } - }, - "babel-plugin-polyfill-regenerator": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.5.0.tgz", - "integrity": "sha512-hDJtKjMLVa7Z+LwnTCxoDLQj6wdc+B8dun7ayF2fYieI6OzfuvcLMB32ihJZ4UhCBwNYGl5bg/x/P9cMdnkc2g==", - "requires": { - "@babel/helper-define-polyfill-provider": "^0.4.0" - } - }, - "babel-plugin-styled-components": { - "version": "2.0.7", - "resolved": "https://registry.npmjs.org/babel-plugin-styled-components/-/babel-plugin-styled-components-2.0.7.tgz", - "integrity": "sha512-i7YhvPgVqRKfoQ66toiZ06jPNA3p6ierpfUuEWxNF+fV27Uv5gxBkf8KZLHUCc1nFA9j6+80pYoIpqCeyW3/bA==", - "requires": { - "@babel/helper-annotate-as-pure": "^7.16.0", - "@babel/helper-module-imports": "^7.16.0", - "babel-plugin-syntax-jsx": "^6.18.0", - "lodash": "^4.17.11", - "picomatch": "^2.3.0" - } - }, - "babel-plugin-syntax-jsx": { - "version": "6.18.0", - "resolved": "https://registry.npmjs.org/babel-plugin-syntax-jsx/-/babel-plugin-syntax-jsx-6.18.0.tgz", - "integrity": "sha512-qrPaCSo9c8RHNRHIotaufGbuOBN8rtdC4QrrFFc43vyWCCz7Kl7GL1PGaXtMGQZUXrkCjNEgxDfmAuAabr/rlw==" - }, - "braces": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", - "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", - "requires": { - "fill-range": "^7.0.1" - } - }, - "browserslist": { - "version": "4.21.7", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.21.7.tgz", - "integrity": "sha512-BauCXrQ7I2ftSqd2mvKHGo85XR0u7Ru3C/Hxsy/0TkfCtjrmAbPdzLGasmoiBxplpDXlPvdjX9u7srIMfgasNA==", - "requires": { - "caniuse-lite": "^1.0.30001489", - "electron-to-chromium": "^1.4.411", - "node-releases": "^2.0.12", - "update-browserslist-db": "^1.0.11" - } - }, - "buffer-from": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", - "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==" - }, - "camelize": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/camelize/-/camelize-1.0.0.tgz", - "integrity": "sha1-FkpUg+Yw+kMh5a8HAg5TGDGyYJs=" - }, - "caniuse-lite": { - "version": "1.0.30001497", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001497.tgz", - "integrity": "sha512-I4/duVK4wL6rAK/aKZl3HXB4g+lIZvaT4VLAn2rCgJ38jVLb0lv2Xug6QuqmxXFVRJMF74SPPWPJ/1Sdm3vCzw==" - }, - "chalk": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "requires": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - } - }, - "chrome-trace-event": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.3.tgz", - "integrity": "sha512-p3KULyQg4S7NIHixdwbGX+nFHkoBiA4YQmyWtjb8XngSKV124nJmRysgAeujbUVb15vh+RvFUfCPqU7rXk+hZg==" - }, - "clone-deep": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/clone-deep/-/clone-deep-4.0.1.tgz", - "integrity": "sha512-neHB9xuzh/wk0dIHweyAXv2aPGZIVk3pLMe+/RNzINf17fe0OG96QroktYAUm7SM1PBnzTabaLboqqxDyMU+SQ==", - "requires": { - "is-plain-object": "^2.0.4", - "kind-of": "^6.0.2", - "shallow-clone": "^3.0.0" - } - }, - "color-convert": { - "version": "1.9.3", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", - "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", - "requires": { - "color-name": "1.1.3" - } - }, - "color-name": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==" - }, - "colorette": { - "version": "2.0.19", - "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.19.tgz", - "integrity": "sha512-3tlv/dIP7FWvj3BsbHrGLJ6l/oKh1O3TcgBqMn+yyCagOxc23fyzDS6HypQbgxWbkpDnf52p1LuR4eWDQ/K9WQ==" - }, - "commander": { - "version": "2.20.3", - "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", - "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==" - }, - "commondir": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz", - "integrity": "sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg==" - }, - "convert-source-map": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.7.0.tgz", - "integrity": "sha512-4FJkXzKXEDB1snCFZlLP4gpC3JILicCpGbzG9f9G7tGqGCzETQ2hWPrcinA9oU4wtf2biUaEH5065UnMeR33oA==", - "requires": { - "safe-buffer": "~5.1.1" - } - }, - "core-js": { - "version": "3.30.2", - "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.30.2.tgz", - "integrity": "sha512-uBJiDmwqsbJCWHAwjrx3cvjbMXP7xD72Dmsn5LOJpiRmE3WbBbN5rCqQ2Qh6Ek6/eOrjlWngEynBWo4VxerQhg==" - }, - "core-js-compat": { - "version": "3.30.2", - "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.30.2.tgz", - "integrity": "sha512-nriW1nuJjUgvkEjIot1Spwakz52V9YkYHZAQG6A1eCgC8AA1p0zngrQEP9R0+V6hji5XilWKG1Bd0YRppmGimA==", - "requires": { - "browserslist": "^4.21.5" - } - }, - "cross-spawn": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", - "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", - "requires": { - "path-key": "^3.1.0", - "shebang-command": "^2.0.0", - "which": "^2.0.1" - } - }, - "css-color-keywords": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/css-color-keywords/-/css-color-keywords-1.0.0.tgz", - "integrity": "sha1-/qJhbcZ2spYmhrOvjb2+GAskTgU=" - }, - "css-to-react-native": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/css-to-react-native/-/css-to-react-native-3.0.0.tgz", - "integrity": "sha512-Ro1yETZA813eoyUp2GDBhG2j+YggidUmzO1/v9eYBKR2EHVEniE2MI/NqpTQ954BMpTPZFsGNPm46qFB9dpaPQ==", - "requires": { - "camelize": "^1.0.0", - "css-color-keywords": "^1.0.0", - "postcss-value-parser": "^4.0.2" - } - }, - "csstype": { - "version": "3.0.8", - "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.0.8.tgz", - "integrity": "sha512-jXKhWqXPmlUeoQnF/EhTtTl4C9SnrxSH/jZUih3jmO6lBKr99rP3/+FmrMj4EFpOXzMtXHAZkd3x0E6h6Fgflw==" - }, - "debug": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz", - "integrity": "sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==", - "requires": { - "ms": "2.1.2" - } - }, - "electron-to-chromium": { - "version": "1.4.427", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.427.tgz", - "integrity": "sha512-HK3r9l+Jm8dYAm1ctXEWIC+hV60zfcjS9UA5BDlYvnI5S7PU/yytjpvSrTNrSSRRkuu3tDyZhdkwIczh+0DWaw==" - }, - "enhanced-resolve": { - "version": "5.14.1", - "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.14.1.tgz", - "integrity": "sha512-Vklwq2vDKtl0y/vtwjSesgJ5MYS7Etuk5txS8VdKL4AOS1aUlD96zqIfsOSLQsdv3xgMRbtkWM8eG9XDfKUPow==", - "requires": { - "graceful-fs": "^4.2.4", - "tapable": "^2.2.0" - } - }, - "envinfo": { - "version": "7.8.1", - "resolved": "https://registry.npmjs.org/envinfo/-/envinfo-7.8.1.tgz", - "integrity": "sha512-/o+BXHmB7ocbHEAs6F2EnG0ogybVVUdkRunTT2glZU9XAaGmhqskrvKwqXuDfNjEO0LZKWdejEEpnq8aM0tOaw==" - }, - "es-module-lexer": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.2.1.tgz", - "integrity": "sha512-9978wrXM50Y4rTMmW5kXIC09ZdXQZqkE4mxhwkd8VbzsGkXGPgV4zWuqQJgCEzYngdo2dYDa0l8xhX4fkSwJSg==" - }, - "escalade": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", - "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==" - }, - "escape-string-regexp": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==" - }, - "eslint-scope": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", - "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", - "requires": { - "esrecurse": "^4.3.0", - "estraverse": "^4.1.1" - } - }, - "esrecurse": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", - "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", - "requires": { - "estraverse": "^5.2.0" - }, - "dependencies": { - "estraverse": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.2.0.tgz", - "integrity": "sha512-BxbNGGNm0RyRYvUdHpIwv9IWzeM9XClbOxwoATuFdOE7ZE6wHL+HQ5T8hoPM+zHvmKzzsEqhgy0GrQ5X13afiQ==" - } - } - }, - "estraverse": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", - "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==" - }, - "esutils": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", - "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==" - }, - "events": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", - "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==" - }, - "fast-deep-equal": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", - "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==" - }, - "fast-json-stable-stringify": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", - "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==" - }, - "fastest-levenshtein": { - "version": "1.0.12", - "resolved": "https://registry.npmjs.org/fastest-levenshtein/-/fastest-levenshtein-1.0.12.tgz", - "integrity": "sha512-On2N+BpYJ15xIC974QNVuYGMOlEVt4s0EOI3wwMqOmK1fdDY+FN/zltPV8vosq4ad4c/gJ1KHScUn/6AWIgiow==" - }, - "fill-range": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", - "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", - "requires": { - "to-regex-range": "^5.0.1" - } - }, - "find-cache-dir": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-3.3.2.tgz", - "integrity": "sha512-wXZV5emFEjrridIgED11OoUKLxiYjAcqot/NJdAkOhlJ+vGzwhOAfcG5OX1jP+S0PcjEn8bdMJv+g2jwQ3Onig==", - "requires": { - "commondir": "^1.0.1", - "make-dir": "^3.0.2", - "pkg-dir": "^4.1.0" - } - }, - "find-up": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", - "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", - "requires": { - "locate-path": "^5.0.0", - "path-exists": "^4.0.0" - } - }, - "function-bind": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", - "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==" - }, - "gensync": { - "version": "1.0.0-beta.2", - "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", - "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==" - }, - "glob-to-regexp": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz", - "integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==" - }, - "globals": { - "version": "11.12.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", - "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==" - }, - "graceful-fs": { - "version": "4.2.10", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.10.tgz", - "integrity": "sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA==" - }, - "has": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", - "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", - "requires": { - "function-bind": "^1.1.1" - } - }, - "has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=" - }, - "hoist-non-react-statics": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz", - "integrity": "sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==", - "requires": { - "react-is": "^16.7.0" - }, - "dependencies": { - "react-is": { - "version": "16.13.1", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", - "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==" - } - } - }, - "iconv-lite": { - "version": "0.6.3", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", - "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", - "requires": { - "safer-buffer": ">= 2.1.2 < 3.0.0" - } - }, - "immutable": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/immutable/-/immutable-4.3.0.tgz", - "integrity": "sha512-0AOCmOip+xgJwEVTQj1EfiDDOkPmuyllDuTuEX+DDXUgapLAsBIfkg3sxCYyCEA8mQqZrrxPUGjcOQ2JS3WLkg==" - }, - "import-local": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/import-local/-/import-local-3.0.2.tgz", - "integrity": "sha512-vjL3+w0oulAVZ0hBHnxa/Nm5TAurf9YLQJDhqRZyqb+VKGOB6LU8t9H1Nr5CIo16vh9XfJTOoHwU0B71S557gA==", - "requires": { - "pkg-dir": "^4.2.0", - "resolve-cwd": "^3.0.0" - } - }, - "interpret": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/interpret/-/interpret-3.1.1.tgz", - "integrity": "sha512-6xwYfHbajpoF0xLW+iwLkhwgvLoZDfjYfoFNu8ftMoXINzwuymNLd9u/KmwtdT2GbR+/Cz66otEGEVVUHX9QLQ==" - }, - "is-core-module": { - "version": "2.12.1", - "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.12.1.tgz", - "integrity": "sha512-Q4ZuBAe2FUsKtyQJoQHlvP8OvBERxO3jEmy1I7hcRXcJBGGHFh/aJBswbXuS9sgrDH2QUO8ilkwNPHvHMd8clg==", - "requires": { - "has": "^1.0.3" - } - }, - "is-number": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", - "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==" - }, - "is-plain-object": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", - "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==", - "requires": { - "isobject": "^3.0.1" - } - }, - "isexe": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==" - }, - "isobject": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", - "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=" - }, - "jest-worker": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-27.5.1.tgz", - "integrity": "sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==", - "requires": { - "@types/node": "*", - "merge-stream": "^2.0.0", - "supports-color": "^8.0.0" - }, - "dependencies": { - "has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==" - }, - "supports-color": { - "version": "8.1.1", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", - "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", - "requires": { - "has-flag": "^4.0.0" - } - } - } - }, - "js-tokens": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", - "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==" - }, - "jsesc": { - "version": "2.5.2", - "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", - "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==" - }, - "json-parse-even-better-errors": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", - "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==" - }, - "json-schema-traverse": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", - "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==" - }, - "json5": { - "version": "2.2.3", - "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", - "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==" - }, - "kind-of": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", - "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==" - }, - "loader-runner": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.2.0.tgz", - "integrity": "sha512-92+huvxMvYlMzMt0iIOukcwYBFpkYJdpl2xsZ7LrlayO7E8SOv+JJUEK17B/dJIHAOLMfh2dZZ/Y18WgmGtYNw==" - }, - "locate-path": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", - "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", - "requires": { - "p-locate": "^4.1.0" - } - }, - "lodash": { - "version": "4.17.21", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", - "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" - }, - "lodash.debounce": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz", - "integrity": "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==" - }, - "loose-envify": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", - "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", - "requires": { - "js-tokens": "^3.0.0 || ^4.0.0" - } - }, - "lru-cache": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", - "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", - "requires": { - "yallist": "^4.0.0" - } - }, - "make-dir": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", - "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", - "requires": { - "semver": "^6.0.0" - } - }, - "merge-stream": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", - "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==" - }, - "micromatch": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.4.tgz", - "integrity": "sha512-pRmzw/XUcwXGpD9aI9q/0XOwLNygjETJ8y0ao0wdqprrzDa4YnxLcz7fQRZr8voh8V10kGhABbNcHVk5wHgWwg==", - "requires": { - "braces": "^3.0.1", - "picomatch": "^2.2.3" - } - }, - "mime-db": { - "version": "1.48.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.48.0.tgz", - "integrity": "sha512-FM3QwxV+TnZYQ2aRqhlKBMHxk10lTbMt3bBkMAp54ddrNeVSfcQYOOKuGuy3Ddrm38I04If834fOUSq1yzslJQ==" - }, - "mime-types": { - "version": "2.1.31", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.31.tgz", - "integrity": "sha512-XGZnNzm3QvgKxa8dpzyhFTHmpP3l5YNusmne07VUOXxou9CqUqYa/HBy124RqtVh/O2pECas/MOcsDgpilPOPg==", - "requires": { - "mime-db": "1.48.0" - } - }, - "ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" - }, - "neo-async": { - "version": "2.6.2", - "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", - "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==" - }, - "node-releases": { - "version": "2.0.12", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.12.tgz", - "integrity": "sha512-QzsYKWhXTWx8h1kIvqfnC++o0pEmpRQA/aenALsL2F4pqNVr7YzcdMlDij5WBnwftRbJCNJL/O7zdKaxKPHqgQ==" - }, - "p-limit": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", - "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", - "requires": { - "p-try": "^2.0.0" - } - }, - "p-locate": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", - "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", - "requires": { - "p-limit": "^2.2.0" - } - }, - "p-try": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", - "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==" - }, - "pako": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/pako/-/pako-2.1.0.tgz", - "integrity": "sha512-w+eufiZ1WuJYgPXbV/PO3NCMEc3xqylkKHzp8bxp1uW4qaSNQUkwmLLEc3kKsfz8lpV1F8Ht3U1Cm+9Srog2ug==" - }, - "path-exists": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", - "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==" - }, - "path-key": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", - "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==" - }, - "path-parse": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", - "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==" - }, - "picocolors": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", - "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==" - }, - "picomatch": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.0.tgz", - "integrity": "sha512-lY1Q/PiJGC2zOv/z391WOTD+Z02bCgsFfvxoXXf6h7kv9o+WmsmzYqrAwY63sNgOxE4xEdq0WyUnXfKeBrSvYw==" - }, - "pkg-dir": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", - "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", - "requires": { - "find-up": "^4.0.0" - } - }, - "postcss-value-parser": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.1.0.tgz", - "integrity": "sha512-97DXOFbQJhk71ne5/Mt6cOu6yxsSfM0QGQyl0L25Gca4yGWEGJaig7l7gbCX623VqTBNGLRLaVUCnNkcedlRSQ==" - }, - "punycode": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.0.tgz", - "integrity": "sha512-rRV+zQD8tVFys26lAGR9WUuS4iUAngJScM+ZRSKtvl5tKeZ2t5bvdNFdNHBW9FWR4guGHlgmsZ1G7BSm2wTbuA==" - }, - "randombytes": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", - "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", - "requires": { - "safe-buffer": "^5.1.0" - } - }, - "react": { - "version": "18.2.0", - "resolved": "https://registry.npmjs.org/react/-/react-18.2.0.tgz", - "integrity": "sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ==", - "requires": { - "loose-envify": "^1.1.0" - } - }, - "react-dom": { - "version": "18.2.0", - "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.2.0.tgz", - "integrity": "sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g==", - "requires": { - "loose-envify": "^1.1.0", - "scheduler": "^0.23.0" - } - }, - "react-is": { - "version": "17.0.2", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", - "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==", - "peer": true - }, - "rechoir": { - "version": "0.8.0", - "resolved": "https://registry.npmjs.org/rechoir/-/rechoir-0.8.0.tgz", - "integrity": "sha512-/vxpCXddiX8NGfGO/mTafwjq4aFa/71pvamip0++IQk3zG8cbCj0fifNPrjjF1XMXUne91jL9OoxmdykoEtifQ==", - "requires": { - "resolve": "^1.20.0" - } - }, - "regenerate": { - "version": "1.4.2", - "resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.4.2.tgz", - "integrity": "sha512-zrceR/XhGYU/d/opr2EKO7aRHUeiBI8qjtfHqADTwZd6Szfy16la6kqD0MIUs5z5hx6AaKa+PixpPrR289+I0A==" - }, - "regenerate-unicode-properties": { - "version": "10.1.0", - "resolved": "https://registry.npmjs.org/regenerate-unicode-properties/-/regenerate-unicode-properties-10.1.0.tgz", - "integrity": "sha512-d1VudCLoIGitcU/hEg2QqvyGZQmdC0Lf8BqdOMXGFSvJP4bNV1+XqbPQeHHLD51Jh4QJJ225dlIFvY4Ly6MXmQ==", - "requires": { - "regenerate": "^1.4.2" - } - }, - "regenerator-runtime": { - "version": "0.13.11", - "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz", - "integrity": "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==" - }, - "regenerator-transform": { - "version": "0.15.1", - "resolved": "https://registry.npmjs.org/regenerator-transform/-/regenerator-transform-0.15.1.tgz", - "integrity": "sha512-knzmNAcuyxV+gQCufkYcvOqX/qIIfHLv0u5x79kRxuGojfYVky1f15TzZEu2Avte8QGepvUNTnLskf8E6X6Vyg==", - "requires": { - "@babel/runtime": "^7.8.4" - } - }, - "regexpu-core": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/regexpu-core/-/regexpu-core-5.3.2.tgz", - "integrity": "sha512-RAM5FlZz+Lhmo7db9L298p2vHP5ZywrVXmVXpmAD9GuL5MPH6t9ROw1iA/wfHkQ76Qe7AaPF0nGuim96/IrQMQ==", - "requires": { - "@babel/regjsgen": "^0.8.0", - "regenerate": "^1.4.2", - "regenerate-unicode-properties": "^10.1.0", - "regjsparser": "^0.9.1", - "unicode-match-property-ecmascript": "^2.0.0", - "unicode-match-property-value-ecmascript": "^2.1.0" - } - }, - "regjsparser": { - "version": "0.9.1", - "resolved": "https://registry.npmjs.org/regjsparser/-/regjsparser-0.9.1.tgz", - "integrity": "sha512-dQUtn90WanSNl+7mQKcXAgZxvUe7Z0SqXlgzv0za4LwiUhyzBC58yQO3liFoUgu8GiJVInAhJjkj1N0EtQ5nkQ==", - "requires": { - "jsesc": "~0.5.0" - }, - "dependencies": { - "jsesc": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-0.5.0.tgz", - "integrity": "sha512-uZz5UnB7u4T9LvwmFqXii7pZSouaRPorGs5who1Ip7VO0wxanFvBL7GkM6dTHlgX+jhBApRetaWpnDabOeTcnA==" - } - } - }, - "require-from-string": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", - "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==" - }, - "resolve": { - "version": "1.22.2", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.2.tgz", - "integrity": "sha512-Sb+mjNHOULsBv818T40qSPeRiuWLyaGMa5ewydRLFimneixmVy2zdivRl+AF6jaYPC8ERxGDmFSiqui6SfPd+g==", - "requires": { - "is-core-module": "^2.11.0", - "path-parse": "^1.0.7", - "supports-preserve-symlinks-flag": "^1.0.0" - } - }, - "resolve-cwd": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-3.0.0.tgz", - "integrity": "sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==", - "requires": { - "resolve-from": "^5.0.0" - } - }, - "resolve-from": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", - "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==" - }, - "safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" - }, - "safer-buffer": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", - "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" - }, - "scheduler": { - "version": "0.23.0", - "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.0.tgz", - "integrity": "sha512-CtuThmgHNg7zIZWAXi3AsyIzA3n4xx7aNyjwC2VJldO2LMVDhFK+63xGqq6CsJH4rTAt6/M+N4GhZiDYPx9eUw==", - "requires": { - "loose-envify": "^1.1.0" - } - }, - "schema-utils": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.1.0.tgz", - "integrity": "sha512-Jw+GZVbP5IggB2WAn6UHI02LBwGmsIeYN/lNbSMZyDziQ7jmtAUrqKqDja+W89YHVs+KL/3IkIMltAklqB1vAw==", - "requires": { - "@types/json-schema": "^7.0.9", - "ajv": "^8.9.0", - "ajv-formats": "^2.1.1", - "ajv-keywords": "^5.1.0" - } - }, - "semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==" - }, - "serialize-javascript": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.1.tgz", - "integrity": "sha512-owoXEFjWRllis8/M1Q+Cw5k8ZH40e3zhp/ovX+Xr/vi1qj6QesbyXXViFbpNvWvPNAD62SutwEXavefrLJWj7w==", - "requires": { - "randombytes": "^2.1.0" - } - }, - "shallow-clone": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/shallow-clone/-/shallow-clone-3.0.1.tgz", - "integrity": "sha512-/6KqX+GVUdqPuPPd2LxDDxzX6CAbjJehAAOKlNpqqUpAqPM6HeL8f+o3a+JsyGjn2lv0WY8UsTgUJjU9Ok55NA==", - "requires": { - "kind-of": "^6.0.2" - } - }, - "shallowequal": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/shallowequal/-/shallowequal-1.1.0.tgz", - "integrity": "sha512-y0m1JoUZSlPAjXVtPPW70aZWfIL/dSP7AFkRnniLCrK/8MDKog3TySTBmckD+RObVxH0v4Tox67+F14PdED2oQ==" - }, - "shebang-command": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", - "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", - "requires": { - "shebang-regex": "^3.0.0" - } - }, - "shebang-regex": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", - "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==" - }, - "source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==" - }, - "source-map-js": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz", - "integrity": "sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==" - }, - "source-map-loader": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/source-map-loader/-/source-map-loader-4.0.1.tgz", - "integrity": "sha512-oqXpzDIByKONVY8g1NUPOTQhe0UTU5bWUl32GSkqK2LjJj0HmwTMVKxcUip0RgAYhY1mqgOxjbQM48a0mmeNfA==", - "requires": { - "abab": "^2.0.6", - "iconv-lite": "^0.6.3", - "source-map-js": "^1.0.2" - } - }, - "source-map-support": { - "version": "0.5.21", - "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", - "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", - "requires": { - "buffer-from": "^1.0.0", - "source-map": "^0.6.0" - } - }, - "styled-components": { - "version": "5.3.11", - "resolved": "https://registry.npmjs.org/styled-components/-/styled-components-5.3.11.tgz", - "integrity": "sha512-uuzIIfnVkagcVHv9nE0VPlHPSCmXIUGKfJ42LNjxCCTDTL5sgnJ8Z7GZBq0EnLYGln77tPpEpExt2+qa+cZqSw==", - "requires": { - "@babel/helper-module-imports": "^7.0.0", - "@babel/traverse": "^7.4.5", - "@emotion/is-prop-valid": "^1.1.0", - "@emotion/stylis": "^0.8.4", - "@emotion/unitless": "^0.7.4", - "babel-plugin-styled-components": ">= 1.12.0", - "css-to-react-native": "^3.0.0", - "hoist-non-react-statics": "^3.0.0", - "shallowequal": "^1.1.0", - "supports-color": "^5.5.0" - } - }, - "supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "requires": { - "has-flag": "^3.0.0" - } - }, - "supports-preserve-symlinks-flag": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", - "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==" - }, - "tapable": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.1.tgz", - "integrity": "sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==" - }, - "terser": { - "version": "5.17.7", - "resolved": "https://registry.npmjs.org/terser/-/terser-5.17.7.tgz", - "integrity": "sha512-/bi0Zm2C6VAexlGgLlVxA0P2lru/sdLyfCVaRMfKVo9nWxbmz7f/sD8VPybPeSUJaJcwmCJis9pBIhcVcG1QcQ==", - "requires": { - "@jridgewell/source-map": "^0.3.3", - "acorn": "^8.8.2", - "commander": "^2.20.0", - "source-map-support": "~0.5.20" - } - }, - "terser-webpack-plugin": { - "version": "5.3.9", - "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.9.tgz", - "integrity": "sha512-ZuXsqE07EcggTWQjXUj+Aot/OMcD0bMKGgF63f7UxYcu5/AJF53aIpK1YoP5xR9l6s/Hy2b+t1AM0bLNPRuhwA==", - "requires": { - "@jridgewell/trace-mapping": "^0.3.17", - "jest-worker": "^27.4.5", - "schema-utils": "^3.1.1", - "serialize-javascript": "^6.0.1", - "terser": "^5.16.8" - }, - "dependencies": { - "ajv": { - "version": "6.12.6", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", - "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", - "requires": { - "fast-deep-equal": "^3.1.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" - } - }, - "ajv-keywords": { - "version": "3.5.2", - "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", - "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==", - "requires": {} - }, - "json-schema-traverse": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==" - }, - "schema-utils": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.2.0.tgz", - "integrity": "sha512-0zTyLGyDJYd/MBxG1AhJkKa6fpEBds4OQO2ut0w7OYG+ZGhGea09lijvzsqegYSik88zc7cUtIlnnO+/BvD6gQ==", - "requires": { - "@types/json-schema": "^7.0.8", - "ajv": "^6.12.5", - "ajv-keywords": "^3.5.2" - } - } - } - }, - "to-fast-properties": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", - "integrity": "sha1-3F5pjL0HkmW8c+A3doGk5Og/YW4=" - }, - "to-regex-range": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", - "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", - "requires": { - "is-number": "^7.0.0" - } - }, - "ts-loader": { - "version": "9.4.3", - "resolved": "https://registry.npmjs.org/ts-loader/-/ts-loader-9.4.3.tgz", - "integrity": "sha512-n3hBnm6ozJYzwiwt5YRiJZkzktftRpMiBApHaJPoWLA+qetQBAXkHqCLM6nwSdRDimqVtA5ocIkcTRLMTt7yzA==", - "requires": { - "chalk": "^4.1.0", - "enhanced-resolve": "^5.0.0", - "micromatch": "^4.0.0", - "semver": "^7.3.4" - }, - "dependencies": { - "ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "requires": { - "color-convert": "^2.0.1" - } - }, - "chalk": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.1.tgz", - "integrity": "sha512-diHzdDKxcU+bAsUboHLPEDQiw0qEe0qd7SYUn3HgcFlWgbDcfLGswOHYeGrHKzG9z6UYf01d9VFMfZxPM1xZSg==", - "requires": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - } - }, - "color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "requires": { - "color-name": "~1.1.4" - } - }, - "color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" - }, - "has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==" - }, - "semver": { - "version": "7.3.5", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.5.tgz", - "integrity": "sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ==", - "requires": { - "lru-cache": "^6.0.0" - } - }, - "supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "requires": { - "has-flag": "^4.0.0" - } - } - } - }, - "typescript": { - "version": "5.1.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.1.3.tgz", - "integrity": "sha512-XH627E9vkeqhlZFQuL+UsyAXEnibT0kWR2FWONlr4sTjvxyJYnyefgrkyECLzM5NenmKzRAy2rR/OlYLA1HkZw==" - }, - "unicode-canonical-property-names-ecmascript": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-2.0.0.tgz", - "integrity": "sha512-yY5PpDlfVIU5+y/BSCxAJRBIS1Zc2dDG3Ujq+sR0U+JjUevW2JhocOF+soROYDSaAezOzOKuyyixhD6mBknSmQ==" - }, - "unicode-match-property-ecmascript": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/unicode-match-property-ecmascript/-/unicode-match-property-ecmascript-2.0.0.tgz", - "integrity": "sha512-5kaZCrbp5mmbz5ulBkDkbY0SsPOjKqVS35VpL9ulMPfSl0J0Xsm+9Evphv9CoIZFwre7aJoa94AY6seMKGVN5Q==", - "requires": { - "unicode-canonical-property-names-ecmascript": "^2.0.0", - "unicode-property-aliases-ecmascript": "^2.0.0" - } - }, - "unicode-match-property-value-ecmascript": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/unicode-match-property-value-ecmascript/-/unicode-match-property-value-ecmascript-2.1.0.tgz", - "integrity": "sha512-qxkjQt6qjg/mYscYMC0XKRn3Rh0wFPlfxB0xkt9CfyTvpX1Ra0+rAmdX2QyAobptSEvuy4RtpPRui6XkV+8wjA==" - }, - "unicode-property-aliases-ecmascript": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/unicode-property-aliases-ecmascript/-/unicode-property-aliases-ecmascript-2.1.0.tgz", - "integrity": "sha512-6t3foTQI9qne+OZoVQB/8x8rk2k1eVy1gRXhV3oFQ5T6R1dqQ1xtin3XqSlx3+ATBkliTaR/hHyJBm+LVPNM8w==" - }, - "update-browserslist-db": { - "version": "1.0.11", - "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.11.tgz", - "integrity": "sha512-dCwEFf0/oT85M1fHBg4F0jtLwJrutGoHSQXCh7u4o2t1drG+c0a9Flnqww6XUKSfQMPpJBRjU8d4RXB09qtvaA==", - "requires": { - "escalade": "^3.1.1", - "picocolors": "^1.0.0" - } - }, - "uri-js": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", - "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", - "requires": { - "punycode": "^2.1.0" - } - }, - "watchpack": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.0.tgz", - "integrity": "sha512-Lcvm7MGST/4fup+ifyKi2hjyIAwcdI4HRgtvTpIUxBRhB+RFtUh8XtDOxUfctVCnhVi+QQj49i91OyvzkJl6cg==", - "requires": { - "glob-to-regexp": "^0.4.1", - "graceful-fs": "^4.1.2" - } - }, - "webpack": { - "version": "5.86.0", - "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.86.0.tgz", - "integrity": "sha512-3BOvworZ8SO/D4GVP+GoRC3fVeg5MO4vzmq8TJJEkdmopxyazGDxN8ClqN12uzrZW9Tv8EED8v5VSb6Sqyi0pg==", - "requires": { - "@types/eslint-scope": "^3.7.3", - "@types/estree": "^1.0.0", - "@webassemblyjs/ast": "^1.11.5", - "@webassemblyjs/wasm-edit": "^1.11.5", - "@webassemblyjs/wasm-parser": "^1.11.5", - "acorn": "^8.7.1", - "acorn-import-assertions": "^1.9.0", - "browserslist": "^4.14.5", - "chrome-trace-event": "^1.0.2", - "enhanced-resolve": "^5.14.1", - "es-module-lexer": "^1.2.1", - "eslint-scope": "5.1.1", - "events": "^3.2.0", - "glob-to-regexp": "^0.4.1", - "graceful-fs": "^4.2.9", - "json-parse-even-better-errors": "^2.3.1", - "loader-runner": "^4.2.0", - "mime-types": "^2.1.27", - "neo-async": "^2.6.2", - "schema-utils": "^3.1.2", - "tapable": "^2.1.1", - "terser-webpack-plugin": "^5.3.7", - "watchpack": "^2.4.0", - "webpack-sources": "^3.2.3" - }, - "dependencies": { - "ajv": { - "version": "6.12.6", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", - "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", - "requires": { - "fast-deep-equal": "^3.1.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" - } - }, - "ajv-keywords": { - "version": "3.5.2", - "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", - "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==", - "requires": {} - }, - "json-schema-traverse": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==" - }, - "schema-utils": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.2.0.tgz", - "integrity": "sha512-0zTyLGyDJYd/MBxG1AhJkKa6fpEBds4OQO2ut0w7OYG+ZGhGea09lijvzsqegYSik88zc7cUtIlnnO+/BvD6gQ==", - "requires": { - "@types/json-schema": "^7.0.8", - "ajv": "^6.12.5", - "ajv-keywords": "^3.5.2" - } - } - } - }, - "webpack-cli": { - "version": "5.1.4", - "resolved": "https://registry.npmjs.org/webpack-cli/-/webpack-cli-5.1.4.tgz", - "integrity": "sha512-pIDJHIEI9LR0yxHXQ+Qh95k2EvXpWzZ5l+d+jIo+RdSm9MiHfzazIxwwni/p7+x4eJZuvG1AJwgC4TNQ7NRgsg==", - "requires": { - "@discoveryjs/json-ext": "^0.5.0", - "@webpack-cli/configtest": "^2.1.1", - "@webpack-cli/info": "^2.0.2", - "@webpack-cli/serve": "^2.0.5", - "colorette": "^2.0.14", - "commander": "^10.0.1", - "cross-spawn": "^7.0.3", - "envinfo": "^7.7.3", - "fastest-levenshtein": "^1.0.12", - "import-local": "^3.0.2", - "interpret": "^3.1.1", - "rechoir": "^0.8.0", - "webpack-merge": "^5.7.3" - }, - "dependencies": { - "commander": { - "version": "10.0.1", - "resolved": "https://registry.npmjs.org/commander/-/commander-10.0.1.tgz", - "integrity": "sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==" - } - } - }, - "webpack-merge": { - "version": "5.7.3", - "resolved": "https://registry.npmjs.org/webpack-merge/-/webpack-merge-5.7.3.tgz", - "integrity": "sha512-6/JUQv0ELQ1igjGDzHkXbVDRxkfA57Zw7PfiupdLFJYrgFqY5ZP8xxbpp2lU3EPwYx89ht5Z/aDkD40hFCm5AA==", - "requires": { - "clone-deep": "^4.0.1", - "wildcard": "^2.0.0" - } - }, - "webpack-sources": { - "version": "3.2.3", - "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.2.3.tgz", - "integrity": "sha512-/DyMEOrDgLKKIG0fmvtz+4dUX/3Ghozwgm6iPp8KRhvn+eQf9+Q7GWxVNMk3+uCPWfdXYC4ExGBckIXdFEfH1w==" - }, - "which": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", - "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", - "requires": { - "isexe": "^2.0.0" - } - }, - "wildcard": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/wildcard/-/wildcard-2.0.0.tgz", - "integrity": "sha512-JcKqAHLPxcdb9KM49dufGXn2x3ssnfjbcaQdLlfZsL9rH9wgDQjUtDxbo8NE0F6SFvydeu1VhZe7hZuHsB2/pw==" - }, - "yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" - } - } -} diff --git a/package.json b/package.json deleted file mode 100644 index 074a16f..0000000 --- a/package.json +++ /dev/null @@ -1,44 +0,0 @@ -{ - "name": "theorytracker", - "version": "0.1.0", - "description": "", - "author": "hlorenzi", - "private": true, - "main": "index.js", - "type": "module", - "scripts": { - "build": "webpack --config webpack.config.cjs", - "dev": "webpack --config webpack.config.cjs --watch --mode development", - "start": "http-server -p 80", - "test": "node ./build/test.js" - }, - "browserslist": [ - "> 2%", - "not dead", - "not ie 11" - ], - "dependencies": { - "@babel/core": "^7.22.5", - "@babel/plugin-proposal-class-properties": "^7.18.6", - "@babel/preset-env": "^7.22.5", - "@babel/preset-react": "^7.22.5", - "@types/node": "^20.2.5", - "@types/react": "^18.2.9", - "@types/react-dom": "^18.2.4", - "@types/styled-components": "^5.1.26", - "@types/wicg-file-system-access": "^2020.9.6", - "babel-loader": "^9.1.2", - "core-js": "^3.30.2", - "immutable": "^4.3.0", - "pako": "^2.1.0", - "react": "^18.2.0", - "react-dom": "^18.2.0", - "regenerator-runtime": "^0.13.11", - "source-map-loader": "^4.0.1", - "styled-components": "^5.3.11", - "ts-loader": "^9.4.3", - "typescript": "^5.1.3", - "webpack": "^5.86.0", - "webpack-cli": "^5.1.4" - } -} diff --git a/run_local_server.bat b/run_local_server.bat deleted file mode 100644 index ebb7225..0000000 --- a/run_local_server.bat +++ /dev/null @@ -1,5 +0,0 @@ -where /q http-server - -IF ERRORLEVEL 1 (npm i -g http-server) - -http-server -p 80 \ No newline at end of file diff --git a/src/App.tsx b/src/App.tsx deleted file mode 100644 index d5a96c4..0000000 --- a/src/App.tsx +++ /dev/null @@ -1,140 +0,0 @@ -import React from "react" -import * as Dockable from "./dockable" -import * as Command from "./command" -import * as Project from "./project" -import * as Playback from "./playback" -import * as Prefs from "./prefs" -import * as Popup from "./popup" -import * as Menubar from "./menubar" -import * as UI from "./ui" -import { useRefState } from "./util/refState" -import PlaybackToolbar from "./PlaybackToolbar" -import MenuFile from "./MenuFile" -import MenuEdit from "./MenuEdit" -import MenuWindow from "./MenuWindow" -import "./types" - - -let initializedGlobals = false - - -export default function App() -{ - if (!initializedGlobals) - { - initializedGlobals = true - Prefs.initGlobal() - Project.initGlobal() - Playback.initGlobal() - Dockable.initGlobal() - Popup.initGlobal() - } - - Project.useGlobal() - Playback.useGlobal() - Dockable.useGlobal() - Popup.useGlobal() - - const [version, setVersion] = React.useState("") - - React.useEffect(() => - { - ;(async () => - { - const versionFile = await fetch("build/build_version.txt") - const versionTxt = await versionFile.text() - if (versionTxt.startsWith("v0-")) - setVersion("v0." + versionTxt.match(".*?\-(.*?)\-")![1]) - })() - - }, []) - - - React.useEffect(() => - { - window.addEventListener("keydown", (ev: KeyboardEvent) => - { - if (document.activeElement && document.activeElement.tagName == "INPUT") - return - - const key = ev.key.toLowerCase() - - for (const command of Command.allCommands) - { - if (!command.shortcut) - continue - - if (command.isShortcutAvailable && !command.isShortcutAvailable()) - continue - - if (command.isAvailable && !command.isAvailable({})) - continue - - for (const shortcut of command.shortcut) - { - if (!!shortcut.ctrl !== ev.ctrlKey) - continue - - if (!!shortcut.shift !== ev.shiftKey) - continue - - if (key !== shortcut.key) - continue - - //console.log("handled keyboard command: ", command.name) - command.func({}) - ev.preventDefault() - ev.stopPropagation() - return - } - } - }) - - }, []) - - - return <> -
- - - - - - - - How to use the app - - - { version } - - - - { !Playback.global.synthLoading ? null : - - } - - - -
- - { !Popup.global.elem ? null : - - } - -} \ No newline at end of file diff --git a/src/MenuEdit.tsx b/src/MenuEdit.tsx deleted file mode 100644 index 6c61daf..0000000 --- a/src/MenuEdit.tsx +++ /dev/null @@ -1,17 +0,0 @@ -import React from "react" -import * as Command from "./command" -import * as Menubar from "./menubar" -import * as Popup from "./popup" - - -export default function MenuEdit() -{ - return <> - - - - - - - -} \ No newline at end of file diff --git a/src/MenuFile.tsx b/src/MenuFile.tsx deleted file mode 100644 index e87345b..0000000 --- a/src/MenuFile.tsx +++ /dev/null @@ -1,96 +0,0 @@ -import React from "react" -import * as Dockable from "./dockable" -import * as Command from "./command" -import * as Project from "./project" -import * as Menubar from "./menubar" -import * as Popup from "./popup" -import * as Playback from "./playback" -import * as Windows from "./windows" - - -export default function MenuFile() -{ - React.useEffect(() => - { - const onOpenFileChange = (ev: Event) => - { - const elem = ev.target as HTMLInputElement - - if (elem.files!.length != 1) - return - - let reader = new FileReader() - reader.readAsArrayBuffer(elem.files![0]) - reader.onload = () => - { - Playback.setPlaying(false) - - const filename = elem.files![0].name - const bytes = new Uint8Array(reader.result as any) - Command.openFromFile(filename, null, bytes) - } - } - - const input = document.getElementById("inputOpenFile") as HTMLInputElement - input!.addEventListener("change", onOpenFileChange) - - return () => - { - input!.removeEventListener("change", onOpenFileChange) - } - - }, []) - - - React.useEffect(() => - { - window.addEventListener("beforeunload", (ev) => - { - if (Project.isUnsaved()) - { - ev.preventDefault() - ev.returnValue = "" - return "" - } - }) - - }, []) - - - const onRender = () => - { - Dockable.createFloating(Windows.Render, null) - } - - - return <> - - - - - - - - - - - - - - - - - - - -} \ No newline at end of file diff --git a/src/MenuWindow.tsx b/src/MenuWindow.tsx deleted file mode 100644 index 109b9bf..0000000 --- a/src/MenuWindow.tsx +++ /dev/null @@ -1,28 +0,0 @@ -import React from "react" -import * as Project from "./project" -import * as Menubar from "./menubar" -import * as Popup from "./popup" -import * as Dockable from "./dockable" -import * as Windows from "./windows" -import Rect from "./util/rect" - - -export default function MenuWindow() -{ - const onOpenWindow = (elem: Dockable.WindowElement, data: any) => - { - Dockable.createFloating(elem, data) - } - - - return <> - - - onOpenWindow(Windows.Timeline, null) } - /> - - - -} \ No newline at end of file diff --git a/src/PlaybackToolbar.tsx b/src/PlaybackToolbar.tsx deleted file mode 100644 index c49b262..0000000 --- a/src/PlaybackToolbar.tsx +++ /dev/null @@ -1,69 +0,0 @@ -import React from "react" -import * as Playback from "./playback" -import * as Project from "./project" - - -export default function PlaybackToolbar() -{ - const onChangeBpm = (ev: React.ChangeEvent) => - { - Project.global.project = - { - ...Project.global.project, - baseBpm: parseInt(ev.target.value) - } - - Project.addUndoPoint("setBpm") - Project.notifyObservers() - } - - - return
- - - - -
- Base BPM: -
- - -
-} \ No newline at end of file diff --git a/src/command/command.ts b/src/command/command.ts deleted file mode 100644 index a04dc08..0000000 --- a/src/command/command.ts +++ /dev/null @@ -1,23 +0,0 @@ -export interface Command -{ - name: string - icon?: string - shortcut?: CommandShortcut[] - isShortcutAvailable?: () => boolean - isAvailable?: (args: CommandArguments) => boolean - func: (args: CommandArguments) => Promise -} - - -export interface CommandShortcut -{ - ctrl?: boolean - shift?: boolean - key: string -} - - -export interface CommandArguments -{ - -} \ No newline at end of file diff --git a/src/command/convertNotesToChords.ts b/src/command/convertNotesToChords.ts deleted file mode 100644 index e4585fa..0000000 --- a/src/command/convertNotesToChords.ts +++ /dev/null @@ -1,201 +0,0 @@ -import * as Command from "../command" -import * as Project from "../project" -import * as Timeline from "../timeline" -import * as Theory from "../theory" -import * as Dockable from "../dockable" -import * as Windows from "../windows" -import Rational from "../util/rational" -import Range from "../util/range" -import { RefState } from "../util/refState" - - -export const convertNotesToChords: Command.Command = -{ - name: "Convert Notes to Chords", - func: async () => - { - const timelineState = Dockable.getMostRecentContentData(Windows.Timeline, (data) => - { - return data.timelineState as RefState - }) - - if (!timelineState) - return - - const state = timelineState.ref.current - const elemIds = timelineState.ref.current.selection - - const elems: Project.Note[] = [] - for (const elemId of elemIds) - { - const note = Project.getElem(Project.global.project, elemId, "note") - if (note) - elems.push(note) - - const noteBlock = Project.getElem(Project.global.project, elemId, "noteBlock") - if (noteBlock) - { - const list = Project.global.project.lists.get(noteBlock.id) - if (list) - { - for (const elem of list.iterAll()) - { - if (elem.type == "note") - elems.push(elem) - } - } - } - } - - if (elems.length == 0) - return - - const trackId = Project.global.project.chordTrackId - - Timeline.selectionClear(state) - - const groups = calculateGroups(elems) - let chordsToAdd: ChordToAdd[] = [] - for (const group of groups) - { - const midiPitches = group.elems.map(elem => elem.midiPitch) - const chordSuggestions = Theory.Chord.suggestChordsForPitches(midiPitches) - - if (chordSuggestions.length >= 1) - { - chordsToAdd.push({ - chord: chordSuggestions[0].chord, - range: group.range, - }) - } - } - - chordsToAdd = coallesceNeighboringChordsToAdd(chordsToAdd) - - for (const chordToAdd of chordsToAdd) - { - const projChord = Project.makeChord( - trackId, - chordToAdd.range, - chordToAdd.chord) - - const id = Project.global.project.nextId - Project.global.project = Project.upsertElement(Project.global.project, projChord) - - Timeline.selectionAdd(state, id) - } - - Project.global.project = Project.withRefreshedRange(Project.global.project) - - state.cursor.visible = false - Timeline.selectionRemoveConflictingBehind(state) - Timeline.sendEventRefresh() - Project.splitUndoPoint() - Project.addUndoPoint("menuConvertNotesToChords") - } -} - - -interface ChordToAdd -{ - chord: Theory.Chord - range: Range -} - - -function coallesceNeighboringChordsToAdd(chordsToAdd: ChordToAdd[]): ChordToAdd[] -{ - const newChordsToAdd: ChordToAdd[] = [] - - let prev = -1 - for (let i = 0; i < chordsToAdd.length; i++) - { - if (prev < 0) - { - prev = newChordsToAdd.length - newChordsToAdd.push({ ...chordsToAdd[i] }) - } - else - { - const prevChord = newChordsToAdd[prev].chord - const nextChord = chordsToAdd[i].chord - - if (prevChord.rootChroma == nextChord.rootChroma && - prevChord.kind == nextChord.kind) - { - newChordsToAdd[prev].range = newChordsToAdd[prev].range.merge(chordsToAdd[i].range) - } - else - { - // Fill holes? - //newChordsToAdd[prev].range = new Range(newChordsToAdd[prev].range.start, chordsToAdd[i].range.start) - - prev = newChordsToAdd.length - newChordsToAdd.push({ ...chordsToAdd[i] }) - } - } - } - - return newChordsToAdd -} - - -interface Group -{ - elems: Project.Note[] - range: Range -} - - -function calculateGroups(elems: Project.Note[]): Group[] -{ - const minTime = elems.reduce((accum, elem) => - { - const start = Project.getAbsoluteTime(Project.global.project, elem.parentId, elem.range.start) - return start.min(accum) - }, - null)! - - const maxTime = elems.reduce((accum, elem) => - { - const end = Project.getAbsoluteTime(Project.global.project, elem.parentId, elem.range.end) - return end.max(accum) - }, - null)! - - const groups: Group[] = [] - - let curTime = minTime - while (curTime.compare(maxTime) < 0) - { - const nextTime = elems.reduce((accum, elem) => - { - const range = Project.getAbsoluteRange(Project.global.project, elem.parentId, elem.range) - if (range.start.compare(curTime) > 0) - return range.start.min(accum) - - if (range.end.compare(curTime) > 0) - return range.end.min(accum) - - return accum - }, - null!) || maxTime - - const groupRange = new Range(curTime, nextTime) - - const groupElems = elems.filter(elem => - { - const range = Project.getAbsoluteRange(Project.global.project, elem.parentId, elem.range) - return groupRange.overlapsRange(range) - }) - - groups.push({ - elems: groupElems, - range: groupRange, - }) - - curTime = nextTime - } - - return groups -} \ No newline at end of file diff --git a/src/command/file.ts b/src/command/file.ts deleted file mode 100644 index 417c71f..0000000 --- a/src/command/file.ts +++ /dev/null @@ -1,227 +0,0 @@ -import * as Command from "../command" -import * as Project from "../project" -import * as Playback from "../playback" - - -function confirmDiscard() -{ - if (!Project.isUnsaved()) - return true - - return window.confirm("Discard current song?") -} - - -function isFileSystemAccessApiAvailable(showAlert?: boolean) -{ - if ("showOpenFilePicker" in window) - return true; - - if (showAlert) - { - (window as Window).alert( - "Your browser doesn't support the File System Access API.\n\n" + - "Use the other commands labeled as \"Browser\", or " + - "switch to a browser with support to that API, like Chrome.") - } - - return false; -} - - -export function openFromFile(filename: string, fileHandle: FileSystemFileHandle | null, bytes: Uint8Array): boolean -{ - if (filename.endsWith(".ttproj") || filename.endsWith(".json")) - { - const text = new TextDecoder("utf-8").decode(bytes) - const json = JSON.parse(text) - Project.open(Project.jsonImport(json)) - Project.setFileHandleForSave(fileHandle) - } - else if (filename.endsWith(".mid")) - { - Project.open(Project.midiImport(bytes)) - Project.setFileHandleForSave(null) - } - else - { - window.alert("Unrecognized file format!") - return false - } - - return true -} - - -export const newProject: Command.Command = -{ - name: "New Project", - icon: "📄", - func: async () => - { - if (!confirmDiscard()) - return - - Project.setNew() - Playback.setPlaying(false) - } -} - - -export const openFile: Command.Command = -{ - name: "Open...", - icon: "📂", - shortcut: [{ ctrl: true, key: "o" }], - isShortcutAvailable: () => isFileSystemAccessApiAvailable(), - func: async () => - { - if (!confirmDiscard()) - return - - if (!isFileSystemAccessApiAvailable(true)) - return - - const handles = await window.showOpenFilePicker({ - multiple: false, - types: [{ - description: "Supported files", - accept: { - "application/json": [".ttproj", ".json"], - "audio/midi": [".mid"], - }, - }] - }) - const handle = handles[0] - const file = await handle.getFile() - const bytes = await file.arrayBuffer() - openFromFile(file.name, handle, new Uint8Array(bytes)) - } -} - - -export const saveProjectAs: Command.Command = -{ - name: "Save Project As...", - icon: "💾", - shortcut: [{ ctrl: true, shift: true, key: "s" }], - isShortcutAvailable: () => isFileSystemAccessApiAvailable(), - func: async () => - { - if (!isFileSystemAccessApiAvailable(true)) - return - - const handle = await window.showSaveFilePicker({ - types: [{ - description: "Project file", - accept: { "application/json": [".ttproj", ".json"] }, - }], - }) - const jsonStr = Project.jsonExport(Project.global.project) - const writer = await handle.createWritable() - await writer.write(jsonStr) - await writer.close() - Project.setFileHandleForSave(handle) - Project.markAsSaved() - } -} - - -export const saveProject: Command.Command = -{ - name: "Save Project", - icon: "💾", - shortcut: [{ ctrl: true, key: "s" }], - isShortcutAvailable: () => isFileSystemAccessApiAvailable(), - func: async (args) => - { - if (!isFileSystemAccessApiAvailable(true)) - return - - if (!Project.global.curFileHandleForSave) - { - saveProjectAs.func(args) - return - } - - const jsonStr = Project.jsonExport(Project.global.project) - const writer = await (Project.global.curFileHandleForSave as any).createWritable() - await writer.write(jsonStr) - await writer.close() - Project.markAsSaved() - } -} - - -export const openFileBrowser: Command.Command = -{ - name: "[Browser] Open...", - icon: "📂", - shortcut: [{ ctrl: true, key: "o" }], - isShortcutAvailable: () => !isFileSystemAccessApiAvailable(), - func: async () => - { - if (!confirmDiscard()) - return - - document.getElementById("inputOpenFile")!.click() - } -} - - -export const downloadProjectBrowser: Command.Command = -{ - name: "[Browser] Download Project", - icon: "đŸ“Ĩ", - shortcut: [{ ctrl: true, key: "s" }], - isShortcutAvailable: () => !isFileSystemAccessApiAvailable(), - func: async () => - { - const jsonStr = Project.jsonExport(Project.global.project) - - const element = document.createElement("a") - element.setAttribute("href", "data:text/json;charset=utf-8," + encodeURIComponent(jsonStr)) - element.setAttribute("download", "song.ttproj") - - element.style.display = "none" - document.body.appendChild(element) - element.click() - document.body.removeChild(element) - } -} - - -export const previewProjectBrowser: Command.Command = -{ - name: "[Browser] Preview Project", - icon: "đŸ“Ĩ", - func: async () => - { - const jsonStr = Project.jsonExport(Project.global.project) - - const newWindow = window.open()! - newWindow.document.write("") - newWindow.document.write(jsonStr) - newWindow.document.write("") - } -} - - -export const downloadMidiBrowser: Command.Command = -{ - name: "[Browser] Download MIDI", - icon: "đŸ“Ĩ", - func: async () => - { - const bytes = Project.midiExport(Project.global.project) - - const element = document.createElement("a") - element.setAttribute("href", "data:audio/midi;base64," + btoa(String.fromCharCode(...bytes))) - element.setAttribute("download", "song.mid") - - element.style.display = "none" - document.body.appendChild(element) - element.click() - document.body.removeChild(element) - } -} \ No newline at end of file diff --git a/src/command/history.ts b/src/command/history.ts deleted file mode 100644 index af5d781..0000000 --- a/src/command/history.ts +++ /dev/null @@ -1,24 +0,0 @@ -import * as Command from "../command" -import * as Project from "../project" - - -export const undo: Command.Command = -{ - name: "Undo", - shortcut: [{ ctrl: true, key: "z" }], - func: async () => - { - Project.undo() - } -} - - -export const redo: Command.Command = -{ - name: "Redo", - shortcut: [{ ctrl: true, key: "y" }, { ctrl: true, shift: true, key: "z" }], - func: async () => - { - Project.redo() - } -} \ No newline at end of file diff --git a/src/command/index.ts b/src/command/index.ts deleted file mode 100644 index d13a27b..0000000 --- a/src/command/index.ts +++ /dev/null @@ -1,31 +0,0 @@ -export * from "./command" -export * from "./file" -export * from "./history" -export * from "./playback" -export * from "./convertNotesToChords" - - -import * as File from "./file" -import * as History from "./history" -import * as Playback from "./playback" -import { convertNotesToChords } from "./convertNotesToChords" - - -export const allCommands = -[ - File.newProject, - File.openFile, - File.saveProject, - File.saveProjectAs, - File.openFileBrowser, - File.downloadProjectBrowser, - File.previewProjectBrowser, - File.downloadMidiBrowser, - - History.undo, - History.redo, - - Playback.togglePlayback, - - convertNotesToChords, -] \ No newline at end of file diff --git a/src/command/playback.ts b/src/command/playback.ts deleted file mode 100644 index 812a721..0000000 --- a/src/command/playback.ts +++ /dev/null @@ -1,13 +0,0 @@ -import * as Command from "../command" -import * as Playback from "../playback" - - -export const togglePlayback: Command.Command = -{ - name: "Toggle Playback", - shortcut: [{ key: " " }], - func: async () => - { - Playback.togglePlaying() - } -} \ No newline at end of file diff --git a/src/dockable/Container.tsx b/src/dockable/Container.tsx deleted file mode 100644 index 46aa41b..0000000 --- a/src/dockable/Container.tsx +++ /dev/null @@ -1,761 +0,0 @@ -import React from "react" -import ReactDOM from "react-dom" -import * as Dockable from "./index" -import * as DockableData from "./state" -import * as Prefs from "../prefs" -import Rect from "../util/rect" -import styled from "styled-components" -import { useRefState } from "../util/refState" - - -const StyledCloseButton = styled.button` - pointer-events: auto; - color: #fff; - border: 1px solid #888; - border-radius: 0.5em; - background-color: #2f3136; - padding: 0.1em 0.3em; - cursor: pointer; - margin-left: 0.25em; - width: 1.5em; - - &:hover - { - border: 1px solid #fff; - } - - &:active - { - border: 1px solid #fff; - background-color: #000; - } -` - - -const DivWindow = styled.div<{ - isCurrentTab: boolean -}>` - display: ${ props => props.isCurrentTab ? "grid" : "none" }; - grid-template: 100% / 100%; - - position: absolute; - box-sizing: border-box; - - color: #fff; - text-align: left; - - background-color: transparent; - border-radius: 0.5em; - overflow: hidden; -` - - -const DivWindowContent = styled.div` - grid-row: 1; - grid-column: 1; - width: 100%; - height: 100%; -` - - -const DivWindowBottomRightResizeHandle = styled.div<{ - size: number, -}>` - width: ${ props => props.size }px; - height: ${ props => props.size }px; - - grid-row: 1; - grid-column: 1; - align-self: end; - justify-self: end; - - cursor: nwse-resize; - z-index: 1; -` - - -export function Container() -{ - const [rect, setRect] = React.useState(new Rect(0, 0, 0, 0)) - const rootRef = React.useRef(null) - - - React.useLayoutEffect(() => - { - const onResize = () => - { - if (!rootRef.current) - return - - const elemRect = rootRef.current!.getBoundingClientRect() - - setRect(new Rect( - elemRect.x, - elemRect.y, - elemRect.width, - elemRect.height)) - } - - onResize() - - window.addEventListener("resize", onResize) - return () => window.removeEventListener("resize", onResize) - - }, []) - - - const layoutRef = React.useRef(null!) - const rectRef = React.useRef(null!) - rectRef.current = rect - layoutRef.current = React.useMemo(() => - { - return DockableData.getLayout(Dockable.global.state, rect) - - }, [rect, Dockable.globalObservable.updateToken]) - - - React.useEffect(() => - { - const onRefreshPreferredSize = () => - { - for (const panel of Dockable.global.state.floatingPanels) - { - if (!panel.justOpened) - continue - - panel.justOpened = false - panel.rect.w = panel.preferredFloatingSize.w - panel.rect.h = panel.preferredFloatingSize.h - - switch (panel.justOpenedAnchorAlignX) - { - case 0: - panel.rect.x = panel.justOpenedAnchorRect.xCenter - panel.rect.w / 2 - break - case 1: - panel.rect.x = panel.justOpenedAnchorRect.x2 - break - case -1: - panel.rect.x = panel.justOpenedAnchorRect.x1 - panel.rect.w - break - } - - switch (panel.justOpenedAnchorAlignY) - { - case 0: - panel.rect.y = panel.justOpenedAnchorRect.yCenter - panel.rect.h / 2 - break - case 1: - panel.rect.y = panel.justOpenedAnchorRect.y2 - break - case -1: - panel.rect.y = panel.justOpenedAnchorRect.y1 - panel.rect.h - break - } - - Dockable.clampFloatingPanelStrictly(Dockable.global.state, panel, rectRef.current) - Dockable.notifyObservers() - } - } - - window.addEventListener("dockableRefreshPreferredSize", onRefreshPreferredSize) - return () => window.removeEventListener("dockableRefreshPreferredSize", onRefreshPreferredSize) - - }, []) - - - const mouseData = useMouseHandler(layoutRef, rectRef) - - const anchorSize = 5 - const anchorColor = Prefs.global.ui.windowAnchorColor - - const resizeHandleSize = 20 - - return
- - { layoutRef.current.panelRects.map(panelRect => - - )} - - { layoutRef.current.windows.map(w => - { - const component = !Dockable.global.contentIdToComponent ? null : - Dockable.global.contentIdToComponent.get(w.windowId) - - const data = !Dockable.global.contentIdToData ? null : - Dockable.global.contentIdToData.get(w.windowId) - - const setTitle = (title: string) => - { - if (w.panel.windowTitles[w.tabIndex] != title) - { - window.requestAnimationFrame(() => - { - w.panel.windowTitles[w.tabIndex] = title - Dockable.notifyObservers() - }) - } - } - - const setPreferredSize = (width: number, height: number) => - { - if (w.tabIndex == w.panel.curWindowIndex && - (width != w.panel.preferredFloatingSize.w || - height != w.panel.preferredFloatingSize.h)) - { - window.requestAnimationFrame(() => - { - w.panel.preferredFloatingSize = new Rect(0, 0, width, height) - Dockable.notifyObservers() - - if (w.panel.justOpened) - window.dispatchEvent(new Event("dockableRefreshPreferredSize")) - }) - } - } - - const marginTop = 30 - const marginOther = 4 - - return mouseData.onPanelActivate(ev, w.panel)) as any } - isCurrentTab={ w.panel.curWindowIndex == w.tabIndex } - style={{ - left: w.panelRect.rect.x + marginOther, - top: w.panelRect.rect.y + marginTop, - width: w.panelRect.rect.w - marginOther * 2, - height: w.panelRect.rect.h - marginTop - marginOther, - zIndex: w.panelRect.zIndex * 3 + 1, - contain: "strict", - }}> - - - { !component ? null : React.createElement(component) } - - - - { !w.panel.floating ? null : - mouseData.onPanelResize(ev, w.panel)) as any } - size={ resizeHandleSize } - /> - } - - })} - - { layoutRef.current.dividers.map(divider => -
mouseData.onDividerResize(ev, divider)) as any } - style={{ - width: (divider.rect.w || 6) + "px", - height: (divider.rect.h || 6) + "px", - - position: "absolute", - left: (divider.rect.x - 3) + "px", - top: (divider.rect.y - 3) + "px", - - cursor: !divider.vertical ? - "ew-resize" : - "ns-resize", - - zIndex: 1, - }}/> - )} - - { !mouseData.showAnchors || !mouseData.draggingAnchor ? null : -
- } - - { !mouseData.showAnchors ? null : layoutRef.current.anchors.map((anchor, i) => - anchor.panel == mouseData.grabbedPanel ? null : -
- )} - -
-} - - -interface PrefsProps -{ - readonly prefs: Prefs.Prefs -} - - -const DivPanel = styled.div` - background-color: ${ props => props.prefs.ui.windowPanelColor }; - border-radius: 0.5em; - box-sizing: border-box; - width: 100%; - height: 100%; - - display: grid; - grid-template: auto 1fr / 1fr; - overflow: hidden; - - border: 1px solid ${ props => props.prefs.ui.windowVoidColor }; - - &.active - { - border: 1px solid ${ props => props.prefs.ui.windowActiveBorderColor }; - } -` - - -function Panel(props: any) -{ - const panelRect: DockableData.PanelRect = props.panelRect - const mouseHandler: MouseHandlerData = props.mouseHandler - - return
- mouseHandler.onPanelActivate(ev, panelRect.panel)) as any } - prefs={ Prefs.global } - > -
mouseHandler.onPanelHeaderMouseDown(ev, panelRect.panel)) as any } - style={{ - backgroundColor: Prefs.global.ui.windowVoidColor, - textAlign: "left", - gridRow: 1, - gridColumn: 1, - - display: "grid", - gridTemplate: `auto / repeat(${ panelRect.panel.windowIds.length }, auto) 1fr`, - gridAutoFlow: "column", - }}> - - { panelRect.panel.windowIds.map((cId, idx) => -
mouseHandler.onPanelTabMouseDown(ev, panelRect.panel, idx)) as any } - onContextMenu={ ev => ev.preventDefault() } - style={{ - display: "inline-block", - gridRow: 1, - gridColumn: idx + 1, - backgroundColor: panelRect.panel.curWindowIndex == idx ? - Prefs.global.ui.windowPanelColor : - Prefs.global.ui.windowVoidColor, - color: "#fff", - padding: "0.25em 0.5em", - marginRight: "0.25em", - userSelect: "none", - borderTopRightRadius: "0.5em", - borderTopLeftRadius: "0.5em", - }}> - { panelRect.panel.windowTitles[idx] || "New Window" } - mouseHandler.onPanelTabClose(ev, panelRect.panel, idx)) as any } - > - x - -
- )} - -
- -
- -
-} - - -interface MouseHandlerState -{ - mouseDown: boolean - mouseDownPos: MousePos - mouseAction: MouseAction - mousePos: MousePos - mouseDragLocked: boolean - - grabbedPanel: Dockable.Panel | null - grabbedTab: number | null - grabbedDivider: Dockable.Divider | null - nearestAnchor: Dockable.Anchor | null -} - - -enum MouseAction -{ - None, - MoveHeader, - ResizePanel, - ResizeDivider, -} - - -interface MousePos -{ - x: number - y: number -} - - -interface MouseHandlerData -{ - grabbedPanel: Dockable.Panel | null - showAnchors: boolean - draggingAnchor: Dockable.Anchor | null - onPanelActivate: (ev: MouseEvent, panel: Dockable.Panel) => void - onPanelHeaderMouseDown: (ev: MouseEvent, panel: Dockable.Panel) => void - onPanelTabMouseDown: (ev: MouseEvent, panel: Dockable.Panel, tab: number) => void - onPanelResize: (ev: MouseEvent, panel: Dockable.Panel) => void - onDividerResize: (ev: MouseEvent, divider: Dockable.Divider) => void - onPanelTabClose: (ev: MouseEvent, panel: Dockable.Panel, tab: number) => void -} - - -function useMouseHandler( - layoutRef: React.MutableRefObject, - rectRef: React.MutableRefObject) - : MouseHandlerData -{ - const stateRef = useRefState(() => - { - return { - mouseDown: false, - mouseDownPos: { x: 0, y: 0 }, - mouseAction: MouseAction.None, - mousePos: { x: 0, y: 0 }, - mouseDragLocked: true, - - grabbedPanel: null, - grabbedTab: null, - grabbedDivider: null, - nearestAnchor: null, - } - }) - - const transformMouse = (ev: MouseEvent): MousePos => - { - return { - x: ev.pageX, - y: ev.pageY, - } - } - - const bringToFront = (ev: any, panel: Dockable.Panel) => - { - const dockable = Dockable.global.state - if (panel.windowIds.length != 0) - dockable.activePanel = panel - - if (!panel.floating) - { - Dockable.notifyObservers() - return - } - - if (dockable.floatingPanels.some(p => p.bugfixAppearOnTop)) - { - dockable.floatingPanels.forEach(p => p.bugfixAppearOnTop = false) - return - } - - dockable.floatingPanels = dockable.floatingPanels.filter(p => p !== panel) - dockable.floatingPanels.push(panel) - - console.log("bringToFront", panel, ev.clickedEphemeral) - if (!panel.ephemeral && !ev.clickedEphemeral) - Dockable.removeEphemerals(dockable) - - Dockable.notifyObservers() - } - - React.useEffect(() => - { - const onMouseMove = (ev: MouseEvent) => - { - const dockable = Dockable.global.state - const state = stateRef.ref.current - const mousePosPrev = state.mousePos - state.mousePos = transformMouse(ev) - - if (state.mouseDown) - { - ev.preventDefault() - - if (state.mouseAction == MouseAction.ResizePanel) - { - state.grabbedPanel!.rect.w += state.mousePos.x - mousePosPrev.x - state.grabbedPanel!.rect.h += state.mousePos.y - mousePosPrev.y - } - else if (state.mouseAction == MouseAction.ResizeDivider) - { - const divider = state.grabbedDivider! - divider.panel.splitSize = Math.max(0.05, Math.min(0.95, - ((divider.vertical ? state.mousePos.y : state.mousePos.x) - divider.resizeMin) / - (divider.resizeMax - divider.resizeMin) - )) - } - else if (state.mouseDragLocked) - { - if (Math.abs(state.mousePos.x - state.mouseDownPos.x) > 10 || - Math.abs(state.mousePos.y - state.mouseDownPos.y) > 10) - { - state.mouseDragLocked = false - - if (state.mouseAction == MouseAction.MoveHeader) - { - if (state.grabbedTab === null || state.grabbedPanel!.windowIds.length == 1) - { - if (!state.grabbedPanel!.floating) - { - const windows = [...state.grabbedPanel!.windowIds] - for (const window of windows) - Dockable.removeWindow(dockable, state.grabbedPanel!, window) - - state.grabbedPanel = Dockable.makePanel(dockable) - for (const window of windows) - Dockable.addWindow(dockable, state.grabbedPanel, window) - - Dockable.coallesceEmptyPanels(dockable) - - state.grabbedPanel!.rect = new Rect(state.mousePos.x, state.mousePos.y, 0, 0) - state.grabbedPanel!.justOpenedAnchorRect = new Rect(state.mousePos.x, state.mousePos.y, 0, 0) - state.grabbedPanel!.justOpenedAnchorAlignX = 0 - state.grabbedPanel!.justOpenedAnchorAlignY = 1 - } - } - else - { - const window = state.grabbedPanel!.windowIds[state.grabbedTab] - Dockable.removeWindow(dockable, state.grabbedPanel!, window) - state.grabbedPanel = Dockable.makePanel(dockable) - Dockable.addWindow(dockable, state.grabbedPanel, window) - Dockable.coallesceEmptyPanels(dockable) - - state.grabbedPanel!.rect = new Rect(state.mousePos.x, state.mousePos.y, 0, 0) - state.grabbedPanel!.justOpenedAnchorRect = new Rect(state.mousePos.x, state.mousePos.y, 0, 0) - state.grabbedPanel!.justOpenedAnchorAlignX = 0 - state.grabbedPanel!.justOpenedAnchorAlignY = 1 - } - - bringToFront(ev, state.grabbedPanel!) - } - } - } - else - { - if (state.mouseAction == MouseAction.MoveHeader) - { - let nearestDistSqr = 50 * 50 - state.nearestAnchor = null - - for (const anchor of layoutRef.current!.anchors) - { - if (anchor.panel === state.grabbedPanel) - continue - - const xx = anchor.x - state.mousePos.x - const yy = anchor.y - state.mousePos.y - const distSqr = xx * xx + yy * yy - if (distSqr < nearestDistSqr) - { - nearestDistSqr = distSqr - state.nearestAnchor = anchor - } - } - - state.grabbedPanel!.rect = state.grabbedPanel!.rect.displace( - state.mousePos.x - mousePosPrev.x, - state.mousePos.y - mousePosPrev.y) - } - } - } - - stateRef.commit() - Dockable.notifyObservers() - } - - const onMouseUp = (ev: MouseEvent) => - { - const state = stateRef.ref.current - const dockable = Dockable.global.state - - if (state.mouseDown && !state.mouseDragLocked) - { - if (state.mouseAction == MouseAction.MoveHeader && - state.nearestAnchor) - { - Dockable.dock( - dockable, - state.grabbedPanel!, - state.nearestAnchor.panel, - state.nearestAnchor.mode) - } - - Dockable.clampFloatingPanels(dockable, rectRef.current) - } - - state.mouseDown = false - state.mouseAction = MouseAction.None - stateRef.commit() - Dockable.notifyObservers() - } - - window.addEventListener("mousemove", onMouseMove) - window.addEventListener("mouseup", onMouseUp) - - return () => - { - window.removeEventListener("mousemove", onMouseMove) - window.removeEventListener("mouseup", onMouseUp) - } - - }, []) - - const onPanelActivate = (ev: MouseEvent, panel: Dockable.Panel) => - { - if (panel.ephemeral) - { - (ev as any).clickedEphemeral = true - } - - bringToFront(ev, panel) - } - - const onPanelHeaderMouseDown = (ev: MouseEvent, panel: Dockable.Panel) => - { - ev.preventDefault() - const state = stateRef.ref.current - state.grabbedPanel = panel - state.grabbedTab = null - state.mouseDown = true - state.mouseAction = MouseAction.MoveHeader - state.mouseDownPos = state.mousePos = transformMouse(ev) - state.mouseDragLocked = true - stateRef.commit() - - bringToFront(ev, panel) - } - - const onPanelTabMouseDown = (ev: MouseEvent, panel: Dockable.Panel, tab: number) => - { - ev.preventDefault() - ev.stopPropagation() - panel.curWindowIndex = tab - const state = stateRef.ref.current - state.grabbedPanel = panel - state.grabbedTab = tab - state.mouseDown = true - state.mouseAction = MouseAction.MoveHeader - state.mouseDownPos = state.mousePos = transformMouse(ev) - state.mouseDragLocked = true - stateRef.commit() - Dockable.notifyObservers() - - bringToFront(ev, panel) - } - - const onPanelResize = (ev: MouseEvent, panel: Dockable.Panel) => - { - ev.preventDefault() - const state = stateRef.ref.current - state.grabbedPanel = panel - state.grabbedTab = null - state.mouseDown = true - state.mouseAction = MouseAction.ResizePanel - state.mouseDownPos = state.mousePos = transformMouse(ev) - state.mouseDragLocked = true - stateRef.commit() - - bringToFront(ev, panel) - } - - const onDividerResize = (ev: MouseEvent, divider: Dockable.Divider) => - { - ev.preventDefault() - const state = stateRef.ref.current - state.grabbedDivider = divider - state.mouseDown = true - state.mouseAction = MouseAction.ResizeDivider - state.mouseDownPos = state.mousePos = transformMouse(ev) - state.mouseDragLocked = true - stateRef.commit() - } - - const onPanelTabClose = (ev: MouseEvent, panel: Dockable.Panel, tab: number) => - { - ev.preventDefault() - const dockable = Dockable.global.state - const window = panel.windowIds[tab] - Dockable.removeWindow(dockable, panel, window) - Dockable.coallesceEmptyPanels(dockable) - Dockable.notifyObservers() - } - - return { - grabbedPanel: stateRef.ref.current.mouseDown ? - stateRef.ref.current.grabbedPanel : - null, - - showAnchors: - stateRef.ref.current.mouseDown && - !stateRef.ref.current.mouseDragLocked && - stateRef.ref.current.mouseAction == MouseAction.MoveHeader, - - draggingAnchor: stateRef.ref.current.nearestAnchor, - onPanelActivate, - onPanelHeaderMouseDown, - onPanelTabMouseDown, - onPanelResize, - onDividerResize, - onPanelTabClose, - } -} \ No newline at end of file diff --git a/src/dockable/_global.ts b/src/dockable/_global.ts deleted file mode 100644 index b666ba6..0000000 --- a/src/dockable/_global.ts +++ /dev/null @@ -1,177 +0,0 @@ -import React from "react" -import * as Dockable from "./index" -import * as Window from "../windows" -import * as GlobalObservable from "../util/globalObservable" -import Rect from "../util/rect" - - -export type WindowElement = (props: WindowProps) => JSX.Element | null - - -export interface Global -{ - state: Dockable.State - - contentIdToComponent: Map - contentIdToData: Map -} - - -export let global: Global = null! - - -export function initGlobal() -{ - const contentIdToComponent = new Map() - const contentIdToData = new Map() - - const state = Dockable.makeRoot() - const panel = Dockable.makePanel(state) - Dockable.addWindow(state, panel, 1) - Dockable.dock(state, panel, state.rootPanel, Dockable.DockMode.Full) - console.log(state) - - contentIdToComponent.set(1, Window.Timeline) - - global = - { - state, - - contentIdToComponent, - contentIdToData, - } -} - - -export const globalObservable = GlobalObservable.makeGlobalObservable() -export function useGlobal() -{ - GlobalObservable.useGlobalObservable(globalObservable) -} - - -export function notifyObservers() -{ - globalObservable.notifyObservers() -} - - -interface MousePos -{ - x: number - y: number -} - - -export const mousePos: MousePos = -{ - x: 0, - y: 0, -} - - -window.addEventListener("mousemove", (ev: MouseEvent) => -{ - mousePos.x = ev.pageX - mousePos.y = ev.pageY -}) - - -export function modifyContentData(contentId: Dockable.WindowId, modifyFn: (data: any) => void) -{ - let data: any = global.contentIdToData.get(contentId) - if (!data) - { - data = {} - global.contentIdToData.set(contentId, data) - } - - modifyFn(data) -} - - -export function getMostRecentContentData(elemType: WindowElement, getFn: (data: any) => T | null): T | null -{ - const panel = global.state.activePanel - if (!panel || panel.windowIds.length < 1) - return null - - const contentId = panel.windowIds[0] - const elem = global.contentIdToComponent.get(contentId) - if (!elem || elem !== elemType) - return null - - const data = global.contentIdToData.get(contentId) - if (!data) - return null - - return getFn(data) -} - - -export function createFloating( - elem: WindowElement, - data: any, - alignX?: number, - alignY?: number, - rect?: Rect) - : Dockable.Panel -{ - let state = Dockable.global.state - - const id = Dockable.global.contentIdToComponent.size + 1 - Dockable.global.contentIdToComponent.set(id, elem) - Dockable.global.contentIdToData.set(id, data) - - const panel = Dockable.makePanel(state) - Dockable.addWindow(state, panel, id) - if (rect) - panel.rect = new Rect(rect.x2, rect.y2, 500, 300) - else - panel.rect = new Rect(mousePos.x, mousePos.y, 500, 300) - - panel.justOpenedAnchorRect = rect ?? new Rect(mousePos.x - 15, mousePos.y - 15, 30, 30) - panel.justOpenedAnchorAlignX = alignX ?? 1 - panel.justOpenedAnchorAlignY = alignY ?? 1 - panel.bugfixAppearOnTop = true - - Dockable.global.state.activePanel = panel - Dockable.notifyObservers() - return panel -} - - -export function createFloatingEphemeral( - elem: WindowElement, - data: any, - alignX?: number, - alignY?: number, - rect?: Rect) - : Dockable.Panel -{ - const panel = createFloating(elem, data, alignX, alignY, rect) - Dockable.removeEphemerals(Dockable.global.state) - panel.ephemeral = true - Dockable.notifyObservers() - return panel -} - - -export interface WindowProps -{ - panel: Dockable.Panel - contentId: Dockable.WindowId - data: any - - setTitle: (title: string) => void - setPreferredSize: (w: number, h: number) => void -} - - -export const WindowContext = React.createContext(null!) - - -export function useWindow(): WindowProps -{ - return React.useContext(WindowContext) -} \ No newline at end of file diff --git a/src/dockable/index.ts b/src/dockable/index.ts deleted file mode 100644 index da2bf44..0000000 --- a/src/dockable/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -export * from "./_global" -export * from "./state" -export * from "./Container" \ No newline at end of file diff --git a/src/dockable/state.ts b/src/dockable/state.ts deleted file mode 100644 index 11c309f..0000000 --- a/src/dockable/state.ts +++ /dev/null @@ -1,505 +0,0 @@ -import Rect from "../util/rect" - - -export type PanelId = number -export type WindowId = number - - -export interface State -{ - idNext: PanelId - rootPanel: Panel - floatingPanels: Panel[] - activePanel: Panel | null -} - - -export enum SplitMode -{ - LeftRight, - TopBottom, -} - - -export enum DockMode -{ - Full, - Left, - Right, - Top, - Bottom, -} - - -export interface Panel -{ - id: PanelId - floating: boolean - rect: Rect - bugfixAppearOnTop: boolean - windowIds: WindowId[] - curWindowIndex: number - splitPanels: Panel[] - splitMode: SplitMode - splitSize: number - - windowTitles: string[] - preferredFloatingSize: Rect - - justOpened: boolean - justOpenedAnchorRect: Rect - justOpenedAnchorAlignX: number - justOpenedAnchorAlignY: number - ephemeral: boolean -} - - -export interface PanelRect -{ - panel: Panel - rect: Rect - floating: boolean - zIndex: number -} - - -export interface Divider -{ - panel: Panel - vertical: boolean - rect: Rect - resizeMin: number - resizeMax: number -} - - -export interface Anchor -{ - panel: Panel - x: number - y: number - mode: DockMode - previewRect: Rect -} - - -export interface Layout -{ - panelRects: PanelRect[] - windows: LayoutWindow[] - dividers: Divider[] - anchors: Anchor[] -} - - -export interface LayoutWindow -{ - windowId: WindowId - tabIndex: number - panel: Panel - panelRect: PanelRect -} - - -export function makeRoot(): State -{ - return { - idNext: 2, - rootPanel: { - id: 1, - floating: false, - bugfixAppearOnTop: false, - rect: new Rect(0, 0, 0, 0), - windowIds: [], - curWindowIndex: 0, - splitPanels: [], - splitMode: SplitMode.LeftRight, - splitSize: 0.5, - - windowTitles: [], - preferredFloatingSize: new Rect(0, 0, 300, 250), - - justOpened: false, - justOpenedAnchorRect: new Rect(0, 0, 0, 0), - justOpenedAnchorAlignX: 0, - justOpenedAnchorAlignY: 1, - ephemeral: false, - }, - floatingPanels: [], - activePanel: null, - } -} - - -export function makePanel(root: State): Panel -{ - const id = root.idNext++ - const panel: Panel = { - id, - floating: true, - bugfixAppearOnTop: false, - rect: new Rect(0, 0, 0, 0), - windowIds: [], - curWindowIndex: 0, - splitPanels: [], - splitMode: SplitMode.LeftRight, - splitSize: 0.5, - - windowTitles: [], - preferredFloatingSize: new Rect(0, 0, 300, 250), - - justOpened: true, - justOpenedAnchorRect: new Rect(0, 0, 0, 0), - justOpenedAnchorAlignX: 0, - justOpenedAnchorAlignY: 1, - ephemeral: false, - } - root.floatingPanels.push(panel) - return panel -} - - -export function detachPanel(root: State, panel: Panel) -{ - if (!panel.floating) - { - panel.floating = true - root.activePanel = panel - root.floatingPanels.push(panel) - } -} - - -export function addWindow(root: State, toPanel: Panel, window: WindowId) -{ - toPanel.windowIds.push(window) - toPanel.windowTitles.push("") - toPanel.curWindowIndex = toPanel.windowIds.length - 1 - toPanel.ephemeral = false -} - - -export function removeWindow(root: State, fromPanel: Panel, window: WindowId) -{ - const windowIndex = fromPanel.windowIds.findIndex(w => w === window) - if (windowIndex < 0) - return - - fromPanel.windowIds.splice(windowIndex, 1) - fromPanel.windowTitles.splice(windowIndex, 1) - fromPanel.curWindowIndex = Math.max(0, Math.min(fromPanel.windowIds.length - 1, fromPanel.curWindowIndex)) -} - - -export function removeEphemerals(root: State) -{ - for (var i = 0; i < root.floatingPanels.length; i++) - removeEphemeralsRecursive(root, root.floatingPanels[i]) - - coallesceEmptyPanels(root) -} - - -export function removeEphemeralsRecursive(root: State, fromPanel: Panel) -{ - for (var i = 0; i < fromPanel.splitPanels.length; i++) - coallesceEmptyPanelsRecursive(root, fromPanel.splitPanels[i]) - - if (fromPanel.ephemeral) - { - fromPanel.windowIds = [] - fromPanel.windowTitles = [] - fromPanel.curWindowIndex = 0 - } -} - - -export function coallesceEmptyPanels(root: State) -{ - coallesceEmptyPanelsRecursive(root, root.rootPanel) - for (var i = 0; i < root.floatingPanels.length; i++) - coallesceEmptyPanelsRecursive(root, root.floatingPanels[i]) - - root.floatingPanels = root.floatingPanels.filter(p => - p.windowIds.length != 0 || p.splitPanels.length != 0) -} - - -export function coallesceEmptyPanelsRecursive(root: State, fromPanel: Panel) -{ - for (var i = 0; i < fromPanel.splitPanels.length; i++) - coallesceEmptyPanelsRecursive(root, fromPanel.splitPanels[i]) - - fromPanel.splitPanels = fromPanel.splitPanels.filter(p => - p.windowIds.length != 0 || p.splitPanels.length != 0) - - if (fromPanel.splitPanels.length == 1) - Object.assign(fromPanel, fromPanel.splitPanels[0]) -} - - -export function dock(root: State, panel: Panel, dockIntoPanel: Panel, mode: DockMode) -{ - if (mode == DockMode.Full || - (dockIntoPanel.windowIds.length == 0 && dockIntoPanel.splitPanels.length == 0)) - { - if (dockIntoPanel.splitPanels.length > 0) - throw "invalid full docking into subdivided panel" - - for (const window of panel.windowIds) - addWindow(root, dockIntoPanel, window) - - detachPanel(root, panel) - panel.windowIds = [] - panel.windowTitles = [] - root.floatingPanels = root.floatingPanels.filter(p => p !== panel) - root.activePanel = dockIntoPanel - } - else if (mode == DockMode.Right || - mode == DockMode.Left || - mode == DockMode.Top || - mode == DockMode.Bottom) - { - const subdivMode = - (mode == DockMode.Right || mode == DockMode.Left) ? - SplitMode.LeftRight : - SplitMode.TopBottom - - const subdivOriginalFirst = - (mode == DockMode.Bottom || mode == DockMode.Right) - - const newSubpanels = [panel] - - const newSubpanel = makePanel(root) - newSubpanel.windowIds = dockIntoPanel.windowIds - newSubpanel.windowTitles = dockIntoPanel.windowTitles - newSubpanel.curWindowIndex = dockIntoPanel.curWindowIndex - newSubpanel.splitMode = dockIntoPanel.splitMode - newSubpanel.splitPanels = dockIntoPanel.splitPanels - newSubpanel.splitSize = dockIntoPanel.splitSize - - dockIntoPanel.windowIds = [] - dockIntoPanel.windowTitles = [] - dockIntoPanel.splitPanels = newSubpanels - dockIntoPanel.splitMode = subdivMode - dockIntoPanel.splitSize = subdivOriginalFirst ? 0.75 : 0.25 - - if (subdivOriginalFirst) - newSubpanels.unshift(newSubpanel) - else - newSubpanels.push(newSubpanel) - - panel.floating = false - dockIntoPanel.floating = false - newSubpanel.floating = false - root.activePanel = panel - root.floatingPanels = root.floatingPanels.filter(p => p !== panel && p !== newSubpanel) - } - else - { - throw "invalid docking" - } -} - - -export function clampFloatingPanels(root: State, rect: Rect) -{ - const margin = 10 - - for (const panel of root.floatingPanels) - { - panel.rect.x = - Math.max(rect.x + margin - panel.rect.w / 2, - Math.min(rect.x2 - margin - panel.rect.w / 2, - panel.rect.x)) - - panel.rect.y = - Math.max(rect.y + margin, - Math.min(rect.y2 - margin - panel.rect.h / 2, - panel.rect.y)) - } -} - - -export function clampFloatingPanelStrictly(root: State, panel: Panel, rect: Rect) -{ - const margin = 10 - - panel.rect.x = - Math.max(rect.x + margin, - Math.min(rect.x2 - margin - panel.rect.w, - panel.rect.x)) - - panel.rect.y = - Math.max(rect.y + margin, - Math.min(rect.y2 - margin - panel.rect.h, - panel.rect.y)) -} - - -export function traverseLayout(panel: Panel, rect: Rect, layout: Layout) -{ - const xMid = (rect.x1 + rect.x2) / 2 - const yMid = (rect.y1 + rect.y2) / 2 - - if (panel.splitPanels.length == 2) - { - if (panel.splitMode == SplitMode.LeftRight) - { - const xSplit = rect.x1 + Math.round((rect.x2 - rect.x1) * panel.splitSize) - - const rect1 = rect.withX2(xSplit) - const rect2 = rect.withX1(xSplit) - const rectDivider = rect.withX1(xSplit).withX2(xSplit) - - traverseLayout(panel.splitPanels[0], rect1, layout) - traverseLayout(panel.splitPanels[1], rect2, layout) - - layout.dividers.push({ - panel, - vertical: false, - rect: rectDivider, - resizeMin: rect.x1, - resizeMax: rect.x2, - }) - } - else if (panel.splitMode == SplitMode.TopBottom) - { - const ySplit = rect.y1 + Math.round((rect.y2 - rect.y1) * panel.splitSize) - - const rect1 = rect.withY2(ySplit) - const rect2 = rect.withY1(ySplit) - const rectDivider = rect.withY1(ySplit).withY2(ySplit) - - traverseLayout(panel.splitPanels[0], rect1, layout) - traverseLayout(panel.splitPanels[1], rect2, layout) - - layout.dividers.push({ - panel, - vertical: true, - rect: rectDivider, - resizeMin: rect.y1, - resizeMax: rect.y2, - }) - } - } - else - { - const panelRect = { - panel, - rect, - floating: false, - zIndex: 0, - } - - for (let w = 0; w < panel.windowIds.length; w++) - { - layout.windows.push( - { - windowId: panel.windowIds[w], - tabIndex: w, - panel, - panelRect, - }) - } - - layout.panelRects.push(panelRect) - - layout.anchors.push({ - panel, - x: xMid, - y: yMid, - mode: DockMode.Full, - previewRect: rect, - }) - } - - layout.anchors.push({ - panel, - x: rect.x2 - 10, - y: yMid, - mode: DockMode.Right, - previewRect: rect.withX1(rect.x1 + (rect.x2 - rect.x1) * 3 / 4), - }) - - layout.anchors.push({ - panel, - x: rect.x1 + 10, - y: yMid, - mode: DockMode.Left, - previewRect: rect.withX2(rect.x1 + (rect.x2 - rect.x1) / 4), - }) - - layout.anchors.push({ - panel, - x: xMid, - y: rect.y2 - 10, - mode: DockMode.Bottom, - previewRect: rect.withY1(rect.y1 + (rect.y2 - rect.y1) * 3 / 4), - }) - - layout.anchors.push({ - panel, - x: xMid, - y: rect.y1 + 10, - mode: DockMode.Top, - previewRect: rect.withY2(rect.y1 + (rect.y2 - rect.y1) / 4), - }) -} - - -export function getLayout(root: State, rect: Rect): Layout -{ - const layout: Layout = - { - panelRects: [], - windows: [], - dividers: [], - anchors: [], - } - - traverseLayout(root.rootPanel, rect, layout) - - for (let fp = 0; fp < root.floatingPanels.length; fp++) - { - const floatingPanel = root.floatingPanels[fp] - - const panelRect = - { - panel: floatingPanel, - rect: floatingPanel.rect, - floating: true, - zIndex: fp + 1, - } - - for (let w = 0; w < floatingPanel.windowIds.length; w++) - { - layout.windows.push( - { - windowId: floatingPanel.windowIds[w], - tabIndex: w, - panel: floatingPanel, - panelRect, - }) - } - - layout.panelRects.push(panelRect) - - layout.anchors.push({ - panel: floatingPanel, - x: floatingPanel.rect.xCenter, - y: floatingPanel.rect.yCenter, - mode: DockMode.Full, - previewRect: floatingPanel.rect, - }) - } - - return layout -} - - -export function getContentRect(root: State, rect: Rect, windowId: WindowId): Rect | undefined -{ - const layout = getLayout(root, rect) - return layout.panelRects.find(p => p.panel.windowIds.some(c => c === windowId))!.rect -} \ No newline at end of file diff --git a/src/main.js b/src/main.js deleted file mode 100644 index 3cbb697..0000000 --- a/src/main.js +++ /dev/null @@ -1,12 +0,0 @@ -import React from "react" -import ReactDOM from "react-dom" -import ReactDOMClient from "react-dom/client" -import App from "./App.tsx" - - -document.body.onload = function() -{ - const container = document.getElementById("divApp") - const root = ReactDOMClient.createRoot(container) - root.render() -} \ No newline at end of file diff --git a/src/menubar/Item.tsx b/src/menubar/Item.tsx deleted file mode 100644 index 2aba16b..0000000 --- a/src/menubar/Item.tsx +++ /dev/null @@ -1,48 +0,0 @@ -import React from "react" -import * as Popup from "../popup" -import Rect from "../util/rect" - - -export interface ItemProps -{ - children: JSX.Element - label: string -} - - -export function Item(props: ItemProps) -{ - const popupElemRef = React.useRef(() => props.children) - - const refButton = React.useRef(null) - const onClick = () => - { - if (!props.children) - return - - const domRect = refButton.current!.getBoundingClientRect() - Popup.global.rect = new Rect(domRect.x, domRect.y, domRect.width, domRect.height) - Popup.global.elem = popupElemRef.current - Popup.notifyObservers() - } - - const isOpen = - !!props.children && - Popup.global.elem === popupElemRef.current - - return -} \ No newline at end of file diff --git a/src/menubar/Root.tsx b/src/menubar/Root.tsx deleted file mode 100644 index 944b995..0000000 --- a/src/menubar/Root.tsx +++ /dev/null @@ -1,20 +0,0 @@ -import React from "react" -import Rect from "../util/rect" -import * as Popup from "../popup" - - -export function Root(props: any) -{ - return
- - { props.children } - -
-} \ No newline at end of file diff --git a/src/menubar/index.ts b/src/menubar/index.ts deleted file mode 100644 index 7d47057..0000000 --- a/src/menubar/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export * from "./Root" -export * from "./Item" \ No newline at end of file diff --git a/src/playback/_global.ts b/src/playback/_global.ts deleted file mode 100644 index e84b227..0000000 --- a/src/playback/_global.ts +++ /dev/null @@ -1,326 +0,0 @@ -import React from "react" -import * as Playback from "./index" -import * as Project from "../project" -import * as Theory from "../theory" -import Rational from "../util/rational" -import Range from "../util/range" -import { SynthManager } from "./synthManager" -import * as AynscUtils from "../util/async" -import * as GlobalObservable from "../util/globalObservable" - - -export interface GlobalData -{ - synth: Playback.SynthManager - synthLoading: boolean - - playing: boolean - firstPlayingFrame: boolean - startTime: Rational - nextStartTime: Rational - playTimeFloat: number - playTimeFloatPrev: number - playTime: Rational - playTimePrev: Rational - refreshTimeMs: number - - requestAnimationFrameId: number - requestAnimationFrameTimestamp: number - requestAnimationFrameDate: Date - setIntervalId: number -} - - -export let global: GlobalData = null! - - -export function initGlobal() -{ - global = - { - synth: new Playback.SynthManager(false), - synthLoading: false, - - playing: false, - firstPlayingFrame: false, - startTime: new Rational(0), - nextStartTime: new Rational(0), - playTimeFloat: 0, - playTimeFloatPrev: 0, - playTime: new Rational(0), - playTimePrev: new Rational(0), - refreshTimeMs: 0, - - requestAnimationFrameId: 0, - requestAnimationFrameTimestamp: 0, - requestAnimationFrameDate: new Date(), - setIntervalId: 0, - } -} - - -export const globalObservable = GlobalObservable.makeGlobalObservable() -export function useGlobal() -{ - GlobalObservable.useGlobalObservable(globalObservable) - - - React.useEffect(() => - { - global.synthLoading = true - globalObservable.notifyObservers() - - global.synth.prepare(Project.global.project).then(() => - { - global.synthLoading = false - globalObservable.notifyObservers() - }) - - }, [Playback.getSflibMeta(), global.playing, Project.global.project.tracks]) -} - - -export function notifyObservers() -{ - globalObservable.notifyObservers() -} - - -function processFrame(deltaTimeMs: number, canRedrawScreen: boolean) -{ - if (deltaTimeMs > 70) - return - - global.playTimeFloatPrev = global.playTimeFloat - global.playTimePrev = global.playTime - - const measuresPerSecond = (Project.global.project.baseBpm / 4 / 60) - - global.synth.process(deltaTimeMs) - global.playTimeFloat += deltaTimeMs / 1000 * measuresPerSecond - global.playTime = Rational.fromFloat(global.playTimeFloat, 10000) - - Playback.feedNotes( - global.synth, - Project.global.project, - global.firstPlayingFrame, - 0.05 + global.synth.audioCtx.currentTime * 1000, - global.startTime, - new Range( - global.playTimePrev, - global.playTime, - true, - false)) - - global.refreshTimeMs += deltaTimeMs - if (canRedrawScreen)// && global.refreshTimeMs >= 1000 / 30) - { - global.refreshTimeMs = 0 - notifyObservers() - } - - global.firstPlayingFrame = false - - if (global.playTime.compare(Project.global.project.range.end) > 0 && - global.synth.isFinished()) - { - setPlaying(false) - } -} - -function processAnimationFrame(timestamp: number) -{ - const prevTimestamp = global.requestAnimationFrameTimestamp - global.requestAnimationFrameTimestamp = timestamp - - global.requestAnimationFrameDate = new Date() - - const deltaTimeMs = - global.synthLoading ? 0 : - (prevTimestamp < 0 ? 0 : timestamp - prevTimestamp) - - if (deltaTimeMs > 0 && deltaTimeMs < 250) - { - processFrame(deltaTimeMs, true) - } - - if (global.playing) - { - global.requestAnimationFrameId = - requestAnimationFrame(processAnimationFrame) - } -} - -function processInterval(deltaTimeMs: number) -{ - // Take over from requestAnimationFrame and - // process playback on the setInterval callback only if - // requestAnimationFrame was blocked (by e.g. being in the background) - - const msSinceLastRequestAnimationFrame = - (new Date().getTime()) - - global.requestAnimationFrameDate.getTime() - - if (msSinceLastRequestAnimationFrame > 250 && - deltaTimeMs > 0 && deltaTimeMs < 250) - { - processFrame(deltaTimeMs, false) - } -} - -export function setPlaying(playing: boolean) -{ - if (global.requestAnimationFrameId != 0) - { - global.synth.stopAll() - - cancelAnimationFrame(global.requestAnimationFrameId) - global.requestAnimationFrameId = 0 - - clearInterval(global.setIntervalId) - global.setIntervalId = 0 - } - - global.playing = playing - global.firstPlayingFrame = true - global.startTime = global.nextStartTime - global.playTime = global.nextStartTime - global.playTimePrev = global.nextStartTime - global.playTimeFloat = global.nextStartTime.asFloat() - global.playTimeFloatPrev = global.nextStartTime.asFloat() - global.refreshTimeMs = 0 - - if (playing) - { - global.requestAnimationFrameId = - requestAnimationFrame(processAnimationFrame) - - global.requestAnimationFrameTimestamp = 0 - global.requestAnimationFrameDate = new Date() - - global.setIntervalId = - +setInterval(() => processInterval(1000 / 60), 1000 / 60) - } - - notifyObservers() -} - - -export function setStartTime(startTime: Rational) -{ - global.nextStartTime = startTime - notifyObservers() -} - - -export function togglePlaying() -{ - setPlaying(!global.playing) -} - - -export function playNotePreview( - trackId: Project.ID, - midiPitch: number, - volumeDb: number, - velocity: number) -{ - if (global.playing) - return - - const time = 0.05 + 1000 * global.synth.audioCtx.currentTime - - //global.synth.stopAll() - global.synth.playNote({ - trackId, - noteId: -1, - - startMs: time, - durationMs: 100, - - midiPitchSeq: [{ timeMs: time, value: midiPitch }], - volumeSeq: [{ timeMs: time, value: volumeDb }], - velocitySeq: [{ timeMs: time, value: velocity }], - }) -} - - -export function playChordPreview( - trackId: Project.ID, - chord: Theory.Chord, - volumeDb: number, - velocity: number) -{ - if (global.playing) - return - - const time = 0.05 + 1000 * global.synth.audioCtx.currentTime - - for (const midiPitch of chord.strummingPitches) - { - global.synth.playNote({ - trackId: trackId, - noteId: -1, - - startMs: time, - durationMs: 250, - - midiPitchSeq: [{ timeMs: time, value: midiPitch }], - volumeSeq: [{ timeMs: time, value: volumeDb }], - velocitySeq: [{ timeMs: time, value: velocity }], - }) - } -} - - -export async function renderToBuffer( - range: Range, - onProgress: (p: number) => void) - : Promise -{ - const project = Project.global.project - const startMs = Project.getMillisecondsAt(project, range.start) - - // FIXME: Compute correct release time for last notes in the song - const endMs = 1000 + Project.getMillisecondsAt(project, range.end) - const durationMs = endMs - startMs - - const sampleRate = 44100 - const sampleCount = sampleRate * Math.ceil(durationMs / 1000) - - console.log( - "renderToBuffer", - "range", range, - "durationMs", durationMs, - "sampleCount", sampleCount) - - const offlineSynth = new SynthManager(true, sampleCount, sampleRate) - await offlineSynth.prepare(project) - - Playback.feedNotes( - offlineSynth, - project, - true, - 0, - range.start, - range) - - const offlineCtx = offlineSynth.audioCtx as OfflineAudioContext - - let audioBuffer: AudioBuffer | null = null - offlineCtx.startRendering().then(b => audioBuffer = b) - - while (!audioBuffer) - { - const progress = offlineCtx.currentTime / (durationMs / 1000) - onProgress(progress) - await AynscUtils.waitSeconds(0.2) - } - - await offlineSynth.destroy() - - onProgress(1) - await AynscUtils.waitSeconds(0.2) - - return audioBuffer -} \ No newline at end of file diff --git a/src/playback/index.ts b/src/playback/index.ts deleted file mode 100644 index 81bccd9..0000000 --- a/src/playback/index.ts +++ /dev/null @@ -1,7 +0,0 @@ -export * from "./_global" -export * from "./synthManager" -export * from "./instrument" -export * from "./instrumentBasic" -export * from "./instrumentSflib" -export * from "./projectFeed" -export * from "./sflib" \ No newline at end of file diff --git a/src/playback/instrument.ts b/src/playback/instrument.ts deleted file mode 100644 index bbbe196..0000000 --- a/src/playback/instrument.ts +++ /dev/null @@ -1,73 +0,0 @@ -import * as Playback from "./index" -import * as Project from "../project" - - -export interface NoteRequest -{ - trackId: Project.ID - noteId: Project.ID - - startMs: number - durationMs: number - - midiPitchSeq: TimeVariableProperty[] - volumeSeq: TimeVariableProperty[] - velocitySeq: TimeVariableProperty[] -} - - -export interface TimeVariableProperty -{ - timeMs: number - value: number -} - - -export class Instrument -{ - synth: Playback.SynthManager - outputNode: AudioNode - - - constructor(synth: Playback.SynthManager, outputNode: AudioNode) - { - this.synth = synth - this.outputNode = outputNode - } - - - async prepare() - { - - } - - - async destroy() - { - - } - - - isFinished() - { - return true - } - - - playNote(request: Playback.NoteRequest) - { - - } - - - stopAll() - { - - } - - - process(deltaTimeMs: number) - { - - } -} \ No newline at end of file diff --git a/src/playback/instrumentBasic.ts b/src/playback/instrumentBasic.ts deleted file mode 100644 index 6ee9149..0000000 --- a/src/playback/instrumentBasic.ts +++ /dev/null @@ -1,260 +0,0 @@ -import * as Playback from "./index" -import * as Project from "../project" -import * as MathUtils from "../util/mathUtils" - - -interface Sample -{ - frequency: number - buffer: AudioBuffer | null -} - - -interface Note -{ - released: boolean - releasedTimerMs: number - voices: Voice[] -} - - -interface Voice -{ - nodeSrc: AudioBufferSourceNode - nodeEnvelope: GainNode - nodeVolume: GainNode -} - - -const releaseDurationMs = 100 - - -export class InstrumentBasic extends Playback.Instrument -{ - samples: Sample[] - notes: Map - - - constructor(synth: Playback.SynthManager, outputNode: AudioNode) - { - super(synth, outputNode) - this.samples = [] - this.notes = new Map() - } - - - async prepare() - { - if (this.samples.length > 0) - return - - const sampleDefs: [number, string][] = - [ - [ 55.0, "audio/piano/a1.mp3"], - [ 110.0, "audio/piano/a2.mp3"], - [ 220.0, "audio/piano/a3.mp3"], - [ 440.0, "audio/piano/a4.mp3"], - [ 880.0, "audio/piano/a5.mp3"], - [1760.0, "audio/piano/a6.mp3"], - //[3520.0, "audio/piano/a7.mp3"] - ] - - this.samples = [] - this.samples = await Promise.all(sampleDefs.map(s => this.load(s[0], s[1]))) - } - - - async load(frequency: number, url: string): Promise - { - const sample: Sample = - { - buffer: null, - frequency - } - - return new Promise((resolve, reject) => - { - fetch(url) - .then(res => res.arrayBuffer()) - .then(arrayBuf => - { - this.synth.audioCtx.decodeAudioData( - arrayBuf, - (audioBuf: AudioBuffer) => - { - sample.buffer = audioBuf - resolve(sample) - }, - () => - { - console.log("Error loading audio <" + url + ">") - reject() - }) - }) - }) - } - - - isFinished() - { - return this.notes.size == 0 - } - - - playNote(request: Playback.NoteRequest) - { - /*if (this.notes.has(request.noteId)) - return - - // Find the primary sample whose frequency is nearest to the desired frequency. - let sample1 = 0 - let sample1Dist = Math.abs(this.samples[0].frequency - desiredFreq) - for (let i = 1; i < this.samples.length; i++) - { - const dist = Math.abs(this.samples[i].frequency - desiredFreq) - if (dist < sample1Dist) - { - sample1 = i - sample1Dist = dist - } - } - - // Find the sample whose frequency is second nearest to the desired frequency, - // unless the primary sample is already exactly a match. - // Then, calculate a cross-fade between the two. - // TODO: Probably should use an equal-power (logarithmic) cross-fade, - // but this seems good enough for now. - let sample2 = null - - let volume1 = 1 - let volume2 = 0 - - if (this.samples[sample1].frequency < desiredFreq) - { - if (sample1 + 1 < this.samples.length) - { - sample2 = sample1 + 1 - - const t = - (desiredFreq - this.samples[sample1].frequency) / - (this.samples[sample2].frequency - this.samples[sample1].frequency) - - volume1 = 1 - t - volume2 = t - } - } - else if (this.samples[sample1].frequency > desiredFreq) - { - if (sample1 - 1 >= 0) - { - sample2 = sample1 - 1 - - const t = - (desiredFreq - this.samples[sample2].frequency) / - (this.samples[sample1].frequency - this.samples[sample2].frequency) - - volume1 = t - volume2 = 1 - t - } - } - - // Play the samples. - const voices = [ this.playVoice(this.samples[sample1], desiredFreq, desiredVolume * volume1) ] - if (sample2 != null) - voices.push(this.playVoice(this.samples[sample2], desiredFreq, desiredVolume * volume2)) - - const note: Note = - { - released: false, - releasedTimerMs: 0, - voices, - } - - this.notes.set(noteId, note)*/ - } - - - playVoice(sample: Sample, midiPitch: number, volume: number): Voice - { - let sourceNode = this.synth.audioCtx.createBufferSource() - sourceNode.playbackRate.value = MathUtils.midiToHertz(midiPitch) / sample.frequency - sourceNode.buffer = sample.buffer - - let envelopeNode = this.synth.audioCtx.createGain() - envelopeNode.gain.value = 1 - - let volumeNode = this.synth.audioCtx.createGain() - volumeNode.gain.value = volume - - sourceNode.connect(envelopeNode) - envelopeNode.connect(volumeNode) - volumeNode.connect(this.outputNode) - - sourceNode.start(0) - - return { - nodeSrc: sourceNode, - nodeEnvelope: envelopeNode, - nodeVolume: volumeNode, - } - } - - - releaseNote(noteId: Project.ID) - { - const note = this.notes.get(noteId) - if (!note) - return - - if (!note.released) - { - note.released = true - for (let i = 0; i < note.voices.length; i++) - { - note.voices[i].nodeEnvelope.gain.setValueAtTime( - 1, this.synth.audioCtx.currentTime) - - note.voices[i].nodeEnvelope.gain.linearRampToValueAtTime( - 0, this.synth.audioCtx.currentTime + releaseDurationMs / 1000) - } - } - } - - - stopNote(noteId: Project.ID) - { - const note = this.notes.get(noteId) - if (!note) - return - - note.released = true - note.releasedTimerMs = releaseDurationMs * 100 - - for (let i = 0; i < note.voices.length; i++) - note.voices[i].nodeSrc.stop() - - this.notes.delete(noteId) - } - - - stopAll() - { - for (const noteId of [...this.notes.keys()]) - this.stopNote(noteId) - } - - - process(deltaTimeMs: number) - { - for (const [noteId, note] of [...this.notes.entries()]) - { - if (note.released) - { - note.releasedTimerMs += deltaTimeMs - - if (note.releasedTimerMs > releaseDurationMs * 2) - this.stopNote(noteId) - } - } - } -} \ No newline at end of file diff --git a/src/playback/instrumentSflib.ts b/src/playback/instrumentSflib.ts deleted file mode 100644 index 758f64a..0000000 --- a/src/playback/instrumentSflib.ts +++ /dev/null @@ -1,256 +0,0 @@ -import * as Playback from "./index" -import * as Project from "../project" -import * as MathUtils from "../util/mathUtils" - - -const notesToDelete = new Set() - - -interface Note -{ - request: Playback.NoteRequest - zone: Playback.SflibInstrumentZone - timePassedMs: number - endTimeMs: number - nodeSrc: AudioBufferSourceNode - nodeEnvelope: GainNode - nodeVolume: GainNode -} - - -export class InstrumentSflib extends Playback.Instrument -{ - collectionId: string - instrumentId: string - sflibInstrument: Playback.SflibInstrument | null - notes: Note[] - - - constructor( - synth: Playback.SynthManager, - outputNode: AudioNode, - collectionId: string, - instrumentId: string) - { - super(synth, outputNode) - this.collectionId = collectionId - this.instrumentId = instrumentId - this.sflibInstrument = null - this.notes = [] - } - - - async prepare() - { - this.sflibInstrument = await Playback.sflibGetInstrument( - this.collectionId, this.instrumentId, this.synth.audioCtx) - - if (!this.sflibInstrument) - { - console.error("could not load sflib " + this.collectionId + "/" + this.instrumentId) - return - } - } - - - async destroy() - { - - } - - - isFinished() - { - return !this.sflibInstrument || this.notes.length == 0 - } - - - playNote(request: Playback.NoteRequest) - { - if (!this.sflibInstrument) - return - - //if (this.notes.has(request.noteId)) - // return - - const midiPitch = request.midiPitchSeq[0].value - - let zone = this.sflibInstrument.zones.find(z => - midiPitch >= z.midiPitchMin && - midiPitch <= z.midiPitchMax) - - if (!zone) - { - let nearestMidiPitchDistance = Infinity - - for (const z of this.sflibInstrument.zones) - { - const distance = Math.min( - Math.abs(z.midiPitchMin - midiPitch), - Math.abs(z.midiPitchMax - midiPitch)) - - if (distance < nearestMidiPitchDistance) - { - nearestMidiPitchDistance = distance - zone = z - } - } - } - - if (!zone) - return - - const freq = MathUtils.midiToHertz(midiPitch) - - const audioBuffer = this.sflibInstrument.audioBuffers[zone.sampleIndex] - - - const sourceNode = this.synth.audioCtx.createBufferSource() - sourceNode.playbackRate.value = freq / MathUtils.midiToHertz(zone.midiPitchBase) - sourceNode.buffer = audioBuffer - sourceNode.loop = (zone.loopMode == "loop") - sourceNode.loopStart = zone.loopStartIndex / audioBuffer.sampleRate - sourceNode.loopEnd = zone.loopEndIndex / audioBuffer.sampleRate - - - const time = 0.05 + Math.max(0, request.startMs / 1000) - const delayTime = time + zone.volEnvDelaySec - const attackTime = delayTime + (zone.volEnvAttackSec || 0.005) - const holdTime = attackTime + zone.volEnvHoldSec - const decayTime = holdTime + zone.volEnvDecaySec - const sustainLevel = zone.volEnvSustain || 0.0001 - const releaseTime = time + request.durationMs / 1000 - const endTime = releaseTime + Math.max(zone.volEnvReleaseSec, 0.05) - - /* - - Envelope parameters: - - | 100% level - | ______ - | /| |\ - | / | | \ sustainLevel - | / | | \____________ - | / | | | |\ - | / | | | | \ - | / | | | | \ - --+------------------------------------------- - ^ ^ ^ ^ ^ ^ ^ endTime - | | | | | + releaseTime - | | | | + decayTime - | | | + holdTime - | | + attackTime - | + delayTime - + time - - */ - - const releaseTimeCap = releaseTime - 0.001 - const cappedDelayTime = Math.min(delayTime, releaseTimeCap) - const cappedAttackTime = Math.min(attackTime, releaseTimeCap) - const cappedHoldTime = Math.min(holdTime, releaseTimeCap) - - const releaseF = (releaseTime - cappedHoldTime) / (decayTime - cappedHoldTime) - const releaseLevel = releaseF >= 1 ? sustainLevel : Math.pow(sustainLevel, releaseF) - - const envelopeNode = this.synth.audioCtx.createGain() - envelopeNode.gain.value = 0 - envelopeNode.gain.setValueAtTime(0.0001, cappedDelayTime) - envelopeNode.gain.exponentialRampToValueAtTime(1, cappedAttackTime) - envelopeNode.gain.setValueAtTime(1, cappedHoldTime) - - if (decayTime < releaseTime) - { - envelopeNode.gain.exponentialRampToValueAtTime(sustainLevel, decayTime) - envelopeNode.gain.setValueAtTime(sustainLevel, releaseTime) - envelopeNode.gain.exponentialRampToValueAtTime(0.0001, endTime) - } - else - { - envelopeNode.gain.exponentialRampToValueAtTime(releaseLevel, releaseTime) - envelopeNode.gain.setValueAtTime(releaseLevel, releaseTime) - envelopeNode.gain.exponentialRampToValueAtTime(0.0001, endTime) - } - - /*console.log("playNote", this.synth.audioCtx.currentTime, request) - console.log("envelope", - "del", (delayTime - time).toFixed(3), - "atk", (attackTime - time).toFixed(3), - "hld", (holdTime - time).toFixed(3), - "dec", (decayTime - time).toFixed(3), - "::", - sustainLevel.toFixed(3), - releaseLevel.toFixed(3), - "::", - "rel", (releaseTimeCap - time).toFixed(3), - "end", (endTime - time).toFixed(3))*/ - - const volumeNode = this.synth.audioCtx.createGain() - volumeNode.gain.value = MathUtils.dbToLinearGain(request.volumeSeq[0].value) - - - sourceNode.connect(envelopeNode) - envelopeNode.connect(volumeNode) - volumeNode.connect(this.outputNode) - - sourceNode.start(time) - - const note: Note = - { - request, - zone, - timePassedMs: -(time - this.synth.audioCtx.currentTime) * 1000, - endTimeMs: (endTime - time) * 1000, - nodeSrc: sourceNode, - nodeEnvelope: envelopeNode, - nodeVolume: volumeNode, - } - - this.notes.push(note) - } - - - stopNote(note: Note) - { - note.nodeSrc.stop() - note.nodeSrc.disconnect() - } - - - stopAll() - { - for (const note of this.notes) - this.stopNote(note) - - this.notes = [] - } - - - process(deltaTimeMs: number) - { - notesToDelete.clear() - - for (const note of this.notes) - { - note.timePassedMs += deltaTimeMs - - if (note.timePassedMs > note.endTimeMs) - { - this.stopNote(note) - notesToDelete.add(note) - } - else if (note.zone.loopMode != "loop") - { - const sample = this.sflibInstrument!.audioBuffers[note.zone.sampleIndex] - if (note.timePassedMs > sample.length / sample.sampleRate * 1000) - { - this.stopNote(note) - notesToDelete.add(note) - } - } - } - - this.notes = this.notes.filter(n => !notesToDelete.has(n)) - notesToDelete.clear() - } -} \ No newline at end of file diff --git a/src/playback/projectFeed.ts b/src/playback/projectFeed.ts deleted file mode 100644 index 7ac9cc1..0000000 --- a/src/playback/projectFeed.ts +++ /dev/null @@ -1,271 +0,0 @@ -import * as Playback from "./index" -import * as Project from "../project" -import * as Theory from "../theory" -import Rational from "../util/rational" -import Range from "../util/range" -import * as MathUtils from "../util/mathUtils" - - -export function feedNotes( - synth: Playback.SynthManager, - project: Project.Root, - isStart: boolean, - audioOffsetMs: number, - playbackStart: Rational, - feedRange: Range) -{ - const feedRangeStartMs = Project.getMillisecondsAt(project, feedRange.start) - - const makeNoteRequest = (trackId: Project.ID, range: Range, midiPitch: number, volumeDb: number, velocity: number) => - { - const startMs = - Project.getMillisecondsAt(project, range.start.max(playbackStart)) + - audioOffsetMs - feedRangeStartMs - - const endMs = - Project.getMillisecondsAt(project, range.end) + - audioOffsetMs - feedRangeStartMs - - const request: Playback.NoteRequest = - { - trackId: trackId, - noteId: -1, - - startMs, - durationMs: endMs - startMs, - - midiPitchSeq: [{ timeMs: startMs, value: midiPitch }], - volumeSeq: [{ timeMs: startMs, value: volumeDb }], - velocitySeq: [{ timeMs: startMs, value: velocity }], - } - - return request - } - - for (const [track, note] of iterNotesAtRange(project, feedRange)) - { - if (feedRange.overlapsPoint(note.range.start) || - (isStart && feedRange.overlapsPoint(playbackStart))) - { - const request = makeNoteRequest( - track.id, note.range, - note.midiPitch, - note.volumeDb, - note.velocity) - - synth.playNote(request) - } - } - - const chordTrack = Project.getTrack(project, project.chordTrackId, "chords") - const chordList = project.lists.get(project.chordTrackId) - if (chordTrack && chordList) - { - for (const chord of chordList.iterAtRange(feedRange)) - { - if (feedRange.overlapsPoint(chord.range.start) || - (isStart && feedRange.overlapsPoint(playbackStart))) - { - playChord(synth, project, chordTrack, chord as Project.Chord, makeNoteRequest) - } - } - } -} - - -function *iterNotesAtRange( - project: Project.Root, - range: Range) - : Generator<[Project.TrackNotes, Project.Note], void, void> -{ - for (const track of project.tracks) - { - if (track.trackType != "notes") - continue - - const trackElems = project.lists.get(track.id) - if (!trackElems) - continue - - for (const noteBlock of trackElems.iterAtRange(range)) - { - if (noteBlock.type != "noteBlock") - continue - - const noteList = project.lists.get(noteBlock.id) - if (!noteList) - continue - - for (const elem of noteList.iterAtRange(range.displace(noteBlock.range.start.negate()))) - { - const newElem = - { - ...elem, - range: elem.range - .displace(noteBlock.range.start) - .intersect(noteBlock.range), - } - - if (newElem.range.duration.isZero()) - continue - - if (!newElem.range.overlapsRange(range)) - continue - - yield [track, newElem as Project.Note] - } - } - } -} - - -function playChord( - synth: Playback.SynthManager, - project: Project.Root, - chordTrack: Project.TrackChords, - chord: Project.Chord, - makeNoteRequest: (trackId: Project.ID, range: Range, midiPitch: number, volumeDb: number, velocity: number) => Playback.NoteRequest) -{ - const addNoteEvent = (start: Rational, duration: Rational, midiPitch: number, volume: number) => - { - const request = makeNoteRequest( - chordTrack.id, - Range.fromStartDuration(start, duration), - midiPitch, - MathUtils.linearGainToDb(volume), - 1) - - synth.playNote(request) - } - - /*if (chord.range.end.compare(startTick) <= 0) - continue*/ - - const pitches = chord.chord.strummingPitches - - /*if (!useChordPatterns) - { - for (let j = 0; j < pitches.length; j++) - addNoteEvent(chord.range.start, chord.range.duration, pitches[j], chordVolumeMul) - - continue - }*/ - - const meterCh = Project.meterChangeAt(project, chordTrack.id, chord.range.start) - const meter = meterCh ? meterCh.meter : Project.defaultMeter() - const meterBeatLength = new Rational(1, meter.denominator) - const measureBreak = null//that.forcedMeasures.findActiveAt(chord.range.start) - const pattern = getChordStrummingPattern(meter) - - let tick = meterCh ? meterCh.range.start : chord.range.start - //if (measureBreak != null) - // tick.max(measureBreak.tick) - - let skipTick = new Rational(0) - let patternIndex = 0 - let mustPlayFirstBeat = false - - while (tick.compare(chord.range.end) < 0) - { - const patternBeat = pattern[patternIndex] - const patternBeatKind = patternBeat[0] - let patternBeatLength = patternBeat[1].multiply(meterBeatLength) - patternIndex = (patternIndex + 1) % pattern.length - - let nextTick = tick.add(patternBeatLength) - if (nextTick.compare(chord.range.end) > 0) - { - nextTick = chord.range.end - patternBeatLength = nextTick.subtract(tick) - } - - // Handle beats after the first one. - if (tick.compare(chord.range.start) > 0 && skipTick.compare(new Rational(0)) <= 0) - { - if (mustPlayFirstBeat) - { - mustPlayFirstBeat = false - for (let j = 0; j < pitches.length; j++) - addNoteEvent(chord.range.start, tick.subtract(chord.range.start), pitches[j], 1) - } - - switch (patternBeatKind) - { - case 0: - { - for (let j = 0; j < pitches.length; j++) - addNoteEvent(tick, patternBeatLength, pitches[j], 0.9) - break - } - case 1: - { - for (let j = 1; j < pitches.length; j++) - addNoteEvent(tick, patternBeatLength, pitches[j], 0.5) - break - } - case 2: - { - addNoteEvent(tick, patternBeatLength, pitches[0], 0.5) - break - } - } - } - - skipTick = skipTick.subtract(patternBeatLength) - - if (tick.compare(chord.range.start) <= 0 && nextTick.compare(chord.range.start) > 0) - { - mustPlayFirstBeat = true - - if (!(tick.compare(chord.range.start) == 0 && patternBeatKind == 0)) - skipTick = pattern[0][1].multiply(meterBeatLength) - } - - tick = nextTick - } - - if (mustPlayFirstBeat) - { - mustPlayFirstBeat = false - for (let j = 0; j < pitches.length; j++) - addNoteEvent(chord.range.start, tick.subtract(chord.range.start), pitches[j], 0.7) - } -} - - -function getChordStrummingPattern(meter: Theory.Meter) -{ - // [[beat kind, duration], ...] - // Beat kinds: - // 0: Full chord - // 1: Full chord minus bass - // 2: Only bass - const one: [number, Rational][] = - [[0, new Rational(1)]] - - const two: [number, Rational][] = - [[0, new Rational(1)], [1, new Rational(1, 2)], [2, new Rational(1, 2)]] - - const three: [number, Rational][] = - [[0, new Rational(1)], [1, new Rational(1) ], [1, new Rational(1) ]] - - switch (meter.numerator) - { - case 2: return two - case 3: return three - case 4: return two.concat(two) - case 5: return three.concat(two) - case 6: return three.concat(three) - case 7: return three.concat(two).concat(two) - case 8: return two.concat(two).concat(two).concat(two) - case 9: return three.concat(three).concat(three) - - default: - { - let pattern: [number, Rational][] = [] - for (let i = 0; i < meter.numerator; i++) - pattern = pattern.concat(one) - return pattern - } - } -} diff --git a/src/playback/sflib.ts b/src/playback/sflib.ts deleted file mode 100644 index 1090717..0000000 --- a/src/playback/sflib.ts +++ /dev/null @@ -1,234 +0,0 @@ -import * as AsyncUtils from "../util/async" - - -export const sflibUrl = "https://sflib.hlorenzi.com/" -export const sflibLocalUrl = "/library/" - - -export interface SflibMeta -{ - ready: boolean - collections: SflibCollectionMeta[] - collectionsById: Map -} - - -export interface SflibCollectionMeta -{ - id: string - name: string - instruments: SflibInstrumentMeta[] - instrumentsById: Map - instrumentsByPreset: Map -} - - -export interface SflibInstrumentMeta -{ - id: string - filename: string - filesize: number - name: string - midiBank: number - midiPreset: number -} - - -export interface SflibInstrument -{ - id: string - name: string - midiPreset: number - midiBank: number - zones: SflibInstrumentZone[] - samples: string[] - audioBuffers: AudioBuffer[] -} - - -export interface SflibInstrumentZone -{ - sampleIndex: number - sampleRate: number - - loopStartIndex: number - loopEndIndex: number - loopMode: string - - midiPitchMin: number - midiPitchMax: number - midiPitchBase: number - midiVelMin: number - midiVelMax: number - - volEnvDelaySec: number - volEnvAttackSec: number - volEnvHoldSec: number - volEnvDecaySec: number - volEnvSustain: number - volEnvReleaseSec: number - - exclusiveClass: number | null -} - - -async function fetchSflibFile(filename: string): Promise -{ - let res = null - - try - { - res = await fetch(sflibLocalUrl + filename) - } - catch {} - - if (res?.ok) - return res - - try - { - res = await fetch(sflibUrl + filename) - } - catch {} - - if (res?.ok) - return res - - return null -} - - -async function loadSflibMeta(): Promise -{ - const data = await fetchSflibFile("library.json") - if (!data) - throw "could not load sflib metadata" - - const meta: SflibMeta = await data.json() - - meta.collectionsById = new Map() - - for (const coll of meta.collections) - { - meta.collectionsById.set(coll.id, coll) - - coll.instrumentsById = new Map() - coll.instrumentsByPreset = new Map() - for (const instr of coll.instruments) - { - const preset = instr.midiBank * 128 + instr.midiPreset - coll.instrumentsById.set(instr.id, instr) - coll.instrumentsByPreset.set(preset, instr) - } - - coll.instruments.sort((a, b) => - { - const aDrumkit = a.midiBank == 128 - const bDrumkit = b.midiBank == 128 - - if (aDrumkit && bDrumkit) - return a.midiPreset - b.midiPreset - - if (aDrumkit != bDrumkit) - return (aDrumkit ? 1 : 0) - (bDrumkit ? 1 : 0) - - // group similar presets, not banks - return (a.midiBank + a.midiPreset * 128) - (b.midiBank + b.midiPreset * 128) - }) - } - - meta.ready = true - console.log("loaded sflib meta", meta) - return meta -} - - -let sflibMeta: SflibMeta | null = null -loadSflibMeta() - .then(meta => sflibMeta = meta) - - -export function getSflibMeta() -{ - return sflibMeta -} - - -// FIXME: Apparently, some kind of `WeakValueMap` is what I actually wanted here. -const sflibCache = new WeakMap() - - -const sflibGetInstrumentLock = new AsyncUtils.Mutex() - - -export async function sflibGetInstrument( - collectionId: string, - instrumentId: string, - audioCtx: BaseAudioContext) - : Promise -{ - if (!sflibMeta) - return null - - const collMeta = sflibMeta.collectionsById.get(collectionId) - if (!collMeta) - return null - - const instrMeta = collMeta.instrumentsById.get(instrumentId) - if (!instrMeta) - return null - - await sflibGetInstrumentLock.acquire() - - const cached = sflibCache.get(instrMeta) - if (cached) - { - await sflibGetInstrumentLock.release() - return cached - } - - const instrFilename = instrMeta.filename - - const data = await fetchSflibFile(collectionId + "/" + instrFilename) - if (!data) - return null - - const instr: SflibInstrument = await data.json() - - let yieldCount = 0 - - instr.audioBuffers = [] - const sampleRate = instr.zones[0].sampleRate - for (const sampleRaw of instr.samples) - { - const bytes = Uint8Array.from(atob(sampleRaw), c => c.charCodeAt(0)) - const audioBuffer = audioCtx.createBuffer(1, bytes.length / 2, sampleRate) - const audioBufferData = audioBuffer.getChannelData(0) - for (let i = 0; i < bytes.length / 2; i++) - { - const b0 = bytes[i * 2] & 0xff - const b1 = bytes[i * 2 + 1] & 0xff - let uint16 = ((b0 << 8) | b1) & 0xffff - if ((uint16 & 0x8000) != 0) - uint16 = -(0x10000 - uint16) - - audioBufferData[i] = (uint16 / 0x8000) * 2 - - yieldCount++ - if (yieldCount >= 100000) - { - yieldCount = 0 - await AsyncUtils.waitFrame() - } - } - - instr.audioBuffers.push(audioBuffer) - } - - console.log("loaded sflib instrument", instr) - - sflibCache.set(instrMeta, instr) - - await sflibGetInstrumentLock.release() - return instr -} \ No newline at end of file diff --git a/src/playback/synthManager.ts b/src/playback/synthManager.ts deleted file mode 100644 index 5a1266e..0000000 --- a/src/playback/synthManager.ts +++ /dev/null @@ -1,148 +0,0 @@ -import * as Playback from "./index" -import * as Project from "../project" -import * as MathUtils from "../util/mathUtils" - - -export class SynthManager -{ - audioCtx: BaseAudioContext - nodeCompressor: DynamicsCompressorNode - nodeGlobalVolume: GainNode - nodeTrackVolumes: GainNode[] - - trackInstruments: Map - - - constructor(toBuffer?: boolean, bufferLen?: number, bufferSampleRate?: number) - { - this.trackInstruments = new Map() - - if (toBuffer) - { - this.audioCtx = new OfflineAudioContext(2, bufferLen!, bufferSampleRate!) - } - else - { - this.audioCtx = new AudioContext() - } - - this.nodeGlobalVolume = this.audioCtx.createGain() - this.nodeGlobalVolume.gain.value = 0.1 - - this.nodeCompressor = this.audioCtx.createDynamicsCompressor() - this.nodeCompressor.threshold.value = -10 - this.nodeCompressor.knee.value = 12 - this.nodeCompressor.ratio.value = 12 - this.nodeCompressor.attack.value = 0 - this.nodeCompressor.release.value = 0.05 - - this.nodeGlobalVolume.connect(this.nodeCompressor) - this.nodeCompressor.connect(this.audioCtx.destination) - - this.nodeTrackVolumes = [] - } - - - async prepare(project: Project.Root) - { - // TODO: Diff instruments and tracks, and only modify what's needed - - for (const [trackId, instrument] of this.trackInstruments) - { - instrument.stopAll() - await instrument.destroy() - } - - this.trackInstruments.clear() - - for (const nodeTrackVolume of this.nodeTrackVolumes) - nodeTrackVolume.disconnect() - - this.nodeTrackVolumes = [] - - - const anySolo = project.tracks.some(tr => tr.solo) - - for (const track of project.tracks) - { - if (track.trackType != "notes" && track.trackType != "chords") - continue - - const trackIsMuted = (anySolo && !track.solo) || (track.mute && !track.solo) - - const trackVolumeNode = this.audioCtx.createGain() - trackVolumeNode.gain.value = trackIsMuted ? 0 : MathUtils.dbToLinearGain(track.volumeDb) - trackVolumeNode.connect(this.nodeGlobalVolume) - this.nodeTrackVolumes.push(trackVolumeNode) - - let instrument = null - switch (track.instrument.type) - { - case "basic": - instrument = new Playback.InstrumentBasic( - this, - trackVolumeNode) - break - case "sflib": - instrument = new Playback.InstrumentSflib( - this, - trackVolumeNode, - track.instrument.collectionId, - track.instrument.instrumentId) - break - } - - await instrument.prepare() - this.trackInstruments.set(track.id, instrument) - } - } - - - async destroy() - { - if (this.audioCtx instanceof AudioContext) - await this.audioCtx.close() - } - - - isFinished() - { - for (const [trackId, instrument] of this.trackInstruments) - { - if (!instrument.isFinished()) - return false - } - - return true - } - - - play() - { - - } - - - stopAll() - { - for (const [trackId, instrument] of this.trackInstruments) - instrument.stopAll() - } - - - process(deltaTimeMs: number) - { - for (const [trackId, instrument] of this.trackInstruments) - instrument.process(deltaTimeMs) - } - - - playNote(request: Playback.NoteRequest) - { - const instrument = this.trackInstruments.get(request.trackId) - if (!instrument) - return - - instrument.playNote(request) - } -} \ No newline at end of file diff --git a/src/popup/Button.tsx b/src/popup/Button.tsx deleted file mode 100644 index 6bc969c..0000000 --- a/src/popup/Button.tsx +++ /dev/null @@ -1,175 +0,0 @@ -import React from "react" -import Rect from "../util/rect" -import { Root } from "./Root" -import * as Popup from "./index" -import * as Command from "../command" - - -interface PopupButtonProps -{ - command?: Command.Command - icon?: any - label?: any - onClick?: (ev: React.MouseEvent) => void - children?: any -} - - -export function Button(props: PopupButtonProps) -{ - const buttonRef = React.useRef(null) - const popupRootCtx = Popup.usePopupRoot() - const [rect, setRect] = React.useState(new Rect(0, 0, 0, 0)) - - const index = popupRootCtx.itemIndex - popupRootCtx.itemIndex++ - - React.useLayoutEffect(() => - { - const buttonCurrent = buttonRef.current - if (!buttonCurrent) - return - - const rect = buttonCurrent.getBoundingClientRect() - setRect(new Rect(rect.x, rect.y - 1 - rect.height, rect.width, rect.height)) - - }, []) - - React.useEffect(() => - { - const buttonCurrent = buttonRef.current - if (!buttonCurrent) - return - - const onEnter = () => - { - popupRootCtx.openSubPopup(buttonCurrent) - } - - const onLeave = () => - { - } - - buttonCurrent.addEventListener("mouseenter", onEnter) - buttonCurrent.addEventListener("mouseleave", onLeave) - - return () => - { - buttonCurrent.removeEventListener("mouseenter", onEnter) - buttonCurrent.removeEventListener("mouseleave", onLeave) - } - - }, []) - - const onClick = (ev: React.MouseEvent) => - { - if (props.onClick) - { - props.onClick(ev) - Popup.global.elem = null - Popup.notifyObservers() - } - else if (props.command) - { - props.command.func({}) - Popup.global.elem = null - Popup.notifyObservers() - } - else - { - ev.preventDefault() - ev.stopPropagation() - } - } - - const label = props.label || props.command?.name - const icon = props.icon || props.command?.icon - - let shortcutStr = "" - if (props.command && props.command.shortcut) - { - if (!props.command.isShortcutAvailable || props.command.isShortcutAvailable()) - { - const shortcut = props.command.shortcut[0] - - if (shortcut.ctrl) - shortcutStr += "Ctrl+" - - if (shortcut.shift) - shortcutStr += "Shift+" - - shortcutStr += shortcut.key.toUpperCase() - } - } - - return <> - - -
- { icon } -
- -
- { label } -
- -
- { !props.children ? shortcutStr : "â–ļ" } -
- - { !buttonRef.current || !props.children || popupRootCtx.curSubPopup !== buttonRef.current ? null : - - { props.children } - - } - -} \ No newline at end of file diff --git a/src/popup/Divider.tsx b/src/popup/Divider.tsx deleted file mode 100644 index 90ade8b..0000000 --- a/src/popup/Divider.tsx +++ /dev/null @@ -1,27 +0,0 @@ -import React from "react" -import * as Popup from "./index" -import Rect from "../util/rect" - - -export function Divider() -{ - const popupRootCtx = Popup.usePopupRoot() - - const index = popupRootCtx.itemIndex - popupRootCtx.itemIndex++ - - const [_, bugfixRefresh] = React.useState(0) - React.useLayoutEffect(() => - { - bugfixRefresh(1) - }, []) - - return <> -
- -} \ No newline at end of file diff --git a/src/popup/Root.tsx b/src/popup/Root.tsx deleted file mode 100644 index 5794dab..0000000 --- a/src/popup/Root.tsx +++ /dev/null @@ -1,73 +0,0 @@ -import React from "react" -import * as Popup from "./index" -import Rect from "../util/rect" - - -interface PopupRootProps -{ - rect?: Rect - isSub?: boolean - popupElem?: any - popupProps?: any - children?: any -} - - -export function Root(props: PopupRootProps) -{ - const rect = props.rect ?? Popup.global.rect - const [subOpen, setSubOpen] = React.useState(null) - - const dismiss = (ev: any) => - { - Popup.global.elem = null - Popup.notifyObservers() - } - - React.useEffect(() => - { - const onClickVoid = (ev: MouseEvent) => - { - dismiss(ev) - } - - window.addEventListener("mousedown", onClickVoid) - return () => - { - window.removeEventListener("mousedown", onClickVoid) - } - }) - - return -
ev.stopPropagation() } - style={{ - zIndex: 100001, - position: "absolute", - left: props.isSub ? rect.x2 : rect.x1, - top: props.isSub ? rect.y1 : rect.y2, - - width: "fit-content", - contain: "layout", - - backgroundColor: "#2f3136", - border: "1px solid #fff", - borderRadius: "0.5em", - //overflow: "hidden", - boxShadow: "0 0.5em 0.5em 0.5em #0004", - - textAlign: "left", - - display: "grid", - gridTemplate: "auto / auto auto auto", - gridAutoFlow: "row", - pointerEvents: "auto", - }}> - { props.children } -
-
-} \ No newline at end of file diff --git a/src/popup/_global.ts b/src/popup/_global.ts deleted file mode 100644 index cd3bca5..0000000 --- a/src/popup/_global.ts +++ /dev/null @@ -1,69 +0,0 @@ -import React from "react" -import * as Dockable from "../dockable" -import * as GlobalObservable from "../util/globalObservable" -import Rect from "../util/rect" - - -export type PopupElement = (() => JSX.Element) | null - - -export interface Global -{ - elem: PopupElement - rect: Rect -} - - -export let global: Global = null! - - -export function initGlobal() -{ - global = { - elem: null, - rect: new Rect(0, 0, 0, 0), - } -} - - -export const globalObservable = GlobalObservable.makeGlobalObservable() -export function useGlobal() -{ - GlobalObservable.useGlobalObservable(globalObservable) -} - - -export function notifyObservers() -{ - globalObservable.notifyObservers() -} - - -export function showAtMouse(elem: PopupElement) -{ - global.elem = elem - - global.rect = new Rect( - Dockable.mousePos.x + 2, - Dockable.mousePos.y + 2, - 0, 0) - - notifyObservers() -} - - -export interface PopupRootContextProps -{ - curSubPopup: any - openSubPopup: (ref: any) => void - itemIndex: number -} - - -export const PopupRootContext = React.createContext(null!) - - -export function usePopupRoot(): PopupRootContextProps -{ - return React.useContext(PopupRootContext) -} \ No newline at end of file diff --git a/src/popup/index.ts b/src/popup/index.ts deleted file mode 100644 index caef254..0000000 --- a/src/popup/index.ts +++ /dev/null @@ -1,4 +0,0 @@ -export * from "./_global" -export * from "./Root" -export * from "./Button" -export * from "./Divider" \ No newline at end of file diff --git a/src/prefs/_global.ts b/src/prefs/_global.ts deleted file mode 100644 index 7c0407d..0000000 --- a/src/prefs/_global.ts +++ /dev/null @@ -1,10 +0,0 @@ -import * as Prefs from "./prefs" - - -export let global: Prefs.Prefs = null! - - -export function initGlobal() -{ - global = Prefs.getDefault() -} \ No newline at end of file diff --git a/src/prefs/index.ts b/src/prefs/index.ts deleted file mode 100644 index 16f38a8..0000000 --- a/src/prefs/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export * from "./_global" -export * from "./prefs" \ No newline at end of file diff --git a/src/prefs/prefs.ts b/src/prefs/prefs.ts deleted file mode 100644 index 930d8af..0000000 --- a/src/prefs/prefs.ts +++ /dev/null @@ -1,116 +0,0 @@ -export interface Prefs -{ - ui: - { - windowAnchorColor: string - windowOverlayColor: string - windowVoidColor: string - windowPanelColor: string - windowActiveBorderColor: string - } - - editor: - { - bkgColor: string - bkgAlternateMeasureColor: string - bkgInactiveOverlayColor: string - trackVBorderColor: string - trackHBorderColor: string - - selectionCursorColor: string - selectionBkgColor: string - playbackCursorColor: string - trackSeparatorColor: string - - measureColor: string - submeasureColor: string - halfSubmeasureColor: string - - measureLabelColor: string - octaveLabelColor: string - - meterChangeColor: string - keyChangeColor: string - noteVelocityMarkerColor: string - noteVelocityMarkerInactiveColor: string - - keyPan: string - keyPencil: string - keySelectMultiple: string - keySelectRange: string - keySelectRect: string - keySelectClone: string - keyDisplaceCursor2: string - keyDisplaceFast: string - keyDisplaceChromatically: string - keyDisplaceStretch: string - - mouseDoubleClickThresholdMs: number - - mouseDragXLockedDistance: number - mouseDragYLockedDistance: number - - mouseEdgeScrollThreshold: number - mouseEdgeScrollSpeed: number - } -} - - -export function getDefault(): Prefs -{ - return { - ui: - { - windowAnchorColor: "#0bf", - windowOverlayColor: "#fff4", - windowVoidColor: "#202225", - windowPanelColor: "#2f3136", - windowActiveBorderColor: "#fff", - }, - - editor: - { - bkgColor: "#202225", - bkgAlternateMeasureColor: "#1a1c1f", - bkgInactiveOverlayColor: "#0008", - trackVBorderColor: "#888", - trackHBorderColor: "#888", - - selectionCursorColor: "#0af", - selectionBkgColor: "#0248", - playbackCursorColor: "#f00", - trackSeparatorColor: "#aaa", - - measureColor: "#040404", - submeasureColor: "#080808", - halfSubmeasureColor: "#181818", - - measureLabelColor: "#aaa", - octaveLabelColor: "#aaa", - - meterChangeColor: "#0cf", - keyChangeColor: "#f0c", - noteVelocityMarkerColor: "#0c4", - noteVelocityMarkerInactiveColor: "#063", - - keyPan: " ", - keyPencil: "a", - keySelectMultiple: "control", - keySelectRange: "shift", - keySelectRect: "shift", - keySelectClone: "alt", - keyDisplaceCursor2: "shift", - keyDisplaceFast: "control", - keyDisplaceChromatically: "shift", - keyDisplaceStretch: "shift", - - mouseDoubleClickThresholdMs: 300, - - mouseDragXLockedDistance: 10, - mouseDragYLockedDistance: 10, - - mouseEdgeScrollThreshold: 10, - mouseEdgeScrollSpeed: 1, - }, - } -} \ No newline at end of file diff --git a/src/project/_global.ts b/src/project/_global.ts deleted file mode 100644 index 5c1f0a1..0000000 --- a/src/project/_global.ts +++ /dev/null @@ -1,210 +0,0 @@ -import * as Project from "./index" -import * as Timeline from "../timeline" -import * as GlobalObservable from "../util/globalObservable" - - -export interface GlobalData -{ - project: Project.Root - savedProject: Project.Root - - curFileHandleForSave: FileSystemFileHandle | null - - undoStack: UndoPoint[] - undoIndex: number - - copiedData: CopiedData | null -} - - -export interface UndoPoint -{ - tag: string - project: Project.Root -} - - -export interface CopiedData -{ - project: Project.Root - elemsByTrack: Project.Element[][] -} - - -export let global: GlobalData = null! - - -export function initGlobal() -{ - const initialProject = loadFromLocalStorage() ?? Project.getDefault() - - global = - { - project: initialProject, - savedProject: initialProject, - - curFileHandleForSave: null, - - undoStack: [{ - tag: "new", - project: initialProject, - }], - undoIndex: 0, - - copiedData: null, - } -} - - -export const globalObservable = GlobalObservable.makeGlobalObservable() -export function useGlobal() -{ - GlobalObservable.useGlobalObservable(globalObservable) -} - - -export function notifyObservers() -{ - globalObservable.notifyObservers() -} - - -export function setNew() -{ - global.project = Project.makeNew() - global.savedProject = global.project - global.curFileHandleForSave = null - - clearUndoStack() - notifyObservers() - Timeline.sendEventReset() -} - - -export function open(openedProject: Project.Root) -{ - global.project = openedProject - global.savedProject = openedProject - - clearUndoStack() - notifyObservers() - Timeline.sendEventReset() -} - - -export function setFileHandleForSave(fileHandle: FileSystemFileHandle | null) -{ - global.curFileHandleForSave = fileHandle - notifyObservers() -} - - -export function markAsSaved() -{ - global.savedProject = global.project - notifyObservers() -} - - -export function isUnsaved(): boolean -{ - return global.project !== global.savedProject -} - - -export function clearUndoStack() -{ - global.undoStack = [{ - tag: "new", - project: global.project, - }] - - global.undoIndex = 0 -} - - -export function addUndoPoint(tag: string) -{ - if (global.project === global.undoStack[global.undoIndex].project) - return - - global.undoStack = - global.undoStack.slice(0, global.undoIndex + 1) - - if (tag === global.undoStack[global.undoStack.length - 1].tag) - global.undoStack = - global.undoStack.slice(0, global.undoStack.length - 1) - - global.undoStack.push({ - tag, - project: global.project, - }) - - global.undoIndex = global.undoStack.length - 1 - saveToLocalStorageWithCooldown(global.project) -} - - -export function splitUndoPoint() -{ - global.undoStack[global.undoStack.length - 1].tag = "" -} - - -export function undo() -{ - if (global.undoIndex <= 0) - return - - global.undoIndex-- - global.project = - global.undoStack[global.undoIndex].project - - saveToLocalStorageWithCooldown(global.project) - notifyObservers() - Timeline.sendEventRefresh() -} - - -export function redo() -{ - if (global.undoIndex >= global.undoStack.length - 1) - return - - global.undoIndex++ - global.project = - global.undoStack[global.undoIndex].project - - saveToLocalStorageWithCooldown(global.project) - notifyObservers() - Timeline.sendEventRefresh() -} - - -let saveToLocalStorageWithCooldownLastDate = new Date() -export function saveToLocalStorageWithCooldown(project: Project.Root) -{ - const newDate = new Date() - if (newDate.getTime() - saveToLocalStorageWithCooldownLastDate.getTime() < 10000) - return - - saveToLocalStorageWithCooldownLastDate = newDate - saveToLocalStorage(project) -} - - -export function saveToLocalStorage(project: Project.Root) -{ - const jsonStr = Project.jsonExport(project) - window.localStorage.setItem("autosave", jsonStr) -} - - -export function loadFromLocalStorage(): Project.Root | null -{ - const jsonStr = window.localStorage.getItem("autosave") - if (jsonStr) - return Project.jsonImport(JSON.parse(jsonStr)) - else - return null -} \ No newline at end of file diff --git a/src/project/elem.ts b/src/project/elem.ts deleted file mode 100644 index 0d46ffb..0000000 --- a/src/project/elem.ts +++ /dev/null @@ -1,376 +0,0 @@ -import Range from "../util/range" -import * as Theory from "../theory" -import * as Playback from "../playback" -import Rational from "../util/rational" -import * as Misc from "../util/misc" - - -export type ID = number - -export const MinVolumeDb = -30 -export const MaxVolumeDb = 0 -export const DefaultVolumeDb = 0 -export const DefaultChordVolumeDb = -4 - - -export interface ElementBase -{ - id: ID - parentId: ID - range: Range -} - - -export interface InstrumentBasic -{ - type: "basic" -} - - -export interface InstrumentSflib -{ - type: "sflib" - collectionId: string - instrumentId: string -} - - -export type Instrument = - InstrumentBasic | - InstrumentSflib - - -export interface TrackBase extends ElementBase -{ - type: "track" - name: string - mute: boolean - solo: boolean -} - - -export interface TrackNotes extends TrackBase -{ - trackType: "notes" - instrument: Instrument - volumeDb: number -} - - -export interface TrackChords extends TrackBase -{ - trackType: "chords" - instrument: Instrument - volumeDb: number -} - - -export interface TrackKeyChanges extends TrackBase -{ - trackType: "keyChanges" -} - - -export interface TrackMeterChanges extends TrackBase -{ - trackType: "meterChanges" -} - - -export type Track = - TrackNotes | - TrackChords | - TrackKeyChanges | - TrackMeterChanges - - -export interface KeyChange extends ElementBase -{ - type: "keyChange" - key: Theory.Key -} - - -export interface MeterChange extends ElementBase -{ - type: "meterChange" - meter: Theory.Meter -} - - -export interface NoteBlock extends ElementBase -{ - type: "noteBlock" -} - - -export interface Note extends ElementBase -{ - type: "note" - midiPitch: number - volumeDb: number - velocity: number -} - - -export interface Chord extends ElementBase -{ - type: "chord" - chord: Theory.Chord -} - - -export type Element = - Track | - KeyChange | - MeterChange | - NoteBlock | - Note | - Chord - - - -export function elemModify(original: T, changes: Partial): T -{ - return { ...original, ...changes } -} - - -export function makeTrackNotes(): TrackNotes -{ - return { - type: "track", - trackType: "notes", - id: -1, - parentId: 0, - range: Range.dummy(), - name: "", - volumeDb: DefaultVolumeDb, - mute: false, - solo: false, - instrument: makeInstrument(), - } -} - - -export function makeTrackChords(): TrackChords -{ - return { - type: "track", - trackType: "chords", - id: -1, - parentId: 0, - range: Range.dummy(), - name: "Chords", - volumeDb: DefaultChordVolumeDb, - mute: false, - solo: false, - instrument: makeInstrument(), - } -} - - -export function makeInstrument(): Instrument -{ - return makeInstrumentOfKind("sflib") -} - - -export function makeInstrumentOfKind(kind: string): Instrument -{ - switch (kind) - { - case "basic": - default: - { - return { - type: "basic", - } - } - case "sflib": - { - return { - type: "sflib", - collectionId: "gm", - instrumentId: "piano_1", - } - } - } -} - - -export function instrumentName(instrument: Instrument): string -{ - switch (instrument.type) - { - case "basic": return "Basic" - case "sflib": - { - const sflibMeta = Playback.getSflibMeta() - if (!sflibMeta) - return instrument.collectionId + "/" + instrument.instrumentId - - const coll = sflibMeta.collectionsById.get(instrument.collectionId)! - const instr = coll.instrumentsById.get(instrument.instrumentId)! - const emoji = Misc.getMidiPresetEmoji(instr.midiBank, instr.midiPreset) - - return emoji + " " + coll.id + "/" + instr.name - } - - default: return "???" - } -} - - -export function instrumentEmoji(instrument: Instrument): string -{ - switch (instrument.type) - { - case "basic": return Misc.getMidiPresetEmoji(0, 0) - case "sflib": - { - const sflibMeta = Playback.getSflibMeta() - if (!sflibMeta) - return Misc.getMidiPresetEmoji(0, 0) - - const coll = sflibMeta.collectionsById.get(instrument.collectionId)! - const instr = coll.instrumentsById.get(instrument.instrumentId)! - return Misc.getMidiPresetEmoji(instr.midiBank, instr.midiPreset) - } - - default: return Misc.getMidiPresetEmoji(0, 0) - } -} - - -export function trackDisplayName(track: Track): string -{ - if (track.trackType == "notes" || track.trackType == "chords") - { - if (track.name) - return instrumentEmoji(track.instrument) + " " + track.name - - return instrumentName(track.instrument) - } - - if (track.name) - return track.name - - return "New Track" -} - - -export function trackHasInstrument(track: Track): boolean -{ - return track.trackType == "notes" || track.trackType == "chords" -} - - -export function trackGetInstrument(track: Track): Instrument | null -{ - if (track.trackType == "notes" || track.trackType == "chords") - return track.instrument - else - return null -} - - -export function makeTrackKeyChanges(): TrackKeyChanges -{ - return { - type: "track", - trackType: "keyChanges", - id: -1, - parentId: 0, - range: Range.dummy(), - name: "Key Changes", - mute: false, - solo: false, - } -} - - -export function makeTrackMeterChanges(): TrackMeterChanges -{ - return { - type: "track", - trackType: "meterChanges", - id: -1, - parentId: 0, - range: Range.dummy(), - name: "Meter Changes", - mute: false, - solo: false, - } -} - - -export function makeMeterChange(parentId: ID, time: Rational, meter: Theory.Meter): MeterChange -{ - return { - type: "meterChange", - id: -1, - parentId, - range: Range.fromPoint(time), - meter, - } -} - - -export function makeKeyChange(parentId: ID, time: Rational, key: Theory.Key): KeyChange -{ - return { - type: "keyChange", - id: -1, - parentId, - range: Range.fromPoint(time), - key, - } -} - - -export function makeNoteBlock(parentId: ID, range: Range): NoteBlock -{ - return { - type: "noteBlock", - id: -1, - parentId, - range, - } -} - - -export function makeNote( - parentId: ID, - range: Range, - midiPitch: number, - volumeDb: number, - velocity: number) - : Note -{ - return { - type: "note", - id: -1, - parentId, - range, - midiPitch, - volumeDb, - velocity, - } -} - - -export function makeChord( - parentId: ID, - range: Range, - chord: Theory.Chord) - : Chord -{ - return { - type: "chord", - id: -1, - parentId, - range, - chord, - } -} \ No newline at end of file diff --git a/src/project/index.ts b/src/project/index.ts deleted file mode 100644 index 11d0d8f..0000000 --- a/src/project/index.ts +++ /dev/null @@ -1,8 +0,0 @@ -export * from "./_global" -export * from "./root" -export * from "./measures" -export * from "./elem" -export * from "./midiImport" -export * from "./midiExport" -export * from "./jsonExport" -export * from "./jsonImport" \ No newline at end of file diff --git a/src/project/jsonExport.ts b/src/project/jsonExport.ts deleted file mode 100644 index 612d2b4..0000000 --- a/src/project/jsonExport.ts +++ /dev/null @@ -1,140 +0,0 @@ -import * as Project from "./index" -import * as Theory from "../theory" -import Rational from "../util/rational" -import Range from "../util/range" -import * as MathUtils from "../util/mathUtils" - - -export function jsonExport(project: Project.Root): string -{ - let json = `` - json += `{\n` - json += `"version": 1,\n` - json += `"baseBpm": ${ project.baseBpm },\n` - json += `"tracks": [\n` - - let firstTrack = true - for (const track of project.tracks) - { - if (!firstTrack) - json += `,\n` - - firstTrack = false - - json += `\t{\n` - json += `\t"id": ${ JSON.stringify(track.id) },\n` - json += `\t"name": ${ JSON.stringify(track.name) },\n` - json += `\t"type": ${ JSON.stringify(track.type) },\n` - json += `\t"trackType": ${ JSON.stringify(track.trackType) },\n` - json += `\t"mute": ${ JSON.stringify(track.mute) },\n` - json += `\t"solo": ${ JSON.stringify(track.solo) },\n` - - if (track.trackType == "notes" || track.trackType == "chords") - { - json += `\t"volumeDb": ${ JSON.stringify(track.volumeDb) },\n` - json += `\t"instrument": ${ JSON.stringify(track.instrument) },\n` - } - - json += `\t"elems": [\n` - - let firstElem = true - for (const elem of project.lists.get(track.id)?.iterAll() ?? []) - { - if (!firstElem) - json += `,\n` - - json += exportElem(2, project, elem, new Rational(0)) - firstElem = false - } - - json += `\n` - json += `\t]\n` - json += `\t}` - } - - json += `\n]\n` - json += `}` - return json -} - - -function exportElem( - indent: number, - project: Project.Root, - elem: Project.Element, - timeOffset: Rational) - : string -{ - const range = elem.range - .displace(timeOffset) - .quantize(Project.MAX_RATIONAL_DENOMINATOR) - - let json = `\t`.repeat(indent) - json += `[` - json += `${ JSON.stringify(elem.type) }` - json += `, ${ JSON.stringify(elem.id) }` - json += `, ${ JSON.stringify(range.toJson(Project.MAX_RATIONAL_DENOMINATOR)) }` - - switch (elem.type) - { - case "note": - { - json += `, ${ JSON.stringify([ - elem.midiPitch, - MathUtils.quantize(elem.volumeDb, 1000), - MathUtils.quantize(elem.velocity, 1000), - ]) }` - break - } - - case "chord": - { - json += `, ${ JSON.stringify([ - elem.chord.rootChroma, - Theory.Chord.kinds[elem.chord.kind].id, - elem.chord.inversion, - ]) }` - break - } - - case "keyChange": - { - json += `, ${ JSON.stringify(elem.key.str) }` - break - } - - case "meterChange": - { - json += `, ${ JSON.stringify(elem.meter.str) }` - break - } - - default: - { - json += `, []` - break - } - } - - const innerList = project.lists.get(elem.id) - if (innerList) - { - json += `, [\n` - let firstElem = true - for (const innerElem of innerList.iterAll()) - { - if (!firstElem) - json += `,\n` - - json += exportElem(indent + 1, project, innerElem, elem.range.start) - firstElem = false - } - - json += `\n` - json += `\t`.repeat(indent) - json += `]` - } - - json += `]` - return json -} \ No newline at end of file diff --git a/src/project/jsonImport.ts b/src/project/jsonImport.ts deleted file mode 100644 index 878c062..0000000 --- a/src/project/jsonImport.ts +++ /dev/null @@ -1,142 +0,0 @@ -import * as Project from "./index" -import * as Theory from "../theory" -import Rational from "../util/rational" -import Range from "../util/range" -import * as MathUtils from "../util/mathUtils" - - -export function jsonImport(json: any): Project.Root -{ - console.log("jsonImport", json) - - let project = Project.makeEmpty() - project.baseBpm = json.baseBpm - - for (const jsonTrack of json.tracks) - { - let track: Project.Track - switch (jsonTrack.trackType as Project.Track["trackType"]) - { - case "notes": - track = Project.makeTrackNotes() - track.instrument = jsonTrack.instrument - track.volumeDb = jsonTrack.volumeDb - break - case "chords": - track = Project.makeTrackChords() - track.instrument = jsonTrack.instrument - track.volumeDb = jsonTrack.volumeDb - break - case "keyChanges": - track = Project.makeTrackKeyChanges() - break - case "meterChanges": - track = Project.makeTrackMeterChanges() - break - } - - track.name = jsonTrack.name - track.mute = jsonTrack.mute - track.solo = jsonTrack.solo - - const trackId = project.nextId - project = Project.upsertTrack(project, track) - - if (track.trackType == "keyChanges") - project.keyChangeTrackId = trackId - else if (track.trackType == "meterChanges") - project.meterChangeTrackId = trackId - else if (track.trackType == "chords") - project.chordTrackId = trackId - - for (const jsonElem of jsonTrack.elems) - { - const projectRef = { ref: project } - importElem(projectRef, trackId, jsonElem, new Rational(0)) - project = projectRef.ref - } - } - - console.log("project", project) - return Project.withRefreshedRange(project) -} - - -function importElem( - projectRef: { ref: Project.Root }, - parentId: Project.ID, - jsonElem: any, - timeOffset: Rational) -{ - const range = Range.fromJson(jsonElem[2]) - .subtract(timeOffset) - .quantize(Project.MAX_RATIONAL_DENOMINATOR) - - let elem: Project.Element - switch (jsonElem[0] as Project.Element["type"]) - { - case "note": - { - elem = Project.makeNote( - parentId, - range, - jsonElem[3][0], - jsonElem[3][1], - jsonElem[3][2]) - break - } - - case "chord": - { - elem = Project.makeChord( - parentId, - range, - new Theory.Chord( - jsonElem[3][0], - Theory.Chord.kindFromId(jsonElem[3][1]), - jsonElem[3][2], - [])) - break - } - - case "keyChange": - { - elem = Project.makeKeyChange( - parentId, - range.start, - Theory.Key.parse(jsonElem[3])) - break - } - - case "meterChange": - { - elem = Project.makeMeterChange( - parentId, - range.start, - Theory.Meter.parse(jsonElem[3])) - break - } - - case "noteBlock": - { - elem = Project.makeNoteBlock( - parentId, - range) - break - } - - case "track": - throw "invalid json" - } - - const id = projectRef.ref.nextId - projectRef.ref = Project.upsertElement(projectRef.ref, elem) - - if (jsonElem[4]) - { - for (const jsonInner of jsonElem[4]) - { - importElem(projectRef, id, jsonInner, range.start) - } - } -} \ No newline at end of file diff --git a/src/project/measures.ts b/src/project/measures.ts deleted file mode 100644 index 2767091..0000000 --- a/src/project/measures.ts +++ /dev/null @@ -1,155 +0,0 @@ -import * as Project from "./index" -import * as Theory from "../theory" -import Immutable from "immutable" -import BinarySearch from "../util/binarySearch" -import Rational from "../util/rational" -import ListOfRanges from "../util/listOfRanges" -import Range from "../util/range" - - -export interface Measure -{ - num: number - numLocal: number - time1: Rational - time2: Rational - meterCh: Project.MeterChange -} - - -let measureCacheKey: ListOfRanges = null! -let measureCache: Measure[] = [] -let firstMeterCh: Project.MeterChange | null = null -let lastMeterCh: Project.MeterChange | null = null - - -export function ensureMeasureCacheRefreshed(project: Project.Root) -{ - const meterChangeTrackId = Project.meterChangeTrackId(project) - const meterChangeList = Project.global.project.lists.get(meterChangeTrackId)! - - if (meterChangeList === measureCacheKey) - return - - measureCacheKey = meterChangeList - measureCache = [] - - if (!meterChangeList) - return - - let num = 0 - - for (const [elem1, elem2] of meterChangeList.iterAllPairwise()) - { - if (!elem1) - firstMeterCh = elem2 as Project.MeterChange | null - - if (!elem2) - lastMeterCh = elem1 as Project.MeterChange | null - - if (!elem1 || !elem2) - continue - - const meterCh1 = elem1 as Project.MeterChange - const meterCh2 = elem2 as (Project.MeterChange | null) - - let numLocal = 0 - - for (const [measureN, measureD, time1, time2] of meterCh1.meter.iterMeasuresPairwise(meterCh1.range.start)) - { - if (meterCh2 && time1.compare(meterCh2.range.start) >= 0) - break - - measureCache.push({ - num, numLocal, - time1, - time2: time2.min(meterCh2 ? meterCh2.range.start : time2), - meterCh: meterCh1, - }) - - num++ - numLocal++ - } - } -} - - -export function *iterMeasuresAtRange( - project: Project.Root, - range: Range) - : Generator -{ - ensureMeasureCacheRefreshed(project) - - const measureStart = BinarySearch.findPreviousOrEqual(measureCache, (m) => range.start.compare(m.time1)) - - if (measureStart === null && firstMeterCh) - { - let time = firstMeterCh.range.start - let num = 0 - - while (time.compare(range.start) > 0) - { - time = time.subtract(firstMeterCh.meter.fullCycleDuration) - num-- - } - - while (time.compare(firstMeterCh.range.start) < 0) - { - const time2 = time.add(firstMeterCh.meter.fullCycleDuration) - - if (time2.compare(range.start) >= 0) - { - yield { - meterCh: firstMeterCh, - num, numLocal: num, - time1: time, - time2, - } - } - - time = time2 - num++ - } - } - - let lastMeasureNum = -1 - let measureIndex = measureStart || 0 - while (true) - { - if (measureIndex >= measureCache.length) - break - - const measure = measureCache[measureIndex] - if (measure.time1.compare(range.end) >= 0) - break - - yield measure - lastMeasureNum = measure.num - measureIndex++ - } - - if (lastMeterCh) - { - let num = lastMeasureNum + 1 - let time = lastMeterCh.range.start - - while (time.compare(range.end) < 0) - { - const time2 = time.add(lastMeterCh.meter.fullCycleDuration) - - if (time2.compare(range.start) >= 0) - { - yield { - meterCh: lastMeterCh, - num, numLocal: num, - time1: time, - time2, - } - } - - time = time2 - num++ - } - } -} \ No newline at end of file diff --git a/src/project/midiExport.ts b/src/project/midiExport.ts deleted file mode 100644 index abeea30..0000000 --- a/src/project/midiExport.ts +++ /dev/null @@ -1,142 +0,0 @@ -import * as Project from "./index" -import * as Playback from "../playback" -import * as Midi from "../util/midi" -import Rational from "../util/rational" -import * as MathUtils from "../util/mathUtils" - - -export function midiExport(project: Project.Root): Uint8Array -{ - let midi = { - ticksPerQuarterNote: 128, - tracks: [], - } as unknown as Midi.Root - - - - const midiMetaTrack: Midi.Track = { - length: 0, - events: [], - } - - midiMetaTrack.events.push({ - kind: "setTempo", - tick: new Rational(0), - bpm: project.baseBpm, - }) - - midiMetaTrack.events.push({ - kind: "endOfTrack", - tick: new Rational(0), - }) - - midi.tracks.push(midiMetaTrack) - - - - let channelIndex = 0 - - for (const track of project.tracks) - { - if (track.trackType !== "notes") - continue - - const midiTrack: Midi.Track = { - length: 0, - events: [], - } - - let channel = channelIndex - channelIndex = (channelIndex + 1) % 16 - if (channelIndex == 9) - channelIndex += 1 - - const trackInstr = Project.trackGetInstrument(track) - const sflibMeta = Playback.getSflibMeta() - if (trackInstr && - trackInstr.type === "sflib" && - sflibMeta) - { - const coll = sflibMeta.collections.find(c => c.id == trackInstr.collectionId)! - const instr = coll.instruments.find(instr => instr.id === trackInstr.instrumentId) - - if (instr) - { - if (instr.midiBank === 128) - channel = 9 - - midiTrack.events.push({ - kind: "programChange", - channel, - tick: new Rational(0), - program: instr.midiPreset, - }) - } - } - - midiTrack.events.push({ - kind: "controller", - channel, - tick: new Rational(0), - controllerName: "channelVolumeCoarse", - controllerNumber: 7, - controllerValue: track.mute ? - 0 : - MathUtils.dbToMidiVolume(track.volumeDb) * 0x7f, - }) - - - const listNoteBlocks = project.lists.get(track.id) - if (!listNoteBlocks) - continue - - for (const elemNoteBlock of listNoteBlocks.iterAll()) - { - if (elemNoteBlock.type !== "noteBlock") - continue - - const listNotes = project.lists.get(elemNoteBlock.id) - if (!listNotes) - continue - - for (const elemNote of listNotes.iterAll()) - { - if (elemNote.type !== "note") - continue - - const absRange = elemNote.range - .displace(elemNoteBlock.range.start) - .subtract(project.range.start) - - midiTrack.events.push({ - kind: "noteOn", - tick: absRange.start, - channel, - key: elemNote.midiPitch, - velocity: MathUtils.dbToMidiVolume(elemNote.velocity) * 0x7f, - }) - - midiTrack.events.push({ - kind: "noteOff", - tick: absRange.end, - channel, - key: elemNote.midiPitch, - velocity: 0x7f, - }) - } - } - - - midiTrack.events.push({ - kind: "endOfTrack", - tick: midiTrack.events.length == 0 ? - new Rational(0) : - midiTrack.events[midiTrack.events.length - 1].tick, - }) - - - midi.tracks.push(midiTrack) - } - - return Midi.Encoder.encode(midi) -} \ No newline at end of file diff --git a/src/project/midiImport.ts b/src/project/midiImport.ts deleted file mode 100644 index 3a54709..0000000 --- a/src/project/midiImport.ts +++ /dev/null @@ -1,414 +0,0 @@ -import * as Project from "./index" -import * as Theory from "../theory" -import * as Playback from "../playback" -import * as Midi from "../util/midi" -import Rational from "../util/rational" -import Range from "../util/range" -import * as MathUtils from "../util/mathUtils" - - -export function midiImport(bytes: number[] | Buffer | Uint8Array): Project.Root -{ - const midi = Midi.Decoder.fromBytes(bytes) - const tracks = splitTracksAtProgramChanges(midi.tracks) - console.log("midiImport", midi) - - - function findFirstEvent( - kind: T) - : Extract | null - { - for (const track of tracks) - for (const ev of track.events) - if (ev.kind === kind) - return ev as Extract - - return null - } - - - function findFirstEventOnChannel( - channel: number, - kind: string) - : Extract | null - { - for (const track of tracks) - for (const ev of track.events) - if (ev.kind === kind && (ev as any).channel === channel) - return ev as Extract - - return null - } - - - function findFirstEventOnTrack( - midiTrack: Midi.Track, - kind: string) - : Extract | null - { - for (const ev of midiTrack.events) - if (ev.kind === kind) - return ev as Extract - - return null - } - - - function findFirstControllerOnChannel(channel: number, kind: string): Midi.EventController | null - { - for (const track of tracks) - for (const ev of track.events) - if (ev.kind === "controller" && ev.controllerName === kind && ev.channel === channel) - return ev - - return null - } - - - function findFirstControllerOnTrack(midiTrack: Midi.Track, kind: string): Midi.EventController | null - { - for (const ev of midiTrack.events) - if (ev.kind === "controller" && ev.controllerName === kind) - return ev - - return null - } - - - let project = Project.makeEmpty() - - const evSetTempo = findFirstEvent("setTempo") - project.baseBpm = Math.round(evSetTempo ? evSetTempo.bpm : 120) - - const trackKeyChanges = project.nextId - project.keyChangeTrackId = trackKeyChanges - project = Project.upsertTrack(project, Project.makeTrackKeyChanges()) - - const trackMeterChanges = project.nextId - project.meterChangeTrackId = trackMeterChanges - project = Project.upsertTrack(project, Project.makeTrackMeterChanges()) - - const trackChords = project.nextId - project.chordTrackId = trackChords - project = Project.upsertTrack(project, Project.makeTrackChords()) - - for (const track of tracks) - { - for (const ev of track.events) - { - if (ev.kind != "setKeySignature") - continue - - const tonicPitches = [ 0, 7, 2, 9, 4, 11, 5, 0, 7, 2, 9, 4, 11, 5, 0, 7, 2, 9, 4, 11] - const tonicLetters = [ 0, 4, 1, 5, 2, 6, 3, 0, 4, 1, 5, 2, 6, 3, 0, 4, 1, 5, 2, 6] - const tonicAccidentals = [-1, -1, -1, -1, -1, -1, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1] - const index = ev.accidentals + 7 + (ev.scale == 0 ? 0 : 2) - - if (index < 0 || index >= tonicPitches.length) - continue - - project = Project.upsertElement(project, Project.makeKeyChange( - trackKeyChanges, - ev.tick, - new Theory.Key( - new Theory.PitchName(tonicLetters[index], tonicAccidentals[index]), - Theory.Scale.parse(ev.scale == 0 ? "Major" : "Minor")))) - } - } - - for (const track of tracks) - { - for (const ev of track.events) - { - if (ev.kind != "setTimeSignature") - continue - - project = Project.upsertElement(project, Project.makeMeterChange( - trackMeterChanges, - ev.tick, - new Theory.Meter(ev.numerator, ev.denominator))) - } - } - - const notes: MidiNote[] = [] - for (let tr = 0; tr < tracks.length; tr++) - { - const midiTrack = tracks[tr] - for (const noteOn of midiTrack.events) - { - if (noteOn.kind != "noteOn") - continue - - let noteOffTime: Rational | null = null - for (const ev of midiTrack.events) - { - if (ev.kind != "noteOn" && ev.kind != "noteOff") - continue - - if (ev.tick.compare(noteOn.tick) <= 0 || - ev.channel != noteOn.channel || - ev.key != noteOn.key) - continue - - if (!noteOffTime || ev.tick.compare(noteOffTime) < 0) - { - noteOffTime = ev.tick - break - } - } - - if (!noteOffTime) - continue - - notes.push({ - range: new Range(noteOn.tick, noteOffTime), - midiPitch: noteOn.key, - velocity: MathUtils.midiVolumeToDb(noteOn.velocity / 127), - channel: noteOn.channel, - midiTrackIndex: tr, - }) - } - } - - const notesByTracks = midi.format == 0 ? - splitNotesBy(notes, note => note.channel) : - splitNotesBy(notes, note => note.midiTrackIndex) - - - for (const notesForTrack of notesByTracks) - { - const midiTrack = tracks[notesForTrack[0].midiTrackIndex] - - - function findFirstEventOnChannelOrTrack( - kind: T) - : Extract | null - { - if (midi.format == 0) - return findFirstEventOnChannel(notesForTrack[0].channel, kind) - else - return findFirstEventOnTrack(midiTrack, kind) - } - - - function findFirstControllerOnChannelOrTrack( - kind: Midi.EventController["controllerName"]) - : Midi.EventController | null - { - if (midi.format == 0) - return findFirstControllerOnChannel(notesForTrack[0].channel, kind) - else - return findFirstControllerOnTrack(midiTrack, kind) - } - - - const trackId = project.nextId - const track = Project.makeTrackNotes() - - const trackName = findFirstEventOnChannelOrTrack("trackName") - if (trackName) - track.name = trackName.name - - const trackVolume = findFirstControllerOnChannelOrTrack("channelVolumeCoarse") - if (trackVolume) - track.volumeDb = MathUtils.midiVolumeToDb(trackVolume.controllerValue / 127) - - const isDrumkit = notesForTrack.some((n: any) => n.channel == 9) - - const programChange = findFirstEventOnChannelOrTrack("programChange") - if (programChange) - { - const gmSflib = Playback.getSflibMeta()!.collections.find(c => c.id == "gm")! - - let instrument = null - if (!isDrumkit) - instrument = gmSflib.instruments.find(i => - i.midiBank != 128 && - i.midiPreset == programChange.program) - else - { - instrument = gmSflib.instruments.find(i => - i.midiBank == 128 && - i.midiPreset == programChange.program) - - if (!instrument) - instrument = gmSflib.instruments.find(i => i.midiBank == 128) - } - - track.instrument = { - type: "sflib", - collectionId: "gm", - instrumentId: instrument?.id ?? "piano_1", - } - } - - project = Project.upsertTrack(project, track) - - const splitNotes = splitNotesIntoBlocks(notesForTrack) - for (const split of splitNotes) - { - const blockId = project.nextId - project = Project.upsertElement(project, - Project.makeNoteBlock(trackId, split.range)) - - for (const note of split.notes) - project = Project.upsertElement(project, - Project.makeNote( - blockId, - note.range.subtract(split.range.start), - note.midiPitch, - note.velocity, - note.velocity)) - } - } - - const keyChangeList = project.lists.get(trackKeyChanges) - if (!keyChangeList || !keyChangeList.findActiveAt(new Rational(0))) - project = Project.upsertElement(project, - Project.makeKeyChange(trackKeyChanges, new Rational(0), Theory.Key.parse("C Major"))) - - const meterChangeList = project.lists.get(trackMeterChanges) - if (!meterChangeList || !meterChangeList.findActiveAt(new Rational(0))) - project = Project.upsertElement(project, - Project.makeMeterChange(trackMeterChanges, new Rational(0), new Theory.Meter(4, 4))) - - return Project.withRefreshedRange(project) -} - - -function splitTracksAtProgramChanges(tracks: Midi.Track[]): Midi.Track[] -{ - // FIXME: Missing program changes in format 0 files? - const newTracks: Midi.Track[] = [] - - for (const track of tracks) - { - const splitTracksByPreset = new Map() - const splitTracksByOrder: Midi.Track[] = [] - - const getOrAddSplitTrack = (midiPreset: number) => - { - let splitTrack = splitTracksByPreset.get(midiPreset) - if (!splitTrack) - { - splitTrack = { - length: 0, - events: [], - } - splitTracksByPreset.set(midiPreset, splitTrack) - splitTracksByOrder.push(splitTrack) - } - - return splitTrack - } - - let curMidiPreset: number = 0 - - for (const event of track.events) - { - if (event.kind == "programChange") - { - curMidiPreset = event.program as number - getOrAddSplitTrack(curMidiPreset).events.push(event) - } - else if (event.kind == "noteOn") - { - getOrAddSplitTrack(curMidiPreset).events.push(event) - } - } - - if (splitTracksByOrder.length == 0) - getOrAddSplitTrack(curMidiPreset) - - for (const event of track.events) - { - if (event.kind != "programChange" && - event.kind != "noteOn") - { - for (const splitTrack of splitTracksByOrder) - splitTrack.events.push(event) - } - } - - for (const splitTrack of splitTracksByOrder) - { - splitTrack.events.sort((a, b) => a.tick.compare(b.tick)) - newTracks.push(splitTrack) - } - } - - return newTracks -} - - -interface MidiNote -{ - range: Range - midiPitch: number - velocity: number - channel: number - midiTrackIndex: number -} - - -function splitNotesBy(notes: MidiNote[], fn: (note: MidiNote) => number): MidiNote[][] -{ - const map = new Map() - - for (const note of notes) - { - const key = fn(note) - - let bucket = map.get(key) - if (bucket) - { - bucket.push(note) - } - else - { - bucket = [note] - map.set(key, bucket) - } - } - - return [...map.values()] -} - - -interface SplitNotes -{ - notes: MidiNote[] - range: Range -} - - -function splitNotesIntoBlocks(notes: MidiNote[]): SplitNotes[] -{ - const result: SplitNotes[] = [] - const margin = new Rational(1) - - while (notes.length > 0) - { - const firstNote = notes.pop()! - let blockRange = firstNote.range - - const blockNotes: Set = new Set() - blockNotes.add(firstNote) - - while (true) - { - const overlappingNoteIndex = notes.findIndex(n => n.range.grow(margin).overlapsRange(blockRange)) - if (overlappingNoteIndex < 0) - break - - const overlappingNote = notes[overlappingNoteIndex] - blockNotes.add(overlappingNote) - blockRange = blockRange.merge(overlappingNote.range) - notes.splice(overlappingNoteIndex, 1) - } - - result.push({ notes: [...blockNotes], range: blockRange }) - } - - return result -} \ No newline at end of file diff --git a/src/project/root.ts b/src/project/root.ts deleted file mode 100644 index f2fab0a..0000000 --- a/src/project/root.ts +++ /dev/null @@ -1,572 +0,0 @@ -import * as Project from "./index" -import * as Theory from "../theory" -import Immutable from "immutable" -import Range from "../util/range" -import Rational from "../util/rational" -import ListOfRanges from "../util/listOfRanges" - - -// Least Common Multiple of 2, 3, 4, 5, 6, 7, 8, 9, and 10. -export const MAX_RATIONAL_DENOMINATOR = 2520 - - -export interface Root -{ - nextId: Project.ID - range: Range - baseBpm: number - tracks: Project.Track[] - lists: Immutable.Map> - elems: Immutable.Map - keyChangeTrackId: number - meterChangeTrackId: number - chordTrackId: number -} - - -export function makeEmpty(): Root -{ - return { - nextId: 1, - range: new Range(new Rational(0), new Rational(4)), - baseBpm: 120, - tracks: [], - lists: Immutable.Map>(), - elems: Immutable.Map(), - keyChangeTrackId: -1, - meterChangeTrackId: -1, - chordTrackId: -1, - } -} - - -export function makeNew(): Root -{ - let project = makeEmpty() - - const track1Id = project.nextId - project.keyChangeTrackId = track1Id - project = upsertTrack(project, Project.makeTrackKeyChanges()) - - const track2Id = project.nextId - project.meterChangeTrackId = track2Id - project = upsertTrack(project, Project.makeTrackMeterChanges()) - - project = upsertElement(project, Project.makeKeyChange( - track1Id, new Rational(0), Theory.Key.parse("C Major"))) - - project = upsertElement(project, Project.makeMeterChange( - track2Id, new Rational(0), new Theory.Meter(4, 4))) - - const track3Id = project.nextId - project.chordTrackId = track3Id - project = upsertTrack(project, Project.makeTrackChords()) - - const track4Id = project.nextId - project = upsertTrack(project, Project.makeTrackNotes()) - - return project -} - - -export function getDefault(): Root -{ - let project = makeEmpty() - - const track1Id = project.nextId - project.keyChangeTrackId = track1Id - project = upsertTrack(project, Project.makeTrackKeyChanges()) - - const track2Id = project.nextId - project.meterChangeTrackId = track2Id - project = upsertTrack(project, Project.makeTrackMeterChanges()) - - project = upsertElement(project, Project.makeKeyChange( - track1Id, new Rational(0), Theory.Key.parse("C Major"))) - - project = upsertElement(project, Project.makeMeterChange( - track2Id, new Rational(0), new Theory.Meter(4, 4))) - - const track3Id = project.nextId - project.chordTrackId = track3Id - project = upsertTrack(project, Project.makeTrackChords()) - - project = upsertElement(project, Project.makeChord( - track3Id, - Range.fromStartDuration(new Rational(0), new Rational(1)), - new Theory.Chord(0, 0, 0, []))) - - const track4Id = project.nextId - project = upsertTrack(project, Project.makeTrackNotes()) - - const noteBlockId = project.nextId - project = upsertElement(project, Project.makeNoteBlock( - track4Id, - Range.fromStartDuration(new Rational(0), new Rational(4)))) - - for (let i = 0; i < 16; i++) - project = upsertElement(project, Project.makeNote( - noteBlockId, - Range.fromStartDuration(new Rational(i, 4), new Rational(1, 4)), - 60 + i, - 0, - 1)) - - return project -} - - -export function upsertTrack( - project: Root, - track: Project.Track, - remove: boolean = false, - insertBefore: number = -1): Root -{ - let nextId = project.nextId - let tracks = project.tracks - - if (track.id < 0) - { - track = Project.elemModify(track, { id: nextId }) - nextId++ - } - - if (remove) - { - const trackIndex = tracks.findIndex(t => t.id === track.id) - if (trackIndex >= 0) - tracks = [ - ...tracks.slice(0, trackIndex), - ...tracks.slice(trackIndex + 1) - ] - } - else - { - const trackIndex = tracks.findIndex(t => t.id === track.id) - if (trackIndex < 0) - { - if (insertBefore < 0) - tracks = [...tracks, track] - else - tracks = [ - ...tracks.slice(0, insertBefore), - track, - ...tracks.slice(insertBefore) - ] - } - else - { - tracks = [ - ...tracks.slice(0, trackIndex), - track, - ...tracks.slice(trackIndex + 1) - ] - } - } - - let elems = project.elems - if (remove) - elems = elems.delete(track.id) - else - elems = elems.set(track.id, track) - - return { ...project, nextId, elems, tracks } -} - - -export function upsertElement(project: Root, elem: Project.Element): Root -{ - let nextId = project.nextId - - if (elem.id < 0) - { - elem = Project.elemModify(elem, { id: nextId }) - nextId++ - } - - const prevElem = project.elems.get(elem.id) - const changeParent = !!prevElem && prevElem.parentId != elem.parentId - - if (!changeParent) - { - let list = project.lists.get(elem.parentId) || new ListOfRanges() - list = list.upsert(elem) - - let elems = project.elems.set(elem.id, elem) - let lists = project.lists.set(elem.parentId, list) - return { ...project, nextId, elems, lists } - } - else if (elem.parentId < 0) - { - let prevList = project.lists.get(prevElem!.parentId) || new ListOfRanges() - prevList = prevList.removeById(prevElem!.id) - - let elems = project.elems.delete(elem.id) - let lists = project.lists.set(prevElem!.parentId, prevList) - - /*console.log("delete elem " + elem.id + " from " + prevElem!.parentId) - console.log(prevList.findById(elem.id)) - console.log(elems.get(elem.id))*/ - - return { ...project, nextId, elems, lists } - } - else - { - let prevList = project.lists.get(prevElem!.parentId) || new ListOfRanges() - prevList = prevList.removeById(prevElem!.id) - - let nextList = project.lists.get(elem.parentId) || new ListOfRanges() - nextList = nextList.upsert(elem) - - /*console.log("change parent from " + prevElem!.parentId + " to " + elem.parentId) - console.log(prevElem!) - console.log(prevList.findById(elem.id)) - console.log(elem) - console.log(nextList.findById(elem.id))*/ - - let elems = project.elems.set(elem.id, elem) - let lists = project.lists - .set(prevElem!.parentId, prevList) - .set(elem.parentId, nextList) - - return { ...project, nextId, elems, lists } - } -} - - -export function keyChangeTrackId(project: Root): Project.ID -{ - return project.keyChangeTrackId -} - - -export function meterChangeTrackId(project: Root): Project.ID -{ - return project.meterChangeTrackId -} - - -export function keyAt(project: Root, trackId: Project.ID, time: Rational): Theory.Key -{ - const keyChangeTrackId = Project.keyChangeTrackId(project) - const keyChangeTrackTimedElems = project.lists.get(keyChangeTrackId) - if (!keyChangeTrackTimedElems) - return defaultKey() - - const keyCh = keyChangeTrackTimedElems.findActiveAt(time) - if (keyCh) - return (keyCh as Project.KeyChange).key - - const firstKeyCh = keyChangeTrackTimedElems.findFirst() - if (firstKeyCh) - return (firstKeyCh as Project.KeyChange).key - - return defaultKey() -} - - -export function meterChangeAt(project: Root, trackId: Project.ID, time: Rational): Project.MeterChange | null -{ - const meterChangeTrackId = Project.meterChangeTrackId(project) - const meterChangeTrackTimedElems = project.lists.get(meterChangeTrackId) - if (!meterChangeTrackTimedElems) - return null - - const meterCh = meterChangeTrackTimedElems.findActiveAt(time) - if (meterCh) - return (meterCh as Project.MeterChange) - - const firstMeterCh = meterChangeTrackTimedElems.findFirst() - if (firstMeterCh) - return (firstMeterCh as Project.MeterChange) - - return null -} - - -export function meterAt(project: Root, trackId: Project.ID, time: Rational): Theory.Meter -{ - const meterCh = meterChangeAt(project, trackId, time) - if (meterCh) - return meterCh.meter - - return defaultMeter() -} - - -export function withRefreshedRange(project: Root): Root -{ - let range = new Range(new Rational(0), new Rational(4)); - - for (const track of project.tracks) - { - const list = project.lists.get(track.id) - if (list) - range = range.merge(list.getTotalRange()) - } - - if (range.start.compare(project.range.start) == 0 && - range.end.compare(project.range.end) == 0) - return project - - return { ...project, range } -} - - -export function getElem( - project: Root, - id: Project.ID, - type: T) - : Extract | null -{ - const elem = project.elems.get(id) - if (!elem || elem.type != type) - return null - - return elem as Extract -} - - -export function getTrack( - project: Root, - id: Project.ID, - trackType: T) - : Extract | null -{ - const elem = project.elems.get(id) - if (!elem || elem.type != "track" || elem.trackType != trackType) - return null - - return elem as Extract -} - - -export function cloneElem( - fromProject: Root, - elem: Project.Element, - toProject: Root) - : Root -{ - const newElem = { ...elem } - newElem.id = -1 - - const newId = toProject.nextId - toProject = Project.upsertElement(toProject, newElem) - - const innerList = fromProject.lists.get(elem.id) - if (innerList) - { - for (const innerElem of innerList.iterAll()) - { - const newInnerElem = { ...innerElem } - newInnerElem.parentId = newId - - toProject = cloneElem(fromProject, newInnerElem, toProject) - } - } - - return toProject -} - - -export function splitElem( - project: Root, - elem: Project.Element, - splitRange: Range) - : Root -{ - const origProject = project - const absRange = getAbsoluteRange(origProject, elem.parentId, elem.range) - - if (!absRange.overlapsRange(splitRange)) - return project - - const removeElem = Project.elemModify(elem, { parentId: -1 }) - project = Project.upsertElement(project, removeElem) - - project = splitInnerElem( - origProject, - project, - elem.parentId, - elem, - new Rational(0), - splitRange, - absRange) - - return project -} - - -function splitInnerElem( - origProject: Root, - project: Root, - newParentId: Project.ID, - elem: Project.Element, - relativeDisplace: Rational, - splitRange: Range, - keepRange: Range) - : Root -{ - const innerList = origProject.lists.get(elem.id) - - // FIXME: Also keep inner elements that were outside parent's range - const absRange = getAbsoluteRange(origProject, elem.parentId, elem.range) - if (!absRange.overlapsRange(keepRange)) - return project - - for (const slice of absRange.iterSlices(splitRange)) - { - const newElemPart = Project.elemModify(elem, { - id: -1, - parentId: newParentId, - range: getRelativeRange(origProject, elem.parentId, slice) - .subtract(relativeDisplace), - }) - - const newElemPartId = project.nextId - project = Project.upsertElement(project, newElemPart) - - if (innerList) - { - const innerRelativeDisplace = slice.start.subtract(absRange.start) - - for (const innerElem of innerList.iterAll()) - { - project = splitInnerElem( - origProject, - project, - newElemPartId, - innerElem, - innerRelativeDisplace, - splitRange, - slice) - } - } - } - - return project -} - - -export function parentTrackFor(project: Root, elemId: Project.ID): Project.Track -{ - while (true) - { - const elem = project.elems.get(elemId) - if (!elem) - return null! - - if (elem.type == "track") - return elem - - elemId = elem.parentId - } -} - - -export function getAbsoluteTime(project: Root, parentId: Project.ID, time: Rational): Rational -{ - while (true) - { - const elem = project.elems.get(parentId) - if (!elem) - return time - - if (elem.type == "track") - return time - - time = time.add(elem.range.start) - parentId = elem.parentId - } -} - - -export function getRelativeTime(project: Root, parentId: Project.ID, time: Rational): Rational -{ - while (true) - { - const elem = project.elems.get(parentId) - if (!elem) - return time - - if (elem.type == "track") - return time - - time = time.subtract(elem.range.start) - parentId = elem.parentId - } -} - - -export function getAbsoluteRange(project: Root, parentId: Project.ID, range: Range): Range -{ - while (true) - { - const elem = project.elems.get(parentId) - if (!elem) - return range - - if (elem.type == "track") - return range - - range = range.displace(elem.range.start) - parentId = elem.parentId - } -} - - -export function getRelativeRange(project: Root, parentId: Project.ID, range: Range): Range -{ - while (true) - { - const elem = project.elems.get(parentId) - if (!elem) - return range - - if (elem.type == "track") - return range - - range = range.subtract(elem.range.start) - parentId = elem.parentId - } -} - - -export function getRangeForElems(project: Root, elemIds: Iterable): Range | null -{ - let range: Range | null = null - - for (const id of elemIds) - { - const elem = project.elems.get(id) as Project.Element - if (!elem) - continue - - if (elem.type == "track") - continue - - const absRange = Project.getAbsoluteRange(project, elem.parentId, elem.range) - range = Range.merge(range, absRange) - } - - return range -} - - -export function getMillisecondsAt(project: Root, time: Rational): number -{ - const measuresPerSecond = (project.baseBpm / 4 / 60) - return time.subtract(project.range.start).asFloat() / measuresPerSecond * 1000 -} - - -export function defaultKey(): Theory.Key -{ - return Theory.Key.parse("C Major") -} - - -export function defaultMeter(): Theory.Meter -{ - return new Theory.Meter(4, 4) -} \ No newline at end of file diff --git a/src/song/chord.js b/src/song/chord.js new file mode 100644 index 0000000..584847f --- /dev/null +++ b/src/song/chord.js @@ -0,0 +1,12 @@ +function Chord(timeRange, chordIndex, rootMidiPitch) +{ + this.timeRange = timeRange; + this.chordIndex = chordIndex; + this.rootMidiPitch = rootMidiPitch; +} + + +Chord.prototype.clone = function() +{ + return new Chord(this.timeRange.clone(), this.chordIndex, this.rootMidiPitch); +} \ No newline at end of file diff --git a/src/song/key.js b/src/song/key.js new file mode 100644 index 0000000..5dc32bc --- /dev/null +++ b/src/song/key.js @@ -0,0 +1,15 @@ +function Key(time, scaleIndex, rootMidiPitch) +{ + this.time = time; + this.scaleIndex = scaleIndex; + this.rootMidiPitch = rootMidiPitch; +} + + +Key.prototype.clone = function() +{ + return new Key( + this.time, + this.scaleIndex, + this.rootMidiPitch); +} \ No newline at end of file diff --git a/src/song/meter.js b/src/song/meter.js new file mode 100644 index 0000000..d19c2ca --- /dev/null +++ b/src/song/meter.js @@ -0,0 +1,15 @@ +function Meter(time, numerator, denominator) +{ + this.time = time; + this.numerator = numerator; + this.denominator = denominator; +} + + +Meter.prototype.clone = function() +{ + return new Meter( + this.time, + this.numerator, + this.denominator); +} \ No newline at end of file diff --git a/src/song/note.js b/src/song/note.js new file mode 100644 index 0000000..cef3de9 --- /dev/null +++ b/src/song/note.js @@ -0,0 +1,11 @@ +function Note(timeRange, pitch) +{ + this.timeRange = timeRange; + this.pitch = pitch; +} + + +Note.prototype.clone = function() +{ + return new Note(this.timeRange.clone(), this.pitch.clone()); +} \ No newline at end of file diff --git a/src/song/song.js b/src/song/song.js new file mode 100644 index 0000000..2fd54d2 --- /dev/null +++ b/src/song/song.js @@ -0,0 +1,38 @@ +function Song() +{ + this.length = 960 * 4; + this.notes = []; + this.chords = []; + this.keys = []; + this.meters = []; +} + + +Song.prototype.setLength = function(length) +{ + this.length = length; +} + + +Song.prototype.noteAdd = function(note) +{ + this.notes.push(note); +} + + +Song.prototype.chordAdd = function(chord) +{ + this.chords.push(chord); +} + + +Song.prototype.keyAdd = function(key) +{ + this.keys.push(key); +} + + +Song.prototype.meterAdd = function(meter) +{ + this.meters.push(meter); +} \ No newline at end of file diff --git a/src/synth/synth.js b/src/synth/synth.js new file mode 100644 index 0000000..f3d21bb --- /dev/null +++ b/src/synth/synth.js @@ -0,0 +1,244 @@ +function Synth() +{ + this.audioCtx = new AudioContext(); + this.globalVolume = 0.1; + this.time = 0; + this.noteOnEvents = []; + this.noteOffEvents = []; + this.voices = []; +} + + +Synth.prototype.process = function(deltaTime) +{ + this.time += deltaTime; + + // Process note events. + var noteOnProcessed = 0; + var noteOffProcessed = 0; + + while (true) + { + var processWhichKind = -1; + var nextEventTime = this.time; + + // Determine which event is next up. + if (noteOnProcessed < this.noteOnEvents.length && + this.noteOnEvents[noteOnProcessed].time < nextEventTime) + { + processWhichKind = 0; + nextEventTime = this.noteOnEvents[noteOnProcessed].time; + } + + if (noteOffProcessed < this.noteOffEvents.length && + this.noteOffEvents[noteOffProcessed].time < nextEventTime) + { + processWhichKind = 1; + nextEventTime = this.noteOffEvents[noteOffProcessed].time; + } + + if (processWhichKind == -1) + break; + + // Process next event. + switch (processWhichKind) + { + case 0: + { + var ev = this.noteOnEvents[noteOnProcessed]; + noteOnProcessed++; + this.voiceStart(ev.instrument, ev.midiPitch, ev.volume); + break; + } + case 1: + { + var ev = this.noteOffEvents[noteOffProcessed]; + + noteOffProcessed++; + processedEvent = true; + + for (var j = 0; j < this.voices.length; j++) + { + var voice = this.voices[j]; + if (voice.instrument == ev.instrument && + voice.midiPitch == ev.midiPitch) + { + this.voiceOff(j); + } + } + break; + } + } + } + + // Remove processed events. + this.noteOnEvents.splice(0, noteOnProcessed); + this.noteOffEvents.splice(0, noteOffProcessed); + + // Update audio output. + for (var i = this.voices.length - 1; i >= 0; i--) + { + var voice = this.voices[i]; + + voice.timer += deltaTime; + + var envelope = 1; + if (voice.off) + envelope = Math.max(0, 1 - voice.timer / 0.1); + else + { + if (voice.timer < 0.1) + envelope = Math.max(1, 1.75 - voice.timer / 0.2); + else + envelope = Math.max(0.5, 1 - voice.timer / 0.5); + } + + if (voice.off && voice.timer > 0.1) + { + this.voiceStop(i); + this.voices.splice(i, 1); + } + else + { + for (var j = 0; j < voice.gainNodes.length; j++) + { + if (voice.gainNodes[j] != null) + voice.gainNodes[j].gain.value = voice.amplitudes[j] * voice.volume * envelope * this.globalVolume; + } + } + } +} + + +Synth.prototype.addNoteOn = function(time, instrument, midiPitch, volume) +{ + this.noteOnEvents.push({ + time: time + this.time, + instrument: instrument, + midiPitch: midiPitch, + volume: volume + }); +} + + +Synth.prototype.addNoteOff = function(time, instrument, midiPitch) +{ + this.noteOffEvents.push({ + time: time + this.time, + instrument: instrument, + midiPitch: midiPitch + }); +} + + +Synth.prototype.sortEvents = function() +{ + this.noteOnEvents.sort(function (a, b) + { return a.time - b.time; }); + + this.noteOffEvents.sort(function (a, b) + { return a.time - b.time; }); +} + + +Synth.prototype.voiceStart = function(instrument, midiPitch, volume) +{ + for (var i = this.voices.length - 1; i >= 0; i--) + { + var otherVoice = this.voices[i]; + if (otherVoice.instrument == instrument && + otherVoice.midiPitch == midiPitch) + { + this.voiceStop(i); + this.voices.splice(i, 1); + } + } + + var voice = { + instrument: instrument, + midiPitch: midiPitch, + volume: volume, + timer: 0, + off: false, + amplitudes: [], + oscillators: [], + gainNodes: [] + }; + + var frequencyInHertz = Math.pow(2, (midiPitch - 69) / 12) * 440; + + switch (instrument) + { + case 0: + { + // Square wave harmonics. + voice.amplitudes = [1, 0, 1 / 3, 0, 1 / 5, 0, 1 / 7, 0, 1 / 9]; + break; + } + case 1: + { + // Triangle wave harmonics. + voice.amplitudes = [1, 0, 1 / 9, 0, 1 / 25, 0, 1 / 49, 0, 1 / 81]; + break; + } + } + + for (var i = 0; i < voice.amplitudes.length; i++) + { + if (voice.amplitudes[i] == 0) + { + voice.oscillators.push(null); + voice.gainNodes.push(null); + } + else + { + var oscillator = this.audioCtx.createOscillator(); + oscillator.frequency.value = frequencyInHertz * (i + 1); + oscillator.type = "sine"; + + var gainNode = this.audioCtx.createGain(); + oscillator.connect(gainNode); + gainNode.connect(this.audioCtx.destination); + gainNode.gain.value = voice.amplitudes[i] * volume * this.globalVolume; + + oscillator.start(0); + + voice.oscillators.push(oscillator); + voice.gainNodes.push(gainNode); + } + } + + this.voices.push(voice); +} + + +Synth.prototype.voiceOff = function(index) +{ + var voice = this.voices[index]; + voice.off = true; + voice.timer = 0; +} + + +Synth.prototype.voiceStop = function(index) +{ + var voice = this.voices[index]; + for (var i = 0; i < voice.oscillators.length; i++) + { + if (voice.oscillators[i] != null) + voice.oscillators[i].stop(0); + } +} + + +Synth.prototype.stopAll = function() +{ + for (var i = this.voices.length - 1; i >= 0; i--) + this.voiceStop(i); + + this.voices = []; + this.noteOnEvents = []; + this.noteOffEvents = []; + + this.time = 0; +} \ No newline at end of file diff --git a/src/theory/chord.ts b/src/theory/chord.ts deleted file mode 100644 index 016324c..0000000 --- a/src/theory/chord.ts +++ /dev/null @@ -1,286 +0,0 @@ -import * as MathUtils from "../util/mathUtils" -import Key from "./key" -import Utils from "./utils" - - -interface ChordMetadata -{ - pitches: number[] - id: string - symbol: [boolean, string, string | null] - name: string - startGroup?: string -} - - -export const chordKinds: ChordMetadata[] = -[ - { pitches: [0, 4, 7], id: "M", symbol: [false, "", null], name: "Major", startGroup: "Triads" }, - { pitches: [0, 3, 7], id: "m", symbol: [true, "", null], name: "Minor" }, - { pitches: [0, 4, 8], id: "+", symbol: [false, "", "+"], name: "Augmented" }, - { pitches: [0, 3, 6], id: "o", symbol: [true, "", "o"], name: "Diminished" }, - { pitches: [0, 2, 6], id: "oo", symbol: [true, "", "oo"], name: "Doubly-Diminished" }, - { pitches: [0, 4, 6], id: "b5", symbol: [false, "", "(b5)"], name: "Flat-Fifth" }, - - { pitches: [0, 7], id: "5", symbol: [false, "", "5"], name: "Power" }, - - { pitches: [0, 4, 7, 9], id: "6", symbol: [false, "", "6"], name: "Major Sixth", startGroup: "Sixths" }, - { pitches: [0, 3, 7, 9], id: "m6", symbol: [true, "", "6"], name: "Minor Sixth" }, - - { pitches: [0, 4, 7, 10], id: "7", symbol: [false, "", "7"], name: "Dominant Seventh", startGroup: "Sevenths" }, - { pitches: [0, 4, 7, 11], id: "maj7", symbol: [false, "", "M7"], name: "Major Seventh" }, - { pitches: [0, 3, 7, 10], id: "m7", symbol: [true, "", "7"], name: "Minor Seventh" }, - { pitches: [0, 3, 7, 11], id: "mmaj7", symbol: [true, "", "M7"], name: "Minor-Major Seventh" }, - { pitches: [0, 4, 8, 10], id: "+7", symbol: [false, "+", "7"], name: "Augmented Seventh" }, - { pitches: [0, 4, 8, 11], id: "+maj7", symbol: [false, "+", "M7"], name: "Augmented Major Seventh" }, - { pitches: [0, 3, 6, 9], id: "o7", symbol: [true, "", "o7"], name: "Diminished Seventh" }, - { pitches: [0, 3, 6, 10], id: "%7", symbol: [true, "", "ø7"], name: "Half-Diminished Seventh" }, - - { pitches: [0, 4, 7, 10, 14], id: "9", symbol: [false, "", "9"], name: "Dominant Ninth", startGroup: "Ninths" }, - { pitches: [0, 4, 7, 11, 14], id: "maj9", symbol: [false, "", "M9"], name: "Major Ninth" }, - { pitches: [0, 3, 7, 10, 14], id: "m9", symbol: [true, "", "9"], name: "Minor Ninth" }, - { pitches: [0, 3, 7, 11, 14], id: "mmaj9", symbol: [true, "", "M9"], name: "Minor-Major Ninth" }, - { pitches: [0, 3, 7, 10, 13], id: "9?", symbol: [true, "", "9?"], name: "???" }, - { pitches: [0, 4, 8, 10, 14], id: "+9", symbol: [false, "+", "9"], name: "Augmented Ninth" }, - { pitches: [0, 4, 8, 11, 14], id: "+maj9", symbol: [false, "+", "M9"], name: "Augmented Major Ninth" }, - { pitches: [0, 3, 6, 9, 14], id: "o9", symbol: [true, "", "o9"], name: "Diminished Ninth" }, - { pitches: [0, 3, 6, 9, 13], id: "ob9", symbol: [true, "", "o♭9"], name: "Diminished Minor Ninth" }, - { pitches: [0, 3, 6, 10, 14], id: "%9", symbol: [true, "", "ø9"], name: "Half-Diminished Ninth" }, - { pitches: [0, 3, 6, 10, 13], id: "%b9", symbol: [true, "", "ø♭9"], name: "Half-Diminished Minor Ninth" }, -] - - -export interface ChordSuggestion -{ - chord: Chord - matches: number - misses: number -} - - -export default class Chord -{ - static kinds: ChordMetadata[] = chordKinds - - - rootChroma: number - kind: number - inversion: number - modifiers: any - - - constructor(rootChroma: number, kind: number, inversion: number = 0, modifiers: any[] = []) - { - this.rootChroma = rootChroma - this.kind = kind - this.inversion = inversion - this.modifiers = modifiers - } - - - withChanges(obj: any): Chord - { - return Object.assign(new Chord(this.rootChroma, this.kind, this.inversion, this.modifiers), obj) - } - - - static kindFromId(id: string): number - { - return chordKinds.findIndex(k => k.id === id) - } - - - static kindFromPitches(pitches: number[]): number - { - return chordKinds.findIndex(k => - k.pitches.length === pitches.length && - k.pitches.every((p, i) => pitches[i] === p)) - } - - - static suggestChordsForPitches(pitches: number[]): ChordSuggestion[] - { - const suggestions: ChordSuggestion[] = [] - - for (let k = 0; k < chordKinds.length; k++) - { - const kind = chordKinds[k] - - for (let root = 0; root < pitches.length; root++) - { - const rootPitch = pitches[root] - const matches = new Set() - let misses = 0 - - for (let i = 0; i < pitches.length; i++) - { - const pitch = pitches[(root + i) % pitches.length] - const relPitch = MathUtils.mod(pitch - rootPitch, 12) - - const kindMatch = kind.pitches.findIndex(p => p === relPitch) - if (kindMatch >= 0) - matches.add(kindMatch) - else - misses++ - } - - suggestions.push({ - chord: new Chord(MathUtils.mod(rootPitch, 12), k, 0, []), - matches: matches.size, - misses, - }) - } - } - - suggestions.sort((a, b) => - { - if (a.misses != b.misses) - return a.misses - b.misses - - return b.matches - a.matches - }) - - //console.log("suggestions", pitches, suggestions) - return suggestions.slice(0, 10) - } - - - get kindId(): string - { - return chordKinds[this.kind].id - } - - - romanBase(key: Key): string - { - const degree = key.degreeForMidi(this.rootChroma) - const chordKind = chordKinds[this.kind] || { symbol: [false, "", "?"] } - - let roman = Math.floor(degree) - let accidental = 0 - if (Math.floor(degree) != degree) - { - roman = MathUtils.mod(roman + 1, 7) - accidental = -1 - } - - let baseStr = Utils.accidentalToStr(accidental, true) + Utils.degreeToRomanStr(roman) - if (chordKind.symbol[0]) - baseStr = baseStr.toLowerCase() - - return baseStr + chordKind.symbol[1] - } - - - romanSup(key: Key): string - { - const chordKind = chordKinds[this.kind] || { symbol: [false, "", "?"] } - - let supStr = chordKind.symbol[2] || "" - - if (this.modifiers) - { - if (this.modifiers.add9) - supStr += "(add9)" - - if (this.modifiers.add11) - supStr += "(add11)" - - if (this.modifiers.add13) - supStr += "(add13)" - - if (this.modifiers.no3) - supStr += "(no3)" - - if (this.modifiers.no5) - supStr += "(no5)" - } - - return supStr - } - - - romanSub(key: Key): string - { - let subStr = "" - - if (this.modifiers) - { - if (this.modifiers.sus2) - { - if (this.modifiers.sus4) - subStr += "sus24" - else - subStr += "sus2" - } - else if (this.modifiers.sus4) - subStr += "sus4" - } - - return subStr - } - - - get pitches(): number[] - { - const chordData = chordKinds[this.kind] - if (!chordData) - return [] - - const rootMidi = Utils.mod(this.rootChroma, 12) - - let pitches = [] - for (let i = 0; i < chordData.pitches.length; i++) - pitches.push(rootMidi + chordData.pitches[i]) - - if (this.modifiers.sus2) - pitches[1] = rootMidi + 2 - - if (this.modifiers.sus4) - { - if (this.modifiers.sus2) - pitches.splice(2, 0, rootMidi + 5) - else - pitches[1] = rootMidi + 5 - } - - return pitches - } - - - get strummingPitches(): number[] - { - const rootMidi = Utils.mod(this.rootChroma, 12) - let pitches = this.pitches - if (pitches.length == 0) - return [] - - let octave = 12 * 4 - if (rootMidi >= 6) - octave -= 12 - - pitches = pitches.map(p => p + octave) - - if (pitches.length <= 3) - pitches.push(pitches[0] + 12) - - pitches = pitches.sort((x, y) => (x - y)) - - let sum = pitches.reduce((x, y) => (x + y)) / pitches.length - while (sum < 60) - { - const x = pitches.shift()! - pitches.push(x + 12) - sum += 12 / pitches.length - } - - if (pitches.length >= 4) - { - pitches[0] += 12 - pitches[3] -= 12 - } - - pitches.unshift(octave + rootMidi) - return pitches - } -} \ No newline at end of file diff --git a/src/theory/index.ts b/src/theory/index.ts deleted file mode 100644 index c08938e..0000000 --- a/src/theory/index.ts +++ /dev/null @@ -1,7 +0,0 @@ -export { default as Pitch } from "./pitch" -export { default as PitchName } from "./pitchName" -export { default as Scale } from "./scale" -export { default as Key } from "./key" -export { default as Meter } from "./meter" -export { default as Chord } from "./chord" -export { default as Utils } from "./utils" \ No newline at end of file diff --git a/src/theory/key.ts b/src/theory/key.ts deleted file mode 100644 index 3bebda3..0000000 --- a/src/theory/key.ts +++ /dev/null @@ -1,199 +0,0 @@ -import PitchName from "./pitchName" -import Pitch from "./pitch" -import Scale from "./scale" -import Utils from "./utils" - - -export default class Key -{ - tonic: PitchName - scale: Scale - _chromaToDegree: number[] - - - constructor(tonic: PitchName, scale: Scale) - { - this.tonic = tonic - this.scale = scale - - this._chromaToDegree = [] - for (let degree = 0; degree < this.scale.chromas.length; degree++) - { - let chroma = Utils.mod(this.tonic.midi + this.scale.chromas[degree], 12) - this._chromaToDegree[chroma] = degree - - const nextDegree = (degree + 1) % this.scale.chromas.length - - let nextChroma = chroma + 1 - while (true) - { - let testNextChroma = Utils.mod(this.tonic.midi + this.scale.chromas[nextDegree], 12) - while (testNextChroma < chroma) - testNextChroma += 12 - - if (nextChroma >= testNextChroma) - break - - this._chromaToDegree[nextChroma % 12] = (degree + 0.5) % this.scale.chromas.length - nextChroma += 1 - } - } - } - - - static fromTonicAndScale(tonic: PitchName, scale: Scale): Key - { - return new Key(tonic, scale) - } - - - static parse(str: string): Key - { - str = str.toLowerCase().trim() - - const separator = str.indexOf(" ") - if (separator < 1) - throw "invalid key string" - - const tonicStr = str.substr(0, separator) - const scaleStr = str.substr(separator) - - const tonic = PitchName.parse(tonicStr) - const scale = Scale.parse(scaleStr) - - return new Key(tonic, scale) - } - - - get str(): string - { - const scaleStr = this.scale.name || "Unknown Scale" - const tonicStr = this.tonic.str - - return tonicStr + " " + scaleStr - } - - - toString(): string - { - return this.str - } - - - degreeForChroma(chroma: number): number - { - return Utils.mod(this._chromaToDegree[chroma], this.scale.chromas.length) - } - - - degreeForMidi(midi: number): number - { - return this.degreeForChroma(Utils.mod(midi, 12)) - } - - - degreeForPitch(pitch: Pitch | PitchName): number - { - return this.degreeForMidi(pitch.midi) - } - - - octavedDegreeForMidi(midi: number): number - { - const degree = this.degreeForMidi(midi) - const degreeOctave = Math.floor((midi - this.tonic.midi) / 12) - - return degree + this.scale.chromas.length * degreeOctave - } - - - octavedDegreeForPitch(pitch: Pitch | PitchName): number - { - return this.octavedDegreeForMidi(pitch.midi) - } - - - midiForDegree(octavedDegree: number): number - { - const degree = Utils.mod(octavedDegree, this.scale.chromas.length) - const degreeOctave = Math.floor(octavedDegree / this.scale.chromas.length) - - return this.tonic.midi + this.scale.chromas[degree] + degreeOctave * 12 - } - - - pitchForDegree(octavedDegree: number): Pitch - { - return Pitch.fromMidi(this.midiForDegree(octavedDegree)) - } - - - chromaForDegree(octavedDegree: number): number - { - return Utils.mod(this.midiForDegree(octavedDegree), 12) - } - - - nameForMidi(midi: number): PitchName - { - const degree = this.degreeForMidi(midi) - - const letter1 = Utils.mod(this.tonic.letter + Math.floor(degree), 7) - const accidental1 = Utils.mod(midi - Utils.letterToChroma(letter1) + 6, 12) - 6 - - if (degree == Math.floor(degree)) - return new PitchName(letter1, accidental1) - - const letter2 = Utils.mod(letter1 + 1, 7) - const accidental2 = Utils.mod(midi - Utils.letterToChroma(letter2) + 6, 12) - 6 - - const letter3 = Utils.mod(letter1 - 1, 7) - const accidental3 = Utils.mod(midi - Utils.letterToChroma(letter3) + 6, 12) - 6 - - const attempts = [ - [letter1, accidental1], - [letter2, accidental2], - [letter3, accidental3], - ] - - attempts.sort((a, b) => Math.abs(a[1]) - Math.abs(b[1])) - - return new PitchName(attempts[0][0], attempts[0][1]) - } - - - nameForPitch(pitch: Pitch | PitchName): PitchName - { - return this.nameForMidi(pitch.midi) - } - - - nameForChroma(chroma: number): PitchName - { - return this.nameForMidi(chroma) - } - - - nameForDegree(degree: number): PitchName - { - return this.nameForMidi(this.midiForDegree(degree)) - } - - - get midi(): number[] - { - return this.scale.chromas.map(chroma => chroma + this.tonic.midi) - } - - - get chroma(): number[] - { - return this.scale.chromas.map(chroma => Utils.mod(chroma + this.tonic.midi, 12)) - } - - - get namedPitches(): PitchName[] - { - return this.scale.chromas.map(chroma => this.nameForMidi(chroma + this.tonic.midi)) - } -} \ No newline at end of file diff --git a/src/theory/meter.ts b/src/theory/meter.ts deleted file mode 100644 index b432c26..0000000 --- a/src/theory/meter.ts +++ /dev/null @@ -1,68 +0,0 @@ -import Rational from "../util/rational" - - -export default class Meter -{ - numerator: number - denominator: number - - - constructor(numerator: number, denominator: number) - { - this.numerator = numerator - this.denominator = denominator - } - - - withChanges(obj: any): Meter - { - return Object.assign(new Meter(this.numerator, this.denominator), obj) - } - - - *iterMeasuresPairwise(time: Rational | null = null): Generator<[number, number, any, any], void, void> - { - time = time || new Rational(0) - - while (true) - { - const nextTime: Rational = time.add(new Rational(this.numerator, this.denominator)) - yield [this.numerator, this.denominator, time, nextTime] - time = nextTime - } - } - - - get fullCycleDuration(): Rational - { - return new Rational(this.numerator, this.denominator) - } - - - get alternatingMeasureCount(): number - { - return 1 - } - - - get str(): string - { - return this.numerator + " / " + this.denominator - } - - - static parse(src: string): Meter - { - const split = src.split("/") - if (split.length != 2) - throw "invalid meter syntax" - - const numerator = parseInt(split[0].trim()) - const denominator = parseInt(split[1].trim()) - - if (!isFinite(numerator) || !isFinite(denominator)) - throw "invalid meter syntax" - - return new Meter(numerator, denominator) - } -} \ No newline at end of file diff --git a/src/theory/pitch.ts b/src/theory/pitch.ts deleted file mode 100644 index d1d41b1..0000000 --- a/src/theory/pitch.ts +++ /dev/null @@ -1,94 +0,0 @@ -import PitchName from "./pitchName" -import Utils from "./utils" - - -export default class Pitch -{ - midi: number - - - constructor(midi: number) - { - this.midi = midi - } - - - static fromMidi(midi: number): Pitch - { - return new Pitch(midi) - } - - - static fromOctaveAndChroma(octave: number, chroma: number): Pitch - { - return Pitch.fromMidi(12 * octave + chroma) - } - - - static fromOctaveAndName(octave: number, pitchName: any): Pitch - { - return Pitch.fromMidi(12 * octave + pitchName.midi) - } - - - static parse(str: string): Pitch - { - const pitchName = PitchName.parse(str) - - str = str.toLowerCase().trim() - - // Determine octave - let octave = 0 - for (let i = 1; i < str.length; i++) - { - const c = str.charCodeAt(i) - - if (c == "-".charCodeAt(0) || (c >= "0".charCodeAt(0) && c <= "9".charCodeAt(0))) - { - octave = parseInt(str.substr(i)) - break - } - } - - if (octave === undefined || octave === null || isNaN(octave) || !isFinite(octave)) - throw "invalid pitch string" - - return Pitch.fromOctaveAndName(octave, pitchName) - } - - - get frequency(): number - { - return Math.pow(2, (this.midi - 69) / 12) * 440 - } - - - get octave(): number - { - return Math.floor(this.midi / 12) - } - - - get chroma(): number - { - return Utils.mod(this.midi, 12) - } - - - get name(): PitchName - { - return PitchName.fromMidi(this.midi) - } - - - get str(): string - { - return this.name.str + this.octave.toString() - } - - - toString(): string - { - return this.str - } -} \ No newline at end of file diff --git a/src/theory/pitchName.ts b/src/theory/pitchName.ts deleted file mode 100644 index c8a88d6..0000000 --- a/src/theory/pitchName.ts +++ /dev/null @@ -1,114 +0,0 @@ -import Utils from "./utils" - - -export default class PitchName -{ - letter: number - accidental: number - - - constructor(letter: number, accidental: number) - { - this.letter = Utils.mod(letter, 7) - this.accidental = accidental - } - - - static fromMidi(midi: number): PitchName - { - const chroma = Utils.mod(midi, 12) - - const letter = Utils.chromaToLetter(chroma) - const accidental = Utils.chromaToAccidental(chroma) - - return new PitchName(letter, accidental) - } - - - static fromChroma(chroma: number): PitchName - { - return PitchName.fromMidi(chroma) - } - - - static parse(str: string): PitchName - { - if (str.length < 1) - throw "invalid pitch string" - - str = str.toLowerCase().trim() - - // Determine letter - const letterStr = str[0] - const letter = Utils.strToLetter(letterStr) - - if (letter === undefined) - throw "invalid pitch string" - - // Determine accidental - let accidental = 0 - for (let i = 1; i < str.length; i++) - { - const c = str.charCodeAt(i) - - if (c == "b".charCodeAt(0) || c == "♭".charCodeAt(0)) - accidental -= 1 - - else if (c == "#".charCodeAt(0) || c == "♯".charCodeAt(0)) - accidental += 1 - } - - return new PitchName(letter, accidental) - } - - - get midi(): number - { - return Utils.letterToChroma(this.letter) + this.accidental - } - - - get chroma(): number - { - return Utils.mod(this.midi, 12) - } - - - altered(additionalAccidental: number): PitchName - { - return new PitchName(this.letter, this.accidental + additionalAccidental) - } - - - get simplified(): PitchName - { - if (this.accidental === 0) - return this - else - return PitchName.fromMidi(this.midi) - } - - - get str(): string - { - const letterStr = Utils.letterToStr(this.letter) - const accidentalStr = Utils.accidentalToStr(this.accidental) - - return letterStr + accidentalStr - } - - - get strUnicode(): string - { - const letterStr = Utils.letterToStr(this.letter) - const accidentalStr = Utils.accidentalToStr(this.accidental, true) - - return letterStr + accidentalStr - } - - - toString(): string - { - return this.str - } -} \ No newline at end of file diff --git a/src/theory/scale.ts b/src/theory/scale.ts deleted file mode 100644 index 2237013..0000000 --- a/src/theory/scale.ts +++ /dev/null @@ -1,109 +0,0 @@ -import PitchName from "./pitchName" - - -interface ScaleMetadata -{ - chromas: number[] - mode: number - id: string - names: string[] -} - - -export const knownScales: ScaleMetadata[] = -[ - { chromas: [0, 2, 4, 5, 7, 9, 11], mode: 0, id: "maj", names: ["Major", "Ionian"] }, - { chromas: [0, 2, 3, 5, 7, 9, 10], mode: 1, id: "dor", names: ["Dorian"] }, - { chromas: [0, 1, 3, 5, 7, 8, 10], mode: 2, id: "phr", names: ["Phrygian"] }, - { chromas: [0, 2, 4, 6, 7, 9, 11], mode: 3, id: "lyd", names: ["Lydian"] }, - { chromas: [0, 2, 4, 5, 7, 9, 10], mode: 4, id: "mix", names: ["Mixolydian"] }, - { chromas: [0, 2, 3, 5, 7, 8, 10], mode: 5, id: "min", names: ["Natural Minor", "Minor", "Aeolian"] }, - { chromas: [0, 1, 3, 5, 6, 8, 10], mode: 6, id: "loc", names: ["Locrian"] }, - - { chromas: [0, 1, 4, 5, 7, 8, 11], mode: 0, id: "dharmaj", names: ["Double Harmonic Major"] }, -] - - -export default class Scale -{ - static list = knownScales - - chromas: number[] - metadata: ScaleMetadata | null | undefined - - - constructor(chromas: number[]) - { - if (chromas.length <= 1 || chromas.length > 12) - throw "invalid scale length" - - this.chromas = chromas - - this.metadata = knownScales.find(s => - s.chromas.length == this.chromas.length && - s.chromas.every((p, index) => p == this.chromas[index])) - } - - - static fromChromas(chromas: number[]): Scale - { - return new Scale(chromas) - } - - - static fromId(id: string): Scale - { - return new Scale(knownScales.find(s => s.id === id)!.chromas) - } - - - static parse(str: string): Scale - { - str = str.toLowerCase().trim() - - const knownScale = knownScales.find(s => s.names.some(n => n.toLowerCase() == str)) - if (!knownScale) - throw "no known scale with given name" - - return new Scale(knownScale.chromas) - } - - - get id(): string | null - { - if (!this.metadata) - return null - - return this.metadata.id - } - - - get name(): string | null - { - if (!this.metadata) - return null - - return this.metadata.names[0] - } - - - get alterationsFromMajor(): number[] - { - if (this.chromas.length != 7) - throw "not a seven-note scale" - - return this.chromas.map((pitch, i) => pitch - knownScales[0].chromas[i]) - } - - - get str(): string - { - return this.name || ("[" + this.chromas.map(chroma => PitchName.fromChroma(chroma).str).join(", ") + "]") - } - - - toString(): string - { - return this.str - } -} \ No newline at end of file diff --git a/src/theory/test.ts b/src/theory/test.ts deleted file mode 100644 index 985fc4b..0000000 --- a/src/theory/test.ts +++ /dev/null @@ -1,83 +0,0 @@ -import * as Theory from "." -import assert from "assert" - - -export function test() -{ - const c = Theory.PitchName.parse("C") - assert.strictEqual(c.letter, 0) - assert.strictEqual(c.accidental, 0) - assert.strictEqual(c.str, "C") - assert.strictEqual(c.midi, 0) - assert.strictEqual(c.chroma, 0) - - const fb = Theory.PitchName.parse("Fb") - assert.strictEqual(fb.letter, 3) - assert.strictEqual(fb.accidental, -1) - assert.strictEqual(fb.str, "Fb") - assert.strictEqual(fb.midi, 4) - assert.strictEqual(fb.chroma, 4) - - const bsss = Theory.PitchName.parse("B###") - assert.strictEqual(bsss.letter, 6) - assert.strictEqual(bsss.accidental, 3) - assert.strictEqual(bsss.str, "B###") - assert.strictEqual(bsss.midi, 14) - assert.strictEqual(bsss.chroma, 2) - - const d5 = Theory.Pitch.parse("D5") - assert.strictEqual(d5.str, "D5") - assert.strictEqual(d5.name.str, "D") - assert.strictEqual(d5.midi, 62) - assert.strictEqual(d5.chroma, 2) - - const cMajorKey = Theory.Key.parse("C Major") - assert.strictEqual(cMajorKey.str, "C Major") - assert.strictEqual(cMajorKey.tonic.str, "C") - assert.strictEqual(cMajorKey.scale.name, "Major") - assert.strictEqual(cMajorKey.degreeForPitch(c), 0) - assert.strictEqual(cMajorKey.degreeForPitch(fb), 2) - assert.strictEqual(cMajorKey.degreeForPitch(bsss), 1) - assert.strictEqual(cMajorKey.degreeForPitch(d5), 1) - assert.strictEqual(cMajorKey.octavedDegreeForPitch(c), 0) - assert.strictEqual(cMajorKey.octavedDegreeForPitch(fb), 2) - assert.strictEqual(cMajorKey.octavedDegreeForPitch(bsss), 8) - assert.strictEqual(cMajorKey.octavedDegreeForPitch(d5), 5 * 7 + 1) - assert.strictEqual(cMajorKey.nameForPitch(c).str, "C") - assert.strictEqual(cMajorKey.nameForPitch(fb).str, "E") - assert.strictEqual(cMajorKey.nameForPitch(bsss).str, "D") - assert.strictEqual(cMajorKey.nameForPitch(d5).str, "D") - assert.strictEqual(cMajorKey.midiForDegree(0), 0) - assert.strictEqual(cMajorKey.midiForDegree(4), 7) - assert.strictEqual(cMajorKey.midiForDegree(6), 11) - - const fsMinorKey = Theory.Key.parse("F# Minor") - assert.strictEqual(fsMinorKey.str, "F# Natural Minor") - assert.strictEqual(fsMinorKey.tonic.str, "F#") - assert.strictEqual(fsMinorKey.scale.name, "Natural Minor") - assert.strictEqual(fsMinorKey.degreeForPitch(c), 3.5) - assert.strictEqual(fsMinorKey.degreeForPitch(fb), 6) - assert.strictEqual(fsMinorKey.degreeForPitch(bsss), 5) - assert.strictEqual(fsMinorKey.degreeForPitch(d5), 5) - assert.strictEqual(fsMinorKey.octavedDegreeForPitch(c), -3.5) - assert.strictEqual(fsMinorKey.octavedDegreeForPitch(fb), -1) - assert.strictEqual(fsMinorKey.octavedDegreeForPitch(bsss), 5) - assert.strictEqual(fsMinorKey.octavedDegreeForPitch(d5), 4 * 7 + 5) - assert.strictEqual(fsMinorKey.nameForPitch(c).str, "C") - assert.strictEqual(fsMinorKey.nameForPitch(fb).str, "E") - assert.strictEqual(fsMinorKey.nameForPitch(bsss).str, "D") - assert.strictEqual(fsMinorKey.nameForPitch(d5).str, "D") - assert.strictEqual(fsMinorKey.midiForDegree(0), 6) - assert.strictEqual(fsMinorKey.midiForDegree(4), 13) - assert.strictEqual(fsMinorKey.midiForDegree(6), 16) - - - /*console.log(cMajorKey.namedPitches.map(p => p.str)) - console.log(fsMinorKey.namedPitches.map(p => p.str)) - console.log(Key.fromTonicAndScale(PitchName.parse("C##"), Scale.fromChroma([0, 1, 2, 3, 4, 5, 6])).namedPitches.map(p => p.str)) - - console.log(cMajorKey.scale.alterationsFromMajor) - console.log(fsMinorKey.scale.alterationsFromMajor)*/ - - console.log("Theory tests passed") -} \ No newline at end of file diff --git a/src/theory/utils.ts b/src/theory/utils.ts deleted file mode 100644 index 6edb860..0000000 --- a/src/theory/utils.ts +++ /dev/null @@ -1,38 +0,0 @@ -export default class Utils -{ - static mod(x: number, m: number) - { - return (x % m + m) % m - } - - - static chromaToLetter = (chroma: number): number => [0, 0, 1, 1, 2, 3, 3, 4, 4, 5, 5, 6][chroma] - static chromaToAccidental = (chroma: number): number => [0, 1, 0, 1, 0, 0, 1, 0, 1, 0, 1, 0][chroma] - static chromaToDegreeInCMajor = (chroma: number): number => [0, 0.5, 1, 1.5, 2, 3, 3.5, 4, 4.5, 5, 5.5, 6][chroma] - - static letterToChroma = (letter: number): number => [0, 2, 4, 5, 7, 9, 11][letter] - static letterToStr = (letter: number): string => ["C", "D", "E", "F", "G", "A", "B"][letter] - static strToLetter = (str: string): number => - { - const map: any = { c: 0, d: 1, e: 2, f: 3, g: 4, a: 5, b: 6 } - return map[str] - } - - static degreeToRomanStr = (degree: number): string => - ["I", "II", "III", "IV", "V", "VI", "VII"][degree] - - static degreeToColor = (degree: number): string => - ["#f00", "#f80", "#fd0", "#0d0", "#00f", "#80f", "#f0f"][degree] - - static degreeToColorFaded = (degree: number): string => - ["#400", "#420", "#430", "#030", "#004", "#204", "#404"][degree] - - - static accidentalToStr(accidental: number, useUnicode: boolean = false): string - { - if (accidental < 0) - return (useUnicode ? "\u{266d}" : "b").repeat(-accidental) - else - return (useUnicode ? "\u{266f}" : "#").repeat(accidental) - } -} \ No newline at end of file diff --git a/src/timeline/Element.tsx b/src/timeline/Element.tsx deleted file mode 100644 index 53b28b2..0000000 --- a/src/timeline/Element.tsx +++ /dev/null @@ -1,393 +0,0 @@ -import React from "react" -import * as Timeline from "./index" -import * as Project from "../project" -import * as Playback from "../playback" -import * as Prefs from "../prefs" -import * as Popup from "../popup" -import * as Dockable from "../dockable" -import * as UI from "../ui" -import { useRefState, RefState } from "../util/refState" -import Rect from "../util/rect" -import styled from "styled-components" - - -const StyledTrackButton = styled.button` - pointer-events: auto; - color: #fff; - border: 1px solid #888; - border-radius: 0.5em; - background-color: #2f3136; - padding: 0.1em 0.3em; - cursor: pointer; - - &:hover - { - border: 1px solid #fff; - } -` - - -export function TimelineElement(props: { state?: RefState }) -{ - const refDiv = React.useRef(null) - const refCanvas = React.useRef(null) - - const editorState = props.state ?? useRefState(() => Timeline.init()) - const dockableWindow = Dockable.useWindow() - - const lastTimelineRenderRef = React.useRef(0) - - const render = (force?: boolean) => - { - if (!refCanvas.current) - return - - const now = new Date().getTime() - if (!force && now < lastTimelineRenderRef.current + 15) - return - - //console.log("Timeline render") - lastTimelineRenderRef.current = now - - Timeline.render( - editorState.ref.current, - refCanvas.current.getContext("2d")!) - } - - const onResize = () => - { - if (!refDiv.current || !refCanvas.current) - return - - const pixelRatio = window.devicePixelRatio || 1 - - const rect = refDiv.current.getBoundingClientRect() - const x = Math.floor(rect.x) - const y = Math.floor(rect.y) - const w = Math.floor(rect.width * pixelRatio) - const h = Math.floor(rect.height * pixelRatio) - - refCanvas.current.style.width = Math.floor(rect.width) + "px" - refCanvas.current.style.height = Math.floor(rect.height) + "px" - refCanvas.current.width = w - refCanvas.current.height = h - - const renderRect = new Rect(x, y, w, h) - - Timeline.resize(editorState.ref.current, pixelRatio, renderRect) - render(true) - } - - React.useLayoutEffect(() => - { - if (!refDiv.current) - return - - const observer = new ResizeObserver(entries => - { - onResize() - }) - - const elem = refDiv.current - observer.observe(elem) - - onResize() - return () => observer.unobserve(elem) - - }, [refDiv.current]) - - React.useEffect(() => - { - Timeline.scrollPlaybackTimeIntoView(editorState.ref.current) - render(!Playback.global.playing) - - }, [Playback.globalObservable.updateToken]) - - React.useEffect(() => - { - Timeline.refreshTracks(editorState.ref.current) - render(true) - - }, [Project.global.project.tracks]) - - React.useEffect(() => - { - const refCanvasCurrent = refCanvas.current - if (!refCanvasCurrent) - return - - const transformMousePos = (canvas: HTMLCanvasElement, ev: MouseEvent) => - { - const rect = canvas.getBoundingClientRect() - return { - x: (ev.clientX - rect.left) * editorState.ref.current.pixelRatio, - y: (ev.clientY - rect.top) * editorState.ref.current.pixelRatio, - } - } - - const preventDefault = (ev: Event) => ev.preventDefault() - - const setCursor = (state: Timeline.State) => - { - let cursor = "text" - const mouseAction = - state.mouse.down ? state.mouse.action : - state.hoverControl != Timeline.TrackControl.None ? Timeline.MouseAction.DragTrackControl : - state.hover ? state.hover.action : - Timeline.MouseAction.None - - if (state.tracks.some((tr: any) => !!tr.pencil) || - mouseAction == Timeline.MouseAction.Pencil) - cursor = "crosshair" - else if (mouseAction == Timeline.MouseAction.DragTrackControl) - cursor = "crosshair" - else if (mouseAction == Timeline.MouseAction.DragClone) - cursor = "crosshair" - else if (mouseAction == Timeline.MouseAction.DragTime || - mouseAction == Timeline.MouseAction.DragTimeAndRow || - mouseAction == Timeline.MouseAction.DragRow || - mouseAction == Timeline.MouseAction.DragTrackHeader) - cursor = (state.mouse.down ? "grabbing" : "grab") - else if (mouseAction == Timeline.MouseAction.Pan) - cursor = "move" - else if (mouseAction == Timeline.MouseAction.StretchTimeStart || - mouseAction == Timeline.MouseAction.StretchTimeEnd) - cursor = "col-resize" - - refCanvasCurrent.style.cursor = cursor - } - - const onMouseMove = (ev: MouseEvent) => - { - const state = editorState.ref.current - if (state.mouse.down) - ev.preventDefault() - - const pos = transformMousePos(refCanvasCurrent, ev) - const projectBefore = Project.global.project - const needsRender1 = Timeline.mouseMove(state, pos) - const needsRender2 = Timeline.mouseDrag(state, pos, false) - - if (projectBefore !== Project.global.project) - Project.notifyObservers() - - if (needsRender1 || needsRender2) - render(true) - - setCursor(state) - } - - const onMouseDown = (ev: MouseEvent) => - { - if (document.activeElement instanceof HTMLElement) - document.activeElement.blur() - - ev.preventDefault() - - Dockable.removeEphemerals(Dockable.global.state) - Dockable.notifyObservers() - - const state = editorState.ref.current - const pos = transformMousePos(refCanvasCurrent, ev) - const projectBefore = Project.global.project - Timeline.mouseMove(state, pos) - Timeline.mouseDown(state, ev.button != 0) - Timeline.mouseDrag(state, pos, true) - - if (projectBefore !== Project.global.project) - Project.notifyObservers() - - render() - setCursor(state) - editorState.commit() - } - - const onMouseUp = (ev: MouseEvent) => - { - const state = editorState.ref.current - if (state.mouse.down) - ev.preventDefault() - - const pos = transformMousePos(refCanvasCurrent, ev) - const projectBefore = Project.global.project - Timeline.mouseUp(state) - - if (projectBefore !== Project.global.project) - { - Project.notifyObservers() - window.dispatchEvent(new Event("timelineRefresh")) - } - - render() - setCursor(state) - editorState.commit() - } - - const onMouseWheel = (ev: WheelEvent) => - { - Timeline.mouseWheel(editorState.ref.current, ev.deltaX, ev.deltaY) - render() - } - - const onKeyDown = (ev: KeyboardEvent) => - { - const isActiveWindow = - Dockable.global.state.activePanel === dockableWindow.panel && - (!document.activeElement || document.activeElement.tagName != "INPUT") - - const projectBefore = Project.global.project - Timeline.keyDown(editorState.ref.current, isActiveWindow, ev.key.toLowerCase()) - - if (projectBefore !== Project.global.project) - Project.notifyObservers() - - render() - editorState.commit() - } - - const onKeyUp = (ev: KeyboardEvent) => - { - Timeline.keyUp(editorState.ref.current, ev.key.toLowerCase()) - } - - const onRewind = (ev: Event) => - { - Timeline.rewind(editorState.ref.current) - editorState.commit() - render(true) - } - - const onRefresh = (ev: Event) => - { - Timeline.refreshTracks(editorState.ref.current) - editorState.commit() - render(true) - } - - const onReset = (ev: Event) => - { - const state = editorState.ref.current - Timeline.modeStackPop(state, 0) - Timeline.reset(state) - Timeline.rewind(state) - Timeline.refreshTracks(state) - editorState.commit() - render(true) - } - - refCanvasCurrent.addEventListener("mousedown", onMouseDown) - refCanvasCurrent.addEventListener("wheel", onMouseWheel) - refCanvasCurrent.addEventListener("contextmenu", preventDefault) - - window.addEventListener("mousemove", onMouseMove) - window.addEventListener("mouseup", onMouseUp) - window.addEventListener("keydown", onKeyDown) - window.addEventListener("keyup", onKeyUp) - - window.addEventListener("timelineRewind", onRewind) - window.addEventListener("timelineRefresh", onRefresh) - window.addEventListener("timelineReset", onReset) - - return () => - { - refCanvasCurrent.removeEventListener("mousedown", onMouseDown) - refCanvasCurrent.removeEventListener("wheel", onMouseWheel) - refCanvasCurrent.removeEventListener("contextmenu", preventDefault) - - window.removeEventListener("mousemove", onMouseMove) - window.removeEventListener("mouseup", onMouseUp) - window.removeEventListener("keydown", onKeyDown) - window.removeEventListener("keyup", onKeyUp) - - window.removeEventListener("timelineRewind", onRewind) - window.removeEventListener("timelineRefresh", onRefresh) - window.removeEventListener("timelineReset", onReset) - } - - }, [refCanvas.current]) - - - const yTrackEnd = - editorState.ref.current.tracks.length == 0 ? - 0 : - editorState.ref.current.tracks[editorState.ref.current.tracks.length - 1].renderRect.y2 - - const onAddTrack = () => - { - let proj = Project.global.project - proj = Project.upsertTrack(proj, Project.makeTrackNotes()) - Project.global.project = proj - Project.splitUndoPoint() - Project.addUndoPoint("addTrack") - Project.notifyObservers() - - window.dispatchEvent(new Event("timelineRefresh")) - } - - const onTrackSettings = (ev: React.MouseEvent, trackIndex: number) => - { - Popup.global.elem = () => - { - return - - - } - Popup.global.rect = Rect.fromElement(ev.target as HTMLElement) - Popup.notifyObservers() - } - - const pixelRatio = window.devicePixelRatio || 1 - - return React.useMemo(() => -
- - -
- - +{/*➕*/} - -
-
- , [ - Project.global.project.tracks, - editorState.ref.current.tracks, - editorState.ref.current.trackScroll, - ]) -} - - -export function sendEventRefresh() -{ - window.dispatchEvent(new Event("timelineRefresh")) -} - - -export function sendEventReset() -{ - window.dispatchEvent(new Event("timelineReset")) -} \ No newline at end of file diff --git a/src/timeline/element.js b/src/timeline/element.js new file mode 100644 index 0000000..baddb7f --- /dev/null +++ b/src/timeline/element.js @@ -0,0 +1,37 @@ +function Element() +{ + this.selected = false; + this.track = null; + this.timeRange = null; + + this.note = null; + this.chord = null; + this.key = null; + this.meter = null; + + this.regions = []; + this.interactKind = 0; + this.interactTimeRange = null; + this.interactPitch = null; + + var that = this; + + this.select = function() + { + this.selected = true; + this.track.selectedElements.push(this); + }; + + this.unselect = function() + { + this.selected = false; + + var index = this.track.selectedElements.indexOf(this); + + if (index != -1) + this.track.selectedElements.splice(index, 1); + }; + + this.modify = function() { that.track.elementModify(this); }; + this.remove = function() { that.track.elementRemove(this); }; +} \ No newline at end of file diff --git a/src/timeline/index.ts b/src/timeline/index.ts deleted file mode 100644 index 924bf5f..0000000 --- a/src/timeline/index.ts +++ /dev/null @@ -1,18 +0,0 @@ -export * from "./state" -export * from "./state_keyDown" -export * from "./state_keyUp" -export * from "./state_mouseMove" -export * from "./state_mouseDown" -export * from "./state_mouseUp" -export * from "./state_mouseDrag" -export * from "./state_mouseWheel" -export * from "./state_render" -export * from "./track" -export * from "./trackKeyChanges" -export * from "./trackMeterChanges" -export * from "./trackNoteBlocks" -export * from "./trackChords" -export * from "./trackNotes" -export * from "./trackNoteVolumes" -export * from "./trackNoteVelocities" -export * from "./Element" \ No newline at end of file diff --git a/src/timeline/state.ts b/src/timeline/state.ts deleted file mode 100644 index c42256a..0000000 --- a/src/timeline/state.ts +++ /dev/null @@ -1,1082 +0,0 @@ -import Immutable from "immutable" -import * as Project from "../project" -import * as Playback from "../playback" -import * as Prefs from "../prefs" -import * as Timeline from "./index" -import * as Popup from "../popup" -import * as Theory from "../theory" -import * as Dockable from "../dockable" -import * as MathUtils from "../util/mathUtils" -import Rational from "../util/rational" -import { RefState } from "../util/refState" -import Range from "../util/range" -import Rect from "../util/rect" - - -export enum MouseAction -{ - None, - Pan, - SelectCursor, - SelectRect, - Pencil, - DragTime, - DragRow, - DragTimeAndRow, - DragTrackHeader, - DragTrackControl, - StretchTimeStart, - StretchTimeEnd, - DragClone, -} - - -export enum TrackControl -{ - None, - Volume, - Pan, - Mute, - Solo, -} - - -export enum Mode -{ - Project, - NoteBlock, -} - - -export interface ModeStack -{ - mode: Mode - modeNoteBlockId: Project.ID - - trackScroll: number -} - - -export interface State -{ - modeStack: ModeStack[] - mode: Mode - modeNoteBlockId: Project.ID - - pixelRatio: number - renderRect: Rect - - trackMeasuresH: number - trackHeaderW: number - trackControlX: number - trackControlY: number - trackControlSize: number - - tracks: Timeline.TimelineTrack[] - trackScroll: number - trackScrollLocked: boolean - - timeScroll: number - timeScale: number - timeSnap: Rational - timeSnapBase: Rational - - noteRowH: number - - cursor: - { - visible: boolean - time1: Rational - time2: Rational - trackIndex1: number - trackIndex2: number - rectY1: number - rectY2: number - } - - keysDown: Set, - - mouse: - { - down: boolean - downDate: Date - - action: MouseAction - - point: Point - pointPrev: Point - - wheelDate: Date - } - - drag: - { - origin: - { - point: Point - range: Range | null - timeScroll: number - trackScroll: number - trackYScroll: number - project: Project.Root - } - - xLocked: boolean - yLocked: boolean - - posDelta: { x: number, y: number } - timeDelta: Rational - rowDelta: number - trackDelta: number - trackInsertionBefore: number - - elemId: Project.ID - notePreviewLast: number | null - } - - insertion: - { - nearMidiPitch: number - duration: Rational - } - - hover: HoverData | null - hoverControl: TrackControl - selection: Immutable.Set - - needsKeyFinish: boolean - rangeSelectOriginTrackIndex: number -} - - -export interface Point -{ - pos: { x: number, y: number } - time: Rational - row: number - trackIndex: number - trackPos: { x: number, y: number } - originTrackPos: { x: number, y: number } -} - - -export interface HoverData -{ - id: Project.ID - range: Range - action: MouseAction -} - - -export function init(): State -{ - return { - modeStack: [], - mode: Mode.Project, - modeNoteBlockId: -1, - - pixelRatio: 1, - renderRect: new Rect(0, 0, 0, 0), - - trackMeasuresH: 20, - trackHeaderW: 200, - trackControlX: 10, - trackControlY: 25, - trackControlSize: 20, - - tracks: [], - trackScroll: -40, - trackScrollLocked: true, - - timeScroll: -2.5, - timeScale: 100, - timeSnap: new Rational(1, 8), - timeSnapBase: new Rational(1, 16), - - noteRowH: 15, - - cursor: - { - visible: true, - time1: new Rational(0), - time2: new Rational(0), - trackIndex1: 0, - trackIndex2: 0, - rectY1: 0, - rectY2: 0, - }, - - keysDown: new Set(), - - mouse: - { - down: false, - downDate: new Date(), - - action: MouseAction.None, - - point: - { - pos: { x: 0, y: 0 }, - time: new Rational(0), - row: 0, - trackIndex: 0, - trackPos: { x: 0, y: 0 }, - originTrackPos: { x: 0, y: 0 }, - }, - - pointPrev: - { - pos: { x: 0, y: 0 }, - time: new Rational(0), - row: 0, - trackIndex: 0, - trackPos: { x: 0, y: 0 }, - originTrackPos: { x: 0, y: 0 }, - }, - - wheelDate: new Date(), - }, - - drag: - { - origin: null!, - xLocked: true, - yLocked: true, - posDelta: { x: 0, y: 0 }, - timeDelta: new Rational(0), - rowDelta: 0, - trackDelta: 0, - trackInsertionBefore: -1, - - elemId: -1, - notePreviewLast: null, - }, - - insertion: - { - nearMidiPitch: 60, - duration: new Rational(1, 4), - }, - - hover: null, - hoverControl: TrackControl.None, - - selection: Immutable.Set(), - - needsKeyFinish: false, - rangeSelectOriginTrackIndex: -1, - } -} - - -export function resize(state: Timeline.State, pixelRatio: number, rect: Rect) -{ - state.pixelRatio = pixelRatio - state.renderRect = rect - refreshTracks(state) -} - - -export function reset(state: Timeline.State) -{ - state.trackScroll = -40 - state.trackScrollLocked = true - - state.timeScroll = -2.5 - state.timeScale = 100 - state.timeSnap = new Rational(1, 8) - state.timeSnapBase = new Rational(1, 16) -} - - -export function rewind(state: Timeline.State) -{ - state.cursor.visible = true - state.cursor.time1 = state.cursor.time2 = Project.global.project.range.start - Playback.setStartTime(Project.global.project.range.start) - scrollTimeIntoView(state, state.cursor.time1) -} - - -export function refreshTracks(state: Timeline.State) -{ - const tracks: Timeline.TimelineTrack[] = [] - - for (let t = 0; t < Project.global.project.tracks.length; t++) - { - const track = Project.global.project.tracks[t] - if (track.trackType == "notes") - { - if (state.mode == Mode.NoteBlock) - { - const noteBlock = Project.global.project.elems.get(state.modeNoteBlockId) - if (noteBlock && track.id == noteBlock.parentId) - { - tracks.push(new Timeline.TimelineTrackNotes(track.id, state.modeNoteBlockId, track.name, 0)) - tracks.push(new Timeline.TimelineTrackNoteVolumes(track.id, state.modeNoteBlockId, 80)) - } - } - else - tracks.push(new Timeline.TimelineTrackNoteBlocks(track.id, track.name, 50)) - } - else if (track.trackType == "chords") - { - tracks.push(new Timeline.TimelineTrackChords(track.id, track.name, 50)) - } - else if (track.trackType == "keyChanges") - { - tracks.push(new Timeline.TimelineTrackKeyChanges(track.id, track.name, 25)) - } - else if (track.trackType == "meterChanges") - { - tracks.push(new Timeline.TimelineTrackMeterChanges(track.id, track.name, 25)) - } - } - - for (let t = 0; t < tracks.length; t++) - tracks[t].copyState(state) - - state.trackScrollLocked = false - - let fixedH = 0 - for (let t = 0; t < tracks.length; t++) - { - if (tracks[t].renderRect.h > 0) - fixedH += tracks[t].renderRect.h - else - state.trackScrollLocked = true - } - - const availableH = state.renderRect.h - (state.trackScrollLocked ? state.trackMeasuresH : 0) - - for (let t = 0; t < tracks.length; t++) - { - if (tracks[t].renderRect.h == 0) - { - tracks[t].renderRect.h = availableH - fixedH - state.trackScroll = 0 - } - } - - let y = state.trackScrollLocked ? state.trackMeasuresH : 0 - for (let t = 0; t < tracks.length; t++) - { - tracks[t].renderRect.y = y - y += tracks[t].renderRect.h - } - - state.tracks = tracks -} - - -export function modeStackPush(state: Timeline.State) -{ - const newStack = - { - mode: state.mode, - modeNoteBlockId: state.modeNoteBlockId, - trackScroll: state.trackScroll, - } - - state.modeStack.push(newStack) - selectionClear(state) -} - - -export function modeStackPop(state: Timeline.State, index?: number) -{ - if (state.modeStack.length == 0) - return - - if (index !== undefined) - { - if (index >= state.modeStack.length) - return - - while (state.modeStack.length > index + 1) - state.modeStack.pop() - } - - const stackElem = state.modeStack.pop()! - - state.mode = stackElem.mode - state.modeNoteBlockId = stackElem.modeNoteBlockId - state.trackScroll = stackElem.trackScroll - - selectionClear(state) -} - - -export function scrollTimeIntoView(state: Timeline.State, time: Rational) -{ - const range = visibleTimeRange(state) - const marginPixels = 100 - const marginTime = Rational.fromFloat(marginPixels / state.timeScale, 10000) - if (time.compare(range.end.subtract(marginTime)) >= 0) - { - state.timeScroll = - time.asFloat() - - (state.renderRect.w - state.trackHeaderW + marginPixels) / state.timeScale - } - else if (time.compare(range.start.add(marginTime)) <= 0) - { - state.timeScroll = - time.asFloat() - - (state.trackHeaderW + marginPixels) / state.timeScale - } -} - - -export function scrollPlaybackTimeIntoView(state: Timeline.State) -{ - if (!Playback.global.playing) - return - - if (state.mouse.down) - return - - const range = visibleTimeRange(state) - const marginPixels = 100 - const marginTime = Rational.fromFloat(marginPixels / state.timeScale, 10000) - if (Playback.global.playTime.compare(range.end.subtract(marginTime)) >= 0 || - Playback.global.playTime.compare(range.start.add(marginTime)) <= 0) - { - state.timeScroll = - Playback.global.playTime.asFloat() - - (state.trackHeaderW + marginPixels) / state.timeScale - } -} - - -export function xAtTime(state: Timeline.State, time: Rational): number -{ - return (time.asFloat() - state.timeScroll) * state.timeScale -} - - -export function timeAtX(state: Timeline.State, x: number, timeSnap?: Rational): Rational -{ - timeSnap = timeSnap || state.timeSnap - const time = x / state.timeScale + state.timeScroll - return Rational.fromFloat(time, timeSnap.denominator) -} - - -export function timeRangeAtX(state: Timeline.State, x1: number, x2: number, timeSnap?: Rational) -{ - timeSnap = timeSnap || state.timeSnap - return new Range( - timeAtX(state, x1, timeSnap).subtract(timeSnap), - timeAtX(state, x2, timeSnap).add(timeSnap)) -} - - -export function trackY(state: Timeline.State, trackIndex: number): number -{ - if (trackIndex < 0 || trackIndex >= state.tracks.length) - return 0 - - return state.tracks[trackIndex].renderRect.y - state.trackScroll -} - - -export function trackAtY(state: Timeline.State, y: number): number | null -{ - y += state.trackScroll - - if (y < 0) - return null - - for (let t = 0; t < state.tracks.length; t++) - { - const track = state.tracks[t] - - if (y >= track.renderRect.y && y <= track.renderRect.y + track.renderRect.h) - return t - } - - return null -} - - -export function trackAtYClamped(state: Timeline.State, y: number): number -{ - y += state.trackScroll - - if (y < 0) - return 0 - - for (let t = 0; t < state.tracks.length; t++) - { - const track = state.tracks[t] - - if (y >= track.renderRect.y && y <= track.renderRect.y + track.renderRect.h) - return t - } - - return state.tracks.length - 1 -} - - -export function trackInsertionAtY(state: Timeline.State, y: number): number -{ - y += state.trackScroll - - if (y < 0) - return 0 - - for (let t = 0; t < state.tracks.length; t++) - { - const track = state.tracks[t] - - if (y <= track.renderRect.y + track.renderRect.h / 2) - return t - } - - return state.tracks.length -} - - -export function trackControlAtPoint( - state: Timeline.State, - trackIndex: number, - pos: { x: number, y: number }) - : TrackControl -{ - const projTrack = Project.getElem(Project.global.project, state.tracks[trackIndex].projectTrackId, "track") - if (!projTrack) - return TrackControl.None - - if (pos.y < state.trackControlY || pos.y > state.trackControlY + state.trackControlSize) - return TrackControl.None - - const xSlot = Math.floor((pos.x - state.trackControlX) / state.trackControlSize) - if (xSlot < 0) - return TrackControl.None - - switch (projTrack.trackType) - { - case "notes": - case "chords": - { - switch (xSlot) - { - case 0: - case 1: return TrackControl.Volume - //case 1: return TrackControl.Pan - case 7: return TrackControl.Mute - case 8: return TrackControl.Solo - } - } - } - - return TrackControl.None -} - - -export function rectForTrack(state: Timeline.State, trackIndex: number): Rect | null -{ - let y = 0 - for (let t = 0; t < state.tracks.length; t++) - { - const h = state.tracks[t].renderRect.h - - if (t == trackIndex) - { - return new Rect( - state.trackHeaderW, - y, - state.renderRect.w - state.trackHeaderW, - h) - } - - y += h - } - - return null -} - - -export function pointAt(state: Timeline.State, pos: { x: number, y: number }): Point -{ - const trackIndex = trackAtYClamped(state, pos.y) - const time = timeAtX(state, pos.x) - - const trackPosY = pos.y - trackY(state, state.mouse.point.trackIndex) - const trackPos = { x: pos.x, y: trackPosY } - - const row = state.tracks[trackIndex].rowAtY(state, trackPosY) - - let originTrackPos = trackPos - if (state.drag.origin) - { - const originTrackPosY = pos.y - trackY(state, state.drag.origin.point.trackIndex) - originTrackPos = { x: pos.x, y: originTrackPosY } - } - - return { - pos, - time, - trackIndex, - trackPos, - row, - originTrackPos, - } -} - - -export function selectionClear(state: Timeline.State) -{ - state.selection = state.selection.clear() -} - - -export function selectionRange(state: Timeline.State): Range | null -{ - return Project.getRangeForElems(Project.global.project, state.selection) -} - - -export function selectionToggleHover( - state: Timeline.State, - hover: HoverData, - selectMultiple: boolean) -{ - const alreadySelected = state.selection.has(hover.id) - - if (!selectMultiple && !alreadySelected) - selectionClear(state) - - if (!alreadySelected || !selectMultiple) - state.selection = state.selection.add(hover.id) - else - state.selection = state.selection.remove(hover.id) - - state.drag.origin.range = selectionRange(state) - state.mouse.action = hover.action -} - - -export function selectionAdd(state: Timeline.State, id: Project.ID) -{ - state.selection = state.selection.add(id) -} - - -export function selectionAddAtCursor( - state: Timeline.State, - verticalRegion?: { y1: number, y2: number }) -{ - const trackMin = Math.min(state.cursor.trackIndex1, state.cursor.trackIndex2) - const trackMax = Math.max(state.cursor.trackIndex1, state.cursor.trackIndex2) - - const time1 = state.cursor.time1 - const time2 = state.cursor.time2 - if (time1.compare(time2) != 0) - { - const range = new Range(time1, time2, false, false).sorted() - - for (let t = trackMin; t <= trackMax; t++) - { - for (const id of state.tracks[t].elemsAtRegion(state, range, verticalRegion)) - selectionAdd(state, id) - } - } -} - - -export function deleteElems(state: Timeline.State, elemIds: Iterable) -{ - const range = Project.getRangeForElems(Project.global.project, elemIds) || - new Range(state.cursor.time1, state.cursor.time1) - - for (const id of elemIds) - { - const elem = Project.global.project.elems.get(id) - if (!elem) - continue - - if (elem.type == "track") - continue - - const removeElem = Project.elemModify(elem, { parentId: -1 }) - Project.global.project = Project.upsertElement(Project.global.project, removeElem) - } - - for (const id of elemIds) - { - const track = Project.global.project.elems.get(id) - if (!track) - continue - - if (track.type != "track") - continue - - if (track.id === Project.global.project.keyChangeTrackId || - track.id === Project.global.project.meterChangeTrackId || - track.id === Project.global.project.chordTrackId) - continue - - Project.global.project = Project.upsertTrack(Project.global.project, track, true) - } - - Project.global.project = Project.withRefreshedRange(Project.global.project) - state.cursor.visible = true - cursorSetTime(state, range.start, range.start) - scrollTimeIntoView(state, range.start) -} - - -export function selectionCopy(state: Timeline.State) -{ - const copiedData: Project.CopiedData = - { - project: Project.global.project, - elemsByTrack: [], - } - - for (let tr = 0; tr < state.tracks.length; tr++) - copiedData.elemsByTrack.push([]) - - for (const elemId of state.selection) - { - const elem = Project.global.project.elems.get(elemId) - if (!elem || elem.type == "track") - continue - - const trackIndex = state.tracks.findIndex(tr => tr.parentId == elem.parentId) - if (trackIndex < 0) - continue - - copiedData.elemsByTrack[trackIndex].push(elem) - } - - while (copiedData.elemsByTrack.length > 0 && copiedData.elemsByTrack[0].length == 0) - copiedData.elemsByTrack.shift() - - let copiedRange: Range | null = null - for (let ctr = 0; ctr < copiedData.elemsByTrack.length; ctr++) - { - const copiedTrack = copiedData.elemsByTrack[ctr] - - for (const elem of copiedTrack) - { - copiedRange = - Project.getAbsoluteRange(copiedData.project, elem.parentId, elem.range) - .merge(copiedRange) - } - } - - if (!copiedRange) - return - - cursorSetTime(state, copiedRange.end, copiedRange.end) - Project.global.copiedData = copiedData - - //console.log("copy", copiedData) -} - - -export function paste(state: Timeline.State) -{ - const copiedData = Project.global.copiedData - if (!copiedData) - return - - const pasteToTime = state.cursor.time1.min(state.cursor.time2) - const pasteToTrackIndex = Math.min(state.cursor.trackIndex1, state.cursor.trackIndex2) - - let copiedRange: Range | null = null - for (let ctr = 0; ctr < copiedData.elemsByTrack.length; ctr++) - { - const copiedTrack = copiedData.elemsByTrack[ctr] - - for (const elem of copiedTrack) - { - copiedRange = - Project.getAbsoluteRange(copiedData.project, elem.parentId, elem.range) - .merge(copiedRange) - } - } - - if (!copiedRange) - return - - //console.log("paste", copiedData, copiedRange) - - selectionClear(state) - - let newProject = Project.global.project - let maxAffectedTrackIndex = pasteToTrackIndex - - for (let ctr = 0; ctr < copiedData.elemsByTrack.length; ctr++) - { - const copiedTrack = copiedData.elemsByTrack[ctr] - - for (const elem of copiedTrack) - { - const pasteTrackIndex = state.tracks - .slice(pasteToTrackIndex + ctr) - .findIndex(tr => tr.acceptedElemTypes.has(elem.type)) - - if (pasteTrackIndex < 0) - continue - - const copyToTrack = state.tracks[pasteToTrackIndex + ctr + pasteTrackIndex] - maxAffectedTrackIndex = Math.max(maxAffectedTrackIndex, pasteToTrackIndex + ctr + pasteTrackIndex) - - const absRange = Project.getAbsoluteRange(copiedData.project, elem.parentId, elem.range) - const newAbsRange = absRange.subtract(copiedRange.start).displace(pasteToTime) - const newRelRange = Project.getRelativeRange(newProject, copyToTrack.parentId, newAbsRange) - - const newElemId = newProject.nextId - newProject = Project.cloneElem(copiedData.project, elem, newProject) - - const newElem = - { - ...elem, - id: newElemId, - parentId: copyToTrack.parentId, - range: newRelRange, - } - - //console.log("paste elem", elem, newElem) - - newProject = Project.upsertElement(newProject, newElem) - selectionAdd(state, newElemId) - } - } - - const newCursorTime = pasteToTime.add(copiedRange.duration) - cursorSetTime(state, newCursorTime, newCursorTime) - cursorSetTrack(state, pasteToTrackIndex, maxAffectedTrackIndex) - scrollTimeIntoView(state, newCursorTime) - - Project.global.project = Project.withRefreshedRange(newProject) - Project.global.project = Project.global.project - selectionRemoveConflictingBehind(state) -} - - -export function cursorSetTime( - state: Timeline.State, - time1: Rational | null, - time2?: Rational | null) -{ - state.cursor.time1 = time1 || state.cursor.time1 - state.cursor.time2 = time2 || state.cursor.time2 -} - - -export function cursorSetTrack( - state: Timeline.State, - trackIndex1: number | null, - trackIndex2?: number | null) -{ - state.cursor.trackIndex1 = - Math.max(0, Math.min(state.tracks.length - 1, - trackIndex1 ?? state.cursor.trackIndex1)) - - state.cursor.trackIndex2 = - Math.max(0, Math.min(state.tracks.length - 1, - trackIndex2 ?? state.cursor.trackIndex2)) -} - - -export function cursorSetTrackByParentId( - state: Timeline.State, - parentId: Project.ID) -{ - const trackIndex = state.tracks.findIndex(tr => tr.parentId === parentId) - if (trackIndex < 0) - return - - state.cursor.trackIndex1 = state.cursor.trackIndex2 = trackIndex -} - - -export function findPreviousAnchor( - state: Timeline.State, - time: Rational, - trackIndex1: number, - trackIndex2: number) - : Rational -{ - let prevAnchor: Rational | null = null - - const trackMin = Math.min(trackIndex1, trackIndex2) - const trackMax = Math.max(trackIndex1, trackIndex2) - - for (let tr = Math.max(0, trackMin); tr <= Math.min(state.tracks.length - 1, trackMax); tr++) - { - const anchor = state.tracks[tr].findPreviousAnchor(state, time) - prevAnchor = Rational.max(prevAnchor, anchor) - } - - if (!prevAnchor) - return Project.global.project.range.start - - return prevAnchor -} - - -export function deleteRange( - state: Timeline.State, - range: Range, - trackIndex1: number, - trackIndex2: number) -{ - const trackMin = Math.min(trackIndex1, trackIndex2) - const trackMax = Math.max(trackIndex1, trackIndex2) - - for (let tr = Math.max(0, trackMin); tr <= Math.min(state.tracks.length - 1, trackMax); tr++) - state.tracks[tr].deleteRange(state, range) -} - - -export function selectionRemoveConflictingBehind(state: Timeline.State) -{ - for (let tr = 0; tr < state.tracks.length; tr++) - state.tracks[tr].selectionRemoveConflictingBehind(state) -} - - -export function keyHandlePendingFinish(state: Timeline.State) -{ - if (!state.needsKeyFinish) - return - - state.needsKeyFinish = false - - selectionRemoveConflictingBehind(state) -} - - -export function insertNote(state: Timeline.State, time: Rational, chroma: number) -{ - keyHandlePendingFinish(state) - - const track = state.tracks[state.cursor.trackIndex1] - if (!(track instanceof Timeline.TimelineTrackNotes)) - return - - const noteBlock = Project.getElem(Project.global.project, track.parentId, "noteBlock") - if (!noteBlock) - return - - const insertOctave = Math.floor(state.insertion.nearMidiPitch / 12) - const possiblePitches = [-1, 0, 1].map(offset => - { - const pitch = (insertOctave + offset) * 12 + (MathUtils.mod(chroma, 12)) - const delta = Math.abs(pitch - state.insertion.nearMidiPitch) - return { pitch, delta } - }) - - possiblePitches.sort((a, b) => a.delta - b.delta) - const chosenPitch = possiblePitches[0].pitch - - const range = new Range(time, time.add(state.insertion.duration)) - const volumeDb = 0 - const velocity = 1 - - const note = Project.makeNote( - noteBlock.id, - range.subtract(noteBlock.range.start), - chosenPitch, volumeDb, velocity) - - let project = Project.global.project - const id = project.nextId - project = Project.upsertElement(project, note) - project = Project.withRefreshedRange(project) - Project.global.project = project - - state.insertion.nearMidiPitch = chosenPitch - - state.cursor.visible = false - cursorSetTime(state, range.end, range.end) - scrollTimeIntoView(state, range.end) - selectionClear(state) - selectionAdd(state, id) - Playback.playNotePreview(noteBlock.parentId, chosenPitch, volumeDb, velocity) - selectionRemoveConflictingBehind(state) -} - - -export function insertChord(state: Timeline.State, time: Rational, chord: Theory.Chord) -{ - keyHandlePendingFinish(state) - - const track = state.tracks[state.cursor.trackIndex1] - if (!(track instanceof Timeline.TimelineTrackChords)) - return - - const range = new Range(time, time.add(state.insertion.duration)) - const volumeDb = 0 - const velocity = 1 - - const projChord = Project.makeChord( - track.projectTrackId, - range, - chord) - - let project = Project.global.project - const id = project.nextId - project = Project.upsertElement(project, projChord) - project = Project.withRefreshedRange(project) - Project.global.project = project - - state.cursor.visible = false - cursorSetTime(state, range.end, range.end) - scrollTimeIntoView(state, range.end) - selectionClear(state) - selectionAdd(state, id) - Playback.playChordPreview(track.projectTrackId, chord, volumeDb, velocity) - selectionRemoveConflictingBehind(state) -} - - -export function insertNoteBlock(state: Timeline.State, time: Rational): Project.ID | null -{ - keyHandlePendingFinish(state) - - const track = state.tracks[state.cursor.trackIndex1] - if (!(track instanceof Timeline.TimelineTrackNoteBlocks)) - return null - - let endTime = time.add(new Rational(4)) - - const list = Project.global.project.lists.get(track.projectTrackId) - if (list) - { - for (const elem of list.iterAtRange(new Range(time, endTime))) - endTime = endTime.min(elem.range.start) - } - - if (endTime.compare(time) <= 0) - return null - - const noteBlock = Project.makeNoteBlock( - track.projectTrackId, - new Range(time, endTime)) - - let project = Project.global.project - const id = project.nextId - project = Project.upsertElement(project, noteBlock) - project = Project.withRefreshedRange(project) - Project.global.project = project - - scrollTimeIntoView(state, time) - selectionClear(state) - selectionAdd(state, id) - selectionRemoveConflictingBehind(state) - return id -} - - -export function visibleTimeRange(state: Timeline.State): Range -{ - return new Range( - timeAtX(state, state.trackHeaderW).subtract(state.timeSnap), - timeAtX(state, state.renderRect.w).add(state.timeSnap)) -} \ No newline at end of file diff --git a/src/timeline/state_keyDown.ts b/src/timeline/state_keyDown.ts deleted file mode 100644 index 8b67cbc..0000000 --- a/src/timeline/state_keyDown.ts +++ /dev/null @@ -1,543 +0,0 @@ -import * as Timeline from "./index" -import * as Project from "../project" -import * as Prefs from "../prefs" -import * as Theory from "../theory" -import * as Playback from "../playback" -import Range from "../util/range" -import Rational from "../util/rational" - - -export function keyDown(state: Timeline.State, activeWindow: boolean, key: string) -{ - state.keysDown.add(key) - - //console.log("keyDown", key, data.activeWindow) - - if (!activeWindow) - return - - switch (key) - { - case "escape": - { - handleEscape(state) - Project.splitUndoPoint() - break - } - - case "enter": - { - handleEnter(state) - Project.splitUndoPoint() - break - } - - case "delete": - { - handleDelete(state) - Project.splitUndoPoint() - Project.addUndoPoint("keyDown_delete") - break - } - - case "backspace": - { - handleBackspace(state) - Project.addUndoPoint("keyDown_backspace") - break - } - - case "c": - { - if (state.keysDown.has("control")) - Timeline.selectionCopy(state) - - break - } - - case "x": - { - if (state.keysDown.has("control")) - { - Timeline.selectionCopy(state) - Timeline.deleteElems(state, state.selection) - Project.splitUndoPoint() - Project.addUndoPoint("cut") - } - - break - } - - case "v": - { - if (state.keysDown.has("control")) - { - Timeline.paste(state) - Project.splitUndoPoint() - Project.addUndoPoint("copy") - } - - break - } - - case "arrowright": - case "arrowleft": - { - handleLeftRight(state, key === "arrowleft") - Project.addUndoPoint("keyDown_move") - break - } - - case "arrowup": - case "arrowdown": - { - handleUpDown(state, key === "arrowup", false) - Project.addUndoPoint("keyDown_move") - break - } - - case ".": - case ">": - case ",": - case "<": - { - handleUpDown(state, key === "." || key === ">", true) - Project.addUndoPoint("keyDown_move") - break - } - - case "1": - case "2": - case "3": - case "4": - case "5": - case "6": - case "7": - { - const degree = key.charCodeAt(0) - "1".charCodeAt(0) - handleNumber(state, degree) - - Project.splitUndoPoint() - Project.addUndoPoint("keyDown_insert") - break - } - - case "h": - { - handleLengthChange(state, 0) - break - } - - case "j": - { - handleLengthChange(state, 1) - break - } - - case "k": - { - handleLengthChange(state, 2) - break - } - - case "l": - { - handleLengthChange(state, 3) - break - } - - case ";": - case ":": - { - handleLengthChange(state, 4) - break - } - } -} - - -function modifySelectedElems( - state: Timeline.State, - func: (elem: Project.Element) => Project.Element) -{ - let newProject = Project.global.project - - for (const id of state.selection) - { - const elem = Project.global.project.elems.get(id) - if (!elem) - continue - - const newElem = func(elem) - if (newElem === elem) - continue - - newProject = Project.upsertElement(newProject, newElem) - } - - Project.global.project = Project.withRefreshedRange(newProject) -} - - -function handleEscape(state: Timeline.State) -{ - if (Playback.global.playing) - { - Playback.setStartTime(Project.global.project.range.start) - Playback.setPlaying(true) - } - else - { - Timeline.rewind(state) - } -} - - -function handleEnter(state: Timeline.State) -{ - if (state.cursor.visible && state.selection.size != 0) - { - state.cursor.visible = false - return - } - - state.cursor.visible = true - - const range = Timeline.selectionRange(state) - if (range) - { - let trackId = state.tracks[0].projectTrackId - for (const id of state.selection) - { - trackId = Project.parentTrackFor(Project.global.project, id).id - } - - const trackIndex = state.tracks.findIndex(tr => tr.projectTrackId === trackId) - - Timeline.cursorSetTime(state, range.end, range.end) - Timeline.cursorSetTrack(state, trackIndex, trackIndex) - Timeline.scrollTimeIntoView(state, range.end) - } - - Timeline.keyHandlePendingFinish(state) - Timeline.selectionClear(state) -} - - -function handleDelete(state: Timeline.State) -{ - Timeline.deleteElems(state, state.selection) -} - - -function handleBackspace(state: Timeline.State) -{ - if (!state.cursor.visible) - { - Timeline.deleteElems(state, state.selection) - return - } - - const track1 = Math.min(state.cursor.trackIndex1, state.cursor.trackIndex2) - const track2 = Math.max(state.cursor.trackIndex1, state.cursor.trackIndex2) - - if (state.cursor.time1.compare(state.cursor.time2) == 0) - { - const time = state.cursor.time1.min(state.cursor.time2) - const prevAnchor = Timeline.findPreviousAnchor(state, time, track1, track2) - const range = new Range(prevAnchor, time, false, false) - Timeline.deleteRange(state, range, track1, track2) - - state.cursor.visible = true - Timeline.cursorSetTime(state, prevAnchor, prevAnchor) - Timeline.scrollTimeIntoView(state, prevAnchor) - } - else - { - const time1 = state.cursor.time1.min(state.cursor.time2) - const time2 = state.cursor.time1.max(state.cursor.time2) - const range = new Range(time1, time2, false, false) - Timeline.deleteRange(state, range, track1, track2) - - state.cursor.visible = true - Timeline.cursorSetTime(state, time1, time1) - Timeline.scrollTimeIntoView(state, time1) - } - - Project.global.project = Project.withRefreshedRange(Project.global.project) -} - - -function handleLeftRight(state: Timeline.State, isLeft: boolean) -{ - const keyFast = state.keysDown.has(Prefs.global.editor.keyDisplaceFast) - const keyCursor2 = state.keysDown.has(Prefs.global.editor.keyDisplaceCursor2) - const keyStretch = state.keysDown.has(Prefs.global.editor.keyDisplaceStretch) - - - if (Playback.global.playing) - { - const timeDelta = state.timeSnap.multiplyByFloat( - (keyFast ? 64 : 16) * (isLeft ? -1 : 1)) - - Playback.setStartTime(Playback.global.playTime.add(timeDelta)) - Playback.setPlaying(true) - } - else if (state.cursor.visible && (state.selection.size == 0 || keyCursor2)) - { - const timeDelta = state.timeSnap.multiplyByFloat( - (keyFast ? 16 : 1) * (isLeft ? -1 : 1)) - - Timeline.keyHandlePendingFinish(state) - - if (keyCursor2) - { - const newTime = state.cursor.time2.add(timeDelta) - Timeline.cursorSetTime(state, null, newTime) - Timeline.selectionClear(state) - Timeline.selectionAddAtCursor(state) - Timeline.scrollTimeIntoView(state, newTime) - Playback.setStartTime(newTime) - } - else - { - const timeMin = state.cursor.time1.min(state.cursor.time2) - const timeMax = state.cursor.time1.max(state.cursor.time2) - - const newTime = (isLeft ? timeMin : timeMax).add(timeDelta) - - Timeline.cursorSetTime(state, newTime, newTime) - Timeline.scrollTimeIntoView(state, newTime) - Playback.setStartTime(newTime) - } - } - else - { - const timeDelta = state.timeSnap.multiplyByFloat( - (keyFast ? 16 : 1) * (isLeft ? -1 : 1)) - - const selectionRange = Timeline.selectionRange(state) - - let playedPreview = false - modifySelectedElems(state, (elem) => - { - if (elem.type == "track") - return elem - - let newRange = elem.range - - if (keyStretch && selectionRange) - { - const absRange = Project.getAbsoluteRange(Project.global.project, elem.parentId, elem.range) - newRange = Project.getRelativeRange(Project.global.project, elem.parentId, absRange.stretch( - timeDelta, - selectionRange.start, - selectionRange.end)) - } - else - { - newRange = elem.range.displace(timeDelta) - } - - if (!elem.range.duration.isZero() && newRange.duration.isZero()) - return elem - - if (!playedPreview) - { - if (elem.type == "note") - { - playedPreview = true - state.insertion.nearMidiPitch = elem.midiPitch - state.insertion.duration = newRange.duration - } - else if (elem.type == "chord") - { - playedPreview = true - state.insertion.duration = newRange.duration - } - } - - return Project.elemModify(elem, { - range: newRange - }) - }) - - const range = Timeline.selectionRange(state) || new Range(new Rational(0), new Rational(0)) - const newTime = (isLeft && !keyStretch ? range.start : range.end) - state.cursor.visible = false - Timeline.cursorSetTime(state, newTime, newTime) - Timeline.scrollTimeIntoView(state, newTime) - - state.needsKeyFinish = true - } -} - - -function handleUpDown(state: Timeline.State, isUp: boolean, isChromatic: boolean) -{ - const keyFast = state.keysDown.has(Prefs.global.editor.keyDisplaceFast) - const keyCursor2 = state.keysDown.has(Prefs.global.editor.keyDisplaceCursor2) - const keyChromatic = state.keysDown.has(Prefs.global.editor.keyDisplaceChromatically) - - - if (!isChromatic && state.cursor.visible && (state.selection.size == 0 || keyCursor2)) - { - const trackDelta = (isUp ? -1 : 1) - - Timeline.keyHandlePendingFinish(state) - - if (keyCursor2) - { - const newTrack = state.cursor.trackIndex2 + trackDelta - Timeline.cursorSetTrack(state, null, newTrack) - Timeline.selectionClear(state) - Timeline.selectionAddAtCursor(state) - } - else - { - const trackMin = Math.min(state.cursor.trackIndex1, state.cursor.trackIndex2) - const trackMax = Math.max(state.cursor.trackIndex1, state.cursor.trackIndex2) - - const newTrack = (isUp ? trackMin : trackMax) + trackDelta - Timeline.cursorSetTrack(state, newTrack, newTrack) - } - } - else - { - const pitchDelta = (keyFast ? 12 : (keyChromatic || isChromatic ? 1 : 0)) * (isUp ? 1 : -1) - const degreeDelta = (keyFast || isChromatic ? 0 : 1) * (isUp ? 1 : -1) - - let playedPreview = false - modifySelectedElems(state, (elem) => - { - if (elem.type == "note") - { - const track = Project.parentTrackFor(Project.global.project, elem.parentId) - const key = Project.keyAt(Project.global.project, track.id, elem.range.start) - const degree = key.octavedDegreeForMidi(elem.midiPitch) - const newDegree = degree + degreeDelta - const newPitch = pitchDelta != 0 ? - elem.midiPitch + pitchDelta : - key.midiForDegree(degreeDelta >= 0 ? Math.floor(newDegree) : Math.ceil(newDegree)) - - if (!playedPreview) - { - playedPreview = true - Playback.playNotePreview(track.id, newPitch, elem.volumeDb, elem.velocity) - state.insertion.nearMidiPitch = newPitch - state.insertion.duration = elem.range.duration - } - - return Project.elemModify(elem, { midiPitch: newPitch }) - } - else if (elem.type == "chord") - { - const track = Project.parentTrackFor(Project.global.project, elem.parentId) - const key = Project.keyAt(Project.global.project, track.id, elem.range.start) - const degree = key.octavedDegreeForMidi(elem.chord.rootChroma) - const newDegree = degree + degreeDelta - const newRoot = pitchDelta != 0 ? - elem.chord.rootChroma + pitchDelta : - key.midiForDegree(degreeDelta >= 0 ? Math.floor(newDegree) : Math.ceil(newDegree)) - - const newChord = new Theory.Chord( - newRoot, - elem.chord.kind, elem.chord.inversion, elem.chord.modifiers) - - if (!playedPreview) - { - playedPreview = true - Playback.playChordPreview(track.id, newChord, 0, 1) - state.insertion.duration = elem.range.duration - } - - return Project.elemModify(elem, { chord: newChord }) - } - else - { - return elem - } - }) - - state.cursor.visible = false - state.needsKeyFinish = true - } -} - - -function handleNumber(state: Timeline.State, degree: number) -{ - const time = state.cursor.time1.min(state.cursor.time2) - const track = state.tracks[state.cursor.trackIndex1] - const trackId = track.projectTrackId - const key = Project.keyAt(Project.global.project, trackId, time) - - if (track instanceof Timeline.TimelineTrackChords) - { - const root = key.midiForDegree(degree) - - let pitches = [0] - pitches.push(key.midiForDegree(degree + 2) - root) - pitches.push(key.midiForDegree(degree + 4) - root) - - const kind = Theory.Chord.kindFromPitches(pitches) - const chord = new Theory.Chord(root, kind, 0, []) - Timeline.insertChord(state, time, chord) - } - else if (track instanceof Timeline.TimelineTrackNoteBlocks) - { - const noteBlockId = Timeline.insertNoteBlock(state, time) - if (!noteBlockId) - return - - Timeline.modeStackPush(state) - state.mode = Timeline.Mode.NoteBlock - state.modeNoteBlockId = noteBlockId - Timeline.refreshTracks(state) - - Timeline.cursorSetTrackByParentId(state, noteBlockId) - - const chroma = key.chromaForDegree(degree) - Timeline.insertNote(state, time, chroma) - } - else - { - const chroma = key.chromaForDegree(degree) - Timeline.insertNote(state, time, chroma) - } -} - - -function handleLengthChange(state: Timeline.State, lengthIndex: number) -{ - const lengths = [ - new Rational(1, 16), - new Rational(1, 8), - new Rational(1, 4), - new Rational(1, 2), - new Rational(1, 1), - ] - - const length = lengths[lengthIndex] - - modifySelectedElems(state, (elem) => - { - if (elem.type == "note" || elem.type == "chord") - { - state.insertion.duration = length - - const newRange = Range.fromStartDuration(elem.range.start, length) - return Project.elemModify(elem, { range: newRange }) - } - else - return elem - }) - - const range = Timeline.selectionRange(state) || new Range(new Rational(0), new Rational(0)) - const newTime = range.end - state.cursor.visible = false - Timeline.cursorSetTime(state, newTime, newTime) - Timeline.scrollTimeIntoView(state, newTime) - - state.needsKeyFinish = true -} \ No newline at end of file diff --git a/src/timeline/state_keyUp.ts b/src/timeline/state_keyUp.ts deleted file mode 100644 index 7809b80..0000000 --- a/src/timeline/state_keyUp.ts +++ /dev/null @@ -1,7 +0,0 @@ -import * as Timeline from "./index" - - -export function keyUp(state: Timeline.State, key: string) -{ - state.keysDown.delete(key) -} \ No newline at end of file diff --git a/src/timeline/state_mouseDown.ts b/src/timeline/state_mouseDown.ts deleted file mode 100644 index 267d0d5..0000000 --- a/src/timeline/state_mouseDown.ts +++ /dev/null @@ -1,185 +0,0 @@ -import * as Timeline from "./index" -import * as Project from "../project" -import * as Prefs from "../prefs" -import * as Playback from "../playback" -import * as Windows from "../windows" -import Rational from "../util/rational" -import Rect from "../util/rect" - - -export function mouseDown(state: Timeline.State, rightButton: boolean) -{ - if (state.mouse.down) - return - - const prevDownDate = state.mouse.downDate - - state.mouse.down = true - state.mouse.downDate = new Date() - state.mouse.action = Timeline.MouseAction.None - - const selectMultiple = state.keysDown.has(Prefs.global.editor.keySelectMultiple) - const selectRange = state.keysDown.has(Prefs.global.editor.keySelectRange) - const selectClone = state.keysDown.has(Prefs.global.editor.keySelectClone) - const selectRect = state.keysDown.has(Prefs.global.editor.keySelectRect) - const forcePan = state.keysDown.has(Prefs.global.editor.keyPan) - const doubleClick = - state.mouse.downDate.getTime() - prevDownDate.getTime() < - Prefs.global.editor.mouseDoubleClickThresholdMs - - state.drag = - { - origin: - { - point: { ...state.mouse.point }, - range: null, - timeScroll: state.timeScroll, - trackScroll: state.trackScroll, - trackYScroll: state.tracks[state.mouse.point.trackIndex].yScroll, - project: Project.global.project, - }, - - xLocked: true, - yLocked: true, - posDelta: { x: 0, y: 0 }, - timeDelta: new Rational(0), - rowDelta: 0, - trackDelta: 0, - trackInsertionBefore: -1, - - elemId: -1, - notePreviewLast: null, - } - - function withTrackAtMouse(fn: (track: Timeline.TimelineTrack) => T | null) - { - for (let t = 0; t < state.tracks.length; t++) - { - if (t == state.mouse.point.trackIndex) - return fn(state.tracks[t]) - } - - return null - } - - const trackAtMouseNoCursor = !!withTrackAtMouse(tr => tr.noCursor) - - if (rightButton || forcePan) - { - state.mouse.action = Timeline.MouseAction.Pan - } - else if (state.mouse.point.pos.x > state.trackHeaderW && - (state.keysDown.has(Prefs.global.editor.keyPencil) || trackAtMouseNoCursor)) - { - if (!trackAtMouseNoCursor) - { - Timeline.selectionClear(state) - state.cursor.visible = false - } - - state.mouse.action = Timeline.MouseAction.Pencil - withTrackAtMouse(tr => tr.pencilStart(state)) - } - else if (state.hover && !selectRect) - { - Timeline.keyHandlePendingFinish(state) - - const elem = Project.global.project.elems.get(state.hover.id) - state.drag.elemId = state.hover.id - - Timeline.selectionToggleHover(state, state.hover, selectMultiple) - state.cursor.visible = false - - if (elem && elem.type == "track") - { - if (!selectRange) - { - state.rangeSelectOriginTrackIndex = state.mouse.point.trackIndex - if (state.hoverControl != Timeline.TrackControl.None) - { - state.mouse.action = Timeline.MouseAction.DragTrackControl - } - } - else if (state.rangeSelectOriginTrackIndex >= 0) - { - selectTrackRange(state, - state.rangeSelectOriginTrackIndex, - state.mouse.point.trackIndex) - } - } - else - { - state.rangeSelectOriginTrackIndex = -1 - - withTrackAtMouse(tr => tr.click(state, state.hover!.id)) - - const range = Timeline.selectionRange(state) - if (range) - { - Timeline.cursorSetTime(state, range.start, range.start) - Timeline.cursorSetTrack(state, state.mouse.point.trackIndex, state.mouse.point.trackIndex) - Playback.setStartTime(range.start) - } - - if (doubleClick) - { - withTrackAtMouse(tr => tr.doubleClick(state, state.hover!.id)) - } - - if (selectClone) - { - state.mouse.action = Timeline.MouseAction.DragClone - } - } - } - else - { - Timeline.keyHandlePendingFinish(state) - - if (!selectMultiple) - Timeline.selectionClear(state) - - if (state.mouse.point.pos.x > state.trackHeaderW) - { - state.mouse.action = selectRect ? - Timeline.MouseAction.SelectRect : - Timeline.MouseAction.SelectCursor - - state.cursor.visible = !selectRect - Timeline.cursorSetTime(state, state.mouse.point.time, state.mouse.point.time) - - state.cursor.rectY1 = state.cursor.rectY2 = - state.mouse.point.trackPos.y - - state.cursor.trackIndex1 = state.cursor.trackIndex2 = - state.mouse.point.trackIndex - - if (doubleClick) - { - const anchor = Timeline.findPreviousAnchor( - state, state.mouse.point.time, - state.mouse.point.trackIndex, state.mouse.point.trackIndex) - - Timeline.cursorSetTime(state, anchor, anchor) - Timeline.scrollTimeIntoView(state, anchor) - } - - Playback.setStartTime(state.cursor.time1) - } - } -} - - -function selectTrackRange( - state: Timeline.State, - trackIndex1: number, - trackIndex2: number) -{ - const trackIndexMin = Math.min(trackIndex1, trackIndex2) - const trackIndexMax = Math.max(trackIndex1, trackIndex2) - - Timeline.selectionClear(state) - - for (var i = trackIndexMin; i <= trackIndexMax; i++) - Timeline.selectionAdd(state, state.tracks[i].projectTrackId) -} \ No newline at end of file diff --git a/src/timeline/state_mouseDrag.ts b/src/timeline/state_mouseDrag.ts deleted file mode 100644 index 82ea653..0000000 --- a/src/timeline/state_mouseDrag.ts +++ /dev/null @@ -1,388 +0,0 @@ -import * as Timeline from "./index" -import * as Project from "../project" -import * as Prefs from "../prefs" -import * as Playback from "../playback" -import Range from "../util/range" - - -export function mouseDrag( - state: Timeline.State, - pos: { x: number, y: number }, - isClick: boolean): boolean -{ - if (!state.mouse.down) - return false - - state.mouse.pointPrev = state.mouse.point - state.mouse.point = Timeline.pointAt(state, pos) - - state.drag.posDelta = - { - x: state.mouse.point.pos.x - state.drag.origin.point.pos.x, - y: state.mouse.point.pos.y - state.drag.origin.point.pos.y, - } - - state.drag.timeDelta = - state.mouse.point.time.subtract(state.drag.origin.point.time) - - state.drag.rowDelta = - state.mouse.point.row - state.drag.origin.point.row - - state.drag.trackDelta = - state.mouse.point.trackIndex - state.drag.origin.point.trackIndex - - state.drag.xLocked = - state.drag.xLocked && - Math.abs(state.drag.posDelta.x) < Prefs.global.editor.mouseDragXLockedDistance - - state.drag.yLocked = - state.drag.yLocked && - Math.abs(state.drag.posDelta.y) < Prefs.global.editor.mouseDragYLockedDistance - - if (!state.drag.yLocked) - { - state.drag.trackInsertionBefore = - Timeline.trackInsertionAtY(state, state.mouse.point.pos.y) - } - - function withTrackAtDragOrigin(fn: (track: Timeline.TimelineTrack) => T) - { - return fn(state.tracks[state.drag.origin.point.trackIndex]) - } - - if (isClick && state.mouse.action != Timeline.MouseAction.DragTrackControl) - { - return false - } - - if (state.mouse.action == Timeline.MouseAction.Pan) - { - state.timeScroll = - state.drag.origin.timeScroll - - (state.drag.posDelta.x / state.timeScale) - - if (!state.trackScrollLocked) - state.trackScroll = - state.drag.origin.trackScroll - - state.drag.posDelta.y - - withTrackAtDragOrigin(tr => - { - if (tr.scrollEnabled) - tr.yScroll = - state.drag.origin.trackYScroll - - state.drag.posDelta.y - }) - - return true - } - else if (state.mouse.action == Timeline.MouseAction.SelectCursor) - { - state.cursor.time2 = state.mouse.point.time - state.cursor.trackIndex2 = state.mouse.point.trackIndex - - Timeline.selectionClear(state) - Timeline.selectionAddAtCursor(state) - return true - } - else if (state.mouse.action == Timeline.MouseAction.SelectRect) - { - state.cursor.time2 = state.mouse.point.time - state.cursor.rectY2 = state.mouse.point.trackPos.y - - const y1 = Math.min(state.cursor.rectY1, state.cursor.rectY2) - const y2 = Math.max(state.cursor.rectY1, state.cursor.rectY2) - - Timeline.selectionClear(state) - Timeline.selectionAddAtCursor(state, { y1, y2 }) - return true - } - else if (state.mouse.action == Timeline.MouseAction.DragTrackHeader) - { - return true - } - else if (state.mouse.action == Timeline.MouseAction.Pencil) - { - withTrackAtDragOrigin(tr => tr.pencilDrag(state)) - return true - } - else if (state.mouse.action == Timeline.MouseAction.DragTrackControl) - { - handleDragTrackControl(state) - return true - } - else if (state.mouse.action == Timeline.MouseAction.DragClone) - { - if (!state.drag.xLocked || !state.drag.yLocked) - { - handleDragClone(state) - return true - } - - return false - } - else - { - let mouseAction = state.mouse.action - - if (state.drag.xLocked) - { - if (mouseAction == Timeline.MouseAction.DragTime || - mouseAction == Timeline.MouseAction.StretchTimeStart || - mouseAction == Timeline.MouseAction.StretchTimeEnd) - mouseAction = Timeline.MouseAction.None - else if (mouseAction == Timeline.MouseAction.DragTimeAndRow) - mouseAction = Timeline.MouseAction.DragRow - } - - if (state.drag.yLocked) - { - if (mouseAction == Timeline.MouseAction.DragTimeAndRow) - mouseAction = Timeline.MouseAction.DragTime - else if (mouseAction == Timeline.MouseAction.DragRow) - mouseAction = Timeline.MouseAction.None - } - - const origProject = state.drag.origin.project - let newProject = state.drag.origin.project - - for (const id of state.selection) - { - const elem = origProject.elems.get(id) - if (!elem) - continue - - if (elem.type == "track") - continue - - let changes: any = {} - - if (mouseAction == Timeline.MouseAction.DragTime || - mouseAction == Timeline.MouseAction.DragTimeAndRow) - changes.range = elem.range - .displace(state.drag.timeDelta) - .quantize(Project.MAX_RATIONAL_DENOMINATOR) - - if (mouseAction == Timeline.MouseAction.StretchTimeStart && - state.drag.origin.range) - { - changes.range = Project.getAbsoluteRange(origProject, elem.parentId, elem.range) - changes.range = changes.range.stretch( - state.drag.timeDelta, - state.drag.origin.range.end, - state.drag.origin.range.start) - - if (elem.range.start.compare(state.drag.origin.range.start) == 0) - changes.range = new Range( - changes.range.start.snap(state.timeSnap), - changes.range.end) - .quantize(Project.MAX_RATIONAL_DENOMINATOR) - - changes.range = changes.range.sorted() - changes.range = Project.getRelativeRange(origProject, elem.parentId, changes.range) - } - - if (mouseAction == Timeline.MouseAction.StretchTimeEnd && - state.drag.origin.range) - { - changes.range = Project.getAbsoluteRange(origProject, elem.parentId, elem.range) - changes.range = changes.range.stretch( - state.drag.timeDelta, - state.drag.origin.range.start, - state.drag.origin.range.end) - - if (elem.range.end.compare(state.drag.origin.range.end) == 0) - changes.range = new Range( - changes.range.start, - changes.range.end.snap(state.timeSnap)) - .quantize(Project.MAX_RATIONAL_DENOMINATOR) - - changes.range = changes.range.sorted() - changes.range = Project.getRelativeRange(origProject, elem.parentId, changes.range) - } - - if ((mouseAction == Timeline.MouseAction.DragRow || - mouseAction == Timeline.MouseAction.DragTimeAndRow) && - elem.type == "note") - { - const note = elem as Project.Note - const trackId = state.tracks[state.drag.origin.point.trackIndex].projectTrackId - const key = Project.keyAt(Project.global.project, trackId, note.range.start) - const degree = key.octavedDegreeForMidi(note.midiPitch) - const newPitch = key.midiForDegree(Math.floor(degree + state.drag.rowDelta)) - changes.midiPitch = newPitch - - if (elem.id == state.drag.elemId) - { - if ((state.drag.notePreviewLast && newPitch != state.drag.notePreviewLast) || - (!state.drag.notePreviewLast && newPitch != note.midiPitch)) - { - state.drag.notePreviewLast = newPitch - Playback.playNotePreview(trackId, newPitch, note.volumeDb, note.velocity) - } - } - } - - if ((mouseAction == Timeline.MouseAction.DragTime || - mouseAction == Timeline.MouseAction.DragTimeAndRow || - mouseAction == Timeline.MouseAction.DragRow) && - state.drag.trackDelta != 0) - { - const origTrackIndex = state.tracks.findIndex(t => t.projectTrackId == elem.parentId) - const newTrackIndex = Math.max(0, Math.min(state.tracks.length - 1, origTrackIndex + state.drag.trackDelta)) - const newTrack = state.tracks[newTrackIndex] - - if (newTrack.acceptedElemTypes.has(elem.type)) - changes.parentId = newTrack.projectTrackId - } - - newProject = Project.upsertElement( - newProject, - Project.elemModify(elem, changes)) - } - - Project.global.project = newProject - return true - } -} - - -function handleDragTrackControl(state: Timeline.State) -{ - const origTrack = Project.getElem(state.drag.origin.project, state.drag.elemId, "track") - if (!origTrack) - return - - - const modifySelectedTracks = (func: (tr: Project.Track) => Project.Track) => - { - let project = Project.global.project - - for (const elemId of state.selection) - { - const track = Project.getElem(state.drag.origin.project, elemId, "track") - if (!track) - continue - - const newTrack = func(track) - project = Project.upsertTrack(project, newTrack) - } - - Project.global.project = project - } - - - const modifyTrackAtMouse = (func: (tr: Project.Track) => Project.Track) => - { - let project = Project.global.project - - const trackIndex = state.mouse.point.trackIndex - if (trackIndex < 0 || trackIndex >= state.tracks.length) - return - - const track = state.tracks[trackIndex] - const projTrack = Project.getElem(state.drag.origin.project, track.projectTrackId, "track") - if (!projTrack) - return - - const newTrack = func(projTrack) - project = Project.upsertTrack(project, newTrack) - - Project.global.project = project - } - - - const modifyTrackAtMouseOrSelected = (func: (tr: Project.Track) => Project.Track) => - { - let numSelected = 0 - for (const elemId of state.selection) - { - const track = Project.getElem(state.drag.origin.project, elemId, "track") - if (track) - numSelected++ - } - - if (numSelected <= 1) - modifyTrackAtMouse(func) - else - modifySelectedTracks(func) - } - - - switch (state.hoverControl) - { - case Timeline.TrackControl.Volume: - { - const volumeDbDelta = -state.drag.posDelta.y / 10 - modifySelectedTracks((track) => - { - if (track.trackType != "notes" && track.trackType != "chords") - return track - - const volumeDb = Math.max(Project.MinVolumeDb, - Math.min(Project.MaxVolumeDb, - track.volumeDb + volumeDbDelta)) - - return Project.elemModify(track, { volumeDb }) - }) - break - } - - case Timeline.TrackControl.Mute: - { - const mute = origTrack.trackType == "notes" || origTrack.trackType == "chords" ? - !origTrack.mute : false - - modifyTrackAtMouseOrSelected((track) => - { - return Project.elemModify(track, { mute }) - }) - break - } - - case Timeline.TrackControl.Solo: - { - const solo = origTrack.trackType == "notes" || origTrack.trackType == "chords" ? - !origTrack.solo : false - - modifyTrackAtMouseOrSelected((track) => - { - return Project.elemModify(track, { solo }) - }) - break - } - } -} - - -function handleDragClone(state: Timeline.State) -{ - const origProject = Project.global.project - - let newProject = Project.global.project - const newIds = [] - - for (const elemId of state.selection) - { - const elem = origProject.elems.get(elemId) - if (!elem || elem.type == "track") - continue - - const newId = newProject.nextId - newIds.push(newId) - - newProject = Project.cloneElem(origProject, elem, newProject) - - if (elemId == state.drag.elemId) - state.drag.elemId = newId - } - - Timeline.selectionClear(state) - for (const id of newIds) - Timeline.selectionAdd(state, id) - - state.mouse.action = Timeline.MouseAction.DragTimeAndRow - - Project.global.project = newProject - state.drag.origin.project = newProject -} \ No newline at end of file diff --git a/src/timeline/state_mouseMove.ts b/src/timeline/state_mouseMove.ts deleted file mode 100644 index ff03f4d..0000000 --- a/src/timeline/state_mouseMove.ts +++ /dev/null @@ -1,68 +0,0 @@ -import * as Timeline from "./index" -import * as Prefs from "../prefs" -import Range from "../util/range" -import Rational from "../util/rational" - - -export function mouseMove(state: Timeline.State, pos: { x: number, y: number }): boolean -{ - if (state.mouse.down) - return false - - state.mouse.pointPrev = state.mouse.point - state.mouse.point = Timeline.pointAt(state, pos) - - const hoverPrev = state.hover - state.hover = null - - for (let t = 0; t < state.tracks.length; t++) - state.tracks[t].pencilClear(state) - - if (state.mouse.point.pos.x < state.trackHeaderW) - { - const trackIndex = Timeline.trackAtY(state, state.mouse.point.pos.y) - if (trackIndex !== null) - { - const track = state.tracks[trackIndex] - state.hover = - { - action: Timeline.MouseAction.DragTrackHeader, - id: track.projectTrackId, - range: new Range(new Rational(0), new Rational(0)), - } - } - - state.hoverControl = Timeline.trackControlAtPoint( - state, - state.mouse.point.trackIndex, - state.mouse.point.trackPos) - } - else if (state.keysDown.has(Prefs.global.editor.keyPencil)) - { - for (let t = 0; t < state.tracks.length; t++) - { - if (t == state.mouse.point.trackIndex) - state.tracks[t].pencilHover(state) - } - return true - } - else - { - for (let t = 0; t < state.tracks.length; t++) - { - if (t == state.mouse.point.trackIndex) - state.tracks[t].hover(state) - } - } - - if (state.hover && hoverPrev) - { - if (state.hover.id != hoverPrev.id || - state.hover.action != hoverPrev.action) - return true - } - else if ((!state.hover && hoverPrev) || (state.hover && !hoverPrev)) - return true - - return false -} \ No newline at end of file diff --git a/src/timeline/state_mouseUp.tsx b/src/timeline/state_mouseUp.tsx deleted file mode 100644 index ba8d92e..0000000 --- a/src/timeline/state_mouseUp.tsx +++ /dev/null @@ -1,161 +0,0 @@ -import React from "react" -import * as Timeline from "./index" -import * as Project from "../project" -import * as Command from "../command" -import * as Dockable from "../dockable" -import * as Popup from "../popup" -import * as Windows from "../windows" -import Rect from "../util/rect" - - -export function mouseUp(state: Timeline.State) -{ - if (!state.mouse.down) - return - - state.mouse.down = false - - if (state.mouse.action == Timeline.MouseAction.DragTrackHeader) - { - handleTrackDragRelease(state) - } - else if (state.mouse.action == Timeline.MouseAction.Pencil) - { - for (let t = 0; t < state.tracks.length; t++) - state.tracks[t].pencilComplete(state) - } - else if (state.mouse.action == Timeline.MouseAction.Pan) - { - handleContextMenu(state) - } - - Timeline.selectionRemoveConflictingBehind(state) - - Project.global.project = Project.withRefreshedRange(Project.global.project) - Project.global.project = Project.global.project - Project.splitUndoPoint() - Project.addUndoPoint("mouseUp") -} - - -function handleTrackDragRelease(state: Timeline.State) -{ - if (state.drag.trackInsertionBefore < 0) - return - - if (state.drag.trackInsertionBefore < state.tracks.length && - state.selection.has(state.tracks[state.drag.trackInsertionBefore].projectTrackId)) - return - - let project = Project.global.project - - const selectedProjectTracks: Project.Track[] = [] - for (const track of state.tracks) - { - if (state.selection.has(track.projectTrackId) && - !selectedProjectTracks.find(tr => tr.id == track.projectTrackId)) - { - const projTrack = project.tracks.find(tr => tr.id == track.projectTrackId) - if (projTrack) - selectedProjectTracks.push(projTrack) - } - } - - for (const track of selectedProjectTracks) - project = Project.upsertTrack(project, track, true) - - let beforeProjectTrackIndex = project.tracks.length - if (state.drag.trackInsertionBefore < state.tracks.length) - { - const trackId = state.tracks[state.drag.trackInsertionBefore].projectTrackId - beforeProjectTrackIndex = project.tracks.findIndex(tr => tr.id == trackId) - } - - for (const track of selectedProjectTracks.reverse()) - project = Project.upsertTrack(project, track, false, beforeProjectTrackIndex) - - Project.global.project = project - Project.global.project = project -} - - -function handleContextMenu(state: Timeline.State) -{ - if (!state.hover) - return - - if (!state.drag.xLocked || !state.drag.yLocked) - return - - Timeline.selectionToggleHover(state, state.hover, false) - - if (state.hover) - { - const clickDurationMs = (new Date().getTime()) - state.mouse.downDate.getTime() - - const fnOpenProperties = () => - { - Dockable.createFloatingEphemeral( - Windows.Inspector, - { elemIds: [...state.selection] }, - 1, 1) - } - - if (clickDurationMs < 150) - { - fnOpenProperties() - } - else - { - Popup.showAtMouse(() => - { - return - { makeContextMenu(state, fnOpenProperties) } - - }) - } - } -} - - -function makeContextMenu(state: Timeline.State, fnOpenProperties: () => void): JSX.Element[] -{ - const menuItems: JSX.Element[] = [] - - menuItems.push() - - menuItems.push( - { - Timeline.deleteElems(state, state.selection) - Timeline.sendEventRefresh() - Project.splitUndoPoint() - Project.addUndoPoint("menuDelete") - }}/>) - - - const hasElemType = (type: Project.Element["type"]) => - { - for (const id of state.selection) - { - const elem = Project.global.project.elems.get(id) - if (elem && elem.type === type) - return true - } - - return false - } - - - if (hasElemType("note") || hasElemType("noteBlock")) - { - menuItems.push() - menuItems.push() - } - - - return menuItems -} \ No newline at end of file diff --git a/src/timeline/state_mouseWheel.ts b/src/timeline/state_mouseWheel.ts deleted file mode 100644 index 14d7e70..0000000 --- a/src/timeline/state_mouseWheel.ts +++ /dev/null @@ -1,46 +0,0 @@ -import * as Timeline from "./index" -import Rational from "../util/rational" - - -export function mouseWheel(state: Timeline.State, deltaX: number, deltaY: number) -{ - if (Math.abs(deltaX) > 0) - { - state.timeScroll = state.timeScroll + 0.01 / (state.timeScale / 100) * deltaX - state.mouse.wheelDate = new Date() - } - else if (new Date().getTime() - state.mouse.wheelDate.getTime() > 250) - { - const snap = new Rational(1, 1024) - const prevMouseTime = Timeline.timeAtX( - state, - state.mouse.point.pos.x, - snap) - - let newTimeScale = state.timeScale * (deltaY > 0 ? 0.8 : 1.25) - newTimeScale = Math.max(4, Math.min(2048, newTimeScale)) - state.timeScale = newTimeScale - - const newMouseTime = Timeline.timeAtX( - state, - state.mouse.point.pos.x, - snap) - - const newTimeScroll = state.timeScroll - newMouseTime.subtract(prevMouseTime).asFloat() - - const timeSnapAdjustThresholdUpper = 24 - const timeSnapAdjustThresholdLower = 8 - let newTimeSnap = state.timeSnapBase - - if (newTimeSnap.asFloat() * newTimeScale > timeSnapAdjustThresholdUpper) - while (newTimeSnap.asFloat() * newTimeScale > timeSnapAdjustThresholdUpper) - newTimeSnap = newTimeSnap.divide(new Rational(2)) - - else if (newTimeSnap.asFloat() * newTimeScale < timeSnapAdjustThresholdLower) - while (newTimeSnap.asFloat() * newTimeScale < timeSnapAdjustThresholdLower) - newTimeSnap = newTimeSnap.divide(new Rational(1, 2)) - - state.timeScroll = newTimeScroll - state.timeSnap = newTimeSnap - } -} \ No newline at end of file diff --git a/src/timeline/state_render.ts b/src/timeline/state_render.ts deleted file mode 100644 index a69b750..0000000 --- a/src/timeline/state_render.ts +++ /dev/null @@ -1,616 +0,0 @@ -import * as Timeline from "./index" -import * as Project from "../project" -import * as Prefs from "../prefs" -import * as Playback from "../playback" -import * as Theory from "../theory" -import Rational from "../util/rational" -import Range from "../util/range" -import Rect from "../util/rect" - - -export function render(state: Timeline.State, canvas: CanvasRenderingContext2D) -{ - canvas.save() - - canvas.fillStyle = Prefs.global.editor.bkgColor - canvas.fillRect(0, 0, state.renderRect.w, state.renderRect.h) - - canvas.save() - canvas.beginPath() - canvas.rect( - state.trackHeaderW, - 0, - state.renderRect.w - state.trackHeaderW, - state.renderRect.h) - canvas.clip() - - renderBackground(state, canvas) - renderCursorHighlight(state, canvas) - - canvas.restore() - - // clip inverse of top measure track - canvas.save() - canvas.beginPath() - canvas.rect( - 0, state.trackMeasuresH, - state.renderRect.w, - state.renderRect.h) - canvas.clip() - - let y = -state.trackScroll + state.tracks[0].renderRect.y - for (let t = 0; t < state.tracks.length; t++) - { - const y2 = y + state.tracks[t].renderRect.h - - if (y >= state.renderRect.h || - y + state.tracks[t].renderRect.h <= 0) - { - y = y2 - continue - } - - canvas.save() - canvas.translate(0, y) - - const hover = !!state.hover && state.hover.id == state.tracks[t].projectTrackId - const selected = state.selection.has(state.tracks[t].projectTrackId) - - canvas.fillStyle = "#000" - canvas.strokeStyle = "transparent" - if (selected) - { - canvas.fillStyle = "#222" - canvas.strokeStyle = "#fff" - } - else if (hover) - { - canvas.fillStyle = "#000" - canvas.strokeStyle = "#888" - } - - canvas.beginPath() - canvas.rect( - 0.5, 1.5, - state.trackHeaderW - 1, - state.tracks[t].renderRect.h - 2) - canvas.fill() - canvas.stroke() - - renderTrackHeader(state, canvas, t) - - const clipY1 = 1 + Math.max(0, state.trackMeasuresH - y) - const clipY2 = state.tracks[t].renderRect.h - - canvas.beginPath() - canvas.rect( - state.trackHeaderW, - clipY1, - state.renderRect.w - state.trackHeaderW, - clipY2 - clipY1) - canvas.clip() - - //canvas.translate(0, -state.tracks[t].yScroll) - - renderRectSelectHighlight(state, canvas, t) - state.tracks[t].render(state, canvas) - renderRectSelectContour(state, canvas, t) - - canvas.restore() - - canvas.strokeStyle = Prefs.global.editor.trackHBorderColor - canvas.beginPath() - canvas.moveTo(0, y + 0.5) - canvas.lineTo(state.renderRect.w, y + 0.5) - canvas.moveTo(0, y2 + 0.5) - canvas.lineTo(state.renderRect.w, y2 + 0.5) - canvas.stroke() - - y = y2 - } - - renderMeasuresTrack(state, canvas) - - if (state.mouse.down && - state.mouse.action == Timeline.MouseAction.DragTrackHeader && - state.drag.trackInsertionBefore >= 0) - { - canvas.save() - - const y = - -state.trackScroll + - (state.tracks.length == 0 ? 0 : - state.drag.trackInsertionBefore >= state.tracks.length ? - state.tracks[state.tracks.length - 1].renderRect.y2 : - state.tracks[state.drag.trackInsertionBefore].renderRect.y) - - canvas.strokeStyle = Prefs.global.editor.selectionCursorColor - canvas.lineWidth = 3 - canvas.beginPath() - canvas.moveTo(0, y + 0.5) - canvas.lineTo(state.renderRect.w, y + 0.5) - canvas.stroke() - - canvas.restore() - } - - canvas.save() - - canvas.beginPath() - canvas.rect( - state.trackHeaderW, - 0, - state.renderRect.w - state.trackHeaderW, - state.renderRect.h) - canvas.clip() - - if (state.cursor.visible) - { - const timeMin = state.cursor.time1.min(state.cursor.time2)! - const timeMax = state.cursor.time1.max(state.cursor.time2)! - renderCursorBeam(state, canvas, timeMin, false) - renderCursorBeam(state, canvas, timeMax, true) - } - - if (Playback.global.playing) - renderPlaybackBeam(state, canvas, Playback.global.playTime) - - canvas.restore() - - canvas.strokeStyle = Prefs.global.editor.trackVBorderColor - canvas.beginPath() - canvas.moveTo(state.trackHeaderW + 0.5, 0) - canvas.lineTo(state.trackHeaderW + 0.5, state.renderRect.h) - canvas.stroke() - - canvas.restore() - canvas.restore() -} - - -function renderTracksByFn( - state: Timeline.State, - canvas: CanvasRenderingContext2D, - fn: (track: Timeline.TimelineTrack) => void) -{ - let y = -state.trackScroll + state.tracks[0].renderRect.y - for (let t = 0; t < state.tracks.length; t++) - { - const y2 = y + state.tracks[t].renderRect.h - - if (y >= state.renderRect.h || - y + state.tracks[t].renderRect.h <= 0) - { - y = y2 - continue - } - - canvas.save() - canvas.translate(0, y) - - const clipY1 = 1 + Math.max(0, state.trackMeasuresH - y) - const clipY2 = state.tracks[t].renderRect.h - - canvas.beginPath() - canvas.rect( - state.trackHeaderW, - clipY1, - state.renderRect.w - state.trackHeaderW, - clipY2 - clipY1) - canvas.clip() - - fn(state.tracks[t]) - - canvas.restore() - - y = y2 - } -} - - -function renderBackground(state: Timeline.State, canvas: CanvasRenderingContext2D) -{ - const visibleRange = Timeline.visibleTimeRange(state) - - // Render alternating measure background and sub-measure dividers. - for (const measure of Project.iterMeasuresAtRange(Project.global.project, visibleRange)) - { - const x1 = 0.5 + Math.floor(Timeline.xAtTime(state, measure.time1)) - const x2 = 0.5 + Math.floor(Timeline.xAtTime(state, measure.time2)) - - if (measure.num % 2 != 0) - { - canvas.fillStyle = Prefs.global.editor.bkgAlternateMeasureColor - canvas.fillRect(x1, 0, x2 - x1, state.renderRect.h) - } - - const submeasureSize = - Timeline.xAtTime(state, new Rational(1, measure.meterCh.meter.denominator)) - - Timeline.xAtTime(state, new Rational(0)) - - if (submeasureSize > 8) - { - canvas.strokeStyle = measure.num % 2 != 0 ? - Prefs.global.editor.bkgColor : - Prefs.global.editor.bkgAlternateMeasureColor - - for (let n = 1; n < measure.meterCh.meter.numerator; n++) - { - const submeasureX = x1 + Math.round(submeasureSize * n) - if (submeasureX >= x2) - break - - canvas.beginPath() - canvas.moveTo(submeasureX, 0) - canvas.lineTo(submeasureX, state.renderRect.h) - canvas.stroke() - } - } - } - - // Render track-specific background, such as note row dividers. - renderTracksByFn(state, canvas, (tr) => tr.renderBackground(state, canvas)) - - // Render dark overlay for areas beyond current active range. - let activeRange: Range | null = null - for (const track of state.tracks) - activeRange = Range.intersect(activeRange, track.getActiveRange(state)) - - if (!activeRange) - activeRange = Project.global.project.range - - const visibleX1 = Timeline.xAtTime(state, visibleRange.start) - const visibleX2 = Timeline.xAtTime(state, visibleRange.end) - const activeX1 = Timeline.xAtTime(state, activeRange.start) - const activeX2 = Timeline.xAtTime(state, activeRange.end) - - canvas.fillStyle = Prefs.global.editor.bkgInactiveOverlayColor - canvas.fillRect(visibleX1, 0, activeX1 - visibleX1, state.renderRect.h) - canvas.fillRect(activeX2, 0, visibleX2 - activeX2, state.renderRect.h) - - // Render numbering for measures at the topmost track. - let suppressLabelUntilX = 0 - for (const measure of Project.iterMeasuresAtRange(Project.global.project, visibleRange)) - { - const x1 = 0.5 + Math.floor(Timeline.xAtTime(state, measure.time1)) - const x2 = 0.5 + Math.floor(Timeline.xAtTime(state, measure.time2)) - - if (x1 > suppressLabelUntilX) - { - canvas.fillStyle = Prefs.global.editor.measureLabelColor - canvas.font = Math.floor(state.trackMeasuresH - 6) + "px system-ui" - canvas.textAlign = "left" - canvas.textBaseline = "middle" - canvas.fillText(measure.num.toString(), x1 + 5, state.trackMeasuresH / 2 + 1.5) - - suppressLabelUntilX = x1 + 40 - } - } - - // Render meter change dividers. - const meterChangeTrackId = Project.meterChangeTrackId(Project.global.project) - const meterChangeList = Project.global.project.lists.get(meterChangeTrackId) - if (meterChangeList) - { - canvas.strokeStyle = Prefs.global.editor.meterChangeColor - canvas.lineCap = "square" - canvas.lineWidth = 1 - - for (const keyCh of meterChangeList.iterAtRange(visibleRange)) - { - const keyChX = 0.5 + Math.floor(Timeline.xAtTime(state, keyCh.range.start)) - - canvas.beginPath() - canvas.moveTo(keyChX, 0) - canvas.lineTo(keyChX, state.renderRect.h) - canvas.stroke() - } - } - - // Render key change dividers. - const keyChangeTrackId = Project.keyChangeTrackId(Project.global.project) - const keyChangeList = Project.global.project.lists.get(keyChangeTrackId) - if (keyChangeList) - { - canvas.strokeStyle = Prefs.global.editor.keyChangeColor - canvas.lineCap = "square" - canvas.lineWidth = 1 - - for (const keyCh of keyChangeList.iterAtRange(visibleRange)) - { - const keyChX = 0.5 + Math.floor(Timeline.xAtTime(state, keyCh.range.start)) - - canvas.beginPath() - canvas.moveTo(keyChX, 0) - canvas.lineTo(keyChX, state.renderRect.h) - canvas.stroke() - } - } -} - - -function renderMeasuresTrack(state: Timeline.State, canvas: CanvasRenderingContext2D) -{ - const y1 = 0 - const y2 = state.trackMeasuresH - - canvas.save() - - canvas.fillStyle = Prefs.global.editor.bkgColor - - canvas.beginPath() - canvas.rect( - 0.5, 1.5, - state.trackHeaderW - 1, - state.trackMeasuresH - 2) - canvas.fill() - - canvas.beginPath() - canvas.rect( - state.trackHeaderW, - 1, - state.renderRect.w - state.trackHeaderW, - state.trackMeasuresH) - canvas.clip() - - canvas.restore() - - canvas.strokeStyle = Prefs.global.editor.trackHBorderColor - canvas.beginPath() - canvas.moveTo(0, y1 + 0.5) - canvas.lineTo(state.renderRect.w, y1 + 0.5) - canvas.moveTo(0, y2 + 0.5) - canvas.lineTo(state.renderRect.w, y2 + 0.5) - canvas.stroke() -} - - -function renderCursorHighlight(state: Timeline.State, canvas: CanvasRenderingContext2D) -{ - if (!state.cursor.visible) - return - - const timeMin = state.cursor.time1.min(state.cursor.time2)! - const timeMax = state.cursor.time1.max(state.cursor.time2)! - const trackMin = Math.min(state.cursor.trackIndex1, state.cursor.trackIndex2) - const trackMax = Math.max(state.cursor.trackIndex1, state.cursor.trackIndex2) - - if (trackMin < 0 || trackMax < 0 || - trackMin >= state.tracks.length || - trackMax >= state.tracks.length) - return - - const y1 = 0.5 + Math.floor(Timeline.trackY(state, trackMin)) - const y2 = 0.5 + Math.floor(Timeline.trackY(state, trackMax) + state.tracks[trackMax].renderRect.h) - - const x1 = Timeline.xAtTime(state, timeMin) - const x2 = Timeline.xAtTime(state, timeMax) - - canvas.fillStyle = Prefs.global.editor.selectionBkgColor - canvas.fillRect(x1, y1, x2 - x1, y2 - y1) -} - - -function renderRectSelectHighlight( - state: Timeline.State, - canvas: CanvasRenderingContext2D, - trackIndex: number) -{ - if (state.mouse.action !== Timeline.MouseAction.SelectRect || - state.cursor.trackIndex1 !== trackIndex) - return - - const timeMin = state.cursor.time1.min(state.cursor.time2)! - const timeMax = state.cursor.time1.max(state.cursor.time2)! - const trackMin = Math.min(state.cursor.trackIndex1, state.cursor.trackIndex2) - const trackMax = Math.max(state.cursor.trackIndex1, state.cursor.trackIndex2) - const y1 = 0.5 + Math.floor(Math.min(state.cursor.rectY1, state.cursor.rectY2)) - const y2 = 0.5 + Math.floor(Math.max(state.cursor.rectY1, state.cursor.rectY2)) - - if (trackMin < 0 || trackMax < 0 || - trackMin >= state.tracks.length || - trackMax >= state.tracks.length) - return - - const x1 = 0.5 + Math.floor(Timeline.xAtTime(state, timeMin)) - const x2 = 0.5 + Math.floor(Timeline.xAtTime(state, timeMax)) - - canvas.fillStyle = Prefs.global.editor.selectionBkgColor - canvas.fillRect(x1, y1, x2 - x1, y2 - y1) -} - - -function renderCursorBeam( - state: Timeline.State, - canvas: CanvasRenderingContext2D, - time: Rational, - tipOffsetSide: boolean) -{ - const trackMin = Math.min(state.cursor.trackIndex1, state.cursor.trackIndex2) - const trackMax = Math.max(state.cursor.trackIndex1, state.cursor.trackIndex2) - - if (trackMin < 0 || trackMax < 0 || - trackMin >= state.tracks.length || - trackMax >= state.tracks.length) - return - - const x = 0.5 + Math.floor(Timeline.xAtTime(state, time)) - - canvas.strokeStyle = Prefs.global.editor.selectionCursorColor - canvas.fillStyle = Prefs.global.editor.selectionCursorColor - canvas.lineCap = "square" - canvas.lineWidth = 1 - - const headYSize = 7 - const headXSize = headYSize * (tipOffsetSide ? -1 : 1) - - const y1 = 0.5 + Math.floor(Timeline.trackY(state, trackMin)) - const y2 = 0.5 + Math.floor(Timeline.trackY(state, trackMax) + state.tracks[trackMax].renderRect.h) - - canvas.beginPath() - canvas.moveTo(x, y1 + headYSize) - canvas.lineTo(x + headXSize, y1) - canvas.lineTo(x, y1) - canvas.fill() - - canvas.beginPath() - canvas.moveTo(x, y2 - headYSize) - canvas.lineTo(x + headXSize, y2) - canvas.lineTo(x, y2) - canvas.fill() - - canvas.beginPath() - canvas.moveTo(x, y1) - canvas.lineTo(x, y2) - canvas.stroke() -} - - -function renderRectSelectContour( - state: Timeline.State, - canvas: CanvasRenderingContext2D, - trackIndex: number) -{ - if (state.mouse.action !== Timeline.MouseAction.SelectRect || - state.cursor.trackIndex1 !== trackIndex) - return - - const timeMin = state.cursor.time1.min(state.cursor.time2)! - const timeMax = state.cursor.time1.max(state.cursor.time2)! - const trackMin = Math.min(state.cursor.trackIndex1, state.cursor.trackIndex2) - const trackMax = Math.max(state.cursor.trackIndex1, state.cursor.trackIndex2) - const y1 = 0.5 + Math.floor(Math.min(state.cursor.rectY1, state.cursor.rectY2)) - const y2 = 0.5 + Math.floor(Math.max(state.cursor.rectY1, state.cursor.rectY2)) - - if (trackMin < 0 || trackMax < 0 || - trackMin >= state.tracks.length || - trackMax >= state.tracks.length) - return - - const x1 = 0.5 + Math.floor(Timeline.xAtTime(state, timeMin)) - const x2 = 0.5 + Math.floor(Timeline.xAtTime(state, timeMax)) - - canvas.strokeStyle = Prefs.global.editor.selectionCursorColor - canvas.strokeRect(x1, y1, x2 - x1, y2 - y1) -} - - -function renderPlaybackBeam( - state: Timeline.State, - canvas: CanvasRenderingContext2D, - time: Rational) -{ - const x = 0.5 + Math.floor(Timeline.xAtTime(state, time)) - - canvas.strokeStyle = Prefs.global.editor.playbackCursorColor - canvas.lineCap = "square" - canvas.lineWidth = 1 - - canvas.beginPath() - canvas.lineTo(x, 0) - canvas.lineTo(x, state.renderRect.h) - canvas.stroke() -} - - -function renderTrackHeader( - state: Timeline.State, - canvas: CanvasRenderingContext2D, - trackIndex: number) -{ - const track = state.tracks[trackIndex] - const projTrack = Project.getElem(Project.global.project, track.projectTrackId, "track") - if (!projTrack) - return - - - if ((track instanceof Timeline.TimelineTrackNoteBlocks || - track instanceof Timeline.TimelineTrackNotes || - track instanceof Timeline.TimelineTrackChords) && - (projTrack.trackType == "notes" || - projTrack.trackType == "chords")) - { - canvas.fillStyle = "#fff" - canvas.textAlign = "left" - canvas.textBaseline = "top" - canvas.font = "10px system-ui" - - const displayName = Project.trackDisplayName(projTrack) - canvas.fillText(displayName, 10, 8, state.trackHeaderW - 20) - - renderControlDial(state, canvas, 0, - (v) => (v >= 0 ? "+" : "") + v.toFixed(1) + " dB", - projTrack.volumeDb, Project.MinVolumeDb, Project.MaxVolumeDb) - - renderControlIcon(state, canvas, 7, "🔇", projTrack.mute) - renderControlIcon(state, canvas, 8, "đŸŽšī¸", projTrack.solo) - } - else - { - canvas.fillStyle = "#fff" - canvas.textAlign = "left" - canvas.textBaseline = "top" - canvas.font = "14px system-ui" - - canvas.fillText(track.name, 10, 8, state.trackHeaderW - 20) - } -} - - -function renderControlDial( - state: Timeline.State, - canvas: CanvasRenderingContext2D, - xSlot: number, - labelFn: (value: number) => string, - value: number, - minValue: number, - maxValue: number) -{ - const x = state.trackControlX + state.trackControlSize * xSlot - const y = state.trackControlY - const size = state.trackControlSize - - const factor = (value - minValue) / (maxValue - minValue) - - canvas.fillStyle = "#888" - canvas.fillRect( - x, y, - 5, size) - - canvas.fillStyle = "#0f0" - canvas.fillRect( - x, y + size - factor * size, - 5, factor * size) - - canvas.fillStyle = "#fff" - canvas.textAlign = "left" - canvas.textBaseline = "middle" - canvas.font = (size * 0.5) + "px system-ui" - - //canvas.fillText(label, x + 5 + (size - 5) / 2, y + (size / 4)) - canvas.fillText( - labelFn(value), - x + 10, - y + (size / 2), - size * 3 - 10) -} - - -function renderControlIcon( - state: Timeline.State, - canvas: CanvasRenderingContext2D, - xSlot: number, - icon: string, - enabled: boolean) -{ - const x = state.trackControlX + state.trackControlSize * xSlot - const y = state.trackControlY - const size = state.trackControlSize - - canvas.fillStyle = enabled ? "#fff" : "#fff4" - canvas.textAlign = "center" - canvas.textBaseline = "middle" - canvas.font = (size * 0.8) + "px system-ui" - - canvas.fillText(icon, x + size / 2, y + size / 2) -} \ No newline at end of file diff --git a/src/timeline/timeline.js b/src/timeline/timeline.js new file mode 100644 index 0000000..f78f06d --- /dev/null +++ b/src/timeline/timeline.js @@ -0,0 +1,658 @@ +function Timeline(canvas, synth) +{ + var that = this; + + // Set up constants. + this.MOUSE = 1; + this.KEYBOARD = 2; + + this.INTERACT_NONE = 0x00; + this.INTERACT_SCROLL = 0x01; + this.INTERACT_CURSOR = 0x02; + this.INTERACT_MOVE_TIME = 0x04; + this.INTERACT_MOVE_PITCH = 0x08; + this.INTERACT_STRETCH_TIME_L = 0x10; + this.INTERACT_STRETCH_TIME_R = 0x20; + + this.TIME_PER_WHOLE_NOTE = 960; + this.MAX_VALID_LENGTH = 960 * 1024; + this.MIN_VALID_MIDI_PITCH = 3 * 12; + this.MAX_VALID_MIDI_PITCH = 8 * 12 - 1; + this.OFFSET_X = 30; + this.REDRAW_TIME_MARGIN = 50; + + // Get canvas context. + this.canvas = canvas; + this.ctx = canvas.getContext("2d"); + + this.canvasWidth = 0; + this.canvasHeight = 0; + + // Store synth context. + this.synth = synth; + + // Set up tracks. + this.length = 0; + this.bpm = 120; + this.trackLength = new TrackLength(this); + this.trackKeys = new TrackKeys(this); + this.trackMeters = new TrackMeters(this); + this.trackNotes = new TrackNotes(this); + this.trackChords = new TrackChords(this); + + this.tracks = []; + this.tracks.push(this.trackLength); + this.tracks.push(this.trackKeys); + this.tracks.push(this.trackMeters); + this.tracks.push(this.trackNotes); + this.tracks.push(this.trackChords); + + this.trackNotesIndex = 3; + this.trackChordsIndex = 4; + + // Set up playback state. + this.playing = false; + this.playingTime = 0; + this.playingTimeEnd = 0; + this.playbackInterval = null; + + // Set up mouse/keyboard interaction. + this.canvas.oncontextmenu = function(ev) { that.handleContextMenu(ev); }; + this.canvas.onmousedown = function(ev) { that.handleMouseDown(ev); }; + window.onmousemove = function(ev) { that.handleMouseMove(ev); }; + window.onmouseup = function(ev) { that.handleMouseUp(ev); }; + window.onkeydown = function(ev) { that.handleKeyDown(ev); }; + window.onkeyup = function(ev) { that.handleKeyUp(ev); }; + + this.mouseDownDate = new Date(); + this.mouseDown = false; + this.mouseDownPos = null; + this.mouseDownTrack = null; + this.mouseDownScrollTime = 0; + this.mouseMoveScrollY = 0; + + this.keyboardHoldSpace = false; + + this.action = this.INTERACT_NONE; + this.actionDevice = 0; + this.actionMoveDeltaTime = 0; + this.actionMovePitch = 0; + this.actionStretchTimePivot = 0; + this.actionStretchTimeOrigin = 0; + + this.createLastPitch = 12 * 5 + theory.Fs; + this.createLastDuration = this.TIME_PER_WHOLE_NOTE / 4; + + this.cursorVisible = true; + this.cursorTime1 = 0; + this.cursorTrack1 = this.trackNotesIndex; + this.cursorTime2 = 0; + this.cursorTrack2 = this.trackNotesIndex; + + this.hoverElement = null; + this.hoverRegion = null; + this.selectedElements = []; + + // Set up display metrics. + this.scrollTime = 0; + this.durationVisible = 0; + this.firstTimeVisible = 0; + this.lastTimeVisible = 0; + this.timeSnap = 960 / 16; + this.timeToPixelsScaling = 100 / 960; + this.noteHeight = 12; + this.lastTrackBottomY = 0; + + this.redrawDirtyTimeMin = -1; + this.redrawDirtyTimeMax = -1; +} + + +Timeline.prototype.setSong = function(song) +{ + this.unselectAll(); + + this.length = song.length; + + for (var i = 0; i < this.tracks.length; i++) + this.tracks[i].setSong(song); + + this.markDirtyAll(); +} + + +Timeline.prototype.setScrollTime = function(time) +{ + var newScrollTime = + Math.max(0, + Math.min(this.length - this.TIME_PER_WHOLE_NOTE, + time)); + + this.scrollTime = newScrollTime; + + this.durationVisible = (this.canvasWidth - this.OFFSET_X) / this.timeToPixelsScaling; + this.firstTimeVisible = this.scrollTime - this.OFFSET_X / this.timeToPixelsScaling; + this.lastTimeVisible = this.scrollTime + this.canvasWidth / this.timeToPixelsScaling; + + this.markDirtyAll(); +} + + +Timeline.prototype.setScrollPitchAtBottom = function(pitch) +{ + this.trackNotes.setScrollPitchAtBottom(pitch); +} + + +Timeline.prototype.setScrollPitchAtCenter = function(pitch) +{ + this.trackNotes.setScrollPitchAtCenter(pitch); +} + + +Timeline.prototype.getScrollPitchAtBottom = function() +{ + return this.trackNotes.getScrollPitchAtBottom(); +} + + +Timeline.prototype.scrollTimeIntoView = function(time) +{ + if (time < this.firstTimeVisible) + this.setScrollTime(time - this.TIME_PER_WHOLE_NOTE * 4); + + else if (time > this.lastTimeVisible) + this.setScrollTime(time - this.durationVisible + this.TIME_PER_WHOLE_NOTE * 4); +} + + +Timeline.prototype.scrollPitchIntoView = function(pitch) +{ + this.trackNotes.scrollPitchIntoView(pitch); +} + + +Timeline.prototype.hideCursor = function() +{ + if (!this.cursorVisible) + return; + + this.markDirtyPixels(this.cursorTime1, 5); + this.markDirtyPixels(this.cursorTime2, 5); + this.markDirty(this.cursorTime1, this.cursorTime2); + + this.cursorVisible = false; +} + + +Timeline.prototype.showCursor = function() +{ + if (this.cursorVisible) + return; + + this.markDirtyPixels(this.cursorTime1, 5); + this.markDirtyPixels(this.cursorTime2, 5); + this.markDirty(this.cursorTime1, this.cursorTime2); + + this.cursorVisible = true; +} + + +Timeline.prototype.setCursor = function(time, trackIndex) +{ + this.markDirtyPixels(this.cursorTime1, 5); + this.markDirtyPixels(this.cursorTime2, 5); + this.markDirty(this.cursorTime1, this.cursorTime2); + + time = + Math.max(0, + Math.min(this.length, + time)); + + trackIndex = + Math.max(0, + Math.min(this.tracks.length - 1, + trackIndex)); + + this.cursorTime1 = time; + this.cursorTime2 = time; + this.cursorTrack1 = trackIndex; + this.cursorTrack2 = trackIndex; + + if (trackIndex == 0) + this.cursorTrack2 = this.tracks.length - 1; + + this.markDirtyPixels(this.cursorTime1, 5); +} + + +Timeline.prototype.setCursorBoth = function(time1, time2, track1, track2) +{ + this.markDirtyPixels(this.cursorTime1, 5); + this.markDirtyPixels(this.cursorTime2, 5); + this.markDirty(this.cursorTime1, this.cursorTime2); + + time1 = + Math.max(0, + Math.min(this.length, + time1)); + + time2 = + Math.max(0, + Math.min(this.length, + time2)); + + track1 = + Math.max(0, + Math.min(this.tracks.length - 1, + track1)); + + track2 = + Math.max(0, + Math.min(this.tracks.length - 1, + track2)); + + this.cursorTime1 = time1; + this.cursorTime2 = time2; + this.cursorTrack1 = track1; + this.cursorTrack2 = track2; + + this.markDirtyPixels(this.cursorTime1, 5); + this.markDirtyPixels(this.cursorTime2, 5); + this.markDirty(this.cursorTime1, this.cursorTime2); +} + + +Timeline.prototype.setCursor2 = function(time, trackIndex) +{ + this.markDirtyPixels(this.cursorTime2, 5); + this.markDirtyPixels(time, 5); + this.markDirty(this.cursorTime2, time); + + time = + Math.max(0, + Math.min(this.length, + time)); + + trackIndex = + Math.max(0, + Math.min(this.tracks.length - 1, + trackIndex)); + + var lastTrack2 = this.cursorTrack2; + + this.cursorTime2 = time; + + if (this.cursorTrack1 != 0) + this.cursorTrack2 = trackIndex; + + if (this.cursorTrack2 != lastTrack2) + { + this.markDirtyPixels(this.cursorTime1, 5); + this.markDirtyPixels(this.cursorTime2, 5); + this.markDirty(this.cursorTime1, this.cursorTime2); + } + + if (this.cursorTime2 != this.cursorTime1) + { + this.unselectAll(); + + var that = this; + var time1 = Math.min(this.cursorTime1, this.cursorTime2); + var time2 = Math.max(this.cursorTime1, this.cursorTime2); + var track1 = Math.min(this.cursorTrack1, this.cursorTrack2); + var track2 = Math.max(this.cursorTrack1, this.cursorTrack2); + var timeRange = new TimeRange(time1, time2); + + for (var i = track1; i <= track2; i++) + { + this.tracks[i].elements.enumerateOverlappingRange(timeRange, function (elem) + { + if (elem.interactTimeRange == null) + return; + + if (elem.interactTimeRange.includedInRange(timeRange)) + that.select(elem); + }); + } + } +} + + +Timeline.prototype.markDirtyAll = function() +{ + this.redrawDirtyTimeMin = this.firstTimeVisible - 100; + this.redrawDirtyTimeMax = this.lastTimeVisible + 100; +} + + +Timeline.prototype.markDirty = function(time1, time2) +{ + var start = Math.min(time1, time2); + var end = Math.max(time1, time2); + + if (this.redrawDirtyTimeMin == -1 || + start - this.REDRAW_TIME_MARGIN < this.redrawDirtyTimeMin) + { + this.redrawDirtyTimeMin = start - this.REDRAW_TIME_MARGIN; + } + + if (this.redrawDirtyTimeMax == -1 || + end + this.REDRAW_TIME_MARGIN > this.redrawDirtyTimeMax) + { + this.redrawDirtyTimeMax = end + this.REDRAW_TIME_MARGIN; + } +} + + +Timeline.prototype.markDirtyTimeRange = function(timeRange) +{ + this.markDirty(timeRange.start, timeRange.end); +} + + +Timeline.prototype.markDirtyPixels = function(time, pixelMargin) +{ + this.markDirty( + time - pixelMargin / this.timeToPixelsScaling, + time + pixelMargin / this.timeToPixelsScaling); +} + + +Timeline.prototype.markDirtyElement = function(elem) +{ + this.markDirty(elem.timeRange.start, elem.timeRange.end); +} + + +Timeline.prototype.markDirtyAllSelectedElements = function(timeDelta) +{ + for (var i = 0; i < this.selectedElements.length; i++) + { + this.markDirty( + this.selectedElements[i].timeRange.start + timeDelta, + this.selectedElements[i].timeRange.end + timeDelta); + } +} + + +Timeline.prototype.unselectAll = function() +{ + for (var i = 0; i < this.selectedElements.length; i++) + { + this.selectedElements[i].unselect(); + this.markDirtyElement(this.selectedElements[i]); + } + + this.selectedElements = []; +} + + +Timeline.prototype.select = function(elem) +{ + if (elem.selected) + return; + + elem.select(); + this.selectedElements.push(elem); + this.markDirtyElement(elem); +} + + +Timeline.prototype.unselect = function(elem) +{ + if (!elem.selected) + return; + + elem.unselect(); + this.markDirtyElement(elem); + + var indexOf = this.selectedElements.indexOf(elem); + if (indexOf >= 0) + this.selectedElements.splice(indexOf, 1); +} + + +Timeline.prototype.getSelectedElementsTimeRange = function() +{ + var allTimeRange = null; + + for (var i = 0; i < this.selectedElements.length; i++) + { + var timeRange = this.selectedElements[i].interactTimeRange; + if (timeRange == null) + timeRange = this.selectedElements[i].timeRange; + + if (allTimeRange == null) + allTimeRange = timeRange.clone(); + else + allTimeRange.merge(timeRange); + } + + return allTimeRange; +} + + +Timeline.prototype.getSelectedElementsInteractTimeRange = function() +{ + var allTimeRange = null; + + for (var i = 0; i < this.selectedElements.length; i++) + { + var timeRange = this.selectedElements[i].interactTimeRange; + if (timeRange == null) + continue; + + if (allTimeRange == null) + allTimeRange = timeRange.clone(); + else + allTimeRange.merge(timeRange); + } + + return allTimeRange; +} + + +Timeline.prototype.getSelectedElementsPitchRange = function() +{ + var pitchMin = null; + var pitchMax = null; + + for (var i = 0; i < this.selectedElements.length; i++) + { + if (this.selectedElements[i].interactPitch == null) + continue; + + var pitch = this.selectedElements[i].interactPitch.midiPitch; + + if (pitchMin == null || pitch < pitchMin.midiPitch) + pitchMin = new Pitch(pitch); + + if (pitchMax == null || pitch > pitchMax.midiPitch) + pitchMax = new Pitch(pitch); + } + + return { + min: pitchMin, + max: pitchMax + }; +} + + +Timeline.prototype.getLastElementTimeUpTo = function(trackIndex, time) +{ + var lastTime = 0; + + this.tracks[trackIndex].elements.enumerateOverlappingRange( + new TimeRange(0, time), + function (elem) + { + if (elem.interactTimeRange == null) + return; + + if (elem.interactTimeRange.end > lastTime && + elem.interactTimeRange.end <= time) + lastTime = elem.interactTimeRange.end; + }); + + return lastTime; +} + + +Timeline.prototype.getTrackIndexAtY = function(y) +{ + for (var i = 0; i < this.tracks.length; i++) + { + var track = this.tracks[i]; + + if (y >= track.y && y < track.y + track.height) + return i; + } + + return -1; +} + + +Timeline.prototype.relayout = function() +{ + this.canvasWidth = parseFloat(this.canvas.width); + this.canvasHeight = parseFloat(this.canvas.height); + + this.trackLength.y = 5; + this.trackLength.height = 20; + + this.trackKeys.y = 25; + this.trackKeys.height = 20; + + this.trackMeters.y = 50; + this.trackMeters.height = 20; + + this.trackNotes.y = 75; + this.trackNotes.height = this.canvasHeight - 80 - 65; + + this.trackChords.y = this.canvasHeight - 60; + this.trackChords.height = 55; + + this.lastTrackBottomY = this.trackChords.y + this.trackChords.height; + + for (var i = 0; i < this.tracks.length; i++) + this.tracks[i].relayout(); + + this.setScrollTime(this.scrollTime); + this.markDirtyAll(); +} + + +Timeline.prototype.redraw = function() +{ + // Return early if nothing dirty. + if (this.redrawDirtyTimeMin == -1 || this.redrawDirtyTimeMax == -1) + return; + + /* + // Clear background for redraw debug. + this.ctx.fillStyle = "#ffffff"; + this.ctx.fillRect( + 0, + 0, + this.canvasWidth, + this.canvasHeight); + */ + + this.ctx.save(); + this.ctx.translate( + Math.floor(this.OFFSET_X - this.scrollTime * this.timeToPixelsScaling), + 0); + + // Restrict drawing to dirty region. + this.ctx.beginPath(); + this.ctx.rect( + Math.floor((this.redrawDirtyTimeMin + this.REDRAW_TIME_MARGIN) * this.timeToPixelsScaling) + 0.5, + 0, + (this.redrawDirtyTimeMax - this.redrawDirtyTimeMin - this.REDRAW_TIME_MARGIN * 2) * this.timeToPixelsScaling, + this.canvasHeight); + this.ctx.clip(); + + // Clear background. + this.ctx.fillStyle = "#ffffff"; + this.ctx.fillRect( + Math.floor(this.redrawDirtyTimeMin * this.timeToPixelsScaling) + 0.5, + 0, + (this.redrawDirtyTimeMax - this.redrawDirtyTimeMin) * this.timeToPixelsScaling, + this.canvasHeight); + + // Draw tracks in reverse order. + for (var i = this.tracks.length - 1; i >= 0; i--) + { + this.ctx.save(); + this.ctx.translate(0, this.tracks[i].y); + + this.tracks[i].redraw(this.redrawDirtyTimeMin, this.redrawDirtyTimeMax); + + this.ctx.restore(); + } + + // Draw cursor. + if (this.playing) + { + this.drawCursor("#ff0000", this.playingTime, this.playingTime, 0, this.tracks.length - 1); + } + else if (this.cursorVisible) + { + var time1 = Math.min(this.cursorTime1, this.cursorTime2); + var time2 = Math.max(this.cursorTime1, this.cursorTime2); + var track1 = Math.min(this.cursorTrack1, this.cursorTrack2); + var track2 = Math.max(this.cursorTrack1, this.cursorTrack2); + + this.drawCursor("#0000ff", time1, time2, track1, track2); + } + + this.redrawDirtyTimeMin = -1; + this.redrawDirtyTimeMax = -1; + this.ctx.restore(); +} + + +Timeline.prototype.drawCursor = function(color, time1, time2, track1, track2) +{ + var x1 = Math.floor(time1 * this.timeToPixelsScaling) + 0.5; + var x2 = Math.floor(time2 * this.timeToPixelsScaling) + 0.5; + var y1 = this.tracks[track1].y; + var y2 = this.tracks[track2].y + this.tracks[track2].height; + + this.ctx.strokeStyle = color; + this.ctx.fillStyle = color; + + this.ctx.beginPath(); + this.ctx.moveTo(x1, y1); + this.ctx.lineTo(x1, y2); + + this.ctx.moveTo(x2, y1); + this.ctx.lineTo(x2, y2); + this.ctx.stroke(); + + this.ctx.beginPath(); + this.ctx.moveTo(x1, y1); + this.ctx.lineTo(x1 - 4, y1 - 4); + this.ctx.lineTo(x1 + 4, y1 - 4); + this.ctx.lineTo(x1, y1); + + this.ctx.moveTo(x2, y1); + this.ctx.lineTo(x2 - 4, y1 - 4); + this.ctx.lineTo(x2 + 4, y1 - 4); + this.ctx.lineTo(x2, y1); + + this.ctx.moveTo(x1, y2); + this.ctx.lineTo(x1 - 4, y2 + 4); + this.ctx.lineTo(x1 + 4, y2 + 4); + this.ctx.lineTo(x1, y2); + + this.ctx.moveTo(x2, y2); + this.ctx.lineTo(x2 - 4, y2 + 4); + this.ctx.lineTo(x2 + 4, y2 + 4); + this.ctx.lineTo(x2, y2); + this.ctx.fill(); + + this.ctx.globalAlpha = 0.25; + this.ctx.fillRect(x1, y1, x2 - x1, y2 - y1); +} diff --git a/src/timeline/timeline_interaction.js b/src/timeline/timeline_interaction.js new file mode 100644 index 0000000..e19dd80 --- /dev/null +++ b/src/timeline/timeline_interaction.js @@ -0,0 +1,134 @@ +Timeline.prototype.interactionBeginMoveTime = function() +{ + this.actionMoveDeltaTime = 0; +} + + +Timeline.prototype.interactionUpdateMoveTime = function(deltaFromOrigin) +{ + // Get merged time ranges of all selected elements. + var allTimeRange = this.getSelectedElementsInteractTimeRange(); + + // Mark elements' previous positions as dirty, + // to redraw over when they move away. + this.markDirtyAllSelectedElements(this.actionMoveDeltaTime); + + // Calculate displacement, + // ensuring that elements cannot fall out of bounds. + if (allTimeRange == null) + this.actionMoveDeltaTime = deltaFromOrigin; + else + this.actionMoveDeltaTime = + Math.max(-allTimeRange.start, + Math.min(this.length - allTimeRange.end, + deltaFromOrigin)); + + this.actionMoveDeltaTime = snap(this.actionMoveDeltaTime, this.timeSnap); + + // Mark elements' new positions as dirty, + // to redraw them at wherever they move to. + this.markDirtyAllSelectedElements(this.actionMoveDeltaTime); +} + + +Timeline.prototype.interactionBeginMovePitch = function() +{ + this.actionMoveDeltaPitch = 0; +} + + +Timeline.prototype.interactionUpdateMovePitch = function(deltaFromOrigin) +{ + // Get the pitch range of all selected elements. + var pitchRange = this.getSelectedElementsPitchRange(); + + // Calculate displacement, + // ensuring that pitches cannot fall out of bounds. + this.actionMoveDeltaPitch = deltaFromOrigin; + + if (pitchRange.min != null && pitchRange.max != null) + { + this.actionMoveDeltaPitch = + Math.max(this.MIN_VALID_MIDI_PITCH - pitchRange.min.midiPitch, + Math.min(this.MAX_VALID_MIDI_PITCH - pitchRange.max.midiPitch, + this.actionMoveDeltaPitch)); + + this.createLastPitch = Math.round(this.actionMoveDeltaPitch + (pitchRange.min.midiPitch + pitchRange.max.midiPitch) / 2); + } + + this.markDirtyAllSelectedElements(0); +} + + +Timeline.prototype.interactionBeginStretchTimeL = function(origin) +{ + this.actionStretchTimePivot = this.getSelectedElementsTimeRange().end; + this.actionStretchTimeOrigin = origin; +} + + +Timeline.prototype.interactionBeginStretchTimeR = function(origin) +{ + this.actionStretchTimePivot = this.getSelectedElementsTimeRange().start; + this.actionStretchTimeOrigin = origin; +} + + +Timeline.prototype.interactionUpdateStretchTime = function(deltaFromOrigin) +{ + // Get merged time ranges of all selected elements. + var allTimeRange = this.getSelectedElementsInteractTimeRange(); + + // Mark elements' previous positions as dirty, + // to redraw over when they stretch away. + var prevStretchedAllTimeRange = allTimeRange.clone(); + prevStretchedAllTimeRange.stretch( + this.actionStretchTimePivot, + this.actionStretchTimeOrigin, + this.actionMoveDeltaTime); + + this.markDirtyTimeRange(prevStretchedAllTimeRange); + + // FIXME: Calculate stretch, + // ensuring that elements cannot stretch out of bounds. + this.actionMoveDeltaTime = deltaFromOrigin; + this.actionMoveDeltaTime = snap(this.actionMoveDeltaTime, this.timeSnap); + + // Mark elements' new positions as dirty, + // to redraw them at wherever they stretch to. + var newStretchedAllTimeRange = allTimeRange.clone(); + newStretchedAllTimeRange.stretch( + this.actionStretchTimePivot, + this.actionStretchTimeOrigin, + this.actionMoveDeltaTime); + + if (this.selectedElements.length == 1) + { + this.createLastDuration = newStretchedAllTimeRange.duration(); + } + + this.markDirtyTimeRange(newStretchedAllTimeRange); +} + + +Timeline.prototype.interactionEnd = function() +{ + if ((this.action & this.INTERACT_MOVE_TIME) != 0) + this.markDirtyAllSelectedElements(this.actionMoveDeltaTime); + + for (var i = 0; i < this.selectedElements.length; i++) + this.selectedElements[i].modify(); + + for (var i = 0; i < this.tracks.length; i++) + this.tracks[i].applyModifications(); + + this.markDirtyAllSelectedElements(0); + + this.action = this.INTERACT_NONE; + this.actionDevice = 0; + + this.actionMoveDeltaTime = 0; + this.actionMoveDeltaPitch = 0; + this.actionStretchTimePivot = 0; + this.actionStretchTimeOrigin = 0; +} \ No newline at end of file diff --git a/src/timeline/timeline_keyboard.js b/src/timeline/timeline_keyboard.js new file mode 100644 index 0000000..6ec4bd3 --- /dev/null +++ b/src/timeline/timeline_keyboard.js @@ -0,0 +1,425 @@ +Timeline.prototype.handleKeyDown = function(ev) +{ + var that = this; + + var ctrl = ev.ctrlKey; + var shift = ev.shiftKey; + var alt = ev.altKey; + + //ev.preventDefault(); + + switch (ev.keyCode) + { + // Enter + case 13: + // Esc + case 27: + { + this.interactionEnd(); + + var elementEndTime = this.getSelectedElementsTimeRange(); + if (elementEndTime != null) + this.setCursorBoth(elementEndTime.end, elementEndTime.end, this.cursorTrack1,this.cursorTrack2); + else + this.setCursorBoth(this.cursorTime1, this.cursorTime1, this.cursorTrack1, this.cursorTrack2); + + this.showCursor(); + this.scrollTimeIntoView(this.cursorTime1); + this.unselectAll(); + break; + } + + // Space + case 32: + { + this.keyboardHoldSpace = true; + + this.playbackToggle(); + + break; + } + + // Backspace + case 8: + { + var eraseToTime = 0; + + var time1 = Math.min(this.cursorTime1, this.cursorTime2); + var time2 = Math.max(this.cursorTime1, this.cursorTime2); + var track1 = Math.min(this.cursorTrack1, this.cursorTrack2); + var track2 = Math.max(this.cursorTrack1, this.cursorTrack2); + + this.unselectAll(); + + if (this.cursorTime1 == this.cursorTime2) + { + for (var i = track1; i <= track2; i++) + { + eraseToTime = Math.max( + this.tracks[i].getBackspaceTime(this.cursorTime1), + eraseToTime); + } + + for (var i = track1; i <= track2; i++) + { + this.tracks[i].clipRange(new TimeRange(eraseToTime, this.cursorTime1)); + this.tracks[i].sanitize(); + } + + this.showCursor(); + this.setCursorBoth(eraseToTime, eraseToTime, this.cursorTrack1, this.cursorTrack2); + this.scrollTimeIntoView(eraseToTime); + } + else + { + for (var i = track1; i <= track2; i++) + { + this.tracks[i].clipRange(new TimeRange(time1, time2)); + this.tracks[i].sanitize(); + } + + this.showCursor(); + this.setCursorBoth(time1, time1, this.cursorTrack1, this.cursorTrack2); + this.scrollTimeIntoView(time1); + } + + break; + } + + // Delete + case 46: + { + for (var i = 0; i < this.selectedElements.length; i++) + { + var elem = this.selectedElements[i]; + + if (!elem.selected) + return; + + elem.unselect(); + this.markDirtyElement(elem); + elem.remove(); + } + + for (var i = 0; i < this.tracks.length; i++) + this.tracks[i].sanitize(); + + this.selectedElements = []; + break; + } + + // Left arrow + case 37: + { + if (alt) + { + this.setScrollTime(this.scrollTime - this.TIME_PER_WHOLE_NOTE); + } + else if (shift && this.cursorVisible) + { + this.setCursor2( + this.cursorTime2 - this.timeSnap, + this.cursorTrack2); + + this.scrollTimeIntoView(this.cursorTime2); + } + else if (this.selectedElements.length == 0) + { + this.setCursorBoth( + this.cursorTime1 - this.timeSnap, + this.cursorTime1 - this.timeSnap, + this.cursorTrack1, + this.cursorTrack2); + + this.scrollTimeIntoView(this.cursorTime1); + } + else + { + if (ctrl) + { + if (this._beginKeyboardAction(this.INTERACT_STRETCH_TIME_R)) + this.interactionBeginStretchTimeR(this.getSelectedElementsTimeRange().end); + + if (this.action == this.INTERACT_STRETCH_TIME_R) + this.interactionUpdateStretchTime(this.actionMoveDeltaTime - this.timeSnap); + } + else + { + if (this._beginKeyboardAction(this.INTERACT_MOVE_TIME)) + this.interactionBeginMoveTime(); + + if (this.action == this.INTERACT_MOVE_TIME) + { + this.interactionUpdateMoveTime(this.actionMoveDeltaTime - this.timeSnap); + this.scrollTimeIntoView(this.actionMoveDeltaTime + this.getSelectedElementsTimeRange().start); + } + } + } + + break; + } + + // Right arrow + case 39: + { + if (alt) + { + this.setScrollTime(this.scrollTime + this.TIME_PER_WHOLE_NOTE); + } + else if (shift && this.cursorVisible) + { + this.setCursor2( + this.cursorTime2 + this.timeSnap, + this.cursorTrack2); + + this.scrollTimeIntoView(this.cursorTime2); + } + else if (this.selectedElements.length == 0) + { + this.setCursorBoth( + this.cursorTime1 + this.timeSnap, + this.cursorTime1 + this.timeSnap, + this.cursorTrack1, + this.cursorTrack2); + + this.scrollTimeIntoView(this.cursorTime1); + } + else + { + if (ctrl) + { + if (this._beginKeyboardAction(this.INTERACT_STRETCH_TIME_R)) + this.interactionBeginStretchTimeR(this.getSelectedElementsTimeRange().end); + + if (this.action == this.INTERACT_STRETCH_TIME_R) + this.interactionUpdateStretchTime(this.actionMoveDeltaTime + this.timeSnap); + } + else + { + if (this._beginKeyboardAction(this.INTERACT_MOVE_TIME)) + this.interactionBeginMoveTime(); + + if (this.action == this.INTERACT_MOVE_TIME) + { + this.interactionUpdateMoveTime(this.actionMoveDeltaTime + this.timeSnap); + this.scrollTimeIntoView(this.actionMoveDeltaTime + this.getSelectedElementsTimeRange().end); + } + } + } + + break; + } + + // Up arrow + case 38: + { + if (alt) + { + this.setScrollPitchAtBottom( + this.getScrollPitchAtBottom() + 4); + } + else if (shift && this.cursorVisible) + { + this.setCursor2( + this.cursorTime2, + this.cursorTrack2 - 1); + } + else if (this.selectedElements.length == 0) + { + this.setCursorBoth( + this.cursorTime1, + this.cursorTime2, + this.cursorTrack1 - 1, + this.cursorTrack1 - 1); + } + else + { + if (this._beginKeyboardAction(this.INTERACT_MOVE_PITCH)) + this.interactionBeginMovePitch(); + + if (this.action == this.INTERACT_MOVE_PITCH) + { + this.interactionUpdateMovePitch(this.actionMoveDeltaPitch + 1); + + var pitchRange = this.getSelectedElementsPitchRange(); + if (pitchRange.max != null) + this.scrollPitchIntoView(this.actionMoveDeltaPitch + pitchRange.max.midiPitch); + } + } + + break; + } + + // Down arrow + case 40: + { + if (alt) + { + this.setScrollPitchAtBottom( + this.getScrollPitchAtBottom() - 4); + } + else if (shift && this.cursorVisible) + { + this.setCursor2( + this.cursorTime2, + this.cursorTrack2 + 1); + } + else if (this.selectedElements.length == 0) + { + this.setCursorBoth( + this.cursorTime1, + this.cursorTime2, + this.cursorTrack1 + 1, + this.cursorTrack1 + 1); + } + else + { + if (this._beginKeyboardAction(this.INTERACT_MOVE_PITCH)) + this.interactionBeginMovePitch(); + + if (this.action == this.INTERACT_MOVE_PITCH) + { + this.interactionUpdateMovePitch(this.actionMoveDeltaPitch - 1); + + var pitchRange = this.getSelectedElementsPitchRange(); + if (pitchRange.min != null) + this.scrollPitchIntoView(this.actionMoveDeltaPitch + pitchRange.min.midiPitch); + } + } + + break; + } + + // S, D, G, H, J, + // Z, X, C, V, B, N, M + case 90: { this._doPitchAction(theory.C); break; } + case 83: { this._doPitchAction(theory.Cs); break; } + case 88: { this._doPitchAction(theory.D); break; } + case 68: { this._doPitchAction(theory.Ds); break; } + case 67: { this._doPitchAction(theory.E); break; } + case 86: { this._doPitchAction(theory.F); break; } + case 71: { this._doPitchAction(theory.Fs); break; } + case 66: { this._doPitchAction(theory.G); break; } + case 72: { this._doPitchAction(theory.Gs); break; } + case 78: { this._doPitchAction(theory.A); break; } + case 74: { this._doPitchAction(theory.As); break; } + case 77: { this._doPitchAction(theory.B); break; } + } + + this.hoverElement = null; + this.hoverRegion = null; + this.redraw(); +} + + +Timeline.prototype.handleKeyUp = function(ev) +{ + switch (ev.keyCode) + { + // Space + case 32: + { + this.keyboardHoldSpace = false; + break; + } + } +} + + +Timeline.prototype._beginKeyboardAction = function(action) +{ + if (this.action != action || + this.actionDevice != this.KEYBOARD) + { + this.interactionEnd(); + this.hideCursor(); + this.actionDevice = this.KEYBOARD; + + // Set action to a common action of + // all selected elements. + this.action = action; + for (var i = 0; i < this.selectedElements.length; i++) + this.action &= this.selectedElements[i].interactKind; + + if (this.action == action) + return true; + } + + return false; +} + + +Timeline.prototype._doPitchAction = function(pitch) +{ + this.interactionEnd(); + + if (this.selectedElements.length != 0) + { + var elementEndTime = this.getSelectedElementsTimeRange(); + if (elementEndTime != null) + this.setCursorBoth(elementEndTime.end, elementEndTime.end, this.cursorTrack1,this.cursorTrack2); + else + this.setCursorBoth(this.cursorTime1, this.cursorTime1, this.cursorTrack1, this.cursorTrack2); + + this.unselectAll(); + } + + if (this.cursorTrack1 == this.cursorTrack2 && + this.cursorTime1 == this.cursorTime2) + { + var time1 = this.cursorTime1; + var time2 = time1 + this.createLastDuration; + + if (this.cursorTrack1 == this.trackNotesIndex) + { + var baseOctave = Math.floor(this.createLastPitch / 12) * 12; + var distToOctaveAbove = Math.abs((baseOctave + 12 + pitch) - this.createLastPitch); + var distToOctaveCur = Math.abs((baseOctave + pitch) - this.createLastPitch); + var distToOctaveBelow = Math.abs((baseOctave - 12 + pitch) - this.createLastPitch); + + var nearestPitch = baseOctave + pitch; + if (distToOctaveAbove < distToOctaveCur) + nearestPitch = baseOctave + 12 + pitch; + else if (distToOctaveBelow < distToOctaveCur) + nearestPitch = baseOctave - 12 + pitch; + + nearestPitch = + Math.max(this.MIN_VALID_MIDI_PITCH, + Math.min(this.MAX_VALID_MIDI_PITCH, + nearestPitch)); + + this.createLastPitch = nearestPitch; + + var noteElem = this.trackNotes.noteAdd( + new Note( + new TimeRange(time1, time2), + new Pitch(nearestPitch))); + + if (noteElem != null) + this.select(noteElem); + + this.scrollTimeIntoView(time2); + this.scrollPitchIntoView(nearestPitch); + this.setCursor(time2, this.trackNotesIndex); + + this.synth.addNoteOn(0, 0, nearestPitch, 1); + this.synth.addNoteOff(0.2, 0, nearestPitch); + this.synth.sortEvents(); + } + + else if (this.cursorTrack1 == this.trackChordsIndex) + { + var chordElem = this.trackChords.chordAdd( + new Chord( + new TimeRange(time1, time2), + 0, + pitch)); + + if (chordElem != null) + this.select(chordElem); + + this.scrollTimeIntoView(time2); + this.setCursor(time2, this.trackChordsIndex); + } + } +} \ No newline at end of file diff --git a/src/timeline/timeline_mouse.js b/src/timeline/timeline_mouse.js new file mode 100644 index 0000000..7b8448f --- /dev/null +++ b/src/timeline/timeline_mouse.js @@ -0,0 +1,255 @@ +Timeline.prototype.mouseToClient = function(ev) +{ + var rect = this.canvas.getBoundingClientRect(); + + return { + x: ev.clientX - rect.left - this.OFFSET_X, + xScrolled: ev.clientX - rect.left - this.OFFSET_X + this.scrollTime * this.timeToPixelsScaling, + y: ev.clientY - rect.top + }; +} + + +Timeline.prototype.handleContextMenu = function(ev) +{ + ev.preventDefault(); + return false; +} + + +Timeline.prototype.handleMouseDown = function(ev) +{ + var that = this; + + ev.preventDefault(); + + if (this.actionDevice != 0) + this.interactionEnd(); + + var ctrl = ev.ctrlKey; + var shift = ev.shiftKey; + var alt = ev.altKey; + var mousePos = this.mouseToClient(ev); + var mouseTime = snap(mousePos.xScrolled / this.timeToPixelsScaling, this.timeSnap); + + var lastMouseDate = this.mouseDownDate; + this.mouseDownDate = new Date(); + this.action = this.INTERACT_NONE; + this.actionDevice = this.MOUSE; + this.mouseDown = true; + this.mouseDownPos = mousePos; + this.mouseDownScrollTime = this.scrollTime; + this.mouseMoveScrollY = 0; + + // Handle scrolling with the middle or right mouse buttons, + // or if Alt is held down. + if (ev.which !== 1 || alt) + { + this.action = this.INTERACT_SCROLL; + this.canvas.style.cursor = "default"; + } + + else + { + // Handle multi-selection with Ctrl. + if (shift || (!ctrl && (this.hoverElement == null || !this.hoverElement.selected))) + this.unselectAll(); + + if (!shift && this.hoverElement != null && this.hoverRegion != null) + { + this.hideCursor(); + + // Select element under mouse. + this.select(this.hoverElement); + + // Set action to a common action of + // all selected elements. + this.action = this.hoverRegion.kind; + for (var i = 0; i < this.selectedElements.length; i++) + this.action &= this.selectedElements[i].interactKind; + + // Redraw all selected elements to + // indicate multiple modification. + this.markDirtyAllSelectedElements(0); + + // Set up interaction variables. + if ((this.action & this.INTERACT_MOVE_TIME) != 0) + this.interactionBeginMoveTime(); + + if ((this.action & this.INTERACT_MOVE_PITCH) != 0) + this.interactionBeginMovePitch(); + + if ((this.action & this.INTERACT_STRETCH_TIME_L) != 0) + this.interactionBeginStretchTimeL(this.hoverElement.interactTimeRange.start); + + if ((this.action & this.INTERACT_STRETCH_TIME_R) != 0) + this.interactionBeginStretchTimeR(this.hoverElement.interactTimeRange.end); + } + + // Handle cursor. + else + { + var trackIndex = this.getTrackIndexAtY(mousePos.y); + if (trackIndex != -1) + { + this.cursorVisible = true; + + // Handle double-click. + if (new Date().getTime() - lastMouseDate < 300) + { + var snapTime = this.getLastElementTimeUpTo(trackIndex, mouseTime); + this.setCursor(snapTime, trackIndex); + } + else + { + this.action = this.INTERACT_CURSOR; + this.setCursor(mouseTime, trackIndex); + } + + this.canvas.style.cursor = "text"; + this.scrollTimeIntoView(this.cursorTime1); + } + } + } + + var trackIndex = this.getTrackIndexAtY(mousePos.y); + this.mouseDownTrack = (trackIndex == -1 ? null : this.tracks[trackIndex]); + + this.hoverElement = null; + this.hoverRegion = null; + this.redraw(); +} + + +Timeline.prototype.handleMouseMove = function(ev) +{ + var that = this; + + ev.preventDefault(); + + var mousePos = this.mouseToClient(ev); + var mouseTime = snap(mousePos.xScrolled / this.timeToPixelsScaling, this.timeSnap); + + // Handle dragging with the mouse. + if (this.actionDevice == this.MOUSE && this.mouseDown) + { + var mouseTimeDelta = (mousePos.xScrolled - this.mouseDownPos.xScrolled) / this.timeToPixelsScaling; + var mousePitchDelta = Math.round((this.mouseDownPos.y - mousePos.y) / this.noteHeight); + + // Handle scrolling. + if (this.action == this.INTERACT_SCROLL) + { + var scrollTimeDelta = (this.mouseDownPos.x - mousePos.x) / this.timeToPixelsScaling; + this.setScrollTime(this.mouseDownScrollTime + scrollTimeDelta); + + this.mouseMoveScrollY = (mousePos.y - this.mouseDownPos.y); + + this.markDirtyAll(); + } + + // Handle cursor selection. + else if (this.action == this.INTERACT_CURSOR) + { + var lastTrack2 = this.cursorTrack2; + + var trackIndex = this.getTrackIndexAtY(mousePos.y); + if (trackIndex != -1) + this.setCursor2(mouseTime, trackIndex); + else + this.setCursor2(mouseTime, this.cursorTrack1); + } + + else if (this.selectedElements.length != 0) + { + // Handle time displacement. + if ((this.action & this.INTERACT_MOVE_TIME) != 0) + this.interactionUpdateMoveTime(mouseTimeDelta); + + // Handle pitch displacement. + if ((this.action & this.INTERACT_MOVE_PITCH) != 0) + this.interactionUpdateMovePitch(mousePitchDelta); + + // Handle time stretching. + if ((this.action & this.INTERACT_STRETCH_TIME_L) != 0 || + (this.action & this.INTERACT_STRETCH_TIME_R) != 0) + this.interactionUpdateStretchTime(mouseTimeDelta); + } + } + + // If mouse is not down, just handle hovering. + else if (this.actionDevice == 0) + { + if (this.hoverElement != null) + this.markDirtyElement(this.hoverElement); + + this.hoverElement = null; + this.hoverRegion = null; + + var trackIndex = this.getTrackIndexAtY(mousePos.y); + if (trackIndex != -1) + { + var track = this.tracks[trackIndex]; + + track.elements.enumerateOverlappingRange( + new TimeRange(mouseTime - 10 * this.timeToPixelsScaling, mouseTime + 10 * this.timeToPixelsScaling), + function (elem) + { + for (var e = 0; e < elem.regions.length; e++) + { + var region = elem.regions[e]; + + if (mousePos.xScrolled >= region.x && + mousePos.xScrolled <= region.x + region.width && + mousePos.y >= region.y + track.y + track.scrollY && + mousePos.y <= region.y + region.height + track.y + track.scrollY) + { + that.hoverElement = elem; + that.hoverRegion = region; + } + } + }); + } + + if (this.hoverElement != null) + this.markDirtyElement(this.hoverElement); + + var regionKind = this.INTERACT_NONE; + if (this.hoverRegion != null) + regionKind = this.hoverRegion.kind; + + if (((regionKind & this.INTERACT_MOVE_TIME) != 0) || + ((regionKind & this.INTERACT_MOVE_PITCH) != 0)) + this.canvas.style.cursor = "pointer"; + else if ((regionKind & this.INTERACT_STRETCH_TIME_L) != 0 || + (regionKind & this.INTERACT_STRETCH_TIME_R) != 0) + this.canvas.style.cursor = "ew-resize" + else + this.canvas.style.cursor = "text"; + } + + this.redraw(); +} + + +Timeline.prototype.handleMouseUp = function(ev) +{ + ev.preventDefault(); + + // Handle releasing the mouse after dragging. + if (this.actionDevice == this.MOUSE && this.mouseDown) + { + if (this.action == this.INTERACT_SCROLL) + { + if (this.mouseDownTrack != null) + this.mouseDownTrack.handleScroll(); + } + else + this.interactionEnd(); + + this.actionDevice = 0; + } + + this.mouseDown = false; + this.mouseDownTrack = null; + this.redraw(); +} \ No newline at end of file diff --git a/src/timeline/timeline_playback.js b/src/timeline/timeline_playback.js new file mode 100644 index 0000000..016690b --- /dev/null +++ b/src/timeline/timeline_playback.js @@ -0,0 +1,145 @@ +Timeline.prototype.playbackToggle = function() +{ + if (this.playing) + this.playbackStop(); + else + this.playbackStart(); +} + + +Timeline.prototype.playbackStart = function() +{ + this.playing = true; + + var playbackStartTime = this.cursorTime1; + + this.playingTime = playbackStartTime; + this.playingTimeEnd = playbackStartTime; + this.scrollTimeIntoView(playbackStartTime); + + var that = this; + this.trackNotes.elements.enumerateAll(function (elem) + { + if (elem.note == null) + return; + + that.playNote( + elem.note.timeRange.start, + elem.note.timeRange.end, + 0, + elem.note.pitch.midiPitch, + 1); + }); + + this.trackChords.elements.enumerateAll(function (elemChord) + { + if (elemChord.chord == null) + return; + + var bassPitch = theory.chordBassPitch(elemChord.chord.chordIndex, elemChord.chord.rootMidiPitch); + var mainPitches = theory.chordMainPitches(elemChord.chord.chordIndex, elemChord.chord.rootMidiPitch); + + that.trackMeters.enumerateMetersAtRange(elemChord.chord.timeRange, function (meter, start, end) + { + var bassVoicing = theory.chordBassVoicingInMeter(meter); + var mainVoicing = theory.chordMainVoicingInMeter(meter); + var beatDuration = that.TIME_PER_WHOLE_NOTE / meter.denominator; + + for (var t = meter.time; t < end; t += beatDuration * meter.numerator) + { + for (var v = 0; v < bassVoicing.length; v++) + { + var noteStart = t + bassVoicing[v][0] * beatDuration; + var noteEnd = Math.min(end, t + bassVoicing[v][1] * beatDuration); + + if (noteStart < start || + noteStart >= end) + continue; + + that.playNote( + noteStart, + noteEnd, + 1, + bassPitch, + bassVoicing[v][2]); + } + + for (var v = 0; v < mainVoicing.length; v++) + { + var noteStart = t + mainVoicing[v][0] * beatDuration; + var noteEnd = Math.min(end, t + mainVoicing[v][1] * beatDuration); + + if (noteStart < start || + noteStart >= end) + continue; + + for (var p = 0; p < mainPitches.length; p++) + { + that.playNote( + noteStart, + noteEnd, + 1, + mainPitches[p], + mainVoicing[v][2]); + } + } + } + }); + }); + + this.synth.sortEvents(); + this.markDirtyAll(); + + this.playbackInterval = setInterval(function() { that.playbackRefresh(1 / 60); }, 1000 / 60); +} + + +Timeline.prototype.playNote = function(start, end, instrument, midiPitch, volume) +{ + if (end < this.playingTime) + return; + + var timeToSecondsScale = 60 / this.bpm / (this.TIME_PER_WHOLE_NOTE / 4); + + this.playingTimeEnd = Math.max( + end, + this.playingTimeEnd); + + var startSecond = Math.max(0, (start - this.playingTime ) * timeToSecondsScale); + var endSecond = Math.max(0, (end - this.playingTime - 0.5) * timeToSecondsScale); + this.synth.addNoteOn (startSecond, instrument, midiPitch, volume); + this.synth.addNoteOff(endSecond, instrument, midiPitch); +} + + +Timeline.prototype.playbackStop = function() +{ + this.playing = false; + + clearInterval(this.playbackInterval); + this.playbackInterval = null; + + this.synth.stopAll(); + this.markDirtyAll(); + this.scrollTimeIntoView(this.cursorTime1); +} + + +Timeline.prototype.playbackRefresh = function(deltaSeconds) +{ + if (!this.playing) + return; + + var timeToSecondsScale = 60 / this.bpm / (this.TIME_PER_WHOLE_NOTE / 4); + + this.markDirtyPixels(this.playingTime, 5); + this.markDirtyPixels(this.playingTime + deltaSeconds / timeToSecondsScale, 5); + this.markDirty(this.playingTime, this.playingTime + deltaSeconds / timeToSecondsScale); + + this.playingTime += deltaSeconds / timeToSecondsScale; + + if (this.playingTime >= this.playingTimeEnd) + this.playbackStop(); + + this.redraw(); +} \ No newline at end of file diff --git a/src/timeline/track.js b/src/timeline/track.js new file mode 100644 index 0000000..eb9484e --- /dev/null +++ b/src/timeline/track.js @@ -0,0 +1,67 @@ +function Track() +{ + this.y = 0; + this.height = 0; + this.scrollY = 0; + + this.elements = null; + + this.selectedElements = []; + this.modifiedElements = []; +} + +Track.prototype.setSong = function(song) { } +Track.prototype.sanitize = function() { } + +Track.prototype.handleScroll = function() { } + +Track.prototype.elementSelect = function(elem) { } +Track.prototype.elementUnselect = function(elem) { } +Track.prototype.elementModify = function(elem) { } +Track.prototype.applyModifications = function() { } + + +Track.prototype.elementRemove = function(elem) +{ + if (elem.selected) + this.timeline.unselect(elem); + + this.timeline.markDirtyElement(elem); + this.elements.remove(elem); +} + + +Track.prototype.getBackspaceTime = function(beforeTime) +{ + var time = 0; + + this.elements.enumerateAll(function (elem) + { + if (elem.interactTimeRange == null) + return; + + if (elem.interactTimeRange.start > beforeTime) + return; + + if (elem.interactTimeRange.end >= beforeTime) + { + time = Math.max( + elem.interactTimeRange.start, + time); + } + else + { + time = Math.max( + elem.interactTimeRange.end, + time); + } + }); + + return time; +} + + +Track.prototype.clipRange = function(timeRange) { } + +Track.prototype.relayout = function() { } +Track.prototype.redraw = function(time1, time2) { } \ No newline at end of file diff --git a/src/timeline/track.tsx b/src/timeline/track.tsx deleted file mode 100644 index 6a78515..0000000 --- a/src/timeline/track.tsx +++ /dev/null @@ -1,403 +0,0 @@ -import React from "react" -import * as Timeline from "./index" -import * as Project from "../project" -import Rect from "../util/rect" -import Rational from "../util/rational" -import Range from "../util/range" - - -export const MARKER_WIDTH = 12 - - -export class TimelineTrack -{ - projectTrackId: Project.ID - parentId: Project.ID - name: string - renderRect: Rect - acceptedElemTypes: Set - - scrollEnabled: boolean - yScroll: number - - noCursor: boolean - pencil: any - - - constructor() - { - this.projectTrackId = -1 - this.parentId = -1 - this.name = "" - this.renderRect = new Rect(0, 0, 0, 0) - this.acceptedElemTypes = new Set() - - this.scrollEnabled = false - this.yScroll = 0 - - this.noCursor = false - this.pencil = null - } - - - copyState(state: Timeline.State) - { - for (const track of state.tracks) - { - if (track.projectTrackId == this.projectTrackId) - { - this.yScroll = track.yScroll - break - } - } - } - - - getActiveRange(state: Timeline.State): Range | null - { - return null - } - - - *elemsAtRegion( - state: Timeline.State, - range: Range, - verticalRegion?: { y1: number, y2: number }) - : Generator - { - - } - - - *iterKeyChangePairsAtRange( - state: Timeline.State, - range: Range) - : Generator<[Project.KeyChange, Project.KeyChange, number, number], void, void> - { - const keyChangeTrackId = Project.keyChangeTrackId(Project.global.project) - const keyChangeTrackTimedElems = Project.global.project.lists.get(keyChangeTrackId) - if (!keyChangeTrackTimedElems) - return - - const firstKeyCh = keyChangeTrackTimedElems.findFirst() as (Project.KeyChange | null) - const defaultKey = firstKeyCh?.key ?? Project.defaultKey() - - for (const pair of keyChangeTrackTimedElems.iterActiveAtRangePairwise(range)) - { - const keyCh1 = pair[0] ?? Project.makeKeyChange(-1, range.start, defaultKey) - const keyCh2 = pair[1] ?? Project.makeKeyChange(-1, range.end, defaultKey) - - const keyCh1X = Timeline.xAtTime(state, keyCh1.range.start) - const keyCh2X = Timeline.xAtTime(state, keyCh2.range.start) - - yield [keyCh1 as Project.KeyChange, keyCh2 as Project.KeyChange, keyCh1X, keyCh2X] - } - } - - - hover(state: Timeline.State) - { - - } - - - hoverBlockElements( - state: Timeline.State, - elemsIter: (range: Range) => Generator) - { - const pos = state.mouse.point.trackPos - - const margin = 10 - const checkRange = Timeline.timeRangeAtX(state, pos.x - margin, pos.x + margin) - - let hoverDrag = null - let hoverStretch = null - - for (const elem of elemsIter(checkRange)) - { - const margin = 8 - - const x1 = Timeline.xAtTime(state, elem.range.start) - const x2 = Timeline.xAtTime(state, elem.range.end) - - const rectDrag = new Rect( - x1, - 0, - x2 - x1, - this.renderRect.h) - - const rectStretch = new Rect( - x1 - margin, - 0, - x2 - x1 + margin * 2, - this.renderRect.h) - - if (rectDrag.contains(pos)) - { - hoverDrag = - { - id: elem.id, - range: elem.range, - action: Timeline.MouseAction.DragTime, - } - } - else if (rectStretch.contains(pos)) - { - hoverStretch = - { - id: elem.id, - range: elem.range, - action: pos.x < (x1 + x2) / 2 ? - Timeline.MouseAction.StretchTimeStart : - Timeline.MouseAction.StretchTimeEnd - } - } - } - - state.hover = hoverDrag ?? hoverStretch - } - - - click(state: Timeline.State, elemId: Project.ID) - { - - } - - - doubleClick(state: Timeline.State, elemId: Project.ID) - { - - } - - - contextMenu(state: Timeline.State, elemId: Project.ID) - { - - } - - - pencilClear(state: Timeline.State) - { - - } - - - pencilHover(state: Timeline.State) - { - - } - - - pencilStart(state: Timeline.State) - { - - } - - - pencilDrag(state: Timeline.State) - { - - } - - - pencilComplete(state: Timeline.State) - { - - } - - - rowAtY(state: Timeline.State, y: number): number - { - return 0 - } - - - findPreviousAnchor(state: Timeline.State, time: Rational): Rational - { - const list = Project.global.project.lists.get(this.parentId) - if (!list) - return Project.global.project.range.start - - const relTime = Project.getRelativeTime( - Project.global.project, - this.parentId, - time) - - const anchor = list.findPreviousDeletionAnchor(relTime) - if (!anchor) - return Project.global.project.range.start - - return Project.getAbsoluteTime( - Project.global.project, - this.parentId, - anchor) - } - - - deleteRange(state: Timeline.State, range: Range) - { - const list = Project.global.project.lists.get(this.parentId) - if (!list) - return - - const relRange = Project.getRelativeRange( - Project.global.project, - this.parentId, - range) - - if (range.duration.isZero()) - { - for (const elem of list.iterAtPoint(relRange.start)) - { - const removeElem = Project.elemModify(elem, { parentId: -1 }) - Project.global.project = Project.upsertElement(Project.global.project, removeElem) - } - } - else - { - for (const elem of list.iterAtRange(relRange)) - { - Project.global.project = Project.splitElem( - Project.global.project, - elem, - range) - } - } - } - - - selectionRemoveConflictingBehind(state: Timeline.State) - { - for (const id of state.selection) - { - const selectedElem = Project.global.project.elems.get(id) - if (!selectedElem) - continue - - if (selectedElem.parentId !== this.parentId) - continue - - const list = Project.global.project.lists.get(this.parentId) - if (!list) - continue - - const absSelectedRange = Project.getAbsoluteRange( - Project.global.project, - selectedElem.parentId, - selectedElem.range) - - if (selectedElem.range.duration.isZero()) - { - for (const elem of list.iterAtPoint(selectedElem.range.start)) - { - if (state.selection.has(elem.id)) - continue - - const removeElem = Project.elemModify(elem, { parentId: -1 }) - Project.global.project = Project.upsertElement(Project.global.project, removeElem) - } - } - else - { - for (const elem of list.iterAtRange(selectedElem.range)) - { - if (state.selection.has(elem.id)) - continue - - if (elem.type == "note" && selectedElem.type == "note" && - elem.midiPitch != selectedElem.midiPitch) - continue - - Project.global.project = Project.splitElem( - Project.global.project, - elem, - absSelectedRange) - } - } - } - } - - - renderBackground(state: Timeline.State, canvas: CanvasRenderingContext2D) - { - - } - - - render(state: Timeline.State, canvas: CanvasRenderingContext2D) - { - - } - - - markerRectAtTime(state: Timeline.State, time: Rational) - { - return new Rect( - Timeline.xAtTime(state, time) - MARKER_WIDTH / 2, - 0, - MARKER_WIDTH, - this.renderRect.h) - } - - - renderMarker( - state: Timeline.State, - canvas: CanvasRenderingContext2D, - time: Rational, - color: string, - label: string | null, - hovering: boolean, - selected: boolean) - { - const xCenter = Math.round(Timeline.xAtTime(state, time)) - const x1 = xCenter - MARKER_WIDTH / 2 - const x2 = xCenter + MARKER_WIDTH / 2 - - const y1 = 0.5 - const y2 = this.renderRect.h - - canvas.beginPath() - canvas.moveTo(x1, y1) - canvas.lineTo(x2, y1) - canvas.lineTo(x2, y2 - MARKER_WIDTH / 2) - canvas.lineTo(xCenter, y2) - canvas.lineTo(x1, y2 - MARKER_WIDTH / 2) - canvas.lineTo(x1, y1) - - canvas.fillStyle = color - canvas.fill() - - if (label && xCenter > state.trackHeaderW) - this.renderMarkerLabel(state, canvas, x2, color, label) - - if (hovering) - { - canvas.fillStyle = "#fff8" - canvas.fill() - } - - if (selected) - { - canvas.strokeStyle = "#fff" - canvas.stroke() - } - } - - - renderMarkerLabel( - state: Timeline.State, - canvas: CanvasRenderingContext2D, - x: number, - color: string, - label: string) - { - const y1 = 0.5 - const y2 = this.renderRect.h - - canvas.fillStyle = color - canvas.font = Math.floor(y2 - y1 - 10) + "px system-ui" - canvas.textAlign = "left" - canvas.textBaseline = "middle" - canvas.fillText(label, x + 5, (y1 + y2) / 2) - } -} \ No newline at end of file diff --git a/src/timeline/trackChords.tsx b/src/timeline/trackChords.tsx deleted file mode 100644 index d312000..0000000 --- a/src/timeline/trackChords.tsx +++ /dev/null @@ -1,221 +0,0 @@ -import React from "react" -import * as Project from "../project" -import * as Prefs from "../prefs" -import * as Popup from "../popup" -import * as Playback from "../playback" -import * as Timeline from "./index" -import * as Theory from "../theory" -import Rational from "../util/rational" -import Range from "../util/range" -import Rect from "../util/rect" -import * as CanvasUtils from "../util/canvasUtils" -import { TimelineTrack } from "./track" - - -export class TimelineTrackChords extends TimelineTrack -{ - pencil: null | - { - time1: Rational - time2: Rational - } - - - constructor(projectTrackId: Project.ID, name: string, h: number) - { - super() - this.projectTrackId = this.parentId = projectTrackId - this.name = name - this.renderRect = new Rect(0, 0, 0, h) - this.acceptedElemTypes.add("chord") - this.pencil = null - } - - - *iterChordsAtRange( - state: Timeline.State, - range: Range) - : Generator - { - const trackElems = Project.global.project.lists.get(this.projectTrackId) - if (!trackElems) - return - - for (const elem of trackElems.iterAtRange(range)) - yield elem as Project.Chord - } - - - *iterChordsAndKeyChangesAtRange( - state: Timeline.State, - range: Range) - : Generator<[Project.Chord, Project.KeyChange, number, number], void, void> - { - for (const [keyCh1, keyCh2, keyCh1X, keyCh2X] of this.iterKeyChangePairsAtRange(state, range)) - { - const time1 = keyCh1.range.start.max(range.start)! - const time2 = keyCh2.range.start.min(range.end)! - - for (const chord of this.iterChordsAtRange(state, new Range(time1, time2))) - yield [chord, keyCh1, keyCh1X, keyCh2X] - } - } - - - *elemsAtRegion( - state: Timeline.State, - range: Range, - verticalRegion?: { y1: number, y2: number }) - : Generator - { - for (const elem of this.iterChordsAtRange(state, range)) - yield elem.id - } - - - hover(state: Timeline.State) - { - this.hoverBlockElements(state, (range) => this.iterChordsAtRange(state, range)) - } - - - click(state: Timeline.State, elemId: Project.ID) - { - const chord = Project.getElem(Project.global.project, elemId, "chord") - if (chord) - { - state.insertion.duration = chord.range.duration - Playback.playChordPreview(this.projectTrackId, chord.chord, 0, 1) - } - } - - - pencilClear(state: Timeline.State) - { - this.pencil = null - } - - - pencilHover(state: Timeline.State) - { - const time = state.mouse.point.time - - this.pencil = - { - time1: time, - time2: time.add(state.timeSnap.multiply(new Rational(4))), - } - } - - - pencilDrag(state: Timeline.State) - { - if (this.pencil) - { - this.pencil.time2 = state.mouse.point.time - - const time1X = Timeline.xAtTime(state, this.pencil.time1) - const time2X = Timeline.xAtTime(state, this.pencil.time2) - if (Math.abs(time1X - time2X) < 5) - this.pencil.time2 = this.pencil.time1.add(state.timeSnap.multiply(new Rational(4))) - } - } - - - pencilComplete(state: Timeline.State) - { - if (this.pencil) - { - const key = Project.keyAt(Project.global.project, this.projectTrackId, this.pencil.time1) - - const elem = Project.makeChord( - this.projectTrackId, - new Range(this.pencil.time1, this.pencil.time2).sorted(), - new Theory.Chord(key.tonic.chroma, 0, 0, [])) - - let project = Project.global.project - const id = project.nextId - Project.global.project = Project.upsertElement(project, elem) - Timeline.selectionAdd(state, id) - } - } - - - render(state: Timeline.State, canvas: CanvasRenderingContext2D) - { - const visibleRange = Timeline.visibleTimeRange(state) - - for (let layer = 0; layer < 2; layer++) - { - for (const [chord, keyCh, xMin, xMax] of this.iterChordsAndKeyChangesAtRange(state, visibleRange)) - { - const selected = state.selection.contains(chord.id) - if (!Playback.global.playing && (layer == 0) == selected) - continue - - if (chord.type != "chord") - continue - - const key = keyCh.key - const hovering = !!state.hover && state.hover.id == chord.id - const playing = Playback.global.playing && chord.range.overlapsPoint(Playback.global.playTime) - - this.renderChord( - state, canvas, chord.range, - xMin, xMax, - chord.chord, key, - hovering, selected, playing) - } - } - - if (this.pencil) - { - canvas.save() - canvas.globalAlpha = 0.4 - - const key = Project.keyAt(Project.global.project, this.projectTrackId, this.pencil.time1) - - const range = new Range(this.pencil.time1, this.pencil.time2).sorted() - this.renderChord( - state, canvas, range, -Infinity, Infinity, - new Theory.Chord(key.tonic.chroma, 0, 0, []), - key, - false, false, false) - - canvas.restore() - } - } - - - renderChord( - state: Timeline.State, - canvas: CanvasRenderingContext2D, - range: Range, - xMin: number, - xMax: number, - chord: Theory.Chord, - key: Theory.Key, - hovering: boolean, - selected: boolean, - playing: boolean) - { - const x1 = Math.floor(Math.max(xMin, Math.min(xMax, - Timeline.xAtTime(state, range.start)))) + 0.5 - const x2 = Math.floor(Math.max(xMin, Math.min(xMax, - Timeline.xAtTime(state, range.end)))) + 0.5 - 1 - - const y1 = 1.5 - const y2 = this.renderRect.h - 0.5 - - canvas.fillStyle = (selected || playing) ? "#222" : "#000" - canvas.fillRect(x1, y1, x2 - x1, y2 - y1) - - CanvasUtils.renderChord( - canvas, - x1, y1, x2, y2, - chord, key) - - canvas.strokeStyle = (selected || playing) ? "#fff" : hovering ? "#888" : "#444" - canvas.strokeRect(x1, y1, x2 - x1, y2 - y1) - } -} \ No newline at end of file diff --git a/src/timeline/trackKeyChanges.tsx b/src/timeline/trackKeyChanges.tsx deleted file mode 100644 index 9cf228d..0000000 --- a/src/timeline/trackKeyChanges.tsx +++ /dev/null @@ -1,177 +0,0 @@ -import React from "react" -import * as Project from "../project" -import * as Prefs from "../prefs" -import * as Popup from "../popup" -import * as Timeline from "./index" -import * as Windows from "../windows" -import Rational from "../util/rational" -import Range from "../util/range" -import Rect from "../util/rect" - - -export class TimelineTrackKeyChanges extends Timeline.TimelineTrack -{ - pencil: null | - { - time: Rational - } - - - constructor(projectTrackId: Project.ID, name: string, h: number) - { - super() - this.projectTrackId = this.parentId = projectTrackId - this.name = name - this.renderRect = new Rect(0, 0, 0, h) - this.acceptedElemTypes.add("keyChange") - this.pencil = null - } - - - *iterAtRange( - state: Timeline.State, - range: Range) - : Generator - { - const trackElems = Project.global.project.lists.get(this.projectTrackId) - if (!trackElems) - return - - for (const elem of trackElems.iterAtRange(range)) - yield elem as Project.KeyChange - } - - - *elemsAtRegion( - state: Timeline.State, - range: Range, - verticalRegion?: { y1: number, y2: number }) - : Generator - { - for (const elem of this.iterAtRange(state, range)) - yield elem.id - } - - - hover(state: Timeline.State) - { - const pos = state.mouse.point.trackPos - - const checkRange = Timeline.timeRangeAtX( - state, - pos.x - Timeline.MARKER_WIDTH, - pos.x + Timeline.MARKER_WIDTH) - - for (const elem of this.iterAtRange(state, checkRange)) - { - const rect = this.markerRectAtTime(state, elem.range.start) - if (rect.contains(pos)) - { - state.hover = - { - id: elem.id, - range: elem.range, - action: Timeline.MouseAction.DragTime, - } - } - } - } - - - pencilClear(state: Timeline.State) - { - this.pencil = null - } - - - pencilHover(state: Timeline.State) - { - const time = state.mouse.point.time - - this.pencil = - { - time: time, - } - } - - - pencilDrag(state: Timeline.State) - { - if (this.pencil) - { - this.pencil.time = state.mouse.point.time - } - } - - - pencilComplete(state: Timeline.State) - { - if (this.pencil) - { - const elem = Project.makeKeyChange( - this.projectTrackId, - this.pencil.time, - Project.defaultKey()) - - let project = Project.global.project - const id = project.nextId - Project.global.project = Project.upsertElement(project, elem) - Timeline.selectionAdd(state, id) - } - } - - - render(state: Timeline.State, canvas: CanvasRenderingContext2D) - { - const visibleRange = Timeline.visibleTimeRange(state) - const activeKeyAtStart = Project.keyAt(Project.global.project, this.projectTrackId, visibleRange.start) - - let suppressStickyLabel = false - for (let layer = 0; layer < 2; layer++) - { - for (const keyCh of this.iterAtRange(state, visibleRange)) - { - const selected = state.selection.contains(keyCh.id) - if ((layer == 0) == selected) - continue - - if (keyCh.type != "keyChange") - continue - - const hovering = !!state.hover && state.hover.id == keyCh.id - this.renderMarker( - state, canvas, keyCh.range.start, - Prefs.global.editor.keyChangeColor, - keyCh.key.str, - hovering, selected) - - const x = Timeline.xAtTime(state, keyCh.range.start) - if (x > state.trackHeaderW && x < state.trackHeaderW + 100) - suppressStickyLabel = true - } - } - - if (!suppressStickyLabel) - { - this.renderMarkerLabel( - state, canvas, - state.trackHeaderW + 5, - Prefs.global.editor.keyChangeColor, - activeKeyAtStart.str) - } - - if (this.pencil) - { - canvas.save() - canvas.globalAlpha = 0.4 - - this.renderMarker( - state, canvas, this.pencil.time, - Prefs.global.editor.keyChangeColor, - null, - false, false) - - canvas.restore() - } - } -} \ No newline at end of file diff --git a/src/timeline/trackMeterChanges.tsx b/src/timeline/trackMeterChanges.tsx deleted file mode 100644 index dd5e20f..0000000 --- a/src/timeline/trackMeterChanges.tsx +++ /dev/null @@ -1,176 +0,0 @@ -import React from "react" -import * as Project from "../project" -import * as Prefs from "../prefs" -import * as Popup from "../popup" -import * as Timeline from "./index" -import Rational from "../util/rational" -import Range from "../util/range" -import Rect from "../util/rect" - - -export class TimelineTrackMeterChanges extends Timeline.TimelineTrack -{ - pencil: null | - { - time: Rational - } - - - constructor(projectTrackId: Project.ID, name: string, h: number) - { - super() - this.projectTrackId = this.parentId = projectTrackId - this.name = name - this.renderRect = new Rect(0, 0, 0, h) - this.acceptedElemTypes.add("meterChange") - this.pencil = null - } - - - *iterAtRange( - state: Timeline.State, - range: Range) - : Generator - { - const trackElems = Project.global.project.lists.get(this.projectTrackId) - if (!trackElems) - return - - for (const elem of trackElems.iterAtRange(range)) - yield elem as Project.MeterChange - } - - - *elemsAtRegion( - state: Timeline.State, - range: Range, - verticalRegion?: { y1: number, y2: number }) - : Generator - { - for (const elem of this.iterAtRange(state, range)) - yield elem.id - } - - - hover(state: Timeline.State) - { - const pos = state.mouse.point.trackPos - - const checkRange = Timeline.timeRangeAtX( - state, - pos.x - Timeline.MARKER_WIDTH, - pos.x + Timeline.MARKER_WIDTH) - - for (const elem of this.iterAtRange(state, checkRange)) - { - const rect = this.markerRectAtTime(state, elem.range.start) - if (rect.contains(pos)) - { - state.hover = - { - id: elem.id, - range: elem.range, - action: Timeline.MouseAction.DragTime, - } - } - } - } - - - pencilClear(state: Timeline.State) - { - this.pencil = null - } - - - pencilHover(state: Timeline.State) - { - const time = state.mouse.point.time - - this.pencil = - { - time: time, - } - } - - - pencilDrag(state: Timeline.State) - { - if (this.pencil) - { - this.pencil.time = state.mouse.point.time - } - } - - - pencilComplete(state: Timeline.State) - { - if (this.pencil) - { - const elem = Project.makeMeterChange( - this.projectTrackId, - this.pencil.time, - Project.defaultMeter()) - - let project = Project.global.project - const id = project.nextId - Project.global.project = Project.upsertElement(project, elem) - Timeline.selectionAdd(state, id) - } - } - - - render(state: Timeline.State, canvas: CanvasRenderingContext2D) - { - const visibleRange = Timeline.visibleTimeRange(state) - const activeMeterAtStart = Project.meterAt(Project.global.project, this.projectTrackId, visibleRange.start) - - let suppressStickyLabel = false - for (let layer = 0; layer < 2; layer++) - { - for (const meterCh of this.iterAtRange(state, visibleRange)) - { - const selected = state.selection.contains(meterCh.id) - if ((layer == 0) == selected) - continue - - if (meterCh.type != "meterChange") - continue - - const hovering = !!state.hover && state.hover.id == meterCh.id - this.renderMarker( - state, canvas, meterCh.range.start, - Prefs.global.editor.meterChangeColor, - meterCh.meter.str, - hovering, selected) - - const x = Timeline.xAtTime(state, meterCh.range.start) - if (x > state.trackHeaderW && x < state.trackHeaderW + 100) - suppressStickyLabel = true - } - } - - if (!suppressStickyLabel) - { - this.renderMarkerLabel( - state, canvas, - state.trackHeaderW + 5, - Prefs.global.editor.meterChangeColor, - activeMeterAtStart.str) - } - - if (this.pencil) - { - canvas.save() - canvas.globalAlpha = 0.4 - - this.renderMarker( - state, canvas, this.pencil.time, - Prefs.global.editor.meterChangeColor, - null, - false, false) - - canvas.restore() - } - } -} \ No newline at end of file diff --git a/src/timeline/trackNoteBlocks.tsx b/src/timeline/trackNoteBlocks.tsx deleted file mode 100644 index b8d8faf..0000000 --- a/src/timeline/trackNoteBlocks.tsx +++ /dev/null @@ -1,218 +0,0 @@ -import React from "react" -import * as Project from "../project" -import * as Prefs from "../prefs" -import * as Popup from "../popup" -import * as Playback from "../playback" -import * as Timeline from "./index" -import Rational from "../util/rational" -import Range from "../util/range" -import Rect from "../util/rect" - - -export class TimelineTrackNoteBlocks extends Timeline.TimelineTrack -{ - pencil: null | - { - time1: Rational - time2: Rational - } - - - constructor(projectTrackId: Project.ID, name: string, h: number) - { - super() - this.projectTrackId = this.parentId = projectTrackId - this.name = name - this.renderRect = new Rect(0, 0, 0, h) - this.acceptedElemTypes.add("noteBlock") - this.pencil = null - } - - - *iterNoteBlocksAtRange( - state: Timeline.State, - range: Range) - : Generator - { - const trackElems = Project.global.project.lists.get(this.projectTrackId) - if (!trackElems) - return - - for (const elem of trackElems.iterAtRange(range)) - yield elem as Project.NoteBlock - } - - - *iterNotesAtNoteBlock( - state: Timeline.State, - noteBlock: Project.NoteBlock, - range: Range) - : Generator - { - const list = Project.global.project.lists.get(noteBlock.id) - if (!list) - return - - for (const elem of list.iterAtRange(range)) - yield elem as Project.Note - } - - - *elemsAtRegion( - state: Timeline.State, - range: Range, - verticalRegion?: { y1: number, y2: number }) - : Generator - { - for (const note of this.iterNoteBlocksAtRange(state, range)) - yield note.id - } - - - hover(state: Timeline.State) - { - this.hoverBlockElements(state, (range) => this.iterNoteBlocksAtRange(state, range)) - } - - - doubleClick(state: Timeline.State, elemId: Project.ID) - { - const elem = Project.global.project.elems.get(elemId) - if (!elem || elem.type != "noteBlock") - return - - Timeline.modeStackPush(state) - state.mode = Timeline.Mode.NoteBlock - state.modeNoteBlockId = elemId - Timeline.refreshTracks(state) - } - - - pencilClear(state: Timeline.State) - { - this.pencil = null - } - - - pencilHover(state: Timeline.State) - { - const time = state.mouse.point.time - - this.pencil = - { - time1: time, - time2: time.add(state.timeSnap.multiply(new Rational(4))), - } - } - - - pencilDrag(state: Timeline.State) - { - if (this.pencil) - { - this.pencil.time2 = state.mouse.point.time - - const time1X = Timeline.xAtTime(state, this.pencil.time1) - const time2X = Timeline.xAtTime(state, this.pencil.time2) - if (Math.abs(time1X - time2X) < 5) - this.pencil.time2 = this.pencil.time1.add(state.timeSnap.multiply(new Rational(4))) - } - } - - - pencilComplete(state: Timeline.State) - { - if (this.pencil) - { - const elem = Project.makeNoteBlock( - this.projectTrackId, - new Range(this.pencil.time1, this.pencil.time2).sorted()) - - let project = Project.global.project - const id = project.nextId - Project.global.project = Project.upsertElement(project, elem) - Timeline.selectionAdd(state, id) - } - } - - - render(state: Timeline.State, canvas: CanvasRenderingContext2D) - { - const visibleRange = Timeline.visibleTimeRange(state) - - for (let layer = 0; layer < 2; layer++) - { - for (const noteBlock of this.iterNoteBlocksAtRange(state, visibleRange)) - { - const selected = state.selection.contains(noteBlock.id) - if ((layer == 0) == selected) - continue - - const hovering = !!state.hover && state.hover.id == noteBlock.id - const playing = Playback.global.playing && noteBlock.range.overlapsPoint(Playback.global.playTime) - - this.renderNoteBlock( - state, canvas, noteBlock.range, - this.iterNotesAtNoteBlock(state, noteBlock, visibleRange.displace(noteBlock.range.start.negate())), - hovering, selected, playing) - } - } - - if (this.pencil) - { - canvas.save() - canvas.globalAlpha = 0.4 - - const range = new Range(this.pencil.time1, this.pencil.time2).sorted() - this.renderNoteBlock(state, canvas, range, null, false, false, false) - - canvas.restore() - } - } - - - renderNoteBlock( - state: Timeline.State, - canvas: CanvasRenderingContext2D, - range: Range, - notes: Generator | null, - hovering: boolean, - selected: boolean, - playing: boolean) - { - const x1 = Math.floor(Timeline.xAtTime(state, range.start)) + 0.5 - const x2 = Math.floor(Timeline.xAtTime(state, range.end)) + 0.5 - 1 - - const y1 = 1.5 - const y2 = this.renderRect.h - 0.5 - - canvas.fillStyle = (selected || playing) ? "#222" : "#000" - canvas.fillRect(x1, y1, x2 - x1, y2 - y1) - - if (notes) - { - canvas.fillStyle = "#fff" - for (const note of notes) - { - const noteH = 2 - const noteSpacing = 1 - - const noteRange = note.range.displace(range.start) - const noteY = this.renderRect.h / 2 + (60 - note.midiPitch) * noteSpacing - const noteY1 = noteY - noteH / 2 - const noteY2 = noteY + noteH / 2 - - const noteX1 = Math.max(x1, Math.min(x2, - Timeline.xAtTime(state, noteRange.start) + 0.5)) - - const noteX2 = Math.max(x1, Math.min(x2, - Timeline.xAtTime(state, noteRange.end) + 0.5)) - - canvas.fillRect(noteX1, noteY1, noteX2 - noteX1, noteY2 - noteY1) - } - } - - canvas.strokeStyle = (selected || playing) ? "#fff" : hovering ? "#888" : "#444" - canvas.strokeRect(x1, y1, x2 - x1, y2 - y1) - } -} \ No newline at end of file diff --git a/src/timeline/trackNoteVelocities.tsx b/src/timeline/trackNoteVelocities.tsx deleted file mode 100644 index d2de37c..0000000 --- a/src/timeline/trackNoteVelocities.tsx +++ /dev/null @@ -1,168 +0,0 @@ -import React from "react" -import * as Project from "../project" -import * as Prefs from "../prefs" -import * as Popup from "../popup" -import * as Theory from "../theory" -import * as Playback from "../playback" -import * as Timeline from "./index" -import Rational from "../util/rational" -import Range from "../util/range" -import Rect from "../util/rect" -import * as CanvasUtils from "../util/canvasUtils" - - -export class TimelineTrackNoteVelocities extends Timeline.TimelineTrack -{ - pencil: null | - { - time1: Rational - time2: Rational - midiPitch: number - } - - - constructor(projectTrackId: Project.ID, noteBlockId: Project.ID, h: number) - { - super() - this.projectTrackId = projectTrackId - this.parentId = noteBlockId - this.name = "Note Velocity" - this.renderRect = new Rect(0, 0, 0, h) - this.pencil = null - this.noCursor = true - } - - - parentStart(state: Timeline.State) - { - const noteBlock = Project.global.project.elems.get(this.parentId) - return noteBlock?.range?.start ?? new Rational(0) - } - - - parentRange(state: Timeline.State) - { - const noteBlock = Project.global.project.elems.get(this.parentId) - return noteBlock?.range ?? new Range(new Rational(0), new Rational(0)) - } - - - *iterNotesAtRange( - state: Timeline.State, - range: Range) - : Generator - { - const list = Project.global.project.lists.get(this.parentId) - if (!list) - return - - for (const elem of list.iterAtRange(range.displace(this.parentStart(state).negate()))) - yield elem as Project.Note - } - - - valueAtY(state: Timeline.State, y: number) - { - return Math.max(0, Math.min(1, 1 - (y / this.renderRect.h))) - } - - - yAtValue(state: Timeline.State, value: number) - { - return (1 - value) * this.renderRect.h - } - - - pencilDrag(state: Timeline.State) - { - const visibleRange = Timeline.visibleTimeRange(state) - const parentStart = this.parentStart(state) - const x1 = state.mouse.pointPrev.pos.x - const x2 = state.mouse.point.pos.x - const xMin = Math.min(x1, x2) - const xMax = Math.max(x1, x2) - - let newProject = Project.global.project - - for (const note of this.iterNotesAtRange(state, visibleRange)) - { - const selected = state.selection.contains(note.id) - const active = state.selection.size == 0 || selected - if (!active) - continue - - const markerX = Timeline.xAtTime(state, note.range.start.add(parentStart)) - if (markerX >= xMin && markerX <= xMax) - { - const velocity = this.valueAtY(state, state.mouse.point.originTrackPos.y) - const newNote = Project.elemModify(note, { velocity }) - - newProject = Project.upsertElement(newProject, newNote) - - Playback.playNotePreview( - Project.parentTrackFor(Project.global.project, newNote.parentId).id, - newNote.midiPitch, - newNote.volumeDb, - newNote.velocity) - } - } - - Project.global.project = newProject - } - - - render(state: Timeline.State, canvas: CanvasRenderingContext2D) - { - const visibleRange = Timeline.visibleTimeRange(state) - const parentRange = this.parentRange(state) - const parentStart = parentRange.start - - const visibleX1 = Timeline.xAtTime(state, visibleRange.start) - const visibleX2 = Timeline.xAtTime(state, visibleRange.end) - const parentX1 = Timeline.xAtTime(state, parentRange.start) - const parentX2 = Timeline.xAtTime(state, parentRange.end) - - canvas.fillStyle = "#0004" - canvas.fillRect(visibleX1, 0, parentX1 - visibleX1, this.renderRect.h) - canvas.fillRect(parentX2, 0, visibleX2 - parentX2, this.renderRect.h) - - for (let layer = 0; layer < 2; layer++) - { - for (const note of this.iterNotesAtRange(state, visibleRange)) - { - const selected = state.selection.contains(note.id) - const active = state.selection.size == 0 || selected - if ((layer == 0) == active) - continue - - this.renderMarkerStick( - state, canvas, parentStart, note.range, note.velocity, active) - } - } - } - - - renderMarkerStick( - state: Timeline.State, - canvas: CanvasRenderingContext2D, - parentStart: Rational, - range: Range, - value: number, - active: boolean) - { - const x = Math.floor(Timeline.xAtTime(state, range.start.add(parentStart))) - const y = Math.floor(this.yAtValue(state, value)) - const rectSize = 3 - - canvas.lineWidth = 2 - canvas.strokeStyle = canvas.fillStyle = active ? - Prefs.global.editor.noteVelocityMarkerColor : - Prefs.global.editor.noteVelocityMarkerInactiveColor - - canvas.beginPath() - canvas.moveTo(x, this.renderRect.h) - canvas.lineTo(x, y + rectSize) - canvas.fillRect(x - rectSize, y - rectSize, rectSize * 2, rectSize * 2) - canvas.stroke() - } -} \ No newline at end of file diff --git a/src/timeline/trackNoteVolumes.tsx b/src/timeline/trackNoteVolumes.tsx deleted file mode 100644 index d5bc02b..0000000 --- a/src/timeline/trackNoteVolumes.tsx +++ /dev/null @@ -1,175 +0,0 @@ -import React from "react" -import * as Project from "../project" -import * as Prefs from "../prefs" -import * as Popup from "../popup" -import * as Theory from "../theory" -import * as Playback from "../playback" -import * as Timeline from "./index" -import Rational from "../util/rational" -import Range from "../util/range" -import Rect from "../util/rect" -import * as CanvasUtils from "../util/canvasUtils" - - -export class TimelineTrackNoteVolumes extends Timeline.TimelineTrack -{ - minValue: number - maxValue: number - - pencil: null | - { - time1: Rational - time2: Rational - midiPitch: number - } - - - constructor(projectTrackId: Project.ID, noteBlockId: Project.ID, h: number) - { - super() - this.projectTrackId = projectTrackId - this.parentId = noteBlockId - this.name = "Note Volume" - this.renderRect = new Rect(0, 0, 0, h) - this.pencil = null - this.noCursor = true - this.minValue = Project.MinVolumeDb - this.maxValue = Project.MaxVolumeDb - } - - - parentStart(state: Timeline.State) - { - const noteBlock = Project.global.project.elems.get(this.parentId) - return noteBlock?.range?.start ?? new Rational(0) - } - - - parentRange(state: Timeline.State) - { - const noteBlock = Project.global.project.elems.get(this.parentId) - return noteBlock?.range ?? new Range(new Rational(0), new Rational(0)) - } - - - *iterNotesAtRange( - state: Timeline.State, - range: Range) - : Generator - { - const list = Project.global.project.lists.get(this.parentId) - if (!list) - return - - for (const elem of list.iterAtRange(range.displace(this.parentStart(state).negate()))) - yield elem as Project.Note - } - - - valueAtY(state: Timeline.State, y: number) - { - const f = Math.max(0, Math.min(1, 1 - (y / this.renderRect.h))) - return this.minValue + f * (this.maxValue - this.minValue) - } - - - yAtValue(state: Timeline.State, value: number) - { - const f = (value - this.minValue) / (this.maxValue - this.minValue) - return (1 - f) * this.renderRect.h - } - - - pencilDrag(state: Timeline.State) - { - const visibleRange = Timeline.visibleTimeRange(state) - const parentStart = this.parentStart(state) - const x1 = state.mouse.pointPrev.pos.x - const x2 = state.mouse.point.pos.x - const xMin = Math.min(x1, x2) - const xMax = Math.max(x1, x2) - - let newProject = Project.global.project - - for (const note of this.iterNotesAtRange(state, visibleRange)) - { - const selected = state.selection.contains(note.id) - const active = state.selection.size == 0 || selected - if (!active) - continue - - const markerX = Timeline.xAtTime(state, note.range.start.add(parentStart)) - if (markerX >= xMin && markerX <= xMax) - { - const volumeDb = this.valueAtY(state, state.mouse.point.originTrackPos.y) - const newNote = Project.elemModify(note, { volumeDb }) - - newProject = Project.upsertElement(newProject, newNote) - - Playback.playNotePreview( - Project.parentTrackFor(Project.global.project, newNote.parentId).id, - newNote.midiPitch, - newNote.volumeDb, - newNote.velocity) - } - } - - Project.global.project = newProject - } - - - render(state: Timeline.State, canvas: CanvasRenderingContext2D) - { - const visibleRange = Timeline.visibleTimeRange(state) - const parentRange = this.parentRange(state) - const parentStart = parentRange.start - - const visibleX1 = Timeline.xAtTime(state, visibleRange.start) - const visibleX2 = Timeline.xAtTime(state, visibleRange.end) - const parentX1 = Timeline.xAtTime(state, parentRange.start) - const parentX2 = Timeline.xAtTime(state, parentRange.end) - - canvas.fillStyle = "#0004" - canvas.fillRect(visibleX1, 0, parentX1 - visibleX1, this.renderRect.h) - canvas.fillRect(parentX2, 0, visibleX2 - parentX2, this.renderRect.h) - - for (let layer = 0; layer < 2; layer++) - { - for (const note of this.iterNotesAtRange(state, visibleRange)) - { - const selected = state.selection.contains(note.id) - const active = state.selection.size == 0 || selected - if ((layer == 0) == active) - continue - - this.renderMarkerStick( - state, canvas, parentStart, note.range, note.volumeDb, active) - } - } - } - - - renderMarkerStick( - state: Timeline.State, - canvas: CanvasRenderingContext2D, - parentStart: Rational, - range: Range, - value: number, - active: boolean) - { - const x = Math.floor(Timeline.xAtTime(state, range.start.add(parentStart))) - const y = Math.floor(this.yAtValue(state, value)) - const rectSize = 3 - - canvas.lineWidth = 2 - canvas.strokeStyle = canvas.fillStyle = active ? - Prefs.global.editor.noteVelocityMarkerColor : - Prefs.global.editor.noteVelocityMarkerInactiveColor - - canvas.beginPath() - canvas.moveTo(x, this.renderRect.h) - canvas.lineTo(x, y + rectSize) - canvas.fillRect(x - rectSize, y - rectSize, rectSize * 2, rectSize * 2) - canvas.stroke() - } -} \ No newline at end of file diff --git a/src/timeline/trackNotes.tsx b/src/timeline/trackNotes.tsx deleted file mode 100644 index 6e4b099..0000000 --- a/src/timeline/trackNotes.tsx +++ /dev/null @@ -1,590 +0,0 @@ -import React from "react" -import * as Project from "../project" -import * as Prefs from "../prefs" -import * as Popup from "../popup" -import * as Theory from "../theory" -import * as Playback from "../playback" -import * as Timeline from "./index" -import Rational from "../util/rational" -import Range from "../util/range" -import Rect from "../util/rect" -import * as CanvasUtils from "../util/canvasUtils" - - -export class TimelineTrackNotes extends Timeline.TimelineTrack -{ - pencil: null | - { - time1: Rational - time2: Rational - midiPitch: number - } - - - constructor(projectTrackId: Project.ID, noteBlockId: Project.ID, name: string, h: number) - { - super() - this.projectTrackId = projectTrackId - this.parentId = noteBlockId - this.name = name - this.renderRect = new Rect(0, 0, 0, h) - this.acceptedElemTypes.add("note") - this.pencil = null - this.scrollEnabled = true - } - - - getActiveRange(state: Timeline.State): Range - { - const noteBlock = Project.global.project.elems.get(this.parentId) - return noteBlock?.range ?? new Range(new Rational(0), new Rational(0)) - } - - - *iterNotesAtRange( - state: Timeline.State, - range: Range) - : Generator - { - const list = Project.global.project.lists.get(this.parentId) - if (!list) - return - - for (const elem of list.iterAtRange(range.subtract(this.getActiveRange(state).start))) - yield elem as Project.Note - } - - - *iterExternalNotesAtRange( - state: Timeline.State, - range: Range) - : Generator<[Project.Note, Project.NoteBlock], void, void> - { - for (const track of Project.global.project.tracks) - { - if (track.trackType != "notes") - continue - - const trackElems = Project.global.project.lists.get(track.id) - if (!trackElems) - continue - - for (const noteBlock of trackElems.iterAtRange(range)) - { - if (noteBlock.id == this.parentId) - continue - - if (noteBlock.type != "noteBlock") - continue - - const noteList = Project.global.project.lists.get(noteBlock.id) - if (!noteList) - continue - - for (const elem of noteList.iterAtRange(range.subtract(noteBlock.range.start))) - { - const newElem = - { - ...elem, - range: elem.range - .displace(noteBlock.range.start) - .intersect(noteBlock.range) - .subtract(noteBlock.range.start), - } - - if (newElem.range.duration.isZero()) - continue - - yield [newElem as Project.Note, noteBlock as Project.NoteBlock] - } - } - } - } - - - *iterNotesAndKeyChangesAtRange( - state: Timeline.State, - range: Range) - : Generator<[Project.Note, Project.KeyChange, number, number], void, void> - { - for (const [keyCh1, keyCh2, keyCh1X, keyCh2X] of this.iterKeyChangePairsAtRange(state, range)) - { - const time1 = keyCh1.range.start.max(range.start)! - const time2 = keyCh2.range.start.min(range.end)! - - for (const note of this.iterNotesAtRange(state, new Range(time1, time2))) - yield [note, keyCh1, keyCh1X, keyCh2X] - } - } - - - *iterExternalNotesAndKeyChangesAtRange( - state: Timeline.State, - range: Range) - : Generator<[Project.Note, Project.NoteBlock, Project.KeyChange, number, number], void, void> - { - for (const [keyCh1, keyCh2, keyCh1X, keyCh2X] of this.iterKeyChangePairsAtRange(state, range)) - { - const time1 = keyCh1.range.start.max(range.start)! - const time2 = keyCh2.range.start.min(range.end)! - - for (const [note, noteBlock] of this.iterExternalNotesAtRange(state, new Range(time1, time2))) - yield [note, noteBlock, keyCh1, keyCh1X, keyCh2X] - } - } - - - *iterChordsAtRange( - state: Timeline.State, - range: Range) - : Generator - { - const trackElems = Project.global.project.lists.get(Project.global.project.chordTrackId) - if (!trackElems) - return - - for (const elem of trackElems.iterAtRange(range)) - yield elem as Project.Chord - } - - - *iterChordsAndKeyChangesAtRange( - state: Timeline.State, - range: Range) - : Generator<[Project.Chord, Project.KeyChange, number, number], void, void> - { - for (const [keyCh1, keyCh2, keyCh1X, keyCh2X] of this.iterKeyChangePairsAtRange(state, range)) - { - const time1 = keyCh1.range.start.max(range.start)! - const time2 = keyCh2.range.start.min(range.end)! - - for (const chord of this.iterChordsAtRange(state, new Range(time1, time2))) - yield [chord, keyCh1, keyCh1X, keyCh2X] - } - } - - - *elemsAtRegion( - state: Timeline.State, - range: Range, - verticalRegion?: { y1: number, y2: number }) - : Generator - { - for (const [note, keyCh, xStart, xEnd] of this.iterNotesAndKeyChangesAtRange(state, range)) - { - if (verticalRegion) - { - const rect = this.rectForNote( - state, - note.range, - this.rowForPitch(state, note.midiPitch, keyCh.key), - xStart, - xEnd, - false) - - if (verticalRegion.y1 > rect.y2 || - verticalRegion.y2 < rect.y1) - continue - } - - yield note.id - } - } - - - hover(state: Timeline.State) - { - const pos = state.mouse.point.trackPos - - const margin = 10 - const checkRange = Timeline.timeRangeAtX(state, pos.x - margin, pos.x + margin) - - let hoverDrag = null - let hoverStretch = null - - const parentStart = this.getActiveRange(state).start - - for (const [note, keyCh, xMin, xMax] of this.iterNotesAndKeyChangesAtRange(state, checkRange)) - { - const margin = 8 - - const row = this.rowForPitch(state, note.midiPitch, keyCh.key) - const rect = this.rectForNote( - state, note.range.displace(parentStart), - row, xMin, xMax, true) - - const rectDrag = rect - const rectStretch = rect.expandW(margin) - - if (rectDrag.contains(pos)) - { - hoverDrag = - { - id: note.id, - range: note.range, - action: - Timeline.MouseAction.DragTimeAndRow, - } - } - else if (rectStretch.contains(pos)) - { - hoverStretch = - { - id: note.id, - range: note.range, - action: pos.x < (rectDrag.x1 + rectDrag.x2) / 2 ? - Timeline.MouseAction.StretchTimeStart : - Timeline.MouseAction.StretchTimeEnd - } - } - } - - state.hover = hoverDrag ?? hoverStretch - } - - - click(state: Timeline.State, elemId: Project.ID) - { - const note = Project.getElem(Project.global.project, elemId, "note") - if (note) - { - state.insertion.nearMidiPitch = note.midiPitch - state.insertion.duration = note.range.duration - Playback.playNotePreview(this.projectTrackId, note.midiPitch, note.volumeDb, note.velocity) - } - } - - - pencilClear(state: Timeline.State) - { - this.pencil = null - } - - - pencilHover(state: Timeline.State) - { - const time = state.mouse.point.time - const key = Project.keyAt(Project.global.project, this.projectTrackId, time) - const row = this.rowAtY(state, state.mouse.point.trackPos.y) - const midiPitch = this.pitchForRow(state, row, key) - - this.pencil = - { - time1: time.subtract(this.getActiveRange(state).start), - time2: time.subtract(this.getActiveRange(state).start).add(state.timeSnap.multiply(new Rational(4))), - midiPitch, - } - } - - - pencilStart(state: Timeline.State) - { - if (this.pencil) - { - Playback.playNotePreview(this.projectTrackId, this.pencil.midiPitch, Project.DefaultVolumeDb, 1) - } - } - - - pencilDrag(state: Timeline.State) - { - if (this.pencil) - { - this.pencil.time2 = state.mouse.point.time.subtract(this.getActiveRange(state).start) - - const time1X = Timeline.xAtTime(state, this.pencil.time1) - const time2X = Timeline.xAtTime(state, this.pencil.time2) - if (Math.abs(time1X - time2X) < 5) - this.pencil.time2 = this.pencil.time1.add(state.timeSnap.multiply(new Rational(4))) - } - } - - - pencilComplete(state: Timeline.State) - { - if (this.pencil) - { - const elem = Project.makeNote( - this.parentId, - new Range(this.pencil.time1, this.pencil.time2).sorted(), - this.pencil.midiPitch, - Project.DefaultVolumeDb, - 1) - - let project = Project.global.project - const id = project.nextId - Project.global.project = Project.upsertElement(project, elem) - Timeline.selectionAdd(state, id) - } - } - - - yForRow(state: Timeline.State, row: number): number - { - return this.renderRect.h / 2 - (row + 1) * state.noteRowH - this.yScroll - } - - - rowAtY(state: Timeline.State, y: number): number - { - return -Math.floor((y + this.yScroll - this.renderRect.h / 2) / state.noteRowH) - 1 - } - - - rowForPitch(state: Timeline.State, pitch: number, key: Theory.Key): number - { - const tonicRowOffset = Theory.Utils.chromaToDegreeInCMajor(key.tonic.chroma) - return key.octavedDegreeForMidi(pitch - 60) + tonicRowOffset - } - - - pitchForRow(state: Timeline.State, row: number, key: Theory.Key): number - { - const tonicRowOffset = Theory.Utils.chromaToDegreeInCMajor(key.tonic.chroma) - return key.midiForDegree(row - Math.floor(tonicRowOffset)) + 60 - } - - - rectForNote( - state: Timeline.State, - range: Range, - row: number, xStart: number, xEnd: number, - clampY: boolean) - { - const noteOrigX1 = Timeline.xAtTime(state, range.start) - const noteOrigX2 = Timeline.xAtTime(state, range.end) - - let noteY = Math.floor(this.yForRow(state, row)) - if (clampY) - { - noteY = - Math.max(-state.noteRowH / 2 + 0.5, - Math.min(this.renderRect.h - state.noteRowH / 2 - 0.5, - noteY)) - } - - let noteX1 = Math.max(noteOrigX1, xStart) - let noteX2 = Math.min(noteOrigX2, xEnd) - - const cutStart = noteOrigX1 < noteX1 - const cutEnd = noteOrigX2 > noteX2 - - //if (!cutStart) noteX1 += 1 - if (!cutEnd) noteX2 -= 1 - - noteX1 = Math.floor(noteX1) - noteX2 = Math.floor(noteX2) - - const noteW = Math.max(2, noteX2 - noteX1) - - return Object.assign( - new Rect(noteX1, noteY, noteW, state.noteRowH), - { cutStart, cutEnd }) - } - - - renderBackground(state: Timeline.State, canvas: CanvasRenderingContext2D) - { - const visibleRange = Timeline.visibleTimeRange(state) - - const rowAtTop = this.rowAtY(state, 0) - const rowAtBottom = this.rowAtY(state, this.renderRect.h) - - const octaveAtTop = Math.ceil(rowAtTop / 7) + 1 - const octaveAtBottom = Math.floor(rowAtBottom / 7) - 1 - - for (const [chord, keyCh, xMin, xMax] of this.iterChordsAndKeyChangesAtRange(state, visibleRange)) - { - for (const midiPitch of chord.chord.pitches) - { - const key = keyCh.key - const row = this.rowForPitch(state, midiPitch + 60, key) - const mode = key.scale.metadata!.mode - const fillStyle = CanvasUtils.fillStyleForDegree( - canvas, key.degreeForMidi(midiPitch) + mode, true) - - for (let i = octaveAtBottom; i <= octaveAtTop; i++) - { - this.renderChordNote( - state, canvas, chord.range, - row + i * 7, xMin, xMax, fillStyle) - } - } - } - - for (const [keyCh1, keyCh2, xMin, xMax] of this.iterKeyChangePairsAtRange(state, visibleRange)) - { - const tonicRowOffset = Theory.Utils.chromaToDegreeInCMajor(keyCh1.key.tonic.chroma) - - let renderOctaveLabels = true - - canvas.fillStyle = Prefs.global.editor.octaveLabelColor - canvas.font = Math.floor(state.noteRowH - 4) + "px system-ui" - canvas.textAlign = "left" - canvas.textBaseline = "bottom" - - for (const measure of Project.iterMeasuresAtRange(Project.global.project, new Range(keyCh1.range.start, keyCh2.range.start))) - { - const x1 = Math.max(xMin, 0.5 + Math.floor(Timeline.xAtTime(state, measure.time1))) - const x2 = Math.min(xMax, 0.5 + Math.floor(Timeline.xAtTime(state, measure.time2))) - - for (let i = octaveAtBottom; i <= octaveAtTop; i++) - { - const y = 0.5 + Math.floor( - this.yForRow(state, tonicRowOffset + i * 7) + state.noteRowH) - - canvas.strokeStyle = - measure.num % 2 != 0 ? - Prefs.global.editor.bkgColor : - Prefs.global.editor.bkgAlternateMeasureColor - - const labelX = Math.max(x1 + 5, state.trackHeaderW + 5) - if (renderOctaveLabels && labelX + 30 < xMax) - { - canvas.fillText(keyCh1.key.tonic.str + (i + 5).toString(), labelX, y - 1) - } - - canvas.beginPath() - canvas.moveTo(x1, y) - canvas.lineTo(x2, y) - canvas.moveTo(x1, y + 1) - canvas.lineTo(x2, y + 1) - canvas.stroke() - - for (let j = 1; j < 7; j += 1) - { - const ySuboctave = 0.5 + Math.floor( - this.yForRow(state, tonicRowOffset + i * 7 + j) + state.noteRowH) - - canvas.beginPath() - canvas.moveTo(x1, ySuboctave) - canvas.lineTo(x2, ySuboctave) - canvas.stroke() - } - } - - renderOctaveLabels = false - } - } - } - - - render(state: Timeline.State, canvas: CanvasRenderingContext2D) - { - const visibleRange = Timeline.visibleTimeRange(state) - const parentStart = this.getActiveRange(state).start - - for (const [note, noteBlock, keyCh, xMin, xMax] of this.iterExternalNotesAndKeyChangesAtRange(state, visibleRange)) - { - const key = keyCh.key - const row = this.rowForPitch(state, note.midiPitch, key) - const mode = key.scale.metadata!.mode - const fillStyle = CanvasUtils.fillStyleForDegree( - canvas, key.degreeForMidi(note.midiPitch) + mode, true) - - const playing = Playback.global.playing && note.range.displace(noteBlock.range.start).overlapsPoint(Playback.global.playTime) - - this.renderNote( - state, canvas, note.range, noteBlock.range.start, row, xMin, xMax, fillStyle, - true, false, false, playing) - } - - for (let layer = 0; layer < (Playback.global.playing ? 1 : 2); layer++) - { - for (const [note, keyCh, xMin, xMax] of this.iterNotesAndKeyChangesAtRange(state, visibleRange)) - { - const selected = state.selection.contains(note.id) - if (!Playback.global.playing && (layer == 0) == selected) - continue - - const key = keyCh.key - const row = this.rowForPitch(state, note.midiPitch, key) - const mode = key.scale.metadata!.mode - const fillStyle = CanvasUtils.fillStyleForDegree( - canvas, key.degreeForMidi(note.midiPitch) + mode, false) - - const hovering = !!state.hover && state.hover.id == note.id - const playing = Playback.global.playing && note.range.displace(parentStart).overlapsPoint(Playback.global.playTime) - - this.renderNote( - state, canvas, note.range, parentStart, row, xMin, xMax, fillStyle, - false, hovering, selected, playing) - } - } - - if (this.pencil) - { - canvas.save() - canvas.globalAlpha = 0.4 - - const range = new Range(this.pencil.time1, this.pencil.time2).sorted() - const key = Project.keyAt(Project.global.project, this.projectTrackId, this.pencil.time1.add(this.getActiveRange(state).start)) - const row = this.rowForPitch(state, this.pencil.midiPitch, key) - const mode = key.scale.metadata!.mode - const fillStyle = CanvasUtils.fillStyleForDegree( - canvas, key.degreeForMidi(this.pencil.midiPitch) + mode, false) - - this.renderNote(state, canvas, range, parentStart, row, -Infinity, Infinity, fillStyle) - - canvas.restore() - } - } - - - renderNote( - state: Timeline.State, - canvas: CanvasRenderingContext2D, - range: Range, - parentStart: Rational, - row: number, - xMin: number, xMax: number, - fillStyle: any, - external?: boolean, - hovering?: boolean, selected?: boolean, playing?: boolean) - { - const rect = this.rectForNote( - state, range.displace(parentStart), - row, xMin, xMax, true) - - canvas.fillStyle = fillStyle - - canvas.beginPath() - canvas.fillRect(rect.x, rect.y, rect.w, rect.h) - - if (hovering || playing) - { - canvas.globalAlpha = 0.4 - canvas.fillStyle = external ? "#888" : "#fff" - canvas.fillRect(rect.x, rect.y, rect.w, rect.h) - canvas.globalAlpha = 1 - } - - if (selected || playing) - { - canvas.strokeStyle = external ? "#888" : "#fff" - canvas.strokeRect(rect.x + 0.5, rect.y + 0.5, rect.w - 1, rect.h - 1) - } - } - - - renderChordNote( - state: Timeline.State, - canvas: CanvasRenderingContext2D, - range: Range, - row: number, - xMin: number, xMax: number, - fillStyle: any) - { - const rect = this.rectForNote( - state, range, - row, xMin, xMax, false) - - canvas.globalAlpha = 0.1 - canvas.fillStyle = fillStyle - - canvas.beginPath() - canvas.fillRect(rect.x, rect.y, rect.w, rect.h) - - canvas.globalAlpha = 1 - } -} \ No newline at end of file diff --git a/src/timeline/track_chords.js b/src/timeline/track_chords.js new file mode 100644 index 0000000..36a572f --- /dev/null +++ b/src/timeline/track_chords.js @@ -0,0 +1,388 @@ +function TrackChords(timeline) +{ + this.timeline = timeline; + this.elements = new ListByTimeRange(); +} + + +TrackChords.prototype = new Track(); + + +TrackChords.prototype.setSong = function(song) +{ + this.elements.clear(); + this.selectedElements = []; + + for (var i = 0; i < song.chords.length; i++) + this.chordAdd(song.chords[i]); +} + + +TrackChords.prototype.chordAdd = function(chord) +{ + this.clipRange(chord.timeRange); + + var elem = new Element(); + elem.track = this; + elem.chord = chord.clone(); + + elem.chord.timeRange.clip(0, this.timeline.length); + if (elem.chord.timeRange.duration() <= 0) + return null; + + this.elementRefresh(elem); + this.elements.add(elem); + this.timeline.markDirtyElement(elem); + + return elem; +} + + +TrackChords.prototype.clipRange = function(timeRange) +{ + // Check for overlapping chords and clip them. + var overlapping = []; + + this.elements.enumerateOverlappingRange(timeRange, function (elem) + { overlapping.push(elem); }); + + for (var i = 0; i < overlapping.length; i++) + this.elements.remove(overlapping[i]); + + for (var i = 0; i < overlapping.length; i++) + { + var elem = overlapping[i]; + + var parts = elem.chord.timeRange.getClippedParts(timeRange); + for (var p = 0; p < parts.length; p++) + { + var clippedChord = elem.chord.clone(); + clippedChord.timeRange = parts[p]; + this.chordAdd(clippedChord); + } + } +} + + +TrackChords.prototype.applyModifications = function() +{ + for (var i = 0; i < this.modifiedElements.length; i++) + { + var elem = this.modifiedElements[i]; + elem.chord.timeRange.clip(0, this.timeline.length); + this.clipRange(elem.chord.timeRange); + } + + for (var i = 0; i < this.modifiedElements.length; i++) + { + var elem = this.modifiedElements[i]; + if (elem.chord.timeRange.duration() <= 0) + { + this.timeline.unselect(elem); + continue; + } + + this.elementRefresh(elem); + this.elements.add(elem); + this.timeline.markDirtyElement(elem); + } + + this.modifiedElements = []; +} + + +TrackChords.prototype.elementModify = function(elem) +{ + this.elements.remove(elem); + + var modifiedElem = this.getModifiedElement(elem); + + elem.chord = elem.chord.clone(); + elem.chord.rootMidiPitch = modifiedElem.rootMidiPitch; + elem.chord.timeRange = + new TimeRange(modifiedElem.start, modifiedElem.start + modifiedElem.duration); + + this.modifiedElements.push(elem); +} + + +TrackChords.prototype.elementRefresh = function(elem) +{ + var toPixels = this.timeline.timeToPixelsScaling; + + elem.timeRange = elem.chord.timeRange.clone(); + elem.interactTimeRange = elem.chord.timeRange.clone(); + elem.interactKind = + this.timeline.INTERACT_MOVE_TIME | this.timeline.INTERACT_MOVE_PITCH | + this.timeline.INTERACT_STRETCH_TIME_L | this.timeline.INTERACT_STRETCH_TIME_R; + + elem.regions = [ + { + kind: this.timeline.INTERACT_MOVE_TIME, + x: elem.chord.timeRange.start * toPixels, + y: 0, + width: elem.chord.timeRange.duration() * toPixels, + height: this.height + }, + { + kind: this.timeline.INTERACT_STRETCH_TIME_L, + x: elem.chord.timeRange.start * toPixels - 4, + y: 0, + width: 4, + height: this.height + }, + { + kind: this.timeline.INTERACT_STRETCH_TIME_R, + x: elem.chord.timeRange.end * toPixels, + y: 0, + width: 4, + height: this.height + } + ]; +} + + +TrackChords.prototype.relayout = function() +{ + var that = this; + + this.elements.enumerateAll(function (elem) + { that.elementRefresh(elem); }); +} + + +TrackChords.prototype.redraw = function(time1, time2) +{ + var that = this; + var ctx = this.timeline.ctx; + var toPixels = this.timeline.timeToPixelsScaling; + + var xMin = Math.floor(Math.max(0, time1) * toPixels); + var xLen = Math.floor(this.timeline.length * toPixels); + var xMax = Math.floor(time2 * toPixels); + + ctx.save(); + + ctx.beginPath(); + ctx.rect(xMin - 10, 0, xMax - xMin + 20, this.height + 1); + ctx.clip(); + + ctx.translate(0.5, 0.5); + + // Draw background. + ctx.fillStyle = "#e4e4e4"; + ctx.fillRect( + xMin, + 0, + xMax, + this.height); + + // Draw background after song length. + if (time2 > this.timeline.length) + { + ctx.fillStyle = "#cccccc"; + ctx.fillRect( + this.timeline.length * toPixels, + 0, + (time2 - this.timeline.length) * toPixels, + this.height); + } + + // Draw beat lines. + ctx.strokeStyle = "#ffffff"; + ctx.beginPath(); + this.timeline.trackMeters.enumerateBeatsAtRange(new TimeRange(time1, time2), function (time, isStrong) + { + var x = Math.floor(time * toPixels); + + ctx.moveTo(x, 0); + ctx.lineTo(x, that.height); + + if (isStrong) + { + ctx.moveTo(x - 1, 0); + ctx.lineTo(x - 1, that.height); + ctx.moveTo(x + 1, 0); + ctx.lineTo(x + 1, that.height); + } + }); + ctx.stroke(); + + // Draw notes. + this.elements.enumerateOverlappingRange(new TimeRange(time1, time2), function (elem) + { that.drawChord(elem); }); + + for (var i = 0; i < this.selectedElements.length; i++) + this.drawChord(this.selectedElements[i]); + + // Draw borders. + ctx.strokeStyle = "#000000"; + ctx.beginPath(); + ctx.moveTo(xMin, 0); + ctx.lineTo(xMax, 0); + + ctx.moveTo(xMin, this.height); + ctx.lineTo(xMax, this.height); + + ctx.moveTo(0, 0); + ctx.lineTo(0, this.height); + + ctx.moveTo(xLen, 0); + ctx.lineTo(xLen, this.height); + ctx.stroke(); + + ctx.restore(); +} + + +TrackChords.prototype.getModifiedElement = function(elem) +{ + var timeRange = elem.chord.timeRange.clone(); + var rootMidiPitch = elem.chord.rootMidiPitch; + + if (elem.selected) + { + if ((this.timeline.action & this.timeline.INTERACT_MOVE_TIME) != 0) + { + timeRange.start += this.timeline.actionMoveDeltaTime; + timeRange.end += this.timeline.actionMoveDeltaTime; + } + + if ((this.timeline.action & this.timeline.INTERACT_MOVE_PITCH) != 0) + rootMidiPitch = mod(rootMidiPitch + this.timeline.actionMoveDeltaPitch, 12); + + if ((this.timeline.action & this.timeline.INTERACT_STRETCH_TIME_L) != 0 || + (this.timeline.action & this.timeline.INTERACT_STRETCH_TIME_R) != 0) + { + timeRange.stretch( + this.timeline.actionStretchTimePivot, + this.timeline.actionStretchTimeOrigin, + this.timeline.actionMoveDeltaTime); + } + } + + return { + start: timeRange.start, + end: timeRange.end, + duration: timeRange.duration(), + rootMidiPitch: rootMidiPitch + }; +} + + +TrackChords.prototype.drawChord = function(elem) +{ + var that = this; + var ctx = this.timeline.ctx; + var toPixels = this.timeline.timeToPixelsScaling; + var modifiedElem = this.getModifiedElement(elem); + + ctx.fillStyle = "#ffffff" + ctx.globalAlpha = 0.75; + + // Draw chord background. + ctx.fillRect( + 0.5 + Math.floor(modifiedElem.start * toPixels), + 0, + Math.floor(modifiedElem.duration * toPixels - 1), + this.height); + + // Draw chord parts, one for each key region it overlaps. + this.timeline.trackKeys.enumerateKeysAtRange( + new TimeRange(modifiedElem.start, modifiedElem.end), + function (key, start, end) + { + var isLast = end == modifiedElem.end; + var x = 0.5 + Math.floor(start * toPixels); + var w = Math.floor((end - start) * toPixels - (isLast ? 1 : 0)) + + var degree = theory.pitchDegreeInKey(key.scaleIndex, key.rootMidiPitch, modifiedElem.rootMidiPitch); + if (Math.floor(degree) == degree) + { + ctx.fillStyle = theory.degreeColor(degree); + ctx.fillRect(x, 0.5, w, 5); + ctx.fillRect(x, that.height - 5, w, 5); + } + else + { + var color1 = theory.degreeColor(Math.floor(degree)); + var color2 = theory.degreeColor(Math.ceil(degree)); + drawStripedRect(ctx, x, 0.5, w, 5, color1, color2); + drawStripedRect(ctx, x, that.height - 5, w, 5, color1, color2); + } + + // Draw roman symbol. + var mainSymbol = theory.chordSymbolInKey( + key.scaleIndex, key.rootMidiPitch, modifiedElem.rootMidiPitch, + theory.chords[elem.chord.chordIndex].uppercase); + + ctx.fillStyle = "#000000"; + ctx.textAlign = "center"; + ctx.textBaseline = "middle"; + + ctx.font = "20px Tahoma"; + var supTextWidth = ctx.measureText(theory.chords[elem.chord.chordIndex].symbolSup).width; + var subTextWidth = ctx.measureText(theory.chords[elem.chord.chordIndex].symbolSub).width; + + ctx.font = "30px Tahoma"; + var mainTextWidth = ctx.measureText(mainSymbol).width; + var totalTextWidth = mainTextWidth + supTextWidth + subTextWidth; + + var maxTextWidth = w - 2; + if (totalTextWidth > maxTextWidth) + { + var proportion = totalTextWidth / maxTextWidth; + supTextWidth /= proportion; + subTextWidth /= proportion; + mainTextWidth /= proportion; + totalTextWidth = mainTextWidth + supTextWidth + subTextWidth; + } + + ctx.fillText( + mainSymbol, + x + w / 2 - totalTextWidth / 2 + mainTextWidth / 2, + that.height / 2, + maxTextWidth - supTextWidth - subTextWidth); + + ctx.font = "20px Tahoma"; + ctx.fillText( + theory.chords[elem.chord.chordIndex].symbolSup, + x + w / 2 - totalTextWidth / 2 + mainTextWidth + supTextWidth / 2, + that.height / 2 - 10, + maxTextWidth - mainTextWidth - subTextWidth); + + ctx.fillText( + theory.chords[elem.chord.chordIndex].symbolSub, + x + w / 2 - totalTextWidth / 2 + mainTextWidth + supTextWidth + subTextWidth / 2, + that.height / 2 + 10, + maxTextWidth - mainTextWidth - supTextWidth); + }); + + // Draw hover white-fade overlay. + if (elem == this.timeline.hoverElement || (elem.selected && this.timeline.mouseDown)) + { + ctx.fillStyle = "#ffffff" + ctx.globalAlpha = 0.35; + + ctx.fillRect( + 0.5 + Math.floor(modifiedElem.start * toPixels), + 0, + Math.floor(modifiedElem.duration * toPixels - 1), + this.height); + } + + // Draw selected double-stripe overlay. + if (elem.selected) + { + ctx.fillStyle = "#ffffff" + ctx.globalAlpha = 0.35; + + ctx.fillRect( + 0.5 + Math.floor(modifiedElem.start * toPixels), + 3, + Math.floor(modifiedElem.duration * toPixels - 1), + this.height - 6); + } + + ctx.globalAlpha = 1; +} \ No newline at end of file diff --git a/src/timeline/track_keys.js b/src/timeline/track_keys.js new file mode 100644 index 0000000..19ab66d --- /dev/null +++ b/src/timeline/track_keys.js @@ -0,0 +1,247 @@ +function TrackKeys(timeline) +{ + this.timeline = timeline; + this.elements = new ListByTimeRange(); + this.keys = new ListByTime(); + + this.KNOB_WIDTH = 10; + this.TEXT_MAX_WIDTH = 180; +} + + +TrackKeys.prototype = new Track(); + + +TrackKeys.prototype.setSong = function(song) +{ + this.elements.clear(); + this.keys.clear(); + this.selectedElements = []; + + for (var i = 0; i < song.keys.length; i++) + { + this.clipRange(new TimeRange(song.keys[i].time, song.keys[i].time)); + this._keyAdd(song.keys[i]); + } + + this.sanitize(); +} + + +TrackKeys.prototype.enumerateKeysAtRange = function(timeRange, callback) +{ + var that = this; + this.keys.enumerateAffectingRange(timeRange, function (elem, start, end) + { + if (elem == null) + return; + + callback(elem.key, start, end); + }); +} + + +TrackKeys.prototype.sanitize = function() +{ + if (this.keys.getItemAffectingTime(0) == null) + this._keyAdd(new Key(0, 0, theory.C)); +} + + +TrackKeys.prototype._keyAdd = function(key) +{ + var elem = new Element(); + elem.track = this; + elem.key = key.clone(); + + this.elementRefresh(elem); + this.elements.add(elem); + this.keys.add(elem); + this.timeline.markDirtyElement(elem); +} + + +TrackKeys.prototype.clipRange = function(timeRange) +{ + // Check for overlapping key changes and clip them. + var overlapping = []; + + this.elements.enumerateOverlappingRange( + new TimeRange(timeRange.start - 1, timeRange.end + 1), + function (elem) + { + if (timeRange.overlapsTime(elem.key.time) || + (timeRange.start == timeRange.end && elem.key.time == timeRange.start)) + overlapping.push(elem); + }); + + for (var i = 0; i < overlapping.length; i++) + { + this.elementRemove(overlapping[i]); + } +} + + +TrackKeys.prototype.applyModifications = function() +{ + for (var i = 0; i < this.modifiedElements.length; i++) + { + var elem = this.modifiedElements[i]; + + this.clipRange(new TimeRange(elem.key.time, elem.key.time)); + this.elementRefresh(elem); + this.elements.add(elem); + this.keys.add(elem); + } + + this.sanitize(); + + this.modifiedElements = []; + this.timeline.markDirtyAll(); +} + + +TrackKeys.prototype.elementRemove = function(elem) +{ + this.timeline.markDirtyAll(); + this.elements.remove(elem); + this.keys.remove(elem); +} + + +TrackKeys.prototype.elementModify = function(elem) +{ + this.elements.remove(elem); + this.keys.remove(elem); + + var modifiedElem = this.getModifiedElement(elem); + + elem.key = new Key( + modifiedElem.time, + elem.key.scaleIndex, + elem.key.rootMidiPitch); + + this.modifiedElements.push(elem); +} + + +TrackKeys.prototype.elementRefresh = function(elem) +{ + var toPixels = this.timeline.timeToPixelsScaling; + + elem.time = elem.key.time; + elem.timeRange = new TimeRange( + elem.key.time - this.KNOB_WIDTH / 2 / toPixels, + elem.key.time + (this.KNOB_WIDTH / 2 + this.TEXT_MAX_WIDTH) / toPixels); + + elem.interactTimeRange = new TimeRange(elem.key.time, elem.key.time); + elem.interactKind = this.timeline.INTERACT_MOVE_TIME | + this.timeline.INTERACT_STRETCH_TIME_L | this.timeline.INTERACT_STRETCH_TIME_R; + + elem.regions = [ + { + kind: this.timeline.INTERACT_MOVE_TIME, + x: elem.key.time * toPixels - this.KNOB_WIDTH / 2, + y: 0, + width: this.KNOB_WIDTH, + height: this.height + } + ]; +} + + +TrackKeys.prototype.relayout = function() +{ + var that = this; + + this.elements.enumerateAll(function (elem) + { that.elementRefresh(elem); }); +} + + +TrackKeys.prototype.redraw = function(time1, time2) +{ + var that = this; + var ctx = this.timeline.ctx; + var toPixels = this.timeline.timeToPixelsScaling; + + ctx.save(); + + ctx.translate(0.5, 0.5); + + // Draw meter changes. + this.elements.enumerateOverlappingRange(new TimeRange(time1, time2), function (elem) + { that.drawKey(elem); }); + + for (var i = 0; i < this.selectedElements.length; i++) + this.drawKey(this.selectedElements[i]); + + ctx.restore(); +} + + +TrackKeys.prototype.getModifiedElement = function(elem) +{ + var time = elem.key.time; + + if (elem.selected) + { + if ((this.timeline.action & this.timeline.INTERACT_MOVE_TIME) != 0) + time += this.timeline.actionMoveDeltaTime; + + if ((this.timeline.action & this.timeline.INTERACT_STRETCH_TIME_L) != 0 || + (this.timeline.action & this.timeline.INTERACT_STRETCH_TIME_R) != 0) + { + time = stretch( + time, + this.timeline.actionStretchTimePivot, + this.timeline.actionStretchTimeOrigin, + this.timeline.actionMoveDeltaTime); + } + } + + return { + time: time + }; +} + + +TrackKeys.prototype.drawKey = function(elem) +{ + var ctx = this.timeline.ctx; + var toPixels = this.timeline.timeToPixelsScaling; + + var modifiedElem = this.getModifiedElement(elem); + + var col = "#ff0088"; + if (elem.selected) + col = "#880022"; + else if (this.timeline.hoverElement == elem) + col = "#ff00cc"; + + ctx.fillStyle = col; + ctx.strokeStyle = col; + + var x = Math.floor(modifiedElem.time * toPixels); + + ctx.fillRect( + Math.floor(x - this.KNOB_WIDTH / 2) - 0.5, + 0.5, + this.KNOB_WIDTH, + this.height - 1); + + ctx.beginPath(); + ctx.moveTo(x, this.height / 2); + ctx.lineTo(x, this.timeline.lastTrackBottomY - this.y); + ctx.stroke(); + + ctx.font = "12px Verdana"; + ctx.textAlign = "start"; + ctx.textBaseline = "middle"; + + ctx.fillText( + theory.pitchNameInScale(elem.key.scaleIndex, elem.key.rootMidiPitch) + + " " + theory.scales[elem.key.scaleIndex].name, + x + 10, + this.height / 2); +} diff --git a/src/timeline/track_length.js b/src/timeline/track_length.js new file mode 100644 index 0000000..00a3236 --- /dev/null +++ b/src/timeline/track_length.js @@ -0,0 +1,209 @@ +function TrackLength(timeline) +{ + this.timeline = timeline; + this.elements = new ListByTimeRange(); + + this.LENGTH_KNOB_WIDTH = 10; + this.lengthKnob = null; +} + + +TrackLength.prototype = new Track(); + + +TrackLength.prototype.setSong = function(song) +{ + this.elements.clear(); + + this.lengthKnob = new Element(); + this.lengthKnob.track = this; + this.lengthKnob.length = 0; + + this.elementRefresh(this.lengthKnob); + this.elements.add(this.lengthKnob); +} + + +TrackLength.prototype.elementModify = function(elem) +{ + this.elements.remove(elem); + + var modifiedElem = this.getModifiedLengthKnob(elem); + + elem.length = modifiedElem.length; + + if (modifiedElem.length < this.timeline.length) + { + // Clip elements that are now out of the song's bounds. + for (var i = 0; i < this.timeline.tracks.length; i++) + { + this.timeline.tracks[i].clipRange(new TimeRange(modifiedElem.length, this.timeline.length + 1)); + this.timeline.tracks[i].sanitize(); + } + } + + this.timeline.length = modifiedElem.length; + + this.elementRefresh(elem); + this.elements.add(elem); + + this.timeline.markDirtyAll(); +} + + +TrackLength.prototype.elementRefresh = function(elem) +{ + var toPixels = this.timeline.timeToPixelsScaling; + + elem.length = this.timeline.length; + + elem.timeRange = new TimeRange( + elem.length - this.LENGTH_KNOB_WIDTH / 2 / toPixels, + elem.length + this.LENGTH_KNOB_WIDTH / 2 / toPixels); + + elem.interactKind = this.timeline.INTERACT_MOVE_TIME | + this.timeline.INTERACT_STRETCH_TIME_L | this.timeline.INTERACT_STRETCH_TIME_R; + + elem.regions = [ + { + kind: this.timeline.INTERACT_MOVE_TIME, + x: elem.length * toPixels - this.LENGTH_KNOB_WIDTH / 2, + y: 0, + width: this.LENGTH_KNOB_WIDTH, + height: this.height + } + ]; +} + + +TrackLength.prototype.relayout = function() +{ + var that = this; + + this.elements.enumerateAll(function (elem) + { that.elementRefresh(elem); }); +} + + +TrackLength.prototype.redraw = function(time1, time2) +{ + var that = this; + var ctx = this.timeline.ctx; + var toPixels = this.timeline.timeToPixelsScaling; + + ctx.save(); + + ctx.translate(0.5, 0.5); + + // Draw track line. + ctx.strokeStyle = "#444444"; + ctx.beginPath(); + ctx.moveTo(0, this.height / 2); + ctx.lineTo(this.timeline.length * toPixels, this.height / 2); + ctx.stroke(); + + ctx.strokeStyle = "#aaaaaa"; + ctx.beginPath(); + ctx.moveTo(this.timeline.length * toPixels, this.height / 2); + ctx.lineTo(this.timeline.lastTimeVisible * toPixels, this.height / 2); + ctx.stroke(); + + // Draw beat markers. + this.timeline.trackMeters.enumerateBeatsAtRange(new TimeRange(time1, time2), function (time, isStrong) + { + var x = Math.floor(time * toPixels); + var h = (isStrong ? 4 : 2); + + if (x > that.timeline.length) + ctx.strokeStyle = "#aaaaaa"; + else + ctx.strokeStyle = "#444444"; + + ctx.beginPath(); + ctx.moveTo(x, that.height / 2 - h); + ctx.lineTo(x, that.height / 2 + h); + ctx.stroke(); + }); + + // Draw length knob. + this.drawLengthKnob(this.lengthKnob); + + ctx.restore(); +} + + +TrackLength.prototype.getModifiedLengthKnob = function(elem) +{ + var length = elem.length; + + if (elem.selected) + { + if ((this.timeline.action & this.timeline.INTERACT_MOVE_TIME) != 0) + length += this.timeline.actionMoveDeltaTime; + + if ((this.timeline.action & this.timeline.INTERACT_STRETCH_TIME_L) != 0 || + (this.timeline.action & this.timeline.INTERACT_STRETCH_TIME_R) != 0) + { + length = stretch( + length, + this.timeline.actionStretchTimePivot, + this.timeline.actionStretchTimeOrigin, + this.timeline.actionMoveDeltaTime); + } + } + + length = + Math.max(0, + Math.min(this.timeline.MAX_VALID_LENGTH, + length)); + + return { + length: length + }; +} + + + +TrackLength.prototype.drawLengthKnob = function(elem) +{ + var ctx = this.timeline.ctx; + var toPixels = this.timeline.timeToPixelsScaling; + + var modifiedKnob = this.getModifiedLengthKnob(elem); + + var col = "#000000"; + if (elem.selected) + col = "#888888"; + else if (this.timeline.hoverElement == elem) + col = "#aaaaaa"; + + ctx.fillStyle = col; + ctx.strokeStyle = col; + + var x = Math.floor(modifiedKnob.length * toPixels); + + ctx.fillRect( + Math.floor(x - this.LENGTH_KNOB_WIDTH / 2) - 0.5, + 0.5, + this.LENGTH_KNOB_WIDTH, + this.height - 1); + + ctx.beginPath(); + ctx.moveTo(x, this.height / 2); + ctx.lineTo(x, this.timeline.lastTrackBottomY - this.y); + ctx.stroke(); + + // Draw create/delete overlay. + if (modifiedKnob.length < this.timeline.length) + ctx.fillStyle = "#ff0000"; + else + ctx.fillStyle = "#0000ff"; + + ctx.globalAlpha = 0.1; + ctx.fillRect( + Math.min(this.timeline.length, modifiedKnob.length) * toPixels, + this.height / 2, + Math.abs(this.timeline.length - modifiedKnob.length) * toPixels, + this.timeline.lastTrackBottomY - this.height / 2 - this.y); + ctx.globalAlpha = 1; +} diff --git a/src/timeline/track_meters.js b/src/timeline/track_meters.js new file mode 100644 index 0000000..f6637af --- /dev/null +++ b/src/timeline/track_meters.js @@ -0,0 +1,264 @@ +function TrackMeters(timeline) +{ + this.timeline = timeline; + this.elements = new ListByTimeRange(); + this.meters = new ListByTime(); + + this.KNOB_WIDTH = 10; + this.TEXT_MAX_WIDTH = 60; +} + + +TrackMeters.prototype = new Track(); + + +TrackMeters.prototype.setSong = function(song) +{ + this.elements.clear(); + this.meters.clear(); + this.selectedElements = []; + + for (var i = 0; i < song.meters.length; i++) + { + this.clipRange(new TimeRange(song.meters[i].time, song.meters[i].time)); + this._meterAdd(song.meters[i]); + } + + this.sanitize(); +} + + +TrackMeters.prototype.enumerateMetersAtRange = function(timeRange, callback) +{ + this.meters.enumerateAffectingRange(timeRange, function (elem, start, end) + { + if (elem == null) + return; + + callback(elem.meter, start, end); + }); +} + + +TrackMeters.prototype.enumerateBeatsAtRange = function(timeRange, callback) +{ + var that = this; + this.meters.enumerateAffectingRange(timeRange, function (elem, start, end) + { + if (elem == null) + return; + + // TODO: Only consider beats contained in the time range. + var beatCount = 0; + for (var t = elem.meter.time; t < end; t += that.timeline.TIME_PER_WHOLE_NOTE / elem.meter.denominator) + { + callback(t, beatCount == 0); + beatCount = (beatCount + 1) % elem.meter.numerator; + } + }); +} + + +TrackMeters.prototype.sanitize = function() +{ + if (this.meters.getItemAffectingTime(0) == null) + this._meterAdd(new Meter(0, 4, 4)); +} + + +TrackMeters.prototype._meterAdd = function(meter) +{ + var elem = new Element(); + elem.track = this; + elem.meter = meter.clone(); + + this.elementRefresh(elem); + this.elements.add(elem); + this.meters.add(elem); + this.timeline.markDirtyElement(elem); +} + + +TrackMeters.prototype.clipRange = function(timeRange) +{ + // Check for overlapping meter changes and clip them. + var overlapping = []; + + this.elements.enumerateOverlappingRange( + new TimeRange(timeRange.start - 1, timeRange.end + 1), + function (elem) + { + if (timeRange.overlapsTime(elem.meter.time) || + (timeRange.start == timeRange.end && elem.meter.time == timeRange.start)) + overlapping.push(elem); + }); + + for (var i = 0; i < overlapping.length; i++) + { + this.elementRemove(overlapping[i]); + } +} + + +TrackMeters.prototype.applyModifications = function() +{ + for (var i = 0; i < this.modifiedElements.length; i++) + { + var elem = this.modifiedElements[i]; + + this.clipRange(new TimeRange(elem.meter.time, elem.meter.time)); + this.elementRefresh(elem); + this.elements.add(elem); + this.meters.add(elem); + } + + this.sanitize(); + + this.modifiedElements = []; + this.timeline.markDirtyAll(); +} + + +TrackMeters.prototype.elementRemove = function(elem) +{ + this.timeline.markDirtyAll(); + this.elements.remove(elem); + this.meters.remove(elem); +} + + +TrackMeters.prototype.elementModify = function(elem) +{ + this.elements.remove(elem); + this.meters.remove(elem); + + var modifiedElem = this.getModifiedElement(elem); + + elem.meter = new Meter( + modifiedElem.time, + elem.meter.numerator, + elem.meter.denominator); + + this.modifiedElements.push(elem); +} + + +TrackMeters.prototype.elementRefresh = function(elem) +{ + var toPixels = this.timeline.timeToPixelsScaling; + + elem.time = elem.meter.time; + elem.timeRange = new TimeRange( + elem.meter.time - this.KNOB_WIDTH / 2 / toPixels, + elem.meter.time + (this.KNOB_WIDTH / 2 + this.TEXT_MAX_WIDTH) / toPixels); + + elem.interactTimeRange = new TimeRange(elem.meter.time, elem.meter.time); + elem.interactKind = this.timeline.INTERACT_MOVE_TIME | + this.timeline.INTERACT_STRETCH_TIME_L | this.timeline.INTERACT_STRETCH_TIME_R; + + elem.regions = [ + { + kind: this.timeline.INTERACT_MOVE_TIME, + x: elem.meter.time * toPixels - this.KNOB_WIDTH / 2, + y: 0, + width: this.KNOB_WIDTH, + height: this.height + } + ]; +} + + +TrackMeters.prototype.relayout = function() +{ + var that = this; + + this.elements.enumerateAll(function (elem) + { that.elementRefresh(elem); }); +} + + +TrackMeters.prototype.redraw = function(time1, time2) +{ + var that = this; + var ctx = this.timeline.ctx; + var toPixels = this.timeline.timeToPixelsScaling; + + ctx.save(); + + ctx.translate(0.5, 0.5); + + // Draw meter changes. + this.elements.enumerateOverlappingRange(new TimeRange(time1, time2), function (elem) + { that.drawMeter(elem); }); + + for (var i = 0; i < this.selectedElements.length; i++) + this.drawMeter(this.selectedElements[i]); + + ctx.restore(); +} + + +TrackMeters.prototype.getModifiedElement = function(elem) +{ + var time = elem.meter.time; + + if (elem.selected) + { + if ((this.timeline.action & this.timeline.INTERACT_MOVE_TIME) != 0) + time += this.timeline.actionMoveDeltaTime; + + if ((this.timeline.action & this.timeline.INTERACT_STRETCH_TIME_L) != 0 || + (this.timeline.action & this.timeline.INTERACT_STRETCH_TIME_R) != 0) + { + time = stretch( + time, + this.timeline.actionStretchTimePivot, + this.timeline.actionStretchTimeOrigin, + this.timeline.actionMoveDeltaTime); + } + } + + return { + time: time + }; +} + + +TrackMeters.prototype.drawMeter = function(elem) +{ + var ctx = this.timeline.ctx; + var toPixels = this.timeline.timeToPixelsScaling; + + var modifiedElem = this.getModifiedElement(elem); + + var col = "#0088ff"; + if (elem.selected) + col = "#002288"; + else if (this.timeline.hoverElement == elem) + col = "#00ccff"; + + ctx.fillStyle = col; + ctx.strokeStyle = col; + + var x = Math.floor(modifiedElem.time * toPixels); + + ctx.fillRect( + Math.floor(x - this.KNOB_WIDTH / 2) - 0.5, + 0.5, + this.KNOB_WIDTH, + this.height - 1); + + ctx.beginPath(); + ctx.moveTo(x, this.height / 2); + ctx.lineTo(x, this.timeline.lastTrackBottomY - this.y); + ctx.stroke(); + + ctx.font = "12px Verdana"; + ctx.textAlign = "start"; + ctx.textBaseline = "middle"; + + ctx.fillText( + "" + elem.meter.numerator + " / " + elem.meter.denominator, + x + 10, + this.height / 2); +} diff --git a/src/timeline/track_notes.js b/src/timeline/track_notes.js new file mode 100644 index 0000000..9505f43 --- /dev/null +++ b/src/timeline/track_notes.js @@ -0,0 +1,489 @@ +function TrackNotes(timeline) +{ + this.timeline = timeline; + this.elements = new ListByTimeRange(); +} + + +TrackNotes.prototype = new Track(); + + +TrackNotes.prototype.setSong = function(song) +{ + this.elements.clear(); + this.selectedElements = []; + + for (var i = 0; i < song.notes.length; i++) + this.noteAdd(song.notes[i]); + + this.setScrollPitchAtCenter(12 * 5 + theory.F + 0.5); +} + + +TrackNotes.prototype.noteAdd = function(note) +{ + this.clipRangeWithPitch(note.timeRange, note.pitch); + + var elem = new Element(); + elem.track = this; + elem.note = note.clone(); + + elem.note.timeRange.clip(0, this.timeline.length); + if (elem.note.timeRange.duration() <= 0) + return null; + + this.elementRefresh(elem); + this.elements.add(elem); + this.timeline.markDirtyElement(elem); + + return elem; +} + + +TrackNotes.prototype.clipRangeWithPitch = function(timeRange, pitch) +{ + // Check for overlapping notes and clip them. + var overlapping = []; + + this.elements.enumerateOverlappingRange(timeRange, function (elem) + { + if (pitch == null || elem.note.pitch.midiPitch == pitch.midiPitch) + overlapping.push(elem); + }); + + for (var i = 0; i < overlapping.length; i++) + this.elements.remove(overlapping[i]); + + for (var i = 0; i < overlapping.length; i++) + { + var elem = overlapping[i]; + + var parts = elem.note.timeRange.getClippedParts(timeRange); + for (var p = 0; p < parts.length; p++) + { + var clippedNote = elem.note.clone(); + clippedNote.timeRange = parts[p]; + this.noteAdd(clippedNote); + } + } +} + + +TrackNotes.prototype.clipRange = function(timeRange) +{ + this.clipRangeWithPitch(timeRange, null); +} + + +TrackNotes.prototype.handleScroll = function() +{ + this.scrollY = this.getModifiedScrollY(); + this.timeline.markDirtyAll(); +} + + +TrackNotes.prototype.getScrollPitchAtBottom = function() +{ + return this.timeline.MIN_VALID_MIDI_PITCH + this.scrollY / this.timeline.noteHeight; +} + + +TrackNotes.prototype.getScrollPitchAtTop = function() +{ + return this.getScrollPitchAtBottom() + this.height / this.timeline.noteHeight; +} + + +TrackNotes.prototype.setScrollPitchAtBottom = function(pitch) +{ + var noteHeight = this.timeline.noteHeight; + var minPitch = this.timeline.MIN_VALID_MIDI_PITCH; + var maxPitch = this.timeline.MAX_VALID_MIDI_PITCH; + + this.scrollY = + Math.floor( + Math.max(0, + Math.min((maxPitch + 1 - minPitch) * noteHeight - this.height, + (pitch - minPitch) * noteHeight))); + + this.timeline.markDirtyAll(); +} + + +TrackNotes.prototype.setScrollPitchAtCenter = function(pitch) +{ + this.setScrollPitchAtBottom(pitch + 0.5 - this.height / this.timeline.noteHeight / 2); +} + + +TrackNotes.prototype.scrollPitchIntoView = function(pitch) +{ + var noteHeight = this.timeline.noteHeight; + var minPitch = this.timeline.MIN_VALID_MIDI_PITCH; + var maxPitch = this.timeline.MAX_VALID_MIDI_PITCH; + + if (pitch < this.getScrollPitchAtBottom()) + this.setScrollPitchAtBottom(pitch - 4); + + else if (pitch + 1 > this.getScrollPitchAtTop()) + this.setScrollPitchAtBottom(pitch + 4 - this.height / noteHeight); +} + + +TrackNotes.prototype.applyModifications = function() +{ + for (var i = 0; i < this.modifiedElements.length; i++) + { + var elem = this.modifiedElements[i]; + this.clipRangeWithPitch(elem.note.timeRange, elem.note.pitch); + elem.note.timeRange.clip(0, this.timeline.length); + } + + for (var i = 0; i < this.modifiedElements.length; i++) + { + var elem = this.modifiedElements[i]; + if (elem.note.timeRange.duration() <= 0) + { + this.timeline.unselect(elem); + continue; + } + + this.elementRefresh(elem); + this.elements.add(elem); + this.timeline.markDirtyElement(elem); + } + + this.modifiedElements = []; +} + + +TrackNotes.prototype.elementModify = function(elem) +{ + this.elements.remove(elem); + + var modifiedElem = this.getModifiedElement(elem); + + elem.note = new Note( + new TimeRange(modifiedElem.start, modifiedElem.start + modifiedElem.duration), + new Pitch(modifiedElem.pitch)); + + this.modifiedElements.push(elem); +} + + +TrackNotes.prototype.elementRefresh = function(elem) +{ + var toPixels = this.timeline.timeToPixelsScaling; + var noteHeight = this.timeline.noteHeight; + var minPitch = this.timeline.MIN_VALID_MIDI_PITCH; + + elem.timeRange = elem.note.timeRange.clone(); + elem.interactTimeRange = elem.note.timeRange.clone(); + elem.interactPitch = elem.note.pitch.clone(); + elem.interactKind = + this.timeline.INTERACT_MOVE_TIME | this.timeline.INTERACT_MOVE_PITCH | + this.timeline.INTERACT_STRETCH_TIME_L | this.timeline.INTERACT_STRETCH_TIME_R; + + elem.regions = [ + { + kind: this.timeline.INTERACT_MOVE_TIME | this.timeline.INTERACT_MOVE_PITCH, + x: elem.note.timeRange.start * toPixels, + y: this.height - noteHeight * (elem.note.pitch.midiPitch - minPitch) - noteHeight, + width: elem.note.timeRange.duration() * toPixels, + height: noteHeight - 1 + }, + { + kind: this.timeline.INTERACT_STRETCH_TIME_L, + x: elem.note.timeRange.start * toPixels - 4, + y: this.height - noteHeight * (elem.note.pitch.midiPitch - minPitch) - noteHeight, + width: 4, + height: noteHeight - 1 + }, + { + kind: this.timeline.INTERACT_STRETCH_TIME_R, + x: elem.note.timeRange.end * toPixels, + y: this.height - noteHeight * (elem.note.pitch.midiPitch - minPitch) - noteHeight, + width: 4, + height: noteHeight - 1 + } + ]; +} + + +TrackNotes.prototype.relayout = function() +{ + var that = this; + + this.elements.enumerateAll(function (elem) + { that.elementRefresh(elem); }); +} + + +TrackNotes.prototype.redraw = function(time1, time2) +{ + var that = this; + var ctx = this.timeline.ctx; + var toPixels = this.timeline.timeToPixelsScaling; + var noteHeight = this.timeline.noteHeight; + var minPitch = this.timeline.MIN_VALID_MIDI_PITCH; + var maxPitch = this.timeline.MAX_VALID_MIDI_PITCH; + var scrollY = this.getModifiedScrollY(); + + var xMin = Math.floor(Math.max(0, time1) * toPixels); + var xLen = Math.floor(this.timeline.length * toPixels); + var xMax = Math.floor(time2 * toPixels); + + ctx.save(); + + ctx.beginPath(); + ctx.rect(xMin - 10, 0, xMax - xMin + 20, this.height + 1); + ctx.clip(); + + ctx.translate(0.5, 0.5); + + ctx.save(); + ctx.translate(0, scrollY); + + // Draw pitch rows. + this.timeline.trackKeys.enumerateKeysAtRange(new TimeRange(time1, time2), function (key, start, end) + { + for (var i = minPitch; i <= maxPitch; i++) + { + var degree = theory.pitchDegreeInKey(key.scaleIndex, key.rootMidiPitch, i); + + switch (degree) + { + case 0: ctx.fillStyle = "#ffdddd"; break; + case 1: ctx.fillStyle = "#ffeedd"; break; + case 2: ctx.fillStyle = "#ffffdd"; break; + case 3: ctx.fillStyle = "#ddeedd"; break; + case 4: ctx.fillStyle = "#ddddff"; break; + case 5: ctx.fillStyle = "#eeddff"; break; + case 6: ctx.fillStyle = "#ffddff"; break; + default: ctx.fillStyle = "#e4e4e4"; break; + } + + ctx.fillRect( + start * toPixels, + that.height - noteHeight * (i - minPitch + 1), + (end - start) * toPixels, + noteHeight - 0.5); + } + }); + + // Draw pitch rows after song length. + if (time2 > this.timeline.length) + { + for (var i = minPitch; i <= maxPitch; i++) + { + ctx.fillStyle = "#cccccc"; + ctx.fillRect( + this.timeline.length * toPixels, + this.height - noteHeight * (i - minPitch + 1), + (time2 - this.timeline.length) * toPixels, + noteHeight - 0.5); + } + } + + // Draw beat lines. + ctx.strokeStyle = "#ffffff"; + ctx.beginPath(); + this.timeline.trackMeters.enumerateBeatsAtRange(new TimeRange(time1, time2), function (time, isStrong) + { + var x = Math.floor(time * toPixels); + + ctx.moveTo(x, that.height - noteHeight * (maxPitch + 1 - minPitch)); + ctx.lineTo(x, that.height); + + if (isStrong) + { + ctx.moveTo(x - 1, that.height - noteHeight * (maxPitch + 1 - minPitch)); + ctx.lineTo(x - 1, that.height); + ctx.moveTo(x + 1, that.height - noteHeight * (maxPitch + 1 - minPitch)); + ctx.lineTo(x + 1, that.height); + } + }); + ctx.stroke(); + + // Draw root pitch markers. + this.timeline.trackKeys.enumerateKeysAtRange(new TimeRange(time1, time2), function (key, start, end) + { + for (var i = Math.floor(minPitch / 12) * 12 + key.rootMidiPitch; i <= maxPitch; i += 12) + { + var y = that.height - noteHeight * (i - minPitch); + + ctx.strokeStyle = "#000000"; + ctx.beginPath(); + ctx.moveTo(start * toPixels, y); + ctx.lineTo( end * toPixels, y); + + if (i == 5 * 12 + key.rootMidiPitch) + { + ctx.moveTo(start * toPixels, y - 1); + ctx.lineTo( end * toPixels, y - 1); + } + else if (i == 6 * 12 + key.rootMidiPitch) + { + ctx.moveTo(start * toPixels, y + 1); + ctx.lineTo( end * toPixels, y + 1); + } + + ctx.stroke(); + } + }); + + // Draw notes. + this.elements.enumerateOverlappingRange(new TimeRange(time1, time2), function (elem) + { that.drawNote(elem); }); + + for (var i = 0; i < this.selectedElements.length; i++) + this.drawNote(this.selectedElements[i]); + + ctx.restore(); + + // Draw borders. + ctx.strokeStyle = "#000000"; + ctx.beginPath(); + ctx.moveTo(xMin, 0); + ctx.lineTo(xMax, 0); + + ctx.moveTo(xMin, this.height); + ctx.lineTo(xMax, this.height); + + ctx.moveTo(0, 0); + ctx.lineTo(0, this.height); + + ctx.moveTo(xLen, 0); + ctx.lineTo(xLen, this.height); + ctx.stroke(); + + ctx.restore(); +} + + +TrackNotes.prototype.getModifiedElement = function(elem) +{ + var timeRange = elem.note.timeRange.clone(); + var pitch = elem.note.pitch.midiPitch; + + if (elem.selected) + { + if ((this.timeline.action & this.timeline.INTERACT_MOVE_TIME) != 0) + { + timeRange.start += this.timeline.actionMoveDeltaTime; + timeRange.end += this.timeline.actionMoveDeltaTime; + } + + if ((this.timeline.action & this.timeline.INTERACT_MOVE_PITCH) != 0) + pitch += this.timeline.actionMoveDeltaPitch; + + if ((this.timeline.action & this.timeline.INTERACT_STRETCH_TIME_L) != 0 || + (this.timeline.action & this.timeline.INTERACT_STRETCH_TIME_R) != 0) + { + timeRange.stretch( + this.timeline.actionStretchTimePivot, + this.timeline.actionStretchTimeOrigin, + this.timeline.actionMoveDeltaTime); + } + } + + return { + start: timeRange.start, + end: timeRange.end, + duration: timeRange.duration(), + pitch: pitch + }; +} + + +TrackNotes.prototype.getModifiedScrollY = function() +{ + var noteHeight = this.timeline.noteHeight; + var minPitch = this.timeline.MIN_VALID_MIDI_PITCH; + var maxPitch = this.timeline.MAX_VALID_MIDI_PITCH; + + var scrollY = this.scrollY; + + if (this.timeline.mouseDownTrack == this && + this.timeline.action == this.timeline.INTERACT_SCROLL) + { + scrollY += this.timeline.mouseMoveScrollY; + scrollY = + Math.max(0, + Math.min((maxPitch + 1 - minPitch) * noteHeight - this.height, + scrollY)); + } + + return scrollY; +} + + +TrackNotes.prototype.drawNote = function(elem) +{ + var that = this; + var ctx = this.timeline.ctx; + var toPixels = this.timeline.timeToPixelsScaling; + var noteHeight = this.timeline.noteHeight; + var minPitch = this.timeline.MIN_VALID_MIDI_PITCH; + var scrollY = this.getModifiedScrollY(); + var modifiedElem = this.getModifiedElement(elem); + + var y = 0.5 + that.height - noteHeight * (modifiedElem.pitch - minPitch); + + // Clamp Y position for out-of-view peek. + y = + Math.max(-scrollY + 3, + Math.min(that.height - scrollY + noteHeight - 2, + y)); + + // Draw note parts, one for each key region it overlaps. + this.timeline.trackKeys.enumerateKeysAtRange( + new TimeRange(modifiedElem.start, modifiedElem.end), + function (key, start, end) + { + var isLast = end == modifiedElem.end; + var x = 0.5 + Math.floor(start * toPixels); + var w = Math.floor((end - start) * toPixels - (isLast ? 1 : 0)) + + var degree = theory.pitchDegreeInKey(key.scaleIndex, key.rootMidiPitch, modifiedElem.pitch); + if (Math.floor(degree) == degree) + { + ctx.fillStyle = theory.degreeColor(degree); + ctx.fillRect(x, y - noteHeight, w, noteHeight - 1); + } + else + { + var color1 = theory.degreeColor(Math.floor(degree)); + var color2 = theory.degreeColor(Math.ceil(degree)); + drawStripedRect(ctx, x, y - noteHeight, w, noteHeight - 1, color1, color2); + } + }); + + // Draw hover white-fade overlay. + if (elem == this.timeline.hoverElement || (elem.selected && this.timeline.mouseDown)) + { + ctx.fillStyle = "#ffffff" + ctx.globalAlpha = 0.35; + + ctx.fillRect( + 0.5 + Math.floor(modifiedElem.start * toPixels), + y - noteHeight, + Math.floor(modifiedElem.duration * toPixels - 1), + noteHeight - 1); + } + + // Draw selected double-stripe overlay. + if (elem.selected) + { + ctx.fillStyle = "#ffffff" + ctx.globalAlpha = 0.35; + + ctx.fillRect( + 0.5 + Math.floor(modifiedElem.start * toPixels), + y - noteHeight + 2, + Math.floor(modifiedElem.duration * toPixels - 1), + noteHeight - 1 - 4); + } + + ctx.globalAlpha = 1; +} \ No newline at end of file diff --git a/src/types.ts b/src/types.ts deleted file mode 100644 index e69de29..0000000 diff --git a/src/ui/Button.tsx b/src/ui/Button.tsx deleted file mode 100644 index 41bad1a..0000000 --- a/src/ui/Button.tsx +++ /dev/null @@ -1,29 +0,0 @@ -import React from "react" - - -interface ButtonProps -{ - label: any - onClick?: () => void - disabled?: boolean -} - - -export function Button(props: ButtonProps) -{ - return -} \ No newline at end of file diff --git a/src/ui/ButtonList.tsx b/src/ui/ButtonList.tsx deleted file mode 100644 index 72d2233..0000000 --- a/src/ui/ButtonList.tsx +++ /dev/null @@ -1,67 +0,0 @@ -import React from "react" - - -interface ButtonListProps -{ - column?: boolean - multiple?: boolean - selected: any - - items: - { - label: any - value: any - bkgColor?: string - width?: string - }[] - - onChange: (item: any, index: number) => void -} - - -export function ButtonList(props: ButtonListProps) -{ - const isSelected = (value: any) => - { - if (props.multiple && Array.isArray(props.selected)) - return props.selected.some(v => v == value) - else - return props.selected == value - } - - - return
- - { props.items.map((item, i) => - - )} - -
-} \ No newline at end of file diff --git a/src/ui/ChordButton.tsx b/src/ui/ChordButton.tsx deleted file mode 100644 index 3aee382..0000000 --- a/src/ui/ChordButton.tsx +++ /dev/null @@ -1,45 +0,0 @@ -import React from "react" -import * as Theory from "../theory" -import * as CanvasUtils from "../util/canvasUtils" - - -interface ChordButtonProps -{ - chord: Theory.Chord - theoryKey: Theory.Key - onClick?: () => void - disabled?: boolean -} - - -export function ChordButton(props: ChordButtonProps) -{ - const refCanvas = React.useRef(null!) - - const w = 60 - const h = 50 - - React.useEffect(() => - { - const ctx = refCanvas.current!.getContext("2d")! - CanvasUtils.renderChord(ctx, 0, 0, w, h, props.chord, props.theoryKey) - - }, [props.theoryKey, props.chord]) - - - return -} \ No newline at end of file diff --git a/src/ui/Dial.tsx b/src/ui/Dial.tsx deleted file mode 100644 index b457a62..0000000 --- a/src/ui/Dial.tsx +++ /dev/null @@ -1,160 +0,0 @@ -import React from "react" -import styled from "styled-components" - - -const StyledInput = styled.input` - background-color: transparent; - color: #fff; - border: 1px solid #888; - border-radius: 0.5em; - margin: 0 0.5em; - padding: 0.5em 0.5em; -` - - -export interface DialProps -{ - label?: string - getValue?: () => number - onChange?: (newValue: number) => void -} - - -export function Dial(props: DialProps) -{ - const value = props.getValue?.() ?? 0 - const [update, setUpdate] = React.useState(false) - - /* - const angle1 = (180 + 45) / 180 * Math.PI - const angle3 = (-45) / 180 * Math.PI - const angle2 = angle1 + (angle3 - angle1) * value - const size = 45 - const arcX1 = 50 + Math.cos(angle1) * size - const arcY1 = 50 - Math.sin(angle1) * size - const arcX2 = 50 + Math.cos(angle2) * size - const arcY2 = 50 - Math.sin(angle2) * size - const arcX3 = 50 + Math.cos(angle3) * size - const arcY3 = 50 - Math.sin(angle3) * size - */ - - const y1 = 90 - const y2 = 90 - 80 * value - const y3 = 10 - - - const refSvg = React.useRef(null) - const refState = React.useRef({ - mouseDown: false, - mouseY: 0, - valueOrig: value, - }) - - - React.useEffect(() => - { - const onMouseDown = (ev: MouseEvent) => - { - ev.preventDefault() - refState.current.mouseDown = true - refState.current.mouseY = ev.screenY - refState.current.valueOrig = props.getValue?.() ?? 0 - } - - const onMouseMove = (ev: MouseEvent) => - { - if (!refState.current.mouseDown) - return - - ev.preventDefault() - - const newValue = - refState.current.valueOrig + - (refState.current.mouseY - ev.screenY) / 100 - - props.onChange?.(newValue) - setUpdate((val) => !val) - } - - const onMouseUp = (ev: MouseEvent) => - { - refState.current.mouseDown = false - } - - const refSvgCurrent = refSvg.current - if (!refSvgCurrent) - return - - refSvgCurrent.addEventListener("mousedown", onMouseDown) - window.addEventListener("mousemove", onMouseMove) - window.addEventListener("mouseup", onMouseUp) - - return () => - { - refSvgCurrent.removeEventListener("mousedown", onMouseDown) - window.removeEventListener("mousemove", onMouseMove) - window.removeEventListener("mouseup", onMouseUp) - } - - }, []) - - - - return
- - - - - - { props.label } - - - - { Math.floor(value * 100) } - - - {/* - */} - -
-} \ No newline at end of file diff --git a/src/ui/DropdownMenu.tsx b/src/ui/DropdownMenu.tsx deleted file mode 100644 index 148fe1f..0000000 --- a/src/ui/DropdownMenu.tsx +++ /dev/null @@ -1,109 +0,0 @@ -import React from "react" - - -interface DropdownMenuProps -{ - selected: any - - items: - { - label: string - value?: any - subitems?: - { - label: string - value: any - }[] - }[] - - onChange?: (item: any, index: number) => void - - style?: any -} - - -export function DropdownMenu(props: DropdownMenuProps) -{ - //const selectedIndex = props.items.findIndex(i => i.value == props.selected) - const hasDefinite = props.selected !== null - - - return -} \ No newline at end of file diff --git a/src/ui/Input.tsx b/src/ui/Input.tsx deleted file mode 100644 index 35f2052..0000000 --- a/src/ui/Input.tsx +++ /dev/null @@ -1,35 +0,0 @@ -import React from "react" -import styled from "styled-components" - - -const StyledInput = styled.input` - background-color: #000; - color: #fff; - border: 1px solid #888; - border-radius: 0.5em; - padding: 0.5em 0.5em; -` - - -export interface InputProps -{ - type?: string, - value: string | null, - onChange?: (newValue: string) => void - style?: React.HTMLAttributes - width?: string -} - - -export function Input(props: InputProps) -{ - return props.onChange?.(ev.target.value) } - style={{ - ...props.style, - width: props.width, - }}/> -} \ No newline at end of file diff --git a/src/ui/ListBox.tsx b/src/ui/ListBox.tsx deleted file mode 100644 index 0cdb1f6..0000000 --- a/src/ui/ListBox.tsx +++ /dev/null @@ -1,109 +0,0 @@ -import React from "react" -import styled from "styled-components" -import "../types" - - -interface StyledListBoxItemProps -{ - readonly active: boolean - readonly selected: boolean -} - - -const StyledListBoxItem = styled.button` - width: 100%; - border: 0; - padding: 0.25em 0.5em; - text-align: left; - font-family: inherit; - color: #fff; - background-color: ${ props => props.selected ? "#08f" : props => props.active ? "#840" : "#000" }; -` - - -export interface ListBoxProps -{ - selected: any - active?: any - - items: - { - label: any - value: any - }[] - - onChange?: (item: any, index: number) => void - - style?: any -} - - -export function ListBox(props: ListBoxProps) -{ - //const selectedIndex = props.items.findIndex(i => i.value == props.selected) - const [columns, setColumns] = React.useState(1) - const refDiv = React.useRef(null!) - - - const itemsRendered = React.useMemo(() => - { - return props.items.map((item, i) => - props.onChange?.(item, i) } - active={ props.active === item.value } - selected={ props.selected === item.value } - > - - { item.label } - - - ) - - }, [props.items, props.active, props.selected]) - - - React.useLayoutEffect(() => - { - const resizeObserver = new ResizeObserver(entries => - { - for (const entry of entries) - { - const newColumns = Math.max(1, Math.floor(entry.contentRect.width / 150)) - setColumns(newColumns) - break - } - }) - - resizeObserver.observe(refDiv.current) - return () => resizeObserver.unobserve(refDiv.current) - - }, [refDiv.current]) - - - return
-
- { itemsRendered } -
-
-} \ No newline at end of file diff --git a/src/ui/LoadingBar.tsx b/src/ui/LoadingBar.tsx deleted file mode 100644 index 920abc5..0000000 --- a/src/ui/LoadingBar.tsx +++ /dev/null @@ -1,108 +0,0 @@ -import React from "react" -import styled, { keyframes } from "styled-components" - - -const loadingBarKeyframes = keyframes` - 0% - { - transform: translate(-320px, 0); - /*background-position-x: -80px;*/ - } - 100% - { - transform: translate(-240px, 0); - /*background-position-x: 0px;*/ - } -` - - -interface LoadingBarStyleProps -{ - thickness: number - size: number - color1: string - color2: string -} - - -const DivLoadingBar = styled.div` - position: relative; - width: 200%; - height: ${ props => props.thickness }px; - outline: none; - background-color: transparent; - background-size: calc(100% + 640px) 100%; - background-repeat: repeat; - background-image: repeating-linear-gradient(-30deg, - ${ props => props.color1 }, - ${ props => props.color1 } ${ props => props.size }px, - ${ props => props.color2 } ${ props => props.size + 1 }px, - ${ props => props.color2 } ${ props => props.size * 2 - 1}px, - ${ props => props.color1 } ${ props => props.size * 2 }px); - animation-name: ${ loadingBarKeyframes }; - animation-duration: 0.75s; - animation-iteration-count: infinite; - animation-timing-function: linear; -` - - -export interface LoadingBarProps -{ - floating?: boolean - progress?: number -} - - -export function LoadingBar(props: LoadingBarProps) -{ - const clipProgress = 100 * (props.progress === undefined ? 1 : props.progress) - const clipPath = - "polygon(0% -10%, " - + clipProgress + "% -10%, " - + clipProgress + "% 100%, " + - "0% 100%)" - - if (props.floating) - { - return
- -
- } - else - { - return
-
- -
-
- } -} \ No newline at end of file diff --git a/src/ui/index.ts b/src/ui/index.ts deleted file mode 100644 index 7a475af..0000000 --- a/src/ui/index.ts +++ /dev/null @@ -1,8 +0,0 @@ -export * from "./Button" -export * from "./ButtonList" -export * from "./ChordButton" -export * from "./Input" -export * from "./ListBox" -export * from "./Dial" -export * from "./DropdownMenu" -export * from "./LoadingBar" \ No newline at end of file diff --git a/src/util/async.ts b/src/util/async.ts deleted file mode 100644 index 1c6f205..0000000 --- a/src/util/async.ts +++ /dev/null @@ -1,39 +0,0 @@ -export async function waitFrame() -{ - return new Promise((resolve, _) => requestAnimationFrame(resolve)) -} - - -export async function waitSeconds(seconds: number) -{ - return new Promise((resolve, _) => setTimeout(resolve, seconds * 1000)) -} - - -export class Mutex -{ - locked: boolean - - - constructor() - { - this.locked = false - } - - - async acquire() - { - while (this.locked) - { - await waitFrame() - } - - this.locked = true - } - - - async release() - { - this.locked = false - } -} \ No newline at end of file diff --git a/src/util/binaryReader.ts b/src/util/binaryReader.ts deleted file mode 100644 index c4d7f9d..0000000 --- a/src/util/binaryReader.ts +++ /dev/null @@ -1,220 +0,0 @@ -export class BinaryReader -{ - bytes: number[] | Buffer | Uint8Array - head: number - - - constructor(bytes: number[] | Buffer | Uint8Array) - { - this.bytes = bytes - this.head = 0 - } - - - dumpNextBytes(n = 100) - { - console.log(this.bytes.slice(this.head, this.head + n)) - } - - - getLength(): number - { - return this.bytes.length - } - - - getPosition(): number - { - return this.head - } - - - seek(index: number) - { - this.head = index - } - - - peekByte(): number - { - return this.bytes[this.head] - } - - - readByte(): number - { - let b = this.bytes[this.head] - this.head += 1 - return b - } - - - readSByte(): number - { - let x = this.readByte() - if ((x & 0x80) == 0) - return x - - return -(0x100 - x) - } - - - readBytes(length: number): number[] - { - let arr = [] - for (let i = 0; i < length; i++) - arr.push(this.readByte()) - - return arr - } - - - readUInt16BE(): number - { - let b0 = this.readByte() - let b1 = this.readByte() - - let result = (b0 << 8) | b1 - - if (result < 0) - return 0x10000 + result - else - return result - } - - - readUInt16LE(): number - { - let b1 = this.readByte() - let b0 = this.readByte() - - let result = (b0 << 8) | b1 - - if (result < 0) - return 0x10000 + result - else - return result - } - - - readUInt16sBE(length: number): number[] - { - let arr = [] - for (let i = 0; i < length; i++) - arr.push(this.readUInt16BE()) - - return arr - } - - - readUInt24BE(): number - { - let b0 = this.readByte() - let b1 = this.readByte() - let b2 = this.readByte() - - let result = (b0 << 16) | (b1 << 8) | b2 - - if (result < 0) - return 0x1000000 + result - else - return result - } - - - readUInt32BE(): number - { - let b0 = this.readByte() - let b1 = this.readByte() - let b2 = this.readByte() - let b3 = this.readByte() - - let result = (b0 << 24) | (b1 << 16) | (b2 << 8) | b3 - - if (result < 0) - return 0x100000000 + result - else - return result - } - - - readUInt32LE(): number - { - let b3 = this.readByte() - let b2 = this.readByte() - let b1 = this.readByte() - let b0 = this.readByte() - - let result = (b0 << 24) | (b1 << 16) | (b2 << 8) | b3 - - if (result < 0) - return 0x100000000 + result - else - return result - } - - - readInt16BE(): number - { - let x = this.readUInt16BE() - if ((x & 0x8000) == 0) - return x - - return -(0x10000 - x) - } - - - readInt32BE(): number - { - let x = this.readUInt32BE() - if ((x & 0x80000000) == 0) - return x - - return -(0x100000000 - x) - } - - - readFloat32(): number - { - let b0 = this.readByte() - let b1 = this.readByte() - let b2 = this.readByte() - let b3 = this.readByte() - - let buf = new ArrayBuffer(4) - let view = new DataView(buf) - - view.setUint8(0, b0) - view.setUint8(1, b1) - view.setUint8(2, b2) - view.setUint8(3, b3) - - return view.getFloat32(0) - } - - - readAsciiLength(length: number): string - { - let str = "" - for (let i = 0; i < length; i++) - str += String.fromCharCode(this.readByte()) - - return str - } - - - readAsciiZeroTerminated(): string - { - let str = "" - while (true) - { - let c = this.readByte() - if (c == 0) - break - - str += String.fromCharCode(c) - } - - return str - } -} \ No newline at end of file diff --git a/src/util/binarySearch.ts b/src/util/binarySearch.ts deleted file mode 100644 index 16701a0..0000000 --- a/src/util/binarySearch.ts +++ /dev/null @@ -1,116 +0,0 @@ -type CompareFn = (value: T) => number - - -export default class BinarySearch -{ - static find(array: T[], compareFn: CompareFn): number - { - /*for (let i = 0; i < array.length; i++) - { - const comparison = compareFn(value, array[i]) - - if (comparison <= 0) - return i - } - - return array.length*/ - - let lo = -1 - let hi = array.length - while (1 + lo < hi) - { - const mi = lo + ((hi - lo) >> 1) - - if (compareFn(array[mi]) <= 0) - hi = mi - else - lo = mi - } - - return hi - } - - - static findExact(array: T[], value: T, compareFn: CompareFn): number | null - { - let i = BinarySearch.find(array, compareFn) - - while (i < array.length) - { - if (array[i] === value) - return i - - i += 1 - } - - return null - } - - - static findPreviousOrEqual(array: T[], compareFn: CompareFn): number | null - { - if (array.length == 0) - return null - - const i = BinarySearch.find(array, compareFn) - - if (i < array.length && compareFn(array[i]) == 0) - return i - - if (i > 0) - return i - 1 - - return null - } - - - static findPreviousNotEqual(array: T[], compareFn: CompareFn): number | null - { - if (array.length == 0) - return null - - const i = BinarySearch.find(array, compareFn) - - if (i > 0) - return i - 1 - - return null - } - - - static findNextNotEqual(array: T[], compareFn: CompareFn): number | null - { - if (array.length == 0) - return null - - let i = BinarySearch.find(array, compareFn) - - while (i < array.length) - { - if (compareFn(array[i]) != 0) - break - - i += 1 - } - - if (i < array.length) - return i - - return null - } - - - static *iterEqual(array: T[], compareFn: CompareFn): Generator - { - let i = BinarySearch.find(array, compareFn) - - while (i < array.length) - { - if (compareFn(array[i]) != 0) - break - - yield i - i += 1 - } - } -} \ No newline at end of file diff --git a/src/util/binaryWriter.ts b/src/util/binaryWriter.ts deleted file mode 100644 index 5f55cf5..0000000 --- a/src/util/binaryWriter.ts +++ /dev/null @@ -1,126 +0,0 @@ -export class BinaryWriter -{ - bytes: number[] - head: number - - - constructor() - { - this.bytes = [] - this.head = 0 - } - - - getBytes() - { - return this.bytes - } - - - getLength() - { - return this.bytes.length - } - - - seek(index: number) - { - while (index > this.bytes.length) - this.bytes.push(0) - - this.head = index - } - - - writeByte(b: number) - { - this.bytes[this.head] = (b & 0xff) - this.head += 1 - } - - - writeSByte(b: number) - { - if (b >= 0) - this.writeByte(b) - else - this.writeByte(0x100 + b) - } - - - writeBytes(bytes: number[]) - { - for (let i = 0; i < bytes.length; i++) - this.writeByte(bytes[i]) - } - - - writeUInt16BE(x: number) - { - this.writeByte(x >> 8) - this.writeByte(x >> 0) - } - - - writeUInt24BE(x: number) - { - this.writeByte(x >> 16) - this.writeByte(x >> 8) - this.writeByte(x >> 0) - } - - - writeUInt32BE(x: number) - { - this.writeByte(x >> 24) - this.writeByte(x >> 16) - this.writeByte(x >> 8) - this.writeByte(x >> 0) - } - - - writeInt16BE(x: number) - { - if (x >= 0) - this.writeUInt16BE(x) - else - this.writeUInt16BE(0x10000 + x) - } - - - writeInt32BE(x: number) - { - if (x >= 0) - this.writeUInt32BE(x) - else - this.writeUInt32BE(0x100000000 + x) - } - - - writeFloat32(x: number) - { - let view = new DataView(new ArrayBuffer(4)) - view.setFloat32(0, x) - this.writeByte(view.getUint8(0)) - this.writeByte(view.getUint8(1)) - this.writeByte(view.getUint8(2)) - this.writeByte(view.getUint8(3)) - } - - - writeAsciiLength(str: string, length: number) - { - for (let i = 0; i < Math.min(str.length, length); i++) - this.writeByte(str.charCodeAt(i)) - - for (let i = str.length; i < length; i++) - this.writeByte(0) - } - - - writeAscii(str: string) - { - for (let i = 0; i < str.length; i++) - this.writeByte(str.charCodeAt(i)) - } -} \ No newline at end of file diff --git a/src/util/callback.js b/src/util/callback.js new file mode 100644 index 0000000..3abcad6 --- /dev/null +++ b/src/util/callback.js @@ -0,0 +1,27 @@ +function Callback() +{ + this.callbacks = []; +} + + +Callback.prototype.add = function(callback) +{ + this.callbacks.push(callback); +} + + +Callback.prototype.remove = function(callback) +{ + var index = this.callbacks.indexOf(callback); + + if (index > -1) { + this.callbacks.splice(index, 1); + } +} + + +Callback.prototype.call = function(callbackWithArgs) +{ + for (var i = 0; i < this.callbacks.length; i++) + callbackWithArgs(this.callbacks[i]); +} \ No newline at end of file diff --git a/src/util/canvasUtils.ts b/src/util/canvasUtils.ts deleted file mode 100644 index 0eca8e5..0000000 --- a/src/util/canvasUtils.ts +++ /dev/null @@ -1,106 +0,0 @@ -import * as Theory from "../theory" -import * as MathUtils from "./mathUtils" - - -const fillPatterns = new Map() - - -export function fillStyleForDegree(ctx: CanvasRenderingContext2D, degree: number, external: boolean): any -{ - degree = MathUtils.mod(degree, 7) - - const colorFn = external ? Theory.Utils.degreeToColorFaded : Theory.Utils.degreeToColor - - if (Math.floor(degree) == degree) - return colorFn(degree) - - const cacheKey = degree + (external ? 100 : 0) - - if (fillPatterns.has(cacheKey)) - return fillPatterns.get(cacheKey) - - const colorBefore = colorFn(MathUtils.mod(Math.floor(degree), 7)) - const colorAfter = colorFn(MathUtils.mod(Math.ceil(degree), 7)) - - const canvas = document.createElement("canvas") - canvas.width = 24 - canvas.height = 24 - canvas.style.display = "none" - document.body.appendChild(canvas) - - let ctxPatt = canvas.getContext("2d")! - ctxPatt.fillStyle = colorBefore - ctxPatt.fillRect(0, 0, 24, 24) - - ctxPatt.fillStyle = colorAfter - ctxPatt.beginPath() - ctxPatt.moveTo(12, 0) - ctxPatt.lineTo(24, 0) - ctxPatt.lineTo(0, 24) - ctxPatt.lineTo(-12, 24) - - ctxPatt.moveTo(24 + 12, 0) - ctxPatt.lineTo(24 + 24, 0) - ctxPatt.lineTo(24 + 0, 24) - ctxPatt.lineTo(24 - 12, 24) - ctxPatt.fill() - - const pattern = ctx.createPattern(canvas, "repeat") - fillPatterns.set(cacheKey, pattern) - return pattern -} - - -export function renderChord( - ctx: CanvasRenderingContext2D, - x1: number, y1: number, x2: number, y2: number, - chord: Theory.Chord, - key: Theory.Key) -{ - const w = x2 - x1 - const h = y2 - y1 - const decorH = 5.5 - - ctx.fillStyle = "#ddd" - ctx.fillRect(x1, y1, w, h) - - const mode = !key.scale.metadata ? 0 : key.scale.metadata.mode - ctx.fillStyle = fillStyleForDegree(ctx, key.degreeForMidi(chord.rootChroma) + mode, false) - - ctx.fillRect(x1, y1, w, decorH) - ctx.fillRect(x1, y2 - decorH, w, decorH) - - if (w < 8) - return - - const nameBase = chord.romanBase(key) - const nameSup = chord.romanSup(key) - const nameSub = chord.romanSub(key) - const nameRootPitch = key.nameForMidi(chord.rootChroma) - - ctx.fillStyle = "#000" - ctx.textAlign = "left" - ctx.textBaseline = "middle" - - ctx.font = "20px Verdana" - const nameBaseW = ctx.measureText(nameBase).width - ctx.font = "15px Verdana" - const nameSupW = ctx.measureText(nameSup).width - const nameSubW = ctx.measureText(nameSub).width - const nameTotalW = nameBaseW + nameSupW + nameSubW - - const nameScale = Math.min(1, (w - 6) / nameTotalW) - - if (nameScale > 0) - { - ctx.font = "20px Verdana" - ctx.fillText(nameBase, x1 + w / 2 + nameScale * (-nameTotalW / 2), y1 + h / 2 - 4, nameScale * nameBaseW) - ctx.font = "15px Verdana" - ctx.fillText(nameSup, x1 + w / 2 + nameScale * (-nameTotalW / 2 + nameBaseW), y1 + h / 2 - 4 - 4, nameScale * nameSupW) - ctx.fillText(nameSub, x1 + w / 2 + nameScale * (-nameTotalW / 2 + nameBaseW + nameSupW), y1 + h / 2 - 4 + 12, nameScale * nameSubW) - - ctx.font = "10px Verdana" - ctx.textAlign = "center" - ctx.fillText(nameRootPitch.strUnicode, x1 + w / 2, y1 + h / 2 + 12, w - 6) - } -} \ No newline at end of file diff --git a/src/util/drawing.js b/src/util/drawing.js new file mode 100644 index 0000000..7b81a54 --- /dev/null +++ b/src/util/drawing.js @@ -0,0 +1,27 @@ +function drawStripedRect(ctx, x, y, w, h, color1, color2) +{ + ctx.save(); + + ctx.beginPath(); + ctx.rect(x, y, w, h); + ctx.clip(); + + ctx.fillStyle = color1; + ctx.fillRect(x, y, w, h); + + ctx.strokeStyle = color2; + ctx.lineWidth = 4; + ctx.beginPath(); + + var stripeX = -(x % 10); + while (stripeX - h * 0.6 < w) + { + ctx.moveTo(x + stripeX + 5, y - 5); + ctx.lineTo(x + stripeX - h * 0.6 - 5, y + h + 5); + stripeX += 10; + } + + ctx.stroke(); + + ctx.restore(); +} \ No newline at end of file diff --git a/src/util/globalObservable.ts b/src/util/globalObservable.ts deleted file mode 100644 index e45ff38..0000000 --- a/src/util/globalObservable.ts +++ /dev/null @@ -1,52 +0,0 @@ -import React from "react" - - -export interface GlobalObservable -{ - updateToken: boolean - observers: (() => void)[] - notifyObservers: () => void -} - - -export function makeGlobalObservable(): GlobalObservable -{ - const globalObs: GlobalObservable = - { - updateToken: false, - observers: [], - notifyObservers: () => {}, - } - - globalObs.notifyObservers = () => - { - globalObs.updateToken = !globalObs.updateToken - for (const obs of globalObs.observers) - obs() - } - - return globalObs -} - - -export function useGlobalObservable(globalObs: GlobalObservable) -{ - const [updateToken, setUpdateToken] = React.useState(globalObs.updateToken) - - - React.useEffect(() => - { - const callback = () => - { - //console.log("useGlobalObservable.callback " + globalObs.updateToken) - setUpdateToken(globalObs.updateToken) - } - - globalObs.observers.push(callback) - return () => - { - globalObs.observers = globalObs.observers.filter(c => c !== callback) - } - - }, []) -} \ No newline at end of file diff --git a/src/util/listOfPoints.ts b/src/util/listOfPoints.ts deleted file mode 100644 index 0b82697..0000000 --- a/src/util/listOfPoints.ts +++ /dev/null @@ -1,359 +0,0 @@ -import Rational from "./rational" -import Range from "./range" -import BinarySearch from "./binarySearch" -import Immutable from "immutable" -import assert from "assert" - - -interface Element -{ - id: number - time: Rational -} - - -export default class ListOfPoints -{ - idMap: Immutable.Map - elems: T[] - - - constructor() - { - this.idMap = Immutable.Map() - this.elems = [] - } - - - clone(): ListOfPoints - { - let cloned = new ListOfPoints() - cloned.idMap = this.idMap - cloned.elems = this.elems - return cloned - } - - - clear(): ListOfPoints - { - return new ListOfPoints() - } - - - get size(): number - { - return this.idMap.size - } - - - add(elem: T): ListOfPoints - { - return this.upsert(elem) - } - - - addMany(elems: T[]): ListOfPoints - { - let newList: ListOfPoints = this - for (const elem of elems) - newList = newList.add(elem) - - return newList - } - - - update(elem: T): ListOfPoints - { - if (!this.idMap.get(elem.id)) - return this - - return this.upsert(elem) - } - - - upsert(elem: T): ListOfPoints - { - //console.log("upsert id", elem.id) - - let newList = this.clone() - newList = newList.removeById(elem.id) - - const newElemIndex = BinarySearch.find(newList.elems, b => elem.time.compare(b.time)) - //console.log("- add at elem", newElemIndex) - - newList.elems = - [ - ...newList.elems.slice(0, newElemIndex), - elem, - ...newList.elems.slice(newElemIndex) - ] - - newList.idMap = newList.idMap.set(elem.id, elem) - return newList - } - - - removeById(id: number): ListOfPoints - { - let newList: ListOfPoints = this - - const elem = this.idMap.get(id) - if (elem) - { - const elemIndex = BinarySearch.findExact(newList.elems, elem, b => elem.time.compare(b.time)) - - if (elemIndex !== null) - { - newList = this.clone() - - //console.log("- remove at elem", elemIndex) - - newList.elems = - [ - ...newList.elems.slice(0, elemIndex), - ...newList.elems.slice(elemIndex + 1) - ] - - newList.idMap = newList.idMap.delete(id) - } - } - - return newList - } - - - findById(id: number): T | undefined - { - return this.idMap.get(id) - } - - - findFirst(): T | null - { - if (this.elems.length > 0) - return this.elems[0] - - return null - } - - - getTotalRange(): Range | null - { - if (this.elems.length == 0) - return null - - return Range.fromPoint(this.elems[0].time).merge(Range.fromPoint(this.elems[this.elems.length - 1].time)) - } - - - *iterAll(): Generator - { - for (let i = 0; i < this.elems.length; i++) - yield this.elems[i] - } - - - *iterAt(point: Rational): Generator - { - for (const i of BinarySearch.iterEqual(this.elems, b => point.compare(b.time))) - yield this.elems[i] - } - - - *iterAtRange(range: Range): Generator - { - let i = BinarySearch.find(this.elems, b => range.start.compare(b.time)) - - while (i < this.elems.length) - { - if (range.end.compare(this.elems[i].time) <= 0) - break - - if (range.overlapsPoint(this.elems[i].time)) - yield this.elems[i] - - i += 1 - } - } - - - *iterActiveAtRangePairwise(range: Range): Generator<[T | null, T | null], void, void> - { - if (this.elems.length == 0) - { - yield [ null, null ] - return - } - - let i = BinarySearch.findPreviousOrEqual(this.elems, b => range.start.compare(b.time)) - let prevItem = (i === null ? null : this.elems[i]) - //console.log("iterActiveAtRangePairwise", i, prevItem) - - if (i === null) - i = -1 - - while (true) - { - i += 1 - while (i < this.elems.length && prevItem && this.elems[i].time.compare(prevItem.time) == 0) - i += 1 - - if (i >= this.elems.length) - break - - yield [ prevItem, this.elems[i] ] - - prevItem = this.elems[i] - - if (!range.overlapsPoint(this.elems[i].time)) - return - } - - yield [ prevItem, null ] - } - - - findAt(point: Rational): T | null - { - for (const elem of this.iterAt(point)) - return elem - - return null - } - - - findActiveAt(point: Rational): T | null - { - const i = BinarySearch.findPreviousOrEqual(this.elems, b => point.compare(b.time)) - if (i === null) - return null - - return this.elems[i] - } - - - findPreviousAnchor(fromPoint: Rational): Rational | null - { - const i = BinarySearch.findPreviousNotEqual(this.elems, b => fromPoint.compare(b.time)) - if (i === null) - return null - - return this.elems[i].time - } - - - findPreviousDeletionAnchor(point: Rational): Rational | null - { - const elem = this.findActiveAt(point) - if (elem === null) - return null - - return elem.time - } - - - findNextNotEqual(fromPoint: Rational): T | null - { - const i = BinarySearch.findNextNotEqual(this.elems, b => fromPoint.compare(b.time)) - if (i === null) - return null - - return this.elems[i] - } - - - static test() - { - let list = new ListOfPoints() - - const elem1 = { id: 1, time: new Rational(0) } - const elem2 = { id: 2, time: new Rational(1) } - const elem3 = { id: 3, time: new Rational(2) } - const elem4 = { id: 4, time: new Rational(4) } - const elem5 = { id: 5, time: new Rational(5) } - - const elem1_2 = { id: 1, time: new Rational(3) } - - const range1 = new Range(new Rational(0), new Rational(0), true, false) - const range2 = new Range(new Rational(0), new Rational(1), true, false) - const range3 = new Range(new Rational(0), new Rational(5), true, false) - const range4 = new Range(new Rational(-1), new Rational(6), true, false) - const range5 = new Range(new Rational(0), new Rational(5, 2), true, false) - - assert.deepEqual([ ...list.iterAll() ], []) - assert.deepEqual([ ...list.iterActiveAtRangePairwise(range2) ], [ [ null, null ] ]) - assert.deepEqual([ ...list.iterActiveAtRangePairwise(range3) ], [ [ null, null ] ]) - assert.deepEqual([ ...list.iterActiveAtRangePairwise(range4) ], [ [ null, null ] ]) - assert.deepEqual(list.getTotalRange(), null) - - list = list.upsert(elem1) - assert.deepEqual([ ...list.iterAll() ], [ elem1 ]) - assert.deepEqual([ ...list.iterAt(elem1.time) ], [ elem1 ]) - assert.deepEqual([ ...list.iterAt(elem2.time) ], []) - assert.deepEqual([ ...list.iterAtRange(range1) ], []) - assert.deepEqual([ ...list.iterAtRange(range2) ], [ elem1 ]) - assert.deepEqual([ ...list.iterAtRange(range5) ], [ elem1 ]) - assert.deepEqual([ ...list.iterActiveAtRangePairwise(range2) ], [ [ elem1, null ] ]) - assert.deepEqual([ ...list.iterActiveAtRangePairwise(range3) ], [ [ elem1, null ] ]) - assert.deepEqual([ ...list.iterActiveAtRangePairwise(range4) ], [ [ null, elem1 ], [ elem1, null ] ]) - assert.deepEqual(list.getTotalRange(), new Range(elem1.time, elem1.time, true, false)) - - list = list.upsert(elem2) - assert.deepEqual([ ...list.iterAll() ], [ elem1, elem2 ]) - assert.deepEqual([ ...list.iterAt(elem1.time) ], [ elem1 ]) - assert.deepEqual([ ...list.iterAt(elem2.time) ], [ elem2 ]) - assert.deepEqual([ ...list.iterAtRange(range2) ], [ elem1 ]) - assert.deepEqual([ ...list.iterAtRange(range3) ], [ elem1, elem2 ]) - assert.deepEqual([ ...list.iterAtRange(range5) ], [ elem1, elem2 ]) - assert.deepEqual([ ...list.iterActiveAtRangePairwise(range2) ], [ [ elem1, elem2 ] ]) - assert.deepEqual([ ...list.iterActiveAtRangePairwise(range3) ], [ [ elem1, elem2 ], [ elem2, null ] ]) - assert.deepEqual([ ...list.iterActiveAtRangePairwise(range4) ], [ [ null, elem1 ], [ elem1, elem2 ], [ elem2, null ] ]) - assert.deepEqual(list.getTotalRange(), new Range(elem1.time, elem2.time, true, false)) - - list = list.upsert(elem4) - assert.deepEqual([ ...list.iterAll() ], [ elem1, elem2, elem4 ]) - assert.deepEqual([ ...list.iterAt(elem1.time) ], [ elem1 ]) - assert.deepEqual([ ...list.iterAt(elem2.time) ], [ elem2 ]) - assert.deepEqual([ ...list.iterAt(elem4.time) ], [ elem4 ]) - assert.deepEqual([ ...list.iterAtRange(range2) ], [ elem1 ]) - assert.deepEqual([ ...list.iterAtRange(range3) ], [ elem1, elem2, elem4 ]) - assert.deepEqual(list.getTotalRange(), new Range(elem1.time, elem4.time, true, false)) - - list = list.upsert(elem3) - assert.deepEqual([ ...list.iterAll() ], [ elem1, elem2, elem3, elem4 ]) - assert.deepEqual([ ...list.iterAt(elem1.time) ], [ elem1 ]) - assert.deepEqual([ ...list.iterAt(elem2.time) ], [ elem2 ]) - assert.deepEqual([ ...list.iterAt(elem3.time) ], [ elem3 ]) - assert.deepEqual([ ...list.iterAt(elem4.time) ], [ elem4 ]) - assert.deepEqual([ ...list.iterAtRange(range2) ], [ elem1 ]) - assert.deepEqual([ ...list.iterAtRange(range3) ], [ elem1, elem2, elem3, elem4 ]) - assert.deepEqual(list.getTotalRange(), new Range(elem1.time, elem4.time, true, false)) - - list = list.upsert(elem5) - assert.deepEqual([ ...list.iterAll() ], [ elem1, elem2, elem3, elem4, elem5 ]) - assert.deepEqual([ ...list.iterAt(elem1.time) ], [ elem1 ]) - assert.deepEqual([ ...list.iterAt(elem2.time) ], [ elem2 ]) - assert.deepEqual([ ...list.iterAt(elem3.time) ], [ elem3 ]) - assert.deepEqual([ ...list.iterAt(elem4.time) ], [ elem4 ]) - assert.deepEqual([ ...list.iterAt(elem5.time) ], [ elem5 ]) - assert.deepEqual([ ...list.iterAtRange(range2) ], [ elem1 ]) - assert.deepEqual([ ...list.iterAtRange(range3) ], [ elem1, elem2, elem3, elem4 ]) - assert.deepEqual([ ...list.iterActiveAtRangePairwise(range3) ], [ [ elem1, elem2 ], [ elem2, elem3 ], [ elem3, elem4 ], [ elem4, elem5 ] ]) - assert.deepEqual([ ...list.iterActiveAtRangePairwise(range4) ], [ [ null, elem1 ], [ elem1, elem2 ], [ elem2, elem3 ], [ elem3, elem4 ], [ elem4, elem5 ], [ elem5, null ] ]) - assert.deepEqual(list.getTotalRange(), new Range(elem1.time, elem5.time, true, false)) - - list = list.upsert(elem1_2) - assert.deepEqual([ ...list.iterAll() ], [ elem2, elem3, elem1_2, elem4, elem5 ]) - assert.deepEqual([ ...list.iterAt(elem1.time) ], []) - assert.deepEqual([ ...list.iterAt(elem2.time) ], [ elem2 ]) - assert.deepEqual([ ...list.iterAt(elem3.time) ], [ elem3 ]) - assert.deepEqual([ ...list.iterAt(elem4.time) ], [ elem4 ]) - assert.deepEqual([ ...list.iterAt(elem5.time) ], [ elem5 ]) - assert.deepEqual([ ...list.iterAt(elem1_2.time) ], [ elem1_2 ]) - assert.deepEqual([ ...list.iterAtRange(range2) ], []) - assert.deepEqual([ ...list.iterAtRange(range3) ], [ elem2, elem3, elem1_2, elem4 ]) - assert.deepEqual(list.getTotalRange(), new Range(elem2.time, elem5.time, true, false)) - - console.log("ListOfPoints tests passed") - } -} \ No newline at end of file diff --git a/src/util/listOfRanges.ts b/src/util/listOfRanges.ts deleted file mode 100644 index d322036..0000000 --- a/src/util/listOfRanges.ts +++ /dev/null @@ -1,509 +0,0 @@ -import Rational from "./rational" -import Range from "./range" -import BinarySearch from "./binarySearch" -import Immutable from "immutable" -import assert from "assert" - - -interface Element -{ - id: number - range: Range -} - - -class Bucket -{ - start: number - elems: T[] - - - constructor(start: number) - { - this.start = start - this.elems = [] - } -} - - -export default class ListOfRanges -{ - idMap: Immutable.Map - buckets: Bucket[] - bucketFn: (t: Rational) => number - - - constructor() - { - this.idMap = Immutable.Map() - this.buckets = [] - this.bucketFn = (t) => t.asFloat() - } - - - clone(): ListOfRanges - { - let cloned = new ListOfRanges() - cloned.idMap = this.idMap - cloned.buckets = this.buckets - cloned.bucketFn = this.bucketFn - return cloned - } - - - clear(): ListOfRanges - { - return Object.assign(new ListOfRanges(), { bucketFn: this.bucketFn }) - } - - - _ensureBucketsAtRange(range: Range) - { - let newBuckets = [ ...this.buckets ] - - if (this.buckets.length == 0) - newBuckets = [ new Bucket(0) ] - - const start = this.bucketFn(range.start) - while (start < newBuckets[0].start) - { - const newBucket = new Bucket(newBuckets[0].start - 1) - newBuckets = [ newBucket, ...newBuckets ] - } - - const end = this.bucketFn(range.end) - while (end >= newBuckets[newBuckets.length - 1].start + 1) - { - const newBucket = new Bucket(newBuckets[newBuckets.length - 1].start + 1) - newBuckets = [ ...newBuckets, newBucket ] - } - - const cloned = this.clone() - cloned.buckets = newBuckets - return cloned - } - - - *_iterBucketIndicesAtRange(range: Range): Generator - { - const start = this.bucketFn(range.start) - const end = this.bucketFn(range.end) - - let b = BinarySearch.find(this.buckets, (bucket) => start - bucket.start) - - if (b > 0 && start < this.buckets[b - 1].start + 1) - b -= 1 - - while (true) - { - if (b >= this.buckets.length || this.buckets[b].start > end) - break - - yield b - b += 1 - } - } - - - get size(): number - { - return this.idMap.size - } - - - update(elem: T): ListOfRanges - { - if (!this.idMap.get(elem.id)) - return this - - return this.upsert(elem) - } - - - upsert(elem: T): ListOfRanges - { - //console.log("upsert id", elem.id, "buckets", this.bucketFn(elem.range.start), this.bucketFn(elem.range.end),) - - let newList = this._ensureBucketsAtRange(elem.range) - newList = newList.removeById(elem.id, false) - - for (const b of newList._iterBucketIndicesAtRange(elem.range)) - { - newList.buckets[b] = Object.assign({}, newList.buckets[b]) - - const newElemIndex = BinarySearch.find(newList.buckets[b].elems, e => elem.range.start.compare(e.range.start)) - //console.log("- add to bucket", b, "at elem", newElemIndex) - - newList.buckets[b].elems = - [ - ...newList.buckets[b].elems.slice(0, newElemIndex), - elem, - ...newList.buckets[b].elems.slice(newElemIndex) - ] - } - - newList.idMap = newList.idMap.set(elem.id, elem) - return newList - } - - - upsertMany(elems: T[]): ListOfRanges - { - let newList: ListOfRanges = this - for (const elem of elems) - newList = newList.upsert(elem) - - return newList - } - - - removeById(id: number, removeId: boolean = true): ListOfRanges - { - let newList: ListOfRanges = this - - const elem = this.idMap.get(id) - if (elem) - { - newList = this.clone() - newList.buckets = [ ...newList.buckets ] - - for (const b of newList._iterBucketIndicesAtRange(elem.range)) - { - const elemIndex = BinarySearch.findExact(newList.buckets[b].elems, elem, b => elem.range.start.compare(b.range.start)) - - if (elemIndex === null) - continue - - //console.log("- remove from bucket", b, "at elem", elemIndex) - - newList.buckets[b] = Object.assign({}, newList.buckets[b]) - newList.buckets[b].elems = - [ - ...newList.buckets[b].elems.slice(0, elemIndex), - ...newList.buckets[b].elems.slice(elemIndex + 1) - ] - } - - if (removeId) - newList.idMap = newList.idMap.delete(id) - } - - return newList - } - - - findById(id: number): T | undefined - { - return this.idMap.get(id) - } - - - *iterAll(): Generator - { - for (const bucket of this.buckets) - for (const elem of bucket.elems) - { - const elemStart = this.bucketFn(elem.range.start) - if (elemStart >= bucket.start && elemStart < bucket.start + 1) - yield elem - } - } - - - *iterAtRange(range: Range): Generator - { - let firstBucket = true - - for (const b of this._iterBucketIndicesAtRange(range)) - { - const bucket = this.buckets[b] - - for (const elem of bucket.elems) - { - const elemStart = this.bucketFn(elem.range.start) - if (range.overlapsRange(elem.range) && - (firstBucket || (elemStart >= bucket.start && elemStart < bucket.start + 1))) - yield elem - } - - firstBucket = false - } - } - - - *iterAtPoint(point: Rational): Generator - { - const range = Range.fromPoint(point, true, true) - for (const item of this.iterAtRange(range)) - yield item - } - - - *iterActiveAtRangePairwise(range: Range): Generator<[T | null, T | null], void, void> - { - if (this.idMap.size == 0) - { - yield [ null, null ] - return - } - - let prevItem = this.findPrevious(range.start) - - while (true) - { - const nextItem = this.findNextNotEqual(prevItem?.range.end ?? range.start) - - yield [ prevItem, nextItem ] - - if (!nextItem) - break - - prevItem = nextItem - } - } - - - *iterAllPairwise(): Generator<[T | null, T | null], void, void> - { - if (this.idMap.size == 0) - { - yield [ null, null ] - return - } - - let prevItem: T | null = null - for (const nextItem of this.iterAll()) - { - yield [ prevItem, nextItem ] - prevItem = nextItem - } - - yield [ prevItem, null ] - } - - - getTotalRange(): Range | null - { - let firstElem: T | null = null - let lastElem: T | null = null - - for (const bucket of this.buckets) - { - if (bucket.elems.length > 0) - { - firstElem = bucket.elems[0] - break - } - } - - for (let b = this.buckets.length - 1; b >= 0; b--) - { - for (const elem of this.buckets[b].elems) - { - if (!lastElem || elem.range.end.compare(lastElem.range.end) > 0) - lastElem = elem - } - - if (lastElem) - break - } - - if (!firstElem || !lastElem) - return null - - return firstElem.range.merge(lastElem.range) - } - - - findFirst(): T | null - { - for (const elem of this.iterAll()) - return elem - - return null - } - - - findActiveAt(time: Rational): T | null - { - let result: T | null = null - - for (const elem of this.iterAll()) - { - if (elem.range.start.compare(time) > 0) - break - - result = elem - } - - return result - } - - - findPreviousAnchor(fromPoint: Rational): Rational | null - { - let previous: Rational | null = null - - const end = this.bucketFn(fromPoint) - const endB = Math.min( - this.buckets.length - 1, - BinarySearch.find(this.buckets, bucket => end - bucket.start)) - - for (let b = endB; b >= 0; b--) - { - if (previous && this.bucketFn(previous) > this.buckets[b].start + 1) - break - - for (const elem of this.buckets[b].elems) - { - if (elem.range.start.compare(fromPoint) < 0 && - (previous === null || elem.range.start.compare(previous) > 0)) - previous = elem.range.start - - if (elem.range.end.compare(fromPoint) < 0 && - (previous === null || elem.range.end.compare(previous) > 0)) - previous = elem.range.end - } - } - - return previous - } - - - findPrevious(fromPoint: Rational): T | null - { - let nearestItem: T | null = null - let nearestPoint: Rational | null = null - - for (const item of this.iterAll()) - { - const itemRange = item.range - - if (itemRange.end.compare(fromPoint) > 0) - continue - - if (nearestPoint == null || itemRange.end.compare(nearestPoint) > 0) - { - nearestItem = item - nearestPoint = itemRange.end - } - } - - return nearestItem - } - - - findPreviousDeletionAnchor(fromPoint: Rational): Rational | null - { - const anchor = this.findPrevious(fromPoint) - if (!anchor) - return null - - const anchorRange = anchor.range - - if (anchorRange.end.compare(fromPoint) != 0) - return anchorRange.end - - let nearestPoint = anchorRange.start - - for (const item of this.iterAll()) - { - const itemRange = item.range - - if (itemRange.end.compare(anchorRange.end) != 0) - continue - - if (itemRange.start.compare(nearestPoint) > 0) - nearestPoint = itemRange.start - } - - return nearestPoint - } - - - findNextNotEqual(fromPoint: Rational): T | null - { - let nearestItem: T | null = null - let nearestPoint: Rational | null= null - - for (const item of this.iterAll()) - { - const itemRange = item.range - - if (itemRange.end.compare(fromPoint) <= 0) - continue - - if (nearestPoint == null || itemRange.end.compare(nearestPoint) < 0) - { - nearestItem = item - nearestPoint = itemRange.end - } - } - - return nearestItem - } - - - static test() - { - let list = new ListOfRanges() - - assert.deepEqual([ ...list.iterAll() ], []) - assert.deepEqual(list.getTotalRange(), null) - - const elem1 = { id: 1, range: new Range(new Rational(2, 4), new Rational(6, 4)) } - const elem2 = { id: 2, range: new Range(new Rational(0, 4), new Rational(3, 4)) } - const elem3 = { id: 3, range: new Range(new Rational(-3, 4), new Rational(11, 4)) } - const elem4 = { id: 4, range: new Range(new Rational(12, 4), new Rational(16, 4)) } - const elem5 = { id: 5, range: new Range(new Rational(5, 4), new Rational(13, 4)) } - - const elem1_2 = { id: 1, range: new Range(new Rational(6, 4), new Rational(11, 4)) } - - list = list.upsert(elem1) - assert.deepEqual([ ...list.iterAll() ], [ elem1 ]) - assert.deepEqual([ ...list.iterAtRange(elem1.range) ], [ elem1 ]) - assert.deepEqual(list.getTotalRange(), elem1.range) - - list = list.upsert(elem2) - assert.deepEqual([ ...list.iterAll() ], [ elem2, elem1 ]) - assert.deepEqual([ ...list.iterAtRange(elem1.range) ], [ elem2, elem1 ]) - assert.deepEqual([ ...list.iterAtRange(elem2.range) ], [ elem2, elem1 ]) - assert.deepEqual(list.getTotalRange(), elem2.range.merge(elem1.range)) - - list = list.upsert(elem3) - assert.deepEqual([ ...list.iterAll() ], [ elem3, elem2, elem1 ]) - assert.deepEqual([ ...list.iterAtRange(elem1.range) ], [ elem3, elem2, elem1 ]) - assert.deepEqual([ ...list.iterAtRange(elem2.range) ], [ elem3, elem2, elem1 ]) - assert.deepEqual([ ...list.iterAtRange(elem3.range) ], [ elem3, elem2, elem1 ]) - assert.deepEqual(list.getTotalRange(), elem3.range.merge(elem1.range)) - - list = list.upsert(elem4) - assert.deepEqual([ ...list.iterAll() ], [ elem3, elem2, elem1, elem4 ]) - assert.deepEqual([ ...list.iterAtRange(elem1.range) ], [ elem3, elem2, elem1 ]) - assert.deepEqual([ ...list.iterAtRange(elem2.range) ], [ elem3, elem2, elem1 ]) - assert.deepEqual([ ...list.iterAtRange(elem3.range) ], [ elem3, elem2, elem1 ]) - assert.deepEqual([ ...list.iterAtRange(elem4.range) ], [ elem4 ]) - assert.deepEqual(list.getTotalRange(), elem3.range.merge(elem4.range)) - - list = list.upsert(elem5) - assert.deepEqual([ ...list.iterAll() ], [ elem3, elem2, elem1, elem5, elem4 ]) - assert.deepEqual([ ...list.iterAtRange(elem1.range) ], [ elem3, elem2, elem1, elem5 ]) - assert.deepEqual([ ...list.iterAtRange(elem2.range) ], [ elem3, elem2, elem1 ]) - assert.deepEqual([ ...list.iterAtRange(elem3.range) ], [ elem3, elem2, elem1, elem5 ]) - assert.deepEqual([ ...list.iterAtRange(elem4.range) ], [ elem5, elem4 ]) - assert.deepEqual([ ...list.iterAtRange(elem5.range) ], [ elem3, elem1, elem5, elem4 ]) - assert.deepEqual(list.getTotalRange(), elem3.range.merge(elem4.range)) - - list = list.upsert(elem1_2) - assert.deepEqual([ ...list.iterAll() ], [ elem3, elem2, elem5, elem1_2, elem4 ]) - assert.deepEqual([ ...list.iterAtRange(elem1.range) ], [ elem3, elem2, elem5 ]) - assert.deepEqual([ ...list.iterAtRange(elem2.range) ], [ elem3, elem2 ]) - assert.deepEqual([ ...list.iterAtRange(elem3.range) ], [ elem3, elem2, elem5, elem1_2 ]) - assert.deepEqual([ ...list.iterAtRange(elem4.range) ], [ elem5, elem4 ]) - assert.deepEqual([ ...list.iterAtRange(elem5.range) ], [ elem3, elem5, elem1_2, elem4 ]) - assert.deepEqual([ ...list.iterAtRange(elem1_2.range) ], [ elem3, elem5, elem1_2 ]) - assert.deepEqual(list.getTotalRange(), elem3.range.merge(elem4.range)) - - console.log("ListOfRanges tests passed") - } -} \ No newline at end of file diff --git a/src/util/list_by_time.js b/src/util/list_by_time.js new file mode 100644 index 0000000..b2fd1d0 --- /dev/null +++ b/src/util/list_by_time.js @@ -0,0 +1,110 @@ +// TODO: Optimize with better data structures. +// +// Querying items contained in a given time range, or +// querying the first item at a time smaller than the given time, +// should be fast operations. +function ListByTime() +{ + this.items = []; +} + + +ListByTime.prototype.clear = function() +{ + this.items = []; +} + + +ListByTime.prototype.add = function(item) +{ + this.items.push(item); + this.items.sort(function (a,b) { return a.time - b.time; }); +} + + +ListByTime.prototype.remove = function(item) +{ + var index = this.items.indexOf(item); + + if (index != -1) + this.items.splice(index, 1); +} + + +ListByTime.prototype.enumerateAll = function(callback) +{ + for (var i = 0; i < this.items.length; i++) + { + if (this.items[i] == null) + continue; + + callback(this.items[i]); + } +} + + +ListByTime.prototype.enumerateOverlappingRange = function(timeRange, callback) +{ + for (var i = 0; i < this.items.length; i++) + { + if (this.items[i] == null) + continue; + + if (timeRange.overlapsTime(this.items[i].time)) + callback(this.items[i]); + } +} + + +ListByTime.prototype.enumerateAffectingRange = function(timeRange, callback) +{ + var lastIndex = this.getIndexAffectingTime(timeRange.start); + + for (var i = lastIndex + 1; i < this.items.length; i++) + { + if (this.items[i] == null) + continue; + + if (this.items[i].time >= timeRange.end) + break; + + if (lastIndex == -1) + callback(null, timeRange.start, this.items[i].time); + else + callback(this.items[lastIndex], Math.max(timeRange.start, this.items[lastIndex].time), this.items[i].time); + + lastIndex = i; + } + + if (lastIndex == -1) + callback(null, timeRange.start, timeRange.end); + else + callback(this.items[lastIndex], Math.max(timeRange.start, this.items[lastIndex].time), timeRange.end); +} + + +ListByTime.prototype.getIndexAffectingTime = function(time) +{ + for (var i = 0; i < this.items.length; i++) + { + if (this.items[i] == null) + continue; + + var itemTime = this.items[i].time; + + if (itemTime > time) + return i - 1; + } + + return this.items.length - 1; +} + + +ListByTime.prototype.getItemAffectingTime = function(time) +{ + var index = this.getIndexAffectingTime(time); + if (index == -1) + return null; + else + return this.items[index]; +} \ No newline at end of file diff --git a/src/util/list_by_timerange.js b/src/util/list_by_timerange.js new file mode 100644 index 0000000..72bfe16 --- /dev/null +++ b/src/util/list_by_timerange.js @@ -0,0 +1,67 @@ +// TODO: Optimize with better data structures. +// +// Querying items contained in a given time range +// should be fast. +function ListByTimeRange() +{ + this.items = []; +} + + +ListByTimeRange.prototype.clear = function() +{ + this.items = []; +} + + +ListByTimeRange.prototype.add = function(item) +{ + this.items.push(item); +} + + +ListByTimeRange.prototype.remove = function(item) +{ + var index = this.items.indexOf(item); + + if (index != -1) + this.items.splice(index, 1); +} + + +ListByTimeRange.prototype.enumerateAll = function(callback) +{ + for (var i = 0; i < this.items.length; i++) + { + if (this.items[i] == null) + continue; + + callback(this.items[i]); + } +} + + +ListByTimeRange.prototype.enumerateOverlappingTime = function(time, callback) +{ + for (var i = 0; i < this.items.length; i++) + { + if (this.items[i] == null) + continue; + + if (this.items[i].timeRange.overlapsTime(time)) + callback(this.items[i]); + } +} + + +ListByTimeRange.prototype.enumerateOverlappingRange = function(timeRange, callback) +{ + for (var i = 0; i < this.items.length; i++) + { + if (this.items[i] == null) + continue; + + if (timeRange.overlapsRange(this.items[i].timeRange)) + callback(this.items[i]); + } +} \ No newline at end of file diff --git a/src/util/math.js b/src/util/math.js new file mode 100644 index 0000000..f971820 --- /dev/null +++ b/src/util/math.js @@ -0,0 +1,20 @@ +function snap(x, step) +{ + return Math.round(x / step) * step; +} + + +function mod(x, m) +{ + return (x % m + m) % m; +} + + +function stretch(x, pivot, origin, delta) +{ + var dist = (origin - pivot); + var p = (x - pivot) / dist; + var move = (origin + delta - pivot) / dist; + + return Math.round(pivot + dist * (p * move)); +} \ No newline at end of file diff --git a/src/util/mathUtils.ts b/src/util/mathUtils.ts deleted file mode 100644 index 28d6204..0000000 --- a/src/util/mathUtils.ts +++ /dev/null @@ -1,55 +0,0 @@ -export function mod(x: number, m: number): number -{ - return (x % m + m) % m -} - - -export function quantize(x: number, step: number): number -{ - return Math.floor(x * step) / step -} - - -export function midiToHertz(midi: number): number -{ - return Math.pow(2, (midi - 69) / 12) * 440 -} - - -export function dbToLinearGain(db: number): number -{ - return Math.pow(10, db / 20) -} - - -export function linearGainToDb(linearGain: number): number -{ - return 20 * Math.log10(linearGain) -} - - -const minMidiDbLevel = -30 - - -export function dbToMidiVolume(db: number): number -{ - return Math.max(0, Math.min(1, 1 - (db / minMidiDbLevel))) -} - - -export function midiVolumeToDb(midiVol: number): number -{ - if (midiVol <= 0) - return 0 - - return minMidiDbLevel * (1 - midiVol) -} - - -export function midiVolumeToLinearGain(midiVol: number): number -{ - if (midiVol <= 0) - return 0 - - return dbToLinearGain(midiVolumeToDb(midiVol)) -} \ No newline at end of file diff --git a/src/util/midi.ts b/src/util/midi.ts deleted file mode 100644 index ad32bb6..0000000 --- a/src/util/midi.ts +++ /dev/null @@ -1,615 +0,0 @@ -import { BinaryReader } from "./binaryReader" -import { BinaryWriter } from "./binaryWriter" -import Rational from "./rational" - - -export interface Root -{ - tracks: Track[] - headerLength: any - format: any - trackNum: number - timeDivisionRaw: number - timeDivisionFormat: boolean - ticksPerQuarterNote: number -} - - -export interface Track -{ - length: number - events: Event[] -} - - -export type Event = - EventUnknown | - EventNoteOn | - EventNoteOff | - EventController | - EventProgramChange | - EventEndOfTrack | - EventTrackName | - EventSetTempo | - EventSetTimeSignature | - EventSetKeySignature | - EventSequencerSpecific - - -export interface EventCommon -{ - tick: Rational -} - - -export interface EventUnknown extends EventCommon -{ - kind: "unknown" - code: number - metaCode: number | null - rawData: number[] -} - - -export interface EventNoteOn extends EventCommon -{ - kind: "noteOn" - channel: number - key: number - velocity: number -} - - -export interface EventNoteOff extends EventCommon -{ - kind: "noteOff" - channel: number - key: number - velocity: number -} - - -export interface EventController extends EventCommon -{ - kind: "controller" - channel: number - controllerNumber: number - controllerValue: number - controllerName: - "channelVolumeCoarse" | - "channelVolumeFine" -} - - -export interface EventProgramChange extends EventCommon -{ - kind: "programChange" - channel: number - program: number -} - - -export interface EventEndOfTrack extends EventCommon -{ - kind: "endOfTrack" -} - - -export interface EventTrackName extends EventCommon -{ - kind: "trackName" - name: string -} - - -export interface EventSetTempo extends EventCommon -{ - kind: "setTempo" - bpm: number -} - - -export interface EventSetTimeSignature extends EventCommon -{ - kind: "setTimeSignature" - numerator: number - denominator: number - ticks: number - quarterNoteTicks: number -} - - -export interface EventSetKeySignature extends EventCommon -{ - kind: "setKeySignature" - accidentals: number - scale: number -} - - -export interface EventSequencerSpecific extends EventCommon -{ - kind: "sequencerSpecific" - text: string -} - - -interface TrackEvent -{ - event: Event, - code: number, - midiTime: number, -} - - -export class Decoder -{ - static fromBytes(bytes: number[] | Buffer | Uint8Array): Root - { - let midi: any = {} - - let r = new BinaryReader(bytes) - Decoder.readHeader(r, midi) - Decoder.readTracks(r, midi) - - return midi as Root - } - - - static readHeader(r: BinaryReader, midi: Root) - { - if (r.readAsciiLength(4) != "MThd") - throw "invalid midi header magic" - - midi.headerLength = r.readUInt32BE() - - midi.format = r.readUInt16BE() - midi.trackNum = r.readUInt16BE() - midi.timeDivisionRaw = r.readUInt16BE() - - midi.timeDivisionFormat = (midi.timeDivisionRaw & 0x8000) != 0 - - if (midi.timeDivisionFormat) - throw "unsupported time division format" - - midi.ticksPerQuarterNote = (midi.timeDivisionRaw & 0x7fff) - - r.seek(8 + midi.headerLength) - } - - - static readTracks(r: BinaryReader, midi: Root) - { - midi.tracks = [] - - for (let i = 0; i < midi.trackNum; i++) - midi.tracks.push(Decoder.readTrack(r, midi)) - } - - - static readTrack(r: BinaryReader, root: Root): Track - { - if (r.readAsciiLength(4) != "MTrk") - throw "invalid midi track magic" - - let track: Track = - { - length: r.readUInt32BE(), - events: [], - } - - let runningModePreviousCode = -1 - let currentMidiTime = 0 - - let eventStartPos = r.getPosition() - while (r.getPosition() < eventStartPos + track.length) - { - const { event, code, midiTime } = Decoder.readTrackEvent( - r, root, currentMidiTime, runningModePreviousCode) - - runningModePreviousCode = code - currentMidiTime = midiTime - track.events.push(event) - } - - r.seek(eventStartPos + track.length) - - return track - } - - - static readTrackEvent( - r: BinaryReader, - root: Root, - currentMidiTime: number, - runningModePreviousCode: number) - : TrackEvent - { - const midiDeltaTime = this.readVarLengthUInt(r) - const midiTime = currentMidiTime + midiDeltaTime - - let code = 0 - - if ((r.peekByte() & 0x80) == 0) - code = runningModePreviousCode - else - code = r.readByte() - - const event: EventCommon = - { - tick: Rational.fromFloat(midiTime / root.ticksPerQuarterNote / 4, 27720), - } - - let finalEvent: Event - - if (code >= 0x80 && code <= 0xef) - { - let rawData: number[] = [] - - if (code >= 0xc0 && code <= 0xdf) - rawData = r.readBytes(1) - else - rawData = r.readBytes(2) - - finalEvent = this.decodeChannelVoiceEvent(event, code, rawData) - } - else if (code >= 0xf0 && code <= 0xfe) - { - const length = this.readVarLengthUInt(r) - const eventUnknown: EventUnknown = - { - ...event, - kind: "unknown", - code, - metaCode: null, - rawData: r.readBytes(length), - } - - finalEvent = eventUnknown - } - else if (code == 0xff) - { - const metaCode = r.readByte() - const length = this.readVarLengthUInt(r) - const rawData = r.readBytes(length) - finalEvent = this.decodeMetaEvent(event, metaCode, rawData) - } - else - throw "invalid track event code 0x" + code.toString(16) - - return { - event: finalEvent, - code, - midiTime, - } - } - - - static decodeChannelVoiceEvent(event: EventCommon, code: number, rawData: number[]): Event - { - const isNoteOff = (code & 0xf0) == 0x80 - const isNoteOn = (code & 0xf0) == 0x90 - const isController = (code & 0xf0) == 0xb0 - const isProgramChange = (code & 0xf0) == 0xc0 - - if (isNoteOff || isNoteOn) - { - const channel = (code & 0x0f) - const key = rawData[0] - const velocity = rawData[1] - - const isReallyNoteOff = isNoteOff || rawData[1] == 0 - - if (isReallyNoteOff) - { - const eventNoteOff: EventNoteOff = - { - ...event, - kind: "noteOff", - channel, key, velocity, - } - - return eventNoteOff - } - else - { - const eventNoteOn: EventNoteOn = - { - ...event, - kind: "noteOn", - channel, key, velocity, - } - - return eventNoteOn - } - } - else if (isController) - { - const controllerNames: { [id: number]: EventController["controllerName"] } = - { - 7: "channelVolumeCoarse", - 39: "channelVolumeFine", - } - - const eventController: EventController = - { - ...event, - kind: "controller", - channel: (code & 0x0f), - controllerNumber: rawData[0], - controllerValue: rawData[1], - controllerName: controllerNames[rawData[0]] || "unknown", - } - - return eventController - } - else if (isProgramChange) - { - const eventProgramChange: EventProgramChange = - { - ...event, - kind: "programChange", - channel: (code & 0x0f), - program: rawData[0], - } - - return eventProgramChange - } - else - { - const eventUnknown: EventUnknown = - { - ...event, - kind: "unknown", - code, - metaCode: null, - rawData, - } - - return eventUnknown - } - } - - - static decodeMetaEvent(event: EventCommon, metaCode: number, rawData: number[]): Event - { - let r = new BinaryReader(rawData) - - if (metaCode == 0x03) - { - const name = r.readAsciiLength(r.getLength()) - - const eventTrackName: EventTrackName = - { - ...event, - kind: "trackName", - name, - } - - return eventTrackName - } - - else if (metaCode == 0x2f) - { - const eventEndOfTrack: EventEndOfTrack = - { - ...event, - kind: "endOfTrack", - } - - return eventEndOfTrack - } - - else if (metaCode == 0x51) - { - const msPerQuarterNote = r.readUInt24BE() - - const eventSetTempo: EventSetTempo = - { - ...event, - kind: "setTempo", - bpm: 60 * 1000 * 1000 / msPerQuarterNote, - } - - return eventSetTempo - } - - else if (metaCode == 0x58) - { - const numerator = r.readByte() - const denominator = Math.pow(2, r.readByte()) - const ticks = r.readByte() - const quarterNoteTicks = r.readByte() - - const eventSetTimeSignature: EventSetTimeSignature = - { - ...event, - kind: "setTimeSignature", - numerator, denominator, - ticks, quarterNoteTicks, - } - - return eventSetTimeSignature - } - - else if (metaCode == 0x59) - { - const accidentals = r.readSByte() - const scale = r.readByte() - - const eventSetKeySignature: EventSetKeySignature = - { - ...event, - kind: "setKeySignature", - accidentals, scale, - } - - return eventSetKeySignature - } - - else if (metaCode == 0x7f) - { - const text = r.readAsciiLength(r.getLength()) - - const eventSequencerSpecific: EventSequencerSpecific = - { - ...event, - kind: "sequencerSpecific", - text, - } - - return eventSequencerSpecific - } - else - { - const eventUnknown: EventUnknown = - { - ...event, - kind: "unknown", - code: 0xff, - metaCode, - rawData - } - - return eventUnknown - } - } - - - static readVarLengthUInt(r: BinaryReader): number - { - let value = 0 - - while (true) - { - let byte = r.readByte() - value = (value << 7) + (byte & 0x7f) - - if ((byte & 0x80) == 0) - break - } - - return value - } -} - - -export class Encoder -{ - static encode(root: Root): Uint8Array - { - var writer = new BinaryWriter() - - Encoder.encodeHeader(writer, root) - - for (const track of root.tracks) - Encoder.encodeTrack(writer, root, track) - - return new Uint8Array(writer.bytes) - } - - - static encodeHeader(writer: BinaryWriter, root: Root) - { - writer.writeAscii("MThd") - writer.writeUInt32BE(6) - writer.writeUInt16BE(1) - writer.writeUInt16BE(root.tracks.length) - writer.writeUInt16BE(root.ticksPerQuarterNote & 0x7fff) - } - - - static encodeTrack(writer: BinaryWriter, root: Root, track: Track) - { - writer.writeAscii("MTrk") - - const trackByteLengthPos = writer.head - writer.writeUInt32BE(0) - - let prevMidiTime = 0 - - track.events.sort((a, b) => a.tick.compare(b.tick)) - - for (const ev of track.events) - { - const midiTime = Math.floor( - ev.tick.asFloat() * 4 * root.ticksPerQuarterNote) - - const deltaMidiTime = Math.max(0, midiTime - prevMidiTime) - prevMidiTime = midiTime - - Encoder.writeVarLengthUInt(writer, deltaMidiTime) - - switch (ev.kind) - { - case "noteOff": - { - writer.writeByte(0x80 | (ev.channel & 0x0f)) - writer.writeByte(ev.key & 0x7f) - writer.writeByte(ev.velocity & 0x7f) - break - } - case "noteOn": - { - writer.writeByte(0x90 | (ev.channel & 0x0f)) - writer.writeByte(ev.key & 0x7f) - writer.writeByte(ev.velocity & 0x7f) - break - } - case "programChange": - { - writer.writeByte(0xc0 | (ev.channel & 0x0f)) - writer.writeByte(ev.program & 0x7f) - break - } - case "controller": - { - writer.writeByte(0xb0 | (ev.channel & 0x0f)) - writer.writeByte(ev.controllerNumber) - writer.writeByte(ev.controllerValue) - break - } - case "setTempo": - { - writer.writeByte(0xff) - writer.writeByte(0x51) - writer.writeByte(0x03) - writer.writeUInt24BE((60 * 1000 * 1000) / ev.bpm) - break - } - case "endOfTrack": - { - writer.writeByte(0xff) - writer.writeByte(0x2f) - writer.writeByte(0x00) - break - } - } - } - - const endPos = writer.head - writer.seek(trackByteLengthPos) - writer.writeUInt32BE(endPos - trackByteLengthPos - 4) - writer.seek(endPos) - } - - - static writeVarLengthUInt(writer: BinaryWriter, value: number) - { - let toWrite: number[] = [] - - while (true) - { - const byte = (value & 0x7f) - value >>= 7 - - toWrite.push(byte) - - if (value == 0) - break - } - - for (let i = toWrite.length - 1; i >= 0; i--) - writer.writeByte((i !== 0 ? 0x80 : 0) | toWrite[i]) - } -} \ No newline at end of file diff --git a/src/util/misc.ts b/src/util/misc.ts deleted file mode 100644 index fe62278..0000000 --- a/src/util/misc.ts +++ /dev/null @@ -1,40 +0,0 @@ -export function getMidiPresetEmoji(midiBank: number, midiPreset: number): string -{ - if (midiBank == 128) // Percussion - return "đŸĨ" - - if (midiPreset <= 7) // Piano - return "🎹" - else if (midiPreset <= 15) // Chromatic Percussion - return "🔔" - else if (midiPreset <= 23) // Organ - return "💨" - else if (midiPreset <= 31) // Guitar - return "🎸" - else if (midiPreset <= 39) // Bass - return "🎸" - else if (midiPreset <= 47) // Strings - return "đŸŽģ" - else if (midiPreset <= 55) // Ensemble - return "đŸŽģ" - else if (midiPreset <= 63) // Brass - return "đŸŽē" - else if (midiPreset <= 71) // Reed - return "🎷" - else if (midiPreset <= 79) // Pipe - return "âœī¸" - else if (midiPreset <= 87) // Synth Lead - return "đŸ•šī¸" - else if (midiPreset <= 95) // Synth Pad - return "đŸ•šī¸" - else if (midiPreset <= 103) // Synth FX - return "đŸ•šī¸" - else if (midiPreset <= 111) // Ethnic - return "đŸĒ•" - else if (midiPreset <= 119) // Percussive - return "đŸĨ" - else if (midiPreset <= 127) // Sound FX - return "🔊" - else - return "🎹" -} \ No newline at end of file diff --git a/src/util/pitch.js b/src/util/pitch.js new file mode 100644 index 0000000..5c5c803 --- /dev/null +++ b/src/util/pitch.js @@ -0,0 +1,10 @@ +function Pitch(midiPitch) +{ + this.midiPitch = midiPitch; +} + + +Pitch.prototype.clone = function() +{ + return new Pitch(this.midiPitch); +} \ No newline at end of file diff --git a/src/util/range.ts b/src/util/range.ts deleted file mode 100644 index c74700c..0000000 --- a/src/util/range.ts +++ /dev/null @@ -1,297 +0,0 @@ -import Rational from "./rational" - - -export default class Range -{ - start: Rational - end: Rational - startInclusive: boolean - endInclusive: boolean - - - constructor(start: Rational, end: Rational, startInclusive: boolean = true, endInclusive: boolean = false) - { - this.start = start - this.end = end - this.startInclusive = startInclusive - this.endInclusive = endInclusive - } - - - static fromPoint(p: Rational, startInclusive: boolean = true, endInclusive: boolean = true): Range - { - return new Range(p, p, startInclusive, endInclusive) - } - - - static fromStartDuration(start: Rational, duration: Rational): Range - { - return new Range( - start, - start.add(duration)) - } - - - static dummy(): Range - { - return new Range(new Rational(0), new Rational(0)) - } - - - get duration(): Rational - { - return this.end.subtract(this.start) - } - - - setDuration(newDuration: Rational): Range - { - return new Range( - this.start, - this.start.add(newDuration), - this.startInclusive, - this.endInclusive) - } - - - sorted(): Range - { - const startInclusive = this.start.lessThan(this.end) ? this.startInclusive : this.endInclusive - const endInclusive = this.end.greaterThan(this.start) ? this.endInclusive : this.startInclusive - - return new Range( - this.start.min(this.end)!, - this.start.max(this.end)!, - startInclusive, - endInclusive) - } - - - max(): Rational - { - return this.start.max(this.end)! - } - - - min(): Rational - { - return this.start.min(this.end)! - } - - - merge(other: Range | null): Range - { - return Range.merge(this, other)! - } - - - static merge(r1: Range | null, r2: Range | null): Range | null - { - if (r1 === null && r2 === null) - return null - - if (r1 === null) - return r2 - - if (r2 === null) - return r1 - - return new Range( - r1.start.min(r2.start)!, - r1.end.max(r2.end)!) - } - - - intersect(other: Range | null): Range - { - return Range.intersect(this, other)! - } - - - static intersect(r1: Range | null, r2: Range | null): Range | null - { - if (r1 === null && r2 === null) - return null - - if (r1 === null) - return r2 - - if (r2 === null) - return r1 - - return new Range( - r1.start.max(r2.start)!, - r1.end.min(r2.end)!) - } - - - atZero(): Range - { - return new Range( - new Rational(0), - this.duration, - this.startInclusive, - this.endInclusive) - } - - - stretch(offset: Rational, pivot: Rational, origin: Rational): Range - { - return new Range( - this.start.stretch(offset, pivot, origin), - this.end.stretch(offset, pivot, origin), - this.startInclusive, - this.endInclusive) - } - - - displace(offset: Rational): Range - { - return new Range( - this.start.add(offset), - this.end.add(offset), - this.startInclusive, - this.endInclusive) - } - - - subtract(offset: Rational): Range - { - return new Range( - this.start.subtract(offset), - this.end.subtract(offset), - this.startInclusive, - this.endInclusive) - } - - - grow(offset: Rational): Range - { - return new Range( - this.start.subtract(offset), - this.end.add(offset), - this.startInclusive, - this.endInclusive) - } - - - shrink(offset: Rational): Range - { - return new Range( - this.start.add(offset), - this.end.subtract(offset), - this.startInclusive, - this.endInclusive) - } - - - snap(step: Rational): Range - { - return new Range( - this.start.snap(step), - this.end.snap(step), - this.startInclusive, - this.endInclusive) - } - - - *iterSlices(slice: Range): Generator - { - if (slice.start.compare(this.start) <= 0) - { - if (slice.end.compare(this.end) < 0) - yield new Range(slice.end.max(this.start), this.end) - } - else - { - if (slice.end.compare(this.end) >= 0) - yield new Range(this.start, slice.start.min(this.end)) - else - { - yield new Range(this.start, slice.start.min(this.end)) - yield new Range(slice.end.max(this.start), this.end) - } - } - } - - - isPoint(): boolean - { - return this.start.equalTo(this.end) - } - - - overlapsPoint(point: Rational): boolean - { - const compStart = this.start.compare(point) - const compEnd = this.end.compare(point) - - const checkStart = (this.startInclusive ? (compStart <= 0) : (compStart < 0)) - const checkEnd = (this.endInclusive ? (compEnd >= 0) : (compEnd > 0)) - - return checkStart && checkEnd - } - - - overlapsRange(range: Range): boolean - { - const compStart = this.start.compare(range.end) - const compEnd = this.end.compare(range.start) - - const checkStart = (this.startInclusive && range.endInclusive ? (compStart <= 0) : (compStart < 0)) - const checkEnd = (this.endInclusive && range.startInclusive ? (compEnd >= 0) : (compEnd > 0)) - - return checkStart && checkEnd - } - - - containsRangeCompletely(range: Range): boolean - { - const compStart = this.start.compare(range.start) - const compEnd = this.end.compare(range.end) - - const checkStart = (this.startInclusive ? (compStart <= 0) : (compStart < 0)) - const checkEnd = (this.endInclusive ? (compEnd >= 0) : (compEnd > 0)) - - return checkStart && checkEnd - } - - - quantize(maxDenom: number): Range - { - return new Range( - this.start.quantize(maxDenom), - this.end.quantize(maxDenom), - this.startInclusive, - this.endInclusive) - } - - - toString(): string - { - return "[" + this.start.toString() + " ~ " + this.end.toString() + "]" - } - - - toJson(quantize?: number): [[number, number, number]] | [[number, number, number], [number, number, number]] - { - if (this.start.compare(this.end) == 0) - return [this.start.toJson()] - else - { - if (quantize !== undefined) - return [this.start.toJson(), this.duration.quantize(quantize).toJson()] - else - return [this.start.toJson(), this.duration.toJson()] - } - } - - - static fromJson(json: [[number, number, number]] | [[number, number, number], [number, number, number]]): Range - { - if (json.length == 1) - return Range.fromPoint(Rational.fromJson(json[0])) - else - return Range.fromStartDuration(Rational.fromJson(json[0]), Rational.fromJson(json[1])) - } -} \ No newline at end of file diff --git a/src/util/rational.ts b/src/util/rational.ts deleted file mode 100644 index 5db5f08..0000000 --- a/src/util/rational.ts +++ /dev/null @@ -1,316 +0,0 @@ -function mod(x: number, m: number): number -{ - return (x % m + m) % m -} - - -export default class Rational -{ - numerator: number - denominator: number - - - constructor(numerator: number = 0, denominator: number = 1) - { - if (denominator == 0) - throw "denominator zero" - - if (!isFinite(numerator) || !isFinite(denominator)) - throw "invalid rational" - - this.numerator = numerator - this.denominator = denominator - this.simplifyInPlace() - } - - - static fromFloat(floatValue: number, maxDenominator: number): Rational - { - const integer = Math.floor(floatValue) - const frac = floatValue - integer - - return new Rational(integer * maxDenominator + Math.round(frac * maxDenominator), maxDenominator) - } - - - static fromIntegerPlusRational(integer: number, numeratorWithoutInteger: number, denominator: number): Rational - { - return new Rational(integer * denominator + numeratorWithoutInteger, denominator) - } - - - asFloat(): number - { - return this.numerator / this.denominator - } - - - get n(): number - { - return this.numerator - } - - - get d(): number - { - return this.denominator - } - - - get integer(): number - { - return Math.floor(this.numerator / this.denominator) - } - - - get numeratorWithoutInteger(): number - { - return mod(this.numerator, this.denominator) - } - - - negate(): Rational - { - return new Rational( - -this.numerator, - this.denominator) - } - - - absolute(): Rational - { - if (this.numerator < 0) - return new Rational(-this.numerator, this.denominator) - else - return this - } - - - add(other: Rational): Rational - { - return new Rational( - this.numerator * other.denominator + other.numerator * this.denominator, - this.denominator * other.denominator) - } - - - subtract(other: Rational): Rational - { - return new Rational( - this.numerator * other.denominator - other.numerator * this.denominator, - this.denominator * other.denominator) - } - - - multiply(other: Rational): Rational - { - return new Rational( - this.numerator * other.numerator, - this.denominator * other.denominator) - } - - - multiplyByFloat(x: number): Rational - { - return new Rational( - this.numerator * x, - this.denominator) - } - - - divide(other: Rational): Rational - { - return new Rational( - this.numerator * other.denominator, - this.denominator * other.numerator) - } - - - snap(step: Rational): Rational - { - return Rational.fromFloat(this.asFloat(), step.denominator) - } - - - quantize(maxDenominator: number): Rational - { - if (this.denominator <= maxDenominator) - return this.simplify() - - return new Rational( - Math.round(this.numerator / this.denominator * maxDenominator), - maxDenominator) - } - - - stretch(offset: Rational, pivot: Rational, origin: Rational): Rational - { - let dist = origin.subtract(pivot) - if (dist.numerator == 0) - return this - - let p = this.subtract(pivot).divide(dist) - let move = origin.add(offset).subtract(pivot).divide(dist) - - return pivot.add(dist.multiply(p).multiply(move)) - } - - - isZero(): boolean - { - return this.numerator == 0 - } - - - compare(other: Rational): number - { - let thisNumerator = this.numerator * other.denominator - let otherNumerator = other.numerator * this.denominator - - if (thisNumerator < otherNumerator) - return -1 - else if (thisNumerator > otherNumerator) - return 1 - else - return 0 - } - - - equalTo(other: Rational): boolean - { - return this.compare(other) == 0 - } - - - notEqualTo(other: Rational): boolean - { - return this.compare(other) != 0 - } - - - lessThan(other: Rational): boolean - { - return this.compare(other) < 0 - } - - - lessThanOrEqual(other: Rational): boolean - { - return this.compare(other) <= 0 - } - - - greaterThan(other: Rational): boolean - { - return this.compare(other) > 0 - } - - - greaterThanOrEqual(other: Rational): boolean - { - return this.compare(other) >= 0 - } - - - static max(a: T, b: T): T - { - if (a === null && b === null) - return null! - - if (a === null) - return b - - if (b === null) - return a - - if (a.compare(b!) > 0) - return a - else - return b - } - - - static min(a: T, b: T): T - { - if (a === null && b === null) - return null! - - if (a === null) - return b - - if (b === null) - return a - - if (a.compare(b!) < 0) - return a - else - return b - } - - - max(other: Rational | null): Rational - { - return Rational.max(this, other)! - } - - - min(other: Rational | null): Rational - { - return Rational.min(this, other)! - } - - - simplify() - { - return new Rational(this.numerator, this.denominator) - } - - - simplifyInPlace() - { - this.trySimplifyInPlaceBy(2) - this.trySimplifyInPlaceBy(3) - this.trySimplifyInPlaceBy(5) - this.trySimplifyInPlaceBy(7) - this.trySimplifyInPlaceBy(11) - this.trySimplifyInPlaceBy(13) - } - - - trySimplifyInPlaceBy(divider: number) - { - while ((this.numerator % divider) == 0 && (this.denominator % divider) == 0) - { - this.numerator /= divider - this.denominator /= divider - } - } - - - toString(): string - { - let integer = Math.floor(this.numerator / this.denominator) - let numerator = this.numerator % this.denominator - - if (numerator == 0) - return integer.toString() - else - return integer.toString() + " + " + numerator.toString() + "/" + this.denominator.toString() - } - - - toJson(): [number, number, number] - { - return [ - this.integer, - this.numeratorWithoutInteger, - this.denominator - ] - } - - - static fromJson(array: [number, number, number]): Rational - { - return new Rational(array[0] * array[2] + array[1], array[2]) - } -} \ No newline at end of file diff --git a/src/util/rect.ts b/src/util/rect.ts deleted file mode 100644 index cade3a1..0000000 --- a/src/util/rect.ts +++ /dev/null @@ -1,171 +0,0 @@ -export default class Rect -{ - x: number - y: number - w: number - h: number - - - constructor(x: number, y: number, w: number, h: number) - { - this.x = x - this.y = y - this.w = w - this.h = h - } - - - static fromVertices(x1: number, y1: number, x2: number, y2: number): Rect - { - return new Rect( - Math.min(x1, x2), - Math.min(y1, y2), - Math.abs(x2 - x1), - Math.abs(y2 - y1)) - } - - - static fromElement(elem: HTMLElement) - { - const clientRect = elem.getBoundingClientRect() - return new Rect( - clientRect.left, - clientRect.top, - clientRect.width, - clientRect.height) - } - - - clone(): Rect - { - return new Rect(this.x, this.y, this.w, this.h) - } - - - get x1(): number - { - return this.x - } - - - get y1(): number - { - return this.y - } - - - get x2(): number - { - return this.x + this.w - } - - - get y2(): number - { - return this.y + this.h - } - - - get xCenter(): number - { - return (this.x1 + this.x2) / 2 - } - - - get yCenter(): number - { - return (this.y1 + this.y2) / 2 - } - - - withX(value: number): Rect - { - return new Rect(value, this.y, this.w, this.h) - } - - - withY(value: number): Rect - { - return new Rect(this.x, value, this.w, this.h) - } - - - withW(value: number): Rect - { - return new Rect(this.x, this.y, value, this.h) - } - - - withH(value: number): Rect - { - return new Rect(this.x, this.y, this.w, value) - } - - - withX1(value: number): Rect - { - return Rect.fromVertices(value, this.y1, this.x2, this.y2) - } - - - withY1(value: number): Rect - { - return Rect.fromVertices(this.x1, value, this.x2, this.y2) - } - - - withX2(value: number): Rect - { - return Rect.fromVertices(this.x1, this.y1, value, this.y2) - } - - - withY2(value: number): Rect - { - return Rect.fromVertices(this.x1, this.y1, this.x2, value) - } - - - displace(x: number, y: number): Rect - { - return new Rect(this.x + x, this.y + y, this.w, this.h) - } - - - expand(amount: number): Rect - { - return Rect.fromVertices( - this.x1 - amount, - this.y1 - amount, - this.x2 + amount, - this.y2 + amount) - } - - - expandW(amount: number): Rect - { - return Rect.fromVertices( - this.x1 - amount, - this.y1, - this.x2 + amount, - this.y2) - } - - - contains(p: { x: number, y: number }): boolean - { - return p.x >= this.x && - p.x < this.x2 && - p.y >= this.y && - p.y < this.y2 - } - - - overlaps(other: Rect): boolean - { - return this.x2 >= other.x && - this.x < other.x2 && - this.y2 >= other.y && - this.y < other.y2 - } -} \ No newline at end of file diff --git a/src/util/refState.ts b/src/util/refState.ts deleted file mode 100644 index 5cdddba..0000000 --- a/src/util/refState.ts +++ /dev/null @@ -1,25 +0,0 @@ -import React from "react" - - -export interface RefState -{ - ref: React.MutableRefObject - update: boolean - commit: () => void -} - - -export function useRefState(initializer: () => T): RefState -{ - const [update, setUpdate] = React.useState(false) - - const ref = React.useRef(null!) - if (ref.current === null) - ref.current = initializer() - - return { - ref, - update, - commit: () => setUpdate(n => !n) - } -} \ No newline at end of file diff --git a/src/util/theory.js b/src/util/theory.js new file mode 100644 index 0000000..9dd11fe --- /dev/null +++ b/src/util/theory.js @@ -0,0 +1,152 @@ +function Theory() +{ + var C = 0; this.C = C; + var Cs = 1; this.Cs = Cs; + var D = 2; this.D = D; + var Ds = 3; this.Ds = Ds; + var E = 4; this.E = E; + var F = 5; this.F = F; + var Fs = 6; this.Fs = Fs; + var G = 7; this.G = G; + var Gs = 8; this.Gs = Gs; + var A = 9; this.A = A; + var As = 10; this.As = As; + var B = 11; this.B = B; + + + this.scales = + [ + { name: "Major", notes: [ C, D, E, F, G, A, B ], degrees: [0, 0.5, 1, 1.5, 2, 3, 3.5, 4, 4.5, 5, 5.5, 6] } + ]; + + + this.chords = + [ + { notes: [ C, E, G ], uppercase: true, symbolSup: "", symbolSub: "", name: "Major" }, + { notes: [ C, Ds, G ], uppercase: false, symbolSup: "", symbolSub: "", name: "Minor" }, + { notes: [ C, Ds, Fs ], uppercase: false, symbolSup: "o", symbolSub: "", name: "Diminished" }, + { notes: [ C, E, Gs ], uppercase: true, symbolSup: "+", symbolSub: "", name: "Augmented" }, + { notes: [ C, E, Fs ], uppercase: true, symbolSup: "(♭5)", symbolSub: "", name: "Flat Fifth(?)" }, + { notes: [ C, D, Fs ], uppercase: true, symbolSup: "(?)", symbolSub: "", name: "(?)" }, + + { notes: [ C, D, G ], uppercase: true, symbolSup: "", symbolSub: "sus2", name: "Major with Suspended Second" }, + { notes: [ C, F, G ], uppercase: true, symbolSup: "", symbolSub: "sus4", name: "Major with Suspended Fourth" }, + { notes: [ C, E, G, A ], uppercase: true, symbolSup: "add6", symbolSub: "", name: "Major with Added Sixth" }, + { notes: [ C, E, G, D ], uppercase: true, symbolSup: "add9", symbolSub: "", name: "Major with Added Ninth" }, + { notes: [ C, E, G, F ], uppercase: true, symbolSup: "add11", symbolSub: "", name: "Major with Added Eleventh" }, + + { notes: [ C, E, G, As ], uppercase: true, symbolSup: "7", symbolSub: "", name: "Dominant Seventh" }, + { notes: [ C, E, G, B ], uppercase: true, symbolSup: "M7", symbolSub: "", name: "Major Seventh" }, + { notes: [ C, Ds, G, As ], uppercase: false, symbolSup: "7", symbolSub: "", name: "Minor Seventh" }, + { notes: [ C, Ds, G, B ], uppercase: true, symbolSup: "m(M7)", symbolSub: "", name: "Minor-Major Seventh" }, + { notes: [ C, Ds, G, A ], uppercase: false, symbolSup: "o7", symbolSub: "", name: "Diminished Seventh" }, + { notes: [ C, Ds, Fs, As ], uppercase: false, symbolSup: "ø7", symbolSub: "", name: "Half-Diminished Seventh" }, + { notes: [ C, E, Gs, As ], uppercase: true, symbolSup: "+7", symbolSub: "", name: "Augmented Seventh" }, + { notes: [ C, E, Gs, B ], uppercase: true, symbolSup: "+(M7)", symbolSub: "", name: "Augmented Major Seventh" }, + + { notes: [ C, E, G, B, D ], uppercase: true, symbolSup: "maj9", symbolSub: "", name: "Major Ninth" }, + { notes: [ C, Ds, G, As, D ], uppercase: true, symbolSup: "m9", symbolSub: "", name: "Minor Ninth" }, + { notes: [ C, E, G, As, D ], uppercase: true, symbolSup: "9", symbolSub: "", name: "Dominant Ninth" }, + { notes: [ C, E, G, As, Cs ], uppercase: true, symbolSup: "7♭9", symbolSub: "", name: "Dominant Minor Ninth" }, + { notes: [ C, E, G, A, D ], uppercase: true, symbolSup: "6/9", symbolSub: "", name: "6/9" }, + ]; +} + + +Theory.prototype.pitchNameInScale = function(scaleIndex, midiPitch) +{ + // TODO: Take into consideration scale accidentals. + var names = ["C", "C♯", "D", "D♯", "E", "F", "F♯", "G", "G♯", "A", "A♯", "B"]; + return names[midiPitch % 12]; +} + + +Theory.prototype.pitchDegreeInKey = function(scaleIndex, rootMidiPitch, midiPitch) +{ + return this.scales[scaleIndex].degrees[(midiPitch + 12 - rootMidiPitch) % 12]; +} + + +Theory.prototype.chordSymbolInKey = function(scaleIndex, rootMidiPitch, midiPitch, uppercase) +{ + // TODO: Take into consideration scale accidentals. + var upper = ["I", "♭II", "II", "♭III", "III", "IV", "♭V", "V", "♭VI", "VI", "♭VII", "VII"]; + var lower = ["i", "♭ii", "ii", "♭iii", "iii", "iv", "♭v", "v", "♭vi", "vi", "♭vii", "vii"]; + return (uppercase ? upper : lower)[(midiPitch + 12 - rootMidiPitch) % 12]; +} + + +Theory.prototype.chordBassPitch = function(chordIndex, rootMidiPitch) +{ + if (rootMidiPitch >= theory.Fs) + return 12 * 3 + rootMidiPitch; + else + return 12 * 4 + rootMidiPitch; +} + + +Theory.prototype.chordMainPitches = function(chordIndex, rootMidiPitch) +{ + var pitches = []; + var lastPitch = 12 * 5; + + for (var i = 0; i < this.chords[chordIndex].notes.length; i++) + { + var pitch = (rootMidiPitch + this.chords[chordIndex].notes[i]) % 12; + + while (pitch < lastPitch) + pitch += 12; + + pitches.push(pitch); + lastPitch = pitch; + } + + if (rootMidiPitch >= theory.Fs) + pitches = pitches.map(function (pitch) { return pitch - 12; }); + + return pitches; +} + + +Theory.prototype.chordBassVoicingInMeter = function(meter) +{ + // [ [ start beat, end beat, volume ], ... ] + switch (meter.numerator) + { + case 3: return [ [ 0, 1.5, 1 ], [ 1.5, 2, 0.7 ] ]; + case 4: return [ [ 0, 1.5, 1 ], [ 1.5, 2, 0.7 ], [ 2, 3.5, 1 ], [ 3.5, 4, 0.7 ] ]; + default: return [ ]; + } +} + + +Theory.prototype.chordMainVoicingInMeter = function(meter) +{ + // [ [ start beat, end beat, volume ], ... ] + switch (meter.numerator) + { + case 3: return [ [ 0, 1, 1 ], [ 1, 2, 0.7 ], [ 2, 3, 0.7 ] ]; + case 4: return [ [ 0, 0.9, 1 ], [ 1, 1.9, 0.5 ], [ 2, 2.9, 0.7 ], [ 3, 3.9, 0.5 ] ]; + default: return [ ]; + } +} + + +Theory.prototype.degreeColor = function(degree) +{ + switch (degree) + { + case 0: return "#ff0000"; + case 1: return "#ff8800"; + case 2: return "#ffdd00"; + case 3: return "#00dd00"; + case 4: return "#0000ff"; + case 5: return "#8800ff"; + case 6: return "#ff00ff"; + case 7: return "#ff0000"; + default: return "#888888"; + } +} + + +var theory = new Theory(); \ No newline at end of file diff --git a/src/util/timerange.js b/src/util/timerange.js new file mode 100644 index 0000000..e95a718 --- /dev/null +++ b/src/util/timerange.js @@ -0,0 +1,84 @@ +function TimeRange(start, end) +{ + this.start = start; + this.end = end; +} + + +TimeRange.prototype.clone = function() +{ + return new TimeRange(this.start, this.end); +} + + +TimeRange.prototype.merge = function(other) +{ + this.start = Math.min(this.start, other.start) + this.end = Math.max(this.end, other.end); +} + + +TimeRange.prototype.stretch = function(pivot, origin, delta) +{ + var start = stretch(this.start, pivot, origin, delta); + var end = stretch(this.end, pivot, origin, delta); + + this.start = Math.min(start, end); + this.end = Math.max(start, end); +} + + +TimeRange.prototype.clip = function(min, max) +{ + this.start = Math.max(this.start, min) + this.end = Math.min(this.end, max); +} + + +TimeRange.prototype.getClippedParts = function(clipRange) +{ + var parts = []; + + if (!this.overlapsRange(clipRange)) + parts.push(this.clone()); + else + { + if (clipRange.start > this.start) + parts.push(new TimeRange(this.start, clipRange.start)); + + if (clipRange.end < this.end) + parts.push(new TimeRange(clipRange.end, this.end)); + } + + return parts; +} + + +TimeRange.prototype.duration = function() +{ + return this.end - this.start; +} + + +TimeRange.prototype.overlapsTime = function(time) +{ + return time > this.start && time < this.end; +} + + +TimeRange.prototype.includesTime = function(time) +{ + return time >= this.start && time < this.end; +} + + +TimeRange.prototype.overlapsRange = function(other) +{ + return this.start < other.end && this.end > other.start; +} + + +TimeRange.prototype.includedInRange = function(other) +{ + return this.start < other.end && this.end >= other.start; +} \ No newline at end of file diff --git a/src/util/wavEncode.ts b/src/util/wavEncode.ts deleted file mode 100644 index 8df2356..0000000 --- a/src/util/wavEncode.ts +++ /dev/null @@ -1,116 +0,0 @@ -// From https://github.com/Jam3/audiobuffer-to-wav -// with modifications - - -export interface WavExportOptions -{ - float32?: boolean -} - - -export function encode(buffer: AudioBuffer, opt?: WavExportOptions): ArrayBuffer -{ - opt = opt || {} - - var numChannels = buffer.numberOfChannels - var sampleRate = buffer.sampleRate - var format = opt.float32 ? 3 : 1 - var bitDepth = format === 3 ? 32 : 16 - - var result - if (numChannels === 2) - result = interleave(buffer.getChannelData(0), buffer.getChannelData(1)) - else - result = buffer.getChannelData(0) - - return encodeWAV(result, format, sampleRate, numChannels, bitDepth) -} - - -function encodeWAV( - samples: Float32Array, - format: number, - sampleRate: number, - numChannels: number, - bitDepth: number) -{ - var bytesPerSample = bitDepth / 8 - var blockAlign = numChannels * bytesPerSample - - var buffer = new ArrayBuffer(44 + samples.length * bytesPerSample) - var view = new DataView(buffer) - - /* RIFF identifier */ - writeString(view, 0, 'RIFF') - /* RIFF chunk length */ - view.setUint32(4, 36 + samples.length * bytesPerSample, true) - /* RIFF type */ - writeString(view, 8, 'WAVE') - /* format chunk identifier */ - writeString(view, 12, 'fmt ') - /* format chunk length */ - view.setUint32(16, 16, true) - /* sample format (raw) */ - view.setUint16(20, format, true) - /* channel count */ - view.setUint16(22, numChannels, true) - /* sample rate */ - view.setUint32(24, sampleRate, true) - /* byte rate (sample rate * block align) */ - view.setUint32(28, sampleRate * blockAlign, true) - /* block align (channel count * bytes per sample) */ - view.setUint16(32, blockAlign, true) - /* bits per sample */ - view.setUint16(34, bitDepth, true) - /* data chunk identifier */ - writeString(view, 36, 'data') - /* data chunk length */ - view.setUint32(40, samples.length * bytesPerSample, true) - if (format === 1) // Raw PCM - floatTo16BitPCM(view, 44, samples) - else - writeFloat32(view, 44, samples) - - return buffer -} - - -function interleave(inputL: Float32Array, inputR: Float32Array) -{ - const length = inputL.length + inputR.length - const result = new Float32Array(length) - - let index = 0 - let inputIndex = 0 - - while (index < length) - { - result[index++] = inputL[inputIndex] - result[index++] = inputR[inputIndex] - inputIndex++ - } - return result -} - - -function writeFloat32(output: DataView, offset: number, input: Float32Array) -{ - for (var i = 0; i < input.length; i++, offset += 4) - output.setFloat32(offset, input[i], true) -} - - -function floatTo16BitPCM(output: DataView, offset: number, input: Float32Array) -{ - for (var i = 0; i < input.length; i++, offset += 2) - { - const s = Math.max(-1, Math.min(1, input[i])) - output.setInt16(offset, s * 0x7fff, true) - } -} - -function writeString(view: DataView, offset: number, string: string) -{ - for (var i = 0; i < string.length; i++) - view.setUint8(offset + i, string.charCodeAt(i)) -} \ No newline at end of file diff --git a/src/windows/Inspector.tsx b/src/windows/Inspector.tsx deleted file mode 100644 index b4d73b8..0000000 --- a/src/windows/Inspector.tsx +++ /dev/null @@ -1,88 +0,0 @@ -import React from "react" -import * as Dockable from "../dockable" -import * as Project from "../project" -import * as UI from "../ui" -import { InspectorMultitype } from "./InspectorMultitype" -import { InspectorTrack } from "./InspectorTrack" -import { InspectorKeyChange } from "./InspectorKeyChange" -import { InspectorMeterChange } from "./InspectorMeterChange" -import { InspectorChord } from "./InspectorChord" - - -interface InspectorProps -{ - elemIds: Project.ID[] -} - - -export function Inspector() -{ - const windowCtx = Dockable.useWindow() - - const props: InspectorProps = windowCtx.data - const [elemIds, setElemIds] = React.useState(props.elemIds) - - const elemTypes = React.useMemo(() => - { - const types = new Map() - - for (const elemId of elemIds) - { - const elem = Project.global.project.elems.get(elemId) - if (!elem) - continue - - const typeCount = types.get(elem.type) - if (typeCount === undefined) - { - types.set(elem.type, 1) - } - else - { - types.set(elem.type, typeCount + 1) - } - } - - return types - - }, [elemIds])! - - - if (elemTypes.size == 1) - { - switch (elemTypes.keys().next().value as Project.Element["type"]) - { - case "track": - { - windowCtx.setPreferredSize(900, 500) - return - } - case "keyChange": - { - windowCtx.setPreferredSize(500, 350) - return - } - case "meterChange": - { - windowCtx.setPreferredSize(500, 350) - return - } - case "chord": - { - windowCtx.setPreferredSize(500, 350) - return - } - default: - return null - } - } - else - { - windowCtx.setPreferredSize(500, 350) - return - } -} \ No newline at end of file diff --git a/src/windows/InspectorChord.tsx b/src/windows/InspectorChord.tsx deleted file mode 100644 index 20e8501..0000000 --- a/src/windows/InspectorChord.tsx +++ /dev/null @@ -1,195 +0,0 @@ -import React from "react" -import * as Dockable from "../dockable" -import * as Project from "../project" -import * as Prefs from "../prefs" -import * as Playback from "../playback" -import * as Theory from "../theory" -import * as UI from "../ui" - - -export interface InspectorChordProps -{ - elemIds: Project.ID[] -} - - -export function InspectorChord(props: InspectorChordProps) -{ - const windowCtx = Dockable.useWindow() - - const [baseChordType, setBaseChordType] = React.useState(5) - - - const chords: Project.Chord[] = [] - for (const elemId of props.elemIds) - { - const chord = Project.getElem(Project.global.project, elemId, "chord") - if (!chord) - continue - - chords.push(chord) - } - - if (chords.length == 0) - return null - - - windowCtx.setTitle(chords.length == 1 ? `Chord` : `${ chords.length } Chords`) - - - let sameChord: Theory.Chord | null = chords[0].chord - let sameKey: Theory.Key = Project.keyAt(Project.global.project, chords[0].parentId, chords[0].range.start) - for (const chord of chords) - { - if (chord.chord !== sameChord) - sameChord = null - - //const key = Project.keyAt(Project.global.project, chord.parentId, chord.range.start) - //if (key !== sameKey) - // sameKey = null - } - - - const modifyChords = (func: (chord: Project.Chord) => Project.Chord) => - { - for (const chord of chords) - { - const newChord = func(chord) - console.log("InspectorKeyChange.modifyChords", chord, newChord) - Project.global.project = Project.upsertElement(Project.global.project, newChord) - } - - Project.notifyObservers() - window.dispatchEvent(new Event("timelineRefresh")) - } - - - const onChooseChord = (chord: Theory.Chord) => - { - Playback.playChordPreview( - Project.global.project.chordTrackId, - chord, - 0, 1) - - modifyChords(ch => - { - return Project.elemModify(ch, { chord }) - }) - } - - - const chordTypeOptions: { label: string, value: any }[] = [] - Theory.Chord.kinds.map((chord, index) => - { - chordTypeOptions.push({ - label: - (chord.symbol[0] ? "iv" : "IV") + - chord.symbol[1] + - (chord.symbol[2] || "") + - " - " + chord.name, - value: chord.id, - }) - }) - - const chordButtons = [] - if (typeof baseChordType === "number") - { - for (let degree = 0; degree < 7; degree++) - { - const root = sameKey.midiForDegree(degree) - - let pitches = [0] - pitches.push(sameKey.midiForDegree(degree + 2) - root) - pitches.push(sameKey.midiForDegree(degree + 4) - root) - - if (baseChordType >= 7) - pitches.push(sameKey.midiForDegree(degree + 6) - root) - - if (baseChordType >= 9) - pitches.push(sameKey.midiForDegree(degree + 8) - root) - - if (baseChordType >= 11) - pitches.push(sameKey.midiForDegree(degree + 10) - root) - - if (baseChordType >= 13) - pitches.push(sameKey.midiForDegree(degree + 12) - root) - - const kind = Theory.Chord.kindFromPitches(pitches) - const chord = new Theory.Chord(root, kind, 0, []) - chordButtons.push({ chord }) - } - } - else - { - for (let rootNote = 0; rootNote < 12; rootNote++) - { - const root = sameKey.tonic.chroma + rootNote - - const kind = Theory.Chord.kindFromId(baseChordType) - const chord = new Theory.Chord(root, kind, 0, []) - - chordButtons.push({ chord }) - } - } - - - return
- -
- - setBaseChordType(item.value) } - /> - -
- { chordButtons.map((cb, i) => - onChooseChord(cb.chord) } - /> - )} -
- -
- -
-} \ No newline at end of file diff --git a/src/windows/InspectorKeyChange.tsx b/src/windows/InspectorKeyChange.tsx deleted file mode 100644 index a36a5c5..0000000 --- a/src/windows/InspectorKeyChange.tsx +++ /dev/null @@ -1,223 +0,0 @@ -import React from "react" -import * as Dockable from "../dockable" -import * as Project from "../project" -import * as Prefs from "../prefs" -import * as Theory from "../theory" -import * as UI from "../ui" - - -export interface InspectorKeyChangeProps -{ - elemIds: Project.ID[] -} - - -export function InspectorKeyChange(props: InspectorKeyChangeProps) -{ - const windowCtx = Dockable.useWindow() - - - const keyChs: Project.KeyChange[] = [] - for (const elemId of props.elemIds) - { - const keyCh = Project.getElem(Project.global.project, elemId, "keyChange") - if (!keyCh) - continue - - keyChs.push(keyCh) - } - - if (keyChs.length == 0) - return null - - - windowCtx.setTitle(keyChs.length == 1 ? `Key Change` : `${ keyChs.length } Key Changes`) - - - let sameTonicLetter: number | null = keyChs[0].key.tonic.letter - let sameTonicAccidental: number | null = keyChs[0].key.tonic.accidental - let sameScale: Theory.Scale | null = keyChs[0].key.scale - for (const keyCh of keyChs) - { - if (keyCh.key.tonic.letter !== sameTonicLetter) - sameTonicLetter = null - - if (keyCh.key.tonic.accidental !== sameTonicAccidental) - sameTonicAccidental = null - - if (sameScale && keyCh.key.scale.id !== sameScale.id) - sameScale = null - } - - const sameKey = - sameTonicLetter === null || - sameTonicAccidental === null || - sameScale === null ? - null : - Theory.Key.fromTonicAndScale( - new Theory.PitchName(sameTonicLetter, sameTonicAccidental), - sameScale) - - - const tonicLetterOptions = /*[0, 4, 1, 5, 2, 6, 3]*/[0, 1, 2, 3, 4, 5, 6].map(letter => - { - return { - label: Theory.Utils.letterToStr(letter), - value: letter, - } - }) - - - const tonicAccidentalOptions = [-1, 0, 1].map(acc => - { - return { - label: acc == 0 ? "♮" : Theory.Utils.accidentalToStr(acc), - value: acc, - } - }) - - - const scaleOptions = Theory.Scale.list.map(scale => - { - return { - label: scale.names[0], - value: scale.id, - } - }) - - - const chromaticOptions = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11].map(chroma => - { - const key = sameKey ?? Theory.Key.parse("C Major") - - return { - label: key.nameForChroma(chroma).str, - value: chroma, - bkgColor: Theory.Utils.degreeToColor(key.degreeForChroma(chroma)), - width: "2em", - } - }) - - - const chromaticSelected = !sameKey ? [] : [0, 1, 2, 3, 4, 5, 6].map(degree => - { - const key = sameKey ?? Theory.Key.parse("C Major") - return key.chromaForDegree(degree) - }) - - - const modifyKeyChs = (func: (keyCh: Project.KeyChange) => Project.KeyChange) => - { - for (const keyCh of keyChs) - { - const newKeyCh = func(keyCh) - console.log("InspectorKeyChange.modifyKeyChs", keyCh, newKeyCh) - Project.global.project = Project.upsertElement(Project.global.project, newKeyCh) - } - - Project.notifyObservers() - window.dispatchEvent(new Event("timelineRefresh")) - } - - - const onChangeTonicLetter = (item: any) => - { - modifyKeyChs((keyCh) => - { - const tonic = new Theory.PitchName(item.value, keyCh.key.tonic.accidental) - const newKey = new Theory.Key(tonic, keyCh.key.scale) - return Project.elemModify(keyCh, { key: newKey }) - }) - } - - - const onChangeTonicAccidental = (item: any) => - { - modifyKeyChs((keyCh) => - { - const tonic = new Theory.PitchName(keyCh.key.tonic.letter, item.value) - const newKey = new Theory.Key(tonic, keyCh.key.scale) - return Project.elemModify(keyCh, { key: newKey }) - }) - } - - - const onChangeScale = (item: any) => - { - modifyKeyChs((keyCh) => - { - const scale = Theory.Scale.fromId(item.value) - const newKey = new Theory.Key(keyCh.key.tonic, scale) - return Project.elemModify(keyCh, { key: newKey }) - }) - } - - - return
- -
-
- Tonic -
- - - -
- Accidental -
- - - -
- Scale -
- - - -
- -
- Pitches -
- - {} } - /> - -
- -
-} \ No newline at end of file diff --git a/src/windows/InspectorMeterChange.tsx b/src/windows/InspectorMeterChange.tsx deleted file mode 100644 index 1464fc8..0000000 --- a/src/windows/InspectorMeterChange.tsx +++ /dev/null @@ -1,136 +0,0 @@ -import React from "react" -import * as Dockable from "../dockable" -import * as Project from "../project" -import * as Prefs from "../prefs" -import * as Theory from "../theory" -import * as UI from "../ui" - - -export interface InspectorMeterChangeProps -{ - elemIds: Project.ID[] -} - - -export function InspectorMeterChange(props: InspectorMeterChangeProps) -{ - const windowCtx = Dockable.useWindow() - - - const meterChs: Project.MeterChange[] = [] - for (const elemId of props.elemIds) - { - const meterCh = Project.getElem(Project.global.project, elemId, "meterChange") - if (!meterCh) - continue - - meterChs.push(meterCh) - } - - if (meterChs.length == 0) - return null - - - windowCtx.setTitle(meterChs.length == 1 ? `Meter Change` : `${ meterChs.length } Meter Changes`) - - - let sameNumerator: number | null = meterChs[0].meter.numerator - let sameDenominator: number | null = meterChs[0].meter.denominator - for (const meterCh of meterChs) - { - if (meterCh.meter.numerator !== sameNumerator) - sameNumerator = null - - if (meterCh.meter.denominator !== sameDenominator) - sameDenominator = null - } - - - const denominatorOptions = [1, 2, 4, 8, 16, 32, 64].map(denominator => - { - return { - label: denominator.toString(), - value: denominator, - } - }) - - - const modifyMeterChs = (func: (meterCh: Project.MeterChange) => Project.MeterChange) => - { - for (const meterCh of meterChs) - { - const newMeterCh = func(meterCh) - console.log("InspectorKeyChange.modifyMeterChs", meterCh, newMeterCh) - Project.global.project = Project.upsertElement(Project.global.project, newMeterCh) - } - - Project.notifyObservers() - window.dispatchEvent(new Event("timelineRefresh")) - } - - - const onChangeNumerator = (value: string) => - { - const numerator = parseInt(value) - if (!isFinite(numerator) || numerator <= 0 || numerator >= 1000) - return - - modifyMeterChs((meterCh) => - { - const newMeter = new Theory.Meter(numerator, meterCh.meter.denominator) - return Project.elemModify(meterCh, { meter: newMeter }) - }) - } - - - const onChangeDenominator = (item: any) => - { - const denominator = item.value - - modifyMeterChs((meterCh) => - { - const newMeter = new Theory.Meter(meterCh.meter.numerator, denominator) - return Project.elemModify(meterCh, { meter: newMeter }) - }) - } - - - return
- -
-
- Meter -
- -
- - { " / " } - -
- -
- -
-} \ No newline at end of file diff --git a/src/windows/InspectorMultitype.tsx b/src/windows/InspectorMultitype.tsx deleted file mode 100644 index b31172b..0000000 --- a/src/windows/InspectorMultitype.tsx +++ /dev/null @@ -1,58 +0,0 @@ -import React from "react" -import * as Dockable from "../dockable" -import * as Project from "../project" -import * as Prefs from "../prefs" -import * as UI from "../ui" - - -export interface InspectorMultitypeProps -{ - elemIds: Project.ID[] - setElemIds: (elemIds: Project.ID[]) => void - elemTypes: Map -} - - -export function InspectorMultitype(props: InspectorMultitypeProps) -{ - const windowCtx = Dockable.useWindow() - windowCtx.setTitle("Multiple Elements") - - const onNarrowSelectionDown = (type: Project.Element["type"]) => - { - const newElemIds: Project.ID[] = [] - - for (const elemId of props.elemIds) - { - const elem = Project.global.project.elems.get(elemId) - if (!elem) - continue - - if (elem.type === type) - newElemIds.push(elem.id) - } - - props.setElemIds(newElemIds) - } - - return -} \ No newline at end of file diff --git a/src/windows/InspectorTrack.tsx b/src/windows/InspectorTrack.tsx deleted file mode 100644 index 9b610ae..0000000 --- a/src/windows/InspectorTrack.tsx +++ /dev/null @@ -1,172 +0,0 @@ -import React from "react" -import * as Windows from "./index" -import * as Dockable from "../dockable" -import * as Project from "../project" -import * as UI from "../ui" -import { InstrumentSelect } from "./InstrumentSelect" -import styled from "styled-components" - - -const StyledButton = styled.button` - font-family: inherit; - color: #fff; - border: 1px solid #888; - border-radius: 0.25em; - background-color: transparent; - padding: 0.5em 1em; - margin: 0.5em 0.25em; - cursor: pointer; - outline: none; - - &:hover - { - background-color: #2f3136; - border: 1px solid #fff; - } - - &:active - { - background-color: #222; - border: 1px solid #fff; - } -` - - -export interface InspectorTrackProps -{ - elemIds: Project.ID[] -} - - -export function InspectorTrack(props: InspectorTrackProps) -{ - const windowCtx = Dockable.useWindow() - - - const tracks: Project.Track[] = [] - for (const elemId of props.elemIds) - { - const track = Project.getElem(Project.global.project, elemId, "track") - if (!track) - continue - - tracks.push(track) - } - - if (tracks.length == 0) - return null - - - windowCtx.setTitle(tracks.length == 1 ? `Track` : `${ tracks.length } Tracks`) - - - let definiteName: string | null = tracks[0].name - let definiteShowInstrument: boolean = Project.trackHasInstrument(tracks[0]) - let definiteInstrument: Project.Instrument | null = Project.trackGetInstrument(tracks[0]) - for (const track of tracks) - { - if (track.name !== definiteName) - definiteName = null - - if (!Project.trackHasInstrument(track)) - definiteShowInstrument = false - - if (Project.trackHasInstrument(track) && definiteInstrument) - { - const instrument = Project.trackGetInstrument(track)! - - if (instrument.type !== definiteInstrument.type) - definiteInstrument = null - else switch (instrument.type) - { - case "sflib": - { - const definiteSflib = definiteInstrument as Project.InstrumentSflib - if (instrument.collectionId !== definiteSflib.collectionId || - instrument.instrumentId !== definiteSflib.instrumentId) - definiteInstrument = null - } - } - } - } - - - const modifyTracks = (func: (tr: Project.Track) => Project.Track) => - { - for (const track of tracks) - { - const newTrack = func(track) - console.log("InspectorTrack.modifyTracks", track, newTrack) - Project.global.project = Project.upsertTrack(Project.global.project, newTrack) - } - - Project.notifyObservers() - window.dispatchEvent(new Event("timelineRefresh")) - } - - - const onRename = (newName: string) => - { - modifyTracks((track) => - { - return Project.elemModify(track, { name: newName }) - }) - } - - - const getInstrument = () => - { - return definiteInstrument - } - - - const setInstrument = (newInstrument: Project.Instrument) => - { - modifyTracks((track) => - { - if (!Project.trackHasInstrument(track)) - return track - - return Project.elemModify(track, { instrument: newInstrument }) - }) - } - - - return
-
- Name -
- -
- -
- - { !definiteShowInstrument ? null : - <> -
- Instrument -
- - - - } -
-} \ No newline at end of file diff --git a/src/windows/InstrumentSelect.tsx b/src/windows/InstrumentSelect.tsx deleted file mode 100644 index bfe6895..0000000 --- a/src/windows/InstrumentSelect.tsx +++ /dev/null @@ -1,140 +0,0 @@ -import React from "react" -import * as Dockable from "../dockable" -import * as Project from "../project" -import * as Playback from "../playback" -import * as UI from "../ui" -import * as Misc from "../util/misc" - - -export interface InstrumentSelectProps -{ - getInstrument: () => Project.Instrument | null - setInstrument: (newInstr: Project.Instrument) => void -} - - -export function InstrumentSelect(props: InstrumentSelectProps) -{ - const onChangeType = (newType: string) => - { - props.setInstrument(Project.makeInstrumentOfKind(newType)) - } - - const instr = props.getInstrument() - const instrType = instr ? instr.type : "sflib" - - return
-
- onChangeType(item.value) } - /> -
- - { instrType != "sflib" ? null : - - } - -
-} - - -function InstrumentSelectSflib(props: { data: InstrumentSelectProps }) -{ - const instr = props.data.getInstrument() as (Project.InstrumentSflib | null) - const sflibMeta = Playback.getSflibMeta() - if (!sflibMeta) - return null - - const [selCollection, setSelCollection] = React.useState(() => instr ? instr.collectionId : "gm") - const curCollection = sflibMeta.collectionsById.get(selCollection)! - - const onChange = (collectionId: string, instrumentId: string) => - { - let newInstr: Project.InstrumentSflib = - { - type: "sflib", - collectionId, - instrumentId, - } - - props.data.setInstrument(newInstr) - } - - const collItems = React.useMemo(() => - { - return sflibMeta.collections.map(coll => - { - return { - value: coll.id, - label: coll.name, - } - }) - - }, []) - - const instrItems = React.useMemo(() => - { - return curCollection.instruments.map(instr => - { - return { - value: instr.id, - label: - Misc.getMidiPresetEmoji(instr.midiBank, instr.midiPreset) + " " + - //"[" + instr.midiBank.toString().padStart(3, "0") + ":" + - //instr.midiPreset.toString().padStart(3, "0") + "] " + - instr.name, - } - }) - - }, [curCollection]) - - return
- -
- Collection -
- -
- Preset -
- - setSelCollection(item.value) } - items={ collItems } - /> - - onChange(curCollection.id, item.value) } - items={ instrItems } - /> - -
-} \ No newline at end of file diff --git a/src/windows/Render.tsx b/src/windows/Render.tsx deleted file mode 100644 index cb7539b..0000000 --- a/src/windows/Render.tsx +++ /dev/null @@ -1,116 +0,0 @@ -import React from "react" -import * as Dockable from "../dockable" -import * as Project from "../project" -import * as Playback from "../playback" -import * as Prefs from "../prefs" -import * as UI from "../ui" -import * as WavEncoder from "../util/wavEncode" - - -interface PrefsProps -{ - readonly prefs: Prefs.Prefs -} - - -export function Render() -{ - const windowCtx = Dockable.useWindow() - windowCtx.setTitle("Render") - windowCtx.setPreferredSize(600, 450) - - - const [rendering, setRendering] = React.useState(false) - const [progress, setProgress] = React.useState(0) - - - const onRenderAndDownload = () => - { - setRendering(true) - setProgress(0) - - ;(async () => - { - try - { - const project = Project.global.project - const range = project.range - //Range.fromStartDuration(playback.ref.current.startTime, new Rational(1)) - - const audioBuffer = await Playback.renderToBuffer(range, setProgress) - - const wavBuffer = WavEncoder.encode(audioBuffer) - console.log("wavBuffer", wavBuffer) - - const blob = new Blob([wavBuffer], { type: "octet/stream" }) - const url = window.URL.createObjectURL(blob) - - const element = document.createElement("a") - element.setAttribute("href", url) - element.setAttribute("download", "song.wav") - - element.style.display = "none" - document.body.appendChild(element) - element.click() - document.body.removeChild(element) - - /*const audioCtx = playback.ref.current.synth.audioCtx - const sourceNode = audioCtx.createBufferSource() - sourceNode.buffer = audioBuffer - sourceNode.connect(audioCtx.destination) - sourceNode.start()*/ - } - catch (e) - { - console.error(e) - window.alert("An error occurred!\n\n" + e) - } - finally - { - setRendering(false) - setProgress(0) - } - })() - } - - - return
- -
- Output Format -
- - - -
- - - - - -
-} \ No newline at end of file diff --git a/src/windows/Test.tsx b/src/windows/Test.tsx deleted file mode 100644 index 68fa109..0000000 --- a/src/windows/Test.tsx +++ /dev/null @@ -1,24 +0,0 @@ -import React from "react" -import { useRefState } from "../util/refState" -import * as Project from "../project" - - -export function Test() -{ - const count = useRefState(() => 0) - - const increment = () => - { - count.ref.current++ - count.commit() - } - - return
- Count: { count.ref.current } -
- -
-
- Project elem count: { Project.global.project.elems.size } -
-} \ No newline at end of file diff --git a/src/windows/Timeline.tsx b/src/windows/Timeline.tsx deleted file mode 100644 index 57ff1d3..0000000 --- a/src/windows/Timeline.tsx +++ /dev/null @@ -1,115 +0,0 @@ -import React from "react" -import * as Dockable from "../dockable" -import * as TimelineData from "../timeline" -import * as Prefs from "../prefs" -import { useRefState } from "../util/refState" -import styled from "styled-components" - - -interface PrefsProps -{ - readonly prefs: Prefs.Prefs -} - - -const StyledModeStackDiv = styled.div` - width: 100%; - background-color: ${ props => props.prefs.ui.windowPanelColor }; - border-bottom: 1px solid #888; -` - - -const StyledModeStackButton = styled.button` - font-family: inherit; - color: #fff; - border: 1px solid transparent; - border-radius: 0.25em; - background-color: transparent; - padding: 0em 0.25em; - margin: 0.5em 0.25em; - cursor: pointer; - outline: none; - - &:hover - { - background-color: #2f3136; - border: 1px solid #fff; - } -` - - -export function Timeline() -{ - const windowCtx = Dockable.useWindow() - windowCtx.setTitle("Timeline") - windowCtx.setPreferredSize(600, 450) - - const editorState = useRefState(() => TimelineData.init()) - - Dockable.modifyContentData(windowCtx.contentId, (data) => - { - data.timelineState = editorState - }) - - const getModeName = (mode: TimelineData.Mode) => - { - switch (mode) - { - case TimelineData.Mode.Project: return "Project Root" - case TimelineData.Mode.NoteBlock: return "Note Block" - } - } - - const onClickModeStack = (index: number) => - { - TimelineData.modeStackPop(editorState.ref.current, index) - editorState.commit() - window.dispatchEvent(new Event("timelineRefresh")) - } - - const modeStack = React.useMemo(() => - { - const modes = - [ - ...editorState.ref.current.modeStack.map(s => s.mode), - editorState.ref.current.mode, - ] - - return modes.map((mode, i) => - { - const name = getModeName(mode) - return - { i == 0 ? null : - " > " - } - onClickModeStack(i) } - > - { name } - - - }) - - }, [editorState.update, editorState.ref.current.modeStack.length]) - - - return
- - { modeStack } - - -
-} \ No newline at end of file diff --git a/src/windows/index.ts b/src/windows/index.ts deleted file mode 100644 index ff923f8..0000000 --- a/src/windows/index.ts +++ /dev/null @@ -1,5 +0,0 @@ -export * from "./Test" -export * from "./Timeline" -export * from "./InstrumentSelect" -export * from "./Inspector" -export * from "./Render" \ No newline at end of file diff --git a/src_old/App.tsx b/src_old/App.tsx deleted file mode 100644 index 889d9bd..0000000 --- a/src_old/App.tsx +++ /dev/null @@ -1,153 +0,0 @@ -import React from "react" -import Dockable from "./dockable/Dockable" -import * as DockableData from "./dockable/state" -import { EditorContent } from "./editor2/EditorContent" -import { AppState, AppReducer, ContentManager, AppManager } from "./AppState" -import { AppContext } from "./AppContext" -import EditorState from "./editor2/editor" -import Project from "./project/project2" -import Immutable from "immutable" -import Rect from "./util/rect" -import MenuBar from "./popup/MenuBar" -import Popup from "./popup/Popup" -import InspectorContent from "./editor2/InspectorContent" -import TrackInstrumentContent from "./editor2/TrackInstrumentContent" -import { usePlaybackController } from "./playback/PlaybackController" -import { useSoundfontLibrary } from "./playback/library" - - -const initialAppState = AppReducer.makeNew() - - -export default function App(props: {}) -{ - const rootRef = React.useRef(null) - - const [, setDummy] = React.useState(0) - const appStateRef = React.useRef(initialAppState) - const appDispatch = React.useCallback((newState: AppState) => - { - //console.log(newState) - appStateRef.current = newState - setDummy(dummy => dummy + 1) - - }, []) - - const setDockableRoot = (newRoot: DockableData.State) => appDispatch({ - ...appStateRef.current, - dockableRoot: newRoot, - }) - - const appManager = new AppManager( - () => appStateRef.current, - (newState) => appStateRef.current = newState, - (newState) => appDispatch(newState)) - - const playbackController = usePlaybackController(appManager) - const soundfontLibrary = useSoundfontLibrary(appManager) - - React.useEffect(() => - { - if (!rootRef.current) - return - - const onResize = () => - { - const elemRect = rootRef.current!.getBoundingClientRect() - - appDispatch({ - ...appStateRef.current, - dockableRect: new Rect( - elemRect.x, - elemRect.y, - elemRect.width, - elemRect.height), - }) - } - - onResize() - - window.addEventListener("resize", onResize) - - return () => - { - window.removeEventListener("resize", onResize) - } - - }, [rootRef.current]) - - - return -
- - -
- - - - { !appStateRef.current.popup ? null : - - } - - -
- -} - - -function contentTypeToComponent(type: string): any -{ - switch (type) - { - case "editor": return EditorContent - case "editorNotes": return EditorContent - case "inspector": return InspectorContent - case "trackInstrument": return TrackInstrumentContent - - default: - throw "invalid content type" - } -} - - -function contentTypeToTitle(type: string): any -{ - switch (type) - { - case "editor": return "Project" - case "editorNotes": return "Note Track" - case "inspector": return "Inspector" - case "trackInstrument": return "Instrument Select" - - default: - throw "invalid content type" - } -} \ No newline at end of file diff --git a/src_old/AppContext.ts b/src_old/AppContext.ts deleted file mode 100644 index fc1b498..0000000 --- a/src_old/AppContext.ts +++ /dev/null @@ -1,17 +0,0 @@ -import React from "react" -import { AppManager } from "./AppState" - - -interface AppContextProps -{ - appManager: AppManager -} - - -export const AppContext = React.createContext(null) - - -export function useAppManager(): AppManager -{ - return React.useContext(AppContext)!.appManager -} \ No newline at end of file diff --git a/src_old/AppState.ts b/src_old/AppState.ts deleted file mode 100644 index f322793..0000000 --- a/src_old/AppState.ts +++ /dev/null @@ -1,429 +0,0 @@ -import DockableData, * as Dock from "./dockable/state" -import Project from "./project/project2" -import Immutable from "immutable" -import Rect from "./util/rect" -import Editor from "./editor2/editor" -import Dockable from "./dockable/Dockable" -import Rational from "./util/rational" -import { SflibMeta } from "./playback/library" - - -export interface AppState -{ - contentIdNext: number - dockableRect: Rect - dockableRoot: Dock.State - dockableContents: Immutable.Map - - project: Project - selection: Immutable.Set - playback: - { - playing: boolean - time: Rational - timeAsFloat: number - timeStart: Rational - } - - sflib: SflibMeta - - popup: null | - { - rect: Rect - elem: any - props: any - } - - prefs: - { - editor: - { - bkgColor: string - trackVBorderColor: string - trackHBorderColor: string - - selectionCursorColor: string - selectionBkgColor: string - playbackCursorColor: string - trackSeparatorColor: string - - measureColor: string - submeasureColor: string - halfSubmeasureColor: string - measureAlternateBkgColor: string - - octaveDividerColor: string - noteRowAlternateBkgColor: string - - meterChangeColor: string - keyChangeColor: string - - keyPan: string - keyDraw: string - keySelectMultiple: string - keySelectRect: string - - mouseDragXLockedDistance: number - mouseDragYLockedDistance: number - - mouseEdgeScrollThreshold: number - mouseEdgeScrollSpeed: number - } - } -} - - -export class AppReducer -{ - static makeNew(): AppState - { - let root = DockableData.makeRoot() - root = DockableData.addPanel(root, 1, Dock.DockMode.Full, 1) - - return { - contentIdNext: 2, - dockableRect: new Rect(0, 0, 0, 0), - dockableRoot: root, - dockableContents: Immutable.Map() - .set(1, { - type: "editor", - state: Editor.makeNewFull(), - }), - - popup: null, - - project: Project.getDefault(), - selection: Immutable.Set(), - playback: { - playing: false, - time: new Rational(0), - timeAsFloat: 0, - timeStart: new Rational(0), - }, - - sflib: { - ready: false, - collections: [], - }, - - prefs: { - editor: { - bkgColor: "#000",//"#29242e", - trackVBorderColor: "#888", - trackHBorderColor: "#888", - - selectionCursorColor: "#0af", - selectionBkgColor: "#024", - playbackCursorColor: "#f00", - trackSeparatorColor: "#aaa", - - measureColor: "#444", - submeasureColor: "#222", - halfSubmeasureColor: "#111", - measureAlternateBkgColor: "#fff1", - - octaveDividerColor: "#444", - noteRowAlternateBkgColor: "#222",//"#19141e", - - meterChangeColor: "#0cf", - keyChangeColor: "#f0c", - - keyPan: " ", - keyDraw: "a", - keySelectMultiple: "control", - keySelectRect: "shift", - - mouseDragXLockedDistance: 10, - mouseDragYLockedDistance: 10, - - mouseEdgeScrollThreshold: 10, - mouseEdgeScrollSpeed: 1, - }, - }, - } - } - - - static merge(state: AppState, newState: Partial): AppState - { - return { - ...state, - ...newState, - } - } - - - static getContent(state: AppState, contentId: number): any - { - return state.dockableContents.get(contentId)!.state - } - - - static setContent(state: AppState, contentId: number, newState: any): AppState - { - return { - ...state, - dockableContents: state.dockableContents.set(contentId, - { - ...state.dockableContents.get(contentId)!, - state: newState, - }), - } - } - - - static mergeContent(state: AppState, contentId: number, newState: any): AppState - { - return AppReducer.setContent(state, contentId, { - ...AppReducer.getContent(state, contentId), - ...newState, - }) - } - - - static createTab(appState: AppState, nextToContentId: number, type: string, state: any): AppState - { - const panel = DockableData.findPanelWithContent(appState.dockableRoot.rootPanel, nextToContentId) - if (!panel) - return appState - - return { - ...appState, - contentIdNext: appState.contentIdNext + 1, - dockableRoot: DockableData.addPanel( - appState.dockableRoot, - panel.id, - Dock.DockMode.Full, - appState.contentIdNext), - dockableContents: appState.dockableContents.set(appState.contentIdNext, - { - type, - state, - }), - } - } - - - static createOrUpdateTab(appState: AppState, nextToContentId: number, type: string, state: any): AppState - { - const updatablePanel = DockableData.findPanelWithType(appState.dockableRoot.rootPanel, type, appState.dockableContents) - if (updatablePanel) - { - const contentId = updatablePanel.contentIds.find(c => appState.dockableContents.get(c)!.type == type)! - const contentIndex = updatablePanel.contentIds.findIndex(c => c == contentId) - - return { - ...appState, - dockableRoot: DockableData.modifyPanelFromRoot( - appState.dockableRoot, updatablePanel.id, (oldPanel) => - { - return { ...oldPanel, - curContent: contentIndex, - } - }), - dockableContents: appState.dockableContents.set(contentId, - { - type, - state, - }) - } - } - - const panel = DockableData.findPanelWithContent(appState.dockableRoot.rootPanel, nextToContentId) - if (!panel) - return appState - - return { - ...appState, - contentIdNext: appState.contentIdNext + 1, - dockableRoot: DockableData.addPanel( - appState.dockableRoot, - panel.id, - Dock.DockMode.Full, - appState.contentIdNext), - dockableContents: appState.dockableContents.set(appState.contentIdNext, - { - type, - state, - }), - } - } - - - static createFloating(appState: AppState, type: string, state: any, rect: Rect): AppState - { - return { - ...appState, - contentIdNext: appState.contentIdNext + 1, - dockableRoot: DockableData.addFloatingPanel( - appState.dockableRoot, - rect, - appState.contentIdNext), - dockableContents: appState.dockableContents.set(appState.contentIdNext, - { - type, - state, - }), - } - } - - - static removeFloating(appState: AppState, type: string): AppState - { - let root = appState.dockableRoot - for (const [key, value] of appState.dockableContents) - { - if (value.type === type) - root = DockableData.removeFloatingContent(root, key) - } - - return { - ...appState, - contentIdNext: appState.contentIdNext + 1, - dockableRoot: root, - } - } - - - static createPopup(appState: AppState, rect: Rect, elem: any, props: any): AppState - { - return { - ...appState, - popup: { - rect, - elem, - props, - } - } - } - - - static removePopup(appState: AppState): AppState - { - return { - ...appState, - popup: null, - } - } -} - - -export class AppManager -{ - getAppStateFn: () => AppState - setAppStateFn: (newState: AppState) => void - dispatchFn: (state: AppState) => void - - - constructor( - getAppState: () => AppState, - setAppState: (newState: AppState) => void, - dispatch: (state: AppState) => void) - { - this.getAppStateFn = getAppState - this.setAppStateFn = setAppState - this.dispatchFn = dispatch - } - - - get appState(): AppState - { - return this.getAppStateFn() - } - - - set appState(newState: AppState) - { - this.setAppStateFn(newState) - } - - - dispatch() - { - this.dispatchFn(this.appState) - } - - - mergeAppState(newState: Partial) - { - this.appState = { - ...this.appState, - ...newState, - } - } - - - makeContentManager(contentId: number) - { - return new ContentManager(this, contentId) - } -} - - -export class ContentManager -{ - appManager: AppManager - contentId: number - - - constructor(appManager: AppManager, contentId: number) - { - this.appManager = appManager - this.contentId = contentId - } - - - get appState(): AppState - { - return this.appManager.appState - } - - - set appState(newState: AppState) - { - this.appManager.appState = newState - } - - - dispatch() - { - this.appManager.dispatch() - } - - - mergeAppState(newState: Partial) - { - this.appManager.appState = { - ...this.appManager.appState, - ...newState, - } - } - - - get contentState(): T - { - return this.appManager.appState.dockableContents.get(this.contentId)!.state - } - - - set contentState(newState: T) - { - this.appManager.appState = { - ...this.appManager.appState, - dockableContents: this.appManager.appState.dockableContents.set(this.contentId, - { - ...this.appManager.appState.dockableContents.get(this.contentId)!, - state: newState, - }), - } - } - - - mergeContentState(newState: Partial) - { - this.contentState = { - ...this.contentState, - ...newState, - } - } -} \ No newline at end of file diff --git a/src_old/editor/editor.js b/src_old/editor/editor.js deleted file mode 100644 index 47904df..0000000 --- a/src_old/editor/editor.js +++ /dev/null @@ -1,1896 +0,0 @@ -import { default as Immutable } from "immutable" -import Track from "./track.js" -import Theory from "../theory.ts" -import Rational from "../util/rational.js" -import Range from "../util/range.js" -import Rect from "../util/rect.js" -import Project from "../project/project.js" -import MathUtils from "../util/math.js" - - -export default class Editor -{ - static actionPan = 0x01 - static actionSelectCursor = 0x02 - static actionSelectRect = 0x04 - static actionDraw = 0x08 - static actionDragTime = 0x10 - static actionDragPitchRow = 0x20 - static actionStretchTimeStart = 0x40 - static actionStretchTimeEnd = 0x80 - - static keyPan = " " - static keyDraw = "a" - static keySelectMultiple = "control" - static keySelectRect = "shift" - - - static reduce(state, action) - { - const shouldLog = ( - action.type !== "playbackStep" && - action.type !== "mouseMove" && - action.type !== "keyDown" && - action.type !== "keyUp" && - action.type !== "keyCommand" - ) - - if (shouldLog) - { - console.log("Editor.oldState", state) - console.log("Editor.action", action) - } - - const reducer = Editor["reduce_" + action.type] - - if (reducer) - { - const newState = reducer(state, action) - - if (shouldLog) - { - console.log("Editor.newState", newState) - console.log("") - } - - return newState - } - else - { - console.error("unhandled Editor.action", action) - return state - } - } - - - static reduce_init(state, action) - { - state = { - w: 0, - h: 0, - - tracks: [], - - timeSnap: new Rational(1, 16), - timeSnapBase: new Rational(1, 16), - timeScale: 200, - timeScroll: -1, - - insert: { - nearPitch: 60, - duration: new Rational(1, 4), - }, - - undoData: { - stack: [], - pointer: -1, - }, - - clipboard: { - elems: new Immutable.Map(), - }, - - playback: { - playing: false, - timeAsFloat: 0, - time: new Rational(0), - startTime: null, - }, - - cursor: { - visible: true, - time1: new Rational(0), - time2: new Rational(0), - track1: 0, - track2: 0, - }, - - rectSelection: null, - - mouse: { - down: false, - action: 0, - dragOrig: null, - drag: null, - x: 0, - y: 0, - xPrev: 0, - yPrev: 0, - hover: null, - draw: null, - lastDownDate: new Date(), - lastWheelDate: new Date(), - }, - - keys: {}, - pendingKeyCommandFinish: false, - - soundPreview: null, - soundPreviewPrev: null, - - prefs: - { - doubleClickThresholdMs: 250, - mouseEdgeScrollThreshold: 60, - mouseEdgeScrollSpeed: 1, - cursorEdgeScrollThreshold: new Rational(4), - cursorEdgeScrollSpeed: 16, - - backgroundColor: "#29242e", - selectionCursorColor: "#0af", - playbackCursorColor: "#f00", - trackSeparatorColor: "#aaa", - submeasureColor: "#0008", - halfSubmeasureColor: "#0002", - meterChangeColor: "#0cf", - keyChangeColor: "#f0c", - } - } - - state = Editor.refreshProjectRange(state) - return Editor.undoPointAdd(state) - } - - - static reduce_set(state, action) - { - state = { ...state, - ...action.state, - } - - return state - } - - - static reduce_resize(state, action) - { - state = { ...state, - w: action.w, - h: action.h, - tracks: [ ...state.tracks ], - } - - let trackY = 0 - for (let tr = 0; tr < state.tracks.length; tr++) - { - if (state.tracks[tr].kind == "notes") - state.tracks[tr] = { ...state.tracks[tr], rect: new Rect(0, trackY, state.w, state.h - trackY) } - else if (state.tracks[tr].kind == "chords") - state.tracks[tr] = { ...state.tracks[tr], rect: new Rect(0, trackY, state.w, 54) } - else - state.tracks[tr] = { ...state.tracks[tr], rect: new Rect(0, trackY, state.w, 44) } - - trackY += state.tracks[tr].rect.h - } - - return state - } - - - static reduce_projectSet(state, action) - { - state = Editor.undoPointAdd(state) - - state = { ...state, - project: action.project, - } - - return state - } - - - static reduce_projectLoad(state, action) - { - state = { ...state, - project: action.project, - } - - state = Editor.selectionClear(state) - state = Editor.reduce_clearUndoStack(state, {}) - state = Editor.reduce_rewind(state, {}) - state = Editor.reduce_playbackSet(state, { playing: false }) - return state - } - - - static reduce_rewind(state, action) - { - if (state.playback.playing) - { - state = Editor.reduce_playbackSet(state, { playing: true, time: state.project.range.start }) - } - else - { - state = Editor.Cursor.setVisible(state, true) - state = Editor.Cursor.place(state, state.project.range.start, null) - state = Editor.scrollTimeIntoView(state, state.project.range.start) - } - - return state - } - - - static reduce_trackAdd(state, action) - { - state = Track.handlerForTrackKind(action.kind).init(state, action) - state = Editor.Cursor.place(state, null, state.tracks.length - 1) - - return Editor.reduce_resize(state, { type: "resize", w: state.w, h: state.h }) - } - - - static reduce_playbackSet(state, action) - { - if (state.mouse.down) - state = Editor.reduce_mouseUp(state, {}) - - const time = action.time || state.playback.time - - state = { ...state, - playback: { ...state.playback, - playing: action.playing, - time, - timeAsFloat: time.asFloat(), - startTime: (action.playing ? time : null), - }, - } - - if (action.playing) - state = Editor.scrollPlaybackIntoView(state, time) - - return state - } - - - static reduce_playbackStep(state, action) - { - const timeAsFloat = action.timeAsFloat === null ? state.playback.timeAsFloat : action.timeAsFloat - const time = Rational.fromFloat(timeAsFloat, 1024) - - state = { ...state, - playback: { ...state.playback, - timeAsFloat, - time, - }, - } - - state = Editor.scrollPlaybackIntoView(state, time) - return state - } - - - static reduce_clearSoundPreview(state, action = {}) - { - let soundPreviewPrev = action.clearPrev ? null : (state.soundPreview || state.soundPreviewPrev) - - if (!state.soundPreview && state.soundPreviewPrev === soundPreviewPrev) - return state - - return { ...state, soundPreview: null, soundPreviewPrev } - } - - - static reduce_insertNote(state, action) - { - state = Editor.handlePendingKeyCommandFinish(state) - - const insertOctave = Math.floor(state.insert.nearPitch / 12) - const possiblePitches = [-1, 0, 1].map(offset => - { - const pitch = (insertOctave + offset) * 12 + (MathUtils.mod(action.chroma, 12)) - const delta = Math.abs(pitch - state.insert.nearPitch) - return { pitch, delta } - }) - - possiblePitches.sort((a, b) => a.delta - b.delta) - const chosenPitch = possiblePitches[0].pitch - - const track = state.tracks.findIndex(t => t.kind === "notes") - - const range = new Range(action.time, action.time.add(state.insert.duration)) - const note = new Project.Note(range, chosenPitch) - const id = state.project.nextId - const project = state.project.upsertNote(note).withRefreshedRange() - - state = { ...state, - project, - insert: { ...state.insert, - nearPitch: chosenPitch, - }, - } - - state = Editor.Cursor.place(state, range.end, track) - state = Editor.Cursor.setVisible(state, false) - state = Editor.scrollTimeIntoView(state, range.end) - state = Editor.soundPreviewSet(state, { kind: "note", pitch: chosenPitch }) - state = Editor.selectionClear(state) - state = Track.selectionAdd(state, track, id) - state = Track.selectionRemoveConflictingBehind(state, track) - state = Editor.undoPointAdd(state) - return state - } - - - static reduce_insertChord(state, action) - { - state = Editor.handlePendingKeyCommandFinish(state) - - const track = state.tracks.findIndex(t => t.kind === "chords") - - const range = new Range(action.time, action.time.add(state.insert.duration)) - const chord = new Project.Chord(range, action.chord) - const id = state.project.nextId - const project = state.project.upsertChord(chord).withRefreshedRange() - - state = { ...state, project } - state = Editor.Cursor.place(state, range.end, track) - state = Editor.Cursor.setVisible(state, false) - state = Editor.scrollTimeIntoView(state, range.end) - state = Editor.soundPreviewSet(state, { kind: "chord", chord: action.chord }) - state = Editor.selectionClear(state) - state = Track.selectionAdd(state, track, id) - state = Track.selectionRemoveConflictingBehind(state, track) - state = Editor.undoPointAdd(state) - return state - } - - - static reduce_insertKeyChange(state, action) - { - state = Editor.handlePendingKeyCommandFinish(state) - - const track = state.tracks.findIndex(t => t.kind === "markers") - - const keyCh = new Project.KeyChange(action.time, action.key) - const id = state.project.nextId - const project = state.project.upsertKeyChange(keyCh).withRefreshedRange() - - state = { ...state, project } - state = Editor.Cursor.place(state, action.time, track) - state = Editor.Cursor.setVisible(state, false) - state = Editor.scrollTimeIntoView(state, action.time) - state = Editor.selectionClear(state) - state = Track.selectionAdd(state, track, id) - state = Track.selectionRemoveConflictingBehind(state, track) - state = Editor.undoPointAdd(state) - return state - } - - - static reduce_insertMeterChange(state, action) - { - state = Editor.handlePendingKeyCommandFinish(state) - - const track = state.tracks.findIndex(t => t.kind === "markers") - - const meterCh = new Project.MeterChange(action.time, action.meter) - const id = state.project.nextId - const project = state.project.upsertMeterChange(meterCh).withRefreshedRange() - - state = { ...state, project } - state = Editor.Cursor.place(state, action.time, track) - state = Editor.Cursor.setVisible(state, false) - state = Editor.scrollTimeIntoView(state, action.time) - state = Editor.selectionClear(state) - state = Track.selectionAdd(state, track, id) - state = Track.selectionRemoveConflictingBehind(state, track) - state = Editor.undoPointAdd(state) - return state - } - - - static reduce_keyDown(state, action) - { - return { ...state, - keys: { ...state.keys, [action.key]: true }, - } - } - - - static reduce_keyUp(state, action) - { - let keys = { ...state.keys } - delete keys[action.key] - - return { ...state, keys } - } - - - static reduce_keyCommand(state, action) - { - let needsUndoPointAfter = true - - state = Editor.reduce_clearSoundPreview(state, { clearPrev: true }) - - switch (action.key) - { - case "z": - { - if (state.playback.playing) - break - - if (!action.ctrlKey) - break - - if (action.shiftKey) - state = Editor.reduce_redo(state, {}) - else - state = Editor.reduce_undo(state, {}) - - needsUndoPointAfter = false - break - } - case "y": - { - if (state.playback.playing) - break - - if (!action.ctrlKey) - break - - state = Editor.reduce_redo(state, {}) - needsUndoPointAfter = false - break - } - case "c": - { - if (state.playback.playing) - break - - if (!action.ctrlKey) - break - - state = Editor.reduce_copy(state, {}) - break - } - case "x": - { - if (state.playback.playing) - break - - if (!action.ctrlKey) - break - - state = Editor.reduce_cut(state, {}) - break - } - case "v": - { - if (state.playback.playing) - break - - if (!action.ctrlKey) - break - - state = Editor.reduce_paste(state, {}) - break - } - case " ": - { - const time = state.cursor.time1.min(state.cursor.time2) - state = Editor.reduce_playbackSet(state, { playing: !state.playback.playing, time }) - break - } - case "delete": - { - if (state.playback.playing) - break - - state = Editor.selectionDelete(state) - break - } - case "backspace": - { - if (state.playback.playing) - break - - if (!state.cursor.visible) - { - state = Editor.selectionDelete(state) - break - } - - const track1 = Math.min(state.cursor.track1, state.cursor.track2) - const track2 = Math.max(state.cursor.track1, state.cursor.track2) - - if (state.cursor.time1.compare(state.cursor.time2) == 0) - { - const time = state.cursor.time1.min(state.cursor.time2) - const prevAnchor = Editor.previousAnchor(state, time, track1, track2) - - for (let tr = track1; tr <= track2; tr++) - state = Track.deleteRange(state, tr, new Range(prevAnchor, time, false, false)) - - state = Editor.Cursor.place(state, prevAnchor, null) - state = Editor.Cursor.setVisible(state, true) - state = Editor.scrollTimeIntoView(state, prevAnchor) - } - else - { - const time1 = state.cursor.time1.min(state.cursor.time2) - const time2 = state.cursor.time1.max(state.cursor.time2) - - for (let tr = track1; tr <= track2; tr++) - state = Track.deleteRange(state, tr, new Range(time1, time2, false, false)) - - state = Editor.Cursor.place(state, time1, null) - state = Editor.Cursor.setVisible(state, true) - state = Editor.scrollTimeIntoView(state, time1) - } - break - } - case "escape": - { - state = Editor.reduce_rewind(state) - break - } - case "enter": - { - if (state.playback.playing) - break - - state = Editor.Cursor.setVisible(state, true) - - const range = Editor.selectionRange(state) - if (range) - { - let track = 0 - for (let tr = 0; tr < state.tracks.length; tr++) - if (Track.selectionHasAny(state, tr)) - track = tr - - state = Editor.Cursor.place(state, range.end, track) - state = Editor.scrollTimeIntoView(state, range.end) - } - - state = Editor.handlePendingKeyCommandFinish(state) - state = Editor.selectionClear(state) - break - } - case "arrowright": - case "arrowleft": - { - const invert = (action.key === "arrowleft") - - if (state.playback.playing) - { - const timeDelta = state.timeSnap.multiplyByFloat((action.ctrlKey ? 64 : 16) * (invert ? -1 : 1)) - const newTime = state.playback.time.add(timeDelta) - state = Editor.reduce_playbackSet(state, { playing: true, time: newTime }) - } - else if (state.cursor.visible && (!Editor.selectionHasAny(state) || action.shiftKey)) - { - const timeDelta = state.timeSnap.multiplyByFloat((action.ctrlKey ? 16 : 1) * (invert ? -1 : 1)) - - state = Editor.handlePendingKeyCommandFinish(state) - - if (action.shiftKey) - { - const newTime = state.cursor.time2.add(timeDelta) - state = Editor.Cursor.drag(state, newTime, null) - state = Editor.selectAtCursor(state) - state = Editor.scrollTimeIntoView(state, newTime) - } - else - { - const timeMin = state.cursor.time1.min(state.cursor.time2) - const timeMax = state.cursor.time1.max(state.cursor.time2) - - const newTime = (invert ? timeMin : timeMax).add(timeDelta) - state = Editor.Cursor.place(state, newTime, null) - state = Editor.scrollTimeIntoView(state, newTime) - } - } - else - { - const commandData = - { - timeDelta: state.timeSnap.multiplyByFloat((action.ctrlKey ? 16 : 1) * (invert ? -1 : 1)), - } - - state = Editor.executeKeyCommand(state, "TimeShift", commandData) - - const range = Editor.selectionRange(state) - const newTime = (invert ? range.start : range.end) - state = Editor.Cursor.setVisible(state, false) - state = Editor.Cursor.place(state, newTime, null) - state = Editor.scrollTimeIntoView(state, newTime) - - state = { ...state, pendingKeyCommandFinish: true } - needsUndoPointAfter = false - } - - break - } - case "arrowup": - case "arrowdown": - { - if (state.playback.playing) - break - - const invert = (action.key === "arrowdown") - if (state.cursor.visible && (!Editor.selectionHasAny(state) || action.shiftKey)) - { - const trackDelta = (invert ? 1 : -1) - - state = Editor.handlePendingKeyCommandFinish(state) - - if (action.shiftKey) - { - const newTrack = state.cursor.track2 + trackDelta - state = Editor.Cursor.drag(state, null, newTrack) - state = Editor.selectAtCursor(state) - } - else - { - const trackMin = Math.min(state.cursor.track1, state.cursor.track2) - const trackMax = Math.max(state.cursor.track1, state.cursor.track2) - - const newTrack = (invert ? trackMax : trackMin) + trackDelta - state = Editor.Cursor.place(state, null, newTrack) - } - } - else - { - const commandData = - { - diatonic: true, - degreeDelta: (action.ctrlKey ? 7 : 1) * (invert ? -1 : 1), - } - - state = Editor.Cursor.setVisible(state, false) - state = Editor.executeKeyCommand(state, "PitchShift", commandData) - state = { ...state, pendingKeyCommandFinish: true } - needsUndoPointAfter = false - } - - break - } - case ".": - case ">": - case ",": - case "<": - { - if (state.playback.playing) - break - - const invert = (action.key === "," || action.key === "<") - const commandData = - { - diatonic: false, - pitchDelta: (action.ctrlKey ? 12 : 1) * (invert ? -1 : 1), - } - - state = Editor.Cursor.setVisible(state, false) - state = Editor.executeKeyCommand(state, "PitchShift", commandData) - state = { ...state, pendingKeyCommandFinish: true } - needsUndoPointAfter = false - break - } - case "1": - case "2": - case "3": - case "4": - case "5": - case "6": - case "7": - { - if (state.playback.playing) - break - - const degree = (action.key.charCodeAt(0) - "1".charCodeAt(0)) - const time = state.cursor.time1.min(state.cursor.time2) - const keyCh = state.project.keyChanges.findActiveAt(time) - const key = keyCh ? keyCh.key : Editor.defaultKey() - - if (state.tracks[state.cursor.track1].kind === "chords") - { - const root = key.midiForDegree(degree) - - let pitches = [0] - pitches.push(key.midiForDegree(degree + 2) - root) - pitches.push(key.midiForDegree(degree + 4) - root) - - const kind = Theory.Chord.kindFromPitches(pitches) - const chord = new Theory.Chord(root, 0, kind, 0, []) - state = Editor.reduce_insertChord(state, { time, chord }) - } - else - { - const chroma = key.midiForDegree(degree) - state = Editor.reduce_insertNote(state, { time, chroma }) - } - } - default: - return state - } - - if (action.ev) - action.ev.preventDefault() - - state = Editor.refreshProjectRange(state) - - if (needsUndoPointAfter) - { - state = { ...state, pendingKeyCommandFinish: false } - state = Editor.undoPointAdd(state) - } - - return state - } - - - static reduce_copy(state, action) - { - if (!Editor.selectionHasAny(state)) - return state - - state = Editor.clipboardClear(state) - state = Editor.selectionAddToClipboard(state) - state = Editor.undoPointAdd(state) - return state - } - - - static reduce_cut(state, action) - { - if (!Editor.selectionHasAny(state)) - return state - - state = Editor.clipboardClear(state) - state = Editor.selectionAddToClipboard(state) - state = Editor.selectionDelete(state) - state = Editor.undoPointAdd(state) - return state - } - - - static reduce_paste(state, action) - { - state = Editor.clipboardPaste(state) - state = Editor.undoPointAdd(state) - return state - } - - - static reduce_mouseMove(state, action) - { - state = Editor.reduce_clearSoundPreview(state) - - state = { ...state, - mouse: { ...state.mouse, - x: action.x, - y: action.y, - xPrev: state.mouse.x, - yPrev: state.mouse.y, - hover: null, - draw: null, - time: Editor.timeAtX(state, action.x), - track: Editor.trackAtY(state, action.y), - } - } - - if (!state.mouse.down) - { - for (let tr = 0; tr < state.tracks.length; tr++) - { - if (state.tracks[tr].rect.contains(state.mouse)) - { - const input = { mouse: { ...state.mouse } } - input.mouse.x -= state.tracks[tr].rect.x - input.mouse.y -= state.tracks[tr].rect.y - - state = Track.updateHover(state, tr, input) - if (state.tracks[tr].hover || state.tracks[tr].draw) - { - state = { ...state, - mouse: { ...state.mouse, - hover: state.tracks[tr].hover, - draw: state.tracks[tr].draw, - }, - } - } - } - else - state = Track.update(state, tr, { hover: null, draw: null }) - } - } - else - { - state = { ...state, - mouse: { ...state.mouse, - drag: { - xDelta: state.mouse.x - state.mouse.dragOrig.x, - yDelta: state.mouse.y - state.mouse.dragOrig.y, - timeDelta: state.mouse.time.subtract(state.mouse.dragOrig.time), - }, - }, - } - - const handleMouseEdgeScroll = (state) => - { - let timeScroll = state.timeScroll - - if (state.mouse.x > state.w - state.prefs.mouseEdgeScrollThreshold) - timeScroll += state.timeSnap.asFloat() * state.prefs.mouseEdgeScrollSpeed - - else if (state.mouse.x < state.prefs.mouseEdgeScrollThreshold) - timeScroll -= state.timeSnap.asFloat() * state.prefs.mouseEdgeScrollSpeed - - return { ...state, timeScroll } - } - - if (state.mouse.action == Editor.actionPan) - { - state = { ...state, - timeScroll: state.mouse.dragOrig.timeScroll - state.mouse.drag.xDelta / state.timeScale, - } - - for (let tr = 0; tr < state.tracks.length; tr++) - state = Track.pan(state, tr) - } - else if (state.mouse.action == Editor.actionSelectCursor) - { - state = Editor.Cursor.drag(state, state.mouse.time, state.mouse.track) - state = Editor.selectAtCursor(state) - state = handleMouseEdgeScroll(state) - } - else if (state.mouse.action == Editor.actionSelectRect) - { - state = { ...state, - rectSelection: { ...state.rectSelection, - time2: state.mouse.time, - y2: state.mouse.y - state.tracks[state.rectSelection.track].rect.y, - }, - } - - for (let tr = 0; tr < state.tracks.length; tr++) - state = Track.selectionClear(state, tr) - - state = Track.selectionAddAtRect(state, state.rectSelection.track) - state = handleMouseEdgeScroll(state) - } - else if (state.mouse.action == Editor.actionDraw) - { - for (let tr = 0; tr < state.tracks.length; tr++) - { - const input = { mouse: { ...state.mouse } } - input.mouse.x -= state.tracks[tr].rect.x - input.mouse.y -= state.tracks[tr].rect.y - - state = Track.drawMove(state, tr, input) - } - - state = handleMouseEdgeScroll(state) - } - else - { - for (let tr = 0; tr < state.tracks.length; tr++) - state = Track.drag(state, tr) - - state = handleMouseEdgeScroll(state) - } - } - - return state - } - - - static reduce_mouseDown(state, action) - { - if (state.mouse.down) - return state - - const timeSinceLastDown = new Date().getTime() - state.mouse.lastDownDate.getTime() - - state = Editor.reduce_clearSoundPreview(state, { clearPrev: true }) - state = Editor.handlePendingKeyCommandFinish(state) - - state = { ...state, - mouse: { ...state.mouse, - down: true, - action: 0, - lastDownDate: new Date(), - dragOrig: { - x: state.mouse.x, - y: state.mouse.y, - time: state.mouse.time, - timeScroll: state.timeScroll, - track: state.mouse.track, - }, - }, - } - - if (action.rightButton || state.keys[Editor.keyPan]) - { - state = { ...state, - mouse: { ...state.mouse, - action: Editor.actionPan, - }, - } - - for (let tr = 0; tr < state.tracks.length; tr++) - state = Track.dragStart(state, tr) - } - else if (state.keys[Editor.keySelectRect]) - { - state = Editor.Cursor.setVisible(state, false) - - state = { ...state, - mouse: { ...state.mouse, - action: Editor.actionSelectRect, - }, - rectSelection: { - track: state.mouse.track, - time1: state.mouse.time, - time2: state.mouse.time, - y1: state.mouse.y - state.tracks[state.mouse.track].rect.y, - y2: state.mouse.y - state.tracks[state.mouse.track].rect.y, - }, - } - } - else if (!state.playback.playing) - { - if (state.mouse.draw) - { - state = Editor.selectionClear(state) - state = Editor.Cursor.setVisible(state, false) - - for (let tr = 0; tr < state.tracks.length; tr++) - state = Track.drawStart(state, tr) - - state = { ...state, - mouse: { ...state.mouse, - action: Editor.actionDraw, - }, - } - } - else - { - if (!state.mouse.hover) - { - if (timeSinceLastDown < state.prefs.doubleClickThresholdMs) - { - const time = Editor.previousAnchor(state, state.mouse.time, state.mouse.track, state.mouse.track) - state = Editor.Cursor.place(state, time, state.mouse.track) - state = Editor.scrollTimeIntoView(state, time) - } - else - { - state = Editor.Cursor.place(state, state.mouse.time, state.mouse.track) - state = { ...state, - mouse: { ...state.mouse, - action: Editor.actionSelectCursor, - }, - } - } - - state = Editor.Cursor.setVisible(state, true) - } - else - { - state = Editor.Cursor.setVisible(state, false) - - if (state.keys[Editor.keySelectMultiple]) - { - for (let tr = 0; tr < state.tracks.length; tr++) - { - state = Track.hoveredPlaySoundPreview(state, tr) - state = Track.hoveredToggleSelection(state, tr) - } - } - else - { - let isHoveringSelected = false - for (let tr = 0; tr < state.tracks.length; tr++) - isHoveringSelected |= Track.hoveredIsSelected(state, tr) - - if (!isHoveringSelected) - for (let tr = 0; tr < state.tracks.length; tr++) - state = Track.selectionClear(state, tr) - - for (let tr = 0; tr < state.tracks.length; tr++) - { - state = Track.hoveredPlaySoundPreview(state, tr) - state = Track.hoveredSelect(state, tr) - } - } - - for (let tr = 0; tr < state.tracks.length; tr++) - state = Track.dragStart(state, tr) - - state = { ...state, - mouse: { ...state.mouse, - action: state.mouse.hover && state.mouse.hover.action, - dragOrig: { ...state.mouse.dragOrig, - range: Editor.selectionRange(state), - }, - }, - } - } - } - } - - return state - } - - - static reduce_mouseUp(state, action) - { - if (!state.mouse.down) - return state - - state = Editor.reduce_clearSoundPreview(state) - - if (state.mouse.action == Editor.actionDraw) - { - for (let tr = 0; tr < state.tracks.length; tr++) - state = Track.drawEnd(state, tr) - } - - state = { ...state, - mouse: { ...state.mouse, down: false, action: 0, dragOrig: null, drag: null }, - rectSelection: null, - } - - for (let tr = 0; tr < state.tracks.length; tr++) - state = Track.selectionRemoveConflictingBehind(state, tr) - - state = Editor.refreshProjectRange(state) - state = Editor.undoPointAdd(state) - - return state - } - - - static reduce_mouseWheel(state, action) - { - if (Math.abs(action.deltaX) > 0) - { - let timeScroll = state.timeScroll + 0.01 / (state.timeScale / 100) * action.deltaX - let lastWheelDate = new Date() - - return { ...state, timeScroll, mouse: { ...state.mouse, lastWheelDate } } - } - else if (new Date().getTime() - state.mouse.lastWheelDate.getTime() > 250) - { - const snap = new Rational(1, 1024) - const prevMouseTime = Editor.timeAtX(state, state.mouse.x, snap) - - let timeScale = state.timeScale * (action.deltaY > 0 ? 0.8 : 1.25) - timeScale = Math.max(4, Math.min(2048, timeScale)) - state = { ...state, timeScale } - - const newMouseTime = Editor.timeAtX(state, state.mouse.x, snap) - - const timeScroll = state.timeScroll - newMouseTime.subtract(prevMouseTime).asFloat() - - const timeSnapAdjustThresholdUpper = 24 - const timeSnapAdjustThresholdLower = 8 - let timeSnap = state.timeSnapBase - - if (timeSnap.asFloat() * timeScale > timeSnapAdjustThresholdUpper) - while (timeSnap.asFloat() * timeScale > timeSnapAdjustThresholdUpper) - timeSnap = timeSnap.divide(new Rational(2)) - - else if (timeSnap.asFloat() * timeScale < timeSnapAdjustThresholdLower) - while (timeSnap.asFloat() * timeScale < timeSnapAdjustThresholdLower) - timeSnap = timeSnap.divide(new Rational(1, 2)) - - return { ...state, timeScroll, timeSnap } - } - - return state - } - - - static reduce_clearUndoStack(state, action) - { - state = { ...state, - undoData: { - stack: [], - pointer: -1, - }, - } - - return Editor.undoPointAdd(state) - } - - - static reduce_undo(state, action) - { - if (state.undoData.pointer <= 0) - return state - - state = Editor.handlePendingKeyCommandFinish(state) - - const undoData = { ...state.undoData, - pointer: state.undoData.pointer - 1, - } - - state = { - ...state, - ...undoData.stack[undoData.pointer].state, - undoData, - } - - return Editor.reduce_resize(state, { w: state.w, h: state.h }) - } - - - static reduce_redo(state, action) - { - if (state.undoData.pointer >= state.undoData.stack.length - 1) - return state - - state = Editor.handlePendingKeyCommandFinish(state) - - const undoData = { ...state.undoData, - pointer: state.undoData.pointer + 1, - } - - state = { - ...state, - ...undoData.stack[undoData.pointer].state, - undoData, - } - - return Editor.reduce_resize(state, { w: state.w, h: state.h }) - } - - - static handlePendingKeyCommandFinish(state) - { - if (!state.pendingKeyCommandFinish) - return state - - for (let tr = 0; tr < state.tracks.length; tr++) - state = Track.selectionRemoveConflictingBehind(state, tr) - - state = { ...state, pendingKeyCommandFinish: false } - state = Editor.undoPointAdd(state) - return state - } - - - static undoPointAdd(state) - { - let stackCutIndex = state.undoData.pointer + 1 - - if (state.undoData.stack.length > 0 && - state.project === state.undoData.stack[stackCutIndex - 1].state.project) - { - //if (state.undoData.pointer != state.undoData.stack.length - 1) - return state - - //stackCutIndex = state.undoData.pointer - } - - for (let tr = 0; tr < state.tracks.length; tr++) - state = Track.selectionRemoveConflictingBehind(state, tr) - - const newUndoPoint = - { - state: { - project: state.project, - cursor: state.cursor, - tracks: state.tracks, - timeScale: state.timeScale, - timeScroll: state.timeScroll, - pendingKeyCommandFinish: state.pendingKeyCommandFinish, - }, - } - - const newStack = - [ - ...state.undoData.stack.slice(0, stackCutIndex), - newUndoPoint, - ] - - //console.log("undoPointAdd", newStack) - - return { ...state, - undoData: { ...state.undoData, - stack: newStack, - pointer: newStack.length - 1, - }, - } - } - - - static canUndo(state) - { - return (state.undoData.pointer > 0) - } - - - static canRedo(state) - { - return (state.undoData.pointer < state.undoData.stack.length - 1) - } - - - static refreshProjectRange(state) - { - const project = state.project.withRefreshedRange() - if (project === state.project) - return state - - return { ...state, project } - } - - - static xAtTime(state, time) - { - return (time.asFloat() - state.timeScroll) * state.timeScale - } - - - static timeAtX(state, x, timeSnap = null) - { - timeSnap = timeSnap || state.timeSnap - const time = x / state.timeScale + state.timeScroll - return Rational.fromFloat(time, timeSnap.denominator) - } - - - static timeRangeAtX(state, x1, x2, timeSnap = null) - { - timeSnap = timeSnap || state.timeSnap - return new Range(Editor.timeAtX(state, x1, timeSnap).subtract(timeSnap), Editor.timeAtX(state, x2, timeSnap).add(timeSnap)) - } - - - static trackAtY(state, y) - { - for (let tr = 0; tr < state.tracks.length; tr++) - if (y < state.tracks[tr].rect.y2) - return tr - - return state.tracks.length - 1 - } - - - static visibleTimeRange(state) - { - return new Range(Editor.timeAtX(state, 0).subtract(state.timeSnap), Editor.timeAtX(state, state.w).add(state.timeSnap)) - } - - - static defaultKey() - { - return Theory.Key.parse("C Major") - } - - - static defaultMeter() - { - return new Theory.Meter(4, 4) - } - - - static insertionTime(state) - { - return state.cursor.time1 - } - - - static insertionTrack(state) - { - return state.cursor.track1 - } - - - static scrollTimeIntoView(state, time) - { - const visibleTimeRange = Editor.visibleTimeRange(state) - - let timeScroll = state.timeScroll - - if (time.compare(visibleTimeRange.end.subtract(state.timeSnap.multiply(state.prefs.cursorEdgeScrollThreshold))) > 0) - timeScroll = time.subtract(visibleTimeRange.duration).subtract(state.timeSnap).asFloat() + state.timeSnap.asFloat() * state.prefs.cursorEdgeScrollSpeed - - else if (time.compare(visibleTimeRange.start.add(state.timeSnap.multiply(state.prefs.cursorEdgeScrollThreshold))) < 0) - timeScroll = time.asFloat() - state.timeSnap.asFloat() * state.prefs.cursorEdgeScrollSpeed - - return { ...state, timeScroll } - } - - - static scrollPlaybackIntoView(state, time) - { - const margin = state.timeSnap.multiplyByFloat(16) - const visibleTimeRange = Editor.visibleTimeRange(state) - - let timeScroll = state.timeScroll - - if (time.compare(visibleTimeRange.end.subtract(margin)) > 0) - timeScroll = time.subtract(margin).asFloat() - - else if (time.compare(visibleTimeRange.start.add(margin)) < 0) - timeScroll = time.subtract(margin).asFloat() - - return { ...state, timeScroll } - } - - - static selectAtCursor(state) - { - for (let tr = 0; tr < state.tracks.length; tr++) - state = Track.selectionClear(state, tr) - - const trackMin = Math.min(state.cursor.track1, state.cursor.track2) - const trackMax = Math.max(state.cursor.track1, state.cursor.track2) - - if (state.cursor.time1.compare(state.cursor.time2) != 0) - for (let tr = trackMin; tr <= trackMax; tr++) - state = Track.selectionAddAtCursor(state, tr) - - return state - } - - - static selectionHasAny(state) - { - for (let tr = 0; tr < state.tracks.length; tr++) - if (Track.selectionHasAny(state, tr)) - return true - - return false - } - - - static selectionClear(state) - { - for (let tr = 0; tr < state.tracks.length; tr++) - state = Track.selectionClear(state, tr) - - return state - } - - - static selectionRange(state) - { - let range = null - - for (let tr = 0; tr < state.tracks.length; tr++) - range = Range.merge(range, Track.selectionRange(state, tr)) - - return range - } - - - static selectionDelete(state) - { - if (!Editor.selectionHasAny(state)) - return state - - const range = Editor.selectionRange(state) - - for (let tr = 0; tr < state.tracks.length; tr++) - state = Track.selectionDelete(state, tr) - - state = Editor.Cursor.place(state, range.start, null) - state = Editor.Cursor.setVisible(state, true) - state = Editor.scrollTimeIntoView(state, range.start) - - return state - } - - - static selectionAddToClipboard(state) - { - for (let tr = 0; tr < state.tracks.length; tr++) - state = Track.selectionAddToClipboard(state, tr) - - return state - } - - - static clipboardClear(state) - { - return { ...state, - clipboard: { - elems: new Immutable.Map(), - range: null, - }, - } - } - - - static clipboardAdd(state, elem) - { - if (state.clipboard.elems.has(elem.id)) - return state - - return { ...state, - clipboard: { ...state.clipboard, - elems: state.clipboard.elems.set(elem.id, elem), - range: Range.merge(state.clipboard.range, elem.range || Range.fromPoint(elem.time)), - }, - } - } - - - static clipboardPaste(state, elem) - { - state = Editor.selectionClear(state) - - if (!state.clipboard.range) - return state - - const trackMin = Math.min(state.cursor.track1, state.cursor.track2) - const trackMax = Math.max(state.cursor.track1, state.cursor.track2) - - const pasteData = - { - time: state.cursor.time1.min(state.cursor.time2), - } - - const projectBefore = state.project - - for (let tr = trackMin; tr <= trackMax; tr++) - state = Track.clipboardPaste(state, tr, pasteData) - - if (state.project === projectBefore) - return state - - const endTime = pasteData.time.add(state.clipboard.range.duration) - state = Editor.Cursor.setVisible(state, false) - state = Editor.Cursor.place(state, endTime, null) - state = Editor.scrollTimeIntoView(state, endTime) - state = Editor.refreshProjectRange(state) - - return state - } - - - static soundPreviewSet(state, soundPreview) - { - if (!soundPreview) - return state - - if (state.soundPreviewPrev && - state.soundPreviewPrev.kind == soundPreview.kind) - { - if (soundPreview.kind == "note" && - state.soundPreviewPrev.pitch == soundPreview.pitch) - return state - } - - return { ...state, soundPreview } - } - - - static executeKeyCommand(state, command, args) - { - for (let tr = 0; tr < state.tracks.length; tr++) - state = Track["keyCommand" + command](state, tr, args) - - return state - } - - - static previousAnchor(state, time, track1, track2) - { - let prevAnchor = null - - const trackMin = Math.min(track1, track2) - const trackMax = Math.max(track1, track2) - - for (let tr = trackMin; tr <= trackMax; tr++) - prevAnchor = Rational.max(prevAnchor, Track.previousAnchor(state, tr, time)) - - if (!prevAnchor) - return state.project.range.start - - return prevAnchor - } - - - static render(state, ctx) - { - ctx.save() - - ctx.fillStyle = state.prefs.backgroundColor - ctx.fillRect(0, 0, state.w, state.h) - - Editor.renderBackgroundMeasures(state, ctx) - Editor.renderCursorHighlight(state, ctx) - - for (let tr = 0; tr < state.tracks.length; tr++) - { - const track = state.tracks[tr] - - ctx.save() - ctx.translate(track.rect.x, track.rect.y) - - ctx.strokeStyle = state.prefs.trackSeparatorColor - ctx.lineCap = "square" - ctx.lineWidth = 1 - - ctx.beginPath() - ctx.moveTo(0, 0) - ctx.lineTo(state.w, 0) - ctx.stroke() - - ctx.beginPath() - ctx.rect(0, 0, track.rect.w, track.rect.h) - ctx.clip() - - Editor.renderRectSelectionHighlight(state, ctx, tr) - Track.render(state, tr, ctx) - Editor.renderRectSelectionContour(state, ctx, tr) - - ctx.restore() - } - - if (state.cursor.visible) - { - const timeMin = state.cursor.time1.min(state.cursor.time2) - const timeMax = state.cursor.time1.max(state.cursor.time2) - Editor.renderCursorBeam(state, ctx, timeMin, false) - Editor.renderCursorBeam(state, ctx, timeMax, true) - } - - Editor.renderPlaybackBeam(state, ctx, state.playback.time) - - ctx.restore() - } - - - static renderBackgroundMeasures(state, ctx) - { - const visibleTimeRange = Editor.visibleTimeRange(state) - - ctx.fillStyle = "#1b191c" - - const songXMin = Editor.xAtTime(state, state.project.range.start) - const songXMax = Editor.xAtTime(state, state.project.range.end) - - if (songXMin > 0) - ctx.fillRect(0, 0, songXMin, state.h) - - if (songXMax < state.w) - ctx.fillRect(songXMax, 0, state.w - songXMax, state.h) - - for (let [meterCh1, meterCh2] of state.project.meterChanges.iterActiveAtRangePairwise(visibleTimeRange)) - { - if (!meterCh1 && !meterCh2) - continue - - let timeMin = (meterCh1 ? meterCh1.time : null) - let timeMax = (meterCh2 ? meterCh2.time : visibleTimeRange.end) - - let measureAlternate = true - - if (!meterCh1) - { - meterCh1 = meterCh2 - timeMin = meterCh2.time - while (timeMin.compare(visibleTimeRange.start) > 0) - { - timeMin = timeMin.subtract(meterCh2.meter.fullCycleDuration) - - if (meterCh2.meter.alternatingMeasureCount % 2 != 0) - measureAlternate = !measureAlternate - } - } - - const meterCh1X = Editor.xAtTime(state, timeMin) - const meterCh2X = Editor.xAtTime(state, timeMax) - - ctx.strokeStyle = state.prefs.meterChangeColor - ctx.lineCap = "square" - ctx.lineWidth = 1 - - ctx.beginPath() - ctx.moveTo(meterCh1X, 0) - ctx.lineTo(meterCh1X, state.h) - ctx.stroke() - - for (const [measureN, measureD, time1, time2] of meterCh1.meter.iterMeasuresPairwise(timeMin)) - { - measureAlternate = !measureAlternate - - if (time2.compare(visibleTimeRange.start) < 0) - continue - - if (time1.compare(timeMax) > 0 || time1.compare(visibleTimeRange.end) > 0) - break - - const measureX1 = Editor.xAtTime(state, time1) - const measureX2 = Editor.xAtTime(state, time2) - - if (measureAlternate) - { - const x1 = Math.max(songXMin, Math.min(songXMax, Math.min(meterCh2X, measureX1))) - const x2 = Math.max(songXMin, Math.min(songXMax, Math.min(meterCh2X, measureX2))) - - ctx.fillStyle = "#cdf" - ctx.globalAlpha = 0.1 - ctx.fillRect(x1, 0, x2 - x1, state.h) - ctx.globalAlpha = 1 - } - - const halfSubmeasureSize = Editor.xAtTime(state, new Rational(1, measureD * 2)) - Editor.xAtTime(state, new Rational(0)) - if (halfSubmeasureSize > 16) - { - let halfSubmeasureTime = time1.add(new Rational(-1, measureD * 2)) - for (let sub = 1; sub <= measureN; sub++) - { - halfSubmeasureTime = halfSubmeasureTime.add(new Rational(2, measureD * 2)) - - const halfSubmeasureX = Editor.xAtTime(state, halfSubmeasureTime) - if (halfSubmeasureX >= meterCh1X && halfSubmeasureX <= meterCh2X) - { - ctx.strokeStyle = state.prefs.halfSubmeasureColor - ctx.beginPath() - ctx.moveTo(halfSubmeasureX, 0) - ctx.lineTo(halfSubmeasureX, state.h) - ctx.stroke() - } - } - } - - const submeasureSize = Editor.xAtTime(state, new Rational(1, measureD)) - Editor.xAtTime(state, new Rational(0)) - if (submeasureSize > 8) - { - let submeasureTime = time1 - for (let sub = 1; sub <= measureN; sub++) - { - submeasureTime = submeasureTime.add(new Rational(1, measureD)) - - const submeasureX = Editor.xAtTime(state, submeasureTime) - if (submeasureX >= meterCh1X && submeasureX <= meterCh2X) - { - ctx.strokeStyle = state.prefs.submeasureColor - ctx.beginPath() - ctx.moveTo(submeasureX, 0) - ctx.lineTo(submeasureX, state.h) - ctx.stroke() - } - } - } - } - } - - for (const keyCh of state.project.keyChanges.iterAtRange(visibleTimeRange)) - { - const keyChX = Editor.xAtTime(state, keyCh.time) - - ctx.strokeStyle = state.prefs.keyChangeColor - ctx.lineCap = "square" - ctx.lineWidth = 1 - - ctx.beginPath() - ctx.moveTo(keyChX, 0) - ctx.lineTo(keyChX, state.h) - ctx.stroke() - } - } - - - static renderPlaybackBeam(state, ctx, time) - { - if (!state.playback.playing) - return - - const trackMin = 0 - const trackMax = state.tracks.length - 1 - - if (trackMin < 0 || trackMax < 0) - return - - ctx.save() - - const x = Editor.xAtTime(state, time) - - ctx.strokeStyle = state.prefs.playbackCursorColor - ctx.lineCap = "round" - ctx.lineWidth = 2 - - const trackMinRect = state.tracks[trackMin].rect - const trackMaxRect = state.tracks[trackMax].rect - - ctx.beginPath() - ctx.moveTo(trackMinRect.x1 + x, trackMinRect.y1) - ctx.lineTo(trackMinRect.x1 + x, trackMinRect.y1) - ctx.lineTo(trackMaxRect.x1 + x, trackMaxRect.y2) - ctx.lineTo(trackMaxRect.x1 + x, trackMaxRect.y2) - ctx.stroke() - - ctx.restore() - } - - - static renderCursorBeam(state, ctx, time, tipOffsetSide) - { - if (state.playback.playing) - return - - if (state.rectSelection) - return - - const trackMin = Math.min(state.cursor.track1, state.cursor.track2) - const trackMax = Math.max(state.cursor.track1, state.cursor.track2) - - if (trackMin < 0 || trackMax < 0) - return - - ctx.save() - - const x = Editor.xAtTime(state, time) - - ctx.strokeStyle = state.prefs.selectionCursorColor - ctx.lineCap = "round" - ctx.lineWidth = 2 - - const headSize = 7 * (tipOffsetSide ? -1 : 1) - - const trackMinRect = state.tracks[trackMin].rect - const trackMaxRect = state.tracks[trackMax].rect - - ctx.beginPath() - ctx.moveTo(trackMinRect.x1 + x + headSize, trackMinRect.y1) - ctx.lineTo(trackMinRect.x1 + x, trackMinRect.y1) - ctx.lineTo(trackMaxRect.x1 + x, trackMaxRect.y2) - ctx.lineTo(trackMaxRect.x1 + x + headSize, trackMaxRect.y2) - ctx.stroke() - - ctx.restore() - } - - - static renderCursorHighlight(state, ctx) - { - if (state.playback.playing) - return - - if (state.rectSelection) - return - - if (!state.cursor.visible) - return - - const timeMin = state.cursor.time1.min(state.cursor.time2) - const timeMax = state.cursor.time1.max(state.cursor.time2) - const trackMin = Math.min(state.cursor.track1, state.cursor.track2) - const trackMax = Math.max(state.cursor.track1, state.cursor.track2) - const trackMinRect = state.tracks[trackMin].rect - const trackMaxRect = state.tracks[trackMax].rect - - ctx.save() - - const x1 = Editor.xAtTime(state, timeMin) - const x2 = Editor.xAtTime(state, timeMax) - - ctx.fillStyle = state.prefs.selectionCursorColor - ctx.globalAlpha = 0.3 - ctx.fillRect(x1, trackMinRect.y1, x2 - x1, trackMaxRect.y2 - trackMinRect.y1) - - ctx.restore() - } - - - static renderRectSelectionContour(state, ctx, trackIndex) - { - if (state.playback.playing) - return - - if (!state.rectSelection) - return - - if (trackIndex != state.rectSelection.track) - return - - const timeMin = state.rectSelection.time1.min(state.rectSelection.time2) - const timeMax = state.rectSelection.time1.max(state.rectSelection.time2) - const y1 = Math.min(state.rectSelection.y1, state.rectSelection.y2) - const y2 = Math.max(state.rectSelection.y1, state.rectSelection.y2) + 1 - - ctx.save() - - const x1 = Editor.xAtTime(state, timeMin) - const x2 = Editor.xAtTime(state, timeMax) - - ctx.strokeStyle = state.prefs.selectionCursorColor - ctx.lineCap = "square" - ctx.lineWidth = 2 - ctx.strokeRect(x1, y1, x2 - x1, y2 - y1) - - ctx.restore() - } - - - static renderRectSelectionHighlight(state, ctx, trackIndex) - { - if (state.playback.playing) - return - - if (!state.rectSelection) - return - - if (trackIndex != state.rectSelection.track) - return - - const timeMin = state.rectSelection.time1.min(state.rectSelection.time2) - const timeMax = state.rectSelection.time1.max(state.rectSelection.time2) - const y1 = Math.min(state.rectSelection.y1, state.rectSelection.y2) - const y2 = Math.max(state.rectSelection.y1, state.rectSelection.y2) + 1 - - ctx.save() - - const x1 = Editor.xAtTime(state, timeMin) - const x2 = Editor.xAtTime(state, timeMax) - - ctx.fillStyle = state.prefs.selectionCursorColor - ctx.globalAlpha = 0.3 - ctx.fillRect(x1, y1, x2 - x1, y2 - y1) - - ctx.restore() - } - - - static Cursor = class EditorCursor - { - static setVisible(state, visible) - { - return { ...state, - cursor: { ...state.cursor, - visible, - }, - } - } - - - static place(state, time, trackIndex) - { - if (trackIndex !== null) - trackIndex = Math.max(0, Math.min(state.tracks.length - 1, trackIndex)) - - return { ...state, - cursor: { ...state.cursor, - time1: time === null ? state.cursor.time1 : time, - time2: time === null ? state.cursor.time2 : time, - track1: trackIndex === null ? state.cursor.track1 : trackIndex, - track2: trackIndex === null ? state.cursor.track2 : trackIndex, - }, - } - } - - - static drag(state, time, trackIndex) - { - if (trackIndex !== null) - trackIndex = Math.max(0, Math.min(state.tracks.length - 1, trackIndex)) - - return { ...state, - cursor: { ...state.cursor, - time2: time === null ? state.cursor.time2 : time, - track2: trackIndex === null ? state.cursor.track2 : trackIndex, - }, - } - } - } -} \ No newline at end of file diff --git a/src_old/editor/track.js b/src_old/editor/track.js deleted file mode 100644 index 225d9fd..0000000 --- a/src_old/editor/track.js +++ /dev/null @@ -1,310 +0,0 @@ -import { default as Immutable } from "immutable" -import Editor from "./editor.js" -import TrackMarkers from "./trackMarkers.js" -import TrackChords from "./trackChords.js" -import TrackNotes from "./trackNotes.js" -import Rational from "../util/rational.js" -import Range from "../util/range.js" -import Rect from "../util/rect.js" - - -export default class Track -{ - static init(state, action) - { - return { - rect: new Rect(0, 0, 0, 0), - yScroll: 0, - hover: null, - draw: null, - selection: new Immutable.Set(), - dragData: null, - } - } - - - static handlerForTrackKind(kind) - { - switch (kind) - { - case "markers": return TrackMarkers - case "chords": return TrackChords - case "notes": return TrackNotes - default: throw "invalid track kind" - } - } - - - static dispatchDerived(fnName, state, trackIndex, args) - { - const handler = Track.handlerForTrackKind(state.tracks[trackIndex].kind) - const fn = handler[fnName] - if (!fn) - return null - - return fn(state, trackIndex, args) - } - - - static update(state, trackIndex, newData) - { - const track = - { - ...state.tracks[trackIndex], - ...newData - } - - return { - ...state, - tracks: [ ...state.tracks.slice(0, trackIndex), track, ...state.tracks.slice(trackIndex + 1) ], - } - } - - - static hoveredIsSelected(state, trackIndex, args) - { - if (!state.tracks[trackIndex].hover) - return false - - return state.tracks[trackIndex].selection.has(state.tracks[trackIndex].hover.id) - } - - - static hoveredSelect(state, trackIndex, args) - { - if (!state.tracks[trackIndex].hover) - return state - - const id = state.tracks[trackIndex].hover.id - - state = Track.cursorSetAtElem(state, trackIndex, state.project.findById(id)) - return Track.selectionAdd(state, trackIndex, id) - } - - - static hoveredToggleSelection(state, trackIndex, args) - { - if (!state.tracks[trackIndex].hover) - return state - - const id = state.tracks[trackIndex].hover.id - - state = Track.cursorSetAtElem(state, trackIndex, state.project.findById(id)) - - if (state.tracks[trackIndex].selection.has(id)) - return Track.selectionRemove(state, trackIndex, id) - else - return Track.selectionAdd(state, trackIndex, id) - } - - - static hoveredPlaySoundPreview(state, trackIndex, args) - { - return Track.dispatchDerived("hoveredPlaySoundPreview", state, trackIndex, args) || state - } - - - static cursorSetAtElem(state, trackIndex, elem) - { - if (!elem) - return state - - if (elem.time) - return Editor.Cursor.place(state, elem.time, trackIndex) - - if (elem.range) - return Editor.Cursor.place(state, elem.range.start, trackIndex) - - return state - } - - - static selectionClear(state, trackIndex, args) - { - return Track.update(state, trackIndex, { selection: new Immutable.Set() }) - } - - - static selectionAdd(state, trackIndex, id) - { - return Track.update(state, trackIndex, { selection: state.tracks[trackIndex].selection.add(id) }) - } - - - static selectionRemove(state, trackIndex, id) - { - return Track.update(state, trackIndex, { selection: state.tracks[trackIndex].selection.delete(id) }) - } - - - static selectionRange(state, trackIndex) - { - let range = null - - for (const id of state.tracks[trackIndex].selection) - { - const elem = state.project.findById(id) - if (!elem) - continue - - if (elem.range) - range = Range.merge(range, elem.range) - else - range = Range.merge(range, Range.fromPoint(elem.time)) - } - - return range - } - - - static selectionHasAny(state, trackIndex, args) - { - const res = Track.dispatchDerived("selectionHasAny", state, trackIndex, args) - if (res !== null) - return res - - for (const id of state.tracks[trackIndex].selection) - if (state.project.findById(id)) - return true - - return false - } - - - static selectionDelete(state, trackIndex, args) - { - const res = Track.dispatchDerived("selectionDelete", state, trackIndex, args) - if (res !== null) - return res - - for (const id of state.tracks[trackIndex].selection) - state = { ...state, project: state.project.removeById(id) } - - return state - } - - - static selectionAddAtCursor(state, trackIndex, args) - { - return Track.dispatchDerived("selectionAddAtCursor", state, trackIndex, args) || state - } - - - static selectionAddAtRect(state, trackIndex, args) - { - return Track.dispatchDerived("selectionAddAtRect", state, trackIndex, args) || state - } - - - static selectionAddToClipboard(state, trackIndex, args) - { - const res = Track.dispatchDerived("selectionAddToClipboard", state, trackIndex, args) - if (res !== null) - return res - - for (const id of state.tracks[trackIndex].selection) - { - const elem = state.project.findById(id) - if (!elem) - continue - - state = Editor.clipboardAdd(state, elem) - } - - return state - } - - - static selectionRemoveConflictingBehind(state, trackIndex, args) - { - return Track.dispatchDerived("selectionRemoveConflictingBehind", state, trackIndex, args) || state - } - - - static clipboardPaste(state, trackIndex, args) - { - return Track.dispatchDerived("clipboardPaste", state, trackIndex, args) || state - } - - - static updateHover(state, trackIndex, args) - { - return Track.dispatchDerived("updateHover", state, trackIndex, args) || state - } - - - static previousAnchor(state, trackIndex, time) - { - return Track.dispatchDerived("previousAnchor", state, trackIndex, time) || null - } - - - static deleteRange(state, trackIndex, range) - { - return Track.dispatchDerived("deleteRange", state, trackIndex, range) || state - } - - - static pan(state, trackIndex, args) - { - return Track.dispatchDerived("pan", state, trackIndex, args) || state - } - - - static dragStart(state, trackIndex, args) - { - let elems = new Immutable.Map() - for (const id of state.tracks[trackIndex].selection) - elems = elems.set(id, state.project.findById(id)) - - let dragData = - { - yScroll: state.tracks[trackIndex].yScroll, - elems, - } - - return Track.update(state, trackIndex, { dragData }) - } - - - static drag(state, trackIndex, args) - { - return Track.dispatchDerived("drag", state, trackIndex, args) || state - } - - - static drawStart(state, trackIndex, args) - { - return Track.dispatchDerived("drawStart", state, trackIndex, args) || state - } - - - static drawMove(state, trackIndex, args) - { - return Track.dispatchDerived("drawMove", state, trackIndex, args) || state - } - - - static drawEnd(state, trackIndex, args) - { - return Track.dispatchDerived("drawEnd", state, trackIndex, args) || state - } - - - static keyCommandTimeShift(state, trackIndex, args) - { - return Track.dispatchDerived("keyCommandTimeShift", state, trackIndex, args) || state - } - - - static keyCommandPitchShift(state, trackIndex, args) - { - return Track.dispatchDerived("keyCommandPitchShift", state, trackIndex, args) || state - } - - - static render(state, trackIndex, args) - { - return Track.dispatchDerived("render", state, trackIndex, args) || state - } -} \ No newline at end of file diff --git a/src_old/editor/trackChords.js b/src_old/editor/trackChords.js deleted file mode 100644 index 075bb6f..0000000 --- a/src_old/editor/trackChords.js +++ /dev/null @@ -1,456 +0,0 @@ -import Project from "../project/project.js" -import Editor from "./editor.js" -import Track from "./track.js" -import Rational from "../util/rational.js" -import Range from "../util/range.js" -import Rect from "../util/rect.js" -import Theory from "../theory.ts" -import CanvasUtils from "../util/canvas.js" - - -export default class TrackChords -{ - static init(state, trackIndex, action) - { - const newTrack = - { - ...Track.init(state, action), - kind: "chords", - } - - return { ...state, - tracks: [ ...state.tracks, newTrack ], - } - } - - - static updateHover(state, trackIndex, input) - { - let hover = null - let draw = null - - if (state.keys[Editor.keyDraw]) - { - const keyCh = state.project.keyChanges.findActiveAt(input.mouse.time) - const key = keyCh ? keyCh.key : Editor.defaultKey() - draw = - { - kind: "chord", - time1: input.mouse.time, - time2: input.mouse.time.add(state.timeSnap.multiply(new Rational(4))), - chord: new Theory.Chord(key.chromaForDegree(0), 0, 0, 0, []) - } - } - else - { - const margin = 10 - const checkRange = Editor.timeRangeAtX(state, input.mouse.x - margin, input.mouse.x + margin) - - let hoverPrimary = null - let hoverSecondary = null - - for (const chord of state.project.chords.iterAtRange(checkRange)) - { - const margin = 2 - - const x1 = Editor.xAtTime(state, chord.range.start) - const x2 = Editor.xAtTime(state, chord.range.end) - const rect = new Rect(x1 - margin, 0, x2 - x1 + margin * 2, state.tracks[trackIndex].rect.h) - - const dragMarginOut = Math.abs(Math.min(0, rect.w - 16)) - const stretchMarginOut = 16 - const stretchMarginIn = Math.max(0, Math.min(10, rect.w / 4)) - const rectDrag = new Rect(rect.x - dragMarginOut, rect.y, rect.w + dragMarginOut * 2, rect.h) - const rectStretchStart = new Rect(rect.x - stretchMarginOut, rect.y, stretchMarginOut + stretchMarginIn, rect.h) - const rectStretchEnd = new Rect(rect.x2 - stretchMarginIn, rect.y, stretchMarginOut + stretchMarginIn, rect.h) - - const exactHover = rect.contains(input.mouse) - - let action = 0 - if (dragMarginOut > 0 && rectDrag.contains(input.mouse)) - action = Editor.actionDragTime - else if (rectStretchStart.contains(input.mouse)) - action = Editor.actionStretchTimeStart - else if (rectStretchEnd.contains(input.mouse)) - action = Editor.actionStretchTimeEnd - else if (exactHover) - action = Editor.actionDragTime - - const hoverChord = - { - id: chord.id, - range: chord.range, - action, - } - - if (action != 0) - { - if (exactHover) - hoverPrimary = hoverChord - else - hoverSecondary = hoverChord - } - - if (hoverPrimary) - break - } - - hover = hoverPrimary || hoverSecondary - } - - return Track.update(state, trackIndex, { hover, draw }) - } - - - static hoveredPlaySoundPreview(state, trackIndex) - { - const hover = state.tracks[trackIndex].hover - if (!hover) - return state - - const id = hover.id - const chord = state.project.chords.findById(id) - - if (!chord) - return state - - return Editor.soundPreviewSet(state, TrackChords.mergeSoundPreview(null, chord.chord)) - } - - - static selectionAddAtCursor(state, trackIndex) - { - const timeRange = new Range(state.cursor.time1, state.cursor.time2, false, false).sorted() - - for (const chord of state.project.chords.iterAtRange(timeRange)) - state = Track.selectionAdd(state, trackIndex, chord.id) - - return state - } - - - static selectionAddAtRect(state, trackIndex) - { - const timeRange = new Range(state.rectSelection.time1, state.rectSelection.time2, false, false).sorted() - - for (const chord of state.project.chords.iterAtRange(timeRange)) - state = Track.selectionAdd(state, trackIndex, chord.id) - - return state - } - - - static selectionRemoveConflictingBehind(state, trackIndex) - { - const selection = state.tracks[trackIndex].selection - - for (const id of selection) - { - const elem = state.project.findById(id) - if (!elem) - continue - - for (const chord of state.project.chords.iterAtRange(elem.range)) - { - if (selection.has(chord.id)) - continue - - state = { ...state, project: state.project.removeById(chord.id) } - - for (const slice of chord.range.iterSlices(elem.range)) - { - const newChord = new Project.Chord(slice, chord.chord) - state = { ...state, project: state.project.upsertChord(newChord) } - } - } - } - - return state - } - - - static clipboardPaste(state, trackIndex, pasteData) - { - for (const [clipboardId, elem] of state.clipboard.elems) - { - if (elem instanceof Project.Chord) - { - const range = elem.range.displace(pasteData.time.subtract(state.clipboard.range.start)) - const chord = new Project.Chord(range, elem.chord) - const id = state.project.nextId - state = { ...state, project: state.project.upsertChord(chord) } - state = Track.selectionAdd(state, trackIndex, id) - } - } - - return state - } - - - static previousAnchor(state, trackIndex, time) - { - return state.project.chords.findPreviousAnchor(time) - } - - - static deleteRange(state, trackIndex, range) - { - for (const chord of state.project.chords.iterAtRange(range)) - { - state = { ...state, project: state.project.removeById(chord.id) } - - for (const slice of chord.range.iterSlices(range)) - { - const newChord = new Project.Chord(slice, chord.chord) - state = { ...state, project: state.project.upsertChord(newChord) } - } - } - - return state - } - - - static drag(state, trackIndex) - { - for (const id of state.tracks[trackIndex].selection) - { - const elem = state.tracks[trackIndex].dragData.elems.get(id) - if (!elem) - continue - - let changes = {} - - if (state.mouse.action & Editor.actionDragTime) - changes.range = elem.range.displace(state.mouse.drag.timeDelta) - - if (state.mouse.action & Editor.actionStretchTimeStart) - { - changes.range = elem.range.stretch(state.mouse.drag.timeDelta, state.mouse.dragOrig.range.end, state.mouse.dragOrig.range.start) - if (elem.range.start.compare(state.mouse.dragOrig.range.start) == 0) - changes.range = new Range(changes.range.start.snap(state.timeSnap), changes.range.end) - - changes.range = changes.range.sorted() - } - - if (state.mouse.action & Editor.actionStretchTimeEnd) - { - changes.range = elem.range.stretch(state.mouse.drag.timeDelta, state.mouse.dragOrig.range.start, state.mouse.dragOrig.range.end) - if (elem.range.end.compare(state.mouse.dragOrig.range.end) == 0) - changes.range = new Range(changes.range.start, changes.range.end.snap(state.timeSnap)) - - changes.range = changes.range.sorted() - } - - state = { ...state, project: state.project.update(elem.withChanges(changes)) } - } - - return state - } - - - static drawMove(state, trackIndex, input) - { - let draw = state.tracks[trackIndex].draw - - if (draw) - { - let time2 = input.mouse.time - if (Math.abs(Editor.xAtTime(state, draw.time1) - Editor.xAtTime(state, time2)) < 5) - time2 = draw.time1.add(state.timeSnap.multiply(new Rational(4))) - - draw = { ...draw, time2 } - } - - return Track.update(state, trackIndex, { draw }) - } - - - static drawEnd(state, trackIndex, input) - { - const draw = state.tracks[trackIndex].draw - if (draw) - { - if (draw.kind == "chord") - { - const chord = new Project.Chord(new Range(draw.time1, draw.time2).sorted(), draw.chord) - const id = state.project.nextId - state = { ...state, project: state.project.upsertChord(chord) } - state = Track.selectionAdd(state, trackIndex, id) - state = Editor.soundPreviewSet(state, TrackChords.mergeSoundPreview(null, draw.chord)) - } - } - - return state - } - - - static keyCommandTimeShift(state, trackIndex, commandData) - { - for (const id of state.tracks[trackIndex].selection) - { - const elem = state.project.findById(id) - if (!elem) - continue - - let changes = {} - changes.range = elem.range.displace(commandData.timeDelta) - - state = { ...state, project: state.project.update(elem.withChanges(changes)) } - } - - return state - } - - - static keyCommandPitchShift(state, trackIndex, commandData) - { - let soundPreview = null - - for (const id of state.tracks[trackIndex].selection) - { - const elem = state.project.findById(id) - if (!elem) - continue - - let changes = {} - if (commandData.diatonic) - { - const keyCh = state.project.keyChanges.findActiveAt(elem.range.start) - const key = keyCh ? keyCh.key : Editor.defaultKey() - const degree = key.octavedDegreeForMidi(elem.chord.rootMidi) - const newDegree = degree + commandData.degreeDelta - const newRoot = key.midiForDegree(commandData.degreeDelta >= 0 ? Math.floor(newDegree) : Math.ceil(newDegree)) - changes.chord = elem.chord.withChanges({ rootMidi: newRoot }) - } - else - changes.chord = elem.chord.withChanges({ rootMidi: elem.chord.rootMidi + commandData.pitchDelta }) - - soundPreview = TrackChords.mergeSoundPreview(soundPreview, changes.chord) - state = { ...state, project: state.project.update(elem.withChanges(changes)) } - } - - state = Editor.soundPreviewSet(state, soundPreview) - return state - } - - - static mergeSoundPreview(soundPreview, chord) - { - return { kind: "chord", chord } - } - - - static *iterKeyChangesAtRange(state, trackIndex, range) - { - const defaultKey = Editor.defaultKey() - - for (const pair of state.project.keyChanges.iterActiveAtRangePairwise(range)) - { - const keyCh1 = pair[0] || new Project.KeyChange(range.start, defaultKey) - const keyCh2 = pair[1] || new Project.KeyChange(range.end, defaultKey) - - const keyCh1X = Editor.xAtTime(state, keyCh1.time) - const keyCh2X = Editor.xAtTime(state, keyCh2.time) - - yield [keyCh1, keyCh1X, keyCh2X] - } - } - - - static *iterChordsAndKeyChangesAtRange(state, trackIndex, range) - { - const defaultKey = Editor.defaultKey() - - for (const pair of state.project.keyChanges.iterActiveAtRangePairwise(range)) - { - const keyCh1 = pair[0] || new Project.KeyChange(range.start, defaultKey) - const keyCh2 = pair[1] || new Project.KeyChange(range.end, defaultKey) - - const keyCh1X = Editor.xAtTime(state, keyCh1.time) - const keyCh2X = Editor.xAtTime(state, keyCh2.time) - - const time1 = keyCh1.time.max(range.start) - const time2 = keyCh2.time.min(range.end) - - for (const chord of state.project.chords.iterAtRange(new Range(time1, time2))) - yield [chord, keyCh1, keyCh1X, keyCh2X] - } - } - - - static render(state, trackIndex, ctx) - { - const track = state.tracks[trackIndex] - const visibleTimeRange = Editor.visibleTimeRange(state) - - for (const [chord, keyCh, xMin, xMax] of TrackChords.iterChordsAndKeyChangesAtRange(state, trackIndex, visibleTimeRange)) - { - if (!state.playback.playing && track.selection.has(chord.id)) - continue - - const hovering = track.hover && track.hover.id == chord.id - const playing = state.playback.playing && chord.range.overlapsPoint(state.playback.time) - TrackChords.renderChord(state, trackIndex, ctx, chord.range, keyCh.key, chord.chord, xMin, xMax, hovering, false, playing) - } - - if (!state.playback.playing) - { - for (const [chord, keyCh, xMin, xMax] of TrackChords.iterChordsAndKeyChangesAtRange(state, trackIndex, visibleTimeRange)) - { - if (!track.selection.has(chord.id)) - continue - - const hovering = track.hover && track.hover.id == chord.id - TrackChords.renderChord(state, trackIndex, ctx, chord.range, keyCh.key, chord.chord, xMin, xMax, hovering, true, false) - } - } - - const draw = state.tracks[trackIndex].draw - if (draw) - { - ctx.globalAlpha = 0.6 - - const keyCh = state.project.keyChanges.findActiveAt(draw.time1) - const key = keyCh ? keyCh.key : Editor.defaultKey() - TrackChords.renderChord(state, trackIndex, ctx, new Range(draw.time1, draw.time2).sorted(), key, draw.chord, -Infinity, Infinity, false, false) - - ctx.globalAlpha = 1 - } - } - - - static renderChord(state, trackIndex, ctx, range, key, chord, xMin, xMax, hovering, selected, playing) - { - const h = state.tracks[trackIndex].rect.h - const chordX1 = Editor.xAtTime(state, range.start) - const chordX2 = Editor.xAtTime(state, range.end) - - let x1 = Math.max(xMin, Math.min(xMax, chordX1)) - let x2 = Math.max(xMin, Math.min(xMax, chordX2)) - - if (x1 == chordX1) - x1 += 1 - - if (x2 == chordX2) - x2 -= 1 - - CanvasUtils.drawChord(ctx, x1, 0, x2, h, chord, key) - - if (hovering) - { - ctx.globalAlpha = 0.4 - ctx.fillStyle = "#fff" - ctx.fillRect(x1, 0, x2 - x1, h) - ctx.globalAlpha = 1 - } - - if (selected || playing) - { - const margin = 3 - ctx.globalAlpha = 0.6 - ctx.fillStyle = "#fff" - ctx.fillRect(x1, margin, x2 - x1, h - margin * 2) - ctx.globalAlpha = 1 - } - } -} \ No newline at end of file diff --git a/src_old/editor/trackMarkers.js b/src_old/editor/trackMarkers.js deleted file mode 100644 index 5c565d9..0000000 --- a/src_old/editor/trackMarkers.js +++ /dev/null @@ -1,477 +0,0 @@ -import Project from "../project/project.js" -import Editor from "./editor.js" -import Track from "./track.js" -import Rational from "../util/rational.js" -import Range from "../util/range.js" -import Rect from "../util/rect.js" -import Theory from "../theory.ts" - - -const markerWidth = 22 -const keyMarkerY = 0 -const keyMarkerHeight = 22 -const meterMarkerY = 22 -const meterMarkerHeight = 22 - - -export default class TrackMarkers -{ - static init(state, trackIndex, action) - { - const newTrack = - { - ...Track.init(state, action), - kind: "markers", - } - - return { ...state, - tracks: [ ...state.tracks, newTrack ], - } - } - - - static updateHover(state, trackIndex, input) - { - let hover = null - let draw = null - - if (state.keys[Editor.keyDraw]) - { - if (input.mouse.y >= meterMarkerY && input.mouse.y <= meterMarkerY + meterMarkerHeight) - draw = { kind: "meter", time: input.mouse.time } - - if (input.mouse.y >= keyMarkerY && input.mouse.y <= keyMarkerY + keyMarkerHeight) - draw = { kind: "key", time: input.mouse.time } - } - else - { - const checkRange = Editor.timeRangeAtX(state, input.mouse.x - markerWidth, input.mouse.x + markerWidth) - - for (const keyCh of state.project.keyChanges.iterAtRange(checkRange)) - { - const rect = TrackMarkers.knobRectForKeyChange(state, keyCh.time) - if (rect.contains(input.mouse)) - { - hover = - { - id: keyCh.id, - range: Range.fromPoint(keyCh.time), - action: Editor.actionDragTime, - } - } - } - - for (const meterCh of state.project.meterChanges.iterAtRange(checkRange)) - { - const rect = TrackMarkers.knobRectForMeterChange(state, meterCh.time) - if (rect.contains(input.mouse)) - { - hover = - { - id: meterCh.id, - range: Range.fromPoint(meterCh.time), - action: Editor.actionDragTime, - } - } - } - } - - return Track.update(state, trackIndex, { hover, draw }) - } - - - static selectionAddAtCursor(state, trackIndex) - { - const timeRange = new Range(state.cursor.time1, state.cursor.time2, false, false).sorted() - - for (const keyCh of state.project.keyChanges.iterAtRange(timeRange)) - state = Track.selectionAdd(state, trackIndex, keyCh.id) - - for (const meterCh of state.project.meterChanges.iterAtRange(timeRange)) - state = Track.selectionAdd(state, trackIndex, meterCh.id) - - return state - } - - - static selectionAddAtRect(state, trackIndex) - { - const timeRange = new Range(state.rectSelection.time1, state.rectSelection.time2, false, false).sorted() - const y1 = Math.min(state.rectSelection.y1, state.rectSelection.y2) - const y2 = Math.max(state.rectSelection.y1, state.rectSelection.y2) - - if (y2 > keyMarkerY && y1 < keyMarkerY + keyMarkerHeight) - for (const keyCh of state.project.keyChanges.iterAtRange(timeRange)) - state = Track.selectionAdd(state, trackIndex, keyCh.id) - - if (y2 > meterMarkerY && y1 < meterMarkerY + meterMarkerHeight) - for (const meterCh of state.project.meterChanges.iterAtRange(timeRange)) - state = Track.selectionAdd(state, trackIndex, meterCh.id) - - return state - } - - - static selectionRemoveConflictingBehind(state, trackIndex) - { - const selection = state.tracks[trackIndex].selection - - for (const id of selection) - { - const elem = state.project.findById(id) - - if (elem instanceof Project.KeyChange) - { - for (const keyCh of state.project.keyChanges.iterAt(elem.time)) - { - if (selection.has(keyCh.id)) - continue - - state = { ...state, project: state.project.removeById(keyCh.id) } - } - } - - if (elem instanceof Project.MeterChange) - { - for (const meterCh of state.project.meterChanges.iterAt(elem.time)) - { - if (selection.has(meterCh.id)) - continue - - state = { ...state, project: state.project.removeById(meterCh.id) } - } - } - } - - return state - } - - - static clipboardPaste(state, trackIndex, pasteData) - { - for (const [clipboardId, elem] of state.clipboard.elems) - { - if (elem instanceof Project.KeyChange) - { - const time = elem.time.add(pasteData.time.subtract(state.clipboard.range.start)) - const keyCh = new Project.KeyChange(time, elem.key) - const id = state.project.nextId - state = { ...state, project: state.project.upsertKeyChange(keyCh) } - state = Track.selectionAdd(state, trackIndex, id) - } - - if (elem instanceof Project.MeterChange) - { - const time = elem.time.add(pasteData.time.subtract(state.clipboard.range.start)) - const meterCh = new Project.MeterChange(time, elem.meter) - const id = state.project.nextId - state = { ...state, project: state.project.upsertMeterChange(meterCh) } - state = Track.selectionAdd(state, trackIndex, id) - } - } - - return state - } - - - static previousAnchor(state, trackIndex, time) - { - const prevAnchorKey = state.project.keyChanges.findPreviousDeletionAnchor(time) - const prevAnchorMeter = state.project.meterChanges.findPreviousDeletionAnchor(time) - - return Rational.max(prevAnchorKey, prevAnchorMeter) - } - - - static deleteRange(state, trackIndex, range) - { - if (range.start.compare(range.end) == 0) - { - for (const keyCh of state.project.keyChanges.iterAt(range.start)) - state = { ...state, project: state.project.removeById(keyCh.id) } - - for (const meterCh of state.project.meterChanges.iterAt(range.start)) - state = { ...state, project: state.project.removeById(meterCh.id) } - } - else - { - for (const keyCh of state.project.keyChanges.iterAtRange(range)) - state = { ...state, project: state.project.removeById(keyCh.id) } - - for (const meterCh of state.project.meterChanges.iterAtRange(range)) - state = { ...state, project: state.project.removeById(meterCh.id) } - } - - return state - } - - - static drag(state, trackIndex) - { - for (const id of state.tracks[trackIndex].selection) - { - const elem = state.tracks[trackIndex].dragData.elems.get(id) - if (!elem) - continue - - let changes = {} - - if (state.mouse.action & Editor.actionDragTime) - changes.time = elem.time.add(state.mouse.drag.timeDelta).snap(state.timeSnap) - - if (state.mouse.action & Editor.actionStretchTimeStart) - { - changes.time = elem.time.stretch(state.mouse.drag.timeDelta, state.mouse.dragOrig.range.end, state.mouse.dragOrig.range.start) - if (elem.time.compare(state.mouse.dragOrig.range.start) == 0) - changes.time = changes.time.snap(state.timeSnap) - } - - if (state.mouse.action & Editor.actionStretchTimeEnd) - { - changes.time = elem.time.stretch(state.mouse.drag.timeDelta, state.mouse.dragOrig.range.start, state.mouse.dragOrig.range.end) - if (elem.time.compare(state.mouse.dragOrig.range.start) == 0) - changes.time = changes.time.snap(state.timeSnap) - } - - state = { ...state, project: state.project.update(elem.withChanges(changes)) } - } - - return state - } - - - static drawMove(state, trackIndex, input) - { - let draw = state.tracks[trackIndex].draw - - if (draw) - draw = { ...draw, time: input.mouse.time } - - return Track.update(state, trackIndex, { draw }) - } - - - static drawEnd(state, trackIndex, input) - { - const draw = state.tracks[trackIndex].draw - if (draw) - { - const id = state.project.nextId - - if (draw.kind == "key") - state = { ...state, project: state.project.upsertKeyChange(new Project.KeyChange(draw.time, Editor.defaultKey())) } - - else if (draw.kind == "meter") - state = { ...state, project: state.project.upsertMeterChange(new Project.MeterChange(draw.time, Editor.defaultMeter())) } - - state = Track.selectionAdd(state, trackIndex, id) - } - - return state - } - - - static keyCommandTimeShift(state, trackIndex, commandData) - { - for (const id of state.tracks[trackIndex].selection) - { - const elem = state.project.findById(id) - if (!elem) - continue - - let changes = {} - changes.time = elem.time.add(commandData.timeDelta) - - state = { ...state, project: state.project.update(elem.withChanges(changes)) } - } - - return state - } - - - static keyCommandPitchShift(state, trackIndex, commandData) - { - for (const id of state.tracks[trackIndex].selection) - { - const elem = state.project.findById(id) - if (!elem) - continue - - if (!(elem instanceof Project.KeyChange)) - continue - - let changes = {} - if (commandData.diatonic) - { - const key = elem.key - const newRootMidi = key.midiForDegree(0) + commandData.degreeDelta - changes.key = new Theory.Key(key.nameForMidi(newRootMidi).simplified, key.scale) - /*const key = elem.key - const newRoot = new Theory.PitchName(key.tonic.letter + commandData.degreeDelta, key.tonic.accidental) - changes.key = new Theory.Key(newRoot, key.scale)*/ - } - else - { - const key = elem.key - const newRootMidi = key.midiForDegree(0) + commandData.pitchDelta - changes.key = new Theory.Key(key.nameForMidi(newRootMidi).simplified, key.scale) - } - - state = { ...state, project: state.project.update(elem.withChanges(changes)) } - } - - return state - } - - - static knobRectForMeterChange(state, time) - { - const x = (time.asFloat() - state.timeScroll) * state.timeScale - - return new Rect(x - markerWidth / 2, meterMarkerY, markerWidth, meterMarkerHeight) - } - - - static knobRectForKeyChange(state, time) - { - const x = (time.asFloat() - state.timeScroll) * state.timeScale - - return new Rect(x - markerWidth / 2, keyMarkerY, markerWidth, keyMarkerHeight) - } - - - static render(state, trackIndex, ctx) - { - const visibleTimeRange = Editor.visibleTimeRange(state) - - for (const keyCh of state.project.keyChanges.iterAtRange(visibleTimeRange)) - TrackMarkers.renderKeyChange(state, trackIndex, ctx, keyCh) - - for (const meterCh of state.project.meterChanges.iterAtRange(visibleTimeRange)) - TrackMarkers.renderMeterChange(state, trackIndex, ctx, meterCh) - - if (state.tracks[trackIndex].draw) - { - ctx.globalAlpha = 0.6 - - if (state.tracks[trackIndex].draw.kind == "key") - TrackMarkers.renderKeyChangeKnob(state, trackIndex, ctx, state.tracks[trackIndex].draw.time) - - else if (state.tracks[trackIndex].draw.kind == "meter") - TrackMarkers.renderMeterChangeKnob(state, trackIndex, ctx, state.tracks[trackIndex].draw.time) - - ctx.globalAlpha = 1 - } - } - - - static renderMeterChangeKnob(state, trackIndex, ctx, time) - { - const rect = TrackMarkers.knobRectForMeterChange(state, time) - - ctx.fillStyle = state.prefs.meterChangeColor - - ctx.beginPath() - ctx.arc(rect.x + rect.w / 2, rect.y + rect.h / 2, rect.w / 2, 0, Math.PI * 2) - ctx.fill() - } - - - static renderMeterChange(state, trackIndex, ctx, meterCh) - { - const rect = TrackMarkers.knobRectForMeterChange(state, meterCh.time) - - ctx.fillStyle = state.prefs.meterChangeColor - - ctx.beginPath() - ctx.arc(rect.x + rect.w / 2, rect.y + rect.h / 2, rect.w / 2, 0, Math.PI * 2) - ctx.fill() - - ctx.font = "14px Verdana" - ctx.textAlign = "left" - ctx.textBaseline = "middle" - ctx.fillText( - meterCh.meter.numerator.toString() + " / " + meterCh.meter.denominator.toString(), - rect.x + rect.w + 5, - rect.y + rect.h / 2) - - if (state.tracks[trackIndex].selection.has(meterCh.id)) - { - ctx.globalAlpha = 0.75 - ctx.fillStyle = "#fff" - - ctx.beginPath() - ctx.arc(rect.x + rect.w / 2, rect.y + rect.h / 2, rect.w / 2 - 3, 0, Math.PI * 2) - ctx.fill() - - ctx.globalAlpha = 1 - } - - const hover = state.tracks[trackIndex].hover - if (hover && hover.id === meterCh.id) - { - ctx.globalAlpha = 0.5 - ctx.fillStyle = "#fff" - - ctx.beginPath() - ctx.arc(rect.x + rect.w / 2, rect.y + rect.h / 2, rect.w / 2, 0, Math.PI * 2) - ctx.fill() - - ctx.globalAlpha = 1 - } - } - - - static renderKeyChangeKnob(state, trackIndex, ctx, time) - { - const rect = TrackMarkers.knobRectForKeyChange(state, time) - - ctx.fillStyle = state.prefs.keyChangeColor - - ctx.beginPath() - ctx.arc(rect.x + rect.w / 2, rect.y + rect.h / 2, rect.w / 2, 0, Math.PI * 2) - ctx.fill() - } - - - static renderKeyChange(state, trackIndex, ctx, keyCh) - { - const rect = TrackMarkers.knobRectForKeyChange(state, keyCh.time) - - ctx.fillStyle = state.prefs.keyChangeColor - - ctx.beginPath() - ctx.arc(rect.x + rect.w / 2, rect.y + rect.h / 2, rect.w / 2, 0, Math.PI * 2) - ctx.fill() - - ctx.font = "14px Verdana" - ctx.textAlign = "left" - ctx.textBaseline = "middle" - ctx.fillText(keyCh.key.str, rect.x + rect.w + 5, rect.y + rect.h / 2) - - if (state.tracks[trackIndex].selection.has(keyCh.id)) - { - ctx.globalAlpha = 0.75 - ctx.fillStyle = "#fff" - - ctx.beginPath() - ctx.arc(rect.x + rect.w / 2, rect.y + rect.h / 2, rect.w / 2 - 3, 0, Math.PI * 2) - ctx.fill() - - ctx.globalAlpha = 1 - } - - const hover = state.tracks[trackIndex].hover - if (hover && hover.id === keyCh.id) - { - ctx.globalAlpha = 0.5 - ctx.fillStyle = "#fff" - - ctx.beginPath() - ctx.arc(rect.x + rect.w / 2, rect.y + rect.h / 2, rect.w / 2, 0, Math.PI * 2) - ctx.fill() - - ctx.globalAlpha = 1 - } - } -} \ No newline at end of file diff --git a/src_old/editor/trackNotes.js b/src_old/editor/trackNotes.js deleted file mode 100644 index 8e4472b..0000000 --- a/src_old/editor/trackNotes.js +++ /dev/null @@ -1,591 +0,0 @@ -import Project from "../project/project.js" -import Editor from "./editor.js" -import Track from "./track.js" -import Rational from "../util/rational.js" -import Range from "../util/range.js" -import Rect from "../util/rect.js" -import Theory from "../theory.ts" -import CanvasUtils from "../util/canvas.js" - - -export default class TrackNotes -{ - static init(state, trackIndex, action) - { - const newTrack = - { - ...Track.init(state, action), - kind: "notes", - rowScale: 15, - } - - return { ...state, - tracks: [ ...state.tracks, newTrack ], - } - } - - - static updateHover(state, trackIndex, input) - { - let hover = null - let draw = null - - if (state.keys[Editor.keyDraw]) - { - const keyCh = state.project.keyChanges.findActiveAt(input.mouse.time) - const key = keyCh ? keyCh.key : Editor.defaultKey() - const row = TrackNotes.rowAtY(state, trackIndex, input.mouse.y, key) - const pitch = TrackNotes.pitchForRow(state, trackIndex, row, key) - draw = - { - kind: "note", - time1: input.mouse.time, - time2: input.mouse.time.add(state.timeSnap.multiply(new Rational(4))), - pitch, - } - } - else - { - const margin = 10 - const checkRange = Editor.timeRangeAtX(state, input.mouse.x - margin, input.mouse.x + margin) - - let hoverPrimary = null - let hoverSecondary = null - - for (const [note, keyCh, xMin, xMax] of TrackNotes.iterNotesAndKeyChangesAtRange(state, trackIndex, checkRange)) - { - const margin = 2 - - const row = TrackNotes.rowForPitch(state, trackIndex, note.pitch, keyCh.key) - const rectExact = TrackNotes.rectForNote(state, trackIndex, note.range, row, xMin, xMax) - const rect = new Rect(rectExact.x - margin, rectExact.y, rectExact.w + margin * 2, rectExact.h) - - const dragMarginOut = Math.abs(Math.min(0, rect.w - 16)) - const stretchMarginOut = 16 - const stretchMarginIn = Math.max(0, Math.min(10, rect.w / 4)) - const rectDrag = new Rect(rect.x - dragMarginOut, rect.y, rect.w + dragMarginOut * 2, rect.h) - const rectStretchStart = new Rect(rect.x - stretchMarginOut, rect.y, stretchMarginOut + stretchMarginIn, rect.h) - const rectStretchEnd = new Rect(rect.x2 - stretchMarginIn, rect.y, stretchMarginOut + stretchMarginIn, rect.h) - - const exactHover = rect.contains(input.mouse) - - let action = 0 - if (dragMarginOut > 0 && rectDrag.contains(input.mouse)) - action = Editor.actionDragTime | Editor.actionDragPitchRow - else if (rectStretchStart.contains(input.mouse) && !rectExact.cutStart) - action = Editor.actionStretchTimeStart - else if (rectStretchEnd.contains(input.mouse) && !rectExact.cutEnd) - action = Editor.actionStretchTimeEnd - else if (exactHover) - action = Editor.actionDragTime | Editor.actionDragPitchRow - - const hoverNote = - { - id: note.id, - range: note.range, - action, - } - - if (action != 0) - { - if (exactHover) - hoverPrimary = hoverNote - else - hoverSecondary = hoverNote - } - - if (hoverPrimary) - break - } - - hover = hoverPrimary || hoverSecondary - } - - return Track.update(state, trackIndex, { hover, draw }) - } - - - static hoveredPlaySoundPreview(state, trackIndex) - { - const hover = state.tracks[trackIndex].hover - if (!hover) - return state - - const id = hover.id - const note = state.project.notes.findById(id) - - if (!note) - return state - - return Editor.soundPreviewSet(state, TrackNotes.mergeSoundPreview(null, note.pitch)) - } - - - static selectionAddAtCursor(state, trackIndex) - { - const timeRange = new Range(state.cursor.time1, state.cursor.time2, false, false).sorted() - - for (const note of state.project.notes.iterAtRange(timeRange)) - state = Track.selectionAdd(state, trackIndex, note.id) - - return state - } - - - static selectionAddAtRect(state, trackIndex) - { - const timeRange = new Range(state.rectSelection.time1, state.rectSelection.time2, false, false).sorted() - const y1 = Math.min(state.rectSelection.y1, state.rectSelection.y2) - const y2 = Math.max(state.rectSelection.y1, state.rectSelection.y2) - - for (const [note, keyCh, xMin, xMax] of TrackNotes.iterNotesAndKeyChangesAtRange(state, trackIndex, timeRange)) - { - const row = TrackNotes.rowForPitch(state, trackIndex, note.pitch, keyCh.key) - const rect = TrackNotes.rectForNote(state, trackIndex, note.range, row, xMin, xMax) - if (y1 < rect.y2 && y2 > rect.y1) - state = Track.selectionAdd(state, trackIndex, note.id) - } - - return state - } - - - static selectionRemoveConflictingBehind(state, trackIndex) - { - const selection = state.tracks[trackIndex].selection - - for (const id of selection) - { - const elem = state.project.findById(id) - if (!elem) - continue - - for (const note of state.project.notes.iterAtRange(elem.range)) - { - if (selection.has(note.id)) - continue - - if (note.pitch !== elem.pitch) - continue - - state = { ...state, project: state.project.removeById(note.id) } - - for (const slice of note.range.iterSlices(elem.range)) - { - const newNote = new Project.Note(slice, note.pitch) - state = { ...state, project: state.project.upsertNote(newNote) } - } - } - } - - return state - } - - - static clipboardPaste(state, trackIndex, pasteData) - { - for (const [clipboardId, elem] of state.clipboard.elems) - { - if (elem instanceof Project.Note) - { - const range = elem.range.displace(pasteData.time.subtract(state.clipboard.range.start)) - const note = new Project.Note(range, elem.pitch) - const id = state.project.nextId - state = { ...state, project: state.project.upsertNote(note) } - state = Track.selectionAdd(state, trackIndex, id) - } - } - - return state - } - - - static previousAnchor(state, trackIndex, time) - { - return state.project.notes.findPreviousAnchor(time) - } - - - static deleteRange(state, trackIndex, range) - { - for (const note of state.project.notes.iterAtRange(range)) - { - state = { ...state, project: state.project.removeById(note.id) } - - for (const slice of note.range.iterSlices(range)) - { - const newNote = new Project.Note(slice, note.pitch) - state = { ...state, project: state.project.upsertNote(newNote) } - } - } - - return state - } - - - static pan(state, trackIndex) - { - const pitchRowDelta = -(state.mouse.drag.yDelta / state.tracks[trackIndex].rowScale) - - const yScroll = state.tracks[trackIndex].dragData.yScroll + pitchRowDelta - state = Track.update(state, trackIndex, { yScroll }) - - return state - } - - - static drag(state, trackIndex) - { - const pitchRowDelta = -(state.mouse.drag.yDelta / state.tracks[trackIndex].rowScale) - - let soundPreview = null - - for (const id of state.tracks[trackIndex].selection) - { - const elem = state.tracks[trackIndex].dragData.elems.get(id) - if (!elem) - continue - - let changes = {} - - if (state.mouse.action & Editor.actionDragTime) - changes.range = elem.range.displace(state.mouse.drag.timeDelta) - - if (state.mouse.action & Editor.actionStretchTimeStart) - { - changes.range = elem.range.stretch(state.mouse.drag.timeDelta, state.mouse.dragOrig.range.end, state.mouse.dragOrig.range.start) - if (elem.range.start.compare(state.mouse.dragOrig.range.start) == 0) - changes.range = new Range(changes.range.start.snap(state.timeSnap), changes.range.end) - - changes.range = changes.range.sorted() - } - - if (state.mouse.action & Editor.actionStretchTimeEnd) - { - changes.range = elem.range.stretch(state.mouse.drag.timeDelta, state.mouse.dragOrig.range.start, state.mouse.dragOrig.range.end) - if (elem.range.end.compare(state.mouse.dragOrig.range.end) == 0) - changes.range = new Range(changes.range.start, changes.range.end.snap(state.timeSnap)) - - changes.range = changes.range.sorted() - } - - if (state.mouse.action & Editor.actionDragPitchRow) - { - const keyCh = state.project.keyChanges.findActiveAt(elem.range.start) - const key = keyCh ? keyCh.key : Editor.defaultKey() - const degree = key.octavedDegreeForMidi(elem.pitch) - const newPitch = key.midiForDegree(Math.floor(degree + pitchRowDelta)) - changes.pitch = newPitch - soundPreview = TrackNotes.mergeSoundPreview(soundPreview, newPitch) - } - - state = { ...state, project: state.project.update(elem.withChanges(changes)) } - } - - state = Editor.soundPreviewSet(state, soundPreview) - return state - } - - - static drawMove(state, trackIndex, input) - { - let draw = state.tracks[trackIndex].draw - - if (draw) - { - let time2 = input.mouse.time - if (Math.abs(Editor.xAtTime(state, draw.time1) - Editor.xAtTime(state, time2)) < 5) - time2 = draw.time1.add(state.timeSnap.multiply(new Rational(4))) - - draw = { ...draw, time2 } - } - - return Track.update(state, trackIndex, { draw }) - } - - - static drawEnd(state, trackIndex, input) - { - const draw = state.tracks[trackIndex].draw - if (draw) - { - if (draw.kind == "note") - { - const note = new Project.Note(new Range(draw.time1, draw.time2).sorted(), draw.pitch) - const id = state.project.nextId - state = { ...state, project: state.project.upsertNote(note) } - state = Track.selectionAdd(state, trackIndex, id) - state = Editor.soundPreviewSet(state, TrackNotes.mergeSoundPreview(null, draw.pitch)) - } - } - - return state - } - - - static keyCommandTimeShift(state, trackIndex, commandData) - { - for (const id of state.tracks[trackIndex].selection) - { - const elem = state.project.findById(id) - if (!elem) - continue - - let changes = {} - changes.range = elem.range.displace(commandData.timeDelta) - - state = { ...state, project: state.project.update(elem.withChanges(changes)) } - } - - return state - } - - - static keyCommandPitchShift(state, trackIndex, commandData) - { - let soundPreview = null - - for (const id of state.tracks[trackIndex].selection) - { - const elem = state.project.findById(id) - if (!elem) - continue - - let changes = {} - if (commandData.diatonic) - { - const keyCh = state.project.keyChanges.findActiveAt(elem.range.start) - const key = keyCh ? keyCh.key : Editor.defaultKey() - const degree = key.octavedDegreeForMidi(elem.pitch) - const newDegree = degree + commandData.degreeDelta - const newPitch = key.midiForDegree(commandData.degreeDelta >= 0 ? Math.floor(newDegree) : Math.ceil(newDegree)) - changes.pitch = newPitch - } - else - changes.pitch = elem.pitch + commandData.pitchDelta - - soundPreview = TrackNotes.mergeSoundPreview(soundPreview, changes.pitch) - state = { ...state, project: state.project.update(elem.withChanges(changes)) } - } - - state = Editor.soundPreviewSet(state, soundPreview) - return state - } - - - static yForRow(state, trackIndex, row) - { - const track = state.tracks[trackIndex] - return track.rect.h / 2 - (row + 1 + track.yScroll) * track.rowScale - } - - - static rowAtY(state, trackIndex, y, key) - { - const track = state.tracks[trackIndex] - return Math.floor((track.rect.h / 2 - y) / track.rowScale - track.yScroll) - } - - - static rowForPitch(state, trackIndex, pitch, key) - { - const tonicRowOffset = Theory.Utils.chromaToDegreeInCMajor(key.tonic.chroma) - return key.octavedDegreeForMidi(pitch - 60) + tonicRowOffset - } - - - static pitchForRow(state, trackIndex, row, key) - { - const tonicRowOffset = Theory.Utils.chromaToDegreeInCMajor(key.tonic.chroma) - return key.midiForDegree(row - Math.floor(tonicRowOffset)) + 60 - } - - - static mergeSoundPreview(soundPreview, pitch) - { - if (soundPreview && soundPreview.pitch >= pitch) - return soundPreview - - return { kind: "note", pitch } - } - - - static rectForNote(state, trackIndex, range, row, xStart, xEnd) - { - const track = state.tracks[trackIndex] - - const noteOrigX1 = Editor.xAtTime(state, range.start) - const noteOrigX2 = Editor.xAtTime(state, range.end) - - const noteY = TrackNotes.yForRow(state, trackIndex, row) - - let noteX1 = Math.max(noteOrigX1, xStart) - let noteX2 = Math.min(noteOrigX2, xEnd) - - const cutStart = noteOrigX1 < noteX1 - const cutEnd = noteOrigX2 > noteX2 - - if (!cutStart) noteX1 += 1 - if (!cutEnd) noteX2 -= 1 - - const noteW = Math.max(2, noteX2 - noteX1) - - return Object.assign(new Rect(noteX1, noteY, noteW, track.rowScale), { cutStart, cutEnd }) - } - - - static *iterKeyChangesAtRange(state, trackIndex, range) - { - const defaultKey = Editor.defaultKey() - - for (const pair of state.project.keyChanges.iterActiveAtRangePairwise(range)) - { - const keyCh1 = pair[0] || new Project.KeyChange(range.start, defaultKey) - const keyCh2 = pair[1] || new Project.KeyChange(range.end, defaultKey) - - const keyCh1X = Editor.xAtTime(state, keyCh1.time) - const keyCh2X = Editor.xAtTime(state, keyCh2.time) - - yield [keyCh1, keyCh1X, keyCh2X] - } - } - - - static *iterNotesAndKeyChangesAtRange(state, trackIndex, range) - { - const defaultKey = Editor.defaultKey() - - for (const pair of state.project.keyChanges.iterActiveAtRangePairwise(range)) - { - const keyCh1 = pair[0] || new Project.KeyChange(range.start, defaultKey) - const keyCh2 = pair[1] || new Project.KeyChange(range.end, defaultKey) - - const keyCh1X = Editor.xAtTime(state, keyCh1.time) - const keyCh2X = Editor.xAtTime(state, keyCh2.time) - - const time1 = keyCh1.time.max(range.start) - const time2 = keyCh2.time.min(range.end) - - for (const note of state.project.notes.iterAtRange(new Range(time1, time2))) - yield [note, keyCh1, keyCh1X, keyCh2X] - } - } - - - static render(state, trackIndex, ctx) - { - const track = state.tracks[trackIndex] - const visibleTimeRange = Editor.visibleTimeRange(state) - - for (const [keyCh, xMin, xMax] of TrackNotes.iterKeyChangesAtRange(state, trackIndex, visibleTimeRange)) - { - const tonicRowOffset = Theory.Utils.chromaToDegreeInCMajor(keyCh.key.tonic.chroma) - - const rowAtTop = TrackNotes.rowAtY(state, trackIndex, 0) - const rowAtBottom = TrackNotes.rowAtY(state, trackIndex, track.rect.h) - - const octaveAtTop = Math.ceil(rowAtTop / 7) + 1 - const octaveAtBottom = Math.floor(rowAtBottom / 7) - 1 - - for (let i = octaveAtBottom; i <= octaveAtTop; i++) - { - const y = TrackNotes.yForRow(state, trackIndex, tonicRowOffset + i * 7) + track.rowScale - - ctx.strokeStyle = "#000" - ctx.beginPath() - ctx.moveTo(xMin, y) - ctx.lineTo(xMax, y) - ctx.stroke() - - for (let j = 2; j < 7; j += 2) - { - const ySuboctave = TrackNotes.yForRow(state, trackIndex, tonicRowOffset + i * 7 + j) + track.rowScale - - ctx.globalAlpha = 0.025 - ctx.fillStyle = "#000" - ctx.fillRect(xMin, ySuboctave, xMax - xMin, track.rowScale) - ctx.globalAlpha = 1 - } - - if (i == 0) - { - ctx.globalAlpha = 0.05 - ctx.fillStyle = "#fff" - ctx.fillRect(xMin, y - 7 * track.rowScale, xMax - xMin, 7 * track.rowScale) - ctx.globalAlpha = 1 - } - } - } - - for (const [note, keyCh, xMin, xMax] of TrackNotes.iterNotesAndKeyChangesAtRange(state, trackIndex, visibleTimeRange)) - { - if (!state.playback.playing && track.selection.has(note.id)) - continue - - const row = TrackNotes.rowForPitch(state, trackIndex, note.pitch, keyCh.key) - const mode = keyCh.key.scale.metadata.mode - const fillStyle = CanvasUtils.fillStyleForDegree(ctx, keyCh.key.degreeForMidi(note.pitch) + mode) - const hovering = track.hover && track.hover.id == note.id - const playing = state.playback.playing && note.range.overlapsPoint(state.playback.time) - TrackNotes.renderNote(state, trackIndex, ctx, note.range, row, xMin, xMax, fillStyle, hovering, false, playing) - } - - if (!state.playback.playing) - { - for (const [note, keyCh, xMin, xMax] of TrackNotes.iterNotesAndKeyChangesAtRange(state, trackIndex, visibleTimeRange)) - { - if (!track.selection.has(note.id)) - continue - - const row = TrackNotes.rowForPitch(state, trackIndex, note.pitch, keyCh.key) - const mode = keyCh.key.scale.metadata.mode - const fillStyle = CanvasUtils.fillStyleForDegree(ctx, keyCh.key.degreeForMidi(note.pitch) + mode) - const hovering = track.hover && track.hover.id == note.id - TrackNotes.renderNote(state, trackIndex, ctx, note.range, row, xMin, xMax, fillStyle, hovering, true, false) - } - } - - const draw = state.tracks[trackIndex].draw - if (draw) - { - ctx.globalAlpha = 0.6 - - const keyCh = state.project.keyChanges.findActiveAt(draw.time1) - const key = keyCh ? keyCh.key : Editor.defaultKey() - const row = TrackNotes.rowForPitch(state, trackIndex, draw.pitch, key) - const mode = key.scale.metadata.mode - const fillStyle = CanvasUtils.fillStyleForDegree(ctx, key.degreeForMidi(draw.pitch) + mode) - TrackNotes.renderNote(state, trackIndex, ctx, new Range(draw.time1, draw.time2).sorted(), row, -Infinity, Infinity, fillStyle) - - ctx.globalAlpha = 1 - } - } - - - static renderNote(state, trackIndex, ctx, range, row, xMin, xMax, fillStyle, hovering, selected, playing) - { - const rect = TrackNotes.rectForNote(state, trackIndex, range, row, xMin, xMax) - - ctx.fillStyle = fillStyle - - ctx.beginPath() - ctx.fillRect(rect.x, rect.y, rect.w, rect.h) - - if (hovering) - { - ctx.globalAlpha = 0.4 - ctx.fillStyle = "#fff" - ctx.fillRect(rect.x, rect.y, rect.w, rect.h) - ctx.globalAlpha = 1 - } - - if (selected || playing) - { - const margin = 3 - ctx.globalAlpha = 0.6 - ctx.fillStyle = "#fff" - ctx.fillRect(rect.x, rect.y + margin, rect.w, rect.h - margin * 2) - ctx.globalAlpha = 1 - } - } -} \ No newline at end of file diff --git a/src_old/editor2/EditorContent.tsx b/src_old/editor2/EditorContent.tsx deleted file mode 100644 index a8d0e9e..0000000 --- a/src_old/editor2/EditorContent.tsx +++ /dev/null @@ -1,228 +0,0 @@ -import React from "react" -import Editor from "./editor" -import EditorState from "./editorState" -import { AppState, AppReducer, ContentManager } from "../AppState" -import { useAppManager } from "../AppContext" -import Rect from "../util/rect" - - -interface EditorContentProps -{ - contentId: number - rect: Rect -} - - -export function EditorContent(props: EditorContentProps) -{ - const appManager = useAppManager() - const contentCtx = appManager.makeContentManager(props.contentId) - - const refDiv = React.useRef(null) - const refCanvas = React.useRef(null) - const needsResetView = React.useRef(true) - - const transformMousePos = (canvas: HTMLCanvasElement, ev: MouseEvent) => - { - const rect = canvas.getBoundingClientRect() - return { - x: (ev.clientX - rect.left), - y: (ev.clientY - rect.top) - } - } - - const resize = (contentCtx: ContentManager) => - { - if (!refDiv.current) - return - - const rect = refDiv.current!.getBoundingClientRect() - const x = Math.floor(rect.x) - const y = Math.floor(rect.y) - const w = Math.floor(rect.width) - const h = Math.floor(rect.height) - - refCanvas.current!.style.width = w + "px" - refCanvas.current!.style.height = h + "px" - refCanvas.current!.width = w - refCanvas.current!.height = h - - Editor.resize(contentCtx, { x, y, w, h }) - Editor.tracksRefresh(contentCtx) - } - - React.useEffect(() => - { - resize(contentCtx) - contentCtx.dispatch() - - }, [props.rect]) - - React.useEffect(() => - { - if (!refCanvas.current) - return - - const refresh = () => - { - if (!contentCtx.contentState || contentCtx.contentState.tracks.length == 0) - resize(contentCtx) - } - - const preventDefault = (ev: Event) => ev.preventDefault() - - const onMouseMove = (ev: MouseEvent) => - { - refresh() - const pos = transformMousePos(refCanvas.current!, ev) - Editor.reduce_mouseMove(contentCtx, { pos }) - contentCtx.dispatch() - } - - const onMouseDown = (ev: MouseEvent) => - { - //if (window.document.activeElement) - // window.document.activeElement.blur() - - refresh() - ev.preventDefault() - const pos = transformMousePos(refCanvas.current!, ev) - - Editor.reduce_mouseMove(contentCtx, { pos }) - Editor.reduce_mouseDown(contentCtx, { rightButton: ev.button != 0, ctrlKey: ev.ctrlKey }) - contentCtx.dispatch() - } - - const onMouseUp = (ev: MouseEvent) => - { - refresh() - const pos = transformMousePos(refCanvas.current!, ev) - Editor.reduce_mouseMove(contentCtx, { pos }) - Editor.reduce_mouseUp(contentCtx, { }) - Editor.reduce_mouseMove(contentCtx, { pos }) - contentCtx.dispatch() - } - - const onMouseWheel = (ev: MouseWheelEvent) => - { - refresh() - ev.preventDefault() - Editor.reduce_mouseWheel(contentCtx, { deltaX: ev.deltaX, deltaY: ev.deltaY }) - contentCtx.dispatch() - } - - const onKeyDown = (ev: KeyboardEvent) => - { - refresh() - Editor.reduce_keyDown(contentCtx, { key: ev.key.toLowerCase() }) - Editor.reduce_keyCommand(contentCtx, { - ev, - key: ev.key.toLowerCase(), - ctrlKey: ev.ctrlKey, - shiftKey: ev.shiftKey, - }) - contentCtx.dispatch() - } - - const onKeyUp = (ev: KeyboardEvent) => - { - refresh() - Editor.reduce_keyUp(contentCtx, { key: ev.key.toLowerCase() }) - contentCtx.dispatch() - } - - window.addEventListener("mousemove", onMouseMove) - window.addEventListener("mouseup", onMouseUp) - window.addEventListener("keydown", onKeyDown) - window.addEventListener("keyup", onKeyUp) - - refCanvas.current!.addEventListener("mousedown", onMouseDown) - refCanvas.current!.addEventListener("wheel", onMouseWheel) - refCanvas.current!.addEventListener("mouseup", preventDefault) - refCanvas.current!.addEventListener("contextmenu", preventDefault) - - return () => - { - window.removeEventListener("mousemove", onMouseMove) - window.removeEventListener("mouseup", onMouseUp) - window.removeEventListener("keydown", onKeyDown) - window.removeEventListener("keyup", onKeyUp) - - if (!refCanvas.current) - return - - refCanvas.current!.removeEventListener("mousedown", onMouseDown) - refCanvas.current!.removeEventListener("wheel", onMouseWheel) - refCanvas.current!.removeEventListener("mouseup", preventDefault) - refCanvas.current!.removeEventListener("contextmenu", preventDefault) - } - - }, [refCanvas.current, props.contentId]) - - React.useEffect(() => - { - if (!refCanvas.current) - return - - Editor.render(contentCtx, refCanvas.current!.getContext("2d")!) - - const mouseAction = - contentCtx.contentState.mouse.action || - (contentCtx.contentState.mouse.hover && contentCtx.contentState.mouse.hover.action)! - - if (contentCtx.contentState.tracks.some((tr: any) => !!tr.draw) || mouseAction & (Editor.actionDraw)) - refCanvas.current!.style.cursor = "crosshair" - else if (mouseAction & (Editor.actionDragTime)) - refCanvas.current!.style.cursor = (contentCtx.contentState.mouse.down ? "grabbing" : "grab") - else if (mouseAction & (Editor.actionPan)) - refCanvas.current!.style.cursor = "move" - else if (mouseAction & (Editor.actionStretchTimeStart | Editor.actionStretchTimeEnd)) - refCanvas.current!.style.cursor = "col-resize" - else - refCanvas.current!.style.cursor = "text" - - }, [refCanvas.current, contentCtx.appState]) - - React.useEffect(() => - { - if (!contentCtx.contentState || contentCtx.contentState.tracks.length == 0) - { - window.requestAnimationFrame(() => - { - resize(contentCtx) - contentCtx.dispatch() - }) - return - } - - }, [props.contentId]) - - - return ( -
- - - { contentCtx.contentState.tracks.map((track, i) => -
- Track Name -
- )} -
- ) -} \ No newline at end of file diff --git a/src_old/editor2/InspectorContent.tsx b/src_old/editor2/InspectorContent.tsx deleted file mode 100644 index f62d6d4..0000000 --- a/src_old/editor2/InspectorContent.tsx +++ /dev/null @@ -1,195 +0,0 @@ -import React from "react" -import ButtonList from "../toolbox/ButtonList" -import DropdownMenu from "../toolbox/DropdownMenu" -import Editor from "./editor" -import { AppState } from "../AppState" -import Rect from "../util/rect" -import Project from "../project/project2" -import * as Theory from "../theory/theory" -import { useAppManager } from "../AppContext" - - -interface InspectorContentProps -{ - contentId: number - rect: Rect -} - - -interface InspectorState -{ - elemIds: Project.ID[] -} - - -export default function InspectorContent(props: InspectorContentProps) -{ - const appManager = useAppManager() - const contentCtx = appManager.makeContentManager(props.contentId) - - - const elem = contentCtx.appState.project.elems.get(contentCtx.contentState.elemIds[0]) - if (!elem) - return null - - - const keyCh = elem as Project.KeyChange - const key = keyCh.key - - - const chromaticOptions = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11].map(chroma => - { - chroma = Theory.Utils.mod(chroma + key.tonic.chroma, 12) - return { - label: key.nameForChroma(chroma).str, - value: chroma, - bkgColor: Theory.Utils.degreeToColor(key.degreeForChroma(chroma)), - width: "2em", - } - }) - - - const chromaticSelected = [0, 1, 2, 3, 4, 5, 6].map(degree => key.chromaForDegree(degree)) - - - const tonicLetterOptions = [0, 4, 1, 5, 2, 6, 3].map(letter => - { - return { - label: Theory.Utils.letterToStr(letter), - value: letter, - } - }) - - - const tonicAccidentalOptions = [-1, 0, 1].map(acc => - { - return { - label: acc == 0 ? "♮" : Theory.Utils.accidentalToStr(acc), - value: acc, - } - }) - - - const scaleOptions = Theory.Scale.list.map(scale => - { - return { - label: scale.names[0], - value: scale.id, - } - }) - - - const onChangeTonicLetter = (item: any) => - { - const tonic = new Theory.PitchName(item.value, key.tonic.accidental) - const newKey = new Theory.Key(tonic, key.scale) - changeKey(newKey) - } - - - const onChangeTonicAccidental = (item: any) => - { - const tonic = new Theory.PitchName(key.tonic.letter, item.value) - const newKey = new Theory.Key(tonic, key.scale) - changeKey(newKey) - } - - - const onChangeScale = (item: any) => - { - const scale = Theory.Scale.fromId(item.value) - const newKey = new Theory.Key(key.tonic, scale) - changeKey(newKey) - } - - - const changeKey = (newKey: Theory.Key) => - { - const newKeyCh = Project.Element.withChanges(keyCh, { key: newKey }) - const project = Project.upsertTimedElement(appManager.appState.project, newKeyCh) - appManager.mergeAppState({ project }) - appManager.dispatch() - } - - - return
- -
- [Key Change]
- { (elem as Project.KeyChange).key.str } -
- -
-
- Tonic -
- - - -
- Accidental -
- - - -
- Scale -
- - - -
-
- -
- Pitches -
- - {} } - /> - -
- -
-} \ No newline at end of file diff --git a/src_old/editor2/TrackInstrumentContent.tsx b/src_old/editor2/TrackInstrumentContent.tsx deleted file mode 100644 index 55612bd..0000000 --- a/src_old/editor2/TrackInstrumentContent.tsx +++ /dev/null @@ -1,159 +0,0 @@ -import React from "react" -import ButtonList from "../toolbox/ButtonList" -import DropdownMenu from "../toolbox/DropdownMenu" -import Editor from "./editor" -import { AppState } from "../AppState" -import Rect from "../util/rect" -import Project from "../project/project2" -import * as Theory from "../theory/theory" -import { useAppManager } from "../AppContext" -import ListBox from "../toolbox/ListBox" - - -interface TrackInstrumentContentProps -{ - contentId: number - rect: Rect -} - - -interface TrackInstrumentContentState -{ - elemIds: Project.ID[] -} - - -function getMidiIcon(midiBank: number, midiPreset: number): string -{ - if (midiBank == 128) // Percussion - return "đŸĨ" - - if (midiPreset <= 7) // Piano - return "🎹" - else if (midiPreset <= 15) // Chromatic Percussion - return "🔔" - else if (midiPreset <= 23) // Organ - return "💨" - else if (midiPreset <= 31) // Guitar - return "🎸" - else if (midiPreset <= 39) // Bass - return "🎸" - else if (midiPreset <= 47) // Strings - return "đŸŽģ" - else if (midiPreset <= 55) // Ensemble - return "đŸŽģ" - else if (midiPreset <= 63) // Brass - return "đŸŽē" - else if (midiPreset <= 71) // Reed - return "🎷" - else if (midiPreset <= 79) // Pipe - return "âœī¸" - else if (midiPreset <= 87) // Synth Lead - return "đŸ•šī¸" - else if (midiPreset <= 95) // Synth Pad - return "đŸ•šī¸" - else if (midiPreset <= 103) // Synth FX - return "đŸ•šī¸" - else if (midiPreset <= 111) // Ethnic - return "đŸĒ•" - else if (midiPreset <= 119) // Percussive - return "đŸĨ" - else if (midiPreset <= 127) // Sound FX - return "🔊" - else - return "🎹" -} - - -export default function TrackInstrumentContent(props: TrackInstrumentContentProps) -{ - const appManager = useAppManager() - const contentCtx = appManager.makeContentManager(props.contentId) - - - const elem = contentCtx.appState.project.elems.get(contentCtx.contentState.elemIds[0]) - if (!elem) - return null - - const track = elem as Project.Track - if (track.trackType != Project.TrackType.Notes) - return null - - const trackNotes = track as Project.TrackNotes - const trackInstr = trackNotes.instrument as Project.TrackInstrumentSflib - const sflibMeta = appManager.appState.sflib - const sflibCurrentColl = sflibMeta.collections.find(coll => coll.id == trackInstr.collectionId)! - const sflibCurrentInstr = sflibCurrentColl.instruments.find(instr => instr.id == trackInstr.instrumentId)! - - - const onChange = (collectionId: string, instrumentId: string) => - { - const newTrack = Project.TrackNotes.withChanges(trackNotes, { - instrument: { - ...trackNotes.instrument, - collectionId, - instrumentId, - } - }) - - const newProject = Project.upsertTrack(contentCtx.appState.project, newTrack) - contentCtx.appManager.mergeAppState({ project: newProject }) - contentCtx.appManager.dispatch() - } - - - return
- -
- -
- Collection -
- -
- Instrument -
- - onChange(item.value, sflibMeta.collections.find(coll => coll.id == item.value)!.instruments[0].id) } - items={ sflibMeta.collections.map(coll => - { - return { - value: coll.id, - label: coll.name, - } - })} - /> - - onChange(sflibCurrentColl.id, item.value) } - items={ sflibCurrentColl.instruments.map(instr => - { - return { - value: instr.id, - label: - getMidiIcon(instr.midiBank, instr.midiPreset) + " " + - //instr.midiBank.toString().padStart(3, "0") + "." + - //instr.midiPreset.toString().padStart(3, "0") + " : " + - instr.name, - } - })} - /> - -
- -
-} \ No newline at end of file diff --git a/src_old/editor2/TrackPopup.tsx b/src_old/editor2/TrackPopup.tsx deleted file mode 100644 index 68d4a43..0000000 --- a/src_old/editor2/TrackPopup.tsx +++ /dev/null @@ -1,21 +0,0 @@ -import React from "react" -import PopupButton from "../popup/PopupButton" -import Rect from "../util/rect" - - -interface TrackPopupProps -{ -} - - -export default function TrackPopup(props: TrackPopupProps) -{ - return <> - - - -} \ No newline at end of file diff --git a/src_old/editor2/editor.ts b/src_old/editor2/editor.ts deleted file mode 100644 index 4c45da9..0000000 --- a/src_old/editor2/editor.ts +++ /dev/null @@ -1,1392 +0,0 @@ -import { ContentManager, AppReducer } from "../AppState" -import EditorState, { EditorMode } from "./editorState" -import Track from "./track" -import TrackState from "./trackState" -import TrackStateManager from "./trackStateManager" -import TrackKeyChanges from "./trackKeyChanges" -import TrackMeterChanges from "./trackMeterChanges" -import Project from "../project/project2" -import Rational from "../util/rational" -import Range from "../util/range" -import Rect from "../util/rect" -import * as Theory from "../theory/theory" -import DockableData from "../dockable/state" -import { stat } from "fs" -import TrackPopup from "./TrackPopup" - - -export default class Editor -{ - static actionPan = 0x01 - static actionSelectCursor = 0x02 - static actionSelectRect = 0x04 - static actionDraw = 0x08 - static actionDragTime = 0x10 - static actionDragPitchRow = 0x20 - static actionStretchTimeStart = 0x40 - static actionStretchTimeEnd = 0x80 - - - static makeNewFull(): EditorState - { - return { - mode: EditorMode.Full, - modeTracks: [], - - x: 0, - y: 0, - w: 0, - h: 0, - - trackHeaderW: 200, - - tracks: [], - trackScroll: 0, - - timeScroll: -0.5, - timeScale: 100, - timeSnap: new Rational(1, 16), - timeSnapBase: new Rational(1, 16), - - cursor: - { - visible: true, - time1: new Rational(0), - time2: new Rational(0), - track1: 0, - track2: 0, - }, - - rectCursor: - { - track: 0, - time1: new Rational(0), - time2: new Rational(0), - y1: 0, - y2: 0, - }, - - keys: {}, - - mouse: - { - down: false, - downDate: new Date(), - - action: 0, - - pos: { x: 0, y: 0 }, - posPrev: { x: 0, y: 0 }, - time: new Rational(0), - track: 0, - trackPos: { x: 0, y: 0 }, - trackYRaw: 0, - row: 0, - - hover: null, - - drag: { - xLocked: true, - yLocked: true, - - posOrigin: { x: 0, y: 0 }, - timeOrigin: new Rational(0), - timeScrollOrigin: 0, - rangeOrigin: new Range(new Rational(), new Rational()), - trackOrigin: 0, - trackScrollOrigin: 0, - trackPosOrigin: { x: 0, y: 0 }, - trackYRawOrigin: 0, - trackYScrollOrigin: 0, - rowOrigin: 0, - projectOrigin: Project.getDefault(), - - posDelta: { x: 0, y: 0 }, - timeDelta: new Rational(0), - trackPosDelta: { x: 0, y: 0 }, - rowDelta: 0, - }, - - wheelDate: new Date(), - }, - } - } - - - static makeNewNoteEditor(trackIds: number[]): EditorState - { - const state = Editor.makeNewFull() - state.mode = EditorMode.NoteEditor - state.modeTracks = trackIds - return state - } - - - static init(state: ContentManager) - { - state.contentState = Editor.makeNewFull() - } - - - static resize(state: ContentManager, action: any) - { - state.mergeContentState({ - x: action.x, - y: action.y, - w: action.w, - h: action.h, - }) - } - - - static tracksRefresh(state: ContentManager) - { - const tracks: TrackState[] = [] - - for (let t = 0; t < state.appState.project.tracks.length; t++) - { - const track = state.appState.project.tracks[t] - if (track.trackType == Project.TrackType.Notes) - { - if (state.contentState.mode == EditorMode.Full) - tracks.push({ - type: "notesPreview", - trackIndex: t, - trackId: track.id, - y: 0, - h: 40, - yScroll: 0, - pinned: false, - }) - else if (state.contentState.modeTracks.some(t => t == track.id)) - tracks.push({ - type: "notes", - trackIndex: t, - trackId: track.id, - y: 0, - h: 0, - yScroll: 0, - pinned: false, - }) - } - else if (track.trackType == Project.TrackType.KeyChanges) - tracks.push({ - type: "keyChanges", - trackIndex: t, - trackId: track.id, - y: 0, - h: TrackKeyChanges.knobHeight, - yScroll: 0, - pinned: false, - }) - else if (track.trackType == Project.TrackType.MeterChanges) - tracks.push({ - type: "meterChanges", - trackIndex: t, - trackId: track.id, - y: 0, - h: TrackMeterChanges.knobHeight, - yScroll: 0, - pinned: false, - }) - } - - let fixedH = 0 - for (let t = 0; t < tracks.length; t++) - { - if (tracks[t].h > 0) - fixedH += tracks[t].h - } - - for (let t = 0; t < tracks.length; t++) - { - if (tracks[t].h == 0) - tracks[t].h = state.contentState.h - fixedH - } - - let y = 0 - for (let t = 0; t < tracks.length; t++) - { - tracks[t].y = y - y += tracks[t].h - } - - state.mergeContentState({ tracks }) - - for (let t = 0; t < state.contentState.tracks.length; t++) - Track.execute("init", state, t) - } - - - static reduce_keyDown(state: ContentManager, action: any) - { - state.mergeContentState({ - keys: { ...state.contentState.keys, [action.key]: true }, - }) - } - - - static reduce_keyUp(state: ContentManager, action: any) - { - let keys = { ...state.contentState.keys } - delete keys[action.key] - - state.mergeContentState({ keys }) - } - - - static reduce_keyCommand(state: ContentManager, action: any) - { - - } - - - static reduce_mouseMove(state: ContentManager, action: any) - { - const track = Editor.trackAtY(state, state.contentState.mouse.pos.y) - const trackDrag = !state.contentState.mouse.down ? track : state.contentState.mouse.drag.trackOrigin - - const trackX = action.pos.x - state.contentState.trackHeaderW - const trackYRaw = state.contentState.mouse.pos.y - Editor.trackY(state, trackDrag) - const trackY = trackYRaw + state.contentState.tracks[trackDrag].yScroll - const trackPos = { x: trackX, y: trackY } - - const row = Track.execute("rowAtY", state, trackDrag, trackY) - - state.mergeContentState({ - mouse: { - ...state.contentState.mouse, - pos: action.pos, - posPrev: state.contentState.mouse.pos, - time: Editor.timeAtX(state, trackX), - track, - trackPos, - trackYRaw, - row, - } - }) - - if (!state.contentState.mouse.down) - { - state.mergeContentState({ - mouse: { - ...state.contentState.mouse, - hover: null, - } - }) - - if (state.contentState.keys[state.appState.prefs.editor.keyDraw]) - { - for (let t = 0; t < state.contentState.tracks.length; t++) - { - if (t == state.contentState.mouse.track) - Track.execute("drawHover", state, t) - else - Track.execute("drawClear", state, t) - } - } - else - { - for (let t = 0; t < state.contentState.tracks.length; t++) - { - Track.execute("drawClear", state, t) - - if (t == state.contentState.mouse.track) - Track.execute("hover", state, t) - } - } - } - else - { - const posDelta = { - x: state.contentState.mouse.pos.x - state.contentState.mouse.drag.posOrigin.x, - y: state.contentState.mouse.pos.y - state.contentState.mouse.drag.posOrigin.y, - } - - const timeDelta = - state.contentState.mouse.time.subtract(state.contentState.mouse.drag.timeOrigin) - - const trackPosDelta = { - x: state.contentState.mouse.trackPos.x - state.contentState.mouse.drag.trackPosOrigin.x, - y: state.contentState.mouse.trackPos.y - state.contentState.mouse.drag.trackPosOrigin.y, - } - - const rowDelta = state.contentState.mouse.row - state.contentState.mouse.drag.rowOrigin - - const xLocked = state.contentState.mouse.drag.xLocked && Math.abs(posDelta.x) < state.appState.prefs.editor.mouseDragXLockedDistance - const yLocked = state.contentState.mouse.drag.yLocked && Math.abs(posDelta.y) < state.appState.prefs.editor.mouseDragYLockedDistance - - state.mergeContentState({ - mouse: { - ...state.contentState.mouse, - drag: { - ...state.contentState.mouse.drag, - xLocked, - yLocked, - posDelta, - timeDelta, - trackPosDelta, - rowDelta, - }, - }, - }) - - if (state.contentState.mouse.action == Editor.actionPan) - { - state.mergeContentState({ - timeScroll: state.contentState.mouse.drag.timeScrollOrigin - state.contentState.mouse.drag.posDelta.x / state.contentState.timeScale, - }) - - if (state.contentState.mouse.drag.posOrigin.x > state.contentState.trackHeaderW && - Track.execute("yScrollEnabled", state, state.contentState.mouse.drag.trackOrigin)) - { - const trackState = new TrackStateManager(state, state.contentState.mouse.drag.trackOrigin) - const yScroll = - state.contentState.mouse.drag.trackYScrollOrigin - - state.contentState.mouse.drag.posDelta.y - - trackState.mergeTrackState({ yScroll }) - } - else - { - const lastTrack = state.contentState.tracks[state.contentState.tracks.length - 1] - const trackScroll = - Math.max(0, - Math.min( - lastTrack.y + lastTrack.h - state.contentState.h, - state.contentState.mouse.drag.trackScrollOrigin - state.contentState.mouse.drag.posDelta.y)) - - state.mergeContentState({ trackScroll }) - } - } - else if (state.contentState.mouse.action == Editor.actionSelectCursor) - { - Editor.cursorDrag(state, state.contentState.mouse.time, state.contentState.mouse.track) - Editor.selectionClear(state) - Editor.selectionAddAtCursor(state) - Editor.handleEdgeScroll(state, false) - } - else if (state.contentState.mouse.action == Editor.actionSelectRect) - { - state.mergeContentState({ - rectCursor: { ...state.contentState.rectCursor, - time2: state.contentState.mouse.time, - y2: state.contentState.mouse.trackPos.y, - }, - }) - - Editor.selectionClear(state) - Editor.selectionAddAtRectCursor(state) - Editor.handleEdgeScroll(state, true) - } - else if (state.contentState.mouse.action == Editor.actionDraw) - { - Track.execute("drawDrag", state, state.contentState.mouse.drag.trackOrigin) - Editor.handleEdgeScroll(state, false) - } - else - { - Editor.drag(state) - Editor.handleEdgeScroll(state, true) - } - } - } - - - static drag(state: ContentManager) - { - let mouseActionBlocked = 0 - if (state.contentState.mouse.drag.xLocked) - mouseActionBlocked |= Editor.actionDragTime | Editor.actionStretchTimeEnd | Editor.actionStretchTimeStart - - if (state.contentState.mouse.drag.yLocked) - mouseActionBlocked |= Editor.actionDragPitchRow - - const mouseAction = state.contentState.mouse.action & ~mouseActionBlocked - const mouseDrag = state.contentState.mouse.drag - - for (const id of state.appState.selection) - { - const elem = mouseDrag.projectOrigin.elems.get(id) - if (!elem) - continue - - let changes: any = {} - - if (elem.type == Project.ElementType.Note) - { - const rangedElem = elem as any as Project.RangedElement - - if (mouseAction & Editor.actionDragTime) - changes.range = rangedElem.range.displace(mouseDrag.timeDelta) - - if (mouseAction & Editor.actionStretchTimeStart) - { - changes.range = rangedElem.range.stretch(mouseDrag.timeDelta, mouseDrag.rangeOrigin.end, mouseDrag.rangeOrigin.start) - if (rangedElem.range.start.compare(mouseDrag.rangeOrigin.start) == 0) - changes.range = new Range(changes.range.start.snap(state.contentState.timeSnap), changes.range.end) - - changes.range = changes.range.sorted() - } - - if (mouseAction & Editor.actionStretchTimeEnd) - { - changes.range = rangedElem.range.stretch(mouseDrag.timeDelta, mouseDrag.rangeOrigin.start, mouseDrag.rangeOrigin.end) - if (rangedElem.range.end.compare(mouseDrag.rangeOrigin.end) == 0) - changes.range = new Range(changes.range.start, changes.range.end.snap(state.contentState.timeSnap)) - - changes.range = changes.range.sorted() - } - - if (mouseAction & Editor.actionDragPitchRow) - { - const note = rangedElem as Project.Note - const key = Editor.keyAt(state, state.contentState.tracks[state.contentState.mouse.drag.trackOrigin].trackId, rangedElem.range.start) - const degree = key.octavedDegreeForMidi(note.pitch) - const newPitch = key.midiForDegree(Math.floor(degree + mouseDrag.rowDelta)) - changes.pitch = newPitch - } - - const newRangedElem = Project.RangedElement.withChanges(rangedElem, changes) - - state.mergeAppState({ - project: Project.upsertRangedElement(state.appState.project, newRangedElem) - }) - } - - else if (elem.type == Project.ElementType.KeyChange || - elem.type == Project.ElementType.MeterChange) - { - const timedElem = elem as any as Project.TimedElement - - if (mouseAction & Editor.actionDragTime) - changes.time = timedElem.time.add(mouseDrag.timeDelta).snap(state.contentState.timeSnap) - - if (mouseAction & Editor.actionStretchTimeStart) - { - changes.time = timedElem.time.stretch(mouseDrag.timeDelta, mouseDrag.rangeOrigin.end, mouseDrag.rangeOrigin.start) - if (timedElem.time.compare(mouseDrag.rangeOrigin.start) == 0) - changes.time = changes.time.snap(state.contentState.timeSnap) - } - - if (mouseAction & Editor.actionStretchTimeEnd) - { - changes.time = timedElem.time.stretch(mouseDrag.timeDelta, mouseDrag.rangeOrigin.start, mouseDrag.rangeOrigin.end) - if (timedElem.time.compare(mouseDrag.rangeOrigin.start) == 0) - changes.time = changes.time.snap(state.contentState.timeSnap) - } - - const newTimedElem = Project.TimedElement.withChanges(timedElem, changes) - - state.mergeAppState({ - project: Project.upsertTimedElement(state.appState.project, newTimedElem) - }) - } - } - } - - - static reduce_mouseDown(state: ContentManager, action: any) - { - if (state.contentState.mouse.down) - return - - const now = new Date() - const timeSinceLastDown = now.getTime() - state.contentState.mouse.downDate.getTime() - const isDoubleClick = timeSinceLastDown < 250 - - state.mergeContentState({ - mouse: { ...state.contentState.mouse, - down: true, - downDate: now, - drag: { ...state.contentState.mouse.drag, - xLocked: true, - yLocked: true, - posOrigin: state.contentState.mouse.pos, - timeOrigin: state.contentState.mouse.time, - timeScrollOrigin: state.contentState.timeScroll, - trackScrollOrigin: state.contentState.trackScroll, - trackOrigin: state.contentState.mouse.track, - trackPosOrigin: state.contentState.mouse.trackPos, - trackYRawOrigin: state.contentState.mouse.trackYRaw, - trackYScrollOrigin: state.contentState.tracks[state.contentState.mouse.track].yScroll, - rowOrigin: state.contentState.mouse.row, - projectOrigin: state.appState.project, - }, - action: 0, - } - }) - - Editor.popupClear(state) - - if (action.rightButton || state.contentState.keys[state.appState.prefs.editor.keyPan]) - { - state.mergeContentState({ - mouse: { ...state.contentState.mouse, - action: Editor.actionPan - } - }) - } - else if (state.contentState.keys[state.appState.prefs.editor.keySelectRect]) - { - Editor.cursorSetVisible(state, false) - - state.mergeContentState({ - rectCursor: { - track: state.contentState.mouse.track, - time1: state.contentState.mouse.time, - time2: state.contentState.mouse.time, - y1: state.contentState.mouse.trackPos.y, - y2: state.contentState.mouse.trackPos.y, - }, - mouse: { ...state.contentState.mouse, - action: Editor.actionSelectRect, - } - }) - } - else if (state.contentState.keys[state.appState.prefs.editor.keyDraw]) - { - Editor.cursorSetVisible(state, false) - Editor.selectionClear(state) - Track.execute("drawStart", state, state.contentState.mouse.track) - - state.mergeContentState({ - mouse: { ...state.contentState.mouse, - action: Editor.actionDraw, - } - }) - } - else - { - const selectMultiple = state.contentState.keys[state.appState.prefs.editor.keySelectMultiple] - - if (isDoubleClick && state.contentState.mode == EditorMode.Full) - { - let noteEditorState = Editor.makeNewNoteEditor([state.contentState.tracks[state.contentState.mouse.track].trackId]) - noteEditorState.timeScale = state.contentState.timeScale - noteEditorState.timeScroll = state.contentState.timeScroll - - state.appState = AppReducer.createOrUpdateTab( - state.appState, state.contentId, - "editorNotes", noteEditorState) - - state.mergeContentState({ - mouse: { ...state.contentState.mouse, - down: false, - } - }) - } - else if (state.contentState.mouse.hover) - { - const alreadySelected = state.appState.selection.has(state.contentState.mouse.hover.id) - - if (!selectMultiple && !alreadySelected) - Editor.selectionClear(state) - - if (!alreadySelected || !selectMultiple) - Editor.selectionAdd(state, state.contentState.mouse.hover.id) - else - Editor.selectionRemove(state, state.contentState.mouse.hover.id) - - Editor.cursorSetVisible(state, false) - - state.mergeContentState({ - mouse: { ...state.contentState.mouse, - drag: { ...state.contentState.mouse.drag, - rangeOrigin: Editor.selectionRange(state)!, - }, - action: state.contentState.mouse.hover.action, - } - }) - } - else - { - if (!selectMultiple) - Editor.selectionClear(state) - - Editor.cursorPlace(state, state.contentState.mouse.time, state.contentState.mouse.track) - Editor.cursorSetVisible(state, true) - state.mergeContentState({ - mouse: { ...state.contentState.mouse, - action: Editor.actionSelectCursor, - } - }) - state.mergeAppState({ - playback: { ...state.appState.playback, - timeStart: state.contentState.mouse.time, - } - }) - } - } - } - - - static reduce_mouseUp(state: ContentManager, action: any) - { - if (!state.contentState.mouse.down) - return state - - if (state.contentState.mouse.action == Editor.actionDraw) - { - Track.execute("drawEnd", state, state.contentState.mouse.track) - } - else if (state.contentState.mouse.action == Editor.actionPan && - state.contentState.mouse.drag.xLocked && - state.contentState.mouse.drag.yLocked) - { - if (state.contentState.mouse.pos.x < state.contentState.trackHeaderW) - { - Editor.popup(state, state.contentState.mouse.track, - "trackInstrument", - new Rect( - state.contentState.mouse.pos.x, - state.contentState.mouse.pos.y, - 350, 500), - { elemIds: [state.contentState.tracks[state.contentState.mouse.track].trackId] }) - - /*state.appState = AppReducer.createPopup( - state.appState, - new Rect( - state.contentState.x + state.contentState.mouse.pos.x, - state.contentState.y + state.contentState.mouse.pos.y, - 0, 0), - TrackPopup, {})*/ - } - else if (state.contentState.mouse.hover) - { - Editor.selectionClear(state) - Editor.selectionAdd(state, state.contentState.mouse.hover.id) - Track.execute("popup", state, state.contentState.mouse.track) - } - } - - state.mergeContentState({ - mouse: { ...state.contentState.mouse, - down: false, - action: 0, - } - }) - } - - - static reduce_mouseWheel(state: ContentManager, action: any) - { - if (Math.abs(action.deltaX) > 0) - { - let timeScroll = state.contentState.timeScroll + 0.01 / (state.contentState.timeScale / 100) * action.deltaX - let wheelDate = new Date() - - state.mergeContentState({ - timeScroll, - mouse: { ...state.contentState.mouse, - wheelDate - } - }) - } - else if (new Date().getTime() - state.contentState.mouse.wheelDate.getTime() > 250) - { - const snap = new Rational(1, 1024) - const prevMouseTime = Editor.timeAtX(state, state.contentState.mouse.pos.x - state.contentState.trackHeaderW, snap) - - let timeScale = state.contentState.timeScale * (action.deltaY > 0 ? 0.8 : 1.25) - timeScale = Math.max(4, Math.min(2048, timeScale)) - state.mergeContentState({ timeScale }) - - const newMouseTime = Editor.timeAtX(state, state.contentState.mouse.pos.x - state.contentState.trackHeaderW, snap) - - const timeScroll = state.contentState.timeScroll - newMouseTime.subtract(prevMouseTime).asFloat() - - const timeSnapAdjustThresholdUpper = 24 - const timeSnapAdjustThresholdLower = 8 - let timeSnap = state.contentState.timeSnapBase - - if (timeSnap.asFloat() * timeScale > timeSnapAdjustThresholdUpper) - while (timeSnap.asFloat() * timeScale > timeSnapAdjustThresholdUpper) - timeSnap = timeSnap.divide(new Rational(2)) - - else if (timeSnap.asFloat() * timeScale < timeSnapAdjustThresholdLower) - while (timeSnap.asFloat() * timeScale < timeSnapAdjustThresholdLower) - timeSnap = timeSnap.divide(new Rational(1, 2)) - - state.mergeContentState({ timeScroll, timeSnap }) - } - } - - - static handleEdgeScroll(state: ContentManager, handleYScroll: boolean) - { - const threshold = state.appState.prefs.editor.mouseEdgeScrollThreshold - const speed = state.appState.prefs.editor.mouseEdgeScrollSpeed - - const mouseX = state.contentState.mouse.pos.x - state.contentState.trackHeaderW - - let timeScroll = state.contentState.timeScroll - - if (mouseX > state.contentState.w - state.contentState.trackHeaderW - threshold) - timeScroll += state.contentState.timeSnap.asFloat() * speed - - else if (mouseX < threshold) - timeScroll -= state.contentState.timeSnap.asFloat() * speed - - state.mergeContentState({ timeScroll }) - - if (handleYScroll && - Track.execute("yScrollEnabled", state, state.contentState.mouse.drag.trackOrigin)) - { - const threshold = state.appState.prefs.editor.mouseEdgeScrollThreshold - const speed = state.appState.prefs.editor.mouseEdgeScrollSpeed * 2 - - const mouseY = state.contentState.mouse.trackYRaw - - const trackState = new TrackStateManager(state, state.contentState.mouse.drag.trackOrigin) - - if (mouseY > trackState.trackState.h - threshold) - trackState.mergeTrackState({ yScroll: trackState.trackState.yScroll + speed }) - - else if (mouseY < threshold) - trackState.mergeTrackState({ yScroll: trackState.trackState.yScroll - speed }) - } - } - - - static popup(state: ContentManager, trackIndex: number, type: string, rect: Rect, popupState: any) - { - const x = state.contentState.x + state.contentState.trackHeaderW - const y = state.contentState.y + Editor.trackY(state, trackIndex) - - state.appState = AppReducer.createFloating(state.appState, type, popupState, rect.displace(x, y)) - } - - - static popupClear(state: ContentManager) - { - state.appState = AppReducer.removeFloating(state.appState, "inspector") - } - - - static cursorSetVisible(state: ContentManager, visible: boolean) - { - state.mergeContentState({ - cursor: { - ...state.contentState.cursor, - visible, - } - }) - } - - - static cursorPlace(state: ContentManager, time: Rational | null, trackIndex: number | null) - { - if (trackIndex !== null) - trackIndex = Math.max(0, Math.min(state.contentState.tracks.length - 1, trackIndex)) - - const cursor = state.contentState.cursor - - state.mergeContentState({ - cursor: { ...cursor, - time1: time === null ? cursor.time1 : time, - time2: time === null ? cursor.time2 : time, - track1: trackIndex === null ? cursor.track1 : trackIndex, - track2: trackIndex === null ? cursor.track2 : trackIndex, - }, - }) - } - - - static cursorDrag(state: ContentManager, time: Rational | null, trackIndex: number | null) - { - if (trackIndex !== null) - trackIndex = Math.max(0, Math.min(state.contentState.tracks.length - 1, trackIndex)) - - const cursor = state.contentState.cursor - - state.mergeContentState({ - cursor: { ...cursor, - time2: time === null ? cursor.time2 : time, - track2: trackIndex === null ? cursor.track2 : trackIndex, - }, - }) - } - - - static selectionAdd(state: ContentManager, id: number) - { - state.mergeAppState({ - selection: state.appState.selection.add(id), - }) - } - - - static selectionAddAtCursor(state: ContentManager) - { - const trackMin = Math.min(state.contentState.cursor.track1, state.contentState.cursor.track2) - const trackMax = Math.max(state.contentState.cursor.track1, state.contentState.cursor.track2) - - const time1 = state.contentState.cursor.time1 - const time2 = state.contentState.cursor.time2 - if (time1.compare(time2) != 0) - { - let selection = state.appState.selection - - const range = new Range(time1, time2, false, false).sorted() - - for (let t = trackMin; t <= trackMax; t++) - { - const elems = Track.execute("elemsAt", state, t, { range }) - - for (const elem of elems) - selection = selection.add(elem) - } - - state.mergeAppState({ selection }) - } - } - - - static selectionAddAtRectCursor(state: ContentManager) - { - const track = state.contentState.rectCursor.track - - const time1 = state.contentState.rectCursor.time1 - const time2 = state.contentState.rectCursor.time2 - if (time1.compare(time2) != 0) - { - let selection = state.appState.selection - - const range = new Range(time1, time2, false, false).sorted() - const y1 = Math.min(state.contentState.rectCursor.y1, state.contentState.rectCursor.y2) - const y2 = Math.max(state.contentState.rectCursor.y1, state.contentState.rectCursor.y2) - const elems = Track.execute("elemsAt", state, track, { range, y1, y2 }) - - for (const elem of elems) - selection = selection.add(elem) - - state.mergeAppState({ selection }) - } - } - - - static selectionRemove(state: ContentManager, id: number) - { - state.mergeAppState({ - selection: state.appState.selection.remove(id), - }) - } - - - static selectionClear(state: ContentManager) - { - state.mergeAppState({ - selection: state.appState.selection.clear(), - }) - } - - - static selectionRange(state: ContentManager): Range | null - { - let range: Range | null = null - - for (const id of state.appState.selection) - { - const elem = state.appState.project.elems.get(id) as any - if (!elem) - continue - - if (elem.range) - { - const rangedElem = elem as Project.RangedElement - range = Range.merge(range, rangedElem.range) - } - - else if (elem.time) - { - const timedElem = elem as Project.TimedElement - range = Range.merge(range, Range.fromPoint(timedElem.time)) - } - } - - return range - } - - - static xAtTime(state: ContentManager, time: Rational): number - { - return (time.asFloat() - state.contentState.timeScroll) * state.contentState.timeScale - } - - - static timeAtX(state: ContentManager, x: number, timeSnap?: Rational): Rational - { - timeSnap = timeSnap || state.contentState.timeSnap - const time = x / state.contentState.timeScale + state.contentState.timeScroll - return Rational.fromFloat(time, timeSnap.denominator) - } - - - static timeRangeAtX(state: ContentManager, x1: number, x2: number, timeSnap?: Rational) - { - timeSnap = timeSnap || state.contentState.timeSnap - return new Range( - Editor.timeAtX(state, x1, timeSnap).subtract(timeSnap), - Editor.timeAtX(state, x2, timeSnap).add(timeSnap)) - } - - - static trackY(state: ContentManager, trackIndex: number): number - { - return state.contentState.tracks[trackIndex].y - state.contentState.trackScroll - } - - - static trackAtY(state: ContentManager, y: number): number - { - y += state.contentState.trackScroll - - if (y < 0) - return 0 - - for (let t = 0; t < state.contentState.tracks.length; t++) - { - const track = state.contentState.tracks[t] - - if (y >= track.y && y <= track.y + track.h) - return t - } - - return state.contentState.tracks.length - 1 - } - - - static rectForTrack(state: ContentManager, trackIndex: number): Rect | null - { - let y = 0 - for (let t = 0; t < state.contentState.tracks.length; t++) - { - const h = state.contentState.tracks[t].h - - if (t == trackIndex) - { - return new Rect( - state.contentState.trackHeaderW, - y, - state.contentState.w - state.contentState.trackHeaderW, - h) - } - - y += h - } - - return null - } - - - static visibleTimeRange(state: ContentManager): Range - { - return new Range( - Editor.timeAtX(state, 0).subtract(state.contentState.timeSnap), - Editor.timeAtX(state, state.contentState.w).add(state.contentState.timeSnap)) - } - - - static keyAt(state: ContentManager, trackId: Project.ID, time: Rational): Theory.Key - { - const keyChangeTrackId = Project.keyChangeTrackForTrack(state.appState.project, trackId) - const keyChangeTrackTimedElems = state.appState.project.timedLists.get(keyChangeTrackId) - if (!keyChangeTrackTimedElems) - return Editor.defaultKey() - - const keyCh = keyChangeTrackTimedElems.findActiveAt(time) - if (keyCh) - return (keyCh as Project.KeyChange).key - - const firstKeyCh = keyChangeTrackTimedElems.findFirst() - if (firstKeyCh) - return (firstKeyCh as Project.KeyChange).key - - return Editor.defaultKey() - } - - - static meterAt(state: ContentManager, trackId: Project.ID, time: Rational): Theory.Meter - { - const meterChangeTrackId = Project.meterChangeTrackForTrack(state.appState.project, trackId) - const meterChangeTrackTimedElems = state.appState.project.timedLists.get(meterChangeTrackId) - if (!meterChangeTrackTimedElems) - return Editor.defaultMeter() - - const meterCh = meterChangeTrackTimedElems.findActiveAt(time) - if (meterCh) - return (meterCh as Project.MeterChange).meter - - const firstMeterCh = meterChangeTrackTimedElems.findFirst() - if (firstMeterCh) - return (firstMeterCh as Project.MeterChange).meter - - return Editor.defaultMeter() - } - - - static defaultKey(): Theory.Key - { - return Theory.Key.parse("C Major") - } - - - static defaultMeter(): Theory.Meter - { - return new Theory.Meter(4, 4) - } - - - static render(state: ContentManager, ctx: CanvasRenderingContext2D) - { - ctx.save() - - ctx.fillStyle = state.appState.prefs.editor.bkgColor - ctx.fillRect(0, 0, state.contentState.w, state.contentState.h) - - ctx.save() - ctx.translate(state.contentState.trackHeaderW, 0) - - ctx.beginPath() - ctx.rect( - 0, - 0, - state.contentState.w - state.contentState.trackHeaderW, - state.contentState.h) - ctx.clip() - - Editor.renderCursorHighlight(state, ctx) - Editor.renderBackgroundMeasures(state, ctx) - - ctx.restore() - - let y = -state.contentState.trackScroll - for (let t = 0; t < state.contentState.tracks.length; t++) - { - ctx.save() - ctx.translate(state.contentState.trackHeaderW, y) - - ctx.beginPath() - ctx.rect( - 0, - 1, - state.contentState.w - state.contentState.trackHeaderW, - state.contentState.tracks[t].h - 1) - ctx.clip() - - ctx.translate(0, -state.contentState.tracks[t].yScroll) - - Editor.renderRectCursorHighlight(state, ctx, t) - Track.execute("render", state, t, ctx) - Editor.renderRectCursorContour(state, ctx, t) - - ctx.restore() - - y += state.contentState.tracks[t].h - - ctx.strokeStyle = state.appState.prefs.editor.trackHBorderColor - ctx.beginPath() - ctx.moveTo(0, y + 0.5) - ctx.lineTo(state.contentState.w, y + 0.5) - ctx.stroke() - } - - ctx.save() - ctx.translate(state.contentState.trackHeaderW, 0) - - ctx.beginPath() - ctx.rect( - 0, - 0, - state.contentState.w - state.contentState.trackHeaderW, - state.contentState.h) - ctx.clip() - - if (state.contentState.cursor.visible) - { - const timeMin = state.contentState.cursor.time1.min(state.contentState.cursor.time2)! - const timeMax = state.contentState.cursor.time1.max(state.contentState.cursor.time2)! - Editor.renderCursorBeam(state, ctx, timeMin, false) - Editor.renderCursorBeam(state, ctx, timeMax, true) - } - - if (state.appState.playback.playing) - Editor.renderPlaybackBeam(state, ctx, state.appState.playback.time) - - ctx.restore() - - ctx.strokeStyle = state.appState.prefs.editor.trackVBorderColor - ctx.beginPath() - ctx.moveTo(state.contentState.trackHeaderW + 0.5, 0) - ctx.lineTo(state.contentState.trackHeaderW + 0.5, state.contentState.h) - ctx.stroke() - - ctx.restore() - } - - - static renderBackgroundMeasures(state: ContentManager, ctx: CanvasRenderingContext2D) - { - const visibleRange = Editor.visibleTimeRange(state) - - /*ctx.fillStyle = "#1b191c" - - const songXMin = Editor.xAtTime(state, state.project.range.start) - const songXMax = Editor.xAtTime(state, state.project.range.end) - - if (songXMin > 0) - ctx.fillRect(0, 0, songXMin, state.h) - - if (songXMax < state.w) - ctx.fillRect(songXMax, 0, state.w - songXMax, state.h)*/ - - const meterChangeTrackId = Project.meterChangeTrackForTrack(state.appState.project, 0) - const meterChangeList = state.appState.project.timedLists.get(meterChangeTrackId)! - - for (let [meterCh1Raw, meterCh2Raw] of meterChangeList.iterActiveAtRangePairwise(visibleRange)) - { - let timeMin = (meterCh1Raw ? meterCh1Raw.time : null) - let timeMax = (meterCh2Raw ? meterCh2Raw.time : visibleRange.end) - - let measureAlternate = true - - let meterCh1 = meterCh1Raw as (Project.MeterChange | null) - let meterCh2 = meterCh2Raw as (Project.MeterChange | null) - - if (!meterCh1) - { - if (!meterCh2) - continue - - meterCh1 = meterCh2 - timeMin = meterCh2.time - while (timeMin.compare(visibleRange.start) > 0) - { - timeMin = timeMin.subtract(meterCh2.meter.fullCycleDuration) - - if (meterCh2.meter.alternatingMeasureCount % 2 != 0) - measureAlternate = !measureAlternate - } - } - - const meterCh1X = 0.5 + Math.floor(Editor.xAtTime(state, timeMin!)) - const meterCh2X = 0.5 + Math.floor(Editor.xAtTime(state, timeMax)) - - ctx.strokeStyle = state.appState.prefs.editor.meterChangeColor - ctx.lineCap = "square" - ctx.lineWidth = 1 - - for (const [measureN, measureD, time1, time2] of meterCh1.meter.iterMeasuresPairwise(timeMin)) - { - measureAlternate = !measureAlternate - - if (time2.compare(visibleRange.start) < 0) - continue - - if (time1.compare(timeMax) > 0 || time1.compare(visibleRange.end) > 0) - break - - const measureX1 = 0.5 + Math.floor(Editor.xAtTime(state, time1)) - const measureX2 = 0.5 + Math.floor(Editor.xAtTime(state, time2)) - - if (measureAlternate) - { - const x1 = Math.min(meterCh2X, measureX1) - const x2 = Math.min(meterCh2X, measureX2) - - ctx.fillStyle = state.appState.prefs.editor.measureAlternateBkgColor - ctx.fillRect(x1, 0, x2 - x1, state.contentState.h) - } - - if (time1.compare(meterCh1.time) == 0) - ctx.strokeStyle = state.appState.prefs.editor.meterChangeColor - else - ctx.strokeStyle = state.appState.prefs.editor.measureColor - - ctx.beginPath() - ctx.moveTo(measureX1, 0) - ctx.lineTo(measureX1, state.contentState.h) - ctx.stroke() - - const halfSubmeasureSize = Editor.xAtTime(state, new Rational(1, measureD * 2)) - Editor.xAtTime(state, new Rational(0)) - if (halfSubmeasureSize > 16) - { - let halfSubmeasureTime = time1.add(new Rational(-1, measureD * 2)) - for (let sub = 1; sub <= measureN; sub++) - { - halfSubmeasureTime = halfSubmeasureTime.add(new Rational(2, measureD * 2)) - - const halfSubmeasureX = 0.5 + Math.floor(Editor.xAtTime(state, halfSubmeasureTime)) - if (halfSubmeasureX >= meterCh1X && halfSubmeasureX <= meterCh2X) - { - ctx.strokeStyle = state.appState.prefs.editor.halfSubmeasureColor - ctx.beginPath() - ctx.moveTo(halfSubmeasureX, 0) - ctx.lineTo(halfSubmeasureX, state.contentState.h) - ctx.stroke() - } - } - } - - const submeasureSize = Editor.xAtTime(state, new Rational(1, measureD)) - Editor.xAtTime(state, new Rational(0)) - if (submeasureSize > 8) - { - let submeasureTime = time1 - for (let sub = 1; sub <= measureN; sub++) - { - submeasureTime = submeasureTime.add(new Rational(1, measureD)) - - const submeasureX = 0.5 + Math.floor(Editor.xAtTime(state, submeasureTime)) - if (submeasureX >= meterCh1X && submeasureX <= meterCh2X) - { - ctx.strokeStyle = state.appState.prefs.editor.submeasureColor - ctx.beginPath() - ctx.moveTo(submeasureX, 0) - ctx.lineTo(submeasureX, state.contentState.h) - ctx.stroke() - } - } - } - } - } - - const keyChangeTrackId = Project.keyChangeTrackForTrack(state.appState.project, 0) - const keyChangeList = state.appState.project.timedLists.get(keyChangeTrackId)! - - for (const keyCh of keyChangeList.iterAtRange(visibleRange)) - { - const keyChX = 0.5 + Math.floor(Editor.xAtTime(state, keyCh.time)) - - ctx.strokeStyle = state.appState.prefs.editor.keyChangeColor - ctx.lineCap = "square" - ctx.lineWidth = 1 - - ctx.beginPath() - ctx.moveTo(keyChX, 0) - ctx.lineTo(keyChX, state.contentState.h) - ctx.stroke() - } - } - - - static renderPlaybackBeam(state: ContentManager, ctx: CanvasRenderingContext2D, time: Rational) - { - const trackMin = 0 - const trackMax = state.contentState.tracks.length - 1 - - const x = 0.5 + Math.floor(Editor.xAtTime(state, time)) - - ctx.strokeStyle = state.appState.prefs.editor.playbackCursorColor - ctx.lineCap = "square" - ctx.lineWidth = 1 - - const y1 = 0.5 + Math.floor(Editor.trackY(state, trackMin)) - const y2 = 0.5 + Math.floor(Editor.trackY(state, trackMax) + state.contentState.tracks[trackMax].h) - - ctx.beginPath() - ctx.lineTo(x, y1) - ctx.lineTo(x, y2) - ctx.stroke() - } - - - static renderCursorHighlight(state: ContentManager, ctx: CanvasRenderingContext2D) - { - if (!state.contentState.cursor.visible) - return - - const timeMin = state.contentState.cursor.time1.min(state.contentState.cursor.time2)! - const timeMax = state.contentState.cursor.time1.max(state.contentState.cursor.time2)! - const trackMin = Math.min(state.contentState.cursor.track1, state.contentState.cursor.track2) - const trackMax = Math.max(state.contentState.cursor.track1, state.contentState.cursor.track2) - - if (trackMin < 0 || trackMax < 0 || - trackMin >= state.contentState.tracks.length || - trackMax >= state.contentState.tracks.length) - return - - const y1 = 0.5 + Math.floor(Editor.trackY(state, trackMin)) - const y2 = 0.5 + Math.floor(Editor.trackY(state, trackMax) + state.contentState.tracks[trackMax].h) - - const x1 = Editor.xAtTime(state, timeMin) - const x2 = Editor.xAtTime(state, timeMax) - - ctx.fillStyle = state.appState.prefs.editor.selectionBkgColor - ctx.fillRect(x1, y1, x2 - x1, y2 - y1) - } - - - static renderCursorBeam(state: ContentManager, ctx: CanvasRenderingContext2D, time: Rational, tipOffsetSide: boolean) - { - const trackMin = Math.min(state.contentState.cursor.track1, state.contentState.cursor.track2) - const trackMax = Math.max(state.contentState.cursor.track1, state.contentState.cursor.track2) - - if (trackMin < 0 || trackMax < 0 || - trackMin >= state.contentState.tracks.length || - trackMax >= state.contentState.tracks.length) - return - - const x = 0.5 + Math.floor(Editor.xAtTime(state, time)) - - ctx.strokeStyle = state.appState.prefs.editor.selectionCursorColor - ctx.lineCap = "square" - ctx.lineWidth = 1 - - const headSize = 7 * (tipOffsetSide ? -1 : 1) - - const y1 = 0.5 + Math.floor(Editor.trackY(state, trackMin)) - const y2 = 0.5 + Math.floor(Editor.trackY(state, trackMax) + state.contentState.tracks[trackMax].h) - - ctx.beginPath() - ctx.moveTo(x + headSize, y1) - ctx.lineTo(x, y1) - ctx.lineTo(x, y2) - ctx.lineTo(x + headSize, y2) - ctx.stroke() - } - - - static renderRectCursorHighlight(state: ContentManager, ctx: CanvasRenderingContext2D, trackIndex: number) - { - if (state.contentState.mouse.action != Editor.actionSelectRect) - return - - if (trackIndex != state.contentState.rectCursor.track) - return - - const timeMin = state.contentState.rectCursor.time1.min(state.contentState.rectCursor.time2)! - const timeMax = state.contentState.rectCursor.time1.max(state.contentState.rectCursor.time2)! - const y1 = 0.5 + Math.floor(Math.min(state.contentState.rectCursor.y1, state.contentState.rectCursor.y2)) - const y2 = 0.5 + Math.floor(Math.max(state.contentState.rectCursor.y1, state.contentState.rectCursor.y2)) + 1 - - const x1 = 0.5 + Math.floor(Editor.xAtTime(state, timeMin)) - const x2 = 0.5 + Math.floor(Editor.xAtTime(state, timeMax)) - - ctx.fillStyle = state.appState.prefs.editor.selectionBkgColor - ctx.fillRect(x1, y1, x2 - x1, y2 - y1) - } - - - static renderRectCursorContour(state: ContentManager, ctx: CanvasRenderingContext2D, trackIndex: number) - { - if (state.contentState.mouse.action != Editor.actionSelectRect) - return - - if (trackIndex != state.contentState.rectCursor.track) - return - - const timeMin = state.contentState.rectCursor.time1.min(state.contentState.rectCursor.time2)! - const timeMax = state.contentState.rectCursor.time1.max(state.contentState.rectCursor.time2)! - const y1 = 0.5 + Math.floor(Math.min(state.contentState.rectCursor.y1, state.contentState.rectCursor.y2)) - const y2 = 0.5 + Math.floor(Math.max(state.contentState.rectCursor.y1, state.contentState.rectCursor.y2)) + 1 - - const x1 = 0.5 + Math.floor(Editor.xAtTime(state, timeMin)) - const x2 = 0.5 + Math.floor(Editor.xAtTime(state, timeMax)) - - ctx.strokeStyle = state.appState.prefs.editor.selectionCursorColor - ctx.lineCap = "square" - ctx.lineWidth = 1 - ctx.strokeRect(x1, y1, x2 - x1, y2 - y1) - } -} \ No newline at end of file diff --git a/src_old/editor2/editorState.ts b/src_old/editor2/editorState.ts deleted file mode 100644 index a59e5f0..0000000 --- a/src_old/editor2/editorState.ts +++ /dev/null @@ -1,101 +0,0 @@ -import TrackState from "./trackState" -import Project from "../project/project2" -import Rational from "../util/rational" -import Range from "../util/range" - - -export enum EditorMode -{ - Full, - NoteEditor, -} - - -export default interface EditorState -{ - mode: EditorMode - modeTracks: number[] - - x: number - y: number - w: number - h: number - - trackHeaderW: number - - tracks: TrackState[] - trackScroll: number - - timeScroll: number - timeScale: number - timeSnap: Rational - timeSnapBase: Rational - - cursor: - { - visible: boolean - time1: Rational - time2: Rational - track1: number - track2: number - } - - rectCursor: - { - track: number - time1: Rational - time2: Rational - y1: number - y2: number - } - - keys: { [key: string]: boolean } - - mouse: - { - down: boolean - downDate: Date - - action: number - - pos: { x: number, y: number } - posPrev: { x: number, y: number } - time: Rational - track: number, - trackPos: { x: number, y: number } - trackYRaw: number, - row: number, - - hover: null | - { - id: number, - range: Range, - action: number, - } - - drag: - { - xLocked: boolean - yLocked: boolean - - posOrigin: { x: number, y: number } - timeOrigin: Rational - timeScrollOrigin: number - trackScrollOrigin: number - rangeOrigin: Range - trackOrigin: number - trackPosOrigin: { x: number, y: number } - trackYRawOrigin: number - trackYScrollOrigin: number - rowOrigin: number - projectOrigin: Project - - posDelta: { x: number, y: number } - timeDelta: Rational - trackPosDelta: { x: number, y: number } - rowDelta: number - } - - wheelDate: Date - } -} \ No newline at end of file diff --git a/src_old/editor2/track.ts b/src_old/editor2/track.ts deleted file mode 100644 index 1d5d8b9..0000000 --- a/src_old/editor2/track.ts +++ /dev/null @@ -1,67 +0,0 @@ -import { ContentManager } from "../AppState" -import EditorState from "./editorState" -import TrackStateManager from "./trackStateManager" -import TrackNotes from "./trackNotes" -import TrackNotesPreview from "./trackNotesPreview" -import TrackKeyChanges from "./trackKeyChanges" -import TrackMeterChanges from "./trackMeterChanges" -import Project from "../project/project2" - - -export default class Track -{ - static init(state: TrackStateManager) - { - state.mergeTrackState({ - yScroll: 0, - }) - } - - - static handlerForTrackType(type: string): any - { - switch (type) - { - case "notes": return TrackNotes - case "notesPreview": return TrackNotesPreview - case "keyChanges": return TrackKeyChanges - case "meterChanges": return TrackMeterChanges - default: throw "invalid track kind" - } - } - - - static yScrollEnabled(state: TrackStateManager): boolean - { - return false - } - - - static rowAtY(state: TrackStateManager, y: number): number - { - return 0 - } - - - static elemsAt(state: TrackStateManager, region: any): Project.ID[] - { - return [] - } - - - static execute(fnName: string, state: ContentManager, trackIndex: number, ...args: any[]): any - { - const trackStateManager = new TrackStateManager(state, trackIndex) - - const handlerDerived = Track.handlerForTrackType(trackStateManager.trackState.type) - const fnDerived = handlerDerived[fnName] - if (fnDerived) - return fnDerived(trackStateManager, ...args) - - const fn = (Track as any)[fnName] - if (fn) - return fn(trackStateManager, ...args) - - return null - } -} \ No newline at end of file diff --git a/src_old/editor2/trackKeyChanges.ts b/src_old/editor2/trackKeyChanges.ts deleted file mode 100644 index 5f7d679..0000000 --- a/src_old/editor2/trackKeyChanges.ts +++ /dev/null @@ -1,249 +0,0 @@ -import Editor from "./editor" -import Track from "./track" -import TrackStateManager from "./trackStateManager" -import TrackKeyChangesState from "./trackKeyChangesState" -import Rect from "../util/rect" -import Range from "../util/range" -import Project from "../project/project2" -import Rational from "../util/rational" -import * as Theory from "../theory/theory" - - -export default class TrackKeyChanges -{ - static knobWidth = 16 - static knobHeight = 22 - - - static init(state: TrackStateManager) - { - Track.init(state) - state.mergeTrackState({ - type: "keyChanges", - }) - } - - - static hover(state: TrackStateManager) - { - const pos = state.contentState.mouse.trackPos - - const checkRange = Editor.timeRangeAtX( - state.contentStateManager, - pos.x - TrackKeyChanges.knobWidth, - pos.x + TrackKeyChanges.knobWidth) - - let hover = null - - for (const keyCh of TrackKeyChanges.iterAtRange(state, checkRange)) - { - const rect = TrackKeyChanges.knobRectForKeyChange(state, keyCh.time) - if (rect.contains(pos)) - { - hover = - { - id: keyCh.id, - range: Range.fromPoint(keyCh.time), - action: Editor.actionDragTime, - } - } - } - - state.mergeContentState({ - mouse: { ...state.contentState.mouse, - hover, - } - }) - } - - - static drawClear(state: TrackStateManager) - { - state.mergeTrackState({ draw: null }) - } - - - static drawHover(state: TrackStateManager) - { - const time = state.contentState.mouse.time - - state.mergeTrackState({ draw: { time } }) - } - - - static drawDrag(state: TrackStateManager) - { - const draw = state.trackState.draw - if (draw) - { - const time = state.contentState.mouse.time - state.mergeTrackState({ draw: { time } }) - } - } - - - static drawEnd(state: TrackStateManager) - { - const draw = state.trackState.draw - if (draw) - { - const keyCh = new Project.KeyChange( - state.trackState.trackId, - draw.time, - Editor.defaultKey()) - - const id = state.appState.project.nextId - state.mergeAppState({ - project: Project.upsertTimedElement(state.appState.project, keyCh), - selection: state.appState.selection.add(id), - }) - } - } - - - static popup(state: TrackStateManager) - { - const elemId = state.contentState.mouse.hover!.id - const elem = state.appState.project.elems.get(elemId) as Project.KeyChange - - const knob = TrackKeyChanges.knobRectForKeyChange(state, elem.time) - - Editor.popup(state.contentStateManager, state.trackIndex, - "inspector", - new Rect(knob.x2 + 10, knob.y2 + 10, 450, 250), - { elemIds: [elemId] }) - } - - - static elemsAt(state: TrackStateManager, region: any): Project.ID[] - { - const elems = [] - - for (const keyCh of TrackKeyChanges.iterAtRange(state, region.range)) - elems.push(keyCh.id) - - return elems - } - - - static *iterAtRange(state: TrackStateManager, range: Range): Generator - { - const trackElems = state.appState.project.timedLists.get(state.trackState.trackId) - if (!trackElems) - return - - for (const keyCh of trackElems.iterAtRange(range)) - yield keyCh as Project.KeyChange - } - - - static knobRectForKeyChange(state: TrackStateManager, time: Rational) - { - return new Rect( - Editor.xAtTime(state.contentStateManager, time) - TrackKeyChanges.knobWidth / 2, - 0, - TrackKeyChanges.knobWidth, - TrackKeyChanges.knobHeight) - } - - - static render(state: TrackStateManager, ctx: CanvasRenderingContext2D) - { - const visibleRange = Editor.visibleTimeRange(state.contentStateManager) - - const firstKey = Editor.keyAt(state.contentStateManager, state.trackState.trackId, visibleRange.start) - let shouldDrawFirstKey = true - - for (const keyCh of TrackKeyChanges.iterAtRange(state, visibleRange)) - { - if (shouldDrawFirstKey) - { - const x = Editor.xAtTime(state.contentStateManager, keyCh.time) - if (x > 80) - TrackKeyChanges.renderKeyChangeLabel(state, ctx, firstKey) - } - - TrackKeyChanges.renderKeyChange(state, ctx, keyCh) - shouldDrawFirstKey = false - } - - if (shouldDrawFirstKey) - TrackKeyChanges.renderKeyChangeLabel(state, ctx, firstKey) - - const draw = state.trackState.draw - if (draw) - { - ctx.globalAlpha = 0.6 - TrackKeyChanges.renderKeyChangeKnob(state, ctx, draw.time) - ctx.globalAlpha = 1 - } - } - - - static renderKeyChangeKnob(state: TrackStateManager, ctx: CanvasRenderingContext2D, time: Rational) - { - const rect = TrackKeyChanges.knobRectForKeyChange(state, time) - - ctx.fillStyle = state.appState.prefs.editor.keyChangeColor - - ctx.beginPath() - ctx.arc(rect.x + rect.w / 2, rect.y + rect.h / 2, rect.w / 2, 0, Math.PI * 2) - ctx.fill() - } - - - static renderKeyChangeLabel(state: TrackStateManager, ctx: CanvasRenderingContext2D, key: Theory.Key) - { - const rect = TrackKeyChanges.knobRectForKeyChange(state, new Rational(0)) - const x = 5 - - ctx.fillStyle = state.appState.prefs.editor.keyChangeColor - - ctx.font = "14px Verdana" - ctx.textAlign = "left" - ctx.textBaseline = "middle" - ctx.fillText(key.str, x, rect.y + rect.h / 2) - } - - - static renderKeyChange(state: TrackStateManager, ctx: CanvasRenderingContext2D, keyCh: Project.KeyChange) - { - const rect = TrackKeyChanges.knobRectForKeyChange(state, keyCh.time) - - ctx.fillStyle = state.appState.prefs.editor.keyChangeColor - - ctx.beginPath() - ctx.arc(rect.x + rect.w / 2, rect.y + rect.h / 2, rect.w / 2, 0, Math.PI * 2) - ctx.fill() - - ctx.font = "14px Verdana" - ctx.textAlign = "left" - ctx.textBaseline = "middle" - ctx.fillText(keyCh.key.str, rect.x + rect.w + 5, rect.y + rect.h / 2) - - if (state.appState.selection.has(keyCh.id)) - { - ctx.globalAlpha = 0.75 - ctx.fillStyle = "#fff" - - ctx.beginPath() - ctx.arc(rect.x + rect.w / 2, rect.y + rect.h / 2, rect.w / 2 - 3, 0, Math.PI * 2) - ctx.fill() - - ctx.globalAlpha = 1 - } - - const hover = state.contentState.mouse.hover - if (hover && hover.id === keyCh.id) - { - ctx.globalAlpha = 0.5 - ctx.fillStyle = "#fff" - - ctx.beginPath() - ctx.arc(rect.x + rect.w / 2, rect.y + rect.h / 2, rect.w / 2, 0, Math.PI * 2) - ctx.fill() - - ctx.globalAlpha = 1 - } - } -} \ No newline at end of file diff --git a/src_old/editor2/trackKeyChangesState.ts b/src_old/editor2/trackKeyChangesState.ts deleted file mode 100644 index 09cd949..0000000 --- a/src_old/editor2/trackKeyChangesState.ts +++ /dev/null @@ -1,11 +0,0 @@ -import TrackState from "./trackState" -import Rational from "../util/rational" - - -export default interface TrackKeyChangesState extends TrackState -{ - draw: null | - { - time: Rational - } -} \ No newline at end of file diff --git a/src_old/editor2/trackMeterChanges.ts b/src_old/editor2/trackMeterChanges.ts deleted file mode 100644 index 4db3ec3..0000000 --- a/src_old/editor2/trackMeterChanges.ts +++ /dev/null @@ -1,236 +0,0 @@ -import Editor from "./editor" -import Track from "./track" -import TrackStateManager from "./trackStateManager" -import TrackMeterChangesState from "./trackMeterChangesState" -import Rect from "../util/rect" -import Range from "../util/range" -import Project from "../project/project2" -import Rational from "../util/rational" -import * as Theory from "../theory/theory" - - -export default class TrackMeterChanges -{ - static knobWidth = 16 - static knobHeight = 22 - - - static init(state: TrackStateManager) - { - Track.init(state) - state.mergeTrackState({ - type: "meterChanges", - }) - } - - - static hover(state: TrackStateManager) - { - const pos = state.contentState.mouse.trackPos - - const checkRange = Editor.timeRangeAtX( - state.contentStateManager, - pos.x - TrackMeterChanges.knobWidth, - pos.x + TrackMeterChanges.knobWidth) - - let hover = null - - for (const keyCh of TrackMeterChanges.iterAtRange(state, checkRange)) - { - const rect = TrackMeterChanges.knobRectForMeterChange(state, keyCh.time) - if (rect.contains(pos)) - { - hover = - { - id: keyCh.id, - range: Range.fromPoint(keyCh.time), - action: Editor.actionDragTime, - } - } - } - - state.mergeContentState({ - mouse: { ...state.contentState.mouse, - hover, - } - }) - } - - - static drawClear(state: TrackStateManager) - { - state.mergeTrackState({ draw: null }) - } - - - static drawHover(state: TrackStateManager) - { - const time = state.contentState.mouse.time - - state.mergeTrackState({ draw: { time } }) - } - - - static drawDrag(state: TrackStateManager) - { - const draw = state.trackState.draw - if (draw) - { - const time = state.contentState.mouse.time - state.mergeTrackState({ draw: { time } }) - } - } - - - static drawEnd(state: TrackStateManager) - { - const draw = state.trackState.draw - if (draw) - { - const meterCh = new Project.MeterChange( - state.trackState.trackId, - draw.time, - Editor.defaultMeter()) - - const id = state.appState.project.nextId - state.mergeAppState({ - project: Project.upsertTimedElement(state.appState.project, meterCh), - selection: state.appState.selection.add(id), - }) - } - } - - - static elemsAt(state: TrackStateManager, region: any): Project.ID[] - { - const elems = [] - - for (const meterCh of TrackMeterChanges.iterAtRange(state, region.range)) - elems.push(meterCh.id) - - return elems - } - - - static *iterAtRange(state: TrackStateManager, range: Range): Generator - { - const trackElems = state.appState.project.timedLists.get(state.trackState.trackId) - if (!trackElems) - return - - for (const keyCh of trackElems.iterAtRange(range)) - yield keyCh as Project.MeterChange - } - - - static knobRectForMeterChange(state: TrackStateManager, time: Rational) - { - return new Rect( - Editor.xAtTime(state.contentStateManager, time) - TrackMeterChanges.knobWidth / 2, - 0, - TrackMeterChanges.knobWidth, - TrackMeterChanges.knobHeight) - } - - - static render(state: TrackStateManager, ctx: CanvasRenderingContext2D) - { - const visibleRange = Editor.visibleTimeRange(state.contentStateManager) - - const firstMeter = Editor.meterAt(state.contentStateManager, state.trackState.trackId, visibleRange.start) - let shouldDrawFirstKey = true - - for (const meterCh of TrackMeterChanges.iterAtRange(state, visibleRange)) - { - if (shouldDrawFirstKey) - { - const x = Editor.xAtTime(state.contentStateManager, meterCh.time) - if (x > 80) - TrackMeterChanges.renderMeterChangeLabel(state, ctx, firstMeter) - } - - TrackMeterChanges.renderMeterChange(state, ctx, meterCh) - shouldDrawFirstKey = false - } - - if (shouldDrawFirstKey) - TrackMeterChanges.renderMeterChangeLabel(state, ctx, firstMeter) - - - const draw = state.trackState.draw - if (draw) - { - ctx.globalAlpha = 0.6 - TrackMeterChanges.renderMeterChangeKnob(state, ctx, draw.time) - ctx.globalAlpha = 1 - } - } - - - static renderMeterChangeKnob(state: TrackStateManager, ctx: CanvasRenderingContext2D, time: Rational) - { - const rect = TrackMeterChanges.knobRectForMeterChange(state, time) - - ctx.fillStyle = state.appState.prefs.editor.meterChangeColor - - ctx.beginPath() - ctx.arc(rect.x + rect.w / 2, rect.y + rect.h / 2, rect.w / 2, 0, Math.PI * 2) - ctx.fill() - } - - - static renderMeterChangeLabel(state: TrackStateManager, ctx: CanvasRenderingContext2D, meter: Theory.Meter) - { - const rect = TrackMeterChanges.knobRectForMeterChange(state, new Rational(0)) - const x = 5 - - ctx.fillStyle = state.appState.prefs.editor.meterChangeColor - - ctx.font = "14px Verdana" - ctx.textAlign = "left" - ctx.textBaseline = "middle" - ctx.fillText(meter.numerator + " / " + meter.denominator, x, rect.y + rect.h / 2) - } - - - static renderMeterChange(state: TrackStateManager, ctx: CanvasRenderingContext2D, meterCh: Project.MeterChange) - { - const rect = TrackMeterChanges.knobRectForMeterChange(state, meterCh.time) - - ctx.fillStyle = state.appState.prefs.editor.meterChangeColor - - ctx.beginPath() - ctx.arc(rect.x + rect.w / 2, rect.y + rect.h / 2, rect.w / 2, 0, Math.PI * 2) - ctx.fill() - - ctx.font = "14px Verdana" - ctx.textAlign = "left" - ctx.textBaseline = "middle" - ctx.fillText(meterCh.meter.numerator + " / " + meterCh.meter.denominator, rect.x + rect.w + 5, rect.y + rect.h / 2) - - if (state.appState.selection.has(meterCh.id)) - { - ctx.globalAlpha = 0.75 - ctx.fillStyle = "#fff" - - ctx.beginPath() - ctx.arc(rect.x + rect.w / 2, rect.y + rect.h / 2, rect.w / 2 - 3, 0, Math.PI * 2) - ctx.fill() - - ctx.globalAlpha = 1 - } - - const hover = state.contentState.mouse.hover - if (hover && hover.id === meterCh.id) - { - ctx.globalAlpha = 0.5 - ctx.fillStyle = "#fff" - - ctx.beginPath() - ctx.arc(rect.x + rect.w / 2, rect.y + rect.h / 2, rect.w / 2, 0, Math.PI * 2) - ctx.fill() - - ctx.globalAlpha = 1 - } - } -} \ No newline at end of file diff --git a/src_old/editor2/trackMeterChangesState.ts b/src_old/editor2/trackMeterChangesState.ts deleted file mode 100644 index 3b2d4e0..0000000 --- a/src_old/editor2/trackMeterChangesState.ts +++ /dev/null @@ -1,11 +0,0 @@ -import TrackState from "./trackState" -import Rational from "../util/rational" - - -export default interface TrackMeterChangesState extends TrackState -{ - draw: null | - { - time: Rational - } -} \ No newline at end of file diff --git a/src_old/editor2/trackNotes.ts b/src_old/editor2/trackNotes.ts deleted file mode 100644 index e2a9698..0000000 --- a/src_old/editor2/trackNotes.ts +++ /dev/null @@ -1,391 +0,0 @@ -import Editor from "./editor" -import Track from "./track" -import TrackStateManager from "./trackStateManager" -import TrackNotesState from "./trackNotesState" -import Rect from "../util/rect" -import Range from "../util/range" -import Project from "../project/project2" -import Rational from "../util/rational" -import CanvasUtils from "../util/canvasUtils" -import * as Theory from "../theory/theory" - - -type StateManager = TrackStateManager - - -export default class TrackNotes -{ - static init(state: StateManager) - { - Track.init(state) - state.mergeTrackState({ - type: "notes", - rowScale: 10, - }) - } - - - static yScrollEnabled(state: StateManager): boolean - { - return true - } - - - static hover(state: StateManager) - { - const pos = state.contentState.mouse.trackPos - - const margin = 10 - const checkRange = Editor.timeRangeAtX(state.contentStateManager, pos.x - margin, pos.x + margin) - - let hoverPrimary = null - let hoverSecondary = null - - for (const [note, keyCh, xMin, xMax] of TrackNotes.iterNotesAndKeyChangesAtRange(state, checkRange)) - { - const margin = 2 - - const row = TrackNotes.rowForPitch(state, note.pitch, keyCh.key) - const rectExact = TrackNotes.rectForNote(state, note.range, row, xMin, xMax) - const rect = new Rect( - rectExact.x - margin, - rectExact.y, - rectExact.w + margin * 2, - rectExact.h) - - const dragMarginOut = Math.abs(Math.min(0, rect.w - 16)) - const stretchMarginOut = 16 - const stretchMarginIn = Math.max(0, Math.min(10, rect.w / 4)) - const rectDrag = new Rect(rect.x - dragMarginOut, rect.y, rect.w + dragMarginOut * 2, rect.h) - const rectStretchStart = new Rect(rect.x - stretchMarginOut, rect.y, stretchMarginOut + stretchMarginIn, rect.h) - const rectStretchEnd = new Rect(rect.x2 - stretchMarginIn, rect.y, stretchMarginOut + stretchMarginIn, rect.h) - - const exactHover = rect.contains(pos) - - let action = 0 - if (dragMarginOut > 0 && rectDrag.contains(pos)) - action = Editor.actionDragTime | Editor.actionDragPitchRow - else if (rectStretchStart.contains(pos) && !rectExact.cutStart) - action = Editor.actionStretchTimeStart - else if (rectStretchEnd.contains(pos) && !rectExact.cutEnd) - action = Editor.actionStretchTimeEnd - else if (exactHover) - action = Editor.actionDragTime | Editor.actionDragPitchRow - - const hoverNote = - { - id: note.id, - range: note.range, - action, - } - - if (action != 0) - { - if (exactHover) - hoverPrimary = hoverNote - else - hoverSecondary = hoverNote - } - - if (hoverPrimary) - break - } - - const hover = hoverPrimary || hoverSecondary - state.mergeContentState({ - mouse: { ...state.contentState.mouse, - hover, - } - }) - } - - - static drawClear(state: StateManager) - { - state.mergeTrackState({ draw: null }) - } - - - static drawHover(state: StateManager) - { - const time = state.contentState.mouse.time - const key = Editor.keyAt(state.contentStateManager, state.trackState.trackId, time) - const row = TrackNotes.rowAtY(state, state.contentState.mouse.trackPos.y) - const pitch = TrackNotes.pitchForRow(state, row, key) - - state.mergeTrackState({ - draw: - { - time1: time, - time2: time.add(state.contentState.timeSnap.multiply(new Rational(4))), - pitch, - } - }) - } - - - static drawDrag(state: StateManager) - { - const draw = state.trackState.draw - if (draw) - { - let time2 = state.contentState.mouse.time - - const time1X = Editor.xAtTime(state.contentStateManager, draw.time1) - const time2X = Editor.xAtTime(state.contentStateManager, time2) - if (Math.abs(time1X - time2X) < 5) - time2 = draw.time1.add(state.contentState.timeSnap.multiply(new Rational(4))) - - state.mergeTrackState({ - draw: { ...draw, - time2, - } - }) - } - } - - - static drawEnd(state: StateManager) - { - const draw = state.trackState.draw - if (draw) - { - const note = new Project.Note( - state.trackState.trackId, - new Range(draw.time1, draw.time2).sorted(), - draw.pitch) - - const id = state.appState.project.nextId - state.mergeAppState({ - project: Project.upsertRangedElement(state.appState.project, note), - selection: state.appState.selection.add(id), - }) - } - } - - - static elemsAt(state: StateManager, region: any): Project.ID[] - { - const elems = [] - - if (region.y1 === undefined) - { - for (const note of TrackNotes.iterNotesAtRange(state, region.range)) - elems.push(note.id) - } - else - { - for (const [note, keyCh, xMin, xMax] of TrackNotes.iterNotesAndKeyChangesAtRange(state, region.range)) - { - const row = TrackNotes.rowForPitch(state, note.pitch, keyCh.key) - const rect = TrackNotes.rectForNote(state, note.range, row, xMin, xMax) - - if (rect.y1 < region.y2 && rect.y2 > region.y1) - elems.push(note.id) - } - } - - return elems - } - - - static *iterNotesAtRange(state: StateManager, range: Range): Generator - { - const trackElems = state.appState.project.rangedLists.get(state.trackState.trackId) - if (!trackElems) - return - - for (const note of trackElems.iterAtRange(range)) - yield note as Project.Note - } - - - static *iterKeyChangePairsAtRange(state: StateManager, range: Range): Generator<[Project.KeyChange, Project.KeyChange, number, number], void, void> - { - const keyChangeTrackId = Project.keyChangeTrackForTrack(state.appState.project, state.trackState.trackId) - const keyChangeTrackTimedElems = state.appState.project.timedLists.get(keyChangeTrackId) - if (!keyChangeTrackTimedElems) - return - - const firstKeyCh = keyChangeTrackTimedElems.findFirst() as (Project.KeyChange | null) - const defaultKey = firstKeyCh ? firstKeyCh.key : Editor.defaultKey() - - for (const pair of keyChangeTrackTimedElems.iterActiveAtRangePairwise(range)) - { - const keyCh1 = pair[0] || new Project.KeyChange(-1, range.start, defaultKey) - const keyCh2 = pair[1] || new Project.KeyChange(-1, range.end, defaultKey) - - const keyCh1X = Editor.xAtTime(state.contentStateManager, keyCh1.time) - const keyCh2X = Editor.xAtTime(state.contentStateManager, keyCh2.time) - - yield [keyCh1 as Project.KeyChange, keyCh2 as Project.KeyChange, keyCh1X, keyCh2X] - } - } - - - static *iterNotesAndKeyChangesAtRange(state: StateManager, range: Range): Generator<[Project.Note, Project.KeyChange, number, number], void, void> - { - for (const [keyCh1, keyCh2, keyCh1X, keyCh2X] of TrackNotes.iterKeyChangePairsAtRange(state, range)) - { - const time1 = keyCh1.time.max(range.start)! - const time2 = keyCh2.time.min(range.end)! - - for (const note of TrackNotes.iterNotesAtRange(state, new Range(time1, time2))) - yield [note, keyCh1, keyCh1X, keyCh2X] - } - } - - - static yForRow(state: StateManager, row: number): number - { - return state.trackState.h / 2 - (row + 1) * state.trackState.rowScale - } - - - static rowAtY(state: StateManager, y: number): number - { - return -Math.floor((y - state.trackState.h / 2) / state.trackState.rowScale) - 1 - } - - - static rowForPitch(state: StateManager, pitch: number, key: Theory.Key): number - { - const tonicRowOffset = Theory.Utils.chromaToDegreeInCMajor(key.tonic.chroma) - return key.octavedDegreeForMidi(pitch - 60) + tonicRowOffset - } - - - static pitchForRow(state: StateManager, row: number, key: Theory.Key): number - { - const tonicRowOffset = Theory.Utils.chromaToDegreeInCMajor(key.tonic.chroma) - return key.midiForDegree(row - Math.floor(tonicRowOffset)) + 60 - } - - - static rectForNote(state: StateManager, range: Range, row: number, xStart: number, xEnd: number) - { - const noteOrigX1 = Editor.xAtTime(state.contentStateManager, range.start) - const noteOrigX2 = Editor.xAtTime(state.contentStateManager, range.end) - - const noteY = Math.floor(TrackNotes.yForRow(state, row)) - - let noteX1 = Math.max(noteOrigX1, xStart) - let noteX2 = Math.min(noteOrigX2, xEnd) - - const cutStart = noteOrigX1 < noteX1 - const cutEnd = noteOrigX2 > noteX2 - - //if (!cutStart) noteX1 += 1 - if (!cutEnd) noteX2 -= 1 - - noteX1 = Math.floor(noteX1) - noteX2 = Math.floor(noteX2) - - const noteW = Math.max(2, noteX2 - noteX1) - - return Object.assign( - new Rect(noteX1, noteY, noteW, state.trackState.rowScale), - { cutStart, cutEnd }) - } - - - static render(state: StateManager, ctx: CanvasRenderingContext2D) - { - const visibleRange = Editor.visibleTimeRange(state.contentStateManager) - - const rowAtTop = TrackNotes.rowAtY(state, state.trackState.yScroll) - const rowAtBottom = TrackNotes.rowAtY(state, state.trackState.yScroll + state.trackState.h) - - const octaveAtTop = Math.ceil(rowAtTop / 7) + 1 - const octaveAtBottom = Math.floor(rowAtBottom / 7) - 1 - - for (const [keyCh1, keyCh2, xMin, xMax] of TrackNotes.iterKeyChangePairsAtRange(state, visibleRange)) - { - const tonicRowOffset = Theory.Utils.chromaToDegreeInCMajor(keyCh1.key.tonic.chroma) - - for (let i = octaveAtBottom; i <= octaveAtTop; i++) - { - const y = 0.5 + Math.floor(TrackNotes.yForRow(state, tonicRowOffset + i * 7) + state.trackState.rowScale) - - ctx.strokeStyle = state.appState.prefs.editor.octaveDividerColor - ctx.beginPath() - ctx.moveTo(xMin, y) - ctx.lineTo(xMax, y) - ctx.stroke() - - for (let j = 1; j < 7; j += 1) - { - const ySuboctave = 0.5 + Math.floor(TrackNotes.yForRow(state, tonicRowOffset + i * 7 + j) + state.trackState.rowScale) - - ctx.strokeStyle = state.appState.prefs.editor.noteRowAlternateBkgColor - ctx.beginPath() - ctx.moveTo(xMin, ySuboctave) - ctx.lineTo(xMax, ySuboctave) - ctx.stroke() - } - - /*if (i == 0) - { - ctx.globalAlpha = 0.05 - ctx.fillStyle = "#fff" - ctx.fillRect(xMin, y - 7 * state.trackState.rowScale, xMax - xMin, 7 * state.trackState.rowScale) - ctx.globalAlpha = 1 - }*/ - } - } - - for (const [note, keyCh, xMin, xMax] of TrackNotes.iterNotesAndKeyChangesAtRange(state, visibleRange)) - { - const key = keyCh.key - const row = TrackNotes.rowForPitch(state, note.pitch, key) - const mode = key.scale.metadata!.mode - const fillStyle = CanvasUtils.fillStyleForDegree(ctx, key.degreeForMidi(note.pitch) + mode) - const hovering = !!state.contentState.mouse.hover && state.contentState.mouse.hover.id == note.id - const selected = state.appState.selection.contains(note.id) - const playing = state.appState.playback.playing && note.range.overlapsPoint(state.appState.playback.time) - TrackNotes.renderNote(state, ctx, note.range, row, xMin, xMax, fillStyle, hovering, selected, playing) - } - - const draw = state.trackState.draw - if (draw) - { - ctx.globalAlpha = 0.6 - - const key = Editor.keyAt(state.contentStateManager, state.trackState.trackId, draw.time1) - const row = TrackNotes.rowForPitch(state, draw.pitch, key) - const mode = key.scale.metadata!.mode - const fillStyle = CanvasUtils.fillStyleForDegree(ctx, key.degreeForMidi(draw.pitch) + mode) - TrackNotes.renderNote(state, ctx, new Range(draw.time1, draw.time2).sorted(), row, -Infinity, Infinity, fillStyle) - - ctx.globalAlpha = 1 - } - } - - - static renderNote(state: StateManager, ctx: CanvasRenderingContext2D, range: Range, row: number, xMin: number, xMax: number, fillStyle: any, hovering?: boolean, selected?: boolean, playing?: boolean) - { - const rect = TrackNotes.rectForNote(state, range, row, xMin, xMax) - - ctx.fillStyle = fillStyle - - ctx.beginPath() - ctx.fillRect(rect.x, rect.y, rect.w, rect.h) - - if (hovering) - { - ctx.globalAlpha = 0.4 - ctx.fillStyle = "#fff" - ctx.fillRect(rect.x, rect.y, rect.w, rect.h) - ctx.globalAlpha = 1 - } - - if (selected || playing) - { - const margin = 3 - ctx.globalAlpha = 0.6 - ctx.fillStyle = "#fff" - ctx.fillRect(rect.x, rect.y + margin, rect.w, rect.h - margin * 2) - ctx.globalAlpha = 1 - } - } -} \ No newline at end of file diff --git a/src_old/editor2/trackNotesPreview.ts b/src_old/editor2/trackNotesPreview.ts deleted file mode 100644 index a656402..0000000 --- a/src_old/editor2/trackNotesPreview.ts +++ /dev/null @@ -1,207 +0,0 @@ -import Editor from "./editor" -import Track from "./track" -import TrackStateManager from "./trackStateManager" -import TrackNotesPreviewState from "./TrackNotesPreviewState" -import Rect from "../util/rect" -import Range from "../util/range" -import Project from "../project/project2" -import Rational from "../util/rational" -import CanvasUtils from "../util/canvasUtils" -import * as Theory from "../theory/theory" - - -type StateManager = TrackStateManager - - -export default class TrackNotesPreview -{ - static init(state: StateManager) - { - Track.init(state) - state.mergeTrackState({ - type: "notesPreview", - rowScale: 2, - }) - } - - - static hover(state: StateManager) - { - state.mergeContentState({ - mouse: { ...state.contentState.mouse, - hover: null, - } - }) - } - - - static elemsAt(state: StateManager, region: any): Project.ID[] - { - const elems = [] - - if (region.y1 === undefined) - { - for (const note of TrackNotesPreview.iterNotesAtRange(state, region.range)) - elems.push(note.id) - } - else - { - for (const [note, keyCh, xMin, xMax] of TrackNotesPreview.iterNotesAndKeyChangesAtRange(state, region.range)) - { - const row = TrackNotesPreview.rowForPitch(state, note.pitch, keyCh.key) - const rect = TrackNotesPreview.rectForNote(state, note.range, row, xMin, xMax) - - if (rect.y1 < region.y2 && rect.y2 > region.y1) - elems.push(note.id) - } - } - - return elems - } - - - static *iterNotesAtRange(state: StateManager, range: Range): Generator - { - const trackElems = state.appState.project.rangedLists.get(state.trackState.trackId) - if (!trackElems) - return - - for (const note of trackElems.iterAtRange(range)) - yield note as Project.Note - } - - - static *iterKeyChangePairsAtRange(state: StateManager, range: Range): Generator<[Project.KeyChange, Project.KeyChange, number, number], void, void> - { - const keyChangeTrackId = Project.keyChangeTrackForTrack(state.appState.project, state.trackState.trackId) - const keyChangeTrackTimedElems = state.appState.project.timedLists.get(keyChangeTrackId) - if (!keyChangeTrackTimedElems) - return - - const firstKeyCh = keyChangeTrackTimedElems.findFirst() as (Project.KeyChange | null) - const defaultKey = firstKeyCh ? firstKeyCh.key : Editor.defaultKey() - - for (const pair of keyChangeTrackTimedElems.iterActiveAtRangePairwise(range)) - { - const keyCh1 = pair[0] || new Project.KeyChange(-1, range.start, defaultKey) - const keyCh2 = pair[1] || new Project.KeyChange(-1, range.end, defaultKey) - - const keyCh1X = Editor.xAtTime(state.contentStateManager, keyCh1.time) - const keyCh2X = Editor.xAtTime(state.contentStateManager, keyCh2.time) - - yield [keyCh1 as Project.KeyChange, keyCh2 as Project.KeyChange, keyCh1X, keyCh2X] - } - } - - - static *iterNotesAndKeyChangesAtRange(state: StateManager, range: Range): Generator<[Project.Note, Project.KeyChange, number, number], void, void> - { - for (const [keyCh1, keyCh2, keyCh1X, keyCh2X] of TrackNotesPreview.iterKeyChangePairsAtRange(state, range)) - { - const time1 = keyCh1.time.max(range.start)! - const time2 = keyCh2.time.min(range.end)! - - for (const note of TrackNotesPreview.iterNotesAtRange(state, new Range(time1, time2))) - yield [note, keyCh1, keyCh1X, keyCh2X] - } - } - - - static yForRow(state: StateManager, row: number): number - { - return state.trackState.h / 2 - (row + 1) * state.trackState.rowScale - } - - - static rowAtY(state: StateManager, y: number): number - { - return -Math.floor((y - state.trackState.h / 2) / state.trackState.rowScale) - 1 - } - - - static rowForPitch(state: StateManager, pitch: number, key: Theory.Key): number - { - const tonicRowOffset = Theory.Utils.chromaToDegreeInCMajor(key.tonic.chroma) - return key.octavedDegreeForMidi(pitch - 60) + tonicRowOffset - } - - - static pitchForRow(state: StateManager, row: number, key: Theory.Key): number - { - const tonicRowOffset = Theory.Utils.chromaToDegreeInCMajor(key.tonic.chroma) - return key.midiForDegree(row - Math.floor(tonicRowOffset)) + 60 - } - - - static rectForNote(state: StateManager, range: Range, row: number, xStart: number, xEnd: number) - { - const noteOrigX1 = Editor.xAtTime(state.contentStateManager, range.start) - const noteOrigX2 = Editor.xAtTime(state.contentStateManager, range.end) - - const noteY = Math.floor(TrackNotesPreview.yForRow(state, row)) - - let noteX1 = Math.max(noteOrigX1, xStart) - let noteX2 = Math.min(noteOrigX2, xEnd) - - const cutStart = noteOrigX1 < noteX1 - const cutEnd = noteOrigX2 > noteX2 - - //if (!cutStart) noteX1 += 1 - if (!cutEnd) noteX2 -= 1 - - noteX1 = Math.floor(noteX1) - noteX2 = Math.floor(noteX2) - - const noteW = Math.max(2, noteX2 - noteX1) - - return Object.assign( - new Rect(noteX1, noteY, noteW, state.trackState.rowScale), - { cutStart, cutEnd }) - } - - - static render(state: StateManager, ctx: CanvasRenderingContext2D) - { - const visibleRange = Editor.visibleTimeRange(state.contentStateManager) - - for (const [note, keyCh, xMin, xMax] of TrackNotesPreview.iterNotesAndKeyChangesAtRange(state, visibleRange)) - { - const key = keyCh.key - const row = TrackNotesPreview.rowForPitch(state, note.pitch, key) - const mode = key.scale.metadata!.mode - const fillStyle = CanvasUtils.fillStyleForDegree(ctx, key.degreeForMidi(note.pitch) + mode) - const hovering = !!state.contentState.mouse.hover && state.contentState.mouse.hover.id == note.id - const selected = state.appState.selection.contains(note.id) - const playing = state.appState.playback.playing && note.range.overlapsPoint(state.appState.playback.time) - TrackNotesPreview.renderNote(state, ctx, note.range, row, xMin, xMax, fillStyle, hovering, selected, playing) - } - } - - - static renderNote(state: StateManager, ctx: CanvasRenderingContext2D, range: Range, row: number, xMin: number, xMax: number, fillStyle: any, hovering?: boolean, selected?: boolean, playing?: boolean) - { - const rect = TrackNotesPreview.rectForNote(state, range, row, xMin, xMax) - - ctx.fillStyle = fillStyle - - ctx.beginPath() - ctx.fillRect(rect.x, rect.y, rect.w, rect.h) - - if (hovering) - { - ctx.globalAlpha = 0.4 - ctx.fillStyle = "#fff" - ctx.fillRect(rect.x, rect.y, rect.w, rect.h) - ctx.globalAlpha = 1 - } - - if (selected || playing) - { - const margin = 3 - ctx.globalAlpha = 0.6 - ctx.fillStyle = "#fff" - ctx.fillRect(rect.x, rect.y + margin, rect.w, rect.h - margin * 2) - ctx.globalAlpha = 1 - } - } -} \ No newline at end of file diff --git a/src_old/editor2/trackNotesPreviewState.ts b/src_old/editor2/trackNotesPreviewState.ts deleted file mode 100644 index 92d1b8b..0000000 --- a/src_old/editor2/trackNotesPreviewState.ts +++ /dev/null @@ -1,8 +0,0 @@ -import TrackState from "./trackState" -import Rational from "../util/rational" - - -export default interface TrackNotesPreviewState extends TrackState -{ - rowScale: number -} \ No newline at end of file diff --git a/src_old/editor2/trackNotesState.ts b/src_old/editor2/trackNotesState.ts deleted file mode 100644 index 936cc5e..0000000 --- a/src_old/editor2/trackNotesState.ts +++ /dev/null @@ -1,15 +0,0 @@ -import TrackState from "./trackState" -import Rational from "../util/rational" - - -export default interface TrackNotesState extends TrackState -{ - rowScale: number - - draw: null | - { - time1: Rational - time2: Rational - pitch: number - } -} \ No newline at end of file diff --git a/src_old/editor2/trackState.ts b/src_old/editor2/trackState.ts deleted file mode 100644 index f17792d..0000000 --- a/src_old/editor2/trackState.ts +++ /dev/null @@ -1,12 +0,0 @@ -export default interface TrackState -{ - type: string, - trackIndex: number, - trackId: number, - - y: number, - h: number, - yScroll: number, - - pinned: boolean, -} \ No newline at end of file diff --git a/src_old/editor2/trackStateManager.ts b/src_old/editor2/trackStateManager.ts deleted file mode 100644 index 2d80a50..0000000 --- a/src_old/editor2/trackStateManager.ts +++ /dev/null @@ -1,74 +0,0 @@ -import { AppState, ContentManager, AppReducer } from "../AppState" -import EditorState from "./editorState" -import TrackState from "./trackState" - - -export default class TrackStateManager -{ - contentStateManager: ContentManager - trackIndex: number - - - constructor(contentStateManager: ContentManager, trackIndex: number) - { - this.contentStateManager = contentStateManager - this.trackIndex = trackIndex - } - - - get appState(): AppState - { - return this.contentStateManager.appState - } - - - mergeAppState(newState: Partial) - { - this.contentStateManager.appState = AppReducer.merge(this.contentStateManager.appState, newState) - } - - - get contentState(): EditorState - { - return this.contentStateManager.contentState - } - - - set contentState(newState: EditorState) - { - this.contentStateManager.contentState = newState - } - - - mergeContentState(newState: Partial) - { - this.contentStateManager.mergeContentState(newState) - } - - - get trackState(): T - { - return this.contentStateManager.contentState.tracks[this.trackIndex] as any as T - } - - - set trackState(newState: T) - { - this.contentStateManager.mergeContentState({ - tracks: [ - ...this.contentStateManager.contentState.tracks.slice(0, this.trackIndex), - newState as any as TrackState, - ...this.contentStateManager.contentState.tracks.slice(this.trackIndex + 1), - ] - }) - } - - - mergeTrackState(newState: Partial) - { - this.trackState = { - ...this.trackState, - ...newState, - } - } -} \ No newline at end of file diff --git a/src_old/playback/PlaybackController.ts b/src_old/playback/PlaybackController.ts deleted file mode 100644 index cb0bd64..0000000 --- a/src_old/playback/PlaybackController.ts +++ /dev/null @@ -1,90 +0,0 @@ -import React from "react" -import { AppManager } from "../AppState" -import { Synth } from "./synth" -import SynthFeed from "./synthFeed" -import Rational from "../util/rational" - - -let synth: Synth | null = null - - -export function usePlaybackController(appManager: AppManager) -{ - const playData = React.useRef({ - playing: false, - timestamp: -1, - }) - - function processFrame(timestamp: number) - { - if (!appManager.appState.playback.playing && playData.current.playing) - { - requestAnimationFrame(() => - { - playData.current.playing = false - - if (synth) - synth.stopAll() - - appManager.mergeAppState({ - playback: { ...appManager.appState.playback, - playing: false, - } - }) - appManager.dispatch() - }) - return - } - - const prevTimestamp = playData.current.timestamp - const deltaTime = (prevTimestamp < 0 ? 0 : timestamp - prevTimestamp) - playData.current.timestamp = timestamp - - if (deltaTime > 0 && deltaTime < 250) - { - const measuresPerSecond = (appManager.appState.project.baseBpm / 4 / 60) - - synth!.process(deltaTime / 1000) - const timeAsFloat = appManager.appState.playback.timeAsFloat + deltaTime / 1000 * measuresPerSecond - appManager.mergeAppState({ - playback: { ...appManager.appState.playback, - timeAsFloat, - time: Rational.fromFloat(timeAsFloat, 64), - } - }) - appManager.dispatch() - } - - requestAnimationFrame(processFrame) - } - - if (appManager.appState.playback.playing && !playData.current.playing) - { - playData.current.playing = true - playData.current.timestamp = -1 - - requestAnimationFrame(() => - { - if (!synth) - synth = new Synth() - - synth.reset() - SynthFeed.feed(appManager.appState.project, synth, appManager.appState.playback.timeStart) - synth.play() - - appManager.mergeAppState({ - playback: { ...appManager.appState.playback, - time: appManager.appState.playback.timeStart, - timeAsFloat: appManager.appState.playback.timeStart.asFloat(), - } - }) - appManager.dispatch() - - requestAnimationFrame(async () => - { - await synth!.prepare(appManager.appState.sflib, appManager.appState.project) - requestAnimationFrame(processFrame) - }) - }) - } -} \ No newline at end of file diff --git a/src_old/playback/instrumentSflib.ts b/src_old/playback/instrumentSflib.ts deleted file mode 100644 index 57d59f3..0000000 --- a/src_old/playback/instrumentSflib.ts +++ /dev/null @@ -1,60 +0,0 @@ -import { Synth } from "./synth" -import Project from "../project/project2" -import { SflibInstrument, SflibMeta } from "./library" -import { sflibGetInstrument } from "./libraryCache" - - -export class InstrumentSflib -{ - synth: Synth - trackId: number - audioWorklet?: AudioWorkletNode - - - constructor(synth: Synth, trackId: number) - { - this.synth = synth - this.trackId = trackId - } - - - async prepare(sflibMeta: SflibMeta, trackInstrument: Project.TrackInstrumentSflib) - { - const instrument = await sflibGetInstrument(sflibMeta, trackInstrument.collectionId, trackInstrument.instrumentId) - if (!instrument) - throw "missing sflib instrument" - - this.audioWorklet = new AudioWorkletNode(this.synth.audioCtx, "SflibAudioProcessor") - this.audioWorklet.connect(this.synth.audioCtxOutput) - this.audioWorklet.port.postMessage({ - type: "setInstrument", - outputSampleRate: this.synth.audioCtx.sampleRate, - instrument - }) - } - - - destroy() - { - this.audioWorklet!.disconnect() - this.audioWorklet = undefined - } - - - noteOn(midiPitch: number) - { - this.audioWorklet!.port.postMessage({ type: "noteOn", midiPitch }) - } - - - noteOff(midiPitch: number) - { - this.audioWorklet!.port.postMessage({ type: "noteOff", midiPitch }) - } - - - stop() - { - this.audioWorklet!.port.postMessage({ type: "stopAll" }) - } -} \ No newline at end of file diff --git a/src_old/playback/library.ts b/src_old/playback/library.ts deleted file mode 100644 index b66f65e..0000000 --- a/src_old/playback/library.ts +++ /dev/null @@ -1,91 +0,0 @@ -import React from "react" -import { AppManager } from "../AppState" - - -export const sflibUrl = "/library/" - - -export interface SflibMeta -{ - ready: boolean - collections: SflibCollectionMeta[] -} - - -export interface SflibCollectionMeta -{ - id: string - name: string - instruments: SflibInstrumentMeta[] -} - - -export interface SflibInstrumentMeta -{ - id: string - filename: string - filesize: number - name: string - midiBank: number - midiPreset: number -} - - -export interface SflibInstrument -{ - id: string - name: string - midiPreset: number - midiBank: number - zones: SflibInstrumentZone[] - samples: string[] -} - - -export interface SflibInstrumentZone -{ - sampleIndex: number - sampleRate: number - startLoop: number - endLoop: number - loopMode: string - - minPitch: number - maxPitch: number - basePitch: number - minVel: number - maxVel: number - - delayVolEnv?: number - attackVolEnv?: number - holdVolEnv?: number - decayVolEnv?: number - sustainVolEnv?: number - releaseVolEnv?: number - exclusiveClass?: number -} - - -async function loadSflibMeta(): Promise -{ - const data = await fetch(sflibUrl + "library.json") - const meta: SflibMeta = await data.json() - meta.ready = true - console.log("loaded sflib meta", meta) - return meta -} - - -export function useSoundfontLibrary(appManager: AppManager) -{ - React.useEffect(() => - { - window.requestAnimationFrame(async () => - { - const sflib = await loadSflibMeta() - appManager.mergeAppState({ sflib }) - appManager.dispatch() - }) - - }, []) -} \ No newline at end of file diff --git a/src_old/playback/libraryCache.ts b/src_old/playback/libraryCache.ts deleted file mode 100644 index 661bc65..0000000 --- a/src_old/playback/libraryCache.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { sflibUrl, SflibMeta, SflibInstrument } from "./library" -import zlib from "zlib" - - -const sflibCache = new Map() - - -export async function sflibGetInstrument(meta: SflibMeta, collectionId: string, instrumentId: string): Promise -{ - const id = collectionId + "/" + instrumentId - const cached = sflibCache.get(id) - if (cached) - return cached - - const collMeta = meta.collections.find(c => c.id == collectionId) - if (!collMeta) - return null - - const instrMeta = collMeta.instruments.find(i => i.id == instrumentId) - if (!instrMeta) - return null - - const instrFilename = instrMeta.filename - - const data = await fetch(sflibUrl + collectionId + "/" + instrFilename) - const dataCompressed = await data.arrayBuffer() - console.log(dataCompressed) - const instr: SflibInstrument = JSON.parse(zlib.inflateSync(Buffer.from(dataCompressed)).toString("utf8")) - - console.log("loaded sflib instrument", instr) - - sflibCache.set(id, instr) - return instr -} \ No newline at end of file diff --git a/src_old/playback/sflibWorklet.ts b/src_old/playback/sflibWorklet.ts deleted file mode 100644 index 8a11180..0000000 --- a/src_old/playback/sflibWorklet.ts +++ /dev/null @@ -1,267 +0,0 @@ -import { SflibInstrument, SflibInstrumentZone } from "./library" - - -interface AudioWorkletProcessor -{ - readonly port: MessagePort - process(inputs: Float32Array[][], outputs: Float32Array[][], parameters: Record): boolean -} - -declare var AudioWorkletProcessor: -{ - prototype: AudioWorkletProcessor - new (options?: AudioWorkletNodeOptions): AudioWorkletProcessor -} - -declare function registerProcessor( - name: string, - processorCtor: (new (options?: AudioWorkletNodeOptions) => AudioWorkletProcessor) & { parameterDescriptors?: AudioParamDescriptor[] } -): void - - -function lerp(a: number, b: number, t: number): number -{ - return a + (b - a) * t -} - - -function sampleEnvelopeStart(envelope: SflibInstrumentZone, time: number): number -{ - const delay = envelope.delayVolEnv || 0 - if (time < delay) - return 0 - - time -= delay - const attack = envelope.attackVolEnv || 0 - if (time < attack) - return time / attack - - time -= attack - const hold = envelope.holdVolEnv || 0 - if (time < hold) - return 1 - - time -= hold - const decay = envelope.decayVolEnv || 0 - const sustain = envelope.sustainVolEnv || 1 - if (time < decay) - return lerp(1, sustain, time / decay) - - return sustain -} - - -function sampleEnvelope(envelope: SflibInstrumentZone, time: number, isOff: boolean, offTime: number): number -{ - const envelopeStart = sampleEnvelopeStart(envelope, time) - - if (!isOff) - { - return envelopeStart - } - else - { - time -= offTime - const release = envelope.releaseVolEnv || 0 - if (time < release) - return lerp(envelopeStart, 0, time / release) - - return 0 - } -} - - -interface Instrument extends SflibInstrument -{ - sampleBuffers: number[][] -} - - -interface PlayingNote -{ - on: boolean - offTime: number - time: number - sampleHead: number - midiPitch: number - zone: SflibInstrumentZone -} - - -class SflibAudioProcessor extends AudioWorkletProcessor -{ - outputSampleRate: number - instrument: Instrument | null - notes: PlayingNote[] - - - constructor(options?: AudioWorkletNodeOptions) - { - super(options) - - this.outputSampleRate = 1 - this.instrument = null - this.notes = [] - - this.port.onmessage = (e) => - { - //console.log(e.data) - - if (e.data.type == "setInstrument") - { - this.outputSampleRate = e.data.outputSampleRate - - this.instrument = e.data.instrument - this.instrument!.sampleBuffers = [] - - for (const sampleRaw of this.instrument!.samples) - { - //console.log(sampleRaw) - const buffer = Buffer.from(sampleRaw, "base64") - //console.log(buffer) - - const array = new Array(buffer.length / 2) - for (let i = 0; i < buffer.length / 2; i++) - { - const b0 = buffer[i * 2] - const b1 = buffer[i * 2 + 1] - let uint16 = (b0 << 8) | b1 - if ((uint16 & 0x8000) != 0) - uint16 = -(0x10000 - uint16) - - array[i] = uint16 / 0x8000 - } - - //console.log(array) - this.instrument!.sampleBuffers.push(array) - } - } - - else if (e.data.type == "noteOn") - { - for (const oldNote of this.notes) - { - if (oldNote.on && oldNote.midiPitch == e.data.midiPitch) - { - oldNote.on = false - oldNote.offTime = oldNote.time - } - } - - const zone = this.instrument!.zones.find( - z => e.data.midiPitch >= z.minPitch && e.data.midiPitch <= z.maxPitch) - - if (zone) - { - const note: PlayingNote = - { - on: true, - offTime: 0, - time: 0, - sampleHead: 0, - midiPitch: e.data.midiPitch, - zone, - } - - this.notes.push(note) - } - } - - else if (e.data.type == "noteOff") - { - for (const note of this.notes) - { - if (note.on && note.midiPitch == e.data.midiPitch) - { - note.on = false - note.offTime = note.time - } - } - } - - else if (e.data.type == "stopAll") - { - for (const note of this.notes) - { - if (note.on) - { - note.on = false - note.offTime = note.time - } - } - } - - //delete this.instrument.samples - } - } - - - process(inputs: Float32Array[][], outputs: Float32Array[][], parameters: Record): boolean - { - for (let n = this.notes.length - 1; n >= 0; n--) - { - const note = this.notes[n] - const output = outputs[0] - const zone = note.zone - const sampleData = this.instrument!.sampleBuffers[zone.sampleIndex] - const rateConversion = zone.sampleRate / this.outputSampleRate - const pitchConversion = midiToHertz(note.midiPitch) / midiToHertz(zone.basePitch) - - const getSampleAt = (index: number): number => - { - const indexBefore = Math.floor(index) - const indexAfter = Math.ceil(index) - - const indexFraction = (index - indexBefore) - - const sampleBefore = sampleData[indexBefore % sampleData.length] - const sampleAfter = sampleData[indexAfter % sampleData.length] - - const sampleInterp = sampleBefore + (sampleAfter - sampleBefore) * indexFraction - //console.log(index, indexFraction, sampleBefore, sampleAfter, sampleInterp) - - return sampleInterp - } - - for (let i = 0; i < output[0].length; i++) - { - const envelope = sampleEnvelope(zone, note.time, !note.on, note.offTime) - const sample = Math.pow(envelope, 4) * getSampleAt(note.sampleHead * rateConversion * pitchConversion) - - for (let c = 0; c < output.length; c++) - { - output[c][i] += sample * 0.25 - } - - note.time += 1 / this.outputSampleRate - - if (zone.loopMode == "noLoop") - { - if (note.sampleHead * rateConversion * pitchConversion < sampleData.length - 1) - note.sampleHead += 1 - } - else - { - note.sampleHead += 1 - if (note.sampleHead * rateConversion * pitchConversion >= zone.endLoop - 1) - note.sampleHead -= (zone.endLoop - zone.startLoop - 1) / rateConversion / pitchConversion - } - } - - if (!note.on && note.time - note.offTime > (zone.releaseVolEnv || 0)) - this.notes = [...this.notes.slice(0, n), ...this.notes.slice(n + 1)] - } - - return true - } -} - - -function midiToHertz(midi: number): number -{ - return Math.pow(2, (midi - 69) / 12) * 440 -} - - -registerProcessor("SflibAudioProcessor", SflibAudioProcessor) -console.log("registered SflibAudioProcessor") \ No newline at end of file diff --git a/src_old/playback/synth.ts b/src_old/playback/synth.ts deleted file mode 100644 index 63b010c..0000000 --- a/src_old/playback/synth.ts +++ /dev/null @@ -1,168 +0,0 @@ -import Project from "../project/project2" -import { sflibGetInstrument } from "./libraryCache" -import { AppState } from "../AppState" -import { SflibMeta } from "./library" -import { InstrumentSflib } from "./instrumentSflib" - - -interface NoteEvent -{ - trackId: number - time: number - midiPitch: number - volume: number - duration: number -} - - -interface PlayingNote -{ - event: NoteEvent - remainingDuration: number -} - - -export class Synth -{ - audioCtx: AudioContext - audioCtxOutput: GainNode - audioTracks: InstrumentSflib[] - - cachedTracks: Project.Track[] = [] - - time: number - noteEvents: NoteEvent[] - playingNotes: PlayingNote[] - - - constructor() - { - this.audioCtx = new AudioContext() - this.audioTracks = [] - - this.audioCtxOutput = this.audioCtx.createGain() - this.audioCtxOutput.connect(this.audioCtx.destination) - this.audioCtxOutput.gain.value = 0.25 - - this.time = 0 - this.noteEvents = [] - this.playingNotes = [] - } - - - destroy() - { - this.audioCtx.close() - } - - - reset() - { - } - - - async prepare(sflibMeta: SflibMeta, project: Project) - { - await this.audioCtx.audioWorklet.addModule("/build/sflibWorklet.js") - - if (this.cachedTracks !== project.tracks) - { - for (const audioTrack of this.audioTracks) - audioTrack.destroy() - - this.audioTracks = [] - - for (const track of project.tracks) - { - if (track.trackType == Project.TrackType.Notes) - { - const trackNotes = track - if (trackNotes.instrument.instrumentType == Project.TrackInstrumentType.Sflib) - { - const audioTrack = new InstrumentSflib(this, track.id) - await audioTrack.prepare(sflibMeta, trackNotes.instrument) - this.audioTracks.push(audioTrack) - } - } - } - } - - this.cachedTracks = project.tracks - } - - - isFinished() - { - return this.noteEvents.length == 0 && this.playingNotes.length == 0 - } - - - play() - { - this.noteEvents.sort(function (a, b) { return a.time - b.time }) - } - - - stopAll() - { - this.time = 0 - this.noteEvents = [] - this.playingNotes = [] - - for (const audioTrack of this.audioTracks) - audioTrack.stop() - } - - - process(deltaTime: number) - { - this.time += deltaTime - - // Update audio output. - for (let i = this.playingNotes.length - 1; i >= 0; i--) - { - const note = this.playingNotes[i] - note.remainingDuration -= deltaTime - if (note.remainingDuration <= 0) - { - this.audioTracks.find(t => t.trackId == note.event.trackId)!.noteOff(note.event.midiPitch) - this.playingNotes.splice(i, 1) - } - } - - // Process pending note events up to the current time. - let noteEventsProcessed = 0 - while (noteEventsProcessed < this.noteEvents.length && - this.noteEvents[noteEventsProcessed].time <= this.time) - { - const ev = this.noteEvents[noteEventsProcessed] - noteEventsProcessed++ - - this.audioTracks.find(t => t.trackId == ev.trackId)!.noteOn(ev.midiPitch) - - const playingNote = - { - event: ev, - remainingDuration: ev.duration, - } - - this.playingNotes.push(playingNote) - } - - // Remove processed events. - this.noteEvents.splice(0, noteEventsProcessed) - } - - - addNoteEvent(trackId: number, time: number, midiPitch: number, volume: number, duration: number) - { - this.noteEvents.push( - { - trackId, - time: time + this.time, - midiPitch, - volume, - duration - }) - } -} \ No newline at end of file diff --git a/src_old/playback/synthFeed.ts b/src_old/playback/synthFeed.ts deleted file mode 100644 index 5029441..0000000 --- a/src_old/playback/synthFeed.ts +++ /dev/null @@ -1,56 +0,0 @@ -import Rational from "../util/rational" -import Project from "../project/project2" -import { Synth } from "./synth" -import MathUtils from "../util/mathUtils" - - -export default class SynthFeed -{ - static feed(project: Project, synth: Synth, startTick: Rational, useChordPatterns = true) - { - const chordVolumeMul = 0.5 - - const addNoteEvent = (trackId: number, tickStart: Rational, duration: Rational, midiPitch: number, volume: number) => - { - const offsetStart = tickStart.subtract(startTick) - - const measuresPerSecond = project.baseBpm / 4 / 60 - - let timeStart = offsetStart.asFloat() / measuresPerSecond - let timeDuration = duration.asFloat() / measuresPerSecond - - if (timeStart <= 0) - { - timeDuration += timeStart - timeStart = 0 - } - - if (timeDuration <= 0) - return - - //console.log("event note [" + pitch + "] at tick " + tickStart.toString() + " = " + timeStart + " s") - synth.addNoteEvent(trackId, timeStart, midiPitch, volume, timeDuration) - } - - // Register notes. - for (const track of project.tracks) - { - const rangedList = project.rangedLists.get(track.id) - if (!rangedList) - continue - - for (const elem of rangedList.iterAll()) - { - if (elem.type != Project.ElementType.Note) - continue - - const note = elem - - if (note.range.end.compare(startTick) <= 0) - continue - - addNoteEvent(track.id, note.range.start, note.range.duration, note.pitch, 1) - } - } - } -} diff --git a/src_old/popup/MenuBar.tsx b/src_old/popup/MenuBar.tsx deleted file mode 100644 index a13b36a..0000000 --- a/src_old/popup/MenuBar.tsx +++ /dev/null @@ -1,79 +0,0 @@ -import React from "react" -import Rect from "../util/rect" -import { useAppManager } from "../AppContext" -import Project from "../project/project2" -import { AppReducer } from "../AppState" -import MenuFilePopup from "./MenuFilePopup" -import PlaybackToolbox from "./PlaybackToolbox" - - -interface MenuBarProps -{ -} - - -interface MenuBarItemProps -{ - label: string - popupElem: any -} - - -function MenuBarItem(props: MenuBarItemProps) -{ - const appManager = useAppManager() - - const refButton = React.useRef(null) - const onClick = () => - { - const domRect = refButton.current!.getBoundingClientRect() - const rect = new Rect(domRect.x, domRect.y, domRect.width, domRect.height) - appManager.appState = AppReducer.createPopup( - appManager.appState, - rect, - props.popupElem, - {}) - appManager.dispatch() - } - - const isOpen = - appManager.appState.popup != null && - appManager.appState.popup.elem == props.popupElem - - return -} - - -export default function MenuBar(props: MenuBarProps) -{ - return
- - - - - -
-} \ No newline at end of file diff --git a/src_old/popup/MenuFilePopup.tsx b/src_old/popup/MenuFilePopup.tsx deleted file mode 100644 index ed4d191..0000000 --- a/src_old/popup/MenuFilePopup.tsx +++ /dev/null @@ -1,80 +0,0 @@ -import React from "react" -import PopupButton from "../popup/PopupButton" -import Rect from "../util/rect" -import { useAppManager } from "../AppContext" -import Project from "../project/project2" -import IoMidi from "../project/ioMidi" - - -interface MenuFilePopupProps -{ -} - - -export default function MenuFilePopup(props: MenuFilePopupProps) -{ - const appManager = useAppManager() - - const doNew = () => - { - appManager.mergeAppState({ project: Project.getDefault() }) - appManager.dispatch() - } - - const doOpen = () => - { - document.getElementById("gInputFileOpen")!.click() - } - - React.useEffect(() => - { - const handleOpenFile = (ev: Event) => - { - const elem = ev.target as HTMLInputElement - - if (elem.files!.length != 1) - return - - let reader = new FileReader() - reader.readAsArrayBuffer(elem.files![0]) - reader.onload = () => - { - const bytes = new Uint8Array(reader.result as any) - - const project = IoMidi.read(bytes) - appManager.mergeAppState({ project }) - appManager.dispatch() - } - } - - const input = document.getElementById("gInputFileOpen") as HTMLInputElement - input!.addEventListener("change", handleOpenFile) - - return () => - { - //input!.removeEventListener("change", handleOpenFile) - } - - }, []) - - return <> - - - - - - - - - -} \ No newline at end of file diff --git a/src_old/popup/PlaybackToolbox.tsx b/src_old/popup/PlaybackToolbox.tsx deleted file mode 100644 index 65b3cb9..0000000 --- a/src_old/popup/PlaybackToolbox.tsx +++ /dev/null @@ -1,56 +0,0 @@ -import React from "react" -import { useAppManager } from "../AppContext" - - -export default function PlaybackToolbox(props: {}) -{ - const appManager = useAppManager() - const togglePlayback = () => - { - appManager.mergeAppState({ playback: { ...appManager.appState.playback, - playing: !appManager.appState.playback.playing, - }}) - appManager.dispatch() - } - - - React.useEffect(() => - { - const handleSpacebarKey = (ev: KeyboardEvent) => - { - if (ev.key == " ") - { - ev.preventDefault() - ev.stopPropagation() - togglePlayback() - } - } - - window.addEventListener("keydown", handleSpacebarKey) - - return () => - { - window.removeEventListener("keydown", handleSpacebarKey) - } - - }, []) - - - return
- - - -
-} \ No newline at end of file diff --git a/src_old/project/ioCompressedStr.js b/src_old/project/ioCompressedStr.js deleted file mode 100644 index 67083b0..0000000 --- a/src_old/project/ioCompressedStr.js +++ /dev/null @@ -1,333 +0,0 @@ -import Project from "./project.js" -import Theory from "../theory.ts" -import { URLBinaryEncoder, URLBinaryDecoder } from "../util/urlBinaryEncoder.js" -import Rational from "../util/rational.js" -import Range from "../util/range.js" - - -export default class IOCompressedStr -{ - static read(str) - { - let song = new Project() - - if (!str.startsWith("3_") && - !str.startsWith("4_")) - throw "unsupported version" - - const r = new URLBinaryDecoder(str.substr(2)) - - // Read header. - const version = r.readInteger() - song.baseBpm = r.readInteger() - - if (version != 3 && version != 4) - throw "unsupported version" - - // Read note tracks. - const trackNum = r.readInteger() - for (let j = 0; j < trackNum; j++) - { - // Read note data as structure-of-arrays. - const noteNum = r.readInteger() - - let noteData = [] - - let prevNoteRangeStartInteger = 0 - for (let i = 0; i < noteNum; i++) - { - prevNoteRangeStartInteger += r.readInteger() // range.start.integer - noteData[i] = [ prevNoteRangeStartInteger ] - } - - for (let i = 0; i < noteNum; i++) - noteData[i][1] = r.readInteger() // range.start.numeratorWithoutInteger - - for (let i = 0; i < noteNum; i++) - noteData[i][2] = r.readInteger() // range.start.denominator - - for (let i = 0; i < noteNum; i++) - noteData[i][3] = r.readInteger() // range.duration.numerator - - for (let i = 0; i < noteNum; i++) - noteData[i][4] = r.readInteger() // range.duration.denominator - - for (let i = 0; i < noteNum; i++) - { - const start = Rational.fromIntegerPlusRational(noteData[i][0], noteData[i][1], noteData[i][2]) - const duration = new Rational(noteData[i][3], noteData[i][4]) - - song = song.upsertNote(new Project.Note( - Range.fromStartDuration(start, duration), - r.readInteger() + 60)) - } - } - - // Read chord data as structure-of-arrays. - const chordNum = r.readInteger() - - let chordData = [] - - let prevChordRangeStartInteger = 0 - for (let i = 0; i < chordNum; i++) - { - prevChordRangeStartInteger += r.readInteger() // range.start.integer - chordData[i] = [ prevChordRangeStartInteger ] - } - - for (let i = 0; i < chordNum; i++) - chordData[i][1] = r.readInteger() // range.start.numeratorWithoutInteger - - for (let i = 0; i < chordNum; i++) - chordData[i][2] = r.readInteger() // range.start.denominator - - for (let i = 0; i < chordNum; i++) - chordData[i][3] = r.readInteger() // range.duration.numerator - - for (let i = 0; i < chordNum; i++) - chordData[i][4] = r.readInteger() // range.duration.denominator - - for (let i = 0; i < chordNum; i++) - chordData[i][5] = r.readInteger() // chord.rootMidi - - for (let i = 0; i < chordNum; i++) - chordData[i][6] = r.readInteger() // chord.rootAccidental - - for (let i = 0; i < chordNum; i++) - chordData[i][7] = r.readString() // chord.kindId - - if (version >= 4) - { - for (let i = 0; i < chordNum; i++) - chordData[i][8] = r.readInteger() // chord.inversion - } - - for (let i = 0; i < chordNum; i++) - chordData[i][9] = r.readInteger() // chord.modifiers.length - - if (version >= 4) - { - for (let i = 0; i < chordNum; i++) - { - chordData[i][10] = [] - for (let mod = 0; mod < chordData[i][9]; mod++) - chordData[i][10].push([r.readString(), r.readInteger()]) // chord.modifiers[mod] - } - } - else - { - for (let i = 0; i < chordNum; i++) - for (let mod = 0; mod < chordData[i][8]; mod++) - r.readString() - } - - for (let i = 0; i < chordNum; i++) - { - const start = Rational.fromIntegerPlusRational(chordData[i][0], chordData[i][1], chordData[i][2]) - const duration = new Rational(chordData[i][3], chordData[i][4]) - - song = song.upsertChord(new Project.Chord( - Range.fromStartDuration(start, duration), - new Theory.Chord( - chordData[i][5], - chordData[i][6], - Theory.Chord.kindFromId(chordData[i][7]), - chordData[i][8], - chordData[i][10]))) - } - - // Read key change data as structure-of-arrays. - const keyChangeNum = r.readInteger() - - let keyChangeData = [] - - for (let i = 0; i < keyChangeNum; i++) - keyChangeData[i] = [ r.readInteger() ] // time.numerator - - for (let i = 0; i < keyChangeNum; i++) - keyChangeData[i][1] = r.readInteger() // time.denominator - - for (let i = 0; i < keyChangeNum; i++) - keyChangeData[i][2] = r.readInteger() // key.tonic.letter - - for (let i = 0; i < keyChangeNum; i++) - keyChangeData[i][3] = r.readInteger() // key.tonic.accidental - - for (let i = 0; i < keyChangeNum; i++) - keyChangeData[i][4] = r.readInteger() // key.scale.chromas.length - - for (let i = 0; i < keyChangeNum; i++) - { - keyChangeData[i][5] = [] - for (let p = 0; p < keyChangeData[i][4]; p++) - keyChangeData[i][5][p] = r.readInteger() // key.scale.chromas[p] - } - - for (let i = 0; i < keyChangeNum; i++) - { - const time = new Rational(keyChangeData[i][0], keyChangeData[i][1]) - - song = song.upsertKeyChange(new Project.KeyChange( - time, - new Theory.Key( - new Theory.PitchName(keyChangeData[i][2], keyChangeData[i][3]), - Theory.Scale.fromChromas(keyChangeData[i][5])))) - } - - // Read meter change data as structure-of-arrays. - const meterChangeNum = r.readInteger() - - let meterChangeData = [] - - for (let i = 0; i < meterChangeNum; i++) - meterChangeData[i] = [ r.readInteger() ] // time.numerator - - for (let i = 0; i < meterChangeNum; i++) - meterChangeData[i][1] = r.readInteger() // time.denominator - - for (let i = 0; i < meterChangeNum; i++) - meterChangeData[i][2] = r.readInteger() // numerator - - for (let i = 0; i < meterChangeNum; i++) - meterChangeData[i][3] = r.readInteger() // denominator - - for (let i = 0; i < meterChangeNum; i++) - { - const time = new Rational(meterChangeData[i][0], meterChangeData[i][1]) - - song = song.upsertMeterChange(new Project.MeterChange( - time, - new Theory.Meter(meterChangeData[i][2], meterChangeData[i][3]))) - } - - return song.withRefreshedRange() - } - - - static write(project) - { - const song = project.withRefreshedRange() - - const w = new URLBinaryEncoder() - - // Write header. - w.writeInteger(4) // Version - w.writeInteger(project.baseBpm) - - // Write note tracks. - const trackNum = 1 - w.writeInteger(trackNum) - for (var j = 0; j < trackNum; j++) - { - // Write note data as a structure-of-arrays. - w.writeInteger(project.notes.size) - - let prevNoteRangeStartInteger = 0 - for (const note of project.notes.iterAll()) - { - w.writeInteger(note.range.start.integer - prevNoteRangeStartInteger) - prevNoteRangeStartInteger = note.range.start.integer - } - - for (const note of project.notes.iterAll()) - w.writeInteger(note.range.start.numeratorWithoutInteger) - - for (const note of project.notes.iterAll()) - w.writeInteger(note.range.start.denominator) - - for (const note of project.notes.iterAll()) - w.writeInteger(note.range.duration.numerator) - - for (const note of project.notes.iterAll()) - w.writeInteger(note.range.duration.denominator) - - for (const note of project.notes.iterAll()) - w.writeInteger(note.pitch - 60) - } - - // Write chord data as a structure-of-arrays. - w.writeInteger(project.chords.size) - - let prevChordRangeStartInteger = 0 - for (const chord of project.chords.iterAll()) - { - w.writeInteger(chord.range.start.integer - prevChordRangeStartInteger) - prevChordRangeStartInteger = chord.range.start.integer - } - - for (const chord of project.chords.iterAll()) - w.writeInteger(chord.range.start.numeratorWithoutInteger) - - for (const chord of project.chords.iterAll()) - w.writeInteger(chord.range.start.denominator) - - for (const chord of project.chords.iterAll()) - w.writeInteger(chord.range.duration.numerator) - - for (const chord of project.chords.iterAll()) - w.writeInteger(chord.range.duration.denominator) - - for (const chord of project.chords.iterAll()) - w.writeInteger(chord.chord.rootMidi) - - for (const chord of project.chords.iterAll()) - w.writeInteger(chord.chord.rootAccidental) - - for (const chord of project.chords.iterAll()) - w.writeString(chord.chord.kindId) - - for (const chord of project.chords.iterAll()) - w.writeInteger(chord.chord.inversion) - - for (const chord of project.chords.iterAll()) - w.writeInteger(chord.chord.modifiers.length) - - for (const chord of project.chords.iterAll()) - for (const mod of chord.chord.modifiers) - { - w.writeString(mod[0]) - w.writeInteger(mod[1]) - } - - // Write key change data as a structure-of-arrays. - w.writeInteger(project.keyChanges.size) - - for (const keyChange of project.keyChanges.iterAll()) - w.writeInteger(keyChange.time.numerator) - - for (const keyChange of project.keyChanges.iterAll()) - w.writeInteger(keyChange.time.denominator) - - for (const keyChange of project.keyChanges.iterAll()) - w.writeInteger(keyChange.key.tonic.letter) - - for (const keyChange of project.keyChanges.iterAll()) - w.writeInteger(keyChange.key.tonic.accidental) - - for (const keyChange of project.keyChanges.iterAll()) - w.writeInteger(keyChange.key.scale.chromas.length) - - for (const keyChange of project.keyChanges.iterAll()) - for (const p of keyChange.key.scale.chromas) - w.writeInteger(p) - - // Write meter change data as a structure-of-arrays. - w.writeInteger(project.meterChanges.size) - - for (const meterChange of project.meterChanges.iterAll()) - w.writeInteger(meterChange.time.numerator) - - for (const meterChange of project.meterChanges.iterAll()) - w.writeInteger(meterChange.time.denominator) - - for (const meterChange of project.meterChanges.iterAll()) - w.writeInteger(meterChange.meter.numerator) - - for (const meterChange of project.meterChanges.iterAll()) - w.writeInteger(meterChange.meter.denominator) - - - console.log(w.getCompressedURLSafe().length) - return "4_" + w.getCompressedURLSafe() - } -} \ No newline at end of file diff --git a/src_old/project/ioJson.js b/src_old/project/ioJson.js deleted file mode 100644 index 9aa5284..0000000 --- a/src_old/project/ioJson.js +++ /dev/null @@ -1,191 +0,0 @@ -import Project from "./project.js" -import Theory from "../theory.ts" -import Rational from "../util/rational.js" -import Range from "../util/range.js" - - -export default class IOJson -{ - static write(project) - { - let str = "{\n" - - str += "\"version\": " + 4 + ",\n" - str += "\"bpm\": " + project.baseBpm + ",\n" - str += "\n" - - str += "\"tracks\":\n" - str += "[\n" - for (let track = 0; track < 1; track++) - { - str += "{\n" - str += "\"notes\":\n" - str += "[\n" - let firstNote = true - for (const note of project.notes.iterAll()) - { - if (!firstNote) - str += ",\n" - - firstNote = false - - str += "[" - str += note.range.start .toJSONString() + "," - str += note.range.duration.toJSONString() + "," - str += note.pitch - str += "]" - } - str += "\n" - str += "]\n" - str += "}\n" - } - str += "],\n" - str += "\n" - - str += "\"chords\":\n" - str += "[\n" - let firstChord = true - for (const chord of project.chords.iterAll()) - { - if (!firstChord) - str += ",\n" - - firstChord = false - - str += "[" - str += chord.range.start .toJSONString() + "," - str += chord.range.duration.toJSONString() + "," - str += chord.chord.rootMidi + "," - str += chord.chord.rootAccidental + "," - str += "\"" + chord.chord.kindId + "\"," - str += chord.chord.inversion + "," - str += JSON.stringify(chord.chord.modifiers) - str += "]" - } - str += "\n" - str += "],\n" - str += "\n" - - str += "\"keyChanges\":\n" - str += "[\n" - let firstKeyChange = true - for (const keyChange of project.keyChanges.iterAll()) - { - if (!firstKeyChange) - str += ",\n" - - firstKeyChange = false - - str += "[" - str += keyChange.time.toJSONString() + "," - str += keyChange.key.tonic.letter + "," - str += keyChange.key.tonic.accidental + "," - str += "[" + keyChange.key.scale.chromas.join(",") + "]" - str += "]" - } - str += "\n" - str += "],\n" - str += "\n" - - str += "\"meterChanges\":\n" - str += "[\n" - let firstMeterChange = true - for (const meterChange of project.meterChanges.iterAll()) - { - if (!firstMeterChange) - str += ",\n" - - firstMeterChange = false - - str += "[" - str += meterChange.time.toJSONString() + "," - str += meterChange.meter.numerator + "," - str += meterChange.meter.denominator - str += "]" - } - str += "\n" - str += "]\n" - - str += "}" - return str - } - - - static read(json) - { - let song = new Project() - song.baseBpm = json.bpm - - for (const note of json.tracks[0].notes) - song = song.upsertNote(new Project.Note( - Range.fromStartDuration(Rational.fromArray(note[0]), Rational.fromArray(note[1])), - note[2])) - - if (json.version >= 4) - { - for (const chord of json.chords) - { - let modifiers = [] - for (const mod of chord[6]) - modifiers.push(mod) - - song = song.upsertChord(new Project.Chord( - Range.fromStartDuration(Rational.fromArray(chord[0]), Rational.fromArray(chord[1])), - new Theory.Chord(chord[2], chord[3], Theory.Chord.kindFromId(chord[4]), chord[5], modifiers))) - } - - for (const keyChange of json.keyChanges) - song = song.upsertKeyChange(new Project.KeyChange( - Rational.fromArray(keyChange[0]), - new Theory.Key( - new Theory.PitchName(keyChange[1], keyChange[2]), - Theory.Scale.fromChromas(keyChange[3])))) - } - else if (json.version == 3) - { - for (const chord of json.chords) - { - song = song.upsertChord(new Project.Chord( - Range.fromStartDuration(Rational.fromArray(chord[0]), Rational.fromArray(chord[1])), - new Theory.Chord(chord[2], chord[3], Theory.Chord.kindFromId(chord[4]), 0, []))) - } - - for (const keyChange of json.keyChanges) - song = song.upsertKeyChange(new Project.KeyChange( - Rational.fromArray(keyChange[0]), - new Theory.Key( - new Theory.PitchName( - Theory.Utils.chromaToLetter(keyChange[1] + keyChange[2]) || 0, - Theory.Utils.chromaToAccidental(keyChange[1] + keyChange[2]) || 0), - Theory.Scale.fromChromas(keyChange[3])))) - } - else - { - const oldChordKinds = - ["M","m","+","o","5","6","m6","7","maj7","m7","mmaj7","+7","+maj7","o7","%7","9","maj9","m9","mmaj9","9?","+9","+maj9","o9","ob9","%9","%b9"] - - for (const chord of json.chords) - song = song.upsertChord(new Project.Chord( - Range.fromStartDuration(Rational.fromArray(chord[0]), Rational.fromArray(chord[1])), - new Theory.Chord(chord[3], chord[4], Theory.Chord.kindFromId(oldChordKinds[chord[2]]), 0, []))) - - const oldScales = [ "Major", "Dorian", "Phrygian", "Lydian", "Mixolydian", "Minor", "Locrian" ] - - for (const keyChange of json.keyChanges) - song = song.upsertKeyChange(new Project.KeyChange( - Rational.fromArray(keyChange[0]), - new Theory.Key( - new Theory.PitchName( - Theory.Utils.chromaToLetter(keyChange[2] + keyChange[3]) || 0, - Theory.Utils.chromaToAccidental(keyChange[2] + keyChange[3]) || 0), - Theory.Scale.parse(oldScales[keyChange[1]])))) - } - - for (const meterChange of json.meterChanges) - song = song.upsertMeterChange(new Project.MeterChange( - Rational.fromArray(meterChange[0]), - new Theory.Meter(meterChange[1], meterChange[2]))) - - return song.withRefreshedRange() - } -} \ No newline at end of file diff --git a/src_old/project/ioMidi.js b/src_old/project/ioMidi.js deleted file mode 100644 index 39bbe4d..0000000 --- a/src_old/project/ioMidi.js +++ /dev/null @@ -1,110 +0,0 @@ -import Project from "./project.js" -import Theory from "../theory.ts" -import { MidiFile } from "../util/midi.js" -import Rational from "../util/rational.js" -import Range from "../util/range.js" - - -export default class IOMidi -{ - static read(bytes) - { - const midi = MidiFile.fromBytes(bytes) - console.log(midi) - - const findFirstEvent = (kind) => - { - for (const track of midi.tracks) - for (const ev of track.events) - if (ev.kind == kind) - return ev - - return null - } - - let song = new Project() - - const tempoEv = findFirstEvent("setTempo") - const msPerQuarterNote = (tempoEv ? tempoEv.msPerQuarterNote : 500000) - song.baseBpm = Math.round(60 * 1000 * 1000 / msPerQuarterNote) - - let notesToAdd = [] - for (const track of midi.tracks) - { - for (const noteOn of track.events) - { - if (noteOn.kind != "noteOn" || noteOn.channel == 9 || noteOn.velocity == 0) - continue - - let noteOff = null - for (const ev of track.events) - { - if (ev.kind != "noteOn" && ev.kind != "noteOff") - continue - - if (ev.time <= noteOn.time || ev.channel != noteOn.channel || ev.key != noteOn.key) - continue - - if (noteOff == null || ev.time < noteOff.time) - { - noteOff = ev - break - } - } - - if (!noteOff) - continue - - const onTick = Rational.fromFloat(noteOn.time / midi.ticksPerQuarterNote / 4, 27720) - const offTick = Rational.fromFloat(noteOff.time / midi.ticksPerQuarterNote / 4, 27720) - - notesToAdd.push(new Project.Note(new Range(onTick, offTick), noteOn.key)) - } - } - - song = song.upsertNotes(notesToAdd) - - for (const track of midi.tracks) - { - for (const ev of track.events) - { - if (ev.kind != "setKeySignature") - continue - - const tonicPitches = [ 0, 7, 2, 9, 4, 11, 5, 0, 7, 2, 9, 4, 11, 5, 0, 7, 2, 9, 4, 11] - const tonicLetters = [ 0, 4, 1, 5, 2, 6, 3, 0, 4, 1, 5, 2, 6, 3, 0, 4, 1, 5, 2, 6] - const tonicAccidentals = [-1, -1, -1, -1, -1, -1, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1] - const index = ev.accidentals + 7 + (ev.scale == 0 ? 0 : 2) - - if (index < 0 || index >= tonicPitches.length) - continue - - const time = Rational.fromFloat(ev.time / midi.ticksPerQuarterNote / 4, 27720) - song = song.upsertKeyChange(new Project.KeyChange(time, - new Theory.Key( - new Theory.PitchName(tonicLetters[index], tonicAccidentals[index]), - Theory.Scale.parse(ev.scale == 0 ? "Major" : "Minor")))) - } - } - - for (const track of midi.tracks) - { - for (const ev of track.events) - { - if (ev.kind != "setTimeSignature") - continue - - const time = Rational.fromFloat(ev.time / midi.ticksPerQuarterNote / 4, 27720) - song = song.upsertMeterChange(new Project.MeterChange(time, new Theory.Meter(ev.numerator, ev.denominator))) - } - } - - if (!song.keyChanges.findActiveAt(new Rational(0))) - song = song.upsertKeyChange(new Project.KeyChange(new Rational(0), Theory.Key.parse("C Major"))) - - if (!song.meterChanges.findActiveAt(new Rational(0))) - song = song.upsertMeterChange(new Project.MeterChange(new Rational(0), new Theory.Meter(4, 4))) - - return song.withRefreshedRange() - } -} \ No newline at end of file diff --git a/src_old/project/ioMidi.ts b/src_old/project/ioMidi.ts deleted file mode 100644 index 387c61e..0000000 --- a/src_old/project/ioMidi.ts +++ /dev/null @@ -1,120 +0,0 @@ -import Project from "../project/project2" -import * as Theory from "../theory/theory" -import { MidiFileReader } from "../util/midi" -import Rational from "../util/rational.js" -import Range from "../util/range.js" - - -export default class IoMidi -{ - static read(bytes: number[] | Buffer | Uint8Array): Project - { - const midi = MidiFileReader.fromBytes(bytes) - console.log(midi) - - const findFirstEvent = (kind: string) => - { - for (const track of midi.tracks) - for (const ev of track.events) - if (ev.kind == kind) - return ev - - return null - } - - let project = new Project() - - const tempoEv = findFirstEvent("setTempo") - const msPerQuarterNote = (tempoEv ? tempoEv.msPerQuarterNote : 500000) - project.baseBpm = Math.round(60 * 1000 * 1000 / msPerQuarterNote) - - const track1Id = project.nextId - project = Project.upsertTrack(project, new Project.TrackKeyChanges()) - - const track2Id = project.nextId - project = Project.upsertTrack(project, new Project.TrackMeterChanges()) - - project = Project.upsertTimedElement(project, new Project.KeyChange(track1Id, new Rational(0), Theory.Key.parse("D Major"))) - project = Project.upsertTimedElement(project, new Project.MeterChange(track2Id, new Rational(0), new Theory.Meter(4, 4))) - - /*for (const track of midi.tracks) - { - for (const ev of track.events) - { - if (ev.kind != "setKeySignature") - continue - - const tonicPitches = [ 0, 7, 2, 9, 4, 11, 5, 0, 7, 2, 9, 4, 11, 5, 0, 7, 2, 9, 4, 11] - const tonicLetters = [ 0, 4, 1, 5, 2, 6, 3, 0, 4, 1, 5, 2, 6, 3, 0, 4, 1, 5, 2, 6] - const tonicAccidentals = [-1, -1, -1, -1, -1, -1, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1] - const index = ev.accidentals + 7 + (ev.scale == 0 ? 0 : 2) - - if (index < 0 || index >= tonicPitches.length) - continue - - const time = Rational.fromFloat(ev.time / midi.ticksPerQuarterNote / 4, 27720) - song = song.upsertKeyChange(new Project.KeyChange(time, - new Theory.Key( - new Theory.PitchName(tonicLetters[index], tonicAccidentals[index]), - Theory.Scale.parse(ev.scale == 0 ? "Major" : "Minor")))) - } - } - - for (const track of midi.tracks) - { - for (const ev of track.events) - { - if (ev.kind != "setTimeSignature") - continue - - const time = Rational.fromFloat(ev.time / midi.ticksPerQuarterNote / 4, 27720) - song = song.upsertMeterChange(new Project.MeterChange(time, new Theory.Meter(ev.numerator, ev.denominator))) - } - }*/ - - for (const track of midi.tracks) - { - const trackId = project.nextId - project = Project.upsertTrack(project, new Project.TrackNotes()) - - for (const noteOn of track.events) - { - if (noteOn.kind != "noteOn" || noteOn.channel == 9 || noteOn.velocity == 0) - continue - - let noteOff = null - for (const ev of track.events) - { - if (ev.kind != "noteOn" && ev.kind != "noteOff") - continue - - if (ev.time <= noteOn.time || ev.channel != noteOn.channel || ev.key != noteOn.key) - continue - - if (noteOff == null || ev.time < noteOff.time) - { - noteOff = ev - break - } - } - - if (!noteOff) - continue - - const onTick = Rational.fromFloat(noteOn.time / midi.ticksPerQuarterNote / 4, 27720) - const offTick = Rational.fromFloat(noteOff.time / midi.ticksPerQuarterNote / 4, 27720) - - project = Project.upsertRangedElement(project, - new Project.Note(trackId, new Range(onTick, offTick), noteOn.key)) - } - } - - /*if (!song.keyChanges.findActiveAt(new Rational(0))) - song = song.upsertKeyChange(new Project.KeyChange(new Rational(0), Theory.Key.parse("C Major"))) - - if (!song.meterChanges.findActiveAt(new Rational(0))) - song = song.upsertMeterChange(new Project.MeterChange(new Rational(0), new Theory.Meter(4, 4))) - */ - return project//.withRefreshedRange() - } -} \ No newline at end of file diff --git a/src_old/project/playbackSynth.js b/src_old/project/playbackSynth.js deleted file mode 100644 index bfcf0dc..0000000 --- a/src_old/project/playbackSynth.js +++ /dev/null @@ -1,174 +0,0 @@ -import Rational from "../util/rational.js" -import MathUtils from "../util/math.js" -import Editor from "../editor/editor.js" - - -export default class PlaybackSynth -{ - static feedToSynth(project, synth, startTick, useChordPatterns = true) - { - const chordVolumeMul = 0.5 - - const addNoteEvent = (tickStart, duration, instrument, pitch, volume) => - { - const offsetStart = tickStart.subtract(startTick) - - const measuresPerSecond = (project.baseBpm / 4 / 60) - - let timeStart = offsetStart.asFloat() / measuresPerSecond - let timeDuration = duration.asFloat() / measuresPerSecond - - if (timeStart <= 0) - { - timeDuration += timeStart - timeStart = 0 - } - - if (timeDuration <= 0) - return - - //console.log("event note [" + pitch + "] at tick " + tickStart.toString() + " = " + timeStart + " s") - synth.addNoteEvent(timeStart, instrument, MathUtils.midiToHertz(pitch), volume, timeDuration) - } - - // Register notes. - for (const note of project.notes.iterAll()) - { - if (note.range.end.compare(startTick) <= 0) - continue - - addNoteEvent(note.range.start, note.range.duration, 0, note.pitch, 1) - } - - // Register chords. - for (const chord of project.chords.iterAll()) - { - if (chord.range.end.compare(startTick) <= 0) - continue - - const pitches = chord.chord.strummingPitches - - if (!useChordPatterns) - { - for (let j = 0; j < pitches.length; j++) - addNoteEvent(chord.range.start, chord.range.duration, 1, pitches[j], chordVolumeMul) - - continue - } - - const meterCh = project.meterChanges.findActiveAt(chord.range.start) - const meter = meterCh ? meterCh.meter : Editor.defaultMeter() - const meterBeatLength = new Rational(1, meter.denominator) - const measureBreak = null//that.forcedMeasures.findActiveAt(chord.range.start) - - const pattern = getChordStrummingPattern(meter) - - let tick = meterCh ? meterCh.time : chord.range.start - //if (measureBreak != null) - // tick.max(measureBreak.tick) - - let skipTick = new Rational(0) - let patternIndex = 0 - let mustPlayFirstBeat = false - - while (tick.compare(chord.range.end) < 0) - { - const patternBeat = pattern[patternIndex] - const patternBeatKind = patternBeat[0] - let patternBeatLength = patternBeat[1].multiply(meterBeatLength) - patternIndex = (patternIndex + 1) % pattern.length - - let nextTick = tick.add(patternBeatLength) - if (nextTick.compare(chord.range.end) > 0) - { - nextTick = chord.range.end - patternBeatLength = nextTick.subtract(tick) - } - - // Handle beats after the first one. - if (tick.compare(chord.range.start) > 0 && skipTick.compare(new Rational(0)) <= 0) - { - if (mustPlayFirstBeat) - { - mustPlayFirstBeat = false - for (let j = 0; j < pitches.length; j++) - addNoteEvent(chord.range.start, tick.subtract(chord.range.start), 1, pitches[j], chordVolumeMul) - } - - switch (patternBeatKind) - { - case 0: - { - for (let j = 0; j < pitches.length; j++) - addNoteEvent(tick, patternBeatLength, 1, pitches[j], 0.9 * chordVolumeMul) - break - } - case 1: - { - for (let j = 1; j < pitches.length; j++) - addNoteEvent(tick, patternBeatLength, 1, pitches[j], 0.5 * chordVolumeMul) - break - } - case 2: - { - addNoteEvent(tick, patternBeatLength, 1, pitches[0], 0.5 * chordVolumeMul) - break - } - } - } - - skipTick = skipTick.subtract(patternBeatLength) - - if (tick.compare(chord.range.start) <= 0 && nextTick.compare(chord.range.start) > 0) - { - mustPlayFirstBeat = true - - if (!(tick.compare(chord.range.start) == 0 && patternBeatKind == 0)) - skipTick = pattern[0][1].multiply(meterBeatLength) - } - - tick = nextTick - } - - if (mustPlayFirstBeat) - { - mustPlayFirstBeat = false - for (let j = 0; j < pitches.length; j++) - addNoteEvent(chord.range.start, tick.subtract(chord.range.start), 1, pitches[j], 0.7) - } - } - } -} - - -function getChordStrummingPattern(meter) -{ - // [[beat kind, duration], ...] - // Beat kinds: - // 0: Full chord - // 1: Full chord minus bass - // 2: Only bass - const one = [[0, new Rational(1)]] - const two = [[0, new Rational(1)], [1, new Rational(1, 2)], [2, new Rational(1, 2)]] - const three = [[0, new Rational(1)], [1, new Rational(1) ], [1, new Rational(1) ]] - - switch (meter.numerator) - { - case 2: return two - case 3: return three - case 4: return two.concat(two) - case 5: return three.concat(two) - case 6: return three.concat(three) - case 7: return three.concat(two).concat(two) - case 8: return two.concat(two).concat(two).concat(two) - case 9: return three.concat(three).concat(three) - - default: - { - let pattern = [] - for (let i = 0; i < meter.numerator; i++) - pattern = pattern.concat(one) - return pattern - } - } -} diff --git a/src_old/project/project.js b/src_old/project/project.js deleted file mode 100644 index 5cd9015..0000000 --- a/src_old/project/project.js +++ /dev/null @@ -1,280 +0,0 @@ -import ListOfPoints from "../util/listOfPoints.js" -import ListOfRanges from "../util/listOfRanges.js" -import Rational from "../util/rational.js" -import Range from "../util/range.js" -import Theory from "../theory.ts" -import IOJson from "./ioJson.js" -import IOMidi from "./ioMidi.js" -import IOCompressedStr from "./ioCompressedStr.js" - - -export default class Project -{ - constructor() - { - this.nextId = 1 - this.baseBpm = 120 - this.range = new Range(new Rational(0), new Rational(4)) - this.notes = new ListOfRanges() - this.chords = new ListOfRanges() - this.meterChanges = new ListOfPoints() - this.keyChanges = new ListOfPoints() - } - - - static getDefault() - { - return new Project() - .upsertKeyChange(new Project.KeyChange(new Rational(0, 4), Theory.Key.parse("C Major"))) - .upsertMeterChange(new Project.MeterChange(new Rational(0, 4), new Theory.Meter(4, 4))) - .withRefreshedRange() - } - - - withChanges(obj) - { - let song = new Project() - song.nextId = this.nextId - song.baseBpm = this.baseBpm - song.range = this.range - song.notes = this.notes - song.chords = this.chords - song.meterChanges = this.meterChanges - song.keyChanges = this.keyChanges - - return Object.assign(song, obj) - } - - - withRefreshedRange() - { - let range = null - range = Range.merge(range, this.notes.getTotalRange()) - range = Range.merge(range, this.chords.getTotalRange()) - range = Range.merge(range, this.meterChanges.getTotalRange()) - range = Range.merge(range, this.keyChanges.getTotalRange()) - - if (range === null) - range = new Range(new Rational(0), new Rational(1)) - - if (range.duration.asFloat() < 1) - range = Range.fromStartDuration(range.start, new Rational(1)) - - if (this.range.start.compare(range.start) == 0 && - this.range.end.compare(range.end) == 0) - return this - - return this.withChanges({ range }) - } - - - _upsertElement(listField, elem, remove = false) - { - let nextId = this.nextId - let list = this[listField] - - if (elem.id < 0) - { - elem = elem.withChanges({ id: nextId }) - nextId++ - } - else - { - list = list.removeById(elem.id) - } - - if (!remove) - list = list.add(elem) - - return this.withChanges({ nextId, [listField]: list }) - } - - - _upsertElements(listField, elems, remove = false) - { - let nextId = this.nextId - let list = this[listField] - - let newElems = [] - for (let i = 0; i < elems.length; i++) - { - const elem = elems[i] - if (elem.id < 0) - { - newElems.push(elem.withChanges({ id: nextId })) - nextId++ - } - else - { - newElems.push(elem) - list = list.removeById(elem.id) - } - } - - if (!remove) - list = list.addMany(newElems) - - return this.withChanges({ nextId, [listField]: list }) - } - - - upsertNote(note, remove = false) - { - return this._upsertElement("notes", note, remove) - } - - - upsertNotes(notes, remove = false) - { - return this._upsertElements("notes", notes, remove) - } - - - upsertChord(chord, remove = false) - { - return this._upsertElement("chords", chord, remove) - } - - - upsertMeterChange(meterChange, remove = false) - { - return this._upsertElement("meterChanges", meterChange, remove) - } - - - upsertKeyChange(keyChange, remove = false) - { - return this._upsertElement("keyChanges", keyChange, remove) - } - - - findById(id) - { - return ( - this.notes.findById(id) || - this.chords.findById(id) || - this.keyChanges.findById(id) || - this.meterChanges.findById(id) - ) - } - - - update(elem) - { - let song = this.withChanges({}) - song.notes = song.notes.update(elem) - song.chords = song.chords.update(elem) - song.keyChanges = song.keyChanges.update(elem) - song.meterChanges = song.meterChanges.update(elem) - return song - } - - - removeById(id) - { - let song = this.withChanges({}) - song.notes = song.notes.removeById(id) - song.chords = song.chords.removeById(id) - song.keyChanges = song.keyChanges.removeById(id) - song.meterChanges = song.meterChanges.removeById(id) - return song - } - - - toJson() - { - return IOJson.write(this) - } - - - static fromJson(json) - { - return IOJson.read(json) - } - - - static fromMidi(bytes) - { - return IOMidi.read(bytes) - } - - - toCompressedStr() - { - return IOCompressedStr.write(this) - } - - - static fromCompressedStr(str) - { - return IOCompressedStr.read(str) - } - - - static Note = class ProjectNote - { - constructor(range, pitch) - { - this.id = -1 - this.range = range - this.pitch = pitch - } - - - withChanges(obj) - { - return Object.assign(new ProjectNote(this.range, this.pitch), { id: this.id }, obj) - } - } - - - static Chord = class ProjectChord - { - constructor(range, chord) - { - this.id = -1 - this.range = range - this.chord = chord - } - - - withChanges(obj) - { - return Object.assign(new ProjectChord(this.range, this.chord), { id: this.id }, obj) - } - } - - - static MeterChange = class ProjectMeterChange - { - constructor(time, meter) - { - this.id = -1 - this.time = time - this.meter = meter - } - - - withChanges(obj) - { - return Object.assign(new ProjectMeterChange(this.time, this.meter), { id: this.id }, obj) - } - } - - - static KeyChange = class ProjectKeyChange - { - constructor(time, key) - { - this.id = -1 - this.time = time - this.key = key - } - - - withChanges(obj) - { - return Object.assign(new ProjectKeyChange(this.time, this.key), { id: this.id }, obj) - } - } -} \ No newline at end of file diff --git a/src_old/project/project2.ts b/src_old/project/project2.ts deleted file mode 100644 index b36f78b..0000000 --- a/src_old/project/project2.ts +++ /dev/null @@ -1,329 +0,0 @@ -import Immutable from "immutable" -import Range from "../util/range" -import Rational from "../util/rational" -import ListOfPoints from "../util/listOfPoints" -import ListOfRanges from "../util/listOfRanges" -import * as Theory from "../theory/theory" - - -export class Project -{ - nextId: Project.ID - baseBpm: number - tracks: Project.Track[] - timedLists: Immutable.Map> - rangedLists: Immutable.Map> - elems: Immutable.Map - - - constructor() - { - this.nextId = 1 - this.baseBpm = 120 - this.tracks = [] - this.timedLists = Immutable.Map>() - this.rangedLists = Immutable.Map>() - this.elems = Immutable.Map() - } - - - static getDefault(): Project - { - let project = new Project() - - const track1Id = project.nextId - project = Project.upsertTrack(project, new Project.TrackKeyChanges()) - - const track2Id = project.nextId - project = Project.upsertTrack(project, new Project.TrackMeterChanges()) - - project = Project.upsertTimedElement(project, new Project.KeyChange(track1Id, new Rational(0), Theory.Key.parse("D Major"))) - project = Project.upsertTimedElement(project, new Project.MeterChange(track2Id, new Rational(0), new Theory.Meter(4, 4))) - - const track3Id = project.nextId - project = Project.upsertTrack(project, new Project.TrackNotes()) - for (let i = 0; i < 16; i++) - project = Project.upsertRangedElement(project, new Project.Note(track3Id, Range.fromStartDuration(new Rational(i, 4), new Rational(1, 4)), 60 + i % 8)) - - /*const track4Id = project.nextId - project = Project.upsertTrack(project, new Project.TrackNotes()) - for (let i = 0; i < 16; i++) - project = Project.upsertRangedElement(project, new Project.Note(track4Id, Range.fromStartDuration(new Rational(i, 4), new Rational(1, 4)), 68 - (i % 8))) - */ - - return project - } - - - static upsertTrack(project: Project, track: Project.Track, remove: boolean = false): Project - { - let nextId = project.nextId - let tracks = project.tracks - - if (!track.id) - { - track = track.withChanges({ id: nextId }) - nextId++ - } - else - { - tracks = tracks.filter(t => t.id !== track.id) - } - - if (!remove) - { - const trackIndex = tracks.findIndex(t => t.id === track.id) - if (trackIndex < 0) - tracks = [...tracks, track] - else - tracks = [ - ...tracks.slice(0, trackIndex), - track, - ...tracks.slice(trackIndex + 1) - ] - } - - let elems = project.elems - if (!remove) - elems = elems.set(track.id, track) - - return Object.assign({}, project, { nextId, elems, tracks }) - } - - - static upsertTimedElement(project: Project, elem: Project.TimedElement, remove: boolean = false): Project - { - let nextId = project.nextId - - if (!elem.id) - { - elem = Project.Element.withChanges(elem, { id: nextId }) - nextId++ - } - - let timedList = project.timedLists.get(elem.parentId) || new ListOfPoints() - timedList = timedList.add(elem) - - let elems = project.elems.set(elem.id, elem) - let timedLists = project.timedLists.set(elem.parentId, timedList) - - return Object.assign({}, project, { nextId, elems, timedLists }) - } - - - static upsertRangedElement(project: Project, elem: Project.RangedElement, remove: boolean = false): Project - { - let nextId = project.nextId - - if (!elem.id) - { - elem = Project.Element.withChanges(elem, { id: nextId }) - nextId++ - } - - let rangedList = project.rangedLists.get(elem.parentId) || new ListOfRanges() - rangedList = rangedList.add(elem) - - let elems = project.elems.set(elem.id, elem) - let rangedLists = project.rangedLists.set(elem.parentId, rangedList) - - return Object.assign({}, project, { nextId, elems, rangedLists }) - } - - - static keyChangeTrackForTrack(project: Project, trackId: Project.ID): Project.ID - { - return 1 - } - - - static meterChangeTrackForTrack(project: Project, trackId: Project.ID): Project.ID - { - return 2 - } -} - - -export namespace Project -{ - export type ID = number - - - export enum ElementType - { - Track, - Note, - KeyChange, - MeterChange, - } - - - export class Element - { - type: Project.ElementType = -1 - id: Project.ID = 0 - parentId: Project.ID = 0 - - - constructor(type: Project.ElementType, parentId: Project.ID) - { - this.type = type - this.parentId = parentId - } - - - static withChanges(original: T, changes: any): T - { - return Object.assign({}, original, changes) - } - } - - - export enum TrackType - { - Notes, - KeyChanges, - MeterChanges, - } - - - export class Track extends Element - { - trackType: Project.TrackType = 0 - - - constructor(trackType: Project.TrackType) - { - super(ElementType.Track, 0) - this.trackType = trackType - } - - - withChanges(changes: any): this - { - return Object.assign({}, this, changes) - } - } - - - export enum TrackInstrumentType - { - Sflib, - } - - - export interface TrackInstrument - { - instrumentType: TrackInstrumentType - } - - - export interface TrackInstrumentSflib extends TrackInstrument - { - collectionId: string - instrumentId: string - } - - - export class TrackNotes extends Track - { - instrument: TrackInstrument - - - constructor() - { - super(Project.TrackType.Notes) - this.instrument = { - instrumentType: TrackInstrumentType.Sflib, - collectionId: "arachno", - instrumentId: "grand_piano", - } - } - } - - - export class TrackKeyChanges extends Track - { - constructor() - { - super(Project.TrackType.KeyChanges) - } - } - - - export class TrackMeterChanges extends Track - { - constructor() - { - super(Project.TrackType.MeterChanges) - } - } - - - export class TimedElement extends Element - { - time: Rational - - - constructor(type: Project.ElementType, parentId: Project.ID, time: Rational) - { - super(type, parentId) - this.time = time - } - } - - - export class RangedElement extends Element - { - range: Range - - - constructor(type: Project.ElementType, parentId: Project.ID, range: Range) - { - super(type, parentId) - this.range = range - } - } - - - export class Note extends RangedElement - { - pitch: number - - - constructor(parentId: Project.ID, range: Range, pitch: number) - { - super(Project.ElementType.Note, parentId, range) - this.pitch = pitch - } - } - - - export class KeyChange extends TimedElement - { - key: Theory.Key - - - constructor(parentId: Project.ID, time: Rational, key: Theory.Key) - { - super(Project.ElementType.KeyChange, parentId, time) - this.key = key - } - } - - - export class MeterChange extends TimedElement - { - meter: Theory.Meter - - - constructor(parentId: Project.ID, time: Rational, meter: Theory.Meter) - { - super(Project.ElementType.MeterChange, parentId, time) - this.meter = meter - } - } -} - - -export default Project \ No newline at end of file diff --git a/src_old/synth/instrument.js b/src_old/synth/instrument.js deleted file mode 100644 index dbe736e..0000000 --- a/src_old/synth/instrument.js +++ /dev/null @@ -1,168 +0,0 @@ -// `sampleDefs` is an array containing each sample's -// URL and frequency, sorted by frequency, like so: -// [ -// [440, "piano/a4.mp3"], -// [880, "piano/a5.mp3"] -// ] -export class Instrument -{ - constructor(synth, sampleDefs) - { - this.synth = synth - - this.samples = [] - for (let i = 0; i < sampleDefs.length; i++) - this.samples.push(this.load(synth, sampleDefs[i][1], sampleDefs[i][0])) - } - - - load(synth, url, frequency) - { - let sample = - { - buffer: null, - frequency - } - - let request = new XMLHttpRequest() - request.open("GET", url, true) - request.responseType = "arraybuffer" - - request.onload = () => - { - synth.audioCtx.decodeAudioData( - request.response, - (buffer) => { sample.buffer = buffer }, - () => { console.log("Error loading audio <" + url + ">") }) - } - - request.send() - return sample - } - - - playNote(desiredFreq, desiredVolume, duration) - { - // Find the primary sample whose frequency is nearest to the desired frequency. - let sample1 = 0 - let sample1Dist = Math.abs(this.samples[0].frequency - desiredFreq) - for (let i = 1; i < this.samples.length; i++) - { - const dist = Math.abs(this.samples[i].frequency - desiredFreq) - if (dist < sample1Dist) - { - sample1 = i - sample1Dist = dist - } - } - - // Find the sample whose frequency is second nearest to the desired frequency, - // unless the primary sample is already exactly a match. - // Then, calculate a cross-fade between the two. - // TODO: Probably should use an equal-power (logarithmic) cross-fade, - // but this seems good enough for now. - let sample2 = null - - let volume1 = 1 - let volume2 = 0 - - if (this.samples[sample1].frequency < desiredFreq) - { - if (sample1 + 1 < this.samples.length) - { - sample2 = sample1 + 1 - - const t = - (desiredFreq - this.samples[sample1].frequency) / - (this.samples[sample2].frequency - this.samples[sample1].frequency) - - volume1 = 1 - t - volume2 = t - } - } - else if (this.samples[sample1].frequency > desiredFreq) - { - if (sample1 - 1 >= 0) - { - sample2 = sample1 - 1 - - const t = - (desiredFreq - this.samples[sample2].frequency) / - (this.samples[sample1].frequency - this.samples[sample2].frequency) - - volume1 = t - volume2 = 1 - t - } - } - - // Play the samples. - const voices = [ this.playVoice(this.samples[sample1], desiredFreq, desiredVolume * volume1) ] - if (sample2 != null) - voices.push(this.playVoice(this.samples[sample2], desiredFreq, desiredVolume * volume2)) - - const data = - { - duration, - released: false, - voices - } - - return data - } - - - playVoice(sample, frequency, volume) - { - let sourceNode = this.synth.audioCtx.createBufferSource() - sourceNode.playbackRate.value = frequency / sample.frequency - sourceNode.buffer = sample.buffer - sourceNode.start(0) - - let envelopeNode = this.synth.audioCtx.createGain() - envelopeNode.gain.setValueAtTime(1, this.synth.audioCtx.currentTime) - - let volumeNode = this.synth.audioCtx.createGain() - volumeNode.gain.value = volume - - sourceNode.connect(envelopeNode) - envelopeNode.connect(volumeNode) - volumeNode.connect(this.synth.audioCtxOutput) - - return { source: sourceNode, envelope: envelopeNode, volume: volumeNode } - } - - - processNote(noteData, deltaTime) - { - noteData.duration -= deltaTime - - const releaseFalloffDuration = 0.1 - - if (noteData.duration <= 0 && !noteData.released) - { - noteData.released = true - for (let i = 0; i < noteData.voices.length; i++) - { - noteData.voices[i].envelope.gain.setValueAtTime(1, this.synth.audioCtx.currentTime) - noteData.voices[i].envelope.gain.linearRampToValueAtTime(0, this.synth.audioCtx.currentTime + releaseFalloffDuration) - } - } - - if (noteData.duration <= -releaseFalloffDuration * 2) - { - for (let i = 0; i < noteData.voices.length; i++) - noteData.voices[i].source.stop() - - return true - } - - return false - } - - - stopNote(noteData) - { - for (let i = 0; i < noteData.voices.length; i++) - noteData.voices[i].source.stop() - } -} \ No newline at end of file diff --git a/src_old/synth/synth.js b/src_old/synth/synth.js deleted file mode 100644 index 9861eb3..0000000 --- a/src_old/synth/synth.js +++ /dev/null @@ -1,111 +0,0 @@ -import { Instrument } from "./instrument.js" - - -export class Synth -{ - constructor() - { - this.audioCtx = new AudioContext() - this.time = 0 - this.noteEvents = [] - this.playingNotes = [] - - this.audioCtxOutput = this.audioCtx.createGain() - this.audioCtxOutput.connect(this.audioCtx.destination) - this.audioCtxOutput.gain.value = 0.5 - - const piano = new Instrument(this, - [ - [ 55.0, "audio/piano/a1.mp3"], - [ 110.0, "audio/piano/a2.mp3"], - [ 220.0, "audio/piano/a3.mp3"], - [ 440.0, "audio/piano/a4.mp3"], - [ 880.0, "audio/piano/a5.mp3"], - [1760.0, "audio/piano/a6.mp3"], - //[3520.0, "audio/piano/a7.mp3"] - ]) - - this.instruments = [piano, piano] - } - - - isFinished() - { - return this.noteEvents.length == 0 && this.playingNotes.length == 0 - } - - - play() - { - this.noteEvents.sort(function (a, b) { return a.time - b.time }) - } - - - stopAll() - { - this.time = 0 - this.noteEvents = [] - - for (let i = 0; i < this.playingNotes.length; i++) - this.playingNotes[i].stop() - - this.playingNotes = [] - } - - - process(deltaTime) - { - this.time += deltaTime - - // Process pending note events up to the current time. - let noteEventsProcessed = 0 - while (noteEventsProcessed < this.noteEvents.length && - this.noteEvents[noteEventsProcessed].time <= this.time) - { - const ev = this.noteEvents[noteEventsProcessed] - noteEventsProcessed++ - - let noteData = - this.instruments[ev.instrumentIndex].playNote(ev.frequency, ev.volume, ev.duration) - - noteData.process = (deltaTime) => - { - return this.instruments[ev.instrumentIndex].processNote(noteData, deltaTime) - } - - noteData.stop = () => - { - this.instruments[ev.instrumentIndex].stopNote(noteData) - } - - this.playingNotes.push(noteData) - //console.log("play note " + ev.frequency) - } - - // Remove processed events. - this.noteEvents.splice(0, noteEventsProcessed) - - // Update audio output. - for (let i = this.playingNotes.length - 1; i >= 0; i--) - { - if (this.playingNotes[i].process(deltaTime)) - this.playingNotes.splice(i, 1) - } - } - - - addNoteEvent(time, instrumentIndex, frequency, volume, duration) - { - if (!isFinite(frequency)) - return - - this.noteEvents.push( - { - time: time + this.time, - instrumentIndex, - frequency, - volume, - duration - }) - } -} \ No newline at end of file diff --git a/src_old/test.ts b/src_old/test.ts deleted file mode 100644 index 968af28..0000000 --- a/src_old/test.ts +++ /dev/null @@ -1,8 +0,0 @@ -import ListOfPoints from "./util/listOfPoints" -import ListOfRanges from "./util/listOfRanges" -import { test as testTheory } from "./theory/test" - - -ListOfPoints.test() -ListOfRanges.test() -testTheory() \ No newline at end of file diff --git a/src_old/toolbox/App.js b/src_old/toolbox/App.js deleted file mode 100644 index 8d2ba0e..0000000 --- a/src_old/toolbox/App.js +++ /dev/null @@ -1,256 +0,0 @@ -import React from "react" -import Editor from "../editor/editor.js" -import Project from "../project/project.js" -import { usePlaybackController } from "./PlaybackController.js" -import ToolboxPlayback from "./ToolboxPlayback.js" -import ToolboxFile from "./ToolboxFile.js" -import ToolboxEdit from "./ToolboxEdit.js" -import ToolboxInput from "./ToolboxInput.js" -import Ribbon from "./Ribbon.js" - - -function EditorComponent(props) -{ - const refCanvas = React.useRef() - const needsResetView = React.useRef(true) - - const transformMousePos = (canvas, ev) => - { - const rect = canvas.getBoundingClientRect() - return { - x: (ev.clientX - rect.left), - y: (ev.clientY - rect.top) - } - } - - React.useEffect(() => - { - const preventDefault = (ev) => ev.preventDefault() - - const onResize = (ev) => - { - const rect = refCanvas.current.getBoundingClientRect() - refCanvas.current.width = rect.width - refCanvas.current.height = rect.height - - props.dispatch({ - type: "resize", - w: rect.width, - h: rect.height, - }) - - if (needsResetView.current) - { - needsResetView.current = false - props.dispatch({ type: "rewind" }) - } - } - - const onMouseMove = (ev) => - { - const p = transformMousePos(refCanvas.current, ev) - props.dispatch({ type: "mouseMove", ...p }) - } - - const onMouseDown = (ev) => - { - if (window.document.activeElement) - window.document.activeElement.blur() - - ev.preventDefault() - const p = transformMousePos(refCanvas.current, ev) - props.dispatch({ type: "mouseMove", ...p }) - props.dispatch({ type: "mouseDown", rightButton: ev.button != 0, ctrlKey: ev.ctrlKey }) - } - - const onMouseUp = (ev) => - { - const p = transformMousePos(refCanvas.current, ev) - props.dispatch({ type: "mouseMove", ...p }) - props.dispatch({ type: "mouseUp" }) - props.dispatch({ type: "mouseMove", ...p }) - } - - const onMouseWheel = (ev) => - { - ev.preventDefault() - props.dispatch({ type: "mouseWheel", deltaX: ev.deltaX, deltaY: ev.deltaY }) - } - - const onKeyDown = (ev) => - { - props.dispatch({ type: "keyDown", key: ev.key.toLowerCase() }) - props.dispatch({ - type: "keyCommand", - ev, - key: ev.key.toLowerCase(), - ctrlKey: ev.ctrlKey, - shiftKey: ev.shiftKey, - }) - } - - const onKeyUp = (ev) => - { - props.dispatch({ type: "keyUp", key: ev.key.toLowerCase() }) - } - - onResize() - - window.addEventListener("resize", onResize) - window.addEventListener("mousemove", onMouseMove) - window.addEventListener("mouseup", onMouseUp) - window.addEventListener("keydown", onKeyDown) - window.addEventListener("keyup", onKeyUp) - - refCanvas.current.addEventListener("mousedown", onMouseDown) - refCanvas.current.addEventListener("wheel", onMouseWheel) - refCanvas.current.addEventListener("mouseup", preventDefault) - refCanvas.current.addEventListener("contextmenu", preventDefault) - - return () => - { - window.removeEventListener("resize", onResize) - window.removeEventListener("mousemove", onMouseMove) - window.removeEventListener("mouseup", onMouseUp) - window.removeEventListener("keydown", onKeyDown) - window.removeEventListener("keyup", onKeyUp) - - refCanvas.current.removeEventListener("mousedown", onMouseDown) - refCanvas.current.removeEventListener("wheel", onMouseWheel) - refCanvas.current.removeEventListener("mouseup", preventDefault) - refCanvas.current.removeEventListener("contextmenu", preventDefault) - } - - }, []) - - React.useEffect(() => - { - Editor.render(props.state, refCanvas.current.getContext("2d")) - - const mouseAction = props.state.mouse.action || (props.state.mouse.hover && props.state.mouse.hover.action) - - if (props.state.mouse.draw || mouseAction & (Editor.actionDraw)) - refCanvas.current.style.cursor = "crosshair" - else if (mouseAction & (Editor.actionDragTime)) - refCanvas.current.style.cursor = (props.state.mouse.down ? "grabbing" : "grab") - else if (mouseAction & (Editor.actionPan)) - refCanvas.current.style.cursor = "move" - else if (mouseAction & (Editor.actionStretchTimeStart | Editor.actionStretchTimeEnd)) - refCanvas.current.style.cursor = "col-resize" - else - refCanvas.current.style.cursor = "text" - - }, [props.state]) - - return ( -
- -
- ) -} - - -export default function App(props) -{ - const initEditor = (state) => - { - state = Editor.reduce(state, { type: "init", project: Project.getDefault() }) - state = Editor.reduce(state, { type: "trackAdd", kind: "markers" }) - state = Editor.reduce(state, { type: "trackAdd", kind: "chords" }) - state = Editor.reduce(state, { type: "trackAdd", kind: "notes" }) - state = Editor.reduce(state, { type: "clearUndoStack" }) - - const urlData = - getQueryParam(window.location.search, "data") || - getQueryParam(window.location.search, "song") - - if (urlData) - { - try - { - state = Editor.reduce(state, { type: "projectLoad", project: Project.fromCompressedStr(urlData) }) - } - catch (e) - { - window.alert("An error occurred loading the project from the URL.\n\n" + e.toString()) - console.error(e) - } - } - - return state - } - - const [state, dispatch] = React.useReducer(Editor.reduce, {}, initEditor) - - const playbackController = usePlaybackController(state, dispatch, props.synth) - - return ( -
-
- - { ToolboxPlayback({ state, dispatch, playbackController }) } - - - - { ToolboxFile({ state, dispatch }) } - { ToolboxEdit({ state, dispatch }) } - { ToolboxInput({ state, dispatch }) } - - - {/* - - */} -
- - -
- ) -} - - -function getQueryParam(url, name) -{ - name = name.replace(/[\[\]]/g, "\\$&") - - const regex = new RegExp("[?&]" + name + "(=([^&#]*)|&|#|$)") - const results = regex.exec(url) - - if (!results) - return null - - if (!results[2]) - return "" - - return decodeURIComponent(results[2].replace(/\+/g, " ")) -} \ No newline at end of file diff --git a/src_old/toolbox/ButtonList.tsx b/src_old/toolbox/ButtonList.tsx deleted file mode 100644 index c3952e3..0000000 --- a/src_old/toolbox/ButtonList.tsx +++ /dev/null @@ -1,67 +0,0 @@ -import React from "react" - - -interface ButtonListProps -{ - column?: boolean - multiple?: boolean - selected: any - - items: - { - label: any - value: any - bkgColor?: string - width?: string - }[] - - onChange: (item: any, index: number) => void -} - - -export default function ButtonList(props: ButtonListProps) -{ - const isSelected = (value: any) => - { - if (props.multiple && Array.isArray(props.selected)) - return props.selected.some(v => v == value) - else - return props.selected == value - } - - - return
- - { props.items.map((item, i) => - - )} - -
-} \ No newline at end of file diff --git a/src_old/toolbox/DropdownMenu.tsx b/src_old/toolbox/DropdownMenu.tsx deleted file mode 100644 index a4ae761..0000000 --- a/src_old/toolbox/DropdownMenu.tsx +++ /dev/null @@ -1,59 +0,0 @@ -import React from "react" - - -interface DropdownMenuProps -{ - selected: any - - items: - { - label: any - value: any - }[] - - onChange: (item: any, index: number) => void - - style?: any -} - - -export default function DropdownMenu(props: DropdownMenuProps) -{ - //const selectedIndex = props.items.findIndex(i => i.value == props.selected) - - - return -} \ No newline at end of file diff --git a/src_old/toolbox/ListBox.tsx b/src_old/toolbox/ListBox.tsx deleted file mode 100644 index e95794a..0000000 --- a/src_old/toolbox/ListBox.tsx +++ /dev/null @@ -1,70 +0,0 @@ -import React from "react" - - -interface ListBoxProps -{ - selected: any - - items: - { - label: any - value: any - }[] - - onChange: (item: any, index: number) => void - - style?: any -} - - -export default function ListBox(props: ListBoxProps) -{ - //const selectedIndex = props.items.findIndex(i => i.value == props.selected) - - - return
-
- - { props.items.map((item, i) => - - )} - -
-
-} \ No newline at end of file diff --git a/src_old/toolbox/PlaybackController.js b/src_old/toolbox/PlaybackController.js deleted file mode 100644 index 68cd238..0000000 --- a/src_old/toolbox/PlaybackController.js +++ /dev/null @@ -1,129 +0,0 @@ -import React from "react" -import PlaybackSynth from "../project/playbackSynth.js" -import Rational from "../util/rational.js" -import MathUtils from "../util/math.js" - - -export function usePlaybackController(state, dispatch, synth) -{ - const stateRef = React.useRef(state) - stateRef.current = state - - const playbackPrevStart = React.useRef(null) - const playbackPrevTimestamp = React.useRef(-1) - const playbackInterval = React.useRef(null) - - const sampleInterval = React.useRef(null) - const samplePrevTimestamp = React.useRef(null) - - function cancelPlaybackInterval() - { - if (playbackInterval.current) - { - window.cancelAnimationFrame(playbackInterval.current) - playbackInterval.current = null - } - } - - function cancelSampleInterval() - { - if (sampleInterval.current) - { - window.cancelAnimationFrame(sampleInterval.current) - sampleInterval.current = null - } - } - - function playbackStep(timestamp) - { - const deltaTime = (playbackPrevTimestamp.current < 0 ? 0 : timestamp - playbackPrevTimestamp.current) - playbackPrevTimestamp.current = timestamp - - if (deltaTime > 0 && deltaTime < 250) - { - const measuresPerSecond = (stateRef.current.project.baseBpm / 4 / 60) - - synth.process(deltaTime / 1000) - const timeAsFloat = stateRef.current.playback.timeAsFloat + deltaTime / 1000 * measuresPerSecond - dispatch({ type: "playbackStep", timeAsFloat }) - } - - if (stateRef.current.playback.time.compare(stateRef.current.project.range.end.add(new Rational(1, 4))) > 0) - dispatch({ type: "playbackSet", playing: false }) - - if (stateRef.current.playback.playing) - playbackInterval.current = window.requestAnimationFrame(playbackStep) - } - - function playbackSet(playing) - { - const time = state.cursor.time1.min(state.cursor.time2) - dispatch({ type: "playbackSet", playing, time }) - } - - function sampleStep(timestamp) - { - const deltaTime = (samplePrevTimestamp.current < 0 ? 0 : timestamp - samplePrevTimestamp.current) - samplePrevTimestamp.current = timestamp - - if (deltaTime > 0 && deltaTime < 250) - synth.process(deltaTime / 1000) - - if (!synth.isFinished()) - sampleInterval.current = window.requestAnimationFrame(sampleStep) - } - - if (state.playback.playing) - { - if (!playbackPrevStart.current || state.playback.startTime.compare(playbackPrevStart.current) != 0) - { - cancelSampleInterval() - cancelPlaybackInterval() - synth.stopAll() - - PlaybackSynth.feedToSynth(state.project, synth, state.playback.startTime) - synth.play() - - playbackPrevStart.current = state.playback.startTime - playbackPrevTimestamp.current = -1 - playbackInterval.current = window.requestAnimationFrame(playbackStep) - } - } - else - { - if (playbackInterval.current) - { - cancelPlaybackInterval() - synth.stopAll() - playbackPrevStart.current = null - } - } - - if (state.soundPreview) - { - cancelSampleInterval() - synth.stopAll() - - if (state.soundPreview.kind == "note") - synth.addNoteEvent(0, 0, MathUtils.midiToHertz(state.soundPreview.pitch), 1, 0.5) - else if (state.soundPreview.kind == "chord") - { - for (const pitch of state.soundPreview.chord.strummingPitches) - synth.addNoteEvent(0, 0, MathUtils.midiToHertz(pitch), 1, 0.75) - } - - synth.play() - samplePrevTimestamp.current = -1 - sampleInterval.current = window.requestAnimationFrame(sampleStep) - dispatch({ type: "clearSoundPreview" }) - } - - const togglePlaying = () => - { - playbackSet(!state.playback.playing) - } - - return { - togglePlaying, - } -} \ No newline at end of file diff --git a/src_old/toolbox/Ribbon.js b/src_old/toolbox/Ribbon.js deleted file mode 100644 index 5c77e91..0000000 --- a/src_old/toolbox/Ribbon.js +++ /dev/null @@ -1,400 +0,0 @@ -import React from "react" - - -function Tab(props) -{ - return null -} - - -function FixedTab(props) -{ - return null -} - - -function Group(props) -{ - return null -} - - -function CustomGroup(props) -{ - return null -} - - -function TallBox(props) -{ - return null -} - - -function SlotLayout(props) -{ - if (props.tall) - { - const iconSize = (typeof props.icon === "string" ? "3em" : undefined) - - return <> -
-
{ props.icon }
-
{ props.label }
-
- - } - else - { - const iconSlotWidth = (props.icon ? "auto" : "0") - const sepSlotWidth = (props.icon ? "0.25em" : "0") - const iconSize = (typeof props.icon === "string" ? "1.15em" : undefined) - - return <> -
-
{ props.icon }
-
{ props.label }
-
- - } -} - - -function Slot(props) -{ - const baseStyle = - { - borderRadius: "0.5em", - boxSizing: "border-box", - } - - const className = - (props.supressClass ? "" : "ribbonSlot") + " " + - (props.selected ? "ribbonButtonSelected" : "") - - if (props.tall) - { - return <> -
- { props.children } -
- - } - else - { - return <> -
- { props.children } -
- - } -} - - -function Button(props) -{ - const className = - "ribbonButton " + - (props.selected ? "ribbonButtonSelected" : "") - - return <> - - -} - - -function SlotButton(props) -{ - return <> - - - - -} - - -function SlotRadioGroup(props) -{ - return <> - { props.options.map(option => - props.onChange(option.value) } - style={{ - height: "100%", - padding: "0.25em 0.5em", - margin: "0 0.125em", - }} - label={ option.render } - /> - )} - -} - - -function InlineRadioGroup(props) -{ - return <> - { props.options.map(option => - - )} - -} - - -function Input(props) -{ - return ev.stopPropagation() } - style={{ - gridRow: 1, - gridColumn: 1, - width: "4em", - border: 0, - fontFamily: "inherit", - textAlign: "left", - padding: "0.25em", - borderRadius: "0.25em", - ...props.style, - }}/> -} - - -function Select(props) -{ - return -} - - -function getChildren(children) -{ - if (!children) - children = [] - - else if (!Array.isArray(children)) - children = [children] - - return children.filter(c => !!c) -} - - -function flattenChildrenOfType(children, type) -{ - const res = [] - - for (const c of getChildren(children)) - { - if (c.type === type) - res.push(c) - else if (c.props && c.props.children) - { - for (const c2 of flattenChildrenOfType(c.props.children, type)) - res.push(c2) - } - } - - return res -} - - -function Toolbar(props) -{ - const [curTab, setCurTab] = React.useState(0) - - //console.log(props.children) - const tabs = flattenChildrenOfType(props.children, Tab) - const fixedTab = flattenChildrenOfType(props.children, FixedTab) - const groups = flattenChildrenOfType(tabs[curTab].props.children, Group) - const fixedGroups = []//fixedTab.length == 0 ? [] : flattenChildrenOfType(fixedTab[0].props.children, Group) - - return <> -
-
- { tabs.map((tab, i) => - - )} -
- -
- { [...fixedGroups, ...groups].map((group, i) => - -
-
- { group.props.children } -
-
-
- { group.props.label } -
-
- )} -
-
- -} - - -export default -{ - Toolbar, - Tab, - FixedTab, - Group, - TallBox, - Slot, - SlotLayout, - Button, - SlotButton, - InlineRadioGroup, - SlotRadioGroup, - Input, - Select, -} \ No newline at end of file diff --git a/src_old/toolbox/ToolboxChord.js b/src_old/toolbox/ToolboxChord.js deleted file mode 100644 index eaa88c0..0000000 --- a/src_old/toolbox/ToolboxChord.js +++ /dev/null @@ -1,178 +0,0 @@ -import React from "react" -import Editor from "../editor/editor.js" -import CanvasUtils from "../util/canvas.js" -import Theory from "../theory.ts" -import Ribbon from "./Ribbon.js" - - -function ChordButton(props) -{ - const refCanvas = React.useRef(null) - - const w = 60 - const h = 54 - - const key = props.theoryKey - const chord = props.chord - - React.useEffect(() => - { - const ctx = refCanvas.current.getContext("2d") - CanvasUtils.drawChord(ctx, 0, 0, w, h, chord, key) - - }, [props.theoryKey, props.chord]) - - const onClick = () => - { - const time = Editor.insertionTime(props.state) - props.dispatch({ type: "insertChord", chord, time }) - } - - return } - label="" - /> -} - - -function KindDropdown(props) -{ - return props.onChange(ev.target.value) } - style={{ height:"2em" }} - > - - { Theory.Chord.kinds.map((chord, index) => - - { chord.startGroup ? : null } - - - )} - - -} - - -export default function ToolboxChord(props) -{ - const state = props.state - const dispatch = props.dispatch - const [kindGroup, setKindGroup] = React.useState("inkey") - const [inKeyType, setInKeyType] = React.useState(5) - const [kindCustomId, setKindCustomId] = React.useState("M") - const [accidental, setAccidental] = React.useState(0) - - const time = Editor.insertionTime(props.state) - const cursorKeyCh = state.project.keyChanges.findActiveAt(time) - const key = cursorKeyCh ? cursorKeyCh.key : Editor.defaultKey() - - let title = null - let chordButtons = null - switch (kindGroup) - { - case "inkey": - switch (inKeyType) - { - case 5: title = "Triads in " + key.str; break - case 7: title = "Seventh chords in " + key.str; break - case 9: title = "Ninth chords in " + key.str; break - case 11: title = "Eleventh chords in " + key.str; break - case 13: title = "Thirteenth chords in " + key.str; break - } - - chordButtons = [0, 1, 2, 3, 4, 5, 6].map(degree => - { - const root = key.midiForDegree(degree) - - let pitches = [0] - pitches.push(key.midiForDegree(degree + 2) - root) - pitches.push(key.midiForDegree(degree + 4) - root) - - if (inKeyType >= 7) - pitches.push(key.midiForDegree(degree + 6) - root) - - if (inKeyType >= 9) - pitches.push(key.midiForDegree(degree + 8) - root) - - if (inKeyType >= 11) - pitches.push(key.midiForDegree(degree + 10) - root) - - if (inKeyType >= 13) - pitches.push(key.midiForDegree(degree + 12) - root) - - const kind = Theory.Chord.kindFromPitches(pitches) - const chord = new Theory.Chord(root, accidental, kind, 0, []) - - return - }) - break - - case "custom": - title = Theory.Chord.kinds[Theory.Chord.kindFromId(kindCustomId)].name + " chords" - chordButtons = [0, 1, 2, 3, 4, 5, 6].map(degree => { - const kind = Theory.Chord.kindFromId(kindCustomId) - const root = key.midiForDegree(degree) - const chord = new Theory.Chord(root, accidental, kind, 0, []) - - return - }) - break - } - - return <> - - { chordButtons } - - - - setKindGroup("inkey") }> - In-Key } - label={ - } - /> - - - setKindGroup("custom") } - > - Other } - label={ } - /> - - - - - - - - -} \ No newline at end of file diff --git a/src_old/toolbox/ToolboxEdit.js b/src_old/toolbox/ToolboxEdit.js deleted file mode 100644 index dd2f9b7..0000000 --- a/src_old/toolbox/ToolboxEdit.js +++ /dev/null @@ -1,44 +0,0 @@ -import React from "react" -import Ribbon from "./Ribbon.js" -import Editor from "../editor/editor.js" - - -export default function ToolboxEdit(props) -{ - return - - - props.dispatch({ type: "undo" })} - /> - props.dispatch({ type: "redo" })} - /> - - - - props.dispatch({ type: "paste" })} - /> - props.dispatch({ type: "copy" })} - /> - props.dispatch({ type: "cut" })} - /> - - - -} \ No newline at end of file diff --git a/src_old/toolbox/ToolboxFile.js b/src_old/toolbox/ToolboxFile.js deleted file mode 100644 index fe1b4cb..0000000 --- a/src_old/toolbox/ToolboxFile.js +++ /dev/null @@ -1,180 +0,0 @@ -import React from "react" -import Ribbon from "./Ribbon.js" -import Project from "../project/project.js" - - -function loadFile(dispatch, elem) -{ - if (elem.files.length != 1) - return - - let reader = new FileReader() - reader.readAsArrayBuffer(elem.files[0]) - reader.onload = () => - { - const bytes = new Uint8Array(reader.result) - if (bytes[0] == "M".charCodeAt(0) && - bytes[1] == "T".charCodeAt(0) && - bytes[2] == "h".charCodeAt(0) && - bytes[3] == "d".charCodeAt(0)) - loadFileMidi(dispatch, elem) - else - loadFileJson(dispatch, elem) - } -} - - -function loadFileJson(dispatch, elem) -{ - if (elem.files.length != 1) - return - - let reader = new FileReader() - reader.readAsText(elem.files[0]) - reader.onload = () => - { - const json = JSON.parse(reader.result) - dispatch({ type: "projectLoad", project: Project.fromJson(json) }) - } -} - - -function loadFileMidi(dispatch, elem) -{ - if (elem.files.length != 1) - return - - let reader = new FileReader() - reader.readAsArrayBuffer(elem.files[0]) - reader.onload = () => - { - const bytes = new Uint8Array(reader.result) - dispatch({ type: "projectLoad", project: Project.fromMidi(bytes) }) - } -} - - -function loadJson(dispatch) -{ - const json = window.prompt("Paste JSON song data:", "") - if (json) - dispatch({ type: "projectLoad", project: Project.fromJson(JSON.parse(json)) }) -} - - -function saveJson(project) -{ - const json = project.toJson() - const newWindow = window.open() - newWindow.document.write("") - newWindow.document.write(json) - newWindow.document.write("") -} - - -function saveAndSetUrl(project) -{ - const str = project.toCompressedStr() - window.location = location.protocol + "//" + location.host + location.pathname + "?data=" + str -} - - -export default function ToolboxFile(props) -{ - const state = props.state - - const inputFileRef = React.useRef(null) - const performInputFileClick = () => inputFileRef.current.click() - - const newProject = () => - { - props.dispatch({ type: "projectSet", project: Project.getDefault() }) - props.dispatch({ type: "rewind" }) - } - - const examples = - [ - "mozartk453", - "letitgo", - "rollercoaster", - "adventure", - "isaac", - "dontstarve", - "chronotrigger", - "jimmyneutron", - "whensomebodylovedme", - ] - - const loadExample = (filename) => - { - if (filename == "") - return - - fetch("examples/" + filename + ".json") - .then(res => res.json()) - .then(json => - { - props.dispatch({ type: "projectLoad", project: Project.fromJson(json) }) - }) - } - - return - - - - - - - saveAndSetUrl(state.project) } - /> - - - - saveJson(state.project) } - /> - loadJson(props.dispatch) } - /> - - loadFile(props.dispatch, ev.target) } - style={{ display:"none", width:"1em" }} - /> - - - - - - loadExample(ev.target.value) }> - - { examples.map(ex => ) } - - } - /> - - - - -} \ No newline at end of file diff --git a/src_old/toolbox/ToolboxInput.js b/src_old/toolbox/ToolboxInput.js deleted file mode 100644 index 34afe71..0000000 --- a/src_old/toolbox/ToolboxInput.js +++ /dev/null @@ -1,26 +0,0 @@ -import React from "react" -import Editor from "../editor/editor.js" -import Ribbon from "./Ribbon.js" -import ToolboxNote from "./ToolboxNote.js" -import ToolboxChord from "./ToolboxChord.js" -import ToolboxMarkers from "./ToolboxMarkers.js" - - -export default function ToolboxInput(props) -{ - const state = props.state - const dispatch = props.dispatch - - const track = Editor.insertionTrack(state) - const trackKind = state.tracks[track].kind - - const toolboxNote = ToolboxNote({ state, dispatch }) - const toolboxChord = ToolboxChord({ state, dispatch }) - const toolboxMarkers = ToolboxMarkers({ state, dispatch }) - - return - { trackKind != "notes" ? null : toolboxNote } - { trackKind != "chords" ? null : toolboxChord } - { trackKind != "markers" ? null : toolboxMarkers } - -} \ No newline at end of file diff --git a/src_old/toolbox/ToolboxMarkers.js b/src_old/toolbox/ToolboxMarkers.js deleted file mode 100644 index 1a530c8..0000000 --- a/src_old/toolbox/ToolboxMarkers.js +++ /dev/null @@ -1,170 +0,0 @@ -import React from "react" -import Editor from "../editor/editor.js" -import Theory from "../theory.ts" -import Ribbon from "./Ribbon.js" - - -function KeyChangeToolbox(props) -{ - const state = props.state - const dispatch = props.dispatch - - const time = Editor.insertionTime(props.state) - const cursorKeyCh = state.project.keyChanges.findActiveAt(time) - const key = cursorKeyCh ? cursorKeyCh.key : Editor.defaultKey() - - const accidentalNames = ["♭♭", "♭", "♮", "♯", "♯♯"] - - const setTonicLetter = (letter) => - { - if (!cursorKeyCh) - return - - const newKey = new Theory.Key(new Theory.PitchName(letter, key.tonic.accidental), key.scale) - const newKeyCh = cursorKeyCh.withChanges({ key: newKey }) - const project = state.project.upsertKeyChange(newKeyCh) - dispatch({ type: "projectSet", project }) - } - - const setTonicAccidental = (acc) => - { - if (!cursorKeyCh) - return - - const newKey = new Theory.Key(new Theory.PitchName(key.tonic.letter, acc), key.scale) - const newKeyCh = cursorKeyCh.withChanges({ key: newKey }) - const project = state.project.upsertKeyChange(newKeyCh) - dispatch({ type: "projectSet", project }) - } - - const setScale = (scale) => - { - if (!cursorKeyCh) - return - - const newKey = new Theory.Key(key.tonic, scale) - const newKeyCh = cursorKeyCh.withChanges({ key: newKey }) - const project = state.project.upsertKeyChange(newKeyCh) - dispatch({ type: "projectSet", project }) - } - - return - - - setTonicLetter(parseInt(ev.target.value)) } - > - { [0, 1, 2, 3, 4, 5, 6].map(i => ) } - - setTonicAccidental(parseInt(ev.target.value)) } - > - { [2, 1, 0, -1, -2].map(acc => ) } - - setScale(Theory.Scale.fromId(ev.target.value)) } - > - { Theory.Scale.list.map(scale => ) } - - } - /> - - -} - - -function MeterChangeToolbox(props) -{ - const state = props.state - const dispatch = props.dispatch - - const time = Editor.insertionTime(props.state) - const cursorMeterCh = state.project.meterChanges.findActiveAt(time) - const meter = cursorMeterCh ? cursorMeterCh.meter : Editor.defaultMeter() - - const meterDenominators = [1, 2, 4, 8, 16, 32, 64] - - const setNumerator = (numerator) => - { - if (!cursorMeterCh) - return - - const newMeter = new Theory.Meter(numerator, meter.denominator) - const newMeterCh = cursorMeterCh.withChanges({ meter: newMeter }) - const project = state.project.upsertMeterChange(newMeterCh) - dispatch({ type: "projectSet", project }) - } - - const setDenominator = (denominator) => - { - if (!cursorMeterCh) - return - - const newMeter = new Theory.Meter(meter.numerator, denominator) - const newMeterCh = cursorMeterCh.withChanges({ meter: newMeter }) - const project = state.project.upsertMeterChange(newMeterCh) - dispatch({ type: "projectSet", project }) - } - - return - - - setNumerator(Math.max(1, Math.min(64, ev.target.value))) } - onKeyDown={ ev => ev.stopPropagation() } - min="1" max="64" - style={{ width:"3em" }} - /> - { " / " } - setDenominator(parseInt(ev.target.value)) } - style={{ height:"2em" }} - > - { meterDenominators.map(d => ) } - - } - /> - - -} - - -export default function ToolboxMarkers(props) -{ - const state = props.state - const dispatch = props.dispatch - - const time = Editor.insertionTime(props.state) - const insertKeyChange = () => dispatch({ type: "insertKeyChange", time, key: Editor.defaultKey() }) - const insertMeterChange = () => dispatch({ type: "insertMeterChange", time, meter: Editor.defaultMeter() }) - - const insertIconStyle = - { - fontWeight: "bold", - } - - return <> - - + } - label="Key Change" - /> - + } - label="Meter Change" - /> - - - { KeyChangeToolbox({ state, dispatch }) } - { MeterChangeToolbox({ state, dispatch }) } - -} \ No newline at end of file diff --git a/src_old/toolbox/ToolboxNote.js b/src_old/toolbox/ToolboxNote.js deleted file mode 100644 index 6db0967..0000000 --- a/src_old/toolbox/ToolboxNote.js +++ /dev/null @@ -1,58 +0,0 @@ -import React from "react" -import Ribbon from "./Ribbon.js" -import Editor from "../editor/editor.js" -import CanvasUtils from "../util/canvas.js" - - -function NoteButton(props) -{ - const refCanvas = React.useRef(null) - - const w = 30 - const h = 50 - - const key = props.theoryKey - const midi = props.noteIndex + key.tonic.midi - const degree = key.degreeForMidi(midi) + key.scale.metadata.mode - - React.useEffect(() => - { - const ctx = refCanvas.current.getContext("2d") - ctx.fillStyle = CanvasUtils.fillStyleForDegree(ctx, degree) - ctx.fillRect(0, 0, w, h) - - }, [props.theoryKey, props.noteIndex]) - - const onClick = () => - { - const time = Editor.insertionTime(props.state) - props.dispatch({ type: "insertNote", chroma: midi, time }) - } - - return } - label={ key.nameForMidi(midi).str } - /> -} - - -export default function ToolboxNote(props) -{ - const state = props.state - const dispatch = props.dispatch - - const time = Editor.insertionTime(props.state) - const cursorKeyCh = state.project.keyChanges.findActiveAt(time) - const key = cursorKeyCh ? cursorKeyCh.key : Editor.defaultKey() - - return - { [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11].map(noteIndex => - - )} - -} \ No newline at end of file diff --git a/src_old/toolbox/ToolboxPlayback.js b/src_old/toolbox/ToolboxPlayback.js deleted file mode 100644 index 9647698..0000000 --- a/src_old/toolbox/ToolboxPlayback.js +++ /dev/null @@ -1,45 +0,0 @@ -import React from "react" -import Ribbon from "./Ribbon.js" - - -export default function ToolboxPlayback(props) -{ - const state = props.state - const playbackController = props.playbackController - - const rewind = () => - { - props.dispatch({ type: "rewind" }) - } - - const setBaseBpm = (value) => - { - props.dispatch({ type: "set", state: { project: state.project.withChanges({ baseBpm: value }) } }) - } - - return - - - - - Base BPM} - label={ setBaseBpm(ev.target.value) } - />} - /> - - - -} \ No newline at end of file diff --git a/tsconfig.json b/tsconfig.json deleted file mode 100644 index 01c2a6b..0000000 --- a/tsconfig.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "compilerOptions": { - "sourceMap": true, - "target": "esnext", - "module": "esnext", - "moduleResolution": "node", - "allowSyntheticDefaultImports": true, - "strict": true, - "jsx": "react" - }, - "exclude": ["src_old"] -} \ No newline at end of file diff --git a/watch.bat b/watch.bat deleted file mode 100644 index 7946708..0000000 --- a/watch.bat +++ /dev/null @@ -1 +0,0 @@ -npm run watch \ No newline at end of file diff --git a/webpack.config.cjs b/webpack.config.cjs deleted file mode 100644 index 7ec9698..0000000 --- a/webpack.config.cjs +++ /dev/null @@ -1,53 +0,0 @@ -const path = require("path") - - -module.exports = -{ - mode: "production", - entry: - { - main: path.resolve(__dirname, "src/main.js"), - }, - - output: - { - filename: "[name].js", - path: path.resolve(__dirname, "build") - }, - - resolve: { - extensions: [".ts", ".tsx", ".js", ".json"], - fallback: { - "assert": false, - /*"zlib": require.resolve("browserify-zlib"), - "util": require.resolve("util/"), - "buffer": require.resolve("buffer/"), - "stream": require.resolve("stream-browserify"),*/ - }, - }, - - module: - { - rules: - [ - { - test: /\.tsx?$/, - exclude: /src_old/, - loader: "ts-loader", - }, - { test: /\.js$/, loader: "source-map-loader" }, - { - test: /\.(js|jsx)$/, - exclude: /node_modules/, - use: - { - loader: "babel-loader", - options: { - plugins: ["@babel/plugin-proposal-class-properties"], - presets: ["@babel/preset-env", "@babel/preset-react"] - } - } - } - ] - } -} \ No newline at end of file