From ded9f13c132acac3f97c63943f0802bfd8020e95 Mon Sep 17 00:00:00 2001 From: pmgl Date: Tue, 13 Jul 2021 10:51:08 +0200 Subject: [PATCH] open source microStudio, initial commit --- .gitignore | 8 + LICENSE.txt | 21 + README.md | 15 + config_local.json | 5 + config_prod.json | 7 + server/app/exportfeatures.coffee | 259 ++ server/app/exportfeatures.js | 375 ++ server/app/jobqueue.coffee | 23 + server/app/jobqueue.js | 39 + server/build/build.coffee | 17 + server/build/build.js | 26 + server/build/builder.coffee | 142 + server/build/builder.js | 187 + server/build/buildmanager.coffee | 100 + server/build/buildmanager.js | 139 + server/concatenator.coffee | 259 ++ server/concatenator.js | 132 + server/content/comments.coffee | 68 + server/content/comments.js | 105 + server/content/content.coffee | 339 ++ server/content/content.js | 458 +++ server/content/project.coffee | 243 ++ server/content/project.js | 354 ++ server/content/tag.coffee | 19 + server/content/tag.js | 30 + server/content/token.coffee | 9 + server/content/token.js | 17 + server/content/translator.coffee | 103 + server/content/translator.js | 163 + server/content/user.coffee | 180 + server/content/user.js | 243 ++ server/db/db.coffee | 64 + server/db/db.js | 105 + server/db/record.coffee | 31 + server/db/record.js | 50 + server/db/table.coffee | 185 + server/db/table.js | 292 ++ server/file_types.coffee | 27 + server/file_types.js | 34 + server/filestorage/filestorage.coffee | 122 + server/filestorage/filestorage.js | 198 + server/fonts.coffee | 21 + server/fonts.js | 41 + server/forum/category.coffee | 76 + server/forum/category.js | 97 + server/forum/forum.coffee | 219 ++ server/forum/forum.js | 294 ++ server/forum/forumapp.coffee | 194 + server/forum/forumapp.js | 221 ++ server/forum/forumsession.coffee | 368 ++ server/forum/forumsession.js | 595 +++ server/forum/indexer.coffee | 80 + server/forum/indexer.js | 116 + server/forum/post.coffee | 145 + server/forum/post.js | 195 + server/forum/reply.coffee | 57 + server/forum/reply.js | 80 + server/gamify/achievements.coffee | 44 + server/gamify/achievements.js | 60 + server/gamify/levels.coffee | 15 + server/gamify/levels.js | 22 + server/gamify/userprogress.coffee | 63 + server/gamify/userprogress.js | 99 + server/package-lock.json | 2315 +++++++++++ server/package.json | 39 + server/ratelimiter.coffee | 76 + server/ratelimiter.js | 101 + server/server.coffee | 209 + server/server.js | 280 ++ server/session/projectmanager.coffee | 417 ++ server/session/projectmanager.js | 613 +++ server/session/session.coffee | 1061 +++++ server/session/session.js | 1695 ++++++++ server/webapp.coffee | 607 +++ server/webapp.js | 744 ++++ static/community/forum_sw.js | 19 + static/community/manifest.json | 29 + static/community/manifest_fr.json | 29 + static/css/404.css | 31 + static/css/assets.css | 191 + static/css/code.css | 514 +++ static/css/console.css | 142 + static/css/doc.css | 147 + static/css/explore.css | 602 +++ static/css/forum/forum.css | 735 ++++ static/css/forum/media.css | 60 + static/css/home.css | 199 + static/css/jquery.terminal.css | 698 ++++ static/css/maps.css | 326 ++ static/css/md.css | 149 + static/css/media.css | 62 + static/css/music.css | 150 + static/css/options.css | 200 + static/css/publish.css | 149 + static/css/sounds.css | 26 + static/css/sprites.css | 656 ++++ static/css/style.css | 1409 +++++++ static/css/synth.css | 256 ++ static/css/terminal.css | 106 + static/css/tutorial.css | 255 ++ static/css/user.css | 261 ++ static/css/userpage.css | 122 + static/doc/en/about.md | 32 + static/doc/en/changelog.md | 328 ++ static/doc/en/doc.md | 1242 ++++++ static/doc/en/privacy.md | 3 + static/doc/en/terms.md | 7 + static/doc/fr/about.md | 36 + static/doc/fr/changelog.md | 328 ++ static/doc/fr/doc.md | 1237 ++++++ static/doc/fr/privacy.md | 3 + static/doc/fr/terms.md | 7 + static/doc/img/screen_coordinates.png | Bin 0 -> 11598 bytes static/fonts/AESystematic.ttf | Bin 0 -> 42760 bytes static/fonts/AESystematic_license.txt | 78 + static/fonts/Alkhemikal.ttf | Bin 0 -> 44728 bytes static/fonts/AlphaBeta.ttf | Bin 0 -> 17872 bytes static/fonts/AlphaBeta_license.txt | 77 + static/fonts/Arpegius.ttf | Bin 0 -> 43488 bytes static/fonts/Awesome.ttf | Bin 0 -> 15184 bytes static/fonts/BitCell.ttf | Bin 0 -> 67612 bytes static/fonts/Blocktopia.ttf | Bin 0 -> 21668 bytes static/fonts/Comicoro.ttf | Bin 0 -> 39384 bytes static/fonts/Commodore64.ttf | Bin 0 -> 20324 bytes static/fonts/Commodore64_license.txt | 127 + static/fonts/DigitalDisco.ttf | Bin 0 -> 44088 bytes static/fonts/Edunline.ttf | Bin 0 -> 34936 bytes static/fonts/Edunline_license.txt | 85 + static/fonts/EnchantedSword.ttf | Bin 0 -> 42316 bytes static/fonts/EnterCommand.ttf | Bin 0 -> 40056 bytes static/fonts/Euxoi.ttf | Bin 0 -> 116260 bytes static/fonts/Euxoi_License.txt | 9 + static/fonts/FixedBold.ttf | Bin 0 -> 45292 bytes static/fonts/GenericMobileSystem.ttf | Bin 0 -> 23616 bytes static/fonts/GenericMobileSystem_LICENSE.txt | 121 + static/fonts/GrapeSoda.ttf | Bin 0 -> 42684 bytes static/fonts/JupiterCrash.ttf | Bin 0 -> 19280 bytes static/fonts/JupiterCrash_License.txt | 77 + static/fonts/Kapel.ttf | Bin 0 -> 73560 bytes static/fonts/KiwiSoda.ttf | Bin 0 -> 49292 bytes static/fonts/Litebulb8bit.ttf | Bin 0 -> 68728 bytes static/fonts/LycheeSoda.ttf | Bin 0 -> 39804 bytes static/fonts/MisterPixel.ttf | Bin 0 -> 84608 bytes static/fonts/ModernDos.ttf | Bin 0 -> 31848 bytes static/fonts/ModernDos_LICENSE.txt | 121 + static/fonts/NokiaCellPhone.ttf | Bin 0 -> 16272 bytes static/fonts/NokiaCellPhone_LICENSE.txt | 136 + static/fonts/PearSoda.ttf | Bin 0 -> 40280 bytes static/fonts/PixAntiqua.ttf | Bin 0 -> 35408 bytes static/fonts/PixChicago.otf | Bin 0 -> 12824 bytes static/fonts/PixChicago.ttf | Bin 0 -> 19808 bytes static/fonts/PixelArial.ttf | Bin 0 -> 57520 bytes static/fonts/PixelOperator.ttf | Bin 0 -> 18624 bytes static/fonts/PixelOperator_LICENSE.txt | 121 + static/fonts/Pixellari.ttf | Bin 0 -> 39908 bytes static/fonts/Pixolde.ttf | Bin 0 -> 44124 bytes static/fonts/PlanetaryContact.ttf | Bin 0 -> 35796 bytes static/fonts/PressStart2P.ttf | Bin 0 -> 82480 bytes static/fonts/PressStart2P_LICENSE.txt | 94 + static/fonts/RainyHearts.ttf | Bin 0 -> 48784 bytes static/fonts/RetroGaming.ttf | Bin 0 -> 31024 bytes static/fonts/Revolute.ttf | Bin 0 -> 34192 bytes static/fonts/Romulus.ttf | Bin 0 -> 36664 bytes static/fonts/Scriptorium.ttf | Bin 0 -> 47768 bytes static/fonts/Squarewave.ttf | Bin 0 -> 37912 bytes static/fonts/Thixel.ttf | Bin 0 -> 34924 bytes static/fonts/Unbalanced.ttf | Bin 0 -> 41300 bytes static/fonts/UpheavalPro.ttf | Bin 0 -> 301376 bytes static/fonts/UpheavalPro_LICENSE.txt | 75 + static/fonts/VeniceClassic.ttf | Bin 0 -> 16128 bytes static/fonts/VeniceClassic_LICENSE.txt | 1 + static/fonts/ZXSpectrum.ttf | Bin 0 -> 25428 bytes static/fonts/ZXSpectrum_LICENSE.txt | 38 + static/fonts/Zepto.ttf | Bin 0 -> 18444 bytes static/fonts/bit_cell.ttf | Bin 0 -> 67612 bytes static/fr/community/forum_sw.js | 19 + static/img/LICENSE_INFORMATION.md | 7 + static/img/android_robot.svg | 1 + static/img/codeeditor.jpg | Bin 0 -> 47022 bytes static/img/community.jpg | Bin 0 -> 58070 bytes static/img/crazygames.svg | 48 + static/img/defaultappicon.png | Bin 0 -> 232 bytes static/img/eyedropper.svg | 8 + static/img/favicon.ico | Bin 0 -> 1150 bytes static/img/favicon.png | Bin 0 -> 232 bytes static/img/favicon16.png | Bin 0 -> 232 bytes static/img/favicon180.png | Bin 0 -> 730 bytes static/img/favicon192.png | Bin 0 -> 745 bytes static/img/favicon32.png | Bin 0 -> 326 bytes static/img/favicon512.png | Bin 0 -> 2130 bytes static/img/faviconlarge.png | Bin 0 -> 2130 bytes static/img/forumicon192.png | Bin 0 -> 2041 bytes static/img/forumicon196.png | Bin 0 -> 2071 bytes static/img/forumicon512.png | Bin 0 -> 5799 bytes static/img/gamedistribution.svg | 108 + static/img/gamejolt.png | Bin 0 -> 1058 bytes static/img/gamepad.svg | 34 + static/img/gamepix.png | Bin 0 -> 9288 bytes static/img/itchio.svg | 1 + static/img/kongregate.svg | 46 + static/img/macos.svg | 159 + static/img/mapeditor.png | Bin 0 -> 69716 bytes static/img/microstudio.jpg | Bin 0 -> 96791 bytes static/img/microstudio.svg | 1 + static/img/microstudiologo.svg | 31 + static/img/microstudiologoblack.svg | 35 + static/img/neuronality.svg | 3489 +++++++++++++++++ static/img/patreon.png | Bin 0 -> 2914 bytes static/img/raspberry.svg | 17 + static/img/smartphone.png | Bin 0 -> 88980 bytes static/img/spriteeditor.jpg | Bin 0 -> 37206 bytes static/img/ubuntu.svg | 5 + static/img/windows.svg | 1 + static/js/about/about.coffee | 46 + static/js/about/about.js | 85 + static/js/app.coffee | 357 ++ static/js/app.js | 514 +++ static/js/appstate.coffee | 117 + static/js/appstate.js | 191 + static/js/appui/appui.coffee | 826 ++++ static/js/appui/appui.js | 1078 +++++ static/js/appui/floatingwindow.coffee | 90 + static/js/appui/floatingwindow.js | 143 + static/js/assets/assetsmanager.coffee | 175 + static/js/assets/assetsmanager.js | 221 ++ static/js/assets/assetviewer.coffee | 161 + static/js/assets/assetviewer.js | 207 + static/js/client/client.coffee | 123 + static/js/client/client.js | 167 + static/js/console/console.coffee | 23 + static/js/console/console.js | 32 + static/js/doc/documentation.coffee | 172 + static/js/doc/documentation.js | 234 ++ static/js/doceditor/doceditor.coffee | 77 + static/js/doceditor/doceditor.js | 116 + static/js/editor/editor.coffee | 668 ++++ static/js/editor/editor.js | 894 +++++ static/js/editor/rulercanvas.coffee | 368 ++ static/js/editor/rulercanvas.js | 363 ++ static/js/editor/runwindow.coffee | 453 +++ static/js/editor/runwindow.js | 610 +++ static/js/editor/valuetool.coffee | 65 + static/js/editor/valuetool.js | 96 + static/js/explore/explore.coffee | 402 ++ static/js/explore/explore.js | 505 +++ static/js/explore/projectdetails.coffee | 590 +++ static/js/explore/projectdetails.js | 726 ++++ static/js/forum/client.coffee | 74 + static/js/forum/client.js | 103 + static/js/forum/forum.coffee | 716 ++++ static/js/forum/forum.js | 1016 +++++ static/js/manager.coffee | 187 + static/js/manager.js | 251 ++ static/js/mapeditor/mapeditor.coffee | 502 +++ static/js/mapeditor/mapeditor.js | 675 ++++ static/js/mapeditor/mapview.coffee | 166 + static/js/mapeditor/mapview.js | 212 + static/js/mapeditor/tilepicker.coffee | 243 ++ static/js/mapeditor/tilepicker.js | 294 ++ static/js/microscript/jstranspiler.coffee | 730 ++++ static/js/microscript/jstranspiler.js | 673 ++++ static/js/microscript/microvm.coffee | 293 ++ static/js/microscript/microvm.js | 428 ++ static/js/microscript/parser.coffee | 487 +++ static/js/microscript/parser.js | 588 +++ static/js/microscript/program.coffee | 926 +++++ static/js/microscript/program.js | 1360 +++++++ static/js/microscript/random.coffee | 28 + static/js/microscript/random.js | 47 + static/js/microscript/test.html | 40 + static/js/microscript/test.ms | 46 + static/js/microscript/todo.txt | 17 + static/js/microscript/token.coffee | 103 + static/js/microscript/token.js | 173 + static/js/microscript/tokenizer.coffee | 186 + static/js/microscript/tokenizer.js | 250 ++ static/js/music/musiceditor.coffee | 61 + static/js/music/musiceditor.js | 81 + static/js/options/options.coffee | 173 + static/js/options/options.js | 252 ++ static/js/play/player.coffee | 144 + static/js/play/player.js | 187 + static/js/play/playerclient.coffee | 101 + static/js/play/playerclient.js | 146 + static/js/project/project.coffee | 410 ++ static/js/project/project.js | 623 +++ static/js/project/projectasset.coffee | 17 + static/js/project/projectasset.js | 29 + static/js/project/projectmap.coffee | 46 + static/js/project/projectmap.js | 72 + static/js/project/projectmusic.coffee | 37 + static/js/project/projectmusic.js | 57 + static/js/project/projectsound.coffee | 35 + static/js/project/projectsound.js | 52 + static/js/project/projectsource.coffee | 20 + static/js/project/projectsource.js | 35 + static/js/project/projectsprite.coffee | 57 + static/js/project/projectsprite.js | 97 + static/js/publish/appbuild.coffee | 56 + static/js/publish/appbuild.js | 92 + static/js/publish/publish.coffee | 134 + static/js/publish/publish.js | 196 + static/js/runtime/audio/audio.coffee | 345 ++ static/js/runtime/audio/audio.js | 302 ++ static/js/runtime/audio/beeper.coffee | 157 + static/js/runtime/audio/beeper.js | 172 + static/js/runtime/audio/music.coffee | 39 + static/js/runtime/audio/music.js | 77 + static/js/runtime/audio/sound.coffee | 59 + static/js/runtime/audio/sound.js | 98 + static/js/runtime/gamepad.coffee | 183 + static/js/runtime/gamepad.js | 246 ++ static/js/runtime/keyboard.coffee | 74 + static/js/runtime/keyboard.js | 97 + static/js/runtime/m3d/m3d.coffee | 186 + static/js/runtime/m3d/m3d.js | 331 ++ static/js/runtime/m3d/screen3d.coffee | 221 ++ static/js/runtime/m3d/screen3d.js | 285 ++ static/js/runtime/map.coffee | 181 + static/js/runtime/map.js | 241 ++ static/js/runtime/runtime.coffee | 412 ++ static/js/runtime/runtime.js | 563 +++ static/js/runtime/screen.coffee | 674 ++++ static/js/runtime/screen.js | 905 +++++ static/js/runtime/sprite.coffee | 102 + static/js/runtime/sprite.js | 149 + static/js/runtime/spriteframe.coffee | 66 + static/js/runtime/spriteframe.js | 104 + static/js/sound/audiocontroller.coffee | 42 + static/js/sound/audiocontroller.js | 79 + .../js/sound/audioengine/audioengine.coffee | 134 + static/js/sound/audioengine/audioengine.js | 159 + static/js/sound/audioengine/effects.coffee | 473 +++ static/js/sound/audioengine/effects.js | 539 +++ static/js/sound/audioengine/filters.coffee | 135 + static/js/sound/audioengine/filters.js | 132 + static/js/sound/audioengine/instrument.coffee | 235 ++ static/js/sound/audioengine/instrument.js | 268 ++ static/js/sound/audioengine/modulation.coffee | 189 + static/js/sound/audioengine/modulation.js | 263 ++ .../js/sound/audioengine/oscillators.coffee | 355 ++ static/js/sound/audioengine/oscillators.js | 426 ++ static/js/sound/audioengine/utils.coffee | 40 + static/js/sound/audioengine/utils.js | 56 + static/js/sound/audioengine/voice.coffee | 573 +++ static/js/sound/audioengine/voice.js | 538 +++ static/js/sound/audiooutput.coffee | 74 + static/js/sound/audiooutput.js | 84 + static/js/sound/audioworker.js | 142 + static/js/sound/audioworklet.js | 242 ++ static/js/sound/instrument.coffee | 11 + static/js/sound/instrument.js | 20 + static/js/sound/keyboard.coffee | 31 + static/js/sound/keyboard.js | 39 + static/js/sound/knob.coffee | 91 + static/js/sound/knob.js | 108 + static/js/sound/midi.coffee | 14 + static/js/sound/midi.js | 36 + static/js/sound/pianoroll.coffee | 0 static/js/sound/polyfill/index.js | 124 + static/js/sound/polyfill/realm.js | 43 + static/js/sound/slider.coffee | 91 + static/js/sound/slider.js | 104 + static/js/sound/soundeditor.coffee | 70 + static/js/sound/soundeditor.js | 93 + static/js/sound/soundthumbnailer.coffee | 31 + static/js/sound/soundthumbnailer.js | 42 + static/js/sound/synth.coffee | 456 +++ static/js/sound/synth.js | 442 +++ static/js/sound/synth/analog.coffee | 59 + static/js/sound/synth/analog.js | 47 + static/js/sound/synth/audioengine.coffee | 2107 ++++++++++ static/js/sound/synth/audioengine.js | 2351 +++++++++++ static/js/sound/synth/oscillator.coffee | 31 + static/js/sound/synth/oscillator.js | 49 + static/js/sound/synth/synthtest.coffee | 553 +++ static/js/sound/synth/synthtest.js | 588 +++ static/js/sound/synthwheel.coffee | 32 + static/js/sound/synthwheel.js | 44 + static/js/sound/test.html | 29 + static/js/spriteeditor/animationpanel.coffee | 248 ++ static/js/spriteeditor/animationpanel.js | 336 ++ static/js/spriteeditor/autopalette.coffee | 124 + static/js/spriteeditor/autopalette.js | 171 + static/js/spriteeditor/colorpicker.coffee | 223 ++ static/js/spriteeditor/colorpicker.js | 270 ++ static/js/spriteeditor/drawtool.coffee | 343 ++ static/js/spriteeditor/drawtool.js | 473 +++ static/js/spriteeditor/spriteeditor.coffee | 711 ++++ static/js/spriteeditor/spriteeditor.js | 965 +++++ static/js/spriteeditor/spritelist.coffee | 69 + static/js/spriteeditor/spritelist.js | 105 + static/js/spriteeditor/spriteview.coffee | 584 +++ static/js/spriteeditor/spriteview.js | 697 ++++ static/js/terminal/terminal.coffee | 147 + static/js/terminal/terminal.js | 208 + static/js/tutorial/highlighter.coffee | 99 + static/js/tutorial/highlighter.js | 149 + static/js/tutorial/tutorial.coffee | 94 + static/js/tutorial/tutorial.js | 120 + static/js/tutorial/tutorials.coffee | 118 + static/js/tutorial/tutorials.js | 153 + static/js/tutorial/tutorialwindow.coffee | 234 ++ static/js/tutorial/tutorialwindow.js | 315 ++ static/js/user/translationapp.coffee | 60 + static/js/user/translationapp.js | 93 + static/js/user/usersettings.coffee | 266 ++ static/js/user/usersettings.js | 356 ++ static/js/util/canvas2d.coffee | 18 + static/js/util/canvas2d.js | 25 + static/js/util/inputvalidator.coffee | 86 + static/js/util/inputvalidator.js | 167 + static/js/util/pixelartscaler.coffee | 148 + static/js/util/pixelartscaler.js | 173 + static/js/util/pixelatedimage.coffee | 47 + static/js/util/pixelatedimage.js | 63 + static/js/util/random.coffee | 27 + static/js/util/random.js | 44 + static/js/util/regexlib.coffee | 19 + static/js/util/regexlib.js | 20 + static/js/util/splitbar.coffee | 72 + static/js/util/splitbar.js | 98 + static/js/util/translator.coffee | 58 + static/js/util/translator.js | 97 + static/js/util/undo.coffee | 32 + static/js/util/undo.js | 45 + static/lib/mode-microscript.js | 243 ++ static/manifest.json | 20 + static/robots.txt | 1 + static/sitemap.xml | 45 + static/sw.js | 45 + templates/404.pug | 13 + templates/about.pug | 13 + templates/assets.pug | 15 + templates/code.pug | 84 + templates/console/console.pug | 38 + templates/doc.pug | 7 + templates/export/html.pug | 59 + templates/forum/forum.pug | 162 + templates/forum/post.pug | 156 + templates/help.pug | 5 + templates/home.pug | 455 +++ templates/maps.pug | 55 + templates/music.pug | 64 + templates/password/reset1.pug | 42 + templates/password/reset2.pug | 35 + templates/play/manifest.json | 33 + templates/play/play.pug | 70 + templates/play/userpage.pug | 55 + templates/projectdetails.pug | 91 + templates/projectoptions.pug | 77 + templates/publish.pug | 108 + templates/server.pug | 0 templates/sounds.pug | 54 + templates/sprites.pug | 102 + templates/synth.pug | 359 ++ templates/tutorials.pug | 4 + templates/user.pug | 114 + 458 files changed, 88064 insertions(+) create mode 100644 .gitignore create mode 100644 LICENSE.txt create mode 100644 README.md create mode 100644 config_local.json create mode 100644 config_prod.json create mode 100644 server/app/exportfeatures.coffee create mode 100644 server/app/exportfeatures.js create mode 100644 server/app/jobqueue.coffee create mode 100644 server/app/jobqueue.js create mode 100644 server/build/build.coffee create mode 100644 server/build/build.js create mode 100644 server/build/builder.coffee create mode 100644 server/build/builder.js create mode 100644 server/build/buildmanager.coffee create mode 100644 server/build/buildmanager.js create mode 100644 server/concatenator.coffee create mode 100644 server/concatenator.js create mode 100644 server/content/comments.coffee create mode 100644 server/content/comments.js create mode 100644 server/content/content.coffee create mode 100644 server/content/content.js create mode 100644 server/content/project.coffee create mode 100644 server/content/project.js create mode 100644 server/content/tag.coffee create mode 100644 server/content/tag.js create mode 100644 server/content/token.coffee create mode 100644 server/content/token.js create mode 100644 server/content/translator.coffee create mode 100644 server/content/translator.js create mode 100644 server/content/user.coffee create mode 100644 server/content/user.js create mode 100644 server/db/db.coffee create mode 100644 server/db/db.js create mode 100644 server/db/record.coffee create mode 100644 server/db/record.js create mode 100644 server/db/table.coffee create mode 100644 server/db/table.js create mode 100644 server/file_types.coffee create mode 100644 server/file_types.js create mode 100644 server/filestorage/filestorage.coffee create mode 100644 server/filestorage/filestorage.js create mode 100644 server/fonts.coffee create mode 100644 server/fonts.js create mode 100644 server/forum/category.coffee create mode 100644 server/forum/category.js create mode 100644 server/forum/forum.coffee create mode 100644 server/forum/forum.js create mode 100644 server/forum/forumapp.coffee create mode 100644 server/forum/forumapp.js create mode 100644 server/forum/forumsession.coffee create mode 100644 server/forum/forumsession.js create mode 100644 server/forum/indexer.coffee create mode 100644 server/forum/indexer.js create mode 100644 server/forum/post.coffee create mode 100644 server/forum/post.js create mode 100644 server/forum/reply.coffee create mode 100644 server/forum/reply.js create mode 100644 server/gamify/achievements.coffee create mode 100644 server/gamify/achievements.js create mode 100644 server/gamify/levels.coffee create mode 100644 server/gamify/levels.js create mode 100644 server/gamify/userprogress.coffee create mode 100644 server/gamify/userprogress.js create mode 100644 server/package-lock.json create mode 100644 server/package.json create mode 100644 server/ratelimiter.coffee create mode 100644 server/ratelimiter.js create mode 100644 server/server.coffee create mode 100644 server/server.js create mode 100644 server/session/projectmanager.coffee create mode 100644 server/session/projectmanager.js create mode 100644 server/session/session.coffee create mode 100644 server/session/session.js create mode 100644 server/webapp.coffee create mode 100644 server/webapp.js create mode 100644 static/community/forum_sw.js create mode 100644 static/community/manifest.json create mode 100644 static/community/manifest_fr.json create mode 100644 static/css/404.css create mode 100644 static/css/assets.css create mode 100644 static/css/code.css create mode 100644 static/css/console.css create mode 100644 static/css/doc.css create mode 100644 static/css/explore.css create mode 100644 static/css/forum/forum.css create mode 100644 static/css/forum/media.css create mode 100644 static/css/home.css create mode 100644 static/css/jquery.terminal.css create mode 100644 static/css/maps.css create mode 100644 static/css/md.css create mode 100644 static/css/media.css create mode 100644 static/css/music.css create mode 100644 static/css/options.css create mode 100644 static/css/publish.css create mode 100644 static/css/sounds.css create mode 100644 static/css/sprites.css create mode 100644 static/css/style.css create mode 100644 static/css/synth.css create mode 100644 static/css/terminal.css create mode 100644 static/css/tutorial.css create mode 100644 static/css/user.css create mode 100644 static/css/userpage.css create mode 100644 static/doc/en/about.md create mode 100644 static/doc/en/changelog.md create mode 100644 static/doc/en/doc.md create mode 100644 static/doc/en/privacy.md create mode 100644 static/doc/en/terms.md create mode 100644 static/doc/fr/about.md create mode 100644 static/doc/fr/changelog.md create mode 100644 static/doc/fr/doc.md create mode 100644 static/doc/fr/privacy.md create mode 100644 static/doc/fr/terms.md create mode 100644 static/doc/img/screen_coordinates.png create mode 100755 static/fonts/AESystematic.ttf create mode 100755 static/fonts/AESystematic_license.txt create mode 100755 static/fonts/Alkhemikal.ttf create mode 100644 static/fonts/AlphaBeta.ttf create mode 100644 static/fonts/AlphaBeta_license.txt create mode 100755 static/fonts/Arpegius.ttf create mode 100755 static/fonts/Awesome.ttf create mode 100755 static/fonts/BitCell.ttf create mode 100755 static/fonts/Blocktopia.ttf create mode 100755 static/fonts/Comicoro.ttf create mode 100755 static/fonts/Commodore64.ttf create mode 100755 static/fonts/Commodore64_license.txt create mode 100755 static/fonts/DigitalDisco.ttf create mode 100755 static/fonts/Edunline.ttf create mode 100755 static/fonts/Edunline_license.txt create mode 100755 static/fonts/EnchantedSword.ttf create mode 100755 static/fonts/EnterCommand.ttf create mode 100755 static/fonts/Euxoi.ttf create mode 100755 static/fonts/Euxoi_License.txt create mode 100644 static/fonts/FixedBold.ttf create mode 100644 static/fonts/GenericMobileSystem.ttf create mode 100644 static/fonts/GenericMobileSystem_LICENSE.txt create mode 100755 static/fonts/GrapeSoda.ttf create mode 100644 static/fonts/JupiterCrash.ttf create mode 100644 static/fonts/JupiterCrash_License.txt create mode 100755 static/fonts/Kapel.ttf create mode 100755 static/fonts/KiwiSoda.ttf create mode 100755 static/fonts/Litebulb8bit.ttf create mode 100755 static/fonts/LycheeSoda.ttf create mode 100755 static/fonts/MisterPixel.ttf create mode 100644 static/fonts/ModernDos.ttf create mode 100644 static/fonts/ModernDos_LICENSE.txt create mode 100644 static/fonts/NokiaCellPhone.ttf create mode 100755 static/fonts/NokiaCellPhone_LICENSE.txt create mode 100755 static/fonts/PearSoda.ttf create mode 100755 static/fonts/PixAntiqua.ttf create mode 100644 static/fonts/PixChicago.otf create mode 100755 static/fonts/PixChicago.ttf create mode 100755 static/fonts/PixelArial.ttf create mode 100644 static/fonts/PixelOperator.ttf create mode 100644 static/fonts/PixelOperator_LICENSE.txt create mode 100755 static/fonts/Pixellari.ttf create mode 100755 static/fonts/Pixolde.ttf create mode 100755 static/fonts/PlanetaryContact.ttf create mode 100644 static/fonts/PressStart2P.ttf create mode 100644 static/fonts/PressStart2P_LICENSE.txt create mode 100755 static/fonts/RainyHearts.ttf create mode 100755 static/fonts/RetroGaming.ttf create mode 100755 static/fonts/Revolute.ttf create mode 100755 static/fonts/Romulus.ttf create mode 100755 static/fonts/Scriptorium.ttf create mode 100755 static/fonts/Squarewave.ttf create mode 100755 static/fonts/Thixel.ttf create mode 100755 static/fonts/Unbalanced.ttf create mode 100755 static/fonts/UpheavalPro.ttf create mode 100755 static/fonts/UpheavalPro_LICENSE.txt create mode 100644 static/fonts/VeniceClassic.ttf create mode 100644 static/fonts/VeniceClassic_LICENSE.txt create mode 100755 static/fonts/ZXSpectrum.ttf create mode 100755 static/fonts/ZXSpectrum_LICENSE.txt create mode 100755 static/fonts/Zepto.ttf create mode 100755 static/fonts/bit_cell.ttf create mode 100644 static/fr/community/forum_sw.js create mode 100644 static/img/LICENSE_INFORMATION.md create mode 100644 static/img/android_robot.svg create mode 100644 static/img/codeeditor.jpg create mode 100644 static/img/community.jpg create mode 100644 static/img/crazygames.svg create mode 100644 static/img/defaultappicon.png create mode 100644 static/img/eyedropper.svg create mode 100644 static/img/favicon.ico create mode 100644 static/img/favicon.png create mode 100644 static/img/favicon16.png create mode 100644 static/img/favicon180.png create mode 100644 static/img/favicon192.png create mode 100644 static/img/favicon32.png create mode 100644 static/img/favicon512.png create mode 100644 static/img/faviconlarge.png create mode 100644 static/img/forumicon192.png create mode 100644 static/img/forumicon196.png create mode 100644 static/img/forumicon512.png create mode 100644 static/img/gamedistribution.svg create mode 100644 static/img/gamejolt.png create mode 100755 static/img/gamepad.svg create mode 100644 static/img/gamepix.png create mode 100755 static/img/itchio.svg create mode 100644 static/img/kongregate.svg create mode 100644 static/img/macos.svg create mode 100644 static/img/mapeditor.png create mode 100644 static/img/microstudio.jpg create mode 100644 static/img/microstudio.svg create mode 100644 static/img/microstudiologo.svg create mode 100644 static/img/microstudiologoblack.svg create mode 100644 static/img/neuronality.svg create mode 100644 static/img/patreon.png create mode 100644 static/img/raspberry.svg create mode 100644 static/img/smartphone.png create mode 100644 static/img/spriteeditor.jpg create mode 100644 static/img/ubuntu.svg create mode 100644 static/img/windows.svg create mode 100644 static/js/about/about.coffee create mode 100644 static/js/about/about.js create mode 100644 static/js/app.coffee create mode 100644 static/js/app.js create mode 100644 static/js/appstate.coffee create mode 100644 static/js/appstate.js create mode 100644 static/js/appui/appui.coffee create mode 100644 static/js/appui/appui.js create mode 100644 static/js/appui/floatingwindow.coffee create mode 100644 static/js/appui/floatingwindow.js create mode 100644 static/js/assets/assetsmanager.coffee create mode 100644 static/js/assets/assetsmanager.js create mode 100644 static/js/assets/assetviewer.coffee create mode 100644 static/js/assets/assetviewer.js create mode 100644 static/js/client/client.coffee create mode 100644 static/js/client/client.js create mode 100644 static/js/console/console.coffee create mode 100644 static/js/console/console.js create mode 100644 static/js/doc/documentation.coffee create mode 100644 static/js/doc/documentation.js create mode 100644 static/js/doceditor/doceditor.coffee create mode 100644 static/js/doceditor/doceditor.js create mode 100644 static/js/editor/editor.coffee create mode 100644 static/js/editor/editor.js create mode 100644 static/js/editor/rulercanvas.coffee create mode 100644 static/js/editor/rulercanvas.js create mode 100644 static/js/editor/runwindow.coffee create mode 100644 static/js/editor/runwindow.js create mode 100644 static/js/editor/valuetool.coffee create mode 100644 static/js/editor/valuetool.js create mode 100644 static/js/explore/explore.coffee create mode 100644 static/js/explore/explore.js create mode 100644 static/js/explore/projectdetails.coffee create mode 100644 static/js/explore/projectdetails.js create mode 100644 static/js/forum/client.coffee create mode 100644 static/js/forum/client.js create mode 100644 static/js/forum/forum.coffee create mode 100644 static/js/forum/forum.js create mode 100644 static/js/manager.coffee create mode 100644 static/js/manager.js create mode 100644 static/js/mapeditor/mapeditor.coffee create mode 100644 static/js/mapeditor/mapeditor.js create mode 100644 static/js/mapeditor/mapview.coffee create mode 100644 static/js/mapeditor/mapview.js create mode 100644 static/js/mapeditor/tilepicker.coffee create mode 100644 static/js/mapeditor/tilepicker.js create mode 100644 static/js/microscript/jstranspiler.coffee create mode 100644 static/js/microscript/jstranspiler.js create mode 100644 static/js/microscript/microvm.coffee create mode 100644 static/js/microscript/microvm.js create mode 100644 static/js/microscript/parser.coffee create mode 100644 static/js/microscript/parser.js create mode 100644 static/js/microscript/program.coffee create mode 100644 static/js/microscript/program.js create mode 100644 static/js/microscript/random.coffee create mode 100644 static/js/microscript/random.js create mode 100644 static/js/microscript/test.html create mode 100644 static/js/microscript/test.ms create mode 100644 static/js/microscript/todo.txt create mode 100644 static/js/microscript/token.coffee create mode 100644 static/js/microscript/token.js create mode 100644 static/js/microscript/tokenizer.coffee create mode 100644 static/js/microscript/tokenizer.js create mode 100644 static/js/music/musiceditor.coffee create mode 100644 static/js/music/musiceditor.js create mode 100644 static/js/options/options.coffee create mode 100644 static/js/options/options.js create mode 100644 static/js/play/player.coffee create mode 100644 static/js/play/player.js create mode 100644 static/js/play/playerclient.coffee create mode 100644 static/js/play/playerclient.js create mode 100644 static/js/project/project.coffee create mode 100644 static/js/project/project.js create mode 100644 static/js/project/projectasset.coffee create mode 100644 static/js/project/projectasset.js create mode 100644 static/js/project/projectmap.coffee create mode 100644 static/js/project/projectmap.js create mode 100644 static/js/project/projectmusic.coffee create mode 100644 static/js/project/projectmusic.js create mode 100644 static/js/project/projectsound.coffee create mode 100644 static/js/project/projectsound.js create mode 100644 static/js/project/projectsource.coffee create mode 100644 static/js/project/projectsource.js create mode 100644 static/js/project/projectsprite.coffee create mode 100644 static/js/project/projectsprite.js create mode 100644 static/js/publish/appbuild.coffee create mode 100644 static/js/publish/appbuild.js create mode 100644 static/js/publish/publish.coffee create mode 100644 static/js/publish/publish.js create mode 100644 static/js/runtime/audio/audio.coffee create mode 100644 static/js/runtime/audio/audio.js create mode 100644 static/js/runtime/audio/beeper.coffee create mode 100644 static/js/runtime/audio/beeper.js create mode 100644 static/js/runtime/audio/music.coffee create mode 100644 static/js/runtime/audio/music.js create mode 100644 static/js/runtime/audio/sound.coffee create mode 100644 static/js/runtime/audio/sound.js create mode 100644 static/js/runtime/gamepad.coffee create mode 100644 static/js/runtime/gamepad.js create mode 100644 static/js/runtime/keyboard.coffee create mode 100644 static/js/runtime/keyboard.js create mode 100644 static/js/runtime/m3d/m3d.coffee create mode 100644 static/js/runtime/m3d/m3d.js create mode 100644 static/js/runtime/m3d/screen3d.coffee create mode 100644 static/js/runtime/m3d/screen3d.js create mode 100644 static/js/runtime/map.coffee create mode 100644 static/js/runtime/map.js create mode 100644 static/js/runtime/runtime.coffee create mode 100644 static/js/runtime/runtime.js create mode 100644 static/js/runtime/screen.coffee create mode 100644 static/js/runtime/screen.js create mode 100644 static/js/runtime/sprite.coffee create mode 100644 static/js/runtime/sprite.js create mode 100644 static/js/runtime/spriteframe.coffee create mode 100644 static/js/runtime/spriteframe.js create mode 100644 static/js/sound/audiocontroller.coffee create mode 100644 static/js/sound/audiocontroller.js create mode 100644 static/js/sound/audioengine/audioengine.coffee create mode 100644 static/js/sound/audioengine/audioengine.js create mode 100644 static/js/sound/audioengine/effects.coffee create mode 100644 static/js/sound/audioengine/effects.js create mode 100644 static/js/sound/audioengine/filters.coffee create mode 100644 static/js/sound/audioengine/filters.js create mode 100644 static/js/sound/audioengine/instrument.coffee create mode 100644 static/js/sound/audioengine/instrument.js create mode 100644 static/js/sound/audioengine/modulation.coffee create mode 100644 static/js/sound/audioengine/modulation.js create mode 100644 static/js/sound/audioengine/oscillators.coffee create mode 100644 static/js/sound/audioengine/oscillators.js create mode 100644 static/js/sound/audioengine/utils.coffee create mode 100644 static/js/sound/audioengine/utils.js create mode 100644 static/js/sound/audioengine/voice.coffee create mode 100644 static/js/sound/audioengine/voice.js create mode 100644 static/js/sound/audiooutput.coffee create mode 100644 static/js/sound/audiooutput.js create mode 100755 static/js/sound/audioworker.js create mode 100755 static/js/sound/audioworklet.js create mode 100644 static/js/sound/instrument.coffee create mode 100644 static/js/sound/instrument.js create mode 100644 static/js/sound/keyboard.coffee create mode 100644 static/js/sound/keyboard.js create mode 100644 static/js/sound/knob.coffee create mode 100644 static/js/sound/knob.js create mode 100644 static/js/sound/midi.coffee create mode 100644 static/js/sound/midi.js create mode 100644 static/js/sound/pianoroll.coffee create mode 100755 static/js/sound/polyfill/index.js create mode 100755 static/js/sound/polyfill/realm.js create mode 100644 static/js/sound/slider.coffee create mode 100644 static/js/sound/slider.js create mode 100644 static/js/sound/soundeditor.coffee create mode 100644 static/js/sound/soundeditor.js create mode 100644 static/js/sound/soundthumbnailer.coffee create mode 100644 static/js/sound/soundthumbnailer.js create mode 100644 static/js/sound/synth.coffee create mode 100644 static/js/sound/synth.js create mode 100644 static/js/sound/synth/analog.coffee create mode 100644 static/js/sound/synth/analog.js create mode 100644 static/js/sound/synth/audioengine.coffee create mode 100644 static/js/sound/synth/audioengine.js create mode 100644 static/js/sound/synth/oscillator.coffee create mode 100644 static/js/sound/synth/oscillator.js create mode 100644 static/js/sound/synth/synthtest.coffee create mode 100644 static/js/sound/synth/synthtest.js create mode 100644 static/js/sound/synthwheel.coffee create mode 100644 static/js/sound/synthwheel.js create mode 100644 static/js/sound/test.html create mode 100644 static/js/spriteeditor/animationpanel.coffee create mode 100644 static/js/spriteeditor/animationpanel.js create mode 100644 static/js/spriteeditor/autopalette.coffee create mode 100644 static/js/spriteeditor/autopalette.js create mode 100644 static/js/spriteeditor/colorpicker.coffee create mode 100644 static/js/spriteeditor/colorpicker.js create mode 100644 static/js/spriteeditor/drawtool.coffee create mode 100644 static/js/spriteeditor/drawtool.js create mode 100644 static/js/spriteeditor/spriteeditor.coffee create mode 100644 static/js/spriteeditor/spriteeditor.js create mode 100644 static/js/spriteeditor/spritelist.coffee create mode 100644 static/js/spriteeditor/spritelist.js create mode 100644 static/js/spriteeditor/spriteview.coffee create mode 100644 static/js/spriteeditor/spriteview.js create mode 100644 static/js/terminal/terminal.coffee create mode 100644 static/js/terminal/terminal.js create mode 100644 static/js/tutorial/highlighter.coffee create mode 100644 static/js/tutorial/highlighter.js create mode 100644 static/js/tutorial/tutorial.coffee create mode 100644 static/js/tutorial/tutorial.js create mode 100644 static/js/tutorial/tutorials.coffee create mode 100644 static/js/tutorial/tutorials.js create mode 100644 static/js/tutorial/tutorialwindow.coffee create mode 100644 static/js/tutorial/tutorialwindow.js create mode 100644 static/js/user/translationapp.coffee create mode 100644 static/js/user/translationapp.js create mode 100644 static/js/user/usersettings.coffee create mode 100644 static/js/user/usersettings.js create mode 100644 static/js/util/canvas2d.coffee create mode 100644 static/js/util/canvas2d.js create mode 100644 static/js/util/inputvalidator.coffee create mode 100644 static/js/util/inputvalidator.js create mode 100644 static/js/util/pixelartscaler.coffee create mode 100644 static/js/util/pixelartscaler.js create mode 100644 static/js/util/pixelatedimage.coffee create mode 100644 static/js/util/pixelatedimage.js create mode 100644 static/js/util/random.coffee create mode 100644 static/js/util/random.js create mode 100644 static/js/util/regexlib.coffee create mode 100644 static/js/util/regexlib.js create mode 100644 static/js/util/splitbar.coffee create mode 100644 static/js/util/splitbar.js create mode 100644 static/js/util/translator.coffee create mode 100644 static/js/util/translator.js create mode 100644 static/js/util/undo.coffee create mode 100644 static/js/util/undo.js create mode 100644 static/lib/mode-microscript.js create mode 100644 static/manifest.json create mode 100644 static/robots.txt create mode 100644 static/sitemap.xml create mode 100644 static/sw.js create mode 100644 templates/404.pug create mode 100644 templates/about.pug create mode 100644 templates/assets.pug create mode 100644 templates/code.pug create mode 100644 templates/console/console.pug create mode 100644 templates/doc.pug create mode 100644 templates/export/html.pug create mode 100644 templates/forum/forum.pug create mode 100644 templates/forum/post.pug create mode 100644 templates/help.pug create mode 100644 templates/home.pug create mode 100644 templates/maps.pug create mode 100644 templates/music.pug create mode 100644 templates/password/reset1.pug create mode 100644 templates/password/reset2.pug create mode 100644 templates/play/manifest.json create mode 100644 templates/play/play.pug create mode 100644 templates/play/userpage.pug create mode 100644 templates/projectdetails.pug create mode 100644 templates/projectoptions.pug create mode 100644 templates/publish.pug create mode 100644 templates/server.pug create mode 100644 templates/sounds.pug create mode 100644 templates/sprites.pug create mode 100644 templates/synth.pug create mode 100644 templates/tutorials.pug create mode 100644 templates/user.pug diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..4ab9910a --- /dev/null +++ b/.gitignore @@ -0,0 +1,8 @@ +plugins/ +server/.greenlockrc +server/greenlock.d +config.json +server/node_modules/ +files/ +data/ +.DS_Store diff --git a/LICENSE.txt b/LICENSE.txt new file mode 100644 index 00000000..bf5e3ff5 --- /dev/null +++ b/LICENSE.txt @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2021 Gilles Pommereuil + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 00000000..8d81e770 --- /dev/null +++ b/README.md @@ -0,0 +1,15 @@ +microStudio is a free, open source game engine online. +It is also a platform to learn and practise programming. + +microStudio can be used for free at https://microstudio.dev + +You can also install your own copy, to work locally or on your own server +for your team or classroom. You will find instructions below. + +# Installing microStudio to work locally + +* Install Node JS (downloads and instructions: https://nodejs.org/en/download/) +* clone this repository +* `cd microstudio/server` +* `npm install` +* `node server.js` diff --git a/config_local.json b/config_local.json new file mode 100644 index 00000000..a779dcb3 --- /dev/null +++ b/config_local.json @@ -0,0 +1,5 @@ +{ + "realm": "local", + "dev-domain": "localhost:8080", + "run-domain": "localhost:8080" +} diff --git a/config_prod.json b/config_prod.json new file mode 100644 index 00000000..c8fb1cd8 --- /dev/null +++ b/config_prod.json @@ -0,0 +1,7 @@ +{ + "realm": "production", + "dev-domain": "yourdevdomain.com", + "run-domain": "youroutputdomain.com", + "backup-key": "your_backup_service_key", + "builder-key": "your_builder_service_key" +} diff --git a/server/app/exportfeatures.coffee b/server/app/exportfeatures.coffee new file mode 100644 index 00000000..19ac22cc --- /dev/null +++ b/server/app/exportfeatures.coffee @@ -0,0 +1,259 @@ +#fs = require "fs" + +JSZip = require "jszip" +pug = require "pug" +JobQueue = require __dirname+"/jobqueue.js" +{ createCanvas, loadImage } = require('canvas') + +class @ExportFeatures + constructor:(@webapp)-> + @addSpritesExport() + @addPublishHTML() + + addSpritesExport:()-> + # /user/project[/code]/export/sprites/ + @webapp.app.get /^\/[^\/\|\?\&\.]+\/[^\/\|\?\&\.]+\/([^\/\|\?\&\.]+\/)?export\/sprites\/$/,(req,res)=> + access = @webapp.getProjectAccess req,res + return if not access? + + user = access.user + project = access.project + manager = @webapp.getProjectManager(project) + + zip = new JSZip + + queue = new JobQueue ()=> + zip.generateAsync({type:"nodebuffer"}).then (content)=> + res.setHeader("Content-Type", "application/zip") + res.setHeader("Content-Disposition","attachement; filename=\"#{project.slug}_sprites.zip\"") + res.send content + + queue.add ()=> + manager.listFiles "sprites",(sprites)=> + for s in sprites + do (s)=> + queue.add ()=> + console.info "reading: "+JSON.stringify s + @webapp.server.content.files.read "#{user.id}/#{project.id}/sprites/#{s.file}","binary",(content)=> + if content? + zip.file(s.file,content) + queue.next() + queue.next() + + queue.add ()=> + manager.listFiles "doc",(docs)=> + for doc in docs + do (doc)=> + queue.add ()=> + console.info "reading: "+JSON.stringify doc + @webapp.server.content.files.read "#{user.id}/#{project.id}/doc/#{doc.file}","text",(content)=> + if content? + zip.file(doc.file,content) + queue.next() + queue.next() + + queue.start() + + addPublishHTML:()-> + # /user/project[/code]/publish/html/ + @webapp.app.get /^\/[^\/\|\?\&\.]+\/[^\/\|\?\&\.]+\/([^\/\|\?\&\.]+\/)?publish\/html\/$/,(req,res)=> + access = @webapp.getProjectAccess req,res + return if not access? + + user = access.user + project = access.project + manager = @webapp.getProjectManager(project) + + zip = new JSZip + # icon16,32,64,180,192,512,1024.png + # manifest.json + maps_dict = {} + images = [] + assets = [] + fonts = [] + sounds_list = [] + music_list = [] + fullsource = "\n\n" + + queue = new JobQueue ()=> + resources = JSON.stringify + images: images + assets: assets + maps: maps_dict + sounds: sounds_list + music: music_list + + resources = "var resources = #{resources};\n" + resources += "var graphics = \"#{project.graphics}\";\n" + + export_funk = pug.compileFile "../templates/export/html.pug" + + html = export_funk + user: user + javascript_files: ["microengine.js"] + fonts: fonts + game: + name: project.slug + title: project.title + author: user.nick + resources: resources + orientation: project.orientation + aspect: project.aspect + code: fullsource + + zip.file("index.html",html) + + mani = @webapp.manifest_template.toString().replace(/SCOPE/g,"") + mani = mani.toString().replace("APPNAME",project.title) + mani = mani.toString().replace("APPSHORTNAME",project.title) + mani = mani.toString().replace("ORIENTATION",project.orientation) + mani = mani.toString().replace(/USER/g,user.nick) + mani = mani.toString().replace(/PROJECT/g,project.slug) + mani = mani.toString().replace(/ICONVERSION/g,"0") + mani = mani.replace("START_URL",".") + + zip.file("manifest.json",mani) + + if project.graphics == "M3D" + zip.file("microengine.js",@webapp.concatenator["player3d_js_concat"]) + else + zip.file("microengine.js",@webapp.concatenator["player_js_concat"]) + + zip.generateAsync({type:"nodebuffer"}).then (content)=> + res.setHeader("Content-Type", "application/zip") + res.setHeader("Content-Disposition","attachement; filename=\"#{project.slug}.zip\"") + res.setHeader("Cache-Control","no-cache") + res.send content + + queue.add ()=> + manager.listFiles "sprites",(sprites)=> + for s in sprites + do (s)=> + queue.add ()=> + console.info "reading: "+JSON.stringify s + @webapp.server.content.files.read "#{user.id}/#{project.id}/sprites/#{s.file}","binary",(content)=> + if content? + zip.file("sprites/#{s.file}",content) + images.push s + queue.next() + queue.next() + + queue.add ()=> + manager.listFiles "maps",(maps)=> + for map in maps + do (map)=> + queue.add ()=> + console.info "reading: "+JSON.stringify map + @webapp.server.content.files.read "#{user.id}/#{project.id}/maps/#{map.file}","text",(content)=> + if content? + maps_dict[map.file.split(".")[0]] = content + queue.next() + queue.next() + + queue.add ()=> + manager.listFiles "sounds",(sounds)=> + for s in sounds + do (s)=> + queue.add ()=> + console.info "reading: "+JSON.stringify s + @webapp.server.content.files.read "#{user.id}/#{project.id}/sounds/#{s.file}","binary",(content)=> + if content? + zip.file("sounds/#{s.file}",content) + sounds_list.push s + queue.next() + queue.next() + + queue.add ()=> + manager.listFiles "music",(music)=> + for m in music + do (m)=> + queue.add ()=> + console.info "reading: "+JSON.stringify m + @webapp.server.content.files.read "#{user.id}/#{project.id}/music/#{m.file}","binary",(content)=> + if content? + zip.file("music/#{m.file}",content) + music_list.push m + queue.next() + queue.next() + + queue.add ()=> + manager.listFiles "assets",(assets)=> + for asset in assets + do (asset)=> + queue.add ()=> + console.info "reading: "+JSON.stringify asset + @webapp.server.content.files.read "#{user.id}/#{project.id}/assets/#{asset.file}","binary",(content)=> + if content? + zip.file("assets/#{asset.file}",content) + assets.push asset + queue.next() + queue.next() + + queue.add ()=> + manager.listFiles "doc",(docs)=> + for doc in docs + do (doc)=> + queue.add ()=> + console.info "reading: "+JSON.stringify doc + @webapp.server.content.files.read "#{user.id}/#{project.id}/doc/#{doc.file}","text",(content)=> + if content? + zip.file(doc.file,content) + queue.next() + queue.next() + + queue.add ()=> + manager.listFiles "ms",(ms)=> + for src in ms + do (src)=> + queue.add ()=> + console.info "reading: "+JSON.stringify src + @webapp.server.content.files.read "#{user.id}/#{project.id}/ms/#{src.file}","text",(content)=> + if content? + fullsource += content+"\n\n" + queue.next() + + queue.add ()=> + for font in @webapp.fonts.fonts + if font == "BitCell" or fullsource.indexOf("\"#{font}\"")>=0 + fonts.push font + do (font)=> + queue.add ()=> + console.info "reading font: #{font}" + @webapp.fonts.read font,(data)=> + if data? + zip.file "fonts/#{font}.ttf",data + queue.next() + + queue.next() + + queue.next() + + queue.add ()=> + path = "#{user.id}/#{project.id}/sprites/icon.png" + path = @webapp.server.content.files.sanitize path + loadImage("../files/#{path}").then ((image)=> + for size in [16,32,64,180,192,512,1024] + do (size)=> + queue.add ()=> + canvas = createCanvas(size,size) + context = canvas.getContext "2d" + context.antialias = "none" + context.imageSmoothingEnabled = false + if image? + context.drawImage image,0,0,size,size + + if size>100 + context.antialias = "default" + context.globalCompositeOperation = "destination-in" + @webapp.fillRoundRect context,0,0,size,size,size/8 + + canvas.toBuffer (err,result)=> + zip.file "icon#{size}.png",result + queue.next() + ),(error)=> + queue.next() + queue.next() + + queue.start() + +module.exports = @ExportFeatures diff --git a/server/app/exportfeatures.js b/server/app/exportfeatures.js new file mode 100644 index 00000000..948754bc --- /dev/null +++ b/server/app/exportfeatures.js @@ -0,0 +1,375 @@ +var JSZip, JobQueue, createCanvas, loadImage, pug, ref; + +JSZip = require("jszip"); + +pug = require("pug"); + +JobQueue = require(__dirname + "/jobqueue.js"); + +ref = require('canvas'), createCanvas = ref.createCanvas, loadImage = ref.loadImage; + +this.ExportFeatures = (function() { + function ExportFeatures(webapp) { + this.webapp = webapp; + this.addSpritesExport(); + this.addPublishHTML(); + } + + ExportFeatures.prototype.addSpritesExport = function() { + return this.webapp.app.get(/^\/[^\/\|\?\&\.]+\/[^\/\|\?\&\.]+\/([^\/\|\?\&\.]+\/)?export\/sprites\/$/, (function(_this) { + return function(req, res) { + var access, manager, project, queue, user, zip; + access = _this.webapp.getProjectAccess(req, res); + if (access == null) { + return; + } + user = access.user; + project = access.project; + manager = _this.webapp.getProjectManager(project); + zip = new JSZip; + queue = new JobQueue(function() { + return zip.generateAsync({ + type: "nodebuffer" + }).then(function(content) { + res.setHeader("Content-Type", "application/zip"); + res.setHeader("Content-Disposition", "attachement; filename=\"" + project.slug + "_sprites.zip\""); + return res.send(content); + }); + }); + queue.add(function() { + return manager.listFiles("sprites", function(sprites) { + var fn, i, len, s; + fn = function(s) { + return queue.add(function() { + console.info("reading: " + JSON.stringify(s)); + return _this.webapp.server.content.files.read(user.id + "/" + project.id + "/sprites/" + s.file, "binary", function(content) { + if (content != null) { + zip.file(s.file, content); + } + return queue.next(); + }); + }); + }; + for (i = 0, len = sprites.length; i < len; i++) { + s = sprites[i]; + fn(s); + } + return queue.next(); + }); + }); + queue.add(function() { + return manager.listFiles("doc", function(docs) { + var doc, fn, i, len; + fn = function(doc) { + return queue.add(function() { + console.info("reading: " + JSON.stringify(doc)); + return _this.webapp.server.content.files.read(user.id + "/" + project.id + "/doc/" + doc.file, "text", function(content) { + if (content != null) { + zip.file(doc.file, content); + } + return queue.next(); + }); + }); + }; + for (i = 0, len = docs.length; i < len; i++) { + doc = docs[i]; + fn(doc); + } + return queue.next(); + }); + }); + return queue.start(); + }; + })(this)); + }; + + ExportFeatures.prototype.addPublishHTML = function() { + return this.webapp.app.get(/^\/[^\/\|\?\&\.]+\/[^\/\|\?\&\.]+\/([^\/\|\?\&\.]+\/)?publish\/html\/$/, (function(_this) { + return function(req, res) { + var access, assets, fonts, fullsource, images, manager, maps_dict, music_list, project, queue, sounds_list, user, zip; + access = _this.webapp.getProjectAccess(req, res); + if (access == null) { + return; + } + user = access.user; + project = access.project; + manager = _this.webapp.getProjectManager(project); + zip = new JSZip; + maps_dict = {}; + images = []; + assets = []; + fonts = []; + sounds_list = []; + music_list = []; + fullsource = "\n\n"; + queue = new JobQueue(function() { + var export_funk, html, mani, resources; + resources = JSON.stringify({ + images: images, + assets: assets, + maps: maps_dict, + sounds: sounds_list, + music: music_list + }); + resources = "var resources = " + resources + ";\n"; + resources += "var graphics = \"" + project.graphics + "\";\n"; + export_funk = pug.compileFile("../templates/export/html.pug"); + html = export_funk({ + user: user, + javascript_files: ["microengine.js"], + fonts: fonts, + game: { + name: project.slug, + title: project.title, + author: user.nick, + resources: resources, + orientation: project.orientation, + aspect: project.aspect, + code: fullsource + } + }); + zip.file("index.html", html); + mani = _this.webapp.manifest_template.toString().replace(/SCOPE/g, ""); + mani = mani.toString().replace("APPNAME", project.title); + mani = mani.toString().replace("APPSHORTNAME", project.title); + mani = mani.toString().replace("ORIENTATION", project.orientation); + mani = mani.toString().replace(/USER/g, user.nick); + mani = mani.toString().replace(/PROJECT/g, project.slug); + mani = mani.toString().replace(/ICONVERSION/g, "0"); + mani = mani.replace("START_URL", "."); + zip.file("manifest.json", mani); + if (project.graphics === "M3D") { + zip.file("microengine.js", _this.webapp.concatenator["player3d_js_concat"]); + } else { + zip.file("microengine.js", _this.webapp.concatenator["player_js_concat"]); + } + return zip.generateAsync({ + type: "nodebuffer" + }).then(function(content) { + res.setHeader("Content-Type", "application/zip"); + res.setHeader("Content-Disposition", "attachement; filename=\"" + project.slug + ".zip\""); + res.setHeader("Cache-Control", "no-cache"); + return res.send(content); + }); + }); + queue.add(function() { + return manager.listFiles("sprites", function(sprites) { + var fn, i, len, s; + fn = function(s) { + return queue.add(function() { + console.info("reading: " + JSON.stringify(s)); + return _this.webapp.server.content.files.read(user.id + "/" + project.id + "/sprites/" + s.file, "binary", function(content) { + if (content != null) { + zip.file("sprites/" + s.file, content); + images.push(s); + } + return queue.next(); + }); + }); + }; + for (i = 0, len = sprites.length; i < len; i++) { + s = sprites[i]; + fn(s); + } + return queue.next(); + }); + }); + queue.add(function() { + return manager.listFiles("maps", function(maps) { + var fn, i, len, map; + fn = function(map) { + return queue.add(function() { + console.info("reading: " + JSON.stringify(map)); + return _this.webapp.server.content.files.read(user.id + "/" + project.id + "/maps/" + map.file, "text", function(content) { + if (content != null) { + maps_dict[map.file.split(".")[0]] = content; + } + return queue.next(); + }); + }); + }; + for (i = 0, len = maps.length; i < len; i++) { + map = maps[i]; + fn(map); + } + return queue.next(); + }); + }); + queue.add(function() { + return manager.listFiles("sounds", function(sounds) { + var fn, i, len, s; + fn = function(s) { + return queue.add(function() { + console.info("reading: " + JSON.stringify(s)); + return _this.webapp.server.content.files.read(user.id + "/" + project.id + "/sounds/" + s.file, "binary", function(content) { + if (content != null) { + zip.file("sounds/" + s.file, content); + sounds_list.push(s); + } + return queue.next(); + }); + }); + }; + for (i = 0, len = sounds.length; i < len; i++) { + s = sounds[i]; + fn(s); + } + return queue.next(); + }); + }); + queue.add(function() { + return manager.listFiles("music", function(music) { + var fn, i, len, m; + fn = function(m) { + return queue.add(function() { + console.info("reading: " + JSON.stringify(m)); + return _this.webapp.server.content.files.read(user.id + "/" + project.id + "/music/" + m.file, "binary", function(content) { + if (content != null) { + zip.file("music/" + m.file, content); + music_list.push(m); + } + return queue.next(); + }); + }); + }; + for (i = 0, len = music.length; i < len; i++) { + m = music[i]; + fn(m); + } + return queue.next(); + }); + }); + queue.add(function() { + return manager.listFiles("assets", function(assets) { + var asset, fn, i, len; + fn = function(asset) { + return queue.add(function() { + console.info("reading: " + JSON.stringify(asset)); + return _this.webapp.server.content.files.read(user.id + "/" + project.id + "/assets/" + asset.file, "binary", function(content) { + if (content != null) { + zip.file("assets/" + asset.file, content); + assets.push(asset); + } + return queue.next(); + }); + }); + }; + for (i = 0, len = assets.length; i < len; i++) { + asset = assets[i]; + fn(asset); + } + return queue.next(); + }); + }); + queue.add(function() { + return manager.listFiles("doc", function(docs) { + var doc, fn, i, len; + fn = function(doc) { + return queue.add(function() { + console.info("reading: " + JSON.stringify(doc)); + return _this.webapp.server.content.files.read(user.id + "/" + project.id + "/doc/" + doc.file, "text", function(content) { + if (content != null) { + zip.file(doc.file, content); + } + return queue.next(); + }); + }); + }; + for (i = 0, len = docs.length; i < len; i++) { + doc = docs[i]; + fn(doc); + } + return queue.next(); + }); + }); + queue.add(function() { + return manager.listFiles("ms", function(ms) { + var fn, i, len, src; + fn = function(src) { + return queue.add(function() { + console.info("reading: " + JSON.stringify(src)); + return _this.webapp.server.content.files.read(user.id + "/" + project.id + "/ms/" + src.file, "text", function(content) { + if (content != null) { + fullsource += content + "\n\n"; + } + return queue.next(); + }); + }); + }; + for (i = 0, len = ms.length; i < len; i++) { + src = ms[i]; + fn(src); + } + queue.add(function() { + var font, j, len1, ref1; + ref1 = _this.webapp.fonts.fonts; + for (j = 0, len1 = ref1.length; j < len1; j++) { + font = ref1[j]; + if (font === "BitCell" || fullsource.indexOf("\"" + font + "\"") >= 0) { + fonts.push(font); + (function(font) { + return queue.add(function() { + console.info("reading font: " + font); + return _this.webapp.fonts.read(font, function(data) { + if (data != null) { + zip.file("fonts/" + font + ".ttf", data); + } + return queue.next(); + }); + }); + })(font); + } + } + return queue.next(); + }); + return queue.next(); + }); + }); + queue.add(function() { + var path; + path = user.id + "/" + project.id + "/sprites/icon.png"; + path = _this.webapp.server.content.files.sanitize(path); + loadImage("../files/" + path).then((function(image) { + var i, len, ref1, results, size; + ref1 = [16, 32, 64, 180, 192, 512, 1024]; + results = []; + for (i = 0, len = ref1.length; i < len; i++) { + size = ref1[i]; + results.push((function(size) { + return queue.add(function() { + var canvas, context; + canvas = createCanvas(size, size); + context = canvas.getContext("2d"); + context.antialias = "none"; + context.imageSmoothingEnabled = false; + if (image != null) { + context.drawImage(image, 0, 0, size, size); + } + if (size > 100) { + context.antialias = "default"; + context.globalCompositeOperation = "destination-in"; + _this.webapp.fillRoundRect(context, 0, 0, size, size, size / 8); + } + return canvas.toBuffer(function(err, result) { + zip.file("icon" + size + ".png", result); + return queue.next(); + }); + }); + })(size)); + } + return results; + }), function(error) { + return queue.next(); + }); + return queue.next(); + }); + return queue.start(); + }; + })(this)); + }; + + return ExportFeatures; + +})(); + +module.exports = this.ExportFeatures; diff --git a/server/app/jobqueue.coffee b/server/app/jobqueue.coffee new file mode 100644 index 00000000..c7d427e2 --- /dev/null +++ b/server/app/jobqueue.coffee @@ -0,0 +1,23 @@ +class @JobQueue + constructor:(@conclude)-> + @jobs = [] + + add:(job)-> + @jobs.push job + + start:()-> + @next() + + next:()-> + setTimeout (()=>@processOne()),0 + + processOne:()-> + if @jobs.length>0 + j = @jobs.splice(0,1)[0] + j(@) + else if @conclude? + c = @conclude + delete @conclude + c @ + +module.exports = @JobQueue diff --git a/server/app/jobqueue.js b/server/app/jobqueue.js new file mode 100644 index 00000000..f1a41bfd --- /dev/null +++ b/server/app/jobqueue.js @@ -0,0 +1,39 @@ +this.JobQueue = (function() { + function JobQueue(conclude) { + this.conclude = conclude; + this.jobs = []; + } + + JobQueue.prototype.add = function(job) { + return this.jobs.push(job); + }; + + JobQueue.prototype.start = function() { + return this.next(); + }; + + JobQueue.prototype.next = function() { + return setTimeout(((function(_this) { + return function() { + return _this.processOne(); + }; + })(this)), 0); + }; + + JobQueue.prototype.processOne = function() { + var c, j; + if (this.jobs.length > 0) { + j = this.jobs.splice(0, 1)[0]; + return j(this); + } else if (this.conclude != null) { + c = this.conclude; + delete this.conclude; + return c(this); + } + }; + + return JobQueue; + +})(); + +module.exports = this.JobQueue; diff --git a/server/build/build.coffee b/server/build/build.coffee new file mode 100644 index 00000000..1d65c520 --- /dev/null +++ b/server/build/build.coffee @@ -0,0 +1,17 @@ +class @Build + constructor:(@project,@target)-> + @status = "request" + @status_text = "Queued" + @progress = 0 + + @version_check = @project.last_modified + + export:()-> + result = + target: @target + progress: @progress + status: @status + status_text: @status_text + error: @error + +module.exports = @Build diff --git a/server/build/build.js b/server/build/build.js new file mode 100644 index 00000000..dd8bda04 --- /dev/null +++ b/server/build/build.js @@ -0,0 +1,26 @@ +this.Build = (function() { + function Build(project, target) { + this.project = project; + this.target = target; + this.status = "request"; + this.status_text = "Queued"; + this.progress = 0; + this.version_check = this.project.last_modified; + } + + Build.prototype["export"] = function() { + var result; + return result = { + target: this.target, + progress: this.progress, + status: this.status, + status_text: this.status_text, + error: this.error + }; + }; + + return Build; + +})(); + +module.exports = this.Build; diff --git a/server/build/builder.coffee b/server/build/builder.coffee new file mode 100644 index 00000000..459582eb --- /dev/null +++ b/server/build/builder.coffee @@ -0,0 +1,142 @@ +class @Builder + constructor:(@manager,@session,@target)-> + @last_active = Date.now() + @downloads = {} + @download_id = 0 + + @session.send "builder_started" + + @session.messageReceived = (msg)=> + @last_active = Date.now() + if typeof msg == "string" + #console.info msg + try + msg = JSON.parse msg + switch msg.name + when "get_job" + @getJob() + + when "build_progress" + @setBuildProgress(msg) + + when "build_complete" + @buildComplete(msg) + + when "build_error" + @buildError(msg) + + catch err + else + id = msg.readUIntBE(0,4) + #console.info "receiving chunk for download #{id}" + if @downloads[id]? + @downloads[id].chunkReceived msg + else + console.info "Download not found: #{id}" + + @session.disconnected = ()=> + @manager.unregisterBuilder @ + @last_active = 0 + + @interval = setInterval (()=> + if not @isActive() + clearInterval @interval + try + @session.socket.close() + @manager.unregisterBuilder @ + catch err + console.error err + ),60000 + + console.info "Builder created" + + getJob:()-> + @current_build = @manager.getJob(@target) + if @current_build? + @current_build.builder = @ + if @current_build.project.public + path = "/#{@current_build.project.owner.nick}/#{@current_build.project.slug}/publish/html/" + else + path = "/#{@current_build.project.owner.nick}/#{@current_build.project.slug}/#{@current_build.project.code}/publish/html/" + + @session.send + name: "job" + project: + title: @current_build.project.title + slug: @current_build.project.slug + owner: @current_build.project.owner.nick + last_modified: @current_build.project.last_modified + orientation: @current_build.project.orientation + download: path + else + @session.send + name: "no_job" + + setBuildProgress:(msg)-> + if @current_build? + @current_build.progress = msg.progress + @current_build.status_text = msg.text + + buildComplete:(msg)-> + @current_build.file_info = + file: msg.file + folder: msg.folder + length: msg.length + + @current_build = null + + buildError:(msg)-> + @current_build.error = msg.error or "Error" + @current_build = null + + + startDownload:(build,res)-> + download = new Downloader(@download_id++,build,res) + @downloads[download.id] = download + download.start() + + removeDownload:(download)-> + delete @downloads[download.id] + + isActive:()-> + @last_active+5*60000>Date.now() + +class Downloader + constructor:(@id,@build,@res)-> + @res.setHeader("Content-Type", "application/octet-stream") + @res.setHeader("Content-Disposition","attachement; filename=\"#{@build.file_info.file}\"") + @res.setHeader("Content-Length",@build.file_info.length) + + @res.on "error",(err)=> + console.error err + + start:()-> + @build.builder.session.send + name: "download" + file: @build.file_info.file + folder: @build.file_info.folder + download_id: @id + + chunkReceived:(buffer)-> + #console.info "processing chunk" + if buffer.byteLength>4 + buf = Buffer.alloc(buffer.byteLength-4) + buffer.copy buf,0,4,buffer.byteLength + #console.info "writing chunk" + cb = ()=> + #console.info "requesting next chunk" + @build.builder.session.send + name: "next_chunk" + download_id: @id + + if @res.write buf,"binary" + cb() + else + @res.once "drain",cb + + else + @res.end() + @build.builder.removeDownload @ + @build.downloaded = true + +module.exports = @Builder diff --git a/server/build/builder.js b/server/build/builder.js new file mode 100644 index 00000000..0b96559b --- /dev/null +++ b/server/build/builder.js @@ -0,0 +1,187 @@ +var Downloader; + +this.Builder = (function() { + function Builder(manager, session, target) { + this.manager = manager; + this.session = session; + this.target = target; + this.last_active = Date.now(); + this.downloads = {}; + this.download_id = 0; + this.session.send("builder_started"); + this.session.messageReceived = (function(_this) { + return function(msg) { + var err, id; + _this.last_active = Date.now(); + if (typeof msg === "string") { + try { + msg = JSON.parse(msg); + switch (msg.name) { + case "get_job": + return _this.getJob(); + case "build_progress": + return _this.setBuildProgress(msg); + case "build_complete": + return _this.buildComplete(msg); + case "build_error": + return _this.buildError(msg); + } + } catch (error) { + err = error; + } + } else { + id = msg.readUIntBE(0, 4); + if (_this.downloads[id] != null) { + return _this.downloads[id].chunkReceived(msg); + } else { + return console.info("Download not found: " + id); + } + } + }; + })(this); + this.session.disconnected = (function(_this) { + return function() { + _this.manager.unregisterBuilder(_this); + return _this.last_active = 0; + }; + })(this); + this.interval = setInterval(((function(_this) { + return function() { + var err; + if (!_this.isActive()) { + clearInterval(_this.interval); + try { + _this.session.socket.close(); + return _this.manager.unregisterBuilder(_this); + } catch (error) { + err = error; + return console.error(err); + } + } + }; + })(this)), 60000); + console.info("Builder created"); + } + + Builder.prototype.getJob = function() { + var path; + this.current_build = this.manager.getJob(this.target); + if (this.current_build != null) { + this.current_build.builder = this; + if (this.current_build.project["public"]) { + path = "/" + this.current_build.project.owner.nick + "/" + this.current_build.project.slug + "/publish/html/"; + } else { + path = "/" + this.current_build.project.owner.nick + "/" + this.current_build.project.slug + "/" + this.current_build.project.code + "/publish/html/"; + } + return this.session.send({ + name: "job", + project: { + title: this.current_build.project.title, + slug: this.current_build.project.slug, + owner: this.current_build.project.owner.nick, + last_modified: this.current_build.project.last_modified, + orientation: this.current_build.project.orientation, + download: path + } + }); + } else { + return this.session.send({ + name: "no_job" + }); + } + }; + + Builder.prototype.setBuildProgress = function(msg) { + if (this.current_build != null) { + this.current_build.progress = msg.progress; + return this.current_build.status_text = msg.text; + } + }; + + Builder.prototype.buildComplete = function(msg) { + this.current_build.file_info = { + file: msg.file, + folder: msg.folder, + length: msg.length + }; + return this.current_build = null; + }; + + Builder.prototype.buildError = function(msg) { + this.current_build.error = msg.error || "Error"; + return this.current_build = null; + }; + + Builder.prototype.startDownload = function(build, res) { + var download; + download = new Downloader(this.download_id++, build, res); + this.downloads[download.id] = download; + return download.start(); + }; + + Builder.prototype.removeDownload = function(download) { + return delete this.downloads[download.id]; + }; + + Builder.prototype.isActive = function() { + return this.last_active + 5 * 60000 > Date.now(); + }; + + return Builder; + +})(); + +Downloader = (function() { + function Downloader(id1, build1, res1) { + this.id = id1; + this.build = build1; + this.res = res1; + this.res.setHeader("Content-Type", "application/octet-stream"); + this.res.setHeader("Content-Disposition", "attachement; filename=\"" + this.build.file_info.file + "\""); + this.res.setHeader("Content-Length", this.build.file_info.length); + this.res.on("error", (function(_this) { + return function(err) { + return console.error(err); + }; + })(this)); + } + + Downloader.prototype.start = function() { + return this.build.builder.session.send({ + name: "download", + file: this.build.file_info.file, + folder: this.build.file_info.folder, + download_id: this.id + }); + }; + + Downloader.prototype.chunkReceived = function(buffer) { + var buf, cb; + if (buffer.byteLength > 4) { + buf = Buffer.alloc(buffer.byteLength - 4); + buffer.copy(buf, 0, 4, buffer.byteLength); + cb = (function(_this) { + return function() { + return _this.build.builder.session.send({ + name: "next_chunk", + download_id: _this.id + }); + }; + })(this); + if (this.res.write(buf, "binary")) { + return cb(); + } else { + return this.res.once("drain", cb); + } + } else { + this.res.end(); + this.build.builder.removeDownload(this); + return this.build.downloaded = true; + } + }; + + return Downloader; + +})(); + +module.exports = this.Builder; diff --git a/server/build/buildmanager.coffee b/server/build/buildmanager.coffee new file mode 100644 index 00000000..927d6631 --- /dev/null +++ b/server/build/buildmanager.coffee @@ -0,0 +1,100 @@ +Builder = require __dirname+"/builder.js" +Build = require __dirname+"/build.js" + +class @BuildManager + constructor:(@server)-> + @builders = {} + @builds = {} + @jobs = [] + + registerBuilder:(session,target)-> + builder = new Builder @,session,target + + if not @builders[target] + @builders[target] = [builder] + else + @builders[target].push(builder) + + unregisterBuilder:(builder)-> + target = builder.target + if @builders[target]? + index = @builders[target].indexOf(builder) + if index >= 0 + @builders[target].splice index,1 + + getActiveBuilders:()-> + s = "" + for key,value of @builders + if value.length>0 + if s.length>0 + s += ", " + s += key+" (#{value.length})" + + s + + startBuild:(project,target)-> + key = project.id+"-"+target + if not @builds[key] + @builds[key] = new Build(project,target) + + @jobs.push @builds[key] + + @builds[key] + + hasBuilder:(target)-> + if @builders[target]? + for b in @builders[target] + if b.isActive() + return true + false + + getBuildInfo:(project,target)-> + key = project.id+"-"+target + build = @builds[key] + if build? + if build.downloaded and build.version_check != project.last_modified + delete @builds[key] + return null + + if build.builder? and not build.builder.isActive() + delete @builds[key] + return null + + build + + getJob:(target)-> + for build,i in @jobs + if build.status == "request" and build.target == target + build.status = "starting" + @jobs.splice i,1 + return build + return null + + createLinks:(app)-> + @addDownloadBuild(app) + + addDownloadBuild:(app)-> + # /user/project[/code]/download// + app.get /^\/[^\/\|\?\&\.]+\/[^\/\|\?\&\.]+\/([^\/\|\?\&\.]+\/)?download\/[^\/\|\?\&\.]+\/$/,(req,res)=> + #console.info "download build" + access = @server.webapp.getProjectAccess req,res + return if not access? + #console.info "access ok" + + user = access.user + project = access.project + manager = @server.webapp.getProjectManager(project) + split = req.path.split("/") + #console.info JSON.stringify split + target = split[split.length-2] + + if project? + #console.info "project ok, target = #{target}" + key = project.id+"-"+target + build = @builds[key] + #console.info JSON.stringify build.export() + if build? and build.progress == 100 and build.file_info? and build.builder? + #console.info "build ok" + build.builder.startDownload(build,res) + +module.exports = @BuildManager diff --git a/server/build/buildmanager.js b/server/build/buildmanager.js new file mode 100644 index 00000000..25b806bb --- /dev/null +++ b/server/build/buildmanager.js @@ -0,0 +1,139 @@ +var Build, Builder; + +Builder = require(__dirname + "/builder.js"); + +Build = require(__dirname + "/build.js"); + +this.BuildManager = (function() { + function BuildManager(server) { + this.server = server; + this.builders = {}; + this.builds = {}; + this.jobs = []; + } + + BuildManager.prototype.registerBuilder = function(session, target) { + var builder; + builder = new Builder(this, session, target); + if (!this.builders[target]) { + return this.builders[target] = [builder]; + } else { + return this.builders[target].push(builder); + } + }; + + BuildManager.prototype.unregisterBuilder = function(builder) { + var index, target; + target = builder.target; + if (this.builders[target] != null) { + index = this.builders[target].indexOf(builder); + if (index >= 0) { + return this.builders[target].splice(index, 1); + } + } + }; + + BuildManager.prototype.getActiveBuilders = function() { + var key, ref, s, value; + s = ""; + ref = this.builders; + for (key in ref) { + value = ref[key]; + if (value.length > 0) { + if (s.length > 0) { + s += ", "; + } + s += key + (" (" + value.length + ")"); + } + } + return s; + }; + + BuildManager.prototype.startBuild = function(project, target) { + var key; + key = project.id + "-" + target; + if (!this.builds[key]) { + this.builds[key] = new Build(project, target); + } + this.jobs.push(this.builds[key]); + return this.builds[key]; + }; + + BuildManager.prototype.hasBuilder = function(target) { + var b, j, len, ref; + if (this.builders[target] != null) { + ref = this.builders[target]; + for (j = 0, len = ref.length; j < len; j++) { + b = ref[j]; + if (b.isActive()) { + return true; + } + } + } + return false; + }; + + BuildManager.prototype.getBuildInfo = function(project, target) { + var build, key; + key = project.id + "-" + target; + build = this.builds[key]; + if (build != null) { + if (build.downloaded && build.version_check !== project.last_modified) { + delete this.builds[key]; + return null; + } + if ((build.builder != null) && !build.builder.isActive()) { + delete this.builds[key]; + return null; + } + } + return build; + }; + + BuildManager.prototype.getJob = function(target) { + var build, i, j, len, ref; + ref = this.jobs; + for (i = j = 0, len = ref.length; j < len; i = ++j) { + build = ref[i]; + if (build.status === "request" && build.target === target) { + build.status = "starting"; + this.jobs.splice(i, 1); + return build; + } + } + return null; + }; + + BuildManager.prototype.createLinks = function(app) { + return this.addDownloadBuild(app); + }; + + BuildManager.prototype.addDownloadBuild = function(app) { + return app.get(/^\/[^\/\|\?\&\.]+\/[^\/\|\?\&\.]+\/([^\/\|\?\&\.]+\/)?download\/[^\/\|\?\&\.]+\/$/, (function(_this) { + return function(req, res) { + var access, build, key, manager, project, split, target, user; + access = _this.server.webapp.getProjectAccess(req, res); + if (access == null) { + return; + } + user = access.user; + project = access.project; + manager = _this.server.webapp.getProjectManager(project); + split = req.path.split("/"); + target = split[split.length - 2]; + if (project != null) { + key = project.id + "-" + target; + build = _this.builds[key]; + if ((build != null) && build.progress === 100 && (build.file_info != null) && (build.builder != null)) { + return build.builder.startDownload(build, res); + } + } + }; + })(this)); + }; + + return BuildManager; + +})(); + +module.exports = this.BuildManager; diff --git a/server/concatenator.coffee b/server/concatenator.coffee new file mode 100644 index 00000000..65475f68 --- /dev/null +++ b/server/concatenator.coffee @@ -0,0 +1,259 @@ +fs = require "fs" + +class @Concatenator + constructor:(@webapp)-> + @webapp.app.get /^\/all.js$/, (req,res)=> + res.setHeader("Content-Type", "text/javascript") + res.send @webapp_js_concat + + @webapp.app.get /^\/all.css$/, (req,res)=> + res.setHeader("Content-Type", "text/css") + res.send @webapp_css_concat + + @webapp.app.get /^\/play.js$/, (req,res)=> + res.setHeader("Content-Type", "text/javascript") + res.send @player_js_concat + + @webapp.app.get /^\/play3d.js$/, (req,res)=> + res.setHeader("Content-Type", "text/javascript") + res.send @player3d_js_concat + + @webapp.app.get /^\/audioengine.js$/, (req,res)=> + res.setHeader("Content-Type", "text/javascript") + if not @webapp.server.use_cache + @concat @audioengine_js,"audioengine_js_concat",()=> + res.send @audioengine_js_concat + else + res.send @audioengine_js_concat + + @webapp_css = [ + "/css/style.css" + "/css/home.css" + "/css/doc.css" + "/css/code.css" + "/css/assets.css" + "/css/sprites.css" + "/css/sounds.css" + "/css/synth.css" + "/css/music.css" + "/css/maps.css" + "/css/publish.css" + "/css/explore.css" + "/css/options.css" + "/css/user.css" + "/css/media.css" + "/css/terminal.css" + "/css/tutorial.css" + "/css/md.css" + ] + + @webapp_js = [ + "/js/microscript/random.js" + "/js/microscript/microvm.js" + "/js/microscript/tokenizer.js" + "/js/microscript/token.js" + "/js/microscript/parser.js" + "/js/microscript/program.js" + "/js/microscript/jstranspiler.js" + + "/js/client/client.js" + + "/js/util/canvas2d.js" + "/js/util/regexlib.js" + "/js/util/inputvalidator.js" + "/js/util/translator.js" + + "/js/manager.js" + + "/js/about/about.js" + "/js/doc/documentation.js" + "/js/doceditor/doceditor.js" + "/js/editor/editor.js" + "/js/editor/runwindow.js" + "/js/editor/rulercanvas.js" + "/js/editor/valuetool.js" + "/js/options/options.js" + + "/js/publish/publish.js" + "/js/publish/appbuild.js" + + "/js/explore/explore.js" + "/js/explore/projectdetails.js" + "/js/user/usersettings.js" + "/js/user/translationapp.js" + + "/js/spriteeditor/drawtool.js" + "/js/spriteeditor/spritelist.js" + "/js/spriteeditor/spriteeditor.js" + "/js/spriteeditor/colorpicker.js" + "/js/spriteeditor/spriteview.js" + "/js/spriteeditor/animationpanel.js" + "/js/spriteeditor/autopalette.js" + + "/js/mapeditor/mapview.js" + "/js/mapeditor/mapeditor.js" + "/js/mapeditor/tilepicker.js" + + "/js/assets/assetsmanager.js" + "/js/assets/assetviewer.js" + + "/js/sound/audiocontroller.js" + "/js/sound/knob.js" + "/js/sound/slider.js" + "/js/sound/keyboard.js" + "/js/sound/synthwheel.js" + "/js/sound/synth.js" + "/js/sound/soundeditor.js" + "/js/sound/soundthumbnailer.js" + + "/js/music/musiceditor.js" + + "/js/util/undo.js" + "/js/util/random.js" + "/js/util/splitbar.js" + "/js/util/pixelartscaler.js" + "/js/util/pixelatedimage.js" + + "/js/runtime/sprite.js" + "/js/runtime/spriteframe.js" + "/js/runtime/map.js" + + "/js/terminal/terminal.js" + + "/js/project/project.js" + "/js/project/projectsource.js" + "/js/project/projectsprite.js" + "/js/project/projectmap.js" + "/js/project/projectasset.js" + "/js/project/projectsound.js" + "/js/project/projectmusic.js" + + "/js/appui/floatingwindow.js" + "/js/appui/appui.js" + "/js/app.js" + "/js/appstate.js" + + "/js/tutorial/tutorials.js" + "/js/tutorial/tutorial.js" + "/js/tutorial/tutorialwindow.js" + "/js/tutorial/highlighter.js" + ] + + @player_js = [ + '/js/util/canvas2d.js' + + "/js/microscript/random.js" + "/js/microscript/microvm.js" + "/js/microscript/tokenizer.js" + "/js/microscript/token.js" + "/js/microscript/parser.js" + "/js/microscript/program.js" + "/js/microscript/jstranspiler.js" + + '/js/runtime/runtime.js' + '/js/runtime/screen.js' + '/js/runtime/keyboard.js' + '/js/runtime/gamepad.js' + '/js/runtime/sprite.js' + '/js/runtime/spriteframe.js' + '/js/runtime/map.js' + "/js/runtime/audio/audio.js" + "/js/runtime/audio/beeper.js" + "/js/runtime/audio/sound.js" + "/js/runtime/audio/music.js" + '/js/play/player.js' + '/js/play/playerclient.js' + ] + + @player3d_js = [ + '/js/util/canvas2d.js' + + "/js/microscript/random.js" + "/js/microscript/microvm.js" + "/js/microscript/tokenizer.js" + "/js/microscript/token.js" + "/js/microscript/parser.js" + "/js/microscript/program.js" + "/js/microscript/jstranspiler.js" + + '/js/runtime/m3d/screen3d.js' + '/js/runtime/m3d/m3d.js' + + '/js/runtime/runtime.js' + '/js/runtime/keyboard.js' + '/js/runtime/gamepad.js' + '/js/runtime/sprite.js' + '/js/runtime/spriteframe.js' + '/js/runtime/map.js' + "/js/runtime/audio/audio.js" + "/js/runtime/audio/beeper.js" + "/js/runtime/audio/sound.js" + "/js/runtime/audio/music.js" + '/js/play/player.js' + '/js/play/playerclient.js' + ] + + @audioengine_js = [ + "/js/sound/audioengine/utils.js" + "/js/sound/audioengine/oscillators.js" + "/js/sound/audioengine/modulation.js" + "/js/sound/audioengine/filters.js" + "/js/sound/audioengine/effects.js" + "/js/sound/audioengine/voice.js" + "/js/sound/audioengine/instrument.js" + "/js/sound/audioengine/audioengine.js" + ] + + @refresh() + + refresh:()-> + @concat(@webapp_js,"webapp_js_concat") + @concat(@player_js,"player_js_concat") + @concat(@player3d_js,"player3d_js_concat") + @concat(@webapp_css,"webapp_css_concat") + @concat(@audioengine_js,"audioengine_js_concat") + + getHomeJSFiles:()-> + if @webapp.server.use_cache and @webapp_js_concat? + ["/all.js"] + else + @webapp_js + + getHomeCSSFiles:()-> + if @webapp.server.use_cache and @webapp_css_concat? + ["/all.css"] + else + @webapp_css + + getPlayerJSFiles:(graphics)-> + if graphics == "M3D" + if @webapp.server.use_cache and @player3d_js_concat? + ["/play3d.js"] + else + @player3d_js + else + if @webapp.server.use_cache and @player_js_concat? + ["/play.js"] + else + @player_js + + concat:(files,variable,callback)-> + list = (f for f in files) + res = "" + funk = ()=> + if list.length>0 + f = list.splice(0,1)[0] + f = "../static"+f + fs.readFile f,(err,data)=> + if data? and not err? + res += data+"\n" + funk() + else if err + console.info err + else + @[variable] = res + callback() if callback? + + funk() + +module.exports = @Concatenator diff --git a/server/concatenator.js b/server/concatenator.js new file mode 100644 index 00000000..7640768f --- /dev/null +++ b/server/concatenator.js @@ -0,0 +1,132 @@ +var fs; + +fs = require("fs"); + +this.Concatenator = (function() { + function Concatenator(webapp) { + this.webapp = webapp; + this.webapp.app.get(/^\/all.js$/, (function(_this) { + return function(req, res) { + res.setHeader("Content-Type", "text/javascript"); + return res.send(_this.webapp_js_concat); + }; + })(this)); + this.webapp.app.get(/^\/all.css$/, (function(_this) { + return function(req, res) { + res.setHeader("Content-Type", "text/css"); + return res.send(_this.webapp_css_concat); + }; + })(this)); + this.webapp.app.get(/^\/play.js$/, (function(_this) { + return function(req, res) { + res.setHeader("Content-Type", "text/javascript"); + return res.send(_this.player_js_concat); + }; + })(this)); + this.webapp.app.get(/^\/play3d.js$/, (function(_this) { + return function(req, res) { + res.setHeader("Content-Type", "text/javascript"); + return res.send(_this.player3d_js_concat); + }; + })(this)); + this.webapp.app.get(/^\/audioengine.js$/, (function(_this) { + return function(req, res) { + res.setHeader("Content-Type", "text/javascript"); + if (!_this.webapp.server.use_cache) { + return _this.concat(_this.audioengine_js, "audioengine_js_concat", function() { + return res.send(_this.audioengine_js_concat); + }); + } else { + return res.send(_this.audioengine_js_concat); + } + }; + })(this)); + this.webapp_css = ["/css/style.css", "/css/home.css", "/css/doc.css", "/css/code.css", "/css/assets.css", "/css/sprites.css", "/css/sounds.css", "/css/synth.css", "/css/music.css", "/css/maps.css", "/css/publish.css", "/css/explore.css", "/css/options.css", "/css/user.css", "/css/media.css", "/css/terminal.css", "/css/tutorial.css", "/css/md.css"]; + this.webapp_js = ["/js/microscript/random.js", "/js/microscript/microvm.js", "/js/microscript/tokenizer.js", "/js/microscript/token.js", "/js/microscript/parser.js", "/js/microscript/program.js", "/js/microscript/jstranspiler.js", "/js/client/client.js", "/js/util/canvas2d.js", "/js/util/regexlib.js", "/js/util/inputvalidator.js", "/js/util/translator.js", "/js/manager.js", "/js/about/about.js", "/js/doc/documentation.js", "/js/doceditor/doceditor.js", "/js/editor/editor.js", "/js/editor/runwindow.js", "/js/editor/rulercanvas.js", "/js/editor/valuetool.js", "/js/options/options.js", "/js/publish/publish.js", "/js/publish/appbuild.js", "/js/explore/explore.js", "/js/explore/projectdetails.js", "/js/user/usersettings.js", "/js/user/translationapp.js", "/js/spriteeditor/drawtool.js", "/js/spriteeditor/spritelist.js", "/js/spriteeditor/spriteeditor.js", "/js/spriteeditor/colorpicker.js", "/js/spriteeditor/spriteview.js", "/js/spriteeditor/animationpanel.js", "/js/spriteeditor/autopalette.js", "/js/mapeditor/mapview.js", "/js/mapeditor/mapeditor.js", "/js/mapeditor/tilepicker.js", "/js/assets/assetsmanager.js", "/js/assets/assetviewer.js", "/js/sound/audiocontroller.js", "/js/sound/knob.js", "/js/sound/slider.js", "/js/sound/keyboard.js", "/js/sound/synthwheel.js", "/js/sound/synth.js", "/js/sound/soundeditor.js", "/js/sound/soundthumbnailer.js", "/js/music/musiceditor.js", "/js/util/undo.js", "/js/util/random.js", "/js/util/splitbar.js", "/js/util/pixelartscaler.js", "/js/util/pixelatedimage.js", "/js/runtime/sprite.js", "/js/runtime/spriteframe.js", "/js/runtime/map.js", "/js/terminal/terminal.js", "/js/project/project.js", "/js/project/projectsource.js", "/js/project/projectsprite.js", "/js/project/projectmap.js", "/js/project/projectasset.js", "/js/project/projectsound.js", "/js/project/projectmusic.js", "/js/appui/floatingwindow.js", "/js/appui/appui.js", "/js/app.js", "/js/appstate.js", "/js/tutorial/tutorials.js", "/js/tutorial/tutorial.js", "/js/tutorial/tutorialwindow.js", "/js/tutorial/highlighter.js"]; + this.player_js = ['/js/util/canvas2d.js', "/js/microscript/random.js", "/js/microscript/microvm.js", "/js/microscript/tokenizer.js", "/js/microscript/token.js", "/js/microscript/parser.js", "/js/microscript/program.js", "/js/microscript/jstranspiler.js", '/js/runtime/runtime.js', '/js/runtime/screen.js', '/js/runtime/keyboard.js', '/js/runtime/gamepad.js', '/js/runtime/sprite.js', '/js/runtime/spriteframe.js', '/js/runtime/map.js', "/js/runtime/audio/audio.js", "/js/runtime/audio/beeper.js", "/js/runtime/audio/sound.js", "/js/runtime/audio/music.js", '/js/play/player.js', '/js/play/playerclient.js']; + this.player3d_js = ['/js/util/canvas2d.js', "/js/microscript/random.js", "/js/microscript/microvm.js", "/js/microscript/tokenizer.js", "/js/microscript/token.js", "/js/microscript/parser.js", "/js/microscript/program.js", "/js/microscript/jstranspiler.js", '/js/runtime/m3d/screen3d.js', '/js/runtime/m3d/m3d.js', '/js/runtime/runtime.js', '/js/runtime/keyboard.js', '/js/runtime/gamepad.js', '/js/runtime/sprite.js', '/js/runtime/spriteframe.js', '/js/runtime/map.js', "/js/runtime/audio/audio.js", "/js/runtime/audio/beeper.js", "/js/runtime/audio/sound.js", "/js/runtime/audio/music.js", '/js/play/player.js', '/js/play/playerclient.js']; + this.audioengine_js = ["/js/sound/audioengine/utils.js", "/js/sound/audioengine/oscillators.js", "/js/sound/audioengine/modulation.js", "/js/sound/audioengine/filters.js", "/js/sound/audioengine/effects.js", "/js/sound/audioengine/voice.js", "/js/sound/audioengine/instrument.js", "/js/sound/audioengine/audioengine.js"]; + this.refresh(); + } + + Concatenator.prototype.refresh = function() { + this.concat(this.webapp_js, "webapp_js_concat"); + this.concat(this.player_js, "player_js_concat"); + this.concat(this.player3d_js, "player3d_js_concat"); + this.concat(this.webapp_css, "webapp_css_concat"); + return this.concat(this.audioengine_js, "audioengine_js_concat"); + }; + + Concatenator.prototype.getHomeJSFiles = function() { + if (this.webapp.server.use_cache && (this.webapp_js_concat != null)) { + return ["/all.js"]; + } else { + return this.webapp_js; + } + }; + + Concatenator.prototype.getHomeCSSFiles = function() { + if (this.webapp.server.use_cache && (this.webapp_css_concat != null)) { + return ["/all.css"]; + } else { + return this.webapp_css; + } + }; + + Concatenator.prototype.getPlayerJSFiles = function(graphics) { + if (graphics === "M3D") { + if (this.webapp.server.use_cache && (this.player3d_js_concat != null)) { + return ["/play3d.js"]; + } else { + return this.player3d_js; + } + } else { + if (this.webapp.server.use_cache && (this.player_js_concat != null)) { + return ["/play.js"]; + } else { + return this.player_js; + } + } + }; + + Concatenator.prototype.concat = function(files, variable, callback) { + var f, funk, list, res; + list = (function() { + var i, len, results; + results = []; + for (i = 0, len = files.length; i < len; i++) { + f = files[i]; + results.push(f); + } + return results; + })(); + res = ""; + funk = (function(_this) { + return function() { + if (list.length > 0) { + f = list.splice(0, 1)[0]; + f = "../static" + f; + return fs.readFile(f, function(err, data) { + if ((data != null) && (err == null)) { + res += data + "\n"; + return funk(); + } else if (err) { + return console.info(err); + } + }); + } else { + _this[variable] = res; + if (callback != null) { + return callback(); + } + } + }; + })(this); + return funk(); + }; + + return Concatenator; + +})(); + +module.exports = this.Concatenator; diff --git a/server/content/comments.coffee b/server/content/comments.coffee new file mode 100644 index 00000000..641a0ca4 --- /dev/null +++ b/server/content/comments.coffee @@ -0,0 +1,68 @@ +class @Comments + constructor:(@project,data)-> + @comments = [] + @load(data) + + load:(data)-> + if data? and data.length>0 + for c in data + user = @project.content.users[c.user] + if user? + @comments.push new Comment @,user,c + return + + save:()-> + res = [] + for c in @comments + res.push + user: c.user.id + text: c.text + flags: c.flags + time: c.time + @project.set("comments",res,false) + + getAll:()-> + res = [] + for c,i in @comments + if not c.flags.deleted and not c.user.flags.censored + res.push + user: c.user.nick + user_info: + tier: c.user.flags.tier + text: c.text + id: i + time: c.time + return res + + get:(id)-> + @comments[id] + + add:(user,text)-> + @comments.push new Comment @,user, + text: text + flags: {} + time: Date.now() + @save() + + remove:(comment)-> + if comment? + comment.flags.deleted = true + + # index = @comments.indexOf(comment) + # if index>=0 + # @comments.splice(index,1) + # @save() + +class Comment + constructor:(@comments,@user,data)-> + @text = data.text + @flags = data.flags + @time = data.time + + edit:(@text)-> + @comments.save() + + remove:()-> + @comments.remove(@) + +module.exports = @Comments diff --git a/server/content/comments.js b/server/content/comments.js new file mode 100644 index 00000000..5e6a6c7b --- /dev/null +++ b/server/content/comments.js @@ -0,0 +1,105 @@ +var Comment; + +this.Comments = (function() { + function Comments(project, data) { + this.project = project; + this.comments = []; + this.load(data); + } + + Comments.prototype.load = function(data) { + var c, j, len, user; + if ((data != null) && data.length > 0) { + for (j = 0, len = data.length; j < len; j++) { + c = data[j]; + user = this.project.content.users[c.user]; + if (user != null) { + this.comments.push(new Comment(this, user, c)); + } + } + } + }; + + Comments.prototype.save = function() { + var c, j, len, ref, res; + res = []; + ref = this.comments; + for (j = 0, len = ref.length; j < len; j++) { + c = ref[j]; + res.push({ + user: c.user.id, + text: c.text, + flags: c.flags, + time: c.time + }); + } + return this.project.set("comments", res, false); + }; + + Comments.prototype.getAll = function() { + var c, i, j, len, ref, res; + res = []; + ref = this.comments; + for (i = j = 0, len = ref.length; j < len; i = ++j) { + c = ref[i]; + if (!c.flags.deleted && !c.user.flags.censored) { + res.push({ + user: c.user.nick, + user_info: { + tier: c.user.flags.tier + }, + text: c.text, + id: i, + time: c.time + }); + } + } + return res; + }; + + Comments.prototype.get = function(id) { + return this.comments[id]; + }; + + Comments.prototype.add = function(user, text) { + this.comments.push(new Comment(this, user, { + text: text, + flags: {}, + time: Date.now() + })); + return this.save(); + }; + + Comments.prototype.remove = function(comment) { + if (comment != null) { + return comment.flags.deleted = true; + } + }; + + return Comments; + +})(); + +Comment = (function() { + function Comment(comments, user1, data) { + this.comments = comments; + this.user = user1; + this.text = data.text; + this.flags = data.flags; + this.time = data.time; + } + + Comment.prototype.edit = function(text1) { + this.text = text1; + return this.comments.save(); + }; + + Comment.prototype.remove = function() { + return this.comments.remove(this); + }; + + return Comment; + +})(); + +module.exports = this.Comments; diff --git a/server/content/content.coffee b/server/content/content.coffee new file mode 100644 index 00000000..37fa6a73 --- /dev/null +++ b/server/content/content.coffee @@ -0,0 +1,339 @@ +usage = require "pidusage" +User = require __dirname+"/user.js" +Project = require __dirname+"/project.js" +Tag = require __dirname+"/tag.js" +Token = require __dirname+"/token.js" +Translator = require __dirname+"/translator.js" +Forum = require __dirname+"/../forum/forum.js" + +class @Content + constructor:(@server,@db,@files)-> + @users = {} + @users_by_email = {} + @users_by_nick = {} + + @tokens = {} + + @projects = {} + @tags = {} + + @project_count = 0 + @user_count = 0 + @guest_count = 0 + + @load() + @hot_projects = [] + @top_projects = [] + @new_projects = [] + @updatePublicProjects() + + console.info "Content loaded: #{@user_count} users and #{@project_count} projects" + + @top_interval = setInterval (()=> @sortPublicProjects()),10001 + @log_interval = setInterval (()=> @statusLog()),6000 + + @translator = new Translator @ + @forum = new Forum @ + #@test_data = new TestData @ + + close:()-> + clearInterval @top_interval + clearInterval @log_interval + @forum.close() + @test_data.close() if @test_data? + + statusLog:()-> + usage process.pid,(err,result)=> + return if not result? + console.info "------------" + console.info "#{new Date().toString()}" + console.info "cpu: #{Math.round(result.cpu)}%" + console.info "memory: #{Math.round(result.memory/1000000)} mb" + console.info "users: #{@user_count}" + console.info "projects: #{@project_count}" + @current_cpu = Math.round(result.cpu) + @current_memory = Math.round(result.memory/1000000) + @server.stats.max("cpu_max",Math.round(result.cpu)) + @server.stats.max("memory_max",@current_memory) + + load:()-> + users = @db.list("users") + for record in users + @loadUser record + + tokens = @db.list("tokens") + for token in tokens + @loadToken token + + projects = @db.list("projects") + for record in projects + @loadProject record + + @initLikes() + + return + + loadUser:(record)-> + data = record.get() + user = new User @,record + return if user.flags.deleted + @users[user.id] = user + if user.email? + @users_by_email[user.email] = user + else + @guest_count += 1 + @users_by_nick[user.nick] = user + @user_count++ + user + + loadProject:(record)-> + data = record.get() + project = new Project @,record + if project.owner? and not project.deleted + @projects[project.id] = project + @loadTags(project) + @project_count++ + project + + loadTags:(project)-> + for t in project.tags + tag = @tags[t] + if not tag? + tag = new Tag(t) + @tags[t] = tag + tag.add project + + return + + loadToken:(record)-> + data = record.get() + token = new Token @,record + @tokens[token.value] = token + token + + initLikes:()-> + for key,user of @users + for f in user.likes + if @projects[f]? + @projects[f].likes++ + + return + + updatePublicProjects:()-> + @hot_projects = [] + @top_projects = [] + @new_projects = [] + for key,project of @projects + if project.public and project.owner.flags["validated"] and not project.deleted and not project.owner.flags["censored"] + @hot_projects.push project + @top_projects.push project + @new_projects.push project + + @sortPublicProjects() + + sortPublicProjects:()-> + time = Date.now() + @top_projects.sort (a,b)-> b.likes-a.likes + @new_projects.sort (a,b)-> b.first_published-a.first_published + + return if @top_projects.length<2 or @new_projects.length<2 + + now = Date.now() + maxLikes = Math.max(1,@top_projects[0].likes) + maxAge = now-@new_projects[@new_projects.length-1].first_published + + note = (p)-> + hours = Math.max(0,(now-p.first_published))/1000/3600 + agemark = Math.exp(-hours/24/7) + p.likes/maxLikes*50+50*agemark + + @hot_projects.sort (a,b)-> + note(b)-note(a) + + console.info "Sorting public projects took: #{Date.now()-time} ms" + + setProjectPublic:(project,pub)-> + project.set("public",pub) + if pub and project.first_published == 0 + project.set "first_published",Date.now() + + if pub + @hot_projects.push(project) if @hot_projects.indexOf(project)<0 + @top_projects.push(project) if @top_projects.indexOf(project)<0 + @new_projects.push(project) if @new_projects.indexOf(project)<0 + #@sortPublicProjects() + else + index = @hot_projects.indexOf(project) + if index>=0 + @hot_projects.splice index,1 + index = @top_projects.indexOf(project) + if index>=0 + @top_projects.splice index,1 + index = @new_projects.indexOf(project) + if index>=0 + @new_projects.splice index,1 + + projectDeleted:(project)-> + @project_count -= 1 + + index = @hot_projects.indexOf(project) + if index>=0 + @hot_projects.splice index,1 + index = @top_projects.indexOf(project) + if index>=0 + @top_projects.splice index,1 + index = @new_projects.indexOf(project) + if index>=0 + @new_projects.splice index,1 + + addProjectTag:(project,t)-> + tag = @tags[t] + if not tag? + tag = new Tag(t) + @tags[t] = tag + tag.add project + + removeProjectTag:(project,t)-> + tag = @tags[t] + if tag? + tag.remove project + + setProjectTags:(project,tags)-> + for t in project.tags + if tags.indexOf(t)<0 + @removeProjectTag(project,t) + + for t in tags + if project.tags.indexOf(t)<0 + @addProjectTag(project,t) + + project.set "tags",tags + + createUser:(data)-> + record = @db.create "users",data + @loadUser record + + createToken:(user)-> + value = "" + chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789" + for i in [0..31] by 1 + value += chars.charAt(Math.floor(Math.random()*chars.length)) + + record = @db.create "tokens", + value: value + user: user.id + date_created: Date.now() + + @loadToken record + + findUserByNick:(nick)-> + @users_by_nick[nick] + + findUserByEmail:(email)-> + @users_by_email[email] + + changeUserNick:(user,nick)-> + delete @users_by_nick[user.nick] + user.set "nick",nick + @users_by_nick[nick] = user + + changeUserEmail:(user,email)-> + if user.email? + delete @users_by_email[user.email] + else + @guest_count -= 1 + user.set "email",email + @users_by_email[email] = user + + userDeleted:(user)-> + delete @users_by_nick[user.nick] + if user.email? + delete @users_by_email[user.email] + else + @guest_count -= 1 + @user_count -= 1 + + findToken:(token)-> + @tokens[token] + + createProject:(owner,data,callback,empty=false)-> + slug = data.slug + if owner.findProjectBySlug(slug) + count = 2 + while owner.findProjectBySlug(slug+count)? + count += 1 + data.slug = slug+count + + d = + title: data.title + slug: data.slug + tags: [] + likes: [] + public: data.public or false + date_created: Date.now() + last_modified: Date.now() + deleted: false + owner: owner.id + + record = @db.create "projects",d + project = @loadProject record + if empty + callback(project) + else + content = """ +init = function() +end + +update = function() +end + +draw = function() +end +""" + @files.write "#{owner.id}/#{project.id}/ms/main.ms",content,()=> + @files.copyFile "../static/img/defaultappicon.png","#{owner.id}/#{project.id}/sprites/icon.png",()=> + callback(project) + + getConsoleGameList:()-> + list = [] + for key,p of @projects + if p.public and not p.deleted + list.push + author: p.owner.nick + slug: p.slug + title: p.title + + list + + sendValidationMail:(user)-> + return if not user.email? + token = user.getValidationToken() + translator = @translator.getTranslator(user.language) + subject = translator.get("Microstudio e-mail validation") + text = translator.get("Thank you for using Microstudio!")+"\n\n" + text += translator.get("Click on the link below to validate your e-mail address:")+"\n\n" + text += "https://microstudio.dev/v/#{user.id}/#{token}"+"\n\n" + + @server.mailer.sendMail user.email,subject,text + + sendPasswordRecoveryMail:(user)-> + return if not user.email? + token = user.getValidationToken() + translator = @translator.getTranslator(user.language) + subject = translator.get("Reset your microStudio password") + text = translator.get("Click on the link below to choose a new microStudio password:")+"\n\n" + text += "https://microstudio.dev/pw/#{user.id}/#{token}"+"\n\n" + @server.mailer.sendMail user.email,subject,text + + checkValidationToken:(user,token)-> + token == user.getValidationToken() + + validateEMailAddress:(user,token)-> + console.info "verifying #{token} against #{user.getValidationToken()}" + if token? and token.length>0 and @checkValidationToken(user,token) + user.resetValidationToken() + user.setFlag("validated",true) + translator = @translator.getTranslator(user.language) + user.notify translator.get "Your e-mail address is now validated" + +module.exports = @Content diff --git a/server/content/content.js b/server/content/content.js new file mode 100644 index 00000000..40124fa8 --- /dev/null +++ b/server/content/content.js @@ -0,0 +1,458 @@ +var Forum, Project, Tag, Token, Translator, User, usage; + +usage = require("pidusage"); + +User = require(__dirname + "/user.js"); + +Project = require(__dirname + "/project.js"); + +Tag = require(__dirname + "/tag.js"); + +Token = require(__dirname + "/token.js"); + +Translator = require(__dirname + "/translator.js"); + +Forum = require(__dirname + "/../forum/forum.js"); + +this.Content = (function() { + function Content(server, db, files) { + this.server = server; + this.db = db; + this.files = files; + this.users = {}; + this.users_by_email = {}; + this.users_by_nick = {}; + this.tokens = {}; + this.projects = {}; + this.tags = {}; + this.project_count = 0; + this.user_count = 0; + this.guest_count = 0; + this.load(); + this.hot_projects = []; + this.top_projects = []; + this.new_projects = []; + this.updatePublicProjects(); + console.info("Content loaded: " + this.user_count + " users and " + this.project_count + " projects"); + this.top_interval = setInterval(((function(_this) { + return function() { + return _this.sortPublicProjects(); + }; + })(this)), 10001); + this.log_interval = setInterval(((function(_this) { + return function() { + return _this.statusLog(); + }; + })(this)), 6000); + this.translator = new Translator(this); + this.forum = new Forum(this); + } + + Content.prototype.close = function() { + clearInterval(this.top_interval); + clearInterval(this.log_interval); + this.forum.close(); + if (this.test_data != null) { + return this.test_data.close(); + } + }; + + Content.prototype.statusLog = function() { + return usage(process.pid, (function(_this) { + return function(err, result) { + if (result == null) { + return; + } + console.info("------------"); + console.info("" + (new Date().toString())); + console.info("cpu: " + (Math.round(result.cpu)) + "%"); + console.info("memory: " + (Math.round(result.memory / 1000000)) + " mb"); + console.info("users: " + _this.user_count); + console.info("projects: " + _this.project_count); + _this.current_cpu = Math.round(result.cpu); + _this.current_memory = Math.round(result.memory / 1000000); + _this.server.stats.max("cpu_max", Math.round(result.cpu)); + return _this.server.stats.max("memory_max", _this.current_memory); + }; + })(this)); + }; + + Content.prototype.load = function() { + var j, k, l, len, len1, len2, projects, record, token, tokens, users; + users = this.db.list("users"); + for (j = 0, len = users.length; j < len; j++) { + record = users[j]; + this.loadUser(record); + } + tokens = this.db.list("tokens"); + for (k = 0, len1 = tokens.length; k < len1; k++) { + token = tokens[k]; + this.loadToken(token); + } + projects = this.db.list("projects"); + for (l = 0, len2 = projects.length; l < len2; l++) { + record = projects[l]; + this.loadProject(record); + } + this.initLikes(); + }; + + Content.prototype.loadUser = function(record) { + var data, user; + data = record.get(); + user = new User(this, record); + if (user.flags.deleted) { + return; + } + this.users[user.id] = user; + if (user.email != null) { + this.users_by_email[user.email] = user; + } else { + this.guest_count += 1; + } + this.users_by_nick[user.nick] = user; + this.user_count++; + return user; + }; + + Content.prototype.loadProject = function(record) { + var data, project; + data = record.get(); + project = new Project(this, record); + if ((project.owner != null) && !project.deleted) { + this.projects[project.id] = project; + this.loadTags(project); + this.project_count++; + } + return project; + }; + + Content.prototype.loadTags = function(project) { + var j, len, ref, t, tag; + ref = project.tags; + for (j = 0, len = ref.length; j < len; j++) { + t = ref[j]; + tag = this.tags[t]; + if (tag == null) { + tag = new Tag(t); + this.tags[t] = tag; + } + tag.add(project); + } + }; + + Content.prototype.loadToken = function(record) { + var data, token; + data = record.get(); + token = new Token(this, record); + this.tokens[token.value] = token; + return token; + }; + + Content.prototype.initLikes = function() { + var f, j, key, len, ref, ref1, user; + ref = this.users; + for (key in ref) { + user = ref[key]; + ref1 = user.likes; + for (j = 0, len = ref1.length; j < len; j++) { + f = ref1[j]; + if (this.projects[f] != null) { + this.projects[f].likes++; + } + } + } + }; + + Content.prototype.updatePublicProjects = function() { + var key, project, ref; + this.hot_projects = []; + this.top_projects = []; + this.new_projects = []; + ref = this.projects; + for (key in ref) { + project = ref[key]; + if (project["public"] && project.owner.flags["validated"] && !project.deleted && !project.owner.flags["censored"]) { + this.hot_projects.push(project); + this.top_projects.push(project); + this.new_projects.push(project); + } + } + return this.sortPublicProjects(); + }; + + Content.prototype.sortPublicProjects = function() { + var maxAge, maxLikes, note, now, time; + time = Date.now(); + this.top_projects.sort(function(a, b) { + return b.likes - a.likes; + }); + this.new_projects.sort(function(a, b) { + return b.first_published - a.first_published; + }); + if (this.top_projects.length < 2 || this.new_projects.length < 2) { + return; + } + now = Date.now(); + maxLikes = Math.max(1, this.top_projects[0].likes); + maxAge = now - this.new_projects[this.new_projects.length - 1].first_published; + note = function(p) { + var agemark, hours; + hours = Math.max(0, now - p.first_published) / 1000 / 3600; + agemark = Math.exp(-hours / 24 / 7); + return p.likes / maxLikes * 50 + 50 * agemark; + }; + this.hot_projects.sort(function(a, b) { + return note(b) - note(a); + }); + return console.info("Sorting public projects took: " + (Date.now() - time) + " ms"); + }; + + Content.prototype.setProjectPublic = function(project, pub) { + var index; + project.set("public", pub); + if (pub && project.first_published === 0) { + project.set("first_published", Date.now()); + } + if (pub) { + if (this.hot_projects.indexOf(project) < 0) { + this.hot_projects.push(project); + } + if (this.top_projects.indexOf(project) < 0) { + this.top_projects.push(project); + } + if (this.new_projects.indexOf(project) < 0) { + return this.new_projects.push(project); + } + } else { + index = this.hot_projects.indexOf(project); + if (index >= 0) { + this.hot_projects.splice(index, 1); + } + index = this.top_projects.indexOf(project); + if (index >= 0) { + this.top_projects.splice(index, 1); + } + index = this.new_projects.indexOf(project); + if (index >= 0) { + return this.new_projects.splice(index, 1); + } + } + }; + + Content.prototype.projectDeleted = function(project) { + var index; + this.project_count -= 1; + index = this.hot_projects.indexOf(project); + if (index >= 0) { + this.hot_projects.splice(index, 1); + } + index = this.top_projects.indexOf(project); + if (index >= 0) { + this.top_projects.splice(index, 1); + } + index = this.new_projects.indexOf(project); + if (index >= 0) { + return this.new_projects.splice(index, 1); + } + }; + + Content.prototype.addProjectTag = function(project, t) { + var tag; + tag = this.tags[t]; + if (tag == null) { + tag = new Tag(t); + this.tags[t] = tag; + } + return tag.add(project); + }; + + Content.prototype.removeProjectTag = function(project, t) { + var tag; + tag = this.tags[t]; + if (tag != null) { + return tag.remove(project); + } + }; + + Content.prototype.setProjectTags = function(project, tags) { + var j, k, len, len1, ref, t; + ref = project.tags; + for (j = 0, len = ref.length; j < len; j++) { + t = ref[j]; + if (tags.indexOf(t) < 0) { + this.removeProjectTag(project, t); + } + } + for (k = 0, len1 = tags.length; k < len1; k++) { + t = tags[k]; + if (project.tags.indexOf(t) < 0) { + this.addProjectTag(project, t); + } + } + return project.set("tags", tags); + }; + + Content.prototype.createUser = function(data) { + var record; + record = this.db.create("users", data); + return this.loadUser(record); + }; + + Content.prototype.createToken = function(user) { + var chars, i, j, record, value; + value = ""; + chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; + for (i = j = 0; j <= 31; i = j += 1) { + value += chars.charAt(Math.floor(Math.random() * chars.length)); + } + record = this.db.create("tokens", { + value: value, + user: user.id, + date_created: Date.now() + }); + return this.loadToken(record); + }; + + Content.prototype.findUserByNick = function(nick) { + return this.users_by_nick[nick]; + }; + + Content.prototype.findUserByEmail = function(email) { + return this.users_by_email[email]; + }; + + Content.prototype.changeUserNick = function(user, nick) { + delete this.users_by_nick[user.nick]; + user.set("nick", nick); + return this.users_by_nick[nick] = user; + }; + + Content.prototype.changeUserEmail = function(user, email) { + if (user.email != null) { + delete this.users_by_email[user.email]; + } else { + this.guest_count -= 1; + } + user.set("email", email); + return this.users_by_email[email] = user; + }; + + Content.prototype.userDeleted = function(user) { + delete this.users_by_nick[user.nick]; + if (user.email != null) { + delete this.users_by_email[user.email]; + } else { + this.guest_count -= 1; + } + return this.user_count -= 1; + }; + + Content.prototype.findToken = function(token) { + return this.tokens[token]; + }; + + Content.prototype.createProject = function(owner, data, callback, empty) { + var content, count, d, project, record, slug; + if (empty == null) { + empty = false; + } + slug = data.slug; + if (owner.findProjectBySlug(slug)) { + count = 2; + while (owner.findProjectBySlug(slug + count) != null) { + count += 1; + } + data.slug = slug + count; + } + d = { + title: data.title, + slug: data.slug, + tags: [], + likes: [], + "public": data["public"] || false, + date_created: Date.now(), + last_modified: Date.now(), + deleted: false, + owner: owner.id + }; + record = this.db.create("projects", d); + project = this.loadProject(record); + if (empty) { + return callback(project); + } else { + content = "init = function()\nend\n\nupdate = function()\nend\n\ndraw = function()\nend"; + return this.files.write(owner.id + "/" + project.id + "/ms/main.ms", content, (function(_this) { + return function() { + return _this.files.copyFile("../static/img/defaultappicon.png", owner.id + "/" + project.id + "/sprites/icon.png", function() { + return callback(project); + }); + }; + })(this)); + } + }; + + Content.prototype.getConsoleGameList = function() { + var key, list, p, ref; + list = []; + ref = this.projects; + for (key in ref) { + p = ref[key]; + if (p["public"] && !p.deleted) { + list.push({ + author: p.owner.nick, + slug: p.slug, + title: p.title + }); + } + } + return list; + }; + + Content.prototype.sendValidationMail = function(user) { + var subject, text, token, translator; + if (user.email == null) { + return; + } + token = user.getValidationToken(); + translator = this.translator.getTranslator(user.language); + subject = translator.get("Microstudio e-mail validation"); + text = translator.get("Thank you for using Microstudio!") + "\n\n"; + text += translator.get("Click on the link below to validate your e-mail address:") + "\n\n"; + text += ("https://microstudio.dev/v/" + user.id + "/" + token) + "\n\n"; + return this.server.mailer.sendMail(user.email, subject, text); + }; + + Content.prototype.sendPasswordRecoveryMail = function(user) { + var subject, text, token, translator; + if (user.email == null) { + return; + } + token = user.getValidationToken(); + translator = this.translator.getTranslator(user.language); + subject = translator.get("Reset your microStudio password"); + text = translator.get("Click on the link below to choose a new microStudio password:") + "\n\n"; + text += ("https://microstudio.dev/pw/" + user.id + "/" + token) + "\n\n"; + return this.server.mailer.sendMail(user.email, subject, text); + }; + + Content.prototype.checkValidationToken = function(user, token) { + return token === user.getValidationToken(); + }; + + Content.prototype.validateEMailAddress = function(user, token) { + var translator; + console.info("verifying " + token + " against " + (user.getValidationToken())); + if ((token != null) && token.length > 0 && this.checkValidationToken(user, token)) { + user.resetValidationToken(); + user.setFlag("validated", true); + translator = this.translator.getTranslator(user.language); + return user.notify(translator.get("Your e-mail address is now validated")); + } + }; + + return Content; + +})(); + +module.exports = this.Content; diff --git a/server/content/project.coffee b/server/content/project.coffee new file mode 100644 index 00000000..fd8ffd59 --- /dev/null +++ b/server/content/project.coffee @@ -0,0 +1,243 @@ +Comments = require __dirname+"/comments.js" + +fs = require "fs" + + +class ProjectLink + constructor:(@project,data)-> + @user = @project.content.users[data.user] + @accepted = data.accepted + if @user? + @user.addProjectLink @ + + accept:()-> + if not @accepted + @accepted = true + @project.saveUsers() + + remove:()-> + @project.removeUser @ + @user.removeLink @project.id + +class @Project + constructor:(@content,@record)-> + data = @record.get() + @id = data.id + @title = data.title + @slug = data.slug + @code = data.code or @createCode() + @tags = data.tags or [] + @description = data.description or "" + @likes = 0 + @public = data.public + @date_created = data.date_created + @last_modified = data.last_modified + @first_published = data.first_published or 0 + @orientation = data.orientation or "any" + @aspect = data.aspect or "free" + @graphics = data.graphics or "M1" + @platforms = data.platforms or ["computer","phone","tablet"] + @controls = data.controls or ["touch","mouse"] + @type = data.type or "app" + @deleted = data.deleted + @users = [] + @comments = new Comments @,data.comments + if not @deleted + @owner = @content.users[data.owner] + if @owner? + @owner.addProject @ + + if data.users? + for u in data.users + link = new ProjectLink @,u + if link.user? + @users.push link + + if data.files? and not @deleted + @files = data.files + else + @files = {} + + @update_project_size = true + + createCode:()-> + letters = "ABCDEFGHJKMNPRSTUVWXYZ23456789" + code = "" + for i in [0..7] by 1 + code += ""+letters.charAt(Math.floor(Math.random()*letters.length)) + @set("code",code) + code + + touch:()-> + @last_modified = Date.now() + data = @record.get() + data.last_modified = @last_modified + @record.set data + + set:(prop,value,update_local=true)-> + data = @record.get() + data[prop] = value + @record.set(data) + @[prop] = value if update_local + + setTitle:(title)-> + if title? and title.trim().length>0 and title.length<50 + @set "title",title + true + else + false + + setSlug:(slug)-> + if slug? and /^([a-z0-9_][a-z0-9_-]{0,29})$/.test(slug) + return false if @owner.findProjectBySlug(slug)? + @set "slug",slug + true + else + false + + setCode:(code)-> + if code? and /^([a-zA-Z0-9_][a-zA-Z0-9_-]{0,29})$/.test(code) + @set "code",code + true + else + false + + setType:(type)-> + @set "type",type + + setOrientation:(orientation)-> + @set "orientation",orientation + + setAspect:(aspect)-> + @set "aspect",aspect + + setGraphics:(graphics)-> + @set "graphics",graphics + + saveUsers:()-> + data = [] + for link in @users + data.push + user: link.user.id + accepted: link.accepted + + @set "users",data,false + + inviteUser:(user)-> + return if user == @owner + for link in @users + return if user == link.user + link = new ProjectLink @, + user: user.id + accepted: false + if link.user? + @users.push link + @saveUsers() + + removeUser:(link)-> + index = @users.indexOf link + if index>=0 + @users.splice index,1 + @saveUsers() + + listUsers:()-> + list = [] + for link in @users + list.push + id: link.user.id + nick: link.user.nick + accepted: link.accepted + list + + delete:()-> + @deleted = true + data = @record.get() + data.deleted = @deleted + @record.set data + @content.projectDeleted @ + for i in [@users.length-1..0] by -1 + link = @users[i] + link.remove() + delete @manager + + folder = "#{@owner.id}/#{@id}" + @content.files.deleteFolder folder + + return + + getFileInfo:(file)-> + @files[file] || {} + + setFileInfo:(file,key,value)-> + info = @getFileInfo(file) + info[key] = value + @files[file] = info + @set "files",@files + @update_project_size = true + + deleteFileInfo:(file)-> + delete @files[file] + @set "files",@files + @update_project_size = true + + getSize:()-> + if @update_project_size + @updateProjectSize() + + @byte_size + + updateProjectSize:()-> + @byte_size = 0 + @update_project_size = false + for key,file of @files + if file.size? + @byte_size += file.size + return + + filenameChanged:(previous,next)-> + if @files[previous]? + @files[next] = @files[previous] + delete @files[previous] + @set "files",@files + + fileDeleted:(file)-> + if @files[file] + delete @files[file] + @set "files",@files + + + + updateFileSizes:(callback)-> + source = "../files/"+@content.files.sanitize "#{@owner.id}/#{@id}/ms" + sprites = "../files/"+@content.files.sanitize "#{@owner.id}/#{@id}/sprites" + maps = "../files/"+@content.files.sanitize "#{@owner.id}/#{@id}/maps" + + list = [] + + process = ()=> + if list.length>0 + f = list.splice(0,1)[0] + file = "../files/"+@content.files.sanitize "#{@owner.id}/#{@id}/#{f}" + fs.lstat file,(err,stat)=> + if stat? and stat.size? + @setFileInfo f,"size",stat.size + setTimeout (()=>process()),0 + else + callback() if callback? + + fs.readdir source,(err,files)=> + if files + for f in files + list.push "ms/#{f}" + fs.readdir sprites,(err,files)=> + if files + for f in files + list.push "sprites/#{f}" + fs.readdir maps,(err,files)=> + if files + for f in files + list.push "maps/#{f}" + process() + + +module.exports = @Project diff --git a/server/content/project.js b/server/content/project.js new file mode 100644 index 00000000..aaf5a37b --- /dev/null +++ b/server/content/project.js @@ -0,0 +1,354 @@ +var Comments, ProjectLink, fs; + +Comments = require(__dirname + "/comments.js"); + +fs = require("fs"); + +ProjectLink = (function() { + function ProjectLink(project, data) { + this.project = project; + this.user = this.project.content.users[data.user]; + this.accepted = data.accepted; + if (this.user != null) { + this.user.addProjectLink(this); + } + } + + ProjectLink.prototype.accept = function() { + if (!this.accepted) { + this.accepted = true; + return this.project.saveUsers(); + } + }; + + ProjectLink.prototype.remove = function() { + this.project.removeUser(this); + return this.user.removeLink(this.project.id); + }; + + return ProjectLink; + +})(); + +this.Project = (function() { + function Project(content, record) { + var data, j, len, link, ref, u; + this.content = content; + this.record = record; + data = this.record.get(); + this.id = data.id; + this.title = data.title; + this.slug = data.slug; + this.code = data.code || this.createCode(); + this.tags = data.tags || []; + this.description = data.description || ""; + this.likes = 0; + this["public"] = data["public"]; + this.date_created = data.date_created; + this.last_modified = data.last_modified; + this.first_published = data.first_published || 0; + this.orientation = data.orientation || "any"; + this.aspect = data.aspect || "free"; + this.graphics = data.graphics || "M1"; + this.platforms = data.platforms || ["computer", "phone", "tablet"]; + this.controls = data.controls || ["touch", "mouse"]; + this.type = data.type || "app"; + this.deleted = data.deleted; + this.users = []; + this.comments = new Comments(this, data.comments); + if (!this.deleted) { + this.owner = this.content.users[data.owner]; + if (this.owner != null) { + this.owner.addProject(this); + } + if (data.users != null) { + ref = data.users; + for (j = 0, len = ref.length; j < len; j++) { + u = ref[j]; + link = new ProjectLink(this, u); + if (link.user != null) { + this.users.push(link); + } + } + } + } + if ((data.files != null) && !this.deleted) { + this.files = data.files; + } else { + this.files = {}; + } + this.update_project_size = true; + } + + Project.prototype.createCode = function() { + var code, i, j, letters; + letters = "ABCDEFGHJKMNPRSTUVWXYZ23456789"; + code = ""; + for (i = j = 0; j <= 7; i = j += 1) { + code += "" + letters.charAt(Math.floor(Math.random() * letters.length)); + } + this.set("code", code); + return code; + }; + + Project.prototype.touch = function() { + var data; + this.last_modified = Date.now(); + data = this.record.get(); + data.last_modified = this.last_modified; + return this.record.set(data); + }; + + Project.prototype.set = function(prop, value, update_local) { + var data; + if (update_local == null) { + update_local = true; + } + data = this.record.get(); + data[prop] = value; + this.record.set(data); + if (update_local) { + return this[prop] = value; + } + }; + + Project.prototype.setTitle = function(title) { + if ((title != null) && title.trim().length > 0 && title.length < 50) { + this.set("title", title); + return true; + } else { + return false; + } + }; + + Project.prototype.setSlug = function(slug) { + if ((slug != null) && /^([a-z0-9_][a-z0-9_-]{0,29})$/.test(slug)) { + if (this.owner.findProjectBySlug(slug) != null) { + return false; + } + this.set("slug", slug); + return true; + } else { + return false; + } + }; + + Project.prototype.setCode = function(code) { + if ((code != null) && /^([a-zA-Z0-9_][a-zA-Z0-9_-]{0,29})$/.test(code)) { + this.set("code", code); + return true; + } else { + return false; + } + }; + + Project.prototype.setType = function(type) { + return this.set("type", type); + }; + + Project.prototype.setOrientation = function(orientation) { + return this.set("orientation", orientation); + }; + + Project.prototype.setAspect = function(aspect) { + return this.set("aspect", aspect); + }; + + Project.prototype.setGraphics = function(graphics) { + return this.set("graphics", graphics); + }; + + Project.prototype.saveUsers = function() { + var data, j, len, link, ref; + data = []; + ref = this.users; + for (j = 0, len = ref.length; j < len; j++) { + link = ref[j]; + data.push({ + user: link.user.id, + accepted: link.accepted + }); + } + return this.set("users", data, false); + }; + + Project.prototype.inviteUser = function(user) { + var j, len, link, ref; + if (user === this.owner) { + return; + } + ref = this.users; + for (j = 0, len = ref.length; j < len; j++) { + link = ref[j]; + if (user === link.user) { + return; + } + } + link = new ProjectLink(this, { + user: user.id, + accepted: false + }); + if (link.user != null) { + this.users.push(link); + return this.saveUsers(); + } + }; + + Project.prototype.removeUser = function(link) { + var index; + index = this.users.indexOf(link); + if (index >= 0) { + this.users.splice(index, 1); + return this.saveUsers(); + } + }; + + Project.prototype.listUsers = function() { + var j, len, link, list, ref; + list = []; + ref = this.users; + for (j = 0, len = ref.length; j < len; j++) { + link = ref[j]; + list.push({ + id: link.user.id, + nick: link.user.nick, + accepted: link.accepted + }); + } + return list; + }; + + Project.prototype["delete"] = function() { + var data, folder, i, j, link, ref; + this.deleted = true; + data = this.record.get(); + data.deleted = this.deleted; + this.record.set(data); + this.content.projectDeleted(this); + for (i = j = ref = this.users.length - 1; j >= 0; i = j += -1) { + link = this.users[i]; + link.remove(); + } + delete this.manager; + folder = this.owner.id + "/" + this.id; + this.content.files.deleteFolder(folder); + }; + + Project.prototype.getFileInfo = function(file) { + return this.files[file] || {}; + }; + + Project.prototype.setFileInfo = function(file, key, value) { + var info; + info = this.getFileInfo(file); + info[key] = value; + this.files[file] = info; + this.set("files", this.files); + return this.update_project_size = true; + }; + + Project.prototype.deleteFileInfo = function(file) { + delete this.files[file]; + this.set("files", this.files); + return this.update_project_size = true; + }; + + Project.prototype.getSize = function() { + if (this.update_project_size) { + this.updateProjectSize(); + } + return this.byte_size; + }; + + Project.prototype.updateProjectSize = function() { + var file, key, ref; + this.byte_size = 0; + this.update_project_size = false; + ref = this.files; + for (key in ref) { + file = ref[key]; + if (file.size != null) { + this.byte_size += file.size; + } + } + }; + + Project.prototype.filenameChanged = function(previous, next) { + if (this.files[previous] != null) { + this.files[next] = this.files[previous]; + delete this.files[previous]; + return this.set("files", this.files); + } + }; + + Project.prototype.fileDeleted = function(file) { + if (this.files[file]) { + delete this.files[file]; + return this.set("files", this.files); + } + }; + + Project.prototype.updateFileSizes = function(callback) { + var list, maps, process, source, sprites; + source = "../files/" + this.content.files.sanitize(this.owner.id + "/" + this.id + "/ms"); + sprites = "../files/" + this.content.files.sanitize(this.owner.id + "/" + this.id + "/sprites"); + maps = "../files/" + this.content.files.sanitize(this.owner.id + "/" + this.id + "/maps"); + list = []; + process = (function(_this) { + return function() { + var f, file; + if (list.length > 0) { + f = list.splice(0, 1)[0]; + file = "../files/" + _this.content.files.sanitize(_this.owner.id + "/" + _this.id + "/" + f); + return fs.lstat(file, function(err, stat) { + if ((stat != null) && (stat.size != null)) { + _this.setFileInfo(f, "size", stat.size); + } + return setTimeout((function() { + return process(); + }), 0); + }); + } else { + if (callback != null) { + return callback(); + } + } + }; + })(this); + return fs.readdir(source, (function(_this) { + return function(err, files) { + var f, j, len; + if (files) { + for (j = 0, len = files.length; j < len; j++) { + f = files[j]; + list.push("ms/" + f); + } + } + return fs.readdir(sprites, function(err, files) { + var k, len1; + if (files) { + for (k = 0, len1 = files.length; k < len1; k++) { + f = files[k]; + list.push("sprites/" + f); + } + } + return fs.readdir(maps, function(err, files) { + var l, len2; + if (files) { + for (l = 0, len2 = files.length; l < len2; l++) { + f = files[l]; + list.push("maps/" + f); + } + } + return process(); + }); + }); + }; + })(this)); + }; + + return Project; + +})(); + +module.exports = this.Project; diff --git a/server/content/tag.coffee b/server/content/tag.coffee new file mode 100644 index 00000000..3b830038 --- /dev/null +++ b/server/content/tag.coffee @@ -0,0 +1,19 @@ +class @Tag + constructor:(@tag)-> + @targets = {} + @uses = 0 + + add:(target)-> + if not @targets[target.id]? + @targets[target.id] = target + @uses++ + + remove:(target)-> + if @targets[target.id]? + delete @targets[target.id] + @uses-- + + test:(target)-> + @targets[target]? + +module.exports = @Tag diff --git a/server/content/tag.js b/server/content/tag.js new file mode 100644 index 00000000..0a280d74 --- /dev/null +++ b/server/content/tag.js @@ -0,0 +1,30 @@ +this.Tag = (function() { + function Tag(tag) { + this.tag = tag; + this.targets = {}; + this.uses = 0; + } + + Tag.prototype.add = function(target) { + if (this.targets[target.id] == null) { + this.targets[target.id] = target; + return this.uses++; + } + }; + + Tag.prototype.remove = function(target) { + if (this.targets[target.id] != null) { + delete this.targets[target.id]; + return this.uses--; + } + }; + + Tag.prototype.test = function(target) { + return this.targets[target] != null; + }; + + return Tag; + +})(); + +module.exports = this.Tag; diff --git a/server/content/token.coffee b/server/content/token.coffee new file mode 100644 index 00000000..f2a34e3b --- /dev/null +++ b/server/content/token.coffee @@ -0,0 +1,9 @@ +class @Token + constructor:(@content,@record)-> + data = @record.get() + @id = data.id + @value = data.value + @date_created = data.date_created + @user = @content.users[data.user] + +module.exports = @Token diff --git a/server/content/token.js b/server/content/token.js new file mode 100644 index 00000000..d4ab96f6 --- /dev/null +++ b/server/content/token.js @@ -0,0 +1,17 @@ +this.Token = (function() { + function Token(content, record) { + var data; + this.content = content; + this.record = record; + data = this.record.get(); + this.id = data.id; + this.value = data.value; + this.date_created = data.date_created; + this.user = this.content.users[data.user]; + } + + return Token; + +})(); + +module.exports = this.Token; diff --git a/server/content/translator.coffee b/server/content/translator.coffee new file mode 100644 index 00000000..0cca13ca --- /dev/null +++ b/server/content/translator.coffee @@ -0,0 +1,103 @@ +class @Translator + constructor:(@content)-> + @languages = {} + @list = {} + @load() + + load:()-> + languages = @content.db.list("translations") + for record in languages + @loadLanguage record + + loadLanguage:(record)-> + language = new Translator.Language @,record + @languages[language.code] = language + + get:(language,text)-> + @reference text + t = @languages[language] + if t? + t.get(text) + else + text + + reference:(text)-> + @list[text] = true if not @list[text] + + createLanguage:(lang)-> + if not @languages[lang] + d = + code: lang + translations: {} + + record = @content.db.create "translations",d + @loadLanguage record + + getTranslator:(language)-> + language = language or "en" + { get:(text)=> @get(language,text) } + +class @Translator.Language + constructor:(@translator,@record)-> + data = @record.get() + @code = data.code + @translations = data.translations + @buffer = null + + get:(text)-> + t = @translations[text] + if t? + t.best + else + text + + export:()-> + if not @buffer? + @buffer = {} + for key,value of @translations + @buffer[key] = value.best + @buffer = JSON.stringify @buffer + + @buffer + + updateBest:(trans)-> + best = 0 + for id,u of trans.list + best = Math.max(best,u.votes) + for id,u of trans.list + if u.votes == best + trans.best = u.trans + return + return + + set:(userid,source,trans)-> + t = @translations[source] + if not t? + t = + best: trans + list: {} + t.list[userid] = + trans: trans + votes: 1 + + @translations[source] = t + @record.setField("translations",@translations) + @buffer = null + return + else + if t.list[userid]? + u = t.list[userid] + if trans != u.trans + u.trans = trans + @updateBest(t) + @buffer = null + @record.setField("translations",@translations) + else + t.list[userid] = + trans: trans + votes: 1 + @updateBest(t) + @buffer = null + @record.setField("translations",@translations) + +module.exports = @Translator diff --git a/server/content/translator.js b/server/content/translator.js new file mode 100644 index 00000000..b7665b2a --- /dev/null +++ b/server/content/translator.js @@ -0,0 +1,163 @@ +this.Translator = (function() { + function Translator(content) { + this.content = content; + this.languages = {}; + this.list = {}; + this.load(); + } + + Translator.prototype.load = function() { + var i, languages, len, record, results; + languages = this.content.db.list("translations"); + results = []; + for (i = 0, len = languages.length; i < len; i++) { + record = languages[i]; + results.push(this.loadLanguage(record)); + } + return results; + }; + + Translator.prototype.loadLanguage = function(record) { + var language; + language = new Translator.Language(this, record); + return this.languages[language.code] = language; + }; + + Translator.prototype.get = function(language, text) { + var t; + this.reference(text); + t = this.languages[language]; + if (t != null) { + return t.get(text); + } else { + return text; + } + }; + + Translator.prototype.reference = function(text) { + if (!this.list[text]) { + return this.list[text] = true; + } + }; + + Translator.prototype.createLanguage = function(lang) { + var d, record; + if (!this.languages[lang]) { + d = { + code: lang, + translations: {} + }; + } + record = this.content.db.create("translations", d); + return this.loadLanguage(record); + }; + + Translator.prototype.getTranslator = function(language) { + language = language || "en"; + return { + get: (function(_this) { + return function(text) { + return _this.get(language, text); + }; + })(this) + }; + }; + + return Translator; + +})(); + +this.Translator.Language = (function() { + function Language(translator, record1) { + var data; + this.translator = translator; + this.record = record1; + data = this.record.get(); + this.code = data.code; + this.translations = data.translations; + this.buffer = null; + } + + Language.prototype.get = function(text) { + var t; + t = this.translations[text]; + if (t != null) { + return t.best; + } else { + return text; + } + }; + + Language.prototype["export"] = function() { + var key, ref, value; + if (this.buffer == null) { + this.buffer = {}; + ref = this.translations; + for (key in ref) { + value = ref[key]; + this.buffer[key] = value.best; + } + this.buffer = JSON.stringify(this.buffer); + } + return this.buffer; + }; + + Language.prototype.updateBest = function(trans) { + var best, id, ref, ref1, u; + best = 0; + ref = trans.list; + for (id in ref) { + u = ref[id]; + best = Math.max(best, u.votes); + } + ref1 = trans.list; + for (id in ref1) { + u = ref1[id]; + if (u.votes === best) { + trans.best = u.trans; + return; + } + } + }; + + Language.prototype.set = function(userid, source, trans) { + var t, u; + t = this.translations[source]; + if (t == null) { + t = { + best: trans, + list: {} + }; + t.list[userid] = { + trans: trans, + votes: 1 + }; + this.translations[source] = t; + this.record.setField("translations", this.translations); + this.buffer = null; + } else { + if (t.list[userid] != null) { + u = t.list[userid]; + if (trans !== u.trans) { + u.trans = trans; + this.updateBest(t); + this.buffer = null; + return this.record.setField("translations", this.translations); + } + } else { + t.list[userid] = { + trans: trans, + votes: 1 + }; + this.updateBest(t); + this.buffer = null; + return this.record.setField("translations", this.translations); + } + } + }; + + return Language; + +})(); + +module.exports = this.Translator; diff --git a/server/content/user.coffee b/server/content/user.coffee new file mode 100644 index 00000000..51c45a7d --- /dev/null +++ b/server/content/user.coffee @@ -0,0 +1,180 @@ +class @User + constructor:(@content,@record)-> + data = @record.get() + @id = data.id + @nick = data.nick + @email = data.email + @language = data.language or "en" + @flags = data.flags or {} + @settings = data.settings or {} + @hash = data.hash + @patches = [] + @likes = data.likes or [] + @projects = {} + @project_links = [] + @listeners = [] + @notifications = [] + @description = data.description or "" + @updateTier() + + updateTier:()-> + switch @flags.tier + when "pixel_master" + @max_storage = 200000000 + @early_access = false + + when "code_ninja" + @max_storage = 400000000 + @early_access = false + + when "gamedev_lord" + @max_storage = 1000000000 + @early_access = true + + when "founder" + @max_storage = 2000000000 + @early_access = true + + when "sponsor" + @max_storage = 2000000000 + @early_access = true + + else + if @flags.guest + @max_storage = 2000000 + @early_access = false + else + @max_storage = 50000000 + @early_access = false + + addListener:(listener)-> + if @listeners.indexOf(listener)<0 + @listeners.push listener + + removeListener:(listener)-> + index = @listeners.indexOf listener + if index>=0 + @listeners.splice index,1 + + addProject:(project)-> + @projects[project.id] = project + + listProjects:()-> + list = [] + for key of @projects + list.push @projects[key] + list + + listPublicProjects:()-> + list = [] + for key,project of @projects + list.push project if project.public + list + + getTotalSize:()-> + size = 0 + for key,project of @projects + size += project.getSize() + size + + addProjectLink:(link)-> + @project_links.push link + + listProjectLinks:()-> + @project_links + + removeLink:(projectid)-> + for link,i in @project_links + if link.project.id == projectid + @project_links.splice i,1 + return + return + + findProject:(id)-> + @projects[id] + + findProjectBySlug:(slug)-> + for key of @projects + p = @projects[key] + if p.slug == slug + return p + + null + + deleteProject:(project)-> + delete @projects[project.id] + project.delete() + + set:(prop,value)-> + data = @record.get() + data[prop] = value + @record.set(data) + @[prop] = value + + addLike:(id)-> + if @likes.indexOf(id)<0 + @likes.push id + @set("likes",@likes) + + removeLike:(id)-> + index = @likes.indexOf(id) + if index>=0 + @likes.splice index,1 + @set("likes",@likes) + + isLiked:(id)-> + @likes.indexOf(id)>=0 + +# Flags reference +# * validated (e-mail validated) +# * deleted (account deleted) +# * + setFlag:(flag,value)-> + if value + @flags[flag] = value + else + delete @flags[flag] + @set "flags",@flags + + setSetting:(setting,value)-> + if value + @settings[setting] = value + else + delete @settings[setting] + @set "settings",@settings + + createValidationToken:()-> + map = "abcdefghijklmnopqrstuvwxyz0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ" + token = "" + for i in [1..32] by 1 + token += map.charAt(Math.floor(Math.random()*map.length)) + @record.setField "validation_token",token + token + + getValidationToken:()-> + code = @record.getField("validation_token") + if not code? + code = @createValidationToken() + code + + resetValidationToken:()-> + @record.setField("validation_token") + + canPublish:()-> @flags.validated and not @flags.deleted and not @flags.banned + showPublicStuff:()-> @flags.validated and not @flags.deleted and not @flags.banned and not @flags.censored + + notify:(text)-> + @notifications.push text + + delete:()-> + @setFlag "deleted",true + @content.userDeleted @ + for key,project of @projects + project.delete() + + @projects = {} + folder = "#{@id}" + @content.files.deleteFolder folder + return + +module.exports = @User diff --git a/server/content/user.js b/server/content/user.js new file mode 100644 index 00000000..92b7e48f --- /dev/null +++ b/server/content/user.js @@ -0,0 +1,243 @@ +this.User = (function() { + function User(content, record) { + var data; + this.content = content; + this.record = record; + data = this.record.get(); + this.id = data.id; + this.nick = data.nick; + this.email = data.email; + this.language = data.language || "en"; + this.flags = data.flags || {}; + this.settings = data.settings || {}; + this.hash = data.hash; + this.patches = []; + this.likes = data.likes || []; + this.projects = {}; + this.project_links = []; + this.listeners = []; + this.notifications = []; + this.description = data.description || ""; + this.updateTier(); + } + + User.prototype.updateTier = function() { + switch (this.flags.tier) { + case "pixel_master": + this.max_storage = 200000000; + return this.early_access = false; + case "code_ninja": + this.max_storage = 400000000; + return this.early_access = false; + case "gamedev_lord": + this.max_storage = 1000000000; + return this.early_access = true; + case "founder": + this.max_storage = 2000000000; + return this.early_access = true; + case "sponsor": + this.max_storage = 2000000000; + return this.early_access = true; + default: + if (this.flags.guest) { + this.max_storage = 2000000; + return this.early_access = false; + } else { + this.max_storage = 50000000; + return this.early_access = false; + } + } + }; + + User.prototype.addListener = function(listener) { + if (this.listeners.indexOf(listener) < 0) { + return this.listeners.push(listener); + } + }; + + User.prototype.removeListener = function(listener) { + var index; + index = this.listeners.indexOf(listener); + if (index >= 0) { + return this.listeners.splice(index, 1); + } + }; + + User.prototype.addProject = function(project) { + return this.projects[project.id] = project; + }; + + User.prototype.listProjects = function() { + var key, list; + list = []; + for (key in this.projects) { + list.push(this.projects[key]); + } + return list; + }; + + User.prototype.listPublicProjects = function() { + var key, list, project, ref; + list = []; + ref = this.projects; + for (key in ref) { + project = ref[key]; + if (project["public"]) { + list.push(project); + } + } + return list; + }; + + User.prototype.getTotalSize = function() { + var key, project, ref, size; + size = 0; + ref = this.projects; + for (key in ref) { + project = ref[key]; + size += project.getSize(); + } + return size; + }; + + User.prototype.addProjectLink = function(link) { + return this.project_links.push(link); + }; + + User.prototype.listProjectLinks = function() { + return this.project_links; + }; + + User.prototype.removeLink = function(projectid) { + var i, j, len, link, ref; + ref = this.project_links; + for (i = j = 0, len = ref.length; j < len; i = ++j) { + link = ref[i]; + if (link.project.id === projectid) { + this.project_links.splice(i, 1); + return; + } + } + }; + + User.prototype.findProject = function(id) { + return this.projects[id]; + }; + + User.prototype.findProjectBySlug = function(slug) { + var key, p; + for (key in this.projects) { + p = this.projects[key]; + if (p.slug === slug) { + return p; + } + } + return null; + }; + + User.prototype.deleteProject = function(project) { + delete this.projects[project.id]; + return project["delete"](); + }; + + User.prototype.set = function(prop, value) { + var data; + data = this.record.get(); + data[prop] = value; + this.record.set(data); + return this[prop] = value; + }; + + User.prototype.addLike = function(id) { + if (this.likes.indexOf(id) < 0) { + this.likes.push(id); + return this.set("likes", this.likes); + } + }; + + User.prototype.removeLike = function(id) { + var index; + index = this.likes.indexOf(id); + if (index >= 0) { + this.likes.splice(index, 1); + return this.set("likes", this.likes); + } + }; + + User.prototype.isLiked = function(id) { + return this.likes.indexOf(id) >= 0; + }; + + User.prototype.setFlag = function(flag, value) { + if (value) { + this.flags[flag] = value; + } else { + delete this.flags[flag]; + } + return this.set("flags", this.flags); + }; + + User.prototype.setSetting = function(setting, value) { + if (value) { + this.settings[setting] = value; + } else { + delete this.settings[setting]; + } + return this.set("settings", this.settings); + }; + + User.prototype.createValidationToken = function() { + var i, j, map, token; + map = "abcdefghijklmnopqrstuvwxyz0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ"; + token = ""; + for (i = j = 1; j <= 32; i = j += 1) { + token += map.charAt(Math.floor(Math.random() * map.length)); + } + this.record.setField("validation_token", token); + return token; + }; + + User.prototype.getValidationToken = function() { + var code; + code = this.record.getField("validation_token"); + if (code == null) { + code = this.createValidationToken(); + } + return code; + }; + + User.prototype.resetValidationToken = function() { + return this.record.setField("validation_token"); + }; + + User.prototype.canPublish = function() { + return this.flags.validated && !this.flags.deleted && !this.flags.banned; + }; + + User.prototype.showPublicStuff = function() { + return this.flags.validated && !this.flags.deleted && !this.flags.banned && !this.flags.censored; + }; + + User.prototype.notify = function(text) { + return this.notifications.push(text); + }; + + User.prototype["delete"] = function() { + var folder, key, project, ref; + this.setFlag("deleted", true); + this.content.userDeleted(this); + ref = this.projects; + for (key in ref) { + project = ref[key]; + project["delete"](); + } + this.projects = {}; + folder = "" + this.id; + this.content.files.deleteFolder(folder); + }; + + return User; + +})(); + +module.exports = this.User; diff --git a/server/db/db.coffee b/server/db/db.coffee new file mode 100644 index 00000000..8c98dbfe --- /dev/null +++ b/server/db/db.coffee @@ -0,0 +1,64 @@ +fs = require "fs" +Table = require __dirname+"/table.js" + +class @DB + constructor:(@folder="../data",@callback)-> + @tables = {} + @save_list = [] + + if not fs.existsSync(@folder) + fs.mkdir(@folder,=>) + else + @load() + + close:()-> + for key of @tables + @tables[key].close() + + load:()-> + console.info "loading DB..." + @load_time = Date.now() + files = fs.readdirSync @folder + for f in files + if fs.lstatSync("#{@folder}/#{f}").isDirectory() + table = new Table @,f + @tables[f] = table + + @tableLoaded() + return + + tableLoaded:()-> + for key,table of @tables + return if not table.loaded + + if @callback? + @load_time = Date.now()-@load_time + console.info "DB load time: #{@load_time} ms" + @callback @ + delete @callback + + create:(table,data)-> + #console.info "creating #{JSON.stringify(data)}" + t = @tables[table] + if not t? + t = new Table(@,table) + @tables[table] = t + + t.create(data) + + get:(table,id,value)-> + t = @tables[table] + if t? then t.get(id,value) else null + + set:(table,id,data)-> + t = @tables[table] + t.set(id,data) if t? + + list:(table)-> + t = @tables[table] + if not t? + [] + else + t.list() + +module.exports = @DB diff --git a/server/db/db.js b/server/db/db.js new file mode 100644 index 00000000..c96ac027 --- /dev/null +++ b/server/db/db.js @@ -0,0 +1,105 @@ +var Table, fs; + +fs = require("fs"); + +Table = require(__dirname + "/table.js"); + +this.DB = (function() { + function DB(folder, callback) { + this.folder = folder != null ? folder : "../data"; + this.callback = callback; + this.tables = {}; + this.save_list = []; + if (!fs.existsSync(this.folder)) { + fs.mkdir(this.folder, (function(_this) { + return function() {}; + })(this)); + } else { + this.load(); + } + } + + DB.prototype.close = function() { + var key, results; + results = []; + for (key in this.tables) { + results.push(this.tables[key].close()); + } + return results; + }; + + DB.prototype.load = function() { + var f, files, i, len, table; + console.info("loading DB..."); + this.load_time = Date.now(); + files = fs.readdirSync(this.folder); + for (i = 0, len = files.length; i < len; i++) { + f = files[i]; + if (fs.lstatSync(this.folder + "/" + f).isDirectory()) { + table = new Table(this, f); + this.tables[f] = table; + } + } + this.tableLoaded(); + }; + + DB.prototype.tableLoaded = function() { + var key, ref, table; + ref = this.tables; + for (key in ref) { + table = ref[key]; + if (!table.loaded) { + return; + } + } + if (this.callback != null) { + this.load_time = Date.now() - this.load_time; + console.info("DB load time: " + this.load_time + " ms"); + this.callback(this); + return delete this.callback; + } + }; + + DB.prototype.create = function(table, data) { + var t; + t = this.tables[table]; + if (t == null) { + t = new Table(this, table); + this.tables[table] = t; + } + return t.create(data); + }; + + DB.prototype.get = function(table, id, value) { + var t; + t = this.tables[table]; + if (t != null) { + return t.get(id, value); + } else { + return null; + } + }; + + DB.prototype.set = function(table, id, data) { + var t; + t = this.tables[table]; + if (t != null) { + return t.set(id, data); + } + }; + + DB.prototype.list = function(table) { + var t; + t = this.tables[table]; + if (t == null) { + return []; + } else { + return t.list(); + } + }; + + return DB; + +})(); + +module.exports = this.DB; diff --git a/server/db/record.coffee b/server/db/record.coffee new file mode 100644 index 00000000..c8f0c68f --- /dev/null +++ b/server/db/record.coffee @@ -0,0 +1,31 @@ +fs = require "fs" + +class @Record + constructor:(@table,@cluster,@id,@data)-> + @data.id = @id + @data = JSON.stringify @data + + get:()-> + JSON.parse @data + + set:(data)-> + old_data = @data + @data = data + @data.id = @id + @data = JSON.stringify @data + @cluster.save_requested = true + @table.updateIndexes @,old_data,data + + setField:(field,value)-> + data = @get() + if not value? + delete data[field] + else + data[field] = value + @set(data) + + getField:(field)-> + data = @get() + data[field] + +module.exports = @Record diff --git a/server/db/record.js b/server/db/record.js new file mode 100644 index 00000000..780d2c23 --- /dev/null +++ b/server/db/record.js @@ -0,0 +1,50 @@ +var fs; + +fs = require("fs"); + +this.Record = (function() { + function Record(table, cluster, id, data1) { + this.table = table; + this.cluster = cluster; + this.id = id; + this.data = data1; + this.data.id = this.id; + this.data = JSON.stringify(this.data); + } + + Record.prototype.get = function() { + return JSON.parse(this.data); + }; + + Record.prototype.set = function(data) { + var old_data; + old_data = this.data; + this.data = data; + this.data.id = this.id; + this.data = JSON.stringify(this.data); + this.cluster.save_requested = true; + return this.table.updateIndexes(this, old_data, data); + }; + + Record.prototype.setField = function(field, value) { + var data; + data = this.get(); + if (value == null) { + delete data[field]; + } else { + data[field] = value; + } + return this.set(data); + }; + + Record.prototype.getField = function(field) { + var data; + data = this.get(); + return data[field]; + }; + + return Record; + +})(); + +module.exports = this.Record; diff --git a/server/db/table.coffee b/server/db/table.coffee new file mode 100644 index 00000000..b8f07421 --- /dev/null +++ b/server/db/table.coffee @@ -0,0 +1,185 @@ +fs = require "fs" +Record = require "./record.js" + +CLUSTER_SIZE = 1000 + +class @Table + constructor:(@db,@name)-> + @folder = @db.folder+"/"+@name + + @records = {} + @clusters = {} + @indexes = {} + @current_id = 0 + + if not fs.existsSync(@folder) + fs.mkdirSync @folder + else + @load() + + @scheduler = setInterval (()=>@timer()),2000 + + close:()-> + clearInterval @scheduler + + load:(dir=@folder)-> + files = fs.readdirSync dir + queue = [] + cluster = 0 + loop + break if files.indexOf(cluster+"")<0 and files.indexOf(cluster+".bak")<0 + fi = dir+"/"+cluster + queue.push cluster + cluster++ + + funk = ()=> + if queue.length>0 + f = queue.splice(0,1)[0] + console.info "loading cluster #{f}" + cluster = new Cluster @,f|0 + @clusters[f|0] = cluster + cluster.load(funk) + else + @loaded = true + @db.tableLoaded() + + funk() + return + + create:(data)-> + cluster_id = Math.floor(@current_id/CLUSTER_SIZE) + cluster = @clusters[cluster_id] + if not cluster? + cluster = new Cluster @,cluster_id + @clusters[cluster_id] = cluster + + record = new Record @,cluster,@current_id++,data + cluster.addRecord record + @records[record.id] = record + + updateIndexes:(record,old_data,data)-> + for key,value of old_data + if data[key] != value + index = @indexes[key] + if index? + v = index[value] + if v? + if v == record + delete index[value] + else if Array.isArray(v) and v.indexOf(record)>=0 + v.splice v.indexOf(record),1 + index[value] = v[0] if v.length == 1 + value = data[key] + i = index[value] + if i? + if not Array.isArray i + i = index[value] = [i] + i.push record + else + index[value] = record + return + + getIndex:(field)-> + index = @indexes[field] + if not index? + @indexes[field] = index = {} + for key,record of @records + value = record.get()[field] + if value? + i = index[value] + if i? + if not Array.isArray i + i = index[value] = [i] + i.push record + else + index[value] = record + index + + get:(id,value)-> + if value? + index = @getIndex(id) + list = index[value] + if list? + if Array.isArray(list) + return (v.get() for v in list) + else + return list.get() + else + null + else + @records[id] + + set:(id,data)-> + record = @records[id] + if record? + record.set data + + list:()-> + list = [] + for key,r of @records + list.push r + list + + timer:()-> + for key,cluster of @clusters + cluster.timer() + return + +class Cluster + constructor:(@table,@id)-> + @records = [] + @save_requested = false + + addRecord:(record)-> + @records.push record + @save_requested = true + + load:(callback)-> + file = "#{@table.folder}/#{@id}" + fs.readFile file,(err,data)=> + try + data = JSON.parse data + for r in data + record = new Record(@table,@,r.id,r) + @records.push record + @table.records[record.id] = record + @table.current_id = Math.max(@table.current_id,record.id+1) + console.info "cluster loaded: "+@id + return callback() + catch err + console.error err + fs.readFile file+".bak",(err,data)=> + try + data = JSON.parse data + for r in data + record = new Record(@table,@,r.id,r) + @records.push record + @table.records[record.id] = record + @table.current_id = Math.max(@table.current_id,record.id+1) + console.info "cluster loaded from backup: "+@id + return callback() + catch err + console.error err + throw "DB loading error with cluster: #{file}" + + save:()-> + data = [] + for r in @records + data.push r.get() + + data = JSON.stringify data + file = "#{@table.folder}/#{@id}" + fs.writeFile file,data,(err)=> + if not err + fs.writeFile file+".bak",data,(err)=> + console.info "cluster #{@table.name}:#{@id} saved" + + timer:()-> + try + if @save_requested + @save_requested = false + @save() + catch err + console.error err + +module.exports = @Table diff --git a/server/db/table.js b/server/db/table.js new file mode 100644 index 00000000..fc6435f5 --- /dev/null +++ b/server/db/table.js @@ -0,0 +1,292 @@ +var CLUSTER_SIZE, Cluster, Record, fs; + +fs = require("fs"); + +Record = require("./record.js"); + +CLUSTER_SIZE = 1000; + +this.Table = (function() { + function Table(db, name) { + this.db = db; + this.name = name; + this.folder = this.db.folder + "/" + this.name; + this.records = {}; + this.clusters = {}; + this.indexes = {}; + this.current_id = 0; + if (!fs.existsSync(this.folder)) { + fs.mkdirSync(this.folder); + } else { + this.load(); + } + this.scheduler = setInterval(((function(_this) { + return function() { + return _this.timer(); + }; + })(this)), 2000); + } + + Table.prototype.close = function() { + return clearInterval(this.scheduler); + }; + + Table.prototype.load = function(dir) { + var cluster, fi, files, funk, queue; + if (dir == null) { + dir = this.folder; + } + files = fs.readdirSync(dir); + queue = []; + cluster = 0; + while (true) { + if (files.indexOf(cluster + "") < 0 && files.indexOf(cluster + ".bak") < 0) { + break; + } + fi = dir + "/" + cluster; + queue.push(cluster); + cluster++; + } + funk = (function(_this) { + return function() { + var f; + if (queue.length > 0) { + f = queue.splice(0, 1)[0]; + console.info("loading cluster " + f); + cluster = new Cluster(_this, f | 0); + _this.clusters[f | 0] = cluster; + return cluster.load(funk); + } else { + _this.loaded = true; + return _this.db.tableLoaded(); + } + }; + })(this); + funk(); + }; + + Table.prototype.create = function(data) { + var cluster, cluster_id, record; + cluster_id = Math.floor(this.current_id / CLUSTER_SIZE); + cluster = this.clusters[cluster_id]; + if (cluster == null) { + cluster = new Cluster(this, cluster_id); + this.clusters[cluster_id] = cluster; + } + record = new Record(this, cluster, this.current_id++, data); + cluster.addRecord(record); + return this.records[record.id] = record; + }; + + Table.prototype.updateIndexes = function(record, old_data, data) { + var i, index, key, v, value; + for (key in old_data) { + value = old_data[key]; + if (data[key] !== value) { + index = this.indexes[key]; + if (index != null) { + v = index[value]; + if (v != null) { + if (v === record) { + delete index[value]; + } else if (Array.isArray(v) && v.indexOf(record) >= 0) { + v.splice(v.indexOf(record), 1); + if (v.length === 1) { + index[value] = v[0]; + } + } + } + value = data[key]; + i = index[value]; + if (i != null) { + if (!Array.isArray(i)) { + i = index[value] = [i]; + } + i.push(record); + } else { + index[value] = record; + } + } + } + } + }; + + Table.prototype.getIndex = function(field) { + var i, index, key, record, ref, value; + index = this.indexes[field]; + if (index == null) { + this.indexes[field] = index = {}; + ref = this.records; + for (key in ref) { + record = ref[key]; + value = record.get()[field]; + if (value != null) { + i = index[value]; + if (i != null) { + if (!Array.isArray(i)) { + i = index[value] = [i]; + } + i.push(record); + } else { + index[value] = record; + } + } + } + } + return index; + }; + + Table.prototype.get = function(id, value) { + var index, list, v; + if (value != null) { + index = this.getIndex(id); + list = index[value]; + if (list != null) { + if (Array.isArray(list)) { + return (function() { + var j, len, results; + results = []; + for (j = 0, len = list.length; j < len; j++) { + v = list[j]; + results.push(v.get()); + } + return results; + })(); + } else { + return list.get(); + } + } else { + return null; + } + } else { + return this.records[id]; + } + }; + + Table.prototype.set = function(id, data) { + var record; + record = this.records[id]; + if (record != null) { + return record.set(data); + } + }; + + Table.prototype.list = function() { + var key, list, r, ref; + list = []; + ref = this.records; + for (key in ref) { + r = ref[key]; + list.push(r); + } + return list; + }; + + Table.prototype.timer = function() { + var cluster, key, ref; + ref = this.clusters; + for (key in ref) { + cluster = ref[key]; + cluster.timer(); + } + }; + + return Table; + +})(); + +Cluster = (function() { + function Cluster(table, id1) { + this.table = table; + this.id = id1; + this.records = []; + this.save_requested = false; + } + + Cluster.prototype.addRecord = function(record) { + this.records.push(record); + return this.save_requested = true; + }; + + Cluster.prototype.load = function(callback) { + var file; + file = this.table.folder + "/" + this.id; + return fs.readFile(file, (function(_this) { + return function(err, data) { + var j, len, r, record; + try { + data = JSON.parse(data); + for (j = 0, len = data.length; j < len; j++) { + r = data[j]; + record = new Record(_this.table, _this, r.id, r); + _this.records.push(record); + _this.table.records[record.id] = record; + _this.table.current_id = Math.max(_this.table.current_id, record.id + 1); + } + console.info("cluster loaded: " + _this.id); + return callback(); + } catch (error) { + err = error; + console.error(err); + return fs.readFile(file + ".bak", function(err, data) { + var k, len1; + try { + data = JSON.parse(data); + for (k = 0, len1 = data.length; k < len1; k++) { + r = data[k]; + record = new Record(_this.table, _this, r.id, r); + _this.records.push(record); + _this.table.records[record.id] = record; + _this.table.current_id = Math.max(_this.table.current_id, record.id + 1); + } + console.info("cluster loaded from backup: " + _this.id); + return callback(); + } catch (error) { + err = error; + console.error(err); + throw "DB loading error with cluster: " + file; + } + }); + } + }; + })(this)); + }; + + Cluster.prototype.save = function() { + var data, file, j, len, r, ref; + data = []; + ref = this.records; + for (j = 0, len = ref.length; j < len; j++) { + r = ref[j]; + data.push(r.get()); + } + data = JSON.stringify(data); + file = this.table.folder + "/" + this.id; + return fs.writeFile(file, data, (function(_this) { + return function(err) { + if (!err) { + fs.writeFile(file + ".bak", data, function(err) {}); + } + return console.info("cluster " + _this.table.name + ":" + _this.id + " saved"); + }; + })(this)); + }; + + Cluster.prototype.timer = function() { + var err; + try { + if (this.save_requested) { + this.save_requested = false; + return this.save(); + } + } catch (error) { + err = error; + return console.error(err); + } + }; + + return Cluster; + +})(); + +module.exports = this.Table; diff --git a/server/file_types.coffee b/server/file_types.coffee new file mode 100644 index 00000000..cfb0f42e --- /dev/null +++ b/server/file_types.coffee @@ -0,0 +1,27 @@ +@FILE_TYPES = + ms: + folder: "ms" + extensions: ["ms"] + property: "sources" + sprites: + folder: "sprites" + extensions: ["png"] + property: "sprites" + maps: + folder: "maps" + extensions: ["json"] + property: "maps" + sounds: + folder: "sounds" + extensions: ["wav"] + property: "sounds" + music: + folder: "music" + extensions: ["mp3"] + property: "music" + assets: + folder: "assets" + extensions: ["glb","jpg","png"] + property: "assets" + +module.exports = @FILE_TYPES diff --git a/server/file_types.js b/server/file_types.js new file mode 100644 index 00000000..4a565520 --- /dev/null +++ b/server/file_types.js @@ -0,0 +1,34 @@ +this.FILE_TYPES = { + ms: { + folder: "ms", + extensions: ["ms"], + property: "sources" + }, + sprites: { + folder: "sprites", + extensions: ["png"], + property: "sprites" + }, + maps: { + folder: "maps", + extensions: ["json"], + property: "maps" + }, + sounds: { + folder: "sounds", + extensions: ["wav"], + property: "sounds" + }, + music: { + folder: "music", + extensions: ["mp3"], + property: "music" + }, + assets: { + folder: "assets", + extensions: ["glb", "jpg", "png"], + property: "assets" + } +}; + +module.exports = this.FILE_TYPES; diff --git a/server/filestorage/filestorage.coffee b/server/filestorage/filestorage.coffee new file mode 100644 index 00000000..c280a103 --- /dev/null +++ b/server/filestorage/filestorage.coffee @@ -0,0 +1,122 @@ +fs = require "fs" +getDirName = require('path').dirname +JobQueue = require __dirname+"/../app/jobqueue.js" + +class @FileStorage + constructor:(@folder="../files")-> + if not fs.existsSync(@folder) + fs.mkdir(@folder,=>) + + list:(file,callback)-> + file = @sanitize file + + f = @folder+"/#{file}" + fs.readdir f,(err,files)=> + callback(files) + + write:(file,content,callback)-> + file = @sanitize file + + @mkdirs getDirName(file),()=> + f = @folder+"/#{file}" + fs.writeFile f,content,()=> + callback() if callback? + + delete:(file,callback)-> + file = @sanitize file + + f = @folder+"/#{file}" + fs.unlink f,()=> + callback() if callback? + + deleteFolder:(srcfile,callback)-> + file = @sanitize srcfile + + queue = new JobQueue() + queue.conclude = ()=> + fs.rmdir "#{@folder}/#{file}",(err)=> + callback() if callback? + + addFile = (f)=> + queue.add ()=> + fs.lstat "#{@folder}/#{file}/#{f}",(err,stats)=> + if err? + queue.next() + else if stats.isDirectory() + @deleteFolder "#{srcfile}/#{f}",()=>queue.next() + else + @delete "#{srcfile}/#{f}",()=>queue.next() + + queue.add ()=> + fs.readdir "#{@folder}/#{file}",(err,files)=> + #console.info "#{@folder}/#{file} => "+JSON.stringify files + if files? + for f in files + addFile f + queue.next() + + queue.start() + + mkdirs:(folder,callback)-> + f = @folder+"/#{folder}" + fs.stat f,(err,stat)=> + if err? + if folder.indexOf("/") > 0 + @mkdirs getDirName(folder),()=> + fs.mkdir f,()=> + callback() + else + fs.mkdir f,()=> + callback() + else + callback() + + return + + sanitize:(file)-> + s = file.split("/") + for i in [s.length-1..0] by -1 + if s[i].indexOf("..")>=0 or s[i] == "" + s.splice i,1 + + d1 = Math.floor(s[0]/10000) + d2 = Math.floor(s[0]/100)%100 + d3 = s[0]%100 + + s[0] = d3 + s.splice(0,0,d2) + s.splice(0,0,d1) + + #console.info "input #{file} sanitized to #{s.join("/")}" + + s.join("/") + + read:(file,encoding,callback)-> + file = @sanitize file + + f = @folder+"/#{file}" + fs.readFile f,(err,data)=> + if data? and not err? + switch encoding + when "base64" + callback data.toString "base64" + when "binary" + callback data + else + callback data.toString "utf8" + else + callback(null) + + copyFile:(path,file,callback)-> + fs.readFile path,(err,data)=> + if data? and not err? + @write(file,data,callback) + else + console.info err + + copy:(source,dest,callback)-> + @read source,"binary",(data)=> + @write dest,data,()=> + callback() if callback? + +module.exports = @FileStorage diff --git a/server/filestorage/filestorage.js b/server/filestorage/filestorage.js new file mode 100644 index 00000000..d4a56569 --- /dev/null +++ b/server/filestorage/filestorage.js @@ -0,0 +1,198 @@ +var JobQueue, fs, getDirName; + +fs = require("fs"); + +getDirName = require('path').dirname; + +JobQueue = require(__dirname + "/../app/jobqueue.js"); + +this.FileStorage = (function() { + function FileStorage(folder1) { + this.folder = folder1 != null ? folder1 : "../files"; + if (!fs.existsSync(this.folder)) { + fs.mkdir(this.folder, (function(_this) { + return function() {}; + })(this)); + } + } + + FileStorage.prototype.list = function(file, callback) { + var f; + file = this.sanitize(file); + f = this.folder + ("/" + file); + return fs.readdir(f, (function(_this) { + return function(err, files) { + return callback(files); + }; + })(this)); + }; + + FileStorage.prototype.write = function(file, content, callback) { + file = this.sanitize(file); + return this.mkdirs(getDirName(file), (function(_this) { + return function() { + var f; + f = _this.folder + ("/" + file); + return fs.writeFile(f, content, function() { + if (callback != null) { + return callback(); + } + }); + }; + })(this)); + }; + + FileStorage.prototype["delete"] = function(file, callback) { + var f; + file = this.sanitize(file); + f = this.folder + ("/" + file); + return fs.unlink(f, (function(_this) { + return function() { + if (callback != null) { + return callback(); + } + }; + })(this)); + }; + + FileStorage.prototype.deleteFolder = function(srcfile, callback) { + var addFile, file, queue; + file = this.sanitize(srcfile); + queue = new JobQueue(); + queue.conclude = (function(_this) { + return function() { + return fs.rmdir(_this.folder + "/" + file, function(err) { + if (callback != null) { + return callback(); + } + }); + }; + })(this); + addFile = (function(_this) { + return function(f) { + return queue.add(function() { + return fs.lstat(_this.folder + "/" + file + "/" + f, function(err, stats) { + if (err != null) { + return queue.next(); + } else if (stats.isDirectory()) { + return _this.deleteFolder(srcfile + "/" + f, function() { + return queue.next(); + }); + } else { + return _this["delete"](srcfile + "/" + f, function() { + return queue.next(); + }); + } + }); + }); + }; + })(this); + queue.add((function(_this) { + return function() { + return fs.readdir(_this.folder + "/" + file, function(err, files) { + var f, j, len; + if (files != null) { + for (j = 0, len = files.length; j < len; j++) { + f = files[j]; + addFile(f); + } + } + return queue.next(); + }); + }; + })(this)); + return queue.start(); + }; + + FileStorage.prototype.mkdirs = function(folder, callback) { + var f; + f = this.folder + ("/" + folder); + fs.stat(f, (function(_this) { + return function(err, stat) { + if (err != null) { + if (folder.indexOf("/") > 0) { + return _this.mkdirs(getDirName(folder), function() { + return fs.mkdir(f, function() { + return callback(); + }); + }); + } else { + return fs.mkdir(f, function() { + return callback(); + }); + } + } else { + return callback(); + } + }; + })(this)); + }; + + FileStorage.prototype.sanitize = function(file) { + var d1, d2, d3, i, j, ref, s; + s = file.split("/"); + for (i = j = ref = s.length - 1; j >= 0; i = j += -1) { + if (s[i].indexOf("..") >= 0 || s[i] === "") { + s.splice(i, 1); + } + } + d1 = Math.floor(s[0] / 10000); + d2 = Math.floor(s[0] / 100) % 100; + d3 = s[0] % 100; + s[0] = d3; + s.splice(0, 0, d2); + s.splice(0, 0, d1); + return s.join("/"); + }; + + FileStorage.prototype.read = function(file, encoding, callback) { + var f; + file = this.sanitize(file); + f = this.folder + ("/" + file); + return fs.readFile(f, (function(_this) { + return function(err, data) { + if ((data != null) && (err == null)) { + switch (encoding) { + case "base64": + return callback(data.toString("base64")); + case "binary": + return callback(data); + default: + return callback(data.toString("utf8")); + } + } else { + return callback(null); + } + }; + })(this)); + }; + + FileStorage.prototype.copyFile = function(path, file, callback) { + return fs.readFile(path, (function(_this) { + return function(err, data) { + if ((data != null) && (err == null)) { + return _this.write(file, data, callback); + } else { + return console.info(err); + } + }; + })(this)); + }; + + FileStorage.prototype.copy = function(source, dest, callback) { + return this.read(source, "binary", (function(_this) { + return function(data) { + return _this.write(dest, data, function() { + if (callback != null) { + return callback(); + } + }); + }; + })(this)); + }; + + return FileStorage; + +})(); + +module.exports = this.FileStorage; diff --git a/server/fonts.coffee b/server/fonts.coffee new file mode 100644 index 00000000..03d77386 --- /dev/null +++ b/server/fonts.coffee @@ -0,0 +1,21 @@ +fs = require "fs" + +class @Fonts + constructor:(@folder="../static/fonts")-> + @fonts = [] + fs.readdir @folder,(err,files)=> + if err? + console.error err + + return if not files? + for f in files + if f.endsWith(".ttf") and f != "bit_cell.ttf" + @fonts.push(f.split(".")[0]) + + console.info JSON.stringify @fonts + + read:(font,callback)-> + fs.readFile "#{@folder}/#{font}.ttf",(err,data)=> + callback(data) + +module.exports = @Fonts diff --git a/server/fonts.js b/server/fonts.js new file mode 100644 index 00000000..b3bc16e4 --- /dev/null +++ b/server/fonts.js @@ -0,0 +1,41 @@ +var fs; + +fs = require("fs"); + +this.Fonts = (function() { + function Fonts(folder) { + this.folder = folder != null ? folder : "../static/fonts"; + this.fonts = []; + fs.readdir(this.folder, (function(_this) { + return function(err, files) { + var f, i, len; + if (err != null) { + console.error(err); + } + if (files == null) { + return; + } + for (i = 0, len = files.length; i < len; i++) { + f = files[i]; + if (f.endsWith(".ttf") && f !== "bit_cell.ttf") { + _this.fonts.push(f.split(".")[0]); + } + } + return console.info(JSON.stringify(_this.fonts)); + }; + })(this)); + } + + Fonts.prototype.read = function(font, callback) { + return fs.readFile(this.folder + "/" + font + ".ttf", (function(_this) { + return function(err, data) { + return callback(data); + }; + })(this)); + }; + + return Fonts; + +})(); + +module.exports = this.Fonts; diff --git a/server/forum/category.coffee b/server/forum/category.coffee new file mode 100644 index 00000000..ceda8569 --- /dev/null +++ b/server/forum/category.coffee @@ -0,0 +1,76 @@ +class @ForumCategory + constructor:(@forum,@record)-> + data = @record.get() + + @id = data.id + @language = data.language + @name = data.name + @slug = data.slug + @description = data.description + @hue = data.hue or 0 + @activity = data.activity + @deleted = data.deleted + @hidden = data.hidden or false + @watch = data.watch or [] + + @permissions = data.permissions or { + post: "user" + reply: "user" + } + + @options = data.options or { + post_progress: false + post_close: false + post_sortorder: false + } + + @posts = [] + + @path = if @language == "en" then "/community/#{@slug}/" else "/#{@language}/community/#{@slug}/" + + set:(prop,value)-> + data = @record.get() + data[prop] = value + @record.set(data) + @[prop] = value + + addPost:(post)-> + @posts.push post + @sorted = false + + removePost:(post)-> + index = @posts.indexOf(post) + if index>=0 + @posts.splice(index,1) + @sorted = false + + updateActivity:()-> + @set "activity",Date.now() + @updateSort() + @forum.updateActivity() + + updateSort:()-> + @sorted = false + + getSortedPosts:()-> + if not @sorted + @posts.sort (a,b)-> b.activity-a.activity + + @posts + + addWatch:(id)-> + index = @watch.indexOf(id) + if index<0 + @watch.push id + @set "watch",@watch + + removeWatch:(id)-> + index = @watch.indexOf(id) + if index>=0 + @watch.splice index,1 + @set "watch",@watch + + isWatching:(id)-> + @watch.indexOf(id)>=0 + +module.exports = @ForumCategory diff --git a/server/forum/category.js b/server/forum/category.js new file mode 100644 index 00000000..07542382 --- /dev/null +++ b/server/forum/category.js @@ -0,0 +1,97 @@ +this.ForumCategory = (function() { + function ForumCategory(forum, record) { + var data; + this.forum = forum; + this.record = record; + data = this.record.get(); + this.id = data.id; + this.language = data.language; + this.name = data.name; + this.slug = data.slug; + this.description = data.description; + this.hue = data.hue || 0; + this.activity = data.activity; + this.deleted = data.deleted; + this.hidden = data.hidden || false; + this.watch = data.watch || []; + this.permissions = data.permissions || { + post: "user", + reply: "user" + }; + this.options = data.options || { + post_progress: false, + post_close: false, + post_sortorder: false + }; + this.posts = []; + this.path = this.language === "en" ? "/community/" + this.slug + "/" : "/" + this.language + "/community/" + this.slug + "/"; + } + + ForumCategory.prototype.set = function(prop, value) { + var data; + data = this.record.get(); + data[prop] = value; + this.record.set(data); + return this[prop] = value; + }; + + ForumCategory.prototype.addPost = function(post) { + this.posts.push(post); + return this.sorted = false; + }; + + ForumCategory.prototype.removePost = function(post) { + var index; + index = this.posts.indexOf(post); + if (index >= 0) { + this.posts.splice(index, 1); + return this.sorted = false; + } + }; + + ForumCategory.prototype.updateActivity = function() { + this.set("activity", Date.now()); + this.updateSort(); + return this.forum.updateActivity(); + }; + + ForumCategory.prototype.updateSort = function() { + return this.sorted = false; + }; + + ForumCategory.prototype.getSortedPosts = function() { + if (!this.sorted) { + this.posts.sort(function(a, b) { + return b.activity - a.activity; + }); + } + return this.posts; + }; + + ForumCategory.prototype.addWatch = function(id) { + var index; + index = this.watch.indexOf(id); + if (index < 0) { + this.watch.push(id); + return this.set("watch", this.watch); + } + }; + + ForumCategory.prototype.removeWatch = function(id) { + var index; + index = this.watch.indexOf(id); + if (index >= 0) { + this.watch.splice(index, 1); + return this.set("watch", this.watch); + } + }; + + ForumCategory.prototype.isWatching = function(id) { + return this.watch.indexOf(id) >= 0; + }; + + return ForumCategory; + +})(); + +module.exports = this.ForumCategory; diff --git a/server/forum/forum.coffee b/server/forum/forum.coffee new file mode 100644 index 00000000..59f4910e --- /dev/null +++ b/server/forum/forum.coffee @@ -0,0 +1,219 @@ +ForumCategory = require __dirname+"/category.js" +ForumPost = require __dirname+"/post.js" +ForumReply = require __dirname+"/reply.js" +Indexer = require __dirname+"/indexer.js" + +class @Forum + constructor:(@content)-> + @db = @content.db + @server = @content.server + + @languages = {} + @categories = {} + @posts = {} + @replies = {} + + @category_by_slug = {} + + @category_count = 0 + @post_count = 0 + @reply_count = 0 + @indexers = {} + @load() + + close:()-> + for key,indexer in @indexers + indexer.stop() + return + + load:()-> + categories = @db.list("forum_categories") + for record in categories + @loadCategory record + + posts = @db.list("forum_posts") + for record in posts + @loadPost record + + replies = @db.list("forum_replies") + for record in replies + @loadReply record + + @updateActivity() + + return + + index:(language,text,target,uid)-> + return if not language? + indexer = @indexers[language] + if not indexer? + @indexers[language] = indexer = new Indexer() + indexer.add text,target,uid + + loadCategory:(record)-> + cat = new ForumCategory @,record + if cat.language? and not cat.deleted + if not @languages[cat.language]? + @languages[cat.language] = [] + @languages[cat.language].push cat + @category_by_slug[cat.slug] = cat + @categories[cat.id] = cat + @category_count++ + + cat + + loadPost:(record)-> + catid = record.getField "category" + category = @categories[catid] + if category? + post = new ForumPost category,record + category.addPost(post) + @posts[post.id] = post + if not post.deleted and not category.hidden + @post_count++ + @index post.category.language,"#{post.title} #{post.category.name} #{post.author.nick}",post,post.id + @index post.category.language,post.text,post,post.id + + post + + loadReply:(record)-> + postid = record.getField "post" + post = @posts[postid] + if post? + reply = new ForumReply post,record + post.addReply(reply) + @replies[reply.id] = reply + if not reply.deleted and not post.category.hidden + @reply_count++ + @index reply.post.category.language,reply.text,reply.post,post.id + reply + + createPost:(category,user,title,text)-> + data = + author: user + category: category + title: title + text: text + date: Date.now() + activity: Date.now() + edits: 0 + + record = @db.create "forum_posts",data + post = @loadPost record + if post? + post.category.updateActivity() + post.addWatch post.author.id + + try + for id in post.category.watch + @sendPostNotification id,post + catch err + console.error err + return post + + createReply:(post,user,text)-> + data = + post: post.id + author: user + text: text + date: Date.now() + activity: Date.now() + edits: 0 + + post.updateActivity() + + record = @db.create "forum_replies",data + reply = @loadReply record + for id in post.watch + @sendReplyNotification id,reply + return reply + + sendPostNotification:(userid,post)-> + user = @content.users[userid] + return if not user? + return if not user.email? + return if not user.flags.validated + return if user == post.author + + translator = @content.translator.getTranslator(user.language) + subject = translator.get("%USER% posted in %CATEGORY%").replace("%USER%",post.author.nick).replace("%CATEGORY%",post.category.name)+ " - #{translator.get("microStudio Community")}" + text = translator.get("%USER% published a new post in %CATEGORY%:").replace("%USER%",post.author.nick).replace("%CATEGORY%",post.category.name)+"\n\n" + text += post.title+"\n" + text += "https://microstudio.dev#{post.getPath()}\n\n" + text += if post.text.length<500 then post.text else post.text.substring(0,500)+" (...)" + + @server.mailer.sendMail user.email,subject,text + + sendReplyNotification:(userid,reply)-> + user = @content.users[userid] + return if not user? + return if not user.email? + return if not user.flags.validated + return if user == reply.author + + translator = @content.translator.getTranslator(user.language) + subject = translator.get("%USER% posted a reply").replace("%USER%",reply.author.nick)+ " - #{translator.get("microStudio Community")}" + text = translator.get("%USER% posted a reply to %POST%:").replace("%USER%",reply.author.nick).replace("%POST%",reply.post.title)+"\n\n" + text += "https://microstudio.dev#{reply.post.getPath()}#{reply.post.replies.length-1}/\n\n" + text += if reply.text.length<500 then reply.text else reply.text.substring(0,500)+" (...)" + + @server.mailer.sendMail user.email,subject,text + + createCategory:(language,name,slug,description,hue,permissions)-> + data = + language: language + name: name + slug: slug + description: description + hue: hue or 0 + permissions: permissions + deleted: false + activity: Date.now() + + record = @db.create "forum_categories",data + cat = @loadCategory record + + editCategory:(user,category,data)-> + + editPost:(user,post,data)-> + + editReply:(user,post,reply,data)-> + + listCategories:(lang)-> + cats = @languages[lang] + if cats? + res = [] + for c in cats + if not c.deleted and not c.hidden + res.push c + res.sort (a,b)-> b.activity - a.activity + res + else + [] + + updateActivity:()-> + @activity = 0 + for key,cat of @categories + @activity = Math.max(@activity,cat.activity) + + @sorted = false + + + getLanguages:()-> + lang = [] + for key of @languages + lang.push key + lang + + getCategory:(language,slug)-> + lang = @languages[language] + return null if not lang + lang[slug] + + escapeHTML:(s)-> + return s.replace(/&/g, '&') + .replace(/"/g, '"') + .replace(//g, '>') + +module.exports = @Forum diff --git a/server/forum/forum.js b/server/forum/forum.js new file mode 100644 index 00000000..aea6a806 --- /dev/null +++ b/server/forum/forum.js @@ -0,0 +1,294 @@ +var ForumCategory, ForumPost, ForumReply, Indexer; + +ForumCategory = require(__dirname + "/category.js"); + +ForumPost = require(__dirname + "/post.js"); + +ForumReply = require(__dirname + "/reply.js"); + +Indexer = require(__dirname + "/indexer.js"); + +this.Forum = (function() { + function Forum(content) { + this.content = content; + this.db = this.content.db; + this.server = this.content.server; + this.languages = {}; + this.categories = {}; + this.posts = {}; + this.replies = {}; + this.category_by_slug = {}; + this.category_count = 0; + this.post_count = 0; + this.reply_count = 0; + this.indexers = {}; + this.load(); + } + + Forum.prototype.close = function() { + var i, indexer, key, len, ref; + ref = this.indexers; + for (indexer = i = 0, len = ref.length; i < len; indexer = ++i) { + key = ref[indexer]; + indexer.stop(); + } + }; + + Forum.prototype.load = function() { + var categories, i, j, k, len, len1, len2, posts, record, replies; + categories = this.db.list("forum_categories"); + for (i = 0, len = categories.length; i < len; i++) { + record = categories[i]; + this.loadCategory(record); + } + posts = this.db.list("forum_posts"); + for (j = 0, len1 = posts.length; j < len1; j++) { + record = posts[j]; + this.loadPost(record); + } + replies = this.db.list("forum_replies"); + for (k = 0, len2 = replies.length; k < len2; k++) { + record = replies[k]; + this.loadReply(record); + } + this.updateActivity(); + }; + + Forum.prototype.index = function(language, text, target, uid) { + var indexer; + if (language == null) { + return; + } + indexer = this.indexers[language]; + if (indexer == null) { + this.indexers[language] = indexer = new Indexer(); + } + return indexer.add(text, target, uid); + }; + + Forum.prototype.loadCategory = function(record) { + var cat; + cat = new ForumCategory(this, record); + if ((cat.language != null) && !cat.deleted) { + if (this.languages[cat.language] == null) { + this.languages[cat.language] = []; + } + this.languages[cat.language].push(cat); + this.category_by_slug[cat.slug] = cat; + this.categories[cat.id] = cat; + this.category_count++; + } + return cat; + }; + + Forum.prototype.loadPost = function(record) { + var category, catid, post; + catid = record.getField("category"); + category = this.categories[catid]; + if (category != null) { + post = new ForumPost(category, record); + category.addPost(post); + this.posts[post.id] = post; + if (!post.deleted && !category.hidden) { + this.post_count++; + this.index(post.category.language, post.title + " " + post.category.name + " " + post.author.nick, post, post.id); + this.index(post.category.language, post.text, post, post.id); + } + return post; + } + }; + + Forum.prototype.loadReply = function(record) { + var post, postid, reply; + postid = record.getField("post"); + post = this.posts[postid]; + if (post != null) { + reply = new ForumReply(post, record); + post.addReply(reply); + this.replies[reply.id] = reply; + if (!reply.deleted && !post.category.hidden) { + this.reply_count++; + this.index(reply.post.category.language, reply.text, reply.post, post.id); + } + return reply; + } + }; + + Forum.prototype.createPost = function(category, user, title, text) { + var data, err, i, id, len, post, record, ref; + data = { + author: user, + category: category, + title: title, + text: text, + date: Date.now(), + activity: Date.now(), + edits: 0 + }; + record = this.db.create("forum_posts", data); + post = this.loadPost(record); + if (post != null) { + post.category.updateActivity(); + post.addWatch(post.author.id); + try { + ref = post.category.watch; + for (i = 0, len = ref.length; i < len; i++) { + id = ref[i]; + this.sendPostNotification(id, post); + } + } catch (error) { + err = error; + console.error(err); + } + } + return post; + }; + + Forum.prototype.createReply = function(post, user, text) { + var data, i, id, len, record, ref, reply; + data = { + post: post.id, + author: user, + text: text, + date: Date.now(), + activity: Date.now(), + edits: 0 + }; + post.updateActivity(); + record = this.db.create("forum_replies", data); + reply = this.loadReply(record); + ref = post.watch; + for (i = 0, len = ref.length; i < len; i++) { + id = ref[i]; + this.sendReplyNotification(id, reply); + } + return reply; + }; + + Forum.prototype.sendPostNotification = function(userid, post) { + var subject, text, translator, user; + user = this.content.users[userid]; + if (user == null) { + return; + } + if (user.email == null) { + return; + } + if (!user.flags.validated) { + return; + } + if (user === post.author) { + return; + } + translator = this.content.translator.getTranslator(user.language); + subject = translator.get("%USER% posted in %CATEGORY%").replace("%USER%", post.author.nick).replace("%CATEGORY%", post.category.name) + (" - " + (translator.get("microStudio Community"))); + text = translator.get("%USER% published a new post in %CATEGORY%:").replace("%USER%", post.author.nick).replace("%CATEGORY%", post.category.name) + "\n\n"; + text += post.title + "\n"; + text += "https://microstudio.dev" + (post.getPath()) + "\n\n"; + text += post.text.length < 500 ? post.text : post.text.substring(0, 500) + " (...)"; + return this.server.mailer.sendMail(user.email, subject, text); + }; + + Forum.prototype.sendReplyNotification = function(userid, reply) { + var subject, text, translator, user; + user = this.content.users[userid]; + if (user == null) { + return; + } + if (user.email == null) { + return; + } + if (!user.flags.validated) { + return; + } + if (user === reply.author) { + return; + } + translator = this.content.translator.getTranslator(user.language); + subject = translator.get("%USER% posted a reply").replace("%USER%", reply.author.nick) + (" - " + (translator.get("microStudio Community"))); + text = translator.get("%USER% posted a reply to %POST%:").replace("%USER%", reply.author.nick).replace("%POST%", reply.post.title) + "\n\n"; + text += "https://microstudio.dev" + (reply.post.getPath()) + (reply.post.replies.length - 1) + "/\n\n"; + text += reply.text.length < 500 ? reply.text : reply.text.substring(0, 500) + " (...)"; + return this.server.mailer.sendMail(user.email, subject, text); + }; + + Forum.prototype.createCategory = function(language, name, slug, description, hue, permissions) { + var cat, data, record; + data = { + language: language, + name: name, + slug: slug, + description: description, + hue: hue || 0, + permissions: permissions, + deleted: false, + activity: Date.now() + }; + record = this.db.create("forum_categories", data); + return cat = this.loadCategory(record); + }; + + Forum.prototype.editCategory = function(user, category, data) {}; + + Forum.prototype.editPost = function(user, post, data) {}; + + Forum.prototype.editReply = function(user, post, reply, data) {}; + + Forum.prototype.listCategories = function(lang) { + var c, cats, i, len, res; + cats = this.languages[lang]; + if (cats != null) { + res = []; + for (i = 0, len = cats.length; i < len; i++) { + c = cats[i]; + if (!c.deleted && !c.hidden) { + res.push(c); + } + } + res.sort(function(a, b) { + return b.activity - a.activity; + }); + return res; + } else { + return []; + } + }; + + Forum.prototype.updateActivity = function() { + var cat, key, ref; + this.activity = 0; + ref = this.categories; + for (key in ref) { + cat = ref[key]; + this.activity = Math.max(this.activity, cat.activity); + } + return this.sorted = false; + }; + + Forum.prototype.getLanguages = function() { + var key, lang; + lang = []; + for (key in this.languages) { + lang.push(key); + } + return lang; + }; + + Forum.prototype.getCategory = function(language, slug) { + var lang; + lang = this.languages[language]; + if (!lang) { + return null; + } + return lang[slug]; + }; + + Forum.prototype.escapeHTML = function(s) { + return s.replace(/&/g, '&').replace(/"/g, '"').replace(//g, '>'); + }; + + return Forum; + +})(); + +module.exports = this.Forum; diff --git a/server/forum/forumapp.coffee b/server/forum/forumapp.coffee new file mode 100644 index 00000000..54225a27 --- /dev/null +++ b/server/forum/forumapp.coffee @@ -0,0 +1,194 @@ +pug = require "pug" + +class @ForumApp + constructor:(@server,@webapp)-> + @app = @webapp.app + + @forum = @server.content.forum + + @getHue = (nick)-> + hue = 0 + for i in [0..nick.length-1] by 1 + hue += nick.charCodeAt(i)*5 + + hue%360 + + # main forum page + @app.get /^\/(..\/)?community\/?$/, (req,res)=> + return if @webapp.ensureDevArea(req,res) + + lang = "en" + for l in @webapp.languages + if req.path.startsWith("/#{l}/") + lang = l + + list = @forum.listCategories(lang) + + cats = [] + cats.push + name: @server.content.translator.get lang,"All" + slug: "all" + hue: 220 + path: "#{if lang != "en" then "/#{lang}" else ""}/community/" + + for c in list + cats.push c + + posts = [] + for c in list + ps = c.getSortedPosts() + for p in ps + if not p.deleted + posts.push p + + posts.sort (a,b)->b.activity-a.activity + + description = """ + #{@server.content.translator.get lang,"Welcome!"} + #{@server.content.translator.get lang,"In this space you can ask for help, share your best tips, read articles, submit bugs or feature requests, follow the roadmap and progress of microStudio."} + #{@server.content.translator.get lang,"You can discuss programming, game development, art, sound, music, useful tools and resources, microStudio and other game engines."} + #{@server.content.translator.get lang,"For anything else there is an Off Topic category."} + """ + + @categoryPage(req,res,lang,cats,"all",@server.content.translator.get(lang,"Community"),description,posts) + + # forum category + @app.get /^\/(..\/)?community\/([^\/\|\?\&\.]+)\/?$/, (req,res)=> + return if @webapp.ensureDevArea(req,res) + + lang = "en" + for l in @webapp.languages + if req.path.startsWith("/#{l}/") + lang = l + + path = req.path.split("/") + path.splice 0,1 + if path[0] != "community" + path.splice 0,1 + + category = path[1] + category = @forum.category_by_slug[category] + if category + list = @forum.listCategories(lang) + + cats = [] + cats.push category + + cats.push + name: @server.content.translator.get lang,"All" + slug: "all" + hue: 220 + path: "#{if lang != "en" then "/#{lang}" else ""}/community/" + + for c in list + if c != category + cats.push c + + list = category.getSortedPosts() + posts = [] + for p in list + if not p.deleted + posts.push p + + description = category.description + + @categoryPage(req,res,lang,cats,category.slug,category.name,description,posts,category.permissions) + else + @webapp.return404(req,res) + + # forum post + @app.get /^\/(..\/)?community\/([^\/\|\?\&\.]+)\/([^\/\|\?\&\.]+)\/\d+(\/\d+)?\/?$/, (req,res)=> + return if @webapp.ensureDevArea(req,res) + + lang = "en" + for l in @webapp.languages + if req.path.startsWith("/#{l}/") + lang = l + + path = req.path.split("/") + path.splice 0,1 + if path[0] != "community" + path.splice 0,1 + + category = path[1] + slug = path[2] + id = path[3] + scroll = path[4] + post = @forum.posts[id] + if post? and not post.deleted and not post.author.flags.censored and not post.author.flags.banned + cat = post.category + list = @forum.listCategories(lang) + + cats = [] + cats.push cat + + cats.push + name: @server.content.translator.get lang,"All" + slug: "all" + hue: 220 + path: "#{if lang != "en" then "/#{lang}" else ""}/community/" + + for c in list + if c != cat + cats.push c + + if not @post? or not @server.use_cache + @post = pug.compileFile "../templates/forum/post.pug" + + theme = req.cookies.theme or "light" + + res.send @post( + translator: @server.content.translator.getTranslator(lang) + language: lang + categories: cats + selected: category + post: post + scroll: scroll + getHue: @getHue + theme: theme + post_info: + id: post.id + slug: post.slug + category: category + permissions: post.category.permissions + post_permissions: post.permissions + url: "#{req.protocol}://#{req.hostname}/#{if lang == "en" then "" else "#{lang}/"}community/#{category}/#{slug}/#{id}/" + community: + language: lang + category: category + ) + + post.view(req.connection.remoteAddress) + else + @webapp.return404(req,res) + + + categoryPage:(req,res,lang,cats,selected,name,description,posts,permissions)-> + if not @category? or not @server.use_cache + @category = pug.compileFile "../templates/forum/forum.pug" + + langpath = if lang == "en" then "" else "/#{lang}" + + theme = req.cookies.theme or "light" + + res.send @category( + translator: @server.content.translator.getTranslator(lang) + language: lang + langpath: langpath + categories: cats + posts: posts + selected: selected + name: name + theme: theme + description: description + community: + language: lang + category: selected + languages: @forum.getLanguages() + permissions: permissions + + getHue: @getHue + ) + + +module.exports = @ForumApp diff --git a/server/forum/forumapp.js b/server/forum/forumapp.js new file mode 100644 index 00000000..aa59ad8e --- /dev/null +++ b/server/forum/forumapp.js @@ -0,0 +1,221 @@ +var pug; + +pug = require("pug"); + +this.ForumApp = (function() { + function ForumApp(server, webapp) { + this.server = server; + this.webapp = webapp; + this.app = this.webapp.app; + this.forum = this.server.content.forum; + this.getHue = function(nick) { + var hue, i, j, ref; + hue = 0; + for (i = j = 0, ref = nick.length - 1; j <= ref; i = j += 1) { + hue += nick.charCodeAt(i) * 5; + } + return hue % 360; + }; + this.app.get(/^\/(..\/)?community\/?$/, (function(_this) { + return function(req, res) { + var c, cats, description, j, k, l, lang, len, len1, len2, len3, list, m, n, p, posts, ps, ref; + if (_this.webapp.ensureDevArea(req, res)) { + return; + } + lang = "en"; + ref = _this.webapp.languages; + for (j = 0, len = ref.length; j < len; j++) { + l = ref[j]; + if (req.path.startsWith("/" + l + "/")) { + lang = l; + } + } + list = _this.forum.listCategories(lang); + cats = []; + cats.push({ + name: _this.server.content.translator.get(lang, "All"), + slug: "all", + hue: 220, + path: (lang !== "en" ? "/" + lang : "") + "/community/" + }); + for (k = 0, len1 = list.length; k < len1; k++) { + c = list[k]; + cats.push(c); + } + posts = []; + for (m = 0, len2 = list.length; m < len2; m++) { + c = list[m]; + ps = c.getSortedPosts(); + for (n = 0, len3 = ps.length; n < len3; n++) { + p = ps[n]; + if (!p.deleted) { + posts.push(p); + } + } + } + posts.sort(function(a, b) { + return b.activity - a.activity; + }); + description = (_this.server.content.translator.get(lang, "Welcome!")) + "\n" + (_this.server.content.translator.get(lang, "In this space you can ask for help, share your best tips, read articles, submit bugs or feature requests, follow the roadmap and progress of microStudio.")) + "\n" + (_this.server.content.translator.get(lang, "You can discuss programming, game development, art, sound, music, useful tools and resources, microStudio and other game engines.")) + "\n" + (_this.server.content.translator.get(lang, "For anything else there is an Off Topic category.")); + return _this.categoryPage(req, res, lang, cats, "all", _this.server.content.translator.get(lang, "Community"), description, posts); + }; + })(this)); + this.app.get(/^\/(..\/)?community\/([^\/\|\?\&\.]+)\/?$/, (function(_this) { + return function(req, res) { + var c, category, cats, description, j, k, l, lang, len, len1, len2, list, m, p, path, posts, ref; + if (_this.webapp.ensureDevArea(req, res)) { + return; + } + lang = "en"; + ref = _this.webapp.languages; + for (j = 0, len = ref.length; j < len; j++) { + l = ref[j]; + if (req.path.startsWith("/" + l + "/")) { + lang = l; + } + } + path = req.path.split("/"); + path.splice(0, 1); + if (path[0] !== "community") { + path.splice(0, 1); + } + category = path[1]; + category = _this.forum.category_by_slug[category]; + if (category) { + list = _this.forum.listCategories(lang); + cats = []; + cats.push(category); + cats.push({ + name: _this.server.content.translator.get(lang, "All"), + slug: "all", + hue: 220, + path: (lang !== "en" ? "/" + lang : "") + "/community/" + }); + for (k = 0, len1 = list.length; k < len1; k++) { + c = list[k]; + if (c !== category) { + cats.push(c); + } + } + list = category.getSortedPosts(); + posts = []; + for (m = 0, len2 = list.length; m < len2; m++) { + p = list[m]; + if (!p.deleted) { + posts.push(p); + } + } + description = category.description; + return _this.categoryPage(req, res, lang, cats, category.slug, category.name, description, posts, category.permissions); + } else { + return _this.webapp.return404(req, res); + } + }; + })(this)); + this.app.get(/^\/(..\/)?community\/([^\/\|\?\&\.]+)\/([^\/\|\?\&\.]+)\/\d+(\/\d+)?\/?$/, (function(_this) { + return function(req, res) { + var c, cat, category, cats, id, j, k, l, lang, len, len1, list, path, post, ref, scroll, slug, theme; + if (_this.webapp.ensureDevArea(req, res)) { + return; + } + lang = "en"; + ref = _this.webapp.languages; + for (j = 0, len = ref.length; j < len; j++) { + l = ref[j]; + if (req.path.startsWith("/" + l + "/")) { + lang = l; + } + } + path = req.path.split("/"); + path.splice(0, 1); + if (path[0] !== "community") { + path.splice(0, 1); + } + category = path[1]; + slug = path[2]; + id = path[3]; + scroll = path[4]; + post = _this.forum.posts[id]; + if ((post != null) && !post.deleted && !post.author.flags.censored && !post.author.flags.banned) { + cat = post.category; + list = _this.forum.listCategories(lang); + cats = []; + cats.push(cat); + cats.push({ + name: _this.server.content.translator.get(lang, "All"), + slug: "all", + hue: 220, + path: (lang !== "en" ? "/" + lang : "") + "/community/" + }); + for (k = 0, len1 = list.length; k < len1; k++) { + c = list[k]; + if (c !== cat) { + cats.push(c); + } + } + if ((_this.post == null) || !_this.server.use_cache) { + _this.post = pug.compileFile("../templates/forum/post.pug"); + } + theme = req.cookies.theme || "light"; + res.send(_this.post({ + translator: _this.server.content.translator.getTranslator(lang), + language: lang, + categories: cats, + selected: category, + post: post, + scroll: scroll, + getHue: _this.getHue, + theme: theme, + post_info: { + id: post.id, + slug: post.slug, + category: category, + permissions: post.category.permissions, + post_permissions: post.permissions, + url: req.protocol + "://" + req.hostname + "/" + (lang === "en" ? "" : lang + "/") + "community/" + category + "/" + slug + "/" + id + "/" + }, + community: { + language: lang, + category: category + } + })); + return post.view(req.connection.remoteAddress); + } else { + return _this.webapp.return404(req, res); + } + }; + })(this)); + } + + ForumApp.prototype.categoryPage = function(req, res, lang, cats, selected, name, description, posts, permissions) { + var langpath, theme; + if ((this.category == null) || !this.server.use_cache) { + this.category = pug.compileFile("../templates/forum/forum.pug"); + } + langpath = lang === "en" ? "" : "/" + lang; + theme = req.cookies.theme || "light"; + return res.send(this.category({ + translator: this.server.content.translator.getTranslator(lang), + language: lang, + langpath: langpath, + categories: cats, + posts: posts, + selected: selected, + name: name, + theme: theme, + description: description, + community: { + language: lang, + category: selected, + languages: this.forum.getLanguages(), + permissions: permissions + }, + getHue: this.getHue + })); + }; + + return ForumApp; + +})(); + +module.exports = this.ForumApp; diff --git a/server/forum/forumsession.coffee b/server/forum/forumsession.coffee new file mode 100644 index 00000000..4bf4e909 --- /dev/null +++ b/server/forum/forumsession.coffee @@ -0,0 +1,368 @@ +class @ForumSession + constructor:(@session)-> + @session.register "create_forum_category",(msg)=>@createForumCategory(msg) + @session.register "edit_forum_category",(msg)=>@editForumCategory(msg) + @session.register "create_forum_post",(msg)=>@createForumPost(msg) + @session.register "edit_forum_post",(msg)=>@editForumPost(msg) + @session.register "create_forum_reply",(msg)=>@createForumReply(msg) + @session.register "edit_forum_reply",(msg)=>@editForumReply(msg) + @session.register "get_raw_post",(msg)=>@getRawPost(msg) + @session.register "get_raw_reply",(msg)=>@getRawReply(msg) + @session.register "get_my_likes",(msg)=>@getMyLikes(msg) + @session.register "set_post_like",(msg)=>@setPostLike(msg) + @session.register "set_reply_like",(msg)=>@setReplyLike(msg) + @session.register "search_forum",(msg)=>@searchForum(msg) + @session.register "set_category_watch",(msg)=>@setCategoryWatch(msg) + @session.register "get_category_watch",(msg)=>@getCategoryWatch(msg) + @session.register "set_post_watch",(msg)=>@setPostWatch(msg) + @session.register "get_post_watch",(msg)=>@getPostWatch(msg) + + @forum = @session.server.content.forum + @server = @session.server + + @MAX_TEXT_LENGTH = 20000 + @MAX_TITLE_LENGTH = 400 + + createForumCategory:(data)-> + return if not @session.user? + return if not @session.user.flags.admin + + return if not data.language + return if not data.title + return if not data.slug + return if not data.description + + cat = @forum.createCategory(data.language,data.title,data.slug,data.description,data.hue,data.permissions) + + @session.send + name: "create_forum_category" + request_id: data.request_id + id: cat.id + + editForumCategory:(data)-> + return if not @session.user? + return if not @session.user.flags.admin + + return if not data.category? + + category = @forum.category_by_slug[data.category] + if category? + if data.deleted? + category.set "deleted",data.deleted + + if data.hidden? + category.set "hidden",data.hidden + + if data.description? + category.set "description",data.description + + if data.title? + category.set "name",data.title + + if data.hue? + category.set "hue",data.hue + + if data.permissions? + category.set "permissions",data.permissions + + @session.send + name: "edit_forum_category" + request_id: data.request_id + + createForumPost:(data)-> + return if not @session.user? + + return if not data.category? + return if not data.title? + return if not data.text? + + return if data.title.trim().length == 0 + return if data.text.trim().length == 0 + + if @session.user? and @session.user.flags.validated and not @session.user.flags.banned and not @session.user.flags.censored + return if not @server.rate_limiter.accept("create_forum_post",@session.user.id) + category = @forum.category_by_slug[data.category] + if category + if category.permissions.post != "user" + return if not @session.user.flags.admin + + post = @forum.createPost(category.id,@session.user.id,data.title,data.text) + @session.send + name: "create_forum_post" + request_id: data.request_id + id: post.id + + + editForumPost:(data)-> + return if not @session.user? + return if not @session.user.flags.validated + return if not data.post? + + post = @forum.posts[data.post] + if post? + return if @session.user != post.author and not @session.user.flags.admin + if data.text? + post.edit(data.text) + + if data.title? + post.setTitle(data.title) + + if data.progress? and @session.user.flags.admin + post.set "progress",data.progress + post.updateActivity() + + if data.status? and @session.user.flags.admin + post.set "status",data.status + post.updateActivity() + + if data.permissions? and @session.user.flags.admin + post.set "permissions",data.permissions + + if data.reverse? and @session.user.flags.admin + post.set "reverse",data.reverse + + if data.deleted? + post.set "deleted",data.deleted + + if data.category? and @session.user.flags.admin + cat1 = post.category + cat2 = @forum.category_by_slug[data.category] + if cat2? + post.setCategoryId(cat2.id) + post.category = cat2 + cat1.removePost(post) + cat2.addPost(post) + + @session.send + name: "edit_forum_post" + request_id: data.request_id + id: post.id + + createForumReply:(data)-> + return if not @session.user? + return if not data.post? + return if not data.text? + + return if data.text.trim().length == 0 + + if @session.user? and @session.user.flags.validated and not @session.user.flags.banned and not @session.user.flags.censored + return if not @server.rate_limiter.accept("create_forum_reply",@session.user.id) + + post = @forum.posts[data.post] + if post? + category = post.category + if category.permissions.reply != "user" + return if not @session.user.flags.admin + + if post.permissions.reply != "user" + return if not @session.user.flags.admin + + reply = @forum.createReply(post,@session.user.id,data.text) + @session.send + name: "create_forum_reply" + request_id: data.request_id + id: reply.id + index: reply.post.replies.length-1 + + + editForumReply:(data)-> + return if not @session.user? + return if not @session.user.flags.validated + return if not data.reply? + + reply = @forum.replies[data.reply] + if reply? + return if reply.author != @session.user and not @session.user.flags.admin + + if data.text? + reply.edit data.text + + if data.deleted? + reply.set "deleted",data.deleted + + @session.send + name: "edit_forum_reply" + request_id: data.request_id + + + getRawPost:(data)-> + return if not @session.user? + return if not data.post? + + post = @forum.posts[data.post] + if post? + return if @session.user != post.author and not @session.user.flags.admin + + @session.send + name: "get_raw_post" + request_id: data.request_id + text: post.text + progress: post.progress + status: post.status + + getRawReply:(data)-> + return if not @session.user? + return if not data.reply? + + reply = @forum.replies[data.reply] + if reply? + return if reply.author != @session.user and not @session.user.flags.admin + + @session.send + name: "get_raw_reply" + request_id: data.request_id + text: reply.text + + getMyLikes:(data)-> + return if not @session.user? + + if data.post? + post = @forum.posts[data.post] + if post? + res = + post: post.isLiked(@session.user.id) + replies: [] + + for r in post.replies + if r.isLiked(@session.user.id) + res.replies.push r.id + + @session.send + name: "get_my_likes" + request_id: data.request_id + data: res + else if data.category? + category = @forum.category_by_slug[data.category] + if category? + res = [] + for post in category.posts + if post.isLiked(@session.user.id) + res.push post.id + + @session.send + name: "get_my_likes" + request_id: data.request_id + data: res + else if data.language? + list = @forum.listCategories(data.language) + res = [] + for cat in list + for post in cat.posts + if post.isLiked(@session.user.id) + res.push post.id + + @session.send + name: "get_my_likes" + request_id: data.request_id + data: res + + setPostLike:(data)-> + return if not @session.user? + + if data.post? + post = @forum.posts[data.post] + if post? + if data.like + post.addLike @session.user.id + else + post.removeLike @session.user.id + + @session.send + name: "set_post_like" + request_id: data.request_id + likes: post.likes.length + + setReplyLike:(data)-> + return if not @session.user? + + if data.reply? + reply = @forum.replies[data.reply] + if reply? + if data.like + reply.addLike @session.user.id + else + reply.removeLike @session.user.id + + @session.send + name: "set_reply_like" + request_id: data.request_id + likes: reply.likes.length + + setCategoryWatch:(data)-> + return if not @session.user? + return if not data.category? + + category = @forum.category_by_slug[data.category] + if category + if data.watch + category.addWatch @session.user.id + else + category.removeWatch @session.user.id + + @session.send + name: "set_category_watch" + request_id: data.request_id + watch: data.watch + + setPostWatch:(data)-> + return if not @session.user? + return if not data.post? + + post = @forum.posts[data.post] + if post? + if data.watch + post.addWatch @session.user.id + else + post.removeWatch @session.user.id + + @session.send + name: "set_post_watch" + request_id: data.request_id + watch: data.watch + + getCategoryWatch:(data)-> + return if not @session.user? + return if not data.category? + + category = @forum.category_by_slug[data.category] + if category + @session.send + name: "get_category_watch" + request_id: data.request_id + watch: category.isWatching(@session.user.id) + + getPostWatch:(data)-> + return if not @session.user? + return if not data.post? + + post = @forum.posts[data.post] + if post? + @session.send + name: "get_post_watch" + request_id: data.request_id + watch: post.isWatching(@session.user.id) + + searchForum:(data)-> + return if not data.language? + return if not data.string? + + return if not @server.rate_limiter.accept("search_forum",@session.socket.remoteAddress) + + index = @forum.indexers[data.language] + if index? + res = index.search(data.string) + list = [] + for r in res + post = if r.target.post? then r.target.post else r.target + list.push + title: post.title + score: r.score + id: post.id + slug: post.slug + + @session.send + name: "search_forum" + request_id: data.request_id + results: list + +module.exports = @ForumSession diff --git a/server/forum/forumsession.js b/server/forum/forumsession.js new file mode 100644 index 00000000..1e796925 --- /dev/null +++ b/server/forum/forumsession.js @@ -0,0 +1,595 @@ +this.ForumSession = (function() { + function ForumSession(session) { + this.session = session; + this.session.register("create_forum_category", (function(_this) { + return function(msg) { + return _this.createForumCategory(msg); + }; + })(this)); + this.session.register("edit_forum_category", (function(_this) { + return function(msg) { + return _this.editForumCategory(msg); + }; + })(this)); + this.session.register("create_forum_post", (function(_this) { + return function(msg) { + return _this.createForumPost(msg); + }; + })(this)); + this.session.register("edit_forum_post", (function(_this) { + return function(msg) { + return _this.editForumPost(msg); + }; + })(this)); + this.session.register("create_forum_reply", (function(_this) { + return function(msg) { + return _this.createForumReply(msg); + }; + })(this)); + this.session.register("edit_forum_reply", (function(_this) { + return function(msg) { + return _this.editForumReply(msg); + }; + })(this)); + this.session.register("get_raw_post", (function(_this) { + return function(msg) { + return _this.getRawPost(msg); + }; + })(this)); + this.session.register("get_raw_reply", (function(_this) { + return function(msg) { + return _this.getRawReply(msg); + }; + })(this)); + this.session.register("get_my_likes", (function(_this) { + return function(msg) { + return _this.getMyLikes(msg); + }; + })(this)); + this.session.register("set_post_like", (function(_this) { + return function(msg) { + return _this.setPostLike(msg); + }; + })(this)); + this.session.register("set_reply_like", (function(_this) { + return function(msg) { + return _this.setReplyLike(msg); + }; + })(this)); + this.session.register("search_forum", (function(_this) { + return function(msg) { + return _this.searchForum(msg); + }; + })(this)); + this.session.register("set_category_watch", (function(_this) { + return function(msg) { + return _this.setCategoryWatch(msg); + }; + })(this)); + this.session.register("get_category_watch", (function(_this) { + return function(msg) { + return _this.getCategoryWatch(msg); + }; + })(this)); + this.session.register("set_post_watch", (function(_this) { + return function(msg) { + return _this.setPostWatch(msg); + }; + })(this)); + this.session.register("get_post_watch", (function(_this) { + return function(msg) { + return _this.getPostWatch(msg); + }; + })(this)); + this.forum = this.session.server.content.forum; + this.server = this.session.server; + this.MAX_TEXT_LENGTH = 20000; + this.MAX_TITLE_LENGTH = 400; + } + + ForumSession.prototype.createForumCategory = function(data) { + var cat; + if (this.session.user == null) { + return; + } + if (!this.session.user.flags.admin) { + return; + } + if (!data.language) { + return; + } + if (!data.title) { + return; + } + if (!data.slug) { + return; + } + if (!data.description) { + return; + } + cat = this.forum.createCategory(data.language, data.title, data.slug, data.description, data.hue, data.permissions); + return this.session.send({ + name: "create_forum_category", + request_id: data.request_id, + id: cat.id + }); + }; + + ForumSession.prototype.editForumCategory = function(data) { + var category; + if (this.session.user == null) { + return; + } + if (!this.session.user.flags.admin) { + return; + } + if (data.category == null) { + return; + } + category = this.forum.category_by_slug[data.category]; + if (category != null) { + if (data.deleted != null) { + category.set("deleted", data.deleted); + } + if (data.hidden != null) { + category.set("hidden", data.hidden); + } + if (data.description != null) { + category.set("description", data.description); + } + if (data.title != null) { + category.set("name", data.title); + } + if (data.hue != null) { + category.set("hue", data.hue); + } + if (data.permissions != null) { + category.set("permissions", data.permissions); + } + return this.session.send({ + name: "edit_forum_category", + request_id: data.request_id + }); + } + }; + + ForumSession.prototype.createForumPost = function(data) { + var category, post; + if (this.session.user == null) { + return; + } + if (data.category == null) { + return; + } + if (data.title == null) { + return; + } + if (data.text == null) { + return; + } + if (data.title.trim().length === 0) { + return; + } + if (data.text.trim().length === 0) { + return; + } + if ((this.session.user != null) && this.session.user.flags.validated && !this.session.user.flags.banned && !this.session.user.flags.censored) { + if (!this.server.rate_limiter.accept("create_forum_post", this.session.user.id)) { + return; + } + category = this.forum.category_by_slug[data.category]; + if (category) { + if (category.permissions.post !== "user") { + if (!this.session.user.flags.admin) { + return; + } + } + post = this.forum.createPost(category.id, this.session.user.id, data.title, data.text); + return this.session.send({ + name: "create_forum_post", + request_id: data.request_id, + id: post.id + }); + } + } + }; + + ForumSession.prototype.editForumPost = function(data) { + var cat1, cat2, post; + if (this.session.user == null) { + return; + } + if (!this.session.user.flags.validated) { + return; + } + if (data.post == null) { + return; + } + post = this.forum.posts[data.post]; + if (post != null) { + if (this.session.user !== post.author && !this.session.user.flags.admin) { + return; + } + if (data.text != null) { + post.edit(data.text); + } + if (data.title != null) { + post.setTitle(data.title); + } + if ((data.progress != null) && this.session.user.flags.admin) { + post.set("progress", data.progress); + post.updateActivity(); + } + if ((data.status != null) && this.session.user.flags.admin) { + post.set("status", data.status); + post.updateActivity(); + } + if ((data.permissions != null) && this.session.user.flags.admin) { + post.set("permissions", data.permissions); + } + if ((data.reverse != null) && this.session.user.flags.admin) { + post.set("reverse", data.reverse); + } + if (data.deleted != null) { + post.set("deleted", data.deleted); + } + if ((data.category != null) && this.session.user.flags.admin) { + cat1 = post.category; + cat2 = this.forum.category_by_slug[data.category]; + if (cat2 != null) { + post.setCategoryId(cat2.id); + post.category = cat2; + cat1.removePost(post); + cat2.addPost(post); + } + } + return this.session.send({ + name: "edit_forum_post", + request_id: data.request_id, + id: post.id + }); + } + }; + + ForumSession.prototype.createForumReply = function(data) { + var category, post, reply; + if (this.session.user == null) { + return; + } + if (data.post == null) { + return; + } + if (data.text == null) { + return; + } + if (data.text.trim().length === 0) { + return; + } + if ((this.session.user != null) && this.session.user.flags.validated && !this.session.user.flags.banned && !this.session.user.flags.censored) { + if (!this.server.rate_limiter.accept("create_forum_reply", this.session.user.id)) { + return; + } + post = this.forum.posts[data.post]; + if (post != null) { + category = post.category; + if (category.permissions.reply !== "user") { + if (!this.session.user.flags.admin) { + return; + } + } + if (post.permissions.reply !== "user") { + if (!this.session.user.flags.admin) { + return; + } + } + reply = this.forum.createReply(post, this.session.user.id, data.text); + return this.session.send({ + name: "create_forum_reply", + request_id: data.request_id, + id: reply.id, + index: reply.post.replies.length - 1 + }); + } + } + }; + + ForumSession.prototype.editForumReply = function(data) { + var reply; + if (this.session.user == null) { + return; + } + if (!this.session.user.flags.validated) { + return; + } + if (data.reply == null) { + return; + } + reply = this.forum.replies[data.reply]; + if (reply != null) { + if (reply.author !== this.session.user && !this.session.user.flags.admin) { + return; + } + if (data.text != null) { + reply.edit(data.text); + } + if (data.deleted != null) { + reply.set("deleted", data.deleted); + } + return this.session.send({ + name: "edit_forum_reply", + request_id: data.request_id + }); + } + }; + + ForumSession.prototype.getRawPost = function(data) { + var post; + if (this.session.user == null) { + return; + } + if (data.post == null) { + return; + } + post = this.forum.posts[data.post]; + if (post != null) { + if (this.session.user !== post.author && !this.session.user.flags.admin) { + return; + } + return this.session.send({ + name: "get_raw_post", + request_id: data.request_id, + text: post.text, + progress: post.progress, + status: post.status + }); + } + }; + + ForumSession.prototype.getRawReply = function(data) { + var reply; + if (this.session.user == null) { + return; + } + if (data.reply == null) { + return; + } + reply = this.forum.replies[data.reply]; + if (reply != null) { + if (reply.author !== this.session.user && !this.session.user.flags.admin) { + return; + } + return this.session.send({ + name: "get_raw_reply", + request_id: data.request_id, + text: reply.text + }); + } + }; + + ForumSession.prototype.getMyLikes = function(data) { + var cat, category, i, j, k, l, len, len1, len2, len3, list, post, r, ref, ref1, ref2, res; + if (this.session.user == null) { + return; + } + if (data.post != null) { + post = this.forum.posts[data.post]; + if (post != null) { + res = { + post: post.isLiked(this.session.user.id), + replies: [] + }; + ref = post.replies; + for (i = 0, len = ref.length; i < len; i++) { + r = ref[i]; + if (r.isLiked(this.session.user.id)) { + res.replies.push(r.id); + } + } + return this.session.send({ + name: "get_my_likes", + request_id: data.request_id, + data: res + }); + } + } else if (data.category != null) { + category = this.forum.category_by_slug[data.category]; + if (category != null) { + res = []; + ref1 = category.posts; + for (j = 0, len1 = ref1.length; j < len1; j++) { + post = ref1[j]; + if (post.isLiked(this.session.user.id)) { + res.push(post.id); + } + } + return this.session.send({ + name: "get_my_likes", + request_id: data.request_id, + data: res + }); + } + } else if (data.language != null) { + list = this.forum.listCategories(data.language); + res = []; + for (k = 0, len2 = list.length; k < len2; k++) { + cat = list[k]; + ref2 = cat.posts; + for (l = 0, len3 = ref2.length; l < len3; l++) { + post = ref2[l]; + if (post.isLiked(this.session.user.id)) { + res.push(post.id); + } + } + } + return this.session.send({ + name: "get_my_likes", + request_id: data.request_id, + data: res + }); + } + }; + + ForumSession.prototype.setPostLike = function(data) { + var post; + if (this.session.user == null) { + return; + } + if (data.post != null) { + post = this.forum.posts[data.post]; + if (post != null) { + if (data.like) { + post.addLike(this.session.user.id); + } else { + post.removeLike(this.session.user.id); + } + return this.session.send({ + name: "set_post_like", + request_id: data.request_id, + likes: post.likes.length + }); + } + } + }; + + ForumSession.prototype.setReplyLike = function(data) { + var reply; + if (this.session.user == null) { + return; + } + if (data.reply != null) { + reply = this.forum.replies[data.reply]; + if (reply != null) { + if (data.like) { + reply.addLike(this.session.user.id); + } else { + reply.removeLike(this.session.user.id); + } + return this.session.send({ + name: "set_reply_like", + request_id: data.request_id, + likes: reply.likes.length + }); + } + } + }; + + ForumSession.prototype.setCategoryWatch = function(data) { + var category; + if (this.session.user == null) { + return; + } + if (data.category == null) { + return; + } + category = this.forum.category_by_slug[data.category]; + if (category) { + if (data.watch) { + category.addWatch(this.session.user.id); + } else { + category.removeWatch(this.session.user.id); + } + return this.session.send({ + name: "set_category_watch", + request_id: data.request_id, + watch: data.watch + }); + } + }; + + ForumSession.prototype.setPostWatch = function(data) { + var post; + if (this.session.user == null) { + return; + } + if (data.post == null) { + return; + } + post = this.forum.posts[data.post]; + if (post != null) { + if (data.watch) { + post.addWatch(this.session.user.id); + } else { + post.removeWatch(this.session.user.id); + } + return this.session.send({ + name: "set_post_watch", + request_id: data.request_id, + watch: data.watch + }); + } + }; + + ForumSession.prototype.getCategoryWatch = function(data) { + var category; + if (this.session.user == null) { + return; + } + if (data.category == null) { + return; + } + category = this.forum.category_by_slug[data.category]; + if (category) { + return this.session.send({ + name: "get_category_watch", + request_id: data.request_id, + watch: category.isWatching(this.session.user.id) + }); + } + }; + + ForumSession.prototype.getPostWatch = function(data) { + var post; + if (this.session.user == null) { + return; + } + if (data.post == null) { + return; + } + post = this.forum.posts[data.post]; + if (post != null) { + return this.session.send({ + name: "get_post_watch", + request_id: data.request_id, + watch: post.isWatching(this.session.user.id) + }); + } + }; + + ForumSession.prototype.searchForum = function(data) { + var i, index, len, list, post, r, res; + if (data.language == null) { + return; + } + if (data.string == null) { + return; + } + if (!this.server.rate_limiter.accept("search_forum", this.session.socket.remoteAddress)) { + return; + } + index = this.forum.indexers[data.language]; + if (index != null) { + res = index.search(data.string); + list = []; + for (i = 0, len = res.length; i < len; i++) { + r = res[i]; + post = r.target.post != null ? r.target.post : r.target; + list.push({ + title: post.title, + score: r.score, + id: post.id, + slug: post.slug + }); + } + return this.session.send({ + name: "search_forum", + request_id: data.request_id, + results: list + }); + } + }; + + return ForumSession; + +})(); + +module.exports = this.ForumSession; diff --git a/server/forum/indexer.coffee b/server/forum/indexer.coffee new file mode 100644 index 00000000..cb372832 --- /dev/null +++ b/server/forum/indexer.coffee @@ -0,0 +1,80 @@ +class @Indexer + constructor:()-> + @queue = [] + @words = {} + @memory = 0 + @start() + + start:()-> + @stop() + @interval = setInterval (()=>@processOne()),1000 + + stop:()-> + if @interval? + clearInterval @interval + + add:(text,target,uid)-> + @queue.push + text: text + target: target + uid: uid + + processOne:()-> + try + if @queue.length>0 + job = @queue.splice(0,1)[0] + words = job.text.match(/\b(\w+)\b/g) + if words? + for word in words + @push word,job.target,job.uid + + if @queue.length == 0 + console.info "index size = #{@memory} ; words = #{Object.keys(@words).length}" + catch err + console.error err + + return + + push:(word,target,uid)-> + word = word.toLowerCase() + w = @words[word] + if not w? + @words[word] = w = {} + @memory += 8+word.length+40 + + if not w[uid]? + w[uid] = + score: 1 + target: target + @memory += 40 + else + w[uid].score += 1 + + search:(string)-> + rlist = [] + rtab = {} + words = string.toLowerCase().match(/\b(\w+)\b/g) + return rlist if not words? + for word in words + w = @words[word] + for uid,data of w + r = rtab[uid] + if not r? + r = + score: data.score + target: data.target + words: {} + r.words[word] = true + rtab[uid] = r + rlist.push r + else + r.score += data.score + r.words[word] = true + + for r in rlist + r.score += Object.keys(r.words).length*100 + + rlist.sort (a,b)->b.score-a.score + rlist + +module.exports = @Indexer diff --git a/server/forum/indexer.js b/server/forum/indexer.js new file mode 100644 index 00000000..076afb38 --- /dev/null +++ b/server/forum/indexer.js @@ -0,0 +1,116 @@ +this.Indexer = (function() { + function Indexer() { + this.queue = []; + this.words = {}; + this.memory = 0; + this.start(); + } + + Indexer.prototype.start = function() { + this.stop(); + return this.interval = setInterval(((function(_this) { + return function() { + return _this.processOne(); + }; + })(this)), 1000); + }; + + Indexer.prototype.stop = function() { + if (this.interval != null) { + return clearInterval(this.interval); + } + }; + + Indexer.prototype.add = function(text, target, uid) { + return this.queue.push({ + text: text, + target: target, + uid: uid + }); + }; + + Indexer.prototype.processOne = function() { + var err, i, job, len, word, words; + try { + if (this.queue.length > 0) { + job = this.queue.splice(0, 1)[0]; + words = job.text.match(/\b(\w+)\b/g); + if (words != null) { + for (i = 0, len = words.length; i < len; i++) { + word = words[i]; + this.push(word, job.target, job.uid); + } + } + if (this.queue.length === 0) { + console.info("index size = " + this.memory + " ; words = " + (Object.keys(this.words).length)); + } + } + } catch (error) { + err = error; + console.error(err); + } + }; + + Indexer.prototype.push = function(word, target, uid) { + var w; + word = word.toLowerCase(); + w = this.words[word]; + if (w == null) { + this.words[word] = w = {}; + this.memory += 8 + word.length + 40; + } + if (w[uid] == null) { + w[uid] = { + score: 1, + target: target + }; + return this.memory += 40; + } else { + return w[uid].score += 1; + } + }; + + Indexer.prototype.search = function(string) { + var data, i, j, len, len1, r, rlist, rtab, uid, w, word, words; + rlist = []; + rtab = {}; + words = string.toLowerCase().match(/\b(\w+)\b/g); + if (words == null) { + return rlist; + } + for (i = 0, len = words.length; i < len; i++) { + word = words[i]; + w = this.words[word]; + for (uid in w) { + data = w[uid]; + r = rtab[uid]; + if (r == null) { + r = { + score: data.score, + target: data.target, + words: {} + }; + r.words[word] = true; + rtab[uid] = r; + rlist.push(r); + } else { + r.score += data.score; + r.words[word] = true; + } + } + } + for (j = 0, len1 = rlist.length; j < len1; j++) { + r = rlist[j]; + r.score += Object.keys(r.words).length * 100; + } + rlist.sort(function(a, b) { + return b.score - a.score; + }); + return rlist; + }; + + return Indexer; + +})(); + +module.exports = this.Indexer; diff --git a/server/forum/post.coffee b/server/forum/post.coffee new file mode 100644 index 00000000..fe22b374 --- /dev/null +++ b/server/forum/post.coffee @@ -0,0 +1,145 @@ +marked = require "marked" +sanitizeHTML = require "sanitize-html" +allowedTags = sanitizeHTML.defaults.allowedTags.concat ["img"] + +class @ForumPost + constructor:(@category,@record)-> + data = @record.get() + + @id = data.id + @author = @category.forum.content.users[data.author] + + @title = data.title + @text = data.text + @media = data.media + @date = data.date + @activity = data.activity + @edits = data.edits + + @views = data.views or 0 + @progress = data.progress or 0 + @status = data.status + @deleted = data.deleted + @likes = data.likes or [] + @watch = data.watch or [] + @users_watching = [] + # @reactions = data.reactions or [] + @pinned = data.pinned + + @replies = [] + + @slug = @slugify @title + @people = [] + + @permissions = data.permissions or { + reply: "user" + } + + @reverse = data.reverse + + addReply:(reply)-> + @replies.push reply + if not reply.deleted + @addPeople reply.author + + addPeople:(author)-> + @sort_people = true + for p in @people + if p.author == author + p.replies += 1 + return + @people.push + author: author + replies: 1 + + getPeople:()-> + if @sort_people + @sort_people = false + @people.sort (a,b)->a.replies-b.replies + + @people + + set:(prop,value)-> + data = @record.get() + data[prop] = value + @record.set(data) + @[prop] = value + + setCategoryId:(id)-> + data = @record.get() + data["category"] = id + @record.set(data) + + edit:(text)-> + @set "text",text + @set "edits",@edits+1 + @updateActivity() + + setTitle:(title)-> + @set "title",title + @slug = @slugify @title + + addLike:(id)-> + index = @likes.indexOf(id) + if index<0 + @likes.push id + @set "likes",@likes + return @likes.length + + removeLike:(id)-> + index = @likes.indexOf(id) + if index>=0 + @likes.splice index,1 + @set "likes",@likes + return @likes.length + + isLiked:(id)-> + @likes.indexOf(id)>=0 + + addWatch:(id)-> + index = @watch.indexOf(id) + if index<0 + @watch.push id + @set "watch",@watch + + removeWatch:(id)-> + index = @watch.indexOf(id) + if index>=0 + @watch.splice index,1 + @set "watch",@watch + + isWatching:(id)-> + @watch.indexOf(id)>=0 + + view:(ip)-> + return if not ip? + + if not @views_buffer? or Date.now()>@views_buffer_expiration + @views_buffer_expiration = Date.now()+2*60*60*1000 # one view per 2 hours per ip + @views_buffer = {} + + return if @views_buffer[ip] + + @views_buffer[ip] = true + @set "views",@views+1 + + updateActivity:()-> + @set "activity",Date.now() + @category.updateActivity() + + getPath:()-> + path = "/" + if @category.language != "en" + path += "#{@category.language}/" + path += "community/#{@category.slug}/" + path += "#{@slug}/#{@id}/" + + slugify:(text)-> + res = text.normalize('NFD').replace(/[\s]/g, "-").replace(/[^a-zA-Z0-9_-]/g, "").toLowerCase() + res = "_" if res.length == 0 + res + + getHTMLText:()-> + sanitizeHTML marked(@text),{allowedTags:allowedTags} + +module.exports = @ForumPost diff --git a/server/forum/post.js b/server/forum/post.js new file mode 100644 index 00000000..aba58bcf --- /dev/null +++ b/server/forum/post.js @@ -0,0 +1,195 @@ +var allowedTags, marked, sanitizeHTML; + +marked = require("marked"); + +sanitizeHTML = require("sanitize-html"); + +allowedTags = sanitizeHTML.defaults.allowedTags.concat(["img"]); + +this.ForumPost = (function() { + function ForumPost(category, record) { + var data; + this.category = category; + this.record = record; + data = this.record.get(); + this.id = data.id; + this.author = this.category.forum.content.users[data.author]; + this.title = data.title; + this.text = data.text; + this.media = data.media; + this.date = data.date; + this.activity = data.activity; + this.edits = data.edits; + this.views = data.views || 0; + this.progress = data.progress || 0; + this.status = data.status; + this.deleted = data.deleted; + this.likes = data.likes || []; + this.watch = data.watch || []; + this.users_watching = []; + this.pinned = data.pinned; + this.replies = []; + this.slug = this.slugify(this.title); + this.people = []; + this.permissions = data.permissions || { + reply: "user" + }; + this.reverse = data.reverse; + } + + ForumPost.prototype.addReply = function(reply) { + this.replies.push(reply); + if (!reply.deleted) { + return this.addPeople(reply.author); + } + }; + + ForumPost.prototype.addPeople = function(author) { + var i, len, p, ref; + this.sort_people = true; + ref = this.people; + for (i = 0, len = ref.length; i < len; i++) { + p = ref[i]; + if (p.author === author) { + p.replies += 1; + return; + } + } + return this.people.push({ + author: author, + replies: 1 + }); + }; + + ForumPost.prototype.getPeople = function() { + if (this.sort_people) { + this.sort_people = false; + this.people.sort(function(a, b) { + return a.replies - b.replies; + }); + } + return this.people; + }; + + ForumPost.prototype.set = function(prop, value) { + var data; + data = this.record.get(); + data[prop] = value; + this.record.set(data); + return this[prop] = value; + }; + + ForumPost.prototype.setCategoryId = function(id) { + var data; + data = this.record.get(); + data["category"] = id; + return this.record.set(data); + }; + + ForumPost.prototype.edit = function(text) { + this.set("text", text); + this.set("edits", this.edits + 1); + return this.updateActivity(); + }; + + ForumPost.prototype.setTitle = function(title) { + this.set("title", title); + return this.slug = this.slugify(this.title); + }; + + ForumPost.prototype.addLike = function(id) { + var index; + index = this.likes.indexOf(id); + if (index < 0) { + this.likes.push(id); + this.set("likes", this.likes); + } + return this.likes.length; + }; + + ForumPost.prototype.removeLike = function(id) { + var index; + index = this.likes.indexOf(id); + if (index >= 0) { + this.likes.splice(index, 1); + this.set("likes", this.likes); + } + return this.likes.length; + }; + + ForumPost.prototype.isLiked = function(id) { + return this.likes.indexOf(id) >= 0; + }; + + ForumPost.prototype.addWatch = function(id) { + var index; + index = this.watch.indexOf(id); + if (index < 0) { + this.watch.push(id); + return this.set("watch", this.watch); + } + }; + + ForumPost.prototype.removeWatch = function(id) { + var index; + index = this.watch.indexOf(id); + if (index >= 0) { + this.watch.splice(index, 1); + return this.set("watch", this.watch); + } + }; + + ForumPost.prototype.isWatching = function(id) { + return this.watch.indexOf(id) >= 0; + }; + + ForumPost.prototype.view = function(ip) { + if (ip == null) { + return; + } + if ((this.views_buffer == null) || Date.now() > this.views_buffer_expiration) { + this.views_buffer_expiration = Date.now() + 2 * 60 * 60 * 1000; + this.views_buffer = {}; + } + if (this.views_buffer[ip]) { + return; + } + this.views_buffer[ip] = true; + return this.set("views", this.views + 1); + }; + + ForumPost.prototype.updateActivity = function() { + this.set("activity", Date.now()); + return this.category.updateActivity(); + }; + + ForumPost.prototype.getPath = function() { + var path; + path = "/"; + if (this.category.language !== "en") { + path += this.category.language + "/"; + } + path += "community/" + this.category.slug + "/"; + return path += this.slug + "/" + this.id + "/"; + }; + + ForumPost.prototype.slugify = function(text) { + var res; + res = text.normalize('NFD').replace(/[\s]/g, "-").replace(/[^a-zA-Z0-9_-]/g, "").toLowerCase(); + if (res.length === 0) { + res = "_"; + } + return res; + }; + + ForumPost.prototype.getHTMLText = function() { + return sanitizeHTML(marked(this.text), { + allowedTags: allowedTags + }); + }; + + return ForumPost; + +})(); + +module.exports = this.ForumPost; diff --git a/server/forum/reply.coffee b/server/forum/reply.coffee new file mode 100644 index 00000000..4d079e82 --- /dev/null +++ b/server/forum/reply.coffee @@ -0,0 +1,57 @@ +marked = require "marked" +sanitizeHTML = require "sanitize-html" +allowedTags = sanitizeHTML.defaults.allowedTags.concat ["img"] + +class @ForumReply + constructor:(@post,@record)-> + data = @record.get() + + @id = data.id + @author = @post.category.forum.content.users[data.author] + + @text = data.text + @date = data.date + @edits = data.edits or 0 + @activity = data.activity + @media = data.media + + @deleted = data.deleted + @likes = data.likes or [] + @pinned = data.pinned + + set:(prop,value)-> + data = @record.get() + data[prop] = value + @record.set(data) + @[prop] = value + + edit:(text)-> + @set "text",text + @set "edits",@edits+1 + @set "activity",Date.now() + + addLike:(id)-> + index = @likes.indexOf(id) + if index<0 + @likes.push id + @set "likes",@likes + return @likes.length + + removeLike:(id)-> + index = @likes.indexOf(id) + if index>=0 + @likes.splice index,1 + @set "likes",@likes + return @likes.length + + isLiked:(id)-> + @likes.indexOf(id)>=0 + + updateActivity:()-> + @set "activity",Date.now() + @post.updateActivity() + + getHTMLText:()-> + sanitizeHTML marked(@text),{allowedTags:allowedTags} + +module.exports = @ForumReply diff --git a/server/forum/reply.js b/server/forum/reply.js new file mode 100644 index 00000000..0ac5fcc3 --- /dev/null +++ b/server/forum/reply.js @@ -0,0 +1,80 @@ +var allowedTags, marked, sanitizeHTML; + +marked = require("marked"); + +sanitizeHTML = require("sanitize-html"); + +allowedTags = sanitizeHTML.defaults.allowedTags.concat(["img"]); + +this.ForumReply = (function() { + function ForumReply(post, record) { + var data; + this.post = post; + this.record = record; + data = this.record.get(); + this.id = data.id; + this.author = this.post.category.forum.content.users[data.author]; + this.text = data.text; + this.date = data.date; + this.edits = data.edits || 0; + this.activity = data.activity; + this.media = data.media; + this.deleted = data.deleted; + this.likes = data.likes || []; + this.pinned = data.pinned; + } + + ForumReply.prototype.set = function(prop, value) { + var data; + data = this.record.get(); + data[prop] = value; + this.record.set(data); + return this[prop] = value; + }; + + ForumReply.prototype.edit = function(text) { + this.set("text", text); + this.set("edits", this.edits + 1); + return this.set("activity", Date.now()); + }; + + ForumReply.prototype.addLike = function(id) { + var index; + index = this.likes.indexOf(id); + if (index < 0) { + this.likes.push(id); + this.set("likes", this.likes); + } + return this.likes.length; + }; + + ForumReply.prototype.removeLike = function(id) { + var index; + index = this.likes.indexOf(id); + if (index >= 0) { + this.likes.splice(index, 1); + this.set("likes", this.likes); + } + return this.likes.length; + }; + + ForumReply.prototype.isLiked = function(id) { + return this.likes.indexOf(id) >= 0; + }; + + ForumReply.prototype.updateActivity = function() { + this.set("activity", Date.now()); + return this.post.updateActivity(); + }; + + ForumReply.prototype.getHTMLText = function() { + return sanitizeHTML(marked(this.text), { + allowedTags: allowedTags + }); + }; + + return ForumReply; + +})(); + +module.exports = this.ForumReply; diff --git a/server/gamify/achievements.coffee b/server/gamify/achievements.coffee new file mode 100644 index 00000000..3c952526 --- /dev/null +++ b/server/gamify/achievements.coffee @@ -0,0 +1,44 @@ + +# describes automatic achievements based on stats +@Achievement = class + constructor:(@id,@name,@description,@stat,@value,@xp)-> + +@Achievements = new class + constructor:()-> + @list = [] + @by_stat = {} + + add:(a)-> + @list.push a + by_stat = @by_stat[a.stat] + if not by_stat? + by_stat = @by_stat[a.stat] = [] + + by_stat.push a + @by_id[a.id] = a + +@Achievements.add new @Achievement "code_rookie","Code Rookie","Spent one hour coding","time_coding",60,1000 +@Achievements.add new @Achievement "code_explorer","Code Explorer","Spent 10 hours coding","time_coding",600,5000 +@Achievements.add new @Achievement "code_hero","Code Hero","Spent 100 hours coding","time_coding",6000,25000 +@Achievements.add new @Achievement "code_boss","Boss of Code","Spent 1000 hours coding","time_coding",60000,100000 + + +# Abstract Artist +# Impressionist +# Romantic Artist +# Realist Artist +# Classic Artist + +@Achievements.add new @Achievement "abstract_artist","Abstract Artist","Spent one hour painting squares","time_drawing",60,1000 +@Achievements.add new @Achievement "impressionist","Impressionist","Spent ten hours painting dots","time_drawing",600,3000 +@Achievements.add new @Achievement "romantic","Romantic Artist","Spent hundred hours creating art","time_drawing",600,3000 +@Achievements.add new @Achievement "impressionist","Impressionist","Spent ten hours painting dots","time_drawing",600,3000 + + + +@Achievements.add new @Achievement "gd_junior","Level Design Junior","Spent one hour creating maps","time_mapping",60,1000 +@Achievements.add new @Achievement "gd_junior","Level Design Holder","Spent 10 hours creating maps","time_mapping",600,3000 +@Achievements.add new @Achievement "gd_junior","Level Design Master","Spent 100 hours creating maps","time_mapping",6000,10000 +@Achievements.add new @Achievement "gd_junior","Level Design Expert","Spent 1000 hours creating maps","time_mapping",60000,100000 + +module.exports = @Achievements diff --git a/server/gamify/achievements.js b/server/gamify/achievements.js new file mode 100644 index 00000000..71a27b50 --- /dev/null +++ b/server/gamify/achievements.js @@ -0,0 +1,60 @@ +this.Achievement = (function() { + function _Class(id, name, description, stat, value, xp) { + this.id = id; + this.name = name; + this.description = description; + this.stat = stat; + this.value = value; + this.xp = xp; + } + + return _Class; + +})(); + +this.Achievements = new ((function() { + function _Class() { + this.list = []; + this.by_stat = {}; + } + + _Class.prototype.add = function(a) { + var by_stat; + this.list.push(a); + by_stat = this.by_stat[a.stat]; + if (by_stat == null) { + by_stat = this.by_stat[a.stat] = []; + } + by_stat.push(a); + return this.by_id[a.id] = a; + }; + + return _Class; + +})()); + +this.Achievements.add(new this.Achievement("code_rookie", "Code Rookie", "Spent one hour coding", "time_coding", 60, 1000)); + +this.Achievements.add(new this.Achievement("code_explorer", "Code Explorer", "Spent 10 hours coding", "time_coding", 600, 5000)); + +this.Achievements.add(new this.Achievement("code_hero", "Code Hero", "Spent 100 hours coding", "time_coding", 6000, 25000)); + +this.Achievements.add(new this.Achievement("code_boss", "Boss of Code", "Spent 1000 hours coding", "time_coding", 60000, 100000)); + +this.Achievements.add(new this.Achievement("abstract_artist", "Abstract Artist", "Spent one hour painting squares", "time_drawing", 60, 1000)); + +this.Achievements.add(new this.Achievement("impressionist", "Impressionist", "Spent ten hours painting dots", "time_drawing", 600, 3000)); + +this.Achievements.add(new this.Achievement("romantic", "Romantic Artist", "Spent hundred hours creating art", "time_drawing", 600, 3000)); + +this.Achievements.add(new this.Achievement("impressionist", "Impressionist", "Spent ten hours painting dots", "time_drawing", 600, 3000)); + +this.Achievements.add(new this.Achievement("gd_junior", "Level Design Junior", "Spent one hour creating maps", "time_mapping", 60, 1000)); + +this.Achievements.add(new this.Achievement("gd_junior", "Level Design Holder", "Spent 10 hours creating maps", "time_mapping", 600, 3000)); + +this.Achievements.add(new this.Achievement("gd_junior", "Level Design Master", "Spent 100 hours creating maps", "time_mapping", 6000, 10000)); + +this.Achievements.add(new this.Achievement("gd_junior", "Level Design Expert", "Spent 1000 hours creating maps", "time_mapping", 60000, 100000)); + +module.exports = this.Achievements; diff --git a/server/gamify/levels.coffee b/server/gamify/levels.coffee new file mode 100644 index 00000000..b45ee067 --- /dev/null +++ b/server/gamify/levels.coffee @@ -0,0 +1,15 @@ +@Levels = class + constructor:()-> + @total_cost = [] + sum = 0 + for i in [0..499] by 1 + @total_cost[i] = sum + console.info(i+" => "+sum) + sum += @costOfLevelUp(i) + + #for i in [0..499] by 1 + + costOfLevelUp:(from_level)-> + xp = (from_level+5)*(from_level+5)*20 + +module.exports = new @Levels() diff --git a/server/gamify/levels.js b/server/gamify/levels.js new file mode 100644 index 00000000..240c02be --- /dev/null +++ b/server/gamify/levels.js @@ -0,0 +1,22 @@ +this.Levels = (function() { + function _Class() { + var i, j, sum; + this.total_cost = []; + sum = 0; + for (i = j = 0; j <= 499; i = j += 1) { + this.total_cost[i] = sum; + console.info(i + " => " + sum); + sum += this.costOfLevelUp(i); + } + } + + _Class.prototype.costOfLevelUp = function(from_level) { + var xp; + return xp = (from_level + 5) * (from_level + 5) * 20; + }; + + return _Class; + +})(); + +module.exports = new this.Levels(); diff --git a/server/gamify/userprogress.coffee b/server/gamify/userprogress.coffee new file mode 100644 index 00000000..044ff708 --- /dev/null +++ b/server/gamify/userprogress.coffee @@ -0,0 +1,63 @@ +Levels = require __dirname+"/levels.js" +Achievements = require __dirname+"/achievements.js" + +class @UserProgress + constructor:(@user,data)-> + @stats = data.stats or { xp: 0 , level: 0 } + @achievements = data.achievements or {} + + @time_ids = {} + + recordTime:(id)-> + t = Math.floor(Date.now()/60000) + + return if t == @time_ids[id] + @time_ids[id] = t + @incrementStat(id,1,false) + @incrementStat("xp",60) + + unlockAchievement:(id,name,description,xp=0)-> + if not @hasAchievement(id) + @achievements[id] = + id: id + name: name + description: description + xp: xp + + @incrementStat("xp",xp) if xp>0 + + @saveAchievements() + + hasAchievement:(id)-> + @achievements[id]? + + incrementStat:(id,value,save=true)-> + if @stats[id]? + @stats[id] += value + else + @stats[id] = value + + if id == "xp" + while @stats.xp>=Levels.total_cost[@stats.level] + @stats.level += 1 + + achievements = Achievements.by_stat[id] + if achievements? + for a in achievements + if @stats[id] >= a.value and not @hasAchievement(a.id) + @unlockAchievement a.id,a.name,a.description,a.xp + + @saveStats() if save + + saveStats:()-> + @user.record.set "stats",@stats + + saveAchievements:()-> + @user.record.set "achievements",@achievements + + get:()-> + return + stats: @stats + achievements: @achievements + +module.exports = @UserProgress diff --git a/server/gamify/userprogress.js b/server/gamify/userprogress.js new file mode 100644 index 00000000..04369f0a --- /dev/null +++ b/server/gamify/userprogress.js @@ -0,0 +1,99 @@ +var Achievements, Levels; + +Levels = require(__dirname + "/levels.js"); + +Achievements = require(__dirname + "/achievements.js"); + +this.UserProgress = (function() { + function UserProgress(user, data) { + this.user = user; + this.stats = data.stats || { + xp: 0, + level: 0 + }; + this.achievements = data.achievements || {}; + this.time_ids = {}; + } + + UserProgress.prototype.recordTime = function(id) { + var t; + t = Math.floor(Date.now() / 60000); + if (t === this.time_ids[id]) { + return; + } + this.time_ids[id] = t; + this.incrementStat(id, 1, false); + return this.incrementStat("xp", 60); + }; + + UserProgress.prototype.unlockAchievement = function(id, name, description, xp) { + if (xp == null) { + xp = 0; + } + if (!this.hasAchievement(id)) { + this.achievements[id] = { + id: id, + name: name, + description: description, + xp: xp + }; + if (xp > 0) { + this.incrementStat("xp", xp); + } + return this.saveAchievements(); + } + }; + + UserProgress.prototype.hasAchievement = function(id) { + return this.achievements[id] != null; + }; + + UserProgress.prototype.incrementStat = function(id, value, save) { + var a, achievements, i, len; + if (save == null) { + save = true; + } + if (this.stats[id] != null) { + this.stats[id] += value; + } else { + this.stats[id] = value; + } + if (id === "xp") { + while (this.stats.xp >= Levels.total_cost[this.stats.level]) { + this.stats.level += 1; + } + } + achievements = Achievements.by_stat[id]; + if (achievements != null) { + for (i = 0, len = achievements.length; i < len; i++) { + a = achievements[i]; + if (this.stats[id] >= a.value && !this.hasAchievement(a.id)) { + this.unlockAchievement(a.id, a.name, a.description, a.xp); + } + } + } + if (save) { + return this.saveStats(); + } + }; + + UserProgress.prototype.saveStats = function() { + return this.user.record.set("stats", this.stats); + }; + + UserProgress.prototype.saveAchievements = function() { + return this.user.record.set("achievements", this.achievements); + }; + + UserProgress.prototype.get = function() { + return { + stats: this.stats, + achievements: this.achievements + }; + }; + + return UserProgress; + +})(); + +module.exports = this.UserProgress; diff --git a/server/package-lock.json b/server/package-lock.json new file mode 100644 index 00000000..ffc44345 --- /dev/null +++ b/server/package-lock.json @@ -0,0 +1,2315 @@ +{ + "name": "sonicpad-server", + "version": "1.0.0", + "lockfileVersion": 1, + "requires": true, + "dependencies": { + "@fontsource/source-sans-pro": { + "version": "4.4.5", + "resolved": "https://registry.npmjs.org/@fontsource/source-sans-pro/-/source-sans-pro-4.4.5.tgz", + "integrity": "sha512-WMAz713kgSnI4Re+6h4oBcmcl3ogq2RDLN8yZhP8GRXgR1wCEPZQKo6VC26ifofxdFu1kft0N3fYszQfKwJH0Q==" + }, + "@fontsource/ubuntu": { + "version": "4.4.5", + "resolved": "https://registry.npmjs.org/@fontsource/ubuntu/-/ubuntu-4.4.5.tgz", + "integrity": "sha512-dK/WdBncYJAZrwhANBGYN7wKrB2+NsYX/QomNBkJuze9kel9uijz7cNQPYdUDogink/X8D4MF2PeLEhF565fSA==" + }, + "@fontsource/ubuntu-mono": { + "version": "4.4.5", + "resolved": "https://registry.npmjs.org/@fontsource/ubuntu-mono/-/ubuntu-mono-4.4.5.tgz", + "integrity": "sha512-TvkybOBDDfZwrIE9imcoMMi4xhdZY+0Q1gmmdrGCG2xFxPM2/fuHXBcn6FKLvm5/regKKq93DO6JIPcH2G1r/w==" + }, + "@fortawesome/fontawesome-free": { + "version": "5.15.3", + "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-free/-/fontawesome-free-5.15.3.tgz", + "integrity": "sha512-rFnSUN/QOtnOAgqFRooTA3H57JLDm0QEG/jPdk+tLQNL/eWd+Aok8g3qCI+Q1xuDPWpGW/i9JySpJVsq8Q0s9w==" + }, + "@greenlock/manager": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@greenlock/manager/-/manager-3.1.0.tgz", + "integrity": "sha512-PBy5CMK+j4oD7sj7hF5qE+xKEOSiiuL2hHd5X5ttEbtnTSDKjNeqbrR5k2ZddwVNdjOVeBIeuqlm81IFZ+Ftew==", + "requires": { + "greenlock-manager-fs": "^3.1.0" + } + }, + "@mapbox/node-pre-gyp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@mapbox/node-pre-gyp/-/node-pre-gyp-1.0.5.tgz", + "integrity": "sha512-4srsKPXWlIxp5Vbqz5uLfBN+du2fJChBoYn/f2h991WLdk7jUvcSk/McVLSv/X+xQIPI8eGD5GjrnygdyHnhPA==", + "requires": { + "detect-libc": "^1.0.3", + "https-proxy-agent": "^5.0.0", + "make-dir": "^3.1.0", + "node-fetch": "^2.6.1", + "nopt": "^5.0.0", + "npmlog": "^4.1.2", + "rimraf": "^3.0.2", + "semver": "^7.3.4", + "tar": "^6.1.0" + } + }, + "@root/acme": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@root/acme/-/acme-3.1.0.tgz", + "integrity": "sha512-GAyaW63cpSYd2KvVp5lHLbCWeEhJPKZK9nsJvZJOKsD9Uv88KEttn4FpDZEJ+2q3Jsey0DWpuQ2I4ft0JV9p2w==", + "requires": { + "@root/csr": "^0.8.1", + "@root/encoding": "^1.0.1", + "@root/keypairs": "^0.10.0", + "@root/pem": "^1.0.4", + "@root/request": "^1.6.1", + "@root/x509": "^0.7.2" + } + }, + "@root/asn1": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@root/asn1/-/asn1-1.0.0.tgz", + "integrity": "sha512-0lfZNuOULKJDJmdIkP8V9RnbV3XaK6PAHD3swnFy4tZwtlMDzLKoM/dfNad7ut8Hu3r91wy9uK0WA/9zym5mig==", + "requires": { + "@root/encoding": "^1.0.1" + } + }, + "@root/csr": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@root/csr/-/csr-0.8.1.tgz", + "integrity": "sha512-hKl0VuE549TK6SnS2Yn9nRvKbFZXn/oAg+dZJU/tlKl/f/0yRXeuUzf8akg3JjtJq+9E592zDqeXZ7yyrg8fSQ==", + "requires": { + "@root/asn1": "^1.0.0", + "@root/pem": "^1.0.4", + "@root/x509": "^0.7.2" + } + }, + "@root/encoding": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@root/encoding/-/encoding-1.0.1.tgz", + "integrity": "sha512-OaEub02ufoU038gy6bsNHQOjIn8nUjGiLcaRmJ40IUykneJkIW5fxDqKxQx48cszuNflYldsJLPPXCrGfHs8yQ==" + }, + "@root/greenlock": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/@root/greenlock/-/greenlock-4.0.5.tgz", + "integrity": "sha512-KR9w3mYE9aH33FCibI8oSYBQV+f7lc3MVPdZ9nxY2tqRLmJp05cMOMz340mtG14VnWDuznLj4TbBj3sHIuoQPQ==", + "requires": { + "@greenlock/manager": "^3.1.0", + "@root/acme": "^3.1.0", + "@root/csr": "^0.8.1", + "@root/keypairs": "^0.10.0", + "@root/mkdirp": "^1.0.0", + "@root/request": "^1.6.1", + "acme-http-01-standalone": "^3.0.5", + "cert-info": "^1.5.1", + "greenlock-store-fs": "^3.2.2", + "safe-replace": "^1.1.0" + } + }, + "@root/greenlock-express": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@root/greenlock-express/-/greenlock-express-4.0.4.tgz", + "integrity": "sha512-a0/9tHNQwt5Uyod7Mp59lo5UwrjdpOaRgaw8oKvDopVvrjsk1Urdhz9MoLh5p4APt7KhAI/hMFSG4gvczgSwaA==", + "requires": { + "@root/greenlock": "^4.0.5", + "redirect-https": "^1.3.1" + } + }, + "@root/keypairs": { + "version": "0.10.1", + "resolved": "https://registry.npmjs.org/@root/keypairs/-/keypairs-0.10.1.tgz", + "integrity": "sha512-LuBheiQwMLCN5iWmhjyW9Mgc7uX16+gABSDfFafaBQnSPVylnOksHa22bqZmDkMCsinOOjUecgy2UgREOT3cOg==", + "requires": { + "@root/encoding": "^1.0.1", + "@root/pem": "^1.0.4", + "@root/x509": "^0.7.2" + } + }, + "@root/mkdirp": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@root/mkdirp/-/mkdirp-1.0.0.tgz", + "integrity": "sha512-hxGAYUx5029VggfG+U9naAhQkoMSXtOeXtbql97m3Hi6/sQSRL/4khKZPyOF6w11glyCOU38WCNLu9nUcSjOfA==" + }, + "@root/pem": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@root/pem/-/pem-1.0.4.tgz", + "integrity": "sha512-rEUDiUsHtild8GfIjFE9wXtcVxeS+ehCJQBwbQQ3IVfORKHK93CFnRtkr69R75lZFjcmKYVc+AXDB+AeRFOULA==" + }, + "@root/request": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/@root/request/-/request-1.7.0.tgz", + "integrity": "sha512-lre7XVeEwszgyrayWWb/kRn5fuJfa+n0Nh+rflM9E+EpC28yIYA+FPm/OL1uhzp3TxhQM0HFN4FE2RDIPGlnmg==" + }, + "@root/x509": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/@root/x509/-/x509-0.7.2.tgz", + "integrity": "sha512-ENq3LGYORK5NiMFHEVeNMt+fTXaC7DTS6sQXoqV+dFdfT0vmiL5cDLjaXQhaklJQq0NiwicZegzJRl1ZOTp3WQ==", + "requires": { + "@root/asn1": "^1.0.0", + "@root/encoding": "^1.0.1" + } + }, + "@types/babel-types": { + "version": "7.0.9", + "resolved": "https://registry.npmjs.org/@types/babel-types/-/babel-types-7.0.9.tgz", + "integrity": "sha512-qZLoYeXSTgQuK1h7QQS16hqLGdmqtRmN8w/rl3Au/l5x/zkHx+a4VHrHyBsi1I1vtK2oBHxSzKIu0R5p6spdOA==" + }, + "@types/babylon": { + "version": "6.16.5", + "resolved": "https://registry.npmjs.org/@types/babylon/-/babylon-6.16.5.tgz", + "integrity": "sha512-xH2e58elpj1X4ynnKp9qSnWlsRTIs6n3tgLGNfwAGHwePw0mulHQllV34n0T25uYSu1k0hRKkWXF890B1yS47w==", + "requires": { + "@types/babel-types": "*" + } + }, + "abbrev": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", + "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==" + }, + "accepts": { + "version": "1.3.7", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.7.tgz", + "integrity": "sha512-Il80Qs2WjYlJIBNzNkK6KYqlVMTbZLXgHx2oT0pU/fjRHyEp+PEfEPY0R3WCwAGVOtauxh1hOxNgIf5bv7dQpA==", + "requires": { + "mime-types": "~2.1.24", + "negotiator": "0.6.2" + } + }, + "ace-builds": { + "version": "1.4.12", + "resolved": "https://registry.npmjs.org/ace-builds/-/ace-builds-1.4.12.tgz", + "integrity": "sha512-G+chJctFPiiLGvs3+/Mly3apXTcfgE45dT5yp12BcWZ1kUs+gm0qd3/fv4gsz6fVag4mM0moHVpjHDIgph6Psg==" + }, + "acme-http-01-standalone": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/acme-http-01-standalone/-/acme-http-01-standalone-3.0.5.tgz", + "integrity": "sha512-W4GfK+39GZ+u0mvxRVUcVFCG6gposfzEnSBF20T/NUwWAKG59wQT1dUbS1NixRIAsRuhpGc4Jx659cErFQH0Pg==" + }, + "acorn": { + "version": "4.0.13", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-4.0.13.tgz", + "integrity": "sha1-EFSVrlNh1pe9GVyCUZLhrX8lN4c=" + }, + "acorn-globals": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/acorn-globals/-/acorn-globals-3.1.0.tgz", + "integrity": "sha1-/YJw9x+7SZawBPqIDuXUZXOnMb8=", + "requires": { + "acorn": "^4.0.4" + } + }, + "after": { + "version": "0.8.2", + "resolved": "https://registry.npmjs.org/after/-/after-0.8.2.tgz", + "integrity": "sha1-/ts5T58OAqqXaOcCvaI7UF+ufh8=" + }, + "agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "requires": { + "debug": "4" + }, + "dependencies": { + "debug": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz", + "integrity": "sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==", + "requires": { + "ms": "2.1.2" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + } + } + }, + "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" + } + }, + "align-text": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/align-text/-/align-text-0.1.4.tgz", + "integrity": "sha1-DNkKVhCT810KmSVsIrcGlDP60Rc=", + "requires": { + "kind-of": "^3.0.2", + "longest": "^1.0.1", + "repeat-string": "^1.5.2" + } + }, + "ansi-regex": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", + "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=" + }, + "aproba": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/aproba/-/aproba-1.2.0.tgz", + "integrity": "sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw==" + }, + "are-we-there-yet": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-1.1.5.tgz", + "integrity": "sha512-5hYdAkZlcG8tOLujVDTgCT+uPX0VnpAH28gWsLfzpXYm7wP6mp5Q/gYyR7YQ0cKVJcXJnl3j2kpBan13PtQf6w==", + "requires": { + "delegates": "^1.0.0", + "readable-stream": "^2.0.6" + } + }, + "array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha1-ml9pkFGx5wczKPKgCJaLZOopVdI=" + }, + "arraybuffer.slice": { + "version": "0.0.7", + "resolved": "https://registry.npmjs.org/arraybuffer.slice/-/arraybuffer.slice-0.0.7.tgz", + "integrity": "sha512-wGUIVQXuehL5TCqQun8OW81jGzAWycqzFF8lFp+GOM5BXLYj3bKNsYC4daB7n6XjCqxQA/qgTJ+8ANR3acjrog==" + }, + "asap": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz", + "integrity": "sha1-5QNHYR1+aQlDIIu9r+vLwvuGbUY=" + }, + "asn1": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.4.tgz", + "integrity": "sha1-jSR136tVO7M+d7VOWeiAu4ziMTY=", + "requires": { + "safer-buffer": "~2.1.0" + } + }, + "assert-plus": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", + "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=" + }, + "asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=" + }, + "aws-sign2": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz", + "integrity": "sha1-tG6JCTSpWR8tL2+G1+ap8bP+dqg=" + }, + "aws4": { + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.11.0.tgz", + "integrity": "sha512-xh1Rl34h6Fi1DC2WWKfxUTVqRsNnr6LsKz2+hfwDxQJWmrx8+c7ylaqBMcHfl1U1r2dsifOvKX3LQuLNZ+XSvA==" + }, + "babel-runtime": { + "version": "6.26.0", + "resolved": "https://registry.npmjs.org/babel-runtime/-/babel-runtime-6.26.0.tgz", + "integrity": "sha1-llxwWGaOgrVde/4E/yM3vItWR/4=", + "requires": { + "core-js": "^2.4.0", + "regenerator-runtime": "^0.11.0" + } + }, + "babel-types": { + "version": "6.26.0", + "resolved": "https://registry.npmjs.org/babel-types/-/babel-types-6.26.0.tgz", + "integrity": "sha1-o7Bz+Uq0nrb6Vc1lInozQ4BjJJc=", + "requires": { + "babel-runtime": "^6.26.0", + "esutils": "^2.0.2", + "lodash": "^4.17.4", + "to-fast-properties": "^1.0.3" + } + }, + "babylon": { + "version": "6.18.0", + "resolved": "https://registry.npmjs.org/babylon/-/babylon-6.18.0.tgz", + "integrity": "sha1-ry87iPpvXB5MY00aD46sT1WzleM=" + }, + "balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" + }, + "base64-arraybuffer": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-0.1.4.tgz", + "integrity": "sha1-mBjHngWbE1X5fgQooBfIOOkLqBI=" + }, + "base64id": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/base64id/-/base64id-2.0.0.tgz", + "integrity": "sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog==" + }, + "bcrypt-pbkdf": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz", + "integrity": "sha1-pDAdOJtqQ/m2f/PKEaP2Y342Dp4=", + "requires": { + "tweetnacl": "^0.14.3" + } + }, + "bindings": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz", + "integrity": "sha1-EDU8npRTNLwFEabZCzj7x8nFBN8=", + "optional": true, + "requires": { + "file-uri-to-path": "1.0.0" + } + }, + "blob": { + "version": "0.0.5", + "resolved": "https://registry.npmjs.org/blob/-/blob-0.0.5.tgz", + "integrity": "sha512-gaqbzQPqOoamawKg0LGVd7SzLgXS+JH61oWprSLH+P+abTczqJbhTR8CmJ2u9/bUYNmHTGJx/UEmn6doAvvuig==" + }, + "body-parser": { + "version": "1.19.0", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.19.0.tgz", + "integrity": "sha512-dhEPs72UPbDnAQJ9ZKMNTP6ptJaionhP5cBb541nXPlW60Jepo9RV/a4fX4XWW9CuFNK22krhrj1+rgzifNCsw==", + "requires": { + "bytes": "3.1.0", + "content-type": "~1.0.4", + "debug": "2.6.9", + "depd": "~1.1.2", + "http-errors": "1.7.2", + "iconv-lite": "0.4.24", + "on-finished": "~2.3.0", + "qs": "6.7.0", + "raw-body": "2.4.0", + "type-is": "~1.6.17" + }, + "dependencies": { + "bytes": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.0.tgz", + "integrity": "sha512-zauLjrfCG+xvoyaqLoV8bLVXXNGC4JqlxFCutSDWA6fJrTo2ZuvLYTqZ7aHBLZSMOopbzwv8f+wZcVzfVTI2Dg==" + } + } + }, + "brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "requires": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "bufferutil": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/bufferutil/-/bufferutil-4.0.3.tgz", + "integrity": "sha512-yEYTwGndELGvfXsImMBLop58eaGW+YdONi1fNjTINSY98tmMmFijBG6WXgdkfuLNt4imzQNtIE+eBp1PVpMCSw==", + "requires": { + "node-gyp-build": "^4.2.0" + } + }, + "bytes": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.0.0.tgz", + "integrity": "sha1-0ygVQE1olpn4Wk6k+odV3ROpYEg=" + }, + "call-bind": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz", + "integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==", + "requires": { + "function-bind": "^1.1.1", + "get-intrinsic": "^1.0.2" + } + }, + "camelcase": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-1.2.1.tgz", + "integrity": "sha1-m7UwTS4LVmmLLHWLCKPqqdqlijk=" + }, + "canvas": { + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/canvas/-/canvas-2.8.0.tgz", + "integrity": "sha512-gLTi17X8WY9Cf5GZ2Yns8T5lfBOcGgFehDFb+JQwDqdOoBOcECS9ZWMEAqMSVcMYwXD659J8NyzjRY/2aE+C2Q==", + "requires": { + "@mapbox/node-pre-gyp": "^1.0.0", + "nan": "^2.14.0", + "simple-get": "^3.0.3" + } + }, + "caseless": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", + "integrity": "sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw=" + }, + "center-align": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/center-align/-/center-align-0.1.3.tgz", + "integrity": "sha1-qg0yYptu6XIgBBHL1EYckHvCt60=", + "requires": { + "align-text": "^0.1.3", + "lazy-cache": "^1.0.3" + } + }, + "cert-info": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/cert-info/-/cert-info-1.5.1.tgz", + "integrity": "sha512-eoQC/yAgW3gKTKxjzyClvi+UzuY97YCjcl+lSqbsGIy7HeGaWxCPOQFivhUYm27hgsBMhsJJFya3kGvK6PMIcQ==" + }, + "character-parser": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/character-parser/-/character-parser-2.2.0.tgz", + "integrity": "sha1-x84o821LzZdE5f/CxfzeHHMmH8A=", + "requires": { + "is-regex": "^1.0.3" + } + }, + "chownr": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz", + "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==" + }, + "clean-css": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/clean-css/-/clean-css-4.2.3.tgz", + "integrity": "sha512-VcMWDN54ZN/DS+g58HYL5/n4Zrqe8vHJpGA8KdgUXFU4fuP/aHNw8eld9SyEIyabIMJX/0RaY/fplOo5hYLSFA==", + "requires": { + "source-map": "~0.6.0" + } + }, + "cliui": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-2.1.0.tgz", + "integrity": "sha1-S0dXYP+AJkx2LDoXGQMukcf+oNE=", + "requires": { + "center-align": "^0.1.1", + "right-align": "^0.1.1", + "wordwrap": "0.0.2" + } + }, + "code-point-at": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz", + "integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=" + }, + "colorette": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/colorette/-/colorette-1.2.2.tgz", + "integrity": "sha512-MKGMzyfeuutC/ZJ1cba9NqcNpfeqMUcYmyF1ZFY6/Cn7CNSAKx6a+s48sqLqyAiZuaP2TcqMhoo+dlwFnVxT9w==" + }, + "combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "requires": { + "delayed-stream": "~1.0.0" + } + }, + "compressible": { + "version": "2.0.18", + "resolved": "https://registry.npmjs.org/compressible/-/compressible-2.0.18.tgz", + "integrity": "sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg==", + "requires": { + "mime-db": ">= 1.43.0 < 2" + } + }, + "compression": { + "version": "1.7.4", + "resolved": "https://registry.npmjs.org/compression/-/compression-1.7.4.tgz", + "integrity": "sha512-jaSIDzP9pZVS4ZfQ+TzvtiWhdpFhE2RDHz8QJkpX9SIpLq88VueF5jJw6t+6CUQcAoA6t+x89MLrWAqpfDE8iQ==", + "requires": { + "accepts": "~1.3.5", + "bytes": "3.0.0", + "compressible": "~2.0.16", + "debug": "2.6.9", + "on-headers": "~1.0.2", + "safe-buffer": "5.1.2", + "vary": "~1.1.2" + } + }, + "concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=" + }, + "console-control-strings": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", + "integrity": "sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4=" + }, + "constantinople": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/constantinople/-/constantinople-3.1.2.tgz", + "integrity": "sha1-1F7XJPV9PRBQABen06iJwTga5kc=", + "requires": { + "@types/babel-types": "^7.0.0", + "@types/babylon": "^6.16.2", + "babel-types": "^6.26.0", + "babylon": "^6.18.0" + } + }, + "content-disposition": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.3.tgz", + "integrity": "sha512-ExO0774ikEObIAEV9kDo50o+79VCUdEB6n6lzKgGwupcVeRlhrj3qGAfwq8G6uBJjkqLrhT0qEYFcWng8z1z0g==", + "requires": { + "safe-buffer": "5.1.2" + } + }, + "content-type": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.4.tgz", + "integrity": "sha1-4TjMdeBAxyexlm/l5fjJruJW/js=" + }, + "cookie": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.0.tgz", + "integrity": "sha512-+Hp8fLp57wnUSt0tY0tHEXh4voZRDnoIrZPqlo3DPiI4y9lwg/jqx+1Om94/W6ZaPDOUbnjOt/99w66zk+l1Xg==" + }, + "cookie-parser": { + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/cookie-parser/-/cookie-parser-1.4.5.tgz", + "integrity": "sha512-f13bPUj/gG/5mDr+xLmSxxDsB9DQiTIfhJS/sqjrmfAWiAN+x2O4i/XguTL9yDZ+/IFDanJ+5x7hC4CXT9Tdzw==", + "requires": { + "cookie": "0.4.0", + "cookie-signature": "1.0.6" + } + }, + "cookie-signature": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", + "integrity": "sha1-4wOogrNCzD7oylE6eZmXNNqzriw=" + }, + "core-js": { + "version": "2.6.12", + "resolved": "https://registry.npmjs.org/core-js/-/core-js-2.6.12.tgz", + "integrity": "sha512-Kb2wC0fvsWfQrgk8HU5lW6U/Lcs8+9aaYcy4ZFc6DDlo4nZ7n70dEgE5rtR0oG6ufKDUnrwfWL1mXR5ljDatrQ==" + }, + "core-util-is": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", + "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=" + }, + "crypto-js": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/crypto-js/-/crypto-js-3.3.0.tgz", + "integrity": "sha512-DIT51nX0dCfKltpRiXV+/TVZq+Qq2NgF4644+K7Ttnla7zEzqc+kjJyiB96BHNyUTBxyjzRcZYpUdZa+QAqi6Q==" + }, + "d": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/d/-/d-1.0.1.tgz", + "integrity": "sha512-m62ShEObQ39CfralilEQRjH6oAMtNCV1xJyEx5LpRYUVN+EviphDgUc/F3hnYbADmkiNs67Y+3ylmlG7Lnu+FA==", + "requires": { + "es5-ext": "^0.10.50", + "type": "^1.0.1" + } + }, + "dashdash": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz", + "integrity": "sha1-hTz6D3y+L+1d4gMmuN1YEDX24vA=", + "requires": { + "assert-plus": "^1.0.0" + } + }, + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "requires": { + "ms": "2.0.0" + } + }, + "decamelize": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", + "integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=" + }, + "decompress-response": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-4.2.1.tgz", + "integrity": "sha512-jOSne2qbyE+/r8G1VU+G/82LBs2Fs4LAsTiLSHOCOMZQl2OKZ6i8i4IyHemTe+/yIXOtTcRQMzPcgyhoFlqPkw==", + "requires": { + "mimic-response": "^2.0.0" + } + }, + "deepmerge": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.2.2.tgz", + "integrity": "sha512-FJ3UgI4gIl+PHZm53knsuSFpE+nESMr7M4v9QcgB7S63Kj/6WqMiFQJpBBYz1Pt+66bZpP3Q7Lye0Oo9MPKEdg==" + }, + "delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=" + }, + "delegates": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", + "integrity": "sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o=" + }, + "depd": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", + "integrity": "sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak=" + }, + "destroy": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.0.4.tgz", + "integrity": "sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA=" + }, + "detect-libc": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-1.0.3.tgz", + "integrity": "sha1-+hN8S9aY7fVc1c0CrFWfkaTEups=" + }, + "doctypes": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/doctypes/-/doctypes-1.1.0.tgz", + "integrity": "sha1-6oCxBqh1OHdOijpKWv4pPeSJ4Kk=" + }, + "dom-serializer": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-1.3.2.tgz", + "integrity": "sha512-5c54Bk5Dw4qAxNOI1pFEizPSjVsx5+bpJKmL2kPn8JhBUq2q09tTCa3mjijun2NfK78NMouDYNMBkOrPZiS+ig==", + "requires": { + "domelementtype": "^2.0.1", + "domhandler": "^4.2.0", + "entities": "^2.0.0" + } + }, + "domelementtype": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.2.0.tgz", + "integrity": "sha512-DtBMo82pv1dFtUmHyr48beiuq792Sxohr+8Hm9zoxklYPfa6n0Z3Byjj2IV7bmr2IyqClnqEQhfgHJJ5QF0R5A==" + }, + "domhandler": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-4.2.0.tgz", + "integrity": "sha512-zk7sgt970kzPks2Bf+dwT/PLzghLnsivb9CcxkvR8Mzr66Olr0Ofd8neSbglHJHaHa2MadfoSdNlKYAaafmWfA==", + "requires": { + "domelementtype": "^2.2.0" + } + }, + "dompurify": { + "version": "2.2.9", + "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-2.2.9.tgz", + "integrity": "sha512-+9MqacuigMIZ+1+EwoEltogyWGFTJZWU3258Rupxs+2CGs4H914G9er6pZbsme/bvb5L67o2rade9n21e4RW/w==" + }, + "domutils": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-2.7.0.tgz", + "integrity": "sha512-8eaHa17IwJUPAiB+SoTYBo5mCdeMgdcAoXJ59m6DT1vw+5iLS3gNoqYaRowaBKtGVrOF1Jz4yDTgYKLK2kvfJg==", + "requires": { + "dom-serializer": "^1.0.1", + "domelementtype": "^2.2.0", + "domhandler": "^4.2.0" + } + }, + "ecc-jsbn": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz", + "integrity": "sha1-OoOpBOVDUyh4dMVkt1SThoSamMk=", + "requires": { + "jsbn": "~0.1.0", + "safer-buffer": "^2.1.0" + } + }, + "eckles": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/eckles/-/eckles-1.4.1.tgz", + "integrity": "sha1-Xpf++oVUp69ZQHDEYeayX+OBk4I=" + }, + "ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=" + }, + "encodeurl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", + "integrity": "sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k=" + }, + "engine.io": { + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-3.5.0.tgz", + "integrity": "sha512-21HlvPUKaitDGE4GXNtQ7PLP0Sz4aWLddMPw2VTyFz1FVZqu/kZsJUO8WNpKuE/OCL7nkfRaOui2ZCJloGznGA==", + "requires": { + "accepts": "~1.3.4", + "base64id": "2.0.0", + "cookie": "~0.4.1", + "debug": "~4.1.0", + "engine.io-parser": "~2.2.0", + "ws": "~7.4.2" + }, + "dependencies": { + "cookie": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.1.tgz", + "integrity": "sha512-ZwrFkGJxUR3EIoXtO+yVE69Eb7KlixbaeAWfBQB9vVsNn/o+Yw69gBWSSDK825hQNdN+wF8zELf3dFNl/kxkUA==" + }, + "debug": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", + "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", + "requires": { + "ms": "^2.1.1" + } + }, + "ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" + }, + "ws": { + "version": "7.4.6", + "resolved": "https://registry.npmjs.org/ws/-/ws-7.4.6.tgz", + "integrity": "sha512-YmhHDO4MzaDLB+M9ym/mDA5z0naX8j7SIlT8f8z+I0VtzsRbekxEutHSme7NPS2qE8StCYQNUnfWdXta/Yu85A==" + } + } + }, + "engine.io-parser": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-2.2.1.tgz", + "integrity": "sha512-x+dN/fBH8Ro8TFwJ+rkB2AmuVw9Yu2mockR/p3W8f8YtExwFgDvBDi0GWyb4ZLkpahtDGZgtr3zLovanJghPqg==", + "requires": { + "after": "0.8.2", + "arraybuffer.slice": "~0.0.7", + "base64-arraybuffer": "0.1.4", + "blob": "0.0.5", + "has-binary2": "~1.0.2" + } + }, + "entities": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-2.2.0.tgz", + "integrity": "sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A==" + }, + "es5-ext": { + "version": "0.10.53", + "resolved": "https://registry.npmjs.org/es5-ext/-/es5-ext-0.10.53.tgz", + "integrity": "sha512-Xs2Stw6NiNHWypzRTY1MtaG/uJlwCk8kH81920ma8mvN8Xq1gsfhZvpkImLQArw8AHnv8MT2I45J3c0R8slE+Q==", + "requires": { + "es6-iterator": "~2.0.3", + "es6-symbol": "~3.1.3", + "next-tick": "~1.0.0" + } + }, + "es6-iterator": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/es6-iterator/-/es6-iterator-2.0.3.tgz", + "integrity": "sha1-p96IkUGgWpSwhUQDstCg+/qY87c=", + "requires": { + "d": "1", + "es5-ext": "^0.10.35", + "es6-symbol": "^3.1.1" + } + }, + "es6-symbol": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/es6-symbol/-/es6-symbol-3.1.3.tgz", + "integrity": "sha512-NJ6Yn3FuDinBaBRWl/q5X/s4koRHBrgKAu+yGI6JCBeiu3qrcbJhwT2GeR/EXVfylRk8dpQVJoLEFhK+Mu31NA==", + "requires": { + "d": "^1.0.1", + "ext": "^1.1.2" + } + }, + "escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg=" + }, + "escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==" + }, + "esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==" + }, + "etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc=" + }, + "express": { + "version": "4.17.1", + "resolved": "https://registry.npmjs.org/express/-/express-4.17.1.tgz", + "integrity": "sha512-mHJ9O79RqluphRrcw2X/GTh3k9tVv8YcoyY4Kkh4WDMUYKRZUq0h1o0w2rrrxBqM7VoeUVqgb27xlEMXTnYt4g==", + "requires": { + "accepts": "~1.3.7", + "array-flatten": "1.1.1", + "body-parser": "1.19.0", + "content-disposition": "0.5.3", + "content-type": "~1.0.4", + "cookie": "0.4.0", + "cookie-signature": "1.0.6", + "debug": "2.6.9", + "depd": "~1.1.2", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "~1.1.2", + "fresh": "0.5.2", + "merge-descriptors": "1.0.1", + "methods": "~1.1.2", + "on-finished": "~2.3.0", + "parseurl": "~1.3.3", + "path-to-regexp": "0.1.7", + "proxy-addr": "~2.0.5", + "qs": "6.7.0", + "range-parser": "~1.2.1", + "safe-buffer": "5.1.2", + "send": "0.17.1", + "serve-static": "1.14.1", + "setprototypeof": "1.1.1", + "statuses": "~1.5.0", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + } + }, + "express-force-https": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/express-force-https/-/express-force-https-1.0.0.tgz", + "integrity": "sha1-KtuBIaaJRhb8eOoFsVvY1LhV/Qw=" + }, + "ext": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/ext/-/ext-1.4.0.tgz", + "integrity": "sha512-Key5NIsUxdqKg3vIsdw9dSuXpPCQ297y6wBjL30edxwPgt2E44WcWBZey/ZvUc6sERLTxKdyCu4gZFmUbk1Q7A==", + "requires": { + "type": "^2.0.0" + }, + "dependencies": { + "type": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/type/-/type-2.5.0.tgz", + "integrity": "sha512-180WMDQaIMm3+7hGXWf12GtdniDEy7nYcyFMKJn/eZz/6tSLXrUN9V0wKSbMjej0I1WHWbpREDEKHtqPQa9NNw==" + } + } + }, + "extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha1-+LETa0Bx+9jrFAr/hYsQGewpFfo=" + }, + "extsprintf": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz", + "integrity": "sha1-lpGEQOMEGnpBT4xS48V06zw+HgU=" + }, + "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==" + }, + "file-uri-to-path": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", + "integrity": "sha1-VTp7hEb/b2hDWcRF8eN6BdrMM90=", + "optional": true + }, + "finalhandler": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.2.tgz", + "integrity": "sha512-aAWcW57uxVNrQZqFXjITpW3sIUQmHGG3qSb9mUah9MgMC4NeWhNOlNjXEYq3HjRAvL6arUviZGGJsBg6z0zsWA==", + "requires": { + "debug": "2.6.9", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "on-finished": "~2.3.0", + "parseurl": "~1.3.3", + "statuses": "~1.5.0", + "unpipe": "~1.0.0" + } + }, + "fontsource-source-sans-pro": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/fontsource-source-sans-pro/-/fontsource-source-sans-pro-4.0.0.tgz", + "integrity": "sha512-1PHVhsemInovKS6TJ2voUzo4akMov2bJB5KkVxdymXkZZ8KVLsz6p1ku4TPQBJjiHFfSG71K8oLty44unmhMiw==" + }, + "forever-agent": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", + "integrity": "sha1-+8cfDEGt6zf5bFd60e1C2P2sypE=" + }, + "form-data": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.3.tgz", + "integrity": "sha1-3M5SwF9kTymManq5Nr1yTO/786Y=", + "requires": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.6", + "mime-types": "^2.1.12" + } + }, + "forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==" + }, + "fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac=" + }, + "fs-minipass": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz", + "integrity": "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==", + "requires": { + "minipass": "^3.0.0" + } + }, + "fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=" + }, + "function-bind": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", + "integrity": "sha1-pWiZ0+o8m6uHS7l3O3xe3pL0iV0=" + }, + "gauge": { + "version": "2.7.4", + "resolved": "https://registry.npmjs.org/gauge/-/gauge-2.7.4.tgz", + "integrity": "sha1-LANAXHU4w51+s3sxcCLjJfsBi/c=", + "requires": { + "aproba": "^1.0.3", + "console-control-strings": "^1.0.0", + "has-unicode": "^2.0.0", + "object-assign": "^4.1.0", + "signal-exit": "^3.0.0", + "string-width": "^1.0.1", + "strip-ansi": "^3.0.1", + "wide-align": "^1.1.0" + } + }, + "get-intrinsic": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.1.1.tgz", + "integrity": "sha512-kWZrnVM42QCiEA2Ig1bG8zjoIMOgxWwYCEeNdwY6Tv/cOSeGpcoX4pXHfKUxNKVoArnrEr2e9srnAxxGIraS9Q==", + "requires": { + "function-bind": "^1.1.1", + "has": "^1.0.3", + "has-symbols": "^1.0.1" + } + }, + "getpass": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz", + "integrity": "sha1-Xv+OPmhNVprkyysSgmBOi6YhSfo=", + "requires": { + "assert-plus": "^1.0.0" + } + }, + "glob": { + "version": "7.1.7", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.7.tgz", + "integrity": "sha512-OvD9ENzPLbegENnYP5UUfJIirTg4+XwMWGaQfQTY0JenxNvvIKP3U3/tAQSPIu/lHxXYSZmpXlUHeqAIdKzBLQ==", + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + }, + "greenlock-express": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/greenlock-express/-/greenlock-express-4.0.3.tgz", + "integrity": "sha512-swTCuaLcZWbwRe9gf28AN1VO2PLiAoPB7StVhEFTBqCqqoHgiu0PJqdz3IbnfXB/8Kckx4sZ+B+Re2BvoCHY4A==", + "requires": { + "@root/greenlock": "^4.0.4", + "@root/greenlock-express": "^4.0.3", + "redirect-https": "^1.1.5" + } + }, + "greenlock-manager-fs": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/greenlock-manager-fs/-/greenlock-manager-fs-3.1.1.tgz", + "integrity": "sha512-np6qdnPIOZx40PAcSQcqK1eMPWjTKxsxcgRd/OVg0ai49WC1Ds74CTrwmB84pq2n53ikbnDBQFmKEQ4AC0DK8w==", + "requires": { + "@root/mkdirp": "^1.0.0", + "safe-replace": "^1.1.0" + } + }, + "greenlock-store-fs": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/greenlock-store-fs/-/greenlock-store-fs-3.2.2.tgz", + "integrity": "sha512-92ejLB4DyV4qv/2b6VLGF2nKfYQeIfg3o+e/1cIoYLjlIaUFdbBXkzLTRozFlHsQPZt2ALi5qYrpC9IwH7GK8A==", + "requires": { + "@root/mkdirp": "^1.0.0", + "safe-replace": "^1.1.0" + } + }, + "har-schema": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz", + "integrity": "sha1-qUwiJOvKwEeCoNkDVSHyRzW37JI=" + }, + "har-validator": { + "version": "5.1.5", + "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.1.5.tgz", + "integrity": "sha512-nmT2T0lljbxdQZfspsno9hgrG3Uir6Ks5afism62poxqBM6sDnMEuPmzTq8XN0OEwqKLLdh1jQI3qyE66Nzb3w==", + "requires": { + "ajv": "^6.12.3", + "har-schema": "^2.0.0" + } + }, + "has": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", + "integrity": "sha1-ci18v8H2qoJB8W3YFOAR4fQeh5Y=", + "requires": { + "function-bind": "^1.1.1" + } + }, + "has-binary2": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has-binary2/-/has-binary2-1.0.3.tgz", + "integrity": "sha512-G1LWKhDSvhGeAQ8mPVQlqNcOB2sJdwATtZKl2pDKKHfpf/rYj24lkinxf69blJbnsvtqqNU+L3SL50vzZhXOnw==", + "requires": { + "isarray": "2.0.1" + } + }, + "has-symbols": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.2.tgz", + "integrity": "sha512-chXa79rL/UC2KlX17jo3vRGz0azaWEx5tGqZg5pO3NUyEJVB17dMruQlzCCOfUvElghKcm5194+BCRvi2Rv/Gw==" + }, + "has-unicode": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz", + "integrity": "sha1-4Ob+aijPUROIVeCG0Wkedx3iqLk=" + }, + "htmlparser2": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-6.1.0.tgz", + "integrity": "sha512-gyyPk6rgonLFEDGoeRgQNaEUvdJ4ktTmmUh/h2t7s+M8oPpIPxgNACWa+6ESR57kXstwqPiCut0V8NRpcwgU7A==", + "requires": { + "domelementtype": "^2.0.1", + "domhandler": "^4.0.0", + "domutils": "^2.5.2", + "entities": "^2.0.0" + } + }, + "http-errors": { + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.7.2.tgz", + "integrity": "sha512-uUQBt3H/cSIVfch6i1EuPNy/YsRSOUBXTVfZ+yR7Zjez3qjBz6i9+i4zjNaoqcoFVI4lQJ5plg63TvGfRSDCRg==", + "requires": { + "depd": "~1.1.2", + "inherits": "2.0.3", + "setprototypeof": "1.1.1", + "statuses": ">= 1.5.0 < 2", + "toidentifier": "1.0.0" + } + }, + "http-signature": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz", + "integrity": "sha1-muzZJRFHcvPZW2WmCruPfBj7rOE=", + "requires": { + "assert-plus": "^1.0.0", + "jsprim": "^1.2.2", + "sshpk": "^1.7.0" + } + }, + "https-proxy-agent": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.0.tgz", + "integrity": "sha512-EkYm5BcKUGiduxzSt3Eppko+PiNWNEpa4ySk9vTC6wDsQJW9rHSa+UhGNJoRYp7bz6Ht1eaRIa6QaJqO5rCFbA==", + "requires": { + "agent-base": "6", + "debug": "4" + }, + "dependencies": { + "debug": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz", + "integrity": "sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==", + "requires": { + "ms": "2.1.2" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + } + } + }, + "iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha1-ICK0sl+93CHS9SSXSkdKr+czkIs=", + "requires": { + "safer-buffer": ">= 2.1.2 < 3" + } + }, + "immediate": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz", + "integrity": "sha1-nbHb0Pr43m++D13V5Wu2BigN5ps=" + }, + "inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", + "requires": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "inherits": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", + "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=" + }, + "ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==" + }, + "is-buffer": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz", + "integrity": "sha1-76ouqdqg16suoTqXsritUf776L4=" + }, + "is-core-module": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.4.0.tgz", + "integrity": "sha512-6A2fkfq1rfeQZjxrZJGerpLCTHRNEBiSgnu0+obeJpEPZRUooHgsizvzv0ZjJwOz3iWIHdJtVWJ/tmPr3D21/A==", + "requires": { + "has": "^1.0.3" + } + }, + "is-expression": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-expression/-/is-expression-3.0.0.tgz", + "integrity": "sha1-Oayqa+f9HzRx3ELHQW5hwkMXrJ8=", + "requires": { + "acorn": "~4.0.2", + "object-assign": "^4.0.1" + } + }, + "is-fullwidth-code-point": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz", + "integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=", + "requires": { + "number-is-nan": "^1.0.0" + } + }, + "is-plain-object": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-5.0.0.tgz", + "integrity": "sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==" + }, + "is-promise": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-2.2.2.tgz", + "integrity": "sha512-+lP4/6lKUBfQjZ2pdxThZvLUAafmZb8OAxFb8XXtiQmS35INgr85hdOGoEs124ez1FCnZJt6jau/T+alh58QFQ==" + }, + "is-regex": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.3.tgz", + "integrity": "sha512-qSVXFz28HM7y+IWX6vLCsexdlvzT1PJNFSBuaQLQ5o0IEw8UDYW6/2+eCMVyIsbM8CNLX2a/QWmSpyxYEHY7CQ==", + "requires": { + "call-bind": "^1.0.2", + "has-symbols": "^1.0.2" + } + }, + "is-typedarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", + "integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=" + }, + "isarray": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.1.tgz", + "integrity": "sha1-o32U7ZzaLVmGXJ92/llu4fM4dB4=" + }, + "isstream": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", + "integrity": "sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo=" + }, + "jquery": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/jquery/-/jquery-3.6.0.tgz", + "integrity": "sha512-JVzAR/AjBvVt2BmYhxRCSYysDsPcssdmTFnzyLEts9qNwmjmu4JTAMYubEfwVOSwpQ1I1sKKFcxhZCI2buerfw==" + }, + "jquery-ui-dist": { + "version": "1.12.1", + "resolved": "https://registry.npmjs.org/jquery-ui-dist/-/jquery-ui-dist-1.12.1.tgz", + "integrity": "sha1-XAgV08xvkP9fqvWyaKbiO0ypBPo=" + }, + "js-stringify": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/js-stringify/-/js-stringify-1.0.2.tgz", + "integrity": "sha1-Fzb939lyTyijaCrcYjCufk6Weds=" + }, + "jsbn": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz", + "integrity": "sha1-peZUwuWi3rXyAdls77yoDA7y9RM=" + }, + "json-schema": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.2.3.tgz", + "integrity": "sha1-tIDIkuWaLwWVTOcnvT8qTogvnhM=" + }, + "json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha1-afaofZUTq4u4/mO9sJecRI5oRmA=" + }, + "json-stringify-safe": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", + "integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=" + }, + "jsprim": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.1.tgz", + "integrity": "sha1-MT5mvB5cwG5Di8G3SZwuXFastqI=", + "requires": { + "assert-plus": "1.0.0", + "extsprintf": "1.3.0", + "json-schema": "0.2.3", + "verror": "1.10.0" + } + }, + "jstransformer": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/jstransformer/-/jstransformer-1.0.0.tgz", + "integrity": "sha1-7Yvwkh4vPx7U1cGkT2hwntJHIsM=", + "requires": { + "is-promise": "^2.0.0", + "promise": "^7.0.1" + } + }, + "jszip": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/jszip/-/jszip-3.6.0.tgz", + "integrity": "sha512-jgnQoG9LKnWO3mnVNBnfhkh0QknICd1FGSrXcgrl67zioyJ4wgx25o9ZqwNtrROSflGBCGYnJfjrIyRIby1OoQ==", + "requires": { + "lie": "~3.3.0", + "pako": "~1.0.2", + "readable-stream": "~2.3.6", + "set-immediate-shim": "~1.0.1" + } + }, + "keypairs": { + "version": "1.2.14", + "resolved": "https://registry.npmjs.org/keypairs/-/keypairs-1.2.14.tgz", + "integrity": "sha1-yXHo0Dri0Ej8aHkRfgUJuuZUOIc=", + "requires": { + "eckles": "^1.4.1", + "rasha": "^1.2.4" + } + }, + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "requires": { + "is-buffer": "^1.1.5" + } + }, + "klona": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/klona/-/klona-2.0.4.tgz", + "integrity": "sha512-ZRbnvdg/NxqzC7L9Uyqzf4psi1OM4Cuc+sJAkQPjO6XkQIJTNbfK2Rsmbw8fx1p2mkZdp2FZYo2+LwXYY/uwIA==" + }, + "lazy-cache": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/lazy-cache/-/lazy-cache-1.0.4.tgz", + "integrity": "sha1-odePw6UEdMuAhF07O24dpJpEbo4=" + }, + "le-acme-core": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/le-acme-core/-/le-acme-core-2.1.4.tgz", + "integrity": "sha1-SYfQ1COLR/KaWsG3OgPssp5GWwY=", + "requires": { + "request": "^2.74.0", + "rsa-compat": "^1.3.2" + } + }, + "lie": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/lie/-/lie-3.3.0.tgz", + "integrity": "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==", + "requires": { + "immediate": "~3.0.5" + } + }, + "lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" + }, + "longest": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/longest/-/longest-1.0.1.tgz", + "integrity": "sha1-MKCy2jj3N3DoKUoNIuZiXtd9AJc=" + }, + "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" + }, + "dependencies": { + "semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==" + } + } + }, + "marked": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/marked/-/marked-2.1.3.tgz", + "integrity": "sha512-/Q+7MGzaETqifOMWYEA7HVMaZb4XbcRfaOzcSsHZEith83KGlvaSG33u0SKu89Mj5h+T8V2hM+8O45Qc5XTgwA==" + }, + "media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g=" + }, + "merge-descriptors": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", + "integrity": "sha1-sAqqVW3YtEVoFQ7J0blT8/kMu2E=" + }, + "methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4=" + }, + "mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==" + }, + "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" + } + }, + "mimic-response": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-2.1.0.tgz", + "integrity": "sha512-wXqjST+SLt7R009ySCglWBCFpjUygmCIfD790/kVbiGmUgfYGuB14PiTd5DwVxSV4NcYHjzMkoj5LjQZwTQLEA==" + }, + "minimatch": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", + "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", + "requires": { + "brace-expansion": "^1.1.7" + } + }, + "minipass": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.1.3.tgz", + "integrity": "sha512-Mgd2GdMVzY+x3IJ+oHnVM+KG3lA5c8tnabyJKmHSaG2kAGpudxuOf8ToDkhumF7UzME7DecbQE9uOZhNm7PuJg==", + "requires": { + "yallist": "^4.0.0" + } + }, + "minizlib": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz", + "integrity": "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==", + "requires": { + "minipass": "^3.0.0", + "yallist": "^4.0.0" + } + }, + "mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==" + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" + }, + "nan": { + "version": "2.14.2", + "resolved": "https://registry.npmjs.org/nan/-/nan-2.14.2.tgz", + "integrity": "sha512-M2ufzIiINKCuDfBSAUr1vWQ+vuVcA9kqx8JJUsbQi6yf1uGRyb7HfpdfUr5qLXf3B/t8dPvcjhKMmlfnP47EzQ==" + }, + "nanoid": { + "version": "3.1.23", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.1.23.tgz", + "integrity": "sha512-FiB0kzdP0FFVGDKlRLEQ1BgDzU87dy5NnzjeW9YZNt+/c3+q82EQDUwniSAUxp/F0gFNI1ZhKU1FqYsMuqZVnw==" + }, + "negotiator": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.2.tgz", + "integrity": "sha512-hZXc7K2e+PgeI1eDBe/10Ard4ekbfrrqG8Ep+8Jmf4JID2bNg7NvCPOZN+kfF574pFQI7mum2AUqDidoKqcTOw==" + }, + "next-tick": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/next-tick/-/next-tick-1.0.0.tgz", + "integrity": "sha1-yobR/ogoFpsBICCOPchCS524NCw=" + }, + "node-fetch": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.1.tgz", + "integrity": "sha512-V4aYg89jEoVRxRb2fJdAg8FHvI7cEyYdVAh94HH0UIK8oJxUfkjlDQN9RbMx+bEjP7+ggMiFRprSti032Oipxw==" + }, + "node-forge": { + "version": "0.7.6", + "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-0.7.6.tgz", + "integrity": "sha1-/fO0GK7h+U8O9kLNY0hsd8qXJKw=", + "optional": true + }, + "node-gyp-build": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.2.3.tgz", + "integrity": "sha512-MN6ZpzmfNCRM+3t57PTJHgHyw/h4OWnZ6mR8P5j/uZtqQr46RRuDE/P+g3n0YR/AiYXeWixZZzaip77gdICfRg==" + }, + "nopt": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-5.0.0.tgz", + "integrity": "sha512-Tbj67rffqceeLpcRXrT7vKAN8CwfPeIBgM7E6iBkmKLV7bEMwpGgYLGv0jACUsECaa/vuxP0IjEont6umdMgtQ==", + "requires": { + "abbrev": "1" + } + }, + "npmlog": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-4.1.2.tgz", + "integrity": "sha512-2uUqazuKlTaSI/dC8AzicUck7+IrEaOnN/e0jd3Xtt1KcGpwx30v50mL7oPyr/h9bL3E4aZccVwpwP+5W9Vjkg==", + "requires": { + "are-we-there-yet": "~1.1.2", + "console-control-strings": "~1.1.0", + "gauge": "~2.7.3", + "set-blocking": "~2.0.0" + } + }, + "number-is-nan": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz", + "integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=" + }, + "oauth-sign": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.9.0.tgz", + "integrity": "sha1-R6ewFrqmi1+g7PPe4IqFxnmsZFU=" + }, + "object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=" + }, + "on-finished": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", + "integrity": "sha1-IPEzZIGwg811M3mSoWlxqi2QaUc=", + "requires": { + "ee-first": "1.1.1" + } + }, + "on-headers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.0.2.tgz", + "integrity": "sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA==" + }, + "once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", + "requires": { + "wrappy": "1" + } + }, + "pako": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz", + "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==" + }, + "parse-srcset": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/parse-srcset/-/parse-srcset-1.0.2.tgz", + "integrity": "sha1-8r0iH2zJcKk42IVWq8WJyqqiveE=" + }, + "parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==" + }, + "path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=" + }, + "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==" + }, + "path-to-regexp": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", + "integrity": "sha1-32BBeABfUi8V60SQ5yR6G/qmf4w=" + }, + "performance-now": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", + "integrity": "sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns=" + }, + "pidusage": { + "version": "2.0.21", + "resolved": "https://registry.npmjs.org/pidusage/-/pidusage-2.0.21.tgz", + "integrity": "sha512-cv3xAQos+pugVX+BfXpHsbyz/dLzX+lr44zNMsYiGxUw+kV5sgQCIcLd1z+0vq+KyC7dJ+/ts2PsfgWfSC3WXA==", + "requires": { + "safe-buffer": "^5.2.1" + }, + "dependencies": { + "safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==" + } + } + }, + "postcss": { + "version": "8.3.5", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.3.5.tgz", + "integrity": "sha512-NxTuJocUhYGsMiMFHDUkmjSKT3EdH4/WbGF6GCi1NDGk+vbcUTun4fpbOqaPtD8IIsztA2ilZm2DhYCuyN58gA==", + "requires": { + "colorette": "^1.2.2", + "nanoid": "^3.1.23", + "source-map-js": "^0.6.2" + } + }, + "process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==" + }, + "promise": { + "version": "7.3.1", + "resolved": "https://registry.npmjs.org/promise/-/promise-7.3.1.tgz", + "integrity": "sha1-BktyYCsY+Q8pGSuLG8QY/9Hr078=", + "requires": { + "asap": "~2.0.3" + } + }, + "proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "requires": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + } + }, + "psl": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/psl/-/psl-1.8.0.tgz", + "integrity": "sha512-RIdOzyoavK+hA18OGGWDqUTsCLhtA7IcZ/6NCs4fFJaHBDab+pDDmDIByWFRQJq2Cd7r1OoQxBGKOaztq+hjIQ==" + }, + "pug": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/pug/-/pug-2.0.4.tgz", + "integrity": "sha512-XhoaDlvi6NIzL49nu094R2NA6P37ijtgMDuWE+ofekDChvfKnzFal60bhSdiy8y2PBO6fmz3oMEIcfpBVRUdvw==", + "requires": { + "pug-code-gen": "^2.0.2", + "pug-filters": "^3.1.1", + "pug-lexer": "^4.1.0", + "pug-linker": "^3.0.6", + "pug-load": "^2.0.12", + "pug-parser": "^5.0.1", + "pug-runtime": "^2.0.5", + "pug-strip-comments": "^1.0.4" + } + }, + "pug-attrs": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/pug-attrs/-/pug-attrs-2.0.4.tgz", + "integrity": "sha512-TaZ4Z2TWUPDJcV3wjU3RtUXMrd3kM4Wzjbe3EWnSsZPsJ3LDI0F3yCnf2/W7PPFF+edUFQ0HgDL1IoxSz5K8EQ==", + "requires": { + "constantinople": "^3.0.1", + "js-stringify": "^1.0.1", + "pug-runtime": "^2.0.5" + } + }, + "pug-code-gen": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pug-code-gen/-/pug-code-gen-2.0.3.tgz", + "integrity": "sha512-r9sezXdDuZJfW9J91TN/2LFbiqDhmltTFmGpHTsGdrNGp3p4SxAjjXEfnuK2e4ywYsRIVP0NeLbSAMHUcaX1EA==", + "requires": { + "constantinople": "^3.1.2", + "doctypes": "^1.1.0", + "js-stringify": "^1.0.1", + "pug-attrs": "^2.0.4", + "pug-error": "^1.3.3", + "pug-runtime": "^2.0.5", + "void-elements": "^2.0.1", + "with": "^5.0.0" + } + }, + "pug-error": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/pug-error/-/pug-error-1.3.3.tgz", + "integrity": "sha512-qE3YhESP2mRAWMFJgKdtT5D7ckThRScXRwkfo+Erqga7dyJdY3ZquspprMCj/9sJ2ijm5hXFWQE/A3l4poMWiQ==" + }, + "pug-filters": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/pug-filters/-/pug-filters-3.1.1.tgz", + "integrity": "sha512-lFfjNyGEyVWC4BwX0WyvkoWLapI5xHSM3xZJFUhx4JM4XyyRdO8Aucc6pCygnqV2uSgJFaJWW3Ft1wCWSoQkQg==", + "requires": { + "clean-css": "^4.1.11", + "constantinople": "^3.0.1", + "jstransformer": "1.0.0", + "pug-error": "^1.3.3", + "pug-walk": "^1.1.8", + "resolve": "^1.1.6", + "uglify-js": "^2.6.1" + } + }, + "pug-lexer": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/pug-lexer/-/pug-lexer-4.1.0.tgz", + "integrity": "sha512-i55yzEBtjm0mlplW4LoANq7k3S8gDdfC6+LThGEvsK4FuobcKfDAwt6V4jKPH9RtiE3a2Akfg5UpafZ1OksaPA==", + "requires": { + "character-parser": "^2.1.1", + "is-expression": "^3.0.0", + "pug-error": "^1.3.3" + } + }, + "pug-linker": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/pug-linker/-/pug-linker-3.0.6.tgz", + "integrity": "sha512-bagfuHttfQOpANGy1Y6NJ+0mNb7dD2MswFG2ZKj22s8g0wVsojpRlqveEQHmgXXcfROB2RT6oqbPYr9EN2ZWzg==", + "requires": { + "pug-error": "^1.3.3", + "pug-walk": "^1.1.8" + } + }, + "pug-load": { + "version": "2.0.12", + "resolved": "https://registry.npmjs.org/pug-load/-/pug-load-2.0.12.tgz", + "integrity": "sha512-UqpgGpyyXRYgJs/X60sE6SIf8UBsmcHYKNaOccyVLEuT6OPBIMo6xMPhoJnqtB3Q3BbO4Z3Bjz5qDsUWh4rXsg==", + "requires": { + "object-assign": "^4.1.0", + "pug-walk": "^1.1.8" + } + }, + "pug-parser": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/pug-parser/-/pug-parser-5.0.1.tgz", + "integrity": "sha512-nGHqK+w07p5/PsPIyzkTQfzlYfuqoiGjaoqHv1LjOv2ZLXmGX1O+4Vcvps+P4LhxZ3drYSljjq4b+Naid126wA==", + "requires": { + "pug-error": "^1.3.3", + "token-stream": "0.0.1" + } + }, + "pug-runtime": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/pug-runtime/-/pug-runtime-2.0.5.tgz", + "integrity": "sha512-P+rXKn9un4fQY77wtpcuFyvFaBww7/91f3jHa154qU26qFAnOe6SW1CbIDcxiG5lLK9HazYrMCCuDvNgDQNptw==" + }, + "pug-strip-comments": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/pug-strip-comments/-/pug-strip-comments-1.0.4.tgz", + "integrity": "sha512-i5j/9CS4yFhSxHp5iKPHwigaig/VV9g+FgReLJWWHEHbvKsbqL0oP/K5ubuLco6Wu3Kan5p7u7qk8A4oLLh6vw==", + "requires": { + "pug-error": "^1.3.3" + } + }, + "pug-walk": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/pug-walk/-/pug-walk-1.1.8.tgz", + "integrity": "sha512-GMu3M5nUL3fju4/egXwZO0XLi6fW/K3T3VTgFQ14GxNi8btlxgT5qZL//JwZFm/2Fa64J/PNS8AZeys3wiMkVA==" + }, + "punycode": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", + "integrity": "sha1-tYsBCsQMIsVldhbI0sLALHv0eew=" + }, + "qs": { + "version": "6.7.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.7.0.tgz", + "integrity": "sha512-VCdBRNFTX1fyE7Nb6FYoURo/SPe62QCaAyzJvUjwRaIsc+NePBEniHlvxFmmX56+HZphIGtV0XeCirBtpDrTyQ==" + }, + "range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==" + }, + "rasha": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/rasha/-/rasha-1.2.5.tgz", + "integrity": "sha1-cmaNox3MaKwnfFtz1hDgh80zsDE=" + }, + "raw-body": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.4.0.tgz", + "integrity": "sha512-4Oz8DUIwdvoa5qMJelxipzi/iJIi40O5cGV1wNYp5hvZP8ZN0T+jiNkL0QepXs+EsQ9XJ8ipEDoiH70ySUJP3Q==", + "requires": { + "bytes": "3.1.0", + "http-errors": "1.7.2", + "iconv-lite": "0.4.24", + "unpipe": "1.0.0" + }, + "dependencies": { + "bytes": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.0.tgz", + "integrity": "sha512-zauLjrfCG+xvoyaqLoV8bLVXXNGC4JqlxFCutSDWA6fJrTo2ZuvLYTqZ7aHBLZSMOopbzwv8f+wZcVzfVTI2Dg==" + } + } + }, + "readable-stream": { + "version": "2.3.7", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", + "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + }, + "dependencies": { + "isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=" + } + } + }, + "redirect-https": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/redirect-https/-/redirect-https-1.3.1.tgz", + "integrity": "sha512-Stex2nI+tMpZXKvy++32TiBXEy+GdpAfp3EUnl5BqCiJ5f5i6XvUSFrs7TR7IoRSlthM7ZtD89uYGTtJBXlFYg==", + "requires": { + "escape-html": "^1.0.3" + } + }, + "regenerator-runtime": { + "version": "0.11.1", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.11.1.tgz", + "integrity": "sha1-vgWtf5v30i4Fb5cmzuUBf78Z4uk=" + }, + "repeat-string": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/repeat-string/-/repeat-string-1.6.1.tgz", + "integrity": "sha1-jcrkcOHIirwtYA//Sndihtp15jc=" + }, + "request": { + "version": "2.88.2", + "resolved": "https://registry.npmjs.org/request/-/request-2.88.2.tgz", + "integrity": "sha512-MsvtOrfG9ZcrOwAW+Qi+F6HbD0CWXEh9ou77uOb7FM2WPhwT7smM833PzanhJLsgXjN89Ir6V2PczXNnMpwKhw==", + "requires": { + "aws-sign2": "~0.7.0", + "aws4": "^1.8.0", + "caseless": "~0.12.0", + "combined-stream": "~1.0.6", + "extend": "~3.0.2", + "forever-agent": "~0.6.1", + "form-data": "~2.3.2", + "har-validator": "~5.1.3", + "http-signature": "~1.2.0", + "is-typedarray": "~1.0.0", + "isstream": "~0.1.2", + "json-stringify-safe": "~5.0.1", + "mime-types": "~2.1.19", + "oauth-sign": "~0.9.0", + "performance-now": "^2.1.0", + "qs": "~6.5.2", + "safe-buffer": "^5.1.2", + "tough-cookie": "~2.5.0", + "tunnel-agent": "^0.6.0", + "uuid": "^3.3.2" + }, + "dependencies": { + "qs": { + "version": "6.5.2", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.2.tgz", + "integrity": "sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA==" + } + } + }, + "resolve": { + "version": "1.20.0", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.20.0.tgz", + "integrity": "sha512-wENBPt4ySzg4ybFQW2TT1zMQucPK95HSh/nq2CFTZVOGut2+pQvSsgtda4d26YrYcr067wjbmzOG8byDPBX63A==", + "requires": { + "is-core-module": "^2.2.0", + "path-parse": "^1.0.6" + } + }, + "right-align": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/right-align/-/right-align-0.1.3.tgz", + "integrity": "sha1-YTObci/mo1FWiSENJOFMlhSGE+8=", + "requires": { + "align-text": "^0.1.1" + } + }, + "rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "requires": { + "glob": "^7.1.3" + } + }, + "rsa-compat": { + "version": "1.9.4", + "resolved": "https://registry.npmjs.org/rsa-compat/-/rsa-compat-1.9.4.tgz", + "integrity": "sha512-VAbxPMNPl7nMizAYQDroTqdFM9TzyawpP9CsYn3IBGWAf+/BaBrD+8vgli6cpsFMoO1VF3iPmt4erOYSkSqEjw==", + "requires": { + "keypairs": "^1.2.14", + "node-forge": "^0.7.6", + "ursa-optional": "^0.9.10" + } + }, + "safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha1-mR7GnSluAxN0fVm9/St0XDX4go0=" + }, + "safe-replace": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/safe-replace/-/safe-replace-1.1.0.tgz", + "integrity": "sha1-QyghtagTmji1NGeNcS8IUP4+UjU=" + }, + "safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha1-RPoWGwGHuVSd2Eu5GAL5vYOFzWo=" + }, + "sanitize-html": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/sanitize-html/-/sanitize-html-2.4.0.tgz", + "integrity": "sha512-Y1OgkUiTPMqwZNRLPERSEi39iOebn2XJLbeiGOBhaJD/yLqtLGu6GE5w7evx177LeGgSE+4p4e107LMiydOf6A==", + "requires": { + "deepmerge": "^4.2.2", + "escape-string-regexp": "^4.0.0", + "htmlparser2": "^6.0.0", + "is-plain-object": "^5.0.0", + "klona": "^2.0.3", + "parse-srcset": "^1.0.2", + "postcss": "^8.0.2" + } + }, + "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" + } + }, + "send": { + "version": "0.17.1", + "resolved": "https://registry.npmjs.org/send/-/send-0.17.1.tgz", + "integrity": "sha512-BsVKsiGcQMFwT8UxypobUKyv7irCNRHk1T0G680vk88yf6LBByGcZJOTJCrTP2xVN6yI+XjPJcNuE3V4fT9sAg==", + "requires": { + "debug": "2.6.9", + "depd": "~1.1.2", + "destroy": "~1.0.4", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "0.5.2", + "http-errors": "~1.7.2", + "mime": "1.6.0", + "ms": "2.1.1", + "on-finished": "~2.3.0", + "range-parser": "~1.2.1", + "statuses": "~1.5.0" + }, + "dependencies": { + "ms": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz", + "integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==" + } + } + }, + "serve-static": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.14.1.tgz", + "integrity": "sha512-JMrvUwE54emCYWlTI+hGrGv5I8dEwmco/00EvkzIIsR7MqrHonbD9pO2MOfFnpFntl7ecpZs+3mW+XbQZu9QCg==", + "requires": { + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "0.17.1" + } + }, + "set-blocking": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", + "integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc=" + }, + "set-immediate-shim": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/set-immediate-shim/-/set-immediate-shim-1.0.1.tgz", + "integrity": "sha1-SysbJ+uAip+NzEgaWOXlb1mfP2E=" + }, + "setprototypeof": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.1.tgz", + "integrity": "sha512-JvdAWfbXeIGaZ9cILp38HntZSFSo3mWg6xGcJJsd+d4aRMOqauag1C63dJfDw7OaMYwEbHMOxEZ1lqVRYP2OAw==" + }, + "signal-exit": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.3.tgz", + "integrity": "sha512-VUJ49FC8U1OxwZLxIbTTrDvLnf/6TDgxZcK8wxR8zs13xpx7xbG60ndBlhNrFi2EMuFRoeDoJO7wthSLq42EjA==" + }, + "simple-concat": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz", + "integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==" + }, + "simple-get": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-3.1.0.tgz", + "integrity": "sha512-bCR6cP+aTdScaQCnQKbPKtJOKDp/hj9EDLJo3Nw4y1QksqaovlW/bnptB6/c1e+qmNIDHRK+oXFDdEqBT8WzUA==", + "requires": { + "decompress-response": "^4.2.0", + "once": "^1.3.1", + "simple-concat": "^1.0.0" + } + }, + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha1-dHIq8y6WFOnCh6jQu95IteLxomM=" + }, + "source-map-js": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-0.6.2.tgz", + "integrity": "sha512-/3GptzWzu0+0MBQFrDKzw/DvvMTUORvgY6k6jd/VS6iCR4RDTKWH6v6WPwQoUO8667uQEf9Oe38DxAYWY5F/Ug==" + }, + "sshpk": { + "version": "1.16.1", + "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.16.1.tgz", + "integrity": "sha1-+2YcC+8ps520B2nuOfpwCT1vaHc=", + "requires": { + "asn1": "~0.2.3", + "assert-plus": "^1.0.0", + "bcrypt-pbkdf": "^1.0.0", + "dashdash": "^1.12.0", + "ecc-jsbn": "~0.1.1", + "getpass": "^0.1.1", + "jsbn": "~0.1.0", + "safer-buffer": "^2.0.2", + "tweetnacl": "~0.14.0" + } + }, + "statuses": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", + "integrity": "sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow=" + }, + "string-width": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz", + "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=", + "requires": { + "code-point-at": "^1.0.0", + "is-fullwidth-code-point": "^1.0.0", + "strip-ansi": "^3.0.0" + } + }, + "string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "requires": { + "safe-buffer": "~5.1.0" + } + }, + "strip-ansi": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", + "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", + "requires": { + "ansi-regex": "^2.0.0" + } + }, + "tar": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/tar/-/tar-6.1.0.tgz", + "integrity": "sha512-DUCttfhsnLCjwoDoFcI+B2iJgYa93vBnDUATYEeRx6sntCTdN01VnqsIuTlALXla/LWooNg0yEGeB+Y8WdFxGA==", + "requires": { + "chownr": "^2.0.0", + "fs-minipass": "^2.0.0", + "minipass": "^3.0.0", + "minizlib": "^2.1.1", + "mkdirp": "^1.0.3", + "yallist": "^4.0.0" + } + }, + "to-fast-properties": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-1.0.3.tgz", + "integrity": "sha1-uDVx+k2MJbguIxsG46MFXeTKGkc=" + }, + "toidentifier": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.0.tgz", + "integrity": "sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw==" + }, + "token-stream": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/token-stream/-/token-stream-0.0.1.tgz", + "integrity": "sha1-zu78cXp2xDFvEm0LnbqlXX598Bo=" + }, + "tough-cookie": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.5.0.tgz", + "integrity": "sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g==", + "requires": { + "psl": "^1.1.28", + "punycode": "^2.1.1" + } + }, + "tunnel-agent": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", + "integrity": "sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0=", + "requires": { + "safe-buffer": "^5.0.1" + } + }, + "tweetnacl": { + "version": "0.14.5", + "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", + "integrity": "sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q=" + }, + "type": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/type/-/type-1.2.0.tgz", + "integrity": "sha512-+5nt5AAniqsCnu2cEQQdpzCAh33kVx8n0VoFidKpB1dVVLAN/F+bgVOqOJqOnEnrhp222clB5p3vUlD+1QAnfg==" + }, + "type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "requires": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + } + }, + "typedarray-to-buffer": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz", + "integrity": "sha512-zdu8XMNEDepKKR+XYOXAVPtWui0ly0NtohUscw+UmaHiAWT8hrV1rr//H6V+0DvJ3OQ19S979M0laLfX8rm82Q==", + "requires": { + "is-typedarray": "^1.0.0" + } + }, + "uglify-js": { + "version": "2.8.29", + "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-2.8.29.tgz", + "integrity": "sha1-KcVzMUgFe7Th913zW3qcty5qWd0=", + "requires": { + "source-map": "~0.5.1", + "uglify-to-browserify": "~1.0.0", + "yargs": "~3.10.0" + }, + "dependencies": { + "source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=" + } + } + }, + "uglify-to-browserify": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/uglify-to-browserify/-/uglify-to-browserify-1.0.2.tgz", + "integrity": "sha1-bgkk1r2mta/jSeOabWMoUKD4grc=", + "optional": true + }, + "unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw=" + }, + "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" + } + }, + "ursa-optional": { + "version": "0.9.10", + "resolved": "https://registry.npmjs.org/ursa-optional/-/ursa-optional-0.9.10.tgz", + "integrity": "sha1-8uq/4LYAHb8Hp4dAzQpuW6brJVQ=", + "optional": true, + "requires": { + "bindings": "^1.3.0", + "nan": "^2.11.1" + } + }, + "utf-8-validate": { + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/utf-8-validate/-/utf-8-validate-5.0.5.tgz", + "integrity": "sha512-+pnxRYsS/axEpkrrEpzYfNZGXp0IjC/9RIxwM5gntY4Koi8SHmUGSfxfWqxZdRxrtaoVstuOzUp/rbs3JSPELQ==", + "requires": { + "node-gyp-build": "^4.2.0" + } + }, + "util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=" + }, + "utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM=" + }, + "uuid": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz", + "integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==" + }, + "vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha1-IpnwLG3tMNSllhsLn3RSShj2NPw=" + }, + "verror": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz", + "integrity": "sha1-OhBcoXBTr1XW4nDB+CiGguGNpAA=", + "requires": { + "assert-plus": "^1.0.0", + "core-util-is": "1.0.2", + "extsprintf": "^1.2.0" + } + }, + "void-elements": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/void-elements/-/void-elements-2.0.1.tgz", + "integrity": "sha1-wGavtYK7HLQSjWDqkjkulNXp2+w=" + }, + "websocket": { + "version": "1.0.34", + "resolved": "https://registry.npmjs.org/websocket/-/websocket-1.0.34.tgz", + "integrity": "sha512-PRDso2sGwF6kM75QykIesBijKSVceR6jL2G8NGYyq2XrItNC2P5/qL5XeR056GhA+Ly7JMFvJb9I312mJfmqnQ==", + "requires": { + "bufferutil": "^4.0.1", + "debug": "^2.2.0", + "es5-ext": "^0.10.50", + "typedarray-to-buffer": "^3.1.5", + "utf-8-validate": "^5.0.2", + "yaeti": "^0.0.6" + } + }, + "wide-align": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.3.tgz", + "integrity": "sha512-QGkOQc8XL6Bt5PwnsExKBPuMKBxnGxWWW3fU55Xt4feHozMUhdUMaBCk290qpm/wG5u/RSKzwdAC4i51YigihA==", + "requires": { + "string-width": "^1.0.2 || 2" + } + }, + "window-size": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/window-size/-/window-size-0.1.0.tgz", + "integrity": "sha1-VDjNLqk7IC76Ohn+iIeu58lPnJ0=" + }, + "with": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/with/-/with-5.1.1.tgz", + "integrity": "sha1-+k2qktrzLE6pTtRTyB8EaGtXXf4=", + "requires": { + "acorn": "^3.1.0", + "acorn-globals": "^3.0.0" + }, + "dependencies": { + "acorn": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-3.3.0.tgz", + "integrity": "sha1-ReN/s56No/JbruP/U2niu18iAXo=" + } + } + }, + "wordwrap": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-0.0.2.tgz", + "integrity": "sha1-t5Zpu0LstAn4PVg8rVLKF+qhZD8=" + }, + "wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" + }, + "ws": { + "version": "7.5.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.0.tgz", + "integrity": "sha512-6ezXvzOZupqKj4jUqbQ9tXuJNo+BR2gU8fFRk3XCP3e0G6WT414u5ELe6Y0vtp7kmSJ3F7YWObSNr1ESsgi4vw==" + }, + "yaeti": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/yaeti/-/yaeti-0.0.6.tgz", + "integrity": "sha1-8m9ITXJoTPQr7ft2lwqhYI+/lXc=" + }, + "yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" + }, + "yargs": { + "version": "3.10.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-3.10.0.tgz", + "integrity": "sha1-9+572FfdfB0tOMDnTvvWgdFDH9E=", + "requires": { + "camelcase": "^1.0.2", + "cliui": "^2.1.0", + "decamelize": "^1.0.0", + "window-size": "0.1.0" + } + } + } +} diff --git a/server/package.json b/server/package.json new file mode 100644 index 00000000..17246ee6 --- /dev/null +++ b/server/package.json @@ -0,0 +1,39 @@ +{ + "name": "sonicpad-server", + "version": "1.0.0", + "description": "Web server", + "main": "server.js", + "dependencies": { + "@fontsource/source-sans-pro": "^4.4.5", + "@fontsource/ubuntu": "^4.4.5", + "@fontsource/ubuntu-mono": "^4.4.5", + "@fortawesome/fontawesome-free": "^5.15.3", + "ace-builds": "^1.4.12", + "canvas": "^2.8.0", + "compression": "^1.7.4", + "cookie-parser": "^1.4.5", + "crypto-js": "^3.3.0", + "dompurify": "^2.2.8", + "engine.io": "^3.5.0", + "express": "^4.17.1", + "express-force-https": "^1.0.0", + "fontsource-source-sans-pro": "^4.0.0", + "greenlock-express": "^4.0.3", + "greenlock-store-fs": "^3.2.2", + "jquery": "^3.6.0", + "jquery-ui-dist": "^1.12.1", + "jszip": "^3.6.0", + "le-acme-core": "^2.1.4", + "marked": "^2.1.2", + "pidusage": "^2.0.21", + "pug": "^2.0.4", + "sanitize-html": "^2.4.0", + "websocket": "^1.0.34", + "ws": "^7.5.0" + }, + "devDependencies": {}, + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "author": "pmgl" +} diff --git a/server/ratelimiter.coffee b/server/ratelimiter.coffee new file mode 100644 index 00000000..a6f521ff --- /dev/null +++ b/server/ratelimiter.coffee @@ -0,0 +1,76 @@ +class @RateLimiter + constructor:(@server)-> + @map = {} + + @map.request = new RateLimiterClass(@,1,1000*100) # > 1000 home page loads per minute (> 5000 realtime) + @map.request_ip = new RateLimiterClass(@,1,100*100) # > 100 home page loads per minute per ip (> 500 realtime) + + @map.login_ip = new RateLimiterClass(@,1,20) # 20 tentatives de login par minute de la même IP + @map.login_user = new RateLimiterClass(@,2,10) # 10 tentatives de login par username par 2 minutes + + @map.send_mail_user = new RateLimiterClass(@,5,5) # 5 mails max en 5 minutes + + @map.create_account_ip = new RateLimiterClass(@,30,30) # 30 comptes pour 30 minutes => classroom check + + @map.create_project_user = new RateLimiterClass(@,60,10) # max 10 projects per hour + + @map.create_file_user = new RateLimiterClass(@,5,40) # max 40 new files per 5 minutes + #@map.change_file_user = new RateLimiterClass(@,10,200) + + @map.post_comment_user = new RateLimiterClass(@,10,10) # max 10 new comments per 10 minutes + + @map.create_forum_post = new RateLimiterClass(@,60,10) # max 10 new posts per hour + @map.create_forum_reply = new RateLimiterClass(@,10,10) # max 10 new replies per 10 minutes + @map.search_forum = new RateLimiterClass(@,1,15) # max 15 searches per minute + + @interval = setInterval (()=>@log()),5000 + + accept:(type,key)-> + if @map[type]? + @map[type].accept(key) + else + false + + close:()-> + clearInterval @interval + + log:()-> + for key,limiter of @map + limiter.checkTime() + max = Math.round(limiter.max_count/limiter.quantity*100) + if max>10 + console.info "rate limiter #{key} at #{max}% for #{limiter.maxer}" + return + +class RateLimiterClass + constructor:(@rate_limiter,@minutes,@quantity)-> + @current = Math.floor(Date.now()/(@minutes*60000)) + @requests = {} + @max_count = 0 + @maxer = "" + + checkTime:()-> + now = Math.floor(Date.now()/(@minutes*60000)) + if now != @current + @current = now + @requests = {} + @max_count = 0 + @maxer = "" + + accept:(key)-> + @checkTime() + + return false if not key? + if not @requests[key]? + @requests[key] = 0 + + if @requests[key] >= @quantity + false + else + @requests[key]++ + if @requests[key]>@max_count + @max_count = @requests[key] + @maxer = key + true + +module.exports = @RateLimiter diff --git a/server/ratelimiter.js b/server/ratelimiter.js new file mode 100644 index 00000000..5c122a78 --- /dev/null +++ b/server/ratelimiter.js @@ -0,0 +1,101 @@ +var RateLimiterClass; + +this.RateLimiter = (function() { + function RateLimiter(server) { + this.server = server; + this.map = {}; + this.map.request = new RateLimiterClass(this, 1, 1000 * 100); + this.map.request_ip = new RateLimiterClass(this, 1, 100 * 100); + this.map.login_ip = new RateLimiterClass(this, 1, 20); + this.map.login_user = new RateLimiterClass(this, 2, 10); + this.map.send_mail_user = new RateLimiterClass(this, 5, 5); + this.map.create_account_ip = new RateLimiterClass(this, 30, 30); + this.map.create_project_user = new RateLimiterClass(this, 60, 10); + this.map.create_file_user = new RateLimiterClass(this, 5, 40); + this.map.post_comment_user = new RateLimiterClass(this, 10, 10); + this.map.create_forum_post = new RateLimiterClass(this, 60, 10); + this.map.create_forum_reply = new RateLimiterClass(this, 10, 10); + this.map.search_forum = new RateLimiterClass(this, 1, 15); + this.interval = setInterval(((function(_this) { + return function() { + return _this.log(); + }; + })(this)), 5000); + } + + RateLimiter.prototype.accept = function(type, key) { + if (this.map[type] != null) { + return this.map[type].accept(key); + } else { + return false; + } + }; + + RateLimiter.prototype.close = function() { + return clearInterval(this.interval); + }; + + RateLimiter.prototype.log = function() { + var key, limiter, max, ref; + ref = this.map; + for (key in ref) { + limiter = ref[key]; + limiter.checkTime(); + max = Math.round(limiter.max_count / limiter.quantity * 100); + if (max > 10) { + console.info("rate limiter " + key + " at " + max + "% for " + limiter.maxer); + } + } + }; + + return RateLimiter; + +})(); + +RateLimiterClass = (function() { + function RateLimiterClass(rate_limiter, minutes, quantity) { + this.rate_limiter = rate_limiter; + this.minutes = minutes; + this.quantity = quantity; + this.current = Math.floor(Date.now() / (this.minutes * 60000)); + this.requests = {}; + this.max_count = 0; + this.maxer = ""; + } + + RateLimiterClass.prototype.checkTime = function() { + var now; + now = Math.floor(Date.now() / (this.minutes * 60000)); + if (now !== this.current) { + this.current = now; + this.requests = {}; + this.max_count = 0; + return this.maxer = ""; + } + }; + + RateLimiterClass.prototype.accept = function(key) { + this.checkTime(); + if (key == null) { + return false; + } + if (this.requests[key] == null) { + this.requests[key] = 0; + } + if (this.requests[key] >= this.quantity) { + return false; + } else { + this.requests[key]++; + if (this.requests[key] > this.max_count) { + this.max_count = this.requests[key]; + this.maxer = key; + } + return true; + } + }; + + return RateLimiterClass; + +})(); + +module.exports = this.RateLimiter; diff --git a/server/server.coffee b/server/server.coffee new file mode 100644 index 00000000..c7a06908 --- /dev/null +++ b/server/server.coffee @@ -0,0 +1,209 @@ +compression = require "compression" +express = require "express" +cookieParser = require('cookie-parser') +fs = require "fs" +path = require "path" +DB = require __dirname+"/db/db.js" +FileStorage = require __dirname+"/filestorage/filestorage.js" +Content = require __dirname+"/content/content.js" +WebApp = require __dirname+"/webapp.js" +Session = require __dirname+"/session/session.js" +RateLimiter = require __dirname+"/ratelimiter.js" +BuildManager = require __dirname+"/build/buildmanager.js" +WebSocket = require "ws" +process = require "process" + +class @Server + constructor:()-> + process.chdir __dirname + @config = + realm: "local" + + @mailer = # STUB + sendMail:(recipient,subject,text)-> + console.info "send mail to:#{recipient} subject:#{subject} text:#{text}" + + @stats = # STUB + set:(name,value)-> + max:(name,value)-> + unique:(name,id)-> + inc:(name)-> + stop:()-> + + @last_backup_time = 0 + + fs.readFile "../config.json",(err,data)=> + if not err + @config = JSON.parse(data) + console.info "config.json loaded" + else + console.info "No config.json file found, running local with default settings" + + if @config.realm == "production" + @PORT = 443 + @PROD = true + else + @PORT = @config.port or 8080 + @PROD = false + + @loadPlugins ()=> + @create() + + create:()-> + app = express() + static_files = "../static" + + @date_started = Date.now() + + @rate_limiter = new RateLimiter @ + app.use (req,res,next)=> + if @rate_limiter.accept("request","general") and @rate_limiter.accept("request_ip",req.connection.remoteAddress) + next() + else + res.status(500).send "" + @stats.inc("http_requests") + @stats.unique("ip_addresses",req.connection.remoteAddress) + referrer = req.get("Referrer") + if referrer? and not referrer.startsWith("http://localhost") and not referrer.startsWith("https://microstudio.io") and not referrer.startsWith("https://microstudio.dev") + @stats.unique("referrer|"+referrer,req.connection.remoteAddress) + + app.use(compression()) + + app.use(cookieParser()) + + for plugin in @plugins + if plugin.getStaticFolder? + folder = plugin.getStaticFolder() + app.use(express.static(folder)) + + app.use(express.static(static_files)) + app.use("/lib/fontlib/ubuntu",express.static("node_modules/@fontsource/ubuntu")) + app.use("/lib/fontlib/ubuntu-mono",express.static("node_modules/@fontsource/ubuntu-mono")) + app.use("/lib/fontlib/source-sans-pro",express.static("node_modules/@fontsource/source-sans-pro")) + app.use("/lib/fontlib/fontawesome",express.static("node_modules/@fortawesome/fontawesome-free")) + app.use("/lib/ace",express.static("node_modules/ace-builds/src-min")) + app.use("/lib/marked/marked.js",express.static("node_modules/marked/marked.min.js")) + app.use("/lib/dompurify/purify.js",express.static("node_modules/dompurify/dist/purify.min.js")) + app.use("/lib/jquery/jquery.js",express.static("node_modules/jquery/dist/jquery.min.js")) + app.use("/lib/jquery-ui",express.static("node_modules/jquery-ui-dist")) + + @db = new DB "../data",(db)=> + for plugin in @plugins + if plugin.dbLoaded? + plugin.dbLoaded(db) + + if @PROD + require('greenlock-express').init + packageRoot: __dirname + configDir: "./greenlock.d" + maintainerEmail: "contact@microstudio.dev" + cluster: false + .ready (glx)=> + @httpserver = glx.httpsServer() + @use_cache = true + glx.serveApp app + @start(app,db) + else + @httpserver = require("http").createServer(app).listen(@PORT) + @use_cache = false + @start(app,db) + + start:(app,db)-> + @active_users = 0 + + @io = new WebSocket.Server + server: @httpserver + maxPayload: 40000000 + + @sessions = [] + + @io.on "connection",(socket,request)=> + socket.request = request + socket.remoteAddress = request.connection.remoteAddress + @sessions.push new Session @,socket + + console.info "MAX PAYLOAD = "+@io.options.maxPayload + + @session_check = setInterval (()=>@sessionCheck()),10000 + + @content = new Content @,db,new FileStorage() + @build_manager = new BuildManager @ + @webapp = new WebApp @,app + + process.on 'SIGINT', ()=> + console.log "caught INT signal" + @exit() + + process.on 'SIGTERM', ()=> + console.log "caught TERM signal" + @exit() + + #process.on 'SIGKILL', ()=> + # console.log "caught KILL signal" + # @exit() + + @exitcheck = setInterval (()=> + if fs.existsSync("exit") + @exit() + fs.unlinkSync("exit") + + if fs.existsSync("update") + @webapp.concatenator.refresh() + fs.unlinkSync("update") + ),2000 + + exit:()=> + if @exited + process.exit(0) + @httpserver.close() + @stats.stop() + @rate_limiter.close() + @io.close() + @db.close() + @content.close() + clearInterval @exitcheck + clearInterval @session_check + @exited = true + setTimeout (()=>@exit()),5000 + + sessionCheck:()-> + for s in @sessions + if s? + s.timeCheck() + return + + sessionClosed:(session)-> + index = @sessions.indexOf(session) + if index>=0 + @sessions.splice index,1 + + loadPlugins:(callback)-> + @plugins = [] + + fs.readdir "../plugins",(err,files)=> + files = [] if not files? + + funk = ()=> + if files.length==0 + callback() + else + f = files.splice(0,1)[0] + @loadPlugin "../plugins/#{f}",funk + + funk() + + loadPlugin:(folder,callback)-> + if fs.existsSync "#{folder}/index.js" + try + Plugin = require "#{folder}/index.js" + p = new Plugin(@) + @plugins.push p + console.info "loaded plugin #{folder}" + catch err + console.error err + callback() + else + console.info "plugin #{folder} has no index.js" + callback() + +server = new @Server() diff --git a/server/server.js b/server/server.js new file mode 100644 index 00000000..a7d2a22d --- /dev/null +++ b/server/server.js @@ -0,0 +1,280 @@ +var BuildManager, Content, DB, FileStorage, RateLimiter, Session, WebApp, WebSocket, compression, cookieParser, express, fs, path, process, server, + bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; }; + +compression = require("compression"); + +express = require("express"); + +cookieParser = require('cookie-parser'); + +fs = require("fs"); + +path = require("path"); + +DB = require(__dirname + "/db/db.js"); + +FileStorage = require(__dirname + "/filestorage/filestorage.js"); + +Content = require(__dirname + "/content/content.js"); + +WebApp = require(__dirname + "/webapp.js"); + +Session = require(__dirname + "/session/session.js"); + +RateLimiter = require(__dirname + "/ratelimiter.js"); + +BuildManager = require(__dirname + "/build/buildmanager.js"); + +WebSocket = require("ws"); + +process = require("process"); + +this.Server = (function() { + function Server() { + this.exit = bind(this.exit, this); + process.chdir(__dirname); + this.config = { + realm: "local" + }; + this.mailer = { + sendMail: function(recipient, subject, text) { + return console.info("send mail to:" + recipient + " subject:" + subject + " text:" + text); + } + }; + this.stats = { + set: function(name, value) {}, + max: function(name, value) {}, + unique: function(name, id) {}, + inc: function(name) {}, + stop: function() {} + }; + this.last_backup_time = 0; + fs.readFile("../config.json", (function(_this) { + return function(err, data) { + if (!err) { + _this.config = JSON.parse(data); + console.info("config.json loaded"); + } else { + console.info("No config.json file found, running local with default settings"); + } + if (_this.config.realm === "production") { + _this.PORT = 443; + _this.PROD = true; + } else { + _this.PORT = _this.config.port || 8080; + _this.PROD = false; + } + return _this.loadPlugins(function() { + return _this.create(); + }); + }; + })(this)); + } + + Server.prototype.create = function() { + var app, folder, i, len, plugin, ref, static_files; + app = express(); + static_files = "../static"; + this.date_started = Date.now(); + this.rate_limiter = new RateLimiter(this); + app.use((function(_this) { + return function(req, res, next) { + var referrer; + if (_this.rate_limiter.accept("request", "general") && _this.rate_limiter.accept("request_ip", req.connection.remoteAddress)) { + next(); + } else { + res.status(500).send(""); + } + _this.stats.inc("http_requests"); + _this.stats.unique("ip_addresses", req.connection.remoteAddress); + referrer = req.get("Referrer"); + if ((referrer != null) && !referrer.startsWith("http://localhost") && !referrer.startsWith("https://microstudio.io") && !referrer.startsWith("https://microstudio.dev")) { + return _this.stats.unique("referrer|" + referrer, req.connection.remoteAddress); + } + }; + })(this)); + app.use(compression()); + app.use(cookieParser()); + ref = this.plugins; + for (i = 0, len = ref.length; i < len; i++) { + plugin = ref[i]; + if (plugin.getStaticFolder != null) { + folder = plugin.getStaticFolder(); + app.use(express["static"](folder)); + } + } + app.use(express["static"](static_files)); + app.use("/lib/fontlib/ubuntu", express["static"]("node_modules/@fontsource/ubuntu")); + app.use("/lib/fontlib/ubuntu-mono", express["static"]("node_modules/@fontsource/ubuntu-mono")); + app.use("/lib/fontlib/source-sans-pro", express["static"]("node_modules/@fontsource/source-sans-pro")); + app.use("/lib/fontlib/fontawesome", express["static"]("node_modules/@fortawesome/fontawesome-free")); + app.use("/lib/ace", express["static"]("node_modules/ace-builds/src-min")); + app.use("/lib/marked/marked.js", express["static"]("node_modules/marked/marked.min.js")); + app.use("/lib/dompurify/purify.js", express["static"]("node_modules/dompurify/dist/purify.min.js")); + app.use("/lib/jquery/jquery.js", express["static"]("node_modules/jquery/dist/jquery.min.js")); + app.use("/lib/jquery-ui", express["static"]("node_modules/jquery-ui-dist")); + return this.db = new DB("../data", (function(_this) { + return function(db) { + var j, len1, ref1; + ref1 = _this.plugins; + for (j = 0, len1 = ref1.length; j < len1; j++) { + plugin = ref1[j]; + if (plugin.dbLoaded != null) { + plugin.dbLoaded(db); + } + } + if (_this.PROD) { + return require('greenlock-express').init({ + packageRoot: __dirname, + configDir: "./greenlock.d", + maintainerEmail: "contact@microstudio.dev", + cluster: false + }).ready(function(glx) { + _this.httpserver = glx.httpsServer(); + _this.use_cache = true; + glx.serveApp(app); + return _this.start(app, db); + }); + } else { + _this.httpserver = require("http").createServer(app).listen(_this.PORT); + _this.use_cache = false; + return _this.start(app, db); + } + }; + })(this)); + }; + + Server.prototype.start = function(app, db) { + this.active_users = 0; + this.io = new WebSocket.Server({ + server: this.httpserver, + maxPayload: 40000000 + }); + this.sessions = []; + this.io.on("connection", (function(_this) { + return function(socket, request) { + socket.request = request; + socket.remoteAddress = request.connection.remoteAddress; + return _this.sessions.push(new Session(_this, socket)); + }; + })(this)); + console.info("MAX PAYLOAD = " + this.io.options.maxPayload); + this.session_check = setInterval(((function(_this) { + return function() { + return _this.sessionCheck(); + }; + })(this)), 10000); + this.content = new Content(this, db, new FileStorage()); + this.build_manager = new BuildManager(this); + this.webapp = new WebApp(this, app); + process.on('SIGINT', (function(_this) { + return function() { + console.log("caught INT signal"); + return _this.exit(); + }; + })(this)); + process.on('SIGTERM', (function(_this) { + return function() { + console.log("caught TERM signal"); + return _this.exit(); + }; + })(this)); + return this.exitcheck = setInterval(((function(_this) { + return function() { + if (fs.existsSync("exit")) { + _this.exit(); + fs.unlinkSync("exit"); + } + if (fs.existsSync("update")) { + _this.webapp.concatenator.refresh(); + return fs.unlinkSync("update"); + } + }; + })(this)), 2000); + }; + + Server.prototype.exit = function() { + if (this.exited) { + process.exit(0); + } + this.httpserver.close(); + this.stats.stop(); + this.rate_limiter.close(); + this.io.close(); + this.db.close(); + this.content.close(); + clearInterval(this.exitcheck); + clearInterval(this.session_check); + this.exited = true; + return setTimeout(((function(_this) { + return function() { + return _this.exit(); + }; + })(this)), 5000); + }; + + Server.prototype.sessionCheck = function() { + var i, len, ref, s; + ref = this.sessions; + for (i = 0, len = ref.length; i < len; i++) { + s = ref[i]; + if (s != null) { + s.timeCheck(); + } + } + }; + + Server.prototype.sessionClosed = function(session) { + var index; + index = this.sessions.indexOf(session); + if (index >= 0) { + return this.sessions.splice(index, 1); + } + }; + + Server.prototype.loadPlugins = function(callback) { + this.plugins = []; + return fs.readdir("../plugins", (function(_this) { + return function(err, files) { + var funk; + if (files == null) { + files = []; + } + funk = function() { + var f; + if (files.length === 0) { + return callback(); + } else { + f = files.splice(0, 1)[0]; + return _this.loadPlugin("../plugins/" + f, funk); + } + }; + return funk(); + }; + })(this)); + }; + + Server.prototype.loadPlugin = function(folder, callback) { + var Plugin, err, p; + if (fs.existsSync(folder + "/index.js")) { + try { + Plugin = require(folder + "/index.js"); + p = new Plugin(this); + this.plugins.push(p); + console.info("loaded plugin " + folder); + } catch (error) { + err = error; + console.error(err); + } + return callback(); + } else { + console.info("plugin " + folder + " has no index.js"); + return callback(); + } + }; + + return Server; + +})(); + +server = new this.Server(); diff --git a/server/session/projectmanager.coffee b/server/session/projectmanager.coffee new file mode 100644 index 00000000..cf9b0662 --- /dev/null +++ b/server/session/projectmanager.coffee @@ -0,0 +1,417 @@ +FILE_TYPES = require __dirname+"/../file_types.js" + +class @ProjectManager + constructor:(@project)-> + @users = [] + @listeners = [] + @files = {} + @locks = {} + @project.manager = @ + + canRead:(user)-> + return true if user == @project.owner or @project.public + for link in @project.users + return true if link.accepted and (link.user == user) + false + + canWrite:(user)-> + return true if user == @project.owner + for link in @project.users + return true if link.accepted and (link.user == user) + false + + canWriteOptions:(user)-> + @project.owner == user + + addUser:(user)-> + if @users.indexOf(user)<0 + @users.push user + + @sendCurrentLocks(user) + + addListener:(listener)-> + if @listeners.indexOf(listener)<0 + @listeners.push listener + + removeUser:(user)-> + index = @users.indexOf user + if index>=0 + @users.splice index,1 + @propagateUserListChange() + + removeListener:(listener)-> + index = @listeners.indexOf listener + if index>=0 + @listeners.splice index,1 + + lockFile:(user,file)-> + lock = @locks[file] + if lock? + if lock.user == user or Date.now()>lock.time + lock.user = user + lock.time = Date.now()+10000 + @propagateLock(user,file) + return true + else + return false + else + lock = + user: user + time: Date.now()+10000 + @locks[file] = lock + @propagateLock(user,file) + return true + + sendCurrentLocks:(user)-> + for file,lock of @locks + if Date.now() + lock = @locks[file] + if lock? + lock.user == user or Date.now()>lock.time + else + true + + getFileVersion:(file)-> + info = @project.getFileInfo(file) + if info.version? + info.version + else + 0 + + setFileVersion:(file,version)-> + @project.setFileInfo(file,"version",version) + + getFileSize:(file)-> + info = @project.getFileInfo(file) + if info.size? + info.size + else + 0 + + setFileSize:(file,size)-> + @project.setFileInfo(file,"size",size) + + getFileProperties:(file)-> + info = @project.getFileInfo(file) + if info.properties? + info.properties + else + {} + + setFileProperties:(file,properties)-> + @project.setFileInfo(file,"properties",properties) + + propagateUserListChange:()-> + for user in @users + if user? + user.send + name:"project_user_list" + project: @project.id + users: @project.listUsers() + return + + propagateLock:(user,file)-> + for u in @users + if u? and u != user + u.send + name: "project_file_locked" + project: @project.id + file: file + user: user.user.nick + return + + propagateFileChange:(author,file,version,content,properties)-> + for user in @users + if user? and user != author + user.send + name: "project_file_update" + project: @project.id + file: file + version: version + content: content + properties: properties + + for listener in @listeners + if listener? + listener.sendProjectFileUpdated file.split("/")[0],file.split("/")[1].split(".")[0],version,content,properties + return + + propagateFileDeleted:(author,file)-> + for user in @users + if user? and user != author + user.send + name: "project_file_deleted" + project: @project.id + file: file + + for listener in @listeners + if listener? + listener.sendProjectFileDeleted file.split("/")[0],file.split("/")[1] + return + + propagateOptions:(author)-> + for user in @users + if user? and user != author + user.send + name: "project_options_updated" + project: @project.id + title: @project.title + slug: @project.slug + platforms: @project.platforms + controls: @project.controls + orientation: @project.orientation + aspect: @project.aspect + public: @project.public + + for listener in @listeners + if listener? + listener.send + name: "project_options_updated" + title: @project.title + slug: @project.slug + platforms: @project.platforms + controls: @project.controls + orientation: @project.orientation + aspect: @project.aspect + public: @project.public + + return + + inviteUser:(source,user)-> + if source.user == @project.owner + @project.inviteUser user + @propagateUserListChange() + for li in user.listeners + li.getProjectList() + return + + acceptInvite:(user)-> + for link in @project.users + if user == link.user and not link.accepted + link.accepted = true + return @propagateUserListChange() + for li in user.listeners + li.getProjectList() + return + + removeUser:(source,user)-> + if source.user == @project.owner or source.user == user + for link in @project.users + if user == link.user + link.remove() + return @propagateUserListChange() + for li in user.listeners + li.getProjectList() + return + + listFiles:(folder,callback)-> + file = "#{@project.owner.id}/#{@project.id}/#{folder}" + + @project.content.files.list file,(files)=> + res = [] + files = files or [] + for f in files + if not f.startsWith(".") + res.push + file: f + version: @getFileVersion(folder+"/"+f) + size: @getFileSize(folder+"/"+f) + properties: @getFileProperties(folder+"/"+f) + + callback(res) + + getFileVersions:(callback)-> + res = {} + jobs = [] + + for folder,t of FILE_TYPES + res[t.property] = {} + jobs.push t + + funk = ()=> + if jobs.length>0 + job = jobs.splice(0,1)[0] + @listFiles job.folder,(files)=> + for f in files + name = f.file.split(".")[0] + ext = f.file.split(".")[1] + res[job.property][name] = + version: f.version + properties: f.properties + ext: ext + funk() + else + callback(res) + + funk() + + listProjectFiles:(session,data)-> + return console.log "unauthorized user" if not @canRead session.user + + file = "#{@project.owner.id}/#{@project.id}/#{data.folder}" + + @project.content.files.list file,(files)=> + res = [] + files = files or [] + for f in files + if not f.startsWith(".") + res.push + file: f + version: @getFileVersion(data.folder+"/"+f) + size: @getFileSize(data.folder+"/"+f) + properties: @getFileProperties(data.folder+"/"+f) + + session.send + name: "list_project_files" + files: res + request_id: data.request_id + + + readProjectFile:(session,data)-> + #console.info "projectmanager.readProjectFile" + return if not @canRead session.user + + file = "#{@project.owner.id}/#{@project.id}/#{data.file}" + + encoding = if data.file.endsWith(".ms") or data.file.endsWith(".json") or data.file.endsWith(".md") then "text" else "base64" + + @project.content.files.read file,encoding,(content)=> + out = if data.file.endsWith(".ms") or data.file.endsWith(".json") or data.file.endsWith(".md") then "utf8" else "base64" + + if content? + session.send + name: "read_project_file" + content: content.toString(out) + request_id: data.request_id + + writeProjectFile:(session,data)-> + return if not @canWrite session.user + return if @project.deleted + return if not data.file? or not data.content? + return if data.content.length>10000000 # max file size 10 megabytes + if not /^(ms|sprites|maps|sounds|music|doc|assets)\/[a-z0-9_]{1,40}(-[a-z0-9_]{1,40}){0,4}.(ms|png|json|wav|mp3|md|glb|jpg)$/.test(data.file) + console.info "wrong file name: #{data.file}" + return + + version = @getFileVersion data.file + if version == 0 # new file + if not session.server.rate_limiter.accept("create_file_user",session.user.id) + return + + file = "#{@project.owner.id}/#{@project.id}/#{data.file}" + + if data.file.endsWith(".ms") or data.file.endsWith(".json") or data.file.endsWith(".md") + content = data.content + else + content = new Buffer(data.content,"base64") + + @project.content.files.write file,content,()=> + version += 1 + @setFileVersion data.file,version + @setFileSize data.file,content.length + if data.properties? + @setFileProperties data.file,data.properties + + if data.request_id? + session.send + name: "write_project_file" + version: version + size: content.length + request_id: data.request_id + + @propagateFileChange(session,data.file,version,data.content,data.properties) + @project.touch() + + if data.thumbnail? + th = new Buffer(data.thumbnail,"base64") + f = file.split("/") + f[2] += "_th" + f[3] = f[3].split(".")[0]+".png" + f = f.join("/") + @project.content.files.write f,th,()=> + console.info "thumbnail saved" + + renameProjectFile:(session,data)-> + return if not @canWrite session.user + return if not data.source? + return if not data.dest? + + if not /^(ms|sprites|maps|sounds|music|doc|assets)\/[a-z0-9_]{1,40}(-[a-z0-9_]{1,40}){0,4}.(ms|png|json|wav|mp3|md|glb|jpg)$/.test(data.source) + console.info "wrong source name: #{data.source}" + return + + if not /^(ms|sprites|maps|sounds|music|doc|assets)\/[a-z0-9_]{1,40}(-[a-z0-9_]{1,40}){0,4}.(ms|png|json|wav|mp3|md|glb|jpg)$/.test(data.dest) + console.info "wrong dest name: #{data.dest}" + return + + source = "#{@project.owner.id}/#{@project.id}/#{data.source}" + dest = "#{@project.owner.id}/#{@project.id}/#{data.dest}" + + @project.content.files.read source,"binary",(content)=> + if content? + @project.content.files.write dest,content,()=> + @project.deleteFileInfo(data.source) + @setFileSize data.dest,content.length + @project.touch() + @project.content.files.delete source,()=> + if data.thumbnail + source = source.split("/") + source[2] += "_th" + source[3] = source[3].split(".")[0]+".png" + source = source.join("/") + + dest = dest.split("/") + dest[2] += "_th" + dest[3] = dest[3].split(".")[0]+".png" + dest = dest.join("/") + + @project.content.files.read source,"binary",(thumbnail)=> + if thumbnail? + @project.content.files.write dest,thumbnail,()=> + session.send + name: "rename_project_file" + request_id: data.request_id + @propagateFileDeleted(session,data.source) + @propagateFileChange(session,data.dest,0,null,{}) + + else + session.send + name: "rename_project_file" + request_id: data.request_id + @propagateFileDeleted(session,data.source) + @propagateFileChange(session,data.dest,0,null,{}) + + deleteProjectFile:(session,data)-> + return if not @canWrite session.user + return if not data.file? + + file = "#{@project.owner.id}/#{@project.id}/#{data.file}" + + @project.content.files.delete file,()=> + @project.deleteFileInfo(data.file) + + if data.request_id? + session.send + name: "delete_project_file" + request_id: data.request_id + + @propagateFileDeleted(session,data.file) + @project.touch() + + if data.thumbnail + f = file.split("/") + f[2] += "_th" + f[3] = f[3].split(".")[0]+".png" + f = f.join("/") + @project.content.files.delete f,()=> + +module.exports = @ProjectManager diff --git a/server/session/projectmanager.js b/server/session/projectmanager.js new file mode 100644 index 00000000..7a556401 --- /dev/null +++ b/server/session/projectmanager.js @@ -0,0 +1,613 @@ +var FILE_TYPES; + +FILE_TYPES = require(__dirname + "/../file_types.js"); + +this.ProjectManager = (function() { + function ProjectManager(project) { + this.project = project; + this.users = []; + this.listeners = []; + this.files = {}; + this.locks = {}; + this.project.manager = this; + } + + ProjectManager.prototype.canRead = function(user) { + var i, len, link, ref; + if (user === this.project.owner || this.project["public"]) { + return true; + } + ref = this.project.users; + for (i = 0, len = ref.length; i < len; i++) { + link = ref[i]; + if (link.accepted && (link.user === user)) { + return true; + } + } + return false; + }; + + ProjectManager.prototype.canWrite = function(user) { + var i, len, link, ref; + if (user === this.project.owner) { + return true; + } + ref = this.project.users; + for (i = 0, len = ref.length; i < len; i++) { + link = ref[i]; + if (link.accepted && (link.user === user)) { + return true; + } + } + return false; + }; + + ProjectManager.prototype.canWriteOptions = function(user) { + return this.project.owner === user; + }; + + ProjectManager.prototype.addUser = function(user) { + if (this.users.indexOf(user) < 0) { + this.users.push(user); + } + return this.sendCurrentLocks(user); + }; + + ProjectManager.prototype.addListener = function(listener) { + if (this.listeners.indexOf(listener) < 0) { + return this.listeners.push(listener); + } + }; + + ProjectManager.prototype.removeUser = function(user) { + var index; + index = this.users.indexOf(user); + if (index >= 0) { + this.users.splice(index, 1); + } + return this.propagateUserListChange(); + }; + + ProjectManager.prototype.removeListener = function(listener) { + var index; + index = this.listeners.indexOf(listener); + if (index >= 0) { + return this.listeners.splice(index, 1); + } + }; + + ProjectManager.prototype.lockFile = function(user, file) { + var lock; + lock = this.locks[file]; + if (lock != null) { + if (lock.user === user || Date.now() > lock.time) { + lock.user = user; + lock.time = Date.now() + 10000; + this.propagateLock(user, file); + return true; + } else { + return false; + } + } else { + lock = { + user: user, + time: Date.now() + 10000 + }; + this.locks[file] = lock; + this.propagateLock(user, file); + return true; + } + }; + + ProjectManager.prototype.sendCurrentLocks = function(user) { + var file, lock, ref; + ref = this.locks; + for (file in ref) { + lock = ref[file]; + if (Date.now() < lock.time) { + user.send({ + name: "project_file_locked", + project: this.project.id, + file: file, + user: lock.user.user.nick + }); + } + } + }; + + ProjectManager.prototype.canWrite = function(user, file) { + var lock; + lock = this.locks[file]; + if (lock != null) { + return lock.user === user || Date.now() > lock.time; + } else { + return true; + } + }; + + ProjectManager.prototype.getFileVersion = function(file) { + var info; + info = this.project.getFileInfo(file); + if (info.version != null) { + return info.version; + } else { + return 0; + } + }; + + ProjectManager.prototype.setFileVersion = function(file, version) { + return this.project.setFileInfo(file, "version", version); + }; + + ProjectManager.prototype.getFileSize = function(file) { + var info; + info = this.project.getFileInfo(file); + if (info.size != null) { + return info.size; + } else { + return 0; + } + }; + + ProjectManager.prototype.setFileSize = function(file, size) { + return this.project.setFileInfo(file, "size", size); + }; + + ProjectManager.prototype.getFileProperties = function(file) { + var info; + info = this.project.getFileInfo(file); + if (info.properties != null) { + return info.properties; + } else { + return {}; + } + }; + + ProjectManager.prototype.setFileProperties = function(file, properties) { + return this.project.setFileInfo(file, "properties", properties); + }; + + ProjectManager.prototype.propagateUserListChange = function() { + var i, len, ref, user; + ref = this.users; + for (i = 0, len = ref.length; i < len; i++) { + user = ref[i]; + if (user != null) { + user.send({ + name: "project_user_list", + project: this.project.id, + users: this.project.listUsers() + }); + } + } + }; + + ProjectManager.prototype.propagateLock = function(user, file) { + var i, len, ref, u; + ref = this.users; + for (i = 0, len = ref.length; i < len; i++) { + u = ref[i]; + if ((u != null) && u !== user) { + u.send({ + name: "project_file_locked", + project: this.project.id, + file: file, + user: user.user.nick + }); + } + } + }; + + ProjectManager.prototype.propagateFileChange = function(author, file, version, content, properties) { + var i, j, len, len1, listener, ref, ref1, user; + ref = this.users; + for (i = 0, len = ref.length; i < len; i++) { + user = ref[i]; + if ((user != null) && user !== author) { + user.send({ + name: "project_file_update", + project: this.project.id, + file: file, + version: version, + content: content, + properties: properties + }); + } + } + ref1 = this.listeners; + for (j = 0, len1 = ref1.length; j < len1; j++) { + listener = ref1[j]; + if (listener != null) { + listener.sendProjectFileUpdated(file.split("/")[0], file.split("/")[1].split(".")[0], version, content, properties); + } + } + }; + + ProjectManager.prototype.propagateFileDeleted = function(author, file) { + var i, j, len, len1, listener, ref, ref1, user; + ref = this.users; + for (i = 0, len = ref.length; i < len; i++) { + user = ref[i]; + if ((user != null) && user !== author) { + user.send({ + name: "project_file_deleted", + project: this.project.id, + file: file + }); + } + } + ref1 = this.listeners; + for (j = 0, len1 = ref1.length; j < len1; j++) { + listener = ref1[j]; + if (listener != null) { + listener.sendProjectFileDeleted(file.split("/")[0], file.split("/")[1]); + } + } + }; + + ProjectManager.prototype.propagateOptions = function(author) { + var i, j, len, len1, listener, ref, ref1, user; + ref = this.users; + for (i = 0, len = ref.length; i < len; i++) { + user = ref[i]; + if ((user != null) && user !== author) { + user.send({ + name: "project_options_updated", + project: this.project.id, + title: this.project.title, + slug: this.project.slug, + platforms: this.project.platforms, + controls: this.project.controls, + orientation: this.project.orientation, + aspect: this.project.aspect, + "public": this.project["public"] + }); + } + } + ref1 = this.listeners; + for (j = 0, len1 = ref1.length; j < len1; j++) { + listener = ref1[j]; + if (listener != null) { + listener.send({ + name: "project_options_updated", + title: this.project.title, + slug: this.project.slug, + platforms: this.project.platforms, + controls: this.project.controls, + orientation: this.project.orientation, + aspect: this.project.aspect, + "public": this.project["public"] + }); + } + } + }; + + ProjectManager.prototype.inviteUser = function(source, user) { + var i, len, li, ref; + if (source.user === this.project.owner) { + this.project.inviteUser(user); + this.propagateUserListChange(); + ref = user.listeners; + for (i = 0, len = ref.length; i < len; i++) { + li = ref[i]; + li.getProjectList(); + } + } + }; + + ProjectManager.prototype.acceptInvite = function(user) { + var i, j, len, len1, li, link, ref, ref1; + ref = this.project.users; + for (i = 0, len = ref.length; i < len; i++) { + link = ref[i]; + if (user === link.user && !link.accepted) { + link.accepted = true; + return this.propagateUserListChange(); + ref1 = user.listeners; + for (j = 0, len1 = ref1.length; j < len1; j++) { + li = ref1[j]; + li.getProjectList(); + } + } + } + }; + + ProjectManager.prototype.removeUser = function(source, user) { + var i, j, len, len1, li, link, ref, ref1; + if (source.user === this.project.owner || source.user === user) { + ref = this.project.users; + for (i = 0, len = ref.length; i < len; i++) { + link = ref[i]; + if (user === link.user) { + link.remove(); + return this.propagateUserListChange(); + ref1 = user.listeners; + for (j = 0, len1 = ref1.length; j < len1; j++) { + li = ref1[j]; + li.getProjectList(); + } + } + } + } + }; + + ProjectManager.prototype.listFiles = function(folder, callback) { + var file; + file = this.project.owner.id + "/" + this.project.id + "/" + folder; + return this.project.content.files.list(file, (function(_this) { + return function(files) { + var f, i, len, res; + res = []; + files = files || []; + for (i = 0, len = files.length; i < len; i++) { + f = files[i]; + if (!f.startsWith(".")) { + res.push({ + file: f, + version: _this.getFileVersion(folder + "/" + f), + size: _this.getFileSize(folder + "/" + f), + properties: _this.getFileProperties(folder + "/" + f) + }); + } + } + return callback(res); + }; + })(this)); + }; + + ProjectManager.prototype.getFileVersions = function(callback) { + var folder, funk, jobs, res, t; + res = {}; + jobs = []; + for (folder in FILE_TYPES) { + t = FILE_TYPES[folder]; + res[t.property] = {}; + jobs.push(t); + } + funk = (function(_this) { + return function() { + var job; + if (jobs.length > 0) { + job = jobs.splice(0, 1)[0]; + return _this.listFiles(job.folder, function(files) { + var ext, f, i, len, name; + for (i = 0, len = files.length; i < len; i++) { + f = files[i]; + name = f.file.split(".")[0]; + ext = f.file.split(".")[1]; + res[job.property][name] = { + version: f.version, + properties: f.properties, + ext: ext + }; + } + return funk(); + }); + } else { + return callback(res); + } + }; + })(this); + return funk(); + }; + + ProjectManager.prototype.listProjectFiles = function(session, data) { + var file; + if (!this.canRead(session.user)) { + return console.log("unauthorized user"); + } + file = this.project.owner.id + "/" + this.project.id + "/" + data.folder; + return this.project.content.files.list(file, (function(_this) { + return function(files) { + var f, i, len, res; + res = []; + files = files || []; + for (i = 0, len = files.length; i < len; i++) { + f = files[i]; + if (!f.startsWith(".")) { + res.push({ + file: f, + version: _this.getFileVersion(data.folder + "/" + f), + size: _this.getFileSize(data.folder + "/" + f), + properties: _this.getFileProperties(data.folder + "/" + f) + }); + } + } + return session.send({ + name: "list_project_files", + files: res, + request_id: data.request_id + }); + }; + })(this)); + }; + + ProjectManager.prototype.readProjectFile = function(session, data) { + var encoding, file; + if (!this.canRead(session.user)) { + return; + } + file = this.project.owner.id + "/" + this.project.id + "/" + data.file; + encoding = data.file.endsWith(".ms") || data.file.endsWith(".json") || data.file.endsWith(".md") ? "text" : "base64"; + return this.project.content.files.read(file, encoding, (function(_this) { + return function(content) { + var out; + out = data.file.endsWith(".ms") || data.file.endsWith(".json") || data.file.endsWith(".md") ? "utf8" : "base64"; + if (content != null) { + return session.send({ + name: "read_project_file", + content: content.toString(out), + request_id: data.request_id + }); + } + }; + })(this)); + }; + + ProjectManager.prototype.writeProjectFile = function(session, data) { + var content, f, file, th, version; + if (!this.canWrite(session.user)) { + return; + } + if (this.project.deleted) { + return; + } + if ((data.file == null) || (data.content == null)) { + return; + } + if (data.content.length > 10000000) { + return; + } + if (!/^(ms|sprites|maps|sounds|music|doc|assets)\/[a-z0-9_]{1,40}(-[a-z0-9_]{1,40}){0,4}.(ms|png|json|wav|mp3|md|glb|jpg)$/.test(data.file)) { + console.info("wrong file name: " + data.file); + return; + } + version = this.getFileVersion(data.file); + if (version === 0) { + if (!session.server.rate_limiter.accept("create_file_user", session.user.id)) { + return; + } + } + file = this.project.owner.id + "/" + this.project.id + "/" + data.file; + if (data.file.endsWith(".ms") || data.file.endsWith(".json") || data.file.endsWith(".md")) { + content = data.content; + } else { + content = new Buffer(data.content, "base64"); + } + this.project.content.files.write(file, content, (function(_this) { + return function() { + version += 1; + _this.setFileVersion(data.file, version); + _this.setFileSize(data.file, content.length); + if (data.properties != null) { + _this.setFileProperties(data.file, data.properties); + } + if (data.request_id != null) { + session.send({ + name: "write_project_file", + version: version, + size: content.length, + request_id: data.request_id + }); + } + _this.propagateFileChange(session, data.file, version, data.content, data.properties); + return _this.project.touch(); + }; + })(this)); + if (data.thumbnail != null) { + th = new Buffer(data.thumbnail, "base64"); + f = file.split("/"); + f[2] += "_th"; + f[3] = f[3].split(".")[0] + ".png"; + f = f.join("/"); + return this.project.content.files.write(f, th, (function(_this) { + return function() { + return console.info("thumbnail saved"); + }; + })(this)); + } + }; + + ProjectManager.prototype.renameProjectFile = function(session, data) { + var dest, source; + if (!this.canWrite(session.user)) { + return; + } + if (data.source == null) { + return; + } + if (data.dest == null) { + return; + } + if (!/^(ms|sprites|maps|sounds|music|doc|assets)\/[a-z0-9_]{1,40}(-[a-z0-9_]{1,40}){0,4}.(ms|png|json|wav|mp3|md|glb|jpg)$/.test(data.source)) { + console.info("wrong source name: " + data.source); + return; + } + if (!/^(ms|sprites|maps|sounds|music|doc|assets)\/[a-z0-9_]{1,40}(-[a-z0-9_]{1,40}){0,4}.(ms|png|json|wav|mp3|md|glb|jpg)$/.test(data.dest)) { + console.info("wrong dest name: " + data.dest); + return; + } + source = this.project.owner.id + "/" + this.project.id + "/" + data.source; + dest = this.project.owner.id + "/" + this.project.id + "/" + data.dest; + return this.project.content.files.read(source, "binary", (function(_this) { + return function(content) { + if (content != null) { + return _this.project.content.files.write(dest, content, function() { + _this.project.deleteFileInfo(data.source); + _this.setFileSize(data.dest, content.length); + _this.project.touch(); + return _this.project.content.files["delete"](source, function() { + if (data.thumbnail) { + source = source.split("/"); + source[2] += "_th"; + source[3] = source[3].split(".")[0] + ".png"; + source = source.join("/"); + dest = dest.split("/"); + dest[2] += "_th"; + dest[3] = dest[3].split(".")[0] + ".png"; + dest = dest.join("/"); + return _this.project.content.files.read(source, "binary", function(thumbnail) { + if (thumbnail != null) { + return _this.project.content.files.write(dest, thumbnail, function() { + session.send({ + name: "rename_project_file", + request_id: data.request_id + }); + _this.propagateFileDeleted(session, data.source); + return _this.propagateFileChange(session, data.dest, 0, null, {}); + }); + } + }); + } else { + session.send({ + name: "rename_project_file", + request_id: data.request_id + }); + _this.propagateFileDeleted(session, data.source); + return _this.propagateFileChange(session, data.dest, 0, null, {}); + } + }); + }); + } + }; + })(this)); + }; + + ProjectManager.prototype.deleteProjectFile = function(session, data) { + var f, file; + if (!this.canWrite(session.user)) { + return; + } + if (data.file == null) { + return; + } + file = this.project.owner.id + "/" + this.project.id + "/" + data.file; + this.project.content.files["delete"](file, (function(_this) { + return function() { + _this.project.deleteFileInfo(data.file); + if (data.request_id != null) { + session.send({ + name: "delete_project_file", + request_id: data.request_id + }); + } + _this.propagateFileDeleted(session, data.file); + return _this.project.touch(); + }; + })(this)); + if (data.thumbnail) { + f = file.split("/"); + f[2] += "_th"; + f[3] = f[3].split(".")[0] + ".png"; + f = f.join("/"); + return this.project.content.files["delete"](f, (function(_this) { + return function() {}; + })(this)); + } + }; + + return ProjectManager; + +})(); + +module.exports = this.ProjectManager; diff --git a/server/session/session.coffee b/server/session/session.coffee new file mode 100644 index 00000000..3d1be44f --- /dev/null +++ b/server/session/session.coffee @@ -0,0 +1,1061 @@ +SHA256 = require("crypto-js/sha256") +ProjectManager = require __dirname+"/projectmanager.js" +RegexLib = require __dirname+"/../../static/js/util/regexlib.js" +ForumSession = require __dirname+"/../forum/forumsession.js" + +class @Session + constructor:(@server,@socket)-> + #console.info "new session" + @content = @server.content + return @socket.close() if not @content? + @translator = @content.translator.getTranslator("en") + @user = null + @token = null + + @checkCookie() + @last_active = Date.now() + + @socket.on "message",(msg)=> + #console.info "received msg: #{msg}" + @messageReceived msg + @last_active = Date.now() + + @socket.on "close",()=> + @server.sessionClosed @ + @disconnected() + + @socket.on "error",(err)=> + if @user + console.error "WS ERROR for user #{@user.id} - #{@user.nick}" + else + console.error "WS ERROR" + + console.error err + + @commands = {} + @register "ping",(msg)=>@send({name:"pong"}) + + @register "create_account",(msg)=>@createAccount(msg) + @register "create_guest",(msg)=>@createGuestAccount(msg) + @register "login",(msg)=>@login(msg) + @register "send_password_recovery",(msg)=>@sendPasswordRecovery(msg) + @register "token",(msg)=>@checkToken(msg) + @register "delete_guest",(msg)=>@deleteGuest(msg) + + @register "send_validation_mail",(msg)=>@sendValidationMail(msg) + @register "change_email",(msg)=>@changeEmail(msg) + @register "change_nick",(msg)=>@changeNick(msg) + @register "change_password",(msg)=>@changePassword(msg) + @register "change_newsletter",(msg)=>@changeNewsletter(msg) + @register "set_user_setting",(msg)=>@setUserSetting(msg) + @register "set_user_profile",(msg)=>@setUserProfile(msg) + + @register "create_project",(msg)=>@createProject(msg) + @register "set_project_option",(msg)=>@setProjectOption(msg) + @register "set_project_public",(msg)=>@setProjectPublic(msg) + @register "set_project_tags",(msg)=>@setProjectTags(msg) + @register "delete_project",(msg)=>@deleteProject(msg) + @register "get_project_list",(msg)=>@getProjectList(msg) + @register "update_code",(msg)=>@updateCode(msg) + @register "lock_project_file",(msg)=>@lockProjectFile(msg) + @register "write_project_file",(msg)=>@writeProjectFile(msg) + @register "read_project_file",(msg)=>@readProjectFile(msg) + @register "rename_project_file",(msg)=>@renameProjectFile(msg) + @register "delete_project_file",(msg)=>@deleteProjectFile(msg) + @register "list_project_files",(msg)=>@listProjectFiles(msg) + @register "list_public_project_files",(msg)=>@listPublicProjectFiles(msg) + @register "read_public_project_file",(msg)=>@readPublicProjectFile(msg) + @register "listen_to_project",(msg)=>@listenToProject(msg) + @register "get_file_versions",(msg)=>@getFileVersions(msg) + + @register "invite_to_project",(msg)=>@inviteToProject(msg) + @register "accept_invite",(msg)=>@acceptInvite(msg) + @register "remove_project_user",(msg)=>@removeProjectUser(msg) + + @register "get_public_projects",(msg)=>@getPublicProjects(msg) + @register "clone_project",(msg)=>@cloneProject(msg) + @register "clone_public_project",(msg)=>@clonePublicProject(msg) + @register "toggle_like",(msg)=>@toggleLike(msg) + + @register "get_language",(msg)=>@getLanguage(msg) + @register "get_translation_list",(msg)=>@getTranslationList(msg) + @register "set_translation",(msg)=>@setTranslation(msg) + @register "add_translation",(msg)=>@addTranslation(msg) + + @register "get_project_comments",(msg)=>@getProjectComments(msg) + @register "add_project_comment",(msg)=>@addProjectComment(msg) + @register "delete_project_comment",(msg)=>@deleteProjectComment(msg) + @register "edit_project_comment",(msg)=>@editProjectComment(msg) + + @register "build_project",(msg)=>@buildProject(msg) + @register "get_build_status",(msg)=>@getBuildStatus(msg) + + @register "start_builder",(msg)=>@startBuilder(msg) + @register "backup_complete",(msg)=>@backupComplete(msg) + + for plugin in @server.plugins + if plugin.registerSessionMessages? + plugin.registerSessionMessages @ + + @forum_session = new ForumSession @ + + @reserved_nicks = + "admin":true + "api":true + "static":true + "blog":true + "news":true + "about":true + "discord":true + "article":true + "forum": true + "community":true + + checkCookie:()-> + try + cookie = @socket.request.headers.cookie + if cookie? and cookie.indexOf("token")>=0 + cookie = cookie.split("token")[1] + cookie = cookie.split("=")[1] + if cookie? + cookie = cookie.split(";")[0] + @token = cookie.trim() + @token = @content.findToken @token + if @token? + @user = @token.user + @user.addListener @ + @send + name: "token_valid" + nick: @user.nick + flags: if not @user.flags.censored then @user.flags else [] + info: @getUserInfo() + settings: @user.settings + @user.set "last_active",Date.now() + @logActiveUser() + catch error + console.error error + + logActiveUser:()-> + return if not @user? + if @user.flags.guest + @server.stats.unique("active_guests",@user.id) + else + @server.stats.unique("active_users",@user.id) + + register:(name,callback)-> + @commands[name] = callback + + disconnected:()-> + if @project? and @project.manager? + @project.manager.removeUser @ + @project.manager.removeListener @ + if @user? + @user.removeListener @ + + setCurrentProject:(project)-> + if project != @project or not @project.manager? + if @project? and @project.manager? + @project.manager.removeUser @ + @project = project + if not @project.manager? + new ProjectManager @project + @project.manager.addUser @ + + messageReceived:(msg)-> + #console.info msg + try + msg = JSON.parse msg + if msg.name? + c = @commands[msg.name] + c(msg) if c? + catch err + console.info err + + @server.stats.inc("websocket_requests") + @logActiveUser() if @user? + + sendCodeUpdated:(file,code)-> + @send + name: "code_updated" + file: file + code: code + return + + sendProjectFileUpdated:(type,file,version,data,properties)-> + @send + name: "project_file_updated" + type: type + file: file + version: version + data: data + properties: properties + + sendProjectFileDeleted:(type,file)-> + @send + name: "project_file_deleted" + type: type + file: file + + createGuestAccount:(data)-> + return if not @server.rate_limiter.accept("create_account_ip",@socket.remoteAddress) + chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789" + loop + nick = "" + for i in [0..9] by 1 + nick += chars.charAt(Math.floor(Math.random()*chars.length)) + + break if not @content.findUserByNick(nick) + + @user = @content.createUser + nick: nick + flags: { guest: true } + language: data.language + date_created: Date.now() + last_active: Date.now() + creation_ip: @socket.remoteAddress + + @user.addListener @ + + @send + name:"guest_created" + nick: nick + flags: @user.flags + info: @getUserInfo() + settings: @user.settings + token: @content.createToken(@user).value + request_id: data.request_id + @logActiveUser() + + deleteGuest:(data)-> + if @user? and @user.flags.guest + @user.delete() + + @send + name:"guest_deleted" + request_id: data.request_id + + createAccount:(data)-> + return @sendError(@translator.get("email not specified"),data.request_id) if not data.email? + return @sendError(@translator.get("nickname not specified"),data.request_id) if not data.nick? + return @sendError(@translator.get("password not specified"),data.request_id) if not data.password? + + return @sendError(@translator.get("email already exists"),data.request_id) if @content.findUserByEmail(data.email) + return @sendError(@translator.get("nickname already exists"),data.request_id) if @content.findUserByNick(data.nick) + return @sendError(@translator.get("nickname already exists"),data.request_id) if @reserved_nicks[data.nick] + + return @sendError(@translator.get("Incorrect nickname. Use 5 characters minimum, only letters, numbers or _"),data.request_id) if not RegexLib.nick.test(data.nick) + return @sendError(@translator.get("Incorrect e-mail address"),data.request_id) if not RegexLib.email.test(data.email) + return @sendError(@translator.get("Password too weak"),data.request_id) if data.password.trim().length<6 + + return if not @server.rate_limiter.accept("create_account_ip",@socket.remoteAddress) + + salt = "" + chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789" + for i in [0..15] by 1 + salt += chars.charAt(Math.floor(Math.random()*chars.length)) + + hash = salt+"|"+SHA256(salt+data.password) + + if @user? and @user.flags.guest + @server.content.changeUserNick @user,data.nick + @server.content.changeUserEmail @user,data.email + + @user.setFlag("guest",false) + @user.setFlag("newsletter",data.newsletter) + @user.set("hash",hash) + @user.resetValidationToken() + else + @user = @content.createUser + nick: data.nick + email: data.email + flags: { newsletter: data.newsletter } + language: data.language + hash: hash + date_created: Date.now() + last_active: Date.now() + creation_ip: @socket.remoteAddress + + @user.addListener @ + + @send + name:"account_created" + nick: data.nick + email: data.email + flags: @user.flags + info: @getUserInfo() + settings: @user.settings + notifications: [@server.content.translator.getTranslator(data.language).get("Account created successfully!")] + token: @content.createToken(@user).value + request_id: data.request_id + + @sendValidationMail() + @logActiveUser() + + login:(data)-> + return if not data.nick? + return if not @server.rate_limiter.accept("login_ip",@socket.remoteAddress) + return if not @server.rate_limiter.accept("login_user",data.nick) + + user = @content.findUserByNick data.nick + if not user? + user = @content.findUserByEmail data.nick + + if user? and user.hash? + hash = user.hash + s = hash.split("|") + h = SHA256(s[0]+data.password) + #console.info "salt: #{s[0]}" + #console.info "hash: #{h}" + #console.info "recorded hash: #{s[1]}" + if h.toString() == s[1] + @user = user + @user.addListener @ + @send + name:"logged_in" + token: @content.createToken(@user).value + nick: @user.nick + email: @user.email + flags: if not @user.flags.censored then @user.flags else {} + info: @getUserInfo() + settings: @user.settings + notifications: @user.notifications + request_id: data.request_id + @user.notifications = [] + @logActiveUser() + else + @sendError "wrong password",data.request_id + else + @sendError "unknown user",data.request_id + + getUserInfo:()-> + return { + size: @user.getTotalSize() + early_access: @user.early_access + max_storage: @user.max_storage + description: @user.description + } + + sendPasswordRecovery:(data)-> + if data.email? + user = @content.findUserByEmail data.email + if user? + if @server.rate_limiter.accept("send_mail_user",user.id) + @server.content.sendPasswordRecoveryMail(user) + @send + name: "send_password_recovery" + request_id: data.request_id + + checkToken:(data)-> + token = @content.findToken data.token + if token? + @user = token.user + @user.addListener @ + @send + name: "token_valid" + nick: @user.nick + email: @user.email + flags: if not @user.flags.censored then @user.flags else {} + info: @getUserInfo() + settings: @user.settings + notifications: @user.notifications + request_id: data.request_id + @user.notifications = [] + @user.set "last_active",Date.now() + @logActiveUser() + else + @sendError "invalid token",data.request_id + + send:(data)-> + @socket.send JSON.stringify data + + sendError:(error,request_id)-> + @send + name: "error" + error: error + request_id: request_id + + createProject:(data)-> + return @sendError("not connected") if not @user? + return if not @server.rate_limiter.accept("create_project_user",@user.id) + + @content.createProject @user,data,(project)=> + @send + name:"project_created" + id: project.id + request_id: data.request_id + + clonePublicProject:(data)-> + return @sendError("not connected") if not @user? + return if not @server.rate_limiter.accept("create_project_user",@user.id) + return @sendError("") if not data.project? + + project = @server.content.projects[data.project] + if project? and project.public + @content.createProject @user,{ + title: project.title + slug: project.slug + public: false + },((clone)=> + clone.setType project.type + clone.setOrientation project.orientation + clone.setAspect project.aspect + clone.setGraphics project.graphics + clone.set "files",JSON.parse JSON.stringify project.files + man = @getProjectManager(project) + + folders = ["ms","sprites","maps","sounds","sounds_th","music","music_th","doc"] + files = [] + funk = ()=> + if folders.length>0 + folder = folders.splice(0,1)[0] + man.listFiles folder,(list)=> + for f in list + files.push + file: f.file + folder: folder + funk() + else if files.length>0 + f = files.splice(0,1)[0] + src = "#{project.owner.id}/#{project.id}/#{f.folder}/#{f.file}" + dest = "#{clone.owner.id}/#{clone.id}/#{f.folder}/#{f.file}" + @server.content.files.copy src,dest,()=> + funk() + else + @send + name:"project_created" + id: clone.id + request_id: data.request_id + + funk()),true + + + cloneProject:(data)-> + return @sendError("not connected") if not @user? + return if not @server.rate_limiter.accept("create_project_user",@user.id) + return @sendError("") if not data.project? + + project = @server.content.projects[data.project] + if project? + manager = @getProjectManager project + if manager.canRead(@user) + @content.createProject @user,{ + title: data.title or project.title + slug: project.slug + public: false + },((clone)=> + clone.setType project.type + clone.setOrientation project.orientation + clone.setAspect project.aspect + clone.setGraphics project.graphics + clone.set "files",JSON.parse JSON.stringify project.files + man = @getProjectManager(project) + + folders = ["ms","sprites","maps","sounds","sounds_th","music","music_th","doc"] + files = [] + funk = ()=> + if folders.length>0 + folder = folders.splice(0,1)[0] + man.listFiles folder,(list)=> + for f in list + files.push + file: f.file + folder: folder + funk() + else if files.length>0 + f = files.splice(0,1)[0] + src = "#{project.owner.id}/#{project.id}/#{f.folder}/#{f.file}" + dest = "#{clone.owner.id}/#{clone.id}/#{f.folder}/#{f.file}" + @server.content.files.copy src,dest,()=> + funk() + else + @send + name:"project_created" + id: clone.id + request_id: data.request_id + + funk()),true + + getProjectManager:(project)-> + if not project.manager? + new ProjectManager project + project.manager + + setProjectPublic:(data)-> + return @sendError("not connected") if not @user? + return if data.public and not @user.flags["validated"] + + project = @user.findProject(data.project) + if project? + @content.setProjectPublic(project,data.public) + @send + name:"set_project_public" + id: project.id + public: project.public + request_id: data.request_id + + setProjectTags:(data)-> + return @sendError("not connected") if not @user? + return if data.public and not @user.flags["validated"] + return if not data.project? + + project = @user.findProject(data.project) + if not project? and @user.flags.admin + project = @content.projects[data.project] + + if project? and data.tags? + @content.setProjectTags(project,data.tags) + @send + name:"set_project_tags" + id: project.id + tags: project.tags + request_id: data.request_id + + setProjectOption:(data)-> + return @sendError("not connected") if not @user? + return @sendError("no value") if not data.value? + + project = @user.findProject(data.project) + if project? + switch data.option + when "title" + if not project.setTitle data.value + @send + name:"error" + value: project.title + request_id: data.request_id + + when "slug" + if not project.setSlug data.value + @send + name:"error" + value: project.slug + request_id: data.request_id + + when "description" + project.set "description",data.value + + when "code" + if not project.setCode data.value + @send + name:"error" + value: project.code + request_id: data.request_id + + when "platforms" + project.setPlatforms data.value if Array.isArray data.value + + when "controls" + project.setControls data.value if Array.isArray data.value + + when "type" + project.setType data.value if typeof data.value == "string" + + when "orientation" + project.setOrientation data.value if typeof data.value == "string" + + when "aspect" + project.setAspect data.value if typeof data.value == "string" + + when "graphics" + if @user.flags.m3d + project.setGraphics data.value if typeof data.value == "string" + + if project.manager? + project.manager.propagateOptions @ + + project.touch() + + deleteProject:(data)-> + return @sendError("not connected") if not @user? + + project = @user.findProject(data.project) + if project? + @user.deleteProject project + @send + name:"project_deleted" + id: project.id + request_id: data.request_id + + getProjectList:(data)-> + return @sendError("not connected") if not @user? + + source = @user.listProjects() + list = [] + for p in source + if not p.deleted + list.push + id: p.id + owner: + id: p.owner.id + nick: p.owner.nick + title: p.title + slug: p.slug + code: p.code + description: p.description + tags: p.tags + platforms: p.platforms + controls: p.controls + type: p.type + orientation: p.orientation + aspect: p.aspect + graphics: p.graphics + date_created: p.date_created + last_modified: p.last_modified + public: p.public + size: p.getSize() + users: p.listUsers() + + source = @user.listProjectLinks() + for link in source + if not link.project.deleted + p = link.project + list.push + id: p.id + owner: + id: p.owner.id + nick: p.owner.nick + accepted: link.accepted + title: p.title + slug: p.slug + code: p.code + description: p.description + tags: p.tags + platforms: p.platforms + controls: p.controls + type: p.type + orientation: p.orientation + aspect: p.aspect + graphics: p.graphics + date_created: p.date_created + last_modified: p.last_modified + public: p.public + users: p.listUsers() + + @send + name: "project_list" + list: list + request_id: if data? then data.request_id else undefined + + lockProjectFile:(data)-> + return @sendError("not connected") if not @user? + #console.info JSON.stringify data + + project = @content.projects[data.project] if data.project? + if project? + @setCurrentProject project + project.manager.lockFile(@,data.file) + + writeProjectFile:(data)-> + return @sendError("not connected") if not @user? + + project = @content.projects[data.project] if data.project? + if project? + @setCurrentProject project + project.manager.writeProjectFile(@,data) + + renameProjectFile:(data)-> + return @sendError("not connected") if not @user? + + project = @content.projects[data.project] if data.project? + if project? + @setCurrentProject project + project.manager.renameProjectFile(@,data) + + deleteProjectFile:(data)-> + return @sendError("not connected") if not @user? + + project = @content.projects[data.project] if data.project? + if project? + @setCurrentProject project + project.manager.deleteProjectFile(@,data) + + readProjectFile:(data)-> + #console.info "session.readProjectFile "+JSON.stringify data + return @sendError("not connected") if not @user? + + project = @content.projects[data.project] if data.project? + if project? + @setCurrentProject project + project.manager.readProjectFile(@,data) + + listProjectFiles:(data)-> + return @sendError("not connected") if not @user? + + project = @content.projects[data.project] if data.project? + if project? + @setCurrentProject project + project.manager.listProjectFiles @,data + + listPublicProjectFiles:(data)-> + project = @content.projects[data.project] if data.project? + if project? + manager = @getProjectManager project + manager.listProjectFiles @,data + + readPublicProjectFile:(data)-> + project = @content.projects[data.project] if data.project? + if project? and project.public + manager = @getProjectManager project + project.manager.readProjectFile(@,data) + + listenToProject:(data)-> + user = data.user + project = data.project + if user? and project? + user = @content.findUserByNick(user) + if user? + project = user.findProjectBySlug(project) + if project? + if @project? and @project.manager? + @project.manager.removeListener @ + + @project = project + new ProjectManager @project if not @project.manager? + @project.manager.addListener @ + + getFileVersions:(data)-> + user = data.user + project = data.project + if user? and project? + user = @content.findUserByNick(user) + if user? + project = user.findProjectBySlug(project) + if project? + new ProjectManager project if not project.manager? + project.manager.getFileVersions (res)=> + @send + name: "project_file_versions" + data: res + request_id: data.request_id + + getPublicProjects:(data)-> + switch data.ranking + when "new" + source = @content.new_projects + when "top" + source = @content.top_projects + else + source = @content.hot_projects + + list = [] + for p,i in source + break if list.length>=300 + if p.public and not p.deleted and not p.owner.flags.censored + list.push + id: p.id + title: p.title + description: p.description + type: p.type + tags: p.tags + slug: p.slug + owner: p.owner.nick + owner_info: + tier: p.owner.flags.tier + likes: p.likes + liked: @user? and @user.isLiked(p.id) + tags: p.tags + date_published: p.first_published + + @send + name: "public_projects" + list: list + request_id: data.request_id + + toggleLike:(data)-> + return @sendError("not connected") if not @user? + return @sendError("not validated") if not @user.flags.validated + + project = @content.projects[data.project] + if project? + if @user.isLiked(project.id) + @user.removeLike(project.id) + project.likes-- + else + @user.addLike(project.id) + project.likes++ + + @send + name:"project_likes" + likes: project.likes + liked: @user.isLiked(project.id) + request_id: data.request_id + + inviteToProject:(data)-> + return @sendError("not connected",data.request_id) if not @user? + + user = @content.findUserByNick(data.user) + return @sendError("user not found",data.request_id) if not user? + + project = @user.findProject(data.project) + return @sendError("project not found",data.request_id) if not project? + + @setCurrentProject project + project.manager.inviteUser @,user + + acceptInvite:(data)-> + return @sendError("not connected") if not @user? + + for link in @user.project_links + if link.project.id == data.project + link.accept() + @setCurrentProject link.project + if link.project.manager? + link.project.manager.propagateUserListChange() + for li in @user.listeners + li.getProjectList() + + return + + removeProjectUser:(data)-> + return @sendError("not connected") if not @user? + + project = @content.projects[data.project] if data.project? + return @sendError("project not found",data.request_id) if not project? + + user = @content.findUserByNick(data.user) if data.user? + return @sendError("user not found",data.request_id) if not user? + + return if @user != project.owner and @user != user + + for link in project.users + if link.user == user + link.remove() + if @user == project.owner + @setCurrentProject project + else + @send + name:"project_link_deleted" + request_id: data.request_id + + if project.manager? + project.manager.propagateUserListChange() + + for li in user.listeners + li.getProjectList() + + return + + sendValidationMail:(data)-> + return @sendError("not connected") if not @user? + if @server.rate_limiter.accept("send_mail_user",@user.id) + @server.content.sendValidationMail(@user) + if data? + @send + name:"send_validation_mail" + request_id: data.request_id + return + + changeNick:(data)-> + return if not @user? + return if not data.nick? + + if not RegexLib.nick.test(data.nick) + @send + name: "error" + value: "Incorrect nickname" + request_id: data.request_id + else + if @server.content.findUserByNick(data.nick)? or @reserved_nicks[data.nick] + @send + name: "error" + value: "Nickname not available" + request_id: data.request_id + else + @server.content.changeUserNick @user,data.nick + @send + name: "change_nick" + nick: data.nick + request_id: data.request_id + + changeEmail:(data)-> + return if not @user? + return if not data.email? + + if not RegexLib.email.test(data.email) + @send + name: "error" + value: "Incorrect email" + request_id: data.request_id + else + if @server.content.findUserByEmail(data.email)? + @send + name: "error" + value: "E-mail is already used for another account" + request_id: data.request_id + else + @user.setFlag "validated",false + @user.resetValidationToken() + @server.content.changeUserEmail @user,data.email + @sendValidationMail() + @send + name: "change_email" + email: data.email + request_id: data.request_id + + changeNewsletter:(data)-> + @user.setFlag "newsletter",data.newsletter + @send + name: "change_newsletter" + newsletter: data.newsletter + request_id: data.request_id + + setUserSetting:(data)-> + return if not data.setting? or not data.value? + @user.setSetting data.setting,data.value + + setUserProfile:(data)-> + return if not @user? + if data.image? + if data.image == 0 + @user.setFlag "profile_image",false + else + file = "#{@user.id}/profile_image.png" + content = new Buffer(data.image,"base64") + @server.content.files.write file,content,()=> + @user.setFlag "profile_image",true + @send + name: "set_user_profile" + request_id: data.request_id + return + + if data.description? + @user.set "description",data.description + + @send + name: "set_user_profile" + request_id: data.request_id + + getLanguage:(msg)-> + return if not msg.language? + lang = @server.content.translator.languages[msg.language] + lang = if lang? then lang.export() else "{}" + + @send + name: "get_language" + language: lang + request_id: msg.request_id + + getTranslationList:(msg)-> + @send + name: "get_translation_list" + list: @server.content.translator.list + request_id: msg.request_id + + setTranslation:(msg)-> + return if not @user? + lang = msg.language + return if not @user.flags["translator_"+lang] + + source = msg.source + translation = msg.translation + if not @server.content.translator.languages[lang] + @server.content.translator.createLanguage(lang) + + @server.content.translator.languages[lang].set(@user.id,source,translation) + + addTranslation:(msg)-> + return if not @user? + #return if not @user.flags.admin + source = msg.source + @server.content.translator.reference source + + getProjectComments:(data)-> + return if not data.project? + + project = @content.projects[data.project] if data.project? + if project? and project.public + @send + name: "project_comments" + request_id: data.request_id + comments: project.comments.getAll() + + addProjectComment:(data)-> + return if not data.project? + return if not data.text? + + project = @content.projects[data.project] if data.project? + if project? and project.public + if @user? and @user.flags.validated and not @user.flags.banned and not @user.flags.censored + return if not @server.rate_limiter.accept("post_comment_user",@user.id) + project.comments.add(@user,data.text) + @send + name: "add_project_comment" + request_id: data.request_id + + deleteProjectComment:(data)-> + return if not data.project? + return if not data.id? + + project = @content.projects[data.project] if data.project? + if project? and project.public + if @user? + c = project.comments.get(data.id) + if c? and (c.user == @user or @user.flags.admin) + c.remove() + @send + name: "delete_project_comment" + request_id: data.request_id + + editProjectComment:(data)-> + return if not data.project? + return if not data.id? + return if not data.text? + + project = @content.projects[data.project] if data.project? + if project? and project.public + if @user? + c = project.comments.get(data.id) + if c? and c.user == @user + c.edit(data.text) + @send + name: "edit_project_comment" + request_id: data.request_id + + + buildProject:(msg)-> + return @sendError("not connected") if not @user? + + project = @content.projects[msg.project] if msg.project? + if project? + @setCurrentProject project + return if not project.manager.canWrite @user + return if not msg.target? + build = @server.build_manager.startBuild(project,msg.target) + @send + name: "build_project" + request_id: msg.request_id + build: if build? then build.export() else null + + getBuildStatus:(msg)-> + return @sendError("not connected") if not @user? + + project = @content.projects[msg.project] if msg.project? + if project? + @setCurrentProject project + return if not project.manager.canWrite @user + return if not msg.target? + build = @server.build_manager.getBuildInfo(project,msg.target) + @send + name: "build_project" + request_id: msg.request_id + build: if build? then build.export() else null + active_target: @server.build_manager.hasBuilder msg.target + + timeCheck:()-> + if Date.now()>@last_active+5*60000 # 5 minutes prevents breaking large assets uploads + @socket.close() + @server.sessionClosed @ + @socket.terminate() + + startBuilder:(msg)-> + if msg.target? + if msg.key == @server.config["builder-key"] + @server.sessionClosed @ + @server.build_manager.registerBuilder @,msg.target + + backupComplete:(msg)-> + if msg.key == @server.config["backup-key"] + @server.sessionClosed @ + @server.last_backup_time = Date.now() + +module.exports = @Session diff --git a/server/session/session.js b/server/session/session.js new file mode 100644 index 00000000..f0399035 --- /dev/null +++ b/server/session/session.js @@ -0,0 +1,1695 @@ +var ForumSession, ProjectManager, RegexLib, SHA256; + +SHA256 = require("crypto-js/sha256"); + +ProjectManager = require(__dirname + "/projectmanager.js"); + +RegexLib = require(__dirname + "/../../static/js/util/regexlib.js"); + +ForumSession = require(__dirname + "/../forum/forumsession.js"); + +this.Session = (function() { + function Session(server, socket) { + var j, len, plugin, ref; + this.server = server; + this.socket = socket; + this.content = this.server.content; + if (this.content == null) { + return this.socket.close(); + } + this.translator = this.content.translator.getTranslator("en"); + this.user = null; + this.token = null; + this.checkCookie(); + this.last_active = Date.now(); + this.socket.on("message", (function(_this) { + return function(msg) { + _this.messageReceived(msg); + return _this.last_active = Date.now(); + }; + })(this)); + this.socket.on("close", (function(_this) { + return function() { + _this.server.sessionClosed(_this); + return _this.disconnected(); + }; + })(this)); + this.socket.on("error", (function(_this) { + return function(err) { + if (_this.user) { + console.error("WS ERROR for user " + _this.user.id + " - " + _this.user.nick); + } else { + console.error("WS ERROR"); + } + return console.error(err); + }; + })(this)); + this.commands = {}; + this.register("ping", (function(_this) { + return function(msg) { + return _this.send({ + name: "pong" + }); + }; + })(this)); + this.register("create_account", (function(_this) { + return function(msg) { + return _this.createAccount(msg); + }; + })(this)); + this.register("create_guest", (function(_this) { + return function(msg) { + return _this.createGuestAccount(msg); + }; + })(this)); + this.register("login", (function(_this) { + return function(msg) { + return _this.login(msg); + }; + })(this)); + this.register("send_password_recovery", (function(_this) { + return function(msg) { + return _this.sendPasswordRecovery(msg); + }; + })(this)); + this.register("token", (function(_this) { + return function(msg) { + return _this.checkToken(msg); + }; + })(this)); + this.register("delete_guest", (function(_this) { + return function(msg) { + return _this.deleteGuest(msg); + }; + })(this)); + this.register("send_validation_mail", (function(_this) { + return function(msg) { + return _this.sendValidationMail(msg); + }; + })(this)); + this.register("change_email", (function(_this) { + return function(msg) { + return _this.changeEmail(msg); + }; + })(this)); + this.register("change_nick", (function(_this) { + return function(msg) { + return _this.changeNick(msg); + }; + })(this)); + this.register("change_password", (function(_this) { + return function(msg) { + return _this.changePassword(msg); + }; + })(this)); + this.register("change_newsletter", (function(_this) { + return function(msg) { + return _this.changeNewsletter(msg); + }; + })(this)); + this.register("set_user_setting", (function(_this) { + return function(msg) { + return _this.setUserSetting(msg); + }; + })(this)); + this.register("set_user_profile", (function(_this) { + return function(msg) { + return _this.setUserProfile(msg); + }; + })(this)); + this.register("create_project", (function(_this) { + return function(msg) { + return _this.createProject(msg); + }; + })(this)); + this.register("set_project_option", (function(_this) { + return function(msg) { + return _this.setProjectOption(msg); + }; + })(this)); + this.register("set_project_public", (function(_this) { + return function(msg) { + return _this.setProjectPublic(msg); + }; + })(this)); + this.register("set_project_tags", (function(_this) { + return function(msg) { + return _this.setProjectTags(msg); + }; + })(this)); + this.register("delete_project", (function(_this) { + return function(msg) { + return _this.deleteProject(msg); + }; + })(this)); + this.register("get_project_list", (function(_this) { + return function(msg) { + return _this.getProjectList(msg); + }; + })(this)); + this.register("update_code", (function(_this) { + return function(msg) { + return _this.updateCode(msg); + }; + })(this)); + this.register("lock_project_file", (function(_this) { + return function(msg) { + return _this.lockProjectFile(msg); + }; + })(this)); + this.register("write_project_file", (function(_this) { + return function(msg) { + return _this.writeProjectFile(msg); + }; + })(this)); + this.register("read_project_file", (function(_this) { + return function(msg) { + return _this.readProjectFile(msg); + }; + })(this)); + this.register("rename_project_file", (function(_this) { + return function(msg) { + return _this.renameProjectFile(msg); + }; + })(this)); + this.register("delete_project_file", (function(_this) { + return function(msg) { + return _this.deleteProjectFile(msg); + }; + })(this)); + this.register("list_project_files", (function(_this) { + return function(msg) { + return _this.listProjectFiles(msg); + }; + })(this)); + this.register("list_public_project_files", (function(_this) { + return function(msg) { + return _this.listPublicProjectFiles(msg); + }; + })(this)); + this.register("read_public_project_file", (function(_this) { + return function(msg) { + return _this.readPublicProjectFile(msg); + }; + })(this)); + this.register("listen_to_project", (function(_this) { + return function(msg) { + return _this.listenToProject(msg); + }; + })(this)); + this.register("get_file_versions", (function(_this) { + return function(msg) { + return _this.getFileVersions(msg); + }; + })(this)); + this.register("invite_to_project", (function(_this) { + return function(msg) { + return _this.inviteToProject(msg); + }; + })(this)); + this.register("accept_invite", (function(_this) { + return function(msg) { + return _this.acceptInvite(msg); + }; + })(this)); + this.register("remove_project_user", (function(_this) { + return function(msg) { + return _this.removeProjectUser(msg); + }; + })(this)); + this.register("get_public_projects", (function(_this) { + return function(msg) { + return _this.getPublicProjects(msg); + }; + })(this)); + this.register("clone_project", (function(_this) { + return function(msg) { + return _this.cloneProject(msg); + }; + })(this)); + this.register("clone_public_project", (function(_this) { + return function(msg) { + return _this.clonePublicProject(msg); + }; + })(this)); + this.register("toggle_like", (function(_this) { + return function(msg) { + return _this.toggleLike(msg); + }; + })(this)); + this.register("get_language", (function(_this) { + return function(msg) { + return _this.getLanguage(msg); + }; + })(this)); + this.register("get_translation_list", (function(_this) { + return function(msg) { + return _this.getTranslationList(msg); + }; + })(this)); + this.register("set_translation", (function(_this) { + return function(msg) { + return _this.setTranslation(msg); + }; + })(this)); + this.register("add_translation", (function(_this) { + return function(msg) { + return _this.addTranslation(msg); + }; + })(this)); + this.register("get_project_comments", (function(_this) { + return function(msg) { + return _this.getProjectComments(msg); + }; + })(this)); + this.register("add_project_comment", (function(_this) { + return function(msg) { + return _this.addProjectComment(msg); + }; + })(this)); + this.register("delete_project_comment", (function(_this) { + return function(msg) { + return _this.deleteProjectComment(msg); + }; + })(this)); + this.register("edit_project_comment", (function(_this) { + return function(msg) { + return _this.editProjectComment(msg); + }; + })(this)); + this.register("build_project", (function(_this) { + return function(msg) { + return _this.buildProject(msg); + }; + })(this)); + this.register("get_build_status", (function(_this) { + return function(msg) { + return _this.getBuildStatus(msg); + }; + })(this)); + this.register("start_builder", (function(_this) { + return function(msg) { + return _this.startBuilder(msg); + }; + })(this)); + this.register("backup_complete", (function(_this) { + return function(msg) { + return _this.backupComplete(msg); + }; + })(this)); + ref = this.server.plugins; + for (j = 0, len = ref.length; j < len; j++) { + plugin = ref[j]; + if (plugin.registerSessionMessages != null) { + plugin.registerSessionMessages(this); + } + } + this.forum_session = new ForumSession(this); + this.reserved_nicks = { + "admin": true, + "api": true, + "static": true, + "blog": true, + "news": true, + "about": true, + "discord": true, + "article": true, + "forum": true, + "community": true + }; + } + + Session.prototype.checkCookie = function() { + var cookie, error; + try { + cookie = this.socket.request.headers.cookie; + if ((cookie != null) && cookie.indexOf("token") >= 0) { + cookie = cookie.split("token")[1]; + cookie = cookie.split("=")[1]; + if (cookie != null) { + cookie = cookie.split(";")[0]; + this.token = cookie.trim(); + this.token = this.content.findToken(this.token); + if (this.token != null) { + this.user = this.token.user; + this.user.addListener(this); + this.send({ + name: "token_valid", + nick: this.user.nick, + flags: !this.user.flags.censored ? this.user.flags : [], + info: this.getUserInfo(), + settings: this.user.settings + }); + this.user.set("last_active", Date.now()); + return this.logActiveUser(); + } + } + } + } catch (error1) { + error = error1; + return console.error(error); + } + }; + + Session.prototype.logActiveUser = function() { + if (this.user == null) { + return; + } + if (this.user.flags.guest) { + return this.server.stats.unique("active_guests", this.user.id); + } else { + return this.server.stats.unique("active_users", this.user.id); + } + }; + + Session.prototype.register = function(name, callback) { + return this.commands[name] = callback; + }; + + Session.prototype.disconnected = function() { + if ((this.project != null) && (this.project.manager != null)) { + this.project.manager.removeUser(this); + this.project.manager.removeListener(this); + } + if (this.user != null) { + return this.user.removeListener(this); + } + }; + + Session.prototype.setCurrentProject = function(project) { + if (project !== this.project || (this.project.manager == null)) { + if ((this.project != null) && (this.project.manager != null)) { + this.project.manager.removeUser(this); + } + this.project = project; + if (this.project.manager == null) { + new ProjectManager(this.project); + } + return this.project.manager.addUser(this); + } + }; + + Session.prototype.messageReceived = function(msg) { + var c, err; + try { + msg = JSON.parse(msg); + if (msg.name != null) { + c = this.commands[msg.name]; + if (c != null) { + c(msg); + } + } + } catch (error1) { + err = error1; + console.info(err); + } + this.server.stats.inc("websocket_requests"); + if (this.user != null) { + return this.logActiveUser(); + } + }; + + Session.prototype.sendCodeUpdated = function(file, code) { + this.send({ + name: "code_updated", + file: file, + code: code + }); + }; + + Session.prototype.sendProjectFileUpdated = function(type, file, version, data, properties) { + return this.send({ + name: "project_file_updated", + type: type, + file: file, + version: version, + data: data, + properties: properties + }); + }; + + Session.prototype.sendProjectFileDeleted = function(type, file) { + return this.send({ + name: "project_file_deleted", + type: type, + file: file + }); + }; + + Session.prototype.createGuestAccount = function(data) { + var chars, i, j, nick; + if (!this.server.rate_limiter.accept("create_account_ip", this.socket.remoteAddress)) { + return; + } + chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; + while (true) { + nick = ""; + for (i = j = 0; j <= 9; i = j += 1) { + nick += chars.charAt(Math.floor(Math.random() * chars.length)); + } + if (!this.content.findUserByNick(nick)) { + break; + } + } + this.user = this.content.createUser({ + nick: nick, + flags: { + guest: true + }, + language: data.language, + date_created: Date.now(), + last_active: Date.now(), + creation_ip: this.socket.remoteAddress + }); + this.user.addListener(this); + this.send({ + name: "guest_created", + nick: nick, + flags: this.user.flags, + info: this.getUserInfo(), + settings: this.user.settings, + token: this.content.createToken(this.user).value, + request_id: data.request_id + }); + return this.logActiveUser(); + }; + + Session.prototype.deleteGuest = function(data) { + if ((this.user != null) && this.user.flags.guest) { + this.user["delete"](); + return this.send({ + name: "guest_deleted", + request_id: data.request_id + }); + } + }; + + Session.prototype.createAccount = function(data) { + var chars, hash, i, j, salt; + if (data.email == null) { + return this.sendError(this.translator.get("email not specified"), data.request_id); + } + if (data.nick == null) { + return this.sendError(this.translator.get("nickname not specified"), data.request_id); + } + if (data.password == null) { + return this.sendError(this.translator.get("password not specified"), data.request_id); + } + if (this.content.findUserByEmail(data.email)) { + return this.sendError(this.translator.get("email already exists"), data.request_id); + } + if (this.content.findUserByNick(data.nick)) { + return this.sendError(this.translator.get("nickname already exists"), data.request_id); + } + if (this.reserved_nicks[data.nick]) { + return this.sendError(this.translator.get("nickname already exists"), data.request_id); + } + if (!RegexLib.nick.test(data.nick)) { + return this.sendError(this.translator.get("Incorrect nickname. Use 5 characters minimum, only letters, numbers or _"), data.request_id); + } + if (!RegexLib.email.test(data.email)) { + return this.sendError(this.translator.get("Incorrect e-mail address"), data.request_id); + } + if (data.password.trim().length < 6) { + return this.sendError(this.translator.get("Password too weak"), data.request_id); + } + if (!this.server.rate_limiter.accept("create_account_ip", this.socket.remoteAddress)) { + return; + } + salt = ""; + chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; + for (i = j = 0; j <= 15; i = j += 1) { + salt += chars.charAt(Math.floor(Math.random() * chars.length)); + } + hash = salt + "|" + SHA256(salt + data.password); + if ((this.user != null) && this.user.flags.guest) { + this.server.content.changeUserNick(this.user, data.nick); + this.server.content.changeUserEmail(this.user, data.email); + this.user.setFlag("guest", false); + this.user.setFlag("newsletter", data.newsletter); + this.user.set("hash", hash); + this.user.resetValidationToken(); + } else { + this.user = this.content.createUser({ + nick: data.nick, + email: data.email, + flags: { + newsletter: data.newsletter + }, + language: data.language, + hash: hash, + date_created: Date.now(), + last_active: Date.now(), + creation_ip: this.socket.remoteAddress + }); + this.user.addListener(this); + } + this.send({ + name: "account_created", + nick: data.nick, + email: data.email, + flags: this.user.flags, + info: this.getUserInfo(), + settings: this.user.settings, + notifications: [this.server.content.translator.getTranslator(data.language).get("Account created successfully!")], + token: this.content.createToken(this.user).value, + request_id: data.request_id + }); + this.sendValidationMail(); + return this.logActiveUser(); + }; + + Session.prototype.login = function(data) { + var h, hash, s, user; + if (data.nick == null) { + return; + } + if (!this.server.rate_limiter.accept("login_ip", this.socket.remoteAddress)) { + return; + } + if (!this.server.rate_limiter.accept("login_user", data.nick)) { + return; + } + user = this.content.findUserByNick(data.nick); + if (user == null) { + user = this.content.findUserByEmail(data.nick); + } + if ((user != null) && (user.hash != null)) { + hash = user.hash; + s = hash.split("|"); + h = SHA256(s[0] + data.password); + if (h.toString() === s[1]) { + this.user = user; + this.user.addListener(this); + this.send({ + name: "logged_in", + token: this.content.createToken(this.user).value, + nick: this.user.nick, + email: this.user.email, + flags: !this.user.flags.censored ? this.user.flags : {}, + info: this.getUserInfo(), + settings: this.user.settings, + notifications: this.user.notifications, + request_id: data.request_id + }); + this.user.notifications = []; + return this.logActiveUser(); + } else { + return this.sendError("wrong password", data.request_id); + } + } else { + return this.sendError("unknown user", data.request_id); + } + }; + + Session.prototype.getUserInfo = function() { + return { + size: this.user.getTotalSize(), + early_access: this.user.early_access, + max_storage: this.user.max_storage, + description: this.user.description + }; + }; + + Session.prototype.sendPasswordRecovery = function(data) { + var user; + if (data.email != null) { + user = this.content.findUserByEmail(data.email); + if (user != null) { + if (this.server.rate_limiter.accept("send_mail_user", user.id)) { + this.server.content.sendPasswordRecoveryMail(user); + } + } + } + return this.send({ + name: "send_password_recovery", + request_id: data.request_id + }); + }; + + Session.prototype.checkToken = function(data) { + var token; + token = this.content.findToken(data.token); + if (token != null) { + this.user = token.user; + this.user.addListener(this); + this.send({ + name: "token_valid", + nick: this.user.nick, + email: this.user.email, + flags: !this.user.flags.censored ? this.user.flags : {}, + info: this.getUserInfo(), + settings: this.user.settings, + notifications: this.user.notifications, + request_id: data.request_id + }); + this.user.notifications = []; + this.user.set("last_active", Date.now()); + return this.logActiveUser(); + } else { + return this.sendError("invalid token", data.request_id); + } + }; + + Session.prototype.send = function(data) { + return this.socket.send(JSON.stringify(data)); + }; + + Session.prototype.sendError = function(error, request_id) { + return this.send({ + name: "error", + error: error, + request_id: request_id + }); + }; + + Session.prototype.createProject = function(data) { + if (this.user == null) { + return this.sendError("not connected"); + } + if (!this.server.rate_limiter.accept("create_project_user", this.user.id)) { + return; + } + return this.content.createProject(this.user, data, (function(_this) { + return function(project) { + return _this.send({ + name: "project_created", + id: project.id, + request_id: data.request_id + }); + }; + })(this)); + }; + + Session.prototype.clonePublicProject = function(data) { + var project; + if (this.user == null) { + return this.sendError("not connected"); + } + if (!this.server.rate_limiter.accept("create_project_user", this.user.id)) { + return; + } + if (data.project == null) { + return this.sendError(""); + } + project = this.server.content.projects[data.project]; + if ((project != null) && project["public"]) { + return this.content.createProject(this.user, { + title: project.title, + slug: project.slug, + "public": false + }, ((function(_this) { + return function(clone) { + var files, folders, funk, man; + clone.setType(project.type); + clone.setOrientation(project.orientation); + clone.setAspect(project.aspect); + clone.setGraphics(project.graphics); + clone.set("files", JSON.parse(JSON.stringify(project.files))); + man = _this.getProjectManager(project); + folders = ["ms", "sprites", "maps", "sounds", "sounds_th", "music", "music_th", "doc"]; + files = []; + funk = function() { + var dest, f, folder, src; + if (folders.length > 0) { + folder = folders.splice(0, 1)[0]; + return man.listFiles(folder, function(list) { + var f, j, len; + for (j = 0, len = list.length; j < len; j++) { + f = list[j]; + files.push({ + file: f.file, + folder: folder + }); + } + return funk(); + }); + } else if (files.length > 0) { + f = files.splice(0, 1)[0]; + src = project.owner.id + "/" + project.id + "/" + f.folder + "/" + f.file; + dest = clone.owner.id + "/" + clone.id + "/" + f.folder + "/" + f.file; + return _this.server.content.files.copy(src, dest, function() { + return funk(); + }); + } else { + return _this.send({ + name: "project_created", + id: clone.id, + request_id: data.request_id + }); + } + }; + return funk(); + }; + })(this)), true); + } + }; + + Session.prototype.cloneProject = function(data) { + var manager, project; + if (this.user == null) { + return this.sendError("not connected"); + } + if (!this.server.rate_limiter.accept("create_project_user", this.user.id)) { + return; + } + if (data.project == null) { + return this.sendError(""); + } + project = this.server.content.projects[data.project]; + if (project != null) { + manager = this.getProjectManager(project); + if (manager.canRead(this.user)) { + return this.content.createProject(this.user, { + title: data.title || project.title, + slug: project.slug, + "public": false + }, ((function(_this) { + return function(clone) { + var files, folders, funk, man; + clone.setType(project.type); + clone.setOrientation(project.orientation); + clone.setAspect(project.aspect); + clone.setGraphics(project.graphics); + clone.set("files", JSON.parse(JSON.stringify(project.files))); + man = _this.getProjectManager(project); + folders = ["ms", "sprites", "maps", "sounds", "sounds_th", "music", "music_th", "doc"]; + files = []; + funk = function() { + var dest, f, folder, src; + if (folders.length > 0) { + folder = folders.splice(0, 1)[0]; + return man.listFiles(folder, function(list) { + var f, j, len; + for (j = 0, len = list.length; j < len; j++) { + f = list[j]; + files.push({ + file: f.file, + folder: folder + }); + } + return funk(); + }); + } else if (files.length > 0) { + f = files.splice(0, 1)[0]; + src = project.owner.id + "/" + project.id + "/" + f.folder + "/" + f.file; + dest = clone.owner.id + "/" + clone.id + "/" + f.folder + "/" + f.file; + return _this.server.content.files.copy(src, dest, function() { + return funk(); + }); + } else { + return _this.send({ + name: "project_created", + id: clone.id, + request_id: data.request_id + }); + } + }; + return funk(); + }; + })(this)), true); + } + } + }; + + Session.prototype.getProjectManager = function(project) { + if (project.manager == null) { + new ProjectManager(project); + } + return project.manager; + }; + + Session.prototype.setProjectPublic = function(data) { + var project; + if (this.user == null) { + return this.sendError("not connected"); + } + if (data["public"] && !this.user.flags["validated"]) { + return; + } + project = this.user.findProject(data.project); + if (project != null) { + this.content.setProjectPublic(project, data["public"]); + return this.send({ + name: "set_project_public", + id: project.id, + "public": project["public"], + request_id: data.request_id + }); + } + }; + + Session.prototype.setProjectTags = function(data) { + var project; + if (this.user == null) { + return this.sendError("not connected"); + } + if (data["public"] && !this.user.flags["validated"]) { + return; + } + if (data.project == null) { + return; + } + project = this.user.findProject(data.project); + if ((project == null) && this.user.flags.admin) { + project = this.content.projects[data.project]; + } + if ((project != null) && (data.tags != null)) { + this.content.setProjectTags(project, data.tags); + return this.send({ + name: "set_project_tags", + id: project.id, + tags: project.tags, + request_id: data.request_id + }); + } + }; + + Session.prototype.setProjectOption = function(data) { + var project; + if (this.user == null) { + return this.sendError("not connected"); + } + if (data.value == null) { + return this.sendError("no value"); + } + project = this.user.findProject(data.project); + if (project != null) { + switch (data.option) { + case "title": + if (!project.setTitle(data.value)) { + this.send({ + name: "error", + value: project.title, + request_id: data.request_id + }); + } + break; + case "slug": + if (!project.setSlug(data.value)) { + this.send({ + name: "error", + value: project.slug, + request_id: data.request_id + }); + } + break; + case "description": + project.set("description", data.value); + break; + case "code": + if (!project.setCode(data.value)) { + this.send({ + name: "error", + value: project.code, + request_id: data.request_id + }); + } + break; + case "platforms": + if (Array.isArray(data.value)) { + project.setPlatforms(data.value); + } + break; + case "controls": + if (Array.isArray(data.value)) { + project.setControls(data.value); + } + break; + case "type": + if (typeof data.value === "string") { + project.setType(data.value); + } + break; + case "orientation": + if (typeof data.value === "string") { + project.setOrientation(data.value); + } + break; + case "aspect": + if (typeof data.value === "string") { + project.setAspect(data.value); + } + break; + case "graphics": + if (this.user.flags.m3d) { + if (typeof data.value === "string") { + project.setGraphics(data.value); + } + } + } + if (project.manager != null) { + project.manager.propagateOptions(this); + } + return project.touch(); + } + }; + + Session.prototype.deleteProject = function(data) { + var project; + if (this.user == null) { + return this.sendError("not connected"); + } + project = this.user.findProject(data.project); + if (project != null) { + this.user.deleteProject(project); + return this.send({ + name: "project_deleted", + id: project.id, + request_id: data.request_id + }); + } + }; + + Session.prototype.getProjectList = function(data) { + var j, k, len, len1, link, list, p, source; + if (this.user == null) { + return this.sendError("not connected"); + } + source = this.user.listProjects(); + list = []; + for (j = 0, len = source.length; j < len; j++) { + p = source[j]; + if (!p.deleted) { + list.push({ + id: p.id, + owner: { + id: p.owner.id, + nick: p.owner.nick + }, + title: p.title, + slug: p.slug, + code: p.code, + description: p.description, + tags: p.tags, + platforms: p.platforms, + controls: p.controls, + type: p.type, + orientation: p.orientation, + aspect: p.aspect, + graphics: p.graphics, + date_created: p.date_created, + last_modified: p.last_modified, + "public": p["public"], + size: p.getSize(), + users: p.listUsers() + }); + } + } + source = this.user.listProjectLinks(); + for (k = 0, len1 = source.length; k < len1; k++) { + link = source[k]; + if (!link.project.deleted) { + p = link.project; + list.push({ + id: p.id, + owner: { + id: p.owner.id, + nick: p.owner.nick + }, + accepted: link.accepted, + title: p.title, + slug: p.slug, + code: p.code, + description: p.description, + tags: p.tags, + platforms: p.platforms, + controls: p.controls, + type: p.type, + orientation: p.orientation, + aspect: p.aspect, + graphics: p.graphics, + date_created: p.date_created, + last_modified: p.last_modified, + "public": p["public"], + users: p.listUsers() + }); + } + } + return this.send({ + name: "project_list", + list: list, + request_id: data != null ? data.request_id : void 0 + }); + }; + + Session.prototype.lockProjectFile = function(data) { + var project; + if (this.user == null) { + return this.sendError("not connected"); + } + if (data.project != null) { + project = this.content.projects[data.project]; + } + if (project != null) { + this.setCurrentProject(project); + return project.manager.lockFile(this, data.file); + } + }; + + Session.prototype.writeProjectFile = function(data) { + var project; + if (this.user == null) { + return this.sendError("not connected"); + } + if (data.project != null) { + project = this.content.projects[data.project]; + } + if (project != null) { + this.setCurrentProject(project); + return project.manager.writeProjectFile(this, data); + } + }; + + Session.prototype.renameProjectFile = function(data) { + var project; + if (this.user == null) { + return this.sendError("not connected"); + } + if (data.project != null) { + project = this.content.projects[data.project]; + } + if (project != null) { + this.setCurrentProject(project); + return project.manager.renameProjectFile(this, data); + } + }; + + Session.prototype.deleteProjectFile = function(data) { + var project; + if (this.user == null) { + return this.sendError("not connected"); + } + if (data.project != null) { + project = this.content.projects[data.project]; + } + if (project != null) { + this.setCurrentProject(project); + return project.manager.deleteProjectFile(this, data); + } + }; + + Session.prototype.readProjectFile = function(data) { + var project; + if (this.user == null) { + return this.sendError("not connected"); + } + if (data.project != null) { + project = this.content.projects[data.project]; + } + if (project != null) { + this.setCurrentProject(project); + return project.manager.readProjectFile(this, data); + } + }; + + Session.prototype.listProjectFiles = function(data) { + var project; + if (this.user == null) { + return this.sendError("not connected"); + } + if (data.project != null) { + project = this.content.projects[data.project]; + } + if (project != null) { + this.setCurrentProject(project); + return project.manager.listProjectFiles(this, data); + } + }; + + Session.prototype.listPublicProjectFiles = function(data) { + var manager, project; + if (data.project != null) { + project = this.content.projects[data.project]; + } + if (project != null) { + manager = this.getProjectManager(project); + return manager.listProjectFiles(this, data); + } + }; + + Session.prototype.readPublicProjectFile = function(data) { + var manager, project; + if (data.project != null) { + project = this.content.projects[data.project]; + } + if ((project != null) && project["public"]) { + manager = this.getProjectManager(project); + return project.manager.readProjectFile(this, data); + } + }; + + Session.prototype.listenToProject = function(data) { + var project, user; + user = data.user; + project = data.project; + if ((user != null) && (project != null)) { + user = this.content.findUserByNick(user); + if (user != null) { + project = user.findProjectBySlug(project); + if (project != null) { + if ((this.project != null) && (this.project.manager != null)) { + this.project.manager.removeListener(this); + } + this.project = project; + if (this.project.manager == null) { + new ProjectManager(this.project); + } + return this.project.manager.addListener(this); + } + } + } + }; + + Session.prototype.getFileVersions = function(data) { + var project, user; + user = data.user; + project = data.project; + if ((user != null) && (project != null)) { + user = this.content.findUserByNick(user); + if (user != null) { + project = user.findProjectBySlug(project); + if (project != null) { + if (project.manager == null) { + new ProjectManager(project); + } + return project.manager.getFileVersions((function(_this) { + return function(res) { + return _this.send({ + name: "project_file_versions", + data: res, + request_id: data.request_id + }); + }; + })(this)); + } + } + } + }; + + Session.prototype.getPublicProjects = function(data) { + var i, j, len, list, p, source; + switch (data.ranking) { + case "new": + source = this.content.new_projects; + break; + case "top": + source = this.content.top_projects; + break; + default: + source = this.content.hot_projects; + } + list = []; + for (i = j = 0, len = source.length; j < len; i = ++j) { + p = source[i]; + if (list.length >= 300) { + break; + } + if (p["public"] && !p.deleted && !p.owner.flags.censored) { + list.push({ + id: p.id, + title: p.title, + description: p.description, + type: p.type, + tags: p.tags, + slug: p.slug, + owner: p.owner.nick, + owner_info: { + tier: p.owner.flags.tier + }, + likes: p.likes, + liked: (this.user != null) && this.user.isLiked(p.id), + tags: p.tags, + date_published: p.first_published + }); + } + } + return this.send({ + name: "public_projects", + list: list, + request_id: data.request_id + }); + }; + + Session.prototype.toggleLike = function(data) { + var project; + if (this.user == null) { + return this.sendError("not connected"); + } + if (!this.user.flags.validated) { + return this.sendError("not validated"); + } + project = this.content.projects[data.project]; + if (project != null) { + if (this.user.isLiked(project.id)) { + this.user.removeLike(project.id); + project.likes--; + } else { + this.user.addLike(project.id); + project.likes++; + } + return this.send({ + name: "project_likes", + likes: project.likes, + liked: this.user.isLiked(project.id), + request_id: data.request_id + }); + } + }; + + Session.prototype.inviteToProject = function(data) { + var project, user; + if (this.user == null) { + return this.sendError("not connected", data.request_id); + } + user = this.content.findUserByNick(data.user); + if (user == null) { + return this.sendError("user not found", data.request_id); + } + project = this.user.findProject(data.project); + if (project == null) { + return this.sendError("project not found", data.request_id); + } + this.setCurrentProject(project); + return project.manager.inviteUser(this, user); + }; + + Session.prototype.acceptInvite = function(data) { + var j, k, len, len1, li, link, ref, ref1; + if (this.user == null) { + return this.sendError("not connected"); + } + ref = this.user.project_links; + for (j = 0, len = ref.length; j < len; j++) { + link = ref[j]; + if (link.project.id === data.project) { + link.accept(); + this.setCurrentProject(link.project); + if (link.project.manager != null) { + link.project.manager.propagateUserListChange(); + } + ref1 = this.user.listeners; + for (k = 0, len1 = ref1.length; k < len1; k++) { + li = ref1[k]; + li.getProjectList(); + } + } + } + }; + + Session.prototype.removeProjectUser = function(data) { + var j, k, len, len1, li, link, project, ref, ref1, user; + if (this.user == null) { + return this.sendError("not connected"); + } + if (data.project != null) { + project = this.content.projects[data.project]; + } + if (project == null) { + return this.sendError("project not found", data.request_id); + } + if (data.user != null) { + user = this.content.findUserByNick(data.user); + } + if (user == null) { + return this.sendError("user not found", data.request_id); + } + if (this.user !== project.owner && this.user !== user) { + return; + } + ref = project.users; + for (j = 0, len = ref.length; j < len; j++) { + link = ref[j]; + if (link.user === user) { + link.remove(); + if (this.user === project.owner) { + this.setCurrentProject(project); + } else { + this.send({ + name: "project_link_deleted", + request_id: data.request_id + }); + } + if (project.manager != null) { + project.manager.propagateUserListChange(); + } + ref1 = user.listeners; + for (k = 0, len1 = ref1.length; k < len1; k++) { + li = ref1[k]; + li.getProjectList(); + } + } + } + }; + + Session.prototype.sendValidationMail = function(data) { + if (this.user == null) { + return this.sendError("not connected"); + } + if (this.server.rate_limiter.accept("send_mail_user", this.user.id)) { + this.server.content.sendValidationMail(this.user); + if (data != null) { + this.send({ + name: "send_validation_mail", + request_id: data.request_id + }); + } + } + }; + + Session.prototype.changeNick = function(data) { + if (this.user == null) { + return; + } + if (data.nick == null) { + return; + } + if (!RegexLib.nick.test(data.nick)) { + return this.send({ + name: "error", + value: "Incorrect nickname", + request_id: data.request_id + }); + } else { + if ((this.server.content.findUserByNick(data.nick) != null) || this.reserved_nicks[data.nick]) { + return this.send({ + name: "error", + value: "Nickname not available", + request_id: data.request_id + }); + } else { + this.server.content.changeUserNick(this.user, data.nick); + return this.send({ + name: "change_nick", + nick: data.nick, + request_id: data.request_id + }); + } + } + }; + + Session.prototype.changeEmail = function(data) { + if (this.user == null) { + return; + } + if (data.email == null) { + return; + } + if (!RegexLib.email.test(data.email)) { + return this.send({ + name: "error", + value: "Incorrect email", + request_id: data.request_id + }); + } else { + if (this.server.content.findUserByEmail(data.email) != null) { + return this.send({ + name: "error", + value: "E-mail is already used for another account", + request_id: data.request_id + }); + } else { + this.user.setFlag("validated", false); + this.user.resetValidationToken(); + this.server.content.changeUserEmail(this.user, data.email); + this.sendValidationMail(); + return this.send({ + name: "change_email", + email: data.email, + request_id: data.request_id + }); + } + } + }; + + Session.prototype.changeNewsletter = function(data) { + this.user.setFlag("newsletter", data.newsletter); + return this.send({ + name: "change_newsletter", + newsletter: data.newsletter, + request_id: data.request_id + }); + }; + + Session.prototype.setUserSetting = function(data) { + if ((data.setting == null) || (data.value == null)) { + return; + } + return this.user.setSetting(data.setting, data.value); + }; + + Session.prototype.setUserProfile = function(data) { + var content, file; + if (this.user == null) { + return; + } + if (data.image != null) { + if (data.image === 0) { + this.user.setFlag("profile_image", false); + } else { + file = this.user.id + "/profile_image.png"; + content = new Buffer(data.image, "base64"); + this.server.content.files.write(file, content, (function(_this) { + return function() { + _this.user.setFlag("profile_image", true); + return _this.send({ + name: "set_user_profile", + request_id: data.request_id + }); + }; + })(this)); + return; + } + } + if (data.description != null) { + this.user.set("description", data.description); + } + return this.send({ + name: "set_user_profile", + request_id: data.request_id + }); + }; + + Session.prototype.getLanguage = function(msg) { + var lang; + if (msg.language == null) { + return; + } + lang = this.server.content.translator.languages[msg.language]; + lang = lang != null ? lang["export"]() : "{}"; + return this.send({ + name: "get_language", + language: lang, + request_id: msg.request_id + }); + }; + + Session.prototype.getTranslationList = function(msg) { + return this.send({ + name: "get_translation_list", + list: this.server.content.translator.list, + request_id: msg.request_id + }); + }; + + Session.prototype.setTranslation = function(msg) { + var lang, source, translation; + if (this.user == null) { + return; + } + lang = msg.language; + if (!this.user.flags["translator_" + lang]) { + return; + } + source = msg.source; + translation = msg.translation; + if (!this.server.content.translator.languages[lang]) { + this.server.content.translator.createLanguage(lang); + } + return this.server.content.translator.languages[lang].set(this.user.id, source, translation); + }; + + Session.prototype.addTranslation = function(msg) { + var source; + if (this.user == null) { + return; + } + source = msg.source; + return this.server.content.translator.reference(source); + }; + + Session.prototype.getProjectComments = function(data) { + var project; + if (data.project == null) { + return; + } + if (data.project != null) { + project = this.content.projects[data.project]; + } + if ((project != null) && project["public"]) { + return this.send({ + name: "project_comments", + request_id: data.request_id, + comments: project.comments.getAll() + }); + } + }; + + Session.prototype.addProjectComment = function(data) { + var project; + if (data.project == null) { + return; + } + if (data.text == null) { + return; + } + if (data.project != null) { + project = this.content.projects[data.project]; + } + if ((project != null) && project["public"]) { + if ((this.user != null) && this.user.flags.validated && !this.user.flags.banned && !this.user.flags.censored) { + if (!this.server.rate_limiter.accept("post_comment_user", this.user.id)) { + return; + } + project.comments.add(this.user, data.text); + return this.send({ + name: "add_project_comment", + request_id: data.request_id + }); + } + } + }; + + Session.prototype.deleteProjectComment = function(data) { + var c, project; + if (data.project == null) { + return; + } + if (data.id == null) { + return; + } + if (data.project != null) { + project = this.content.projects[data.project]; + } + if ((project != null) && project["public"]) { + if (this.user != null) { + c = project.comments.get(data.id); + if ((c != null) && (c.user === this.user || this.user.flags.admin)) { + c.remove(); + return this.send({ + name: "delete_project_comment", + request_id: data.request_id + }); + } + } + } + }; + + Session.prototype.editProjectComment = function(data) { + var c, project; + if (data.project == null) { + return; + } + if (data.id == null) { + return; + } + if (data.text == null) { + return; + } + if (data.project != null) { + project = this.content.projects[data.project]; + } + if ((project != null) && project["public"]) { + if (this.user != null) { + c = project.comments.get(data.id); + if ((c != null) && c.user === this.user) { + c.edit(data.text); + return this.send({ + name: "edit_project_comment", + request_id: data.request_id + }); + } + } + } + }; + + Session.prototype.buildProject = function(msg) { + var build, project; + if (this.user == null) { + return this.sendError("not connected"); + } + if (msg.project != null) { + project = this.content.projects[msg.project]; + } + if (project != null) { + this.setCurrentProject(project); + if (!project.manager.canWrite(this.user)) { + return; + } + if (msg.target == null) { + return; + } + build = this.server.build_manager.startBuild(project, msg.target); + return this.send({ + name: "build_project", + request_id: msg.request_id, + build: build != null ? build["export"]() : null + }); + } + }; + + Session.prototype.getBuildStatus = function(msg) { + var build, project; + if (this.user == null) { + return this.sendError("not connected"); + } + if (msg.project != null) { + project = this.content.projects[msg.project]; + } + if (project != null) { + this.setCurrentProject(project); + if (!project.manager.canWrite(this.user)) { + return; + } + if (msg.target == null) { + return; + } + build = this.server.build_manager.getBuildInfo(project, msg.target); + return this.send({ + name: "build_project", + request_id: msg.request_id, + build: build != null ? build["export"]() : null, + active_target: this.server.build_manager.hasBuilder(msg.target) + }); + } + }; + + Session.prototype.timeCheck = function() { + if (Date.now() > this.last_active + 5 * 60000) { + this.socket.close(); + this.server.sessionClosed(this); + return this.socket.terminate(); + } + }; + + Session.prototype.startBuilder = function(msg) { + if (msg.target != null) { + if (msg.key === this.server.config["builder-key"]) { + this.server.sessionClosed(this); + return this.server.build_manager.registerBuilder(this, msg.target); + } + } + }; + + Session.prototype.backupComplete = function(msg) { + if (msg.key === this.server.config["backup-key"]) { + this.server.sessionClosed(this); + return this.server.last_backup_time = Date.now(); + } + }; + + return Session; + +})(); + +module.exports = this.Session; diff --git a/server/webapp.coffee b/server/webapp.coffee new file mode 100644 index 00000000..22061261 --- /dev/null +++ b/server/webapp.coffee @@ -0,0 +1,607 @@ +SHA256 = require("crypto-js/sha256") +pug = require "pug" +fs = require "fs" +ProjectManager = require __dirname+"/session/projectmanager.js" +{ createCanvas, loadImage } = require('canvas') +Concatenator = require __dirname+"/concatenator.js" +Fonts = require __dirname+"/fonts.js" +ExportFeatures = require __dirname+"/app/exportfeatures.js" +ForumApp = require __dirname+"/forum/forumapp.js" + +marked = require "marked" +sanitizeHTML = require "sanitize-html" +allowedTags = sanitizeHTML.defaults.allowedTags.concat ["img"] + + +class @WebApp + constructor:(@server,@app)-> + @code = "" + fs.readFile "../templates/play/manifest.json",(err,data)=> + @manifest_template = data + + #@app.get /^\/[A-Za-z0-9]+(?:[ _-][A-Za-z0-9]+)*\/[A-Za-z0-9]+(?:[ _-][A-Za-z0-9]+)*$/,(req,res)=> + # redir = req.protocol+'://' + req.get('host') + req.url+"/" + # console.info "redirecting to: "+redir + # redir = res.redirect(redir) + + @forum_app = new ForumApp @server,@ + + @home_funk = {} + @concatenator = new Concatenator @ + @fonts = new Fonts + + @export_features = new ExportFeatures @ + @server.build_manager.createLinks(@app) + + @languages = ["en","fr","pl","de"] + home_exp = "^(\\/" + for i in [1..@languages.length-1] by 1 + home_exp += "|\\/#{@languages[i]}\\/?" + if i==@languages.length-1 + home_exp += "|" + + @reserved = ["explore","documentation","projects","about","login"] + @reserved_exact = ["tutorials"] + + for r in @reserved + home_exp += "\\/#{r}|\\/#{r}\\/.*|" + + for r in @reserved_exact + home_exp += "\\/#{r}|\\/#{r}\\/|" + + home_exp += "\\/tutorial\\/[^\\/\\|\\?\\&\\.]+\\/[^\\/\\|\\?\\&\\.]+(\\/([^\\/\\|\\?\\&\\.]+\\/?)?)|" + + home_exp += "(\\/i\\/.*))$" + + + console.info "home_exp = #{home_exp}" + + @app.get new RegExp(home_exp), (req,res)=> + return if @ensureDevArea(req,res) + + lang = @getLanguage(req) + for l in @languages + if req.path == "/#{l}" or req.path == "/#{l}/" + lang = l + #console.info "language=#{lang}" + + if not @home_funk[lang]? or not @server.use_cache + @home_funk[lang] = pug.compileFile "../templates/home.pug" + + res.send @home_funk[lang]( + tags: @server.content.tag_list + name: "microStudio" + patches: @server.content.hot_patches + javascript_files: @concatenator.getHomeJSFiles() + css_files: @concatenator.getHomeCSSFiles() + select: "hot" + path: req.path + translator: @server.content.translator.getTranslator(lang) + language: lang + ) + + for plugin in @server.plugins + if plugin.addWebHooks? + plugin.addWebHooks @app + + @app.get /^\/discord\/?$/, (req,res)=> + res.redirect "https://discord.gg/nEMpBU7" + + # email validation + @app.get /^\/v\/\d+\/[a-z0-9A-Z]+\/?$/,(req,res,next)=> + #console.info "matched email validation" + return if @ensureDevArea(req,res) + s = req.path.split("/") + userid = s[2] + token = s[3] + #console.info "userid = #{userid}" + #console.info "token = #{token}" + user = @server.content.users[userid] + if user? + @server.content.validateEMailAddress(user,token) + redir = req.protocol+'://' + req.get("host") + #console.info "redirecting to: "+redir + return res.redirect(redir) + @return404(req,res) + + # password recovery 1 + @app.get /^\/pw\/\d+\/[a-z0-9A-Z]+\/?$/,(req,res,next)=> + #console.info "matched email validation" + return if @ensureDevArea(req,res) + s = req.path.split("/") + userid = s[2] + token = s[3] + #console.info "userid = #{userid}" + #console.info "token = #{token}" + user = @server.content.users[userid] + if user? and user.getValidationToken() == token + page = pug.compileFile "../templates/password/reset1.pug" + return res.send page + userid: userid + token: token + return + else + @return404(req,res) + + # password recovery 2 + @app.get /^\/pwd\/\d+\/[a-z0-9A-Z]+\/.*\/?$/,(req,res,next)=> + #console.info "matched email validation" + return if @ensureDevArea(req,res) + s = req.path.split("/") + userid = s[2] + token = s[3] + pass = s[4] + console.info "userid = #{userid}" + console.info "token = #{token}" + user = @server.content.users[userid] + if user? and user.getValidationToken() == token + salt = "" + chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789" + for i in [0..15] by 1 + salt += chars.charAt(Math.floor(Math.random()*chars.length)) + hash = salt+"|"+SHA256(salt+pass) + user.set "hash",hash + user.resetValidationToken() + translator = @server.content.translator.getTranslator(user.language) + user.notify translator.get "Your password was successfully changed" + page = pug.compileFile "../templates/password/reset2.pug" + return res.send page({}) + else + @return404(req,res) + + @app.get /^\/lang\/list\/?$/, (req,res)=> + return if @ensureDevArea(req,res) + res.setHeader("Content-Type", "application/json") + res.send JSON.stringify @server.content.translator.list + + @app.get /^\/lang\/[a-z]+\/?$/, (req,res)=> + return if @ensureDevArea(req,res) + lang = req.path.split("/")[2] + lang = @server.content.translator.languages[lang] + res.setHeader("Content-Type", "application/json") + if lang? + res.send lang.export() + else + res.send "{}" + + @app.get /^\/box\/?$/, (req,res)=> + return if @ensureIOArea(req,res) + #if not @console_funk? or not @server.use_cache + @console_funk = pug.compileFile "../templates/console/console.pug" + + res.send @console_funk + gamelist: @server.content.getConsoleGameList() + + # /user/project[/code/] + @app.get /^\/[^\/\|\?\&\.]+\/[^\/\|\?\&\.]+(\/([^\/\|\?\&\.]+\/?)?)?$/,(req,res)=> + return if @ensureIOArea(req,res) + if req.path.charAt(req.path.length-1) != "/" + redir = req.protocol+'://' + req.get("host") + req.url+"/" + console.info "redirecting to: "+redir + return res.redirect(redir) + + access = @getProjectAccess req,res + return if not access? + + user = access.user + project = access.project + + file = "#{user.id}/#{project.id}/ms/main.ms" + + encoding = "text" + + manager = @getProjectManager(project) + + manager.listFiles "ms",(sources)=> + manager.listFiles "sprites",(sprites)=> + manager.listFiles "maps",(maps)=> + manager.listFiles "sounds",(sounds)=> + manager.listFiles "music",(music)=> + resources = JSON.stringify + sources: sources + images: sprites + maps: maps + sounds: sounds + music: music + + resources = "var resources = #{resources};\n" + if not @play_funk? or not @server.use_cache + @play_funk = pug.compileFile "../templates/play/play.pug" + + res.send @play_funk + user: user + javascript_files: @concatenator.getPlayerJSFiles(project.graphics) + fonts: @fonts.fonts + game: + name: project.slug + title: project.title + author: user.nick + resources: resources + orientation: project.orientation + aspect: project.aspect + graphics: project.graphics + + @app.get /^\/[A-Za-z0-9]+(?:[ _-][A-Za-z0-9]+)*\/?$/,(req,res)=> + return if @ensureIOArea(req,res) + @getUserPublicPage(req,res) + + @app.get /^\/[^\/\|\?\&\.]+\/[^\/\|\?\&\.]+(\/([^\/\|\?\&\.]+)?)?\/manifest.json$/,(req,res)=> + return if @ensureIOArea(req,res) + + access = @getProjectAccess req,res + return if not access? + + user = access.user + project = access.project + + manager = @getProjectManager(project) + iconversion = manager.getFileVersion("sprites/icon.png") + + path = if project.public then "/#{user.nick}/#{project.slug}/" else "/#{user.nick}/#{project.slug}/#{project.code}/" + + res.setHeader("Content-Type", "application/json") + s = req.path.split("/") + mani = @manifest_template.toString().replace(/SCOPE/g,path) + mani = mani.toString().replace("APPNAME",project.title) + mani = mani.toString().replace("APPSHORTNAME",project.title) + mani = mani.toString().replace("ORIENTATION",project.orientation) + mani = mani.toString().replace(/USER/g,user.nick) + mani = mani.toString().replace(/PROJECT/g,project.slug) + mani = mani.toString().replace(/ICONVERSION/g,iconversion) + mani = mani.replace("START_URL",path) + res.send mani + + @app.get /^\/[^\/\|\?\&\.]+\/[^\/\|\?\&\.]+(\/([^\/\|\?\&\.]+)?)?\/sw.js$/,(req,res)=> + return if @ensureIOArea(req,res) + + access = @getProjectAccess req,res + return if not access? + + user = access.user + project = access.project + + fs.readFile "../static/sw.js",(err,data)=> + res.setHeader("Content-Type", "application/javascript") + res.send data + + # User Profile Image + @app.get /^\/[^\/\|\?\&\.]+.png$/,(req,res)=> + s = req.path.split("/") + user = s[1].split(".")[0] + + user = @server.content.findUserByNick(user) + if not user? or not user.flags.profile_image + @return404(req,res) + return null + + path = "#{user.id}/profile_image.png" + + @server.content.files.read path,"binary",(content)=> + if content? + res.setHeader("Content-Type", "image/png") + res.send content + else + @return404(req,res) + return null + + @app.get /^\/[^\/\|\?\&\.]+\/[^\/\|\?\&\.]+(\/([^\/\|\?\&\.]+)?)?\/icon[0-9]+.png$/,(req,res)=> + access = @getProjectAccess req,res + return if not access? + + user = access.user + project = access.project + + size = req.path.split("icon") + size = size[size.length-1] + size = Math.min(1024,size.split(".")[0]|0) + + path = "#{user.id}/#{project.id}/sprites/icon.png" + path = @server.content.files.sanitize path + + loadImage("../files/#{path}").then ((image)=> + canvas = createCanvas(size,size) + context = canvas.getContext "2d" + context.antialias = "none" + context.imageSmoothingEnabled = false + if image? + context.drawImage image,0,0,size,size + + context.antialias = "default" + context.globalCompositeOperation = "destination-in" + @fillRoundRect context,0,0,size,size,size/8 + canvas.toBuffer (err,result)=> + res.setHeader("Content-Type", "image/png") + res.send result + ),(error)=> + canvas = createCanvas(size,size) + context = canvas.getContext "2d" + context.fillStyle = "#000" + context.fillRect 0,0,size,size + context.antialias = "default" + context.globalCompositeOperation = "destination-in" + @fillRoundRect context,0,0,size,size,size/8 + + canvas.toBuffer (err,result)=> + res.setHeader("Content-Type", "image/png") + res.send result + + # source files for player + @app.get /^\/[^\/\|\?\&\.]+\/[^\/\|\?\&\.]+(\/([^\/\|\?\&\.]+)?)?\/ms\/[A-Za-z0-9_]+.ms$/,(req,res)=> + s = req.path.split("/") + access = @getProjectAccess req,res + return if not access? + + user = access.user + project = access.project + ms = s[s.length-1] + + @server.content.files.read "#{user.id}/#{project.id}/ms/#{ms}","text",(content)=> + if content? + res.setHeader("Content-Type", "application/javascript") + res.send content + else + console.info "couldn't read file: #{req.path}" + res.status(404).send("Error 404") + + # asset thumbnail + @app.get /^\/[^\/\|\?\&\.]+\/[^\/\|\?\&\.]+(\/([^\/\|\?\&\.]+)?)?\/(assets_th|sounds_th|music_th)\/[A-Za-z0-9_]+.png$/,(req,res)=> + s = req.path.split("/") + access = @getProjectAccess req,res + return if not access? + + user = access.user + project = access.project + folder = s[s.length-2] + asset = s[s.length-1] + + console.info "loading #{user.id}/#{project.id}/#{folder}/#{asset}" + + @server.content.files.read "#{user.id}/#{project.id}/#{folder}/#{asset}","binary",(content)=> + if content? + res.setHeader("Content-Type", "image/png") + res.send content + else + console.info "couldn't read file: #{req.path}" + res.status(404).send("Error 404") + + # image files for player ; should be deprecated in favor of /sprites/ + @app.get /^\/[^\/\|\?\&\.]+\/[^\/\|\?\&\.]+(\/([^\/\|\?\&\.]+)?)?\/[A-Za-z0-9_]+.png$/,(req,res)=> + s = req.path.split("/") + access = @getProjectAccess req,res + return if not access? + + user = access.user + project = access.project + image = s[s.length-1] + + @server.content.files.read "#{user.id}/#{project.id}/sprites/#{image}","binary",(content)=> + if content? + res.setHeader("Content-Type", "image/png") + res.send content + else + console.info "couldn't read file: #{req.path}" + res.status(404).send("Error 404") + + # image files for player and all + @app.get /^\/[^\/\|\?\&\.]+\/[^\/\|\?\&\.]+(\/([^\/\|\?\&\.]+)?)?\/sprites\/[A-Za-z0-9_]+.png$/,(req,res)=> + s = req.path.split("/") + access = @getProjectAccess req,res + return if not access? + + user = access.user + project = access.project + image = s[s.length-1] + + @server.content.files.read "#{user.id}/#{project.id}/sprites/#{image}","binary",(content)=> + if content? + res.setHeader("Content-Type", "image/png") + res.send content + else + console.info "couldn't read file: #{req.path}" + res.status(404).send("Error 404") + + # map files for player + @app.get /^\/[^\/\|\?\&\.]+\/[^\/\|\?\&\.]+(\/([^\/\|\?\&\.]+)?)?\/maps\/[A-Za-z0-9_]+.json$/,(req,res)=> + s = req.path.split("/") + access = @getProjectAccess req,res + return if not access? + + user = access.user + project = access.project + map = s[s.length-1] + + @server.content.files.read "#{user.id}/#{project.id}/maps/#{map}","text",(content)=> + if content? + res.setHeader("Content-Type", "application/json") + res.send content + else + console.info "couldn't read file: #{req.path}" + res.status(404).send("Error 404") + + # sound files for player and all + @app.get /^\/[^\/\|\?\&\.]+\/[^\/\|\?\&\.]+(\/([^\/\|\?\&\.]+)?)?\/sounds\/[A-Za-z0-9_]+.wav$/,(req,res)=> + s = req.path.split("/") + access = @getProjectAccess req,res + return if not access? + + user = access.user + project = access.project + sound = s[s.length-1] + + @server.content.files.read "#{user.id}/#{project.id}/sounds/#{sound}","binary",(content)=> + if content? + res.setHeader("Content-Type", "audio/wav") + res.send content + else + console.info "couldn't read file: #{req.path}" + res.status(404).send("Error 404") + + + # music files for player and all + @app.get /^\/[^\/\|\?\&\.]+\/[^\/\|\?\&\.]+(\/([^\/\|\?\&\.]+)?)?\/music\/[A-Za-z0-9_]+.mp3$/,(req,res)=> + s = req.path.split("/") + access = @getProjectAccess req,res + return if not access? + + user = access.user + project = access.project + music = s[s.length-1] + + @server.content.files.read "#{user.id}/#{project.id}/music/#{music}","binary",(content)=> + if content? + res.setHeader("Content-Type", "audio/mp3") + res.send content + else + console.info "couldn't read file: #{req.path}" + res.status(404).send("Error 404") + + + # asset files + @app.get /^\/[^\/\|\?\&\.]+\/[^\/\|\?\&\.]+(\/([^\/\|\?\&\.]+)?)?\/assets\/[A-Za-z0-9_]+.(glb|jpg|png)$/,(req,res)=> + s = req.path.split("/") + access = @getProjectAccess req,res + return if not access? + + user = access.user + project = access.project + asset = s[s.length-1] + + @server.content.files.read "#{user.id}/#{project.id}/assets/#{asset}","binary",(content)=> + if content? + switch asset.split(".")[1] + when "png" then res.setHeader("Content-Type", "image/png") + when "jpg" then res.setHeader("Content-Type", "image/jpg") + when "glb" then res.setHeader("Content-Type", "model/gltf-binary") + + res.send content + else + console.info "couldn't read file: #{req.path}" + res.status(404).send("Error 404") + + # doc files + @app.get /^\/[^\/\|\?\&\.]+\/[^\/\|\?\&\.]+(\/([^\/\|\?\&\.]+)?)?\/doc\/[A-Za-z0-9_]+.md$/,(req,res)=> + s = req.path.split("/") + access = @getProjectAccess req,res + return if not access? + + user = access.user + project = access.project + doc = s[s.length-1] + + @server.content.files.read "#{user.id}/#{project.id}/doc/#{doc}","text",(content)=> + if content? + res.setHeader("Content-Type", "text/markdown") + res.send content + else + console.info "couldn't read file: #{req.path}" + res.status(404).send("Error 404") + + @app.use (req,res)=> + @return404(req,res) + + return404:(req,res)-> + if not @err404_funk? or not @server.use_cache + @err404_funk = pug.compileFile "../templates/404.pug" + + res.status(404).send @err404_funk {} + + ensureDevArea:(req,res)-> + #console.info req.get("host") + + if req.get("host").indexOf(".io")>0 + host = req.get("host").replace(".io",".dev") + redir = req.protocol+'://' + host + req.url + console.info "redirecting to: "+redir + redir = res.redirect(redir) + true + else + false + + ensureIOArea:(req,res)-> + #console.info req.get("host") + + if req.get("host").indexOf(".dev")>0 + host = req.get("host").replace(".dev",".io") + redir = req.protocol+'://' + host + req.url + console.info "redirecting to: "+redir + redir = res.redirect(redir) + true + else + false + + getUserPublicPage:(req,res)-> + s = req.path.split("/") + user = s[1] + + user = @server.content.findUserByNick(user) + if not user? + @return404(req,res) + + projects = user.listPublicProjects() + projects.sort (a,b)-> b.last_modified-a.last_modified + + funk = pug.compileFile "../templates/play/userpage.pug" + res.send funk + user: user.nick + profile_image: user.flags.profile_image == true + description: sanitizeHTML marked(user.description),{allowedTags:allowedTags} + projects: projects + + getLanguage:(request)-> + if request.cookies.language? + lang = request.cookies.language + else + lang = request.headers["accept-language"] + if lang? and lang.length>=2 + lang = lang.substring(0,2) + else + lang = "en" + + if lang not in @languages + lang = "en" + + lang + + getProjectAccess:(req,res)-> + s = req.path.split("/") + user = s[1] + project = s[2] + code = s[3] + + user = @server.content.findUserByNick(user) + if not user? + @return404(req,res) + return null + + project = user.findProjectBySlug project + if not project? + @return404(req,res) + return null + + if project.public or project.code == code + return { user: user, project: project } + + res.send "Project does not exist" + return null + + getProjectManager:(project)-> + if not project.manager? + new ProjectManager project + project.manager + + roundRect: (context,x, y, w, h, r)-> + r = w / 2 if (w < 2 * r) + r = h / 2 if (h < 2 * r) + context.beginPath() + context.moveTo(x+r, y) + context.arcTo(x+w, y, x+w, y+h, r) + context.arcTo(x+w, y+h, x, y+h, r) + context.arcTo(x, y+h, x, y, r) + context.arcTo(x, y, x+w, y, r) + context.closePath() + + fillRoundRect: (context,x, y, w, h, r)-> + @roundRect context,x,y,w,h,r + context.fill() + +module.exports = @WebApp diff --git a/server/webapp.js b/server/webapp.js new file mode 100644 index 00000000..eac2a629 --- /dev/null +++ b/server/webapp.js @@ -0,0 +1,744 @@ +var Concatenator, ExportFeatures, Fonts, ForumApp, ProjectManager, SHA256, allowedTags, createCanvas, fs, loadImage, marked, pug, ref, sanitizeHTML, + indexOf = [].indexOf || function(item) { for (var i = 0, l = this.length; i < l; i++) { if (i in this && this[i] === item) return i; } return -1; }; + +SHA256 = require("crypto-js/sha256"); + +pug = require("pug"); + +fs = require("fs"); + +ProjectManager = require(__dirname + "/session/projectmanager.js"); + +ref = require('canvas'), createCanvas = ref.createCanvas, loadImage = ref.loadImage; + +Concatenator = require(__dirname + "/concatenator.js"); + +Fonts = require(__dirname + "/fonts.js"); + +ExportFeatures = require(__dirname + "/app/exportfeatures.js"); + +ForumApp = require(__dirname + "/forum/forumapp.js"); + +marked = require("marked"); + +sanitizeHTML = require("sanitize-html"); + +allowedTags = sanitizeHTML.defaults.allowedTags.concat(["img"]); + +this.WebApp = (function() { + function WebApp(server, app) { + var home_exp, i, j, k, len, len1, len2, m, n, plugin, r, ref1, ref2, ref3, ref4; + this.server = server; + this.app = app; + this.code = ""; + fs.readFile("../templates/play/manifest.json", (function(_this) { + return function(err, data) { + return _this.manifest_template = data; + }; + })(this)); + this.forum_app = new ForumApp(this.server, this); + this.home_funk = {}; + this.concatenator = new Concatenator(this); + this.fonts = new Fonts; + this.export_features = new ExportFeatures(this); + this.server.build_manager.createLinks(this.app); + this.languages = ["en", "fr", "pl", "de"]; + home_exp = "^(\\/"; + for (i = j = 1, ref1 = this.languages.length - 1; j <= ref1; i = j += 1) { + home_exp += "|\\/" + this.languages[i] + "\\/?"; + if (i === this.languages.length - 1) { + home_exp += "|"; + } + } + this.reserved = ["explore", "documentation", "projects", "about", "login"]; + this.reserved_exact = ["tutorials"]; + ref2 = this.reserved; + for (k = 0, len = ref2.length; k < len; k++) { + r = ref2[k]; + home_exp += "\\/" + r + "|\\/" + r + "\\/.*|"; + } + ref3 = this.reserved_exact; + for (m = 0, len1 = ref3.length; m < len1; m++) { + r = ref3[m]; + home_exp += "\\/" + r + "|\\/" + r + "\\/|"; + } + home_exp += "\\/tutorial\\/[^\\/\\|\\?\\&\\.]+\\/[^\\/\\|\\?\\&\\.]+(\\/([^\\/\\|\\?\\&\\.]+\\/?)?)|"; + home_exp += "(\\/i\\/.*))$"; + console.info("home_exp = " + home_exp); + this.app.get(new RegExp(home_exp), (function(_this) { + return function(req, res) { + var l, lang, len2, n, ref4; + if (_this.ensureDevArea(req, res)) { + return; + } + lang = _this.getLanguage(req); + ref4 = _this.languages; + for (n = 0, len2 = ref4.length; n < len2; n++) { + l = ref4[n]; + if (req.path === ("/" + l) || req.path === ("/" + l + "/")) { + lang = l; + } + } + if ((_this.home_funk[lang] == null) || !_this.server.use_cache) { + _this.home_funk[lang] = pug.compileFile("../templates/home.pug"); + } + return res.send(_this.home_funk[lang]({ + tags: _this.server.content.tag_list, + name: "microStudio", + patches: _this.server.content.hot_patches, + javascript_files: _this.concatenator.getHomeJSFiles(), + css_files: _this.concatenator.getHomeCSSFiles(), + select: "hot", + path: req.path, + translator: _this.server.content.translator.getTranslator(lang), + language: lang + })); + }; + })(this)); + ref4 = this.server.plugins; + for (n = 0, len2 = ref4.length; n < len2; n++) { + plugin = ref4[n]; + if (plugin.addWebHooks != null) { + plugin.addWebHooks(this.app); + } + } + this.app.get(/^\/discord\/?$/, (function(_this) { + return function(req, res) { + return res.redirect("https://discord.gg/nEMpBU7"); + }; + })(this)); + this.app.get(/^\/v\/\d+\/[a-z0-9A-Z]+\/?$/, (function(_this) { + return function(req, res, next) { + var redir, s, token, user, userid; + if (_this.ensureDevArea(req, res)) { + return; + } + s = req.path.split("/"); + userid = s[2]; + token = s[3]; + user = _this.server.content.users[userid]; + if (user != null) { + _this.server.content.validateEMailAddress(user, token); + redir = req.protocol + '://' + req.get("host"); + return res.redirect(redir); + } + return _this.return404(req, res); + }; + })(this)); + this.app.get(/^\/pw\/\d+\/[a-z0-9A-Z]+\/?$/, (function(_this) { + return function(req, res, next) { + var page, s, token, user, userid; + if (_this.ensureDevArea(req, res)) { + return; + } + s = req.path.split("/"); + userid = s[2]; + token = s[3]; + user = _this.server.content.users[userid]; + if ((user != null) && user.getValidationToken() === token) { + page = pug.compileFile("../templates/password/reset1.pug"); + return res.send(page({ + userid: userid, + token: token + })); + } else { + return _this.return404(req, res); + } + }; + })(this)); + this.app.get(/^\/pwd\/\d+\/[a-z0-9A-Z]+\/.*\/?$/, (function(_this) { + return function(req, res, next) { + var chars, hash, o, page, pass, s, salt, token, translator, user, userid; + if (_this.ensureDevArea(req, res)) { + return; + } + s = req.path.split("/"); + userid = s[2]; + token = s[3]; + pass = s[4]; + console.info("userid = " + userid); + console.info("token = " + token); + user = _this.server.content.users[userid]; + if ((user != null) && user.getValidationToken() === token) { + salt = ""; + chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; + for (i = o = 0; o <= 15; i = o += 1) { + salt += chars.charAt(Math.floor(Math.random() * chars.length)); + } + hash = salt + "|" + SHA256(salt + pass); + user.set("hash", hash); + user.resetValidationToken(); + translator = _this.server.content.translator.getTranslator(user.language); + user.notify(translator.get("Your password was successfully changed")); + page = pug.compileFile("../templates/password/reset2.pug"); + return res.send(page({})); + } else { + return _this.return404(req, res); + } + }; + })(this)); + this.app.get(/^\/lang\/list\/?$/, (function(_this) { + return function(req, res) { + if (_this.ensureDevArea(req, res)) { + return; + } + res.setHeader("Content-Type", "application/json"); + return res.send(JSON.stringify(_this.server.content.translator.list)); + }; + })(this)); + this.app.get(/^\/lang\/[a-z]+\/?$/, (function(_this) { + return function(req, res) { + var lang; + if (_this.ensureDevArea(req, res)) { + return; + } + lang = req.path.split("/")[2]; + lang = _this.server.content.translator.languages[lang]; + res.setHeader("Content-Type", "application/json"); + if (lang != null) { + return res.send(lang["export"]()); + } else { + return res.send("{}"); + } + }; + })(this)); + this.app.get(/^\/box\/?$/, (function(_this) { + return function(req, res) { + if (_this.ensureIOArea(req, res)) { + return; + } + _this.console_funk = pug.compileFile("../templates/console/console.pug"); + return res.send(_this.console_funk({ + gamelist: _this.server.content.getConsoleGameList() + })); + }; + })(this)); + this.app.get(/^\/[^\/\|\?\&\.]+\/[^\/\|\?\&\.]+(\/([^\/\|\?\&\.]+\/?)?)?$/, (function(_this) { + return function(req, res) { + var access, encoding, file, manager, project, redir, user; + if (_this.ensureIOArea(req, res)) { + return; + } + if (req.path.charAt(req.path.length - 1) !== "/") { + redir = req.protocol + '://' + req.get("host") + req.url + "/"; + console.info("redirecting to: " + redir); + return res.redirect(redir); + } + access = _this.getProjectAccess(req, res); + if (access == null) { + return; + } + user = access.user; + project = access.project; + file = user.id + "/" + project.id + "/ms/main.ms"; + encoding = "text"; + manager = _this.getProjectManager(project); + return manager.listFiles("ms", function(sources) { + return manager.listFiles("sprites", function(sprites) { + return manager.listFiles("maps", function(maps) { + return manager.listFiles("sounds", function(sounds) { + return manager.listFiles("music", function(music) { + var resources; + resources = JSON.stringify({ + sources: sources, + images: sprites, + maps: maps, + sounds: sounds, + music: music + }); + resources = "var resources = " + resources + ";\n"; + if ((_this.play_funk == null) || !_this.server.use_cache) { + _this.play_funk = pug.compileFile("../templates/play/play.pug"); + } + return res.send(_this.play_funk({ + user: user, + javascript_files: _this.concatenator.getPlayerJSFiles(project.graphics), + fonts: _this.fonts.fonts, + game: { + name: project.slug, + title: project.title, + author: user.nick, + resources: resources, + orientation: project.orientation, + aspect: project.aspect, + graphics: project.graphics + } + })); + }); + }); + }); + }); + }); + }; + })(this)); + this.app.get(/^\/[A-Za-z0-9]+(?:[ _-][A-Za-z0-9]+)*\/?$/, (function(_this) { + return function(req, res) { + if (_this.ensureIOArea(req, res)) { + return; + } + return _this.getUserPublicPage(req, res); + }; + })(this)); + this.app.get(/^\/[^\/\|\?\&\.]+\/[^\/\|\?\&\.]+(\/([^\/\|\?\&\.]+)?)?\/manifest.json$/, (function(_this) { + return function(req, res) { + var access, iconversion, manager, mani, path, project, s, user; + if (_this.ensureIOArea(req, res)) { + return; + } + access = _this.getProjectAccess(req, res); + if (access == null) { + return; + } + user = access.user; + project = access.project; + manager = _this.getProjectManager(project); + iconversion = manager.getFileVersion("sprites/icon.png"); + path = project["public"] ? "/" + user.nick + "/" + project.slug + "/" : "/" + user.nick + "/" + project.slug + "/" + project.code + "/"; + res.setHeader("Content-Type", "application/json"); + s = req.path.split("/"); + mani = _this.manifest_template.toString().replace(/SCOPE/g, path); + mani = mani.toString().replace("APPNAME", project.title); + mani = mani.toString().replace("APPSHORTNAME", project.title); + mani = mani.toString().replace("ORIENTATION", project.orientation); + mani = mani.toString().replace(/USER/g, user.nick); + mani = mani.toString().replace(/PROJECT/g, project.slug); + mani = mani.toString().replace(/ICONVERSION/g, iconversion); + mani = mani.replace("START_URL", path); + return res.send(mani); + }; + })(this)); + this.app.get(/^\/[^\/\|\?\&\.]+\/[^\/\|\?\&\.]+(\/([^\/\|\?\&\.]+)?)?\/sw.js$/, (function(_this) { + return function(req, res) { + var access, project, user; + if (_this.ensureIOArea(req, res)) { + return; + } + access = _this.getProjectAccess(req, res); + if (access == null) { + return; + } + user = access.user; + project = access.project; + return fs.readFile("../static/sw.js", function(err, data) { + res.setHeader("Content-Type", "application/javascript"); + return res.send(data); + }); + }; + })(this)); + this.app.get(/^\/[^\/\|\?\&\.]+.png$/, (function(_this) { + return function(req, res) { + var path, s, user; + s = req.path.split("/"); + user = s[1].split(".")[0]; + user = _this.server.content.findUserByNick(user); + if ((user == null) || !user.flags.profile_image) { + _this.return404(req, res); + return null; + } + path = user.id + "/profile_image.png"; + return _this.server.content.files.read(path, "binary", function(content) { + if (content != null) { + res.setHeader("Content-Type", "image/png"); + return res.send(content); + } else { + _this.return404(req, res); + return null; + } + }); + }; + })(this)); + this.app.get(/^\/[^\/\|\?\&\.]+\/[^\/\|\?\&\.]+(\/([^\/\|\?\&\.]+)?)?\/icon[0-9]+.png$/, (function(_this) { + return function(req, res) { + var access, path, project, size, user; + access = _this.getProjectAccess(req, res); + if (access == null) { + return; + } + user = access.user; + project = access.project; + size = req.path.split("icon"); + size = size[size.length - 1]; + size = Math.min(1024, size.split(".")[0] | 0); + path = user.id + "/" + project.id + "/sprites/icon.png"; + path = _this.server.content.files.sanitize(path); + return loadImage("../files/" + path).then((function(image) { + var canvas, context; + canvas = createCanvas(size, size); + context = canvas.getContext("2d"); + context.antialias = "none"; + context.imageSmoothingEnabled = false; + if (image != null) { + context.drawImage(image, 0, 0, size, size); + } + context.antialias = "default"; + context.globalCompositeOperation = "destination-in"; + _this.fillRoundRect(context, 0, 0, size, size, size / 8); + return canvas.toBuffer(function(err, result) { + res.setHeader("Content-Type", "image/png"); + return res.send(result); + }); + }), function(error) { + var canvas, context; + canvas = createCanvas(size, size); + context = canvas.getContext("2d"); + context.fillStyle = "#000"; + context.fillRect(0, 0, size, size); + context.antialias = "default"; + context.globalCompositeOperation = "destination-in"; + _this.fillRoundRect(context, 0, 0, size, size, size / 8); + return canvas.toBuffer(function(err, result) { + res.setHeader("Content-Type", "image/png"); + return res.send(result); + }); + }); + }; + })(this)); + this.app.get(/^\/[^\/\|\?\&\.]+\/[^\/\|\?\&\.]+(\/([^\/\|\?\&\.]+)?)?\/ms\/[A-Za-z0-9_]+.ms$/, (function(_this) { + return function(req, res) { + var access, ms, project, s, user; + s = req.path.split("/"); + access = _this.getProjectAccess(req, res); + if (access == null) { + return; + } + user = access.user; + project = access.project; + ms = s[s.length - 1]; + return _this.server.content.files.read(user.id + "/" + project.id + "/ms/" + ms, "text", function(content) { + if (content != null) { + res.setHeader("Content-Type", "application/javascript"); + return res.send(content); + } else { + console.info("couldn't read file: " + req.path); + return res.status(404).send("Error 404"); + } + }); + }; + })(this)); + this.app.get(/^\/[^\/\|\?\&\.]+\/[^\/\|\?\&\.]+(\/([^\/\|\?\&\.]+)?)?\/(assets_th|sounds_th|music_th)\/[A-Za-z0-9_]+.png$/, (function(_this) { + return function(req, res) { + var access, asset, folder, project, s, user; + s = req.path.split("/"); + access = _this.getProjectAccess(req, res); + if (access == null) { + return; + } + user = access.user; + project = access.project; + folder = s[s.length - 2]; + asset = s[s.length - 1]; + console.info("loading " + user.id + "/" + project.id + "/" + folder + "/" + asset); + return _this.server.content.files.read(user.id + "/" + project.id + "/" + folder + "/" + asset, "binary", function(content) { + if (content != null) { + res.setHeader("Content-Type", "image/png"); + return res.send(content); + } else { + console.info("couldn't read file: " + req.path); + return res.status(404).send("Error 404"); + } + }); + }; + })(this)); + this.app.get(/^\/[^\/\|\?\&\.]+\/[^\/\|\?\&\.]+(\/([^\/\|\?\&\.]+)?)?\/[A-Za-z0-9_]+.png$/, (function(_this) { + return function(req, res) { + var access, image, project, s, user; + s = req.path.split("/"); + access = _this.getProjectAccess(req, res); + if (access == null) { + return; + } + user = access.user; + project = access.project; + image = s[s.length - 1]; + return _this.server.content.files.read(user.id + "/" + project.id + "/sprites/" + image, "binary", function(content) { + if (content != null) { + res.setHeader("Content-Type", "image/png"); + return res.send(content); + } else { + console.info("couldn't read file: " + req.path); + return res.status(404).send("Error 404"); + } + }); + }; + })(this)); + this.app.get(/^\/[^\/\|\?\&\.]+\/[^\/\|\?\&\.]+(\/([^\/\|\?\&\.]+)?)?\/sprites\/[A-Za-z0-9_]+.png$/, (function(_this) { + return function(req, res) { + var access, image, project, s, user; + s = req.path.split("/"); + access = _this.getProjectAccess(req, res); + if (access == null) { + return; + } + user = access.user; + project = access.project; + image = s[s.length - 1]; + return _this.server.content.files.read(user.id + "/" + project.id + "/sprites/" + image, "binary", function(content) { + if (content != null) { + res.setHeader("Content-Type", "image/png"); + return res.send(content); + } else { + console.info("couldn't read file: " + req.path); + return res.status(404).send("Error 404"); + } + }); + }; + })(this)); + this.app.get(/^\/[^\/\|\?\&\.]+\/[^\/\|\?\&\.]+(\/([^\/\|\?\&\.]+)?)?\/maps\/[A-Za-z0-9_]+.json$/, (function(_this) { + return function(req, res) { + var access, map, project, s, user; + s = req.path.split("/"); + access = _this.getProjectAccess(req, res); + if (access == null) { + return; + } + user = access.user; + project = access.project; + map = s[s.length - 1]; + return _this.server.content.files.read(user.id + "/" + project.id + "/maps/" + map, "text", function(content) { + if (content != null) { + res.setHeader("Content-Type", "application/json"); + return res.send(content); + } else { + console.info("couldn't read file: " + req.path); + return res.status(404).send("Error 404"); + } + }); + }; + })(this)); + this.app.get(/^\/[^\/\|\?\&\.]+\/[^\/\|\?\&\.]+(\/([^\/\|\?\&\.]+)?)?\/sounds\/[A-Za-z0-9_]+.wav$/, (function(_this) { + return function(req, res) { + var access, project, s, sound, user; + s = req.path.split("/"); + access = _this.getProjectAccess(req, res); + if (access == null) { + return; + } + user = access.user; + project = access.project; + sound = s[s.length - 1]; + return _this.server.content.files.read(user.id + "/" + project.id + "/sounds/" + sound, "binary", function(content) { + if (content != null) { + res.setHeader("Content-Type", "audio/wav"); + return res.send(content); + } else { + console.info("couldn't read file: " + req.path); + return res.status(404).send("Error 404"); + } + }); + }; + })(this)); + this.app.get(/^\/[^\/\|\?\&\.]+\/[^\/\|\?\&\.]+(\/([^\/\|\?\&\.]+)?)?\/music\/[A-Za-z0-9_]+.mp3$/, (function(_this) { + return function(req, res) { + var access, music, project, s, user; + s = req.path.split("/"); + access = _this.getProjectAccess(req, res); + if (access == null) { + return; + } + user = access.user; + project = access.project; + music = s[s.length - 1]; + return _this.server.content.files.read(user.id + "/" + project.id + "/music/" + music, "binary", function(content) { + if (content != null) { + res.setHeader("Content-Type", "audio/mp3"); + return res.send(content); + } else { + console.info("couldn't read file: " + req.path); + return res.status(404).send("Error 404"); + } + }); + }; + })(this)); + this.app.get(/^\/[^\/\|\?\&\.]+\/[^\/\|\?\&\.]+(\/([^\/\|\?\&\.]+)?)?\/assets\/[A-Za-z0-9_]+.(glb|jpg|png)$/, (function(_this) { + return function(req, res) { + var access, asset, project, s, user; + s = req.path.split("/"); + access = _this.getProjectAccess(req, res); + if (access == null) { + return; + } + user = access.user; + project = access.project; + asset = s[s.length - 1]; + return _this.server.content.files.read(user.id + "/" + project.id + "/assets/" + asset, "binary", function(content) { + if (content != null) { + switch (asset.split(".")[1]) { + case "png": + res.setHeader("Content-Type", "image/png"); + break; + case "jpg": + res.setHeader("Content-Type", "image/jpg"); + break; + case "glb": + res.setHeader("Content-Type", "model/gltf-binary"); + } + return res.send(content); + } else { + console.info("couldn't read file: " + req.path); + return res.status(404).send("Error 404"); + } + }); + }; + })(this)); + this.app.get(/^\/[^\/\|\?\&\.]+\/[^\/\|\?\&\.]+(\/([^\/\|\?\&\.]+)?)?\/doc\/[A-Za-z0-9_]+.md$/, (function(_this) { + return function(req, res) { + var access, doc, project, s, user; + s = req.path.split("/"); + access = _this.getProjectAccess(req, res); + if (access == null) { + return; + } + user = access.user; + project = access.project; + doc = s[s.length - 1]; + return _this.server.content.files.read(user.id + "/" + project.id + "/doc/" + doc, "text", function(content) { + if (content != null) { + res.setHeader("Content-Type", "text/markdown"); + return res.send(content); + } else { + console.info("couldn't read file: " + req.path); + return res.status(404).send("Error 404"); + } + }); + }; + })(this)); + this.app.use((function(_this) { + return function(req, res) { + return _this.return404(req, res); + }; + })(this)); + } + + WebApp.prototype.return404 = function(req, res) { + if ((this.err404_funk == null) || !this.server.use_cache) { + this.err404_funk = pug.compileFile("../templates/404.pug"); + } + return res.status(404).send(this.err404_funk({})); + }; + + WebApp.prototype.ensureDevArea = function(req, res) { + var host, redir; + if (req.get("host").indexOf(".io") > 0) { + host = req.get("host").replace(".io", ".dev"); + redir = req.protocol + '://' + host + req.url; + console.info("redirecting to: " + redir); + redir = res.redirect(redir); + return true; + } else { + return false; + } + }; + + WebApp.prototype.ensureIOArea = function(req, res) { + var host, redir; + if (req.get("host").indexOf(".dev") > 0) { + host = req.get("host").replace(".dev", ".io"); + redir = req.protocol + '://' + host + req.url; + console.info("redirecting to: " + redir); + redir = res.redirect(redir); + return true; + } else { + return false; + } + }; + + WebApp.prototype.getUserPublicPage = function(req, res) { + var funk, projects, s, user; + s = req.path.split("/"); + user = s[1]; + user = this.server.content.findUserByNick(user); + if (user == null) { + this.return404(req, res); + } + projects = user.listPublicProjects(); + projects.sort(function(a, b) { + return b.last_modified - a.last_modified; + }); + funk = pug.compileFile("../templates/play/userpage.pug"); + return res.send(funk({ + user: user.nick, + profile_image: user.flags.profile_image === true, + description: sanitizeHTML(marked(user.description), { + allowedTags: allowedTags + }), + projects: projects + })); + }; + + WebApp.prototype.getLanguage = function(request) { + var lang; + if (request.cookies.language != null) { + lang = request.cookies.language; + } else { + lang = request.headers["accept-language"]; + } + if ((lang != null) && lang.length >= 2) { + lang = lang.substring(0, 2); + } else { + lang = "en"; + } + if (indexOf.call(this.languages, lang) < 0) { + lang = "en"; + } + return lang; + }; + + WebApp.prototype.getProjectAccess = function(req, res) { + var code, project, s, user; + s = req.path.split("/"); + user = s[1]; + project = s[2]; + code = s[3]; + user = this.server.content.findUserByNick(user); + if (user == null) { + this.return404(req, res); + return null; + } + project = user.findProjectBySlug(project); + if (project == null) { + this.return404(req, res); + return null; + } + if (project["public"] || project.code === code) { + return { + user: user, + project: project + }; + } + res.send("Project does not exist"); + return null; + }; + + WebApp.prototype.getProjectManager = function(project) { + if (project.manager == null) { + new ProjectManager(project); + } + return project.manager; + }; + + WebApp.prototype.roundRect = function(context, x, y, w, h, r) { + if (w < 2 * r) { + r = w / 2; + } + if (h < 2 * r) { + r = h / 2; + } + context.beginPath(); + context.moveTo(x + r, y); + context.arcTo(x + w, y, x + w, y + h, r); + context.arcTo(x + w, y + h, x, y + h, r); + context.arcTo(x, y + h, x, y, r); + context.arcTo(x, y, x + w, y, r); + return context.closePath(); + }; + + WebApp.prototype.fillRoundRect = function(context, x, y, w, h, r) { + this.roundRect(context, x, y, w, h, r); + return context.fill(); + }; + + return WebApp; + +})(); + +module.exports = this.WebApp; diff --git a/static/community/forum_sw.js b/static/community/forum_sw.js new file mode 100644 index 00000000..19020f81 --- /dev/null +++ b/static/community/forum_sw.js @@ -0,0 +1,19 @@ +var VERSION = 'v2'; + +var cacheFiles = []; + +self.addEventListener('install', event => { + event.waitUntil( + caches.open(VERSION).then(cache => { + return cache.addAll(cacheFiles); + }) + ); +}); + +self.addEventListener("fetch", function (event) { + event.respondWith( + fetch(event.request).catch(function() { + return caches.open(VERSION).then(cache => cache.match(event.request)) ; + }) + ) +}) ; diff --git a/static/community/manifest.json b/static/community/manifest.json new file mode 100644 index 00000000..28c7ca2b --- /dev/null +++ b/static/community/manifest.json @@ -0,0 +1,29 @@ +{ + "name": "microStudio Community", + "short_name": "mS Community", + "start_url": "/community/", + "display": "fullscreen", + "orientation": "portrait", + "scope": "/community/", + + "icons": [ + { + "src": "/img/forumicon192.png", + "type": "image/png", + "sizes": "192x192" + }, + { + "src": "/img/forumicon196.png", + "type": "image/png", + "sizes": "196x196", + "purpose": "maskable" + }, + { + "src": "/img/forumicon512.png", + "type": "image/png", + "sizes": "512x512" + } + ], + "theme_color": "hsl(160,50%,20%)", + "background_color": "hsl(160,40%,10%)" +} diff --git a/static/community/manifest_fr.json b/static/community/manifest_fr.json new file mode 100644 index 00000000..39b45e1b --- /dev/null +++ b/static/community/manifest_fr.json @@ -0,0 +1,29 @@ +{ + "name": "microStudio Community", + "short_name": "mS Community", + "start_url": "/fr/community/", + "display": "fullscreen", + "orientation": "portrait", + "scope": "/fr/community/", + + "icons": [ + { + "src": "/img/forumicon192.png", + "type": "image/png", + "sizes": "192x192" + }, + { + "src": "/img/forumicon196.png", + "type": "image/png", + "sizes": "196x196", + "purpose": "maskable" + }, + { + "src": "/img/forumicon512.png", + "type": "image/png", + "sizes": "512x512" + } + ], + "theme_color": "hsl(160,50%,20%)", + "background_color": "hsl(160,40%,10%)" +} diff --git a/static/css/404.css b/static/css/404.css new file mode 100644 index 00000000..2e2bb3bc --- /dev/null +++ b/static/css/404.css @@ -0,0 +1,31 @@ +body { + text-align: center ; + background: hsl(200,20%,10%); + padding: 50px ; + color: #FFF ; + font-family: Verdana, sans-serif; + font-size: 16px ; +} + +img { + width: 200px ; +} + +div,a { + margin: 60px 0 0 0; + text-decoration: none ; + color: #FFF ; +} + +.button { + display: inline-block ; + background: hsl(160,50%,40%); + border-radius: 5px ; + padding: 20px 40px ; + cursor: pointer ; +} + +.e404 { + font-size: 120px ; + color: rgba(255,255,255,.2); +} diff --git a/static/css/assets.css b/static/css/assets.css new file mode 100644 index 00000000..1c65b266 --- /dev/null +++ b/static/css/assets.css @@ -0,0 +1,191 @@ +.assets-drop { + position: absolute ; + bottom: 200px ; + left: 40px ; + right: 40px ; + text-align: center; + color: rgba(255,255,255,.2); + z-index: -1 +} + +.assets-drop i { + font-size: 100px ; + margin-bottom: 30px ; +} +.assets-drop div { + font-size: 20px ; +} + +.assetlist { + position: absolute; + background: rgba(0,0,0,.25) ; + left: 2px ; + top: 2px ; + bottom: 40px ; + right: 2px ; + overflow: auto ; + text-align: center ; + line-height: 0 ; + padding-top: 10px ; + padding-bottom: 10px ; +} + +.assetlist.dragover { + background: rgba(255,255,255,.1) ; +} + +.asseteditor { + position: absolute; + top: 40px ; + bottom: 0 ; + left: 220px ; + right: 0 ; +} + +.asset-box { + position: relative ; + cursor: pointer ; + display: inline-block ; + margin: 1px ; + width: 64px ; + height: 84px ; + font-size: 12px ; + background: hsl(200,20%,20%); + box-shadow: 0 0 4px 0px #000; + border-radius: 4px ; + overflow: hidden ; +} +.asset-box.selected { + margin: 0px; + border: solid 1px rgba(255,255,255,.5) ; + background: hsl(200,30%,30%); +} +.asset-box img { + position: absolute ; + top: 0 ; + left: 0 ; + right: 0 ; +} +.asset-box .asset-box-name { + position: absolute ; + bottom: 0 ; + left: 0 ; + right: 0 ; + height: 18px ; + line-height: normal ; +} + +.asset-box .active-user { + display: none ; + position: absolute ; + bottom: 20px ; + left: 4px ; + font-size: 8px ; + color: #FFF ; + background: hsl(0,50%,50%); + padding: 4px ; + border-radius: 8px ; +} + +.assetbar { + position: absolute; + padding: 0px; + right: 0; + top: 40px; + bottom: 0; + width: 220px; + background: rgba(0,0,0,.25); + /*border-left: solid 4px rgba(255,255,255,.2);*/ +} + +.assetbar h3 { + margin: 5px 0 0 0 ; + font-size: 14px; +} + +.assetinfo { + position: absolute; + right: 0px ; + left: 2px ; + top: 0px ; + height: 40px; + padding-left: 10px ; + background: hsl(200,30%,30%); + white-space: nowrap; +} + +.assetinfo * { + display: inline-block ; +} +.assetinfo .buttons { + display: inline-block ; + margin-left: 40px ; +} +.assetinfo .buttons div { + margin-left: 1px ; + padding: 10px 15px ; + cursor: pointer ; + background: hsl(160,50%,40%); +} +.assetinfo .buttons div:hover { + background: hsl(160,50%,60%); +} +.assetinfo .buttons div.delete { + background: hsl(0,50%,50%); +} +.assetinfo .buttons div.delete:hover { + background: hsl(0,50%,60%); +} + +.assetinfo input { + border: none ; + border-radius: 3px ; + margin:6px 3px 0px 0px ; + font-size: 14px ; + padding: 5px ; + background: rgba(255,255,255,.4); +} +.assetinfo input:focus { + outline: none ; + background: rgba(255,255,255,.6); +} + +.assetinfo .validate-button-container { + display: inline-block ; + width: 100px ; + margin-left: 7px ; + transition-duration: .5s ; + transition-property: width ; + overflow: hidden ; + height: 30px ; + vertical-align: middle ; +} +.assetinfo .validate-button { + display: inline-block ; + padding: 5px 10px ; + font-size: 14px ; + border-radius: 3px ; + background: hsl(160,50%,40%); + color: #FFF ; + cursor: pointer ; + white-space: nowrap; +} +.create-asset-button { + position: absolute ; + bottom: 0px ; + height: 40px; + left: 0 ; + right: 0 ; + padding: 0px 30px ; + background: hsl(160,50%,40%); + font-size: 18px ; + cursor: pointer ; + color: #FFF ; + z-index: 1 ; + box-shadow: 0 -10px 20px #000; + line-height: 40px ; + text-align: center ; +} +.create-asset-button i { + margin-right: 10px ; +} diff --git a/static/css/code.css b/static/css/code.css new file mode 100644 index 00000000..d2256b7c --- /dev/null +++ b/static/css/code.css @@ -0,0 +1,514 @@ +.editor-view,#editor-view { + position: absolute ; + top: 40px ; + bottom: 0px ; + left: 40px ; + right: 0 ; + transition-duration: .5s ; + transition-property: left ; +} +#editor-locked { + position: absolute ; + top: 60px ; + left: 240px ; + right: 40px ; + padding: 20px ; + z-index: 100 ; + height: 20px ; + transition-duration: .5s ; + transition-property: left ; + border-radius: 10px ; + color: #FFF ; + font-size: 16px ; + display: none ; +} +#editor-locked i { + margin-right: 5px ; +} + +.code-splitbar { + background: hsl(200,30%,30%); + position: absolute ; + left: 400px ; + top: 0 ; + bottom: 0 ; + width: 10px ; + cursor: ew-resize; + z-index: 2 ; +} + +.editor-separator { + background: linear-gradient(hsl(200,30%,30%) 0%,hsl(200,20%,20%) 10%,hsl(200,20%,10%) 90%,hsl(200,20%,0%) 100%); height: 20px ; + position: absolute ; + padding: 0px 10px; + font-size: 12px; + top: 400px ; + left: 0px ; + right: 0px ; + cursor: ns-resize; +} +.editor-separator i { + padding: 1px 5px ; + font-size: 16px; +} + +.runbar { + position: absolute; + right: 0px ; + left: 0px ; + top: 0px ; + height: 40px; + background: hsl(200,30%,30%); + border-left: solid 2px rgba(0,0,0,.5); + text-align: left ; + padding-left: 20px ; + white-space: nowrap ; +} +.runbar *,.runtime-splitbar * { + display: inline-block ; +} +.runbar .buttons,.runtime-splitbar .buttons { + display: inline-block ; + margin-left: 60px ; +} +.runbar .buttons div,.runtime-splitbar .buttons div { + margin-left: 1px ; + padding: 10px 15px ; + cursor: pointer ; + background: hsl(160,50%,40%); +} +.runbar .buttons div:hover,.runtime-splitbar .buttons div:hover { + background: hsl(160,50%,60%); +} + +.runbar .buttons div:active,.runtime-splitbar .buttons div:active { + background: hsl(160,30%,30%); +} + +.runbar .buttons .selected,.runtime-splitbar .buttons .selected, +.runbar .buttons div.selected:hover,.runtime-splitbar .buttons div.selected:hover +{ + background: hsl(160,20%,35%); + cursor: default ; +} +#run-link { + display: inline-block ; + font-size: 14px ; + padding: 4px 12px ; + border-radius: 14px ; + background: hsl(200,50%,40%); + margin-left: 40px ; + white-space: nowrap ; + overflow: hidden ; + cursor: pointer ; + vertical-align: middle; +} +a#run-link { + color: #FFF ; + text-decoration: none ; +} + +.runtime-container { + position: absolute ; + bottom: 0 ; + top: 0 ; + right: 0 ; + width: 50% ; +} +.runtime { + position: absolute ; + top:0 ; + left: 0 ; + right: 0 ; + height: 100px ; + text-align: center; +} +.runtime-splitbar { + background: hsl(200,30%,30%); + border-left: solid 2px rgba(0,0,0,.5); + position: absolute ; + left: 0px ; + top: 100px ; + right: 0 ; + height: 40px ; + transition-property: height ; + transition-duration: 0.5s ; + cursor: ns-resize; + z-index: 3 ; + overflow: hidden ; + white-space: nowrap; +} + +.runtime-splitbar span { + margin-left: 20px ; +} + +#console-options { + background: rgba(0,0,0,.2); + display: block ; + padding: 20px ; + line-height: 25px ; + white-space: nowrap; + color: rgba(255,255,255,.8); +} + +#console-options label { + margin-left: 5px ; +} + +.devicecontainer { + position: absolute ; + top: 40px ; + left: 0 ; + right: 0 ; + bottom: 0 ; +} +#device { + position: absolute ; + top: 0px ; + bottom: 0 ; + left: 0 ; + right: 0 ; + background: #000 ; +} +#device iframe { + border: none ; +} +#device iframe { + width: 960px; + height: 540px; +} +#ruler { + position: absolute ; + top: 0 ; + bottom: 0 ; + left: 0 ; + right: 0 ; + pointer-events: none ; +} +.help-window { + position: absolute; + right: 5px ; + bottom: 5px ; + background: hsl(30,100%,90%); + border-radius: 20px ; + padding: 10px ; + color: hsl(20,20%,20%); + z-index: 4 ; + margin-left: 5px ; + overflow: hidden ; + min-height: 20px ; + min-width: 20px ; + cursor: pointer ; +} + +.help-window.disabled { + background: hsl(20,0%,60%); +} + +.help-window i { + margin: 0 ; +} + +.help-window .left-side { + position: absolute ; + left: 0 ; + top: 0 ; + bottom: 0 ; + width: 20px ; + padding: 10px ; + background: rgba(0,0,0,.1); + text-align: center ; +} + +.help-window .content { + margin-left: 40px; +} + +.help-window p { + margin: 0 ; +} + +.help-window h1, .help-window h2, .help-window h3, .help-window h4, .help-window h5, .help-window h6 { + font-size: 18px ; + padding: 0 ; + margin: 0 0 5px 0 ; +} + +.help-window .see-doc-button { + border-radius: 3px ; + background: hsl(0,50%,40%); + font-size: 12px ; + color: #FFF ; + padding: 4px 8px ; + float: right ; + margin-left: 10px ; +} + +.help-window .see-doc-button i { + margin-right: 4px ; +} + +.code-tools { + position: absolute ; + right: 2px ; + top: 0px ; + height: 40px ; +} + +.code-tools .buttons { + display: inline-block ; +} +.code-tools .buttons div { + display: inline-block ; + margin-left: 1px ; + padding: 10px 15px ; + cursor: pointer ; + background: hsl(160,50%,40%); + vertical-align: middle ; + height: 20px ; +} +.code-tools .buttons div:hover { + background: hsl(160,50%,60%); +} + +.code-tools .buttons div:active { + background: hsl(160,30%,30%); +} +#code-font-plus { + padding: 8px 11px ; + height: 24px ; + font-size: 19px ; +} +#code-font-minus { + padding: 12px 14px ; + height: 16px ; + font-size: 12px ; +} + +.value-tool-button { + position: relative ; + display: inline-block ; + background: hsl(200,50%,50%); + color: #FFF ; + border-radius: 5px ; + padding: 6px ; + font-size: 13px ; + z-index: 4 ; + cursor: pointer ; + width: 80px ; + display: none ; + margin-left: 20px ; +} + +.value-tool-button span { + background: #FFF ; + border-radius: 3px ; + padding: 2px 4px ; + font-size: 11px ; + color: hsl(200,50%,20%); +} + +.value-tool-button i { + position: absolute; + right: 15px; + top: 5px; + font-size: 20px ; +} + +.value-tool-button .slider-track { + background: rgba(255,255,255,.5) ; + border-radius: 2px ; + width: 30px ; + height: 3px ; + position: absolute ; + right: 10px ; + top: 15px ; +} + +.value-tool-button .slider-cursor { + background: #FFF ; + border-radius: 9px ; + width: 9px ; + height: 9px ; + position: absolute ; + right: 23px ; + top: 12px ; + box-shadow: 0 0 5px hsl(200,50%,20%); +} + +#color-value-tool-button { + display: none ; +} + +.source-list-panel { + position: absolute ; + top: 40px ; + bottom: 0 ; + left: 0 ; + width: 40px ; + transition-duration: .5s ; + transition-property: width ; + overflow: hidden ; +} +.source-list-header { + position: absolute ; + top: 0 ; + left: 0 ; + right: 0 ; + height: 40px ; + cursor: pointer ; + white-space: nowrap ; +} +.source-list-header div { + position: absolute ; + right: 180px ; + transform: translateX(100%); + padding: 10px ; +} +.source-list-header i { + position: absolute ; + top: 0 ; + right: 0 ; + bottom: 0 ; + width: 18px; + padding: 11px 11px; + font-size: 18px; + color: rgba(255,255,255,.9); +} + +.source-list { + position: absolute ; + top: 40px ; + bottom : 40px ; + left: 0 ; + right: 0 ; + font-size: 14px ; + overflow-y: auto ; + overflow-x: hidden ; +} +.source-list i { + padding: 10px 5px ; + font-size: 15px ; +} +.create-source-button { + position: absolute ; + bottom: 0px ; + height: 20px; + left: 0 ; + right: 0 ; + padding: 10px 12px ; + background: hsl(160,50%,40%); + font-size: 18px ; + cursor: pointer ; + color: #FFF ; + z-index: 1 ; + box-shadow: 0 -10px 20px #000; + text-align: center ; + white-space: nowrap ; +} +.create-source-button i { + margin-right: 10px ; +} + +.source-list-item { + white-space: nowrap ; + margin: 1px 0px ; + background: rgba(0,0,0,.25); + cursor: pointer ; + position: relative ; +} +.source-list-item.selected { + background: hsl(200,30%,30%); +} +.source-list-item i { + padding: 10px 6px 10px 14px ; + font-size: 18px ; + white-space: nowrap ; +} +.source-list-item .filename { + display: inline-block; + margin: 10px 0 ; + padding: 2px 8px ; + /*background: rgba(255,255,255,.1);*/ + border-radius: 3px ; + max-width: 150px ; +} +.source-list-item input { + border: none ; + border-radius: 3px ; + font-size: 14px ; + width: 100px ; + padding: 3px 5px ; + background: rgba(0,0,0,.5); + color: rgba(255,255,255,.8); + margin: 5px 10px 5px 0px ; + font-family: "Source Sans Pro",Verdana ; +} +.source-list-item .active-user { + display: none ; + position: absolute ; + bottom: 4px ; + left: 4px ; + font-size: 8px ; + color: #FFF ; + background: hsl(0,50%,50%); + padding: 4px ; + border-radius: 8px ; +} + +.source-list-item input:focus { + outline: none ; +} +.source-tools { + text-align: right ; + position: absolute ; + left: 150px ; +} +.source-tools i { + border-radius: 3px ; + padding: 5px 4px; + margin: 7px 2px; + background: hsl(160,50%,40%); + display: inline-block ; + font-size: 14px ; + cursor: pointer ; +} +.source-tools i.fa-trash { + background: hsl(0,50%,40%); +} + +.value-tool { + padding: 10px ; + background: #333 ; + border:solid 1px rgba(255,255,255,.2); + box-shadow: 0 0 5px #000 ; + border-radius: 5px ; + position: fixed ; +} +.value-tool input { + width: 300px ; +} +.value-tool canvas { + /*background: linear-gradient(to right,#444,rgba(0,0,0,0));*/ +} + +#run-window .content { + text-align: center ; +} + +#run-window .titlebar.runbar { + padding: 2px 0 2px 20px ; + height: 36px ; + border: none ; + background: none ; +} + +#run-window { + background: hsl(200,30%,30%); +} + +#run-window .minify { + top:7px ; + right: 8px ; +} diff --git a/static/css/console.css b/static/css/console.css new file mode 100644 index 00000000..e43139b5 --- /dev/null +++ b/static/css/console.css @@ -0,0 +1,142 @@ +html,body { + background: #EEE ; + font-family: "Ubuntu" ; +} + +.content { + position: fixed ; + top: 0 ; + bottom: 0 ; + left: 0 ; + right: 0 ; + padding: 10vh ; + text-align: left ; +} + +.content .filters { + position: relative ; + border-bottom: solid .5vh rgba(0,0,0,.1); + padding-bottom: 3vh ; +} + +.poweroff { + position: absolute; + font-size: 4vh; + color: hsl(180,50%,40%); + right: 0; + top: 2vh; +} + +.content ul { + display: inline-block ; + margin: 0px 0 0 0 ; + padding: 0 ; + white-space: nowrap; +} + +.content li { + display: inline-block ; + margin-right: 2vh ; + font-size: 3vh; + padding: 1vh 2vh ; + color: hsl(180,50%,40%); + text-align: center; + cursor: pointer ; + border-radius: 1vh ; +} + +.content li.selected { + color: #EEE ; + background: hsl(180,50%,40%); +} + + + +.content .list { + text-align: center ; + white-space: nowrap ; + overflow: hidden ; +} + +.content .list .box { + width: 25vh ; + height: 30vh ; + border-radius: 1vh ; + margin: 17vh 10vh 17vh 0 ; + display: inline-block; + position: relative ; +} + +.box .title { + position: absolute ; + bottom: 0 ; + left: 0 ; + right: 0 ; + height: 2vh ; + color: #444 ; + text-align: center ; + padding: 1vh 0 ; + font-size: 3vh ; +} + +.box img { + position: absolute ; + top: 0 ; + left: 0 ; + right: 0 ; + width: 25vh ; + height: 25vh ; + image-rendering: pixelated ; + border-radius: 1vh ; +} + +.footer { + position: absolute; + bottom: 0; + left: 10vh; + right: 10vh; + height: 11vh; + border-top: solid .5vh rgba(0,0,0,.1); + padding-top: 6vh; + text-align: center; + font-size: 3vh; + color: rgba(0,0,0,.5); +} +.footer div { + display: inline-block ; +} +.footer img { + position: relative ; + top: .1vh ; + height: 4vh ; +} +.footer span { + margin-left: 2vh ; +} + +.focusable { + +} + +.focus { + border: solid 1vh hsl(180,50%,50%); + box-shadow: 0 0 2vh hsl(180,100%,60%) ; + animation: focus-animation 2s infinite; +} + +@keyframes focus-animation { + 0% { + border: solid 1vh hsl(180,50%,70%); + box-shadow: 0 0 2vh hsl(180,100%,70%) ; + } + + 50% { + border: solid 1vh hsl(180,50%,50%); + box-shadow: 0 0 2vh hsl(180,100%,30%) ; + } + + 100% { + border: solid 1vh hsl(180,50%,70%); + box-shadow: 0 0 2vh hsl(180,100%,70%) ; + } +} diff --git a/static/css/doc.css b/static/css/doc.css new file mode 100644 index 00000000..c905ec57 --- /dev/null +++ b/static/css/doc.css @@ -0,0 +1,147 @@ +#doc-section { + border-top: solid 10px hsl(200,30%,30%); +} + +.doc-editor,#doc-editor { + position: absolute ; + top: 0px ; + bottom: 0px ; + left: 0 ; + width: 50% ; +} +.doc-splitbar { + background: hsl(200,7%,28%); + position: absolute ; + left: 400px ; + top: 0 ; + bottom: 0 ; + width: 10px ; + cursor: ew-resize; +} + +.doc-view { + background: #222 ; + position: absolute ; + bottom: 0 ; + top: 0 ; + right: 0 ; + width: 50% ; + background: hsl(30,50%,80%); + overflow: auto ; +} +.doc-view::-webkit-scrollbar-track { + background: rgba(0,0,0,.15); + border-radius: 5px; + margin: 4px 2px; +} + +.doc-view::-webkit-scrollbar-thumb { + background-color: rgba(0,0,0,.25); + border-radius: 5px; + margin: 4px 1px; +} +.doc-render{ + padding: 40px ; + color: rgba(0,0,0,.8); + font-family: Helvetica,sans-serif; + user-select: text ; +} +.doc-render h1,.doc-render h2,.doc-render h3,.doc-render h4,.doc-render h5,.doc-render h6 { + color: rgba(0,0,0,.8); + font-family: Helvetica,sans-serif; +} + +.doc-render h1 { + margin: 0 ; + padding: 40px 0 10px 0 ; +} +.doc-render h2 { + margin: 0 ; + padding: 30px 0 10px 0 ; +} +.doc-render h3 { + margin: 0 ; + padding: 20px 0 10px 0 ; +} +.doc-render .doc-render h4,.doc-render h5,.doc-render h6 { + margin: 0 ; + padding: 10px 0 10px 0 ; +} +.doc-render hr { + height: 40px ; + border: none ; +} +.doc-render p { + margin: 0 0 10px 0 ; +} +.doc-render h5 { + font-size: 15px ; + background: rgba(0,0,0,.1); + border-radius: 5px ; + padding: 5px ; + margin-top: 20px ; + margin-bottom: 10px ; +} + + +.doc-render table { + border-radius: 3px ; + margin-bottom: 20px ; +} + +.doc-render table,tr,td { + border-collapse: collapse; +} + +.doc-render tr,td,th { + border: solid 1px rgba(0,0,0,.1); + padding: 10px ; +} +.doc-render th { + font-weight: bold ; + background: rgba(0,0,0,.05); +} +.doc-render code { + background: rgba(0,0,0,.1); + padding: 2px 5px ; + border-radius: 3px ; + font-family: "Ubuntu Mono"; + line-height: 1.2em; + font-size: 14px ; +} +.doc-render pre code { + display: block ; + background: rgba(0,0,0,.7); + color: rgba(255,255,255,.9); + padding: 10px ; + border-radius: 3px ; + color: rgba(255,255,255,.8); + padding: 5px ; + border-radius: 3px ; + font-family: "Ubuntu Mono"; + line-height: 1.2em; + white-space: pre-wrap ; +} +.doc-render a { + text-decoration: none ; + color: hsla(200,100%,30%,.8); +} + +.doc-render img { + max-width: 100% ; +} + +#doceditor-start-tutorial { + position: absolute ; + right: 20px ; + top: 20px ; + padding: 10px 20px ; + color: white ; + background: hsl(200,50%,40%); + border-radius: 5px ; + cursor: pointer ; +} + +#doceditor-start-tutorial i { + margin-right: 10px ; +} diff --git a/static/css/explore.css b/static/css/explore.css new file mode 100644 index 00000000..440b58f4 --- /dev/null +++ b/static/css/explore.css @@ -0,0 +1,602 @@ +.exploreview { + position: absolute; top:0 ; bottom: 0 ; left: 0 ; right: 0 ; + display: none ; +} +.exploreview i { + margin-right: 10px ; +} +#explore-contents { + position: absolute ; + top: 40px ; + bottom: 0px ; + left: 0 ; + right: 0 ; + overflow: auto ; + padding: 5px ; +} + +#explore-box-list { + text-align: center ; +} +.explore-project-box { + position: relative ; + margin: 20px ; + padding: 10px ; + display: inline-block ; + cursor: pointer ; + overflow: hidden ; + border-radius: 10px ; + width: 200px ; +} +.explore-project-box:hover { + box-shadow: 0 0 10px rgba(0,0,0,.5) ; + background: rgba(255,255,255,.1); +} +.explore-project-box .project-tag { + position: absolute; + right: 5px; + top: 200px; + color: #FFF; + background: hsl(200,50%,40%); + padding: 2px 8px; + border-radius: 25px; + font-size: 12px; + box-shadow: 0 0 4px 1px #000; + transform: rotate(-8deg); +} + +.explore-project-box .icon { + display: block ; + width:200px ; + height: 200px ; +} + +.explore-project-box i { + margin-right: 5px ; +} +.explore-project-box .icon { + background: #000 ; + border-radius: 10px ; +} +.explore-project-box .run-button { + background: hsl(160,50%,40%); + padding: 8px 16px ; + border-radius: 5px ; +} +.explore-project-box .run-button i { + margin-right: 10px ; +} + +.explore-infobox { + margin: 10px 0; + text-align: left ; + font-size: 14px ; +} + +.explore-project-title { + color: #FFF ; + font-size: 24px ; + margin: 5px 0 15px 0 ; + white-space: nowrap ; + text-align: center ; +} + +.explore-project-button { + background: hsl(160,50%,40%); + padding: 4px 8px ; + border-radius: 3px ; + margin: 3px 0 ; + width: 60px ; + display: inline-block ; + text-align: left ; +} + +.explore-project-button i { + width: 20px ; +} + +.explore-project-likes { + background: hsl(200,50%,40%); + padding: 2px 4px ; + border-radius: 3px ; + margin: 20px 0 ; + cursor: pointer ; + position: absolute ; + top: 250px ; + right: 10px ; +} + +.explore-project-likes.voted { + background: hsl(0,50%,50%); +} + +#explore-bar { + position: absolute ; + top: 10px ; + height: 30px ; + left: 0 ; + right: 0 ; + padding: 5px ; + vertical-align: middle ; +} +#explore-back-button { + display: none ; + border-radius: 5px ; + padding: 4px 10px ; + font-size: 14px; + cursor: pointer ; + margin: 2px 40px 0 20px ; + background: hsl(0,50%,50%); +} +#explore-tools { + display: inline-block ; +} +#explore-tools div { + display: inline-block ; +} +#explore-sort-button { + background: hsl(0,50%,50%); + border-radius: 3px ; + padding: 4px 8px ; + font-size: 16px ; + margin-left: 12px ; + margin-top: 1px ; + cursor: pointer ; +} +#explore-sort-button.new { + background: hsl(160,50%,50%); +} +#explore-sort-button.top { + background: hsl(200,50%,50%); +} +#explore-sort-button i { + margin-right: 5px ; +} +#explore-search { + position: absolute ; + right: 0 ; + top: 0 ; + bottom: 0 ; + width: 200px ; +} +#explore-search input { + border-radius: 25px; + border: none; + margin: 6px; + font-size: 14px; + padding: 5px 15px; + background: rgba(255,255,255,.9); + width: 148px ; +} +#explore-search input:focus { + outline: none ; +} +#explore-search i { + position: absolute; + right: 16px; + top: 12px; + z-index: 1; + color: rgba(0,0,0,.3); + font-size: 16px; +} +#explore-tags { + position: absolute ; + left: 150px ; + right: 200px ; + top: 0 ; + bottom: 0 ; + white-space: nowrap ; + overflow: hidden; +} +#explore-tags div { + display: inline-block ; + border-radius: 20px ; + padding: 3px 8px ; + margin: 10px 2px ; + background: rgba(255,255,255,.1); + color: #FFF ; + cursor: pointer ; + font-size: 12px ; +} +#explore-tags span { + font-size: 8px ; + padding: 3px 5px ; + border-radius: 15px ; + background: rgba(255,255,255,.8); + color: rgba(0,0,0,.8); +} +#explore-tags div.active { + background: hsl(200,70%,50%); + box-shadow: 0 0 10px hsl(200,70%,50%); +} +#explore-tag div:hover { + background: hsl(200,70%,50%); +} +#explore-project-details { + display: none; + position: absolute; + top: 52px; + left: 0; + right: 0; + bottom: 0; + border-top: solid 10px hsl(200,30%,30%); +} +#project-details-info { + position: absolute ; + left: 0 ; + top: 40px ; + bottom: 0 ; + width: 45% ; + overflow: auto ; +} +#project-details-contents { + position: absolute ; + right: 0 ; + top: 0px ; + bottom: 0 ; + width: 55% ; +} + +#project-details-info .inside { + padding: 0px 40px 40px 40px ; + position: relative ; +} +#project-details-contents .inside { + /*padding: 40px 0 40px 40px ;*/ + padding-top: 1px ; + padding-left: 1px ; +} +#project-details-image { + width: 200px ; + height: 200px ; + border-radius: 10px ; + /*position: absolute ; + top: 40px ; + right: 40px ;*/ +} +.project-details-summary { +} +.project-details-right { + float: right ; + margin-left: 40px ; + margin-bottom: 40px ; + text-align: right ; +} +#project-details-title { + font-size: 32px ; + margin-bottom: 20px ; +} +#project-details-author { + font-size: 18px ; + margin-bottom: 20px ; +} +#project-details-author i { + margin-right: 5px ; +} +#project-details-likes { + background: hsl(200,50%,40%); + padding: 2px 4px ; + border-radius: 3px ; + margin: 20px 0 ; + cursor: pointer ; + display: inline-block ; +} +#project-details-likes.voted { + background: hsl(0,50%,50%); +} + +#project-details-runbutton { + border-radius: 5px ; +/* position: absolute ; + top: 280px ; + right: 40px ;*/ + padding: 8px 20px ; + background: hsl(160,50%,40%); + text-decoration: none ; + color: #FFF ; + font-size: 18px ; +} +#project-details-clonebutton { + border-radius: 5px ; +/* position: absolute ; + top: 280px ; + right: 40px ;*/ + padding: 8px 20px ; + background: hsl(0,50%,50%); + text-decoration: none ; + color: #FFF ; + font-size: 18px ; +} +#project-details-tags { +} +#project-details-tags div { + display: inline-block ; + margin: 2px 4px 2px 0 ; + border-radius: 15px ; + background: hsl(200,50%,40%); + padding: 3px 8px ; + font-size: 14px ; +} +#project-details-description { + margin-top: 40px ; +} + +#project-details-description a { + color: hsl(200,50%,50%) ; + text-decoration: none ; +} +#project-details-description a:visited { + color: hsl(200,50%,50%) ; +} + +#project-details-comments { +} +#project-comment-list p { + margin: 10px 0 0 0 ; +} +#project-details-comments .comment { +} +#project-details-comments .comment .author { +} +#project-details-comments .comment .contents { +} + +#project-contents-menu ul { + display: inline-block ; + margin: 0px 0 0 0 ; + padding: 0 ; + /*border-bottom: solid 2px rgba(255,255,255,.1);*/ +} + +#project-contents-menu li { + display: block ; + padding: 15px 0px 8px 0 ; + font-size: 12px; + font-family: "Source Sans Pro"; + color: hsla(200,10%,90%,.9); + text-align: center; + width: 60px; + cursor: pointer ; + margin: 0px 0px ; +} + +#project-contents-menu li .fa-code { color: hsla(0,100%,80%,.85);} +#project-contents-menu li .fa-image { color: hsla(45,100%,80%,.85);} +#project-contents-menu li .fa-map { color: hsla(90,100%,80%,.85);} +#project-contents-menu li .fa-volume-up { color: hsla(135,100%,80%,.85);} +#project-contents-menu li .fa-music { color: hsla(180,100%,80%,.85);} +#project-contents-menu li .fa-book { color: hsla(225,100%,80%,.85);} +#project-contents-menu li .fa-server { color: hsla(270,100%,80%,.85);} + +#project-contents-menu li.selected .fa-code { color: hsla(0,100%,70%,1);} +#project-contents-menu li.selected .fa-image { color: hsla(45,100%,70%,1);} +#project-contents-menu li.selected .fa-map { color: hsla(90,100%,70%,1);} +#project-contents-menu li.selected .fa-volume-up { color: hsla(135,100%,70%,1);} +#project-contents-menu li.selected .fa-music { color: hsla(180,100%,70%,1);} +#project-contents-menu li.selected .fa-book { color: hsla(225,100%,70%,1);} +#project-contents-menu li.selected .fa-server { color: hsla(270,100%,70%,1);} + +#project-contents-menu li:hover { + background-color: rgba(255,255,255,.05) ; + color: hsla(200,10%,100%,.9); +} +#project-contents-menu .selected,#project-contents-menu .selected:hover { + background-color: rgba(255,255,255,.2); + margin-bottom: 0px; + color: hsla(200,10%,100%,1); + /*box-shadow: 10px 0px 10px -10px hsla(200,100%,70%,1) inset;*/ +} + +#project-contents-menu li i { + font-size: 20px; + padding-bottom: 5px; +} +#project-contents-view { + position: absolute ; + right: 0 ; + left: 60px ; + top: 0 ; + bottom: 0 ; + overflow: auto ; +} +#project-contents-view .item-list-container { + position: absolute ; + top: 10px ; + left: 10px ; + right: 10px ; + bottom: 70px ; + overflow: auto ; +} +#project-contents-view .sprite-list { + line-height: 0px ; +} +#project-contents-view .sprites .export-panel { + position: absolute ; + bottom: 0 ; + height: 45px ; + left: 0 ; + right: 0 ; + text-align: center ; +} + +#project-contents-view .sprites .export-panel a { + border-radius: 5px ; + background: hsl(160,50%,40%); + color: #FFF ; + font-size: 16px ; + padding: 10px 20px ; + cursor: pointer ; + text-decoration: none ; +} + +#project-contents-view .sprite { + display: inline-block ; + margin: 2px ; + border-radius: 10px ; + overflow: hidden ; + background: rgba(0,0,0,.5); + width: 100px ; + transition-duration: 1s ; + transition-property: width ; +} +#project-contents-view .sprite div { + background: hsl(160,50%,40%); + cursor: pointer ; + font-size: 12px ; + padding: 4px 8px ; + line-height: 14px ; + text-align: center ; + width: 84px ; +} + +#project-contents-view .sound { + display: inline-block ; + margin: 2px ; + border-radius: 10px ; + overflow: hidden ; + background: rgba(0,0,0,.5); + transition-duration: 1s ; + transition-property: width ; + box-shadow: 0 0 4px #000 ; + text-align: center; + font-size: 12px ; + cursor: pointer ; +} + +#project-contents-view .music { + display: inline-block ; + margin: 2px ; + border-radius: 10px ; + overflow: hidden ; + background: rgba(0,0,0,.5); + transition-duration: 1s ; + transition-property: width ; + box-shadow: 0 0 4px #000 ; + text-align: center; + font-size: 12px ; + cursor: pointer ; +} + + +#project-contents-view .doc-render { + background: hsl(30,50%,80%); + padding: 40px ; + border-radius: 10px ; +} +#project-contents-view .code-list { + position: absolute ; + left: 0 ; + width: 100px ; + top: 0px ; + bottom: 0 ; +} +#project-contents-view .code-list div { + font-size: 14px ; + margin: 1px ; + padding: 6.5px ; + background: rgba(255,255,255,.1); + cursor: pointer ; +} +#project-contents-view .code-list div.selected { + background: rgba(255,255,255,.2); +} +#project-contents-view .code-list div i { + margin-right: 5px ; +} +#project-contents-view-editor { + position: absolute ; + left: 100px ; + right: 0 ; + top: 0 ; + bottom: 0 ; + overflow: hidden ; + border-radius: 10px ; +} +#project-contents-view .import-button { + background: hsl(160,50%,40%); + padding: 10px 20px ; + border-radius: 10px ; + cursor: pointer ; + position: absolute ; + top: 40px ; + right: 40px ; + font-size: 16px ; + line-height: 20px ; + text-align: center ; + width: 200px ; + z-index: 1 ; +} +#project-contents-view .import-button.done { + background: hsl(30,50%,40%); +} + +#project-contents-view .import-button i { + margin-right: 10px ; +} +#project-contents-view .doc { + position: absolute ; + top: 10px ; + left: 10px ; + right: 10px ; + bottom: 10px ; + overflow: auto ; +} +#project-contents-view .doc-render { + padding: 40px ; +} + +.project-details-comments { + clear: both ; + margin-top:40px ; +} +.project-details-comments .comment { + margin: 10px 0 ; + padding: 10px ; + background: rgba(255,255,255,.1); + border-radius: 5px ; +} +.project-details-comments .author { + margin-bottom: 10px ; +} +.project-details-comments .buttons { + margin-bottom: 10px ; + margin-left: 10px ; + float: right ; + text-align: right ; +} +.project-details-comments .buttons div { + margin: 5px 0 ; +} +.project-details-comments .time { + margin-bottom: 10px ; + margin-left: 10px ; + padding: 2px 8px ; + border-radius: 10px ; + font-size: 12px ; + background-color: hsla(200,50%,40%,.5); + color: #FFF ; + float: right ; +} +.project-details-comments .contents { + user-select: text ; +} +.project-details-comments .contents a { + color: hsl(200,50%,50%) ; + text-decoration: none ; +} +.project-details-comments .contents a:visited { + color: hsl(200,50%,50%) ; +} +.project-details-comments textarea { + margin: 10px 0 ; + padding: 10px ; + box-sizing: border-box ; + width: 100% ; + background: rgba(255,255,255,.2); + color: #FFF ; + border-radius: 10px ; + border: none ; + outline: none ; + font-size: 14px; + font-family: "Source Sans Pro",Verdana; +} diff --git a/static/css/forum/forum.css b/static/css/forum/forum.css new file mode 100644 index 00000000..d5fca01b --- /dev/null +++ b/static/css/forum/forum.css @@ -0,0 +1,735 @@ +html,body { + font-family: "Source Sans Pro",Verdana; + color: #111 ; + margin: 0px; +} + +header { + position: fixed ; + background: hsl(170,40%,30%); + top: 0 ; + left: 0; + right: 0 ; + height: 60px; + font-size: 20px; + color: rgba(255,255,255,.7); + white-space: nowrap; + z-index: 1 ; +} + +header .logo { + display: inline-block ; + margin-left: 20px ; + margin-top: 18px; + margin-right: 20px ; + color: rgba(255,255,255,.9); + width: 160px ; + vertical-align: top ; + cursor: pointer ; +} + +header .community-icon { + display: inline-block ; + padding-top: 20px ; + font-size: 18px ; + text-align: center; + color: rgba(255,255,255,.8); +} +header .community-icon i { + font-size: 18px ; + margin-right: 10px ; +} + +header .username { + display: none ; + float: right ; + margin: 12px 20px 0 0 ; + padding: 0 10px 0 0 ; + border-radius: 30px ; + background: rgba(255,255,255,.2); + color: rgba(255,255,255,.8) ; + font-size: 18px ; + cursor: pointer ; + height: 38px ; +} +header .username i { + margin-right: 10px ; + margin-left: 10px ; + vertical-align: middle ; +} +header .username img { + width: 32px ; + height: 32px ; + border-radius: 16px ; + margin: 3px 10px 3px 3px; + vertical-align: middle ; +} +.username span { + display: inline-block ; + vertical-align: middle ; + margin: 6px ; +} + +.content { + margin-top: 60px ; +} + +.topright { + float: right ; + margin: 0px 0px 20px 20px ; +} +.topright div, .topright select { + display: inline-block ; +} +.theme { + background: #333 ; + color: #CCC ; + font-size: 10px ; + width: 40px ; + padding: 12px 10px ; + height: 32px ; + text-align: center ; + font-family: Ubuntu,sans-serif ; + cursor: pointer ; +} +.languages { + font-size: 16px ; + padding: 18px 20px ; + border-radius: 0 ; + background: rgba(0,0,0,.1); + border: none ; + outline: none ; + -moz-appearance:none; + -webkit-appearance:none; + appearance:none; +} + +.categories { + background: #EEE ; + padding: 5px ; + font-family: Ubuntu ; + text-align: center ; + margin: 0 ; +} +.categories li { + display: inline-block; +} + +.category { + display: inline-block ; + margin: 5px ; + font-size: 14px ; + padding: 10px ; + border-radius: 5px ; + background: hsl(160,30%,80%) ; + color: rgba(0,0,0,.4); + cursor: pointer ; + text-decoration: none ; +} +.category:hover { + color: rgba(0,0,0,.8); + box-shadow: 0 0 10px 10px #FFF ; + padding: 12px ; + margin: 3px ; +} + +.category.selected { + background: hsl(190,50%,40%); + color: white ; + margin-right: 40px ; +} + +.watch { + float: right ; + text-align: right ; + font-size: 16px ; + color: #888 ; + margin: 0 0 8px 20px ; + display: none ; +} +.watch .text { + margin-bottom: 5px ; + max-width: 300px ; +} +.watch .button { + display: inline-block ; + background: hsl(190,40%,50%); + color: white ; + padding: 5px 10px ; + border-radius: 5px ; + cursor: pointer ; +} +.watch .button.unwatch { + background: hsl(20,50%,50%); +} +.watch .button i { + margin-right: 5px ; +} + +.description { + max-width: 1200px ; + margin-left: auto ; + margin-right: auto ; + padding: 40px ; + font-size: 20px ; + clear: both ; +} + +.description h1 { + margin: 0 0 10px 0; + font-size: 32px ; + font-family: Ubuntu,sans-serif; +} +.description p { + margin: 0 ; +} +.filtering { + max-width: 1200px ; + margin-left: auto ; + margin-right: auto ; + padding: 0 40px ; + font-size: 16px ; +} +#searchbar { + float: right ; + position: relative ; +} +#searchbar input { + box-sizing: border-box; + margin-bottom: 20px ; + font-size: 20px ; + padding: 5px 32px 5px 10px; + border-radius: 5px ; + border: solid 1px rgba(0,0,0,.2); + background: #EEE ; + color: #444 ; + width: 300px ; + transition-property: width ; + transition-duration: 250ms ; +} + +#searchbar input:focus { + outline: none ; + box-shadow: 0 0 0px 3px hsla(190,50%,30%,.3) ; +} +#searchbar i { + position: absolute ; + top: 5px ; + right: 5px ; + color: rgba(0,0,0,.3); + font-size: 24px ; + cursor: pointer ; +} + +#sorting { + display: inline-block ; + background: #789 ; + color: white ; + padding: 7px 12px ; + border-radius: 3px ; + cursor: pointer ; + margin-bottom: 20px ; +} +#sorting i { + margin-right: 10px ; +} + +.posts { + max-width: 1200px ; + margin-left: auto ; + margin-right: auto ; + padding: 0 40px 100px 40px ; +} + +.post { + border-bottom: solid 1px #EEE ; + position: relative ; + padding-top: 10px ; +} + +.author-icon { + background: hsl(30,60%,60%) ; + width: 64px; + height: 64px ; + border-radius: 32px ; + position: absolute ; + top: 10px ; + left: 0 ; +} +.author-icon span { + position: absolute ; + color: white ; + font-size: 30px; + text-align: center ; + top: 12px ; + left: 0 ; + right: 0 ; +} +.post-top { + position: relative ; + padding-left: 80px ; + padding-right: 10px ; +} +.post-top .title { + font-size: 24px ; + font-family: Ubuntu ; + text-decoration: none; + color: #222; +} +.post-author { + float: right ; + margin-left: 40px ; + font-size: 18px ; +} +.post-category { + display: inline-block ; + padding: 5px 10px ; + font-size: 14px ; + border-radius: 5px ; + background: hsl(160,50%,40%); + color: white ; + margin-top: 10px ; + margin-right: 20px ; + text-decoration: none ; +} +.post-progress { + width: 200px ; + height: 20px ; + margin: 10px 20px 10px 0 ; + background: linear-gradient(90deg, hsl(190,80%,40%) 0%, hsl(190,80%,40%) 50%,hsl(190,10%,80%) 50%); + border-radius: 20px ; + display:inline-block ; + color: white ; + font-family: Ubuntu ; + font-size: 14px ; + padding-top: 2px ; + text-align: center; +} +.post-status { + margin: 10px 20px 10px 0 ; + padding: 4px 10px ; + background: hsl(190,50%,40%) ; + border-radius: 20px ; + display:inline-block ; + color: white ; + font-family: Ubuntu ; + font-size: 14px ; + text-align: center; +} +.post-status i { + margin-right: 5px ; +} +.post-info { + padding-left: 80px ; + margin-bottom: 10px ; +} +.post-info .post-author { + font-size: 20px ; +} +.post-stats { + float: right ; + font-family: Ubuntu ; +} +.post-replies,.post-views,.post-activity,.post-likes { + display: inline-block ; + margin-left: 10px ; + margin-top: 10px ; + padding: 5px 10px ; + border-radius: 5px ; + font-size: 14px ; + background: #AAA ; + color: white ; +} +.post-likes { + background: hsl(190,40%,50%); +} +.post-likes.self { + background: hsl(20,50%,50%); +} + +.post-stats div i { + margin-right: 5px ; +} + +.post-users { + display: inline-block ; + margin-top: 10px ; +} + +.post-users .user { + display: inline-block ; + background: hsl(0,50%,40%); + color: white ; + margin-right: 5px ; + padding: 8px 8px ; + border-radius: 50px ; + vertical-align: middle ; + font-size: 13px ; + width: 16px ; + height: 16px ; + text-align: center ; +} + +.post-users img.user { + display: inline-block ; + width: 32px ; + height: 32px ; + border-radius: 16px ; + padding: 0 ; +} + +.create-post-button-container { + position: fixed ; + bottom: 0 ; + left: 0 ; + right: 0 ; +} + +.create-post-button-wrapper { + position: relative ; + max-width: 1200px ; + margin-left: auto ; + margin-right: auto ; + padding: 40px ; +} + +.create-post { + position: absolute ; + bottom: 20px ; + right: 40px ; + border-radius: 10px ; + font-size: 24px ; + padding: 10px 15px ; + color: white ; + background: hsl(190,50%,50%); + box-shadow: 0 0 10px 10px #FFF ; + cursor: pointer ; +} + +.create-post span { + margin-left: 10px ; +} + +.post-container { + max-width: 1200px ; + margin-left: auto ; + margin-right: auto ; + padding: 40px ; +} + +.post-content { + position: relative ; + font-family: Source Sans Pro ; + border-bottom: solid 1px #DDD ; + padding: 10px 0 40px 0 ; +} + +.post-content .link { + position: absolute ; + left: -35px ; + top: 20px ; + color: #AAA ; + padding: 5px ; + border-radius: 3px ; +} + +.post-content .link:hover { + background: #AAA ; + color: white ; +} + + +.post-content h1 { + margin: 0 ; +} + +.post-content-info { + margin-bottom: 20px ; + vertical-align: middle ; +} + +.post-content-info .post-content-author { + margin-left: 5px ; + margin-right: 20px ; +} + +.post-text { + font-size: 18px ; +} + +.edit-post { + display: none ; + position: fixed ; + z-index: 2 ; + top: 0 ; + bottom: 0 ; + left: 0 ; + right: 0 ; + background: hsla(210,20%,20%,.9); + text-align: left ; + padding: 70px 40px 40px 40px ; + overflow: auto ; +} + +.edit-post-content { + box-sizing: border-box; + max-width: 1200px ; + margin-left: auto ; + margin-right: auto ; + padding: 40px ; + border-radius: 20px ; + background: white ; + border: solid 1px rgba(0,0,0,.2); +} + +.edit-post-content h1 { + margin-top: 0 ; +} + +.edit-post-content input, .edit-post-content select,.edit-post-content textarea { + box-sizing: border-box; + margin-bottom: 20px ; + font-size: 20px ; + padding: 10px ; + border-radius: 5px ; + border: solid 1px rgba(0,0,0,.2); + background: #EEE ; + color: #444 ; +} + +.edit-post-content input:focus, .edit-post-content select:focus, .edit-post-content textarea:focus { + outline: none ; + box-shadow: 0 0 0px 3px hsla(190,50%,30%,.3) ; +} + +.edit-post-content select { + border-radius: 5px ; +} + +.edit-post-content input { + width: 100% ; + outline: none ; +} + + +.edit-post-content textarea { + display: block ; + font-size: 14px ; + height: 400px ; + width: 100% ; +} + +.edit-post-buttons { + text-align: right ; +} + +.edit-post-content .button { + display: inline-block ; + padding: 10px 20px ; + border-radius: 5px ; + background: hsl(190,40%,50%); + color: white ; + font-size: 20px ; + margin-left: 20px ; + cursor: pointer ; +} + +.edit-post-content .button i { + margin-right: 10px ; +} + +.edit-post-content .button.preview { + float: left ; + margin-left: 0 ; +} + +.edit-post-content .button.cancel { + background: hsl(0,40%,50%); +} + +.edit-post-content .button.post { + background: hsl(160,40%,50%); +} + +.edit-reply-content { + border: none ; + padding: 0 ; +} + +#edit-post-preview-area { + box-sizing: border-box ; + display: none ; + height: 400px ; + padding: 0 20px 0 0 ; + margin-bottom: 20px ; + overflow: auto ; +} + +.edit-bar { + margin: 20px 0 ; + text-align: left ; +} + +.edit-bar div { + display: inline-block ; + margin-right: 10px ; + border-radius: 5px ; + padding: 5px 10px ; + font-size: 14px ; + color: white ; + cursor: pointer ; +} + +.edit-bar div.delete { + background: hsl(0,40%,50%) ; +} + +.edit-bar div.edit { + background: hsl(190,40%,50%) ; +} + +.edit-bar i { + margin-right: 5px ; +} + +#edit-post-reserved { + display: none ; +} + +#edit-post-reserved input { + width: 200px ; +} +#edit-post-reserved input[type=range] { + padding: 0 ; +} + +#validate-your-email { + position: fixed ; + display: none ; + bottom: 10px ; + left: 0 ; + right: 0 ; + text-align: center ; +} + +#validate-your-email div { + display: inline-block ; + padding: 5px 10px ; + background: hsl(190,40%,50%) ; + border-radius: 5px ; + color: white ; + box-shadow: 0 0 20px 4px #FFF ; +} +#validate-your-email i { + margin-right: 5px ; +} + +/* DARK MODE */ + +body.dark { + background: hsl(190,10%,20%) ; + color: #DDD ; +} + +body.dark .theme { + background: #CCC ; + color: #333 ; +} + +body.dark .post-top .title { + color: #DDD ; +} + +body.dark .post { + border-bottom: solid 1px rgba(255,255,255,.2) ; +} + +body.dark .post-replies, body.dark .post-views, body.dark .post-activity { + background: #888 ; +} + +body.dark .categories { + background: rgba(255,255,255,.1) ; +} +body.dark .category { + color: #111 ; +} +body.dark .category.selected { + color: white ; +} + +body.dark .languages { + color: #DDD ; + background: rgba(255,255,255,.2) ; +} + +body.dark .create-post { + box-shadow: 0 0 10px 0px #000 ; +} + +body.dark .edit-post-content { + background: hsl(190,10%,20%) ; +} + +body.dark .edit-post-content input, body.dark .edit-post-content select, body.dark .edit-post-content textarea { + background: #333 ; + color: #DDD ; + border: solid 2px rgba(255,255,255,.2); +} + +body.dark .md { + color: rgba(255,255,255,.8) ; +} + +body.dark .md h1,body.dark .md h2,body.dark .md h3,body.dark .md h4,body.dark .md h5,body.dark .md h6 { + color: rgba(255,255,255,.8); + font-family: Helvetica,sans-serif; +} + +body.dark .md pre code, body.dark .md code { + background: hsl(200,20%,15%); +} + +body.dark .md blockquote { + border-left: solid 4px rgba(255,255,255,.2); + background: rgba(255,255,255,.1); +} + +body.dark .md th { + background: rgba(255,255,255,.1); +} + +body.dark .md tr,body.dark .md td,body.dark .md th { + border: solid 1px rgba(255,255,255,.1); +} + +body.dark .md a { + color: hsla(200,100%,75%,.8); +} + +body.dark .md hr { + margin: 40px 0; + border: solid 2px rgba(255,255,255,.2); +} + +body.dark .category:hover { + color: rgba(255,255,255,.8); + box-shadow: 0 0 10px 0px #000 ; +} + +body.dark .edit-post { + background: rgba(255,255,255,.2); +} + +body.dark .post-content { + border-bottom: solid 1px rgba(255,255,255,.2) ; +} + +iframe { + max-width: 100% ; + width: 100vmin ; + height: 56.25vmin ; + border: none ; + border-radius: 5px ; + margin: 10px 0 ; +} diff --git a/static/css/forum/media.css b/static/css/forum/media.css new file mode 100644 index 00000000..734d7239 --- /dev/null +++ b/static/css/forum/media.css @@ -0,0 +1,60 @@ +@media screen and (max-width: 800px) { + /*.post-activity { + display: none ; + }*/ + .post-container, .posts, .description, .create-post-button-wrapper { + padding: 20px ; + } + .create-post { + right: 20px ; + } + .post-author { + display: none ; + } + .post-users { + display: none ; + } + .title-zone { + padding-right: 10px ; + } + + .description { + /* display: none ;*/ + padding-bottom: 0 ; + } + + .description h1 { + display: none ; + } + .filtering { + padding: 0 20px ; + margin-top: 20px ; + } + + .create-post { + border-radius: 60px ; + font-size: 30px ; + padding: 10px 15px ; + } + .create-post span { + display: none ; + } + .post-content-info span { + margin-left: 0 ; + } + + .post-content .link { + display: none ; + } + + .edit-post { + padding: 70px 10px 10px 10px ; + } + + .edit-post-content { + padding: 20px ; + } + .edit-reply-content { + padding: 0px ; + } +} diff --git a/static/css/home.css b/static/css/home.css new file mode 100644 index 00000000..66d9625c --- /dev/null +++ b/static/css/home.css @@ -0,0 +1,199 @@ +.home-section { + display: none ; + position: fixed ; + top: 0px ; + bottom: 0 ; + left: 0 ; + right: 0 ; + overflow: auto ; + text-align: center ; + font-size: 22px ; + color: rgba(255,255,255,.7); +} + +#home-header-background { + position: fixed ; + height: 0px ; + top: 0 ; + left: 0 ; + right: 0 ; + background: linear-gradient(135deg,hsl(200,50%,20%),hsl(200,20%,10%)) ; + transition-property: height ; + transition-duration: 1s ; +} +.home-section::-webkit-scrollbar { + width: 10px; + background: #AAA; +} + +.home-section::-webkit-scrollbar-track { + background: #EEE; + border-radius: 5px; + margin: 4px 1px; +} + +.home-section::-webkit-scrollbar-thumb { + background-color: #888; + border-radius: 5px; + margin: 4px 1px; +} + +.home-section i { + font-size: 150px ; + color: hsl(200,30%,70%); +} +.home-section h2 { + font-size: 32px ; + color: #444 ; + margin-top: 0 ; +} +.home-section img.screen { + box-shadow: 0 0 20px #000 ; + width: 100% ; +} + +.home-section div.part { + padding: 60px 0 ; + background: hsl(200,10%,90%); + color: #222 ; + overflow: hidden ; +} + +@keyframes screenshot-animation { + from { + transform: perspective(1000px) rotateY(-25deg) scale(0.9,0.9) translateX(-50px); + } + + to { + transform: perspective(1000px) rotateY(0deg) scale(1,1) translateX(0); + } +} + +@keyframes text-animation { + from { + transform: translateX(-20000px) ; + } + to { + transform: translateX(0px) ; + } +} + +@keyframes logo-animation { + from { + transform: perspective(100px) rotateX(60deg) scale(0.7,0.7); + } + + to { + transform: perspective(100px) rotateX(0deg) scale(1,1) ; + } +} + +.home-section .mainlogo { + animation: 2s ease-out logo-animation ; +} + +.home-section div.part.part1 { + position: relative ; + padding: 160px 0 100px 0 ; + background: linear-gradient(135deg, hsl(200,50%,30%), hsl(10,50%,30%) 120%); + color: #FFF ; + padding-top: 160px ; +} +.home-section div.part.part1 .right { + width: auto ; +} +.home-section div.part.part1 .right img { + max-width: 800px ; + margin-bottom: -20%; + animation: 3s ease-in-out screenshot-animation ; +} +.home-section div.part.part1 .p1 { + animation: 1s ease-out text-animation ; +} +.home-section div.part.part1 .p2 { + animation: 2s ease-out text-animation ; +} + +.home-section div.part.last { + background: linear-gradient(135deg, hsl(200,50%,30%), hsl(10,50%,30%) 120%); + border-top: solid 5px hsl(200,50%,15%); + border-bottom: solid 5px hsl(200,50%,15%); + padding: 30px 0; +} + +.home-section div.part:nth-child(even) { + background: #FFF; +} + +.home-section div.right { + display: inline-block ; + text-align: right ; + width: 400px ; + padding: 20px 40px ; + vertical-align: top; +} +.home-section div.left { + display: inline-block ; + text-align: left ; + width: 400px ; + padding: 20px 40px ; + vertical-align: top; +} +.home-section div.center { + display: inline-block ; + text-align: center ; + width: 400px ; + padding: 20px 40px ; + vertical-align: middle; +} + +.container h2 { + margin: 100px 50px 50px 50px ; + font-weight: normal ; + text-align: center ; +} + +.button-container { + margin: 20px 0 ; + text-align: center ; +} +.home-section .button { + margin: 20px ; + display: inline-block; + text-align: center ; + padding: 20px 30px ; + border-radius: 10px ; + background: hsl(160,50%,40%); + box-shadow: 0 0 5px rgba(255,255,255,.5) inset, 0 0 5px rgba(0,0,0,.5); + color: rgba(255,255,255,.9); + font-size: 24px ; + cursor: pointer ; + width: 300px ; +} +.home-section .button i { + font-size: 40px ; + margin-bottom: 10px ; + color: rgba(255,255,255,.5); +} + +.home-section .footer { + background: linear-gradient(175deg, hsl(200,50%,20%), transparent); + padding: 40px 20% ; + text-align: center ; + color: hsl(200,20%,80%); + font-size: 14px ; +} +.home-section .footer i { + font-size: 14px ; +} +.home-section .footer p { + margin: 40px 0 ; +} +.home-section .footer p a { + color: hsl(200,100%,80%); + text-decoration: none ; +} +.home-section .footer p i { + margin: 0 20px ; + font-size: 10px ; +} diff --git a/static/css/jquery.terminal.css b/static/css/jquery.terminal.css new file mode 100644 index 00000000..8cc2caf3 --- /dev/null +++ b/static/css/jquery.terminal.css @@ -0,0 +1,698 @@ +/*! + * __ _____ ________ __ + * / // _ /__ __ _____ ___ __ _/__ ___/__ ___ ______ __ __ __ ___ / / + * __ / // // // // // _ // _// // / / // _ // _// // // \/ // _ \/ / + * / / // // // // // ___// / / // / / // ___// / / / / // // /\ // // / /__ + * \___//____ \\___//____//_/ _\_ / /_//____//_/ /_/ /_//_//_/ /_/ \__\_\___/ + * \/ /____/ version 1.15.0 + * http://terminal.jcubic.pl + * + * This file is part of jQuery Terminal. + * + * Copyright (c) 2011-2018 Jakub Jankiewicz + * Released under the MIT license + * + * Date: Sun, 13 May 2018 09:34:11 +0000 + */ +.terminal .terminal-output .format, .cmd .format, +.cmd .prompt, .cmd .prompt div, .terminal .terminal-output div div{ + display: inline-block; +} +.terminal h1, .terminal h2, .terminal h3, .terminal h4, .terminal h5, .terminal h6, .terminal pre, .cmd { + margin: 0; +} +.terminal h1, .terminal h2, .terminal h3, .terminal h4, .terminal h5, .terminal h6 { + line-height: 1.2em; +} +/* +.cmd .mask { + width: 10px; + height: 11px; + background: black; + z-index: 100; +} +*/ +.cmd .clipboard { + position: absolute; + left: -16px; + top: 0; + width: 20px; + height: 16px; + /* this seems to work after all on Android */ + /*left: -99999px; + clip: rect(1px,1px,1px,1px); + /* on desktop textarea appear when paste */ + /* opacity is needed for Edge and IE + opacity: 0.01; + filter: alpha(opacity = 0.01); + filter: progid:DXImageTransform.Microsoft.Alpha(opacity=0.01);*/ + background: transparent; + border: none; + color: transparent; + outline: none; + padding: 0; + resize: none; + z-index: 1000; + overflow: hidden; + white-space: pre; + text-indent: -9999em; /* better cursor hiding for Safari and IE */ +} +.terminal img, .terminal value, .terminal audio, .terminal object, .terminal canvas { + cursor: default; +} +.terminal .error { + color: #f00; +} +.terminal { + position: relative; + /*overflow: hidden;*/ + overflow-y: auto; + /* overflow-x: hidden; */ +} +.terminal, .cmd { + contain: content; +} +body.terminal { + height: 100%; + min-height: 100vh; + margin: 0; +} +.terminal > div { + overflow: hidden; +} +.terminal > .resizer, .terminal > .font .resizer{ + position: absolute; + left: 0; + top: 0; + right: 0; + bottom: 0; + overflow: hidden; + z-index: -1; + visibility: hidden; + height: 100%; + border: none; + padding: 0; + width: 100% +} +.cmd { + padding: 0; + position: relative; + /*margin-top: 3px; */ + float: left; + padding-bottom: 3px; +} +.terminal a[tabindex="1000"], +.terminal a[tabindex="1000"]:active, +.terminal a[tabindex="1000"]:focus { + outline: none; +} +.terminal .inverted, .cmd .inverted { + background-color: #aaa; + color: #000; +} +.cmd .cursor { + border-bottom: 3px solid transparent; + margin-bottom: -3px; + background-clip: content-box; +} +.cmd .cursor.blink { + -webkit-animation: terminal-blink 1s infinite steps(1, start); + -moz-animation: terminal-blink 1s infinite steps(1, start); + -ms-animation: terminal-blink 1s infinite steps(1, start); + animation: terminal-blink 1s infinite steps(1, start); + border-left: 1px solid transparent; + margin-left: -1px; +} +.bar.terminal .inverted, .bar.cmd .inverted { + border-left-color: #aaa; +} +.terminal .terminal-output div div, .cmd .prompt { + display: block; + line-height: 14px; + height: auto; +} +.terminal .terminal-output > div:not(.raw) div { + white-space: nowrap; +} +.cmd .prompt > span { + float: left; +} +.terminal, .cmd { + font-family: monospace; + /*font-family: FreeMono, monospace; this don't work on Android */ + color: #aaa; + background-color: #000; + font-size: 12px; + line-height: 14px; + box-sizing: border-box; + cursor: text; +} +.cmd div { + clear: both; +} +.cmd .prompt + div { + clear: right; +} +.terminal-output > div > div { + min-height: 14px; +} +terminal .terminal-output > div { + margin-top: -1px; +} +.terminal-output > div.raw > div * { + overflow-wrap: break-word; + word-wrap: break-word; +} +.terminal .font { + position: absolute; + font-size: inherit; + width: 1em; + height: 1em; + top: -100%; + left: 0; + margin-bottom: 1px; +} +.terminal .terminal-output div span { + display: inline-block; +} +.cmd > span:not(.prompt) { + float: left; +} +.cmd .prompt span.line { + display: block; + float: none; +} +.terminal table { + border-collapse: collapse; +} +.terminal td { + border: 1px solid #aaa; +} +.terminal h1::-moz-selection, +.terminal h2::-moz-selection, +.terminal h3::-moz-selection, +.terminal h4::-moz-selection, +.terminal h5::-moz-selection, +.terminal h6::-moz-selection, +.terminal pre::-moz-selection, +.terminal td::-moz-selection, +.terminal .terminal-output div div::-moz-selection, +.terminal .terminal-output div span::-moz-selection, +.terminal .terminal-output div div a::-moz-selection, +.terminal .terminal-output .raw div::-moz-selection, +.cmd div::-moz-selection, +.cmd > span::-moz-selection, +.cmd > span span::-moz-selection, +.cmd > div::-moz-selection, +.cmd > div span::-moz-selection, +.cmd .prompt span::-moz-selection { + background-color: #aaa; + color: #000; +} +/* this don't work in Chrome +.terminal tr td::-moz-selection { + border-color: #000; +} +.terminal tr td::selection { + border-color: #000; +} +*/ +.terminal h1::selection, +.terminal h2::selection, +.terminal h3::selection, +.terminal h4::selection, +.terminal h5::selection, +.terminal h6::selection, +.terminal pre::selection, +.terminal td::selection, +.terminal .terminal-output div div::selection, +.terminal .terminal-output div div a::selection, +.terminal .terminal-output div span::selection, +.terminal .terminal-output .raw div::selection, +.cmd div::selection, +.cmd > span::selection, +.cmd > span span::selection, +.cmd > div::selection, +.cmd > div span::selection, +.cmd .prompt span::selection { + /* + * use rgba to fix transparent selection in chrome + * http://stackoverflow.com/questions/7224445/css3-selection-behaves-differently-in-ff-chrome + */ + background-color: rgba(170, 170, 170, 0.99); + color: #000; +} +.terminal .terminal-output div.error, .terminal .terminal-output div.error div { + color: red; + color: var(--error-color, red); +} +.tilda { + position: fixed; + top: 0; + left: 0; + width: 100%; + z-index: 1100; +} +.ui-dialog-content .terminal { + width: 100%; + height: 100%; + box-sizing: border-box; +} +.ui-dialog .ui-dialog-content.dterm { + padding: 0; +} +.clear { + clear: both; +} +.terminal a { + color: #0F60FF; + color: var(--link-color, #0F60FF); +} +.terminal a:hover { + background: #0F60FF; + background: var(--link-color, #0F60FF); + color: var(--background, #000); + text-decoration: none; +} +.terminal .terminal-fill { + position: absolute; + left: 0; + top: -100%; + width: 100%; + height: 100%; + margin: 1px 0 0; + border: none; + opacity: 0.01; + pointer-events: none; + box-sizing: border-box; +} +.terminal, .terminal .terminal-fill { + padding: 10px; +} +@-webkit-keyframes terminal-blink { + 0%, 100% { + background-color: #000; + color: #aaa; + } + 50% { + background-color: #bbb; + color: #000; + } +} + +@-ms-keyframes terminal-blink { + 0%, 100% { + background-color: #000; + color: #aaa; + } + 50% { + background-color: #bbb; + color: #000; + } +} + +@-moz-keyframes terminal-blink { + 0%, 100% { + background-color: #000; + color: #aaa; + } + 50% { + background-color: #bbb; + color: #000; + } +} +@keyframes terminal-blink { + 0%, 100% { + background-color: #000; + color: #aaa; + } + 50% { + background-color: #bbb; /* not #aaa because it's seems there is Google Chrome bug */ + color: #000; + } +} +@-webkit-keyframes terminal-bar { + 0%, 100% { + border-left-color: #aaa; + } + 50% { + border-left-color: #000; + } +} +@-ms-keyframes terminal-bar { + 0%, 100% { + border-left-color: #aaa; + } + 50% { + border-left-color: #000; + } +} +@-moz-keyframes terminal-bar { + 0%, 100% { + border-left-color: #aaa; + } + 50% { + border-left-color: #000; + } +} +@keyframes terminal-bar { + 0%, 100% { + border-left-color: #aaa; + } + 50% { + border-left-color: #000; + } +} +@-webkit-keyframes terminal-underline { + 0%, 100% { + border-bottom-color: #aaa; + position: relative; + line-height: 12px; + margin-top: 1px; + border-left: none; + margin-left: 0; + } + 50% { + border-bottom-color: #000; + position: relative; + line-height: 12px; + margin-top: 1px; + border-left: none; + margin-left: 0; + } +} +@-ms-keyframes terminal-underline { + 0%, 100% { + border-bottom-color: #aaa; + position: relative; + line-height: 12px; + margin-top: 1px; + border-left: none; + margin-left: 0; + } + 50% { + border-bottom-color: #000; + position: relative; + line-height: 12px; + margin-top: 1px; + border-left: none; + margin-left: 0; + } +} +@-moz-keyframes terminal-underline { + 0%, 100% { + border-bottom-color: #aaa; + position: relative; + line-height: 12px; + margin-top: 1px; + border-left: none; + margin-left: 0; + } + 50% { + border-bottom-color: #000; + position: relative; + line-height: 12px; + margin-top: 1px; + border-left: none; + margin-left: 0; + } +} +@keyframes terminal-underline { + 0%, 100% { + border-bottom-color: #aaa; + position: relative; + line-height: 12px; + margin-top: 1px; + border-left: none; + margin-left: 0; + } + 50% { + border-bottom-color: #000; + position: relative; + line-height: 12px; + margin-top: 1px; + border-left: none; + margin-left: 0; + } +} +/* shorthand classes for IE */ +.underline-animation .cursor.blink { + border-left: none; + -webkit-animation-name: terminal-underline; + -moz-animation-name: terminal-underline; + -ms-animation-name: terminal-underline; + animation-name: terminal-underline; +} +.bar-animation .cursor.blink { + -webkit-animation-name: terminal-bar; + -moz-animation-name: terminal-bar; + -ms-animation-name: terminal-bar; + animation-name: terminal-bar; +} +@supports (--css: variables) { + .terminal, .cmd { + color: var(--color, #aaa); + background-color: var(--background, #000); + } + .terminal .font { + width: calc(var(--size, 1) * 1em); + height: calc(var(--size, 1) * 1em); + } + .terminal span[style*="--length"] { + /* + * default value for char-width taken from Google Chrome for default font + * to silence warning in webpack #371 + */ + width: calc(var(--length, 1) * var(--char-width, 7.23438) * 1px); + display: inline-block; + } + .terminal, .cmd, .terminal .terminal-output > div > div, .cmd .prompt { + font-size: calc(var(--size, 1) * 12px); + line-height: calc(var(--size, 1) * 14px); + } + .terminal .terminal-output > div > div { + min-height: calc(var(--size, 1) * 14px); + } + .terminal .inverted, .cmd .inverted { + background-color: var(--color, #aaa); + color: var(--background, #000); + } + .cmd .cursor.blink { + -webkit-animation: var(--animation, terminal-blink) 1s infinite steps(1, start); + -moz-animation: var(--animation, terminal-blink) 1s infinite steps(1, start); + -ms-animation: var(--animation, terminal-blink) 1s infinite steps(1, start); + animation: var(--animation, terminal-blink) 1s infinite steps(1, start); + color: var(--color, rgba(255,255,255,.2)); + background-color: var(--background, rgba(255,255,255,0)); + } + .terminal h1::-moz-selection, + .terminal h2::-moz-selection, + .terminal h3::-moz-selection, + .terminal h4::-moz-selection, + .terminal h5::-moz-selection, + .terminal h6::-moz-selection, + .terminal pre::-moz-selection, + .terminal td::-moz-selection, + .terminal .terminal-output div div::-moz-selection, + .terminal .terminal-output div span::-moz-selection, + .terminal .terminal-output div div a::-moz-selection, + .cmd div::-moz-selection, + .cmd > span::-moz-selection, + .cmd > span span::-moz-selection, + .cmd > div::-moz-selection, + .cmd > div span::-moz-selection, + .cmd .prompt span::-moz-selection { + background-color: var(--color, #aaa); + color: var(--background, #000); + } + .terminal h1::selection, + .terminal h2::selection, + .terminal h3::selection, + .terminal h4::selection, + .terminal h5::selection, + .terminal h6::selection, + .terminal pre::selection, + .terminal td::selection, + .terminal .terminal-output div div::selection, + .terminal .terminal-output div div a::selection, + .terminal .terminal-output div span::selection, + .cmd div::selection, + .cmd > span::selection, + .cmd > span span::selection, + .cmd > div::selection, + .cmd > div span::selection, + .cmd .prompt span::selection { + background-color: var(--color, rgba(170, 170, 170, 0.99)); + color: var(--background, #000); + } + @-webkit-keyframes terminal-blink { + 0%, 100% { + background-color: var(--background, rgba(255,255,255,0)); + color: var(--color, rgba(255,255,255,.2)); + } + 50% { + background-color: var(--color, #aaa); + color: var(--background, rgba(255,255,255,0)); + } + } + + @-ms-keyframes terminal-blink { + 0%, 100% { + background-color: var(--background, rgba(255,255,255,0)); + color: var(--color, rgba(255,255,255,.2)); + } + 50% { + background-color: var(--color, rgba(255,255,255,.2)); + color: var(--background, rgba(255,255,255,0)); + } + } + @-moz-keyframes terminal-blink { + 0%, 100% { + background-color: var(--background, rgba(255,255,255,0)); + color: var(--color, rgba(255,255,255,.2)); + } + 50% { + background-color: var(--color, rgba(255,255,255,.2)); + color: var(--background, rgba(255,255,255,0)); + } + } + @keyframes terminal-blink { + 0%, 100% { + background-color: var(--background, rgba(255,255,255,0)); + color: var(--color, rgba(255,255,255,.2)); + } + 50% { + background-color: var(--color, rgba(255,255,255,.2)); + color: var(--background, rgba(255,255,255,0)); + } + } + @-webkit-keyframes terminal-bar { + 0%, 100% { + border-left-color: var(--background, rgba(255,255,255,0)); + } + 50% { + border-left-color: var(--color, rgba(255,255,255,.2)); + } + } + @-ms-keyframes terminal-bar { + 0%, 100% { + border-left-color: var(--background, rgba(255,255,255,0)); + } + 50% { + border-left-color: var(--color, rgba(255,255,255,.2)); + } + } + @-moz-keyframes terminal-bar { + 0%, 100% { + border-left-color: var(--background, rgba(255,255,255,0)); + } + 50% { + border-left-color: var(--color, rgba(255,255,255,.2)); + } + } + @keyframes terminal-bar { + 0%, 100% { + border-left-color: var(--background, rgba(255,255,255,0)); + } + 50% { + border-left-color: var(--color, rgba(255,255,255,.2)); + } + } + @-webkit-keyframes terminal-underline { + 0%, 100% { + border-bottom-color: var(--color, rgba(255,255,255,.2)); + position: relative; + line-height: calc(var(--size, 1) * 12px); + margin-top: calc(var(--size, 1) * 1px); + border-left: none; + margin-left: 0; + } + 50% { + border-bottom-color: var(--background, rgba(255,255,255,0)); + position: relative; + line-height: calc(var(--size, 1) * 12px); + margin-top: calc(var(--size, 1) * 1px); + border-left: none; + margin-left: 0; + } + } + @-ms-keyframes terminal-underline { + 0%, 100% { + border-bottom-color: var(--background, rgba(255,255,255,0)); + position: relative; + line-height: calc(var(--size, 1) * 12px); + margin-top: calc(var(--size, 1) * 1px); + border-left: none; + margin-left: 0; + } + 50% { + border-bottom-color: var(--color, rgba(255,255,255,.2)); + position: relative; + line-height: calc(var(--size, 1) * 12px); + margin-top: calc(var(--size, 1) * 1px); + border-left: none; + margin-left: 0; + } + } + @-moz-keyframes terminal-underline { + 0%, 100% { + border-bottom-color: var(--background, rgba(255,255,255,0)); + position: relative; + line-height: calc(var(--size, 1) * 12px); + margin-top: calc(var(--size, 1) * 1px); + border-left: none; + margin-left: 0; + } + 50% { + border-bottom-color: var(--color, #aaa); + position: relative; + line-height: calc(var(--size, 1) * 12px); + margin-top: calc(var(--size, 1) * 1px); + border-left: none; + margin-left: 0; + } + } + @keyframes terminal-underline { + 0%, 100% { + border-bottom-color: var(--background, rgba(255,255,255,0)); + position: relative; + line-height: calc(var(--size, 1) * 12px); + margin-top: calc(var(--size, 1) * 1px); + border-left: none; + margin-left: 0; + } + 50% { + border-bottom-color: var(--color, rgba(255,255,255,.2)); + position: relative; + line-height: calc(var(--size, 1) * 12px); + margin-top: calc(var(--size, 1) * 1px); + border-left: none; + margin-left: 0; + } + } +} +/* + * overwrite css variables that don't work with selection in Edge + */ +@supports (-ms-ime-align:auto) { + .terminal h1::selection, + .terminal h2::selection, + .terminal h3::selection, + .terminal h4::selection, + .terminal h5::selection, + .terminal h6::selection, + .terminal pre::selection, + .terminal td::selection, + .terminal .terminal-output div div::selection, + .terminal .terminal-output div div a::selection, + .terminal .terminal-output div span::selection, + .cmd div::selection, + .cmd > span::selection, + .cmd > span span::selection, + .cmd > div::selection, + .cmd > div span::selection, + .cmd .prompt span::selection { + background-color: rgba(170, 170, 170, 0.99); + color: #000; + } +} diff --git a/static/css/maps.css b/static/css/maps.css new file mode 100644 index 00000000..0edfb9fe --- /dev/null +++ b/static/css/maps.css @@ -0,0 +1,326 @@ +.maps-left { + position: absolute ; + left: 0 ; + width: 400px ; + top: 0 ; + bottom: 0 ; + border-top: solid 40px hsl(200,30%,30%); + overflow: hidden ; +} + +.maps-right { + position: absolute ; + right: 0px ; + width: 400px ; + top: 0 ; + bottom: 0 ; + overflow: hidden ; +} + +.maps-splitbar { + background: hsl(200,30%,30%); + position: absolute ; + left: 400px ; + top: 0 ; + bottom: 0 ; + width: 10px ; + cursor: ew-resize; + z-index: 2 ; +} + +.maplist { + position: absolute; + background: rgba(0,0,0,.25) ; + left: 0 ; + top: 40px ; + bottom: 40px ; + right: 0 ; + text-align: center ; + line-height: 0 ; + padding-top: 10px ; + padding-bottom: 10px ; + overflow-x: hidden ; + overflow-y: auto ; +} +#create-map-button { + position: absolute ; + bottom: 0px ; + height: 40px; + left: 0 ; + right: 0 ; + padding: 0px 30px ; + background: hsl(160,50%,40%); + font-size: 18px ; + cursor: pointer ; + color: #FFF ; + z-index: 1 ; + box-shadow: 0 -10px 20px #000; + line-height: 40px ; + text-align: center ; +} +#create-map-button i { + margin-right: 0px ; +} +#create-map-button span { + white-space: nowrap ; + margin-left: 10px ; +} +.map-box { + position: relative ; + cursor: pointer ; + display: inline-block ; + margin: 1px ; + width: 96px ; + height: 116px ; + font-size: 12px ; + padding: 4px 0 ; + background: rgba(255,255,255,.1); + border-radius: 4px ; + overflow: hidden ; +} +.map-box .active-user { + display: none ; + position: absolute ; + bottom: 20px ; + left: 4px ; + font-size: 8px ; + color: #FFF ; + background: hsl(0,50%,50%); + padding: 4px ; + border-radius: 8px ; +} +.map-box.selected { + margin: 0px; + border: solid 1px rgba(255,255,255,.5) ; + background: rgba(255,255,255,.2); +} + +.map-box .icon-box { + height: 96px ; + width: 96px ; + overflow: hidden ; + margin-bottom: 10px ; +} + +.map-box canvas { + width: 100%; +} + +.mapbar { + position: absolute; + right: 0; + top: 0px; + bottom: 0; + width: 274px; + background: rgba(0,0,0,.25); + overflow: auto ; +} + +.mapbar h3 { + margin: 5px 0 0 0 ; + font-size: 14px; +} + +.mapbar input { + width: 100% ; +} +.map-sprite-list { + text-align: center ; + padding: 10px ; +} +.mapinfo { + position: absolute; + right: 0px ; + left: 2px ; + top: 0px ; + height: 40px; + padding-left: 10px ; + background: hsl(200,30%,30%); + white-space: nowrap; +} +.mapinfo * { + display: inline-block ; + font-size: 16px ; +} +.mapinfo .buttons { + display: inline-block ; + margin-left: 20px ; +} +.mapinfo .buttons div { + margin-left: 1px ; + padding: 10px 15px ; + cursor: pointer ; + background: hsl(160,50%,40%); +} +.mapinfo .buttons div:hover { + background: hsl(160,50%,60%); +} +.mapinfo .buttons div#delete-map { + background: hsl(0,50%,50%); +} +.mapinfo .buttons div#delete-map:hover { + background: hsl(0,50%,60%); +} + +.mapinfo input { + border: none ; + border-radius: 3px ; + margin:6px 20px 0px 0px ; + font-size: 14px ; + padding: 5px ; + background: rgba(255,255,255,.4); +} +.mapinfo input:focus { + outline: none ; + background: rgba(255,255,255,.6); +} +.mapinfo input#map-width,.mapinfo input#map-height,.mapinfo input#map-block-width,.mapinfo input#map-block-height { + width: 30px ; + text-align: center ; + margin: 0 3px ; +} +.mapinfo .validate-button-container { + display: inline-block ; + width: 100px ; + margin-left: 7px ; + transition-duration: .5s ; + transition-property: width ; + overflow: hidden ; + height: 30px ; + vertical-align: middle ; +} +.mapinfo .validate-button { + display: inline-block ; + padding: 5px 10px ; + font-size: 14px ; + border-radius: 3px ; + background: hsl(160,50%,40%); + color: #FFF ; + cursor: pointer ; + white-space: nowrap; +} +#mapeditor-container { + position: absolute; + right: 0px ; + left: 0px ; + top: 40px ; + bottom: 0 ; +} +#mapeditor-wrapper { + position: absolute; + right: 0px ; + left: 0px ; + top: 0px ; + bottom: 40px ; +} +#mapeditor-bottombar { + position: absolute; + right: 2px ; + left: 2px ; + bottom: 0 ; + height: 40px ; + background: hsl(200,30%,30%); + text-align: left ; + padding: 0 20px ; +} +#map-background-color { + display: inline-block ; + margin: 7px 20px ; + height: 26px ; + width: 26px ; + border-radius: 5px ; + border: solid 1px rgba(255,255,255,.5); + background: rgb(0,0,0) ; + vertical-align: middle ; + cursor: pointer ; +} +#map-underlay-select { + padding: 3px 6px ; + border-radius: 5px ; +} +#map-coordinates { + position: absolute ; + right: 0 ; + top: 0 ; + bottom: 0 ; + padding: 10px 20px ; +} +.mapeditor-splitbar { + background: hsl(200,30%,30%); + position: absolute ; + left: 400px ; + top: 0 ; + bottom: 0 ; + width: 10px ; + cursor: ew-resize; + z-index: 2 ; +} +.mapeditor { + position: absolute; + right: 100px ; + left: 0px ; + top: 0px ; + bottom: 0 ; + text-align: center ; +} +.mapeditor canvas { + box-shadow: 0 0 30px #000 ; + border: solid 1px rgba(255,255,255,.2); + border-radius: 4px ; + display: none ; + cursor: crosshair ; +} +#map-editor-locked { + position: absolute ; + right: 264px ; + left: 260px ; + top: 60px ; + padding: 20px ; + z-index: 100 ; + height: 20px ; + transition-duration: .5s ; + transition-property: left ; + border-radius: 10px ; + color: #FFF ; + font-size: 16px ; + display: none ; +} +#map-editor-locked i { + margin-right: 5px ; +} +.map-sprite-box { + cursor: pointer ; + display: inline-block ; + margin: 1px ; + width: 64px ; + height: 64px ; + padding: 4px 0 ; + overflow: hidden ; + background: rgba(255,255,255,.1); + border-radius: 4px ; + vertical-align: top ; +} +.map-sprite-box.selected { + margin: 0px; + border: solid 1px rgba(255,255,255,.5) ; + background: rgba(255,255,255,.2); +} + +.map-sprite-box .icon-box { + height: 64px ; + width: 64px ; + margin-bottom: 10px ; +} + +.map-sprite-box img { + width: 100% ; + image-rendering: pixelated; +} +.map-tilepicker { + text-align: center ; +} +.map-tilepicker canvas { + margin: 20px 10px ; + box-shadow: 0 0 3px #FFF ; + cursor: crosshair ; +} diff --git a/static/css/md.css b/static/css/md.css new file mode 100644 index 00000000..a9015cce --- /dev/null +++ b/static/css/md.css @@ -0,0 +1,149 @@ +.md { + color: rgba(0,0,0,.8) ; +} + +.md h1,.md h2,.md h3,.md h4,.md h5,.md h6 { + color: rgba(0,0,0,.8); + font-family: Helvetica,sans-serif; +} + +.md h1 { + margin: 0 ; + padding: 40px 0 10px 0 ; +} +.md h2 { + margin: 0 ; + padding: 30px 0 10px 0 ; +} +.md h3 { + margin: 0 ; + padding: 20px 0 10px 0 ; +} +.md .md h4,.md h5,.md h6 { + margin: 0 ; + padding: 10px 0 10px 0 ; +} +.md hr { + margin: 40px 0; + border: solid 2px rgba(0,0,0,.2); +} +.md p { + margin: 0 0 10px 0 ; +} +.md h5 { + font-size: 15px ; + background: rgba(0,0,0,.1); + border-radius: 5px ; + padding: 5px ; + margin-top: 20px ; + margin-bottom: 10px ; +} + +.md table { + border-radius: 3px ; + margin-bottom: 20px ; +} + +.md table,tr,td { + border-collapse: collapse; +} + +.md tr,td,th { + border: solid 1px rgba(0,0,0,.1); + padding: 10px ; +} +.md th { + font-weight: bold ; + background: rgba(0,0,0,.05); +} +.md code { + background: hsla(200,50%,20%,.7); + color: hsl(200,100%,95%); + border-radius: 3px ; + font-family: "Ubuntu Mono"; + line-height: 1.2em; + font-size: 16px ; + padding: 2px 6px ; +} +.md pre { + position: relative ; +} +.md pre code { + display: block ; + background: hsl(200,50%,20%); + color: hsl(200,100%,90%); + padding: 10px ; + border-radius: 10px ; + font-family: "Ubuntu Mono"; + line-height: 1.2em; + white-space: pre-wrap ; + user-select: text; +} +.md pre .copy-button { + display: inline-block; + background-color: hsl(0,50%,50%); + color: #FFF; + font-size: 14px; + border-radius: 5px; + padding: 4px 8px; + position: absolute; + right: 10px; + top: 10px; + cursor: pointer; + transition-property: background-color ; + transition-duration: 1s ; + font-family: Source Sans Pro; +} +.md pre .copy-button.copied { + background-color: hsl(0,0%,50%); +} +.md a { + text-decoration: none ; + color: hsla(200,100%,30%,.8); +} + +.md img { + max-width: 100% ; +} + +.md blockquote { + border-left: solid 4px rgba(0,0,0,.15) ; + padding: 10px 20px; + background: rgba(0,0,0,.05); + margin: 10px 0 ; +} + +.dark.md { + color: rgba(255,255,255,.8) ; +} + +.dark.md h1,.dark.md h2,.dark.md h3,.dark.md h4,.dark.md h5,.dark.md h6 { + color: rgba(255,255,255,.8); + font-family: Helvetica,sans-serif; +} + +.dark.md pre code, .dark.md code { + background: rgba(255,255,255,.1); +} + +.dark.md blockquote { + border-left: solid 4px rgba(255,255,255,.2); + background: rgba(255,255,255,.1); +} + +.dark.md th { + background: rgba(255,255,255,.1); +} + +.dark.md tr,.dark.md td,.dark.md th { + border: solid 1px rgba(255,255,255,.1); +} + +.dark.md a { + color: hsla(200,100%,75%,.8); +} + +.dark.md hr { + margin: 40px 0; + border: solid 2px rgba(255,255,255,.2); +} diff --git a/static/css/media.css b/static/css/media.css new file mode 100644 index 00000000..4c15659c --- /dev/null +++ b/static/css/media.css @@ -0,0 +1,62 @@ +@media screen and (max-device-width: 800px) { + .explore-project-author, .explore-project-likes, .run-button, #language-setting { + display: none ; + } + .help-list { + display: none ; + } + .help-view { + left: 0 ; + } + #about-menu br { + display: none ; + } + #about-menu { + bottom: unset ; + text-align: center ; + top: 40px ; + right: 30px ; + width: unset ; + } + #about-menu div { + margin: 5px 5px ; + } + #about-content { + left: 30px ; + right: 30px ; + top: 150px ; + bottom: 30px ; + } +} + + +@media screen and (max-width: 1000px) { + .titlemenu span { + display: none ; + } + .titlemenu li { + padding: 16px 18px 15px 18px; + margin: 4px 1px 0 0; + } + .titlemenu li i { + margin: 0 ; + } + .titlemenu li.selected { + background: rgba(255,255,255,.1); + } +} + +@media screen and (max-width: 1200px) { + #login-button span { + display: none ; + } + #login-button { + margin: 0 ; + } + #create-account-button span { + display: none ; + } + #login-button i, #create-account-button i { + margin: 0 ; + } +} diff --git a/static/css/music.css b/static/css/music.css new file mode 100644 index 00000000..411f0da9 --- /dev/null +++ b/static/css/music.css @@ -0,0 +1,150 @@ +.music-left { + position: absolute ; + left: 0 ; + width: 400px ; + top: 0 ; + bottom: 0 ; + border-top: solid 40px hsl(200,30%,30%); + overflow: hidden ; +} + +.music-right { + position: absolute ; + right: 0px ; + width: 400px ; + top: 0 ; + bottom: 0 ; + overflow: hidden ; +} + +.music-splitbar { + background: hsl(200,7%,28%); + position: absolute ; + left: 400px ; + top: 0 ; + bottom: 0 ; + width: 10px ; + cursor: ew-resize; + z-index: 2 ; +} + + +.musiclist { + position: absolute; + background: rgba(0,0,0,.25) ; + left: 0 ; + top: 40px ; + bottom: 40px ; + right: 0 ; + text-align: center ; + line-height: 0 ; + padding-top: 10px ; + padding-bottom: 10px ; + overflow-x: hidden ; + overflow-y: auto ; +} + +#create-music-button { + position: absolute ; + bottom: 0px ; + height: 40px; + left: 0 ; + right: 0 ; + padding: 0px 30px ; + background: hsl(160,50%,40%); + font-size: 18px ; + cursor: pointer ; + color: #FFF ; + z-index: 1 ; + box-shadow: 0 -10px 20px #000; + line-height: 40px ; + text-align: center ; +} +#create-music-button i { + margin-right: 0px ; +} +#create-music-button span { + white-space: nowrap ; + margin-left: 10px ; +} + +#music-editor { + position: absolute ; + top: 40px; + left: 0 ; + right: 0 ; + bottom: 0 ; +} + +#daw-transportbar { + position: absolute ; + top: 0px ; + left: 0px ; + right: 0px ; + height: 60px ; +} + +#daw-container { + position: absolute ; + top: 60px ; + bottom: 0 ; + left: 0 ; + right: 0 ; +} + +#daw-tracks { + position: absolute ; + top: 0 ; + left: 0 ; + right: 0 ; + height: 500px ; + overflow-y: auto ; +} + +#daw-splitbar { + background: hsl(200,7%,28%); + position: absolute ; + left: 0 ; + top: 400px ; + right: 0 ; + height: 10px ; + cursor: ns-resize; + z-index: 2 ; +} + +#daw-pianoroll { + position: absolute ; + bottom: 0 ; + left: 0 ; + right: 0 ; + height: 100px ; +} + +#daw-track-headers { + width: 200px ; + position: absolute ; + top: 0 ; + bottom: 0 ; + left: 0 ; +} + +#daw-track-content { + position:absolute ; + left: 200px ; + top: 0 ; + right: 0 ; + overflow-x: auto ; + overflow-y: hidden ; +} + +.daw-track-header { + border: solid 1px #FFF ; + width: 190px ; + height: 60px ; +} + +.daw-track { + border: solid 1px #FFF ; + width: 2000px ; + height: 60.0px ; +} diff --git a/static/css/options.css b/static/css/options.css new file mode 100644 index 00000000..35ae8043 --- /dev/null +++ b/static/css/options.css @@ -0,0 +1,200 @@ +#options-section { + overflow: auto ; + /*background: rgba(255,255,255,.05);*/ + border-top: solid 10px hsl(200,30%,30%); +} + +#projectoptions { + color: rgba(255,255,255,.8); +} + +#projectoptions-left { + position: absolute ; + left: 0 ; + top: 0 ; + bottom: 0 ; + width: 160px ; + padding: 40px 0px 40px 40px ; +} + +#projectoptions-icon { + width: 160px ; + border-radius: 10px ; + image-rendering: pixelated ; + background: #000 ; +} + +#projectoptions-right { + position: absolute ; + left: 240px ; + top: 0 ; + bottom: 0 ; + right: 0 ; + padding: 40px 40px 40px 0px ; +} + +#projectoptions-general { + position: absolute ; + left: 0px ; + top: 0 ; + bottom: 0 ; + width: 50% ; +} + +#projectoptions-general-content { + padding: 40px ; +} + +#projectoptions-users { + position: absolute ; + right: 0px ; + top: 0 ; + bottom: 0 ; + width: 50% ; +} + +#projectoptions-users-content { + padding: 40px 40px 40px 0px ; +} + +.projectoption { + margin-bottom: 40px ; +} + +.projectoption .tip { + font-size: 12px ; + color: rgba(255,255,255,.5); +} + +.projectoption input { + border: none ; + border-radius: 3px ; + font-size: 16px ; + padding: 5px ; + background: rgba(255,255,255,.2); + color: rgba(255,255,255,.8); + margin: 5px 10px 5px 0px ; + font-family: "Source Sans Pro",Verdana ; +} + +.projectoption input:focus { + outline: none ; + background: rgba(255,255,255,.3); +} + +.projectoption .sluginput { + width: 70px ; + text-align: center ; + margin: 0 2px ; +} + +.projectoptions h3 { + margin: 0px 0px 4px 0px ; +} + +.projectoption select { + font-size: 16px ; + background: rgba(255,255,255,.2); + color: rgba(255,255,255,.8); + border: none; + padding: 2px; + border-radius: 5px ; + height: 30px; + font-family: "Source Sans Pro",Verdana ; +} + +.projectoption select option { + background-color: hsl(200,50%,10%); + padding: 2px ; +} + +.projectoption select:focus { + outline: none ; + background: rgba(255,255,255,.4); +} + +.projectoptions .adduser { + margin-top: 20px ; + border-top: solid 2px rgba(255,255,255,.1); + padding-top: 5px ; +} + +.projectoptions .adduser .addbutton { + float: right ; + border-radius: 5px ; + background: hsl(160,50%,40%); + font-size: 16px ; + padding: 6px 20px ; + margin-top: 4px ; + cursor: pointer ; +} +.projectoptions .adduser .addbutton i { + margin-right: 10px ; +} + +.projectoptions .adduser input { + border: none ; + border-radius: 3px ; + font-size: 16px ; + padding: 5px ; + background: rgba(255,255,255,.2); + color: rgba(255,255,255,.8); + margin: 5px 10px 5px 0px ; + font-family: "Source Sans Pro",Verdana ; + width: 50%; +} + +.projectoptions .adduser input:focus { + outline: none ; + background: rgba(255,255,255,.4); +} + +.projectoptions .userlist .user { + margin: 2px ; + background: rgba(255,255,255,.1); + border-radius: 5px ; +} + +.projectoptions .userlist .username { + padding: 6px ; +} +.projectoptions .userlist .username i { + margin-left: 10px ; +} + +.projectoptions .userlist .remove { + float: right ; + border-radius: 5px ; + background: hsl(0,50%,40%); + font-size: 16px ; + padding: 6px 20px ; + cursor: pointer ; +} +.projectoptions .userlist .remove i { + margin-right: 10px ; +} + +#project-option-graphics { + display: none ; +} + +.projectoptions .validate-button-container { + display: inline-block ; + width: 100px ; + margin-left: 7px ; + transition-duration: .5s ; + transition-property: width ; + overflow: hidden ; + height: 30px ; + vertical-align: middle ; +} +.projectoptions .validate-button { + display: inline-block ; + padding: 5px 10px ; + font-size: 14px ; + border-radius: 3px ; + background: hsl(160,50%,40%); + color: #FFF ; + cursor: pointer ; + white-space: nowrap; +} diff --git a/static/css/publish.css b/static/css/publish.css new file mode 100644 index 00000000..dca4d6eb --- /dev/null +++ b/static/css/publish.css @@ -0,0 +1,149 @@ + +#publish-section { + padding: 10px ; + overflow: auto ; + border-top: solid 10px hsl(200,30%,30%); +} + +.publish-box { + margin: 10px 10px 30px 10px ; + padding: 20px ; + border-bottom: solid 2px rgba(255,255,255,.1); +} +.publish-box:last-child { + border: none ; +} +.publish-box h2 { + margin: 0 ; + font-size: 24px ; + font-family: Ubuntu ; + color: rgba(255,255,255,.8); +} +.publish-box h2 .beta { + border-radius: 10px ; + font-size: 12px ; + padding: 2px 8px ; + position: relative ; + bottom: 15px ; + left: 5px ; + background: hsl(0,50%,50%); +} +.publish-box p { +} + +.publish-box .action-box { + float:right ; + margin-left: 40px ; + width: 35%; +} + +.publish-button { + border-radius: 5px ; + padding: 10px 20px ; + background: hsl(160,50%,40%); + margin-bottom: 20px ; + text-align: center ; + cursor: pointer ; +} + +.publish-button.disabled { + background: hsla(160,50%,40%,.2); + cursor: default ; +} + +.publish-text { + margin-top: 20px ; + font-style: italic ; + color:rgba(255,255,255,.7); +} + +.publish-button i { + margin-right: 10px ; + } + + +.publish-box input, .publish-box textarea { + border: none ; + border-radius: 3px ; + font-size: 16px ; + padding: 5px ; + background: rgba(255,255,255,.2); + color: rgba(255,255,255,.8); + margin: 5px 10px 5px 0px ; + font-family: "Source Sans Pro",Verdana ; +} + +.publish-box input:focus, .publish-box textarea:focus { + outline: none ; + background: rgba(255,255,255,.3); +} + +.publish-box h3 { + margin-bottom: 5px ; +} + +.publish-box i { + margin-right: 5px ; +} + +.publish-box input { + display: inline-block ; + margin-right: 5px ; +} + +.publish-box .validate-button-container { + display: inline-block ; + width: 200px ; + transition-duration: .5s ; + transition-property: width ; + overflow: hidden ; + height: 30px ; + vertical-align: middle ; +} + +.publish-box .validate-button { + border-radius: 5px ; + background: hsl(160,50%,40%); + padding: 4px 12px ; + cursor: pointer ; + display: inline-block ; +} + +#publish-tag-list { + margin-top: 10px ; +} + +#publish-tag-list div { + display: inline-block ; + font-size: 14px ; + padding: 3px 3px 3px 10px ; + border-radius: 14px ; + background: hsl(200,50%,40%); + margin-right: 5px ; +} +#publish-tag-list div i { + margin-left: 10px ; + cursor: pointer ; +} + +.publish-box .logos { + text-align: center ; +} + +.publish-box .logo { + width: 120px ; + margin-right: 60px ; + margin-top: 20px ; + margin-bottom: 10px ; + vertical-align: middle ; +} + +.publish-box img.title { + float:left ; + margin: 0 40px 40px 0 ; + width: 100px ;5 +} + +.clear { + clear: both ; +} diff --git a/static/css/sounds.css b/static/css/sounds.css new file mode 100644 index 00000000..1c2b583f --- /dev/null +++ b/static/css/sounds.css @@ -0,0 +1,26 @@ +.sounds-left { + position: absolute ; + left: 0 ; + width: 400px ; + top: 0 ; + bottom: 0 ; + border-top: solid 40px hsl(200,30%,30%); + overflow: hidden ; +} + +.sounds-right { + position: absolute ; + right: 0px ; + width: 400px ; + top: 0 ; + bottom: 0 ; + overflow: hidden ; +} + +#sample-editor { + position: absolute ; + top: 40px; + left: 0 ; + right: 0 ; + bottom: 0 ; +} diff --git a/static/css/sprites.css b/static/css/sprites.css new file mode 100644 index 00000000..2026a381 --- /dev/null +++ b/static/css/sprites.css @@ -0,0 +1,656 @@ +.sprites-left { + position: absolute ; + left: 0 ; + width: 400px ; + top: 0 ; + bottom: 0 ; + border-top: solid 40px hsl(200,30%,30%); + overflow: hidden ; +} + +.sprites-right { + position: absolute ; + right: 0px ; + width: 400px ; + top: 0 ; + bottom: 0 ; + overflow: hidden ; +} + +.sprites-splitbar { + background: hsl(200,30%,30%); + position: absolute ; + left: 400px ; + top: 0 ; + bottom: 0 ; + width: 10px ; + cursor: ew-resize; + z-index: 2 ; +} + +.spritelist { + position: absolute; + background: rgba(0,0,0,.25) ; + left: 2px ; + right: 2px ; + top: 2px ; + bottom: 42px ; + text-align: center ; + line-height: 0 ; + padding-top: 10px ; + padding-bottom: 10px ; + overflow-x: hidden ; + overflow-y: auto ; +} +#create-sprite-button { + position: absolute ; + bottom: 0px ; + height: 40px; + left: 0 ; + right: 0 ; + padding: 0px 30px ; + background: hsl(160,50%,40%); + font-size: 18px ; + cursor: pointer ; + color: #FFF ; + z-index: 1 ; + box-shadow: 0 -10px 20px #000; + line-height: 40px ; + text-align: center ; +} +#create-sprite-button i { + margin-right: 0px ; +} +#create-sprite-button span { + white-space: nowrap ; + margin-left: 10px ; +} +.sprite-box { + position: relative ; + cursor: pointer ; + display: inline-block ; + margin: 1px ; + width: 64px ; + height: 84px ; + font-size: 12px ; + padding: 4px 0 ; + background: hsl(200,20%,20%); + box-shadow: 0 0 4px 0px #000; + border-radius: 4px ; + overflow: hidden ; +} +.sprite-box.selected { + margin: 0px; + border: solid 1px rgba(255,255,255,.5) ; + background: rgba(255,255,255,.2); +} + +.sprite-box .icon-box { + height: 64px ; + width: 64px ; + overflow: hidden ; + margin-bottom: 10px ; +} +/* +.sprite-box img { + width: 100% ; + image-rendering: pixelated; +}*/ +.sprite-box .active-user { + display: none ; + position: absolute ; + bottom: 20px ; + left: 4px ; + font-size: 8px ; + color: #FFF ; + background: hsl(0,50%,50%); + padding: 4px ; + border-radius: 8px ; +} + +.spritebar { + position: absolute; + padding: 0px; + right: 0; + top: 40px; + bottom: 0; + width: 220px; + background: rgba(0,0,0,.25); + /*border-left: solid 4px rgba(255,255,255,.2);*/ +} + +.spritebar h3 { + margin: 5px 0 0 0 ; + font-size: 14px; +} + +.spritetools { + position: absolute ; + left: 0 ; + top: 0 ; + bottom: 0 ; + width: 44px ; +} + +.spritetooloptions { + position: absolute ; + left: 58px ; + top: 0 ; + bottom: 0 ; + right: 0 ; + padding: 8px ; +} + +.spritetooloptions input { + margin-bottom: 20px ; +} + +.spritetooloptions label { + font-size: 14px ; +} + +.spritetooloptions .toolbox { + text-align: center ; +} + +.spritetoolbutton { + display: inline-block ; + margin: 4px 0px 0px 4px ; + border-radius: 5px ; + background: rgba(255,255,255,.1); + padding: 10px 2px 5px 2px; + line-height: 16px ; + font-size: 10px ; + text-align: center ; + width: 46px ; + cursor: pointer ; +} + +.spritetoolbutton:hover { + background: rgba(255,255,255,.2); +} + +.spritetoolbutton i { + background: none ; + font-size: 18px ; +} + +.spritetoolbutton.selected { + background: rgba(255,255,255,.8); + color: rgba(0,0,0,.8); +} + +.spritetooloptions .toolbox .spritetoolbutton.selected { + background: hsl(200,50%,50%); + color: #FFF; +} + + +.spritebar input { + width: 100% ; +} +.spriteinfo { + position: absolute; + right: 0px ; + left: 2px ; + top: 0px ; + height: 40px; + padding-left: 10px ; + background: hsl(200,30%,30%); + white-space: nowrap; +} +.spriteinfo * { + display: inline-block ; +} +.spriteinfo .buttons { + display: inline-block ; + margin-left: 40px ; +} +.spriteinfo .buttons div { + margin-left: 1px ; + padding: 10px 15px ; + cursor: pointer ; + background: hsl(160,50%,40%); +} +.spriteinfo .buttons div:hover { + background: hsl(160,50%,60%); +} +.spriteinfo .buttons div#delete-sprite { + background: hsl(0,50%,50%); +} +.spriteinfo .buttons div#delete-sprite:hover { + background: hsl(0,50%,60%); +} + +.spriteinfo input { + border: none ; + border-radius: 3px ; + margin:6px 3px 0px 0px ; + font-size: 14px ; + padding: 5px ; + background: rgba(255,255,255,.4); +} +.spriteinfo input:focus { + outline: none ; + background: rgba(255,255,255,.6); +} +.spriteinfo input#sprite-width,.spriteinfo input#sprite-height { + width: 40px ; + text-align: center ; + margin: 0 3px ; +} +.spriteinfo input#sprite-width { + margin-left: 40px ; +} +.spriteinfo .validate-button-container { + display: inline-block ; + width: 100px ; + margin-left: 7px ; + transition-duration: .5s ; + transition-property: width ; + overflow: hidden ; + height: 30px ; + vertical-align: middle ; +} +.spriteinfo .validate-button { + display: inline-block ; + padding: 5px 10px ; + font-size: 14px ; + border-radius: 3px ; + background: hsl(160,50%,40%); + color: #FFF ; + cursor: pointer ; + white-space: nowrap; +} +#spriteeditorcontainer { + position: absolute ; + right: 222px ; + left: 2px ; + top: 42px ; + bottom: 122px ; +} +.spriteeditor { + position: absolute; + right: 0 ; + left: 0 ; + top: 0 ; + bottom: 0 ; + text-align: center ; + overflow: hidden ; + line-height: 0px ; + /*border-left: solid 4px rgba(255,255,255,.2);*/ +} +.spriteeditor:focus { + outline: none ; +} +#sprite-zoom { + position: absolute ; + right: 12px ; + bottom: 12px ; +} +#sprite-zoom i { + margin-top: 8px ; + padding: 8px ; + border-radius: 20px ; + font-size: 18px ; + background: hsl(200,30%,30%); + box-shadow: 0 0 8px #000; + color: rgba(255,255,255,.8) ; + cursor: pointer ; +} +#sprite-zoom i:hover { + background: hsl(200,50%,50%); + color: #FFF ; +} +#sprite-grab-info-container { + position: absolute ; + bottom: 12px ; + left: 0 ; + right: 0 ; + text-align: center ; +} +#sprite-grab-info { + display: inline-block ; + border-radius: 20px ; + padding: 5px 10px ; + background: hsl(200,30%,30%); + color: rgba(255,255,255,.8) ; + box-shadow: 0 0 8px #000 ; + display: none ; +} +#sprite-grab-info.active { + background: hsl(200,50%,50%); + color: #FFF ; +} +#sprite-grab-info i { + margin-right: 10px ; +} +#spriteeditorcontainer.expanded { + bottom: 2px ; +} +#sprite-editor-locked { + position: absolute ; + right: 264px ; + left: 260px ; + top: 60px ; + padding: 20px ; + z-index: 100 ; + height: 20px ; + transition-duration: .5s ; + transition-property: left ; + border-radius: 10px ; + color: #FFF ; + font-size: 16px ; + display: none ; +} +#sprite-editor-locked i { + margin-right: 5px ; +} +.colorpicker { +/* margin-top: 20px;*/ +} +.colorpicker canvas { + /*width: 100% ;*/ + cursor: pointer ; +} +.spriteeditor canvas { + box-shadow: 0 0 20px #000 ; + /*border: solid 1px rgba(255,255,255,.2); + border-radius: 4px ;*/ + margin: 40px ; + display: none ; + cursor: crosshair ; +} +.spriteeditor canvas.colorpicker { + cursor: url( '/img/eyedropper.svg' ) 0 24, pointer; +} + +.spritehelpers { + position: absolute ; + bottom: 0px ; + left: 0px ; + right: 0px ; + height: 60px ; +} + +.spritehelper { + display: inline-block ; + margin: 4px 0px 0px 4px ; + border-radius: 5px ; + background: rgba(255,255,255,.1); + padding: 10px 2px 5px 2px; + line-height: 16px ; + font-size: 10px ; + text-align: center ; + width: 64px ; + cursor: pointer ; +} + +.spritehelper:hover { + background: rgba(255,255,255,.2); +} + +.spritehelper i { + background: none ; + font-size: 18px ; +} + +.spritehelper.selected { + background: rgba(255,255,255,.8); + color: rgba(0,0,0,.8); +} +#colorpicker-group { + text-align: center ; +} +#colorpicker-group .spritetoolbutton { + width: 140px ; + margin: 2px 0 ; +} +#sprite-animation-panel { + position: absolute; + left: 2px ; + right: 222px ; + bottom: 0px ; + height: 120px ; + background: hsl(200,40%,20%); + text-align: center ; + overflow: hidden ; + transition-property: bottom; + transition-duration: .5s; +} +#sprite-animation-panel.collapsed { + bottom: -120px ; +} +#sprite-animation-title { + position: absolute; + left: 10px; + padding: 7px 10px; + bottom: 110px; + height: 14px; + text-align: left; + font-size: 12px; + cursor: pointer; + border-radius: 10px; + background: linear-gradient(180deg,hsl(200,50%,50%) 0%,hsl(200,50%,50%) 10%,hsl(200,40%,20%) 20%) ; + transition-property: bottom,background,padding; + transition-duration: .5s; + z-index: 1; + display: block; + box-shadow: 0 -1px 1px #000; +} +#sprite-animation-title.collapsed { + bottom: 10px ; + background: hsl(200,50%,50%); + padding: 3px 10px; + box-shadow: 0 0 1px #000; +} +#sprite-animation-title i { + margin-right: 5px +} +#sprite-animation-panel .button { + color: #FFF; + margin: 28px 25px; + border-radius: 3px; + cursor: pointer; + display: inline-block; + position: relative; + top: 0px; + bottom: 0px; + vertical-align: top; + font-size: 24px; + cursor: pointer; +} +#sprite-animation-steps { + position: absolute; + right: 140px ; + left: 0px ; + padding: 18px 10px 0 10px; + top: 0px ; + bottom: 0px ; + text-align: left ; + font-size: 12px ; + overflow: auto ; + white-space: nowrap ; + line-height: 0 ; +} +#sprite-animation-list { + white-space: nowrap ; + line-height: 0 ; + display: inline-block ; +} +.sprite-animation-frame { + position: relative ; + margin: 2px 2px ; + line-height: 0 ; + display: inline-block ; +} +.sprite-animation-frame canvas { + background: hsla(200,0%,50%,.5); +} +.sprite-animation-frame i { + position: absolute ; + padding: 5px ; + border-radius: 3px ; + color: #FFF ; + font-size: 18px ; + right: 0 ; + bottom: 0 ; + cursor: pointer ; + display: none ; + text-shadow: 0 0 2px #000 ; +} +.sprite-animation-frame.selected { + border: solid 2px rgba(255,255,255,.8); + margin: 0 ; +} +.sprite-animation-frame:hover { + border: solid 2px rgba(255,255,255,.5); + margin: 0 ; +} +.sprite-animation-frame:hover i { + display: block ; +} +.sprite-animation-frame .remove { + top: 0 ; + bottom: unset ; +} +.sprite-animation-frame .clone { + top: 0 ; + right: unset ; + bottom: unset ; + left: 0 ; +} +.sprite-animation-frame .moveleft { + right: unset ; + left: 0 ; +} +.sprite-animation-frame span { + position: absolute ; + color: #FFF ; + text-shadow: 0.5px 0.5px 2px #000; + font-size: 14px ; + top: 10px ; + left: 5px ; + font-family: Ubuntu ; +} +.sprite-animation-frame:hover span { + display: none ; +} + +#sprite-animation-preview { + position: absolute ; + top: 10px ; + right: 0 ; + bottom: 0 ; + width: 110px ; +} +#sprite-animation-preview input { + width: 80px ; +} + +.selection-hint { + margin: 10px 0 ; + border-radius: 5px ; + background: rgba(255,255,255,.1) ; + padding: 5px 10px ; + font-size: 14px ; +} +.selection-hint i { + margin-left: 5px ; + margin-right: 10px ; +} +.selection-hint span { + background: rgba(255,255,255,.8); + color: rgba(0,0,0,.8); + padding: 2px 5px ; + border-radius: 3px ; +} +.selection-hint.active { + background: hsl(200,50%,40%); +} +.selection-operation { + margin: 10px 0 ; + border-radius: 5px ; + background: hsl(160,50%,40%); + padding: 10px ; + font-size: 14px ; + cursor: pointer ; + text-align: center ; +} +.selection-operation:hover { + background: hsl(160,50%,50%); +} +.selection-operation i.fa { + font-size: 14px ; + margin: 0 3px 10px 3px; + vertical-align: middle ; +} +.selection-operation i.fa-th { + font-size: 24px ; + margin-bottom: 10px ; +} +.selection-operation i.fa-play { + font-size: 14px ; + margin-bottom: 10px ; + color: hsl(160,50%,40%); + background: #FFF ; + border-radius: 3px ; + padding: 5px 10px ; +} + +.auto-palette { + padding: 5px; + border-radius: 5px; + background: rgba(255,255,255,.1); + margin-top: 10px; +} + +.auto-palette-title { + margin-bottom: 5px ; + font-size: 14px ; + text-align: left ; +} + +#auto-palette-lock { + border-radius: 20px ; + margin-left: 5px ; + padding: 5px ; + font-size: 10px ; + background: hsl(160,50%,50%) ; + color: #FFF ; + cursor: pointer ; +} +#auto-palette-lock.locked { + background: hsl(0,50%,50%) ; +} +#auto-palette-lock:hover { + box-shadow: 0 0 4px #FFF ; +} + +#auto-palette-list { + line-height: 0 ; + text-align: left ; +} + +#auto-palette-list div { + border-radius: 3px ; + margin: 1px ; + width: 15px ; + height: 15px ; + box-shadow: 0 0 2px #444 ; + display: inline-block ; + cursor: pointer ; +} + +#auto-palette-list div:hover { + box-shadow: 0 0 2px #FFF ; +} + +#auto-palette-list div.selected { + border-radius: 3px ; + margin: 0px ; + border: solid 1px #FFF ; + box-shadow: 0 0 2px #000 inset ; + display: inline-block ; + cursor: pointer ; +} diff --git a/static/css/style.css b/static/css/style.css new file mode 100644 index 00000000..808104ca --- /dev/null +++ b/static/css/style.css @@ -0,0 +1,1409 @@ +html,body { + font-family: "Source Sans Pro",Verdana; + color: rgba(255,255,255,.9); + margin: 0px; + overflow: hidden; + + user-select: none; +} + +html { + height: 100% ; +} + +body { + background: linear-gradient(135deg, hsl(200,35%,16%), hsl(200,20%,11%)); +} + +div::-webkit-scrollbar { + width: 10px; + height: 10px ; +} + +div::-webkit-scrollbar-track { + background: rgba(255,255,255,.15); + border-radius: 5px; + margin: 4px 2px; +} + +div::-webkit-scrollbar-thumb { + background-color: rgba(255,255,255,.3); + border-radius: 5px; + margin: 4px 1px; +} + +h1 { + font-family: "Ubuntu",Verdana; + margin: 0px ; + font-size: 32px ; + color: hsla(30,100%,100%,.7); +} + +header { + position: fixed; + top: 0 ; + left: 0; + right: 0 ; + height: 60px; + font-size: 20px; + /*box-shadow: 0 0 8px hsla(200,100%,0%,1);*/ + color: hsla(200,10%,100%,.7); + /*text-shadow: 1px 1px 2px #000 ;*/ + /*z-index: 3;*/ + /*border-bottom: solid 2px rgba(255,255,255,.05);*/ + white-space: nowrap; + /*box-shadow: 0 0 10px rgba(0,0,0,.5);*/ + transition-property: transform; + transition-duration: 1s; + transform: translateY(-100%); + z-index: 1 ; +} + +header .logo { + display: inline-block ; + margin-left: 20px ; + margin-top: 18px; + margin-right: 40px ; + color: rgba(255,255,255,.9); + width: 180px ; + vertical-align: top ; + cursor: pointer ; +} + +header ul { + display: inline-block ; + margin: 0px 0 0 0 ; + padding: 0 ; + white-space: nowrap; +} + +header li { + display: inline-block ; + position: relative ; + margin: 0px 10px ; + font-size: 14px; + margin-top: 7px ; + padding: 6px 10px 3px 10px; + font-family: "Source Sans Pro"; + color: rgba(255,255,255,.7); + text-align: center; + cursor: pointer ; + border-radius: 5px ; +} + +header li i { + margin-right: 5px ; + font-size: 18px ; +/* background: rgba(255,255,255,.2); + padding: 7px ; + border-radius: 15px ;*/ +} + +header li.selected, header li:hover { + background:rgba(255,255,255,.1) ; +} + +header li .fa-patreon { + position: absolute; + background: #FF424D; + padding: 7px; + border-radius: 30px; + font-size: 12px; + color: #FFF; + right: -10px; + top: 0px; +} + +header li .pastille { + position: absolute; + background: hsl(330,40%,50%); + padding: 5px; + border-radius: 30px; + font-size: 12px; + color: #FFF; + right: -10px; + top: 0px; +} + +#language-setting { + position: relative ; + float: right; + right: 20px ; + top: 6px ; + padding: 8px 0; + border-radius: 20px; + font-size: 12px; + color: #FFF; + width:32px; + height:32px ; + transition: width 0.5s ease ; + overflow: hidden ; +} +#language-setting i { + position: absolute ; + font-size: 32px ; + color: hsl(200,50%,40%); + cursor: pointer; +} +#language-setting .lang { + position: absolute ; + left: 0 ; + right: 0 ; + text-align:center ; + padding: 8px 10px ; + cursor: pointer; +} +.language-menu { + float: right ; + position: relative ; + width: 0px ; + overflow: hidden ; + padding: 8px ; + top: 6px ; + transition: width 0.5s ease ; + font-size: 12px ; + width: 0px ; + height: 32px ; + padding: 8px 0 ; + z-index: 10 ; +} +.language-menu-open { + width: 200px; + background: hsl(200,25%,20%); + border-radius: 5px; + padding-left: 10px; + /*box-shadow: 0 0 10px #000;*/ +} +.language-choice { + display: inline-block ; + position: relative ; + width: 32px ; + height: 32px ; + margin-right: 20px ; +} +.language-choice i { + position: absolute ; + font-size: 32px ; + color: hsl(200,50%,40%); + cursor: pointer; +} +.language-choice div { + position: absolute ; + left: 0 ; + right: 0 ; + text-align:center ; + padding: 8px 10px ; + cursor: pointer; +} + +#login-button { + float: right; + /* margin: 0px 0px; */ + color: #FFF; + font-size: 18px; + font-family: "Source Sans Pro"; + padding: 18.5px 0px 18.5px 20px ; + /* border-radius: 5px; */ + cursor: pointer; + display: none; +} + +#login-button i { + font-size: 18px; + margin-right: 5px; +} +#create-account-button { + float: right; + margin: 13px 20px; + color: #FFF; + font-size: 18px; + background: hsl(160,50%,40%); + font-family: "Source Sans Pro"; + padding: 5px 20px; + border-radius: 5px; + cursor: pointer; + display: none; +} + +#create-account-button i { + font-size: 18px; + margin-right: 5px; +} + +#user-info { + float: right ; + color: #FFF; + font-size: 16px; + font-family: "Source Sans Pro"; + display: none ; +} +#user-info i { + font-size: 16px; +} + +.login-info { + display: none ; + float: right ; + font-size: 18px ; +} +.login-info i { + margin: 0 2px ; +} + +header .username { + display: inline-block ; + margin-right: 20px ; + background: rgba(255,255,255,.2); + border-radius: 30px ; + padding: 0px 10px 0px 0px ; + cursor: pointer ; + margin-top: 10px ; + height: 38px ; +} +header .username i { + margin: 0 5px 0 15px ; + vertical-align: middle ; +} +header .username:hover { + background: rgba(255,255,255,.4); +} +header .username.guest { + background: hsl(0,50%,40%); +} +header .username img { + width: 32px ; + height: 32px ; + border-radius: 16px ; + vertical-align: middle ; + margin: 3px 10px 3px 3px ; +} +header .username span { + margin: 6px ; + vertical-align: middle ; +} + +.main-container { + position: fixed ; + top: 60px ; + bottom: 0 ; + left: 0 ; + right: 0 ; +} +.projects-section { + display: none ; +} +.projectheader { + /*background:rgba(255,255,255,.1) ;*/ + position: absolute ; + top: 0px ; + left: 0 ; + right: 0 ; + height: 30px ; + font-size: 18px ; + padding: 10px 20px 0px 16px ; +/* border-bottom: solid 1px rgba(0,0,0,.2);*/ +} + +.projectheader img { + height: 32px ; + vertical-align: middle ; + margin-right: 20px ; + background: #000 ; + border-radius: 3px ; + image-rendering: pixelated ; +} + +.projectheader .backtoprojects { + float: right ; + border-radius: 5px ; + background: hsl(0,50%,40%); + padding: 4px 10px ; + font-size: 14px; + cursor: pointer ; +} +.projectheader #save-status { + transition-property: transform, opacity, color ; + transition-duration: .5s ; + font-size: 20px ; + margin: 0 20px ; + opacity: 0 ; + color: #FFF; +} + +.projectheader #active-project-users { + display: inline-block ; + margin-left: 80px ; +} + +.projectheader #active-project-users div { + padding: 3px 8px ; + border-radius: 10px ; + background: hsl(0,50%,50%) ; + color: #FFF ; + font-size: 12px ; + margin-right: 10px ; + display: inline-block ; +} +.projectheader #active-project-users div i { + margin-right: 5px ; +} +.about-section { + display: none ; + position: absolute ; + top: 0 ; + bottom: 0 ; + left: 0 ; + right: 0 ; + overflow: auto ; +} +#about-content { + position: absolute; + top: 80px ; + bottom: 80px ; + overflow: auto ; + right: 80px ; + left: 300px ; +} +#about-content i { + font-size: 25px; + vertical-align: middle; + margin-right: 10px; +} +#about-menu { + position: absolute ; + top: 80px ; + bottom: 80px ; + left: 30px ; + width: 200px ; + text-align: right ; + padding-top: 20px ; +} +#about-menu div { + display: inline-block ; + background: rgba(255,255,255,.1); + border-radius: 15px ; + padding: 4px 15px ; + margin: 10px 0 ; + cursor: pointer ; +} +#about-menu div.selected { + background: rgba(255,255,255,.3); +} +.about { + padding: 80px ; +} +.about p { + color: rgba(255,255,255,.7); +} +.about h2 { + color: rgba(255,255,255,.8); +} +.about img { + width: 150px ; + vertical-align: middle ; +} +.about a { + text-decoration: none ; + color: hsl(200,100%,70%); +} +.about a:visited { + color: hsl(200,100%,70%); +} +.about-button-div { + text-align: center ; + margin: 40px ; +} +.about code { + background: rgba(255,255,255,.15); + padding: 2px 5px ; + border-radius: 3px ; + font-family: "Ubuntu Mono"; + line-height: 1.2em; + font-size: 14px ; +} +.about pre code { + display: block ; + background: rgba(255,255,255,.15); + padding: 10px ; + color: rgba(255,255,255,.8); + border-radius: 3px ; + font-family: "Ubuntu Mono"; + line-height: 1.2em; + white-space: pre-wrap ; + margin-right: 20px ; + margin-left: 40px ; +} +#about-button { + padding: 10px 30px ; + color: #FFF ; + background: hsl(160,50%,40%); + font-size: 20px ; + display: inline-block ; + border-radius: 5px ; + cursor: pointer ; +} +#about-button i { + margin-right: 5px ; +} + +.myprojects { + position: absolute; top:40px ; bottom: 0 ; left: 0 ; right: 0 ; + display: none ; + overflow: auto ; +} + +.myprojects .title { + margin: 0px 20px ; +} +.project-list { + margin: 10px ; +} +.project-list h2 { + margin: 100px 50px ; + text-align: center ; + color: rgba(255,255,255,.5); + font-size: 24px ; +} + +.project-invites-list { + margin: 0 10px 20px 20px ; + float: right ; +} + +.project-invites-list h2 { + margin: 20px 0 ; + font-size: 18px ; + font-family: Ubuntu ; + color: rgba(255,255,255,.7); +} + +.project-invites-list .invite { + background: rgba(0,0,0,.2); + border-radius: 5px ; + margin-bottom: 5px ; + width: 300px ; + overflow: hidden; +} + +.project-invites-list .invite .buttons { + float: right ; +} + +.project-invites-list .invite img { + height: 32px ; + image-rendering: pixelated ; + margin-right: 5px ; + vertical-align: middle ; +} + + +.project-invites-list .invite .buttons div { + display: inline-block ; + padding: 5px ; + font-size: 18px ; + width: 24px ; + height: 24px ; + text-align: center ; + cursor: pointer ; +} + +.project-invites-list .invite .buttons .accept { + background: hsl(160,50%,40%); +} +.project-invites-list .invite .buttons .reject { + background: hsl(0,50%,40%); +} + +.projectview { + position: absolute; top:0 ; bottom: 0 ; left: 0 ; right: 0 ; + display: none ; +} + +.container { + position: fixed; + top: 92px; + bottom: 0px; + left: 252px; + right: 1px; + padding: 10px; + overflow: auto ; +} + +.container::-webkit-scrollbar { + width: 10px; +} + +.container::-webkit-scrollbar-track { + background: rgba(255,255,255,.05); + border-radius: 5px; + margin: 4px 1px; +} + +.container::-webkit-scrollbar-thumb { + background-color: hsla(200,10%,80%,.5); + border-radius: 5px; + margin: 4px 1px; +} +.runbutton { + display: inline-block; + padding: 5px 10px ; + border-radius: 5px ; + background: hsl(120,50%,40%); + color: rgba(255,255,255,.9); + font-size: 18px ; + cursor: pointer ; +} +.runbutton i { + font-size: 14px; +} + +.patch-caption { + width: 200px ; + height: 150px; + margin: 10px ; + padding: 5px ; + background: hsl(0,40%,30%) ; + border-radius: 5px; + /*box-shadow: 0 0 10px hsla(0,50%,60%,1);*/ + display: inline-block; + cursor: pointer ; + transition: .1s filter, .1s transform ; + transition-timing-function: ease; +} + +.patch-caption .top { +/* text-shadow: 0 0 5px rgba(0,0,0,1);*/ +padding-left: 2px ; +} + +.patch-caption .center { + background-color: rgba(0,0,0,.5); + height: 100px; + margin: 5px 0; +} + +.patch-caption .bottom { +/* font-size: 14px;*/ +padding-left: 2px ; + +} + +.patch-caption .rating { + float: right ; +} + +.patch-caption:hover { + filter: brightness(1.2); + transform: scale(1.05,1.05); +} + +.patch-caption .toolbar { + float: right ; + text-shadow: none ; +} + +.sidemenu { + position: absolute; + top: 56px; + bottom: 0px; + left: 0px; + width: 60px; + z-index: 2 ; +} + +.sidemenu ul { + display: inline-block ; + margin: 0px 0 0 0 ; + padding: 0 ; + /*border-bottom: solid 2px rgba(255,255,255,.1);*/ +} + +.sidemenu li { + display: inline-block ; + padding: 15px 0px 8px 0 ; + font-size: 12px; + font-family: "Source Sans Pro"; + color: hsla(200,10%,90%,.9); + text-align: center; + width: 60px; + cursor: pointer ; + margin: 0px 0px ; +} + +.sidemenu li .fa-code { color: hsla(0,100%,80%,.85);} +.sidemenu li .fa-image { color: hsla(45,100%,80%,.85);} +.sidemenu li .fa-map { color: hsla(90,100%,80%,.85);} +.sidemenu li .fa-cube { color: hsla(180,100%,80%,.85);} +.sidemenu li .fa-volume-up { color: hsla(135,100%,80%,.85);} +.sidemenu li .fa-music { color: hsla(180,100%,80%,.85);} +.sidemenu li .fa-book { color: hsla(225,100%,80%,.85);} +.sidemenu li .fa-server { color: hsla(270,100%,80%,.85);} +.sidemenu li .fa-cogs { color: hsla(315,100%,80%,.85);} +.sidemenu li .fa-upload { color: hsla(0,100%,80%,.85);} + +.sidemenu li.selected .fa-code { color: hsla(0,100%,70%,1);} +.sidemenu li.selected .fa-image { color: hsla(45,100%,70%,1);} +.sidemenu li.selected .fa-map { color: hsla(90,100%,70%,1);} +.sidemenu li.selected .fa-cube { color: hsla(180,100%,70%,1);} +.sidemenu li.selected .fa-volume-up { color: hsla(135,100%,70%,1);} +.sidemenu li.selected .fa-music { color: hsla(180,100%,70%,1);} +.sidemenu li.selected .fa-book { color: hsla(225,100%,70%,1);} +.sidemenu li.selected .fa-server { color: hsla(270,100%,70%,1);} +.sidemenu li.selected .fa-cogs { color: hsla(315,100%,70%,1);} +.sidemenu li.selected .fa-upload { color: hsla(0,100%,70%,1);} + +.sidemenu li#menuitem-code.selected { width: 56px ; border-left: solid 4px hsl(0,100%,70%) ; } +.sidemenu li#menuitem-sprites.selected { width: 56px ; border-left: solid 4px hsl(45,50%,50%) ; } +.sidemenu li#menuitem-maps.selected { width: 56px ; border-left: solid 4px hsl(90,50%,50%) ; } +.sidemenu li#menuitem-assets.selected { width: 56px ; border-left: solid 4px hsl(180,50%,50%) ; } +.sidemenu li#menuitem-sounds.selected { width: 56px ; border-left: solid 4px hsl(135,50%,50%) ; } +.sidemenu li#menuitem-music.selected { width: 56px ; border-left: solid 4px hsl(180,50%,50%) ; } +.sidemenu li#menuitem-doc.selected { width: 56px ; border-left: solid 4px hsl(225,50%,50%) ; } +.sidemenu li#menuitem-server.selected { width: 56px ; border-left: solid 4px hsl(270,50%,50%) ; } +.sidemenu li#menuitem-options.selected { width: 56px ; border-left: solid 4px hsl(315,50%,50%) ; } +.sidemenu li#menuitem-publish.selected { width: 56px ; border-left: solid 4px hsl(0,50%,50%) ; } + +.sidemenu li:hover { + background-color: hsl(200,30%,30%) ; + color: hsla(200,10%,100%,.9); +} +.sidemenu .selected,.sidemenu .selected:hover { + background-color: hsl(200,30%,30%); + margin-bottom: 0px; + color: hsla(200,10%,100%,1); + /*box-shadow: 10px 0px 10px -10px hsla(200,100%,70%,1) inset;*/ +} + +.sidemenu li i { + font-size: 20px; + padding-bottom: 5px; +} + +.section-container { + position: absolute; + top: 46px; + bottom: 0px; + left: 60px; + right: 0px; + padding: 0px; + overflow: hidden; + border-left: solid 10px hsl(200,30%,30%); + border-radius: 10px 0px 0px 0px ; +} + +.section { + position: absolute; + top: 0px; + bottom: 0px; + left: 0px; + right: 0px; + padding: 0px; + overflow: hidden ; + display: none; +} + +.patchtree { + overflow: auto; + position: fixed; + bottom: 0px; + top: 52px ; + width: 252px; +} +.patchtree::-webkit-scrollbar { + width: 10px; +} + +.patchtree::-webkit-scrollbar-track { + background: rgba(255,255,255,.05); + border-radius: 5px; + margin: 4px 1px; +} + +.patchtree::-webkit-scrollbar-thumb { + background-color: hsla(200,10%,80%,.5); + border-radius: 5px; + margin: 4px 1px; +} + +.patchfoldertitle { + padding: 10px; + /*background: rgba(255,255,255,.1);*/ + cursor: pointer ; + border-bottom: solid 1px rgba(255,255,255,.05); + border-left: solid 1px rgba(255,255,255,.05); +} +.patchfoldertitle i { + margin-right:10px; +} + +.patchfoldercontent { + padding: 2px 0px 1px 0px ; + background: rgba(255,255,255,.03); + border-left: solid 30px hsla(200,50%,40%,.1); + margin-left: 0px; +} +.patchfoldercontent .patchfoldercontent { + padding: 5px 0px 20px 10px ; + border-left: solid 1px rgba(255,255,255,.05) ; + background: none ; +} + +.patchfoldercontent.closed { + display: none ; +} + + +.patchfile { + margin: 2px 0px 2px 30px ; + display: inline-block ; + padding: 5px 10px; + background: linear-gradient(hsl(200,40%,40%) 0%,hsl(200,40%,50%) 10%,hsl(200,40%,40%) 10%,hsl(200,40%,30%) 90%,hsl(200,40%,30%) 90%,hsl(200,40%,20%) 100%); + border-radius: 2px ; + cursor: pointer ; + min-width: 100px; + box-shadow: 0 0 4px rgba(0,0,0,.5); + color: rgba(255,255,255,.9); +} + +.patchfile i { + margin-right: 5px; +} +.editor-toolbar { + position: absolute; + bottom: 0; + right: 0; + z-index:1 ; + padding-bottom: 5px; +} + +.editor-toolbar i { + font-size: 20px ; + margin: 5px; + padding: 8px; + border-radius: 25px ; + background: hsl(200,10%,80%); + color: rgba(0,0,0,.8); + box-shadow: 0 0 10px rgba(0,0,0,.5); + cursor: pointer ; +} +.editor-toolbar i:hover { + background: hsl(200,10%,90%); +} +.editor-toolbar i:active { + background: hsl(200,10%,100%); +} +#popmenu { + position: fixed; + width: 300px; + min-height: 200px; + background: hsl(200,30%,10%); + border: solid 1px rgba(255,255,255,.1); + border-radius: 5px; + z-index: 4 ; + box-shadow: 0 0 10px rgba(0,0,0,.5); +} +.hidden { + display: none ; +} + +.code-editor,#code-editor { + position: absolute ; + top: 0px ; + bottom: 0px ; + left: 0 ; + width: 50% ; +} +.code-toolbar,#code-toolbar { + position: absolute ; + top: 0px ; + left: 0px ; + right: 0px ; + height: 20px ; + padding: 10px 20px ; + padding-left: 50px ; + transition-property: padding-left ; + transition-duration: .5s ; + background: hsl(200,30%,30%); +} +#code-toolbar i { + margin-right: 5px ; +} + +.overlay { + z-index: 4 ; + background: rgba(0,0,0,.7); + position: fixed ; + top: 0 ; + bottom: 0 ; + left: 0 ; + right: 0 ; + text-align: center ; + display: none; + overflow: auto ; +} +.window-wrapper { + text-align: center ; +} +#login-window { + margin-top: 100px ; + padding: 20px ; + border-radius: 10px; + background: #DDD; + color: #222; + display: inline-block ; + text-align: center ; + margin-bottom:100px; +} +#login-window h2 { + margin-top: 0px; +} +#login-window p { + margin: 0 ; + padding: 0 ; +} +#create-account-panel { + display: none ; +} +#forgot-password-panel { + display: none ; +} +#login-window input { + font-size: 18px; + padding: 5px; + margin-bottom: 20px; +} +#login-window button { + font-size: 18px; + background: hsl(200,50%,40%); + border-radius: 5px; + padding: 10px 20px; + color: #FFF; +} + +#login-window div.button { + background: hsl(160,50%,40%); + border-radius: 10px ; + padding: 20px ; + color: #FFF; + margin: 20px 0 0 0; + cursor: pointer ; + width: 300px ; +} +#login-window div.button i { + font-size: 40px ; +} +#create-account-terms { + max-width: 600px ; + padding: 20px ; + background: rgba(255,255,255,.5); + border-radius: 10px ; + margin: 20px 0 ; + text-align: left ; + font-size: 14px ; + color: #000 ; + display: none ; +} +#create-account-terms p { + margin-bottom: 20px ; +} +#create-account-terms h2 { + font-size: 18px ; + margin: 10px 0; +} +#create-account-terms img { + width: 110px ; + vertical-align: middle ; + filter: drop-shadow(0px 0px 3px #000); +} +#create-project-button { + display: inline-block ; + margin: 0 40px ; + padding: 7px 30px ; + background: hsl(160,70%,35%); + font-size: 18px ; + border-radius: 2px; + cursor: pointer ; + color: #FFF ; + vertical-align: middle ; +} +#create-project-button i { + margin-right: 5px; +} +#create-project-window { + margin-top: 200px ; + padding: 20px ; + border-radius: 10px; + background: #DDD; + color: #222; + display: inline-block ; + text-align: center ; +} +#create-project-window input { + font-size: 18px; + padding: 5px; + margin-bottom: 20px; +} +#create-project-window button { + font-size: 18px; + background: hsl(200,50%,40%); + border-radius: 5px; + padding: 10px 20px; + color: #FFF; + border: none ; + cursor: pointer ; + outline: none ; +} +#create-project-window select { + font-size: 18px; +} +#create-project-window h2 { + margin-top: 0px; +} + +.project-box { + position: relative ; + margin: 10px ; + padding: 10px ; + background: rgba(255,255,255,.1); + border-radius: 5px ; + display: inline-block ; + cursor: pointer ; + min-width: 250px ; + height: 200px ; + box-shadow: 0 0 30px rgba(0,0,0,.5); +} + +.project-box img { + border-radius: 10px ; + background: #000 ; + image-rendering: pixelated; + width: 144px ; + height: 144px ; +} + +.project-box .buttons { + position: absolute ; + bottom: 10px ; + right: 10px ; +} + +.project-box .buttons div { + padding: 4px 12px ; + background: hsl(0,50%,40%); + border-radius: 2px ; + font-size: 14px ; + margin-top: 8px ; +} + +.project-box .buttons div.clone { + background: hsl(200,50%,40%); +} + +.project-box .buttons div i { + margin-right: 5px ; +} + + +.project-title { + color: #FFF ; + font-size: 24px ; +} + +.ignoreMouseEvents { + pointer-events: none ; +} + +.usermenu { + position: fixed ; + top: 60px ; + right: 0px ; + height: 0px ; + overflow: hidden ; + z-index: 2 ; + background: hsl(200,50%,20%); + transition: height 0.5s ease ; +} +.usermenu i { + margin-right: 10px ; +} +.usermenu div { + padding: 10px 20px ; + font-size: 16px ; + border-top: solid 2px rgba(255,255,255,.1); + cursor: pointer ; +} +.usermenu div:hover { + background: rgba(255,255,255,.2); +} +.usermenu.regular .create-account, .usermenu.regular .discard-account { + display: none ; +} +.usermenu.guest .settings, .usermenu.guest .logout, .usermenu.guest .profile { + display: none ; +} +.usermenu .discard-account { + background: hsl(0,50%,40%); +} + +.help-section { + display: none ; + position: absolute; + top: 0; + bottom: 0; + left: 0; + right: 0; + background: hsl(30,85%,90%); +} +.doc-render.documentation { + padding: 50px ; + max-width: 1000px ; +} +.help-view { + position: absolute ; + top: 0 ; + bottom: 0 ; + left: 300px ; + right: 0 ; + overflow: auto ; +} +.help-view i { + padding: 4px 6px; + border-radius: 5px; + font-size: 10px; + background: rgba(0,0,0,.2); + color: rgba(0,0,0,.5); +} +.helplist { + position: absolute ; + top: 0 ; + bottom: 0 ; + left: 0px ; + width: 260px ; + overflow: auto ; +} +.help-list { + padding: 0px 20px 40px 20px ; +} + +.help-section div::-webkit-scrollbar-track { + background: rgba(0,0,0,.15); + border-radius: 5px; + margin: 4px 2px; +} + +.help-section div::-webkit-scrollbar-thumb { + background-color: rgba(0,0,0,.25); + border-radius: 5px; + margin: 4px 1px; +} +.help-list h1 { + color: hsl(200,80%,25%); + font-size: 20px; + margin: 40px 0 0 0 ; + padding: 0 ; + cursor: pointer ; +} +.help-list h2 { + color: hsl(200,80%,25%); + font-size: 18px; + margin: 10px 0 5px 20px ; + padding: 0 ; + cursor: pointer ; +} +.help-list h3 { + color: hsl(200,80%,25%); + font-size: 16px; + margin: 0 0 0 22px; + padding: 0; + border-left: solid 4px rgba(0,0,0,.1); + padding-left: 10px; + cursor: pointer ; +} + +#notification-container { + position: fixed ; + top: 60px ; + left: 0 ; + right: 0 ; + text-align: center ; + transform: translateY(-150px); + transition-duration: 1s ; + transition-property: transform ; +} +#notification-bubble { + padding: 10px 20px ; + border-radius: 10px ; + color: #FFF ; + background: hsl(30,60%,50%) ; + display: inline-block ; +} +#notification-bubble i { + margin-right: 10px ; +} +.greenbutton { + display: inline-block ; + border-radius: 5px ; + background: hsl(160,50%,40%); + color: #FFF ; + padding: 8px 20px ; + font-size: 14px ; + cursor: pointer ; +} +.greenbutton i { + margin-right: 10px ; +} +.redbutton { + display: inline-block ; + border-radius: 5px ; + background: hsl(0,50%,50%); + color: #FFF ; + padding: 8px 20px ; + font-size: 14px ; + cursor: pointer ; +} +.redbutton i { + margin-right: 10px ; +} +.smallgreenbutton { + display: inline-block ; + border-radius: 5px ; + background: hsl(160,50%,40%); + color: #FFF ; + padding: 2px 8px ; + font-size: 12px ; + cursor: pointer ; +} +.smallgreenbutton i { + margin-right: 10px ; +} +.smallredbutton { + display: inline-block ; + border-radius: 5px ; + background: hsl(0,50%,50%); + color: #FFF ; + padding: 2px 8px ; + font-size: 12px ; + cursor: pointer ; +} +.smallredbutton i { + margin-right: 10px ; +} +.right { + text-align: right ; +} +.center { + text-align: center ; +} +.left { + text-align: left ; +} + +.floating-window { + position: fixed ; + top: 200px ; + left: 300px ; + width: 400px ; + height: 300px ; + border-radius: 10px ; + box-shadow: 0 0 20px #000, 0 0 2px 0px #000 ; + background: hsl(200,30%,30%); + z-index: 10 ; + /* display: none ;*/ + overflow: hidden ; + display: none ; +} +.floating-window .titlebar { + position: absolute ; + left: 0 ; + right: 0 ; + top: 0 ; + height: 20px; + padding: 10px 10px; + background: rgba(0,0,0,.1); + color: #FFF; + font-size: 16px; + cursor: move; +} +.floating-window .titlebar .icon { + display: inline-block ; + margin-right: 10px ; +} +.floating-window .titlebar .title { + display: inline-block ; +} +.floating-window .titlebar .minify { + background: rgba(0,0,0,.5); + border-radius: 20px ; + padding: 6px 7px ; + position: absolute ; + right: 4px ; + top: 4px ; + text-align: center ; + cursor: pointer ; + font-size: 20px ; +} +.floating-window .navigation { + position: absolute; + bottom: 0 ; + height: 40px ; + left: 0 ; + right: 0 ; + background: rgba(0,0,0,.1); +} + +.floating-window .navigation i.resize { + padding: 12px 10px ; + text-align: center ; + position: absolute ; + right: 0px ; + width: 20px ; + transform: rotateZ(-45deg); + color: rgba(0,0,0,.5) ; + cursor: nwse-resize ; +} + +.floating-window .content { + position: absolute ; + left: 5px ; + right: 5px ; + top: 45px ; + bottom: 45px ; + padding: 5px; + overflow: hidden ; +} + +.usertag { + display: inline-block ; + position: relative ; + background: none ; + padding: 2px 18px 2px 6px ; + border-radius: 5px ; + overflow: visible ; +} +.usertag img { + vertical-align: middle ; + position: absolute ; + right: -16px ; + top: 0px ; +} +.usertag.founder { + background: hsl(0,50%,40%) ; +} +.usertag.pixel_master { + background: hsl(140,50%,40%) ; +} +.usertag.code_ninja { + background: hsl(180,50%,40%) ; +} +.usertag.gamedev_lord { + background: hsl(300,50%,40%) ; +} +.usertag.sponsor { + background: hsl(40,50%,40%) ; +} + +.horizontal-splitbar { + background: hsl(200,30%,30%); + position: absolute ; + left: 400px ; + top: 0 ; + bottom: 0 ; + width: 10px ; + cursor: ew-resize; + z-index: 2 ; +} + +.placeholder { + padding: 40px ; + color: rgba(255,255,255,.5) ; + font-size: 24px ; + text-align: center ; +} + +.meta-message { + position: fixed ; + top: 60px ; + left: 0 ; + right: 0 ; + height: 22px ; + background: hsl(300,30%,75%); + color: #222 ; + padding: 9px 20px ; + font-size: 16px ; +} +.meta-message i { + margin-right: 10px ; +} + + +#projects-bar { + position: absolute ; + top: 0 ; + left: 0 ; + right: 0 ; + height: 30px ; + padding: 5px ; + vertical-align: middle ; +} + +#projects-search { + position: relative ; + float: right ; + right: 0 ; + top: 0 ; + bottom: 0 ; + width: 200px ; +} +#projects-search input { + border-radius: 25px; + border: none; + margin: 6px; + font-size: 18px; + padding: 5px 15px; + background: rgba(255,255,255,.9); + width: 148px ; +} +#projects-search input:focus { + outline: none ; +} +#projects-search i { + position: absolute; + right: 26px; + top: 13px; + z-index: 1; + color: rgba(0,0,0,.3); + font-size: 18px; +} +#projects-tags { + position: absolute ; + left: 150px ; + right: 200px ; + top: 0 ; + bottom: 0 ; + white-space: nowrap ; + overflow: hidden; +} +#projects-tags div { + display: inline-block ; + border-radius: 20px ; + padding: 3px 8px ; + margin: 10px 2px ; + background: hsl(200,30%,15%); + color: #FFF ; + cursor: pointer ; + font-size: 12px ; +} +#projects-tags span { + font-size: 8px ; + padding: 3px 5px ; + border-radius: 15px ; + background: rgba(255,255,255,.8); + color: rgba(0,0,0,.8); +} +#projects-tags div.active { + background: hsl(200,70%,50%); + box-shadow: 0 0 10px hsl(200,70%,50%); +} +#projects-tag div:hover { + background: hsl(200,70%,50%); +} diff --git a/static/css/synth.css b/static/css/synth.css new file mode 100644 index 00000000..4bdd25f8 --- /dev/null +++ b/static/css/synth.css @@ -0,0 +1,256 @@ +.synth { + font-family: Ubuntu ; +} + +.synth h1 { + font-size: 30px ; + margin-bottom: 10px ; +} + +.synth .toolbar { + background: rgba(0,0,0,.1) ; + height: 40px ; + margin-bottom: 10px ; +} + +.synth .modules { + white-space: nowrap ; + line-height: 0 ; +} + +#synth .content { + position: static ; +} + +.synth .module { + margin: 0px 10px 10px 0 ; + display: inline-block ; + height: 155px ; + background: linear-gradient(hsl(200,25%,30%),hsl(200,25%,20%)); + border-radius: 5px ; + overflow: hidden ; + padding: 5px ; + border-top: solid 4px hsl(200,15%,50%); + line-height: 12px ; + box-shadow: 0 0 5px rgba(0,0,0,.5); +} + +.synth .separator { + position: relative; + display: inline-block; + width: 40px; + height: 220px; + border-radius: 0px; + overflow: hidden; + /*background: rgba(255,255,255,.2);*/ + padding: 2px; + border-radius: 10px; + display: none ; +} + +.synth .module .inline { + display: inline-block ; + vertical-align: top ; + text-align: center; +} +.synth .module .center { + text-align: center ; +} + + +.synth .module .label { + padding: 3px 3px 4px 3px ; + font-size: 12px ; + color: #FFF ; +} + +.synth .layers { + background: rgba(255,255,255,.1) ; + height: 40px ; + margin-top: 10px ; +} + +.synth .layers div { + display: inline-block ; +} + +.synth .module .knobline { +} + +.synth .module .labelledknob { + display: inline-block ; + text-align: center ; + margin: 5px ; +} +.synth .module .knoblabel { + font-size: 11px ; +} +.synth .knob { + cursor: pointer ; + display: block ; +} + +.synth .module .labelledslider { + display: inline-block ; + text-align: center ; + margin: 5px ; +} + +.synth .module .buttongroup { + margin-top: 5px ; +} +.synth .module .button { + width: 30px ; + height: 15px ; + box-shadow: 0 0 3px #000 ; + background: radial-gradient(#EEE,#AAA); + display: block ; + border-radius: 5px ; + margin: 5px 5px ; + cursor: pointer ; +} + +.synth .module .button.selected { + box-shadow: 0 0 10px hsl(30,100%,80%) ; + background: radial-gradient(hsl(30,100%,85%),hsl(30,100%,70%)); +} + +.synth .module .selector { + margin: 5px ; + border-radius: 3px; + overflow: hidden; + line-height: 0; + box-shadow: 0 0 3px #000; + display: inline-block ; + text-align: center ; +} + +.synth .module .selector i { + display: inline-block ; + font-size: 16px; + padding: 2px 6.5px; + width: 7px; + height: 16px; + vertical-align: middle; + color: #000 ; + background: radial-gradient(#EEE,#AAA) ; + cursor: pointer ; +} +.synth .module .selector i:active { + background: radial-gradient(hsl(30,100%,85%),hsl(30,100%,70%)) ; + box-shadow: 0 0 10px hsl(30,100%,80%); +} + +.synth .module .selector .screen { + display: inline-block ; + width: 60px ; + height: 14px ; + background: radial-gradient(hsl(200,60%,20%),hsl(200,30%,10%)) ; + vertical-align: middle ; + color: hsl(200,100%,75%); + text-shadow: 0 0 3px hsl(200,100%,50%); + font-size: 9px; + line-height: normal; + text-align: center; + padding-top: 6px; + white-space: nowrap; +} + +.synth .separator #combine-button { + position: absolute; + left: 2px; + top: 85px; + width: 40px; + height: 40px; + /* box-shadow: 0 0 10px hsl(30,100%,80%); */ + background: rgba(0,0,0,.5); + display: block; + border-radius: 40px; + cursor: pointer; + color: hsl(200,100%,80%); + text-shadow: 0 0 5px hsl(200,100%,60%); + font-size: 31px; + padding: 0px; + text-align: center; +} + +.synth .separator #combine-button.rotate { + transform: rotate(45deg); +} + +#synth-window { + width: auto ; + height: auto ; + border-radius: 10px ; + box-shadow: 0 0 20px #000, 0 0 2px 0px #000 ; + background: hsl(200,30%,15%); + z-index: 10 ; + display: block ; + overflow: hidden ; +} +#synth-window .titlebar { + position: static ; + height: 20px; + padding: 10px 10px; + background: rgba(0,0,0,.1); + color: #FFF; + font-size: 16px; + cursor: move; +} +#synth-window .titlebar .icon { + display: inline-block ; + margin-right: 10px ; +} +#synth-window .titlebar .title { + display: inline-block ; +} +#synth-window .titlebar .minify { + background: rgba(0,0,0,.5); + border-radius: 20px ; + padding: 6px 7px ; + position: absolute ; + right: 4px ; + top: 4px ; + text-align: center ; + cursor: pointer ; + font-size: 20px ; +} +#synth-window .navigation { + position: absolute; + bottom: 0 ; + height: 40px ; + left: 0 ; + right: 0 ; + background: rgba(0,0,0,.1); +} + +#synth-window .navigation i.resize { + padding: 12px 10px ; + text-align: center ; + position: absolute ; + right: 0px ; + width: 20px ; + transform: rotateZ(-45deg); + color: rgba(0,0,0,.5) ; + cursor: nwse-resize ; +} + +#synth-window .content { + position: static ; + padding: 5px 0 0 10px; + overflow: auto ; + font-size: 18px; + font-family: Helvetica,sans-serif; +} + +#synth-window .content::-webkit-scrollbar-track { + background: rgba(0,0,0,.15); + border-radius: 5px; + margin: 4px 2px; +} + +#synth-window .content::-webkit-scrollbar-thumb { + background-color: rgba(0,0,0,.25); + border-radius: 5px; + margin: 4px 1px; +} diff --git a/static/css/terminal.css b/static/css/terminal.css new file mode 100644 index 00000000..80d33dbb --- /dev/null +++ b/static/css/terminal.css @@ -0,0 +1,106 @@ +.runtime-terminal { + position: absolute ; + left: 0 ; + right: 0 ; + bottom: 0 ; + height: 100px ; + border-left: solid 2px rgba(0,0,0,.5); + z-index: 2 ; +} +#terminal { + position: absolute ; + left: 0 ; + right: 0 ; + bottom: 0 ; + top: 0 ; +} +#terminal-view { + position: absolute ; + top:0 ; + left: 0 ; + right: 0 ; + bottom: 40px ; + background: hsl(200,50%,12%) ; + font-family: "Ubuntu Mono"; + font-size: 16px; + overflow: auto ; +/* box-shadow: 0 0 200px 0 rgba(0,0,0,1) inset;*/ +} + +#terminal-input-line { + position: absolute ; + bottom: 0 ; + left: 0 ; + right: 0 ; + height: 40px ; + background: hsl(200,50%,15%); +} +#terminal-input-gt { + position: absolute ; + left: 0 ; + width: 10px ; + top: 0 ; + bottom: 0 ; + font-family: "Ubuntu Mono"; + font-size: 20px; + color: hsl(200,100%,70%); + padding: 11px 10px ; +} +#terminal-input-container { + position: absolute ; + bottom: 0 ; + left: 30px ; + right: 0 ; + height: 40px ; + border: none ; + background: hsl(200,50%,15%); +} +#terminal-input { + position: absolute ; + bottom: 0 ; + left: 0 ; + right: 0 ; + top: 0 ; + bottom: 0 ; + height: 40px ; + border: none ; + width: 100% ; + background: hsl(200,50%,15%); + font-family: "Ubuntu Mono"; + font-size: 16px; + color: hsl(200,100%,70%); + outline: none ; + padding: 0 0 0 0 ; +} +#terminal-lines { + padding: 5px ; + font-family: "Ubuntu Mono"; + font-size: 16px; + color: hsl(200,100%,80%); + user-select: text ; +} +#terminal-lines div { + padding: 0 4px 0 18px ; + white-space: pre-wrap ; + line-height: 22px ; +} +#terminal-lines div.error { + color: hsl(20,100%,70%); +} +#terminal-lines div.input { + background: rgba(255,255,255,.05) ; + border-radius: 3px ; + padding: 0 4px ; +} +#terminal-lines div.input i { + opacity: .25 ; +} +.fa-ellipsis-v { + animation: blinker 1s linear infinite; +} + +@keyframes blinker { + 50% { + opacity: 0; + } +} diff --git a/static/css/tutorial.css b/static/css/tutorial.css new file mode 100644 index 00000000..f8df33f6 --- /dev/null +++ b/static/css/tutorial.css @@ -0,0 +1,255 @@ +#tutorials-section { + display: none ; +} + +.tutorials { + position: absolute ; + top: 0 ; + bottom: 0 ; + left: 0 ; + right: 0 ; + padding: 30px ; + text-align: center ; + overflow: auto ; +} + +.tutorials h1 { + color: rgba(255,255,255,.7); + margin-top: 40px ; +} +.tutorials h3 { + color: rgba(255,255,255,.7); + margin-bottom: 50px ; +} + +.tutorials .course { + border-radius: 10px ; + background: hsl(30,50%,90%); + color: #000 ; + display: inline-block ; + padding: 20px ; + margin: 10px ; + vertical-align: top ; + width: 300px ; +} +.tutorials .course p { + margin-bottom: 20px ; + text-align: left ; + font-size: 14px ; +} + +.tutorials .course ul { + list-style-type: none ; + padding: 0 ; + margin: 0 ; +} +.tutorials .course li { + position: relative ; + list-style-type: none ; + text-align: left ; + margin: 4px 0 ; + padding-right: 10px ; + border-radius: 5px ; + background: rgba(0,0,0,.1); + text-align: left ; + vertical-align: middle ; + cursor: pointer ; + overflow: hidden ; +} +.tutorials .course li:hover { + background: hsl(200,50%,70%) ; +} +.tutorials .course li i { + margin-right: 10px ; + padding: 10px ; + background: hsl(160,50%,50%) ; + color: #FFF ; + font-size: 10px ; +} +.tutorials .course li i.fa-check { + background: none ; +} +.tutorials .course li a { + position: absolute ; + right: 0 ; + top: 0 ; +} + +.tutorials .course li i.fa-file { + background: none ; + color: rgba(0,0,0,.4) ; + font-size: 18px ; + padding: 6px 0 ; + cursor: pointer ; +} +.tutorials .course li i.fa-check { + color: rgba(0,0,0,.5); + font-size: 16px ; + padding: 7px ; +} + +#tutorial-window { + position: fixed ; + top: 200px ; + left: 300px ; + width: 400px ; + height: 300px ; + border-radius: 10px ; + box-shadow: 0 0 20px #000, 0 0 2px 0px #000 ; + background: hsl(30,100%,95%); + z-index: 10 ; + display: none ; + overflow: hidden ; +} +#tutorial-window .titlebar { + position: absolute ; + left: 0 ; + right: 0 ; + top: 0 ; + height: 20px; + padding: 10px 10px; + background: hsl(200,50%,40%); + color: #FFF; + font-size: 16px; + cursor: move; +} +#tutorial-window .titlebar .icon { + display: inline-block ; + margin-right: 10px ; +} +#tutorial-window .titlebar .title { + display: inline-block ; +} +#tutorial-window .titlebar .minify { + background: rgba(0,0,0,.5); + border-radius: 20px ; + padding: 6px 7px ; + position: absolute ; + right: 4px ; + top: 4px ; + text-align: center ; + cursor: pointer ; + font-size: 20px ; +} +#tutorial-window .navigation { + position: absolute; + bottom: 0 ; + height: 40px ; + left: 0 ; + right: 0 ; + background: rgba(0,0,0,.1); +} +#tutorial-window .navigation i.previous { + background: hsl(200,50%,50%); + padding: 12px 10px ; + position: absolute ; + left: 0 ; + width: 20px ; + text-align: center ; + cursor: pointer ; +} +#tutorial-window .navigation i.next { + background: hsl(200,50%,50%); + padding: 12px 10px ; + position: absolute ; + right: 40px ; + width: 20px ; + text-align: center ; + cursor: pointer ; +} +#tutorial-window .navigation i.next.fa-check { + background: hsl(160,50%,50%); +} +#tutorial-window .navigation span.step { + padding: 12px 10px ; + text-align: center ; + position: absolute ; + right: 80px ; + left: 40px ; + color: rgba(0,0,0,.9) ; +} +#tutorial-window .navigation i.resize { + padding: 12px 10px ; + text-align: center ; + position: absolute ; + right: 0px ; + width: 20px ; + transform: rotateZ(-45deg); + color: rgba(0,0,0,.5) ; + cursor: nwse-resize ; +} + +#tutorial-window .content { + position: absolute ; + left: 5px ; + right: 5px ; + top: 45px ; + bottom: 45px ; + padding: 15px; + overflow: auto ; + font-size: 18px; + font-family: Helvetica,sans-serif; +} + +#tutorial-window .content::-webkit-scrollbar-track { + background: rgba(0,0,0,.15); + border-radius: 5px; + margin: 4px 2px; +} + +#tutorial-window .content::-webkit-scrollbar-thumb { + background-color: rgba(0,0,0,.25); + border-radius: 5px; + margin: 4px 1px; +} + +#tutorial-window.minimized { + transition-duration: .2s ; + transition-property: top,left,width,height; + border-radius: 20px; +} + +#tutorial-window.minimized .navigation { + display: none ; +} + +#tutorial-window.minimized .title { + display: none ; +} + +#tutorial-window.minimized .minify { + display: none ; +} + +#tutorial-window.minimized .icon { + position: absolute; + top: 0; + left: 0; + cursor: pointer; + padding: 7px 9px; +} + +#highlighter { + position: fixed ; + z-index: 15 ; + display: none ; + pointer-events: none ; +} +#highlighter-arrow { + position: fixed ; + z-index: 5 ; + display: none ; + pointer-events: none ; +} + +#tutorial-overlay { + position: fixed ; + top: 0 ; + bottom: 0 ; + left: 0 ; + right: 0 ; + z-index: 4 ; + display: none ; + background: hsla(200,100%,10%,.9) ; + pointer-events: none ; +} diff --git a/static/css/user.css b/static/css/user.css new file mode 100644 index 00000000..ff2b6855 --- /dev/null +++ b/static/css/user.css @@ -0,0 +1,261 @@ +#usersettings-section { + display: none ; + padding: 80px ; + position:absolute ; + top: 0 ; + bottom: 0 ; + left: 0 ; + right: 0 ; + overflow: auto ; +} + + + +.usersettings-page { + position: absolute; + top: 80px ; + bottom: 80px ; + overflow: auto ; + right: 80px ; + left: 300px ; +} + +#usersettings-menu { + position: absolute ; + top: 80px ; + bottom: 80px ; + left: 30px ; + width: 200px ; + text-align: right ; + padding-top: 20px ; +} +#usersettings-menu div { + display: inline-block ; + background: rgba(255,255,255,.1); + border-radius: 25px ; + padding: 6px 15px ; + margin: 10px 0 ; + cursor: pointer ; +} +#usersettings-menu div.selected { + background: rgba(255,255,255,.3); +} +#usersettings-menu div i { + margin-left: 10px ; +} + + + + + + +#usersettings { + max-width: 800px ; +} + +.usersetting { + margin-bottom: 40px ; +} + +.usersettings textarea { + border: none ; + border-radius: 3px ; + font-size: 16px ; + padding: 5px ; + background: rgba(255,255,255,.1); + color: rgba(255,255,255,.8); + margin: 5px 10px 5px 0px ; + font-family: "Source Sans Pro",Verdana ; + width: 80% ; + height: 200px ; +} + +.usersettings textarea:focus { + outline: none ; + background: rgba(255,255,255,.2); +} + +.usersettings input { + border: none ; + border-radius: 3px ; + font-size: 16px ; + padding: 5px ; + background: rgba(255,255,255,.2); + color: rgba(255,255,255,.8); + margin: 5px 10px 5px 0px ; + font-family: "Source Sans Pro",Verdana ; + width: 200px ; +} +.usersettings input#subscribe-newsletter { + width: initial ; +} + +.usersettings input:focus { + outline: none ; + background: rgba(255,255,255,.3); +} + +.usersettings h3 { + margin: 0px 0px 4px 0px ; +} + +.usersettings .tip { + font-size: 12px ; + color: rgba(255,255,255,.5); +} + +.usersettings h1 { + margin-bottom: 40px ; +} + +.usersettings .validate-button-container { + display: inline-block ; + width: 200px ; + transition-duration: .5s ; + transition-property: width ; + overflow: hidden ; + height: 30px ; + vertical-align: middle ; +} + +.usersettings .validate-button { + display: inline-block ; + padding: 5px 10px ; + font-size: 15px ; + border-radius: 5px ; + background: hsl(160,50%,40%); + color: #FFF ; + cursor: pointer ; + white-space: nowrap; +} + +.usersettings .error { + color: hsl(30,100%,70%); + white-space: nowrap; + transition-duration: .5s ; + transition-property: width ; + width: 500px ; + overflow: hidden ; + display: inline-block ; +} + + +.usersetting .button { + display: inline-block ; + padding: 10px 20px ; + border-radius: 5px ; + background: hsl(160,50%,40%); + color: #FFF ; + cursor: pointer ; +} +.usersetting .button i { + margin-right: 5px ; +} +.usersettings a { + color: hsl(200,100%,80%); + text-decoration: none ; + padding: 10px 0 ; + /*background: rgba(255,255,255,.1); + border-radius: 15px ;*/ +} +.usersettings a:visited { + color: hsl(200,50%,80%); + text-decoration: none ; +} +.usersettings a i { + margin-left: 10px ; +} + +#translation-app { + display: none ; +} + +#translation-app-back-button { + display: inline-block ; + border-radius: 5px ; + background: hsl(0,50%,40%); + padding: 4px 10px ; + font-size: 14px; + cursor: pointer ; + margin: 2px 40px 40px 20px ; +} + +#open-translation-app { + display: none ; +} + +#translation-app { + display: none ; +} + +#translation-app input { + width: 46% ; + margin: 10px 1% ; +} + + +.usersettings table { + border-radius: 3px ; + margin-bottom: 20px ; +} + +.usersettings table,.usersettings tr,.usersettings td { + color: rgba(255,255,255,.8); + font-size: 20px ; + border-spacing: 3px ; +} + +.usersettings tr,.usersettings td,.usersettings th { + border: solid 1px rgba(0,0,0,.1); + padding: 10px ; + background: rgba(255,255,255,.1); + border-radius: 3px ; +} +.usersettings th { + font-weight: bold ; + background: rgba(0,0,0,.05); +} + +#usersettings-profile-image { + position: relative ; + width: 128px ; + height: 128px ; + margin: 10px 0 ; + cursor: pointer ; +} + +#usersettings-profile-image .fa-times-circle { + position: absolute ; + top: -10px ; + right: -10px ; + font-size: 20px ; + color: hsl(0,50%,70%) ; + cursor: pointer ; +} +#usersettings-profile-image .fa-user-circle { + position: absolute ; + top: 0 ; + bottom: 0 ; + left: 0 ; + right: 0 ; + font-size: 64px ; + padding: 32px ; + background: rgba(255,255,255,.1) ; + border-radius: 10px ; + color: rgba(255,255,255,.2) ; +} +#usersettings-profile-image img { + position: absolute ; + top: 0 ; + bottom: 0 ; + left: 0 ; + right: 0 ; + width: 128px ; + height: 128px ; + border-radius: 64px ; + transition-property: border-radius ; + transition-duration: .2s ; +} +#usersettings-profile-image img:hover { + border-radius: 10px ; +} diff --git a/static/css/userpage.css b/static/css/userpage.css new file mode 100644 index 00000000..60dddfa2 --- /dev/null +++ b/static/css/userpage.css @@ -0,0 +1,122 @@ +html { + height: 100% ; +} + +html,body { + font-family: "Source Sans Pro",Verdana; + color: hsla(200,10%,90%,.8); + margin: 0px; + + -webkit-user-select: none; + -moz-user-select: -moz-none; + -ms-user-select: none; + user-select: none; +} + +body { + background: linear-gradient(135deg, hsl(200,35%,16%), hsl(200,20%,11%)); +} + +header { + font-size: 20px; + color: #FFF; + margin-bottom: 20px ; +} + +.headerdiv { + margin-left: 20px ; + margin-top: 14px; + font-family: "Source Sans Pro"; + text-align: center ; +} + +.headerdiv img { + width: 200px ; + margin: 20px 0 ; +} + +.content { + padding: 40px ; +} + +.projects { + text-align: left; + clear: both ; +} + +.projectbox { + display: inline-block ; + margin: 10px ; + padding: 10px ; + font-size: 18px; + text-align: center ; + vertical-align: top ; + width: 120px ; +} + +.projectbox .icon { + display: inline-block; + width: 72px ; + height: 72px; + border-radius: 10px ; + background: hsl(200,50%,50%); +} + +.projectbox .title { + text-align: center ; + font-family: "Ubuntu" ; + margin-top: 10px ; + text-shadow: 0 0 3px #000 ; +} + +a { + color: #FFF; +} +.noselect { + -webkit-touch-callout: none; + -webkit-user-select: none; + -khtml-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; +} +#canvaswrapper { + text-align: center ; +} + +.profile_image { + display: inline-block ; + margin-right: 40px ; + border-radius: 64px ; + vertical-align: middle ; +} + +.username { + display: inline-block ; + font-size: 36px ; + margin-bottom: 10px ; + vertical-align: middle ; +} + +.description { + margin-top: 40px ; +} + +.sections { + margin: 40px 0 ; +} + +.sections div { + border-radius: 30px ; + background: rgba(255,255,255,.1) ; + display: inline-block ; + margin: 10px 20px 10px 0 ; + padding: 10px 20px ; + cursor: pointer ; +} + +.sections div.selected { + border-radius: 30px ; + background: rgba(255,255,255,.9) ; + color: rgba(0,0,0,.9); +} diff --git a/static/doc/en/about.md b/static/doc/en/about.md new file mode 100644 index 00000000..59913719 --- /dev/null +++ b/static/doc/en/about.md @@ -0,0 +1,32 @@ +![microStudio](/img/microstudiologo.svg "microStudio") + +**microStudio** is a free game development environment. It also aims to be a platform for learning and sharing. + +See changelog for information on updates. + +
+ +Support microStudio on Patreon and get exclusive rewards! + +Patreon + +Many thanks to all the contributors and especially to our wonderful Founders: Steven Landman, Code Club France + +Thanks a lot to our awesome translators: FeniXb3 (contributed Polish language) and TinkerSmith (contributed German language)! + + Join us on Discord: [https://discord.gg/BDMqjxd](https://discord.gg/BDMqjxd) + + Contact e-mail: [contact@microstudio.dev](mailto:contact@microstudio.dev) + +Created by Gilles Pommereuil in Strasbourg, France + +Published by: + +Neuronality SARL
+2 Chemin des Rochers
+67120 MOLSHEIM
+FRANCE + +Siret: R.C.S. Saverne TI 808 517 858
+ +[![Neuronality](/img/neuronality.svg "Neuronality")](https://www.neuronality.com) diff --git a/static/doc/en/changelog.md b/static/doc/en/changelog.md new file mode 100644 index 00000000..c392c260 --- /dev/null +++ b/static/doc/en/changelog.md @@ -0,0 +1,328 @@ +## Changelog + +### Update 2021-06-30 +* slight UI refresh (see home page and project pages!) +* Profile image and profile description (/bio) ; note that the profile pic does not show everywhere yet, will be improved soon +* Improved public user page (still in progress too, more to come) +* Prepared gamification features (nothing available yet) +* Fixed `a.b += "word"` not working correctly when `a.b` was previously set to `""` +* Fixed setLineWidth not coming up as a suggestion in the code editor + +### Update 2021-06-21 +* map editor: you can choose a background color (just affects visualization in the editor) +* map editor: you can display another map underneath your current map being edited. Useful if you use maps to create multiple layers of information for your game. +* map editor: now displays the coordinates of the block pointed at. +* sound playback: new property to check if sound playback is over: `sound.finished` +* `continue` keyword is now properly highlighted in code editor + +### Update 2021-06-14 +* Fixed bug: sounds not working in Android / APK builds + +### Update 2021-06-10 +* Android builder + +### Update 2021-06-04 +* Storage tiers boosted! free: 20->50 Mb ; pixel master: 50->200 Mb ; code ninja: 200->400Mb +* Refactoring continued, new internal backup system + +### Update 2021-05-31 +* added `sprite.getFrame()` +* Big refactoring work, anticipating server migration and open-sourcing + +### Update 2021-05-11 +* Fixed bug when calling `screen.drawPolygon` and `screen.fillPolygon` with array +* `screen.drawPolygon` now automatically closes path +* Added `screen.drawPolyline` to draw a path without closing +* Added `screen.setLineDash`to define a dash line style for all subsequent drawing operations +* Added all compositing modes available in HTML5 Canvas (see: https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/globalCompositeOperation ), to be used with `screen.setBlending(...)` +* Added missing documentation for `setBlending` + +### Update 2021-05-04 +* Removed undefined variable warning when parent class not resolved at loading time +* Improved upload retries, lowered websocket keep alive (ping) periodicity +* Added `system.say(text)` + +### Update 2021-04-29 +* Transpilation can now be activated as an option (alpha status) +* Fixed sprites thumbnails not synchronizing when receiving updates from server + +### Update 2021-04-15 +* You can now watch forum categories / forum posts (receive notifications by e-mail) +* Fixed microStudioCeption bug: https://microstudio.dev/community/bugs/accessing-my-project-inside-my-project/99/ +* Fixed the project slug field annoyingly resetting while you are typing (project options) + +### Update 2021-04-07 +* Fixed bug in the render chain when sprites are filtered out by the web client (e.g. by ad blocker) +* Sounds / music can now start playing without clicking in runtime window + +### Update 2021-03-25 +* Embed microStudio apps in your forum posts and replies + +### Update 2021-03-23 +* Fixed code editor clipping bug after collapsing / expanding file list +* Text search feature in Community +* Sort posts by activity, likes, views, replies or creation date +* A new search field helps filtering your own project list. + +### Update 2021-03-22 +* Fixed bug in microScript ; when using return statement from ```for ... in``` loop, return value was not correct + +### Update 2021-03-20 +* Fixed issue causing posts and replies to be sent multiple times in the forum. + +### Update 2021-03-12 +* Like posts and replies in the Community forum +* support for (externally linked) images in community posts and replies + +### Update 2021-03-09 +* microStudio Community dark mode +* microStudio Community can be installed as a PWA app + +### Update 2021-03-08 +* release of microStudio Community! + +### Update 2021-03-03 +* New function ```screen.drawTextOutline()``` +* Screen clear color: ```screen.clear(color)``` +* added support for Android maskable app icons + +### Update 2021-03-02 +* New function ```list.sortList(compareFunction)``` + +### Update 2021-02-19 +* You can now create and share your own interactive microStudio tutorials +* Launch a custom tutorial from a URL +* View built-in tutorials source code +* fixed regression, sprite thumbnail not updating after renamed +* fixed regression, map thumbnail not updating after renamed +* fixed bug: public user page not scrollable + +### Update 2021-02-01 +* Sounds! Drop your wav files into your project and play them from your code +* Music! Drop your mp3 files into your project and play music from your code +* Added options to enable / disable warnings in the console +* Temporary fix for disappearing public projects (limit raised from 100 to 300 projects, will now rework that page) +* New split bar when opening a public project from the Explore section + +Note about sounds: when exporting a project to HTML5, the sounds can only play when the page is hosted on a HTTP server ; sounds will not play when the page is opened with the file:// protocol. + +### Update 2021-01-21 +* Issuing warning when using an undefined variable +* Issuing warning when assigning property to an undefined variable / property +* Issuing warning when making a function call on something that isn't a function +* integrated translations of the documentation (loaded directly from Github repository: https://github.com/pmgl/microstudio-documentation) + +### Update 2021-01-13 +* activated new languages Polish and German! Thanks to contributors FeniX and TinkerSmith who will start working on the translations! +* label sprite frames from zero in the sprite editor +* fixed server crash case caused by messages buffering + +### Update 2020-12-17 +* New map editor split view allowing to enlarge your tileset +* You can also zoom in/out your tileset with the mouse wheel and move the view by holding the spacebar +* Fixed bug with continuous gamepad triggers LT and RT + +### Update 2020-12-10 +* code editor: You can now change the font size +* code editor: Search button to find string in code (you can also use Ctrl+F) +* Your Patreon badge is now displayed with your username in the public projects and comments +* assign all kind of property names when creating an object, with double quotes: +``` +myobj = object + x = 1 + "-this doesn't need to be a legal identifier-" = 2 +end +``` +* you can also use quotes to access a property name which isn't an identifier ```myvar."-this isn't even an id-"``` or ```keyboard.press."("``` +* You can now see the byte size of your project files (project settings tab) +* Gamepad triggers (B6 and B7) are now continuous (range from 0 - released to 1 - fully pressed) +* Fixed a bug with cloning a project adding a *main* source file with default draw, update, init, when the cloned project doesn't have a *main* file + +### Update 2020-11-28 +* Fixed parser bug when two comment sequences ```//``` are on the same line +* Documented ```break``` and ```continue``` statements for loops + +### Update 2020-11-26 +* Store data permanently and reload with ```storage.set( name , value )``` and ```storage.get( name )``` +* Clone your own private project in a click +* New string functions: ```split```, ```lastIndexOf```, ```toLowerCase```, ```toUpperCase``` +* Cursor location now saved when switching from a source code file to another +* Predefined "boolean" constants ```true = 1``` and ```false = 0``` +* Fix: cloned map wasn't automatically updated and seemed empty when drawn on screen +* Fix: liking a public project used to toggle like on other opened projects in the same session +* Fix: documentation: touch.touches and not touch.keys +* Documented ```screen.setCursorVisible()``` + +### Update 2020-11-18 +* documented screen.clear() +* New trigonometry functions in degrees (sind,cosd,tand,asind,acosd,atand,atan2d) +* Added missing function ```tan()``` +* you can now test ```keyboard.press.``` or ```keyboard.release.``` to check whether a key was just pressed or just released +* you can now test ```gamepad.press. +

+
+ + + + + + + + + diff --git a/static/js/microscript/test.ms b/static/js/microscript/test.ms new file mode 100644 index 00000000..37574abe --- /dev/null +++ b/static/js/microscript/test.ms @@ -0,0 +1,46 @@ +x = 5 + +test = function(y) + local s = "" + for i in 1 to y + print(i) + end +end + +test(x) + +i = 0 +level_names = while i<10 i = i+1 "level "+i end + +print(for i in 1 to 10 "level "+i end) + + +map = object + x = 0 + y = 0 +end + +map.x == map["x"] + +empty = object end + +shape = class(sides=3) + sides = sides + x = 0 + y = 0 +end + +square = class() from shape + _parent(4) + x = 2 + y = 2 +end + +s = new square(4) + +do in 1000 status = "play" end + +timer = function() + if countdown>0 then in 1000 do timer() end end + countdown = countdown-1 +end diff --git a/static/js/microscript/todo.txt b/static/js/microscript/todo.txt new file mode 100644 index 00000000..435b9c66 --- /dev/null +++ b/static/js/microscript/todo.txt @@ -0,0 +1,17 @@ +* function(x,y=x) can't work as expected + +DONE +* implemented correct function scope +* list insert push pop pull remove length +* implement random +* implement Date.now() replacement +* micro.tick micro.time() +* += -= *= /= +* implement not +* implement function arguments default value +* implement return +* Bug parseLine can return null and causing endless lists + +NEXT +* classes, instanciation +* in 1000 do plop() end (???? vraiment ????) diff --git a/static/js/microscript/token.coffee b/static/js/microscript/token.coffee new file mode 100644 index 00000000..5776ff16 --- /dev/null +++ b/static/js/microscript/token.coffee @@ -0,0 +1,103 @@ +class @Token + constructor:(@tokenizer,@type,@value,@string_value)-> + @line = @tokenizer.line + @column = @tokenizer.column + @start = @tokenizer.token_start + @length = @tokenizer.index-@start + @index = @tokenizer.index + if @type == Token.TYPE_IDENTIFIER and Token.predefined.hasOwnProperty(@value) + @type = Token.predefined[@value] + @reserved_keyword = true + + @is_binary_operator = (@type>=30 and @type<=35) or (@type>=200 and @type<=201) or (@type>=2 and @type<=7) + + toString:()-> + return @value+ " : "+@type + +@Token.TYPE_EQUALS = 1 +@Token.TYPE_DOUBLE_EQUALS = 2 +@Token.TYPE_GREATER = 3 +@Token.TYPE_GREATER_OR_EQUALS = 4 +@Token.TYPE_LOWER = 5 +@Token.TYPE_LOWER_OR_EQUALS = 6 +@Token.TYPE_UNEQUALS = 7 + +@Token.TYPE_IDENTIFIER = 10 +@Token.TYPE_NUMBER = 11 +@Token.TYPE_STRING = 12 + +@Token.TYPE_OPEN_BRACE = 20 +@Token.TYPE_CLOSED_BRACE = 21 +@Token.TYPE_OPEN_CURLY_BRACE = 22 +@Token.TYPE_CLOSED_CURLY_BRACE = 23 +@Token.TYPE_OPEN_BRACKET = 24 +@Token.TYPE_CLOSED_BRACKET = 25 +@Token.TYPE_COMMA = 26 +@Token.TYPE_DOT = 27 + +@Token.TYPE_PLUS = 30 +@Token.TYPE_MINUS = 31 +@Token.TYPE_MULTIPLY = 32 +@Token.TYPE_DIVIDE = 33 +@Token.TYPE_POWER = 34 +@Token.TYPE_MODULO = 35 + +@Token.TYPE_PLUS_EQUALS = 40 +@Token.TYPE_MINUS_EQUALS = 41 +@Token.TYPE_MULTIPLY_EQUALS = 42 +@Token.TYPE_DIVIDE_EQUALS = 43 + +@Token.TYPE_RETURN = 50 +@Token.TYPE_BREAK = 51 +@Token.TYPE_CONTINUE = 52 + +@Token.TYPE_FUNCTION = 60 +@Token.TYPE_LOCAL = 70 +@Token.TYPE_OBJECT = 80 + +@Token.TYPE_CLASS = 90 +@Token.TYPE_EXTENDS = 91 +@Token.TYPE_NEW = 92 + +@Token.TYPE_FOR = 100 +@Token.TYPE_TO = 101 +@Token.TYPE_BY = 102 +@Token.TYPE_IN = 103 +@Token.TYPE_WHILE = 104 +@Token.TYPE_IF = 105 +@Token.TYPE_THEN = 106 +@Token.TYPE_ELSE = 107 +@Token.TYPE_ELSIF = 108 +@Token.TYPE_END = 120 + +@Token.TYPE_AND = 200 +@Token.TYPE_OR = 201 +@Token.TYPE_NOT = 202 + +@Token.TYPE_ERROR = 404 + +@Token.predefined = {} +@Token.predefined["return"] = @Token.TYPE_RETURN +@Token.predefined["break"] = @Token.TYPE_BREAK +@Token.predefined["continue"] = @Token.TYPE_CONTINUE +@Token.predefined["function"] = @Token.TYPE_FUNCTION +@Token.predefined["for"] = @Token.TYPE_FOR +@Token.predefined["to"] = @Token.TYPE_TO +@Token.predefined["by"] = @Token.TYPE_BY +@Token.predefined["in"] = @Token.TYPE_IN +@Token.predefined["while"] = @Token.TYPE_WHILE +@Token.predefined["if"] = @Token.TYPE_IF +@Token.predefined["then"] = @Token.TYPE_THEN +@Token.predefined["else"] = @Token.TYPE_ELSE +@Token.predefined["elsif"] = @Token.TYPE_ELSIF +@Token.predefined["end"] = @Token.TYPE_END +@Token.predefined["object"] = @Token.TYPE_OBJECT +@Token.predefined["class"] = @Token.TYPE_CLASS +@Token.predefined["extends"] = @Token.TYPE_EXTENDS +@Token.predefined["new"] = @Token.TYPE_NEW + +@Token.predefined["and"] = @Token.TYPE_AND +@Token.predefined["or"] = @Token.TYPE_OR +@Token.predefined["not"] = @Token.TYPE_NOT + +@Token.predefined["local"] = @Token.TYPE_LOCAL diff --git a/static/js/microscript/token.js b/static/js/microscript/token.js new file mode 100644 index 00000000..68d6673d --- /dev/null +++ b/static/js/microscript/token.js @@ -0,0 +1,173 @@ +this.Token = (function() { + function Token(tokenizer, type, value, string_value) { + this.tokenizer = tokenizer; + this.type = type; + this.value = value; + this.string_value = string_value; + this.line = this.tokenizer.line; + this.column = this.tokenizer.column; + this.start = this.tokenizer.token_start; + this.length = this.tokenizer.index - this.start; + this.index = this.tokenizer.index; + if (this.type === Token.TYPE_IDENTIFIER && Token.predefined.hasOwnProperty(this.value)) { + this.type = Token.predefined[this.value]; + this.reserved_keyword = true; + } + this.is_binary_operator = (this.type >= 30 && this.type <= 35) || (this.type >= 200 && this.type <= 201) || (this.type >= 2 && this.type <= 7); + } + + Token.prototype.toString = function() { + return this.value + " : " + this.type; + }; + + return Token; + +})(); + +this.Token.TYPE_EQUALS = 1; + +this.Token.TYPE_DOUBLE_EQUALS = 2; + +this.Token.TYPE_GREATER = 3; + +this.Token.TYPE_GREATER_OR_EQUALS = 4; + +this.Token.TYPE_LOWER = 5; + +this.Token.TYPE_LOWER_OR_EQUALS = 6; + +this.Token.TYPE_UNEQUALS = 7; + +this.Token.TYPE_IDENTIFIER = 10; + +this.Token.TYPE_NUMBER = 11; + +this.Token.TYPE_STRING = 12; + +this.Token.TYPE_OPEN_BRACE = 20; + +this.Token.TYPE_CLOSED_BRACE = 21; + +this.Token.TYPE_OPEN_CURLY_BRACE = 22; + +this.Token.TYPE_CLOSED_CURLY_BRACE = 23; + +this.Token.TYPE_OPEN_BRACKET = 24; + +this.Token.TYPE_CLOSED_BRACKET = 25; + +this.Token.TYPE_COMMA = 26; + +this.Token.TYPE_DOT = 27; + +this.Token.TYPE_PLUS = 30; + +this.Token.TYPE_MINUS = 31; + +this.Token.TYPE_MULTIPLY = 32; + +this.Token.TYPE_DIVIDE = 33; + +this.Token.TYPE_POWER = 34; + +this.Token.TYPE_MODULO = 35; + +this.Token.TYPE_PLUS_EQUALS = 40; + +this.Token.TYPE_MINUS_EQUALS = 41; + +this.Token.TYPE_MULTIPLY_EQUALS = 42; + +this.Token.TYPE_DIVIDE_EQUALS = 43; + +this.Token.TYPE_RETURN = 50; + +this.Token.TYPE_BREAK = 51; + +this.Token.TYPE_CONTINUE = 52; + +this.Token.TYPE_FUNCTION = 60; + +this.Token.TYPE_LOCAL = 70; + +this.Token.TYPE_OBJECT = 80; + +this.Token.TYPE_CLASS = 90; + +this.Token.TYPE_EXTENDS = 91; + +this.Token.TYPE_NEW = 92; + +this.Token.TYPE_FOR = 100; + +this.Token.TYPE_TO = 101; + +this.Token.TYPE_BY = 102; + +this.Token.TYPE_IN = 103; + +this.Token.TYPE_WHILE = 104; + +this.Token.TYPE_IF = 105; + +this.Token.TYPE_THEN = 106; + +this.Token.TYPE_ELSE = 107; + +this.Token.TYPE_ELSIF = 108; + +this.Token.TYPE_END = 120; + +this.Token.TYPE_AND = 200; + +this.Token.TYPE_OR = 201; + +this.Token.TYPE_NOT = 202; + +this.Token.TYPE_ERROR = 404; + +this.Token.predefined = {}; + +this.Token.predefined["return"] = this.Token.TYPE_RETURN; + +this.Token.predefined["break"] = this.Token.TYPE_BREAK; + +this.Token.predefined["continue"] = this.Token.TYPE_CONTINUE; + +this.Token.predefined["function"] = this.Token.TYPE_FUNCTION; + +this.Token.predefined["for"] = this.Token.TYPE_FOR; + +this.Token.predefined["to"] = this.Token.TYPE_TO; + +this.Token.predefined["by"] = this.Token.TYPE_BY; + +this.Token.predefined["in"] = this.Token.TYPE_IN; + +this.Token.predefined["while"] = this.Token.TYPE_WHILE; + +this.Token.predefined["if"] = this.Token.TYPE_IF; + +this.Token.predefined["then"] = this.Token.TYPE_THEN; + +this.Token.predefined["else"] = this.Token.TYPE_ELSE; + +this.Token.predefined["elsif"] = this.Token.TYPE_ELSIF; + +this.Token.predefined["end"] = this.Token.TYPE_END; + +this.Token.predefined["object"] = this.Token.TYPE_OBJECT; + +this.Token.predefined["class"] = this.Token.TYPE_CLASS; + +this.Token.predefined["extends"] = this.Token.TYPE_EXTENDS; + +this.Token.predefined["new"] = this.Token.TYPE_NEW; + +this.Token.predefined["and"] = this.Token.TYPE_AND; + +this.Token.predefined["or"] = this.Token.TYPE_OR; + +this.Token.predefined["not"] = this.Token.TYPE_NOT; + +this.Token.predefined["local"] = this.Token.TYPE_LOCAL; diff --git a/static/js/microscript/tokenizer.coffee b/static/js/microscript/tokenizer.coffee new file mode 100644 index 00000000..d18cc409 --- /dev/null +++ b/static/js/microscript/tokenizer.coffee @@ -0,0 +1,186 @@ +class @Tokenizer + constructor:(@input,@filename)-> + @index = 0 + @line = 1 + @column = 0 + @last_column = 0 + + @buffer = [] + + @chars = {} + @chars["("] = Token.TYPE_OPEN_BRACE + @chars[")"] = Token.TYPE_CLOSED_BRACE + @chars["["] = Token.TYPE_OPEN_BRACKET + @chars["]"] = Token.TYPE_CLOSED_BRACKET + @chars["{"] = Token.TYPE_OPEN_CURLY_BRACE + @chars["}"] = Token.TYPE_CLOSED_CURLY_BRACE + @chars["^"] = Token.TYPE_POWER + @chars[","] = Token.TYPE_COMMA + @chars["."] = Token.TYPE_DOT + @chars["%"] = Token.TYPE_MODULO + + @doubles = {} + @doubles[">"] = [Token.TYPE_GREATER,Token.TYPE_GREATER_OR_EQUALS] + @doubles["<"] = [Token.TYPE_LOWER,Token.TYPE_LOWER_OR_EQUALS] + @doubles["="] = [Token.TYPE_EQUALS,Token.TYPE_DOUBLE_EQUALS] + @doubles["+"] = [Token.TYPE_PLUS,Token.TYPE_PLUS_EQUALS] + @doubles["-"] = [Token.TYPE_MINUS,Token.TYPE_MINUS_EQUALS] + @doubles["*"] = [Token.TYPE_MULTIPLY,Token.TYPE_MULTIPLY_EQUALS] + @doubles["/"] = [Token.TYPE_DIVIDE,Token.TYPE_DIVIDE_EQUALS] + + pushBack:(token)-> + @buffer.splice(0,0,token) + + finished:()-> + @index>=@input.length + + nextChar:()-> + c = @input.charAt(@index++) + if c == "\n" + @line += 1 + @last_column = @column + @column = 0 + else if c == "/" + if @input.charAt(@index) == "/" + loop + c = @input.charAt(@index++) + break if c == "\n" or @index>=@input.length + @line += 1 + @last_column = @column + @column = 0 + return @nextChar() + else + @column += 1 + c + + rewind:()-> + @index -= 1 + @column -= 1 + if @input.charAt(@index) == "\n" + @line -=1 + @column = @last_column + + next:()-> + if @buffer.length>0 + return @buffer.splice(0,1)[0] + + loop + return null if @index>=@input.length + c = @nextChar() + code = c.charCodeAt(0) + break if code > 32 + + @token_start = @index-1 + + if @doubles[c]? + return @parseDouble(c,@doubles[c]) + + if @chars[c]? + return new Token @,@chars[c],c + + if c == "!" + return @parseUnequals(c) + else if (code>=48 and code<=57) + return @parseNumber(c) + else if (code>=65 and code<=90) or (code>=97 and code<=122) or code == 95 + return @parseIdentifier(c) + else if c == '"' + return @parseString(c,'"') + else if c == "'" + return @parseString(c,"'") + else + return @error "Syntax Error" + + changeNumberToIdentifier:()-> + token = @next() + if token? and token.type == Token.TYPE_NUMBER + v = token.string_value.split(".") + for i in [v.length-1..0] by -1 + if v[i].length>0 + @pushBack new Token @,Token.TYPE_IDENTIFIER,v[i] + if i>0 + @pushBack new Token @,Token.TYPE_DOT,"." + else if token? and token.type == Token.TYPE_STRING + @pushBack new Token @,Token.TYPE_IDENTIFIER,token.value + else + @pushBack token + + parseDouble:(c,d)-> + if @index<@input.length and @input.charAt(@index) == "=" + @nextChar() + return new Token @,d[1],c+"=" + else + return new Token @,d[0],c + + parseEquals:(c)-> + if @index<@input.length and @input.charAt(@index) == "=" + @nextChar() + return new Token @,Token.TYPE_DOUBLE_EQUALS,"==" + else + return new Token @,Token.TYPE_EQUALS,"=" + + parseGreater:(c)-> + if @index<@input.length and @input.charAt(@index) == "=" + @nextChar() + return new Token @,Token.TYPE_GREATER_OR_EQUALS,">=" + else + return new Token @,Token.TYPE_GREATER_OR_EQUALS,">" + + parseLower:(c)-> + if @index<@input.length and @input.charAt(@index) == "=" + @nextChar() + return new Token @,Token.TYPE_LOWER_OR_EQUALS,"<=" + else + return new Token @,Token.TYPE_LOWER,"<" + + parseUnequals:(c)-> + if @index<@input.length and @input.charAt(@index) == "=" + @nextChar() + return new Token @,Token.TYPE_UNEQUALS,"!=" + else + return @error "Expected inequality !=" + + parseIdentifier:(s)-> + loop + return new Token(@,Token.TYPE_IDENTIFIER,s) if @index>=@input.length + c = @nextChar() + code = c.charCodeAt(0) + if (code>=65 and code<=90) or (code>=97 and code<=122) or code == 95 or (code>=48 and code<=57) + s += c + else + @rewind() + return new Token(@,Token.TYPE_IDENTIFIER,s) + + parseNumber:(s)-> + pointed = false + loop + return new Token(@,Token.TYPE_NUMBER,(if pointed then Number.parseFloat(s) else Number.parseInt(s)),s) if @index>=@input.length + c = @nextChar() + code = c.charCodeAt(0) + if c == "." and not pointed + pointed = true + s += c + else if code>=48 and code<=57 + s += c + else + @rewind() + return new Token(@,Token.TYPE_NUMBER,(if pointed then Number.parseFloat(s) else Number.parseInt(s)),s) + + parseString:(s,close='"')-> + loop + return @error("Unclosed string value") if @index>=@input.length + c = @nextChar() + code = c.charCodeAt(0) + if c == close + n = @nextChar() + if n == close + s += c + else + @rewind() + s += c + return new Token @,Token.TYPE_STRING,s.substring(1,s.length-1) + else + s += c + + error:(s)-> + throw s diff --git a/static/js/microscript/tokenizer.js b/static/js/microscript/tokenizer.js new file mode 100644 index 00000000..73e947de --- /dev/null +++ b/static/js/microscript/tokenizer.js @@ -0,0 +1,250 @@ +this.Tokenizer = (function() { + function Tokenizer(input, filename) { + this.input = input; + this.filename = filename; + this.index = 0; + this.line = 1; + this.column = 0; + this.last_column = 0; + this.buffer = []; + this.chars = {}; + this.chars["("] = Token.TYPE_OPEN_BRACE; + this.chars[")"] = Token.TYPE_CLOSED_BRACE; + this.chars["["] = Token.TYPE_OPEN_BRACKET; + this.chars["]"] = Token.TYPE_CLOSED_BRACKET; + this.chars["{"] = Token.TYPE_OPEN_CURLY_BRACE; + this.chars["}"] = Token.TYPE_CLOSED_CURLY_BRACE; + this.chars["^"] = Token.TYPE_POWER; + this.chars[","] = Token.TYPE_COMMA; + this.chars["."] = Token.TYPE_DOT; + this.chars["%"] = Token.TYPE_MODULO; + this.doubles = {}; + this.doubles[">"] = [Token.TYPE_GREATER, Token.TYPE_GREATER_OR_EQUALS]; + this.doubles["<"] = [Token.TYPE_LOWER, Token.TYPE_LOWER_OR_EQUALS]; + this.doubles["="] = [Token.TYPE_EQUALS, Token.TYPE_DOUBLE_EQUALS]; + this.doubles["+"] = [Token.TYPE_PLUS, Token.TYPE_PLUS_EQUALS]; + this.doubles["-"] = [Token.TYPE_MINUS, Token.TYPE_MINUS_EQUALS]; + this.doubles["*"] = [Token.TYPE_MULTIPLY, Token.TYPE_MULTIPLY_EQUALS]; + this.doubles["/"] = [Token.TYPE_DIVIDE, Token.TYPE_DIVIDE_EQUALS]; + } + + Tokenizer.prototype.pushBack = function(token) { + return this.buffer.splice(0, 0, token); + }; + + Tokenizer.prototype.finished = function() { + return this.index >= this.input.length; + }; + + Tokenizer.prototype.nextChar = function() { + var c; + c = this.input.charAt(this.index++); + if (c === "\n") { + this.line += 1; + this.last_column = this.column; + this.column = 0; + } else if (c === "/") { + if (this.input.charAt(this.index) === "/") { + while (true) { + c = this.input.charAt(this.index++); + if (c === "\n" || this.index >= this.input.length) { + break; + } + } + this.line += 1; + this.last_column = this.column; + this.column = 0; + return this.nextChar(); + } + } else { + this.column += 1; + } + return c; + }; + + Tokenizer.prototype.rewind = function() { + this.index -= 1; + this.column -= 1; + if (this.input.charAt(this.index) === "\n") { + this.line -= 1; + return this.column = this.last_column; + } + }; + + Tokenizer.prototype.next = function() { + var c, code; + if (this.buffer.length > 0) { + return this.buffer.splice(0, 1)[0]; + } + while (true) { + if (this.index >= this.input.length) { + return null; + } + c = this.nextChar(); + code = c.charCodeAt(0); + if (code > 32) { + break; + } + } + this.token_start = this.index - 1; + if (this.doubles[c] != null) { + return this.parseDouble(c, this.doubles[c]); + } + if (this.chars[c] != null) { + return new Token(this, this.chars[c], c); + } + if (c === "!") { + return this.parseUnequals(c); + } else if (code >= 48 && code <= 57) { + return this.parseNumber(c); + } else if ((code >= 65 && code <= 90) || (code >= 97 && code <= 122) || code === 95) { + return this.parseIdentifier(c); + } else if (c === '"') { + return this.parseString(c, '"'); + } else if (c === "'") { + return this.parseString(c, "'"); + } else { + return this.error("Syntax Error"); + } + }; + + Tokenizer.prototype.changeNumberToIdentifier = function() { + var i, j, ref, results, token, v; + token = this.next(); + if ((token != null) && token.type === Token.TYPE_NUMBER) { + v = token.string_value.split("."); + results = []; + for (i = j = ref = v.length - 1; j >= 0; i = j += -1) { + if (v[i].length > 0) { + this.pushBack(new Token(this, Token.TYPE_IDENTIFIER, v[i])); + } + if (i > 0) { + results.push(this.pushBack(new Token(this, Token.TYPE_DOT, "."))); + } else { + results.push(void 0); + } + } + return results; + } else if ((token != null) && token.type === Token.TYPE_STRING) { + return this.pushBack(new Token(this, Token.TYPE_IDENTIFIER, token.value)); + } else { + return this.pushBack(token); + } + }; + + Tokenizer.prototype.parseDouble = function(c, d) { + if (this.index < this.input.length && this.input.charAt(this.index) === "=") { + this.nextChar(); + return new Token(this, d[1], c + "="); + } else { + return new Token(this, d[0], c); + } + }; + + Tokenizer.prototype.parseEquals = function(c) { + if (this.index < this.input.length && this.input.charAt(this.index) === "=") { + this.nextChar(); + return new Token(this, Token.TYPE_DOUBLE_EQUALS, "=="); + } else { + return new Token(this, Token.TYPE_EQUALS, "="); + } + }; + + Tokenizer.prototype.parseGreater = function(c) { + if (this.index < this.input.length && this.input.charAt(this.index) === "=") { + this.nextChar(); + return new Token(this, Token.TYPE_GREATER_OR_EQUALS, ">="); + } else { + return new Token(this, Token.TYPE_GREATER_OR_EQUALS, ">"); + } + }; + + Tokenizer.prototype.parseLower = function(c) { + if (this.index < this.input.length && this.input.charAt(this.index) === "=") { + this.nextChar(); + return new Token(this, Token.TYPE_LOWER_OR_EQUALS, "<="); + } else { + return new Token(this, Token.TYPE_LOWER, "<"); + } + }; + + Tokenizer.prototype.parseUnequals = function(c) { + if (this.index < this.input.length && this.input.charAt(this.index) === "=") { + this.nextChar(); + return new Token(this, Token.TYPE_UNEQUALS, "!="); + } else { + return this.error("Expected inequality !="); + } + }; + + Tokenizer.prototype.parseIdentifier = function(s) { + var c, code; + while (true) { + if (this.index >= this.input.length) { + return new Token(this, Token.TYPE_IDENTIFIER, s); + } + c = this.nextChar(); + code = c.charCodeAt(0); + if ((code >= 65 && code <= 90) || (code >= 97 && code <= 122) || code === 95 || (code >= 48 && code <= 57)) { + s += c; + } else { + this.rewind(); + return new Token(this, Token.TYPE_IDENTIFIER, s); + } + } + }; + + Tokenizer.prototype.parseNumber = function(s) { + var c, code, pointed; + pointed = false; + while (true) { + if (this.index >= this.input.length) { + return new Token(this, Token.TYPE_NUMBER, (pointed ? Number.parseFloat(s) : Number.parseInt(s)), s); + } + c = this.nextChar(); + code = c.charCodeAt(0); + if (c === "." && !pointed) { + pointed = true; + s += c; + } else if (code >= 48 && code <= 57) { + s += c; + } else { + this.rewind(); + return new Token(this, Token.TYPE_NUMBER, (pointed ? Number.parseFloat(s) : Number.parseInt(s)), s); + } + } + }; + + Tokenizer.prototype.parseString = function(s, close) { + var c, code, n; + if (close == null) { + close = '"'; + } + while (true) { + if (this.index >= this.input.length) { + return this.error("Unclosed string value"); + } + c = this.nextChar(); + code = c.charCodeAt(0); + if (c === close) { + n = this.nextChar(); + if (n === close) { + s += c; + } else { + this.rewind(); + s += c; + return new Token(this, Token.TYPE_STRING, s.substring(1, s.length - 1)); + } + } else { + s += c; + } + } + }; + + Tokenizer.prototype.error = function(s) { + throw s; + }; + + return Tokenizer; + +})(); diff --git a/static/js/music/musiceditor.coffee b/static/js/music/musiceditor.coffee new file mode 100644 index 00000000..5ab7a14a --- /dev/null +++ b/static/js/music/musiceditor.coffee @@ -0,0 +1,61 @@ +class @MusicEditor extends Manager + constructor:(@app)-> + @folder = "music" + @item = "music" + @list_change_event = "musiclist" + @get_item = "getMusic" + @use_thumbnails = true + + @extensions = ["mp3"] + @update_list = "updateMusicList" + @box_width = 192 + @box_height = 84 + + @init() + + openItem:(name)-> + super(name) + music = @app.project.getMusic(name) + if music? + music.play() + + fileDropped:(file)-> + console.info "processing #{file.name}" + reader = new FileReader() + reader.addEventListener "load",()=> + console.info "file read, size = "+ reader.result.length + return if reader.result.length>5000000 + audioContext = new AudioContext() + audioContext.decodeAudioData reader.result,(decoded)=> + console.info decoded + thumbnailer = new SoundThumbnailer(decoded,192,64,"hsl(200,80%,60%)") + name = file.name.split(".")[0] + name = @findNewFilename name,"getMusic" + + music = @app.project.createMusic(name,thumbnailer.canvas.toDataURL(),reader.result.length) + music.uploading = true + @setSelectedItem name + + r2 = new FileReader() + r2.addEventListener "load",()=> + music.local_url = r2.result + data = r2.result.split(",")[1] + @app.project.addPendingChange @ + + @app.client.sendRequest { + name: "write_project_file" + project: @app.project.id + file: "music/#{name}.mp3" + properties: {} + content: data + thumbnail: thumbnailer.canvas.toDataURL().split(",")[1] + },(msg)=> + console.info msg + @app.project.removePendingChange(@) + music.uploading = false + @app.project.updateMusicList() + @checkNameFieldActivation() + + r2.readAsDataURL(file) + + reader.readAsArrayBuffer(file) diff --git a/static/js/music/musiceditor.js b/static/js/music/musiceditor.js new file mode 100644 index 00000000..1c528feb --- /dev/null +++ b/static/js/music/musiceditor.js @@ -0,0 +1,81 @@ +var extend = function(child, parent) { for (var key in parent) { if (hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor(); child.__super__ = parent.prototype; return child; }, + hasProp = {}.hasOwnProperty; + +this.MusicEditor = (function(superClass) { + extend(MusicEditor, superClass); + + function MusicEditor(app) { + this.app = app; + this.folder = "music"; + this.item = "music"; + this.list_change_event = "musiclist"; + this.get_item = "getMusic"; + this.use_thumbnails = true; + this.extensions = ["mp3"]; + this.update_list = "updateMusicList"; + this.box_width = 192; + this.box_height = 84; + this.init(); + } + + MusicEditor.prototype.openItem = function(name) { + var music; + MusicEditor.__super__.openItem.call(this, name); + music = this.app.project.getMusic(name); + if (music != null) { + return music.play(); + } + }; + + MusicEditor.prototype.fileDropped = function(file) { + var reader; + console.info("processing " + file.name); + reader = new FileReader(); + reader.addEventListener("load", (function(_this) { + return function() { + var audioContext; + console.info("file read, size = " + reader.result.length); + if (reader.result.length > 5000000) { + return; + } + audioContext = new AudioContext(); + return audioContext.decodeAudioData(reader.result, function(decoded) { + var music, name, r2, thumbnailer; + console.info(decoded); + thumbnailer = new SoundThumbnailer(decoded, 192, 64, "hsl(200,80%,60%)"); + name = file.name.split(".")[0]; + name = _this.findNewFilename(name, "getMusic"); + music = _this.app.project.createMusic(name, thumbnailer.canvas.toDataURL(), reader.result.length); + music.uploading = true; + _this.setSelectedItem(name); + r2 = new FileReader(); + r2.addEventListener("load", function() { + var data; + music.local_url = r2.result; + data = r2.result.split(",")[1]; + _this.app.project.addPendingChange(_this); + return _this.app.client.sendRequest({ + name: "write_project_file", + project: _this.app.project.id, + file: "music/" + name + ".mp3", + properties: {}, + content: data, + thumbnail: thumbnailer.canvas.toDataURL().split(",")[1] + }, function(msg) { + console.info(msg); + _this.app.project.removePendingChange(_this); + music.uploading = false; + _this.app.project.updateMusicList(); + return _this.checkNameFieldActivation(); + }); + }); + return r2.readAsDataURL(file); + }); + }; + })(this)); + return reader.readAsArrayBuffer(file); + }; + + return MusicEditor; + +})(Manager); diff --git a/static/js/options/options.coffee b/static/js/options/options.coffee new file mode 100644 index 00000000..a651a9cc --- /dev/null +++ b/static/js/options/options.coffee @@ -0,0 +1,173 @@ +class @Options + constructor:(@app)-> + @textInput "projectoption-name",(value)=>@optionChanged("title",value) + + @project_slug_validator = new InputValidator document.getElementById("projectoption-slug"), + document.getElementById("project-slug-button"), + null, + (value)=> + @optionChanged("slug",value[0]) + + @project_code_validator = new InputValidator document.getElementById("projectoption-code"), + document.getElementById("project-code-button"), + null, + (value)=> + @optionChanged("code",value[0]) + + @selectInput "projectoption-orientation",(value)=>@orientationChanged(value) + @selectInput "projectoption-aspect",(value)=>@aspectChanged(value) + @selectInput "projectoption-graphics",(value)=>@graphicsChanged(value) + + @app.appui.setAction "add-project-user",()=> + @addProjectUser() + + document.getElementById("add-project-user-nick").addEventListener "keyup",(event)=> + if event.keyCode == 13 + @addProjectUser() + + textInput:(element,action)-> + e = document.getElementById(element) + e.addEventListener "input",(event)=>action(e.value) + + selectInput:(element,action)-> + e = document.getElementById(element) + e.addEventListener "change",(event)=>action(e.options[e.selectedIndex].value) + + projectOpened:()-> + PixelatedImage.setURL document.getElementById("projectoptions-icon"),@app.project.getFullURL()+"icon.png",160 + #document.getElementById("projectoptions-icon").setAttribute("src","#{@app.project.getFullURL()}icon.png") + document.getElementById("projectoption-name").value = @app.project.title + @project_slug_validator.set @app.project.slug + + document.getElementById("projectoption-slugprefix").innerText = location.origin.replace(".dev",".io")+"/#{@app.project.owner.nick}/" + document.getElementById("projectoption-orientation").value = @app.project.orientation + document.getElementById("projectoption-aspect").value = @app.project.aspect + document.getElementById("projectoption-graphics").value = @app.project.graphics or "M1" + @updateSecretCodeLine() + @updateUserList() + @app.project.addListener @ + + document.querySelector("#projectoptions-users").style.display = if @app.user.flags.guest then "none" else "block" + + updateSecretCodeLine:()-> + @project_code_validator.set @app.project.code + document.getElementById("projectoption-codeprefix").innerText = location.origin.replace(".dev",".io")+"/#{@app.project.owner.nick}/#{@app.project.slug}/" + + projectUpdate:(name)-> + if name == "spritelist" + icon = @app.project.getSprite("icon") + if icon? + icon.addImage document.getElementById("projectoptions-icon"),160 + + update:()-> + storage = @app.appui.displayByteSize @app.project.getSize() + document.getElementById("projectoption-storage-used").innerText = storage + + optionChanged:(name,value)-> + return if value.trim().length == 0 + switch name + when "title" + @app.project.setTitle value + when "slug" + if value != RegexLib.slugify value + value = RegexLib.slugify value + @project_slug_validator.set value + + return if value.length == 0 or value == @app.project.slug + @app.project.setSlug value + @updateSecretCodeLine() + + when "code" + @app.project.setCode value + + @app.client.sendRequest { + name: "set_project_option" + project: @app.project.id + option: name + value: value + },(msg)=> + if msg.name == "error" and msg.value? + switch name + when "title" + document.getElementById("projectoption-name").value = msg.value + @app.project.setTitle msg.value + when "slug" + @project_slug_validator.set msg.value + @app.project.setSlug msg.value + @updateSecretCodeLine() + + orientationChanged:(value)-> + @app.project.setOrientation(value) + @app.client.sendRequest { + name: "set_project_option" + project: @app.project.id + option: "orientation" + value: value + },(msg)=> + + aspectChanged:(value)-> + @app.project.setAspect(value) + @app.client.sendRequest { + name: "set_project_option" + project: @app.project.id + option: "aspect" + value: value + },(msg)=> + + graphicsChanged:(value)-> + @app.project.setGraphics(value) + @app.client.sendRequest { + name: "set_project_option" + project: @app.project.id + option: "graphics" + value: value + },(msg)=> + @app.appui.updateAllowedSections() + + setType:(type)-> + if type != @app.project.type + console.info("setting type to #{type}") + @app.project.setType(type) + @app.client.sendRequest { + name: "set_project_option" + project: @app.project.id + option: "type" + value: type + },(msg)=> + + addProjectUser:()-> + nick = document.getElementById("add-project-user-nick").value + if nick.trim().length>0 + @app.client.sendRequest { + name: "invite_to_project" + project: @app.project.id + user: nick + },(msg)=> + console.info msg + document.getElementById("add-project-user-nick").value = "" + + + updateUserList:()-> + div = document.getElementById("project-user-list") + div.innerHTML = "" + for user in @app.project.users + do (user)=> + e = document.createElement "div" + e.classList.add "user" + name = document.createElement "div" + name.classList.add "username" + name.innerHTML = user.nick+" "+if user.accepted then "" else "" + remove = document.createElement "div" + remove.classList.add "remove" + remove.innerHTML = " Remove" + remove.addEventListener "click",(event)=> + @app.client.sendRequest + name:"remove_project_user" + project: @app.project.id + user: user.nick + + e.appendChild remove + e.appendChild name + + div.appendChild e + return diff --git a/static/js/options/options.js b/static/js/options/options.js new file mode 100644 index 00000000..c01c5e73 --- /dev/null +++ b/static/js/options/options.js @@ -0,0 +1,252 @@ +this.Options = (function() { + function Options(app) { + this.app = app; + this.textInput("projectoption-name", (function(_this) { + return function(value) { + return _this.optionChanged("title", value); + }; + })(this)); + this.project_slug_validator = new InputValidator(document.getElementById("projectoption-slug"), document.getElementById("project-slug-button"), null, (function(_this) { + return function(value) { + return _this.optionChanged("slug", value[0]); + }; + })(this)); + this.project_code_validator = new InputValidator(document.getElementById("projectoption-code"), document.getElementById("project-code-button"), null, (function(_this) { + return function(value) { + return _this.optionChanged("code", value[0]); + }; + })(this)); + this.selectInput("projectoption-orientation", (function(_this) { + return function(value) { + return _this.orientationChanged(value); + }; + })(this)); + this.selectInput("projectoption-aspect", (function(_this) { + return function(value) { + return _this.aspectChanged(value); + }; + })(this)); + this.selectInput("projectoption-graphics", (function(_this) { + return function(value) { + return _this.graphicsChanged(value); + }; + })(this)); + this.app.appui.setAction("add-project-user", (function(_this) { + return function() { + return _this.addProjectUser(); + }; + })(this)); + document.getElementById("add-project-user-nick").addEventListener("keyup", (function(_this) { + return function(event) { + if (event.keyCode === 13) { + return _this.addProjectUser(); + } + }; + })(this)); + } + + Options.prototype.textInput = function(element, action) { + var e; + e = document.getElementById(element); + return e.addEventListener("input", (function(_this) { + return function(event) { + return action(e.value); + }; + })(this)); + }; + + Options.prototype.selectInput = function(element, action) { + var e; + e = document.getElementById(element); + return e.addEventListener("change", (function(_this) { + return function(event) { + return action(e.options[e.selectedIndex].value); + }; + })(this)); + }; + + Options.prototype.projectOpened = function() { + PixelatedImage.setURL(document.getElementById("projectoptions-icon"), this.app.project.getFullURL() + "icon.png", 160); + document.getElementById("projectoption-name").value = this.app.project.title; + this.project_slug_validator.set(this.app.project.slug); + document.getElementById("projectoption-slugprefix").innerText = location.origin.replace(".dev", ".io") + ("/" + this.app.project.owner.nick + "/"); + document.getElementById("projectoption-orientation").value = this.app.project.orientation; + document.getElementById("projectoption-aspect").value = this.app.project.aspect; + document.getElementById("projectoption-graphics").value = this.app.project.graphics || "M1"; + this.updateSecretCodeLine(); + this.updateUserList(); + this.app.project.addListener(this); + return document.querySelector("#projectoptions-users").style.display = this.app.user.flags.guest ? "none" : "block"; + }; + + Options.prototype.updateSecretCodeLine = function() { + this.project_code_validator.set(this.app.project.code); + return document.getElementById("projectoption-codeprefix").innerText = location.origin.replace(".dev", ".io") + ("/" + this.app.project.owner.nick + "/" + this.app.project.slug + "/"); + }; + + Options.prototype.projectUpdate = function(name) { + var icon; + if (name === "spritelist") { + icon = this.app.project.getSprite("icon"); + if (icon != null) { + return icon.addImage(document.getElementById("projectoptions-icon"), 160); + } + } + }; + + Options.prototype.update = function() { + var storage; + storage = this.app.appui.displayByteSize(this.app.project.getSize()); + return document.getElementById("projectoption-storage-used").innerText = storage; + }; + + Options.prototype.optionChanged = function(name, value) { + if (value.trim().length === 0) { + return; + } + switch (name) { + case "title": + this.app.project.setTitle(value); + break; + case "slug": + if (value !== RegexLib.slugify(value)) { + value = RegexLib.slugify(value); + this.project_slug_validator.set(value); + } + if (value.length === 0 || value === this.app.project.slug) { + return; + } + this.app.project.setSlug(value); + this.updateSecretCodeLine(); + break; + case "code": + this.app.project.setCode(value); + } + return this.app.client.sendRequest({ + name: "set_project_option", + project: this.app.project.id, + option: name, + value: value + }, (function(_this) { + return function(msg) { + if (msg.name === "error" && (msg.value != null)) { + switch (name) { + case "title": + document.getElementById("projectoption-name").value = msg.value; + return _this.app.project.setTitle(msg.value); + case "slug": + _this.project_slug_validator.set(msg.value); + _this.app.project.setSlug(msg.value); + return _this.updateSecretCodeLine(); + } + } + }; + })(this)); + }; + + Options.prototype.orientationChanged = function(value) { + this.app.project.setOrientation(value); + return this.app.client.sendRequest({ + name: "set_project_option", + project: this.app.project.id, + option: "orientation", + value: value + }, (function(_this) { + return function(msg) {}; + })(this)); + }; + + Options.prototype.aspectChanged = function(value) { + this.app.project.setAspect(value); + return this.app.client.sendRequest({ + name: "set_project_option", + project: this.app.project.id, + option: "aspect", + value: value + }, (function(_this) { + return function(msg) {}; + })(this)); + }; + + Options.prototype.graphicsChanged = function(value) { + this.app.project.setGraphics(value); + this.app.client.sendRequest({ + name: "set_project_option", + project: this.app.project.id, + option: "graphics", + value: value + }, (function(_this) { + return function(msg) {}; + })(this)); + return this.app.appui.updateAllowedSections(); + }; + + Options.prototype.setType = function(type) { + if (type !== this.app.project.type) { + console.info("setting type to " + type); + this.app.project.setType(type); + return this.app.client.sendRequest({ + name: "set_project_option", + project: this.app.project.id, + option: "type", + value: type + }, (function(_this) { + return function(msg) {}; + })(this)); + } + }; + + Options.prototype.addProjectUser = function() { + var nick; + nick = document.getElementById("add-project-user-nick").value; + if (nick.trim().length > 0) { + this.app.client.sendRequest({ + name: "invite_to_project", + project: this.app.project.id, + user: nick + }, (function(_this) { + return function(msg) { + return console.info(msg); + }; + })(this)); + return document.getElementById("add-project-user-nick").value = ""; + } + }; + + Options.prototype.updateUserList = function() { + var div, fn, i, len, ref, user; + div = document.getElementById("project-user-list"); + div.innerHTML = ""; + ref = this.app.project.users; + fn = (function(_this) { + return function(user) { + var e, name, remove; + e = document.createElement("div"); + e.classList.add("user"); + name = document.createElement("div"); + name.classList.add("username"); + name.innerHTML = user.nick + " " + (user.accepted ? "" : ""); + remove = document.createElement("div"); + remove.classList.add("remove"); + remove.innerHTML = " Remove"; + remove.addEventListener("click", function(event) { + return _this.app.client.sendRequest({ + name: "remove_project_user", + project: _this.app.project.id, + user: user.nick + }); + }); + e.appendChild(remove); + e.appendChild(name); + return div.appendChild(e); + }; + })(this); + for (i = 0, len = ref.length; i < len; i++) { + user = ref[i]; + fn(user); + } + }; + + return Options; + +})(); diff --git a/static/js/play/player.coffee b/static/js/play/player.coffee new file mode 100644 index 00000000..bdcadbd4 --- /dev/null +++ b/static/js/play/player.coffee @@ -0,0 +1,144 @@ +class @Player + constructor:()-> + #src = document.getElementById("code").innerText + @source_count = 0 + @sources = {} + @resources = resources + if resources.sources? + for source in resources.sources + @loadSource(source) + else + @sources.main = document.getElementById("code").innerText + @start() + + loadSource:(source)-> + req = new XMLHttpRequest() + req.onreadystatechange = (event) => + if req.readyState == XMLHttpRequest.DONE + if req.status == 200 + name = source.file.split(".")[0] + @sources[name] = req.responseText + @source_count++ + if @source_count>=resources.sources.length and not @runtime? + @start() + + req.open "GET",location.origin+location.pathname+"ms/#{source.file}?v=#{source.version}" + req.send() + + start:()-> + @runtime = new Runtime((if window.exported_project then "" else location.origin+location.pathname),@sources,resources,@) + + @client = new PlayerClient @ + + wrapper = document.getElementById("canvaswrapper") + wrapper.appendChild @runtime.screen.canvas + + window.addEventListener "resize",()=>@resize() + @resize() + #@runtime.start() + + touchStartListener = (event)=> + event.preventDefault() + #event.stopPropagation() + #event.stopImmediatePropagation() + @runtime.screen.canvas.removeEventListener "touchstart",touchStartListener + true + + touchListener = (event)=> + #event.preventDefault() + #event.stopPropagation() + #event.stopImmediatePropagation() + #@runtime.screen.canvas.removeEventListener "touchend",touchListener + @setFullScreen() + true + + @runtime.screen.canvas.addEventListener "touchstart",touchStartListener + @runtime.screen.canvas.addEventListener "touchend",touchListener + @runtime.start() + + window.addEventListener "message",(msg)=>@messageReceived(msg) + + @postMessage + name: "focus" + + resize:()-> + @runtime.screen.resize() + if @runtime.vm? + if not @runtime.vm.context.global.draw? + @runtime.update_memory = {} + for file,src of @runtime.sources + @runtime.updateSource(file,src,false) + + setFullScreen:()-> + if document.documentElement.webkitRequestFullScreen? and not document.webkitIsFullScreen + document.documentElement.webkitRequestFullScreen() + else if document.documentElement.requestFullScreen? and not document.fullScreen + document.documentElement.requestFullScreen() + else if document.documentElement.mozRequestFullScreen? and not document.mozFullScreen + document.documentElement.mozRequestFullScreen() + + if window.screen? and window.screen.orientation? and window.orientation in ["portrait","landscape"] + window.screen.orientation.lock(window.orientation).then(null, (error)-> + #console.error error + ) + + reportError:(err)-> + @postMessage + name:"error" + data: err + + log:(text)-> + @postMessage + name:"log" + data: text + + messageReceived:(msg)-> + data = msg.data + try + data = JSON.parse data + switch data.name + when "command" + res = @runtime.runCommand data.line + if not data.line.trim().startsWith("print") + @postMessage + name: "output" + data: res + id: data.id + + when "pause" + @runtime.stop() + + when "resume" + @runtime.resume() + + when "code_updated" + code = data.code + file = data.file.split(".")[0] + if @runtime.vm? + @runtime.vm.clearWarnings() + @runtime.updateSource(file,code,true) + + when "sprite_updated" + file = data.file + @runtime.updateSprite(file,0,data.data,data.properties) + + when "map_updated" + file = data.file + @runtime.updateMap(file,0,data.data) + + catch err + console.error err + + postMessage:(data)-> + if window != window.parent + window.parent.postMessage JSON.stringify(data),"*" + +window.addEventListener "load",()-> + window.player = new Player() + document.body.focus() + +if navigator.serviceWorker? and not window.skip_service_worker + navigator.serviceWorker.register('sw.js', { scope: location.pathname }).then((reg)-> + console.log('Registration succeeded. Scope is' + reg.scope) + ).catch (error)-> + console.log('Registration failed with' + error) diff --git a/static/js/play/player.js b/static/js/play/player.js new file mode 100644 index 00000000..3366508c --- /dev/null +++ b/static/js/play/player.js @@ -0,0 +1,187 @@ +this.Player = (function() { + function Player() { + var i, len, ref, source; + this.source_count = 0; + this.sources = {}; + this.resources = resources; + if (resources.sources != null) { + ref = resources.sources; + for (i = 0, len = ref.length; i < len; i++) { + source = ref[i]; + this.loadSource(source); + } + } else { + this.sources.main = document.getElementById("code").innerText; + this.start(); + } + } + + Player.prototype.loadSource = function(source) { + var req; + req = new XMLHttpRequest(); + req.onreadystatechange = (function(_this) { + return function(event) { + var name; + if (req.readyState === XMLHttpRequest.DONE) { + if (req.status === 200) { + name = source.file.split(".")[0]; + _this.sources[name] = req.responseText; + _this.source_count++; + if (_this.source_count >= resources.sources.length && (_this.runtime == null)) { + return _this.start(); + } + } + } + }; + })(this); + req.open("GET", location.origin + location.pathname + ("ms/" + source.file + "?v=" + source.version)); + return req.send(); + }; + + Player.prototype.start = function() { + var touchListener, touchStartListener, wrapper; + this.runtime = new Runtime((window.exported_project ? "" : location.origin + location.pathname), this.sources, resources, this); + this.client = new PlayerClient(this); + wrapper = document.getElementById("canvaswrapper"); + wrapper.appendChild(this.runtime.screen.canvas); + window.addEventListener("resize", (function(_this) { + return function() { + return _this.resize(); + }; + })(this)); + this.resize(); + touchStartListener = (function(_this) { + return function(event) { + event.preventDefault(); + _this.runtime.screen.canvas.removeEventListener("touchstart", touchStartListener); + return true; + }; + })(this); + touchListener = (function(_this) { + return function(event) { + _this.setFullScreen(); + return true; + }; + })(this); + this.runtime.screen.canvas.addEventListener("touchstart", touchStartListener); + this.runtime.screen.canvas.addEventListener("touchend", touchListener); + this.runtime.start(); + window.addEventListener("message", (function(_this) { + return function(msg) { + return _this.messageReceived(msg); + }; + })(this)); + return this.postMessage({ + name: "focus" + }); + }; + + Player.prototype.resize = function() { + var file, ref, results, src; + this.runtime.screen.resize(); + if (this.runtime.vm != null) { + if (this.runtime.vm.context.global.draw == null) { + this.runtime.update_memory = {}; + ref = this.runtime.sources; + results = []; + for (file in ref) { + src = ref[file]; + results.push(this.runtime.updateSource(file, src, false)); + } + return results; + } + } + }; + + Player.prototype.setFullScreen = function() { + var ref; + if ((document.documentElement.webkitRequestFullScreen != null) && !document.webkitIsFullScreen) { + document.documentElement.webkitRequestFullScreen(); + } else if ((document.documentElement.requestFullScreen != null) && !document.fullScreen) { + document.documentElement.requestFullScreen(); + } else if ((document.documentElement.mozRequestFullScreen != null) && !document.mozFullScreen) { + document.documentElement.mozRequestFullScreen(); + } + if ((window.screen != null) && (window.screen.orientation != null) && ((ref = window.orientation) === "portrait" || ref === "landscape")) { + return window.screen.orientation.lock(window.orientation).then(null, function(error) {}); + } + }; + + Player.prototype.reportError = function(err) { + return this.postMessage({ + name: "error", + data: err + }); + }; + + Player.prototype.log = function(text) { + return this.postMessage({ + name: "log", + data: text + }); + }; + + Player.prototype.messageReceived = function(msg) { + var code, data, err, file, res; + data = msg.data; + try { + data = JSON.parse(data); + switch (data.name) { + case "command": + res = this.runtime.runCommand(data.line); + if (!data.line.trim().startsWith("print")) { + return this.postMessage({ + name: "output", + data: res, + id: data.id + }); + } + break; + case "pause": + return this.runtime.stop(); + case "resume": + return this.runtime.resume(); + case "code_updated": + code = data.code; + file = data.file.split(".")[0]; + if (this.runtime.vm != null) { + this.runtime.vm.clearWarnings(); + } + return this.runtime.updateSource(file, code, true); + case "sprite_updated": + file = data.file; + return this.runtime.updateSprite(file, 0, data.data, data.properties); + case "map_updated": + file = data.file; + return this.runtime.updateMap(file, 0, data.data); + } + } catch (error1) { + err = error1; + return console.error(err); + } + }; + + Player.prototype.postMessage = function(data) { + if (window !== window.parent) { + return window.parent.postMessage(JSON.stringify(data), "*"); + } + }; + + return Player; + +})(); + +window.addEventListener("load", function() { + window.player = new Player(); + return document.body.focus(); +}); + +if ((navigator.serviceWorker != null) && !window.skip_service_worker) { + navigator.serviceWorker.register('sw.js', { + scope: location.pathname + }).then(function(reg) { + return console.log('Registration succeeded. Scope is' + reg.scope); + })["catch"](function(error) { + return console.log('Registration failed with' + error); + }); +} diff --git a/static/js/play/playerclient.coffee b/static/js/play/playerclient.coffee new file mode 100644 index 00000000..a62e8e96 --- /dev/null +++ b/static/js/play/playerclient.coffee @@ -0,0 +1,101 @@ +class @PlayerClient + constructor:(@player)-> + @pending_requests = {} + @request_id = 0 + @version_checked = false + @reconnect_delay = 1000 + try + @connect() + catch err + console.error err + + setInterval (()=>@sendRequest({name:"ping"}) if @socket?),30000 + + connect:()-> + @socket = new WebSocket(window.location.origin.replace("http","ws")) + + @socket.onmessage = (msg)=> + console.info "received: "+msg.data + try + msg = JSON.parse msg.data + if msg.request_id? + if @pending_requests[msg.request_id]? + @pending_requests[msg.request_id](msg) + delete @pending_requests[msg.request_id] + + #if msg.name == "code_updated" + # @player.runtime.updateSource(msg.file,msg.code,true) + + #if msg.name == "sprite_updated" + # @player.runtime.updateSprite(msg.sprite) + + if msg.name == "project_file_updated" + @player.runtime.projectFileUpdated(msg.type,msg.file,msg.version,msg.data,msg.properties) + + if msg.name == "project_file_deleted" + @player.runtime.projectFileDeleted(msg.type,msg.file) + + if msg.name == "project_options_updated" + @player.runtime.projectOptionsUpdated(msg) + + catch err + console.error err + + @socket.onopen = ()=> + #console.info "socket opened" + @reconnect_delay = 1000 + user = location.pathname.split("/")[1] + project = location.pathname.split("/")[2] + + @send + name: "listen_to_project" + user: user + project: project + + if not @version_checked + @version_checked = true + sprites = {} + maps = {} + sources = {} + for s in @player.resources.images + sprites[s.file.split(".")[0]] = s.version + for s in @player.resources.maps + maps[s.file.split(".")[0]] = s.version + for s in @player.resources.sources + sources[s.file.split(".")[0]] = s.version + + @sendRequest { + name: "get_file_versions" + user: user + project: project + },(msg)=> + for name,info of msg.data.sources + v = sources[name] + if not v? or v!= info.version + #console.info "updating #{name} to version #{version}" + @player.runtime.projectFileUpdated("ms",name,info.version,null,info.properties) + + for name,info of msg.data.sprites + v = sprites[name] + if not v? or v!= info.version + #console.info "updating #{name} to version #{version}" + @player.runtime.projectFileUpdated("sprites",name,info.version,null,info.properties) + + for name,info of msg.data.maps + v = maps[name] + if not v? or v!= info.version + #console.info "updating #{name} to version #{version}" + @player.runtime.projectFileUpdated("maps",name,info.version,null,info.properties) + + @socket.onclose = ()=> + #console.info "socket closed" + setTimeout (()=>@connect()),@reconnect_delay + @reconnect_delay += 1000 + + send:(data)-> + @socket.send JSON.stringify data + + sendRequest:(msg,callback)-> + msg.request_id = @request_id++ + @pending_requests[msg.request_id] = callback + @send msg diff --git a/static/js/play/playerclient.js b/static/js/play/playerclient.js new file mode 100644 index 00000000..6770ae49 --- /dev/null +++ b/static/js/play/playerclient.js @@ -0,0 +1,146 @@ +this.PlayerClient = (function() { + function PlayerClient(player) { + var err; + this.player = player; + this.pending_requests = {}; + this.request_id = 0; + this.version_checked = false; + this.reconnect_delay = 1000; + try { + this.connect(); + } catch (error) { + err = error; + console.error(err); + } + setInterval(((function(_this) { + return function() { + if (_this.socket != null) { + return _this.sendRequest({ + name: "ping" + }); + } + }; + })(this)), 30000); + } + + PlayerClient.prototype.connect = function() { + this.socket = new WebSocket(window.location.origin.replace("http", "ws")); + this.socket.onmessage = (function(_this) { + return function(msg) { + var err; + console.info("received: " + msg.data); + try { + msg = JSON.parse(msg.data); + if (msg.request_id != null) { + if (_this.pending_requests[msg.request_id] != null) { + _this.pending_requests[msg.request_id](msg); + delete _this.pending_requests[msg.request_id]; + } + } + if (msg.name === "project_file_updated") { + _this.player.runtime.projectFileUpdated(msg.type, msg.file, msg.version, msg.data, msg.properties); + } + if (msg.name === "project_file_deleted") { + _this.player.runtime.projectFileDeleted(msg.type, msg.file); + } + if (msg.name === "project_options_updated") { + return _this.player.runtime.projectOptionsUpdated(msg); + } + } catch (error) { + err = error; + return console.error(err); + } + }; + })(this); + this.socket.onopen = (function(_this) { + return function() { + var i, j, k, len, len1, len2, maps, project, ref, ref1, ref2, s, sources, sprites, user; + _this.reconnect_delay = 1000; + user = location.pathname.split("/")[1]; + project = location.pathname.split("/")[2]; + _this.send({ + name: "listen_to_project", + user: user, + project: project + }); + if (!_this.version_checked) { + _this.version_checked = true; + sprites = {}; + maps = {}; + sources = {}; + ref = _this.player.resources.images; + for (i = 0, len = ref.length; i < len; i++) { + s = ref[i]; + sprites[s.file.split(".")[0]] = s.version; + } + ref1 = _this.player.resources.maps; + for (j = 0, len1 = ref1.length; j < len1; j++) { + s = ref1[j]; + maps[s.file.split(".")[0]] = s.version; + } + ref2 = _this.player.resources.sources; + for (k = 0, len2 = ref2.length; k < len2; k++) { + s = ref2[k]; + sources[s.file.split(".")[0]] = s.version; + } + return _this.sendRequest({ + name: "get_file_versions", + user: user, + project: project + }, function(msg) { + var info, name, ref3, ref4, ref5, results, v; + ref3 = msg.data.sources; + for (name in ref3) { + info = ref3[name]; + v = sources[name]; + if ((v == null) || v !== info.version) { + _this.player.runtime.projectFileUpdated("ms", name, info.version, null, info.properties); + } + } + ref4 = msg.data.sprites; + for (name in ref4) { + info = ref4[name]; + v = sprites[name]; + if ((v == null) || v !== info.version) { + _this.player.runtime.projectFileUpdated("sprites", name, info.version, null, info.properties); + } + } + ref5 = msg.data.maps; + results = []; + for (name in ref5) { + info = ref5[name]; + v = maps[name]; + if ((v == null) || v !== info.version) { + results.push(_this.player.runtime.projectFileUpdated("maps", name, info.version, null, info.properties)); + } else { + results.push(void 0); + } + } + return results; + }); + } + }; + })(this); + return this.socket.onclose = (function(_this) { + return function() { + setTimeout((function() { + return _this.connect(); + }), _this.reconnect_delay); + return _this.reconnect_delay += 1000; + }; + })(this); + }; + + PlayerClient.prototype.send = function(data) { + return this.socket.send(JSON.stringify(data)); + }; + + PlayerClient.prototype.sendRequest = function(msg, callback) { + msg.request_id = this.request_id++; + this.pending_requests[msg.request_id] = callback; + return this.send(msg); + }; + + return PlayerClient; + +})(); diff --git a/static/js/project/project.coffee b/static/js/project/project.coffee new file mode 100644 index 00000000..fe4dbfc4 --- /dev/null +++ b/static/js/project/project.coffee @@ -0,0 +1,410 @@ +class @Project + constructor:(@app,data)-> + @id = data.id + @owner = data.owner + @accepted = data.accepted + @slug = data.slug + @code = data.code + @title = data.title + @description = data.description + @tags = data.tags + @public = data.public + @platforms = data.platforms + @controls = data.controls + @type = data.type + @orientation = data.orientation + @graphics = data.graphics or "M1" + @aspect = data.aspect + @users = data.users + + @file_types = ["source","sprite","map","asset","sound","music"] + for f in @file_types + @["#{f}_list"] = [] + @["#{f}_table"] = {} + + @locks = {} + @lock_time = {} + @friends = {} + + @url = location.origin+"/#{@owner.nick}/#{@slug}/" + @listeners = [] + setInterval (()=>@checkLocks()),1000 + + @pending_changes = [] + @onbeforeunload = null + + getFullURL:()-> + if @public + @url + else + location.origin+"/#{@owner.nick}/#{@slug}/#{@code}/" + + addListener:(lis)-> + @listeners.push lis + + notifyListeners:(change)-> + for lis in @listeners + lis.projectUpdate(change) + return + + load:()-> + @updateSourceList() + @updateSpriteList() + @updateMapList() + @updateSoundList() + @updateMusicList() + if @graphics == "M3D" + @updateAssetList() + @loadDoc() + + loadDoc:()-> + @app.doc_editor.setDoc "" + @app.readProjectFile @id,"doc/doc.md",(content)=> + @app.doc_editor.setDoc content + + updateFileList:(folder,callback)-> + @app.client.sendRequest { + name: "list_project_files" + project: @app.project.id + folder: folder + },(msg)=> + @[callback] msg.files + + updateSourceList:()-> @updateFileList "ms","setSourceList" + updateSpriteList:()-> @updateFileList "sprites","setSpriteList" + updateMapList:()-> @updateFileList "maps","setMapList" + updateSoundList:()-> @updateFileList "sounds","setSoundList" + updateMusicList:()-> @updateFileList "music","setMusicList" + updateAssetList:()-> @updateFileList "assets","setAssetList" + + lockFile:(file)-> + lock = @lock_time[file] + return if lock? and Date.now() + + fileLocked:(msg)-> + @locks[msg.file] = + user: msg.user + time: Date.now()+10000 + + @friends[msg.user] = Date.now()+120000 + + @notifyListeners("locks") + + isLocked:(file)-> + lock = @locks[file] + if lock? and Date.now() + change = false + for file,lock of @locks + if Date.now()>lock.time + delete @locks[file] + change = true + + for user,time of @friends + if Date.now()>time + delete @friends[user] + change = true + + @notifyListeners("locks") if change + + changeSpriteName:(old,name)-> + @sprite_table[name] = @sprite_table[old] + delete @sprite_table[old] + + for map in @map_list + changed = false + for i in [0..map.width-1] by 1 + for j in [0..map.height-1] by 1 + s = map.get(i,j) + if s? and s.length>0 + s = s.split(":") + if s[0] == old + changed = true + if s[1]? + map.set(i,j,name+":"+s[1]) + else + map.set(i,j,name) + + if changed + @app.client.sendRequest { + name: "write_project_file" + project: @app.project.id + file: "maps/#{map.name}.json" + content: map.save() + },(msg)=> + + return + + changeMapName:(old,name)-> + @map_table[name] = @map_table[old] + delete @map_table[old] + + fileUpdated:(msg)-> + if msg.file.indexOf("ms/") == 0 + name = msg.file.substring("ms/".length,msg.file.indexOf(".ms")) + if @source_table[name]? + @source_table[name].reload() + else + @updateSourceList() + else if msg.file == "doc/doc.md" + @app.doc_editor.setDoc msg.content + else if msg.file.indexOf("sprites/") == 0 + name = msg.file.substring("sprites/".length,msg.file.indexOf(".png")) + if @sprite_table[name]? + @sprite_table[name].reload ()=> + if name == @app.sprite_editor.selected_sprite + @app.sprite_editor.currentSpriteUpdated() + else + @updateSpriteList() + else if msg.file.indexOf("maps/") == 0 + name = msg.file.substring("maps/".length,msg.file.indexOf(".json")) + if @map_table[name]? + @map_table[name].loadFile() + else + @updateMapList() + + fileDeleted:(msg)-> + if msg.file.indexOf("ms/") == 0 + @updateSourceList() + else if msg.file.indexOf("sprites/") == 0 + @updateSpriteList() + else if msg.file.indexOf("maps/") == 0 + @updateMapList() + + optionsUpdated:(data)-> + @slug = data.slug + @title = data.title + @public = data.public + @platforms = data.platforms + @controls = data.controls + @type = data.type + @orientation = data.orientation + @aspect = data.aspect + + addSprite:(sprite)-> + s = new ProjectSprite @,sprite.file,null,null,sprite.properties,sprite.size + @sprite_table[s.name] = s + @sprite_list.push s + s + + getSprite:(name)-> + @sprite_table[name] + + createSprite:(width,height,name="sprite")-> + if @getSprite(name) + count = 2 + loop + filename = "#{name}#{count++}" + break if not @getSprite(filename)? + else + filename = name + + sprite = new ProjectSprite @,filename+".png",width,height + @sprite_table[sprite.name] = sprite + @sprite_list.push sprite + @notifyListeners "spritelist" + sprite + + addSource:(file)-> + s = new ProjectSource @,file.file,file.size + @source_table[s.name] = s + @source_list.push s + s + + getSource:(name)-> + @source_table[name] + + createSource:()-> + count = 1 + loop + filename = "source#{count++}" + break if not @getSource(filename)? + + source = new ProjectSource @,filename+".ms" + @source_table[source.name] = source + @source_list.push source + @notifyListeners "sourcelist" + source + + getFullSource:()-> + res = "" + for s in @source_list + res += s+"\n" + res + + setFileList:(list,target_list,target_table,get,add,notification)-> + li = [] + + for f in list + li.push f.file + + for i in [target_list.length-1..0] by -1 + s = target_list[i] + if li.indexOf(s.filename)<0 + target_list.splice i,1 + delete target_table[s.name] + + for s in list + if not @[get] s.file.split(".")[0] + @[add] s + + @notifyListeners notification + + setSourceList: (list) => @setFileList list,@source_list,@source_table,"getSource","addSource","sourcelist" + setSpriteList: (list) => @setFileList list,@sprite_list,@sprite_table,"getSprite","addSprite","spritelist" + setMapList: (list) => @setFileList list,@map_list,@map_table,"getMap","addMap","maplist" + setSoundList: (list) => @setFileList list,@sound_list,@sound_table,"getSound","addSound","soundlist" + setMusicList: (list) => @setFileList list,@music_list,@music_table,"getMusic","addMusic","musiclist" + setMapList: (list) => @setFileList list,@map_list,@map_table,"getMap","addMap","maplist" + setAssetList: (list) => @setFileList list,@asset_list,@asset_table,"getAsset","addAsset","assetlist" + + addMap:(file)-> + m = new ProjectMap @,file.file,file.size + @map_table[m.name] = m + @map_list.push m + m + + getMap:(name)-> + @map_table[name] + + addAsset:(file)-> + m = new ProjectAsset @,file.file,file.size + @asset_table[m.name] = m + @asset_list.push m + m + + getAsset:(name)-> + @asset_table[name] + + createMap:()-> + count = 1 + loop + filename = "map#{count++}" + break if not @getMap(filename)? + + m = @addMap + file: filename+".json" + size: 0 + + @notifyListeners "maplist" + m + + createSound:(name="sound",thumbnail,size)-> + if @getSound(name) + count = 2 + loop + filename = "#{name}#{count++}" + break if not @getSound(filename)? + else + filename = name + + sound = new ProjectSound @,filename+".wav",size + if thumbnail then sound.thumbnail_url = thumbnail + @sound_table[sound.name] = sound + @sound_list.push sound + @notifyListeners "soundlist" + sound + + addSound:(file)-> + m = new ProjectSound @,file.file,file.size + @sound_table[m.name] = m + @sound_list.push m + m + + getSound:(name)-> + @sound_table[name] + + createMusic:(name="music",thumbnail,size)-> + if @getMusic(name) + count = 2 + loop + filename = "#{name}#{count++}" + break if not @getMusic(filename)? + else + filename = name + + music = new ProjectMusic @,filename+".mp3",size + if thumbnail then music.thumbnail_url = thumbnail + @music_table[music.name] = music + @music_list.push music + @notifyListeners "musiclist" + music + + addMusic:(file)-> + m = new ProjectMusic @,file.file,file.size + @music_table[m.name] = m + @music_list.push m + m + + getMusic:(name)-> + @music_table[name] + + setTitle:(@title)-> + @notifyListeners "title" + + setSlug:(@slug)-> + @notifyListeners "slug" + + setCode:(@code)-> + @notifyListeners "code" + + setType:(@type)-> + + setOrientation:(@orientation)-> + #window.dispatchEvent(new Event('resize')) + + setAspect:(@aspect)-> + #window.dispatchEvent(new Event('resize')) + + setGraphics:(@graphics)-> + #window.dispatchEvent(new Event('resize')) + + addPendingChange:(item)-> + if @pending_changes.indexOf(item)<0 + @pending_changes.push item + if not @onbeforeunload? + @onbeforeunload = (event)=> + event.preventDefault() + event.returnValue = "You have pending unsaved changed." + @savePendingChanges() + return event.returnValue + + window.addEventListener "beforeunload",@onbeforeunload + + removePendingChange:(item)-> + index = @pending_changes.indexOf(item) + if index>=0 + @pending_changes.splice index,1 + if @pending_changes.length == 0 + if @onbeforeunload? + window.removeEventListener "beforeunload",@onbeforeunload + @onbeforeunload = null + + savePendingChanges:(callback)-> + if @pending_changes.length>0 + save = @pending_changes.splice(0,1)[0] + save.forceSave ()=> + @savePendingChanges(callback) + else + callback() if callback? + + getSize:()-> + size = 0 + + for type in @file_types + t = @["#{type}_list"] + for s in t + size += s.size + + size diff --git a/static/js/project/project.js b/static/js/project/project.js new file mode 100644 index 00000000..3b2e3628 --- /dev/null +++ b/static/js/project/project.js @@ -0,0 +1,623 @@ +var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; }; + +this.Project = (function() { + function Project(app, data) { + var f, k, len, ref; + this.app = app; + this.setAssetList = bind(this.setAssetList, this); + this.setMapList = bind(this.setMapList, this); + this.setMusicList = bind(this.setMusicList, this); + this.setSoundList = bind(this.setSoundList, this); + this.setMapList = bind(this.setMapList, this); + this.setSpriteList = bind(this.setSpriteList, this); + this.setSourceList = bind(this.setSourceList, this); + this.id = data.id; + this.owner = data.owner; + this.accepted = data.accepted; + this.slug = data.slug; + this.code = data.code; + this.title = data.title; + this.description = data.description; + this.tags = data.tags; + this["public"] = data["public"]; + this.platforms = data.platforms; + this.controls = data.controls; + this.type = data.type; + this.orientation = data.orientation; + this.graphics = data.graphics || "M1"; + this.aspect = data.aspect; + this.users = data.users; + this.file_types = ["source", "sprite", "map", "asset", "sound", "music"]; + ref = this.file_types; + for (k = 0, len = ref.length; k < len; k++) { + f = ref[k]; + this[f + "_list"] = []; + this[f + "_table"] = {}; + } + this.locks = {}; + this.lock_time = {}; + this.friends = {}; + this.url = location.origin + ("/" + this.owner.nick + "/" + this.slug + "/"); + this.listeners = []; + setInterval(((function(_this) { + return function() { + return _this.checkLocks(); + }; + })(this)), 1000); + this.pending_changes = []; + this.onbeforeunload = null; + } + + Project.prototype.getFullURL = function() { + if (this["public"]) { + return this.url; + } else { + return location.origin + ("/" + this.owner.nick + "/" + this.slug + "/" + this.code + "/"); + } + }; + + Project.prototype.addListener = function(lis) { + return this.listeners.push(lis); + }; + + Project.prototype.notifyListeners = function(change) { + var k, len, lis, ref; + ref = this.listeners; + for (k = 0, len = ref.length; k < len; k++) { + lis = ref[k]; + lis.projectUpdate(change); + } + }; + + Project.prototype.load = function() { + this.updateSourceList(); + this.updateSpriteList(); + this.updateMapList(); + this.updateSoundList(); + this.updateMusicList(); + if (this.graphics === "M3D") { + this.updateAssetList(); + } + return this.loadDoc(); + }; + + Project.prototype.loadDoc = function() { + this.app.doc_editor.setDoc(""); + return this.app.readProjectFile(this.id, "doc/doc.md", (function(_this) { + return function(content) { + return _this.app.doc_editor.setDoc(content); + }; + })(this)); + }; + + Project.prototype.updateFileList = function(folder, callback) { + return this.app.client.sendRequest({ + name: "list_project_files", + project: this.app.project.id, + folder: folder + }, (function(_this) { + return function(msg) { + return _this[callback](msg.files); + }; + })(this)); + }; + + Project.prototype.updateSourceList = function() { + return this.updateFileList("ms", "setSourceList"); + }; + + Project.prototype.updateSpriteList = function() { + return this.updateFileList("sprites", "setSpriteList"); + }; + + Project.prototype.updateMapList = function() { + return this.updateFileList("maps", "setMapList"); + }; + + Project.prototype.updateSoundList = function() { + return this.updateFileList("sounds", "setSoundList"); + }; + + Project.prototype.updateMusicList = function() { + return this.updateFileList("music", "setMusicList"); + }; + + Project.prototype.updateAssetList = function() { + return this.updateFileList("assets", "setAssetList"); + }; + + Project.prototype.lockFile = function(file) { + var lock; + lock = this.lock_time[file]; + if ((lock != null) && Date.now() < lock) { + return; + } + this.lock_time[file] = Date.now() + 2000; + console.info("locking file " + file); + return this.app.client.sendRequest({ + name: "lock_project_file", + project: this.id, + file: file + }, (function(_this) { + return function(msg) {}; + })(this)); + }; + + Project.prototype.fileLocked = function(msg) { + this.locks[msg.file] = { + user: msg.user, + time: Date.now() + 10000 + }; + this.friends[msg.user] = Date.now() + 120000; + return this.notifyListeners("locks"); + }; + + Project.prototype.isLocked = function(file) { + var lock; + lock = this.locks[file]; + if ((lock != null) && Date.now() < lock.time) { + return lock; + } else { + return false; + } + }; + + Project.prototype.checkLocks = function() { + var change, file, lock, ref, ref1, time, user; + change = false; + ref = this.locks; + for (file in ref) { + lock = ref[file]; + if (Date.now() > lock.time) { + delete this.locks[file]; + change = true; + } + } + ref1 = this.friends; + for (user in ref1) { + time = ref1[user]; + if (Date.now() > time) { + delete this.friends[user]; + change = true; + } + } + if (change) { + return this.notifyListeners("locks"); + } + }; + + Project.prototype.changeSpriteName = function(old, name) { + var changed, i, j, k, l, len, map, n, ref, ref1, ref2, s; + this.sprite_table[name] = this.sprite_table[old]; + delete this.sprite_table[old]; + ref = this.map_list; + for (k = 0, len = ref.length; k < len; k++) { + map = ref[k]; + changed = false; + for (i = l = 0, ref1 = map.width - 1; l <= ref1; i = l += 1) { + for (j = n = 0, ref2 = map.height - 1; n <= ref2; j = n += 1) { + s = map.get(i, j); + if ((s != null) && s.length > 0) { + s = s.split(":"); + if (s[0] === old) { + changed = true; + if (s[1] != null) { + map.set(i, j, name + ":" + s[1]); + } else { + map.set(i, j, name); + } + } + } + } + } + if (changed) { + this.app.client.sendRequest({ + name: "write_project_file", + project: this.app.project.id, + file: "maps/" + map.name + ".json", + content: map.save() + }, (function(_this) { + return function(msg) {}; + })(this)); + } + } + }; + + Project.prototype.changeMapName = function(old, name) { + this.map_table[name] = this.map_table[old]; + return delete this.map_table[old]; + }; + + Project.prototype.fileUpdated = function(msg) { + var name; + if (msg.file.indexOf("ms/") === 0) { + name = msg.file.substring("ms/".length, msg.file.indexOf(".ms")); + if (this.source_table[name] != null) { + return this.source_table[name].reload(); + } else { + return this.updateSourceList(); + } + } else if (msg.file === "doc/doc.md") { + return this.app.doc_editor.setDoc(msg.content); + } else if (msg.file.indexOf("sprites/") === 0) { + name = msg.file.substring("sprites/".length, msg.file.indexOf(".png")); + if (this.sprite_table[name] != null) { + return this.sprite_table[name].reload((function(_this) { + return function() { + if (name === _this.app.sprite_editor.selected_sprite) { + return _this.app.sprite_editor.currentSpriteUpdated(); + } + }; + })(this)); + } else { + return this.updateSpriteList(); + } + } else if (msg.file.indexOf("maps/") === 0) { + name = msg.file.substring("maps/".length, msg.file.indexOf(".json")); + if (this.map_table[name] != null) { + return this.map_table[name].loadFile(); + } else { + return this.updateMapList(); + } + } + }; + + Project.prototype.fileDeleted = function(msg) { + if (msg.file.indexOf("ms/") === 0) { + return this.updateSourceList(); + } else if (msg.file.indexOf("sprites/") === 0) { + return this.updateSpriteList(); + } else if (msg.file.indexOf("maps/") === 0) { + return this.updateMapList(); + } + }; + + Project.prototype.optionsUpdated = function(data) { + this.slug = data.slug; + this.title = data.title; + this["public"] = data["public"]; + this.platforms = data.platforms; + this.controls = data.controls; + this.type = data.type; + this.orientation = data.orientation; + return this.aspect = data.aspect; + }; + + Project.prototype.addSprite = function(sprite) { + var s; + s = new ProjectSprite(this, sprite.file, null, null, sprite.properties, sprite.size); + this.sprite_table[s.name] = s; + this.sprite_list.push(s); + return s; + }; + + Project.prototype.getSprite = function(name) { + return this.sprite_table[name]; + }; + + Project.prototype.createSprite = function(width, height, name) { + var count, filename, sprite; + if (name == null) { + name = "sprite"; + } + if (this.getSprite(name)) { + count = 2; + while (true) { + filename = "" + name + (count++); + if (this.getSprite(filename) == null) { + break; + } + } + } else { + filename = name; + } + sprite = new ProjectSprite(this, filename + ".png", width, height); + this.sprite_table[sprite.name] = sprite; + this.sprite_list.push(sprite); + this.notifyListeners("spritelist"); + return sprite; + }; + + Project.prototype.addSource = function(file) { + var s; + s = new ProjectSource(this, file.file, file.size); + this.source_table[s.name] = s; + this.source_list.push(s); + return s; + }; + + Project.prototype.getSource = function(name) { + return this.source_table[name]; + }; + + Project.prototype.createSource = function() { + var count, filename, source; + count = 1; + while (true) { + filename = "source" + (count++); + if (this.getSource(filename) == null) { + break; + } + } + source = new ProjectSource(this, filename + ".ms"); + this.source_table[source.name] = source; + this.source_list.push(source); + this.notifyListeners("sourcelist"); + return source; + }; + + Project.prototype.getFullSource = function() { + var k, len, ref, res, s; + res = ""; + ref = this.source_list; + for (k = 0, len = ref.length; k < len; k++) { + s = ref[k]; + res += s + "\n"; + } + return res; + }; + + Project.prototype.setFileList = function(list, target_list, target_table, get, add, notification) { + var f, i, k, l, len, len1, li, n, ref, s; + li = []; + for (k = 0, len = list.length; k < len; k++) { + f = list[k]; + li.push(f.file); + } + for (i = l = ref = target_list.length - 1; l >= 0; i = l += -1) { + s = target_list[i]; + if (li.indexOf(s.filename) < 0) { + target_list.splice(i, 1); + delete target_table[s.name]; + } + } + for (n = 0, len1 = list.length; n < len1; n++) { + s = list[n]; + if (!this[get](s.file.split(".")[0])) { + this[add](s); + } + } + return this.notifyListeners(notification); + }; + + Project.prototype.setSourceList = function(list) { + return this.setFileList(list, this.source_list, this.source_table, "getSource", "addSource", "sourcelist"); + }; + + Project.prototype.setSpriteList = function(list) { + return this.setFileList(list, this.sprite_list, this.sprite_table, "getSprite", "addSprite", "spritelist"); + }; + + Project.prototype.setMapList = function(list) { + return this.setFileList(list, this.map_list, this.map_table, "getMap", "addMap", "maplist"); + }; + + Project.prototype.setSoundList = function(list) { + return this.setFileList(list, this.sound_list, this.sound_table, "getSound", "addSound", "soundlist"); + }; + + Project.prototype.setMusicList = function(list) { + return this.setFileList(list, this.music_list, this.music_table, "getMusic", "addMusic", "musiclist"); + }; + + Project.prototype.setMapList = function(list) { + return this.setFileList(list, this.map_list, this.map_table, "getMap", "addMap", "maplist"); + }; + + Project.prototype.setAssetList = function(list) { + return this.setFileList(list, this.asset_list, this.asset_table, "getAsset", "addAsset", "assetlist"); + }; + + Project.prototype.addMap = function(file) { + var m; + m = new ProjectMap(this, file.file, file.size); + this.map_table[m.name] = m; + this.map_list.push(m); + return m; + }; + + Project.prototype.getMap = function(name) { + return this.map_table[name]; + }; + + Project.prototype.addAsset = function(file) { + var m; + m = new ProjectAsset(this, file.file, file.size); + this.asset_table[m.name] = m; + this.asset_list.push(m); + return m; + }; + + Project.prototype.getAsset = function(name) { + return this.asset_table[name]; + }; + + Project.prototype.createMap = function() { + var count, filename, m; + count = 1; + while (true) { + filename = "map" + (count++); + if (this.getMap(filename) == null) { + break; + } + } + m = this.addMap({ + file: filename + ".json", + size: 0 + }); + this.notifyListeners("maplist"); + return m; + }; + + Project.prototype.createSound = function(name, thumbnail, size) { + var count, filename, sound; + if (name == null) { + name = "sound"; + } + if (this.getSound(name)) { + count = 2; + while (true) { + filename = "" + name + (count++); + if (this.getSound(filename) == null) { + break; + } + } + } else { + filename = name; + } + sound = new ProjectSound(this, filename + ".wav", size); + if (thumbnail) { + sound.thumbnail_url = thumbnail; + } + this.sound_table[sound.name] = sound; + this.sound_list.push(sound); + this.notifyListeners("soundlist"); + return sound; + }; + + Project.prototype.addSound = function(file) { + var m; + m = new ProjectSound(this, file.file, file.size); + this.sound_table[m.name] = m; + this.sound_list.push(m); + return m; + }; + + Project.prototype.getSound = function(name) { + return this.sound_table[name]; + }; + + Project.prototype.createMusic = function(name, thumbnail, size) { + var count, filename, music; + if (name == null) { + name = "music"; + } + if (this.getMusic(name)) { + count = 2; + while (true) { + filename = "" + name + (count++); + if (this.getMusic(filename) == null) { + break; + } + } + } else { + filename = name; + } + music = new ProjectMusic(this, filename + ".mp3", size); + if (thumbnail) { + music.thumbnail_url = thumbnail; + } + this.music_table[music.name] = music; + this.music_list.push(music); + this.notifyListeners("musiclist"); + return music; + }; + + Project.prototype.addMusic = function(file) { + var m; + m = new ProjectMusic(this, file.file, file.size); + this.music_table[m.name] = m; + this.music_list.push(m); + return m; + }; + + Project.prototype.getMusic = function(name) { + return this.music_table[name]; + }; + + Project.prototype.setTitle = function(title) { + this.title = title; + return this.notifyListeners("title"); + }; + + Project.prototype.setSlug = function(slug) { + this.slug = slug; + return this.notifyListeners("slug"); + }; + + Project.prototype.setCode = function(code) { + this.code = code; + return this.notifyListeners("code"); + }; + + Project.prototype.setType = function(type1) { + this.type = type1; + }; + + Project.prototype.setOrientation = function(orientation) { + this.orientation = orientation; + }; + + Project.prototype.setAspect = function(aspect) { + this.aspect = aspect; + }; + + Project.prototype.setGraphics = function(graphics) { + this.graphics = graphics; + }; + + Project.prototype.addPendingChange = function(item) { + if (this.pending_changes.indexOf(item) < 0) { + this.pending_changes.push(item); + } + if (this.onbeforeunload == null) { + this.onbeforeunload = (function(_this) { + return function(event) { + event.preventDefault(); + event.returnValue = "You have pending unsaved changed."; + _this.savePendingChanges(); + return event.returnValue; + }; + })(this); + return window.addEventListener("beforeunload", this.onbeforeunload); + } + }; + + Project.prototype.removePendingChange = function(item) { + var index; + index = this.pending_changes.indexOf(item); + if (index >= 0) { + this.pending_changes.splice(index, 1); + } + if (this.pending_changes.length === 0) { + if (this.onbeforeunload != null) { + window.removeEventListener("beforeunload", this.onbeforeunload); + return this.onbeforeunload = null; + } + } + }; + + Project.prototype.savePendingChanges = function(callback) { + var save; + if (this.pending_changes.length > 0) { + save = this.pending_changes.splice(0, 1)[0]; + return save.forceSave((function(_this) { + return function() { + return _this.savePendingChanges(callback); + }; + })(this)); + } else { + if (callback != null) { + return callback(); + } + } + }; + + Project.prototype.getSize = function() { + var k, l, len, len1, ref, s, size, t, type; + size = 0; + ref = this.file_types; + for (k = 0, len = ref.length; k < len; k++) { + type = ref[k]; + t = this[type + "_list"]; + for (l = 0, len1 = t.length; l < len1; l++) { + s = t[l]; + size += s.size; + } + } + return size; + }; + + return Project; + +})(); diff --git a/static/js/project/projectasset.coffee b/static/js/project/projectasset.coffee new file mode 100644 index 00000000..16f0ba3a --- /dev/null +++ b/static/js/project/projectasset.coffee @@ -0,0 +1,17 @@ +class @ProjectAsset + constructor:(@project,@file,@size=0)-> + @name = @file.split(".")[0] + @ext = @file.split(".")[1] + @filename = @file + @file = "assets/#{@file}" + + getURL:()-> + @project.getFullURL()+@file + + getThumbnailURL:()-> + f = @file.split(".")[0]+".png" + f = f.replace("assets/","assets_th/") + @project.getFullURL()+f + + loaded:()-> + @project.notifyListeners @ diff --git a/static/js/project/projectasset.js b/static/js/project/projectasset.js new file mode 100644 index 00000000..f344100f --- /dev/null +++ b/static/js/project/projectasset.js @@ -0,0 +1,29 @@ +this.ProjectAsset = (function() { + function ProjectAsset(project, file, size) { + this.project = project; + this.file = file; + this.size = size != null ? size : 0; + this.name = this.file.split(".")[0]; + this.ext = this.file.split(".")[1]; + this.filename = this.file; + this.file = "assets/" + this.file; + } + + ProjectAsset.prototype.getURL = function() { + return this.project.getFullURL() + this.file; + }; + + ProjectAsset.prototype.getThumbnailURL = function() { + var f; + f = this.file.split(".")[0] + ".png"; + f = f.replace("assets/", "assets_th/"); + return this.project.getFullURL() + f; + }; + + ProjectAsset.prototype.loaded = function() { + return this.project.notifyListeners(this); + }; + + return ProjectAsset; + +})(); diff --git a/static/js/project/projectmap.coffee b/static/js/project/projectmap.coffee new file mode 100644 index 00000000..c4f936ef --- /dev/null +++ b/static/js/project/projectmap.coffee @@ -0,0 +1,46 @@ +class @ProjectMap extends MicroMap + constructor:(@project,@file,@size=0)-> + super 16,10,16,16,@project.sprite_table + @url = @project.getFullURL()+@file + @canvases = [] + @name = @file.substring 0,@file.length-5 + @filename = @file + @loadFile() + + addCanvas:(canvas)-> + @canvases.push canvas + @updateCanvas canvas + + updateCanvases:()-> + for c in @canvases + @updateCanvas c + return + + updateCanvas:(c)-> + r = Math.min(96/@width,96/@height) + w = r*@width + h = r*@height + c.width = 96 + c.height = 96 + + context = c.getContext "2d" + context.clearRect 0,0,c.width,c.height + context.imageSmoothingEnabled = false + context.drawImage @canvas,48-w/2,48-h/2,w,h + + loadFile:()-> + @project.app.client.sendRequest { + name: "read_project_file" + project: @project.id + file: "maps/#{@file}" + },(msg)=> + @load(msg.content,@project.sprite_table) + @update() + @updateCanvases() + if @project.app.map_editor.selected_map == @name + @project.app.map_editor.currentMapUpdated() + + rename:(@name)-> + @file = @name+".json" + @filename = @file + @url = @project.getFullURL()+@file diff --git a/static/js/project/projectmap.js b/static/js/project/projectmap.js new file mode 100644 index 00000000..2ba9cdab --- /dev/null +++ b/static/js/project/projectmap.js @@ -0,0 +1,72 @@ +var extend = function(child, parent) { for (var key in parent) { if (hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor(); child.__super__ = parent.prototype; return child; }, + hasProp = {}.hasOwnProperty; + +this.ProjectMap = (function(superClass) { + extend(ProjectMap, superClass); + + function ProjectMap(project, file, size) { + this.project = project; + this.file = file; + this.size = size != null ? size : 0; + ProjectMap.__super__.constructor.call(this, 16, 10, 16, 16, this.project.sprite_table); + this.url = this.project.getFullURL() + this.file; + this.canvases = []; + this.name = this.file.substring(0, this.file.length - 5); + this.filename = this.file; + this.loadFile(); + } + + ProjectMap.prototype.addCanvas = function(canvas) { + this.canvases.push(canvas); + return this.updateCanvas(canvas); + }; + + ProjectMap.prototype.updateCanvases = function() { + var c, i, len, ref; + ref = this.canvases; + for (i = 0, len = ref.length; i < len; i++) { + c = ref[i]; + this.updateCanvas(c); + } + }; + + ProjectMap.prototype.updateCanvas = function(c) { + var context, h, r, w; + r = Math.min(96 / this.width, 96 / this.height); + w = r * this.width; + h = r * this.height; + c.width = 96; + c.height = 96; + context = c.getContext("2d"); + context.clearRect(0, 0, c.width, c.height); + context.imageSmoothingEnabled = false; + return context.drawImage(this.canvas, 48 - w / 2, 48 - h / 2, w, h); + }; + + ProjectMap.prototype.loadFile = function() { + return this.project.app.client.sendRequest({ + name: "read_project_file", + project: this.project.id, + file: "maps/" + this.file + }, (function(_this) { + return function(msg) { + _this.load(msg.content, _this.project.sprite_table); + _this.update(); + _this.updateCanvases(); + if (_this.project.app.map_editor.selected_map === _this.name) { + return _this.project.app.map_editor.currentMapUpdated(); + } + }; + })(this)); + }; + + ProjectMap.prototype.rename = function(name) { + this.name = name; + this.file = this.name + ".json"; + this.filename = this.file; + return this.url = this.project.getFullURL() + this.file; + }; + + return ProjectMap; + +})(MicroMap); diff --git a/static/js/project/projectmusic.coffee b/static/js/project/projectmusic.coffee new file mode 100644 index 00000000..aa222cd4 --- /dev/null +++ b/static/js/project/projectmusic.coffee @@ -0,0 +1,37 @@ +class @ProjectMusic + constructor:(@project,@file,@size=0)-> + @name = @file.split(".")[0] + @ext = @file.split(".")[1] + @filename = @file + @file = "music/#{@file}" + + getURL:()-> + return @local_url if @local_url? + + @project.getFullURL()+@file + + getThumbnailURL:()-> + return @thumbnail_url if @thumbnail_url? + + f = @file.split(".")[0]+".png" + f = f.replace("music/","music_th/") + @project.getFullURL()+f + + loaded:()-> + @project.notifyListeners @ + + play:()-> + if not @audio? + @audio = new Audio(@getURL()) + @audio.loop = true + + funk = ()=> + document.body.removeEventListener "mousedown",funk + @audio.pause() + + document.body.addEventListener "mousedown",funk + @audio.play() + + rename:(@name)-> + @filename = @name + "." + @ext + @file = "music/"+@filename diff --git a/static/js/project/projectmusic.js b/static/js/project/projectmusic.js new file mode 100644 index 00000000..05297537 --- /dev/null +++ b/static/js/project/projectmusic.js @@ -0,0 +1,57 @@ +this.ProjectMusic = (function() { + function ProjectMusic(project, file, size) { + this.project = project; + this.file = file; + this.size = size != null ? size : 0; + this.name = this.file.split(".")[0]; + this.ext = this.file.split(".")[1]; + this.filename = this.file; + this.file = "music/" + this.file; + } + + ProjectMusic.prototype.getURL = function() { + if (this.local_url != null) { + return this.local_url; + } + return this.project.getFullURL() + this.file; + }; + + ProjectMusic.prototype.getThumbnailURL = function() { + var f; + if (this.thumbnail_url != null) { + return this.thumbnail_url; + } + f = this.file.split(".")[0] + ".png"; + f = f.replace("music/", "music_th/"); + return this.project.getFullURL() + f; + }; + + ProjectMusic.prototype.loaded = function() { + return this.project.notifyListeners(this); + }; + + ProjectMusic.prototype.play = function() { + var funk; + if (this.audio == null) { + this.audio = new Audio(this.getURL()); + this.audio.loop = true; + } + funk = (function(_this) { + return function() { + document.body.removeEventListener("mousedown", funk); + return _this.audio.pause(); + }; + })(this); + document.body.addEventListener("mousedown", funk); + return this.audio.play(); + }; + + ProjectMusic.prototype.rename = function(name) { + this.name = name; + this.filename = this.name + "." + this.ext; + return this.file = "music/" + this.filename; + }; + + return ProjectMusic; + +})(); diff --git a/static/js/project/projectsound.coffee b/static/js/project/projectsound.coffee new file mode 100644 index 00000000..9167dba4 --- /dev/null +++ b/static/js/project/projectsound.coffee @@ -0,0 +1,35 @@ +class @ProjectSound + constructor:(@project,@file,@size=0)-> + @name = @file.split(".")[0] + @ext = @file.split(".")[1] + @filename = @file + @file = "sounds/#{@file}" + + getURL:()-> + return @local_url if @local_url? + + @project.getFullURL()+@file + + getThumbnailURL:()-> + return @thumbnail_url if @thumbnail_url? + + f = @file.split(".")[0]+".png" + f = f.replace("sounds/","sounds_th/") + @project.getFullURL()+f + + loaded:()-> + @project.notifyListeners @ + + play:()-> + audio = new Audio(@getURL()) + audio.play() + + funk = ()-> + audio.pause() + document.body.removeEventListener "mousedown",funk + + document.body.addEventListener "mousedown",funk + + rename:(@name)-> + @filename = @name + "." + @ext + @file = "sounds/"+@filename diff --git a/static/js/project/projectsound.js b/static/js/project/projectsound.js new file mode 100644 index 00000000..97be633e --- /dev/null +++ b/static/js/project/projectsound.js @@ -0,0 +1,52 @@ +this.ProjectSound = (function() { + function ProjectSound(project, file, size) { + this.project = project; + this.file = file; + this.size = size != null ? size : 0; + this.name = this.file.split(".")[0]; + this.ext = this.file.split(".")[1]; + this.filename = this.file; + this.file = "sounds/" + this.file; + } + + ProjectSound.prototype.getURL = function() { + if (this.local_url != null) { + return this.local_url; + } + return this.project.getFullURL() + this.file; + }; + + ProjectSound.prototype.getThumbnailURL = function() { + var f; + if (this.thumbnail_url != null) { + return this.thumbnail_url; + } + f = this.file.split(".")[0] + ".png"; + f = f.replace("sounds/", "sounds_th/"); + return this.project.getFullURL() + f; + }; + + ProjectSound.prototype.loaded = function() { + return this.project.notifyListeners(this); + }; + + ProjectSound.prototype.play = function() { + var audio, funk; + audio = new Audio(this.getURL()); + audio.play(); + funk = function() { + audio.pause(); + return document.body.removeEventListener("mousedown", funk); + }; + return document.body.addEventListener("mousedown", funk); + }; + + ProjectSound.prototype.rename = function(name) { + this.name = name; + this.filename = this.name + "." + this.ext; + return this.file = "sounds/" + this.filename; + }; + + return ProjectSound; + +})(); diff --git a/static/js/project/projectsource.coffee b/static/js/project/projectsource.coffee new file mode 100644 index 00000000..907fc4c4 --- /dev/null +++ b/static/js/project/projectsource.coffee @@ -0,0 +1,20 @@ +class @ProjectSource + constructor:(@project,@file,@size=0)-> + @name = @file.substring 0,@file.length-3 + @filename = @file + @file = "ms/#{@file}" + @content = "" + @reload() + + reload:(callback)-> + @project.app.client.sendRequest { + name: "read_project_file" + project: @project.id + file: @file + },(msg)=> + @content = msg.content + @loaded() + callback() if callback? + + loaded:()-> + @project.notifyListeners @ diff --git a/static/js/project/projectsource.js b/static/js/project/projectsource.js new file mode 100644 index 00000000..4b7b86e3 --- /dev/null +++ b/static/js/project/projectsource.js @@ -0,0 +1,35 @@ +this.ProjectSource = (function() { + function ProjectSource(project, file, size) { + this.project = project; + this.file = file; + this.size = size != null ? size : 0; + this.name = this.file.substring(0, this.file.length - 3); + this.filename = this.file; + this.file = "ms/" + this.file; + this.content = ""; + this.reload(); + } + + ProjectSource.prototype.reload = function(callback) { + return this.project.app.client.sendRequest({ + name: "read_project_file", + project: this.project.id, + file: this.file + }, (function(_this) { + return function(msg) { + _this.content = msg.content; + _this.loaded(); + if (callback != null) { + return callback(); + } + }; + })(this)); + }; + + ProjectSource.prototype.loaded = function() { + return this.project.notifyListeners(this); + }; + + return ProjectSource; + +})(); diff --git a/static/js/project/projectsprite.coffee b/static/js/project/projectsprite.coffee new file mode 100644 index 00000000..dda0d5f6 --- /dev/null +++ b/static/js/project/projectsprite.coffee @@ -0,0 +1,57 @@ +class @ProjectSprite extends Sprite + constructor:(@project,name,width,height,properties,@size=0)-> + if width? and height? + super width,height,properties + @file = name + @url = @project.getFullURL()+"sprites/"+@file + else + @file = name + @url = @project.getFullURL()+"sprites/"+@file + super @url,undefined,properties + + @name = @file.substring 0,@file.length-4 + @filename = @file + @images = [] + @load_listeners = [] + + addLoadListener:(listener)-> + if @ready + listener() + else + @load_listeners.push listener + + addImage:(img,size)-> + throw "Size must be defined" if not size? + @images.push + image: img + size: size + + updated:(url=@project.getFullURL()+"sprites/"+@file+"?v=#{Date.now()}")-> + for i in @images + PixelatedImage.setURL i.image,url,i.size + return + + reload:(callback)-> + url=@project.getFullURL()+"sprites/"+@file+"?v=#{Date.now()}" + img = new Image + img.crossOrigin = "Anonymous" + img.src = url + img.onload = ()=> + @load img + @updated(url) + callback() if callback? + + loaded:()-> + for m in @project.map_list + m.update() + m.updateCanvases() + + for l in @load_listeners + l() + @project.notifyListeners @ + return + + rename:(@name)-> + @file = @name + ".png" + @filename = @file + @url = @project.getFullURL()+"sprites/"+@file diff --git a/static/js/project/projectsprite.js b/static/js/project/projectsprite.js new file mode 100644 index 00000000..e8931996 --- /dev/null +++ b/static/js/project/projectsprite.js @@ -0,0 +1,97 @@ +var extend = function(child, parent) { for (var key in parent) { if (hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor(); child.__super__ = parent.prototype; return child; }, + hasProp = {}.hasOwnProperty; + +this.ProjectSprite = (function(superClass) { + extend(ProjectSprite, superClass); + + function ProjectSprite(project, name, width, height, properties, size1) { + this.project = project; + this.size = size1 != null ? size1 : 0; + if ((width != null) && (height != null)) { + ProjectSprite.__super__.constructor.call(this, width, height, properties); + this.file = name; + this.url = this.project.getFullURL() + "sprites/" + this.file; + } else { + this.file = name; + this.url = this.project.getFullURL() + "sprites/" + this.file; + ProjectSprite.__super__.constructor.call(this, this.url, void 0, properties); + } + this.name = this.file.substring(0, this.file.length - 4); + this.filename = this.file; + this.images = []; + this.load_listeners = []; + } + + ProjectSprite.prototype.addLoadListener = function(listener) { + if (this.ready) { + return listener(); + } else { + return this.load_listeners.push(listener); + } + }; + + ProjectSprite.prototype.addImage = function(img, size) { + if (size == null) { + throw "Size must be defined"; + } + return this.images.push({ + image: img, + size: size + }); + }; + + ProjectSprite.prototype.updated = function(url) { + var i, j, len, ref; + if (url == null) { + url = this.project.getFullURL() + "sprites/" + this.file + ("?v=" + (Date.now())); + } + ref = this.images; + for (j = 0, len = ref.length; j < len; j++) { + i = ref[j]; + PixelatedImage.setURL(i.image, url, i.size); + } + }; + + ProjectSprite.prototype.reload = function(callback) { + var img, url; + url = this.project.getFullURL() + "sprites/" + this.file + ("?v=" + (Date.now())); + img = new Image; + img.crossOrigin = "Anonymous"; + img.src = url; + return img.onload = (function(_this) { + return function() { + _this.load(img); + _this.updated(url); + if (callback != null) { + return callback(); + } + }; + })(this); + }; + + ProjectSprite.prototype.loaded = function() { + var j, k, l, len, len1, m, ref, ref1; + ref = this.project.map_list; + for (j = 0, len = ref.length; j < len; j++) { + m = ref[j]; + m.update(); + m.updateCanvases(); + } + ref1 = this.load_listeners; + for (k = 0, len1 = ref1.length; k < len1; k++) { + l = ref1[k]; + l(); + } + this.project.notifyListeners(this); + }; + + ProjectSprite.prototype.rename = function(name1) { + this.name = name1; + this.file = this.name + ".png"; + this.filename = this.file; + return this.url = this.project.getFullURL() + "sprites/" + this.file; + }; + + return ProjectSprite; + +})(Sprite); diff --git a/static/js/publish/appbuild.coffee b/static/js/publish/appbuild.coffee new file mode 100644 index 00000000..21be4ee1 --- /dev/null +++ b/static/js/publish/appbuild.coffee @@ -0,0 +1,56 @@ +class @AppBuild + constructor:(@app,@target)-> + @button = document.querySelector "##{@target}-export .publish-button" + @button.addEventListener "click",()=>@buttonClicked() + + loadProject:(@project)-> + if @interval? + clearInterval @interval + @interval = null + @updateBuildStatus() + + buttonClicked:()-> + if not @build? + @app.client.sendRequest { + name:"build_project" + project: @project.id + target: @target + },(msg)=> + @handleBuildStatus(msg.build) + else if @build.progress == 100 + loc = "/#{@project.owner.nick}/#{@project.slug}/" + if not @project.public + loc += @project.code+"/" + console.info loc+"download/#{@target}/"+"?v=#{@project.last_modified}" + window.location = loc+"download/#{@target}/"+"?v=#{@project.last_modified}" + + updateBuildStatus:()-> + @app.client.sendRequest { + name:"get_build_status" + project: @project.id + target: @target + },(msg)=> + if msg.active_target + document.querySelector("#publish-box-#{@target}").style.display = "block" + else + document.querySelector("#publish-box-#{@target}").style.display = "none" + @handleBuildStatus(msg.build) + + handleBuildStatus:(@build)-> + if not @build? + @button.style.background = "hsl(160,50%,50%)" + @button.innerHTML = ' '+@app.translator.get "Build" + if @interval? + clearInterval @interval + @interval = null + else if @build.progress<100 + @setBuildProgress(@build.status_text,@build.progress) + if not @interval? + @interval = setInterval (()=>@updateBuildStatus()),1000 + else + @button.style.background = "hsl(200,50%,50%)" + @button.innerHTML = ' '+@app.translator.get "Download" + + setBuildProgress:(text,progress)-> + @button.style.background = "linear-gradient(90deg,hsl(30,50%,50%) 0%,hsl(30,50%,50%) #{progress}%,rgba(255,255,255,.1) #{progress}%)" + @button.innerHTML = ' '+text diff --git a/static/js/publish/appbuild.js b/static/js/publish/appbuild.js new file mode 100644 index 00000000..ff185910 --- /dev/null +++ b/static/js/publish/appbuild.js @@ -0,0 +1,92 @@ +this.AppBuild = (function() { + function AppBuild(app, target) { + this.app = app; + this.target = target; + this.button = document.querySelector("#" + this.target + "-export .publish-button"); + this.button.addEventListener("click", (function(_this) { + return function() { + return _this.buttonClicked(); + }; + })(this)); + } + + AppBuild.prototype.loadProject = function(project) { + this.project = project; + if (this.interval != null) { + clearInterval(this.interval); + this.interval = null; + } + return this.updateBuildStatus(); + }; + + AppBuild.prototype.buttonClicked = function() { + var loc; + if (this.build == null) { + return this.app.client.sendRequest({ + name: "build_project", + project: this.project.id, + target: this.target + }, (function(_this) { + return function(msg) { + return _this.handleBuildStatus(msg.build); + }; + })(this)); + } else if (this.build.progress === 100) { + loc = "/" + this.project.owner.nick + "/" + this.project.slug + "/"; + if (!this.project["public"]) { + loc += this.project.code + "/"; + } + console.info(loc + ("download/" + this.target + "/") + ("?v=" + this.project.last_modified)); + return window.location = loc + ("download/" + this.target + "/") + ("?v=" + this.project.last_modified); + } + }; + + AppBuild.prototype.updateBuildStatus = function() { + return this.app.client.sendRequest({ + name: "get_build_status", + project: this.project.id, + target: this.target + }, (function(_this) { + return function(msg) { + if (msg.active_target) { + document.querySelector("#publish-box-" + _this.target).style.display = "block"; + } else { + document.querySelector("#publish-box-" + _this.target).style.display = "none"; + } + return _this.handleBuildStatus(msg.build); + }; + })(this)); + }; + + AppBuild.prototype.handleBuildStatus = function(build) { + this.build = build; + if (this.build == null) { + this.button.style.background = "hsl(160,50%,50%)"; + this.button.innerHTML = ' ' + this.app.translator.get("Build"); + if (this.interval != null) { + clearInterval(this.interval); + return this.interval = null; + } + } else if (this.build.progress < 100) { + this.setBuildProgress(this.build.status_text, this.build.progress); + if (this.interval == null) { + return this.interval = setInterval(((function(_this) { + return function() { + return _this.updateBuildStatus(); + }; + })(this)), 1000); + } + } else { + this.button.style.background = "hsl(200,50%,50%)"; + return this.button.innerHTML = ' ' + this.app.translator.get("Download"); + } + }; + + AppBuild.prototype.setBuildProgress = function(text, progress) { + this.button.style.background = "linear-gradient(90deg,hsl(30,50%,50%) 0%,hsl(30,50%,50%) " + progress + "%,rgba(255,255,255,.1) " + progress + "%)"; + return this.button.innerHTML = ' ' + text; + }; + + return AppBuild; + +})(); diff --git a/static/js/publish/publish.coffee b/static/js/publish/publish.coffee new file mode 100644 index 00000000..d91dadbf --- /dev/null +++ b/static/js/publish/publish.coffee @@ -0,0 +1,134 @@ +class @Publish + constructor:(@app)-> + @app.appui.setAction "publish-button",()=> + @setProjectPublic(true) + + @app.appui.setAction "unpublish-button",()=> + @setProjectPublic(false) + + @tags_validator = new InputValidator document.getElementById("publish-add-tags"), + document.getElementById("publish-add-tags-button"), + null, + (value)=> + @addTags(value[0]) + + @description_save = 0 + document.querySelector("#publish-box-textarea").addEventListener "input",()=> + @description_save = Date.now()+2000 + + setInterval (()=>@checkDescriptionSave()),1000 + + @builders = [] + @builders.push new AppBuild(@app,"android") + @builders.push new AppBuild(@app,"windows") + @builders.push new AppBuild(@app,"macos") + @builders.push new AppBuild(@app,"linux") + @builders.push new AppBuild(@app,"raspbian") + + loadProject:(project)-> + if project.public + document.getElementById("publish-box").style.display = "none" + document.getElementById("unpublish-box").style.display = "block" + else + document.getElementById("publish-box").style.display = "block" + document.getElementById("unpublish-box").style.display = "none" + + document.getElementById("publish-validate-first").style.display = if @app.user.flags["validated"] then "none" else "block" + + document.querySelector("#publish-box-textarea").value = project.description + @updateTags() + project.addListener @ + + if @app.user.flags["validated"] + document.querySelector("#publish-box .publish-button").classList.remove "disabled" + else + document.querySelector("#publish-box .publish-button").classList.add "disabled" + + b = document.querySelector("#html-export .publish-button") + b.onclick = ()=> + loc = "/#{project.owner.nick}/#{project.slug}/" + if not project.public + loc += project.code+"/" + window.location = loc+"publish/html/" + + for build in @builders + build.loadProject(project) + return + + updateTags:()-> + list = document.getElementById("publish-tag-list") + list.innerHTML = "" + for t in @app.project.tags + do (t)=> + e = document.createElement "div" + t = t.replace(/[<>&;"']/g,"") + span = document.createElement "span" + span.innerText = t + e.appendChild span + i = document.createElement "i" + i.classList.add "fa" + i.classList.add "fa-times-circle" + i.addEventListener "click",()=> + @removeTag(t) + e.appendChild i + list.appendChild e + return + + removeTag:(t)-> + tags = @app.project.tags + if tags.indexOf(t)>=0 + tags.splice(tags.indexOf(t),1) + @app.client.sendRequest { + name:"set_project_tags" + project: @app.project.id + tags: tags + },(msg)=> + @updateTags() + + addTags:(value)-> + tags = @app.project.tags + value = value.toLowerCase().split(",") + change = false + for v in value + v = v.trim() + if tags.indexOf(v)<0 + change = true + tags.push(v) + if change + @app.client.sendRequest { + name:"set_project_tags" + project: @app.project.id + tags: tags + },(msg)=> + @updateTags() + @tags_validator.reset() + + projectUpdate:(type)-> + switch type + when "tags" + @updateTags() + + checkDescriptionSave:(force=false)-> + if @description_save>0 and (Date.now()>@description_save or force) + @description_save = 0 + @app.project.description = document.querySelector("#publish-box-textarea").value + @app.client.sendRequest { + name:"set_project_option" + project: @app.project.id + option: "description" + value: document.querySelector("#publish-box-textarea").value + },(msg)=> + + setProjectPublic:(pub)-> + return if pub and not @app.user.flags["validated"] + @checkDescriptionSave(true) + if @app.project? + @app.client.sendRequest { + name:"set_project_public" + project: @app.project.id + public: pub + },(msg)=> + if msg.public? + @app.project.public = msg.public + @app.project.notifyListeners("public") + @loadProject(@app.project) diff --git a/static/js/publish/publish.js b/static/js/publish/publish.js new file mode 100644 index 00000000..cfc77a2a --- /dev/null +++ b/static/js/publish/publish.js @@ -0,0 +1,196 @@ +this.Publish = (function() { + function Publish(app) { + this.app = app; + this.app.appui.setAction("publish-button", (function(_this) { + return function() { + return _this.setProjectPublic(true); + }; + })(this)); + this.app.appui.setAction("unpublish-button", (function(_this) { + return function() { + return _this.setProjectPublic(false); + }; + })(this)); + this.tags_validator = new InputValidator(document.getElementById("publish-add-tags"), document.getElementById("publish-add-tags-button"), null, (function(_this) { + return function(value) { + return _this.addTags(value[0]); + }; + })(this)); + this.description_save = 0; + document.querySelector("#publish-box-textarea").addEventListener("input", (function(_this) { + return function() { + return _this.description_save = Date.now() + 2000; + }; + })(this)); + setInterval(((function(_this) { + return function() { + return _this.checkDescriptionSave(); + }; + })(this)), 1000); + this.builders = []; + this.builders.push(new AppBuild(this.app, "android")); + this.builders.push(new AppBuild(this.app, "windows")); + this.builders.push(new AppBuild(this.app, "macos")); + this.builders.push(new AppBuild(this.app, "linux")); + this.builders.push(new AppBuild(this.app, "raspbian")); + } + + Publish.prototype.loadProject = function(project) { + var b, build, j, len, ref; + if (project["public"]) { + document.getElementById("publish-box").style.display = "none"; + document.getElementById("unpublish-box").style.display = "block"; + } else { + document.getElementById("publish-box").style.display = "block"; + document.getElementById("unpublish-box").style.display = "none"; + } + document.getElementById("publish-validate-first").style.display = this.app.user.flags["validated"] ? "none" : "block"; + document.querySelector("#publish-box-textarea").value = project.description; + this.updateTags(); + project.addListener(this); + if (this.app.user.flags["validated"]) { + document.querySelector("#publish-box .publish-button").classList.remove("disabled"); + } else { + document.querySelector("#publish-box .publish-button").classList.add("disabled"); + } + b = document.querySelector("#html-export .publish-button"); + b.onclick = (function(_this) { + return function() { + var loc; + loc = "/" + project.owner.nick + "/" + project.slug + "/"; + if (!project["public"]) { + loc += project.code + "/"; + } + return window.location = loc + "publish/html/"; + }; + })(this); + ref = this.builders; + for (j = 0, len = ref.length; j < len; j++) { + build = ref[j]; + build.loadProject(project); + } + }; + + Publish.prototype.updateTags = function() { + var fn, j, len, list, ref, t; + list = document.getElementById("publish-tag-list"); + list.innerHTML = ""; + ref = this.app.project.tags; + fn = (function(_this) { + return function(t) { + var e, i, span; + e = document.createElement("div"); + t = t.replace(/[<>&;"']/g, ""); + span = document.createElement("span"); + span.innerText = t; + e.appendChild(span); + i = document.createElement("i"); + i.classList.add("fa"); + i.classList.add("fa-times-circle"); + i.addEventListener("click", function() { + return _this.removeTag(t); + }); + e.appendChild(i); + return list.appendChild(e); + }; + })(this); + for (j = 0, len = ref.length; j < len; j++) { + t = ref[j]; + fn(t); + } + }; + + Publish.prototype.removeTag = function(t) { + var tags; + tags = this.app.project.tags; + if (tags.indexOf(t) >= 0) { + tags.splice(tags.indexOf(t), 1); + return this.app.client.sendRequest({ + name: "set_project_tags", + project: this.app.project.id, + tags: tags + }, (function(_this) { + return function(msg) { + return _this.updateTags(); + }; + })(this)); + } + }; + + Publish.prototype.addTags = function(value) { + var change, j, len, tags, v; + tags = this.app.project.tags; + value = value.toLowerCase().split(","); + change = false; + for (j = 0, len = value.length; j < len; j++) { + v = value[j]; + v = v.trim(); + if (tags.indexOf(v) < 0) { + change = true; + tags.push(v); + } + } + if (change) { + return this.app.client.sendRequest({ + name: "set_project_tags", + project: this.app.project.id, + tags: tags + }, (function(_this) { + return function(msg) { + _this.updateTags(); + return _this.tags_validator.reset(); + }; + })(this)); + } + }; + + Publish.prototype.projectUpdate = function(type) { + switch (type) { + case "tags": + return this.updateTags(); + } + }; + + Publish.prototype.checkDescriptionSave = function(force) { + if (force == null) { + force = false; + } + if (this.description_save > 0 && (Date.now() > this.description_save || force)) { + this.description_save = 0; + this.app.project.description = document.querySelector("#publish-box-textarea").value; + return this.app.client.sendRequest({ + name: "set_project_option", + project: this.app.project.id, + option: "description", + value: document.querySelector("#publish-box-textarea").value + }, (function(_this) { + return function(msg) {}; + })(this)); + } + }; + + Publish.prototype.setProjectPublic = function(pub) { + if (pub && !this.app.user.flags["validated"]) { + return; + } + this.checkDescriptionSave(true); + if (this.app.project != null) { + return this.app.client.sendRequest({ + name: "set_project_public", + project: this.app.project.id, + "public": pub + }, (function(_this) { + return function(msg) { + if (msg["public"] != null) { + _this.app.project["public"] = msg["public"]; + _this.app.project.notifyListeners("public"); + return _this.loadProject(_this.app.project); + } + }; + })(this)); + } + }; + + return Publish; + +})(); diff --git a/static/js/runtime/audio/audio.coffee b/static/js/runtime/audio/audio.coffee new file mode 100644 index 00000000..82e409ab --- /dev/null +++ b/static/js/runtime/audio/audio.coffee @@ -0,0 +1,345 @@ +class @AudioCore + constructor:(@runtime)-> + @buffer = [] + @getContext() + @playing = [] + @wakeup_list = [] + + isStarted:()-> + @context.state == "running" + + addToWakeUpList:(item)-> + @wakeup_list.push item + + getInterface:()-> + audio = @ + @interface = + beep:(sequence)->audio.beep(sequence) + cancelBeeps:()->audio.cancelBeeps() + playSound:(sound,volume,pitch,pan,loopit)->audio.playSound(sound,volume,pitch,pan,loopit) + playMusic:(music,volume,loopit)->audio.playMusic(music,volume,loopit) + + playSound:(sound,volume=1,pitch=1,pan=0,loopit=0)-> + if typeof sound == "string" + s = @runtime.sounds[sound] + if s? + return s.play(volume,pitch,pan,loopit) + + return 0 + + playMusic:(music,volume=1,loopit=0)-> + if typeof music == "string" + m = @runtime.music[music] + if m? + return m.play(volume,loopit) + + return 0 + + start:()-> + if false #@context.audioWorklet? + blob = new Blob([ AudioCore.processor ] , { type: "text/javascript" }) + @context.audioWorklet.addModule(window.URL.createObjectURL(blob)).then ()=> + @node = new AudioWorkletNode(@context,"my-worklet-processor") + @node.connect(@context.destination) + @flushBuffer() + else + @script_processor = @context.createScriptProcessor(4096,2,2) + @processor_funk = (event) => @onAudioProcess(event) + @script_processor.onaudioprocess = @processor_funk + @script_processor.connect @context.destination + #@context.destination.start() + #@script_processor.start() + + src = """ + class AudioWorkletProcessor { + constructor() { + this.port = {} ; + + var _this = this ; + + this.port.postMessage = function(data) { + var event = { data: data } ; + _this.port.onmessage(event) ; + } + } + + } ; + registerProcessor = function(a,b) { + return new MyWorkletProcessor() + } ; + + """ + src += AudioCore.processor + @node = eval(src) + @flushBuffer() + @bufferizer = new AudioBufferizer(@node) + + flushBuffer:()-> + while @buffer.length>0 + @node.port.postMessage @buffer.splice(0,1)[0] + + onAudioProcess:(event)-> + left = event.outputBuffer.getChannelData(0) + right = event.outputBuffer.getChannelData(1) + outputs = [[left,right]] + @bufferizer.flush(outputs) + #@node.process(null,outputs,null) + return + + getContext:()-> + if not @context? + @context = new (window.AudioContext || window.webkitAudioContext) + + if @context.state != "running" + activate = ()=> + console.info "resuming context" + @context.resume() + @start() if @beeper? + for item in @wakeup_list + item.wakeUp() + document.body.removeEventListener "touchend",activate + document.body.removeEventListener "mouseup",activate + + document.body.addEventListener "touchend",activate + document.body.addEventListener "mouseup",activate + else if @beeper? + @start() + + @context + + getBeeper:()-> + if not @beeper? + @beeper = new Beeper @ + + if @context.state == "running" + @start() + + @beeper + + beep:(sequence)-> + @getBeeper().beep(sequence) + + addBeeps:(beeps)-> + for b in beeps + b.duration *= @context.sampleRate + b.increment = b.frequency/@context.sampleRate + + if @node? + @node.port.postMessage JSON.stringify + name: "beep" + sequence: beeps + else + @buffer.push JSON.stringify + name: "beep" + sequence: beeps + + cancelBeeps:()-> + if @node? + @node.port.postMessage JSON.stringify + name: "cancel_beeps" + else + @buffer.push JSON.stringify + name: "cancel_beeps" + + @stopAll() + + addPlaying:(item)-> + @playing.push item + + removePlaying:(item)-> + index = @playing.indexOf item + if index>=0 + @playing.splice(index,1) + + stopAll:()-> + for p in @playing + try + p.stop() + catch err + console.error err + @playing = [] + + @processor: """ +class MyWorkletProcessor extends AudioWorkletProcessor { + constructor() { + super(); + this.beeps = [] ; + this.last = 0 ; + this.port.onmessage = (event) => { + let data = JSON.parse(event.data) ; + if (data.name == "cancel_beeps") + { + this.beeps = [] ; + } + else if (data.name == "beep") + { + let seq = data.sequence ; + for (let i=0;i0) + { + seq[i-1].next = note ; + } + + if (note.loopto != null) + { + note.loopto = seq[note.loopto] ; + } + + note.phase = 0 ; + note.time = 0 ; + } + + this.beeps.push(seq[0]) ; + } + } ; + } + + process(inputs, outputs, parameters) { + var output = outputs[0] ; + var phase ; + for (var i=0;i0) + { + for (var j=0;j=0;k--) + { + let b = this.beeps[k]; + let volume = b.volume ; + if (b.time/b.duration>b.span) + { + volume = 0 ; + } + if (b.waveform == "square") + { + sig += b.phase>.5? volume : -volume ; + } + else if (b.waveform == "saw") + { + sig += (b.phase*2-1)*volume ; + } + else if (b.waveform == "noise") + { + sig += (Math.random()*2-1)*volume ; + } + else + { + sig += Math.sin(b.phase*Math.PI*2)*volume ; + } + + b.phase = (b.phase+b.increment)%1 ; + b.time += 1 ; + if (b.time>=b.duration) + { + b.time = 0 ; + if (b.loopto != null) + { + if (b.repeats != null && b.repeats>0) + { + if (b.loopcount == null) + { + b.loopcount = 0 ; + } + b.loopcount++ ; + if (b.loopcount>=b.repeats) + { + b.loopcount = 0 ; + if (b.next != null) + { + b.next.phase = b.phase ; + b = b.next ; + this.beeps[k] = b ; + } + else + { + this.beeps.splice(k,1) ; + } + } + else + { + b.loopto.phase = b.phase ; + b = b.loopto ; + this.beeps[k] = b ; + } + } + else + { + b.loopto.phase = b.phase ; + b = b.loopto ; + this.beeps[k] = b ; + } + } + else if (b.next != null) + { + b.next.phase = b.phase ; + b = b.next ; + this.beeps[k] = b ; + } + else + { + this.beeps.splice(k,1) ; + } + } + } + this.last = this.last*.9+sig*.1 ; + channel[j] = this.last ; + } + } + } + return true ; + } +} + +registerProcessor('my-worklet-processor', MyWorkletProcessor); +""" + +class @AudioBufferizer + constructor:(@node)-> + @buffer_size = 4096 + @chunk_size = 512 + @chunks = [] + @nb_chunks = @buffer_size/@chunk_size + for i in [0..@nb_chunks-1] by 1 + left = (0 for k in [0..@chunk_size-1]) + right = (0 for k in [0..@chunk_size-1]) + @chunks[i] = [[left,right]] + + @current = 0 + setInterval (()=>@step()),@chunk_size/44100*1000 + + step:()-> + return if @current>=@chunks.length + @node.process(null,@chunks[@current],null) + @current++ + return + + flush:(outputs)-> + while @current<@chunks.length + @step() + @current = 0 + + left = outputs[0][0] + right = outputs[0][1] + index = 0 + chunk = 0 + for i in [0..@chunks.length-1] + chunk = @chunks[i] + l = chunk[0][0] + r = chunk[0][1] + for k in [0..l.length-1] + left[index] = l[k] + right[index] = r[k] + index += 1 + return diff --git a/static/js/runtime/audio/audio.js b/static/js/runtime/audio/audio.js new file mode 100644 index 00000000..f38b941b --- /dev/null +++ b/static/js/runtime/audio/audio.js @@ -0,0 +1,302 @@ +this.AudioCore = (function() { + function AudioCore(runtime) { + this.runtime = runtime; + this.buffer = []; + this.getContext(); + this.playing = []; + this.wakeup_list = []; + } + + AudioCore.prototype.isStarted = function() { + return this.context.state === "running"; + }; + + AudioCore.prototype.addToWakeUpList = function(item) { + return this.wakeup_list.push(item); + }; + + AudioCore.prototype.getInterface = function() { + var audio; + audio = this; + return this["interface"] = { + beep: function(sequence) { + return audio.beep(sequence); + }, + cancelBeeps: function() { + return audio.cancelBeeps(); + }, + playSound: function(sound, volume, pitch, pan, loopit) { + return audio.playSound(sound, volume, pitch, pan, loopit); + }, + playMusic: function(music, volume, loopit) { + return audio.playMusic(music, volume, loopit); + } + }; + }; + + AudioCore.prototype.playSound = function(sound, volume, pitch, pan, loopit) { + var s; + if (volume == null) { + volume = 1; + } + if (pitch == null) { + pitch = 1; + } + if (pan == null) { + pan = 0; + } + if (loopit == null) { + loopit = 0; + } + if (typeof sound === "string") { + s = this.runtime.sounds[sound]; + if (s != null) { + return s.play(volume, pitch, pan, loopit); + } + } + return 0; + }; + + AudioCore.prototype.playMusic = function(music, volume, loopit) { + var m; + if (volume == null) { + volume = 1; + } + if (loopit == null) { + loopit = 0; + } + if (typeof music === "string") { + m = this.runtime.music[music]; + if (m != null) { + return m.play(volume, loopit); + } + } + return 0; + }; + + AudioCore.prototype.start = function() { + var blob, src; + if (false) { + blob = new Blob([AudioCore.processor], { + type: "text/javascript" + }); + return this.context.audioWorklet.addModule(window.URL.createObjectURL(blob)).then((function(_this) { + return function() { + _this.node = new AudioWorkletNode(_this.context, "my-worklet-processor"); + _this.node.connect(_this.context.destination); + return _this.flushBuffer(); + }; + })(this)); + } else { + this.script_processor = this.context.createScriptProcessor(4096, 2, 2); + this.processor_funk = (function(_this) { + return function(event) { + return _this.onAudioProcess(event); + }; + })(this); + this.script_processor.onaudioprocess = this.processor_funk; + this.script_processor.connect(this.context.destination); + src = "class AudioWorkletProcessor {\n constructor() {\n this.port = {} ;\n\n var _this = this ;\n\n this.port.postMessage = function(data) {\n var event = { data: data } ;\n _this.port.onmessage(event) ;\n }\n }\n\n} ;\nregisterProcessor = function(a,b) {\n return new MyWorkletProcessor()\n} ;\n"; + src += AudioCore.processor; + this.node = eval(src); + this.flushBuffer(); + return this.bufferizer = new AudioBufferizer(this.node); + } + }; + + AudioCore.prototype.flushBuffer = function() { + var results; + results = []; + while (this.buffer.length > 0) { + results.push(this.node.port.postMessage(this.buffer.splice(0, 1)[0])); + } + return results; + }; + + AudioCore.prototype.onAudioProcess = function(event) { + var left, outputs, right; + left = event.outputBuffer.getChannelData(0); + right = event.outputBuffer.getChannelData(1); + outputs = [[left, right]]; + this.bufferizer.flush(outputs); + }; + + AudioCore.prototype.getContext = function() { + var activate; + if (this.context == null) { + this.context = new (window.AudioContext || window.webkitAudioContext); + if (this.context.state !== "running") { + activate = (function(_this) { + return function() { + var item, j, len, ref; + console.info("resuming context"); + _this.context.resume(); + if (_this.beeper != null) { + _this.start(); + } + ref = _this.wakeup_list; + for (j = 0, len = ref.length; j < len; j++) { + item = ref[j]; + item.wakeUp(); + } + document.body.removeEventListener("touchend", activate); + return document.body.removeEventListener("mouseup", activate); + }; + })(this); + document.body.addEventListener("touchend", activate); + document.body.addEventListener("mouseup", activate); + } else if (this.beeper != null) { + this.start(); + } + } + return this.context; + }; + + AudioCore.prototype.getBeeper = function() { + if (this.beeper == null) { + this.beeper = new Beeper(this); + if (this.context.state === "running") { + this.start(); + } + } + return this.beeper; + }; + + AudioCore.prototype.beep = function(sequence) { + return this.getBeeper().beep(sequence); + }; + + AudioCore.prototype.addBeeps = function(beeps) { + var b, j, len; + for (j = 0, len = beeps.length; j < len; j++) { + b = beeps[j]; + b.duration *= this.context.sampleRate; + b.increment = b.frequency / this.context.sampleRate; + } + if (this.node != null) { + return this.node.port.postMessage(JSON.stringify({ + name: "beep", + sequence: beeps + })); + } else { + return this.buffer.push(JSON.stringify({ + name: "beep", + sequence: beeps + })); + } + }; + + AudioCore.prototype.cancelBeeps = function() { + if (this.node != null) { + this.node.port.postMessage(JSON.stringify({ + name: "cancel_beeps" + })); + } else { + this.buffer.push(JSON.stringify({ + name: "cancel_beeps" + })); + } + return this.stopAll(); + }; + + AudioCore.prototype.addPlaying = function(item) { + return this.playing.push(item); + }; + + AudioCore.prototype.removePlaying = function(item) { + var index; + index = this.playing.indexOf(item); + if (index >= 0) { + return this.playing.splice(index, 1); + } + }; + + AudioCore.prototype.stopAll = function() { + var err, j, len, p, ref; + ref = this.playing; + for (j = 0, len = ref.length; j < len; j++) { + p = ref[j]; + try { + p.stop(); + } catch (error) { + err = error; + console.error(err); + } + } + return this.playing = []; + }; + + AudioCore.processor = "class MyWorkletProcessor extends AudioWorkletProcessor {\n constructor() {\n super();\n this.beeps = [] ;\n this.last = 0 ;\n this.port.onmessage = (event) => {\n let data = JSON.parse(event.data) ;\n if (data.name == \"cancel_beeps\")\n {\n this.beeps = [] ;\n }\n else if (data.name == \"beep\")\n {\n let seq = data.sequence ;\n for (let i=0;i0)\n {\n seq[i-1].next = note ;\n }\n\n if (note.loopto != null)\n {\n note.loopto = seq[note.loopto] ;\n }\n\n note.phase = 0 ;\n note.time = 0 ;\n }\n\n this.beeps.push(seq[0]) ;\n }\n } ;\n }\n\n process(inputs, outputs, parameters) {\n var output = outputs[0] ;\n var phase ;\n for (var i=0;i0)\n {\n for (var j=0;j=0;k--)\n {\n let b = this.beeps[k];\n let volume = b.volume ;\n if (b.time/b.duration>b.span)\n {\n volume = 0 ;\n }\n if (b.waveform == \"square\")\n {\n sig += b.phase>.5? volume : -volume ;\n }\n else if (b.waveform == \"saw\")\n {\n sig += (b.phase*2-1)*volume ;\n }\n else if (b.waveform == \"noise\")\n {\n sig += (Math.random()*2-1)*volume ;\n }\n else\n {\n sig += Math.sin(b.phase*Math.PI*2)*volume ;\n }\n\n b.phase = (b.phase+b.increment)%1 ;\n b.time += 1 ;\n if (b.time>=b.duration)\n {\n b.time = 0 ;\n if (b.loopto != null)\n {\n if (b.repeats != null && b.repeats>0)\n {\n if (b.loopcount == null)\n {\n b.loopcount = 0 ;\n }\n b.loopcount++ ;\n if (b.loopcount>=b.repeats)\n {\n b.loopcount = 0 ;\n if (b.next != null)\n {\n b.next.phase = b.phase ;\n b = b.next ;\n this.beeps[k] = b ;\n }\n else\n {\n this.beeps.splice(k,1) ;\n }\n }\n else\n {\n b.loopto.phase = b.phase ;\n b = b.loopto ;\n this.beeps[k] = b ;\n }\n }\n else\n {\n b.loopto.phase = b.phase ;\n b = b.loopto ;\n this.beeps[k] = b ;\n }\n }\n else if (b.next != null)\n {\n b.next.phase = b.phase ;\n b = b.next ;\n this.beeps[k] = b ;\n }\n else\n {\n this.beeps.splice(k,1) ;\n }\n }\n }\n this.last = this.last*.9+sig*.1 ;\n channel[j] = this.last ;\n }\n }\n }\n return true ;\n }\n}\n\nregisterProcessor('my-worklet-processor', MyWorkletProcessor);"; + + return AudioCore; + +})(); + +this.AudioBufferizer = (function() { + function AudioBufferizer(node) { + var i, j, k, left, ref, right; + this.node = node; + this.buffer_size = 4096; + this.chunk_size = 512; + this.chunks = []; + this.nb_chunks = this.buffer_size / this.chunk_size; + for (i = j = 0, ref = this.nb_chunks - 1; j <= ref; i = j += 1) { + left = (function() { + var n, ref1, results; + results = []; + for (k = n = 0, ref1 = this.chunk_size - 1; 0 <= ref1 ? n <= ref1 : n >= ref1; k = 0 <= ref1 ? ++n : --n) { + results.push(0); + } + return results; + }).call(this); + right = (function() { + var n, ref1, results; + results = []; + for (k = n = 0, ref1 = this.chunk_size - 1; 0 <= ref1 ? n <= ref1 : n >= ref1; k = 0 <= ref1 ? ++n : --n) { + results.push(0); + } + return results; + }).call(this); + this.chunks[i] = [[left, right]]; + } + this.current = 0; + setInterval(((function(_this) { + return function() { + return _this.step(); + }; + })(this)), this.chunk_size / 44100 * 1000); + } + + AudioBufferizer.prototype.step = function() { + if (this.current >= this.chunks.length) { + return; + } + this.node.process(null, this.chunks[this.current], null); + this.current++; + }; + + AudioBufferizer.prototype.flush = function(outputs) { + var chunk, i, index, j, k, l, left, n, r, ref, ref1, right; + while (this.current < this.chunks.length) { + this.step(); + } + this.current = 0; + left = outputs[0][0]; + right = outputs[0][1]; + index = 0; + chunk = 0; + for (i = j = 0, ref = this.chunks.length - 1; 0 <= ref ? j <= ref : j >= ref; i = 0 <= ref ? ++j : --j) { + chunk = this.chunks[i]; + l = chunk[0][0]; + r = chunk[0][1]; + for (k = n = 0, ref1 = l.length - 1; 0 <= ref1 ? n <= ref1 : n >= ref1; k = 0 <= ref1 ? ++n : --n) { + left[index] = l[k]; + right[index] = r[k]; + index += 1; + } + } + }; + + return AudioBufferizer; + +})(); diff --git a/static/js/runtime/audio/beeper.coffee b/static/js/runtime/audio/beeper.coffee new file mode 100644 index 00000000..174c76c4 --- /dev/null +++ b/static/js/runtime/audio/beeper.coffee @@ -0,0 +1,157 @@ +class @Beeper + constructor:(@audio)-> + @notes = {} + @plain_notes = {} + + text = [ + ["C","DO"] + ["C#","DO#","Db","REb"] + ["D","RE"] + ["D#","RE#","Eb","MIb"] + ["E","MI"] + ["F","FA"] + ["F#","FA#","Gb","SOLb"] + ["G","SOL"] + ["G#","SOL#","Ab","LAb"] + ["A","LA"] + ["A#","LA#","Bb","SIb"] + ["B","SI"] + ] + + for i in [0..127] by 1 + @notes[i] = i + oct = Math.floor(i/12)-1 + for n in text[i%12] + @notes[n+oct] = i + + if oct == -1 + for n in text[i%12] + @plain_notes[n] = i + + @current_octave = 5 + @current_duration = .5 + @current_volume = .5 + @current_span = 1 + @current_waveform = "square" + + beep:(input)-> + test = "loop 0 square tempo 120 duration 500 volume 50 span 50 DO2 DO - FA SOL SOL FA -" + status = "normal" + + sequence = [] + loops = [] + + parsed = input.split(" ") + for t in parsed + continue if t == "" + switch status + when "normal" + if @notes[t]? + note = @notes[t] + @current_octave = Math.floor(note/12) + sequence.push + frequency: 440*Math.pow(Math.pow(2,1/12),note-69) + volume: @current_volume + span: @current_span + duration: @current_duration + waveform: @current_waveform + + else if @plain_notes[t]? + note = @plain_notes[t]+@current_octave*12 + sequence.push + frequency: 440*Math.pow(Math.pow(2,1/12),note-69) + volume: @current_volume + span: @current_span + duration: @current_duration + waveform: @current_waveform + + else if t in ["square","sine","saw","noise"] + @current_waveform = t + else if t in ["tempo","duration","volume","span","loop","to"] + status = t + else if t == "-" + sequence.push + frequency: 440 + volume: 0 + span: @current_span + duration: @current_duration + waveform: @current_waveform + else if t == "end" + if loops.length>0 and sequence.length>0 + sequence.push + frequency: 440 + volume: 0 + span: @current_span + duration: 0 + waveform: @current_waveform + + lop = loops.splice(loops.length-1,1)[0] + sequence[sequence.length-1].loopto = lop.start + sequence[sequence.length-1].repeats = lop.repeats + + when "tempo" + status = "normal" + t = Number.parseFloat(t) + if not Number.isNaN(t) and t>0 + @current_duration = 60/t + + when "duration" + status = "normal" + t = Number.parseFloat(t) + if not Number.isNaN(t) and t>0 + @current_duration = t/1000 + + when "volume" + status = "normal" + t = Number.parseFloat(t) + if not Number.isNaN(t) + @current_volume = t/100 + + when "span" + status = "normal" + t = Number.parseFloat(t) + if not Number.isNaN(t) + @current_span = t/100 + + when "loop" + status = "normal" + loops.push + start: sequence.length + t = Number.parseFloat(t) + if not Number.isNaN(t) + loops[loops.length-1].repeats = t + + when "to" + status = "normal" + if note? + n = null + + if @notes[t]? + n = @notes[t] + else if @plain_notes[t]? + n = @plain_notes[t]+@current_octave*12 + + if n? and n != note + for i in [note..n] + if i != note + sequence.push + frequency: 440*Math.pow(Math.pow(2,1/12),i-69) + volume: @current_volume + span: @current_span + duration: @current_duration + waveform: @current_waveform + note = n + + if loops.length>0 and sequence.length>0 + lop = loops.splice(loops.length-1,1)[0] + sequence.push + frequency: 440 + volume: 0 + span: @current_span + duration: 0 + waveform: @current_waveform + + sequence[sequence.length-1].loopto = lop.start + sequence[sequence.length-1].repeats = lop.repeats + + @audio.addBeeps(sequence) diff --git a/static/js/runtime/audio/beeper.js b/static/js/runtime/audio/beeper.js new file mode 100644 index 00000000..cc1d6505 --- /dev/null +++ b/static/js/runtime/audio/beeper.js @@ -0,0 +1,172 @@ +this.Beeper = (function() { + function Beeper(audio) { + var i, j, k, l, len, len1, n, oct, ref, ref1, text; + this.audio = audio; + this.notes = {}; + this.plain_notes = {}; + text = [["C", "DO"], ["C#", "DO#", "Db", "REb"], ["D", "RE"], ["D#", "RE#", "Eb", "MIb"], ["E", "MI"], ["F", "FA"], ["F#", "FA#", "Gb", "SOLb"], ["G", "SOL"], ["G#", "SOL#", "Ab", "LAb"], ["A", "LA"], ["A#", "LA#", "Bb", "SIb"], ["B", "SI"]]; + for (i = j = 0; j <= 127; i = j += 1) { + this.notes[i] = i; + oct = Math.floor(i / 12) - 1; + ref = text[i % 12]; + for (k = 0, len = ref.length; k < len; k++) { + n = ref[k]; + this.notes[n + oct] = i; + } + if (oct === -1) { + ref1 = text[i % 12]; + for (l = 0, len1 = ref1.length; l < len1; l++) { + n = ref1[l]; + this.plain_notes[n] = i; + } + } + } + this.current_octave = 5; + this.current_duration = .5; + this.current_volume = .5; + this.current_span = 1; + this.current_waveform = "square"; + } + + Beeper.prototype.beep = function(input) { + var i, j, k, len, loops, lop, n, note, parsed, ref, ref1, sequence, status, t, test; + test = "loop 0 square tempo 120 duration 500 volume 50 span 50 DO2 DO - FA SOL SOL FA -"; + status = "normal"; + sequence = []; + loops = []; + parsed = input.split(" "); + for (j = 0, len = parsed.length; j < len; j++) { + t = parsed[j]; + if (t === "") { + continue; + } + switch (status) { + case "normal": + if (this.notes[t] != null) { + note = this.notes[t]; + this.current_octave = Math.floor(note / 12); + sequence.push({ + frequency: 440 * Math.pow(Math.pow(2, 1 / 12), note - 69), + volume: this.current_volume, + span: this.current_span, + duration: this.current_duration, + waveform: this.current_waveform + }); + } else if (this.plain_notes[t] != null) { + note = this.plain_notes[t] + this.current_octave * 12; + sequence.push({ + frequency: 440 * Math.pow(Math.pow(2, 1 / 12), note - 69), + volume: this.current_volume, + span: this.current_span, + duration: this.current_duration, + waveform: this.current_waveform + }); + } else if (t === "square" || t === "sine" || t === "saw" || t === "noise") { + this.current_waveform = t; + } else if (t === "tempo" || t === "duration" || t === "volume" || t === "span" || t === "loop" || t === "to") { + status = t; + } else if (t === "-") { + sequence.push({ + frequency: 440, + volume: 0, + span: this.current_span, + duration: this.current_duration, + waveform: this.current_waveform + }); + } else if (t === "end") { + if (loops.length > 0 && sequence.length > 0) { + sequence.push({ + frequency: 440, + volume: 0, + span: this.current_span, + duration: 0, + waveform: this.current_waveform + }); + lop = loops.splice(loops.length - 1, 1)[0]; + sequence[sequence.length - 1].loopto = lop.start; + sequence[sequence.length - 1].repeats = lop.repeats; + } + } + break; + case "tempo": + status = "normal"; + t = Number.parseFloat(t); + if (!Number.isNaN(t) && t > 0) { + this.current_duration = 60 / t; + } + break; + case "duration": + status = "normal"; + t = Number.parseFloat(t); + if (!Number.isNaN(t) && t > 0) { + this.current_duration = t / 1000; + } + break; + case "volume": + status = "normal"; + t = Number.parseFloat(t); + if (!Number.isNaN(t)) { + this.current_volume = t / 100; + } + break; + case "span": + status = "normal"; + t = Number.parseFloat(t); + if (!Number.isNaN(t)) { + this.current_span = t / 100; + } + break; + case "loop": + status = "normal"; + loops.push({ + start: sequence.length + }); + t = Number.parseFloat(t); + if (!Number.isNaN(t)) { + loops[loops.length - 1].repeats = t; + } + break; + case "to": + status = "normal"; + if (note != null) { + n = null; + if (this.notes[t] != null) { + n = this.notes[t]; + } else if (this.plain_notes[t] != null) { + n = this.plain_notes[t] + this.current_octave * 12; + } + if ((n != null) && n !== note) { + for (i = k = ref = note, ref1 = n; ref <= ref1 ? k <= ref1 : k >= ref1; i = ref <= ref1 ? ++k : --k) { + if (i !== note) { + sequence.push({ + frequency: 440 * Math.pow(Math.pow(2, 1 / 12), i - 69), + volume: this.current_volume, + span: this.current_span, + duration: this.current_duration, + waveform: this.current_waveform + }); + } + } + note = n; + } + } + } + } + if (loops.length > 0 && sequence.length > 0) { + lop = loops.splice(loops.length - 1, 1)[0]; + sequence.push({ + frequency: 440, + volume: 0, + span: this.current_span, + duration: 0, + waveform: this.current_waveform + }); + sequence[sequence.length - 1].loopto = lop.start; + sequence[sequence.length - 1].repeats = lop.repeats; + } + return this.audio.addBeeps(sequence); + }; + + return Beeper; + +})(); diff --git a/static/js/runtime/audio/music.coffee b/static/js/runtime/audio/music.coffee new file mode 100644 index 00000000..226f550a --- /dev/null +++ b/static/js/runtime/audio/music.coffee @@ -0,0 +1,39 @@ +class @Music + constructor:(@audio,@url)-> + @tag = new Audio(@url) + @playing = false + + play:(volume=1,loopit=false)-> + @playing = true + @tag.loop = if loopit then true else false + + if @audio.isStarted() + @tag.play() + else + @audio.addToWakeUpList @ + + @audio.addPlaying @ + + { + play:()=> @tag.play() + stop:()=> + @playing = false + @tag.pause() + @audio.removePlaying @ + setVolume:(volume)=> @tag.volume = Math.max(0,Math.min(1,volume)) + getPosition: ()=> @tag.currentTime + getDuration: ()=> @tag.duration + setPosition: (pos)=> + @tag.pause() + @tag.currentTime = Math.max(0,Math.min(@tag.duration,pos)) + if @playing + @tag.play() + } + + wakeUp:()-> + if @playing + @tag.play() + + stop:()-> + @playing = false + @tag.pause() diff --git a/static/js/runtime/audio/music.js b/static/js/runtime/audio/music.js new file mode 100644 index 00000000..80035b53 --- /dev/null +++ b/static/js/runtime/audio/music.js @@ -0,0 +1,77 @@ +this.Music = (function() { + function Music(audio, url) { + this.audio = audio; + this.url = url; + this.tag = new Audio(this.url); + this.playing = false; + } + + Music.prototype.play = function(volume, loopit) { + if (volume == null) { + volume = 1; + } + if (loopit == null) { + loopit = false; + } + this.playing = true; + this.tag.loop = loopit ? true : false; + if (this.audio.isStarted()) { + this.tag.play(); + } else { + this.audio.addToWakeUpList(this); + } + this.audio.addPlaying(this); + return { + play: (function(_this) { + return function() { + return _this.tag.play(); + }; + })(this), + stop: (function(_this) { + return function() { + _this.playing = false; + _this.tag.pause(); + return _this.audio.removePlaying(_this); + }; + })(this), + setVolume: (function(_this) { + return function(volume) { + return _this.tag.volume = Math.max(0, Math.min(1, volume)); + }; + })(this), + getPosition: (function(_this) { + return function() { + return _this.tag.currentTime; + }; + })(this), + getDuration: (function(_this) { + return function() { + return _this.tag.duration; + }; + })(this), + setPosition: (function(_this) { + return function(pos) { + _this.tag.pause(); + _this.tag.currentTime = Math.max(0, Math.min(_this.tag.duration, pos)); + if (_this.playing) { + return _this.tag.play(); + } + }; + })(this) + }; + }; + + Music.prototype.wakeUp = function() { + if (this.playing) { + return this.tag.play(); + } + }; + + Music.prototype.stop = function() { + this.playing = false; + return this.tag.pause(); + }; + + return Music; + +})(); diff --git a/static/js/runtime/audio/sound.coffee b/static/js/runtime/audio/sound.coffee new file mode 100644 index 00000000..76b65969 --- /dev/null +++ b/static/js/runtime/audio/sound.coffee @@ -0,0 +1,59 @@ +class @Sound + constructor:(@audio,@url)-> + request = new XMLHttpRequest() + request.open('GET', @url, true) + request.responseType = 'arraybuffer' + + request.onload = ()=> + @audio.context.decodeAudioData request.response, (@buffer)=> + + request.send() + + play:(volume=1,pitch=1,pan=0,loopit=false)-> + return if not @buffer? + + source = @audio.context.createBufferSource() + source.playbackRate.value = pitch + source.buffer = @buffer + if loopit then source.loop = true + + gain = @audio.context.createGain() + gain.gain.value = volume + + if false and @audio.context.createStereoPanner? + panner = @audio.context.createStereoPanner() + panner.setPan = (pan)-> panner.pan.value = pan + else + panner = @audio.context.createPanner() + panner.panningModel = "equalpower" + panner.setPan = (pan)->panner.setPosition(pan, 0, 1 - Math.abs(pan)) + + panner.setPan pan + + source.connect gain + gain.connect panner + panner.connect @audio.context.destination + + source.start() + + playing = null + + if loopit + playing = { + stop: ()=> + source.stop() + } + @audio.addPlaying playing + + res = + stop:()-> + source.stop() + if playing then @audio.removePlaying playing + + setVolume:(volume)-> gain.gain.value = Math.max(0,Math.min(1,volume)) + setPitch:(pitch)-> source.playbackRate.value = Math.max(.001,Math.min(1000,pitch)) + setPan:(pan)-> panner.setPan Math.max(-1,Math.min(1,pan)) + finished: false + + source.onended = ()-> res.finished = true + res diff --git a/static/js/runtime/audio/sound.js b/static/js/runtime/audio/sound.js new file mode 100644 index 00000000..1a809886 --- /dev/null +++ b/static/js/runtime/audio/sound.js @@ -0,0 +1,98 @@ +this.Sound = (function() { + function Sound(audio, url) { + var request; + this.audio = audio; + this.url = url; + request = new XMLHttpRequest(); + request.open('GET', this.url, true); + request.responseType = 'arraybuffer'; + request.onload = (function(_this) { + return function() { + return _this.audio.context.decodeAudioData(request.response, function(buffer) { + _this.buffer = buffer; + }); + }; + })(this); + request.send(); + } + + Sound.prototype.play = function(volume, pitch, pan, loopit) { + var gain, panner, playing, res, source; + if (volume == null) { + volume = 1; + } + if (pitch == null) { + pitch = 1; + } + if (pan == null) { + pan = 0; + } + if (loopit == null) { + loopit = false; + } + if (this.buffer == null) { + return; + } + source = this.audio.context.createBufferSource(); + source.playbackRate.value = pitch; + source.buffer = this.buffer; + if (loopit) { + source.loop = true; + } + gain = this.audio.context.createGain(); + gain.gain.value = volume; + if (false && (this.audio.context.createStereoPanner != null)) { + panner = this.audio.context.createStereoPanner(); + panner.setPan = function(pan) { + return panner.pan.value = pan; + }; + } else { + panner = this.audio.context.createPanner(); + panner.panningModel = "equalpower"; + panner.setPan = function(pan) { + return panner.setPosition(pan, 0, 1 - Math.abs(pan)); + }; + } + panner.setPan(pan); + source.connect(gain); + gain.connect(panner); + panner.connect(this.audio.context.destination); + source.start(); + playing = null; + if (loopit) { + playing = { + stop: (function(_this) { + return function() { + return source.stop(); + }; + })(this) + }; + this.audio.addPlaying(playing); + } + res = { + stop: function() { + source.stop(); + if (playing) { + return this.audio.removePlaying(playing); + } + }, + setVolume: function(volume) { + return gain.gain.value = Math.max(0, Math.min(1, volume)); + }, + setPitch: function(pitch) { + return source.playbackRate.value = Math.max(.001, Math.min(1000, pitch)); + }, + setPan: function(pan) { + return panner.setPan(Math.max(-1, Math.min(1, pan))); + }, + finished: false + }; + source.onended = function() { + return res.finished = true; + }; + return res; + }; + + return Sound; + +})(); diff --git a/static/js/runtime/gamepad.coffee b/static/js/runtime/gamepad.coffee new file mode 100644 index 00000000..12b0a3ec --- /dev/null +++ b/static/js/runtime/gamepad.coffee @@ -0,0 +1,183 @@ +class @Gamepad + constructor:(@listener,@index = 0)-> + if navigator.getGamepads? + pads = navigator.getGamepads() + if @index + pads = navigator.getGamepads() + pad_count = 0 + for pad,i in pads + break if not pad? + pad_count++ + if not @status[i] + @status[i] = + press: {} + release: {} + + for key,value of @buttons_map + if pad.buttons[key]? + @status[i][value] = if pad.buttons[key].pressed then 1 else 0 + + for key,value of @triggers_map + if pad.buttons[key]? + @status[i][value] = pad.buttons[key].value + + if pad.axes.length>=2 + x = pad.axes[0] + y = -pad.axes[1] + r = Math.sqrt(x*x+y*y) + angle = Math.floor(((Math.atan2(y,x)+Math.PI*2)%(Math.PI*2))/(Math.PI*2)*360) + @status[i].LEFT_STICK_ANGLE = angle + @status[i].LEFT_STICK_AMOUNT = r + @status[i].LEFT_STICK_UP = y>.5 + @status[i].LEFT_STICK_DOWN = y<-.5 + @status[i].LEFT_STICK_LEFT = x<-.5 + @status[i].LEFT_STICK_RIGHT = x>.5 + + if pad.axes.length>=4 + x = pad.axes[2] + y = -pad.axes[3] + r = Math.sqrt(x*x+y*y) + angle = Math.floor(((Math.atan2(y,x)+Math.PI*2)%(Math.PI*2))/(Math.PI*2)*360) + @status[i].RIGHT_STICK_ANGLE = angle + @status[i].RIGHT_STICK_AMOUNT = r + @status[i].RIGHT_STICK_UP = y>.5 + @status[i].RIGHT_STICK_DOWN = y<-.5 + @status[i].RIGHT_STICK_LEFT = x<-.5 + @status[i].RIGHT_STICK_RIGHT = x>.5 + +# if pad.axes.length>=6 +# @status[i].LT = pad.axes[2]>0 +# @status[i].RT = pad.axes[5]>0 + + for key,value of @buttons_map + @status[value] = 0 + for pad in pads + break if not pad? + if pad.buttons[key]? and pad.buttons[key].pressed + @status[value] = 1 + + for key,value of @triggers_map + @status[value] = 0 + for pad in pads + break if not pad? + if pad.buttons[key]? + @status[value] = pad.buttons[key].value + + @status.UP = 0 + @status.DOWN = 0 + @status.LEFT = 0 + @status.RIGHT = 0 + @status.LEFT_STICK_UP = 0 + @status.LEFT_STICK_DOWN = 0 + @status.LEFT_STICK_LEFT = 0 + @status.LEFT_STICK_RIGHT = 0 + @status.RIGHT_STICK_UP = 0 + @status.RIGHT_STICK_DOWN = 0 + @status.RIGHT_STICK_LEFT = 0 + @status.RIGHT_STICK_RIGHT = 0 + @status.LEFT_STICK_ANGLE = 0 + @status.LEFT_STICK_AMOUNT = 0 + @status.RIGHT_STICK_ANGLE = 0 + @status.RIGHT_STICK_AMOUNT = 0 + @status.RT = 0 + @status.LT = 0 + + for i in [0..pad_count-1] by 1 + @status[i].UP = @status[i].DPAD_UP or @status[i].LEFT_STICK_UP or @status[i].RIGHT_STICK_UP + @status[i].DOWN = @status[i].DPAD_DOWN or @status[i].LEFT_STICK_DOWN or @status[i].RIGHT_STICK_DOWN + @status[i].LEFT = @status[i].DPAD_LEFT or @status[i].LEFT_STICK_LEFT or @status[i].RIGHT_STICK_LEFT + @status[i].RIGHT = @status[i].DPAD_RIGHT or @status[i].LEFT_STICK_RIGHT or @status[i].RIGHT_STICK_RIGHT + + @status.UP = 1 if @status[i].UP + @status.DOWN = 1 if @status[i].DOWN + @status.LEFT = 1 if @status[i].LEFT + @status.RIGHT = 1 if @status[i].RIGHT + @status.LEFT_STICK_UP = 1 if @status[i].LEFT_STICK_UP + @status.LEFT_STICK_DOWN = 1 if @status[i].LEFT_STICK_DOWN + @status.LEFT_STICK_LEFT = 1 if @status[i].LEFT_STICK_LEFT + @status.LEFT_STICK_RIGHT = 1 if @status[i].LEFT_STICK_RIGHT + @status.RIGHT_STICK_UP = 1 if @status[i].RIGHT_STICK_UP + @status.RIGHT_STICK_DOWN = 1 if @status[i].RIGHT_STICK_DOWN + @status.RIGHT_STICK_LEFT = 1 if @status[i].RIGHT_STICK_LEFT + @status.RIGHT_STICK_RIGHT = 1 if @status[i].RIGHT_STICK_RIGHT + + @status.LT = @status[i].LT if @status[i].LT + @status.RT = @status[i].RT if @status[i].RT + + if @status[i].LEFT_STICK_AMOUNT>@status.LEFT_STICK_AMOUNT + @status.LEFT_STICK_AMOUNT = @status[i].LEFT_STICK_AMOUNT + @status.LEFT_STICK_ANGLE = @status[i].LEFT_STICK_ANGLE + + if @status[i].RIGHT_STICK_AMOUNT>@status.RIGHT_STICK_AMOUNT + @status.RIGHT_STICK_AMOUNT = @status[i].RIGHT_STICK_AMOUNT + @status.RIGHT_STICK_ANGLE = @status[i].RIGHT_STICK_ANGLE + + for i in [pad_count..3] by 1 + delete @status[i] + + @count = pad_count + + @updateChanges(@status,@previous.global) + for i in [0..pad_count-1] by 1 + @updateChanges(@status[i],@previous[i]) + return + + + updateChanges:(current,previous)-> + for key of current.press + current.press[key] = 0 + + for key of current.release + current.release[key] = 0 + + for key of previous + if previous[key] and not current[key] + current.release[key] = 1 + + for key of current + continue if key == "press" or key == "release" + if current[key] and not previous[key] + current.press[key] = 1 + + for key of previous + previous[key] = 0 + + for key of current + continue if key == "press" or key == "release" + previous[key] = current[key] + + return diff --git a/static/js/runtime/gamepad.js b/static/js/runtime/gamepad.js new file mode 100644 index 00000000..e1f69d46 --- /dev/null +++ b/static/js/runtime/gamepad.js @@ -0,0 +1,246 @@ +this.Gamepad = (function() { + function Gamepad(listener, index) { + var pads; + this.listener = listener; + this.index = index != null ? index : 0; + if (navigator.getGamepads != null) { + pads = navigator.getGamepads(); + if (this.index < pads.length && (pads[this.index] != null)) { + this.pad = pads[this.index]; + } + } + this.buttons_map = { + 0: "A", + 1: "B", + 2: "X", + 3: "Y", + 4: "LB", + 5: "RB", + 8: "VIEW", + 9: "MENU", + 10: "LS", + 11: "RS", + 12: "DPAD_UP", + 13: "DPAD_DOWN", + 14: "DPAD_LEFT", + 15: "DPAD_RIGHT" + }; + this.triggers_map = { + 6: "LT", + 7: "RT" + }; + this.status = { + press: {}, + release: {} + }; + this.previous = { + global: {}, + 0: {}, + 1: {}, + 2: {}, + 3: {} + }; + } + + Gamepad.prototype.update = function() { + var angle, i, j, k, key, l, len, len1, len2, m, n, o, pad, pad_count, pads, r, ref, ref1, ref2, ref3, ref4, ref5, ref6, value, x, y; + pads = navigator.getGamepads(); + pad_count = 0; + for (i = j = 0, len = pads.length; j < len; i = ++j) { + pad = pads[i]; + if (pad == null) { + break; + } + pad_count++; + if (!this.status[i]) { + this.status[i] = { + press: {}, + release: {} + }; + } + ref = this.buttons_map; + for (key in ref) { + value = ref[key]; + if (pad.buttons[key] != null) { + this.status[i][value] = pad.buttons[key].pressed ? 1 : 0; + } + } + ref1 = this.triggers_map; + for (key in ref1) { + value = ref1[key]; + if (pad.buttons[key] != null) { + this.status[i][value] = pad.buttons[key].value; + } + } + if (pad.axes.length >= 2) { + x = pad.axes[0]; + y = -pad.axes[1]; + r = Math.sqrt(x * x + y * y); + angle = Math.floor(((Math.atan2(y, x) + Math.PI * 2) % (Math.PI * 2)) / (Math.PI * 2) * 360); + this.status[i].LEFT_STICK_ANGLE = angle; + this.status[i].LEFT_STICK_AMOUNT = r; + this.status[i].LEFT_STICK_UP = y > .5; + this.status[i].LEFT_STICK_DOWN = y < -.5; + this.status[i].LEFT_STICK_LEFT = x < -.5; + this.status[i].LEFT_STICK_RIGHT = x > .5; + } + if (pad.axes.length >= 4) { + x = pad.axes[2]; + y = -pad.axes[3]; + r = Math.sqrt(x * x + y * y); + angle = Math.floor(((Math.atan2(y, x) + Math.PI * 2) % (Math.PI * 2)) / (Math.PI * 2) * 360); + this.status[i].RIGHT_STICK_ANGLE = angle; + this.status[i].RIGHT_STICK_AMOUNT = r; + this.status[i].RIGHT_STICK_UP = y > .5; + this.status[i].RIGHT_STICK_DOWN = y < -.5; + this.status[i].RIGHT_STICK_LEFT = x < -.5; + this.status[i].RIGHT_STICK_RIGHT = x > .5; + } + } + ref2 = this.buttons_map; + for (key in ref2) { + value = ref2[key]; + this.status[value] = 0; + for (k = 0, len1 = pads.length; k < len1; k++) { + pad = pads[k]; + if (pad == null) { + break; + } + if ((pad.buttons[key] != null) && pad.buttons[key].pressed) { + this.status[value] = 1; + } + } + } + ref3 = this.triggers_map; + for (key in ref3) { + value = ref3[key]; + this.status[value] = 0; + for (l = 0, len2 = pads.length; l < len2; l++) { + pad = pads[l]; + if (pad == null) { + break; + } + if (pad.buttons[key] != null) { + this.status[value] = pad.buttons[key].value; + } + } + } + this.status.UP = 0; + this.status.DOWN = 0; + this.status.LEFT = 0; + this.status.RIGHT = 0; + this.status.LEFT_STICK_UP = 0; + this.status.LEFT_STICK_DOWN = 0; + this.status.LEFT_STICK_LEFT = 0; + this.status.LEFT_STICK_RIGHT = 0; + this.status.RIGHT_STICK_UP = 0; + this.status.RIGHT_STICK_DOWN = 0; + this.status.RIGHT_STICK_LEFT = 0; + this.status.RIGHT_STICK_RIGHT = 0; + this.status.LEFT_STICK_ANGLE = 0; + this.status.LEFT_STICK_AMOUNT = 0; + this.status.RIGHT_STICK_ANGLE = 0; + this.status.RIGHT_STICK_AMOUNT = 0; + this.status.RT = 0; + this.status.LT = 0; + for (i = m = 0, ref4 = pad_count - 1; m <= ref4; i = m += 1) { + this.status[i].UP = this.status[i].DPAD_UP || this.status[i].LEFT_STICK_UP || this.status[i].RIGHT_STICK_UP; + this.status[i].DOWN = this.status[i].DPAD_DOWN || this.status[i].LEFT_STICK_DOWN || this.status[i].RIGHT_STICK_DOWN; + this.status[i].LEFT = this.status[i].DPAD_LEFT || this.status[i].LEFT_STICK_LEFT || this.status[i].RIGHT_STICK_LEFT; + this.status[i].RIGHT = this.status[i].DPAD_RIGHT || this.status[i].LEFT_STICK_RIGHT || this.status[i].RIGHT_STICK_RIGHT; + if (this.status[i].UP) { + this.status.UP = 1; + } + if (this.status[i].DOWN) { + this.status.DOWN = 1; + } + if (this.status[i].LEFT) { + this.status.LEFT = 1; + } + if (this.status[i].RIGHT) { + this.status.RIGHT = 1; + } + if (this.status[i].LEFT_STICK_UP) { + this.status.LEFT_STICK_UP = 1; + } + if (this.status[i].LEFT_STICK_DOWN) { + this.status.LEFT_STICK_DOWN = 1; + } + if (this.status[i].LEFT_STICK_LEFT) { + this.status.LEFT_STICK_LEFT = 1; + } + if (this.status[i].LEFT_STICK_RIGHT) { + this.status.LEFT_STICK_RIGHT = 1; + } + if (this.status[i].RIGHT_STICK_UP) { + this.status.RIGHT_STICK_UP = 1; + } + if (this.status[i].RIGHT_STICK_DOWN) { + this.status.RIGHT_STICK_DOWN = 1; + } + if (this.status[i].RIGHT_STICK_LEFT) { + this.status.RIGHT_STICK_LEFT = 1; + } + if (this.status[i].RIGHT_STICK_RIGHT) { + this.status.RIGHT_STICK_RIGHT = 1; + } + if (this.status[i].LT) { + this.status.LT = this.status[i].LT; + } + if (this.status[i].RT) { + this.status.RT = this.status[i].RT; + } + if (this.status[i].LEFT_STICK_AMOUNT > this.status.LEFT_STICK_AMOUNT) { + this.status.LEFT_STICK_AMOUNT = this.status[i].LEFT_STICK_AMOUNT; + this.status.LEFT_STICK_ANGLE = this.status[i].LEFT_STICK_ANGLE; + } + if (this.status[i].RIGHT_STICK_AMOUNT > this.status.RIGHT_STICK_AMOUNT) { + this.status.RIGHT_STICK_AMOUNT = this.status[i].RIGHT_STICK_AMOUNT; + this.status.RIGHT_STICK_ANGLE = this.status[i].RIGHT_STICK_ANGLE; + } + } + for (i = n = ref5 = pad_count; n <= 3; i = n += 1) { + delete this.status[i]; + } + this.count = pad_count; + this.updateChanges(this.status, this.previous.global); + for (i = o = 0, ref6 = pad_count - 1; o <= ref6; i = o += 1) { + this.updateChanges(this.status[i], this.previous[i]); + } + }; + + Gamepad.prototype.updateChanges = function(current, previous) { + var key; + for (key in current.press) { + current.press[key] = 0; + } + for (key in current.release) { + current.release[key] = 0; + } + for (key in previous) { + if (previous[key] && !current[key]) { + current.release[key] = 1; + } + } + for (key in current) { + if (key === "press" || key === "release") { + continue; + } + if (current[key] && !previous[key]) { + current.press[key] = 1; + } + } + for (key in previous) { + previous[key] = 0; + } + for (key in current) { + if (key === "press" || key === "release") { + continue; + } + previous[key] = current[key]; + } + }; + + return Gamepad; + +})(); diff --git a/static/js/runtime/keyboard.coffee b/static/js/runtime/keyboard.coffee new file mode 100644 index 00000000..d18d1668 --- /dev/null +++ b/static/js/runtime/keyboard.coffee @@ -0,0 +1,74 @@ +class @Keyboard + constructor:()-> + document.addEventListener "keydown",(event)=>@keydown(event) + document.addEventListener "keyup",(event)=>@keyup(event) + @keyboard = + press: {} + release: {} + + @previous = {} + + convertCode:(code)-> + res = "" + low = false + for i in [0..code.length-1] + c = code.charAt(i) + if c == c.toUpperCase() and low + res += "_" + low = false + else + low = true + res += c.toUpperCase() + res + + keydown:(event)-> + event.preventDefault() + #console.info event + code = event.code + key = event.key + + @keyboard[@convertCode(code)] = 1 + @keyboard[key.toUpperCase()] = 1 + + @updateDirectional() + + keyup:(event)-> + #console.info event + code = event.code + key = event.key + + @keyboard[@convertCode(code)] = 0 + @keyboard[key.toUpperCase()] = 0 + + @updateDirectional() + + updateDirectional:()-> + @keyboard.UP = @keyboard.KEY_W or @keyboard.ARROW_UP + @keyboard.DOWN = @keyboard.KEY_S or @keyboard.ARROW_DOWN + @keyboard.LEFT = @keyboard.KEY_A or @keyboard.ARROW_LEFT + @keyboard.RIGHT = @keyboard.KEY_D or @keyboard.ARROW_RIGHT + + update:()-> + for key of @keyboard.press + @keyboard.press[key] = 0 + + for key of @keyboard.release + @keyboard.release[key] = 0 + + for key of @previous + if @previous[key] and not @keyboard[key] + @keyboard.release[key] = 1 + + for key of @keyboard + continue if key == "press" or key == "release" + if @keyboard[key] and not @previous[key] + @keyboard.press[key] = 1 + + for key of @previous + @previous[key] = 0 + + for key of @keyboard + continue if key == "press" or key == "release" + @previous[key] = @keyboard[key] + + return diff --git a/static/js/runtime/keyboard.js b/static/js/runtime/keyboard.js new file mode 100644 index 00000000..d1d945c3 --- /dev/null +++ b/static/js/runtime/keyboard.js @@ -0,0 +1,97 @@ +this.Keyboard = (function() { + function Keyboard() { + document.addEventListener("keydown", (function(_this) { + return function(event) { + return _this.keydown(event); + }; + })(this)); + document.addEventListener("keyup", (function(_this) { + return function(event) { + return _this.keyup(event); + }; + })(this)); + this.keyboard = { + press: {}, + release: {} + }; + this.previous = {}; + } + + Keyboard.prototype.convertCode = function(code) { + var c, i, j, low, ref, res; + res = ""; + low = false; + for (i = j = 0, ref = code.length - 1; 0 <= ref ? j <= ref : j >= ref; i = 0 <= ref ? ++j : --j) { + c = code.charAt(i); + if (c === c.toUpperCase() && low) { + res += "_"; + low = false; + } else { + low = true; + } + res += c.toUpperCase(); + } + return res; + }; + + Keyboard.prototype.keydown = function(event) { + var code, key; + event.preventDefault(); + code = event.code; + key = event.key; + this.keyboard[this.convertCode(code)] = 1; + this.keyboard[key.toUpperCase()] = 1; + return this.updateDirectional(); + }; + + Keyboard.prototype.keyup = function(event) { + var code, key; + code = event.code; + key = event.key; + this.keyboard[this.convertCode(code)] = 0; + this.keyboard[key.toUpperCase()] = 0; + return this.updateDirectional(); + }; + + Keyboard.prototype.updateDirectional = function() { + this.keyboard.UP = this.keyboard.KEY_W || this.keyboard.ARROW_UP; + this.keyboard.DOWN = this.keyboard.KEY_S || this.keyboard.ARROW_DOWN; + this.keyboard.LEFT = this.keyboard.KEY_A || this.keyboard.ARROW_LEFT; + return this.keyboard.RIGHT = this.keyboard.KEY_D || this.keyboard.ARROW_RIGHT; + }; + + Keyboard.prototype.update = function() { + var key; + for (key in this.keyboard.press) { + this.keyboard.press[key] = 0; + } + for (key in this.keyboard.release) { + this.keyboard.release[key] = 0; + } + for (key in this.previous) { + if (this.previous[key] && !this.keyboard[key]) { + this.keyboard.release[key] = 1; + } + } + for (key in this.keyboard) { + if (key === "press" || key === "release") { + continue; + } + if (this.keyboard[key] && !this.previous[key]) { + this.keyboard.press[key] = 1; + } + } + for (key in this.previous) { + this.previous[key] = 0; + } + for (key in this.keyboard) { + if (key === "press" || key === "release") { + continue; + } + this.previous[key] = this.keyboard[key]; + } + }; + + return Keyboard; + +})(); diff --git a/static/js/runtime/m3d/m3d.coffee b/static/js/runtime/m3d/m3d.coffee new file mode 100644 index 00000000..7af3d683 --- /dev/null +++ b/static/js/runtime/m3d/m3d.coffee @@ -0,0 +1,186 @@ +class @M3D + constructor:(@runtime)-> + + createScene:()-> new M3DScene @ + createCamera:()-> new M3DCamera @ + createBox:()-> new M3DBox @ + createSphere:()-> new M3DSphere @ + createPlane:(width,height)-> new M3DPlane @,width,height + createTexture:(image)-> new M3DTexture @,image + createCubeTexture:(px,nx,py,ny,pz,nz)-> new M3DCubeTexture @,px,nx,py,ny,pz,nz + + loadImage:()-> + loadModel:(asset)-> new M3DModelLoad @,asset + + createDirectionalLight:()-> new M3DDirectionalLight + +class @M3DCamera + constructor:()-> + @camera = new THREE.PerspectiveCamera(45,1,1,1000) + @position = @camera.position + + lookAt:(x,y,z)-> + @camera.lookAt(x,y,z) + +class @M3DScene + constructor:()-> + @scene = new THREE.Scene() + + setFog:(color,density)-> + if not @scene.fog? + @scene.fog = new THREE.FogExp2(M3DConvertColor(color),density) + else + @scene.fog.color = M3DConvertColor(color) if color? + @scene.fog.density = density if density? + + setAmbientLight:(color)-> + color = M3DConvertColor(color) + if not @ambient_light? + @ambient_light = new THREE.AmbientLight(color) + @scene.add @ambient_light + else + @ambient_light.color = color + + add:(object)-> + if object? and object.object? + @scene.add object.object + return + + setBackground:(rgb)-> + @scene.background = M3DConvertColor rgb + return + + setEnvironment:(cube_texture)-> + @scene.environment = cube_texture.texture + +class @M3DDirectionalLight + constructor:(color=0xFFFFFF,intensity=1)-> + @object = new THREE.DirectionalLight color,intensity + @position = @object.position + + setShadowFrame:(left,right,bottom,top)-> + @object.castShadow = true + @object.shadow.camera.left = left + @object.shadow.camera.right = right + @object.shadow.camera.bottom = bottom + @object.shadow.camera.top = top + #@object.shadow.mapSize.width = 2048 + #@object.shadow.mapSize.height = 2048 + +class @M3DObject + constructor:(@m3d)-> + + setObject:(@object)-> + @rotation = @object.rotation + @position = @object.position + @scale = @object.scale + + setColor:(rgb)-> + @material.color = M3DConvertColor rgb + return + + setRotationOrder:(order)-> + if @object? + @object.rotation.order = order + + setCastShadow:(cast)-> + @object.castShadow = cast != 0 + + setReceiveShadow:(receive)-> + @object.receiveShadow = receive != 0 + + clone:()-> + o = new M3DObject @m3d + o.setObject SkeletonUtils.clone(@object) #@object.clone() + o.animations = @animations + o + + setMap:(@texture)-> + @object.material.map = @texture.texture + @object.material.needsUpdate = true + + startAnimation:(num)-> + return if not @animations? + if not @mixer? + @mixer = new THREE.AnimationMixer( @object ) + return if not @animations[num]? + action = @mixer.clipAction( @animations[num] ) + action.play() + + updateAnimation:(t)-> + if @mixer? + @mixer.update(t) + +class @M3DBox extends @M3DObject + constructor:(m3d)-> + super m3d + @geometry = new THREE.BoxGeometry(1,1,1) + @material = new THREE.MeshStandardMaterial + color: 0xFFFFFF + + @setObject new THREE.Mesh @geometry,@material + +class @M3DSphere extends @M3DObject + constructor:(m3d)-> + super m3d + @geometry = new THREE.SphereGeometry(.5,12,12) + @material = new THREE.MeshStandardMaterial + color: 0xFFFFFF + + @setObject new THREE.Mesh @geometry,@material + +class @M3DPlane extends @M3DObject + constructor:(m3d,width=1,height=1)-> + super m3d + @geometry = new THREE.PlaneGeometry(width,height) + @material = new THREE.MeshStandardMaterial + color: 0xFFFFFF + + @setObject new THREE.Mesh @geometry,@material + +class M3DModelLoad extends @M3DObject + constructor:(m3d,asset)-> + super(m3d) + @ready = false + url = m3d.runtime.getAssetURL asset + loader = new THREE.GLTFLoader + loader.load url,(glb)=> + @animations = glb.animations + @object = glb.scene + @rotation = @object.rotation + @position = @object.position + @scale = @object.scale + @ready = true + + +class M3DTexture + constructor:(@m3d,image)-> + image = M3DResolveImage @m3d,image + + @texture = new THREE.Texture image + @texture.needsUpdate = true + +class M3DCubeTexture + constructor:(@m3d,px,nx,py,ny,pz,nz)-> + px = M3DResolveImage @m3d,px + nx = M3DResolveImage @m3d,nx + py = M3DResolveImage @m3d,py + ny = M3DResolveImage @m3d,ny + pz = M3DResolveImage @m3d,pz + nz = M3DResolveImage @m3d,nz + + @texture = new THREE.CubeTexture [px,nx,py,ny,pz,nz] + @texture.needsUpdate = true + +@M3DConvertColor = (rgb)-> + try + s = rgb.split("(")[1].split(")")[0] + s = s.split(",") + if s.length == 3 + return new THREE.Color(s[0]/255,s[1]/255,s[2]/255) + catch err + return new THREE.Color 0,0,0 + +@M3DResolveImage = (m3d,img)-> + if m3d.runtime.sprites[img]? + m3d.runtime.sprites[img].frames[0].canvas diff --git a/static/js/runtime/m3d/m3d.js b/static/js/runtime/m3d/m3d.js new file mode 100644 index 00000000..eb02708b --- /dev/null +++ b/static/js/runtime/m3d/m3d.js @@ -0,0 +1,331 @@ +var M3DCubeTexture, M3DModelLoad, M3DTexture, + extend = function(child, parent) { for (var key in parent) { if (hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor(); child.__super__ = parent.prototype; return child; }, + hasProp = {}.hasOwnProperty; + +this.M3D = (function() { + function M3D(runtime) { + this.runtime = runtime; + } + + M3D.prototype.createScene = function() { + return new M3DScene(this); + }; + + M3D.prototype.createCamera = function() { + return new M3DCamera(this); + }; + + M3D.prototype.createBox = function() { + return new M3DBox(this); + }; + + M3D.prototype.createSphere = function() { + return new M3DSphere(this); + }; + + M3D.prototype.createPlane = function(width, height) { + return new M3DPlane(this, width, height); + }; + + M3D.prototype.createTexture = function(image) { + return new M3DTexture(this, image); + }; + + M3D.prototype.createCubeTexture = function(px, nx, py, ny, pz, nz) { + return new M3DCubeTexture(this, px, nx, py, ny, pz, nz); + }; + + M3D.prototype.loadImage = function() {}; + + M3D.prototype.loadModel = function(asset) { + return new M3DModelLoad(this, asset); + }; + + M3D.prototype.createDirectionalLight = function() { + return new M3DDirectionalLight; + }; + + return M3D; + +})(); + +this.M3DCamera = (function() { + function M3DCamera() { + this.camera = new THREE.PerspectiveCamera(45, 1, 1, 1000); + this.position = this.camera.position; + } + + M3DCamera.prototype.lookAt = function(x, y, z) { + return this.camera.lookAt(x, y, z); + }; + + return M3DCamera; + +})(); + +this.M3DScene = (function() { + function M3DScene() { + this.scene = new THREE.Scene(); + } + + M3DScene.prototype.setFog = function(color, density) { + if (this.scene.fog == null) { + return this.scene.fog = new THREE.FogExp2(M3DConvertColor(color), density); + } else { + if (color != null) { + this.scene.fog.color = M3DConvertColor(color); + } + if (density != null) { + return this.scene.fog.density = density; + } + } + }; + + M3DScene.prototype.setAmbientLight = function(color) { + color = M3DConvertColor(color); + if (this.ambient_light == null) { + this.ambient_light = new THREE.AmbientLight(color); + return this.scene.add(this.ambient_light); + } else { + return this.ambient_light.color = color; + } + }; + + M3DScene.prototype.add = function(object) { + if ((object != null) && (object.object != null)) { + this.scene.add(object.object); + } + }; + + M3DScene.prototype.setBackground = function(rgb) { + this.scene.background = M3DConvertColor(rgb); + }; + + M3DScene.prototype.setEnvironment = function(cube_texture) { + return this.scene.environment = cube_texture.texture; + }; + + return M3DScene; + +})(); + +this.M3DDirectionalLight = (function() { + function M3DDirectionalLight(color, intensity) { + if (color == null) { + color = 0xFFFFFF; + } + if (intensity == null) { + intensity = 1; + } + this.object = new THREE.DirectionalLight(color, intensity); + this.position = this.object.position; + } + + M3DDirectionalLight.prototype.setShadowFrame = function(left, right, bottom, top) { + this.object.castShadow = true; + this.object.shadow.camera.left = left; + this.object.shadow.camera.right = right; + this.object.shadow.camera.bottom = bottom; + return this.object.shadow.camera.top = top; + }; + + return M3DDirectionalLight; + +})(); + +this.M3DObject = (function() { + function M3DObject(m3d1) { + this.m3d = m3d1; + } + + M3DObject.prototype.setObject = function(object1) { + this.object = object1; + this.rotation = this.object.rotation; + this.position = this.object.position; + return this.scale = this.object.scale; + }; + + M3DObject.prototype.setColor = function(rgb) { + this.material.color = M3DConvertColor(rgb); + }; + + M3DObject.prototype.setRotationOrder = function(order) { + if (this.object != null) { + return this.object.rotation.order = order; + } + }; + + M3DObject.prototype.setCastShadow = function(cast) { + return this.object.castShadow = cast !== 0; + }; + + M3DObject.prototype.setReceiveShadow = function(receive) { + return this.object.receiveShadow = receive !== 0; + }; + + M3DObject.prototype.clone = function() { + var o; + o = new M3DObject(this.m3d); + o.setObject(SkeletonUtils.clone(this.object)); + o.animations = this.animations; + return o; + }; + + M3DObject.prototype.setMap = function(texture) { + this.texture = texture; + this.object.material.map = this.texture.texture; + return this.object.material.needsUpdate = true; + }; + + M3DObject.prototype.startAnimation = function(num) { + var action; + if (this.animations == null) { + return; + } + if (this.mixer == null) { + this.mixer = new THREE.AnimationMixer(this.object); + } + if (this.animations[num] == null) { + return; + } + action = this.mixer.clipAction(this.animations[num]); + return action.play(); + }; + + M3DObject.prototype.updateAnimation = function(t) { + if (this.mixer != null) { + return this.mixer.update(t); + } + }; + + return M3DObject; + +})(); + +this.M3DBox = (function(superClass) { + extend(M3DBox, superClass); + + function M3DBox(m3d) { + M3DBox.__super__.constructor.call(this, m3d); + this.geometry = new THREE.BoxGeometry(1, 1, 1); + this.material = new THREE.MeshStandardMaterial({ + color: 0xFFFFFF + }); + this.setObject(new THREE.Mesh(this.geometry, this.material)); + } + + return M3DBox; + +})(this.M3DObject); + +this.M3DSphere = (function(superClass) { + extend(M3DSphere, superClass); + + function M3DSphere(m3d) { + M3DSphere.__super__.constructor.call(this, m3d); + this.geometry = new THREE.SphereGeometry(.5, 12, 12); + this.material = new THREE.MeshStandardMaterial({ + color: 0xFFFFFF + }); + this.setObject(new THREE.Mesh(this.geometry, this.material)); + } + + return M3DSphere; + +})(this.M3DObject); + +this.M3DPlane = (function(superClass) { + extend(M3DPlane, superClass); + + function M3DPlane(m3d, width, height) { + if (width == null) { + width = 1; + } + if (height == null) { + height = 1; + } + M3DPlane.__super__.constructor.call(this, m3d); + this.geometry = new THREE.PlaneGeometry(width, height); + this.material = new THREE.MeshStandardMaterial({ + color: 0xFFFFFF + }); + this.setObject(new THREE.Mesh(this.geometry, this.material)); + } + + return M3DPlane; + +})(this.M3DObject); + +M3DModelLoad = (function(superClass) { + extend(M3DModelLoad, superClass); + + function M3DModelLoad(m3d, asset) { + var loader, url; + M3DModelLoad.__super__.constructor.call(this, m3d); + this.ready = false; + url = m3d.runtime.getAssetURL(asset); + loader = new THREE.GLTFLoader; + loader.load(url, (function(_this) { + return function(glb) { + _this.animations = glb.animations; + _this.object = glb.scene; + _this.rotation = _this.object.rotation; + _this.position = _this.object.position; + _this.scale = _this.object.scale; + return _this.ready = true; + }; + })(this)); + } + + return M3DModelLoad; + +})(this.M3DObject); + +M3DTexture = (function() { + function M3DTexture(m3d1, image) { + this.m3d = m3d1; + image = M3DResolveImage(this.m3d, image); + this.texture = new THREE.Texture(image); + this.texture.needsUpdate = true; + } + + return M3DTexture; + +})(); + +M3DCubeTexture = (function() { + function M3DCubeTexture(m3d1, px, nx, py, ny, pz, nz) { + this.m3d = m3d1; + px = M3DResolveImage(this.m3d, px); + nx = M3DResolveImage(this.m3d, nx); + py = M3DResolveImage(this.m3d, py); + ny = M3DResolveImage(this.m3d, ny); + pz = M3DResolveImage(this.m3d, pz); + nz = M3DResolveImage(this.m3d, nz); + this.texture = new THREE.CubeTexture([px, nx, py, ny, pz, nz]); + this.texture.needsUpdate = true; + } + + return M3DCubeTexture; + +})(); + +this.M3DConvertColor = function(rgb) { + var err, s; + try { + s = rgb.split("(")[1].split(")")[0]; + s = s.split(","); + if (s.length === 3) { + return new THREE.Color(s[0] / 255, s[1] / 255, s[2] / 255); + } + } catch (error) { + err = error; + return new THREE.Color(0, 0, 0); + } +}; + +this.M3DResolveImage = function(m3d, img) { + if (m3d.runtime.sprites[img] != null) { + return m3d.runtime.sprites[img].frames[0].canvas; + } +}; diff --git a/static/js/runtime/m3d/screen3d.coffee b/static/js/runtime/m3d/screen3d.coffee new file mode 100644 index 00000000..11c07413 --- /dev/null +++ b/static/js/runtime/m3d/screen3d.coffee @@ -0,0 +1,221 @@ +class @Screen3D + constructor:(@runtime)-> + @renderer = new THREE.WebGLRenderer + antialias: true + @renderer.setPixelRatio( window.devicePixelRatio ) + @renderer.setSize 1080,1920 + + @canvas = @renderer.domElement + + @touches = {} + @mouse = + x: -10000 + y: -10000 + pressed: 0 + left: 0 + middle: 0 + right: 0 + + getInterface:()-> + return @interface if @interface? + screen = @ + @interface = + width: @width + height: @height + render: (scene,camera)->screen.render(scene,camera) + + updateInterface:()-> + @interface.width = @width + @interface.height = @height + + initDraw:()-> + + clear:()-> + + resize:()-> + cw = window.innerWidth + ch = window.innerHeight + + ratio = { + "4x3": 4/3 + "16x9": 16/9 + "2x1": 2/1 + "1x1": 1/1 + ">4x3": 4/3 + ">16x9": 16/9 + ">2x1": 2/1 + ">1x1": 1/1 + }[@runtime.aspect] + + min = @runtime.aspect.startsWith(">") + + if ratio? + if min + switch @runtime.orientation + when "portrait" + ratio = Math.max(ratio,ch/cw) + when "landscape" + ratio = Math.max(ratio,cw/ch) + else + if ch>cw + ratio = Math.max(ratio,ch/cw) + else + ratio = Math.max(ratio,cw/ch) + + switch @runtime.orientation + when "portrait" + r = Math.min(cw,ch/ratio)/cw + w = cw*r + h = cw*r*ratio + + when "landscape" + r = Math.min(cw/ratio,ch)/ch + w = ch*r*ratio + h = ch*r + + else + if cw>ch + r = Math.min(cw/ratio,ch)/ch + w = ch*r*ratio + h = ch*r + else + r = Math.min(cw,ch/ratio)/cw + w = cw*r + h = cw*r*ratio + else + w = cw + h = ch + + @canvas.style["margin-top"] = Math.round((ch-h)/2)+"px" + @canvas.style.width = Math.round(w)+"px" + @canvas.style.height = Math.round(h)+"px" + + @camera_aspect = w/h + @update_camera = true + + @renderer.setSize( w,h ) + + render:(scene,camera)-> + if @update_camera + camera.camera.aspect = @camera_aspect + camera.camera.updateProjectionMatrix() + @update_camera = false + + #@renderer.shadowMap.enabled = true + #@renderer.shadowMap.type = THREE.PCFSoftShadowMap + + @renderer.render scene.scene,camera.camera + + startControl:(@element)-> + @canvas.addEventListener "touchstart", (event) => @touchStart(event) + @canvas.addEventListener "touchmove", (event) => @touchMove(event) + document.addEventListener "touchend" , (event) => @touchRelease(event) + document.addEventListener "touchcancel" , (event) => @touchRelease(event) + + @canvas.addEventListener "mousedown", (event) => @mouseDown(event) + @canvas.addEventListener "mousemove", (event) => @mouseMove(event) + document.addEventListener "mouseup", (event) => @mouseUp(event) + + @ratio = devicePixelRatio + + touchStart:(event)-> + event.preventDefault() + event.stopPropagation() + b = @canvas.getBoundingClientRect() + for i in [0..event.changedTouches.length-1] by 1 + t = event.changedTouches[i] + min = Math.min @canvas.clientWidth,@canvas.clientHeight + x = (t.clientX-b.left-@canvas.clientWidth/2)/min*200 + y = (@canvas.clientHeight/2-(t.clientY-b.top))/min*200 + @touches[t.identifier] = + x: x + y: y + @mouse.x = x + @mouse.y = y + @mouse.pressed = 1 + @mouse.left = 1 + false + + touchMove:(event)-> + event.preventDefault() + event.stopPropagation() + b = @canvas.getBoundingClientRect() + for i in [0..event.changedTouches.length-1] by 1 + t = event.changedTouches[i] + if @touches[t.identifier]? + min = Math.min @canvas.clientWidth,@canvas.clientHeight + x = (t.clientX-b.left-@canvas.clientWidth/2)/min*200 + y = (@canvas.clientHeight/2-(t.clientY-b.top))/min*200 + @touches[t.identifier].x = x + @touches[t.identifier].y = y + @mouse.x = x + @mouse.y = y + false + + touchRelease:(event)-> + for i in [0..event.changedTouches.length-1] by 1 + t = event.changedTouches[i] + x = (t.clientX-@canvas.offsetLeft)*@ratio + y = (t.clientY-@canvas.offsetTop)*@ratio + delete @touches[t.identifier] + + @mouse.pressed = 0 + @mouse.left = 0 + @mouse.right = 0 + @mouse.middle = 0 + false + + mouseDown:(event)-> + @mousepressed = true + b = @canvas.getBoundingClientRect() + min = Math.min @canvas.clientWidth,@canvas.clientHeight + x = (event.clientX-b.left-@canvas.clientWidth/2)/min*200 + y = (@canvas.clientHeight/2-(event.clientY-b.top))/min*200 + @touches["mouse"] = + x: x + y: y + #console.info @touches["mouse"] + + @mouse.x = x + @mouse.y = y + switch event.button + when 0 then @mouse.left = 1 + when 1 then @mouse.middle = 1 + when 2 then @mouse.right = 1 + + @mouse.pressed = Math.min(1,@mouse.left+@mouse.right+@mouse.middle) + false + + mouseMove:(event)-> + event.preventDefault() + b = @canvas.getBoundingClientRect() + min = Math.min @canvas.clientWidth,@canvas.clientHeight + x = (event.clientX-b.left-@canvas.clientWidth/2)/min*200 + y = (@canvas.clientHeight/2-(event.clientY-b.top))/min*200 + if @touches["mouse"]? + @touches["mouse"].x = x + @touches["mouse"].y = y + + @mouse.x = x + @mouse.y = y + + false + + mouseUp:(event)-> + delete @touches["mouse"] + + b = @canvas.getBoundingClientRect() + min = Math.min @canvas.clientWidth,@canvas.clientHeight + x = (event.clientX-b.left-@canvas.clientWidth/2)/min*200 + y = (@canvas.clientHeight/2-(event.clientY-b.top))/min*200 + + @mouse.x = x + @mouse.y = y + switch event.button + when 0 then @mouse.left = 0 + when 1 then @mouse.middle = 0 + when 2 then @mouse.right = 0 + + @mouse.pressed = Math.min(1,@mouse.left+@mouse.right+@mouse.middle) + + false diff --git a/static/js/runtime/m3d/screen3d.js b/static/js/runtime/m3d/screen3d.js new file mode 100644 index 00000000..ac2b7bb2 --- /dev/null +++ b/static/js/runtime/m3d/screen3d.js @@ -0,0 +1,285 @@ +this.Screen3D = (function() { + function Screen3D(runtime) { + this.runtime = runtime; + this.renderer = new THREE.WebGLRenderer({ + antialias: true + }); + this.renderer.setPixelRatio(window.devicePixelRatio); + this.renderer.setSize(1080, 1920); + this.canvas = this.renderer.domElement; + this.touches = {}; + this.mouse = { + x: -10000, + y: -10000, + pressed: 0, + left: 0, + middle: 0, + right: 0 + }; + } + + Screen3D.prototype.getInterface = function() { + var screen; + if (this["interface"] != null) { + return this["interface"]; + } + screen = this; + return this["interface"] = { + width: this.width, + height: this.height, + render: function(scene, camera) { + return screen.render(scene, camera); + } + }; + }; + + Screen3D.prototype.updateInterface = function() { + this["interface"].width = this.width; + return this["interface"].height = this.height; + }; + + Screen3D.prototype.initDraw = function() {}; + + Screen3D.prototype.clear = function() {}; + + Screen3D.prototype.resize = function() { + var ch, cw, h, min, r, ratio, w; + cw = window.innerWidth; + ch = window.innerHeight; + ratio = { + "4x3": 4 / 3, + "16x9": 16 / 9, + "2x1": 2 / 1, + "1x1": 1 / 1, + ">4x3": 4 / 3, + ">16x9": 16 / 9, + ">2x1": 2 / 1, + ">1x1": 1 / 1 + }[this.runtime.aspect]; + min = this.runtime.aspect.startsWith(">"); + if (ratio != null) { + if (min) { + switch (this.runtime.orientation) { + case "portrait": + ratio = Math.max(ratio, ch / cw); + break; + case "landscape": + ratio = Math.max(ratio, cw / ch); + break; + default: + if (ch > cw) { + ratio = Math.max(ratio, ch / cw); + } else { + ratio = Math.max(ratio, cw / ch); + } + } + } + switch (this.runtime.orientation) { + case "portrait": + r = Math.min(cw, ch / ratio) / cw; + w = cw * r; + h = cw * r * ratio; + break; + case "landscape": + r = Math.min(cw / ratio, ch) / ch; + w = ch * r * ratio; + h = ch * r; + break; + default: + if (cw > ch) { + r = Math.min(cw / ratio, ch) / ch; + w = ch * r * ratio; + h = ch * r; + } else { + r = Math.min(cw, ch / ratio) / cw; + w = cw * r; + h = cw * r * ratio; + } + } + } else { + w = cw; + h = ch; + } + this.canvas.style["margin-top"] = Math.round((ch - h) / 2) + "px"; + this.canvas.style.width = Math.round(w) + "px"; + this.canvas.style.height = Math.round(h) + "px"; + this.camera_aspect = w / h; + this.update_camera = true; + return this.renderer.setSize(w, h); + }; + + Screen3D.prototype.render = function(scene, camera) { + if (this.update_camera) { + camera.camera.aspect = this.camera_aspect; + camera.camera.updateProjectionMatrix(); + this.update_camera = false; + } + return this.renderer.render(scene.scene, camera.camera); + }; + + Screen3D.prototype.startControl = function(element) { + this.element = element; + this.canvas.addEventListener("touchstart", (function(_this) { + return function(event) { + return _this.touchStart(event); + }; + })(this)); + this.canvas.addEventListener("touchmove", (function(_this) { + return function(event) { + return _this.touchMove(event); + }; + })(this)); + document.addEventListener("touchend", (function(_this) { + return function(event) { + return _this.touchRelease(event); + }; + })(this)); + document.addEventListener("touchcancel", (function(_this) { + return function(event) { + return _this.touchRelease(event); + }; + })(this)); + this.canvas.addEventListener("mousedown", (function(_this) { + return function(event) { + return _this.mouseDown(event); + }; + })(this)); + this.canvas.addEventListener("mousemove", (function(_this) { + return function(event) { + return _this.mouseMove(event); + }; + })(this)); + document.addEventListener("mouseup", (function(_this) { + return function(event) { + return _this.mouseUp(event); + }; + })(this)); + return this.ratio = devicePixelRatio; + }; + + Screen3D.prototype.touchStart = function(event) { + var b, i, j, min, ref, t, x, y; + event.preventDefault(); + event.stopPropagation(); + b = this.canvas.getBoundingClientRect(); + for (i = j = 0, ref = event.changedTouches.length - 1; j <= ref; i = j += 1) { + t = event.changedTouches[i]; + min = Math.min(this.canvas.clientWidth, this.canvas.clientHeight); + x = (t.clientX - b.left - this.canvas.clientWidth / 2) / min * 200; + y = (this.canvas.clientHeight / 2 - (t.clientY - b.top)) / min * 200; + this.touches[t.identifier] = { + x: x, + y: y + }; + this.mouse.x = x; + this.mouse.y = y; + this.mouse.pressed = 1; + this.mouse.left = 1; + } + return false; + }; + + Screen3D.prototype.touchMove = function(event) { + var b, i, j, min, ref, t, x, y; + event.preventDefault(); + event.stopPropagation(); + b = this.canvas.getBoundingClientRect(); + for (i = j = 0, ref = event.changedTouches.length - 1; j <= ref; i = j += 1) { + t = event.changedTouches[i]; + if (this.touches[t.identifier] != null) { + min = Math.min(this.canvas.clientWidth, this.canvas.clientHeight); + x = (t.clientX - b.left - this.canvas.clientWidth / 2) / min * 200; + y = (this.canvas.clientHeight / 2 - (t.clientY - b.top)) / min * 200; + this.touches[t.identifier].x = x; + this.touches[t.identifier].y = y; + this.mouse.x = x; + this.mouse.y = y; + } + } + return false; + }; + + Screen3D.prototype.touchRelease = function(event) { + var i, j, ref, t, x, y; + for (i = j = 0, ref = event.changedTouches.length - 1; j <= ref; i = j += 1) { + t = event.changedTouches[i]; + x = (t.clientX - this.canvas.offsetLeft) * this.ratio; + y = (t.clientY - this.canvas.offsetTop) * this.ratio; + delete this.touches[t.identifier]; + this.mouse.pressed = 0; + this.mouse.left = 0; + this.mouse.right = 0; + this.mouse.middle = 0; + } + return false; + }; + + Screen3D.prototype.mouseDown = function(event) { + var b, min, x, y; + this.mousepressed = true; + b = this.canvas.getBoundingClientRect(); + min = Math.min(this.canvas.clientWidth, this.canvas.clientHeight); + x = (event.clientX - b.left - this.canvas.clientWidth / 2) / min * 200; + y = (this.canvas.clientHeight / 2 - (event.clientY - b.top)) / min * 200; + this.touches["mouse"] = { + x: x, + y: y + }; + this.mouse.x = x; + this.mouse.y = y; + switch (event.button) { + case 0: + this.mouse.left = 1; + break; + case 1: + this.mouse.middle = 1; + break; + case 2: + this.mouse.right = 1; + } + this.mouse.pressed = Math.min(1, this.mouse.left + this.mouse.right + this.mouse.middle); + return false; + }; + + Screen3D.prototype.mouseMove = function(event) { + var b, min, x, y; + event.preventDefault(); + b = this.canvas.getBoundingClientRect(); + min = Math.min(this.canvas.clientWidth, this.canvas.clientHeight); + x = (event.clientX - b.left - this.canvas.clientWidth / 2) / min * 200; + y = (this.canvas.clientHeight / 2 - (event.clientY - b.top)) / min * 200; + if (this.touches["mouse"] != null) { + this.touches["mouse"].x = x; + this.touches["mouse"].y = y; + } + this.mouse.x = x; + this.mouse.y = y; + return false; + }; + + Screen3D.prototype.mouseUp = function(event) { + var b, min, x, y; + delete this.touches["mouse"]; + b = this.canvas.getBoundingClientRect(); + min = Math.min(this.canvas.clientWidth, this.canvas.clientHeight); + x = (event.clientX - b.left - this.canvas.clientWidth / 2) / min * 200; + y = (this.canvas.clientHeight / 2 - (event.clientY - b.top)) / min * 200; + this.mouse.x = x; + this.mouse.y = y; + switch (event.button) { + case 0: + this.mouse.left = 0; + break; + case 1: + this.mouse.middle = 0; + break; + case 2: + this.mouse.right = 0; + } + this.mouse.pressed = Math.min(1, this.mouse.left + this.mouse.right + this.mouse.middle); + return false; + }; + + return Screen3D; + +})(); diff --git a/static/js/runtime/map.coffee b/static/js/runtime/map.coffee new file mode 100644 index 00000000..66957fee --- /dev/null +++ b/static/js/runtime/map.coffee @@ -0,0 +1,181 @@ +class @MicroMap + constructor:(@width,@height,@block_width,@block_height,@sprites)-> + @map = [] + if @width? and typeof @width == "string" + @ready = false + req = new XMLHttpRequest() + req.onreadystatechange = (event) => + if req.readyState == XMLHttpRequest.DONE + @ready = true + if req.status == 200 + @load req.responseText,@sprites + @update() + + if @loaded? + @loaded() + + req.open "GET",@width + req.send() + + @width = 10 + @height = 10 + @block_width = 10 + @block_height = 10 + else + @ready = true + + @clear() + @update() + + clear:()-> + for j in [0..@height-1] by 1 + for i in [0..@width-1] by 1 + @map[i+j*@width] = null + return + + set:(x,y,ref)-> + if x>=0 and x<@width and y>=0 and y<@height + @map[x+y*@width] = ref + @needs_update = true + + get:(x,y)-> + return "" if x<0 or y<0 or x>=@width or y>=@height + @map[x+y*@width] or "" + + getCanvas:()-> + if not @canvas? or @needs_update + @update() + @canvas + + update:()-> + @needs_update = false + if not @canvas? + @canvas = document.createElement "canvas" + + if @canvas.width != @width*@block_width or @canvas.height != @height*@block_height + @canvas.width = @width*@block_width + @canvas.height = @height*@block_height + + context = @canvas.getContext "2d" + context.clearRect 0,0,@canvas.width,@canvas.height + + for j in [0..@height-1] by 1 + for i in [0..@width-1] by 1 + index = i+(@height-1-j)*@width + s = @map[index] + if s? and s.length>0 + s = s.split(":") + sprite = @sprites[s[0]] + if sprite? and sprite.frames[0]? + if s[1]? + xy = s[1].split(",") + tx = xy[0]*@block_width + ty = xy[1]*@block_height + c = sprite.frames[0].canvas + if c? and c.width>0 and c.height>0 + context.drawImage(c,tx,ty,@block_width,@block_height,@block_width*i,@block_height*j,@block_width,@block_height) + else + c = sprite.frames[0].canvas + if c? and c.width>0 and c.height>0 + context.drawImage(c,@block_width*i,@block_height*j) + + return + + resize:(w,h,@block_width=@block_width,@block_height=@block_height)-> + map = [] + for j in [0..h-1] by 1 + for i in [0..w-1] by 1 + if j<@height and i<@width + map[i+j*w] = @map[i+j*@width] + else + map[i+j*w] = null + + @map = map + @width = w + @height = h + + save:()-> + index = 1 + list = [0] + table = {} + for j in [0..@height-1] by 1 + for i in [0..@width-1] by 1 + s = @map[i+j*@width] + if s? and s.length>0 and not table[s]? + list.push s + table[s] = index++ + + map = [] + for j in [0..@height-1] by 1 + for i in [0..@width-1] by 1 + s = @map[i+j*@width] + map[i+j*@width] = if s? and s.length>0 then table[s] else 0 + + data = + width: @width + height: @height + block_width: @block_width + block_height: @block_height + sprites: list + data: map + + JSON.stringify data + + loadFile:(url)-> + req = new XMLHttpRequest() + req.onreadystatechange = (event) => + if req.readyState == XMLHttpRequest.DONE + if req.status == 200 + @load req.responseText,@sprites + @update() + + req.open "GET",url + req.send() + + load:(data,sprites)-> + data = JSON.parse data + @width = data.width + @height = data.height + @block_width = data.block_width + @block_height = data.block_height + + for j in [0..data.height-1] by 1 + for i in [0..data.width-1] by 1 + s = data.data[i+j*data.width] + if s>0 + @map[i+j*data.width] = data.sprites[s] + else + @map[i+j*data.width] = null + + return + + @loadMap:(data,sprites)-> + data = JSON.parse data + map = new MicroMap(data.width,data.height,data.block_width,data.block_height,sprites) + for j in [0..data.height-1] by 1 + for i in [0..data.width-1] by 1 + s = data.data[i+j*data.width] + if s>0 + map.map[i+j*data.width] = data.sprites[s] + + map + + clone:()-> + map = new MicroMap(@width,@height,@block_width,@block_height,@sprites) + for j in [0..@height-1] by 1 + for i in [0..@width-1] by 1 + map.map[i+j*@width] = @map[i+j*@width] + + map.needs_update = true + map + + copyFrom:(map)-> + @width = map.width + @height = map.height + @block_width = map.block_width + @block_height = map.block_height + for j in [0..@height-1] by 1 + for i in [0..@width-1] by 1 + @map[i+j*@width] = map.map[i+j*@width] + @update() + @ diff --git a/static/js/runtime/map.js b/static/js/runtime/map.js new file mode 100644 index 00000000..b6a52787 --- /dev/null +++ b/static/js/runtime/map.js @@ -0,0 +1,241 @@ +this.MicroMap = (function() { + function MicroMap(width, height, block_width, block_height, sprites1) { + var req; + this.width = width; + this.height = height; + this.block_width = block_width; + this.block_height = block_height; + this.sprites = sprites1; + this.map = []; + if ((this.width != null) && typeof this.width === "string") { + this.ready = false; + req = new XMLHttpRequest(); + req.onreadystatechange = (function(_this) { + return function(event) { + if (req.readyState === XMLHttpRequest.DONE) { + _this.ready = true; + if (req.status === 200) { + _this.load(req.responseText, _this.sprites); + _this.update(); + } + if (_this.loaded != null) { + return _this.loaded(); + } + } + }; + })(this); + req.open("GET", this.width); + req.send(); + this.width = 10; + this.height = 10; + this.block_width = 10; + this.block_height = 10; + } else { + this.ready = true; + } + this.clear(); + this.update(); + } + + MicroMap.prototype.clear = function() { + var i, j, k, l, ref1, ref2; + for (j = k = 0, ref1 = this.height - 1; k <= ref1; j = k += 1) { + for (i = l = 0, ref2 = this.width - 1; l <= ref2; i = l += 1) { + this.map[i + j * this.width] = null; + } + } + }; + + MicroMap.prototype.set = function(x, y, ref) { + if (x >= 0 && x < this.width && y >= 0 && y < this.height) { + this.map[x + y * this.width] = ref; + return this.needs_update = true; + } + }; + + MicroMap.prototype.get = function(x, y) { + if (x < 0 || y < 0 || x >= this.width || y >= this.height) { + return ""; + } + return this.map[x + y * this.width] || ""; + }; + + MicroMap.prototype.getCanvas = function() { + if ((this.canvas == null) || this.needs_update) { + this.update(); + } + return this.canvas; + }; + + MicroMap.prototype.update = function() { + var c, context, i, index, j, k, l, ref1, ref2, s, sprite, tx, ty, xy; + this.needs_update = false; + if (this.canvas == null) { + this.canvas = document.createElement("canvas"); + } + if (this.canvas.width !== this.width * this.block_width || this.canvas.height !== this.height * this.block_height) { + this.canvas.width = this.width * this.block_width; + this.canvas.height = this.height * this.block_height; + } + context = this.canvas.getContext("2d"); + context.clearRect(0, 0, this.canvas.width, this.canvas.height); + for (j = k = 0, ref1 = this.height - 1; k <= ref1; j = k += 1) { + for (i = l = 0, ref2 = this.width - 1; l <= ref2; i = l += 1) { + index = i + (this.height - 1 - j) * this.width; + s = this.map[index]; + if ((s != null) && s.length > 0) { + s = s.split(":"); + sprite = this.sprites[s[0]]; + if ((sprite != null) && (sprite.frames[0] != null)) { + if (s[1] != null) { + xy = s[1].split(","); + tx = xy[0] * this.block_width; + ty = xy[1] * this.block_height; + c = sprite.frames[0].canvas; + if ((c != null) && c.width > 0 && c.height > 0) { + context.drawImage(c, tx, ty, this.block_width, this.block_height, this.block_width * i, this.block_height * j, this.block_width, this.block_height); + } + } else { + c = sprite.frames[0].canvas; + if ((c != null) && c.width > 0 && c.height > 0) { + context.drawImage(c, this.block_width * i, this.block_height * j); + } + } + } + } + } + } + }; + + MicroMap.prototype.resize = function(w, h, block_width, block_height) { + var i, j, k, l, map, ref1, ref2; + this.block_width = block_width != null ? block_width : this.block_width; + this.block_height = block_height != null ? block_height : this.block_height; + map = []; + for (j = k = 0, ref1 = h - 1; k <= ref1; j = k += 1) { + for (i = l = 0, ref2 = w - 1; l <= ref2; i = l += 1) { + if (j < this.height && i < this.width) { + map[i + j * w] = this.map[i + j * this.width]; + } else { + map[i + j * w] = null; + } + } + } + this.map = map; + this.width = w; + return this.height = h; + }; + + MicroMap.prototype.save = function() { + var data, i, index, j, k, l, list, m, map, n, ref1, ref2, ref3, ref4, s, table; + index = 1; + list = [0]; + table = {}; + for (j = k = 0, ref1 = this.height - 1; k <= ref1; j = k += 1) { + for (i = l = 0, ref2 = this.width - 1; l <= ref2; i = l += 1) { + s = this.map[i + j * this.width]; + if ((s != null) && s.length > 0 && (table[s] == null)) { + list.push(s); + table[s] = index++; + } + } + } + map = []; + for (j = m = 0, ref3 = this.height - 1; m <= ref3; j = m += 1) { + for (i = n = 0, ref4 = this.width - 1; n <= ref4; i = n += 1) { + s = this.map[i + j * this.width]; + map[i + j * this.width] = (s != null) && s.length > 0 ? table[s] : 0; + } + } + data = { + width: this.width, + height: this.height, + block_width: this.block_width, + block_height: this.block_height, + sprites: list, + data: map + }; + return JSON.stringify(data); + }; + + MicroMap.prototype.loadFile = function(url) { + var req; + req = new XMLHttpRequest(); + req.onreadystatechange = (function(_this) { + return function(event) { + if (req.readyState === XMLHttpRequest.DONE) { + if (req.status === 200) { + _this.load(req.responseText, _this.sprites); + return _this.update(); + } + } + }; + })(this); + req.open("GET", url); + return req.send(); + }; + + MicroMap.prototype.load = function(data, sprites) { + var i, j, k, l, ref1, ref2, s; + data = JSON.parse(data); + this.width = data.width; + this.height = data.height; + this.block_width = data.block_width; + this.block_height = data.block_height; + for (j = k = 0, ref1 = data.height - 1; k <= ref1; j = k += 1) { + for (i = l = 0, ref2 = data.width - 1; l <= ref2; i = l += 1) { + s = data.data[i + j * data.width]; + if (s > 0) { + this.map[i + j * data.width] = data.sprites[s]; + } else { + this.map[i + j * data.width] = null; + } + } + } + }; + + MicroMap.loadMap = function(data, sprites) { + var i, j, k, l, map, ref1, ref2, s; + data = JSON.parse(data); + map = new MicroMap(data.width, data.height, data.block_width, data.block_height, sprites); + for (j = k = 0, ref1 = data.height - 1; k <= ref1; j = k += 1) { + for (i = l = 0, ref2 = data.width - 1; l <= ref2; i = l += 1) { + s = data.data[i + j * data.width]; + if (s > 0) { + map.map[i + j * data.width] = data.sprites[s]; + } + } + } + return map; + }; + + MicroMap.prototype.clone = function() { + var i, j, k, l, map, ref1, ref2; + map = new MicroMap(this.width, this.height, this.block_width, this.block_height, this.sprites); + for (j = k = 0, ref1 = this.height - 1; k <= ref1; j = k += 1) { + for (i = l = 0, ref2 = this.width - 1; l <= ref2; i = l += 1) { + map.map[i + j * this.width] = this.map[i + j * this.width]; + } + } + map.needs_update = true; + return map; + }; + + MicroMap.prototype.copyFrom = function(map) { + var i, j, k, l, ref1, ref2; + this.width = map.width; + this.height = map.height; + this.block_width = map.block_width; + this.block_height = map.block_height; + for (j = k = 0, ref1 = this.height - 1; k <= ref1; j = k += 1) { + for (i = l = 0, ref2 = this.width - 1; l <= ref2; i = l += 1) { + this.map[i + j * this.width] = map.map[i + j * this.width]; + } + } + this.update(); + return this; + }; + + return MicroMap; + +})(); diff --git a/static/js/runtime/runtime.coffee b/static/js/runtime/runtime.coffee new file mode 100644 index 00000000..8db34c3c --- /dev/null +++ b/static/js/runtime/runtime.coffee @@ -0,0 +1,412 @@ +class @Runtime + constructor:(@url,@sources,@resources,@listener)-> + if window.graphics == "M3D" + @screen = new Screen3D @ + else + @screen = new Screen @ + + @audio = new AudioCore @ + @keyboard = new Keyboard() + @gamepad = new Gamepad() + @sprites = {} + @maps = {} + @sounds = {} + @music = {} + @touch = {} + @mouse = @screen.mouse + @previous_init = null + @random = new Random(0) + @orientation = window.orientation + @aspect = window.aspect + @report_errors = true + + @log = (text)=> + @listener.log text + + @update_memory = {} + + updateSource:(file,src,reinit=false)-> + return false if not @vm? + return false if src == @update_memory[file] + @update_memory[file] = src + @audio.cancelBeeps() + @screen.clear() + + try + parser = new Parser(src,file) + parser.parse() + if parser.error_info? + err = parser.error_info + err.type = "compile" + err.file = file + @listener.reportError err + return false + else + @listener.postMessage + name: "compile_success" + file: file + + @vm.run(parser.program) + @reportWarnings() + if @vm.error_info? + err = @vm.error_info + err.type = "init" + err.file = file + @listener.reportError err + return false + + init = @vm.context.global.init + if init? and init.source != @previous_init and reinit + @previous_init = init.source + @vm.call("init") + + return true + catch err + if @report_errors + console.error err + err.file = file + @listener.reportError err + return false + + start:()-> + for i in @resources.images + s = new Sprite(@url+"sprites/"+i.file+"?v="+i.version,null,i.properties) + name = i.file.split(".")[0] + s.name = name + @sprites[name] = s + s.loaded = ()=> + @updateMaps() + @checkStartReady() + + if Array.isArray(@resources.maps) + for m in @resources.maps + name = m.file.split(".")[0] + @maps[name] = new MicroMap(@url+"maps/#{m.file}?v=#{m.version}",0,0,0,@sprites) + @maps[name].loaded = ()=> + @checkStartReady() + + else if @resources.maps? + for key,value of @resources.maps + @updateMap(key,0,value) + + for s in @resources.sounds + name = s.file.split(".")[0] + s = new Sound(@audio,@url+"sounds/"+s.file+"?v="+s.version) + s.name = name + @sounds[name] = s + + for m in @resources.music + name = m.file.split(".")[0] + m = new Music(@audio,@url+"music/"+m.file+"?v="+m.version) + m.name = name + @music[name] = m + + return + + checkStartReady:()-> + if not @start_ready + for key,value of @sprites + return if not value.ready + + for key,value of @maps + return if not value.ready + + @start_ready = true + @startReady() + + startReady:()-> + meta = + print: (text)=> + if typeof text == "object" + text = Program.toString(text) + @listener.log(text) + + global = + screen: @screen.getInterface() + audio: @audio.getInterface() + keyboard: @keyboard.keyboard + gamepad: @gamepad.status + sprites: @sprites + sounds: @sounds + music: @music + maps: @maps + touch: @touch + mouse: @mouse + fonts: window.fonts + + if window.graphics == "M3D" + global.M3D = new M3D @ + + namespace = location.pathname + @vm = new MicroVM(meta,global,namespace,location.hash == "#transpiler") + for file,src of @sources + @updateSource(file,src,false) + + init = @vm.context.global.init + if init? + @previous_init = init.source + @vm.call("init") + + @dt = 1000/60 + @last_time = Date.now() + @current_frame = 0 + @floating_frame = 0 + requestAnimationFrame(()=>@timer()) + @screen.startControl() + + updateMaps:()-> + for key,map of @maps + map.needs_update = true + return + + runCommand:(command)-> + try + parser = new Parser(command) + parser.parse() + if parser.error_info? + err = parser.error_info + err.type = "compile" + @listener.reportError err + return 0 + + warnings = @vm.context.warnings + @vm.clearWarnings() + res = @vm.run(parser.program) + @reportWarnings() + @vm.context.warnings = warnings + if @vm.error_info? + err = @vm.error_info + err.type = "exec" + @listener.reportError err + res + catch err + @listener.reportError err + + projectFileUpdated:(type,file,version,data,properties)-> + switch type + when "sprites" + @updateSprite(file,version,data,properties) + when "maps" + @updateMap(file,version,data) + when "ms" + @updateCode(file,version,data) + + projectFileDeleted:(type,file)-> + switch type + when "sprites" + delete @sprites[file.substring(0,file.length-4)] + when "maps" + delete @maps[file.substring(0,file.length-5)] + + projectOptionsUpdated:(msg)-> + @orientation = msg.orientation + @aspect = msg.aspect + @screen.resize() + + updateSprite:(name,version,data,properties)-> + if data? + data = "data:image/png;base64,"+data + if @sprites[name]? + img = new Image + img.crossOrigin = "Anonymous" + img.src = data + img.onload = ()=> + @sprites[name].load(img,properties) + @updateMaps() + else + @sprites[name] = new Sprite(data,null,properties) + @sprites[name].name = name + @sprites[name].loaded = ()=> + @updateMaps() + else + if @sprites[name]? + img = new Image + img.crossOrigin = "Anonymous" + img.src = @url+"sprites/"+name+".png?v=#{version}" + img.onload = ()=> + @sprites[name].load(img,properties) + @updateMaps() + else + @sprites[name] = new Sprite(@url+"sprites/"+name+".png?v=#{version}",null,properties) + @sprites[name].name = name + @sprites[name].loaded = ()=> + @updateMaps() + + updateMap:(name,version,data)-> + if data? + m = @maps[name] + if m? + m.load(data,@sprites) + m.needs_update = true + else + m = new MicroMap(1,1,1,1,@sprites) + m.load data,@sprites + @maps[name] = m + else + url = @url+"maps/#{name}.json?v=#{version}" + m = @maps[name] + if m? + m.loadFile(url) + else + @maps[name] = new MicroMap(url,0,0,0,@sprites) + + updateCode:(name,version,data)-> + if data? + @sources[name] = data + if @vm? + @vm.clearWarnings() + @updateSource name,data,true + else + url = @url+"ms/#{name}.ms?v=#{version}" + + req = new XMLHttpRequest() + req.onreadystatechange = (event) => + if req.readyState == XMLHttpRequest.DONE + if req.status == 200 + @sources[name] = req.responseText + @updateSource(name,@sources[name],true) + + req.open "GET",url + req.send() + + stop:()-> + @stopped = true + @audio.cancelBeeps() + + resume:()-> + @stopped = false + requestAnimationFrame(()=>@timer()) + + timer:()-> + return if @stopped + requestAnimationFrame(()=>@timer()) + time = Date.now() + if Math.abs(time-@last_time)>1000 + @last_time = time-16 + + dt = time-@last_time + @dt = @dt*.99+dt*.01 + @last_time = time + + @floating_frame += @dt*60/1000 + ds = Math.min(30,Math.round(@floating_frame-@current_frame)) + for i in [1..ds] by 1 + @updateCall() + + @current_frame += ds + @drawCall() + #if ds == 0 + # console.info "frame missed" + #if @current_frame%60 == 0 + # console.info("fps: #{Math.round(1000/@dt)}") + + updateCall:()-> + @updateControls() + try + #time = Date.now() + @vm.call("update") + @reportWarnings() + #console.info "update time: "+(Date.now()-time) + if @vm.error_info? + err = @vm.error_info + err.type = "update" + @listener.reportError err + catch err + @listener.reportError err if @report_errors + + drawCall:()-> + try + @screen.initDraw() + @screen.updateInterface() + + @vm.call("draw") + @reportWarnings() + if @vm.error_info? + err = @vm.error_info + err.type = "draw" + @listener.reportError err + catch err + @listener.reportError err if @report_errors + + reportWarnings:()-> + if @vm? + for key,value of @vm.context.warnings.invoking_non_function + if not value.reported + value.reported = true + @listener.reportError + error: "" + type: "non_function" + expression: value.expression + line: value.line + column: value.column + file: value.file + + for key,value of @vm.context.warnings.using_undefined_variable + if not value.reported + value.reported = true + @listener.reportError + error: "" + type: "undefined_variable" + expression: value.expression + line: value.line + column: value.column + file: value.file + + for key,value of @vm.context.warnings.assigning_field_to_undefined + if not value.reported + value.reported = true + @listener.reportError + error: "" + type: "assigning_undefined" + expression: value.expression + line: value.line + column: value.column + file: value.file + + updateControls:()-> + touches = Object.keys(@screen.touches) + @touch.touching = touches.length>0 + @touch.touches = [] + for key in touches + t = @screen.touches[key] + @touch.x = t.x + @touch.y = t.y + @touch.touches.push + x: t.x + y: t.y + id: key + + if @mouse.pressed and not @previous_mouse_pressed + @previous_mouse_pressed = true + @mouse.press = 1 + else + @mouse.press = 0 + + if not @mouse.pressed and @previous_mouse_pressed + @previous_mouse_pressed = false + @mouse.release = 1 + else + @mouse.release = 0 + + if @touch.touching and not @previous_touch + @previous_touch = true + @touch.press = 1 + else + @touch.press = 0 + + if not @touch.touching and @previous_touch + @previous_touch = false + @touch.release = 1 + else + @touch.release = 0 + + @gamepad.update() + @keyboard.update() + try + @vm.context.global.system.inputs.gamepad = if @gamepad.count>0 then 1 else 0 + catch err + + return + + getAssetURL:(asset)-> + @url+"assets/"+asset+".glb" diff --git a/static/js/runtime/runtime.js b/static/js/runtime/runtime.js new file mode 100644 index 00000000..5519b1e8 --- /dev/null +++ b/static/js/runtime/runtime.js @@ -0,0 +1,563 @@ +this.Runtime = (function() { + function Runtime(url1, sources, resources, listener) { + this.url = url1; + this.sources = sources; + this.resources = resources; + this.listener = listener; + if (window.graphics === "M3D") { + this.screen = new Screen3D(this); + } else { + this.screen = new Screen(this); + } + this.audio = new AudioCore(this); + this.keyboard = new Keyboard(); + this.gamepad = new Gamepad(); + this.sprites = {}; + this.maps = {}; + this.sounds = {}; + this.music = {}; + this.touch = {}; + this.mouse = this.screen.mouse; + this.previous_init = null; + this.random = new Random(0); + this.orientation = window.orientation; + this.aspect = window.aspect; + this.report_errors = true; + this.log = (function(_this) { + return function(text) { + return _this.listener.log(text); + }; + })(this); + this.update_memory = {}; + } + + Runtime.prototype.updateSource = function(file, src, reinit) { + var err, init, parser; + if (reinit == null) { + reinit = false; + } + if (this.vm == null) { + return false; + } + if (src === this.update_memory[file]) { + return false; + } + this.update_memory[file] = src; + this.audio.cancelBeeps(); + this.screen.clear(); + try { + parser = new Parser(src, file); + parser.parse(); + if (parser.error_info != null) { + err = parser.error_info; + err.type = "compile"; + err.file = file; + this.listener.reportError(err); + return false; + } else { + this.listener.postMessage({ + name: "compile_success", + file: file + }); + } + this.vm.run(parser.program); + this.reportWarnings(); + if (this.vm.error_info != null) { + err = this.vm.error_info; + err.type = "init"; + err.file = file; + this.listener.reportError(err); + return false; + } + init = this.vm.context.global.init; + if ((init != null) && init.source !== this.previous_init && reinit) { + this.previous_init = init.source; + this.vm.call("init"); + } + return true; + } catch (error) { + err = error; + if (this.report_errors) { + console.error(err); + err.file = file; + this.listener.reportError(err); + return false; + } + } + }; + + Runtime.prototype.start = function() { + var i, j, k, key, l, len, len1, len2, len3, m, n, name, ref, ref1, ref2, ref3, ref4, s, value; + ref = this.resources.images; + for (j = 0, len = ref.length; j < len; j++) { + i = ref[j]; + s = new Sprite(this.url + "sprites/" + i.file + "?v=" + i.version, null, i.properties); + name = i.file.split(".")[0]; + s.name = name; + this.sprites[name] = s; + s.loaded = (function(_this) { + return function() { + _this.updateMaps(); + return _this.checkStartReady(); + }; + })(this); + } + if (Array.isArray(this.resources.maps)) { + ref1 = this.resources.maps; + for (k = 0, len1 = ref1.length; k < len1; k++) { + m = ref1[k]; + name = m.file.split(".")[0]; + this.maps[name] = new MicroMap(this.url + ("maps/" + m.file + "?v=" + m.version), 0, 0, 0, this.sprites); + this.maps[name].loaded = (function(_this) { + return function() { + return _this.checkStartReady(); + }; + })(this); + } + } else if (this.resources.maps != null) { + ref2 = this.resources.maps; + for (key in ref2) { + value = ref2[key]; + this.updateMap(key, 0, value); + } + } + ref3 = this.resources.sounds; + for (l = 0, len2 = ref3.length; l < len2; l++) { + s = ref3[l]; + name = s.file.split(".")[0]; + s = new Sound(this.audio, this.url + "sounds/" + s.file + "?v=" + s.version); + s.name = name; + this.sounds[name] = s; + } + ref4 = this.resources.music; + for (n = 0, len3 = ref4.length; n < len3; n++) { + m = ref4[n]; + name = m.file.split(".")[0]; + m = new Music(this.audio, this.url + "music/" + m.file + "?v=" + m.version); + m.name = name; + this.music[name] = m; + } + }; + + Runtime.prototype.checkStartReady = function() { + var key, ref, ref1, value; + if (!this.start_ready) { + ref = this.sprites; + for (key in ref) { + value = ref[key]; + if (!value.ready) { + return; + } + } + ref1 = this.maps; + for (key in ref1) { + value = ref1[key]; + if (!value.ready) { + return; + } + } + this.start_ready = true; + return this.startReady(); + } + }; + + Runtime.prototype.startReady = function() { + var file, global, init, meta, namespace, ref, src; + meta = { + print: (function(_this) { + return function(text) { + if (typeof text === "object") { + text = Program.toString(text); + } + return _this.listener.log(text); + }; + })(this) + }; + global = { + screen: this.screen.getInterface(), + audio: this.audio.getInterface(), + keyboard: this.keyboard.keyboard, + gamepad: this.gamepad.status, + sprites: this.sprites, + sounds: this.sounds, + music: this.music, + maps: this.maps, + touch: this.touch, + mouse: this.mouse, + fonts: window.fonts + }; + if (window.graphics === "M3D") { + global.M3D = new M3D(this); + } + namespace = location.pathname; + this.vm = new MicroVM(meta, global, namespace, location.hash === "#transpiler"); + ref = this.sources; + for (file in ref) { + src = ref[file]; + this.updateSource(file, src, false); + } + init = this.vm.context.global.init; + if (init != null) { + this.previous_init = init.source; + this.vm.call("init"); + } + this.dt = 1000 / 60; + this.last_time = Date.now(); + this.current_frame = 0; + this.floating_frame = 0; + requestAnimationFrame((function(_this) { + return function() { + return _this.timer(); + }; + })(this)); + return this.screen.startControl(); + }; + + Runtime.prototype.updateMaps = function() { + var key, map, ref; + ref = this.maps; + for (key in ref) { + map = ref[key]; + map.needs_update = true; + } + }; + + Runtime.prototype.runCommand = function(command) { + var err, parser, res, warnings; + try { + parser = new Parser(command); + parser.parse(); + if (parser.error_info != null) { + err = parser.error_info; + err.type = "compile"; + this.listener.reportError(err); + return 0; + } + warnings = this.vm.context.warnings; + this.vm.clearWarnings(); + res = this.vm.run(parser.program); + this.reportWarnings(); + this.vm.context.warnings = warnings; + if (this.vm.error_info != null) { + err = this.vm.error_info; + err.type = "exec"; + this.listener.reportError(err); + } + return res; + } catch (error) { + err = error; + return this.listener.reportError(err); + } + }; + + Runtime.prototype.projectFileUpdated = function(type, file, version, data, properties) { + switch (type) { + case "sprites": + return this.updateSprite(file, version, data, properties); + case "maps": + return this.updateMap(file, version, data); + case "ms": + return this.updateCode(file, version, data); + } + }; + + Runtime.prototype.projectFileDeleted = function(type, file) { + switch (type) { + case "sprites": + return delete this.sprites[file.substring(0, file.length - 4)]; + case "maps": + return delete this.maps[file.substring(0, file.length - 5)]; + } + }; + + Runtime.prototype.projectOptionsUpdated = function(msg) { + this.orientation = msg.orientation; + this.aspect = msg.aspect; + return this.screen.resize(); + }; + + Runtime.prototype.updateSprite = function(name, version, data, properties) { + var img; + if (data != null) { + data = "data:image/png;base64," + data; + if (this.sprites[name] != null) { + img = new Image; + img.crossOrigin = "Anonymous"; + img.src = data; + return img.onload = (function(_this) { + return function() { + _this.sprites[name].load(img, properties); + return _this.updateMaps(); + }; + })(this); + } else { + this.sprites[name] = new Sprite(data, null, properties); + this.sprites[name].name = name; + return this.sprites[name].loaded = (function(_this) { + return function() { + return _this.updateMaps(); + }; + })(this); + } + } else { + if (this.sprites[name] != null) { + img = new Image; + img.crossOrigin = "Anonymous"; + img.src = this.url + "sprites/" + name + (".png?v=" + version); + return img.onload = (function(_this) { + return function() { + _this.sprites[name].load(img, properties); + return _this.updateMaps(); + }; + })(this); + } else { + this.sprites[name] = new Sprite(this.url + "sprites/" + name + (".png?v=" + version), null, properties); + this.sprites[name].name = name; + return this.sprites[name].loaded = (function(_this) { + return function() { + return _this.updateMaps(); + }; + })(this); + } + } + }; + + Runtime.prototype.updateMap = function(name, version, data) { + var m, url; + if (data != null) { + m = this.maps[name]; + if (m != null) { + m.load(data, this.sprites); + return m.needs_update = true; + } else { + m = new MicroMap(1, 1, 1, 1, this.sprites); + m.load(data, this.sprites); + return this.maps[name] = m; + } + } else { + url = this.url + ("maps/" + name + ".json?v=" + version); + m = this.maps[name]; + if (m != null) { + return m.loadFile(url); + } else { + return this.maps[name] = new MicroMap(url, 0, 0, 0, this.sprites); + } + } + }; + + Runtime.prototype.updateCode = function(name, version, data) { + var req, url; + if (data != null) { + this.sources[name] = data; + if (this.vm != null) { + this.vm.clearWarnings(); + } + return this.updateSource(name, data, true); + } else { + url = this.url + ("ms/" + name + ".ms?v=" + version); + req = new XMLHttpRequest(); + req.onreadystatechange = (function(_this) { + return function(event) { + if (req.readyState === XMLHttpRequest.DONE) { + if (req.status === 200) { + _this.sources[name] = req.responseText; + return _this.updateSource(name, _this.sources[name], true); + } + } + }; + })(this); + req.open("GET", url); + return req.send(); + } + }; + + Runtime.prototype.stop = function() { + this.stopped = true; + return this.audio.cancelBeeps(); + }; + + Runtime.prototype.resume = function() { + this.stopped = false; + return requestAnimationFrame((function(_this) { + return function() { + return _this.timer(); + }; + })(this)); + }; + + Runtime.prototype.timer = function() { + var ds, dt, i, j, ref, time; + if (this.stopped) { + return; + } + requestAnimationFrame((function(_this) { + return function() { + return _this.timer(); + }; + })(this)); + time = Date.now(); + if (Math.abs(time - this.last_time) > 1000) { + this.last_time = time - 16; + } + dt = time - this.last_time; + this.dt = this.dt * .99 + dt * .01; + this.last_time = time; + this.floating_frame += this.dt * 60 / 1000; + ds = Math.min(30, Math.round(this.floating_frame - this.current_frame)); + for (i = j = 1, ref = ds; j <= ref; i = j += 1) { + this.updateCall(); + } + this.current_frame += ds; + return this.drawCall(); + }; + + Runtime.prototype.updateCall = function() { + var err; + this.updateControls(); + try { + this.vm.call("update"); + this.reportWarnings(); + if (this.vm.error_info != null) { + err = this.vm.error_info; + err.type = "update"; + return this.listener.reportError(err); + } + } catch (error) { + err = error; + if (this.report_errors) { + return this.listener.reportError(err); + } + } + }; + + Runtime.prototype.drawCall = function() { + var err; + try { + this.screen.initDraw(); + this.screen.updateInterface(); + this.vm.call("draw"); + this.reportWarnings(); + if (this.vm.error_info != null) { + err = this.vm.error_info; + err.type = "draw"; + return this.listener.reportError(err); + } + } catch (error) { + err = error; + if (this.report_errors) { + return this.listener.reportError(err); + } + } + }; + + Runtime.prototype.reportWarnings = function() { + var key, ref, ref1, ref2, results, value; + if (this.vm != null) { + ref = this.vm.context.warnings.invoking_non_function; + for (key in ref) { + value = ref[key]; + if (!value.reported) { + value.reported = true; + this.listener.reportError({ + error: "", + type: "non_function", + expression: value.expression, + line: value.line, + column: value.column, + file: value.file + }); + } + } + ref1 = this.vm.context.warnings.using_undefined_variable; + for (key in ref1) { + value = ref1[key]; + if (!value.reported) { + value.reported = true; + this.listener.reportError({ + error: "", + type: "undefined_variable", + expression: value.expression, + line: value.line, + column: value.column, + file: value.file + }); + } + } + ref2 = this.vm.context.warnings.assigning_field_to_undefined; + results = []; + for (key in ref2) { + value = ref2[key]; + if (!value.reported) { + value.reported = true; + results.push(this.listener.reportError({ + error: "", + type: "assigning_undefined", + expression: value.expression, + line: value.line, + column: value.column, + file: value.file + })); + } else { + results.push(void 0); + } + } + return results; + } + }; + + Runtime.prototype.updateControls = function() { + var err, j, key, len, t, touches; + touches = Object.keys(this.screen.touches); + this.touch.touching = touches.length > 0; + this.touch.touches = []; + for (j = 0, len = touches.length; j < len; j++) { + key = touches[j]; + t = this.screen.touches[key]; + this.touch.x = t.x; + this.touch.y = t.y; + this.touch.touches.push({ + x: t.x, + y: t.y, + id: key + }); + } + if (this.mouse.pressed && !this.previous_mouse_pressed) { + this.previous_mouse_pressed = true; + this.mouse.press = 1; + } else { + this.mouse.press = 0; + } + if (!this.mouse.pressed && this.previous_mouse_pressed) { + this.previous_mouse_pressed = false; + this.mouse.release = 1; + } else { + this.mouse.release = 0; + } + if (this.touch.touching && !this.previous_touch) { + this.previous_touch = true; + this.touch.press = 1; + } else { + this.touch.press = 0; + } + if (!this.touch.touching && this.previous_touch) { + this.previous_touch = false; + this.touch.release = 1; + } else { + this.touch.release = 0; + } + this.gamepad.update(); + this.keyboard.update(); + try { + this.vm.context.global.system.inputs.gamepad = this.gamepad.count > 0 ? 1 : 0; + } catch (error) { + err = error; + } + }; + + Runtime.prototype.getAssetURL = function(asset) { + return this.url + "assets/" + asset + ".glb"; + }; + + return Runtime; + +})(); diff --git a/static/js/runtime/screen.coffee b/static/js/runtime/screen.coffee new file mode 100644 index 00000000..9706d44c --- /dev/null +++ b/static/js/runtime/screen.coffee @@ -0,0 +1,674 @@ +class @Screen + constructor:(@runtime)-> + @canvas = document.createElement "canvas" + @canvas.width = 1080 + @canvas.height = 1920 + @touches = {} + @mouse = + x: -10000 + y: -10000 + pressed: 0 + left: 0 + middle: 0 + right: 0 + + @translation_x = 0 + @translation_y = 0 + @anchor_x = 0 + @anchor_y = 0 + @supersampling = @previous_supersampling = 1 + @font = "BitCell" + @initContext() + + @cursor = "default" + + @canvas.addEventListener "mousemove",()=> + @last_mouse_move = Date.now() + if @cursor != "default" and @cursor_visibility == "auto" + @cursor = "default" + @canvas.style.cursor = "default" + + setInterval (()=>@checkMouseCursor()),1000 + @cursor_visibility = "auto" + + checkMouseCursor:()-> + if Date.now()>@last_mouse_move+4000 and @cursor_visibility == "auto" + if @cursor != "none" + @cursor = "none" + @canvas.style.cursor = "none" + + setCursorVisible:(visible)-> + @cursor_visibility = visible + if visible + @cursor = "default" + @canvas.style.cursor = "default" + else + @cursor = "none" + @canvas.style.cursor = "none" + + initContext:()-> + c = @canvas.getContext "2d",{alpha: false} + if c != @context + @context = c + else + @context.restore() + @context.save() + @context.translate @canvas.width/2,@canvas.height/2 + ratio = Math.min(@canvas.width/200,@canvas.height/200) + @context.scale ratio,ratio + @width = @canvas.width/ratio + @height = @canvas.height/ratio + @alpha = 1 + @line_width = 1 + @object_rotation = 0 + @object_scale_x = 1 + @object_scale_y = 1 + @context.lineCap = "round" + @blending = + normal: "source-over" + additive: "lighter" + + for b in ["source-over","source-in","source-out","source-atop","destination-over","destination-in","destination-out","destination-atop","lighter","copy","xor","multiply","screen","overlay","darken","lighten","color-dodge","color-burn","hard-light","soft-light","difference","exclusion","hue","saturation","color","luminosity"] + @blending[b] = b + return + + getInterface:()-> + return @interface if @interface? + screen = @ + @interface = + width: @width + height: @height + clear: (color)->screen.clear(color) + setColor: (color)->screen.setColor(color) + setAlpha: (alpha)->screen.setAlpha(alpha) + setBlending: (blending)->screen.setBlending(blending) + setLinearGradient: (x1,y1,x2,y2,c1,c2)->screen.setLinearGradient(x1,y1,x2,y2,c1,c2) + setRadialGradient: (x,y,radius,c1,c2)->screen.setRadialGradient(x,y,radius,c1,c2) + setFont: (font)->screen.setFont(font) + setTranslation: (tx,ty)->screen.setTranslation(tx,ty) + setDrawAnchor: (ax,ay)->screen.setDrawAnchor(ax,ay) + setDrawRotation: (rotation)->screen.setDrawRotation(rotation) + setDrawScale: (x,y)->screen.setDrawScale(x,y) + fillRect: (x,y,w,h,c)->screen.fillRect(x,y,w,h,c) + fillRoundRect: (x,y,w,h,r,c)->screen.fillRoundRect(x,y,w,h,r,c) + fillRound: (x,y,w,h,c)->screen.fillRound(x,y,w,h,c) + drawRect: (x,y,w,h,c)->screen.drawRect(x,y,w,h,c) + drawRoundRect: (x,y,w,h,r,c)->screen.drawRoundRect(x,y,w,h,r,c) + drawRound: (x,y,w,h,c)->screen.drawRound(x,y,w,h,c) + drawSprite: (sprite,x,y,w,h)->screen.drawSprite(sprite,x,y,w,h) + drawSpritePart: (sprite,sx,sy,sw,sh,x,y,w,h)->screen.drawSpritePart(sprite,sx,sy,sw,sh,x,y,w,h) + drawMap: (map,x,y,w,h)->screen.drawMap(map,x,y,w,h) + drawText: (text,x,y,size,color)->screen.drawText(text,x,y,size,color) + drawTextOutline: (text,x,y,size,color)->screen.drawTextOutline(text,x,y,size,color) + textWidth: (text,size)-> screen.textWidth(text,size) + setLineWidth: (width)->screen.setLineWidth(width) + setLineDash: (dash)->screen.setLineDash(dash) + drawLine: (x1,y1,x2,y2,color)->screen.drawLine(x1,y1,x2,y2,color) + drawPolygon: ()->screen.drawPolygon(arguments) + drawPolyline: ()->screen.drawPolyline(arguments) + fillPolygon: ()->screen.fillPolygon(arguments) + setCursorVisible: (visible)->screen.setCursorVisible(visible) + + updateInterface:()-> + @interface.width = @width + @interface.height = @height + + clear:(color)-> + c = @context.fillStyle + s = @context.strokeStyle + blending_save = @context.globalCompositeOperation + + @context.globalAlpha = 1 + @context.globalCompositeOperation = "source-over" + if color? + @setColor(color) + else + @context.fillStyle = "#000" + @context.fillRect -@width/2,-@height/2,@width,@height + + @context.fillStyle = c + @context.strokeStyle = s + @context.globalCompositeOperation = blending_save + + initDraw:()-> + @alpha = 1 + @line_width = 1 + if @supersampling != @previous_supersampling + @resize() + @previous_supersampling = @supersampling + + setColor:(color)-> + return if not color? + if not Number.isNaN(Number.parseInt(color)) + r = (Math.floor(color/100)%10)/9*255 + g = (Math.floor(color/10)%10)/9*255 + b = (Math.floor(color)%10)/9*255 + c = 0xFF000000 + c += r<<16 + c += g<<8 + c += b + c = "#"+c.toString(16).substring(2,8) + @context.fillStyle = c + @context.strokeStyle = c + else if typeof color == "string" + @context.fillStyle = color + @context.strokeStyle = color + + setAlpha:(@alpha)-> + + setBlending:(blending)-> + blending = @blending[blending or "normal"] or "source-over" + @context.globalCompositeOperation = blending + + setLineWidth:(@line_width)-> + + setLineDash:(dash)-> + if not Array.isArray(dash) + @context.setLineDash [] + else + @context.setLineDash dash + + setLinearGradient:(x1,y1,x2,y2,c1,c2)-> + grd = @context.createLinearGradient(x1,-y1,x2,-y2) + grd.addColorStop(0,c1) + grd.addColorStop(1,c2) + @context.fillStyle = grd + @context.strokeStyle = grd + + setRadialGradient:(x,y,radius,c1,c2)-> + grd = @context.createRadialGradient(x,-y,0,x,-y,radius) + grd.addColorStop(0,c1) + grd.addColorStop(1,c2) + @context.fillStyle = grd + @context.strokeStyle = grd + + setFont:(font)-> + @font = font or "Verdana" + + setTranslation:(@translation_x,@translation_y)-> + + setDrawAnchor:(@anchor_x,@anchor_y)-> + @anchor_x = 0 if typeof @anchor_x != "number" + @anchor_y = 0 if typeof @anchor_y != "number" + + setDrawRotation:(@object_rotation)-> + + setDrawScale:(@object_scale_x,@object_scale_y=@object_scale_x)-> + + initDrawOp:(x,y)-> + res = false + + if @translation_x != 0 or @translation_y != 0 + @context.save() + res = true + @context.translate x+@translation_x,y-@translation_y + + if @object_rotation != 0 or @object_scale_x != 1 or @object_scale_y != 1 + if not res + @context.save() + res = true + @context.translate x,y + + if @object_rotation != 0 + @context.rotate -@object_rotation/180*Math.PI + + if @object_scale_x != 1 or @object_scale_y != 1 + @context.scale @object_scale_x,@object_scale_y + + res + + closeDrawOp:(x,y)-> + #if @object_scale_x != 1 or @object_scale_y != 1 + # @context.scale 1/@object_scale_x,1/@object_scale_y + + #if @object_rotation != 0 + # @context.rotate -@object_rotation/180*Math.PI + #@context.translate -x,-y + @context.restore() + + fillRect:(x,y,w,h,color)-> + @setColor color + @context.globalAlpha = @alpha + if @initDrawOp(x,-y) + @context.fillRect -w/2-@anchor_x*w/2,-h/2+@anchor_y*h/2,w,h + @closeDrawOp(x,-y) + else + @context.fillRect x-w/2-@anchor_x*w/2,-y-h/2+@anchor_y*h/2,w,h + + fillRoundRect:(x,y,w,h,round=10,color)-> + @setColor color + @context.globalAlpha = @alpha + if @initDrawOp(x,-y) + @context.fillRoundRect -w/2-@anchor_x*w/2,-h/2+@anchor_y*h/2,w,h,round + @closeDrawOp(x,-y) + else + @context.fillRoundRect x-w/2-@anchor_x*w/2,-y-h/2+@anchor_y*h/2,w,h,round + + fillRound:(x,y,w,h,color)-> + @setColor color + @context.globalAlpha = @alpha + w = Math.abs(w) + h = Math.abs(h) + if @initDrawOp(x,-y) + @context.beginPath() + @context.ellipse -@anchor_x*w/2,0+@anchor_y*h/2,w/2,h/2,0,0,Math.PI*2,false + @context.fill() + @closeDrawOp(x,-y) + else + @context.beginPath() + @context.ellipse x-@anchor_x*w/2,-y+@anchor_y*h/2,w/2,h/2,0,0,Math.PI*2,false + @context.fill() + + drawRect:(x,y,w,h,color)-> + @setColor color + @context.globalAlpha = @alpha + @context.lineWidth = @line_width + if @initDrawOp(x,-y) + @context.strokeRect -w/2-@anchor_x*w/2,-h/2+@anchor_y*h/2,w,h + @closeDrawOp(x,-y) + else + @context.strokeRect x-w/2-@anchor_x*w/2,-y-h/2+@anchor_y*h/2,w,h + + drawRoundRect:(x,y,w,h,round=10,color)-> + @setColor color + @context.globalAlpha = @alpha + @context.lineWidth = @line_width + if @initDrawOp(x,-y) + @context.strokeRoundRect -w/2-@anchor_x*w/2,-h/2+@anchor_y*h/2,w,h,round + @closeDrawOp(x,-y) + else + @context.strokeRoundRect x-w/2-@anchor_x*w/2,-y-h/2+@anchor_y*h/2,w,h,round + + drawRound:(x,y,w,h,color)-> + @setColor color + @context.globalAlpha = @alpha + @context.lineWidth = @line_width + w = Math.abs(w) + h = Math.abs(h) + if @initDrawOp(x,-y) + @context.beginPath() + @context.ellipse 0-@anchor_x*w/2,0+@anchor_y*h/2,w/2,h/2,0,0,Math.PI*2,false + @context.stroke() + @closeDrawOp(x,-y) + else + @context.beginPath() + @context.ellipse x-@anchor_x*w/2,-y+@anchor_y*h/2,w/2,h/2,0,0,Math.PI*2,false + @context.stroke() + + drawLine:(x1,y1,x2,y2,color)-> + @setColor color + @context.globalAlpha = @alpha + @context.lineWidth = @line_width + transform = @initDrawOp 0,0 + @context.beginPath() + @context.moveTo x1,-y1 + @context.lineTo x2,-y2 + @context.stroke() + @closeDrawOp() if transform + + drawPolyline:(args)-> + if args.length>0 and args.length%2 == 1 and typeof args[args.length-1] == "string" + @setColor args[args.length-1] + + if Array.isArray args[0] + if args[1]? and typeof args[1] == "string" + @setColor args[1] + args = args[0] + + @context.globalAlpha = @alpha + @context.lineWidth = @line_width + return if args.length<4 + len = Math.floor(args.length/2) + transform = @initDrawOp 0,0 + @context.beginPath() + @context.moveTo args[0],-args[1] + for i in [1..len-1] + @context.lineTo args[i*2],-args[i*2+1] + + @context.stroke() + @closeDrawOp() if transform + + + drawPolygon:(args)-> + if args.length>0 and args.length%2 == 1 and typeof args[args.length-1] == "string" + @setColor args[args.length-1] + + if Array.isArray args[0] + if args[1]? and typeof args[1] == "string" + @setColor args[1] + args = args[0] + + @context.globalAlpha = @alpha + @context.lineWidth = @line_width + return if args.length<4 + len = Math.floor(args.length/2) + transform = @initDrawOp 0,0 + @context.beginPath() + @context.moveTo args[0],-args[1] + for i in [1..len-1] + @context.lineTo args[i*2],-args[i*2+1] + + @context.closePath() + @context.stroke() + @closeDrawOp() if transform + + fillPolygon:(args)-> + if args.length>0 and args.length%2 == 1 and typeof args[args.length-1] == "string" + @setColor args[args.length-1] + + if Array.isArray args[0] + if args[1]? and typeof args[1] == "string" + @setColor args[1] + args = args[0] + + @context.globalAlpha = @alpha + @context.lineWidth = @line_width + return if args.length<4 + len = Math.floor(args.length/2) + transform = @initDrawOp 0,0 + @context.beginPath() + @context.moveTo args[0],-args[1] + for i in [1..len-1] + @context.lineTo args[i*2],-args[i*2+1] + + @context.fill() + @closeDrawOp() if transform + + textWidth:(text,size)-> + @context.font = "#{size}pt #{@font}" + @context.measureText(text).width + + drawText:(text,x,y,size,color)-> + @setColor color + @context.globalAlpha = @alpha + @context.font = "#{size}pt #{@font}" + @context.textAlign = "center" + @context.textBaseline = "middle" + w = @context.measureText(text).width + h = size + if @initDrawOp(x,-y) + @context.fillText text,0-@anchor_x*w/2,0+@anchor_y*h/2 + @closeDrawOp(x,-y) + else + @context.fillText text,x-@anchor_x*w/2,-y+@anchor_y*h/2 + + drawTextOutline:(text,x,y,size,color)-> + @setColor color + @context.globalAlpha = @alpha + @context.font = "#{size}pt #{@font}" + @context.lineWidth = @line_width + @context.textAlign = "center" + @context.textBaseline = "middle" + w = @context.measureText(text).width + h = size + if @initDrawOp(x,-y) + @context.strokeText text,0-@anchor_x*w/2,0+@anchor_y*h/2 + @closeDrawOp(x,-y) + else + @context.strokeText text,x-@anchor_x*w/2,-y+@anchor_y*h/2 + + getSpriteFrame:(sprite)-> + frame = null + if typeof sprite == "string" + s = @runtime.sprites[sprite] + if s? + sprite = s + else + s = sprite.split(".") + if s.length>1 + sprite = @runtime.sprites[s[0]] + frame = s[1]|0 + + return null if not sprite? or not sprite.ready + + if sprite.frames.length>1 + if not frame? + dt = 1000/sprite.fps + frame = Math.floor((Date.now()-sprite.animation_start)/dt)%sprite.frames.length + if frame>=0 and frame + canvas = @getSpriteFrame(sprite) + return if not canvas? + + if not h + h = w/canvas.width*canvas.height + + @context.globalAlpha = @alpha + @context.imageSmoothingEnabled = false + if @initDrawOp(x,-y) + @context.drawImage canvas,-w/2-@anchor_x*w/2,-h/2+@anchor_y*h/2,w,h + @closeDrawOp(x,-y) + else + @context.drawImage canvas,x-w/2-@anchor_x*w/2,-y-h/2+@anchor_y*h/2,w,h + + drawSpritePart:(sprite,sx,sy,sw,sh,x,y,w,h)-> + canvas = @getSpriteFrame(sprite) + return if not canvas? + + if not h + h = w/sw*sh + + @context.globalAlpha = @alpha + @context.imageSmoothingEnabled = false + if @initDrawOp(x,-y) + @context.drawImage canvas,sx,sy,sw,sh,-w/2-@anchor_x*w/2,-h/2+@anchor_y*h/2,w,h + @closeDrawOp(x,-y) + else + @context.drawImage canvas,sx,sy,sw,sh,x-w/2-@anchor_x*w/2,-y-h/2+@anchor_y*h/2,w,h + + drawMap:(map,x,y,w,h)-> + map = @runtime.maps[map] if typeof map == "string" + return if not map? or not map.ready or not map.canvas? + @context.globalAlpha = @alpha + @context.imageSmoothingEnabled = false + if @initDrawOp(x,-y) + @context.drawImage map.getCanvas(),-w/2-@anchor_x*w/2,-h/2+@anchor_y*h/2,w,h + @closeDrawOp(x,-y) + else + @context.drawImage map.getCanvas(),x-w/2-@anchor_x*w/2,-y-h/2+@anchor_y*h/2,w,h + + resize:()-> + cw = window.innerWidth + ch = window.innerHeight + + ratio = { + "4x3": 4/3 + "16x9": 16/9 + "2x1": 2/1 + "1x1": 1/1 + ">4x3": 4/3 + ">16x9": 16/9 + ">2x1": 2/1 + ">1x1": 1/1 + }[@runtime.aspect] + + min = @runtime.aspect.startsWith(">") + + #if not ratio? and @runtime.orientation in ["portrait","landscape"] + # ratio = 16/9 + + if ratio? + if min + switch @runtime.orientation + when "portrait" + ratio = Math.max(ratio,ch/cw) + when "landscape" + ratio = Math.max(ratio,cw/ch) + else + if ch>cw + ratio = Math.max(ratio,ch/cw) + else + ratio = Math.max(ratio,cw/ch) + + switch @runtime.orientation + when "portrait" + r = Math.min(cw,ch/ratio)/cw + w = cw*r + h = cw*r*ratio + + when "landscape" + r = Math.min(cw/ratio,ch)/ch + w = ch*r*ratio + h = ch*r + + else + if cw>ch + r = Math.min(cw/ratio,ch)/ch + w = ch*r*ratio + h = ch*r + else + r = Math.min(cw,ch/ratio)/cw + w = cw*r + h = cw*r*ratio + else + w = cw + h = ch + + @canvas.style["margin-top"] = Math.round((ch-h)/2)+"px" + @canvas.style.width = Math.round(w)+"px" + @canvas.style.height = Math.round(h)+"px" + + devicePixelRatio = window.devicePixelRatio || 1 + backingStoreRatio = @context.webkitBackingStorePixelRatio || + @context.mozBackingStorePixelRatio || + @context.msBackingStorePixelRatio || + @context.oBackingStorePixelRatio || + @context.backingStorePixelRatio || 1 + + @ratio = devicePixelRatio/backingStoreRatio*Math.max(1,Math.min(2,@supersampling)) + + @width = w*@ratio + @height = h*@ratio + + @canvas.width = @width + @canvas.height = @height + @initContext() + + startControl:(@element)-> + @canvas.addEventListener "touchstart", (event) => @touchStart(event) + @canvas.addEventListener "touchmove", (event) => @touchMove(event) + document.addEventListener "touchend" , (event) => @touchRelease(event) + document.addEventListener "touchcancel" , (event) => @touchRelease(event) + + @canvas.addEventListener "mousedown", (event) => @mouseDown(event) + @canvas.addEventListener "mousemove", (event) => @mouseMove(event) + document.addEventListener "mouseup", (event) => @mouseUp(event) + + devicePixelRatio = window.devicePixelRatio || 1 + backingStoreRatio = @context.webkitBackingStorePixelRatio || + @context.mozBackingStorePixelRatio || + @context.msBackingStorePixelRatio || + @context.oBackingStorePixelRatio || + @context.backingStorePixelRatio || 1 + + @ratio = devicePixelRatio/backingStoreRatio + + touchStart:(event)-> + event.preventDefault() + event.stopPropagation() + b = @canvas.getBoundingClientRect() + for i in [0..event.changedTouches.length-1] by 1 + t = event.changedTouches[i] + min = Math.min @canvas.clientWidth,@canvas.clientHeight + x = (t.clientX-b.left-@canvas.clientWidth/2)/min*200 + y = (@canvas.clientHeight/2-(t.clientY-b.top))/min*200 + @touches[t.identifier] = + x: x + y: y + @mouse.x = x + @mouse.y = y + @mouse.pressed = 1 + @mouse.left = 1 + false + + touchMove:(event)-> + event.preventDefault() + event.stopPropagation() + b = @canvas.getBoundingClientRect() + for i in [0..event.changedTouches.length-1] by 1 + t = event.changedTouches[i] + if @touches[t.identifier]? + min = Math.min @canvas.clientWidth,@canvas.clientHeight + x = (t.clientX-b.left-@canvas.clientWidth/2)/min*200 + y = (@canvas.clientHeight/2-(t.clientY-b.top))/min*200 + @touches[t.identifier].x = x + @touches[t.identifier].y = y + @mouse.x = x + @mouse.y = y + false + + touchRelease:(event)-> + for i in [0..event.changedTouches.length-1] by 1 + t = event.changedTouches[i] + x = (t.clientX-@canvas.offsetLeft)*@ratio + y = (t.clientY-@canvas.offsetTop)*@ratio + delete @touches[t.identifier] + + @mouse.pressed = 0 + @mouse.left = 0 + @mouse.right = 0 + @mouse.middle = 0 + false + + mouseDown:(event)-> + @mousepressed = true + b = @canvas.getBoundingClientRect() + min = Math.min @canvas.clientWidth,@canvas.clientHeight + x = (event.clientX-b.left-@canvas.clientWidth/2)/min*200 + y = (@canvas.clientHeight/2-(event.clientY-b.top))/min*200 + @touches["mouse"] = + x: x + y: y + #console.info @touches["mouse"] + + @mouse.x = x + @mouse.y = y + switch event.button + when 0 then @mouse.left = 1 + when 1 then @mouse.middle = 1 + when 2 then @mouse.right = 1 + + @mouse.pressed = Math.min(1,@mouse.left+@mouse.right+@mouse.middle) + false + + mouseMove:(event)-> + event.preventDefault() + b = @canvas.getBoundingClientRect() + min = Math.min @canvas.clientWidth,@canvas.clientHeight + x = (event.clientX-b.left-@canvas.clientWidth/2)/min*200 + y = (@canvas.clientHeight/2-(event.clientY-b.top))/min*200 + if @touches["mouse"]? + @touches["mouse"].x = x + @touches["mouse"].y = y + + @mouse.x = x + @mouse.y = y + + false + + mouseUp:(event)-> + delete @touches["mouse"] + + b = @canvas.getBoundingClientRect() + min = Math.min @canvas.clientWidth,@canvas.clientHeight + x = (event.clientX-b.left-@canvas.clientWidth/2)/min*200 + y = (@canvas.clientHeight/2-(event.clientY-b.top))/min*200 + + @mouse.x = x + @mouse.y = y + switch event.button + when 0 then @mouse.left = 0 + when 1 then @mouse.middle = 0 + when 2 then @mouse.right = 0 + + @mouse.pressed = Math.min(1,@mouse.left+@mouse.right+@mouse.middle) + + false diff --git a/static/js/runtime/screen.js b/static/js/runtime/screen.js new file mode 100644 index 00000000..8fff9d4d --- /dev/null +++ b/static/js/runtime/screen.js @@ -0,0 +1,905 @@ +this.Screen = (function() { + function Screen(runtime) { + this.runtime = runtime; + this.canvas = document.createElement("canvas"); + this.canvas.width = 1080; + this.canvas.height = 1920; + this.touches = {}; + this.mouse = { + x: -10000, + y: -10000, + pressed: 0, + left: 0, + middle: 0, + right: 0 + }; + this.translation_x = 0; + this.translation_y = 0; + this.anchor_x = 0; + this.anchor_y = 0; + this.supersampling = this.previous_supersampling = 1; + this.font = "BitCell"; + this.initContext(); + this.cursor = "default"; + this.canvas.addEventListener("mousemove", (function(_this) { + return function() { + _this.last_mouse_move = Date.now(); + if (_this.cursor !== "default" && _this.cursor_visibility === "auto") { + _this.cursor = "default"; + return _this.canvas.style.cursor = "default"; + } + }; + })(this)); + setInterval(((function(_this) { + return function() { + return _this.checkMouseCursor(); + }; + })(this)), 1000); + this.cursor_visibility = "auto"; + } + + Screen.prototype.checkMouseCursor = function() { + if (Date.now() > this.last_mouse_move + 4000 && this.cursor_visibility === "auto") { + if (this.cursor !== "none") { + this.cursor = "none"; + return this.canvas.style.cursor = "none"; + } + } + }; + + Screen.prototype.setCursorVisible = function(visible) { + this.cursor_visibility = visible; + if (visible) { + this.cursor = "default"; + return this.canvas.style.cursor = "default"; + } else { + this.cursor = "none"; + return this.canvas.style.cursor = "none"; + } + }; + + Screen.prototype.initContext = function() { + var b, c, j, len1, ratio, ref; + c = this.canvas.getContext("2d", { + alpha: false + }); + if (c !== this.context) { + this.context = c; + } else { + this.context.restore(); + } + this.context.save(); + this.context.translate(this.canvas.width / 2, this.canvas.height / 2); + ratio = Math.min(this.canvas.width / 200, this.canvas.height / 200); + this.context.scale(ratio, ratio); + this.width = this.canvas.width / ratio; + this.height = this.canvas.height / ratio; + this.alpha = 1; + this.line_width = 1; + this.object_rotation = 0; + this.object_scale_x = 1; + this.object_scale_y = 1; + this.context.lineCap = "round"; + this.blending = { + normal: "source-over", + additive: "lighter" + }; + ref = ["source-over", "source-in", "source-out", "source-atop", "destination-over", "destination-in", "destination-out", "destination-atop", "lighter", "copy", "xor", "multiply", "screen", "overlay", "darken", "lighten", "color-dodge", "color-burn", "hard-light", "soft-light", "difference", "exclusion", "hue", "saturation", "color", "luminosity"]; + for (j = 0, len1 = ref.length; j < len1; j++) { + b = ref[j]; + this.blending[b] = b; + } + }; + + Screen.prototype.getInterface = function() { + var screen; + if (this["interface"] != null) { + return this["interface"]; + } + screen = this; + return this["interface"] = { + width: this.width, + height: this.height, + clear: function(color) { + return screen.clear(color); + }, + setColor: function(color) { + return screen.setColor(color); + }, + setAlpha: function(alpha) { + return screen.setAlpha(alpha); + }, + setBlending: function(blending) { + return screen.setBlending(blending); + }, + setLinearGradient: function(x1, y1, x2, y2, c1, c2) { + return screen.setLinearGradient(x1, y1, x2, y2, c1, c2); + }, + setRadialGradient: function(x, y, radius, c1, c2) { + return screen.setRadialGradient(x, y, radius, c1, c2); + }, + setFont: function(font) { + return screen.setFont(font); + }, + setTranslation: function(tx, ty) { + return screen.setTranslation(tx, ty); + }, + setDrawAnchor: function(ax, ay) { + return screen.setDrawAnchor(ax, ay); + }, + setDrawRotation: function(rotation) { + return screen.setDrawRotation(rotation); + }, + setDrawScale: function(x, y) { + return screen.setDrawScale(x, y); + }, + fillRect: function(x, y, w, h, c) { + return screen.fillRect(x, y, w, h, c); + }, + fillRoundRect: function(x, y, w, h, r, c) { + return screen.fillRoundRect(x, y, w, h, r, c); + }, + fillRound: function(x, y, w, h, c) { + return screen.fillRound(x, y, w, h, c); + }, + drawRect: function(x, y, w, h, c) { + return screen.drawRect(x, y, w, h, c); + }, + drawRoundRect: function(x, y, w, h, r, c) { + return screen.drawRoundRect(x, y, w, h, r, c); + }, + drawRound: function(x, y, w, h, c) { + return screen.drawRound(x, y, w, h, c); + }, + drawSprite: function(sprite, x, y, w, h) { + return screen.drawSprite(sprite, x, y, w, h); + }, + drawSpritePart: function(sprite, sx, sy, sw, sh, x, y, w, h) { + return screen.drawSpritePart(sprite, sx, sy, sw, sh, x, y, w, h); + }, + drawMap: function(map, x, y, w, h) { + return screen.drawMap(map, x, y, w, h); + }, + drawText: function(text, x, y, size, color) { + return screen.drawText(text, x, y, size, color); + }, + drawTextOutline: function(text, x, y, size, color) { + return screen.drawTextOutline(text, x, y, size, color); + }, + textWidth: function(text, size) { + return screen.textWidth(text, size); + }, + setLineWidth: function(width) { + return screen.setLineWidth(width); + }, + setLineDash: function(dash) { + return screen.setLineDash(dash); + }, + drawLine: function(x1, y1, x2, y2, color) { + return screen.drawLine(x1, y1, x2, y2, color); + }, + drawPolygon: function() { + return screen.drawPolygon(arguments); + }, + drawPolyline: function() { + return screen.drawPolyline(arguments); + }, + fillPolygon: function() { + return screen.fillPolygon(arguments); + }, + setCursorVisible: function(visible) { + return screen.setCursorVisible(visible); + } + }; + }; + + Screen.prototype.updateInterface = function() { + this["interface"].width = this.width; + return this["interface"].height = this.height; + }; + + Screen.prototype.clear = function(color) { + var blending_save, c, s; + c = this.context.fillStyle; + s = this.context.strokeStyle; + blending_save = this.context.globalCompositeOperation; + this.context.globalAlpha = 1; + this.context.globalCompositeOperation = "source-over"; + if (color != null) { + this.setColor(color); + } else { + this.context.fillStyle = "#000"; + } + this.context.fillRect(-this.width / 2, -this.height / 2, this.width, this.height); + this.context.fillStyle = c; + this.context.strokeStyle = s; + return this.context.globalCompositeOperation = blending_save; + }; + + Screen.prototype.initDraw = function() { + this.alpha = 1; + this.line_width = 1; + if (this.supersampling !== this.previous_supersampling) { + this.resize(); + return this.previous_supersampling = this.supersampling; + } + }; + + Screen.prototype.setColor = function(color) { + var b, c, g, r; + if (color == null) { + return; + } + if (!Number.isNaN(Number.parseInt(color))) { + r = (Math.floor(color / 100) % 10) / 9 * 255; + g = (Math.floor(color / 10) % 10) / 9 * 255; + b = (Math.floor(color) % 10) / 9 * 255; + c = 0xFF000000; + c += r << 16; + c += g << 8; + c += b; + c = "#" + c.toString(16).substring(2, 8); + this.context.fillStyle = c; + return this.context.strokeStyle = c; + } else if (typeof color === "string") { + this.context.fillStyle = color; + return this.context.strokeStyle = color; + } + }; + + Screen.prototype.setAlpha = function(alpha1) { + this.alpha = alpha1; + }; + + Screen.prototype.setBlending = function(blending) { + blending = this.blending[blending || "normal"] || "source-over"; + return this.context.globalCompositeOperation = blending; + }; + + Screen.prototype.setLineWidth = function(line_width) { + this.line_width = line_width; + }; + + Screen.prototype.setLineDash = function(dash) { + if (!Array.isArray(dash)) { + return this.context.setLineDash([]); + } else { + return this.context.setLineDash(dash); + } + }; + + Screen.prototype.setLinearGradient = function(x1, y1, x2, y2, c1, c2) { + var grd; + grd = this.context.createLinearGradient(x1, -y1, x2, -y2); + grd.addColorStop(0, c1); + grd.addColorStop(1, c2); + this.context.fillStyle = grd; + return this.context.strokeStyle = grd; + }; + + Screen.prototype.setRadialGradient = function(x, y, radius, c1, c2) { + var grd; + grd = this.context.createRadialGradient(x, -y, 0, x, -y, radius); + grd.addColorStop(0, c1); + grd.addColorStop(1, c2); + this.context.fillStyle = grd; + return this.context.strokeStyle = grd; + }; + + Screen.prototype.setFont = function(font) { + return this.font = font || "Verdana"; + }; + + Screen.prototype.setTranslation = function(translation_x, translation_y) { + this.translation_x = translation_x; + this.translation_y = translation_y; + }; + + Screen.prototype.setDrawAnchor = function(anchor_x, anchor_y) { + this.anchor_x = anchor_x; + this.anchor_y = anchor_y; + if (typeof this.anchor_x !== "number") { + this.anchor_x = 0; + } + if (typeof this.anchor_y !== "number") { + return this.anchor_y = 0; + } + }; + + Screen.prototype.setDrawRotation = function(object_rotation) { + this.object_rotation = object_rotation; + }; + + Screen.prototype.setDrawScale = function(object_scale_x, object_scale_y) { + this.object_scale_x = object_scale_x; + this.object_scale_y = object_scale_y != null ? object_scale_y : this.object_scale_x; + }; + + Screen.prototype.initDrawOp = function(x, y) { + var res; + res = false; + if (this.translation_x !== 0 || this.translation_y !== 0) { + this.context.save(); + res = true; + this.context.translate(x + this.translation_x, y - this.translation_y); + } + if (this.object_rotation !== 0 || this.object_scale_x !== 1 || this.object_scale_y !== 1) { + if (!res) { + this.context.save(); + res = true; + this.context.translate(x, y); + } + if (this.object_rotation !== 0) { + this.context.rotate(-this.object_rotation / 180 * Math.PI); + } + if (this.object_scale_x !== 1 || this.object_scale_y !== 1) { + this.context.scale(this.object_scale_x, this.object_scale_y); + } + } + return res; + }; + + Screen.prototype.closeDrawOp = function(x, y) { + return this.context.restore(); + }; + + Screen.prototype.fillRect = function(x, y, w, h, color) { + this.setColor(color); + this.context.globalAlpha = this.alpha; + if (this.initDrawOp(x, -y)) { + this.context.fillRect(-w / 2 - this.anchor_x * w / 2, -h / 2 + this.anchor_y * h / 2, w, h); + return this.closeDrawOp(x, -y); + } else { + return this.context.fillRect(x - w / 2 - this.anchor_x * w / 2, -y - h / 2 + this.anchor_y * h / 2, w, h); + } + }; + + Screen.prototype.fillRoundRect = function(x, y, w, h, round, color) { + if (round == null) { + round = 10; + } + this.setColor(color); + this.context.globalAlpha = this.alpha; + if (this.initDrawOp(x, -y)) { + this.context.fillRoundRect(-w / 2 - this.anchor_x * w / 2, -h / 2 + this.anchor_y * h / 2, w, h, round); + return this.closeDrawOp(x, -y); + } else { + return this.context.fillRoundRect(x - w / 2 - this.anchor_x * w / 2, -y - h / 2 + this.anchor_y * h / 2, w, h, round); + } + }; + + Screen.prototype.fillRound = function(x, y, w, h, color) { + this.setColor(color); + this.context.globalAlpha = this.alpha; + w = Math.abs(w); + h = Math.abs(h); + if (this.initDrawOp(x, -y)) { + this.context.beginPath(); + this.context.ellipse(-this.anchor_x * w / 2, 0 + this.anchor_y * h / 2, w / 2, h / 2, 0, 0, Math.PI * 2, false); + this.context.fill(); + return this.closeDrawOp(x, -y); + } else { + this.context.beginPath(); + this.context.ellipse(x - this.anchor_x * w / 2, -y + this.anchor_y * h / 2, w / 2, h / 2, 0, 0, Math.PI * 2, false); + return this.context.fill(); + } + }; + + Screen.prototype.drawRect = function(x, y, w, h, color) { + this.setColor(color); + this.context.globalAlpha = this.alpha; + this.context.lineWidth = this.line_width; + if (this.initDrawOp(x, -y)) { + this.context.strokeRect(-w / 2 - this.anchor_x * w / 2, -h / 2 + this.anchor_y * h / 2, w, h); + return this.closeDrawOp(x, -y); + } else { + return this.context.strokeRect(x - w / 2 - this.anchor_x * w / 2, -y - h / 2 + this.anchor_y * h / 2, w, h); + } + }; + + Screen.prototype.drawRoundRect = function(x, y, w, h, round, color) { + if (round == null) { + round = 10; + } + this.setColor(color); + this.context.globalAlpha = this.alpha; + this.context.lineWidth = this.line_width; + if (this.initDrawOp(x, -y)) { + this.context.strokeRoundRect(-w / 2 - this.anchor_x * w / 2, -h / 2 + this.anchor_y * h / 2, w, h, round); + return this.closeDrawOp(x, -y); + } else { + return this.context.strokeRoundRect(x - w / 2 - this.anchor_x * w / 2, -y - h / 2 + this.anchor_y * h / 2, w, h, round); + } + }; + + Screen.prototype.drawRound = function(x, y, w, h, color) { + this.setColor(color); + this.context.globalAlpha = this.alpha; + this.context.lineWidth = this.line_width; + w = Math.abs(w); + h = Math.abs(h); + if (this.initDrawOp(x, -y)) { + this.context.beginPath(); + this.context.ellipse(0 - this.anchor_x * w / 2, 0 + this.anchor_y * h / 2, w / 2, h / 2, 0, 0, Math.PI * 2, false); + this.context.stroke(); + return this.closeDrawOp(x, -y); + } else { + this.context.beginPath(); + this.context.ellipse(x - this.anchor_x * w / 2, -y + this.anchor_y * h / 2, w / 2, h / 2, 0, 0, Math.PI * 2, false); + return this.context.stroke(); + } + }; + + Screen.prototype.drawLine = function(x1, y1, x2, y2, color) { + var transform; + this.setColor(color); + this.context.globalAlpha = this.alpha; + this.context.lineWidth = this.line_width; + transform = this.initDrawOp(0, 0); + this.context.beginPath(); + this.context.moveTo(x1, -y1); + this.context.lineTo(x2, -y2); + this.context.stroke(); + if (transform) { + return this.closeDrawOp(); + } + }; + + Screen.prototype.drawPolyline = function(args) { + var i, j, len, ref, transform; + if (args.length > 0 && args.length % 2 === 1 && typeof args[args.length - 1] === "string") { + this.setColor(args[args.length - 1]); + } + if (Array.isArray(args[0])) { + if ((args[1] != null) && typeof args[1] === "string") { + this.setColor(args[1]); + } + args = args[0]; + } + this.context.globalAlpha = this.alpha; + this.context.lineWidth = this.line_width; + if (args.length < 4) { + return; + } + len = Math.floor(args.length / 2); + transform = this.initDrawOp(0, 0); + this.context.beginPath(); + this.context.moveTo(args[0], -args[1]); + for (i = j = 1, ref = len - 1; 1 <= ref ? j <= ref : j >= ref; i = 1 <= ref ? ++j : --j) { + this.context.lineTo(args[i * 2], -args[i * 2 + 1]); + } + this.context.stroke(); + if (transform) { + return this.closeDrawOp(); + } + }; + + Screen.prototype.drawPolygon = function(args) { + var i, j, len, ref, transform; + if (args.length > 0 && args.length % 2 === 1 && typeof args[args.length - 1] === "string") { + this.setColor(args[args.length - 1]); + } + if (Array.isArray(args[0])) { + if ((args[1] != null) && typeof args[1] === "string") { + this.setColor(args[1]); + } + args = args[0]; + } + this.context.globalAlpha = this.alpha; + this.context.lineWidth = this.line_width; + if (args.length < 4) { + return; + } + len = Math.floor(args.length / 2); + transform = this.initDrawOp(0, 0); + this.context.beginPath(); + this.context.moveTo(args[0], -args[1]); + for (i = j = 1, ref = len - 1; 1 <= ref ? j <= ref : j >= ref; i = 1 <= ref ? ++j : --j) { + this.context.lineTo(args[i * 2], -args[i * 2 + 1]); + } + this.context.closePath(); + this.context.stroke(); + if (transform) { + return this.closeDrawOp(); + } + }; + + Screen.prototype.fillPolygon = function(args) { + var i, j, len, ref, transform; + if (args.length > 0 && args.length % 2 === 1 && typeof args[args.length - 1] === "string") { + this.setColor(args[args.length - 1]); + } + if (Array.isArray(args[0])) { + if ((args[1] != null) && typeof args[1] === "string") { + this.setColor(args[1]); + } + args = args[0]; + } + this.context.globalAlpha = this.alpha; + this.context.lineWidth = this.line_width; + if (args.length < 4) { + return; + } + len = Math.floor(args.length / 2); + transform = this.initDrawOp(0, 0); + this.context.beginPath(); + this.context.moveTo(args[0], -args[1]); + for (i = j = 1, ref = len - 1; 1 <= ref ? j <= ref : j >= ref; i = 1 <= ref ? ++j : --j) { + this.context.lineTo(args[i * 2], -args[i * 2 + 1]); + } + this.context.fill(); + if (transform) { + return this.closeDrawOp(); + } + }; + + Screen.prototype.textWidth = function(text, size) { + this.context.font = size + "pt " + this.font; + return this.context.measureText(text).width; + }; + + Screen.prototype.drawText = function(text, x, y, size, color) { + var h, w; + this.setColor(color); + this.context.globalAlpha = this.alpha; + this.context.font = size + "pt " + this.font; + this.context.textAlign = "center"; + this.context.textBaseline = "middle"; + w = this.context.measureText(text).width; + h = size; + if (this.initDrawOp(x, -y)) { + this.context.fillText(text, 0 - this.anchor_x * w / 2, 0 + this.anchor_y * h / 2); + return this.closeDrawOp(x, -y); + } else { + return this.context.fillText(text, x - this.anchor_x * w / 2, -y + this.anchor_y * h / 2); + } + }; + + Screen.prototype.drawTextOutline = function(text, x, y, size, color) { + var h, w; + this.setColor(color); + this.context.globalAlpha = this.alpha; + this.context.font = size + "pt " + this.font; + this.context.lineWidth = this.line_width; + this.context.textAlign = "center"; + this.context.textBaseline = "middle"; + w = this.context.measureText(text).width; + h = size; + if (this.initDrawOp(x, -y)) { + this.context.strokeText(text, 0 - this.anchor_x * w / 2, 0 + this.anchor_y * h / 2); + return this.closeDrawOp(x, -y); + } else { + return this.context.strokeText(text, x - this.anchor_x * w / 2, -y + this.anchor_y * h / 2); + } + }; + + Screen.prototype.getSpriteFrame = function(sprite) { + var dt, frame, s; + frame = null; + if (typeof sprite === "string") { + s = this.runtime.sprites[sprite]; + if (s != null) { + sprite = s; + } else { + s = sprite.split("."); + if (s.length > 1) { + sprite = this.runtime.sprites[s[0]]; + frame = s[1] | 0; + } + } + } + if ((sprite == null) || !sprite.ready) { + return null; + } + if (sprite.frames.length > 1) { + if (frame == null) { + dt = 1000 / sprite.fps; + frame = Math.floor((Date.now() - sprite.animation_start) / dt) % sprite.frames.length; + } + if (frame >= 0 && frame < sprite.frames.length) { + return sprite.frames[frame].canvas; + } else { + return sprite.frames[0].canvas; + } + } else if (sprite.frames[0] != null) { + return sprite.frames[0].canvas; + } else { + return null; + } + }; + + Screen.prototype.drawSprite = function(sprite, x, y, w, h) { + var canvas; + canvas = this.getSpriteFrame(sprite); + if (canvas == null) { + return; + } + if (!h) { + h = w / canvas.width * canvas.height; + } + this.context.globalAlpha = this.alpha; + this.context.imageSmoothingEnabled = false; + if (this.initDrawOp(x, -y)) { + this.context.drawImage(canvas, -w / 2 - this.anchor_x * w / 2, -h / 2 + this.anchor_y * h / 2, w, h); + return this.closeDrawOp(x, -y); + } else { + return this.context.drawImage(canvas, x - w / 2 - this.anchor_x * w / 2, -y - h / 2 + this.anchor_y * h / 2, w, h); + } + }; + + Screen.prototype.drawSpritePart = function(sprite, sx, sy, sw, sh, x, y, w, h) { + var canvas; + canvas = this.getSpriteFrame(sprite); + if (canvas == null) { + return; + } + if (!h) { + h = w / sw * sh; + } + this.context.globalAlpha = this.alpha; + this.context.imageSmoothingEnabled = false; + if (this.initDrawOp(x, -y)) { + this.context.drawImage(canvas, sx, sy, sw, sh, -w / 2 - this.anchor_x * w / 2, -h / 2 + this.anchor_y * h / 2, w, h); + return this.closeDrawOp(x, -y); + } else { + return this.context.drawImage(canvas, sx, sy, sw, sh, x - w / 2 - this.anchor_x * w / 2, -y - h / 2 + this.anchor_y * h / 2, w, h); + } + }; + + Screen.prototype.drawMap = function(map, x, y, w, h) { + if (typeof map === "string") { + map = this.runtime.maps[map]; + } + if ((map == null) || !map.ready || (map.canvas == null)) { + return; + } + this.context.globalAlpha = this.alpha; + this.context.imageSmoothingEnabled = false; + if (this.initDrawOp(x, -y)) { + this.context.drawImage(map.getCanvas(), -w / 2 - this.anchor_x * w / 2, -h / 2 + this.anchor_y * h / 2, w, h); + return this.closeDrawOp(x, -y); + } else { + return this.context.drawImage(map.getCanvas(), x - w / 2 - this.anchor_x * w / 2, -y - h / 2 + this.anchor_y * h / 2, w, h); + } + }; + + Screen.prototype.resize = function() { + var backingStoreRatio, ch, cw, devicePixelRatio, h, min, r, ratio, w; + cw = window.innerWidth; + ch = window.innerHeight; + ratio = { + "4x3": 4 / 3, + "16x9": 16 / 9, + "2x1": 2 / 1, + "1x1": 1 / 1, + ">4x3": 4 / 3, + ">16x9": 16 / 9, + ">2x1": 2 / 1, + ">1x1": 1 / 1 + }[this.runtime.aspect]; + min = this.runtime.aspect.startsWith(">"); + if (ratio != null) { + if (min) { + switch (this.runtime.orientation) { + case "portrait": + ratio = Math.max(ratio, ch / cw); + break; + case "landscape": + ratio = Math.max(ratio, cw / ch); + break; + default: + if (ch > cw) { + ratio = Math.max(ratio, ch / cw); + } else { + ratio = Math.max(ratio, cw / ch); + } + } + } + switch (this.runtime.orientation) { + case "portrait": + r = Math.min(cw, ch / ratio) / cw; + w = cw * r; + h = cw * r * ratio; + break; + case "landscape": + r = Math.min(cw / ratio, ch) / ch; + w = ch * r * ratio; + h = ch * r; + break; + default: + if (cw > ch) { + r = Math.min(cw / ratio, ch) / ch; + w = ch * r * ratio; + h = ch * r; + } else { + r = Math.min(cw, ch / ratio) / cw; + w = cw * r; + h = cw * r * ratio; + } + } + } else { + w = cw; + h = ch; + } + this.canvas.style["margin-top"] = Math.round((ch - h) / 2) + "px"; + this.canvas.style.width = Math.round(w) + "px"; + this.canvas.style.height = Math.round(h) + "px"; + devicePixelRatio = window.devicePixelRatio || 1; + backingStoreRatio = this.context.webkitBackingStorePixelRatio || this.context.mozBackingStorePixelRatio || this.context.msBackingStorePixelRatio || this.context.oBackingStorePixelRatio || this.context.backingStorePixelRatio || 1; + this.ratio = devicePixelRatio / backingStoreRatio * Math.max(1, Math.min(2, this.supersampling)); + this.width = w * this.ratio; + this.height = h * this.ratio; + this.canvas.width = this.width; + this.canvas.height = this.height; + return this.initContext(); + }; + + Screen.prototype.startControl = function(element) { + var backingStoreRatio, devicePixelRatio; + this.element = element; + this.canvas.addEventListener("touchstart", (function(_this) { + return function(event) { + return _this.touchStart(event); + }; + })(this)); + this.canvas.addEventListener("touchmove", (function(_this) { + return function(event) { + return _this.touchMove(event); + }; + })(this)); + document.addEventListener("touchend", (function(_this) { + return function(event) { + return _this.touchRelease(event); + }; + })(this)); + document.addEventListener("touchcancel", (function(_this) { + return function(event) { + return _this.touchRelease(event); + }; + })(this)); + this.canvas.addEventListener("mousedown", (function(_this) { + return function(event) { + return _this.mouseDown(event); + }; + })(this)); + this.canvas.addEventListener("mousemove", (function(_this) { + return function(event) { + return _this.mouseMove(event); + }; + })(this)); + document.addEventListener("mouseup", (function(_this) { + return function(event) { + return _this.mouseUp(event); + }; + })(this)); + devicePixelRatio = window.devicePixelRatio || 1; + backingStoreRatio = this.context.webkitBackingStorePixelRatio || this.context.mozBackingStorePixelRatio || this.context.msBackingStorePixelRatio || this.context.oBackingStorePixelRatio || this.context.backingStorePixelRatio || 1; + return this.ratio = devicePixelRatio / backingStoreRatio; + }; + + Screen.prototype.touchStart = function(event) { + var b, i, j, min, ref, t, x, y; + event.preventDefault(); + event.stopPropagation(); + b = this.canvas.getBoundingClientRect(); + for (i = j = 0, ref = event.changedTouches.length - 1; j <= ref; i = j += 1) { + t = event.changedTouches[i]; + min = Math.min(this.canvas.clientWidth, this.canvas.clientHeight); + x = (t.clientX - b.left - this.canvas.clientWidth / 2) / min * 200; + y = (this.canvas.clientHeight / 2 - (t.clientY - b.top)) / min * 200; + this.touches[t.identifier] = { + x: x, + y: y + }; + this.mouse.x = x; + this.mouse.y = y; + this.mouse.pressed = 1; + this.mouse.left = 1; + } + return false; + }; + + Screen.prototype.touchMove = function(event) { + var b, i, j, min, ref, t, x, y; + event.preventDefault(); + event.stopPropagation(); + b = this.canvas.getBoundingClientRect(); + for (i = j = 0, ref = event.changedTouches.length - 1; j <= ref; i = j += 1) { + t = event.changedTouches[i]; + if (this.touches[t.identifier] != null) { + min = Math.min(this.canvas.clientWidth, this.canvas.clientHeight); + x = (t.clientX - b.left - this.canvas.clientWidth / 2) / min * 200; + y = (this.canvas.clientHeight / 2 - (t.clientY - b.top)) / min * 200; + this.touches[t.identifier].x = x; + this.touches[t.identifier].y = y; + this.mouse.x = x; + this.mouse.y = y; + } + } + return false; + }; + + Screen.prototype.touchRelease = function(event) { + var i, j, ref, t, x, y; + for (i = j = 0, ref = event.changedTouches.length - 1; j <= ref; i = j += 1) { + t = event.changedTouches[i]; + x = (t.clientX - this.canvas.offsetLeft) * this.ratio; + y = (t.clientY - this.canvas.offsetTop) * this.ratio; + delete this.touches[t.identifier]; + this.mouse.pressed = 0; + this.mouse.left = 0; + this.mouse.right = 0; + this.mouse.middle = 0; + } + return false; + }; + + Screen.prototype.mouseDown = function(event) { + var b, min, x, y; + this.mousepressed = true; + b = this.canvas.getBoundingClientRect(); + min = Math.min(this.canvas.clientWidth, this.canvas.clientHeight); + x = (event.clientX - b.left - this.canvas.clientWidth / 2) / min * 200; + y = (this.canvas.clientHeight / 2 - (event.clientY - b.top)) / min * 200; + this.touches["mouse"] = { + x: x, + y: y + }; + this.mouse.x = x; + this.mouse.y = y; + switch (event.button) { + case 0: + this.mouse.left = 1; + break; + case 1: + this.mouse.middle = 1; + break; + case 2: + this.mouse.right = 1; + } + this.mouse.pressed = Math.min(1, this.mouse.left + this.mouse.right + this.mouse.middle); + return false; + }; + + Screen.prototype.mouseMove = function(event) { + var b, min, x, y; + event.preventDefault(); + b = this.canvas.getBoundingClientRect(); + min = Math.min(this.canvas.clientWidth, this.canvas.clientHeight); + x = (event.clientX - b.left - this.canvas.clientWidth / 2) / min * 200; + y = (this.canvas.clientHeight / 2 - (event.clientY - b.top)) / min * 200; + if (this.touches["mouse"] != null) { + this.touches["mouse"].x = x; + this.touches["mouse"].y = y; + } + this.mouse.x = x; + this.mouse.y = y; + return false; + }; + + Screen.prototype.mouseUp = function(event) { + var b, min, x, y; + delete this.touches["mouse"]; + b = this.canvas.getBoundingClientRect(); + min = Math.min(this.canvas.clientWidth, this.canvas.clientHeight); + x = (event.clientX - b.left - this.canvas.clientWidth / 2) / min * 200; + y = (this.canvas.clientHeight / 2 - (event.clientY - b.top)) / min * 200; + this.mouse.x = x; + this.mouse.y = y; + switch (event.button) { + case 0: + this.mouse.left = 0; + break; + case 1: + this.mouse.middle = 0; + break; + case 2: + this.mouse.right = 0; + } + this.mouse.pressed = Math.min(1, this.mouse.left + this.mouse.right + this.mouse.middle); + return false; + }; + + return Screen; + +})(); diff --git a/static/js/runtime/sprite.coffee b/static/js/runtime/sprite.coffee new file mode 100644 index 00000000..6077ec1d --- /dev/null +++ b/static/js/runtime/sprite.coffee @@ -0,0 +1,102 @@ +class @Sprite + constructor:(@width,@height,properties)-> + @name = "" + @frames=[] + + if @width? and typeof @width == "string" + @ready = false + img = new Image + img.crossOrigin = "Anonymous" if location.protocol != "file:" + img.src = @width + @width = 0 + @height = 0 + img.onload = ()=> + @ready = true + @load(img,properties) + @loaded() + + img.onerror = ()=> + @ready = true + else + @frames.push new SpriteFrame @,@width,@height + @ready = true + + @current_frame = 0 + @animation_start = 0 + @fps = if properties then properties.fps or 5 else 5 + + setFrame:(f)-> + @animation_start = Date.now()-1000/@fps*f + + getFrame:()-> + dt = 1000/@fps + Math.floor((Date.now()-@animation_start)/dt)%@frames.length + + cutFrames:(num)-> + height = Math.round(@height/num) + for i in [0..num-1] + frame = new Sprite @width,height + frame.getContext().drawImage @getCanvas(),0,-i*height + @frames[i] = frame + @height = height + @canvas = @frames[0].canvas + + saveData:()-> + if @frames.length>1 + canvas = document.createElement "canvas" + canvas.width = @width + canvas.height = @height*@frames.length + context = canvas.getContext "2d" + for i in [0..@frames.length-1] + context.drawImage @frames[i].getCanvas(),0,@height*i + canvas.toDataURL() + else + @frames[0].getCanvas().toDataURL() + + loaded:()-> + + setCurrentFrame:(index)-> + if index>=0 and index<@frames.length + @current_frame = index + + clone:()-> + sprite = new Sprite @width,@height + sprite.copyFrom @ + sprite + + resize:(@width,@height)-> + for f in @frames + f.resize @width,@height + return + + load:(img,properties)-> + if img.width>0 and img.height>0 + numframes = 1 + if properties? and properties.frames? + numframes = properties.frames + @width = img.width + @height = Math.round(img.height/numframes) + @frames = [] + for i in [0..numframes-1] + frame = new SpriteFrame @,@width,@height + frame.getContext().drawImage img,0,-i*@height + @frames.push frame + @ready = true + + copyFrom:(sprite)-> + @width = sprite.width + @height = sprite.height + @frames = [] + for f in sprite.frames + @frames.push f.clone() + + @current_frame = sprite.current_frame + return + + clear:()-> + for f in @frames + f.clear() + return + + addFrame:()-> + @frames.push new SpriteFrame @,@width,@height diff --git a/static/js/runtime/sprite.js b/static/js/runtime/sprite.js new file mode 100644 index 00000000..8ee51aa6 --- /dev/null +++ b/static/js/runtime/sprite.js @@ -0,0 +1,149 @@ +this.Sprite = (function() { + function Sprite(width, height1, properties) { + var img; + this.width = width; + this.height = height1; + this.name = ""; + this.frames = []; + if ((this.width != null) && typeof this.width === "string") { + this.ready = false; + img = new Image; + if (location.protocol !== "file:") { + img.crossOrigin = "Anonymous"; + } + img.src = this.width; + this.width = 0; + this.height = 0; + img.onload = (function(_this) { + return function() { + _this.ready = true; + _this.load(img, properties); + return _this.loaded(); + }; + })(this); + img.onerror = (function(_this) { + return function() { + return _this.ready = true; + }; + })(this); + } else { + this.frames.push(new SpriteFrame(this, this.width, this.height)); + this.ready = true; + } + this.current_frame = 0; + this.animation_start = 0; + this.fps = properties ? properties.fps || 5 : 5; + } + + Sprite.prototype.setFrame = function(f) { + return this.animation_start = Date.now() - 1000 / this.fps * f; + }; + + Sprite.prototype.getFrame = function() { + var dt; + dt = 1000 / this.fps; + return Math.floor((Date.now() - this.animation_start) / dt) % this.frames.length; + }; + + Sprite.prototype.cutFrames = function(num) { + var frame, height, i, j, ref; + height = Math.round(this.height / num); + for (i = j = 0, ref = num - 1; 0 <= ref ? j <= ref : j >= ref; i = 0 <= ref ? ++j : --j) { + frame = new Sprite(this.width, height); + frame.getContext().drawImage(this.getCanvas(), 0, -i * height); + this.frames[i] = frame; + } + this.height = height; + return this.canvas = this.frames[0].canvas; + }; + + Sprite.prototype.saveData = function() { + var canvas, context, i, j, ref; + if (this.frames.length > 1) { + canvas = document.createElement("canvas"); + canvas.width = this.width; + canvas.height = this.height * this.frames.length; + context = canvas.getContext("2d"); + for (i = j = 0, ref = this.frames.length - 1; 0 <= ref ? j <= ref : j >= ref; i = 0 <= ref ? ++j : --j) { + context.drawImage(this.frames[i].getCanvas(), 0, this.height * i); + } + return canvas.toDataURL(); + } else { + return this.frames[0].getCanvas().toDataURL(); + } + }; + + Sprite.prototype.loaded = function() {}; + + Sprite.prototype.setCurrentFrame = function(index) { + if (index >= 0 && index < this.frames.length) { + return this.current_frame = index; + } + }; + + Sprite.prototype.clone = function() { + var sprite; + sprite = new Sprite(this.width, this.height); + sprite.copyFrom(this); + return sprite; + }; + + Sprite.prototype.resize = function(width, height1) { + var f, j, len, ref; + this.width = width; + this.height = height1; + ref = this.frames; + for (j = 0, len = ref.length; j < len; j++) { + f = ref[j]; + f.resize(this.width, this.height); + } + }; + + Sprite.prototype.load = function(img, properties) { + var frame, i, j, numframes, ref; + if (img.width > 0 && img.height > 0) { + numframes = 1; + if ((properties != null) && (properties.frames != null)) { + numframes = properties.frames; + } + this.width = img.width; + this.height = Math.round(img.height / numframes); + this.frames = []; + for (i = j = 0, ref = numframes - 1; 0 <= ref ? j <= ref : j >= ref; i = 0 <= ref ? ++j : --j) { + frame = new SpriteFrame(this, this.width, this.height); + frame.getContext().drawImage(img, 0, -i * this.height); + this.frames.push(frame); + } + return this.ready = true; + } + }; + + Sprite.prototype.copyFrom = function(sprite) { + var f, j, len, ref; + this.width = sprite.width; + this.height = sprite.height; + this.frames = []; + ref = sprite.frames; + for (j = 0, len = ref.length; j < len; j++) { + f = ref[j]; + this.frames.push(f.clone()); + } + this.current_frame = sprite.current_frame; + }; + + Sprite.prototype.clear = function() { + var f, j, len, ref; + ref = this.frames; + for (j = 0, len = ref.length; j < len; j++) { + f = ref[j]; + f.clear(); + } + }; + + Sprite.prototype.addFrame = function() { + return this.frames.push(new SpriteFrame(this, this.width, this.height)); + }; + + return Sprite; + +})(); diff --git a/static/js/runtime/spriteframe.coffee b/static/js/runtime/spriteframe.coffee new file mode 100644 index 00000000..e8f2bddc --- /dev/null +++ b/static/js/runtime/spriteframe.coffee @@ -0,0 +1,66 @@ +class @SpriteFrame + constructor:(@sprite,@width,@height)-> + @name = "" + @canvas = document.createElement "canvas" + @canvas.width = @width + @canvas.height = @height + + clone:()-> + sf = new SpriteFrame @sprite,@width,@height + sf.getContext().drawImage @canvas,0,0 + sf + + getContext:()-> + return null if not @canvas? + @context = @getCanvas().getContext "2d" + + getCanvas:()-> + return null if not @canvas? + if @canvas not instanceof HTMLCanvasElement + c = document.createElement "canvas" + c.width = @canvas.width + c.height = @canvas.height + c.getContext("2d").drawImage @canvas,0,0 + @canvas = c + + @canvas + + setPixel:(x,y,color,alpha=1)-> + c = @getContext() + c.globalAlpha = alpha + c.fillStyle = color + c.fillRect x,y,1,1 + c.globalAlpha = 1 + + erasePixel:(x,y,alpha=1)-> + c = @getContext() + data = c.getImageData x,y,1,1 + data.data[3] *= (1-alpha) + c.putImageData data,x,y + + getRGB:(x,y)-> + return [0,0,0] if x<0 or x>=@width or y<0 or y>=@height + c = @getContext() + data = c.getImageData x,y,1,1 + return data.data + + clear:()-> + @getContext().clearRect 0,0,@canvas.width,@canvas.height + + resize:(w,h)-> + return if w == @width and h == @height + c = new PixelArtScaler().rescale(@canvas,w,h) + @canvas = c + @context = null + @width = w + @height = h + + load:(img)-> + @resize(img.width,img.height) + @clear() + @canvas.getContext("2d").drawImage img,0,0 + + copyFrom:(frame)-> + @resize frame.width,frame.height + @clear() + @getContext().drawImage frame.canvas,0,0 diff --git a/static/js/runtime/spriteframe.js b/static/js/runtime/spriteframe.js new file mode 100644 index 00000000..f837344b --- /dev/null +++ b/static/js/runtime/spriteframe.js @@ -0,0 +1,104 @@ +this.SpriteFrame = (function() { + function SpriteFrame(sprite, width, height) { + this.sprite = sprite; + this.width = width; + this.height = height; + this.name = ""; + this.canvas = document.createElement("canvas"); + this.canvas.width = this.width; + this.canvas.height = this.height; + } + + SpriteFrame.prototype.clone = function() { + var sf; + sf = new SpriteFrame(this.sprite, this.width, this.height); + sf.getContext().drawImage(this.canvas, 0, 0); + return sf; + }; + + SpriteFrame.prototype.getContext = function() { + if (this.canvas == null) { + return null; + } + return this.context = this.getCanvas().getContext("2d"); + }; + + SpriteFrame.prototype.getCanvas = function() { + var c; + if (this.canvas == null) { + return null; + } + if (!(this.canvas instanceof HTMLCanvasElement)) { + c = document.createElement("canvas"); + c.width = this.canvas.width; + c.height = this.canvas.height; + c.getContext("2d").drawImage(this.canvas, 0, 0); + this.canvas = c; + } + return this.canvas; + }; + + SpriteFrame.prototype.setPixel = function(x, y, color, alpha) { + var c; + if (alpha == null) { + alpha = 1; + } + c = this.getContext(); + c.globalAlpha = alpha; + c.fillStyle = color; + c.fillRect(x, y, 1, 1); + return c.globalAlpha = 1; + }; + + SpriteFrame.prototype.erasePixel = function(x, y, alpha) { + var c, data; + if (alpha == null) { + alpha = 1; + } + c = this.getContext(); + data = c.getImageData(x, y, 1, 1); + data.data[3] *= 1 - alpha; + return c.putImageData(data, x, y); + }; + + SpriteFrame.prototype.getRGB = function(x, y) { + var c, data; + if (x < 0 || x >= this.width || y < 0 || y >= this.height) { + return [0, 0, 0]; + } + c = this.getContext(); + data = c.getImageData(x, y, 1, 1); + return data.data; + }; + + SpriteFrame.prototype.clear = function() { + return this.getContext().clearRect(0, 0, this.canvas.width, this.canvas.height); + }; + + SpriteFrame.prototype.resize = function(w, h) { + var c; + if (w === this.width && h === this.height) { + return; + } + c = new PixelArtScaler().rescale(this.canvas, w, h); + this.canvas = c; + this.context = null; + this.width = w; + return this.height = h; + }; + + SpriteFrame.prototype.load = function(img) { + this.resize(img.width, img.height); + this.clear(); + return this.canvas.getContext("2d").drawImage(img, 0, 0); + }; + + SpriteFrame.prototype.copyFrom = function(frame) { + this.resize(frame.width, frame.height); + this.clear(); + return this.getContext().drawImage(frame.canvas, 0, 0); + }; + + return SpriteFrame; + +})(); diff --git a/static/js/sound/audiocontroller.coffee b/static/js/sound/audiocontroller.coffee new file mode 100644 index 00000000..cbf4e107 --- /dev/null +++ b/static/js/sound/audiocontroller.coffee @@ -0,0 +1,42 @@ +class @AudioController + constructor:(@app)-> + + init:()-> + @midiScan() + funk = ()=> + @start() + document.body.removeEventListener "click",funk + + document.body.addEventListener "click",funk + + start:()-> + @context = new (window.AudioContext || window.webkitAudioContext) + + if @context.audioWorklet? + url = "/audioengine.js" + @context.audioWorklet.addModule(url).then ()=> + @node = new AudioWorkletNode(@context,"my-worklet-processor",{outputChannelCount: [2] }) + @node.connect(@context.destination) + + sendParam:(id,value)-> + @node.port.postMessage JSON.stringify + name: "param" + id: id + value: value + + sendNote:(data)-> + @node.port.postMessage JSON.stringify + name: "note" + data: data + + midiScan:()-> + if navigator.requestMIDIAccess? + navigator.requestMIDIAccess().then ((midi)=>@midiAccess(midi)),(()=>) + + midiAccess:(midi)-> + midi.inputs.forEach (port)=>port.onmidimessage = (message)=>@onMidiMessage(message) + true + + onMidiMessage:(message)-> + console.info message.data + @sendNote message.data diff --git a/static/js/sound/audiocontroller.js b/static/js/sound/audiocontroller.js new file mode 100644 index 00000000..14b4713b --- /dev/null +++ b/static/js/sound/audiocontroller.js @@ -0,0 +1,79 @@ +this.AudioController = (function() { + function AudioController(app) { + this.app = app; + } + + AudioController.prototype.init = function() { + var funk; + this.midiScan(); + funk = (function(_this) { + return function() { + _this.start(); + return document.body.removeEventListener("click", funk); + }; + })(this); + return document.body.addEventListener("click", funk); + }; + + AudioController.prototype.start = function() { + var url; + this.context = new (window.AudioContext || window.webkitAudioContext); + if (this.context.audioWorklet != null) { + url = "/audioengine.js"; + return this.context.audioWorklet.addModule(url).then((function(_this) { + return function() { + _this.node = new AudioWorkletNode(_this.context, "my-worklet-processor", { + outputChannelCount: [2] + }); + return _this.node.connect(_this.context.destination); + }; + })(this)); + } + }; + + AudioController.prototype.sendParam = function(id, value) { + return this.node.port.postMessage(JSON.stringify({ + name: "param", + id: id, + value: value + })); + }; + + AudioController.prototype.sendNote = function(data) { + return this.node.port.postMessage(JSON.stringify({ + name: "note", + data: data + })); + }; + + AudioController.prototype.midiScan = function() { + if (navigator.requestMIDIAccess != null) { + return navigator.requestMIDIAccess().then(((function(_this) { + return function(midi) { + return _this.midiAccess(midi); + }; + })(this)), ((function(_this) { + return function() {}; + })(this))); + } + }; + + AudioController.prototype.midiAccess = function(midi) { + midi.inputs.forEach((function(_this) { + return function(port) { + return port.onmidimessage = function(message) { + return _this.onMidiMessage(message); + }; + }; + })(this)); + return true; + }; + + AudioController.prototype.onMidiMessage = function(message) { + console.info(message.data); + return this.sendNote(message.data); + }; + + return AudioController; + +})(); diff --git a/static/js/sound/audioengine/audioengine.coffee b/static/js/sound/audioengine/audioengine.coffee new file mode 100644 index 00000000..21a1b29e --- /dev/null +++ b/static/js/sound/audioengine/audioengine.coffee @@ -0,0 +1,134 @@ +class AudioEngine + constructor:(@sampleRate)-> + @voices = [] + @voice_index = 0 + @num_voices = 8 + + for i in [0..@num_voices-1] + @voices[i] = new Voice @ + + @instruments = [] + @instruments.push new Instrument @ + + @avg = 0 + @samples = 0 + @time = 0 + + @layer = + inputs: @inputs + + @start = Date.now() + + event:(data)-> + if data[0] == 144 and data[2]>0 + @instruments[0].noteOn(data[1],data[2]) + else if data[0] == 128 or (data[0] == 144 and data[2] == 0) + @instruments[0].noteOff(data[1]) + else if data[0]>=176 and data[0]<192 and data[1] == 1 # modulation wheel + @instruments[0].setModulation(data[2]/127) + else if data[0]>=224 and data[0]<240 # pitch bend + v = (data[1]+128*data[2]) + console.info "PB value=#{v}" + if v>=8192 + v = (v-8192)/(16383-8192) + v = .5+.5*v + else + v = .5*v/8192 + console.info("Pitch Bend = #{v}") + @instruments[0].setPitchBend(v) + + return + + getTime:()-> + (Date.now()-@start)/1000 + + getVoice:()-> + best = @voices[0] + + for i in [1..@voices.length-1] + v = @voices[i] + if best.on + if v.on + if v.noteon_time + for v in @voices + v.update() + + for l in @instruments[0].layers + l.update() + return + + process:(inputs,outputs,parameters)-> + output = outputs[0] + time = Date.now() + res = [0,0] + + for inst in @instruments + inst.process(output[0].length) + + for channel,i in output + if i<2 + for j in [0..channel.length-1] by 1 + sig = 0 + + for inst in @instruments + sig += inst.output[i][j] + + sig *= .125 + sig = if sig<0 then -(1-Math.exp(sig)) else 1-Math.exp(-sig) + + channel[j] = sig + + @time += Date.now()-time + @samples += channel.length + if @samples >= @sampleRate + @samples -= @sampleRate + console.info @time+" ms ; buffer size = "+channel.length + @time = 0 + + return + +` +class MyWorkletProcessor extends AudioWorkletProcessor { + constructor() { + super() + this.synth = new AudioEngine(sampleRate) + this.port.onmessage = (e) => { + console.info(e) + var data = JSON.parse(e.data) + if (data.name == "note") + { + this.synth.event(data.data) + } + else if (data.name == "param") + { + var value = data.value + var s = data.id.split(".") + data = this.synth.instruments[0].layers[0].inputs + while (s.length>1) + { + data = data[s.splice(0,1)[0]] + } + data[s[0]] = value + this.synth.updateVoices() + } + } + } + + process(inputs, outputs, parameters) { + this.synth.process(inputs,outputs,parameters) + return true + } +} + +registerProcessor('my-worklet-processor', MyWorkletProcessor) +` diff --git a/static/js/sound/audioengine/audioengine.js b/static/js/sound/audioengine/audioengine.js new file mode 100644 index 00000000..42fc32fe --- /dev/null +++ b/static/js/sound/audioengine/audioengine.js @@ -0,0 +1,159 @@ +var AudioEngine; + +AudioEngine = (function() { + function AudioEngine(sampleRate) { + var i, k, ref; + this.sampleRate = sampleRate; + this.voices = []; + this.voice_index = 0; + this.num_voices = 8; + for (i = k = 0, ref = this.num_voices - 1; 0 <= ref ? k <= ref : k >= ref; i = 0 <= ref ? ++k : --k) { + this.voices[i] = new Voice(this); + } + this.instruments = []; + this.instruments.push(new Instrument(this)); + this.avg = 0; + this.samples = 0; + this.time = 0; + this.layer = { + inputs: this.inputs + }; + this.start = Date.now(); + } + + AudioEngine.prototype.event = function(data) { + var v; + if (data[0] === 144 && data[2] > 0) { + this.instruments[0].noteOn(data[1], data[2]); + } else if (data[0] === 128 || (data[0] === 144 && data[2] === 0)) { + this.instruments[0].noteOff(data[1]); + } else if (data[0] >= 176 && data[0] < 192 && data[1] === 1) { + this.instruments[0].setModulation(data[2] / 127); + } else if (data[0] >= 224 && data[0] < 240) { + v = data[1] + 128 * data[2]; + console.info("PB value=" + v); + if (v >= 8192) { + v = (v - 8192) / (16383 - 8192); + v = .5 + .5 * v; + } else { + v = .5 * v / 8192; + } + console.info("Pitch Bend = " + v); + this.instruments[0].setPitchBend(v); + } + }; + + AudioEngine.prototype.getTime = function() { + return (Date.now() - this.start) / 1000; + }; + + AudioEngine.prototype.getVoice = function() { + var best, i, k, ref, v; + best = this.voices[0]; + for (i = k = 1, ref = this.voices.length - 1; 1 <= ref ? k <= ref : k >= ref; i = 1 <= ref ? ++k : --k) { + v = this.voices[i]; + if (best.on) { + if (v.on) { + if (v.noteon_time < best.noteon_time) { + best = v; + } + } else { + best = v; + } + } else { + if (!v.on && v.env1.sig < best.env1.sig) { + best = v; + } + } + } + return best; + }; + + AudioEngine.prototype.updateVoices = function() { + var k, l, len, len1, m, ref, ref1, v; + ref = this.voices; + for (k = 0, len = ref.length; k < len; k++) { + v = ref[k]; + v.update(); + } + ref1 = this.instruments[0].layers; + for (m = 0, len1 = ref1.length; m < len1; m++) { + l = ref1[m]; + l.update(); + } + }; + + AudioEngine.prototype.process = function(inputs, outputs, parameters) { + var channel, i, inst, j, k, len, len1, len2, m, n, o, output, ref, ref1, ref2, res, sig, time; + output = outputs[0]; + time = Date.now(); + res = [0, 0]; + ref = this.instruments; + for (k = 0, len = ref.length; k < len; k++) { + inst = ref[k]; + inst.process(output[0].length); + } + for (i = m = 0, len1 = output.length; m < len1; i = ++m) { + channel = output[i]; + if (i < 2) { + for (j = n = 0, ref1 = channel.length - 1; n <= ref1; j = n += 1) { + sig = 0; + ref2 = this.instruments; + for (o = 0, len2 = ref2.length; o < len2; o++) { + inst = ref2[o]; + sig += inst.output[i][j]; + } + sig *= .125; + sig = sig < 0 ? -(1 - Math.exp(sig)) : 1 - Math.exp(-sig); + channel[j] = sig; + } + } + } + this.time += Date.now() - time; + this.samples += channel.length; + if (this.samples >= this.sampleRate) { + this.samples -= this.sampleRate; + console.info(this.time + " ms ; buffer size = " + channel.length); + this.time = 0; + } + }; + + return AudioEngine; + +})(); + + +class MyWorkletProcessor extends AudioWorkletProcessor { + constructor() { + super() + this.synth = new AudioEngine(sampleRate) + this.port.onmessage = (e) => { + console.info(e) + var data = JSON.parse(e.data) + if (data.name == "note") + { + this.synth.event(data.data) + } + else if (data.name == "param") + { + var value = data.value + var s = data.id.split(".") + data = this.synth.instruments[0].layers[0].inputs + while (s.length>1) + { + data = data[s.splice(0,1)[0]] + } + data[s[0]] = value + this.synth.updateVoices() + } + } + } + + process(inputs, outputs, parameters) { + this.synth.process(inputs,outputs,parameters) + return true + } +} + +registerProcessor('my-worklet-processor', MyWorkletProcessor) +; diff --git a/static/js/sound/audioengine/effects.coffee b/static/js/sound/audioengine/effects.coffee new file mode 100644 index 00000000..716a6b89 --- /dev/null +++ b/static/js/sound/audioengine/effects.coffee @@ -0,0 +1,473 @@ + +class Distortion + constructor:()-> + @amount = .5 + @rate = .5 + @left = @right = 0 + + update:(data)-> + @amount = data.amount + @rate = data.rate + + process:(buffer,length)-> + for i in [0..length-1] by 1 + sig = buffer[0][i] + sig = @left = @left*.5+sig*.5 + s = sig*(1+@rate*@rate*99) + disto = if s<0 then -1+Math.exp(s) else 1-Math.exp(-s) + buffer[0][i] = (1-@amount)*sig+@amount*disto + + sig = buffer[1][i] + sig = @right = @right*.5+sig*.5 + s = sig*(1+@rate*@rate*99) + disto = if s<0 then -1+Math.exp(s) else 1-Math.exp(-s) + buffer[1][i] = (1-@amount)*sig+@amount*disto + return + +class BitCrusher + constructor:()-> + @phase = 0 + @left = 0 + @right = 0 + + update:(data)-> + @amount = Math.pow(2,data.amount*8) + @rate = Math.pow(2,(1-data.rate)*8) + + process:(buffer,length)-> + for i in [0..length-1] by 1 + @phase += 1 + if @phase>@amount + @phase -= @amount + left = buffer[0][i] + right = buffer[1][i] + @left = Math.round(left*@rate)/@rate + @right = Math.round(right*@rate)/@rate + + buffer[0][i] = @left + buffer[1][i] = @right + return + +class Chorus + constructor:(@engine)-> + @sampleRate = @engine.sampleRate + @left_buffer = new Float64Array(@sampleRate) + @right_buffer = new Float64Array(@sampleRate) + @phase1 = Math.random() + @phase2 = Math.random() + @phase3 = Math.random() + @f1 = 1.031/@sampleRate + @f2 = 1.2713/@sampleRate + @f3 = 0.9317/@sampleRate + @index = 0 + + update:(data)-> + @amount = Math.pow(data.amount,.5) + @rate = data.rate + + read:(buffer,pos)-> + pos += buffer.length if pos<0 + i = Math.floor(pos) + a = pos-i + buffer[i]*(1-a)+buffer[(i+1)%buffer.length]*a + + process:(buffer,length)-> + for i in [0..length-1] by 1 + left = @left_buffer[@index] = buffer[0][i] + right = @right_buffer[@index] = buffer[1][i] + + @phase1 += @f1*(.5+.5*@rate) + @phase2 += @f2*(.5+.5*@rate) + @phase3 += @f3*(.5+.5*@rate) + + p1 = (1+Math.sin(@phase1*Math.PI*2))*@left_buffer.length*.002 + p2 = (1+Math.sin(@phase2*Math.PI*2))*@left_buffer.length*.002 + p3 = (1+Math.sin(@phase3*Math.PI*2))*@left_buffer.length*.002 + + @phase1 -= 1 if @phase1>=1 + @phase2 -= 1 if @phase2>=1 + @phase3 -= 1 if @phase3>=1 + + s1 = @read(@left_buffer,@index-p1) + s2 = @read(@right_buffer,@index-p2) + s3 = @read(@right_buffer,@index-p3) + + pleft = @amount*(s1*.2+s2*.7+s3*.1) + pright = @amount*(s1*.6+s2*.2+s3*.2) + + left += pleft + right += pright + + @left_buffer[@index] += pleft*.5*@amount + @right_buffer[@index] += pleft*.5*@amount + @index += 1 + @index = 0 if @index>=@left_buffer.length + + buffer[0][i] = left + buffer[1][i] = right + return + +class Phaser + constructor:(@engine)-> + @sampleRate = @engine.sampleRate + @left_buffer = new Float64Array(@sampleRate) + @right_buffer = new Float64Array(@sampleRate) + @phase1 = Math.random() + @phase2 = Math.random() + @f1 = .0573/@sampleRate + @f2 = .0497/@sampleRate + @index = 0 + + update:(data)-> + @amount = data.amount + @rate = data.rate + + read:(buffer,pos)-> + pos += buffer.length if pos<0 + i = Math.floor(pos) + a = pos-i + buffer[i]*(1-a)+buffer[(i+1)%buffer.length]*a + + process:(buffer,length)-> + for i in [0..length-1] by 1 + left = buffer[0][i] + right = buffer[1][i] + + @phase1 += @f1*(.5+.5*@rate) + @phase2 += @f2*(.5+.5*@rate) + + o1 = (1+Math.sin(@phase1*Math.PI*2))/2 + p1 = @sampleRate*(.0001+.05*o1) + + o2 = (1+Math.sin(@phase2*Math.PI*2))/2 + p2 = @sampleRate*(.0001+.05*o2) + + @phase1 -= 1 if @phase1>=1 + @phase2 -= 1 if @phase2>=1 + + @left_buffer[@index] = left + @right_buffer[@index] = right + + s1 = @read(@left_buffer,@index-p1) + s2 = @read(@right_buffer,@index-p2) + + @left_buffer[@index] += s1*@rate*.9 + @right_buffer[@index] += s2*@rate*.9 + + buffer[0][i] = s1*@amount-left + buffer[1][i] = s2*@amount-right + + @index += 1 + @index = 0 if @index>=@left_buffer.length + return + +class Flanger + constructor:(@engine)-> + @sampleRate = @engine.sampleRate + @left_buffer = new Float64Array(@sampleRate) + @right_buffer = new Float64Array(@sampleRate) + @phase1 = 0 #Math.random() + @phase2 = 0 #Math.random() + @f1 = .0573/@sampleRate + @f2 = .0497/@sampleRate + @index = 0 + + update:(data)-> + @amount = data.amount + @rate = data.rate + + read:(buffer,pos)-> + pos += buffer.length if pos<0 + i = Math.floor(pos) + a = pos-i + buffer[i]*(1-a)+buffer[(i+1)%buffer.length]*a + + process:(buffer,length)-> + for i in [0..length-1] by 1 + left = buffer[0][i] + right = buffer[1][i] + + @phase1 += @f1 + @phase2 += @f2 + + o1 = (1+Math.sin(@phase1*Math.PI*2))/2 + p1 = @sampleRate*(.0001+.05*o1) + + o2 = (1+Math.sin(@phase2*Math.PI*2))/2 + p2 = @sampleRate*(.0001+.05*o2) + + @phase1 -= 1 if @phase1>=1 + @phase2 -= 1 if @phase2>=1 + + @left_buffer[@index] = left #+s1*.3 + @right_buffer[@index] = right #+s2*.3 + + s1 = @read(@left_buffer,@index-p1) + s2 = @read(@right_buffer,@index-p2) + + @left_buffer[@index] += s1*@rate*.9 + @right_buffer[@index] += s2*@rate*.9 + + buffer[0][i] = s1*@amount+left + buffer[1][i] = s2*@amount+right + + @index += 1 + @index = 0 if @index>=@left_buffer.length + return + +class Delay + constructor:(@engine)-> + @sampleRate = @engine.sampleRate + @left_buffer = new Float64Array(@sampleRate*3) + @right_buffer = new Float64Array(@sampleRate*3) + @index = 0 + @ml = 0 + @mr = 0 + + update:(data)-> + @amount = data.amount + @rate = data.rate + tempo = (30+Math.pow(@rate,2)*170*4)*4 + tick = @sampleRate/(tempo/60) + @L = Math.round(tick*4) + @R = Math.round(tick*4+@sampleRate*0.00075) + @fb = @amount*.95 + + process:(buffer,length)-> + for i in [0..length-1] by 1 + left = buffer[0][i] + right = buffer[1][i] + + left += @right_buffer[(@index+@left_buffer.length-@L)%@left_buffer.length]*@fb + right += @left_buffer[(@index+@right_buffer.length-@R)%@right_buffer.length]*@fb + + buffer[0][i] = left + buffer[1][i] = right + + @left_buffer[@index] = @ml = @ml*.5+left*.5 + @right_buffer[@index] = @mr = @mr*.5+right*.5 + + @index += 1 + @index = 0 if @index>=@left_buffer.length + return + +class Spatializer + constructor:(@engine)-> + @sampleRate = @engine.sampleRate + @left_buffer = new Float64Array(@sampleRate/10) + @right_buffer = new Float64Array(@sampleRate/10) + @index = 0 + @left_delay1 = 0 + @left_delay2 = 0 + @right_delay1 = 0 + @right_delay2 = 0 + + @left = 0 + @right = 0 + + @left_delay = 577 + @right_delay = 733 + + process:(buffer,length,spatialize,pan)-> + mnt = spatialize + + left_pan = Math.cos(pan*Math.PI/2)/(1+spatialize) + right_pan = Math.sin(pan*Math.PI/2)/(1+spatialize) + + left_buffer = buffer[0] + right_buffer = buffer[1] + + for i in [0..length-1] by 1 + left = left_buffer[i] + right = right_buffer[i] + + @left = left*.1+@left*.9 + @right = right*.1+@right*.9 + + @left_buffer[@index] = @left + @right_buffer[@index] = @right + + left_buffer[i] = (left+mnt*@right_buffer[(@index+@right_buffer.length-@left_delay)%@right_buffer.length])*left_pan + right_buffer[i] = (right+mnt*@left_buffer[(@index+@right_buffer.length-@right_delay)%@right_buffer.length])*right_pan + + @index += 1 + @index = 0 if @index>=@left_buffer.length + return + +class EQ + constructor:(@engine)-> + @sampleRate = @engine.sampleRate + + + # band pass for medium freqs + @mid = 900 + q = .5 + w0 = 2*Math.PI*@mid/@sampleRate + cosw0 = Math.cos(w0) + sinw0 = Math.sin(w0) + alpha = sinw0 / (2 * q) + invOnePlusAlpha = 1/(1 + alpha) + oneLessCosw0 = 1 - cosw0 + + @a0 = (-2 * cosw0) * invOnePlusAlpha + @a1 = (1 - alpha) * invOnePlusAlpha + + @b0 = q * alpha * invOnePlusAlpha + @b1 = 0 + @b2 = -@b0 + + @llow = 0 + @rlow = 0 + + @lfm0 = 0 + @lfm1 = 0 + @rfm0 = 0 + @rfm1 = 0 + + @low = 1 + @mid = 1 + @high = 1 + + update:(data)-> + @low = data.low*2 + @mid = data.mid*2 + @high = data.high*2 + + processBandPass:(sig,cutoff,q)-> + w = sig - a0*@fm0 - a1*@fm1 + sig = b0*w + b1*@fm0 + b2*@fm1 + + @fm1 = @fm0 + @fm0 = w + + sig + + process:(buffer,length)-> + for i in [0..length-1] by 1 + left = buffer[0][i] + right = buffer[1][i] + + lw = left - @a0*@lfm0 - @a1*@lfm1 + lmid = @b0*lw + @b1*@lfm0 + @b2*@lfm1 + + @lfm1 = @lfm0 + @lfm0 = lw + + left -= lmid + + @llow = left*.1+@llow*.9 + + lhigh = left-@llow + + buffer[0][i] = @llow*@low+lmid*@mid+lhigh*@high + + rw = right - @a0*@rfm0 - @a1*@rfm1 + rmid = @b0*rw + @b1*@rfm0 + @b2*@rfm1 + + @rfm1 = @rfm0 + @rfm0 = rw + + right -= rmid + + @rlow = right*.1+@rlow*.9 + + rhigh = right-@rlow + + buffer[1][i] = @rlow*@low+rmid*@mid+rhigh*@high + + return + +class CombFilter + constructor:(@length,@feedback = .5)-> + @buffer = new Float64Array(@length) + @index = 0 + @store = 0 + @damp = .2 + + process:(input)-> + output = @buffer[@index] + @store = output*(1-@damp)+@store*@damp + @buffer[@index++] = input+@store*@feedback + if @index>=@length + @index = 0 + + output + +class AllpassFilter + constructor:(@length,@feedback = .5)-> + @buffer = new Float64Array(@length) + @index = 0 + + process:(input)-> + bufout = @buffer[@index] + output = -input+bufout + @buffer[@index++] = input+bufout*@feedback + if @index>=@length + @index = 0 + + output + +class Reverb + constructor:(@engine)-> + @sampleRate = @engine.sampleRate + combtuning = [1116,1188,1277,1356,1422,1491,1557,1617] + allpasstuning = [556,441,341,225] + stereospread = 23 + @left_combs = [] + @right_combs = [] + @left_allpass = [] + @right_allpass = [] + @res = [0,0] + @spread = .25 + @wet = .025 + + for c in combtuning + @left_combs.push new CombFilter c,.9 + @right_combs.push new CombFilter c+stereospread,.9 + + for a in allpasstuning + @left_allpass.push new AllpassFilter a,.5 + @right_allpass.push new AllpassFilter a+stereospread,.5 + + #@reflections = [] + #@reflections.push new Reflection 7830,.1,.5,1 + #@reflections.push new Reflection 10670,-.2,.6,.9 + #@reflections.push new Reflection 13630,.7,.7,.8 + #@reflections.push new Reflection 21870,-.6,.8,.7 + #@reflections.push new Reflection 35810,-.1,.9,.6 + + update:(data)-> + @wet = data.amount*.05 + feedback = .7+Math.pow(data.rate,.25)*.29 + damp = .2 #.4-.4*data.rate + for i in [0..@left_combs.length-1] by 1 + @left_combs[i].feedback = feedback + @right_combs[i].feedback = feedback + @left_combs[i].damp = damp + @right_combs[i].damp = damp + + @spread = .5-data.rate*.5 + + process:(buffer,length)-> + for s in [0..length-1] by 1 + outL = 0 + outR = 0 + left = buffer[0][s] + right = buffer[1][s] + input = (left+right)*.5 + for i in [0..@left_combs.length-1] + outL += @left_combs[i].process(input) + outR += @right_combs[i].process(input) + + for i in [0..@left_allpass.length-1] + outL = @left_allpass[i].process(outL) + outR = @right_allpass[i].process(outR) + + #for i in [0..@reflections.length-1] + # r = @reflections[i].process(input) + # outL += r[0] + # outR += r[1] + + buffer[0][s] = (outL*(1-@spread)+outR*@spread)*@wet+left*(1-@wet) + buffer[1][s] = (outR*(1-@spread)+outL*@spread)*@wet+right*(1-@wet) + return diff --git a/static/js/sound/audioengine/effects.js b/static/js/sound/audioengine/effects.js new file mode 100644 index 00000000..c5164b40 --- /dev/null +++ b/static/js/sound/audioengine/effects.js @@ -0,0 +1,539 @@ +var AllpassFilter, BitCrusher, Chorus, CombFilter, Delay, Distortion, EQ, Flanger, Phaser, Reverb, Spatializer; + +Distortion = (function() { + function Distortion() { + this.amount = .5; + this.rate = .5; + this.left = this.right = 0; + } + + Distortion.prototype.update = function(data) { + this.amount = data.amount; + return this.rate = data.rate; + }; + + Distortion.prototype.process = function(buffer, length) { + var disto, i, j, ref, s, sig; + for (i = j = 0, ref = length - 1; j <= ref; i = j += 1) { + sig = buffer[0][i]; + sig = this.left = this.left * .5 + sig * .5; + s = sig * (1 + this.rate * this.rate * 99); + disto = s < 0 ? -1 + Math.exp(s) : 1 - Math.exp(-s); + buffer[0][i] = (1 - this.amount) * sig + this.amount * disto; + sig = buffer[1][i]; + sig = this.right = this.right * .5 + sig * .5; + s = sig * (1 + this.rate * this.rate * 99); + disto = s < 0 ? -1 + Math.exp(s) : 1 - Math.exp(-s); + buffer[1][i] = (1 - this.amount) * sig + this.amount * disto; + } + }; + + return Distortion; + +})(); + +BitCrusher = (function() { + function BitCrusher() { + this.phase = 0; + this.left = 0; + this.right = 0; + } + + BitCrusher.prototype.update = function(data) { + this.amount = Math.pow(2, data.amount * 8); + return this.rate = Math.pow(2, (1 - data.rate) * 8); + }; + + BitCrusher.prototype.process = function(buffer, length) { + var i, j, left, ref, right; + for (i = j = 0, ref = length - 1; j <= ref; i = j += 1) { + this.phase += 1; + if (this.phase > this.amount) { + this.phase -= this.amount; + left = buffer[0][i]; + right = buffer[1][i]; + this.left = Math.round(left * this.rate) / this.rate; + this.right = Math.round(right * this.rate) / this.rate; + } + buffer[0][i] = this.left; + buffer[1][i] = this.right; + } + }; + + return BitCrusher; + +})(); + +Chorus = (function() { + function Chorus(engine) { + this.engine = engine; + this.sampleRate = this.engine.sampleRate; + this.left_buffer = new Float64Array(this.sampleRate); + this.right_buffer = new Float64Array(this.sampleRate); + this.phase1 = Math.random(); + this.phase2 = Math.random(); + this.phase3 = Math.random(); + this.f1 = 1.031 / this.sampleRate; + this.f2 = 1.2713 / this.sampleRate; + this.f3 = 0.9317 / this.sampleRate; + this.index = 0; + } + + Chorus.prototype.update = function(data) { + this.amount = Math.pow(data.amount, .5); + return this.rate = data.rate; + }; + + Chorus.prototype.read = function(buffer, pos) { + var a, i; + if (pos < 0) { + pos += buffer.length; + } + i = Math.floor(pos); + a = pos - i; + return buffer[i] * (1 - a) + buffer[(i + 1) % buffer.length] * a; + }; + + Chorus.prototype.process = function(buffer, length) { + var i, j, left, p1, p2, p3, pleft, pright, ref, right, s1, s2, s3; + for (i = j = 0, ref = length - 1; j <= ref; i = j += 1) { + left = this.left_buffer[this.index] = buffer[0][i]; + right = this.right_buffer[this.index] = buffer[1][i]; + this.phase1 += this.f1 * (.5 + .5 * this.rate); + this.phase2 += this.f2 * (.5 + .5 * this.rate); + this.phase3 += this.f3 * (.5 + .5 * this.rate); + p1 = (1 + Math.sin(this.phase1 * Math.PI * 2)) * this.left_buffer.length * .002; + p2 = (1 + Math.sin(this.phase2 * Math.PI * 2)) * this.left_buffer.length * .002; + p3 = (1 + Math.sin(this.phase3 * Math.PI * 2)) * this.left_buffer.length * .002; + if (this.phase1 >= 1) { + this.phase1 -= 1; + } + if (this.phase2 >= 1) { + this.phase2 -= 1; + } + if (this.phase3 >= 1) { + this.phase3 -= 1; + } + s1 = this.read(this.left_buffer, this.index - p1); + s2 = this.read(this.right_buffer, this.index - p2); + s3 = this.read(this.right_buffer, this.index - p3); + pleft = this.amount * (s1 * .2 + s2 * .7 + s3 * .1); + pright = this.amount * (s1 * .6 + s2 * .2 + s3 * .2); + left += pleft; + right += pright; + this.left_buffer[this.index] += pleft * .5 * this.amount; + this.right_buffer[this.index] += pleft * .5 * this.amount; + this.index += 1; + if (this.index >= this.left_buffer.length) { + this.index = 0; + } + buffer[0][i] = left; + buffer[1][i] = right; + } + }; + + return Chorus; + +})(); + +Phaser = (function() { + function Phaser(engine) { + this.engine = engine; + this.sampleRate = this.engine.sampleRate; + this.left_buffer = new Float64Array(this.sampleRate); + this.right_buffer = new Float64Array(this.sampleRate); + this.phase1 = Math.random(); + this.phase2 = Math.random(); + this.f1 = .0573 / this.sampleRate; + this.f2 = .0497 / this.sampleRate; + this.index = 0; + } + + Phaser.prototype.update = function(data) { + this.amount = data.amount; + return this.rate = data.rate; + }; + + Phaser.prototype.read = function(buffer, pos) { + var a, i; + if (pos < 0) { + pos += buffer.length; + } + i = Math.floor(pos); + a = pos - i; + return buffer[i] * (1 - a) + buffer[(i + 1) % buffer.length] * a; + }; + + Phaser.prototype.process = function(buffer, length) { + var i, j, left, o1, o2, p1, p2, ref, right, s1, s2; + for (i = j = 0, ref = length - 1; j <= ref; i = j += 1) { + left = buffer[0][i]; + right = buffer[1][i]; + this.phase1 += this.f1 * (.5 + .5 * this.rate); + this.phase2 += this.f2 * (.5 + .5 * this.rate); + o1 = (1 + Math.sin(this.phase1 * Math.PI * 2)) / 2; + p1 = this.sampleRate * (.0001 + .05 * o1); + o2 = (1 + Math.sin(this.phase2 * Math.PI * 2)) / 2; + p2 = this.sampleRate * (.0001 + .05 * o2); + if (this.phase1 >= 1) { + this.phase1 -= 1; + } + if (this.phase2 >= 1) { + this.phase2 -= 1; + } + this.left_buffer[this.index] = left; + this.right_buffer[this.index] = right; + s1 = this.read(this.left_buffer, this.index - p1); + s2 = this.read(this.right_buffer, this.index - p2); + this.left_buffer[this.index] += s1 * this.rate * .9; + this.right_buffer[this.index] += s2 * this.rate * .9; + buffer[0][i] = s1 * this.amount - left; + buffer[1][i] = s2 * this.amount - right; + this.index += 1; + if (this.index >= this.left_buffer.length) { + this.index = 0; + } + } + }; + + return Phaser; + +})(); + +Flanger = (function() { + function Flanger(engine) { + this.engine = engine; + this.sampleRate = this.engine.sampleRate; + this.left_buffer = new Float64Array(this.sampleRate); + this.right_buffer = new Float64Array(this.sampleRate); + this.phase1 = 0; + this.phase2 = 0; + this.f1 = .0573 / this.sampleRate; + this.f2 = .0497 / this.sampleRate; + this.index = 0; + } + + Flanger.prototype.update = function(data) { + this.amount = data.amount; + return this.rate = data.rate; + }; + + Flanger.prototype.read = function(buffer, pos) { + var a, i; + if (pos < 0) { + pos += buffer.length; + } + i = Math.floor(pos); + a = pos - i; + return buffer[i] * (1 - a) + buffer[(i + 1) % buffer.length] * a; + }; + + Flanger.prototype.process = function(buffer, length) { + var i, j, left, o1, o2, p1, p2, ref, right, s1, s2; + for (i = j = 0, ref = length - 1; j <= ref; i = j += 1) { + left = buffer[0][i]; + right = buffer[1][i]; + this.phase1 += this.f1; + this.phase2 += this.f2; + o1 = (1 + Math.sin(this.phase1 * Math.PI * 2)) / 2; + p1 = this.sampleRate * (.0001 + .05 * o1); + o2 = (1 + Math.sin(this.phase2 * Math.PI * 2)) / 2; + p2 = this.sampleRate * (.0001 + .05 * o2); + if (this.phase1 >= 1) { + this.phase1 -= 1; + } + if (this.phase2 >= 1) { + this.phase2 -= 1; + } + this.left_buffer[this.index] = left; + this.right_buffer[this.index] = right; + s1 = this.read(this.left_buffer, this.index - p1); + s2 = this.read(this.right_buffer, this.index - p2); + this.left_buffer[this.index] += s1 * this.rate * .9; + this.right_buffer[this.index] += s2 * this.rate * .9; + buffer[0][i] = s1 * this.amount + left; + buffer[1][i] = s2 * this.amount + right; + this.index += 1; + if (this.index >= this.left_buffer.length) { + this.index = 0; + } + } + }; + + return Flanger; + +})(); + +Delay = (function() { + function Delay(engine) { + this.engine = engine; + this.sampleRate = this.engine.sampleRate; + this.left_buffer = new Float64Array(this.sampleRate * 3); + this.right_buffer = new Float64Array(this.sampleRate * 3); + this.index = 0; + this.ml = 0; + this.mr = 0; + } + + Delay.prototype.update = function(data) { + var tempo, tick; + this.amount = data.amount; + this.rate = data.rate; + tempo = (30 + Math.pow(this.rate, 2) * 170 * 4) * 4; + tick = this.sampleRate / (tempo / 60); + this.L = Math.round(tick * 4); + this.R = Math.round(tick * 4 + this.sampleRate * 0.00075); + return this.fb = this.amount * .95; + }; + + Delay.prototype.process = function(buffer, length) { + var i, j, left, ref, right; + for (i = j = 0, ref = length - 1; j <= ref; i = j += 1) { + left = buffer[0][i]; + right = buffer[1][i]; + left += this.right_buffer[(this.index + this.left_buffer.length - this.L) % this.left_buffer.length] * this.fb; + right += this.left_buffer[(this.index + this.right_buffer.length - this.R) % this.right_buffer.length] * this.fb; + buffer[0][i] = left; + buffer[1][i] = right; + this.left_buffer[this.index] = this.ml = this.ml * .5 + left * .5; + this.right_buffer[this.index] = this.mr = this.mr * .5 + right * .5; + this.index += 1; + if (this.index >= this.left_buffer.length) { + this.index = 0; + } + } + }; + + return Delay; + +})(); + +Spatializer = (function() { + function Spatializer(engine) { + this.engine = engine; + this.sampleRate = this.engine.sampleRate; + this.left_buffer = new Float64Array(this.sampleRate / 10); + this.right_buffer = new Float64Array(this.sampleRate / 10); + this.index = 0; + this.left_delay1 = 0; + this.left_delay2 = 0; + this.right_delay1 = 0; + this.right_delay2 = 0; + this.left = 0; + this.right = 0; + this.left_delay = 577; + this.right_delay = 733; + } + + Spatializer.prototype.process = function(buffer, length, spatialize, pan) { + var i, j, left, left_buffer, left_pan, mnt, ref, right, right_buffer, right_pan; + mnt = spatialize; + left_pan = Math.cos(pan * Math.PI / 2) / (1 + spatialize); + right_pan = Math.sin(pan * Math.PI / 2) / (1 + spatialize); + left_buffer = buffer[0]; + right_buffer = buffer[1]; + for (i = j = 0, ref = length - 1; j <= ref; i = j += 1) { + left = left_buffer[i]; + right = right_buffer[i]; + this.left = left * .1 + this.left * .9; + this.right = right * .1 + this.right * .9; + this.left_buffer[this.index] = this.left; + this.right_buffer[this.index] = this.right; + left_buffer[i] = (left + mnt * this.right_buffer[(this.index + this.right_buffer.length - this.left_delay) % this.right_buffer.length]) * left_pan; + right_buffer[i] = (right + mnt * this.left_buffer[(this.index + this.right_buffer.length - this.right_delay) % this.right_buffer.length]) * right_pan; + this.index += 1; + if (this.index >= this.left_buffer.length) { + this.index = 0; + } + } + }; + + return Spatializer; + +})(); + +EQ = (function() { + function EQ(engine) { + var alpha, cosw0, invOnePlusAlpha, oneLessCosw0, q, sinw0, w0; + this.engine = engine; + this.sampleRate = this.engine.sampleRate; + this.mid = 900; + q = .5; + w0 = 2 * Math.PI * this.mid / this.sampleRate; + cosw0 = Math.cos(w0); + sinw0 = Math.sin(w0); + alpha = sinw0 / (2 * q); + invOnePlusAlpha = 1 / (1 + alpha); + oneLessCosw0 = 1 - cosw0; + this.a0 = (-2 * cosw0) * invOnePlusAlpha; + this.a1 = (1 - alpha) * invOnePlusAlpha; + this.b0 = q * alpha * invOnePlusAlpha; + this.b1 = 0; + this.b2 = -this.b0; + this.llow = 0; + this.rlow = 0; + this.lfm0 = 0; + this.lfm1 = 0; + this.rfm0 = 0; + this.rfm1 = 0; + this.low = 1; + this.mid = 1; + this.high = 1; + } + + EQ.prototype.update = function(data) { + this.low = data.low * 2; + this.mid = data.mid * 2; + return this.high = data.high * 2; + }; + + EQ.prototype.processBandPass = function(sig, cutoff, q) { + var w; + w = sig - a0 * this.fm0 - a1 * this.fm1; + sig = b0 * w + b1 * this.fm0 + b2 * this.fm1; + this.fm1 = this.fm0; + this.fm0 = w; + return sig; + }; + + EQ.prototype.process = function(buffer, length) { + var i, j, left, lhigh, lmid, lw, ref, rhigh, right, rmid, rw; + for (i = j = 0, ref = length - 1; j <= ref; i = j += 1) { + left = buffer[0][i]; + right = buffer[1][i]; + lw = left - this.a0 * this.lfm0 - this.a1 * this.lfm1; + lmid = this.b0 * lw + this.b1 * this.lfm0 + this.b2 * this.lfm1; + this.lfm1 = this.lfm0; + this.lfm0 = lw; + left -= lmid; + this.llow = left * .1 + this.llow * .9; + lhigh = left - this.llow; + buffer[0][i] = this.llow * this.low + lmid * this.mid + lhigh * this.high; + rw = right - this.a0 * this.rfm0 - this.a1 * this.rfm1; + rmid = this.b0 * rw + this.b1 * this.rfm0 + this.b2 * this.rfm1; + this.rfm1 = this.rfm0; + this.rfm0 = rw; + right -= rmid; + this.rlow = right * .1 + this.rlow * .9; + rhigh = right - this.rlow; + buffer[1][i] = this.rlow * this.low + rmid * this.mid + rhigh * this.high; + } + }; + + return EQ; + +})(); + +CombFilter = (function() { + function CombFilter(length1, feedback1) { + this.length = length1; + this.feedback = feedback1 != null ? feedback1 : .5; + this.buffer = new Float64Array(this.length); + this.index = 0; + this.store = 0; + this.damp = .2; + } + + CombFilter.prototype.process = function(input) { + var output; + output = this.buffer[this.index]; + this.store = output * (1 - this.damp) + this.store * this.damp; + this.buffer[this.index++] = input + this.store * this.feedback; + if (this.index >= this.length) { + this.index = 0; + } + return output; + }; + + return CombFilter; + +})(); + +AllpassFilter = (function() { + function AllpassFilter(length1, feedback1) { + this.length = length1; + this.feedback = feedback1 != null ? feedback1 : .5; + this.buffer = new Float64Array(this.length); + this.index = 0; + } + + AllpassFilter.prototype.process = function(input) { + var bufout, output; + bufout = this.buffer[this.index]; + output = -input + bufout; + this.buffer[this.index++] = input + bufout * this.feedback; + if (this.index >= this.length) { + this.index = 0; + } + return output; + }; + + return AllpassFilter; + +})(); + +Reverb = (function() { + function Reverb(engine) { + var a, allpasstuning, c, combtuning, j, k, len, len1, stereospread; + this.engine = engine; + this.sampleRate = this.engine.sampleRate; + combtuning = [1116, 1188, 1277, 1356, 1422, 1491, 1557, 1617]; + allpasstuning = [556, 441, 341, 225]; + stereospread = 23; + this.left_combs = []; + this.right_combs = []; + this.left_allpass = []; + this.right_allpass = []; + this.res = [0, 0]; + this.spread = .25; + this.wet = .025; + for (j = 0, len = combtuning.length; j < len; j++) { + c = combtuning[j]; + this.left_combs.push(new CombFilter(c, .9)); + this.right_combs.push(new CombFilter(c + stereospread, .9)); + } + for (k = 0, len1 = allpasstuning.length; k < len1; k++) { + a = allpasstuning[k]; + this.left_allpass.push(new AllpassFilter(a, .5)); + this.right_allpass.push(new AllpassFilter(a + stereospread, .5)); + } + } + + Reverb.prototype.update = function(data) { + var damp, feedback, i, j, ref; + this.wet = data.amount * .05; + feedback = .7 + Math.pow(data.rate, .25) * .29; + damp = .2; + for (i = j = 0, ref = this.left_combs.length - 1; j <= ref; i = j += 1) { + this.left_combs[i].feedback = feedback; + this.right_combs[i].feedback = feedback; + this.left_combs[i].damp = damp; + this.right_combs[i].damp = damp; + } + return this.spread = .5 - data.rate * .5; + }; + + Reverb.prototype.process = function(buffer, length) { + var i, input, j, k, l, left, outL, outR, ref, ref1, ref2, right, s; + for (s = j = 0, ref = length - 1; j <= ref; s = j += 1) { + outL = 0; + outR = 0; + left = buffer[0][s]; + right = buffer[1][s]; + input = (left + right) * .5; + for (i = k = 0, ref1 = this.left_combs.length - 1; 0 <= ref1 ? k <= ref1 : k >= ref1; i = 0 <= ref1 ? ++k : --k) { + outL += this.left_combs[i].process(input); + outR += this.right_combs[i].process(input); + } + for (i = l = 0, ref2 = this.left_allpass.length - 1; 0 <= ref2 ? l <= ref2 : l >= ref2; i = 0 <= ref2 ? ++l : --l) { + outL = this.left_allpass[i].process(outL); + outR = this.right_allpass[i].process(outR); + } + buffer[0][s] = (outL * (1 - this.spread) + outR * this.spread) * this.wet + left * (1 - this.wet); + buffer[1][s] = (outR * (1 - this.spread) + outL * this.spread) * this.wet + right * (1 - this.wet); + } + }; + + return Reverb; + +})(); diff --git a/static/js/sound/audioengine/filters.coffee b/static/js/sound/audioengine/filters.coffee new file mode 100644 index 00000000..e1f2986a --- /dev/null +++ b/static/js/sound/audioengine/filters.coffee @@ -0,0 +1,135 @@ + +class Filter + constructor:(@voice)-> + @sampleRate = @voice.engine.sampleRate + @invSampleRate = 1/@voice.engine.sampleRate + @halfSampleRate = @sampleRate*.5 + + init:(@layer)-> + @fm00 = 0 + @fm01 = 0 + @fm10 = 0 + @fm11 = 0 + + @update() + + update:()-> + switch @layer.inputs.filter.type + when 0 + @process = @processLowPass + when 1 + @process = @processBandPass + when 2 + @process = @processHighPass + + @slope = @layer.inputs.filter.slope + + processHighPass:(sig,cutoff,q)-> + w0 = Math.max(0,Math.min(@halfSampleRate,cutoff))*@invSampleRate + w0 *= 10000 + iw0 = Math.floor(w0) + aw0 = w0-iw0 + cosw0 = (1-aw0)*SIN_TABLE[iw0+2500]+aw0*SIN_TABLE[iw0+2501] + sinw0 = (1-aw0)*SIN_TABLE[iw0]+aw0*SIN_TABLE[iw0+1] + + if not @slope + q *= q + + alpha = sinw0 / (2 * q) + invOnePlusAlpha = 1/(1 + alpha) + + a0 = (-2 * cosw0) * invOnePlusAlpha + a1 = (1 - alpha) * invOnePlusAlpha + + onePlusCosw0 = 1 + cosw0 + b0 = onePlusCosw0*.5* invOnePlusAlpha + b1 = -onePlusCosw0* invOnePlusAlpha + b2 = b0 + + w = sig - a0*@fm00 - a1*@fm01 + sig = b0*w + b1*@fm00 + b2*@fm01 + + @fm01 = @fm00 + @fm00 = w + + if @slope + w = sig - a0*@fm10 - a1*@fm11 + sig = b0*w + b1*@fm10 + b2*@fm11 + + @fm11 = @fm10 + @fm10 = w + + sig + + processBandPass:(sig,cutoff,q)-> + w0 = Math.max(0,Math.min(@halfSampleRate,cutoff))*@invSampleRate + w0 *= 10000 + iw0 = Math.floor(w0) + aw0 = w0-iw0 + cosw0 = (1-aw0)*SIN_TABLE[iw0+2500]+aw0*SIN_TABLE[iw0+2501] + sinw0 = (1-aw0)*SIN_TABLE[iw0]+aw0*SIN_TABLE[iw0+1] + + if not @slope + q *= q + + alpha = sinw0 / (2 * q) + invOnePlusAlpha = 1/(1 + alpha) + + oneLessCosw0 = 1 - cosw0 + + a0 = (-2 * cosw0) * invOnePlusAlpha + a1 = (1 - alpha) * invOnePlusAlpha + + b0 = q * alpha * invOnePlusAlpha + b1 = 0 + b2 = -b0 + + w = sig - a0*@fm00 - a1*@fm01 + sig = b0*w + b1*@fm00 + b2*@fm01 + + @fm01 = @fm00 + @fm00 = w + + if @slope + w = sig - a0*@fm10 - a1*@fm11 + sig = b0*w + b1*@fm10 + b2*@fm11 + + @fm11 = @fm10 + @fm10 = w + + sig + + processLowPass:(sig,cutoff,q)-> + w0 = Math.max(0,Math.min(@halfSampleRate,cutoff))*@invSampleRate + w0 *= 10000 + iw0 = Math.floor(w0) + aw0 = w0-iw0 + cosw0 = (1-aw0)*SIN_TABLE[iw0+2500]+aw0*SIN_TABLE[iw0+2501] + sinw0 = (1-aw0)*SIN_TABLE[iw0]+aw0*SIN_TABLE[iw0+1] + + if not @slope + q *= q + alpha = sinw0 / (2 * q) + invOnePlusAlpha = 1/(1 + alpha) + + oneLessCosw0 = 1 - cosw0 + b1 = oneLessCosw0 * invOnePlusAlpha + b0 = b1*.5 + b2 = b0 + a0 = (-2 * cosw0) * invOnePlusAlpha + a1 = (1 - alpha) * invOnePlusAlpha + + w = sig - a0*@fm00 - a1*@fm01 + sig = b0*w + b1*@fm00 + b2*@fm01 + + @fm01 = @fm00 + @fm00 = w + + if @slope + w = sig - a0*@fm10 - a1*@fm11 + sig = b0*w + b1*@fm10 + b2*@fm11 + + @fm11 = @fm10 + @fm10 = w + + sig diff --git a/static/js/sound/audioengine/filters.js b/static/js/sound/audioengine/filters.js new file mode 100644 index 00000000..587ef0c3 --- /dev/null +++ b/static/js/sound/audioengine/filters.js @@ -0,0 +1,132 @@ +var Filter; + +Filter = (function() { + function Filter(voice) { + this.voice = voice; + this.sampleRate = this.voice.engine.sampleRate; + this.invSampleRate = 1 / this.voice.engine.sampleRate; + this.halfSampleRate = this.sampleRate * .5; + } + + Filter.prototype.init = function(layer) { + this.layer = layer; + this.fm00 = 0; + this.fm01 = 0; + this.fm10 = 0; + this.fm11 = 0; + return this.update(); + }; + + Filter.prototype.update = function() { + switch (this.layer.inputs.filter.type) { + case 0: + this.process = this.processLowPass; + break; + case 1: + this.process = this.processBandPass; + break; + case 2: + this.process = this.processHighPass; + } + return this.slope = this.layer.inputs.filter.slope; + }; + + Filter.prototype.processHighPass = function(sig, cutoff, q) { + var a0, a1, alpha, aw0, b0, b1, b2, cosw0, invOnePlusAlpha, iw0, onePlusCosw0, sinw0, w, w0; + w0 = Math.max(0, Math.min(this.halfSampleRate, cutoff)) * this.invSampleRate; + w0 *= 10000; + iw0 = Math.floor(w0); + aw0 = w0 - iw0; + cosw0 = (1 - aw0) * SIN_TABLE[iw0 + 2500] + aw0 * SIN_TABLE[iw0 + 2501]; + sinw0 = (1 - aw0) * SIN_TABLE[iw0] + aw0 * SIN_TABLE[iw0 + 1]; + if (!this.slope) { + q *= q; + } + alpha = sinw0 / (2 * q); + invOnePlusAlpha = 1 / (1 + alpha); + a0 = (-2 * cosw0) * invOnePlusAlpha; + a1 = (1 - alpha) * invOnePlusAlpha; + onePlusCosw0 = 1 + cosw0; + b0 = onePlusCosw0 * .5 * invOnePlusAlpha; + b1 = -onePlusCosw0 * invOnePlusAlpha; + b2 = b0; + w = sig - a0 * this.fm00 - a1 * this.fm01; + sig = b0 * w + b1 * this.fm00 + b2 * this.fm01; + this.fm01 = this.fm00; + this.fm00 = w; + if (this.slope) { + w = sig - a0 * this.fm10 - a1 * this.fm11; + sig = b0 * w + b1 * this.fm10 + b2 * this.fm11; + this.fm11 = this.fm10; + this.fm10 = w; + } + return sig; + }; + + Filter.prototype.processBandPass = function(sig, cutoff, q) { + var a0, a1, alpha, aw0, b0, b1, b2, cosw0, invOnePlusAlpha, iw0, oneLessCosw0, sinw0, w, w0; + w0 = Math.max(0, Math.min(this.halfSampleRate, cutoff)) * this.invSampleRate; + w0 *= 10000; + iw0 = Math.floor(w0); + aw0 = w0 - iw0; + cosw0 = (1 - aw0) * SIN_TABLE[iw0 + 2500] + aw0 * SIN_TABLE[iw0 + 2501]; + sinw0 = (1 - aw0) * SIN_TABLE[iw0] + aw0 * SIN_TABLE[iw0 + 1]; + if (!this.slope) { + q *= q; + } + alpha = sinw0 / (2 * q); + invOnePlusAlpha = 1 / (1 + alpha); + oneLessCosw0 = 1 - cosw0; + a0 = (-2 * cosw0) * invOnePlusAlpha; + a1 = (1 - alpha) * invOnePlusAlpha; + b0 = q * alpha * invOnePlusAlpha; + b1 = 0; + b2 = -b0; + w = sig - a0 * this.fm00 - a1 * this.fm01; + sig = b0 * w + b1 * this.fm00 + b2 * this.fm01; + this.fm01 = this.fm00; + this.fm00 = w; + if (this.slope) { + w = sig - a0 * this.fm10 - a1 * this.fm11; + sig = b0 * w + b1 * this.fm10 + b2 * this.fm11; + this.fm11 = this.fm10; + this.fm10 = w; + } + return sig; + }; + + Filter.prototype.processLowPass = function(sig, cutoff, q) { + var a0, a1, alpha, aw0, b0, b1, b2, cosw0, invOnePlusAlpha, iw0, oneLessCosw0, sinw0, w, w0; + w0 = Math.max(0, Math.min(this.halfSampleRate, cutoff)) * this.invSampleRate; + w0 *= 10000; + iw0 = Math.floor(w0); + aw0 = w0 - iw0; + cosw0 = (1 - aw0) * SIN_TABLE[iw0 + 2500] + aw0 * SIN_TABLE[iw0 + 2501]; + sinw0 = (1 - aw0) * SIN_TABLE[iw0] + aw0 * SIN_TABLE[iw0 + 1]; + if (!this.slope) { + q *= q; + } + alpha = sinw0 / (2 * q); + invOnePlusAlpha = 1 / (1 + alpha); + oneLessCosw0 = 1 - cosw0; + b1 = oneLessCosw0 * invOnePlusAlpha; + b0 = b1 * .5; + b2 = b0; + a0 = (-2 * cosw0) * invOnePlusAlpha; + a1 = (1 - alpha) * invOnePlusAlpha; + w = sig - a0 * this.fm00 - a1 * this.fm01; + sig = b0 * w + b1 * this.fm00 + b2 * this.fm01; + this.fm01 = this.fm00; + this.fm00 = w; + if (this.slope) { + w = sig - a0 * this.fm10 - a1 * this.fm11; + sig = b0 * w + b1 * this.fm10 + b2 * this.fm11; + this.fm11 = this.fm10; + this.fm10 = w; + } + return sig; + }; + + return Filter; + +})(); diff --git a/static/js/sound/audioengine/instrument.coffee b/static/js/sound/audioengine/instrument.coffee new file mode 100644 index 00000000..2612aff4 --- /dev/null +++ b/static/js/sound/audioengine/instrument.coffee @@ -0,0 +1,235 @@ +class Instrument + constructor:(@engine)-> + @layers = [] + @layers.push new Layer @ + @modulation = 0 + @pitch_bend = .5 + + noteOn:(key,velocity)-> + for l in @layers + l.noteOn(key,velocity) + return + + noteOff:(key)-> + for l in @layers + l.noteOff(key) + return + + setModulation:(@modulation)-> + + setPitchBend:(@pitch_bend)-> + + process:(length)-> + if false #@layers.length == 1 + @layers[0].process(length) + @output = @layers[0].output + else + if not @output? or @output[0].length + @engine = @instrument.engine + @voices = [] + @eq = new EQ(@engine) + @spatializer = new Spatializer(@engine) + @inputs = + osc1: + type: 0 + tune: .5 + coarse: .5 + amp: .5 + mod: 0 + + osc2: + type: 0 + tune: .5 + coarse: .5 + amp: .5 + mod: 0 + + combine: 0 + + noise: + amp: 0 + mod: 0 + + filter: + cutoff: 1 + resonance: 0 + type: 0 + slope: 1 + follow: 0 + + disto: + wet:0 + drive:0 + + bitcrusher: + wet: 0 + drive: 0 + crush: 0 + + env1: + a: 0 + d: 0 + s: 1 + r: 0 + + env2: + a: .1 + d: .1 + s: .5 + r: .1 + out: 0 + amount: .5 + + lfo1: + type: 0 + amount: 0 + rate: .5 + out: 0 + + lfo2: + type: 0 + amount: 0 + rate: .5 + out: 0 + + fx1: + type: -1 + amount: 0 + rate: 0 + + fx2: + type: -1 + amount: 0 + rate: 0 + + eq: + low: .5 + mid: .5 + high: .5 + + spatialize: .5 + pan: .5 + polyphony: 1 + glide: .5 + sync: 1 + + velocity: + out: 0 + amount: .5 + amp: .5 + + modulation: + out: 0 + amount: .5 + + noteOn:(key,velocity)-> + if @inputs.polyphony == 1 and @last_voice? and @last_voice.on + voice = @last_voice + voice.noteOn @,key,velocity,true + @voices.push voice + else + voice = @engine.getVoice() + voice.noteOn @,key,velocity + @voices.push voice + @last_voice = voice + + @last_key = key + + removeVoice:(voice)-> + index = @voices.indexOf(voice) + if index>=0 + @voices.splice index,1 + + noteOff:(key)-> + for v in @voices + if v.key == key + v.noteOff() + return + + update:()-> + if @inputs.fx1.type>=0 + if not @fx1? or @fx1 not instanceof FX1[@inputs.fx1.type] + @fx1 = new FX1[@inputs.fx1.type] @engine + else + @fx1 = null + + if @inputs.fx2.type>=0 + if not @fx2? or @fx2 not instanceof FX2[@inputs.fx2.type] + @fx2 = new FX2[@inputs.fx2.type] @engine + else + @fx2 = null + + if @fx1? + @fx1.update(@inputs.fx1) + + if @fx2? + @fx2.update(@inputs.fx2) + + @eq.update(@inputs.eq) + + process:(length)-> + if not @output? or @output[0].length= 0) { + return this.voices.splice(index, 1); + } + }; + + Layer.prototype.noteOff = function(key) { + var j, len, ref, v; + ref = this.voices; + for (j = 0, len = ref.length; j < len; j++) { + v = ref[j]; + if (v.key === key) { + v.noteOff(); + } + } + }; + + Layer.prototype.update = function() { + if (this.inputs.fx1.type >= 0) { + if ((this.fx1 == null) || !(this.fx1 instanceof FX1[this.inputs.fx1.type])) { + this.fx1 = new FX1[this.inputs.fx1.type](this.engine); + } + } else { + this.fx1 = null; + } + if (this.inputs.fx2.type >= 0) { + if ((this.fx2 == null) || !(this.fx2 instanceof FX2[this.inputs.fx2.type])) { + this.fx2 = new FX2[this.inputs.fx2.type](this.engine); + } + } else { + this.fx2 = null; + } + if (this.fx1 != null) { + this.fx1.update(this.inputs.fx1); + } + if (this.fx2 != null) { + this.fx2.update(this.inputs.fx2); + } + return this.eq.update(this.inputs.eq); + }; + + Layer.prototype.process = function(length) { + var i, j, k, len, m, ref, ref1, ref2, sig, v; + if ((this.output == null) || this.output[0].length < length) { + this.output = [new Float64Array(length), new Float64Array(length)]; + } + for (i = j = ref = this.voices.length - 1; j >= 0; i = j += -1) { + v = this.voices[i]; + if (!v.on && v.env1.sig < .00001) { + v.env1.sig = 0; + this.removeVoice(v); + } + } + for (i = k = 0, ref1 = length - 1; k <= ref1; i = k += 1) { + sig = 0; + ref2 = this.voices; + for (m = 0, len = ref2.length; m < len; m++) { + v = ref2[m]; + sig += v.process(); + } + this.output[0][i] = sig; + this.output[1][i] = sig; + } + this.spatializer.process(this.output, length, this.inputs.spatialize, this.inputs.pan); + if (this.fx1 != null) { + this.fx1.process(this.output, length); + } + if (this.fx2 != null) { + this.fx2.process(this.output, length); + } + this.eq.process(this.output, length); + }; + + return Layer; + +})(); diff --git a/static/js/sound/audioengine/modulation.coffee b/static/js/sound/audioengine/modulation.coffee new file mode 100644 index 00000000..b0f00bce --- /dev/null +++ b/static/js/sound/audioengine/modulation.coffee @@ -0,0 +1,189 @@ + +class ModEnvelope + constructor:(@voice)-> + @sampleRate = @voice.engine.sampleRate + + init:(@params)-> + @phase = 0 + @update() + + update:(a = @params.a)-> + @a = 1/(@sampleRate*20*Math.pow(a,3)+1) + @d = 1/(@sampleRate*20*Math.pow(@params.d,3)+1) + @s = @params.s + @r = 1/(@sampleRate*20*Math.pow(@params.r,3)+1) + + process:(noteon)-> + if @phase<1 + sig = @sig = @phase = Math.min(1,@phase+@a) + else if @phase<2 + @phase = Math.min(2,@phase+@d) + sig = @sig = 1-(@phase-1)*(1-@s) + else if @phase<3 + sig = @sig = @s + else + @phase = @phase+@r + sig = Math.max(0,@sig*(1-(@phase-3))) + + if @phase<3 and not noteon + @phase = 3 + + sig + +class AmpEnvelope + constructor:(@voice)-> + @sampleRate = @voice.engine.sampleRate + @sig = 0 + + init:(@params)-> + @phase = 0 + @sig = 0 + @update() + + update:(a = @params.a)-> + #@a = Math.exp(Math.log(1/@epsilon)/(@sampleRate*10*Math.pow(@params.a,3)+1)) + @a2 = 1/(@sampleRate*(20*Math.pow(a,3)+.00025)) + @d = Math.exp(Math.log(0.5)/(@sampleRate*(10*Math.pow(@params.d,3)+0.001))) + @s = DBSCALE(@params.s,2) + console.info("sustain #{@s}") + @r = Math.exp(Math.log(0.5)/(@sampleRate*(10*Math.pow(@params.r,3)+0.001))) + + process:(noteon)-> + if @phase<1 + @phase += @a2 + sig = @sig = (@phase*.75+.25)*@phase + + if @phase>=1 + sig = @sig = 1 + @phase = 1 + else if @phase==1 + sig = @sig = @sig*@d + if sig <= @s + sig = @sig = @s + @phase = 2 + else if @phase<3 + sig = @sig = @s + else + sig = @sig = @sig*@r + + if @phase<3 and not noteon + @phase = 3 + + sig + +class LFO + constructor:(@voice)-> + @invSampleRate = 1/@voice.engine.sampleRate + + init:(@params,sync)-> + if sync + rate = @params.rate + rate = .1+rate*rate*rate*(100-.1) + t = @voice.engine.getTime()*rate + @phase = t%1 + else + @phase = Math.random() + @process = @processSine + @update() + @r1 = Math.random()*2-1 + @r2 = Math.random()*2-1 + @out = 0 + + update:()-> + switch @params.type + when 0 then @process = @processSaw + when 1 then @process = @processSquare + when 2 then @process = @processSine + when 3 then @process = @processTriangle + when 4 then @process = @processRandom + when 5 then @process = @processRandomStep + when 6 then @process = @processSawInv + + @audio_freq = 440*Math.pow(Math.pow(2,1/12),@voice.key-57) + + processSine:(rate)-> + if @params.audio + r = 1+rate*rate*4 + #r = if rate<.5 then .25+rate*rate/.25*.75 else rate*rate*4 + #r = Math.pow(2,(Math.round(rate*48))/12)*.25 + rate = @audio_freq*r + else + rate = .01+rate*rate*20 + + @phase = (@phase+rate*@invSampleRate) + @phase -= 1 if @phase>=1 + p = @phase*2 + if p<1 + p*p*(3-2*p)*2-1 + else + p -= 1 + 1-p*p*(3-2*p)*2 + + processTriangle:(rate)-> + if @params.audio + rate = @audio_freq*(1+rate*rate*4) + else + rate = .01+rate*rate*20 + + @phase = (@phase+rate*@invSampleRate) + @phase -= 1 if @phase>=1 + return (1-4*Math.abs(@phase-.5)) + + processSaw:(rate)-> + if @params.audio + rate = @audio_freq*(1+rate*rate*4) + else + rate = .01+rate*rate*20 + + @phase = (@phase+rate*@invSampleRate) + @phase -= 1 if @phase>=1 + out = (1-@phase*2) + @out = @out*.97+out*.03 + + processSawInv:(rate)-> + if @params.audio + rate = @audio_freq*(1+rate*rate*4) + else + rate = .01+rate*rate*20 + + @phase = (@phase+rate*@invSampleRate) + @phase -= 1 if @phase>=1 + out = (@phase*2-1) + @out = @out*.97+out*.03 + + processSquare:(rate)-> + if @params.audio + rate = @audio_freq*(1+rate*rate*4) + else + rate = .01+rate*rate*20 + + @phase = (@phase+rate*@invSampleRate) + @phase -= 1 if @phase>=1 + out = if @phase<.5 then 1 else -1 + @out = @out*.97+out*.03 + + processRandom:(rate)-> + if @params.audio + rate = @audio_freq*(1+rate*rate*4) + else + rate = .01+rate*rate*20 + + @phase = (@phase+rate*@invSampleRate) + if @phase>=1 + @phase -= 1 + @r1 = @r2 + @r2 = Math.random()*2-1 + + @r1*(1-@phase)+@r2*@phase + + processRandomStep:(rate)-> + if @params.audio + rate = @audio_freq*(1+rate*rate*4) + else + rate = .01+rate*rate*20 + + @phase = (@phase+rate*@invSampleRate) + if @phase>=1 + @phase -= 1 + @r1 = Math.random()*2-1 + @out = @out*.97+@r1*.03 diff --git a/static/js/sound/audioengine/modulation.js b/static/js/sound/audioengine/modulation.js new file mode 100644 index 00000000..1bed927b --- /dev/null +++ b/static/js/sound/audioengine/modulation.js @@ -0,0 +1,263 @@ +var AmpEnvelope, LFO, ModEnvelope; + +ModEnvelope = (function() { + function ModEnvelope(voice) { + this.voice = voice; + this.sampleRate = this.voice.engine.sampleRate; + } + + ModEnvelope.prototype.init = function(params) { + this.params = params; + this.phase = 0; + return this.update(); + }; + + ModEnvelope.prototype.update = function(a) { + if (a == null) { + a = this.params.a; + } + this.a = 1 / (this.sampleRate * 20 * Math.pow(a, 3) + 1); + this.d = 1 / (this.sampleRate * 20 * Math.pow(this.params.d, 3) + 1); + this.s = this.params.s; + return this.r = 1 / (this.sampleRate * 20 * Math.pow(this.params.r, 3) + 1); + }; + + ModEnvelope.prototype.process = function(noteon) { + var sig; + if (this.phase < 1) { + sig = this.sig = this.phase = Math.min(1, this.phase + this.a); + } else if (this.phase < 2) { + this.phase = Math.min(2, this.phase + this.d); + sig = this.sig = 1 - (this.phase - 1) * (1 - this.s); + } else if (this.phase < 3) { + sig = this.sig = this.s; + } else { + this.phase = this.phase + this.r; + sig = Math.max(0, this.sig * (1 - (this.phase - 3))); + } + if (this.phase < 3 && !noteon) { + this.phase = 3; + } + return sig; + }; + + return ModEnvelope; + +})(); + +AmpEnvelope = (function() { + function AmpEnvelope(voice) { + this.voice = voice; + this.sampleRate = this.voice.engine.sampleRate; + this.sig = 0; + } + + AmpEnvelope.prototype.init = function(params) { + this.params = params; + this.phase = 0; + this.sig = 0; + return this.update(); + }; + + AmpEnvelope.prototype.update = function(a) { + if (a == null) { + a = this.params.a; + } + this.a2 = 1 / (this.sampleRate * (20 * Math.pow(a, 3) + .00025)); + this.d = Math.exp(Math.log(0.5) / (this.sampleRate * (10 * Math.pow(this.params.d, 3) + 0.001))); + this.s = DBSCALE(this.params.s, 2); + console.info("sustain " + this.s); + return this.r = Math.exp(Math.log(0.5) / (this.sampleRate * (10 * Math.pow(this.params.r, 3) + 0.001))); + }; + + AmpEnvelope.prototype.process = function(noteon) { + var sig; + if (this.phase < 1) { + this.phase += this.a2; + sig = this.sig = (this.phase * .75 + .25) * this.phase; + if (this.phase >= 1) { + sig = this.sig = 1; + this.phase = 1; + } + } else if (this.phase === 1) { + sig = this.sig = this.sig * this.d; + if (sig <= this.s) { + sig = this.sig = this.s; + this.phase = 2; + } + } else if (this.phase < 3) { + sig = this.sig = this.s; + } else { + sig = this.sig = this.sig * this.r; + } + if (this.phase < 3 && !noteon) { + this.phase = 3; + } + return sig; + }; + + return AmpEnvelope; + +})(); + +LFO = (function() { + function LFO(voice) { + this.voice = voice; + this.invSampleRate = 1 / this.voice.engine.sampleRate; + } + + LFO.prototype.init = function(params, sync) { + var rate, t; + this.params = params; + if (sync) { + rate = this.params.rate; + rate = .1 + rate * rate * rate * (100 - .1); + t = this.voice.engine.getTime() * rate; + this.phase = t % 1; + } else { + this.phase = Math.random(); + } + this.process = this.processSine; + this.update(); + this.r1 = Math.random() * 2 - 1; + this.r2 = Math.random() * 2 - 1; + return this.out = 0; + }; + + LFO.prototype.update = function() { + switch (this.params.type) { + case 0: + this.process = this.processSaw; + break; + case 1: + this.process = this.processSquare; + break; + case 2: + this.process = this.processSine; + break; + case 3: + this.process = this.processTriangle; + break; + case 4: + this.process = this.processRandom; + break; + case 5: + this.process = this.processRandomStep; + break; + case 6: + this.process = this.processSawInv; + } + return this.audio_freq = 440 * Math.pow(Math.pow(2, 1 / 12), this.voice.key - 57); + }; + + LFO.prototype.processSine = function(rate) { + var p, r; + if (this.params.audio) { + r = 1 + rate * rate * 4; + rate = this.audio_freq * r; + } else { + rate = .01 + rate * rate * 20; + } + this.phase = this.phase + rate * this.invSampleRate; + if (this.phase >= 1) { + this.phase -= 1; + } + p = this.phase * 2; + if (p < 1) { + return p * p * (3 - 2 * p) * 2 - 1; + } else { + p -= 1; + return 1 - p * p * (3 - 2 * p) * 2; + } + }; + + LFO.prototype.processTriangle = function(rate) { + if (this.params.audio) { + rate = this.audio_freq * (1 + rate * rate * 4); + } else { + rate = .01 + rate * rate * 20; + } + this.phase = this.phase + rate * this.invSampleRate; + if (this.phase >= 1) { + this.phase -= 1; + } + return 1 - 4 * Math.abs(this.phase - .5); + }; + + LFO.prototype.processSaw = function(rate) { + var out; + if (this.params.audio) { + rate = this.audio_freq * (1 + rate * rate * 4); + } else { + rate = .01 + rate * rate * 20; + } + this.phase = this.phase + rate * this.invSampleRate; + if (this.phase >= 1) { + this.phase -= 1; + } + out = 1 - this.phase * 2; + return this.out = this.out * .97 + out * .03; + }; + + LFO.prototype.processSawInv = function(rate) { + var out; + if (this.params.audio) { + rate = this.audio_freq * (1 + rate * rate * 4); + } else { + rate = .01 + rate * rate * 20; + } + this.phase = this.phase + rate * this.invSampleRate; + if (this.phase >= 1) { + this.phase -= 1; + } + out = this.phase * 2 - 1; + return this.out = this.out * .97 + out * .03; + }; + + LFO.prototype.processSquare = function(rate) { + var out; + if (this.params.audio) { + rate = this.audio_freq * (1 + rate * rate * 4); + } else { + rate = .01 + rate * rate * 20; + } + this.phase = this.phase + rate * this.invSampleRate; + if (this.phase >= 1) { + this.phase -= 1; + } + out = this.phase < .5 ? 1 : -1; + return this.out = this.out * .97 + out * .03; + }; + + LFO.prototype.processRandom = function(rate) { + if (this.params.audio) { + rate = this.audio_freq * (1 + rate * rate * 4); + } else { + rate = .01 + rate * rate * 20; + } + this.phase = this.phase + rate * this.invSampleRate; + if (this.phase >= 1) { + this.phase -= 1; + this.r1 = this.r2; + this.r2 = Math.random() * 2 - 1; + } + return this.r1 * (1 - this.phase) + this.r2 * this.phase; + }; + + LFO.prototype.processRandomStep = function(rate) { + if (this.params.audio) { + rate = this.audio_freq * (1 + rate * rate * 4); + } else { + rate = .01 + rate * rate * 20; + } + this.phase = this.phase + rate * this.invSampleRate; + if (this.phase >= 1) { + this.phase -= 1; + this.r1 = Math.random() * 2 - 1; + } + return this.out = this.out * .97 + this.r1 * .03; + }; + + return LFO; + +})(); diff --git a/static/js/sound/audioengine/oscillators.coffee b/static/js/sound/audioengine/oscillators.coffee new file mode 100644 index 00000000..f0025271 --- /dev/null +++ b/static/js/sound/audioengine/oscillators.coffee @@ -0,0 +1,355 @@ + +class SquareOscillator + constructor:(@voice,osc)-> + @invSampleRate = 1/@voice.engine.sampleRate + @tune = 1 + @buffer = new Float64Array(32) + @init(osc) if osc? + + init:(osc,@sync)-> + @phase = if @sync then 0 else Math.random() + @sig = -1 + @index = 0 + @update(osc) + for i in [0..@buffer.length-1] + @buffer[i] = 0 + return + + update:(osc,@sync)-> + c = Math.round(osc.coarse*48)/48 + fine = osc.tune*2-1 + fine = fine*(1+fine*fine)*.5 + @tune = 1*Math.pow(2,c*4)*.25*Math.pow(Math.pow(2,1/12),fine) + @analog_tune = @tune + + process:(freq,mod)-> + dp = @analog_tune*freq*@invSampleRate + @phase += dp + + m = .5-mod*.49 + avg = 1-2*m + + if @sig<0 + if @phase>=m + @sig = 1 + + dp = Math.max(0,Math.min(1,(@phase-m)/dp)) + dp *= 16 + dpi = Math.floor(dp) + a = dp-dpi + index = @index + + for i in [0..31] by 1 + break if dpi>=512 + @buffer[index] += -1+BLIP[dpi]*(1-a)+BLIP[dpi+1]*a + dpi += 16 + index = (index+1)%@buffer.length + else + if @phase>=1 + dp = Math.max(0,Math.min(1,(@phase-1)/dp)) + dp *= 16 + dpi = Math.floor(dp) + a = dp-dpi + index = @index + + @sig = -1 + @phase -= 1 + @analog_tune = if @sync then @tune else @tune*(1+(Math.random()-.5)*.002) + + for i in [0..31] by 1 + break if dpi>=512 + @buffer[index] += 1-BLIP[dpi]*(1-a)-BLIP[dpi+1]*a + dpi += 16 + index = (index+1)%@buffer.length + + sig = @sig+@buffer[@index] + @buffer[@index] = 0 + @index = (@index+1)%@buffer.length + sig-avg + +class SawOscillator + constructor:(@voice,osc)-> + @invSampleRate = 1/@voice.engine.sampleRate + @tune = 1 + @buffer = new Float64Array(32) + @init(osc) if osc? + + init:(osc,@sync)-> + @phase = if @sync then 0 else Math.random() + @sig = -1 + @index = 0 + @jumped = false + @update(osc) + for i in [0..@buffer.length-1] + @buffer[i] = 0 + return + + update:(osc,@sync)-> + c = Math.round(osc.coarse*48)/48 + fine = osc.tune*2-1 + fine = fine*(1+fine*fine)*.5 + @tune = 1*Math.pow(2,c*4)*.25*Math.pow(Math.pow(2,1/12),fine) + @analog_tune = @tune + + process:(freq,mod)-> + dphase = @analog_tune*freq*@invSampleRate + @phase += dphase + + #if @phase>=1 + # @phase -= 1 + #return 1-2*@phase + + slope = 1+mod + + if not @jumped + sig = 1-2*@phase*slope + if @phase>=.5 + @jumped = true + sig = mod-2*(@phase-.5)*slope + + dp = Math.max(0,Math.min(1,(@phase-.5)/dphase)) + dp *= 16 + dpi = Math.floor(dp) + a = dp-dpi + index = @index + + if mod>0 + for i in [0..31] by 1 + break if dpi>=512 + @buffer[index] += (-1+BLIP[dpi]*(1-a)+BLIP[dpi+1]*a)*mod + dpi += 16 + index = (index+1)%@buffer.length + else + sig = mod-2*(@phase-.5)*slope + if @phase>=1 + @jumped = false + + dp = Math.max(0,Math.min(1,(@phase-1)/dphase)) + dp *= 16 + dpi = Math.floor(dp) + a = dp-dpi + index = @index + + @phase -= 1 + sig = 1-2*@phase*slope + @analog_tune = if @sync then @tune else @tune*(1+(Math.random()-.5)*.002) + + for i in [0..31] by 1 + break if dpi>=512 + @buffer[index] += -1+BLIP[dpi]*(1-a)+BLIP[dpi+1]*a + dpi += 16 + index = (index+1)%@buffer.length + + sig += @buffer[@index] + @buffer[@index] = 0 + @index = (@index+1)%@buffer.length + offset = 16*2*dphase*slope + sig+offset + +class SineOscillator + constructor:(@voice,osc)-> + @sampleRate = @voice.engine.sampleRate + @invSampleRate = 1/@voice.engine.sampleRate + @maxRatio = @sampleRate/Math.PI/5/(2*Math.PI) + @tune = 1 + @init(osc) if osc? + + init:(osc,@sync)-> + @phase = if @sync then .25 else Math.random() + @update(osc) + + update:(osc,@sync)-> + c = Math.round(osc.coarse*48)/48 + fine = osc.tune*2-1 + fine = fine*(1+fine*fine)*.5 + @tune = 1*Math.pow(2,c*4)*.25*Math.pow(Math.pow(2,1/12),fine) + @modnorm = 25/Math.sqrt(@tune*@voice.freq) + @dphase = @tune*@invSampleRate + + sinWave:(x)-> + x = (x-Math.floor(x))*10000 + ix = Math.floor(x) + ax = x-ix + SIN_TABLE[ix]*(1-ax)+SIN_TABLE[ix+1]*ax + + sinWave2:(x)-> + x = 2*(x-Math.floor(x)) + if x>1 + x = 2-x + x*x*(3-2*x)*2-1 + + process:(freq,mod)-> + @phase = (@phase+freq*@dphase) + + while @phase<0 + @phase += 1 + + while @phase>=1 + @phase -= 1 + @analog_tune = if @sync then @tune else @tune*(1+(Math.random()-.5)*.002) + @dphase = @analog_tune*@invSampleRate + + m1 = mod*@modnorm + m2 = mod*m1 + p = @phase + return @sinWave2(p+m1*@sinWave2(p+m2*@sinWave2(p))) + +class VoiceOscillator + constructor:(@voice,osc)-> + @sampleRate = @voice.engine.sampleRate + @invSampleRate = 1/@voice.engine.sampleRate + @tune = 1 + @init(osc) if osc? + + @f1 = [320,500,700,1000,500,320,700,500,320,320] + @f2 = [800,1000,1150,1400,1500,1650,1800,2300,3200,3200] + + init:(osc)-> + @phase = 0 + @grain1_p1 = .25 + @grain1_p2 = .25 + @grain2_p1 = .25 + @grain2_p2 = .25 + @update(osc) + + update:(osc)-> + c = Math.round(osc.coarse*48)/48 + fine = osc.tune*2-1 + fine = fine*(1+fine*fine)*.5 + @tune = 1*Math.pow(2,c*4)*.25*Math.pow(Math.pow(2,1/12),fine) + @modnorm = 25/Math.sqrt(@tune*@voice.freq) + @dphase = @tune*@invSampleRate + + sinWave2:(x)-> + x = 2*(x-Math.floor(x)) + if x>1 + x = 2-x + x*x*(3-2*x)*2-1 + + process:(freq,mod)-> + p1 = @phase<1 + @phase = (@phase+freq*@dphase) + + m = mod*(@f1.length-2) + im = Math.floor(m) + am = m-im + + f1 = @f1[im]*(1-am)+@f1[im+1]*am + f2 = @f2[im]*(1-am)+@f2[im+1]*am + + if p1 and @phase>=1 + @grain2_p1 = .25 + @grain2_p2 = .25 + + if @phase>=2 + @phase -= 2 + @grain1_p1 = .25 + @grain1_p2 = .25 + + x = @phase-1 + x *= x*x + vol = 1-Math.abs(1-@phase) + #vol = (@sinWave2(x*.5+.5)+1)*.5 + + sig = vol*(@sinWave2(@grain1_p1)*.25+.125*@sinWave2(@grain1_p2)) + + @grain1_p1 += f1*@invSampleRate + @grain1_p2 += f2*@invSampleRate + + x = ((@phase+1)%2)-1 + x *= x*x + #vol = (@sinWave2(x*.5+.5)+1)*.5 + vol = if @phase<1 then 1-@phase else @phase-1 + + sig += vol*(@sinWave2(@grain2_p1)*.25+.125*@sinWave2(@grain2_p2)) + + sig += @sinWave2(@phase+.25) + + @grain2_p1 += f1*@invSampleRate + @grain2_p2 += f2*@invSampleRate + + sig + +class StringOscillator + constructor:(@voice,osc)-> + @sampleRate = @voice.engine.sampleRate + @invSampleRate = 1/@voice.engine.sampleRate + @maxRatio = @sampleRate/Math.PI/5/(2*Math.PI) + @tune = 1 + @init(osc) if osc? + + init:(osc)-> + @index = 0 + @buffer = new Float64Array(@sampleRate/10) + @update(osc) + @prev = 0 + @power = 1 + + update:(osc)-> + c = Math.round(osc.coarse*48)/48 + fine = osc.tune*2-1 + fine = fine*(1+fine*fine)*.5 + @tune = 1*Math.pow(2,c*4)*.25*Math.pow(Math.pow(2,1/12),fine) + + process:(freq,mod)-> + period = @sampleRate/(freq*@tune) + x = (@index-period+@buffer.length)%@buffer.length + ix = Math.floor(x) + a = x-ix + reflection = @buffer[ix]*(1-a)+@buffer[(ix+1)%@buffer.length]*a + + m = Math.exp(Math.log(0.99)/freq) + + m = 1 + r = reflection*m+@prev*(1-m) + @prev = r + + n = Math.random()*2-1 + + + m = mod*.5 + m = m*m*.999+.001 + sig = n*m+r + + @power = Math.max(Math.abs(sig)*.1+@power*.9,@power*.999) + sig /= (@power+.0001) + + @buffer[@index] = sig + @index += 1 + @index = 0 if @index>=@buffer.length + sig + + processOld:(freq,mod)-> + period = @sampleRate/(freq*@tune) + x = (@index-period+@buffer.length)%@buffer.length + ix = Math.floor(x) + a = x-ix + reflection = @buffer[ix]*(1-a)+@buffer[(ix+1)%@buffer.length]*a + + #m = .1+Math.exp(-period*mod*.01)*.89 + m = mod + r = reflection*m+@prev*(1-m) + @prev = r + + sig = (Math.random()*2-1)*.01+r*.99 + @buffer[@index] = sig/@power + @power = Math.abs(sig)*.001+@power*.999 + @index += 1 + @index = 0 if @index>=@buffer.length + sig + +class Noise + constructor:(@voice)-> + + init:()-> + @phase = 0 + @seed = 1382 + @n = 0 + + process:(mod)-> + @seed = (@seed*13907+12345)&0x7FFFFFFF + white = (@seed/0x80000000)*2-1 + @n = @n*.9+white*.1 + low = @n + high = white-low + 2*(low*(1-mod)+high*mod) diff --git a/static/js/sound/audioengine/oscillators.js b/static/js/sound/audioengine/oscillators.js new file mode 100644 index 00000000..e7cd4082 --- /dev/null +++ b/static/js/sound/audioengine/oscillators.js @@ -0,0 +1,426 @@ +var Noise, SawOscillator, SineOscillator, SquareOscillator, StringOscillator, VoiceOscillator; + +SquareOscillator = (function() { + function SquareOscillator(voice, osc) { + this.voice = voice; + this.invSampleRate = 1 / this.voice.engine.sampleRate; + this.tune = 1; + this.buffer = new Float64Array(32); + if (osc != null) { + this.init(osc); + } + } + + SquareOscillator.prototype.init = function(osc, sync) { + var i, j, ref; + this.sync = sync; + this.phase = this.sync ? 0 : Math.random(); + this.sig = -1; + this.index = 0; + this.update(osc); + for (i = j = 0, ref = this.buffer.length - 1; 0 <= ref ? j <= ref : j >= ref; i = 0 <= ref ? ++j : --j) { + this.buffer[i] = 0; + } + }; + + SquareOscillator.prototype.update = function(osc, sync) { + var c, fine; + this.sync = sync; + c = Math.round(osc.coarse * 48) / 48; + fine = osc.tune * 2 - 1; + fine = fine * (1 + fine * fine) * .5; + this.tune = 1 * Math.pow(2, c * 4) * .25 * Math.pow(Math.pow(2, 1 / 12), fine); + return this.analog_tune = this.tune; + }; + + SquareOscillator.prototype.process = function(freq, mod) { + var a, avg, dp, dpi, i, index, j, k, m, sig; + dp = this.analog_tune * freq * this.invSampleRate; + this.phase += dp; + m = .5 - mod * .49; + avg = 1 - 2 * m; + if (this.sig < 0) { + if (this.phase >= m) { + this.sig = 1; + dp = Math.max(0, Math.min(1, (this.phase - m) / dp)); + dp *= 16; + dpi = Math.floor(dp); + a = dp - dpi; + index = this.index; + for (i = j = 0; j <= 31; i = j += 1) { + if (dpi >= 512) { + break; + } + this.buffer[index] += -1 + BLIP[dpi] * (1 - a) + BLIP[dpi + 1] * a; + dpi += 16; + index = (index + 1) % this.buffer.length; + } + } + } else { + if (this.phase >= 1) { + dp = Math.max(0, Math.min(1, (this.phase - 1) / dp)); + dp *= 16; + dpi = Math.floor(dp); + a = dp - dpi; + index = this.index; + this.sig = -1; + this.phase -= 1; + this.analog_tune = this.sync ? this.tune : this.tune * (1 + (Math.random() - .5) * .002); + for (i = k = 0; k <= 31; i = k += 1) { + if (dpi >= 512) { + break; + } + this.buffer[index] += 1 - BLIP[dpi] * (1 - a) - BLIP[dpi + 1] * a; + dpi += 16; + index = (index + 1) % this.buffer.length; + } + } + } + sig = this.sig + this.buffer[this.index]; + this.buffer[this.index] = 0; + this.index = (this.index + 1) % this.buffer.length; + return sig - avg; + }; + + return SquareOscillator; + +})(); + +SawOscillator = (function() { + function SawOscillator(voice, osc) { + this.voice = voice; + this.invSampleRate = 1 / this.voice.engine.sampleRate; + this.tune = 1; + this.buffer = new Float64Array(32); + if (osc != null) { + this.init(osc); + } + } + + SawOscillator.prototype.init = function(osc, sync) { + var i, j, ref; + this.sync = sync; + this.phase = this.sync ? 0 : Math.random(); + this.sig = -1; + this.index = 0; + this.jumped = false; + this.update(osc); + for (i = j = 0, ref = this.buffer.length - 1; 0 <= ref ? j <= ref : j >= ref; i = 0 <= ref ? ++j : --j) { + this.buffer[i] = 0; + } + }; + + SawOscillator.prototype.update = function(osc, sync) { + var c, fine; + this.sync = sync; + c = Math.round(osc.coarse * 48) / 48; + fine = osc.tune * 2 - 1; + fine = fine * (1 + fine * fine) * .5; + this.tune = 1 * Math.pow(2, c * 4) * .25 * Math.pow(Math.pow(2, 1 / 12), fine); + return this.analog_tune = this.tune; + }; + + SawOscillator.prototype.process = function(freq, mod) { + var a, dp, dphase, dpi, i, index, j, k, offset, sig, slope; + dphase = this.analog_tune * freq * this.invSampleRate; + this.phase += dphase; + slope = 1 + mod; + if (!this.jumped) { + sig = 1 - 2 * this.phase * slope; + if (this.phase >= .5) { + this.jumped = true; + sig = mod - 2 * (this.phase - .5) * slope; + dp = Math.max(0, Math.min(1, (this.phase - .5) / dphase)); + dp *= 16; + dpi = Math.floor(dp); + a = dp - dpi; + index = this.index; + if (mod > 0) { + for (i = j = 0; j <= 31; i = j += 1) { + if (dpi >= 512) { + break; + } + this.buffer[index] += (-1 + BLIP[dpi] * (1 - a) + BLIP[dpi + 1] * a) * mod; + dpi += 16; + index = (index + 1) % this.buffer.length; + } + } + } + } else { + sig = mod - 2 * (this.phase - .5) * slope; + if (this.phase >= 1) { + this.jumped = false; + dp = Math.max(0, Math.min(1, (this.phase - 1) / dphase)); + dp *= 16; + dpi = Math.floor(dp); + a = dp - dpi; + index = this.index; + this.phase -= 1; + sig = 1 - 2 * this.phase * slope; + this.analog_tune = this.sync ? this.tune : this.tune * (1 + (Math.random() - .5) * .002); + for (i = k = 0; k <= 31; i = k += 1) { + if (dpi >= 512) { + break; + } + this.buffer[index] += -1 + BLIP[dpi] * (1 - a) + BLIP[dpi + 1] * a; + dpi += 16; + index = (index + 1) % this.buffer.length; + } + } + } + sig += this.buffer[this.index]; + this.buffer[this.index] = 0; + this.index = (this.index + 1) % this.buffer.length; + offset = 16 * 2 * dphase * slope; + return sig + offset; + }; + + return SawOscillator; + +})(); + +SineOscillator = (function() { + function SineOscillator(voice, osc) { + this.voice = voice; + this.sampleRate = this.voice.engine.sampleRate; + this.invSampleRate = 1 / this.voice.engine.sampleRate; + this.maxRatio = this.sampleRate / Math.PI / 5 / (2 * Math.PI); + this.tune = 1; + if (osc != null) { + this.init(osc); + } + } + + SineOscillator.prototype.init = function(osc, sync) { + this.sync = sync; + this.phase = this.sync ? .25 : Math.random(); + return this.update(osc); + }; + + SineOscillator.prototype.update = function(osc, sync) { + var c, fine; + this.sync = sync; + c = Math.round(osc.coarse * 48) / 48; + fine = osc.tune * 2 - 1; + fine = fine * (1 + fine * fine) * .5; + this.tune = 1 * Math.pow(2, c * 4) * .25 * Math.pow(Math.pow(2, 1 / 12), fine); + this.modnorm = 25 / Math.sqrt(this.tune * this.voice.freq); + return this.dphase = this.tune * this.invSampleRate; + }; + + SineOscillator.prototype.sinWave = function(x) { + var ax, ix; + x = (x - Math.floor(x)) * 10000; + ix = Math.floor(x); + ax = x - ix; + return SIN_TABLE[ix] * (1 - ax) + SIN_TABLE[ix + 1] * ax; + }; + + SineOscillator.prototype.sinWave2 = function(x) { + x = 2 * (x - Math.floor(x)); + if (x > 1) { + x = 2 - x; + } + return x * x * (3 - 2 * x) * 2 - 1; + }; + + SineOscillator.prototype.process = function(freq, mod) { + var m1, m2, p; + this.phase = this.phase + freq * this.dphase; + while (this.phase < 0) { + this.phase += 1; + } + while (this.phase >= 1) { + this.phase -= 1; + this.analog_tune = this.sync ? this.tune : this.tune * (1 + (Math.random() - .5) * .002); + this.dphase = this.analog_tune * this.invSampleRate; + } + m1 = mod * this.modnorm; + m2 = mod * m1; + p = this.phase; + return this.sinWave2(p + m1 * this.sinWave2(p + m2 * this.sinWave2(p))); + }; + + return SineOscillator; + +})(); + +VoiceOscillator = (function() { + function VoiceOscillator(voice, osc) { + this.voice = voice; + this.sampleRate = this.voice.engine.sampleRate; + this.invSampleRate = 1 / this.voice.engine.sampleRate; + this.tune = 1; + if (osc != null) { + this.init(osc); + } + this.f1 = [320, 500, 700, 1000, 500, 320, 700, 500, 320, 320]; + this.f2 = [800, 1000, 1150, 1400, 1500, 1650, 1800, 2300, 3200, 3200]; + } + + VoiceOscillator.prototype.init = function(osc) { + this.phase = 0; + this.grain1_p1 = .25; + this.grain1_p2 = .25; + this.grain2_p1 = .25; + this.grain2_p2 = .25; + return this.update(osc); + }; + + VoiceOscillator.prototype.update = function(osc) { + var c, fine; + c = Math.round(osc.coarse * 48) / 48; + fine = osc.tune * 2 - 1; + fine = fine * (1 + fine * fine) * .5; + this.tune = 1 * Math.pow(2, c * 4) * .25 * Math.pow(Math.pow(2, 1 / 12), fine); + this.modnorm = 25 / Math.sqrt(this.tune * this.voice.freq); + return this.dphase = this.tune * this.invSampleRate; + }; + + VoiceOscillator.prototype.sinWave2 = function(x) { + x = 2 * (x - Math.floor(x)); + if (x > 1) { + x = 2 - x; + } + return x * x * (3 - 2 * x) * 2 - 1; + }; + + VoiceOscillator.prototype.process = function(freq, mod) { + var am, f1, f2, im, m, p1, sig, vol, x; + p1 = this.phase < 1; + this.phase = this.phase + freq * this.dphase; + m = mod * (this.f1.length - 2); + im = Math.floor(m); + am = m - im; + f1 = this.f1[im] * (1 - am) + this.f1[im + 1] * am; + f2 = this.f2[im] * (1 - am) + this.f2[im + 1] * am; + if (p1 && this.phase >= 1) { + this.grain2_p1 = .25; + this.grain2_p2 = .25; + } + if (this.phase >= 2) { + this.phase -= 2; + this.grain1_p1 = .25; + this.grain1_p2 = .25; + } + x = this.phase - 1; + x *= x * x; + vol = 1 - Math.abs(1 - this.phase); + sig = vol * (this.sinWave2(this.grain1_p1) * .25 + .125 * this.sinWave2(this.grain1_p2)); + this.grain1_p1 += f1 * this.invSampleRate; + this.grain1_p2 += f2 * this.invSampleRate; + x = ((this.phase + 1) % 2) - 1; + x *= x * x; + vol = this.phase < 1 ? 1 - this.phase : this.phase - 1; + sig += vol * (this.sinWave2(this.grain2_p1) * .25 + .125 * this.sinWave2(this.grain2_p2)); + sig += this.sinWave2(this.phase + .25); + this.grain2_p1 += f1 * this.invSampleRate; + this.grain2_p2 += f2 * this.invSampleRate; + return sig; + }; + + return VoiceOscillator; + +})(); + +StringOscillator = (function() { + function StringOscillator(voice, osc) { + this.voice = voice; + this.sampleRate = this.voice.engine.sampleRate; + this.invSampleRate = 1 / this.voice.engine.sampleRate; + this.maxRatio = this.sampleRate / Math.PI / 5 / (2 * Math.PI); + this.tune = 1; + if (osc != null) { + this.init(osc); + } + } + + StringOscillator.prototype.init = function(osc) { + this.index = 0; + this.buffer = new Float64Array(this.sampleRate / 10); + this.update(osc); + this.prev = 0; + return this.power = 1; + }; + + StringOscillator.prototype.update = function(osc) { + var c, fine; + c = Math.round(osc.coarse * 48) / 48; + fine = osc.tune * 2 - 1; + fine = fine * (1 + fine * fine) * .5; + return this.tune = 1 * Math.pow(2, c * 4) * .25 * Math.pow(Math.pow(2, 1 / 12), fine); + }; + + StringOscillator.prototype.process = function(freq, mod) { + var a, ix, m, n, period, r, reflection, sig, x; + period = this.sampleRate / (freq * this.tune); + x = (this.index - period + this.buffer.length) % this.buffer.length; + ix = Math.floor(x); + a = x - ix; + reflection = this.buffer[ix] * (1 - a) + this.buffer[(ix + 1) % this.buffer.length] * a; + m = Math.exp(Math.log(0.99) / freq); + m = 1; + r = reflection * m + this.prev * (1 - m); + this.prev = r; + n = Math.random() * 2 - 1; + m = mod * .5; + m = m * m * .999 + .001; + sig = n * m + r; + this.power = Math.max(Math.abs(sig) * .1 + this.power * .9, this.power * .999); + sig /= this.power + .0001; + this.buffer[this.index] = sig; + this.index += 1; + if (this.index >= this.buffer.length) { + this.index = 0; + } + return sig; + }; + + StringOscillator.prototype.processOld = function(freq, mod) { + var a, ix, m, period, r, reflection, sig, x; + period = this.sampleRate / (freq * this.tune); + x = (this.index - period + this.buffer.length) % this.buffer.length; + ix = Math.floor(x); + a = x - ix; + reflection = this.buffer[ix] * (1 - a) + this.buffer[(ix + 1) % this.buffer.length] * a; + m = mod; + r = reflection * m + this.prev * (1 - m); + this.prev = r; + sig = (Math.random() * 2 - 1) * .01 + r * .99; + this.buffer[this.index] = sig / this.power; + this.power = Math.abs(sig) * .001 + this.power * .999; + this.index += 1; + if (this.index >= this.buffer.length) { + this.index = 0; + } + return sig; + }; + + return StringOscillator; + +})(); + +Noise = (function() { + function Noise(voice) { + this.voice = voice; + } + + Noise.prototype.init = function() { + this.phase = 0; + this.seed = 1382; + return this.n = 0; + }; + + Noise.prototype.process = function(mod) { + var high, low, white; + this.seed = (this.seed * 13907 + 12345) & 0x7FFFFFFF; + white = (this.seed / 0x80000000) * 2 - 1; + this.n = this.n * .9 + white * .1; + low = this.n; + high = white - low; + return 2 * (low * (1 - mod) + high * mod); + }; + + return Noise; + +})(); diff --git a/static/js/sound/audioengine/utils.coffee b/static/js/sound/audioengine/utils.coffee new file mode 100644 index 00000000..8d858697 --- /dev/null +++ b/static/js/sound/audioengine/utils.coffee @@ -0,0 +1,40 @@ +` +const TWOPI = 2*Math.PI +const SIN_TABLE = new Float64Array(10001) +const WHITE_NOISE = new Float64Array(100000) +const COLORED_NOISE = new Float64Array(100000) +` +do -> + for i in [0..10000] by 1 + SIN_TABLE[i] = Math.sin(i/10000*Math.PI*2) + +` +const BLIP_SIZE = 512 +const BLIP = new Float64Array(BLIP_SIZE+1) +` + +do -> + for p in [1..31] by 2 + for i in [0..BLIP_SIZE] by 1 + x = (i/BLIP_SIZE-.5)*.5 + BLIP[i] += Math.sin(x*2*Math.PI*p)/p + + norm = BLIP[BLIP_SIZE] + + for i in [0..BLIP_SIZE] by 1 + BLIP[i] /= norm + +do -> + n = 0 + b0 = b1 = b2 = b3 = b4 = b5 = b6 = 0 + for i in [0..99999] by 1 + white = Math.random()*2-1 + + n = .99*n+.01*white + + pink = n*6 + + WHITE_NOISE[i] = white + COLORED_NOISE[i] = pink + +DBSCALE = (value,range)-> (Math.exp(value*range)/Math.exp(range)-1/Math.exp(range))/(1-1/Math.exp(range)) diff --git a/static/js/sound/audioengine/utils.js b/static/js/sound/audioengine/utils.js new file mode 100644 index 00000000..69020af0 --- /dev/null +++ b/static/js/sound/audioengine/utils.js @@ -0,0 +1,56 @@ + +const TWOPI = 2*Math.PI +const SIN_TABLE = new Float64Array(10001) +const WHITE_NOISE = new Float64Array(100000) +const COLORED_NOISE = new Float64Array(100000) +; +var DBSCALE; + +(function() { + var i, j, results; + results = []; + for (i = j = 0; j <= 10000; i = j += 1) { + results.push(SIN_TABLE[i] = Math.sin(i / 10000 * Math.PI * 2)); + } + return results; +})(); + + +const BLIP_SIZE = 512 +const BLIP = new Float64Array(BLIP_SIZE+1) +; + +(function() { + var i, j, k, l, norm, p, ref, ref1, results, x; + for (p = j = 1; j <= 31; p = j += 2) { + for (i = k = 0, ref = BLIP_SIZE; k <= ref; i = k += 1) { + x = (i / BLIP_SIZE - .5) * .5; + BLIP[i] += Math.sin(x * 2 * Math.PI * p) / p; + } + } + norm = BLIP[BLIP_SIZE]; + results = []; + for (i = l = 0, ref1 = BLIP_SIZE; l <= ref1; i = l += 1) { + results.push(BLIP[i] /= norm); + } + return results; +})(); + +(function() { + var b0, b1, b2, b3, b4, b5, b6, i, j, n, pink, results, white; + n = 0; + b0 = b1 = b2 = b3 = b4 = b5 = b6 = 0; + results = []; + for (i = j = 0; j <= 99999; i = j += 1) { + white = Math.random() * 2 - 1; + n = .99 * n + .01 * white; + pink = n * 6; + WHITE_NOISE[i] = white; + results.push(COLORED_NOISE[i] = pink); + } + return results; +})(); + +DBSCALE = function(value, range) { + return (Math.exp(value * range) / Math.exp(range) - 1 / Math.exp(range)) / (1 - 1 / Math.exp(range)); +}; diff --git a/static/js/sound/audioengine/voice.coffee b/static/js/sound/audioengine/voice.coffee new file mode 100644 index 00000000..a7bf5f2d --- /dev/null +++ b/static/js/sound/audioengine/voice.coffee @@ -0,0 +1,573 @@ +class Voice + @oscillators = [SawOscillator,SquareOscillator,SineOscillator,VoiceOscillator,StringOscillator] + + constructor:(@engine)-> + @osc1 = new SineOscillator @ + @osc2 = new SineOscillator @ + @noise = new Noise @ + @lfo1 = new LFO @ + @lfo2 = new LFO @ + @filter = new Filter @ + @env1 = new AmpEnvelope @ + @env2 = new ModEnvelope @ + @filter_increment = 0.0005*44100/@engine.sampleRate + @modulation = 0 + @noteon_time = 0 + + init:(layer)-> + if @osc1 not instanceof Voice.oscillators[layer.inputs.osc1.type] + @osc1 = new Voice.oscillators[layer.inputs.osc1.type] @ + + if @osc2 not instanceof Voice.oscillators[layer.inputs.osc2.type] + @osc2 = new Voice.oscillators[layer.inputs.osc2.type] @ + + @osc1.init(layer.inputs.osc1,layer.inputs.sync) + @osc2.init(layer.inputs.osc2,layer.inputs.sync) + @noise.init(layer,layer.inputs.sync) + @lfo1.init(layer.inputs.lfo1,layer.inputs.sync) + @lfo2.init(layer.inputs.lfo2,layer.inputs.sync) + @filter.init(layer) + @env1.init(layer.inputs.env1) + @env2.init(layer.inputs.env2) + + @updateConstantMods() + + update:()-> + return if not @layer? + + if @osc1 not instanceof Voice.oscillators[@layer.inputs.osc1.type] + @osc1 = new Voice.oscillators[@layer.inputs.osc1.type] @,@layer.inputs.osc1 + + if @osc2 not instanceof Voice.oscillators[@layer.inputs.osc2.type] + @osc2 = new Voice.oscillators[@layer.inputs.osc2.type] @,@layer.inputs.osc2 + + @osc1.update(@layer.inputs.osc1,@layer.inputs.sync) + @osc2.update(@layer.inputs.osc2,@layer.inputs.sync) + + @env1.update() + @env2.update() + + @lfo1.update() + @lfo2.update() + @filter.update() + + @updateConstantMods() + + updateConstantMods:()-> + @osc1_on = @inputs.osc1.amp>0 or @inputs.lfo1.out == 5 or @inputs.lfo2.out == 5 or @inputs.env2.out == 7 or @inputs.velocity.out == 4 or @inputs.modulation.out == 2 + @osc2_on = @inputs.osc2.amp>0 or @inputs.lfo1.out == 8 or @inputs.lfo2.out == 8 or @inputs.env2.out == 10 or @inputs.velocity.out == 6 or @inputs.modulation.out == 4 + @noise_on = @inputs.noise.amp>0 or @inputs.lfo1.out == 9 or @inputs.lfo2.out == 9 or @inputs.env2.out == 11 or @inputs.velocity.out == 7 or @inputs.modulation.out == 6 + + @osc1_amp = DBSCALE(@inputs.osc1.amp,3) + @osc2_amp = DBSCALE(@inputs.osc2.amp,3) + @osc1_mod = @inputs.osc1.mod + @osc2_mod = @inputs.osc2.mod + @noise_amp = DBSCALE(@inputs.noise.amp,3) + @noise_mod = @inputs.noise.mod + + if @inputs.velocity.amp>0 + p = @inputs.velocity.amp + p *= p + p *= 4 + #amp = Math.pow(@velocity,p) #Math.exp((-1+@velocity)*5) + norm = @inputs.velocity.amp*4 + amp = Math.exp(@velocity*norm)/Math.exp(norm) + #amp = @inputs.velocity.amp*amp+(1-@inputs.velocity.amp) + else + amp = 1 + + @osc1_amp *= amp + @osc2_amp *= amp + @noise_amp *= amp + + + c = Math.log(1024*@freq/22000)/(10*Math.log(2)) + @cutoff_keymod = (c-.5)*@inputs.filter.follow + @cutoff_base = @inputs.filter.cutoff+@cutoff_keymod + + @env2_amount = @inputs.env2.amount + + @lfo1_rate = @inputs.lfo1.rate + @lfo1_amount = @inputs.lfo1.amount + @lfo2_rate = @inputs.lfo2.rate + @lfo2_amount = @inputs.lfo2.amount + + + # "Mod" + # + # "Filter Cutoff" + # "Filter Resonance" + # + # "Osc1 Mod" + # "Osc1 Amp" + # + # "Osc2 Mod" + # "Osc2 Amp" + # + # "Noise Amp" + # "Noise Color" + # + # "Env1 Attack" + # "Env2 Attack" + # "Env2 Amount" + # + # "LFO1 Amount" + # "LFO1 Rate" + # + # "LFO2 Amount" + # "LFO2 Rate" + # ] + + switch @inputs.velocity.out + when 0 # Oscs Mod + mod = @velocity*(@inputs.velocity.amount-.5)*2 + @osc1_mod = Math.min(1,Math.max(0,@osc1_mod+mod)) + @osc2_mod = Math.min(1,Math.max(0,@osc2_mod+mod)) + + when 1 # Filter Cutoff + mod = @velocity*(@inputs.velocity.amount-.5)*2 + @cutoff_base += mod + + when 3 # Osc1 Mod + mod = @velocity*(@inputs.velocity.amount-.5)*2 + @osc1_mod = Math.min(1,Math.max(0,@osc1_mod+mod)) + + when 4 # Osc1 Amp + mod = @velocity*(@inputs.velocity.amount-.5)*2 + @osc1_amp = Math.max(0,@osc1_amp+mod) + + when 5 # Osc2 Mod + mod = @velocity*(@inputs.velocity.amount-.5)*2 + @osc2_mod = Math.min(1,Math.max(0,@osc2_mod+mod)) + + when 6 # Osc2 Amp + mod = @velocity*(@inputs.velocity.amount-.5)*2 + @osc2_amp = Math.max(0,@osc2_amp+mod) + + when 7 # Noise Amp + mod = @velocity*(@inputs.velocity.amount-.5)*2 + @noise_amp = Math.max(0,@noise_amp+mod) + + when 8 # Noise Color + mod = @velocity*(@inputs.velocity.amount-.5)*2 + @noise_mod = Math.min(1,Math.max(0,@noise_mod+mod)) + + when 9 # Env1 Attack + mod = @velocity*(@inputs.velocity.amount-.5)*2 + a = Math.max(0,Math.min(@inputs.env1.a+mod,1)) + @env1.update(a) + + when 10 # Env2 Attack + mod = @velocity*(@inputs.velocity.amount-.5)*2 + a = Math.max(0,Math.min(@inputs.env2.a+mod,1)) + @env2.update(a) + + when 11 # Env2 Amount + mod = @velocity*(@inputs.velocity.amount-.5)*2 + @env2_amount = Math.max(0,Math.min(1,@env2_amount+mod)) + + when 12 # LFO1 Amount + mod = @velocity*(@inputs.velocity.amount-.5)*2 + @lfo1_amount = Math.max(0,@lfo1_amount+mod) + + when 13 # LFO1 Rate + mod = @velocity*(@inputs.velocity.amount-.5)*2 + @lfo1_rate = Math.min(1,Math.max(0,@lfo1_rate+mod)) + + when 14 # LFO2 Amount + mod = @velocity*(@inputs.velocity.amount-.5)*2 + @lfo2_amount = Math.max(0,@lfo2_amount+mod) + + when 15 # LFO2 Rate + mod = @velocity*(@inputs.velocity.amount-.5)*2 + @lfo2_rate = Math.min(1,Math.max(0,@lfo2_rate+mod)) + + + if @freq? + c = Math.log(1024*@freq/22000)/(10*Math.log(2)) + @cutoff_keymod = (c-.5)*@inputs.filter.follow + + noteOn:(layer,@key,velocity,legato=false)-> + @velocity = velocity/127 + + if @layer? + @layer.removeVoice @ + + @layer = layer + @inputs = @layer.inputs + if legato and @on + @freq = 440*Math.pow(Math.pow(2,1/12),@key-57) + + if layer.last_key? + @glide_from = layer.last_key + @glide = true + @glide_phase = 0 + glide_time = (@inputs.glide*.025+Math.pow(@inputs.glide,16)*.975)*10 + @glide_inc = 1/(glide_time*@engine.sampleRate+1) + else + @freq = 440*Math.pow(Math.pow(2,1/12),@key-57) + @init @layer + @on = true + @cutoff = 0 + @modulation = @layer.instrument.modulation + @pitch_bend = @layer.instrument.pitch_bend + @modulation_v = 0 + @pitch_bend_v = 0 + + @noteon_time = Date.now() + + noteOff:()-> + @on = false + + process:()-> + osc1_mod = @osc1_mod + osc2_mod = @osc2_mod + osc1_amp = @osc1_amp + osc2_amp = @osc2_amp + + if @glide + k = @glide_from*(1-@glide_phase)+@key*@glide_phase + osc1_freq = osc2_freq = 440*Math.pow(Math.pow(2,1/12),k-57) + @glide_phase += @glide_inc + if @glide_phase>=1 + @glide = false + else + osc1_freq = osc2_freq = @freq + + if Math.abs(@pitch_bend-@layer.instrument.pitch_bend)>.0001 + @pitch_bend_v += .001*(@layer.instrument.pitch_bend-@pitch_bend) + @pitch_bend_v *= .5 + @pitch_bend += @pitch_bend_v + + if Math.abs(@pitch_bend-.5)>.0001 + p = @pitch_bend*2-1 + p *= 2 + f = Math.pow(Math.pow(2,1/12),p) + osc1_freq *= f + osc2_freq *= f + + noise_amp = @noise_amp + noise_mod = @noise_mod + lfo1_rate = @lfo1_rate + lfo1_amount = @lfo1_amount + lfo2_rate = @lfo2_rate + lfo2_amount = @lfo2_amount + + cutoff = @cutoff_base + q = @inputs.filter.resonance + + if Math.abs(@modulation-@layer.instrument.modulation)>.0001 + @modulation_v += .001*(@layer.instrument.modulation-@modulation) + @modulation_v *= .5 + @modulation += @modulation_v + + switch @inputs.modulation.out + when 0 # Amp + mod = (@inputs.modulation.amount-.5)*2*@modulation + osc1_amp *= Math.max(0,1+mod) + osc2_amp *= Math.max(0,1+mod) + noise_amp *= Math.max(0,1+mod) + + when 1 # Mod + mod = (@inputs.modulation.amount-.5)*2*@modulation + osc1_mod = Math.min(1,Math.max(0,osc1_mod+mod)) + osc2_mod = Math.min(1,Math.max(0,osc2_mod+mod)) + + when 2 # Osc1 Amp + mod = (@inputs.modulation.amount-.5)*2*@modulation + osc1_amp = Math.max(0,osc1_amp+mod) + + when 3 # Osc1 Mod + mod = (@inputs.modulation.amount-.5)*2*@modulation + osc1_mod = Math.min(1,Math.max(0,osc1_mod+mod)) + + when 4 # Osc2 Amp + mod = (@inputs.modulation.amount-.5)*2*@modulation + osc2_amp = Math.max(0,osc2_amp+mod) + + when 5 # Osc2 Mod + mod = (@inputs.modulation.amount-.5)*2*@modulation + osc2_mod = Math.min(1,Math.max(0,osc2_mod+mod)) + + when 6 # Noise Amp + mod = (@inputs.modulation.amount-.5)*2*@modulation + noise_amp = Math.max(0,noise_amp+mod) + + when 7 # Noise Color + mod = (@inputs.modulation.amount-.5)*2*@modulation + noise_mod = Math.min(1,Math.max(0,noise_mod+mod)) + + when 8 # Filter Cutoff + mod = (@inputs.modulation.amount-.5)*2*@modulation + cutoff += mod + + when 9 # Filter Resonance + mod = (@inputs.modulation.amount-.5)*2*@modulation + q = Math.max(0,Math.min(1,q+mod)) + + when 10 # LFO1 Amount + mod = (@inputs.modulation.amount-.5)*2*@modulation + lfo1_amount = Math.max(0,Math.min(1,lfo1_amount+mod)) + + when 11 # LFO1 Rate + mod = (@inputs.modulation.amount-.5)*2*@modulation + lfo1_rate = Math.max(0,Math.min(1,lfo1_rate+mod)) + + when 12 # LFO2 Amount + mod = (@inputs.modulation.amount-.5)*2*@modulation + lfo2_amount = Math.max(0,Math.min(1,lfo2_amount+mod)) + + when 13 # LFO2 Rate + mod = (@inputs.modulation.amount-.5)*2*@modulation + lfo2_rate = Math.max(0,Math.min(1,lfo2_rate+mod)) + + + switch @inputs.env2.out + when 0 # Filter Cutoff + cutoff += @env2.process(@on)*(@env2_amount*2-1) + + when 1 # Filter Resonance + q = Math.max(0,Math.min(1,@env2.process(@on)*(@env2_amount*2-1))) + + when 2 # Pitch + mod = @env2_amount*2-1 + mod *= @env2.process(@on) + mod = 1+mod + osc1_freq *= mod + osc2_freq *= mod + + when 3 # Mod + mod = @env2.process(@on)*(@env2_amount*2-1) + osc1_mod = Math.min(1,Math.max(0,osc1_mod+mod)) + osc2_mod = Math.min(1,Math.max(0,osc2_mod+mod)) + + when 4 # Amp + mod = @env2.process(@on)*(@env2_amount*2-1) + osc1_amp *= Math.max(0,1+mod) + osc2_amp *= Math.max(0,1+mod) + noise_amp *= Math.max(0,1+mod) + + when 5 #Osc1 Pitch + mod = @env2_amount*2-1 + mod *= @env2.process(@on) + osc1_freq *= 1+mod + + when 6 #Osc1 Mod + mod = @env2.process(@on)*(@env2_amount*2-1) + osc1_mod = Math.min(1,Math.max(0,osc1_mod+mod)) + + when 7 #Osc1 Amp + mod = @env2.process(@on)*(@env2_amount*2-1) + osc1_amp = Math.max(0,osc1_amp+mod) + + when 8 #Osc2 Pitch + mod = @env2_amount*2-1 + mod *= @env2.process(@on) + osc1_freq *= 1+mod + + when 9 #Osc2 Mod + mod = @env2.process(@on)*(@env2_amount*2-1) + osc2_mod = Math.min(1,Math.max(0,osc2_mod+mod)) + + when 10 #Osc2 Amp + mod = @env2.process(@on)*(@env2_amount*2-1) + osc2_amp = Math.max(0,osc2_amp+mod) + + when 11 # Noise amp + mod = @env2.process(@on)*(@env2_amount*2-1) + noise_amp = Math.max(0,noise_amp+mod) + + when 12 # Noise color + mod = @env2.process(@on)*(@env2_amount*2-1) + noise_mod = Math.min(1,Math.max(0,noise_mod+mod)) + + when 13 # LFO1 Amount + mod = @env2.process(@on)*(@env2_amount*2-1) + lfo1_amount = Math.min(1,Math.max(0,lfo1_amount+mod)) + + when 14 # LFO1 rate + mod = @env2.process(@on)*(@env2_amount*2-1) + lfo1_rate = Math.min(1,Math.max(0,lfo1_rate+mod)) + + when 15 # LFO2 Amount + mod = @env2.process(@on)*(@env2_amount*2-1) + lfo2_amount = Math.min(1,Math.max(0,lfo2_amount+mod)) + + when 16 # LFO2 rate + mod = @env2.process(@on)*(@env2_amount*2-1) + lfo2_rate = Math.min(1,Math.max(0,lfo2_rate+mod)) + + switch @inputs.lfo1.out + when 0 # Pitch + mod = lfo1_amount + if @inputs.lfo1.audio + mod = 1+mod*mod*@lfo1.process(lfo1_rate)*16 + else + mod = 1+mod*mod*@lfo1.process(lfo1_rate) + osc1_freq *= mod + osc2_freq *= mod + + when 1 # Mod + mod = @lfo1.process(lfo1_rate)*lfo1_amount + osc1_mod = Math.min(1,Math.max(0,osc1_mod+mod)) + osc2_mod = Math.min(1,Math.max(0,osc2_mod+mod)) + + when 2 # Amp + mod = @lfo1.process(lfo1_rate)*lfo1_amount + osc1_amp *= Math.max(0,1+mod) + osc2_amp *= Math.max(0,1+mod) + noise_amp *= Math.max(0,1+mod) + + when 3 #Osc1 Pitch + mod = lfo1_amount + mod = 1+mod*mod*@lfo1.process(lfo1_rate) + osc1_freq *= mod + + when 4 #Osc1 Mod + mod = @lfo1.process(lfo1_rate)*lfo1_amount + osc1_mod = Math.min(1,Math.max(0,osc1_mod+mod)) + + when 5 #Osc1 Amp + mod = @lfo1.process(lfo1_rate)*lfo1_amount + osc1_amp = Math.max(0,osc1_amp+mod) + + when 6 #Osc2 Pitch + mod = lfo1_amount + mod = 1+mod*mod*@lfo1.process(lfo1_rate) + osc2_freq *= mod + + when 7 #Osc2 Mod + mod = @lfo1.process(lfo1_rate)*lfo1_amount + osc2_mod = Math.min(1,Math.max(0,osc2_mod+mod)) + + when 8 #Osc2 Amp + mod = @lfo1.process(lfo1_rate)*lfo1_amount + osc2_amp = Math.max(0,osc2_amp+mod) + + when 9 # Noise amp + mod = @lfo1.process(lfo1_rate)*lfo1_amount + noise_amp = Math.max(0,noise_amp+mod) + + when 10 # Noise color + mod = @lfo1.process(lfo1_rate)*lfo1_amount + noise_mod = Math.min(1,Math.max(0,noise_mod+mod)) + + when 11 + cutoff += @lfo1.process(lfo1_rate)*lfo1_amount + + when 12 + q = Math.max(0,Math.min(1,@lfo1.process(lfo1_rate)*lfo1_amount)) + + when 13 # LFO2 Amount + mod = @lfo1.process(lfo1_rate)*lfo1_amount + lfo2_amount = Math.min(1,Math.max(0,lfo2_amount+mod)) + + when 14 # LFO2 rate + mod = @lfo1.process(lfo1_rate)*lfo1_amount + lfo2_rate = Math.min(1,Math.max(0,lfo2_rate+mod)) + + + switch @inputs.lfo2.out + when 0 # Pitch + mod = lfo2_amount + if @inputs.lfo2.audio + mod = 1+mod*mod*@lfo2.process(lfo2_rate)*16 + else + mod = 1+mod*mod*@lfo2.process(lfo2_rate) + osc1_freq *= mod + osc2_freq *= mod + + when 1 # Mod + mod = @lfo2.process(lfo2_rate)*lfo2_amount + osc1_mod = Math.min(1,Math.max(0,osc1_mod+mod)) + osc2_mod = Math.min(1,Math.max(0,osc2_mod+mod)) + + when 2 # Amp + mod = @lfo2.process(lfo2_rate)*lfo2_amount + osc1_amp *= Math.max(0,1+mod) + osc2_amp *= Math.max(0,1+mod) + noise_amp *= Math.max(0,1+mod) + + when 3 #Osc1 Pitch + mod = lfo2_amount + mod = 1+mod*mod*@lfo2.process(lfo2_rate) + osc1_freq *= mod + + when 4 #Osc1 Mod + mod = @lfo2.process(lfo2_rate)*lfo2_amount + osc1_mod = Math.min(1,Math.max(0,osc1_mod+mod)) + + when 5 #Osc1 Amp + mod = @lfo2.process(lfo2_rate)*lfo2_amount + osc1_amp = Math.max(0,osc1_amp+mod) + + when 6 #Osc2 Pitch + mod = lfo2_amount + mod = 1+mod*mod*@lfo2.process(lfo2_rate) + osc2_freq *= mod + + when 7 #Osc2 Mod + mod = @lfo2.process(lfo2_rate)*lfo2_amount + osc2_mod = Math.min(1,Math.max(0,osc2_mod+mod)) + + when 8 #Osc2 Amp + mod = @lfo2.process(lfo2_rate)*lfo2_amount + osc2_amp = Math.max(0,osc2_amp+mod) + + when 9 # Noise amp + mod = @lfo2.process(lfo2_rate)*lfo2_amount + noise_amp = Math.max(0,noise_amp+mod) + + when 10 # Noise color + mod = @lfo2.process(lfo2_rate)*lfo2_amount + noise_mod = Math.min(1,Math.max(0,noise_mod+mod)) + + when 11 + cutoff += @lfo2.process(lfo2_rate)*lfo2_amount + + when 12 + q = Math.max(0,Math.min(1,@lfo2.process(lfo2_rate)*lfo2_amount)) + + # switch @inputs.combine + # when 1 + # s1 = @osc1.process(osc1_freq,osc1_mod)*osc1_amp + # s2 = @osc2.process(osc2_freq,osc2_mod)*osc2_amp + # sig = (s1+s2)*(s1+s2) + # #sig = @osc1.process(osc1_freq,osc1_mod)*@osc2.process(osc2_freq,osc2_mod)*Math.max(osc1_amp,osc2_amp)*4 + # when 2 + # sig = @osc2.process(osc2_freq*Math.max(0,1-@osc1.process(osc1_freq,osc1_mod)*osc1_amp),osc2_mod)*osc2_amp + # else + + if @osc1_on + if @osc2_on + sig = @osc1.process(osc1_freq,osc1_mod)*osc1_amp+@osc2.process(osc2_freq,osc2_mod)*osc2_amp + else + sig = @osc1.process(osc1_freq,osc1_mod)*osc1_amp + else if @osc2_on + sig = @osc2.process(osc2_freq,osc2_mod)*osc2_amp + else + sig = 0 + + if @noise_on + sig += @noise.process(noise_mod)*noise_amp + + mod = @env2.process(@on) + + if not @cutoff + @cutoff = cutoff + else + if @cutoffcutoff + @cutoff += Math.max(cutoff-@cutoff,-@filter_increment) + + cutoff = @cutoff + + # VCF + cutoff = Math.pow(2,Math.max(0,Math.min(cutoff,1))*10)*22000/1024 + + sig *= @env1.process(@on) + + #@cutoff += 0.001*(cutoff-@cutoff) + + sig = @filter.process(sig,cutoff,q*q*9.5+.5) diff --git a/static/js/sound/audioengine/voice.js b/static/js/sound/audioengine/voice.js new file mode 100644 index 00000000..3ca16dd0 --- /dev/null +++ b/static/js/sound/audioengine/voice.js @@ -0,0 +1,538 @@ +var Voice; + +Voice = (function() { + Voice.oscillators = [SawOscillator, SquareOscillator, SineOscillator, VoiceOscillator, StringOscillator]; + + function Voice(engine) { + this.engine = engine; + this.osc1 = new SineOscillator(this); + this.osc2 = new SineOscillator(this); + this.noise = new Noise(this); + this.lfo1 = new LFO(this); + this.lfo2 = new LFO(this); + this.filter = new Filter(this); + this.env1 = new AmpEnvelope(this); + this.env2 = new ModEnvelope(this); + this.filter_increment = 0.0005 * 44100 / this.engine.sampleRate; + this.modulation = 0; + this.noteon_time = 0; + } + + Voice.prototype.init = function(layer) { + if (!(this.osc1 instanceof Voice.oscillators[layer.inputs.osc1.type])) { + this.osc1 = new Voice.oscillators[layer.inputs.osc1.type](this); + } + if (!(this.osc2 instanceof Voice.oscillators[layer.inputs.osc2.type])) { + this.osc2 = new Voice.oscillators[layer.inputs.osc2.type](this); + } + this.osc1.init(layer.inputs.osc1, layer.inputs.sync); + this.osc2.init(layer.inputs.osc2, layer.inputs.sync); + this.noise.init(layer, layer.inputs.sync); + this.lfo1.init(layer.inputs.lfo1, layer.inputs.sync); + this.lfo2.init(layer.inputs.lfo2, layer.inputs.sync); + this.filter.init(layer); + this.env1.init(layer.inputs.env1); + this.env2.init(layer.inputs.env2); + return this.updateConstantMods(); + }; + + Voice.prototype.update = function() { + if (this.layer == null) { + return; + } + if (!(this.osc1 instanceof Voice.oscillators[this.layer.inputs.osc1.type])) { + this.osc1 = new Voice.oscillators[this.layer.inputs.osc1.type](this, this.layer.inputs.osc1); + } + if (!(this.osc2 instanceof Voice.oscillators[this.layer.inputs.osc2.type])) { + this.osc2 = new Voice.oscillators[this.layer.inputs.osc2.type](this, this.layer.inputs.osc2); + } + this.osc1.update(this.layer.inputs.osc1, this.layer.inputs.sync); + this.osc2.update(this.layer.inputs.osc2, this.layer.inputs.sync); + this.env1.update(); + this.env2.update(); + this.lfo1.update(); + this.lfo2.update(); + this.filter.update(); + return this.updateConstantMods(); + }; + + Voice.prototype.updateConstantMods = function() { + var a, amp, c, mod, norm, p; + this.osc1_on = this.inputs.osc1.amp > 0 || this.inputs.lfo1.out === 5 || this.inputs.lfo2.out === 5 || this.inputs.env2.out === 7 || this.inputs.velocity.out === 4 || this.inputs.modulation.out === 2; + this.osc2_on = this.inputs.osc2.amp > 0 || this.inputs.lfo1.out === 8 || this.inputs.lfo2.out === 8 || this.inputs.env2.out === 10 || this.inputs.velocity.out === 6 || this.inputs.modulation.out === 4; + this.noise_on = this.inputs.noise.amp > 0 || this.inputs.lfo1.out === 9 || this.inputs.lfo2.out === 9 || this.inputs.env2.out === 11 || this.inputs.velocity.out === 7 || this.inputs.modulation.out === 6; + this.osc1_amp = DBSCALE(this.inputs.osc1.amp, 3); + this.osc2_amp = DBSCALE(this.inputs.osc2.amp, 3); + this.osc1_mod = this.inputs.osc1.mod; + this.osc2_mod = this.inputs.osc2.mod; + this.noise_amp = DBSCALE(this.inputs.noise.amp, 3); + this.noise_mod = this.inputs.noise.mod; + if (this.inputs.velocity.amp > 0) { + p = this.inputs.velocity.amp; + p *= p; + p *= 4; + norm = this.inputs.velocity.amp * 4; + amp = Math.exp(this.velocity * norm) / Math.exp(norm); + } else { + amp = 1; + } + this.osc1_amp *= amp; + this.osc2_amp *= amp; + this.noise_amp *= amp; + c = Math.log(1024 * this.freq / 22000) / (10 * Math.log(2)); + this.cutoff_keymod = (c - .5) * this.inputs.filter.follow; + this.cutoff_base = this.inputs.filter.cutoff + this.cutoff_keymod; + this.env2_amount = this.inputs.env2.amount; + this.lfo1_rate = this.inputs.lfo1.rate; + this.lfo1_amount = this.inputs.lfo1.amount; + this.lfo2_rate = this.inputs.lfo2.rate; + this.lfo2_amount = this.inputs.lfo2.amount; + switch (this.inputs.velocity.out) { + case 0: + mod = this.velocity * (this.inputs.velocity.amount - .5) * 2; + this.osc1_mod = Math.min(1, Math.max(0, this.osc1_mod + mod)); + this.osc2_mod = Math.min(1, Math.max(0, this.osc2_mod + mod)); + break; + case 1: + mod = this.velocity * (this.inputs.velocity.amount - .5) * 2; + this.cutoff_base += mod; + break; + case 3: + mod = this.velocity * (this.inputs.velocity.amount - .5) * 2; + this.osc1_mod = Math.min(1, Math.max(0, this.osc1_mod + mod)); + break; + case 4: + mod = this.velocity * (this.inputs.velocity.amount - .5) * 2; + this.osc1_amp = Math.max(0, this.osc1_amp + mod); + break; + case 5: + mod = this.velocity * (this.inputs.velocity.amount - .5) * 2; + this.osc2_mod = Math.min(1, Math.max(0, this.osc2_mod + mod)); + break; + case 6: + mod = this.velocity * (this.inputs.velocity.amount - .5) * 2; + this.osc2_amp = Math.max(0, this.osc2_amp + mod); + break; + case 7: + mod = this.velocity * (this.inputs.velocity.amount - .5) * 2; + this.noise_amp = Math.max(0, this.noise_amp + mod); + break; + case 8: + mod = this.velocity * (this.inputs.velocity.amount - .5) * 2; + this.noise_mod = Math.min(1, Math.max(0, this.noise_mod + mod)); + break; + case 9: + mod = this.velocity * (this.inputs.velocity.amount - .5) * 2; + a = Math.max(0, Math.min(this.inputs.env1.a + mod, 1)); + this.env1.update(a); + break; + case 10: + mod = this.velocity * (this.inputs.velocity.amount - .5) * 2; + a = Math.max(0, Math.min(this.inputs.env2.a + mod, 1)); + this.env2.update(a); + break; + case 11: + mod = this.velocity * (this.inputs.velocity.amount - .5) * 2; + this.env2_amount = Math.max(0, Math.min(1, this.env2_amount + mod)); + break; + case 12: + mod = this.velocity * (this.inputs.velocity.amount - .5) * 2; + this.lfo1_amount = Math.max(0, this.lfo1_amount + mod); + break; + case 13: + mod = this.velocity * (this.inputs.velocity.amount - .5) * 2; + this.lfo1_rate = Math.min(1, Math.max(0, this.lfo1_rate + mod)); + break; + case 14: + mod = this.velocity * (this.inputs.velocity.amount - .5) * 2; + this.lfo2_amount = Math.max(0, this.lfo2_amount + mod); + break; + case 15: + mod = this.velocity * (this.inputs.velocity.amount - .5) * 2; + this.lfo2_rate = Math.min(1, Math.max(0, this.lfo2_rate + mod)); + } + if (this.freq != null) { + c = Math.log(1024 * this.freq / 22000) / (10 * Math.log(2)); + return this.cutoff_keymod = (c - .5) * this.inputs.filter.follow; + } + }; + + Voice.prototype.noteOn = function(layer, key, velocity, legato) { + var glide_time; + this.key = key; + if (legato == null) { + legato = false; + } + this.velocity = velocity / 127; + if (this.layer != null) { + this.layer.removeVoice(this); + } + this.layer = layer; + this.inputs = this.layer.inputs; + if (legato && this.on) { + this.freq = 440 * Math.pow(Math.pow(2, 1 / 12), this.key - 57); + if (layer.last_key != null) { + this.glide_from = layer.last_key; + this.glide = true; + this.glide_phase = 0; + glide_time = (this.inputs.glide * .025 + Math.pow(this.inputs.glide, 16) * .975) * 10; + this.glide_inc = 1 / (glide_time * this.engine.sampleRate + 1); + } + } else { + this.freq = 440 * Math.pow(Math.pow(2, 1 / 12), this.key - 57); + this.init(this.layer); + this.on = true; + this.cutoff = 0; + this.modulation = this.layer.instrument.modulation; + this.pitch_bend = this.layer.instrument.pitch_bend; + this.modulation_v = 0; + this.pitch_bend_v = 0; + } + return this.noteon_time = Date.now(); + }; + + Voice.prototype.noteOff = function() { + return this.on = false; + }; + + Voice.prototype.process = function() { + var cutoff, f, k, lfo1_amount, lfo1_rate, lfo2_amount, lfo2_rate, mod, noise_amp, noise_mod, osc1_amp, osc1_freq, osc1_mod, osc2_amp, osc2_freq, osc2_mod, p, q, sig; + osc1_mod = this.osc1_mod; + osc2_mod = this.osc2_mod; + osc1_amp = this.osc1_amp; + osc2_amp = this.osc2_amp; + if (this.glide) { + k = this.glide_from * (1 - this.glide_phase) + this.key * this.glide_phase; + osc1_freq = osc2_freq = 440 * Math.pow(Math.pow(2, 1 / 12), k - 57); + this.glide_phase += this.glide_inc; + if (this.glide_phase >= 1) { + this.glide = false; + } + } else { + osc1_freq = osc2_freq = this.freq; + } + if (Math.abs(this.pitch_bend - this.layer.instrument.pitch_bend) > .0001) { + this.pitch_bend_v += .001 * (this.layer.instrument.pitch_bend - this.pitch_bend); + this.pitch_bend_v *= .5; + this.pitch_bend += this.pitch_bend_v; + } + if (Math.abs(this.pitch_bend - .5) > .0001) { + p = this.pitch_bend * 2 - 1; + p *= 2; + f = Math.pow(Math.pow(2, 1 / 12), p); + osc1_freq *= f; + osc2_freq *= f; + } + noise_amp = this.noise_amp; + noise_mod = this.noise_mod; + lfo1_rate = this.lfo1_rate; + lfo1_amount = this.lfo1_amount; + lfo2_rate = this.lfo2_rate; + lfo2_amount = this.lfo2_amount; + cutoff = this.cutoff_base; + q = this.inputs.filter.resonance; + if (Math.abs(this.modulation - this.layer.instrument.modulation) > .0001) { + this.modulation_v += .001 * (this.layer.instrument.modulation - this.modulation); + this.modulation_v *= .5; + this.modulation += this.modulation_v; + } + switch (this.inputs.modulation.out) { + case 0: + mod = (this.inputs.modulation.amount - .5) * 2 * this.modulation; + osc1_amp *= Math.max(0, 1 + mod); + osc2_amp *= Math.max(0, 1 + mod); + noise_amp *= Math.max(0, 1 + mod); + break; + case 1: + mod = (this.inputs.modulation.amount - .5) * 2 * this.modulation; + osc1_mod = Math.min(1, Math.max(0, osc1_mod + mod)); + osc2_mod = Math.min(1, Math.max(0, osc2_mod + mod)); + break; + case 2: + mod = (this.inputs.modulation.amount - .5) * 2 * this.modulation; + osc1_amp = Math.max(0, osc1_amp + mod); + break; + case 3: + mod = (this.inputs.modulation.amount - .5) * 2 * this.modulation; + osc1_mod = Math.min(1, Math.max(0, osc1_mod + mod)); + break; + case 4: + mod = (this.inputs.modulation.amount - .5) * 2 * this.modulation; + osc2_amp = Math.max(0, osc2_amp + mod); + break; + case 5: + mod = (this.inputs.modulation.amount - .5) * 2 * this.modulation; + osc2_mod = Math.min(1, Math.max(0, osc2_mod + mod)); + break; + case 6: + mod = (this.inputs.modulation.amount - .5) * 2 * this.modulation; + noise_amp = Math.max(0, noise_amp + mod); + break; + case 7: + mod = (this.inputs.modulation.amount - .5) * 2 * this.modulation; + noise_mod = Math.min(1, Math.max(0, noise_mod + mod)); + break; + case 8: + mod = (this.inputs.modulation.amount - .5) * 2 * this.modulation; + cutoff += mod; + break; + case 9: + mod = (this.inputs.modulation.amount - .5) * 2 * this.modulation; + q = Math.max(0, Math.min(1, q + mod)); + break; + case 10: + mod = (this.inputs.modulation.amount - .5) * 2 * this.modulation; + lfo1_amount = Math.max(0, Math.min(1, lfo1_amount + mod)); + break; + case 11: + mod = (this.inputs.modulation.amount - .5) * 2 * this.modulation; + lfo1_rate = Math.max(0, Math.min(1, lfo1_rate + mod)); + break; + case 12: + mod = (this.inputs.modulation.amount - .5) * 2 * this.modulation; + lfo2_amount = Math.max(0, Math.min(1, lfo2_amount + mod)); + break; + case 13: + mod = (this.inputs.modulation.amount - .5) * 2 * this.modulation; + lfo2_rate = Math.max(0, Math.min(1, lfo2_rate + mod)); + } + switch (this.inputs.env2.out) { + case 0: + cutoff += this.env2.process(this.on) * (this.env2_amount * 2 - 1); + break; + case 1: + q = Math.max(0, Math.min(1, this.env2.process(this.on) * (this.env2_amount * 2 - 1))); + break; + case 2: + mod = this.env2_amount * 2 - 1; + mod *= this.env2.process(this.on); + mod = 1 + mod; + osc1_freq *= mod; + osc2_freq *= mod; + break; + case 3: + mod = this.env2.process(this.on) * (this.env2_amount * 2 - 1); + osc1_mod = Math.min(1, Math.max(0, osc1_mod + mod)); + osc2_mod = Math.min(1, Math.max(0, osc2_mod + mod)); + break; + case 4: + mod = this.env2.process(this.on) * (this.env2_amount * 2 - 1); + osc1_amp *= Math.max(0, 1 + mod); + osc2_amp *= Math.max(0, 1 + mod); + noise_amp *= Math.max(0, 1 + mod); + break; + case 5: + mod = this.env2_amount * 2 - 1; + mod *= this.env2.process(this.on); + osc1_freq *= 1 + mod; + break; + case 6: + mod = this.env2.process(this.on) * (this.env2_amount * 2 - 1); + osc1_mod = Math.min(1, Math.max(0, osc1_mod + mod)); + break; + case 7: + mod = this.env2.process(this.on) * (this.env2_amount * 2 - 1); + osc1_amp = Math.max(0, osc1_amp + mod); + break; + case 8: + mod = this.env2_amount * 2 - 1; + mod *= this.env2.process(this.on); + osc1_freq *= 1 + mod; + break; + case 9: + mod = this.env2.process(this.on) * (this.env2_amount * 2 - 1); + osc2_mod = Math.min(1, Math.max(0, osc2_mod + mod)); + break; + case 10: + mod = this.env2.process(this.on) * (this.env2_amount * 2 - 1); + osc2_amp = Math.max(0, osc2_amp + mod); + break; + case 11: + mod = this.env2.process(this.on) * (this.env2_amount * 2 - 1); + noise_amp = Math.max(0, noise_amp + mod); + break; + case 12: + mod = this.env2.process(this.on) * (this.env2_amount * 2 - 1); + noise_mod = Math.min(1, Math.max(0, noise_mod + mod)); + break; + case 13: + mod = this.env2.process(this.on) * (this.env2_amount * 2 - 1); + lfo1_amount = Math.min(1, Math.max(0, lfo1_amount + mod)); + break; + case 14: + mod = this.env2.process(this.on) * (this.env2_amount * 2 - 1); + lfo1_rate = Math.min(1, Math.max(0, lfo1_rate + mod)); + break; + case 15: + mod = this.env2.process(this.on) * (this.env2_amount * 2 - 1); + lfo2_amount = Math.min(1, Math.max(0, lfo2_amount + mod)); + break; + case 16: + mod = this.env2.process(this.on) * (this.env2_amount * 2 - 1); + lfo2_rate = Math.min(1, Math.max(0, lfo2_rate + mod)); + } + switch (this.inputs.lfo1.out) { + case 0: + mod = lfo1_amount; + if (this.inputs.lfo1.audio) { + mod = 1 + mod * mod * this.lfo1.process(lfo1_rate) * 16; + } else { + mod = 1 + mod * mod * this.lfo1.process(lfo1_rate); + } + osc1_freq *= mod; + osc2_freq *= mod; + break; + case 1: + mod = this.lfo1.process(lfo1_rate) * lfo1_amount; + osc1_mod = Math.min(1, Math.max(0, osc1_mod + mod)); + osc2_mod = Math.min(1, Math.max(0, osc2_mod + mod)); + break; + case 2: + mod = this.lfo1.process(lfo1_rate) * lfo1_amount; + osc1_amp *= Math.max(0, 1 + mod); + osc2_amp *= Math.max(0, 1 + mod); + noise_amp *= Math.max(0, 1 + mod); + break; + case 3: + mod = lfo1_amount; + mod = 1 + mod * mod * this.lfo1.process(lfo1_rate); + osc1_freq *= mod; + break; + case 4: + mod = this.lfo1.process(lfo1_rate) * lfo1_amount; + osc1_mod = Math.min(1, Math.max(0, osc1_mod + mod)); + break; + case 5: + mod = this.lfo1.process(lfo1_rate) * lfo1_amount; + osc1_amp = Math.max(0, osc1_amp + mod); + break; + case 6: + mod = lfo1_amount; + mod = 1 + mod * mod * this.lfo1.process(lfo1_rate); + osc2_freq *= mod; + break; + case 7: + mod = this.lfo1.process(lfo1_rate) * lfo1_amount; + osc2_mod = Math.min(1, Math.max(0, osc2_mod + mod)); + break; + case 8: + mod = this.lfo1.process(lfo1_rate) * lfo1_amount; + osc2_amp = Math.max(0, osc2_amp + mod); + break; + case 9: + mod = this.lfo1.process(lfo1_rate) * lfo1_amount; + noise_amp = Math.max(0, noise_amp + mod); + break; + case 10: + mod = this.lfo1.process(lfo1_rate) * lfo1_amount; + noise_mod = Math.min(1, Math.max(0, noise_mod + mod)); + break; + case 11: + cutoff += this.lfo1.process(lfo1_rate) * lfo1_amount; + break; + case 12: + q = Math.max(0, Math.min(1, this.lfo1.process(lfo1_rate) * lfo1_amount)); + break; + case 13: + mod = this.lfo1.process(lfo1_rate) * lfo1_amount; + lfo2_amount = Math.min(1, Math.max(0, lfo2_amount + mod)); + break; + case 14: + mod = this.lfo1.process(lfo1_rate) * lfo1_amount; + lfo2_rate = Math.min(1, Math.max(0, lfo2_rate + mod)); + } + switch (this.inputs.lfo2.out) { + case 0: + mod = lfo2_amount; + if (this.inputs.lfo2.audio) { + mod = 1 + mod * mod * this.lfo2.process(lfo2_rate) * 16; + } else { + mod = 1 + mod * mod * this.lfo2.process(lfo2_rate); + } + osc1_freq *= mod; + osc2_freq *= mod; + break; + case 1: + mod = this.lfo2.process(lfo2_rate) * lfo2_amount; + osc1_mod = Math.min(1, Math.max(0, osc1_mod + mod)); + osc2_mod = Math.min(1, Math.max(0, osc2_mod + mod)); + break; + case 2: + mod = this.lfo2.process(lfo2_rate) * lfo2_amount; + osc1_amp *= Math.max(0, 1 + mod); + osc2_amp *= Math.max(0, 1 + mod); + noise_amp *= Math.max(0, 1 + mod); + break; + case 3: + mod = lfo2_amount; + mod = 1 + mod * mod * this.lfo2.process(lfo2_rate); + osc1_freq *= mod; + break; + case 4: + mod = this.lfo2.process(lfo2_rate) * lfo2_amount; + osc1_mod = Math.min(1, Math.max(0, osc1_mod + mod)); + break; + case 5: + mod = this.lfo2.process(lfo2_rate) * lfo2_amount; + osc1_amp = Math.max(0, osc1_amp + mod); + break; + case 6: + mod = lfo2_amount; + mod = 1 + mod * mod * this.lfo2.process(lfo2_rate); + osc2_freq *= mod; + break; + case 7: + mod = this.lfo2.process(lfo2_rate) * lfo2_amount; + osc2_mod = Math.min(1, Math.max(0, osc2_mod + mod)); + break; + case 8: + mod = this.lfo2.process(lfo2_rate) * lfo2_amount; + osc2_amp = Math.max(0, osc2_amp + mod); + break; + case 9: + mod = this.lfo2.process(lfo2_rate) * lfo2_amount; + noise_amp = Math.max(0, noise_amp + mod); + break; + case 10: + mod = this.lfo2.process(lfo2_rate) * lfo2_amount; + noise_mod = Math.min(1, Math.max(0, noise_mod + mod)); + break; + case 11: + cutoff += this.lfo2.process(lfo2_rate) * lfo2_amount; + break; + case 12: + q = Math.max(0, Math.min(1, this.lfo2.process(lfo2_rate) * lfo2_amount)); + } + if (this.osc1_on) { + if (this.osc2_on) { + sig = this.osc1.process(osc1_freq, osc1_mod) * osc1_amp + this.osc2.process(osc2_freq, osc2_mod) * osc2_amp; + } else { + sig = this.osc1.process(osc1_freq, osc1_mod) * osc1_amp; + } + } else if (this.osc2_on) { + sig = this.osc2.process(osc2_freq, osc2_mod) * osc2_amp; + } else { + sig = 0; + } + if (this.noise_on) { + sig += this.noise.process(noise_mod) * noise_amp; + } + mod = this.env2.process(this.on); + if (!this.cutoff) { + this.cutoff = cutoff; + } else { + if (this.cutoff < cutoff) { + this.cutoff += Math.min(cutoff - this.cutoff, this.filter_increment); + } else if (this.cutoff > cutoff) { + this.cutoff += Math.max(cutoff - this.cutoff, -this.filter_increment); + } + cutoff = this.cutoff; + } + cutoff = Math.pow(2, Math.max(0, Math.min(cutoff, 1)) * 10) * 22000 / 1024; + sig *= this.env1.process(this.on); + return sig = this.filter.process(sig, cutoff, q * q * 9.5 + .5); + }; + + return Voice; + +})(); diff --git a/static/js/sound/audiooutput.coffee b/static/js/sound/audiooutput.coffee new file mode 100644 index 00000000..9ad677e8 --- /dev/null +++ b/static/js/sound/audiooutput.coffee @@ -0,0 +1,74 @@ +class @AudioOutput + constructor:()-> + + start:()-> + @context = new (window.AudioContext || window.webkitAudioContext) + + if @context.audioWorklet? + url = "synth/audioengine.js" + @context.audioWorklet.addModule(url).then ()=> + @node = new AudioWorkletNode(@context,"my-worklet-processor",{outputChannelCount: [2] }) + @node.connect(@context.destination) + + @addParam "Osc1 coarse","osc1_coarse" + @addParam "Osc1 tune","osc1_tune" + @addParam "Osc1 amp","osc1_amp" + @addParam "Osc1 mod","osc1_mod" + + @addParam "Osc2 coarse","osc2_coarse" + @addParam "Osc2 tune","osc2_tune" + @addParam "Osc2 amp","osc2_amp" + @addParam "Osc2 mod","osc2_mod" + + @addParam "Noise","noise" + @addParam "Noise mod","noise_mod" + + @addParam "Env1 A","env1.a" + @addParam "Env1 D","env1.d" + @addParam "Env1 S","env1.s" + @addParam "Env1 R","env1.r" + + @addParam "Slope","filter_slope" + @addParam "Type","filter_type" + @addParam "Cutoff","filter_cutoff" + @addParam "Resonance","filter_resonance" + @addParam "Env","filter_env_amount" + + @addParam "Env2 A","env2.a" + @addParam "Env2 D","env2.d" + @addParam "Env2 S","env2.s" + @addParam "Env2 R","env2.r" + + @addParam "LFO1 form","lfo1.form" + @addParam "LFO1 rate","lfo1.rate" + @addParam "LFO1 amp","lfo1.amp" + + @addParam "Disto Wet","disto.wet" + @addParam "Disto Drive","disto.drive" + + @addParam "Bitcrusher Wet","bitcrusher.wet" + @addParam "Bitcrusher Drive","bitcrusher.drive" + @addParam "Bitcrusher Crush","bitcrusher.crush" + + addParam:(name,id)-> + h = document.createElement "h4" + h.innerText = name + document.body.appendChild h + + input = document.createElement "input" + input.id = id + input.type = "range" + document.body.appendChild input + input.addEventListener "input",()=> + @sendParam id,input.value/100 + + sendParam:(id,value)-> + @node.port.postMessage JSON.stringify + name: "param" + id: id + value: value + + sendNote:(data)-> + @node.port.postMessage JSON.stringify + name: "note" + data: data diff --git a/static/js/sound/audiooutput.js b/static/js/sound/audiooutput.js new file mode 100644 index 00000000..2a91a65d --- /dev/null +++ b/static/js/sound/audiooutput.js @@ -0,0 +1,84 @@ +this.AudioOutput = (function() { + function AudioOutput() {} + + AudioOutput.prototype.start = function() { + var url; + this.context = new (window.AudioContext || window.webkitAudioContext); + if (this.context.audioWorklet != null) { + url = "synth/audioengine.js"; + this.context.audioWorklet.addModule(url).then((function(_this) { + return function() { + _this.node = new AudioWorkletNode(_this.context, "my-worklet-processor", { + outputChannelCount: [2] + }); + return _this.node.connect(_this.context.destination); + }; + })(this)); + } + this.addParam("Osc1 coarse", "osc1_coarse"); + this.addParam("Osc1 tune", "osc1_tune"); + this.addParam("Osc1 amp", "osc1_amp"); + this.addParam("Osc1 mod", "osc1_mod"); + this.addParam("Osc2 coarse", "osc2_coarse"); + this.addParam("Osc2 tune", "osc2_tune"); + this.addParam("Osc2 amp", "osc2_amp"); + this.addParam("Osc2 mod", "osc2_mod"); + this.addParam("Noise", "noise"); + this.addParam("Noise mod", "noise_mod"); + this.addParam("Env1 A", "env1.a"); + this.addParam("Env1 D", "env1.d"); + this.addParam("Env1 S", "env1.s"); + this.addParam("Env1 R", "env1.r"); + this.addParam("Slope", "filter_slope"); + this.addParam("Type", "filter_type"); + this.addParam("Cutoff", "filter_cutoff"); + this.addParam("Resonance", "filter_resonance"); + this.addParam("Env", "filter_env_amount"); + this.addParam("Env2 A", "env2.a"); + this.addParam("Env2 D", "env2.d"); + this.addParam("Env2 S", "env2.s"); + this.addParam("Env2 R", "env2.r"); + this.addParam("LFO1 form", "lfo1.form"); + this.addParam("LFO1 rate", "lfo1.rate"); + this.addParam("LFO1 amp", "lfo1.amp"); + this.addParam("Disto Wet", "disto.wet"); + this.addParam("Disto Drive", "disto.drive"); + this.addParam("Bitcrusher Wet", "bitcrusher.wet"); + this.addParam("Bitcrusher Drive", "bitcrusher.drive"); + return this.addParam("Bitcrusher Crush", "bitcrusher.crush"); + }; + + AudioOutput.prototype.addParam = function(name, id) { + var h, input; + h = document.createElement("h4"); + h.innerText = name; + document.body.appendChild(h); + input = document.createElement("input"); + input.id = id; + input.type = "range"; + document.body.appendChild(input); + return input.addEventListener("input", (function(_this) { + return function() { + return _this.sendParam(id, input.value / 100); + }; + })(this)); + }; + + AudioOutput.prototype.sendParam = function(id, value) { + return this.node.port.postMessage(JSON.stringify({ + name: "param", + id: id, + value: value + })); + }; + + AudioOutput.prototype.sendNote = function(data) { + return this.node.port.postMessage(JSON.stringify({ + name: "note", + data: data + })); + }; + + return AudioOutput; + +})(); diff --git a/static/js/sound/audioworker.js b/static/js/sound/audioworker.js new file mode 100755 index 00000000..02a0976f --- /dev/null +++ b/static/js/sound/audioworker.js @@ -0,0 +1,142 @@ +// AudioWorklet polyfill +// Jari Kleimola 2017-18 (jari@webaudiomodules.org) +// +var AWGS = { processors:[] } + +// -------------------------------------------------------------------------- +// +// +AWGS.AudioWorkletGlobalScope = function () { + var ctors = {}; // node name to processor definition map + + function registerOnWorker(name, ctor) { + if (!ctors[name]) { + ctors[name] = ctor; + postMessage({ type:"register", name:name, descriptor:ctor.parameterDescriptors }); + } + else { + postMessage({ type:"state", node:nodeID, state:"error" }); + throw new Error("AlreadyRegistered"); + } + }; + + function constructOnWorker (name, port, options) { + if (ctors[name]) { + options = options || {} + options._port = port; + var processor = new ctors[name](options); + if (!(processor instanceof AudioWorkletProcessor)) { + postMessage({ type:"state", node:nodeID, state:"error" }); + throw new Error("InvalidStateError"); + } + return processor; + } + else { + postMessage({ type:"state", node:nodeID, state:"error" }); + throw new Error("NotSupportedException"); + } + } + + class AudioWorkletProcessorPolyfill { + constructor (options) { this.port = options._port; } + process (inputs, outputs, params) {} + } + + return { + 'AudioWorkletProcessor': AudioWorkletProcessorPolyfill, + 'registerProcessor': registerOnWorker, + '_createProcessor': constructOnWorker + } +} + + +AudioWorkletGlobalScope = AWGS.AudioWorkletGlobalScope(); +AudioWorkletProcessor = AudioWorkletGlobalScope.AudioWorkletProcessor; +registerProcessor = AudioWorkletGlobalScope.registerProcessor; +sampleRate = 44100; +hasSAB = true; + +onmessage = function (e) { + var msg = e.data; + switch (msg.type) { + + case "init": + sampleRate = AudioWorkletGlobalScope.sampleRate = msg.sampleRate; + break; + + case "import": + importScripts(msg.url); + postMessage({ type:"load", url:msg.url }); + break; + + case "createProcessor": + // -- slice io to match with SPN bufferlength + var a = msg.args; + var slices = []; + var buflen = a.options.buflenAWP; + var numSlices = (a.options.buflenSPN / buflen)|0; + + hasSAB = a.hasSAB; + if (hasSAB) { + for (var i=0; i 0) { + var nchannels = 0; + var channelCount = (type == "input") ? options.inputChannelCount : options.outputChannelCount; + if (channelCount.length > 0) nchannels = channelCount[0]; + if (nchannels <= 0) throw new Error("InvalidArgumentException"); + var port = new Array(nchannels); + for (var c=0; c + @white = 7*@octaves+1 + @white_width = @canvas.width/@white + + @black_pos = [0,1,3,4,5] + + @update() + + + update:()-> + context = @canvas.getContext "2d" + context.fillStyle = "#111" + context.fillRect(0,0,@canvas.width,@canvas.height) + + context.fillStyle = "#FFF" + + for i in [0..@white-1] by 1 + context.fillRect @white_width*i+.5,.5,@white_width-1,@canvas.height-1 + + context.fillStyle = "#000" + for o in [0..@octaves-1] by 1 + for k in @black_pos + p = o*7+k + context.fillRect @white_width*(p+.7)+.5,.5,@white_width*.6,@canvas.height*.6 + + grd = context.createLinearGradient(0,0,0,@canvas.height/8) + grd.addColorStop(0,"rgba(0,0,0,1)") + grd.addColorStop(1,"rgba(0,0,0,0)") + context.fillStyle = grd + context.fillRect 0,0,@canvas.width,@canvas.height/8 diff --git a/static/js/sound/keyboard.js b/static/js/sound/keyboard.js new file mode 100644 index 00000000..462b5e8d --- /dev/null +++ b/static/js/sound/keyboard.js @@ -0,0 +1,39 @@ +this.SynthKeyboard = (function() { + function SynthKeyboard(canvas, octaves, listener) { + this.canvas = canvas; + this.octaves = octaves; + this.listener = listener; + this.white = 7 * this.octaves + 1; + this.white_width = this.canvas.width / this.white; + this.black_pos = [0, 1, 3, 4, 5]; + this.update(); + } + + SynthKeyboard.prototype.update = function() { + var context, grd, i, j, k, l, len, m, o, p, ref, ref1, ref2; + context = this.canvas.getContext("2d"); + context.fillStyle = "#111"; + context.fillRect(0, 0, this.canvas.width, this.canvas.height); + context.fillStyle = "#FFF"; + for (i = j = 0, ref = this.white - 1; j <= ref; i = j += 1) { + context.fillRect(this.white_width * i + .5, .5, this.white_width - 1, this.canvas.height - 1); + } + context.fillStyle = "#000"; + for (o = l = 0, ref1 = this.octaves - 1; l <= ref1; o = l += 1) { + ref2 = this.black_pos; + for (m = 0, len = ref2.length; m < len; m++) { + k = ref2[m]; + p = o * 7 + k; + context.fillRect(this.white_width * (p + .7) + .5, .5, this.white_width * .6, this.canvas.height * .6); + } + } + grd = context.createLinearGradient(0, 0, 0, this.canvas.height / 8); + grd.addColorStop(0, "rgba(0,0,0,1)"); + grd.addColorStop(1, "rgba(0,0,0,0)"); + context.fillStyle = grd; + return context.fillRect(0, 0, this.canvas.width, this.canvas.height / 8); + }; + + return SynthKeyboard; + +})(); diff --git a/static/js/sound/knob.coffee b/static/js/sound/knob.coffee new file mode 100644 index 00000000..bc97b617 --- /dev/null +++ b/static/js/sound/knob.coffee @@ -0,0 +1,91 @@ +class @Knob + constructor:(@canvas,@value=0.5,@listener)-> + @canvas.width = 40 + @canvas.height = 40 + @id = @canvas.id + @type = @canvas.dataset.type + @quantize = @canvas.dataset.quantize + @value = .5 + @update() + + @canvas.addEventListener "mousedown",(event)=> + @mouseDown event + + document.addEventListener "mousemove",(event)=> + @mouseMove event + + document.addEventListener "mouseup",(event)=> + @mouseUp event + + update:()-> + context = @canvas.getContext("2d") + context.clearRect(0,0,@canvas.width,@canvas.height) + + context.strokeStyle = "rgba(0,0,0,.5)" + context.lineWidth = 6 + context.beginPath() + context.arc(@canvas.width/2,@canvas.height/2,@canvas.width/2-3,.7*Math.PI,2.3*Math.PI,false) + context.stroke() + + context.beginPath() + context.fillStyle = "rgba(0,0,0,.5)" + context.arc(@canvas.width/2,@canvas.height/2,@canvas.width/3.2,0,Math.PI*2,false) + context.fill() + + context.beginPath() + context.lineWidth = 2 + context.strokeStyle = "rgba(255,255,255,.5)" + alpha = (.7+(2.3-.7)*@value)*Math.PI + x1 = @canvas.width/2+@canvas.width/8*Math.cos(alpha) + y1 = @canvas.height/2+@canvas.height/8*Math.sin(alpha) + x2 = @canvas.width/2+@canvas.width/4*Math.cos(alpha) + y2 = @canvas.height/2+@canvas.height/4*Math.sin(alpha) + context.moveTo x1,y1 + context.lineTo x2,y2 + context.stroke() + + context.shadowBlur = 6 + context.shadowColor = "hsl(200,100%,60%)" + context.shadowOpacity = 1 + context.strokeStyle = "hsl(200,100%,60%)" + context.lineWidth = 1.5 + + if @type == "centered" + if @value>.5 + context.beginPath() + context.arc(@canvas.width/2,@canvas.height/2,@canvas.width/2-3,1.5*Math.PI,(.7+(2.3-.7)*@value)*Math.PI,false) + context.stroke() + else if @value<.5 + context.shadowColor = "hsl(20,100%,60%)" + context.strokeStyle = "hsl(20,100%,60%)" + context.beginPath() + context.arc(@canvas.width/2,@canvas.height/2,@canvas.width/2-3,(.7+(2.3-.7)*@value)*Math.PI,1.5*Math.PI,false) + context.stroke() + + else + context.beginPath() + context.arc(@canvas.width/2,@canvas.height/2,@canvas.width/2-3,.7*Math.PI,(.7+(2.3-.7)*@value)*Math.PI,false) + context.stroke() + + context.shadowBlur = context.shadowOpacity = 0 + + mouseDown:(event)-> + @mousepressed = true + @start_y = event.clientY + @start_value = @value + + mouseMove:(event)-> + return if not @mousepressed + dy = event.clientY-@start_y + v = Math.max(0,Math.min(1,@start_value-dy*.005)) + + if @quantize and @quantize>0 + v = Math.round(v*@quantize)/@quantize + + if v != @value + @value = v + @update() + @listener.knobChange(@id,@value) + + mouseUp:(event)-> + @mousepressed = false diff --git a/static/js/sound/knob.js b/static/js/sound/knob.js new file mode 100644 index 00000000..cf2d7048 --- /dev/null +++ b/static/js/sound/knob.js @@ -0,0 +1,108 @@ +this.Knob = (function() { + function Knob(canvas, value, listener) { + this.canvas = canvas; + this.value = value != null ? value : 0.5; + this.listener = listener; + this.canvas.width = 40; + this.canvas.height = 40; + this.id = this.canvas.id; + this.type = this.canvas.dataset.type; + this.quantize = this.canvas.dataset.quantize; + this.value = .5; + this.update(); + this.canvas.addEventListener("mousedown", (function(_this) { + return function(event) { + return _this.mouseDown(event); + }; + })(this)); + document.addEventListener("mousemove", (function(_this) { + return function(event) { + return _this.mouseMove(event); + }; + })(this)); + document.addEventListener("mouseup", (function(_this) { + return function(event) { + return _this.mouseUp(event); + }; + })(this)); + } + + Knob.prototype.update = function() { + var alpha, context, x1, x2, y1, y2; + context = this.canvas.getContext("2d"); + context.clearRect(0, 0, this.canvas.width, this.canvas.height); + context.strokeStyle = "rgba(0,0,0,.5)"; + context.lineWidth = 6; + context.beginPath(); + context.arc(this.canvas.width / 2, this.canvas.height / 2, this.canvas.width / 2 - 3, .7 * Math.PI, 2.3 * Math.PI, false); + context.stroke(); + context.beginPath(); + context.fillStyle = "rgba(0,0,0,.5)"; + context.arc(this.canvas.width / 2, this.canvas.height / 2, this.canvas.width / 3.2, 0, Math.PI * 2, false); + context.fill(); + context.beginPath(); + context.lineWidth = 2; + context.strokeStyle = "rgba(255,255,255,.5)"; + alpha = (.7 + (2.3 - .7) * this.value) * Math.PI; + x1 = this.canvas.width / 2 + this.canvas.width / 8 * Math.cos(alpha); + y1 = this.canvas.height / 2 + this.canvas.height / 8 * Math.sin(alpha); + x2 = this.canvas.width / 2 + this.canvas.width / 4 * Math.cos(alpha); + y2 = this.canvas.height / 2 + this.canvas.height / 4 * Math.sin(alpha); + context.moveTo(x1, y1); + context.lineTo(x2, y2); + context.stroke(); + context.shadowBlur = 6; + context.shadowColor = "hsl(200,100%,60%)"; + context.shadowOpacity = 1; + context.strokeStyle = "hsl(200,100%,60%)"; + context.lineWidth = 1.5; + if (this.type === "centered") { + if (this.value > .5) { + context.beginPath(); + context.arc(this.canvas.width / 2, this.canvas.height / 2, this.canvas.width / 2 - 3, 1.5 * Math.PI, (.7 + (2.3 - .7) * this.value) * Math.PI, false); + context.stroke(); + } else if (this.value < .5) { + context.shadowColor = "hsl(20,100%,60%)"; + context.strokeStyle = "hsl(20,100%,60%)"; + context.beginPath(); + context.arc(this.canvas.width / 2, this.canvas.height / 2, this.canvas.width / 2 - 3, (.7 + (2.3 - .7) * this.value) * Math.PI, 1.5 * Math.PI, false); + context.stroke(); + } + } else { + context.beginPath(); + context.arc(this.canvas.width / 2, this.canvas.height / 2, this.canvas.width / 2 - 3, .7 * Math.PI, (.7 + (2.3 - .7) * this.value) * Math.PI, false); + context.stroke(); + } + return context.shadowBlur = context.shadowOpacity = 0; + }; + + Knob.prototype.mouseDown = function(event) { + this.mousepressed = true; + this.start_y = event.clientY; + return this.start_value = this.value; + }; + + Knob.prototype.mouseMove = function(event) { + var dy, v; + if (!this.mousepressed) { + return; + } + dy = event.clientY - this.start_y; + v = Math.max(0, Math.min(1, this.start_value - dy * .005)); + if (this.quantize && this.quantize > 0) { + v = Math.round(v * this.quantize) / this.quantize; + } + if (v !== this.value) { + this.value = v; + this.update(); + return this.listener.knobChange(this.id, this.value); + } + }; + + Knob.prototype.mouseUp = function(event) { + return this.mousepressed = false; + }; + + return Knob; + +})(); diff --git a/static/js/sound/midi.coffee b/static/js/sound/midi.coffee new file mode 100644 index 00000000..ec68b2b6 --- /dev/null +++ b/static/js/sound/midi.coffee @@ -0,0 +1,14 @@ +class @MIDI + constructor:(@callback)-> + @scan() + + scan:()-> + if navigator.requestMIDIAccess? + navigator.requestMIDIAccess().then ((midi)=>@midiAccess(midi)),(()=>) + + midiAccess:(midi)-> + midi.inputs.forEach (port)=>port.onmidimessage = (message)=>@onMidiMessage(message) + + onMidiMessage:(message)-> + console.info message.data + @callback message.data diff --git a/static/js/sound/midi.js b/static/js/sound/midi.js new file mode 100644 index 00000000..de421491 --- /dev/null +++ b/static/js/sound/midi.js @@ -0,0 +1,36 @@ +this.MIDI = (function() { + function MIDI(callback) { + this.callback = callback; + this.scan(); + } + + MIDI.prototype.scan = function() { + if (navigator.requestMIDIAccess != null) { + return navigator.requestMIDIAccess().then(((function(_this) { + return function(midi) { + return _this.midiAccess(midi); + }; + })(this)), ((function(_this) { + return function() {}; + })(this))); + } + }; + + MIDI.prototype.midiAccess = function(midi) { + return midi.inputs.forEach((function(_this) { + return function(port) { + return port.onmidimessage = function(message) { + return _this.onMidiMessage(message); + }; + }; + })(this)); + }; + + MIDI.prototype.onMidiMessage = function(message) { + console.info(message.data); + return this.callback(message.data); + }; + + return MIDI; + +})(); diff --git a/static/js/sound/pianoroll.coffee b/static/js/sound/pianoroll.coffee new file mode 100644 index 00000000..e69de29b diff --git a/static/js/sound/polyfill/index.js b/static/js/sound/polyfill/index.js new file mode 100755 index 00000000..f5bcfaf0 --- /dev/null +++ b/static/js/sound/polyfill/index.js @@ -0,0 +1,124 @@ +/** + * Copyright 2018 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +import { Realm } from './realm'; + +const PARAMS = []; +let nextPort; + +if (typeof AudioWorkletNode !== 'function') { + self.AudioWorkletNode = function AudioWorkletNode (context, name, options) { + const processor = getProcessorsForContext(context)[name]; + const outputChannels = options && options.outputChannelCount ? options.outputChannelCount[0] : 2; + const scriptProcessor = context.createScriptProcessor(undefined, 2, outputChannels); + + scriptProcessor.parameters = new Map(); + if (processor.properties) { + for (let i = 0; i < processor.properties.length; i++) { + const prop = processor.properties[i]; + const node = context.createGain().gain; + node.value = prop.defaultValue; + // @TODO there's no good way to construct the proxy AudioParam here + scriptProcessor.parameters.set(prop.name, node); + } + } + + const mc = new MessageChannel(); + nextPort = mc.port2; + const inst = new processor.Processor(options || {}); + nextPort = null; + + scriptProcessor.port = mc.port1; + scriptProcessor.processor = processor; + scriptProcessor.instance = inst; + scriptProcessor.onaudioprocess = onAudioProcess; + return scriptProcessor; + }; + + Object.defineProperty((self.AudioContext || self.webkitAudioContext).prototype, 'audioWorklet', { + get () { + return this.$$audioWorklet || (this.$$audioWorklet = new self.AudioWorklet(this)); + } + }); + + self.AudioWorklet = class AudioWorklet { + constructor (audioContext) { + this.$$context = audioContext; + } + + addModule (url, options) { + return fetch(url).then(r => { + if (!r.ok) throw Error(r.status); + return r.text(); + }).then(code => { + const context = { + sampleRate: 0, + currentTime: 0, + AudioWorkletProcessor () { + this.port = nextPort; + }, + registerProcessor: (name, Processor) => { + const processors = getProcessorsForContext(this.$$context); + processors[name] = { + realm, + context, + Processor, + properties: Processor.parameterDescriptors || [] + }; + } + }; + context.self = context; + const realm = new Realm(context, document.documentElement); + realm.exec(((options && options.transpile) || String)(code)); + return null; + }); + } + }; +} + +function onAudioProcess (e) { + const parameters = {}; + let index = -1; + this.parameters.forEach((value, key) => { + const arr = PARAMS[++index] || (PARAMS[index] = new Float32Array(this.bufferSize)); + // @TODO proper values here if possible + arr.fill(value.value); + parameters[key] = arr; + }); + this.processor.realm.exec( + 'self.sampleRate=sampleRate=' + this.context.sampleRate + ';' + + 'self.currentTime=currentTime=' + this.context.currentTime + ); + const inputs = channelToArray(e.inputBuffer); + const outputs = channelToArray(e.outputBuffer); + this.instance.process([inputs], [outputs], parameters); + + // @todo - keepalive + // let ret = this.instance.process([inputs], [outputs], parameters); + // if (ret === true) { } +} + +function channelToArray (ch) { + const out = []; + for (let i = 0; i < ch.numberOfChannels; i++) { + out[i] = ch.getChannelData(i); + } + return out; +} + +function getProcessorsForContext (audioContext) { + return audioContext.$$processors || (audioContext.$$processors = {}); +} diff --git a/static/js/sound/polyfill/realm.js b/static/js/sound/polyfill/realm.js new file mode 100755 index 00000000..47c06282 --- /dev/null +++ b/static/js/sound/polyfill/realm.js @@ -0,0 +1,43 @@ +/** + * Copyright 2018 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +export function Realm (scope, parentElement) { + const frame = document.createElement('iframe'); + frame.style.cssText = 'position:absolute;left:0;top:-999px;width:1px;height:1px;'; + parentElement.appendChild(frame); + const win = frame.contentWindow; + const doc = win.document; + let vars = 'var window,$hook'; + for (const i in win) { + if (!(i in scope) && i !== 'eval') { + vars += ','; + vars += i; + } + } + for (const i in scope) { + vars += ','; + vars += i; + vars += '=self.'; + vars += i; + } + const script = doc.createElement('script'); + script.appendChild(doc.createTextNode( + `function $hook(self,console) {"use strict"; + ${vars};return function() {return eval(arguments[0])}}` + )); + doc.body.appendChild(script); + this.exec = win.$hook(scope, console); +} diff --git a/static/js/sound/slider.coffee b/static/js/sound/slider.coffee new file mode 100644 index 00000000..ef6ad526 --- /dev/null +++ b/static/js/sound/slider.coffee @@ -0,0 +1,91 @@ +class @Slider + constructor:(@canvas,@value=0.5,@listener)-> + @canvas.width = 40 + @canvas.height = 110 + @margin = 8 + @id = @canvas.id + @type = @canvas.dataset.type + @value = .5 + @update() + + @canvas.addEventListener "mousedown",(event)=> + @mouseDown event + + document.addEventListener "mousemove",(event)=> + @mouseMove event + + document.addEventListener "mouseup",(event)=> + @mouseUp event + + update:()-> + context = @canvas.getContext("2d") + context.clearRect(0,0,@canvas.width,@canvas.height) + + context.strokeStyle = "rgba(0,0,0,.5)" + context.lineWidth = 6 + #context.beginPath() + #context.moveTo 3,@canvas.height-@margin+1 + #context.lineTo 3,@margin-1 + #context.stroke() + + for i in [0..16] by 1 + if i%2 == 1 + op = .25 + else if (i/2)%2 == 1 + op = .25 + else if (i/4)%2 == 1 + op = .5 + else + op = 1 + + context.lineWidth = op*3 + context.strokeStyle = "rgba(255,255,255,.5)" + context.beginPath() + context.moveTo @canvas.width*.15,@margin+i*(@canvas.height-2*@margin)/16 + context.lineTo @canvas.width*.85,@margin+i*(@canvas.height-2*@margin)/16 + context.stroke() + + h = @canvas.height-2*@margin + context.clearRect @canvas.width/2-7,@margin-4,14,h+8 + context.fillStyle = "hsl(200,50%,10%)" + context.fillRoundRect @canvas.width/2-3,@margin-4,6,h+8,3 + + context.shadowBlur = 4 + context.shadowColor = "hsl(200,100%,70%)" + context.shadowOpacity = 1 + context.strokeStyle = "hsl(200,100%,70%)" + context.lineWidth = 1.5 + context.beginPath() + context.moveTo @canvas.width/2,@canvas.height-@margin + context.lineTo @canvas.width/2,@canvas.height-@margin-@value*h + context.stroke() + context.shadowBlur = context.shadowOpacity = 0 + + context.fillStyle = "#FFF" + context.shadowBlur = 4 + context.shadowColor = "#000" + context.shadowOpacity = 1 + context.fillRoundRect @canvas.width*.25,@canvas.height-@margin-h*@value-8,@canvas.width*.5,16,3 + context.shadowBlur = context.shadowOpacity = 0 + context.fillStyle = "#AAA" + context.fillRoundRect @canvas.width*.25+1,@canvas.height-@margin-h*@value+1,@canvas.width*.5-2,7,2 + context.fillStyle = "#000" + #context.fillRect @canvas.width*.3,@canvas.height-@margin-h*@value-.5,@canvas.width*.4,1 + + + mouseDown:(event)-> + @mousepressed = true + @start_y = event.clientY + @start_value = @value + + mouseMove:(event)-> + return if not @mousepressed + dy = event.clientY-@start_y + v = Math.max(0,Math.min(1,@start_value-dy*.005)) + if v != @value + @value = v + @update() + @listener.knobChange(@id,@value) + + mouseUp:(event)-> + @mousepressed = false diff --git a/static/js/sound/slider.js b/static/js/sound/slider.js new file mode 100644 index 00000000..eb283b8a --- /dev/null +++ b/static/js/sound/slider.js @@ -0,0 +1,104 @@ +this.Slider = (function() { + function Slider(canvas, value, listener) { + this.canvas = canvas; + this.value = value != null ? value : 0.5; + this.listener = listener; + this.canvas.width = 40; + this.canvas.height = 110; + this.margin = 8; + this.id = this.canvas.id; + this.type = this.canvas.dataset.type; + this.value = .5; + this.update(); + this.canvas.addEventListener("mousedown", (function(_this) { + return function(event) { + return _this.mouseDown(event); + }; + })(this)); + document.addEventListener("mousemove", (function(_this) { + return function(event) { + return _this.mouseMove(event); + }; + })(this)); + document.addEventListener("mouseup", (function(_this) { + return function(event) { + return _this.mouseUp(event); + }; + })(this)); + } + + Slider.prototype.update = function() { + var context, h, i, j, op; + context = this.canvas.getContext("2d"); + context.clearRect(0, 0, this.canvas.width, this.canvas.height); + context.strokeStyle = "rgba(0,0,0,.5)"; + context.lineWidth = 6; + for (i = j = 0; j <= 16; i = j += 1) { + if (i % 2 === 1) { + op = .25; + } else if ((i / 2) % 2 === 1) { + op = .25; + } else if ((i / 4) % 2 === 1) { + op = .5; + } else { + op = 1; + } + context.lineWidth = op * 3; + context.strokeStyle = "rgba(255,255,255,.5)"; + context.beginPath(); + context.moveTo(this.canvas.width * .15, this.margin + i * (this.canvas.height - 2 * this.margin) / 16); + context.lineTo(this.canvas.width * .85, this.margin + i * (this.canvas.height - 2 * this.margin) / 16); + context.stroke(); + } + h = this.canvas.height - 2 * this.margin; + context.clearRect(this.canvas.width / 2 - 7, this.margin - 4, 14, h + 8); + context.fillStyle = "hsl(200,50%,10%)"; + context.fillRoundRect(this.canvas.width / 2 - 3, this.margin - 4, 6, h + 8, 3); + context.shadowBlur = 4; + context.shadowColor = "hsl(200,100%,70%)"; + context.shadowOpacity = 1; + context.strokeStyle = "hsl(200,100%,70%)"; + context.lineWidth = 1.5; + context.beginPath(); + context.moveTo(this.canvas.width / 2, this.canvas.height - this.margin); + context.lineTo(this.canvas.width / 2, this.canvas.height - this.margin - this.value * h); + context.stroke(); + context.shadowBlur = context.shadowOpacity = 0; + context.fillStyle = "#FFF"; + context.shadowBlur = 4; + context.shadowColor = "#000"; + context.shadowOpacity = 1; + context.fillRoundRect(this.canvas.width * .25, this.canvas.height - this.margin - h * this.value - 8, this.canvas.width * .5, 16, 3); + context.shadowBlur = context.shadowOpacity = 0; + context.fillStyle = "#AAA"; + context.fillRoundRect(this.canvas.width * .25 + 1, this.canvas.height - this.margin - h * this.value + 1, this.canvas.width * .5 - 2, 7, 2); + return context.fillStyle = "#000"; + }; + + Slider.prototype.mouseDown = function(event) { + this.mousepressed = true; + this.start_y = event.clientY; + return this.start_value = this.value; + }; + + Slider.prototype.mouseMove = function(event) { + var dy, v; + if (!this.mousepressed) { + return; + } + dy = event.clientY - this.start_y; + v = Math.max(0, Math.min(1, this.start_value - dy * .005)); + if (v !== this.value) { + this.value = v; + this.update(); + return this.listener.knobChange(this.id, this.value); + } + }; + + Slider.prototype.mouseUp = function(event) { + return this.mousepressed = false; + }; + + return Slider; + +})(); diff --git a/static/js/sound/soundeditor.coffee b/static/js/sound/soundeditor.coffee new file mode 100644 index 00000000..ac85304d --- /dev/null +++ b/static/js/sound/soundeditor.coffee @@ -0,0 +1,70 @@ +class @SoundEditor extends Manager + constructor:(@app)-> + super(@app) + + @folder = "sounds" + @item = "sound" + @list_change_event = "soundlist" + @get_item = "getSound" + @use_thumbnails = true + @extensions = ["wav"] + @update_list = "updateSoundList" + @box_width = 96 + @box_height = 84 + + @init() + + update:()-> + super() + if @app.user? and @app.user.flags.admin + if not @synth? + @app.audio_controller.init() + @synth = new Synth @app + @synth.synth_window.show() + + openItem:(name)-> + super(name) + sound = @app.project.getSound(name) + if sound? + sound.play() + + fileDropped:(file)-> + console.info "processing #{file.name}" + reader = new FileReader() + reader.addEventListener "load",()=> + console.info "file read, size = "+ reader.result.length + return if reader.result.length>5000000 + audioContext = new AudioContext() + audioContext.decodeAudioData reader.result,(decoded)=> + console.info decoded + thumbnailer = new SoundThumbnailer(decoded,96,64) + name = file.name.split(".")[0] + name = @findNewFilename name,"getSound" + + sound = @app.project.createSound(name,thumbnailer.canvas.toDataURL(),reader.result.length) + sound.uploading = true + @setSelectedItem name + + r2 = new FileReader() + r2.addEventListener "load",()=> + sound.local_url = r2.result + data = r2.result.split(",")[1] + @app.project.addPendingChange @ + + @app.client.sendRequest { + name: "write_project_file" + project: @app.project.id + file: "sounds/#{name}.wav" + properties: {} + content: data + thumbnail: thumbnailer.canvas.toDataURL().split(",")[1] + },(msg)=> + console.info msg + @app.project.removePendingChange(@) + sound.uploading = false + @app.project.updateSoundList() + @checkNameFieldActivation() + + r2.readAsDataURL(file) + + reader.readAsArrayBuffer(file) diff --git a/static/js/sound/soundeditor.js b/static/js/sound/soundeditor.js new file mode 100644 index 00000000..ec5eccc6 --- /dev/null +++ b/static/js/sound/soundeditor.js @@ -0,0 +1,93 @@ +var extend = function(child, parent) { for (var key in parent) { if (hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor(); child.__super__ = parent.prototype; return child; }, + hasProp = {}.hasOwnProperty; + +this.SoundEditor = (function(superClass) { + extend(SoundEditor, superClass); + + function SoundEditor(app) { + this.app = app; + SoundEditor.__super__.constructor.call(this, this.app); + this.folder = "sounds"; + this.item = "sound"; + this.list_change_event = "soundlist"; + this.get_item = "getSound"; + this.use_thumbnails = true; + this.extensions = ["wav"]; + this.update_list = "updateSoundList"; + this.box_width = 96; + this.box_height = 84; + this.init(); + } + + SoundEditor.prototype.update = function() { + SoundEditor.__super__.update.call(this); + if ((this.app.user != null) && this.app.user.flags.admin) { + if (this.synth == null) { + this.app.audio_controller.init(); + this.synth = new Synth(this.app); + return this.synth.synth_window.show(); + } + } + }; + + SoundEditor.prototype.openItem = function(name) { + var sound; + SoundEditor.__super__.openItem.call(this, name); + sound = this.app.project.getSound(name); + if (sound != null) { + return sound.play(); + } + }; + + SoundEditor.prototype.fileDropped = function(file) { + var reader; + console.info("processing " + file.name); + reader = new FileReader(); + reader.addEventListener("load", (function(_this) { + return function() { + var audioContext; + console.info("file read, size = " + reader.result.length); + if (reader.result.length > 5000000) { + return; + } + audioContext = new AudioContext(); + return audioContext.decodeAudioData(reader.result, function(decoded) { + var name, r2, sound, thumbnailer; + console.info(decoded); + thumbnailer = new SoundThumbnailer(decoded, 96, 64); + name = file.name.split(".")[0]; + name = _this.findNewFilename(name, "getSound"); + sound = _this.app.project.createSound(name, thumbnailer.canvas.toDataURL(), reader.result.length); + sound.uploading = true; + _this.setSelectedItem(name); + r2 = new FileReader(); + r2.addEventListener("load", function() { + var data; + sound.local_url = r2.result; + data = r2.result.split(",")[1]; + _this.app.project.addPendingChange(_this); + return _this.app.client.sendRequest({ + name: "write_project_file", + project: _this.app.project.id, + file: "sounds/" + name + ".wav", + properties: {}, + content: data, + thumbnail: thumbnailer.canvas.toDataURL().split(",")[1] + }, function(msg) { + console.info(msg); + _this.app.project.removePendingChange(_this); + sound.uploading = false; + _this.app.project.updateSoundList(); + return _this.checkNameFieldActivation(); + }); + }); + return r2.readAsDataURL(file); + }); + }; + })(this)); + return reader.readAsArrayBuffer(file); + }; + + return SoundEditor; + +})(Manager); diff --git a/static/js/sound/soundthumbnailer.coffee b/static/js/sound/soundthumbnailer.coffee new file mode 100644 index 00000000..a5dc3da4 --- /dev/null +++ b/static/js/sound/soundthumbnailer.coffee @@ -0,0 +1,31 @@ +class @SoundThumbnailer + constructor:(@buffer,@width=128,@height=64,@color="hsl(20,80%,60%)")-> + @canvas = document.createElement "canvas" + @canvas.width = @width + @canvas.height = @height + + @channels = Math.min(2,@buffer.numberOfChannels) + @context = @canvas.getContext "2d" + @context.fillStyle = "#222" + @context.fillRect 0,0,@width,@height + @context.fillStyle = @color + + switch @channels + when 1 + @context.translate 0,@height/2 + @drawChannel @buffer.getChannelData(0) + + when 2 + @context.translate 0,@height/4 + @context.scale 1,.5 + @drawChannel @buffer.getChannelData(0) + @context.translate 0,@height + @drawChannel @buffer.getChannelData(1) + + drawChannel:(data)-> + max = 0 + for i in [0..@width-1] by .5 + d = Math.abs(data[Math.floor(data.length*i/@width)]) + max = Math.max(d,max) + @context.fillRect i,-@height/2*d,1,@height*d + console.info "max signal: #{max}" diff --git a/static/js/sound/soundthumbnailer.js b/static/js/sound/soundthumbnailer.js new file mode 100644 index 00000000..5d983ec0 --- /dev/null +++ b/static/js/sound/soundthumbnailer.js @@ -0,0 +1,42 @@ +this.SoundThumbnailer = (function() { + function SoundThumbnailer(buffer, width, height, color) { + this.buffer = buffer; + this.width = width != null ? width : 128; + this.height = height != null ? height : 64; + this.color = color != null ? color : "hsl(20,80%,60%)"; + this.canvas = document.createElement("canvas"); + this.canvas.width = this.width; + this.canvas.height = this.height; + this.channels = Math.min(2, this.buffer.numberOfChannels); + this.context = this.canvas.getContext("2d"); + this.context.fillStyle = "#222"; + this.context.fillRect(0, 0, this.width, this.height); + this.context.fillStyle = this.color; + switch (this.channels) { + case 1: + this.context.translate(0, this.height / 2); + this.drawChannel(this.buffer.getChannelData(0)); + break; + case 2: + this.context.translate(0, this.height / 4); + this.context.scale(1, .5); + this.drawChannel(this.buffer.getChannelData(0)); + this.context.translate(0, this.height); + this.drawChannel(this.buffer.getChannelData(1)); + } + } + + SoundThumbnailer.prototype.drawChannel = function(data) { + var d, i, j, max, ref; + max = 0; + for (i = j = 0, ref = this.width - 1; j <= ref; i = j += .5) { + d = Math.abs(data[Math.floor(data.length * i / this.width)]); + max = Math.max(d, max); + this.context.fillRect(i, -this.height / 2 * d, 1, this.height * d); + } + return console.info("max signal: " + max); + }; + + return SoundThumbnailer; + +})(); diff --git a/static/js/sound/synth.coffee b/static/js/sound/synth.coffee new file mode 100644 index 00000000..10165b42 --- /dev/null +++ b/static/js/sound/synth.coffee @@ -0,0 +1,456 @@ +class @Synth + constructor:(@app)-> + @synth_window = new FloatingWindow(@app,"synth-window",@,{fixed_size:true}) + + @knobs = [] + @sliders = [] + + knobs = document.getElementsByClassName("knob") + for k in knobs + @knobs.push new Knob(k,null,@) + + sliders = document.getElementsByClassName("synth-slider") + for s in sliders + @sliders.push new Slider(s,null,@) + + @waveforms = ["saw","square","sine"] #,"voice","string"] + + for i in [1..2] + for j in @waveforms + new WaveFormButton "osc"+i,j,@ + + @filtertypes = ["lowpass","bandpass","highpass"] + for f in @filtertypes + new FilterTypeButton f,@ + + @polyphony = ["poly","mono"] + + new PolyphonyButton "poly",@ + new PolyphonyButton "mono",@ + + new FilterSlopeButton @ + + @sync_button = new TextToggleButton "sync","SYNC",(s)=>@syncChanged(s) + + @lfo1_audio = new TextToggleButton "lfo1-audio","AUDIO",(s)=>@lfoAudioChanged("lfo1",s) + @lfo2_audio = new TextToggleButton "lfo2-audio","AUDIO",(s)=>@lfoAudioChanged("lfo2",s) + + @lfowaveforms = ["saw","square","sine","triangle","random","randomstep","invsaw"] + + for i in [1..2] + for j in @lfowaveforms + new WaveFormButton "lfo"+i,j,@ + + @keyboard = new SynthKeyboard document.getElementById("synth-keyboard"),5,{ + noteOn:()-> + noteOff:()-> + } + @mod_wheel = new SynthWheel document.getElementById("synth-modwheel") + @pitch_wheel = new SynthWheel document.getElementById("synth-pitchwheel") + + @velocity_outputs = [ + "OFF" + + "Mod" + + "Cutoff" + "Resonance" + + "Osc1 Mod" + "Osc1 Amp" + + "Osc2 Mod" + "Osc2 Amp" + + "Noise Amp" + "Noise Color" + + "Env1 Attack" + "Env2 Attack" + "Env2 Amt" + + "LFO1 Amt" + "LFO1 Rate" + + "LFO2 Amt" + "LFO2 Rate" + ] + + @modulation_outputs = [ + "OFF" + + "Amp" + "Mod" + + "Osc1 Amp" + "Osc1 Mod" + + "Osc2 Amp" + "Osc2 Mod" + + "Noise Amp" + "Noise Color" + + "Cutoff" + "Resonance" + + "LFO 1 Amt" + "LFO 1 Rate" + + "LFO 2 Amt" + "LFO 2 Rate" + ] + + @lfo1_outputs = [ + "OFF" + + "Pitch" + "Mod" + "Amp" + + "Osc1 Pitch" + "Osc1 Mod" + "Osc1 Amp" + + "Osc2 Pitch" + "Osc2 Mod" + "Osc2 Amp" + + "Noise Amp" + "Noise Color" + + "Cutoff" + "Resonance" + + "LFO 2 Amt" + "LFO 2 Rate" + ] + + @lfo2_outputs = [ + "OFF" + + "Pitch" + "Mod" + "Amp" + + "Osc1 Pitch" + "Osc1 Mod" + "Osc1 Amp" + + "Osc2 Pitch" + "Osc2 Mod" + "Osc2 Amp" + + "Noise Amp" + "Noise Color" + + "Cutoff" + "Resonance" + ] + + @env2_outputs = [ + "OFF" + + "Cutoff" + "Resonance" + + "Pitch" + "Mod" + "Amp" + + "Osc1 Pitch" + "Osc1 Mod" + "Osc1 Amp" + + "Osc2 Pitch" + "Osc2 Mod" + "Osc2 Amp" + + "Noise Amp" + "Noise Color" + + "LFO1 Amt" + "LFO1 Rate" + + "LFO2 Amt" + "LFO2 Rate" + ] + + @fx1_types = [ + "None" + "Distortion" + "Bit Crusher" + "Chorus" + "Flanger" + "Phaser" + "Delay" + ] + + @fx2_types = [ + "None" + "Delay" + "Reverb" + "Chorus" + "Flanger" + "Phaser" + ] + + new SynthSelector "lfo1-out",@lfo1_outputs,(index)=> + @app.audio_controller.sendParam("lfo1.out",index-1) + + new SynthSelector "lfo2-out",@lfo2_outputs,(index)=> + @app.audio_controller.sendParam("lfo2.out",index-1) + + new SynthSelector "env2-out",@env2_outputs,(index)=> + @app.audio_controller.sendParam("env2.out",index-1) + + new SynthSelector "velocity-out",@velocity_outputs,(index)=> + @app.audio_controller.sendParam("velocity.out",index-1) + + new SynthSelector "modulation-out",@modulation_outputs,(index)=> + @app.audio_controller.sendParam("modulation.out",index-1) + + new SynthSelector "fx1-type",@fx1_types,(index)=> + @app.audio_controller.sendParam("fx1.type",index-1) + + new SynthSelector "fx2-type",@fx2_types,(index)=> + @app.audio_controller.sendParam("fx2.type",index-1) + + @combine_modes = ["+","×","~"] + + @combine = 0 + document.getElementById("combine-button").addEventListener "click",()=> + @combine = (@combine+1)%@combine_modes.length + document.getElementById("combine-button").innerText = @combine_modes[@combine] + @app.audio_controller.sendParam "combine",@combine + + knobChange:(id,value)-> + @app.audio_controller.sendParam(id.replace("-","."),value) + + waveFormChange:(osc,form)-> + wf = if osc.startsWith("lfo") then @lfowaveforms else @waveforms + for f in wf + c = document.getElementById "#{osc}-#{f}" + if f == form + c.classList.add "selected" + else + c.classList.remove "selected" + + @app.audio_controller.sendParam "#{osc}.type",wf.indexOf form + + filterTypeChange:(type)-> + for t in @filtertypes + c = document.getElementById "filter-#{t}" + if t == type + c.classList.add "selected" + else + c.classList.remove "selected" + + @app.audio_controller.sendParam "filter.type",@filtertypes.indexOf type + + polyphonyTypeChange:(poly)-> + for p in @polyphony + c = document.getElementById "polyphony-#{p}" + if p == poly + c.classList.add "selected" + else + c.classList.remove "selected" + + @app.audio_controller.sendParam "polyphony",@polyphony.indexOf poly + + filterSlopeChanged:(selected)-> + document.getElementById("filter-slope").classList[if selected then "add" else "remove"]("selected") + @app.audio_controller.sendParam "filter.slope",if selected then 1 else 0 + + syncChanged:(selected)-> + document.getElementById("sync").classList[if selected then "add" else "remove"]("selected") + @app.audio_controller.sendParam "sync",if selected then 1 else 0 + + lfoAudioChanged:(lfo,selected)-> + document.getElementById("#{lfo}-audio").classList[if selected then "add" else "remove"]("selected") + @app.audio_controller.sendParam "#{lfo}.audio",if selected then 1 else 0 + +class @WaveFormButton + constructor:(@osc,@form,@listener)-> + @canvas = document.getElementById("#{@osc}-#{@form}") + @canvas.width = 30 + @canvas.height = 15 + @update() + @canvas.addEventListener "click",()=> + @listener.waveFormChange @osc,@form + + update:()-> + w = @canvas.width + h = @canvas.height + margin = 3 + wmargin = 6 + + context = @canvas.getContext "2d" + + context.beginPath() + context.strokeStyle = "#000" + context.lineWidth = 1 + + switch @form + when "saw" + context.moveTo wmargin,h-margin + context.lineTo wmargin,margin + context.lineTo w-wmargin,h-margin + context.lineTo w-wmargin,margin + + when "square" + context.moveTo wmargin,h-margin + context.lineTo wmargin,margin + context.lineTo w/2,margin + context.lineTo w/2,h-margin + context.lineTo w-wmargin,h-margin + context.lineTo w-wmargin,margin + + when "sine" + context.moveTo wmargin,h/2 + for i in [0..40] by 1 + a = i/40*Math.PI*2 + context.lineTo wmargin+(w-2*wmargin)*i/40,h/2+(h/2-margin)*Math.sin(a) + + when "triangle" + ww = w-2*wmargin + context.moveTo wmargin,h/2 + context.lineTo w/2-ww/4,margin + context.lineTo w/2+ww/4,h-margin + context.lineTo w-wmargin,h/2 + + when "random" + random = new Random(1) + context.moveTo wmargin,h/2 + for i in [0..20] by 1 + context.lineTo wmargin+(w-2*wmargin)*i/20,h/2+(h/2-margin)*(random.next()*2-1) + + when "randomstep" + random = new Random(1) + context.moveTo wmargin,h/2 + level = 0 + for i in [0..5] by 1 + context.lineTo wmargin+(w-2*wmargin)*i/5,h/2+(h/2-margin)*level + level = random.next()*2-1 + context.lineTo wmargin+(w-2*wmargin)*i/5,h/2+(h/2-margin)*level + + context.stroke() + + +class @FilterTypeButton + constructor:(@type,@listener)-> + @canvas = document.getElementById("filter-#{@type}") + @canvas.width = 30 + @canvas.height = 15 + @update() + @canvas.addEventListener "click",()=> + @listener.filterTypeChange @type + + update:()-> + w = @canvas.width + h = @canvas.height + margin = 3 + wmargin = 6 + + context = @canvas.getContext "2d" + + context.fillStyle = "#000" + context.font = "7pt Ubuntu" + context.textAlign = "center" + context.textBaseline = "middle" + context.fillText @type.substring(0,@type.indexOf("pass")).toUpperCase(),w/2,h/2 + +class @PolyphonyButton + constructor:(@type,@listener)-> + @canvas = document.getElementById("polyphony-#{@type}") + @canvas.width = 30 + @canvas.height = 15 + @update() + @canvas.addEventListener "click",()=> + @listener.polyphonyTypeChange @type + + update:()-> + w = @canvas.width + h = @canvas.height + margin = 3 + wmargin = 6 + + context = @canvas.getContext "2d" + + context.fillStyle = "#000" + context.font = "7pt Ubuntu" + context.textAlign = "center" + context.textBaseline = "middle" + context.fillText @type.toUpperCase(),w/2,h/2 + +class @FilterSlopeButton + constructor:(@listener)-> + @canvas = document.getElementById("filter-slope") + @canvas.width = 30 + @canvas.height = 15 + @update() + @selected = true + @canvas.addEventListener "click",()=> + @selected = not @selected + @listener.filterSlopeChanged @selected + + update:()-> + w = @canvas.width + h = @canvas.height + margin = 3 + wmargin = 6 + + context = @canvas.getContext "2d" + + context.fillStyle = "#000" + context.font = "7pt Ubuntu" + context.textAlign = "center" + context.textBaseline = "middle" + context.fillText "24 dB",w/2,h/2 + +class @TextToggleButton + constructor:(@id,@text,@callback)-> + @canvas = document.getElementById(@id) + @canvas.width = 30 + @canvas.height = 15 + @update() + @selected = true + @canvas.addEventListener "click",()=> + @selected = not @selected + @callback @selected + + update:()-> + w = @canvas.width + h = @canvas.height + margin = 3 + wmargin = 6 + + context = @canvas.getContext "2d" + + context.fillStyle = "#000" + context.font = "7pt Ubuntu" + context.textAlign = "center" + context.textBaseline = "middle" + context.fillText @text,w/2,h/2 + + +class @SynthSelector + constructor:(@id,@values,@callback)-> + @element = document.getElementById @id + @previous = document.querySelector "##{@id} .fa-caret-left" + @next = document.querySelector "##{@id} .fa-caret-right" + @screen = document.querySelector "##{@id} .screen" + @previous.addEventListener "click",()=>@doPrevious() + @next.addEventListener "click",()=>@doNext() + @setIndex(0) + + setIndex:(@index)-> + @screen.innerText = @values[@index] + + doPrevious:()-> + @setIndex (@index-1+@values.length)%@values.length + @callback @index + + doNext:()-> + @setIndex (@index+1)%@values.length + @callback @index diff --git a/static/js/sound/synth.js b/static/js/sound/synth.js new file mode 100644 index 00000000..64736191 --- /dev/null +++ b/static/js/sound/synth.js @@ -0,0 +1,442 @@ +this.Synth = (function() { + function Synth(app) { + var f, i, j, k, knobs, l, len, len1, len2, len3, len4, m, n, o, q, r, ref, ref1, ref2, s, sliders, u; + this.app = app; + this.synth_window = new FloatingWindow(this.app, "synth-window", this, { + fixed_size: true + }); + this.knobs = []; + this.sliders = []; + knobs = document.getElementsByClassName("knob"); + for (l = 0, len = knobs.length; l < len; l++) { + k = knobs[l]; + this.knobs.push(new Knob(k, null, this)); + } + sliders = document.getElementsByClassName("synth-slider"); + for (m = 0, len1 = sliders.length; m < len1; m++) { + s = sliders[m]; + this.sliders.push(new Slider(s, null, this)); + } + this.waveforms = ["saw", "square", "sine"]; + for (i = n = 1; n <= 2; i = ++n) { + ref = this.waveforms; + for (o = 0, len2 = ref.length; o < len2; o++) { + j = ref[o]; + new WaveFormButton("osc" + i, j, this); + } + } + this.filtertypes = ["lowpass", "bandpass", "highpass"]; + ref1 = this.filtertypes; + for (q = 0, len3 = ref1.length; q < len3; q++) { + f = ref1[q]; + new FilterTypeButton(f, this); + } + this.polyphony = ["poly", "mono"]; + new PolyphonyButton("poly", this); + new PolyphonyButton("mono", this); + new FilterSlopeButton(this); + this.sync_button = new TextToggleButton("sync", "SYNC", (function(_this) { + return function(s) { + return _this.syncChanged(s); + }; + })(this)); + this.lfo1_audio = new TextToggleButton("lfo1-audio", "AUDIO", (function(_this) { + return function(s) { + return _this.lfoAudioChanged("lfo1", s); + }; + })(this)); + this.lfo2_audio = new TextToggleButton("lfo2-audio", "AUDIO", (function(_this) { + return function(s) { + return _this.lfoAudioChanged("lfo2", s); + }; + })(this)); + this.lfowaveforms = ["saw", "square", "sine", "triangle", "random", "randomstep", "invsaw"]; + for (i = r = 1; r <= 2; i = ++r) { + ref2 = this.lfowaveforms; + for (u = 0, len4 = ref2.length; u < len4; u++) { + j = ref2[u]; + new WaveFormButton("lfo" + i, j, this); + } + } + this.keyboard = new SynthKeyboard(document.getElementById("synth-keyboard"), 5, { + noteOn: function() {}, + noteOff: function() {} + }); + this.mod_wheel = new SynthWheel(document.getElementById("synth-modwheel")); + this.pitch_wheel = new SynthWheel(document.getElementById("synth-pitchwheel")); + this.velocity_outputs = ["OFF", "Mod", "Cutoff", "Resonance", "Osc1 Mod", "Osc1 Amp", "Osc2 Mod", "Osc2 Amp", "Noise Amp", "Noise Color", "Env1 Attack", "Env2 Attack", "Env2 Amt", "LFO1 Amt", "LFO1 Rate", "LFO2 Amt", "LFO2 Rate"]; + this.modulation_outputs = ["OFF", "Amp", "Mod", "Osc1 Amp", "Osc1 Mod", "Osc2 Amp", "Osc2 Mod", "Noise Amp", "Noise Color", "Cutoff", "Resonance", "LFO 1 Amt", "LFO 1 Rate", "LFO 2 Amt", "LFO 2 Rate"]; + this.lfo1_outputs = ["OFF", "Pitch", "Mod", "Amp", "Osc1 Pitch", "Osc1 Mod", "Osc1 Amp", "Osc2 Pitch", "Osc2 Mod", "Osc2 Amp", "Noise Amp", "Noise Color", "Cutoff", "Resonance", "LFO 2 Amt", "LFO 2 Rate"]; + this.lfo2_outputs = ["OFF", "Pitch", "Mod", "Amp", "Osc1 Pitch", "Osc1 Mod", "Osc1 Amp", "Osc2 Pitch", "Osc2 Mod", "Osc2 Amp", "Noise Amp", "Noise Color", "Cutoff", "Resonance"]; + this.env2_outputs = ["OFF", "Cutoff", "Resonance", "Pitch", "Mod", "Amp", "Osc1 Pitch", "Osc1 Mod", "Osc1 Amp", "Osc2 Pitch", "Osc2 Mod", "Osc2 Amp", "Noise Amp", "Noise Color", "LFO1 Amt", "LFO1 Rate", "LFO2 Amt", "LFO2 Rate"]; + this.fx1_types = ["None", "Distortion", "Bit Crusher", "Chorus", "Flanger", "Phaser", "Delay"]; + this.fx2_types = ["None", "Delay", "Reverb", "Chorus", "Flanger", "Phaser"]; + new SynthSelector("lfo1-out", this.lfo1_outputs, (function(_this) { + return function(index) { + return _this.app.audio_controller.sendParam("lfo1.out", index - 1); + }; + })(this)); + new SynthSelector("lfo2-out", this.lfo2_outputs, (function(_this) { + return function(index) { + return _this.app.audio_controller.sendParam("lfo2.out", index - 1); + }; + })(this)); + new SynthSelector("env2-out", this.env2_outputs, (function(_this) { + return function(index) { + return _this.app.audio_controller.sendParam("env2.out", index - 1); + }; + })(this)); + new SynthSelector("velocity-out", this.velocity_outputs, (function(_this) { + return function(index) { + return _this.app.audio_controller.sendParam("velocity.out", index - 1); + }; + })(this)); + new SynthSelector("modulation-out", this.modulation_outputs, (function(_this) { + return function(index) { + return _this.app.audio_controller.sendParam("modulation.out", index - 1); + }; + })(this)); + new SynthSelector("fx1-type", this.fx1_types, (function(_this) { + return function(index) { + return _this.app.audio_controller.sendParam("fx1.type", index - 1); + }; + })(this)); + new SynthSelector("fx2-type", this.fx2_types, (function(_this) { + return function(index) { + return _this.app.audio_controller.sendParam("fx2.type", index - 1); + }; + })(this)); + this.combine_modes = ["+", "×", "~"]; + this.combine = 0; + document.getElementById("combine-button").addEventListener("click", (function(_this) { + return function() { + _this.combine = (_this.combine + 1) % _this.combine_modes.length; + document.getElementById("combine-button").innerText = _this.combine_modes[_this.combine]; + return _this.app.audio_controller.sendParam("combine", _this.combine); + }; + })(this)); + } + + Synth.prototype.knobChange = function(id, value) { + return this.app.audio_controller.sendParam(id.replace("-", "."), value); + }; + + Synth.prototype.waveFormChange = function(osc, form) { + var c, f, l, len, wf; + wf = osc.startsWith("lfo") ? this.lfowaveforms : this.waveforms; + for (l = 0, len = wf.length; l < len; l++) { + f = wf[l]; + c = document.getElementById(osc + "-" + f); + if (f === form) { + c.classList.add("selected"); + } else { + c.classList.remove("selected"); + } + } + return this.app.audio_controller.sendParam(osc + ".type", wf.indexOf(form)); + }; + + Synth.prototype.filterTypeChange = function(type) { + var c, l, len, ref, t; + ref = this.filtertypes; + for (l = 0, len = ref.length; l < len; l++) { + t = ref[l]; + c = document.getElementById("filter-" + t); + if (t === type) { + c.classList.add("selected"); + } else { + c.classList.remove("selected"); + } + } + return this.app.audio_controller.sendParam("filter.type", this.filtertypes.indexOf(type)); + }; + + Synth.prototype.polyphonyTypeChange = function(poly) { + var c, l, len, p, ref; + ref = this.polyphony; + for (l = 0, len = ref.length; l < len; l++) { + p = ref[l]; + c = document.getElementById("polyphony-" + p); + if (p === poly) { + c.classList.add("selected"); + } else { + c.classList.remove("selected"); + } + } + return this.app.audio_controller.sendParam("polyphony", this.polyphony.indexOf(poly)); + }; + + Synth.prototype.filterSlopeChanged = function(selected) { + document.getElementById("filter-slope").classList[selected ? "add" : "remove"]("selected"); + return this.app.audio_controller.sendParam("filter.slope", selected ? 1 : 0); + }; + + Synth.prototype.syncChanged = function(selected) { + document.getElementById("sync").classList[selected ? "add" : "remove"]("selected"); + return this.app.audio_controller.sendParam("sync", selected ? 1 : 0); + }; + + Synth.prototype.lfoAudioChanged = function(lfo, selected) { + document.getElementById(lfo + "-audio").classList[selected ? "add" : "remove"]("selected"); + return this.app.audio_controller.sendParam(lfo + ".audio", selected ? 1 : 0); + }; + + return Synth; + +})(); + +this.WaveFormButton = (function() { + function WaveFormButton(osc1, form1, listener) { + this.osc = osc1; + this.form = form1; + this.listener = listener; + this.canvas = document.getElementById(this.osc + "-" + this.form); + this.canvas.width = 30; + this.canvas.height = 15; + this.update(); + this.canvas.addEventListener("click", (function(_this) { + return function() { + return _this.listener.waveFormChange(_this.osc, _this.form); + }; + })(this)); + } + + WaveFormButton.prototype.update = function() { + var a, context, h, i, l, level, m, margin, n, random, w, wmargin, ww; + w = this.canvas.width; + h = this.canvas.height; + margin = 3; + wmargin = 6; + context = this.canvas.getContext("2d"); + context.beginPath(); + context.strokeStyle = "#000"; + context.lineWidth = 1; + switch (this.form) { + case "saw": + context.moveTo(wmargin, h - margin); + context.lineTo(wmargin, margin); + context.lineTo(w - wmargin, h - margin); + context.lineTo(w - wmargin, margin); + break; + case "square": + context.moveTo(wmargin, h - margin); + context.lineTo(wmargin, margin); + context.lineTo(w / 2, margin); + context.lineTo(w / 2, h - margin); + context.lineTo(w - wmargin, h - margin); + context.lineTo(w - wmargin, margin); + break; + case "sine": + context.moveTo(wmargin, h / 2); + for (i = l = 0; l <= 40; i = l += 1) { + a = i / 40 * Math.PI * 2; + context.lineTo(wmargin + (w - 2 * wmargin) * i / 40, h / 2 + (h / 2 - margin) * Math.sin(a)); + } + break; + case "triangle": + ww = w - 2 * wmargin; + context.moveTo(wmargin, h / 2); + context.lineTo(w / 2 - ww / 4, margin); + context.lineTo(w / 2 + ww / 4, h - margin); + context.lineTo(w - wmargin, h / 2); + break; + case "random": + random = new Random(1); + context.moveTo(wmargin, h / 2); + for (i = m = 0; m <= 20; i = m += 1) { + context.lineTo(wmargin + (w - 2 * wmargin) * i / 20, h / 2 + (h / 2 - margin) * (random.next() * 2 - 1)); + } + break; + case "randomstep": + random = new Random(1); + context.moveTo(wmargin, h / 2); + level = 0; + for (i = n = 0; n <= 5; i = n += 1) { + context.lineTo(wmargin + (w - 2 * wmargin) * i / 5, h / 2 + (h / 2 - margin) * level); + level = random.next() * 2 - 1; + context.lineTo(wmargin + (w - 2 * wmargin) * i / 5, h / 2 + (h / 2 - margin) * level); + } + } + return context.stroke(); + }; + + return WaveFormButton; + +})(); + +this.FilterTypeButton = (function() { + function FilterTypeButton(type1, listener) { + this.type = type1; + this.listener = listener; + this.canvas = document.getElementById("filter-" + this.type); + this.canvas.width = 30; + this.canvas.height = 15; + this.update(); + this.canvas.addEventListener("click", (function(_this) { + return function() { + return _this.listener.filterTypeChange(_this.type); + }; + })(this)); + } + + FilterTypeButton.prototype.update = function() { + var context, h, margin, w, wmargin; + w = this.canvas.width; + h = this.canvas.height; + margin = 3; + wmargin = 6; + context = this.canvas.getContext("2d"); + context.fillStyle = "#000"; + context.font = "7pt Ubuntu"; + context.textAlign = "center"; + context.textBaseline = "middle"; + return context.fillText(this.type.substring(0, this.type.indexOf("pass")).toUpperCase(), w / 2, h / 2); + }; + + return FilterTypeButton; + +})(); + +this.PolyphonyButton = (function() { + function PolyphonyButton(type1, listener) { + this.type = type1; + this.listener = listener; + this.canvas = document.getElementById("polyphony-" + this.type); + this.canvas.width = 30; + this.canvas.height = 15; + this.update(); + this.canvas.addEventListener("click", (function(_this) { + return function() { + return _this.listener.polyphonyTypeChange(_this.type); + }; + })(this)); + } + + PolyphonyButton.prototype.update = function() { + var context, h, margin, w, wmargin; + w = this.canvas.width; + h = this.canvas.height; + margin = 3; + wmargin = 6; + context = this.canvas.getContext("2d"); + context.fillStyle = "#000"; + context.font = "7pt Ubuntu"; + context.textAlign = "center"; + context.textBaseline = "middle"; + return context.fillText(this.type.toUpperCase(), w / 2, h / 2); + }; + + return PolyphonyButton; + +})(); + +this.FilterSlopeButton = (function() { + function FilterSlopeButton(listener) { + this.listener = listener; + this.canvas = document.getElementById("filter-slope"); + this.canvas.width = 30; + this.canvas.height = 15; + this.update(); + this.selected = true; + this.canvas.addEventListener("click", (function(_this) { + return function() { + _this.selected = !_this.selected; + return _this.listener.filterSlopeChanged(_this.selected); + }; + })(this)); + } + + FilterSlopeButton.prototype.update = function() { + var context, h, margin, w, wmargin; + w = this.canvas.width; + h = this.canvas.height; + margin = 3; + wmargin = 6; + context = this.canvas.getContext("2d"); + context.fillStyle = "#000"; + context.font = "7pt Ubuntu"; + context.textAlign = "center"; + context.textBaseline = "middle"; + return context.fillText("24 dB", w / 2, h / 2); + }; + + return FilterSlopeButton; + +})(); + +this.TextToggleButton = (function() { + function TextToggleButton(id1, text, callback) { + this.id = id1; + this.text = text; + this.callback = callback; + this.canvas = document.getElementById(this.id); + this.canvas.width = 30; + this.canvas.height = 15; + this.update(); + this.selected = true; + this.canvas.addEventListener("click", (function(_this) { + return function() { + _this.selected = !_this.selected; + return _this.callback(_this.selected); + }; + })(this)); + } + + TextToggleButton.prototype.update = function() { + var context, h, margin, w, wmargin; + w = this.canvas.width; + h = this.canvas.height; + margin = 3; + wmargin = 6; + context = this.canvas.getContext("2d"); + context.fillStyle = "#000"; + context.font = "7pt Ubuntu"; + context.textAlign = "center"; + context.textBaseline = "middle"; + return context.fillText(this.text, w / 2, h / 2); + }; + + return TextToggleButton; + +})(); + +this.SynthSelector = (function() { + function SynthSelector(id1, values, callback) { + this.id = id1; + this.values = values; + this.callback = callback; + this.element = document.getElementById(this.id); + this.previous = document.querySelector("#" + this.id + " .fa-caret-left"); + this.next = document.querySelector("#" + this.id + " .fa-caret-right"); + this.screen = document.querySelector("#" + this.id + " .screen"); + this.previous.addEventListener("click", (function(_this) { + return function() { + return _this.doPrevious(); + }; + })(this)); + this.next.addEventListener("click", (function(_this) { + return function() { + return _this.doNext(); + }; + })(this)); + this.setIndex(0); + } + + SynthSelector.prototype.setIndex = function(index1) { + this.index = index1; + return this.screen.innerText = this.values[this.index]; + }; + + SynthSelector.prototype.doPrevious = function() { + this.setIndex((this.index - 1 + this.values.length) % this.values.length); + return this.callback(this.index); + }; + + SynthSelector.prototype.doNext = function() { + this.setIndex((this.index + 1) % this.values.length); + return this.callback(this.index); + }; + + return SynthSelector; + +})(); diff --git a/static/js/sound/synth/analog.coffee b/static/js/sound/synth/analog.coffee new file mode 100644 index 00000000..48428af7 --- /dev/null +++ b/static/js/sound/synth/analog.coffee @@ -0,0 +1,59 @@ +class @Analog + constructor:()-> + @osc1_wave = 0 + @osc1_pw = 0 + @osc1_pitch = 0 + @osc1_tune = 0 + @osc1_amp = 0 + + @osc2_wave = 0 + @osc2_pw = 0 + @osc2_pitch = 0 + @osc2_tune = 0 + @osc2_amp = 0 + + @noise_type = 0 + @noise_amp = 0 + + @filter_type = 0 + @filter_cutoff = 0 + @filter_resonance = 0 + + @lfo1_wave = 0 + @lfo1_rate = 0 + @lfo1_out1 = 0 + @lfo1_out2 = 0 + @lfo1_out1_target = 0 + @lfo1_out2_target = 0 + + @lfo2_wave = 0 + @lfo2_rate = 0 + @lfo2_out1 = 0 + @lfo2_out2 = 0 + @lfo2_out1_target = 0 + @lfo2_out2_target = 0 + + @env1_a = 0 + @env1_d = 0 + @env1_s = 0 + @env1_r = 0 + @env1_out1 = 0 + @env1_out2 = 0 + + @env2_a = 0 + @env2_d = 0 + @env2_s = 0 + @env2_r = 0 + @env2_out1 = 0 + @env2_out2 = 0 + + @glide = 0 + + # modulation sources + # * keyboard + # * velocity + # * mod wheel + # * envelope1 + # * envelope2 + # * lfo1 + # * lfo2 diff --git a/static/js/sound/synth/analog.js b/static/js/sound/synth/analog.js new file mode 100644 index 00000000..9ca51158 --- /dev/null +++ b/static/js/sound/synth/analog.js @@ -0,0 +1,47 @@ +this.Analog = (function() { + function Analog() { + this.osc1_wave = 0; + this.osc1_pw = 0; + this.osc1_pitch = 0; + this.osc1_tune = 0; + this.osc1_amp = 0; + this.osc2_wave = 0; + this.osc2_pw = 0; + this.osc2_pitch = 0; + this.osc2_tune = 0; + this.osc2_amp = 0; + this.noise_type = 0; + this.noise_amp = 0; + this.filter_type = 0; + this.filter_cutoff = 0; + this.filter_resonance = 0; + this.lfo1_wave = 0; + this.lfo1_rate = 0; + this.lfo1_out1 = 0; + this.lfo1_out2 = 0; + this.lfo1_out1_target = 0; + this.lfo1_out2_target = 0; + this.lfo2_wave = 0; + this.lfo2_rate = 0; + this.lfo2_out1 = 0; + this.lfo2_out2 = 0; + this.lfo2_out1_target = 0; + this.lfo2_out2_target = 0; + this.env1_a = 0; + this.env1_d = 0; + this.env1_s = 0; + this.env1_r = 0; + this.env1_out1 = 0; + this.env1_out2 = 0; + this.env2_a = 0; + this.env2_d = 0; + this.env2_s = 0; + this.env2_r = 0; + this.env2_out1 = 0; + this.env2_out2 = 0; + this.glide = 0; + } + + return Analog; + +})(); diff --git a/static/js/sound/synth/audioengine.coffee b/static/js/sound/synth/audioengine.coffee new file mode 100644 index 00000000..138c8c19 --- /dev/null +++ b/static/js/sound/synth/audioengine.coffee @@ -0,0 +1,2107 @@ +` +const TWOPI = 2*Math.PI +const SIN_TABLE = new Float64Array(10001) +const WHITE_NOISE = new Float64Array(100000) +const COLORED_NOISE = new Float64Array(100000) +` +do -> + for i in [0..10000] by 1 + SIN_TABLE[i] = Math.sin(i/10000*Math.PI*2) + +` +const BLIP_SIZE = 512 +const BLIP = new Float64Array(BLIP_SIZE+1) +` + +do -> + for p in [1..31] by 2 + for i in [0..BLIP_SIZE] by 1 + x = (i/BLIP_SIZE-.5)*.5 + BLIP[i] += Math.sin(x*2*Math.PI*p)/p + + norm = BLIP[BLIP_SIZE] + + for i in [0..BLIP_SIZE] by 1 + BLIP[i] /= norm + +do -> + n = 0 + b0 = b1 = b2 = b3 = b4 = b5 = b6 = 0 + for i in [0..99999] by 1 + white = Math.random()*2-1 + + n = .99*n+.01*white + + pink = n*6 + + WHITE_NOISE[i] = white + COLORED_NOISE[i] = pink + +DBSCALE = (value,range)-> (Math.exp(value*range)/Math.exp(range)-1/Math.exp(range))/(1-1/Math.exp(range)) + +class SquareOscillator + constructor:(@voice,osc)-> + @invSampleRate = 1/@voice.engine.sampleRate + @tune = 1 + @buffer = new Float64Array(32) + @init(osc) if osc? + + init:(osc,@sync)-> + @phase = if @sync then 0 else Math.random() + @sig = -1 + @index = 0 + @update(osc) + for i in [0..@buffer.length-1] + @buffer[i] = 0 + return + + update:(osc,@sync)-> + c = Math.round(osc.coarse*48)/48 + fine = osc.tune*2-1 + fine = fine*(1+fine*fine)*.5 + @tune = 1*Math.pow(2,c*4)*.25*Math.pow(Math.pow(2,1/12),fine) + @analog_tune = @tune + + process:(freq,mod)-> + dp = @analog_tune*freq*@invSampleRate + @phase += dp + + m = .5-mod*.49 + avg = 1-2*m + + if @sig<0 + if @phase>=m + @sig = 1 + + dp = Math.max(0,Math.min(1,(@phase-m)/dp)) + dp *= 16 + dpi = Math.floor(dp) + a = dp-dpi + index = @index + + for i in [0..31] by 1 + break if dpi>=512 + @buffer[index] += -1+BLIP[dpi]*(1-a)+BLIP[dpi+1]*a + dpi += 16 + index = (index+1)%@buffer.length + else + if @phase>=1 + dp = Math.max(0,Math.min(1,(@phase-1)/dp)) + dp *= 16 + dpi = Math.floor(dp) + a = dp-dpi + index = @index + + @sig = -1 + @phase -= 1 + @analog_tune = if @sync then @tune else @tune*(1+(Math.random()-.5)*.002) + + for i in [0..31] by 1 + break if dpi>=512 + @buffer[index] += 1-BLIP[dpi]*(1-a)-BLIP[dpi+1]*a + dpi += 16 + index = (index+1)%@buffer.length + + sig = @sig+@buffer[@index] + @buffer[@index] = 0 + @index = (@index+1)%@buffer.length + sig-avg + +class SawOscillator + constructor:(@voice,osc)-> + @invSampleRate = 1/@voice.engine.sampleRate + @tune = 1 + @buffer = new Float64Array(32) + @init(osc) if osc? + + init:(osc,@sync)-> + @phase = if @sync then 0 else Math.random() + @sig = -1 + @index = 0 + @jumped = false + @update(osc) + for i in [0..@buffer.length-1] + @buffer[i] = 0 + return + + update:(osc,@sync)-> + c = Math.round(osc.coarse*48)/48 + fine = osc.tune*2-1 + fine = fine*(1+fine*fine)*.5 + @tune = 1*Math.pow(2,c*4)*.25*Math.pow(Math.pow(2,1/12),fine) + @analog_tune = @tune + + process:(freq,mod)-> + dphase = @analog_tune*freq*@invSampleRate + @phase += dphase + + #if @phase>=1 + # @phase -= 1 + #return 1-2*@phase + + slope = 1+mod + + if not @jumped + sig = 1-2*@phase*slope + if @phase>=.5 + @jumped = true + sig = mod-2*(@phase-.5)*slope + + dp = Math.max(0,Math.min(1,(@phase-.5)/dphase)) + dp *= 16 + dpi = Math.floor(dp) + a = dp-dpi + index = @index + + if mod>0 + for i in [0..31] by 1 + break if dpi>=512 + @buffer[index] += (-1+BLIP[dpi]*(1-a)+BLIP[dpi+1]*a)*mod + dpi += 16 + index = (index+1)%@buffer.length + else + sig = mod-2*(@phase-.5)*slope + if @phase>=1 + @jumped = false + + dp = Math.max(0,Math.min(1,(@phase-1)/dphase)) + dp *= 16 + dpi = Math.floor(dp) + a = dp-dpi + index = @index + + @phase -= 1 + sig = 1-2*@phase*slope + @analog_tune = if @sync then @tune else @tune*(1+(Math.random()-.5)*.002) + + for i in [0..31] by 1 + break if dpi>=512 + @buffer[index] += -1+BLIP[dpi]*(1-a)+BLIP[dpi+1]*a + dpi += 16 + index = (index+1)%@buffer.length + + sig += @buffer[@index] + @buffer[@index] = 0 + @index = (@index+1)%@buffer.length + offset = 16*2*dphase*slope + sig+offset + +class SineOscillator + constructor:(@voice,osc)-> + @sampleRate = @voice.engine.sampleRate + @invSampleRate = 1/@voice.engine.sampleRate + @maxRatio = @sampleRate/Math.PI/5/(2*Math.PI) + @tune = 1 + @init(osc) if osc? + + init:(osc,@sync)-> + @phase = if @sync then .25 else Math.random() + @update(osc) + + update:(osc,@sync)-> + c = Math.round(osc.coarse*48)/48 + fine = osc.tune*2-1 + fine = fine*(1+fine*fine)*.5 + @tune = 1*Math.pow(2,c*4)*.25*Math.pow(Math.pow(2,1/12),fine) + @modnorm = 25/Math.sqrt(@tune*@voice.freq) + @dphase = @tune*@invSampleRate + + sinWave:(x)-> + x = (x-Math.floor(x))*10000 + ix = Math.floor(x) + ax = x-ix + SIN_TABLE[ix]*(1-ax)+SIN_TABLE[ix+1]*ax + + sinWave2:(x)-> + x = 2*(x-Math.floor(x)) + if x>1 + x = 2-x + x*x*(3-2*x)*2-1 + + process:(freq,mod)-> + @phase = (@phase+freq*@dphase) + + if @phase>=1 + @phase -= 1 + @analog_tune = if @sync then @tune else @tune*(1+(Math.random()-.5)*.002) + @dphase = @analog_tune*@invSampleRate + + m1 = mod*@modnorm + m2 = mod*m1 + p = @phase + return @sinWave2(p+m1*@sinWave2(p+m2*@sinWave2(p))) + +class VoiceOscillator + constructor:(@voice,osc)-> + @sampleRate = @voice.engine.sampleRate + @invSampleRate = 1/@voice.engine.sampleRate + @tune = 1 + @init(osc) if osc? + + @f1 = [320,500,700,1000,500,320,700,500,320,320] + @f2 = [800,1000,1150,1400,1500,1650,1800,2300,3200,3200] + + init:(osc)-> + @phase = 0 + @grain1_p1 = .25 + @grain1_p2 = .25 + @grain2_p1 = .25 + @grain2_p2 = .25 + @update(osc) + + update:(osc)-> + c = Math.round(osc.coarse*48)/48 + fine = osc.tune*2-1 + fine = fine*(1+fine*fine)*.5 + @tune = 1*Math.pow(2,c*4)*.25*Math.pow(Math.pow(2,1/12),fine) + @modnorm = 25/Math.sqrt(@tune*@voice.freq) + @dphase = @tune*@invSampleRate + + sinWave2:(x)-> + x = 2*(x-Math.floor(x)) + if x>1 + x = 2-x + x*x*(3-2*x)*2-1 + + process:(freq,mod)-> + p1 = @phase<1 + @phase = (@phase+freq*@dphase) + + m = mod*(@f1.length-2) + im = Math.floor(m) + am = m-im + + f1 = @f1[im]*(1-am)+@f1[im+1]*am + f2 = @f2[im]*(1-am)+@f2[im+1]*am + + if p1 and @phase>=1 + @grain2_p1 = .25 + @grain2_p2 = .25 + + if @phase>=2 + @phase -= 2 + @grain1_p1 = .25 + @grain1_p2 = .25 + + x = @phase-1 + x *= x*x + vol = 1-Math.abs(1-@phase) + #vol = (@sinWave2(x*.5+.5)+1)*.5 + + sig = vol*(@sinWave2(@grain1_p1)*.25+.125*@sinWave2(@grain1_p2)) + + @grain1_p1 += f1*@invSampleRate + @grain1_p2 += f2*@invSampleRate + + x = ((@phase+1)%2)-1 + x *= x*x + #vol = (@sinWave2(x*.5+.5)+1)*.5 + vol = if @phase<1 then 1-@phase else @phase-1 + + sig += vol*(@sinWave2(@grain2_p1)*.25+.125*@sinWave2(@grain2_p2)) + + sig += @sinWave2(@phase+.25) + + @grain2_p1 += f1*@invSampleRate + @grain2_p2 += f2*@invSampleRate + + sig + +class StringOscillator + constructor:(@voice,osc)-> + @sampleRate = @voice.engine.sampleRate + @invSampleRate = 1/@voice.engine.sampleRate + @maxRatio = @sampleRate/Math.PI/5/(2*Math.PI) + @tune = 1 + @init(osc) if osc? + + init:(osc)-> + @index = 0 + @buffer = new Float64Array(@sampleRate/10) + @update(osc) + @prev = 0 + @power = 1 + + update:(osc)-> + c = Math.round(osc.coarse*48)/48 + fine = osc.tune*2-1 + fine = fine*(1+fine*fine)*.5 + @tune = 1*Math.pow(2,c*4)*.25*Math.pow(Math.pow(2,1/12),fine) + + process:(freq,mod)-> + period = @sampleRate/(freq*@tune) + x = (@index-period+@buffer.length)%@buffer.length + ix = Math.floor(x) + a = x-ix + reflection = @buffer[ix]*(1-a)+@buffer[(ix+1)%@buffer.length]*a + + m = Math.exp(Math.log(0.99)/freq) + + m = 1 + r = reflection*m+@prev*(1-m) + @prev = r + + n = Math.random()*2-1 + + + m = mod*.5 + m = m*m*.999+.001 + sig = n*m+r + + @power = Math.max(Math.abs(sig)*.1+@power*.9,@power*.999) + sig /= (@power+.0001) + + @buffer[@index] = sig + @index += 1 + @index = 0 if @index>=@buffer.length + sig + + processOld:(freq,mod)-> + period = @sampleRate/(freq*@tune) + x = (@index-period+@buffer.length)%@buffer.length + ix = Math.floor(x) + a = x-ix + reflection = @buffer[ix]*(1-a)+@buffer[(ix+1)%@buffer.length]*a + + #m = .1+Math.exp(-period*mod*.01)*.89 + m = mod + r = reflection*m+@prev*(1-m) + @prev = r + + sig = (Math.random()*2-1)*.01+r*.99 + @buffer[@index] = sig/@power + @power = Math.abs(sig)*.001+@power*.999 + @index += 1 + @index = 0 if @index>=@buffer.length + sig + +class Noise + constructor:(@voice)-> + + init:()-> + @phase = 0 + @seed = 1382 + @n = 0 + + process:(mod)-> + @seed = (@seed*13907+12345)&0x7FFFFFFF + white = (@seed/0x80000000)*2-1 + @n = @n*.99+white*.01 + pink = @n*6 + white*(1-mod)+pink*mod + +class ModEnvelope + constructor:(@voice)-> + @sampleRate = @voice.engine.sampleRate + + init:(@params)-> + @phase = 0 + @update() + + update:(a = @params.a)-> + @a = 1/(@sampleRate*20*Math.pow(a,3)+1) + @d = 1/(@sampleRate*20*Math.pow(@params.d,3)+1) + @s = @params.s + @r = 1/(@sampleRate*20*Math.pow(@params.r,3)+1) + + process:(noteon)-> + if @phase<1 + sig = @sig = @phase = Math.min(1,@phase+@a) + else if @phase<2 + @phase = Math.min(2,@phase+@d) + sig = @sig = 1-(@phase-1)*(1-@s) + else if @phase<3 + sig = @sig = @s + else + @phase = @phase+@r + sig = Math.max(0,@sig*(1-(@phase-3))) + + if @phase<3 and not noteon + @phase = 3 + + sig + +class AmpEnvelope + constructor:(@voice)-> + @sampleRate = @voice.engine.sampleRate + @sig = 0 + + init:(@params)-> + @phase = 0 + @sig = 0 + @update() + + update:(a = @params.a)-> + #@a = Math.exp(Math.log(1/@epsilon)/(@sampleRate*10*Math.pow(@params.a,3)+1)) + @a2 = 1/(@sampleRate*(20*Math.pow(a,3)+.00025)) + @d = Math.exp(Math.log(0.5)/(@sampleRate*(10*Math.pow(@params.d,3)+0.001))) + @s = DBSCALE(@params.s,2) + console.info("sustain #{@s}") + @r = Math.exp(Math.log(0.5)/(@sampleRate*(10*Math.pow(@params.r,3)+0.001))) + + process:(noteon)-> + if @phase<1 + @phase += @a2 + sig = @sig = (@phase*.75+.25)*@phase + + if @phase>=1 + sig = @sig = 1 + @phase = 1 + else if @phase==1 + sig = @sig = @sig*@d + if sig <= @s + sig = @sig = @s + @phase = 2 + else if @phase<3 + sig = @sig = @s + else + sig = @sig = @sig*@r + + if @phase<3 and not noteon + @phase = 3 + + sig + +class LFO + constructor:(@voice)-> + @invSampleRate = 1/@voice.engine.sampleRate + + init:(@params,sync)-> + if sync + rate = @params.rate + rate = .1+rate*rate*rate*(100-.1) + t = @voice.engine.getTime()*rate + @phase = t%1 + else + @phase = Math.random() + @process = @processSine + @update() + @r1 = Math.random()*2-1 + @r2 = Math.random()*2-1 + @out = 0 + + update:()-> + switch @params.type + when 0 then @process = @processSaw + when 1 then @process = @processSquare + when 2 then @process = @processSine + when 3 then @process = @processTriangle + when 4 then @process = @processRandom + when 5 then @process = @processRandomStep + + @audio_freq = 440*Math.pow(Math.pow(2,1/12),@voice.key-57) + + processSine:(rate)-> + if @params.audio + r = if rate<.5 then .25+rate*rate/.25*.75 else rate*rate*4 + #r = Math.pow(2,(Math.round(rate*48))/12)*.25 + rate = @audio_freq*r + else + rate = .01+rate*rate*20 + + @phase = (@phase+rate*@invSampleRate) + @phase -= 1 if @phase>=1 + p = @phase*2 + if p<1 + p*p*(3-2*p)*2-1 + else + p -= 1 + 1-p*p*(3-2*p)*2 + + processTriangle:(rate)-> + rate *= rate + rate *= rate + rate = .05+rate*rate*10000 + @phase = (@phase+rate*@invSampleRate) + @phase -= 1 if @phase>=1 + return (1-4*Math.abs(@phase-.5)) + + processSaw:(rate)-> + rate *= rate + rate *= rate + rate = .05+rate*rate*10000 + @phase = (@phase+rate*@invSampleRate) + @phase -= 1 if @phase>=1 + out = (1-@phase*2) + @out = @out*.97+out*.03 + + processSquare:(rate)-> + rate *= rate + rate *= rate + rate = .05+rate*rate*10000 + @phase = (@phase+rate*@invSampleRate) + @phase -= 1 if @phase>=1 + out = if @phase<.5 then 1 else -1 + @out = @out*.97+out*.03 + + processRandom:(rate)-> + rate *= rate + rate *= rate + rate = .05+rate*rate*10000 + @phase = (@phase+rate*@invSampleRate) + if @phase>=1 + @phase -= 1 + @r1 = @r2 + @r2 = Math.random()*2-1 + + @r1*(1-@phase)+@r2*@phase + + processRandomStep:(rate)-> + rate *= rate + rate *= rate + rate = .05+rate*rate*10000 + @phase = (@phase+rate*@invSampleRate) + if @phase>=1 + @phase -= 1 + @r1 = Math.random()*2-1 + @out = @out*.97+@r1*.03 + + processDiscreteRandom:()-> + + processSmoothRandom:()-> + +class Filter + constructor:(@voice)-> + @sampleRate = @voice.engine.sampleRate + @invSampleRate = 1/@voice.engine.sampleRate + @halfSampleRate = @sampleRate*.5 + + init:(@layer)-> + @fm00 = 0 + @fm01 = 0 + @fm10 = 0 + @fm11 = 0 + + @update() + + update:()-> + switch @layer.inputs.filter.type + when 0 + @process = @processLowPass + when 1 + @process = @processBandPass + when 2 + @process = @processHighPass + + processHighPass:(sig,cutoff,q)-> + w0 = Math.max(0,Math.min(@halfSampleRate,cutoff))*@invSampleRate + w0 *= 10000 + iw0 = Math.floor(w0) + aw0 = w0-iw0 + cosw0 = (1-aw0)*SIN_TABLE[iw0+2500]+aw0*SIN_TABLE[iw0+2501] + sinw0 = (1-aw0)*SIN_TABLE[iw0]+aw0*SIN_TABLE[iw0+1] + + alpha = sinw0 / (2 * q) + invOnePlusAlpha = 1/(1 + alpha) + + a0 = (-2 * cosw0) * invOnePlusAlpha + a1 = (1 - alpha) * invOnePlusAlpha + + onePlusCosw0 = 1 + cosw0 + b0 = onePlusCosw0*.5* invOnePlusAlpha + b1 = -onePlusCosw0* invOnePlusAlpha + b2 = b0 + + w = sig - a0*@fm00 - a1*@fm01 + sig = b0*w + b1*@fm00 + b2*@fm01 + + @fm01 = @fm00 + @fm00 = w + + if @layer.inputs.filter.slope + w = sig - a0*@fm10 - a1*@fm11 + sig = b0*w + b1*@fm10 + b2*@fm11 + + @fm11 = @fm10 + @fm10 = w + + sig + + processBandPass:(sig,cutoff,q)-> + w0 = Math.max(0,Math.min(@halfSampleRate,cutoff))*@invSampleRate + w0 *= 10000 + iw0 = Math.floor(w0) + aw0 = w0-iw0 + cosw0 = (1-aw0)*SIN_TABLE[iw0+2500]+aw0*SIN_TABLE[iw0+2501] + sinw0 = (1-aw0)*SIN_TABLE[iw0]+aw0*SIN_TABLE[iw0+1] + + alpha = sinw0 / (2 * q) + invOnePlusAlpha = 1/(1 + alpha) + + oneLessCosw0 = 1 - cosw0 + + a0 = (-2 * cosw0) * invOnePlusAlpha + a1 = (1 - alpha) * invOnePlusAlpha + + b0 = q * alpha * invOnePlusAlpha + b1 = 0 + b2 = -b0 + + w = sig - a0*@fm00 - a1*@fm01 + sig = b0*w + b1*@fm00 + b2*@fm01 + + @fm01 = @fm00 + @fm00 = w + + if @layer.inputs.filter.slope + w = sig - a0*@fm10 - a1*@fm11 + sig = b0*w + b1*@fm10 + b2*@fm11 + + @fm11 = @fm10 + @fm10 = w + + sig + + processLowPass:(sig,cutoff,q)-> + w0 = Math.max(0,Math.min(@halfSampleRate,cutoff))*@invSampleRate + w0 *= 10000 + iw0 = Math.floor(w0) + aw0 = w0-iw0 + cosw0 = (1-aw0)*SIN_TABLE[iw0+2500]+aw0*SIN_TABLE[iw0+2501] + sinw0 = (1-aw0)*SIN_TABLE[iw0]+aw0*SIN_TABLE[iw0+1] + + alpha = sinw0 / (2 * q) + invOnePlusAlpha = 1/(1 + alpha) + + oneLessCosw0 = 1 - cosw0 + b1 = oneLessCosw0 * invOnePlusAlpha + b0 = b1*.5 + b2 = b0 + a0 = (-2 * cosw0) * invOnePlusAlpha + a1 = (1 - alpha) * invOnePlusAlpha + + w = sig - a0*@fm00 - a1*@fm01 + sig = b0*w + b1*@fm00 + b2*@fm01 + + @fm01 = @fm00 + @fm00 = w + + if @layer.inputs.filter.slope + w = sig - a0*@fm10 - a1*@fm11 + sig = b0*w + b1*@fm10 + b2*@fm11 + + @fm11 = @fm10 + @fm10 = w + + sig + +class Distortion + constructor:()-> + @amount = .5 + @rate = .5 + + update:(data)-> + @amount = data.amount + @rate = data.rate + + process:(buffer,length)-> + for i in [0..length-1] by 1 + sig = buffer[0][i] + s = sig*(1+@rate*@rate*99) + disto = if s<0 then -1+Math.exp(s) else 1-Math.exp(-s) + buffer[0][i] = (1-@amount)*sig+@amount*disto + + sig = buffer[1][i] + s = sig*(1+@rate*@rate*99) + disto = if s<0 then -1+Math.exp(s) else 1-Math.exp(-s) + buffer[1][i] = (1-@amount)*sig+@amount*disto + return + +class BitCrusher + constructor:()-> + @phase = 0 + @left = 0 + @right = 0 + + update:(data)-> + @amount = Math.pow(2,data.amount*8) + @rate = Math.pow(2,(1-data.rate)*16)*2 + + process:(buffer,length)-> + r = 1-@rate + crush = 1+15*r*r + + for i in [0..length-1] by 1 + left = buffer[0][i] + right = buffer[1][i] + + @phase += 1 + if @phase>@amount + @phase -= @amount + @left = if left>0 then Math.ceil(left*@rate)/@rate else Math.floor(left*@rate)/@rate + @right = if right>0 then Math.ceil(right*@rate)/@rate else Math.floor(right*@rate)/@rate + + buffer[0][i] = @left + buffer[1][i] = @right + return + +class Chorus + constructor:(@engine)-> + @sampleRate = @engine.sampleRate + @left_buffer = new Float64Array(@sampleRate) + @right_buffer = new Float64Array(@sampleRate) + @phase1 = Math.random() + @phase2 = Math.random() + @phase3 = Math.random() + @f1 = 1.031/@sampleRate + @f2 = 1.2713/@sampleRate + @f3 = 0.9317/@sampleRate + @index = 0 + + update:(data)-> + @amount = Math.pow(data.amount,.5) + @rate = data.rate + + read:(buffer,pos)-> + pos += buffer.length if pos<0 + i = Math.floor(pos) + a = pos-i + buffer[i]*(1-a)+buffer[(i+1)%buffer.length]*a + + process:(buffer,length)-> + for i in [0..length-1] by 1 + left = @left_buffer[@index] = buffer[0][i] + right = @right_buffer[@index] = buffer[1][i] + + @phase1 += @f1*(.5+.5*@rate) + @phase2 += @f2*(.5+.5*@rate) + @phase3 += @f3*(.5+.5*@rate) + + p1 = (1+Math.sin(@phase1*Math.PI*2))*@left_buffer.length*.002 + p2 = (1+Math.sin(@phase2*Math.PI*2))*@left_buffer.length*.002 + p3 = (1+Math.sin(@phase3*Math.PI*2))*@left_buffer.length*.002 + + @phase1 -= 1 if @phase1>=1 + @phase2 -= 1 if @phase2>=1 + @phase3 -= 1 if @phase3>=1 + + s1 = @read(@left_buffer,@index-p1) + s2 = @read(@right_buffer,@index-p2) + s3 = @read(@right_buffer,@index-p3) + + pleft = @amount*(s1*.2+s2*.7+s3*.1) + pright = @amount*(s1*.6+s2*.2+s3*.2) + + left += pleft + right += pright + + @left_buffer[@index] += pleft*.5*@amount + @right_buffer[@index] += pleft*.5*@amount + @index += 1 + @index = 0 if @index>=@left_buffer.length + + buffer[0][i] = left + buffer[1][i] = right + return + +class Phaser + constructor:(@engine)-> + @sampleRate = @engine.sampleRate + @left_buffer = new Float64Array(@sampleRate) + @right_buffer = new Float64Array(@sampleRate) + @phase1 = Math.random() + @phase2 = Math.random() + @f1 = .0573/@sampleRate + @f2 = .0497/@sampleRate + @index = 0 + + update:(data)-> + @amount = data.amount + @rate = data.rate + + read:(buffer,pos)-> + pos += buffer.length if pos<0 + i = Math.floor(pos) + a = pos-i + buffer[i]*(1-a)+buffer[(i+1)%buffer.length]*a + + process:(buffer,length)-> + for i in [0..length-1] by 1 + left = buffer[0][i] + right = buffer[1][i] + + @phase1 += @f1*(.5+.5*@rate) + @phase2 += @f2*(.5+.5*@rate) + + o1 = (1+Math.sin(@phase1*Math.PI*2))/2 + p1 = @sampleRate*(.0001+.05*o1) + + o2 = (1+Math.sin(@phase2*Math.PI*2))/2 + p2 = @sampleRate*(.0001+.05*o2) + + @phase1 -= 1 if @phase1>=1 + @phase2 -= 1 if @phase2>=1 + + @left_buffer[@index] = left + @right_buffer[@index] = right + + s1 = @read(@left_buffer,@index-p1) + s2 = @read(@right_buffer,@index-p2) + + @left_buffer[@index] += s1*@rate*.9 + @right_buffer[@index] += s2*@rate*.9 + + buffer[0][i] = s1*@amount-left + buffer[1][i] = s2*@amount-right + + @index += 1 + @index = 0 if @index>=@left_buffer.length + return + +class Flanger + constructor:(@engine)-> + @sampleRate = @engine.sampleRate + @left_buffer = new Float64Array(@sampleRate) + @right_buffer = new Float64Array(@sampleRate) + @phase1 = 0 #Math.random() + @phase2 = 0 #Math.random() + @f1 = .0573/@sampleRate + @f2 = .0497/@sampleRate + @index = 0 + + update:(data)-> + @amount = data.amount + @rate = data.rate + + read:(buffer,pos)-> + pos += buffer.length if pos<0 + i = Math.floor(pos) + a = pos-i + buffer[i]*(1-a)+buffer[(i+1)%buffer.length]*a + + process:(buffer,length)-> + for i in [0..length-1] by 1 + left = buffer[0][i] + right = buffer[1][i] + + @phase1 += @f1 + @phase2 += @f2 + + o1 = (1+Math.sin(@phase1*Math.PI*2))/2 + p1 = @sampleRate*(.0001+.05*o1) + + o2 = (1+Math.sin(@phase2*Math.PI*2))/2 + p2 = @sampleRate*(.0001+.05*o2) + + @phase1 -= 1 if @phase1>=1 + @phase2 -= 1 if @phase2>=1 + + @left_buffer[@index] = left #+s1*.3 + @right_buffer[@index] = right #+s2*.3 + + s1 = @read(@left_buffer,@index-p1) + s2 = @read(@right_buffer,@index-p2) + + @left_buffer[@index] += s1*@rate*.9 + @right_buffer[@index] += s2*@rate*.9 + + buffer[0][i] = s1*@amount+left + buffer[1][i] = s2*@amount+right + + @index += 1 + @index = 0 if @index>=@left_buffer.length + return + +class Delay + constructor:(@engine)-> + @sampleRate = @engine.sampleRate + @left_buffer = new Float64Array(@sampleRate*3) + @right_buffer = new Float64Array(@sampleRate*3) + @index = 0 + + update:(data)-> + @amount = data.amount + @rate = data.rate + tempo = (30+Math.pow(@rate,2)*170*4)*4 + tick = @sampleRate/(tempo/60) + @L = Math.round(tick*4) + @R = Math.round(tick*4+@sampleRate*0.00075) + @fb = @amount*.95 + + process:(buffer,length)-> + for i in [0..length-1] by 1 + left = buffer[0][i] + right = buffer[1][i] + + left += @right_buffer[(@index+@left_buffer.length-@L)%@left_buffer.length]*@fb + right += @left_buffer[(@index+@right_buffer.length-@R)%@right_buffer.length]*@fb + + buffer[0][i] = @left_buffer[@index] = left + buffer[1][i] = @right_buffer[@index] = right + + @index += 1 + @index = 0 if @index>=@left_buffer.length + return + +class Spatializer + constructor:(@engine)-> + @sampleRate = @engine.sampleRate + @left_buffer = new Float64Array(@sampleRate/10) + @right_buffer = new Float64Array(@sampleRate/10) + @index = 0 + @left_delay1 = 0 + @left_delay2 = 0 + @right_delay1 = 0 + @right_delay2 = 0 + + @left = 0 + @right = 0 + + @left_delay1 = Math.round(@sampleRate*9.7913/340) + @right_delay1 = Math.round(@sampleRate*11.1379/340) + @left_delay2 = Math.round(@sampleRate*11.3179/340) + @right_delay2 = Math.round(@sampleRate*12.7913/340) + + process:(buffer,length,spatialize,pan)-> + mnt = spatialize + mnt2 = mnt*mnt + + left_pan = Math.cos(pan*Math.PI/2)/(1+spatialize) + right_pan = Math.sin(pan*Math.PI/2)/(1+spatialize) + + left_buffer = buffer[0] + right_buffer = buffer[1] + + for i in [0..length-1] by 1 + left = left_buffer[i] + right = right_buffer[i] + + @left = left*.5+@left*.5 + @right = right*.5+@right*.5 + + @left_buffer[@index] = @left + @right_buffer[@index] = @right + + left_buffer[i] = (left-mnt*@right_buffer[(@index+@right_buffer.length-@left_delay1)%@right_buffer.length])*left_pan + right_buffer[i] = (right-mnt*@left_buffer[(@index+@right_buffer.length-@right_delay1)%@right_buffer.length])*right_pan + + @index += 1 + @index = 0 if @index>=@left_buffer.length + return + +class EQ + constructor:(@engine)-> + @sampleRate = @engine.sampleRate + + + # band pass for medium freqs + @mid = 900 + q = .5 + w0 = 2*Math.PI*@mid/@sampleRate + cosw0 = Math.cos(w0) + sinw0 = Math.sin(w0) + alpha = sinw0 / (2 * q) + invOnePlusAlpha = 1/(1 + alpha) + oneLessCosw0 = 1 - cosw0 + + @a0 = (-2 * cosw0) * invOnePlusAlpha + @a1 = (1 - alpha) * invOnePlusAlpha + + @b0 = q * alpha * invOnePlusAlpha + @b1 = 0 + @b2 = -@b0 + + @llow = 0 + @rlow = 0 + + @lfm0 = 0 + @lfm1 = 0 + @rfm0 = 0 + @rfm1 = 0 + + @low = 1 + @mid = 1 + @high = 1 + + update:(data)-> + @low = data.low*2 + @mid = data.mid*2 + @high = data.high*2 + + processBandPass:(sig,cutoff,q)-> + w = sig - a0*@fm0 - a1*@fm1 + sig = b0*w + b1*@fm0 + b2*@fm1 + + @fm1 = @fm0 + @fm0 = w + + sig + + process:(buffer,length)-> + for i in [0..length-1] by 1 + left = buffer[0][i] + right = buffer[1][i] + + lw = left - @a0*@lfm0 - @a1*@lfm1 + lmid = @b0*lw + @b1*@lfm0 + @b2*@lfm1 + + @lfm1 = @lfm0 + @lfm0 = lw + + left -= lmid + + @llow = left*.1+@llow*.9 + + lhigh = left-@llow + + buffer[0][i] = @llow*@low+lmid*@mid+lhigh*@high + + rw = right - @a0*@rfm0 - @a1*@rfm1 + rmid = @b0*rw + @b1*@rfm0 + @b2*@rfm1 + + @rfm1 = @rfm0 + @rfm0 = rw + + right -= rmid + + @rlow = right*.1+@rlow*.9 + + rhigh = right-@rlow + + buffer[1][i] = @rlow*@low+rmid*@mid+rhigh*@high + + return + +class CombFilter + constructor:(@length,@feedback = .5)-> + @buffer = new Float64Array(@length) + @index = 0 + @store = 0 + @damp = .2 + + process:(input)-> + output = @buffer[@index] + @store = output*(1-@damp)+@store*@damp + @buffer[@index++] = input+@store*@feedback + if @index>=@length + @index = 0 + + output + +class AllpassFilter + constructor:(@length,@feedback = .5)-> + @buffer = new Float64Array(@length) + @index = 0 + + process:(input)-> + bufout = @buffer[@index] + output = -input+bufout + @buffer[@index++] = input+bufout*@feedback + if @index>=@length + @index = 0 + + output + +class Reverb + constructor:(@engine)-> + @sampleRate = @engine.sampleRate + combtuning = [1116,1188,1277,1356,1422,1491,1557,1617] + allpasstuning = [556,441,341,225] + stereospread = 23 + @left_combs = [] + @right_combs = [] + @left_allpass = [] + @right_allpass = [] + @res = [0,0] + @spread = .25 + @wet = .025 + + for c in combtuning + @left_combs.push new CombFilter c,.9 + @right_combs.push new CombFilter c+stereospread,.9 + + for a in allpasstuning + @left_allpass.push new AllpassFilter a,.5 + @right_allpass.push new AllpassFilter a+stereospread,.5 + + #@reflections = [] + #@reflections.push new Reflection 7830,.1,.5,1 + #@reflections.push new Reflection 10670,-.2,.6,.9 + #@reflections.push new Reflection 13630,.7,.7,.8 + #@reflections.push new Reflection 21870,-.6,.8,.7 + #@reflections.push new Reflection 35810,-.1,.9,.6 + + update:(data)-> + @wet = data.amount*.05 + feedback = .7+Math.pow(data.rate,.25)*.29 + damp = .2 #.4-.4*data.rate + for i in [0..@left_combs.length-1] by 1 + @left_combs[i].feedback = feedback + @right_combs[i].feedback = feedback + @left_combs[i].damp = damp + @right_combs[i].damp = damp + + @spread = .5-data.rate*.5 + + process:(buffer,length)-> + for s in [0..length-1] by 1 + outL = 0 + outR = 0 + left = buffer[0][s] + right = buffer[1][s] + input = (left+right)*.5 + for i in [0..@left_combs.length-1] + outL += @left_combs[i].process(input) + outR += @right_combs[i].process(input) + + for i in [0..@left_allpass.length-1] + outL = @left_allpass[i].process(outL) + outR = @right_allpass[i].process(outR) + + #for i in [0..@reflections.length-1] + # r = @reflections[i].process(input) + # outL += r[0] + # outR += r[1] + + buffer[0][s] = (outL*(1-@spread)+outR*@spread)*@wet+left*(1-@wet) + buffer[1][s] = (outR*(1-@spread)+outL*@spread)*@wet+right*(1-@wet) + return + +class Voice + @oscillators = [SawOscillator,SquareOscillator,SineOscillator,VoiceOscillator,StringOscillator] + + constructor:(@engine)-> + @osc1 = new SineOscillator @ + @osc2 = new SineOscillator @ + @noise = new Noise @ + @lfo1 = new LFO @ + @lfo2 = new LFO @ + @filter = new Filter @ + @env1 = new AmpEnvelope @ + @env2 = new ModEnvelope @ + @filter_increment = 0.0005*44100/@engine.sampleRate + @modulation = 0 + @noteon_time = 0 + + init:(layer)-> + if @osc1 not instanceof Voice.oscillators[layer.inputs.osc1.type] + @osc1 = new Voice.oscillators[layer.inputs.osc1.type] @ + + if @osc2 not instanceof Voice.oscillators[layer.inputs.osc2.type] + @osc2 = new Voice.oscillators[layer.inputs.osc2.type] @ + + @osc1.init(layer.inputs.osc1,layer.inputs.sync) + @osc2.init(layer.inputs.osc2,layer.inputs.sync) + @noise.init(layer,layer.inputs.sync) + @lfo1.init(layer.inputs.lfo1,layer.inputs.sync) + @lfo2.init(layer.inputs.lfo2,layer.inputs.sync) + @filter.init(layer) + @env1.init(layer.inputs.env1) + @env2.init(layer.inputs.env2) + + @updateConstantMods() + + update:()-> + return if not @layer? + + if @osc1 not instanceof Voice.oscillators[@layer.inputs.osc1.type] + @osc1 = new Voice.oscillators[@layer.inputs.osc1.type] @,@layer.inputs.osc1 + + if @osc2 not instanceof Voice.oscillators[@layer.inputs.osc2.type] + @osc2 = new Voice.oscillators[@layer.inputs.osc2.type] @,@layer.inputs.osc2 + + @osc1.update(@layer.inputs.osc1,@layer.inputs.sync) + @osc2.update(@layer.inputs.osc2,@layer.inputs.sync) + + @env1.update() + @env2.update() + + @lfo1.update() + @lfo2.update() + @filter.update() + + @updateConstantMods() + + updateConstantMods:()-> + @osc1_amp = DBSCALE(@inputs.osc1.amp,3) + @osc2_amp = DBSCALE(@inputs.osc2.amp,3) + @osc1_mod = @inputs.osc1.mod + @osc2_mod = @inputs.osc2.mod + @noise_amp = DBSCALE(@inputs.noise.amp,3) + @noise_mod = @inputs.noise.mod + + if @inputs.velocity.amp>0 + p = @inputs.velocity.amp + p *= p + p *= 4 + #amp = Math.pow(@velocity,p) #Math.exp((-1+@velocity)*5) + norm = @inputs.velocity.amp*4 + amp = Math.exp(@velocity*norm)/Math.exp(norm) + #amp = @inputs.velocity.amp*amp+(1-@inputs.velocity.amp) + else + amp = 1 + + @osc1_amp *= amp + @osc2_amp *= amp + @noise_amp *= amp + + + c = Math.log(1024*@freq/22000)/(10*Math.log(2)) + @cutoff_keymod = (c-.5)*@inputs.filter.follow + @cutoff_base = @inputs.filter.cutoff+@cutoff_keymod + + @env2_amount = @inputs.env2.amount + + @lfo1_rate = @inputs.lfo1.rate + @lfo1_amount = @inputs.lfo1.amount + @lfo2_rate = @inputs.lfo2.rate + @lfo2_amount = @inputs.lfo2.amount + + + # "Mod" + # + # "Filter Cutoff" + # "Filter Resonance" + # + # "Osc1 Mod" + # "Osc1 Amp" + # + # "Osc2 Mod" + # "Osc2 Amp" + # + # "Noise Amp" + # "Noise Color" + # + # "Env1 Attack" + # "Env2 Attack" + # "Env2 Amount" + # + # "LFO1 Amount" + # "LFO1 Rate" + # + # "LFO2 Amount" + # "LFO2 Rate" + # ] + + switch @inputs.velocity.out + when 0 # Oscs Mod + mod = @velocity*(@inputs.velocity.amount-.5)*2 + @osc1_mod = Math.min(1,Math.max(0,@osc1_mod+mod)) + @osc2_mod = Math.min(1,Math.max(0,@osc2_mod+mod)) + + when 1 # Filter Cutoff + mod = @velocity*(@inputs.velocity.amount-.5)*2 + @cutoff_base += mod + + when 3 # Osc1 Mod + mod = @velocity*(@inputs.velocity.amount-.5)*2 + @osc1_mod = Math.min(1,Math.max(0,@osc1_mod+mod)) + + when 4 # Osc1 Amp + mod = @velocity*(@inputs.velocity.amount-.5)*2 + @osc1_amp = Math.max(0,@osc1_amp+mod) + + when 5 # Osc2 Mod + mod = @velocity*(@inputs.velocity.amount-.5)*2 + @osc2_mod = Math.min(1,Math.max(0,@osc2_mod+mod)) + + when 6 # Osc2 Amp + mod = @velocity*(@inputs.velocity.amount-.5)*2 + @osc2_amp = Math.max(0,@osc2_amp+mod) + + when 7 # Noise Amp + mod = @velocity*(@inputs.velocity.amount-.5)*2 + @noise_amp = Math.max(0,@noise_amp+mod) + + when 8 # Noise Color + mod = @velocity*(@inputs.velocity.amount-.5)*2 + @noise_mod = Math.min(1,Math.max(0,@noise_mod+mod)) + + when 9 # Env1 Attack + mod = @velocity*(@inputs.velocity.amount-.5)*2 + a = Math.max(0,Math.min(@inputs.env1.a+mod,1)) + @env1.update(a) + + when 10 # Env2 Attack + mod = @velocity*(@inputs.velocity.amount-.5)*2 + a = Math.max(0,Math.min(@inputs.env2.a+mod,1)) + @env2.update(a) + + when 11 # Env2 Amount + mod = @velocity*(@inputs.velocity.amount-.5)*2 + @env2_amount = Math.max(0,Math.min(1,@env2_amount+mod)) + + when 12 # LFO1 Amount + mod = @velocity*(@inputs.velocity.amount-.5)*2 + @lfo1_amount = Math.max(0,@lfo1_amount+mod) + + when 13 # LFO1 Rate + mod = @velocity*(@inputs.velocity.amount-.5)*2 + @lfo1_rate = Math.min(1,Math.max(0,@lfo1_rate+mod)) + + when 14 # LFO2 Amount + mod = @velocity*(@inputs.velocity.amount-.5)*2 + @lfo2_amount = Math.max(0,@lfo2_amount+mod) + + when 15 # LFO2 Rate + mod = @velocity*(@inputs.velocity.amount-.5)*2 + @lfo2_rate = Math.min(1,Math.max(0,@lfo2_rate+mod)) + + + if @freq? + c = Math.log(1024*@freq/22000)/(10*Math.log(2)) + @cutoff_keymod = (c-.5)*@inputs.filter.follow + + noteOn:(layer,@key,velocity,legato=false)-> + @velocity = velocity/127 + + if @layer? + @layer.removeVoice @ + + @layer = layer + @inputs = @layer.inputs + if legato and @on + @freq = 440*Math.pow(Math.pow(2,1/12),@key-57) + + if layer.last_key? + @glide_from = layer.last_key + @glide = true + @glide_phase = 0 + glide_time = (@inputs.glide*.025+Math.pow(@inputs.glide,16)*.975)*10 + @glide_inc = 1/(glide_time*@engine.sampleRate+1) + else + @freq = 440*Math.pow(Math.pow(2,1/12),@key-57) + @init @layer + @on = true + @cutoff = 0 + @modulation = @layer.instrument.modulation + @pitch_bend = @layer.instrument.pitch_bend + @modulation_v = 0 + @pitch_bend_v = 0 + + @noteon_time = Date.now() + + noteOff:()-> + @on = false + + process:()-> + osc1_mod = @osc1_mod + osc2_mod = @osc2_mod + osc1_amp = @osc1_amp + osc2_amp = @osc2_amp + + if @glide + k = @glide_from*(1-@glide_phase)+@key*@glide_phase + osc1_freq = osc2_freq = 440*Math.pow(Math.pow(2,1/12),k-57) + @glide_phase += @glide_inc + if @glide_phase>=1 + @glide = false + else + osc1_freq = osc2_freq = @freq + + if Math.abs(@pitch_bend-@layer.instrument.pitch_bend)>.0001 + @pitch_bend_v += .001*(@layer.instrument.pitch_bend-@pitch_bend) + @pitch_bend_v *= .5 + @pitch_bend += @pitch_bend_v + + if Math.abs(@pitch_bend-.5)>.0001 + p = @pitch_bend*2-1 + p *= 2 + f = Math.pow(Math.pow(2,1/12),p) + osc1_freq *= f + osc2_freq *= f + + noise_amp = @noise_amp + noise_mod = @noise_mod + lfo1_rate = @lfo1_rate + lfo1_amount = @lfo1_amount + lfo2_rate = @lfo2_rate + lfo2_amount = @lfo2_amount + + cutoff = @cutoff_base + q = @inputs.filter.resonance + + if Math.abs(@modulation-@layer.instrument.modulation)>.0001 + @modulation_v += .001*(@layer.instrument.modulation-@modulation) + @modulation_v *= .5 + @modulation += @modulation_v + + switch @inputs.modulation.out + when 0 # Amp + mod = (@inputs.modulation.amount-.5)*2*@modulation + osc1_amp = Math.max(0,osc1_amp+mod) + osc2_amp = Math.max(0,osc2_amp+mod) + noise_amp = Math.max(0,noise_amp*(1+mod)) + + when 1 # Mod + mod = (@inputs.modulation.amount-.5)*2*@modulation + osc1_mod = Math.min(1,Math.max(0,osc1_mod+mod)) + osc2_mod = Math.min(1,Math.max(0,osc2_mod+mod)) + + when 2 # Osc1 Amp + mod = (@inputs.modulation.amount-.5)*2*@modulation + osc1_amp = Math.max(0,osc1_amp+mod) + + when 3 # Osc1 Mod + mod = (@inputs.modulation.amount-.5)*2*@modulation + osc1_mod = Math.min(1,Math.max(0,osc1_mod+mod)) + + when 4 # Osc2 Amp + mod = (@inputs.modulation.amount-.5)*2*@modulation + osc2_amp = Math.max(0,osc2_amp+mod) + + when 5 # Osc2 Mod + mod = (@inputs.modulation.amount-.5)*2*@modulation + osc2_mod = Math.min(1,Math.max(0,osc2_mod+mod)) + + when 6 # Noise Amp + mod = (@inputs.modulation.amount-.5)*2*@modulation + noise_amp = Math.max(0,noise_amp+mod) + + when 7 # Noise Color + mod = (@inputs.modulation.amount-.5)*2*@modulation + noise_mod = Math.min(1,Math.max(0,noise_mod+mod)) + + when 8 # Filter Cutoff + mod = (@inputs.modulation.amount-.5)*2*@modulation + cutoff += mod + + when 9 # Filter Resonance + mod = (@inputs.modulation.amount-.5)*2*@modulation + q = Math.max(0,Math.min(1,q+mod)) + + when 10 # LFO1 Amount + mod = (@inputs.modulation.amount-.5)*2*@modulation + lfo1_amount = Math.max(0,Math.min(1,lfo1_amount+mod)) + + when 11 # LFO1 Rate + mod = (@inputs.modulation.amount-.5)*2*@modulation + lfo1_rate = Math.max(0,Math.min(1,lfo1_rate+mod)) + + when 12 # LFO2 Amount + mod = (@inputs.modulation.amount-.5)*2*@modulation + lfo2_amount = Math.max(0,Math.min(1,lfo2_amount+mod)) + + when 13 # LFO2 Rate + mod = (@inputs.modulation.amount-.5)*2*@modulation + lfo2_rate = Math.max(0,Math.min(1,lfo2_rate+mod)) + + + switch @inputs.env2.out + when 0 # Filter Cutoff + cutoff += @env2.process(@on)*(@env2_amount*2-1) + + when 1 # Filter Resonance + q = Math.max(0,Math.min(1,@env2.process(@on)*(@env2_amount*2-1))) + + when 2 # Pitch + mod = @env2_amount*2-1 + mod *= @env2.process(@on) + mod = 1+mod + osc1_freq *= mod + osc2_freq *= mod + + when 3 # Mod + mod = @env2.process(@on)*(@env2_amount*2-1) + osc1_mod = Math.min(1,Math.max(0,osc1_mod+mod)) + osc2_mod = Math.min(1,Math.max(0,osc2_mod+mod)) + + when 4 # Amp + mod = @env2.process(@on)*(@env2_amount*2-1) + osc1_amp = Math.max(0,osc1_amp+mod) + osc2_amp = Math.max(0,osc2_amp+mod) + noise_amp = Math.max(0,noise_amp*(1+mod)) + + when 5 #Osc1 Pitch + mod = @env2_amount*2-1 + mod *= @env2.process(@on) + osc1_freq *= 1+mod + + when 6 #Osc1 Mod + mod = @env2.process(@on)*(@env2_amount*2-1) + osc1_mod = Math.min(1,Math.max(0,osc1_mod+mod)) + + when 7 #Osc1 Amp + mod = @env2.process(@on)*(@env2_amount*2-1) + osc1_amp = Math.max(0,osc1_amp+mod) + + when 8 #Osc2 Pitch + mod = @env2_amount*2-1 + mod *= @env2.process(@on) + osc1_freq *= 1+mod + + when 9 #Osc2 Mod + mod = @env2.process(@on)*(@env2_amount*2-1) + osc2_mod = Math.min(1,Math.max(0,osc2_mod+mod)) + + when 10 #Osc2 Amp + mod = @env2.process(@on)*(@env2_amount*2-1) + osc2_amp = Math.max(0,osc2_amp+mod) + + when 11 # Noise amp + mod = @env2.process(@on)*(@env2_amount*2-1) + noise_amp = Math.max(0,noise_amp+mod) + + when 12 # Noise color + mod = @env2.process(@on)*(@env2_amount*2-1) + noise_mod = Math.min(1,Math.max(0,noise_mod+mod)) + + when 13 # LFO1 Amount + mod = @env2.process(@on)*(@env2_amount*2-1) + lfo1_amount = Math.min(1,Math.max(0,lfo1_amount+mod)) + + when 14 # LFO1 rate + mod = @env2.process(@on)*(@env2_amount*2-1) + lfo1_rate = Math.min(1,Math.max(0,lfo1_rate+mod)) + + when 15 # LFO2 Amount + mod = @env2.process(@on)*(@env2_amount*2-1) + lfo2_amount = Math.min(1,Math.max(0,lfo2_amount+mod)) + + when 16 # LFO2 rate + mod = @env2.process(@on)*(@env2_amount*2-1) + lfo2_rate = Math.min(1,Math.max(0,lfo2_rate+mod)) + + switch @inputs.lfo1.out + when 0 # Pitch + mod = lfo1_amount + if @inputs.lfo1.audio + mod = 1+mod*mod*@lfo1.process(lfo1_rate)*16 + else + mod = 1+mod*mod*@lfo1.process(lfo1_rate) + osc1_freq *= mod + osc2_freq *= mod + + when 1 # Mod + mod = @lfo1.process(lfo1_rate)*lfo1_amount + osc1_mod = Math.min(1,Math.max(0,osc1_mod+mod)) + osc2_mod = Math.min(1,Math.max(0,osc2_mod+mod)) + + when 2 # Amp + mod = @lfo1.process(lfo1_rate)*lfo1_amount + osc1_amp = Math.max(0,osc1_amp+mod) + osc2_amp = Math.max(0,osc2_amp+mod) + noise_amp = Math.max(0,noise_amp*(1+mod)) + + when 3 #Osc1 Pitch + mod = lfo1_amount + mod = 1+mod*mod*@lfo1.process(lfo1_rate) + osc1_freq *= mod + + when 4 #Osc1 Mod + mod = @lfo1.process(lfo1_rate)*lfo1_amount + osc1_mod = Math.min(1,Math.max(0,osc1_mod+mod)) + + when 5 #Osc1 Amp + mod = @lfo1.process(lfo1_rate)*lfo1_amount + osc1_amp = Math.max(0,osc1_amp+mod) + + when 6 #Osc2 Pitch + mod = lfo1_amount + mod = 1+mod*mod*@lfo1.process(lfo1_rate) + osc2_freq *= mod + + when 7 #Osc2 Mod + mod = @lfo1.process(lfo1_rate)*lfo1_amount + osc2_mod = Math.min(1,Math.max(0,osc2_mod+mod)) + + when 8 #Osc2 Amp + mod = @lfo1.process(lfo1_rate)*lfo1_amount + osc2_amp = Math.max(0,osc2_amp+mod) + + when 9 # Noise amp + mod = @lfo1.process(lfo1_rate)*lfo1_amount + noise_amp = Math.max(0,noise_amp+mod) + + when 10 # Noise color + mod = @lfo1.process(lfo1_rate)*lfo1_amount + noise_mod = Math.min(1,Math.max(0,noise_mod+mod)) + + when 11 + cutoff += @lfo1.process(lfo1_rate)*lfo1_amount + + when 12 + q = Math.max(0,Math.min(1,@lfo1.process(lfo1_rate)*lfo1_amount)) + + when 13 # LFO2 Amount + mod = @lfo1.process(lfo1_rate)*lfo1_amount + lfo2_amount = Math.min(1,Math.max(0,lfo2_amount+mod)) + + when 14 # LFO2 rate + mod = @lfo1.process(lfo1_rate)*lfo1_amount + lfo2_rate = Math.min(1,Math.max(0,lfo2_rate+mod)) + + + switch @inputs.lfo2.out + when 0 # Pitch + mod = lfo2_amount + if @inputs.lfo2.audio + mod = 1+mod*mod*@lfo2.process(lfo2_rate)*16 + else + mod = 1+mod*mod*@lfo2.process(lfo2_rate) + osc1_freq *= mod + osc2_freq *= mod + + when 1 # Mod + mod = @lfo2.process(lfo2_rate)*lfo2_amount + osc1_mod = Math.min(1,Math.max(0,osc1_mod+mod)) + osc2_mod = Math.min(1,Math.max(0,osc2_mod+mod)) + + when 2 # Amp + mod = @lfo2.process(lfo2_rate)*lfo2_amount + osc1_amp = Math.max(0,osc1_amp+mod) + osc2_amp = Math.max(0,osc2_amp+mod) + noise_amp = Math.max(0,noise_amp*(1+mod)) + + when 3 #Osc1 Pitch + mod = lfo2_amount + mod = 1+mod*mod*@lfo2.process(lfo2_rate) + osc1_freq *= mod + + when 4 #Osc1 Mod + mod = @lfo2.process(lfo2_rate)*lfo2_amount + osc1_mod = Math.min(1,Math.max(0,osc1_mod+mod)) + + when 5 #Osc1 Amp + mod = @lfo2.process(lfo2_rate)*lfo2_amount + osc1_amp = Math.max(0,osc1_amp+mod) + + when 6 #Osc2 Pitch + mod = lfo2_amount + mod = 1+mod*mod*@lfo2.process(lfo2_rate) + osc2_freq *= mod + + when 7 #Osc2 Mod + mod = @lfo2.process(lfo2_rate)*lfo2_amount + osc2_mod = Math.min(1,Math.max(0,osc2_mod+mod)) + + when 8 #Osc2 Amp + mod = @lfo2.process(lfo2_rate)*lfo2_amount + osc2_amp = Math.max(0,osc2_amp+mod) + + when 9 # Noise amp + mod = @lfo2.process(lfo2_rate)*lfo2_amount + noise_amp = Math.max(0,noise_amp+mod) + + when 10 # Noise color + mod = @lfo2.process(lfo2_rate)*lfo2_amount + noise_mod = Math.min(1,Math.max(0,noise_mod+mod)) + + when 11 + cutoff += @lfo2.process(lfo2_rate)*lfo2_amount + + when 12 + q = Math.max(0,Math.min(1,@lfo2.process(lfo2_rate)*lfo2_amount)) + + switch @inputs.combine + when 1 + s1 = @osc1.process(osc1_freq,osc1_mod)*osc1_amp + s2 = @osc2.process(osc2_freq,osc2_mod)*osc2_amp + sig = (s1+s2)*(s1+s2) + #sig = @osc1.process(osc1_freq,osc1_mod)*@osc2.process(osc2_freq,osc2_mod)*Math.max(osc1_amp,osc2_amp)*4 + when 2 + sig = @osc2.process(osc2_freq*Math.max(0,1-@osc1.process(osc1_freq,osc1_mod)*osc1_amp),osc2_mod)*osc2_amp + else + sig = @osc1.process(osc1_freq,osc1_mod)*osc1_amp+@osc2.process(osc2_freq,osc2_mod)*osc2_amp + + if noise_amp>0 + sig += @noise.process(noise_mod)*noise_amp + + mod = @env2.process(@on) + + if not @cutoff + @cutoff = cutoff + else + if @cutoffcutoff + @cutoff += Math.max(cutoff-@cutoff,-@filter_increment) + + cutoff = @cutoff + + # VCF + cutoff = Math.pow(2,Math.max(0,Math.min(cutoff,1))*10)*22000/1024 + + sig *= @env1.process(@on) + + #@cutoff += 0.001*(cutoff-@cutoff) + + sig = @filter.process(sig,cutoff,q*q*9.5+.5) + +class Instrument + constructor:(@engine)-> + @layers = [] + @layers.push new Layer @ + @modulation = 0 + @pitch_bend = .5 + + noteOn:(key,velocity)-> + for l in @layers + l.noteOn(key,velocity) + return + + noteOff:(key)-> + for l in @layers + l.noteOff(key) + return + + setModulation:(@modulation)-> + + setPitchBend:(@pitch_bend)-> + + process:(length)-> + if false #@layers.length == 1 + @layers[0].process(length) + @output = @layers[0].output + else + if not @output? or @output[0].length + @engine = @instrument.engine + @voices = [] + @eq = new EQ(@engine) + @spatializer = new Spatializer(@engine) + @inputs = + osc1: + type: 0 + tune: .5 + coarse: .5 + amp: .5 + mod: 0 + + osc2: + type: 0 + tune: .5 + coarse: .5 + amp: .5 + mod: 0 + + combine: 0 + + noise: + amp: 0 + mod: 0 + + filter: + cutoff: 1 + resonance: 0 + type: 0 + slope: 1 + follow: 0 + + disto: + wet:0 + drive:0 + + bitcrusher: + wet: 0 + drive: 0 + crush: 0 + + env1: + a: 0 + d: 0 + s: 1 + r: 0 + + env2: + a: .1 + d: .1 + s: .5 + r: .1 + out: 0 + amount: .5 + + lfo1: + type: 0 + amount: 0 + rate: .5 + out: 0 + + lfo2: + type: 0 + amount: 0 + rate: .5 + out: 0 + + fx1: + type: -1 + amount: 0 + rate: 0 + + fx2: + type: -1 + amount: 0 + rate: 0 + + eq: + low: .5 + mid: .5 + high: .5 + + spatialize: .5 + pan: .5 + polyphony: 1 + glide: .5 + sync: 1 + + velocity: + out: 0 + amount: .5 + amp: .5 + + modulation: + out: 0 + amount: .5 + + noteOn:(key,velocity)-> + if @inputs.polyphony == 1 and @last_voice? and @last_voice.on + voice = @last_voice + voice.noteOn @,key,velocity,true + @voices.push voice + else + voice = @engine.getVoice() + voice.noteOn @,key,velocity + @voices.push voice + @last_voice = voice + + @last_key = key + + removeVoice:(voice)-> + index = @voices.indexOf(voice) + if index>=0 + @voices.splice index,1 + + noteOff:(key)-> + for v in @voices + if v.key == key + v.noteOff() + return + + update:()-> + if @inputs.fx1.type>=0 + if not @fx1? or @fx1 not instanceof FX1[@inputs.fx1.type] + @fx1 = new FX1[@inputs.fx1.type] @engine + else + @fx1 = null + + if @inputs.fx2.type>=0 + if not @fx2? or @fx2 not instanceof FX2[@inputs.fx2.type] + @fx2 = new FX2[@inputs.fx2.type] @engine + else + @fx2 = null + + if @fx1? + @fx1.update(@inputs.fx1) + + if @fx2? + @fx2.update(@inputs.fx2) + + @eq.update(@inputs.eq) + + process:(length)-> + if not @output? or @output[0].length + @voices = [] + @voice_index = 0 + @num_voices = 8 + + for i in [0..@num_voices-1] + @voices[i] = new Voice @ + + @instruments = [] + @instruments.push new Instrument @ + + @avg = 0 + @samples = 0 + @time = 0 + + @layer = + inputs: @inputs + + @start = Date.now() + + event:(data)-> + if data[0] == 144 and data[2]>0 + @instruments[0].noteOn(data[1],data[2]) + else if data[0] == 128 or (data[0] == 144 and data[2] == 0) + @instruments[0].noteOff(data[1]) + else if data[0]>=176 and data[0]<192 and data[1] == 1 # modulation wheel + @instruments[0].setModulation(data[2]/127) + else if data[0]>=224 and data[0]<240 # pitch bend + v = (data[1]+128*data[2]) + console.info "PB value=#{v}" + if v>=8192 + v = (v-8192)/(16383-8192) + v = .5+.5*v + else + v = .5*v/8192 + console.info("Pitch Bend = #{v}") + @instruments[0].setPitchBend(v) + + return + + getTime:()-> + (Date.now()-@start)/1000 + + getVoice:()-> + best = @voices[0] + + for i in [1..@voices.length-1] + v = @voices[i] + if best.on + if v.on + if v.noteon_time + for v in @voices + v.update() + + for l in @instruments[0].layers + l.update() + return + + process:(inputs,outputs,parameters)-> + output = outputs[0] + time = Date.now() + res = [0,0] + + for inst in @instruments + inst.process(output[0].length) + + for channel,i in output + if i<2 + for j in [0..channel.length-1] by 1 + sig = 0 + + for inst in @instruments + sig += inst.output[i][j] + + sig *= .125 + sig = if sig<0 then -(1-Math.exp(sig)) else 1-Math.exp(-sig) + + channel[j] = sig + + @time += Date.now()-time + @samples += channel.length + if @samples >= @sampleRate + @samples -= @sampleRate + console.info @time+" ms ; buffer size = "+channel.length + @time = 0 + + return + +class Blip + constructor:()-> + @size = 512 + + @samples = new Float64Array(@size+1) + + for p in [1..31] by 2 + for i in [0..@size] by 1 + x = (i/@size-.5)*.5 + @samples[i] += Math.sin(x*2*Math.PI*p)/p + + norm = @samples[@size] + + for i in [0..@size] by 1 + @samples[i] /= norm + + +` +class MyWorkletProcessor extends AudioWorkletProcessor { + constructor() { + super() + this.synth = new AudioEngine(sampleRate) + this.port.onmessage = (e) => { + console.info(e) + var data = JSON.parse(e.data) + if (data.name == "note") + { + this.synth.event(data.data) + } + else if (data.name == "param") + { + var value = data.value + var s = data.id.split(".") + data = this.synth.instruments[0].layers[0].inputs + while (s.length>1) + { + data = data[s.splice(0,1)[0]] + } + data[s[0]] = value + this.synth.updateVoices() + } + } + } + + process(inputs, outputs, parameters) { + this.synth.process(inputs,outputs,parameters) + return true + } +} + +registerProcessor('my-worklet-processor', MyWorkletProcessor) +` diff --git a/static/js/sound/synth/audioengine.js b/static/js/sound/synth/audioengine.js new file mode 100644 index 00000000..dc3a050c --- /dev/null +++ b/static/js/sound/synth/audioengine.js @@ -0,0 +1,2351 @@ + +const TWOPI = 2*Math.PI +const SIN_TABLE = new Float64Array(10001) +const WHITE_NOISE = new Float64Array(100000) +const COLORED_NOISE = new Float64Array(100000) +; +var AllpassFilter, AmpEnvelope, AudioEngine, BitCrusher, Blip, Chorus, CombFilter, DBSCALE, Delay, Distortion, EQ, FX1, FX2, Filter, Flanger, Instrument, LFO, Layer, ModEnvelope, Noise, Phaser, Reverb, SawOscillator, SineOscillator, Spatializer, SquareOscillator, StringOscillator, Voice, VoiceOscillator; + +(function() { + var i, o, results; + results = []; + for (i = o = 0; o <= 10000; i = o += 1) { + results.push(SIN_TABLE[i] = Math.sin(i / 10000 * Math.PI * 2)); + } + return results; +})(); + + +const BLIP_SIZE = 512 +const BLIP = new Float64Array(BLIP_SIZE+1) +; + +(function() { + var i, norm, o, p, ref, ref1, results, u, x, y; + for (p = o = 1; o <= 31; p = o += 2) { + for (i = u = 0, ref = BLIP_SIZE; u <= ref; i = u += 1) { + x = (i / BLIP_SIZE - .5) * .5; + BLIP[i] += Math.sin(x * 2 * Math.PI * p) / p; + } + } + norm = BLIP[BLIP_SIZE]; + results = []; + for (i = y = 0, ref1 = BLIP_SIZE; y <= ref1; i = y += 1) { + results.push(BLIP[i] /= norm); + } + return results; +})(); + +(function() { + var b0, b1, b2, b3, b4, b5, b6, i, n, o, pink, results, white; + n = 0; + b0 = b1 = b2 = b3 = b4 = b5 = b6 = 0; + results = []; + for (i = o = 0; o <= 99999; i = o += 1) { + white = Math.random() * 2 - 1; + n = .99 * n + .01 * white; + pink = n * 6; + WHITE_NOISE[i] = white; + results.push(COLORED_NOISE[i] = pink); + } + return results; +})(); + +DBSCALE = function(value, range) { + return (Math.exp(value * range) / Math.exp(range) - 1 / Math.exp(range)) / (1 - 1 / Math.exp(range)); +}; + +SquareOscillator = (function() { + function SquareOscillator(voice1, osc) { + this.voice = voice1; + this.invSampleRate = 1 / this.voice.engine.sampleRate; + this.tune = 1; + this.buffer = new Float64Array(32); + if (osc != null) { + this.init(osc); + } + } + + SquareOscillator.prototype.init = function(osc, sync1) { + var i, o, ref; + this.sync = sync1; + this.phase = this.sync ? 0 : Math.random(); + this.sig = -1; + this.index = 0; + this.update(osc); + for (i = o = 0, ref = this.buffer.length - 1; 0 <= ref ? o <= ref : o >= ref; i = 0 <= ref ? ++o : --o) { + this.buffer[i] = 0; + } + }; + + SquareOscillator.prototype.update = function(osc, sync1) { + var c, fine; + this.sync = sync1; + c = Math.round(osc.coarse * 48) / 48; + fine = osc.tune * 2 - 1; + fine = fine * (1 + fine * fine) * .5; + this.tune = 1 * Math.pow(2, c * 4) * .25 * Math.pow(Math.pow(2, 1 / 12), fine); + return this.analog_tune = this.tune; + }; + + SquareOscillator.prototype.process = function(freq, mod) { + var a, avg, dp, dpi, i, index, m, o, sig, u; + dp = this.analog_tune * freq * this.invSampleRate; + this.phase += dp; + m = .5 - mod * .49; + avg = 1 - 2 * m; + if (this.sig < 0) { + if (this.phase >= m) { + this.sig = 1; + dp = Math.max(0, Math.min(1, (this.phase - m) / dp)); + dp *= 16; + dpi = Math.floor(dp); + a = dp - dpi; + index = this.index; + for (i = o = 0; o <= 31; i = o += 1) { + if (dpi >= 512) { + break; + } + this.buffer[index] += -1 + BLIP[dpi] * (1 - a) + BLIP[dpi + 1] * a; + dpi += 16; + index = (index + 1) % this.buffer.length; + } + } + } else { + if (this.phase >= 1) { + dp = Math.max(0, Math.min(1, (this.phase - 1) / dp)); + dp *= 16; + dpi = Math.floor(dp); + a = dp - dpi; + index = this.index; + this.sig = -1; + this.phase -= 1; + this.analog_tune = this.sync ? this.tune : this.tune * (1 + (Math.random() - .5) * .002); + for (i = u = 0; u <= 31; i = u += 1) { + if (dpi >= 512) { + break; + } + this.buffer[index] += 1 - BLIP[dpi] * (1 - a) - BLIP[dpi + 1] * a; + dpi += 16; + index = (index + 1) % this.buffer.length; + } + } + } + sig = this.sig + this.buffer[this.index]; + this.buffer[this.index] = 0; + this.index = (this.index + 1) % this.buffer.length; + return sig - avg; + }; + + return SquareOscillator; + +})(); + +SawOscillator = (function() { + function SawOscillator(voice1, osc) { + this.voice = voice1; + this.invSampleRate = 1 / this.voice.engine.sampleRate; + this.tune = 1; + this.buffer = new Float64Array(32); + if (osc != null) { + this.init(osc); + } + } + + SawOscillator.prototype.init = function(osc, sync1) { + var i, o, ref; + this.sync = sync1; + this.phase = this.sync ? 0 : Math.random(); + this.sig = -1; + this.index = 0; + this.jumped = false; + this.update(osc); + for (i = o = 0, ref = this.buffer.length - 1; 0 <= ref ? o <= ref : o >= ref; i = 0 <= ref ? ++o : --o) { + this.buffer[i] = 0; + } + }; + + SawOscillator.prototype.update = function(osc, sync1) { + var c, fine; + this.sync = sync1; + c = Math.round(osc.coarse * 48) / 48; + fine = osc.tune * 2 - 1; + fine = fine * (1 + fine * fine) * .5; + this.tune = 1 * Math.pow(2, c * 4) * .25 * Math.pow(Math.pow(2, 1 / 12), fine); + return this.analog_tune = this.tune; + }; + + SawOscillator.prototype.process = function(freq, mod) { + var a, dp, dphase, dpi, i, index, o, offset, sig, slope, u; + dphase = this.analog_tune * freq * this.invSampleRate; + this.phase += dphase; + slope = 1 + mod; + if (!this.jumped) { + sig = 1 - 2 * this.phase * slope; + if (this.phase >= .5) { + this.jumped = true; + sig = mod - 2 * (this.phase - .5) * slope; + dp = Math.max(0, Math.min(1, (this.phase - .5) / dphase)); + dp *= 16; + dpi = Math.floor(dp); + a = dp - dpi; + index = this.index; + if (mod > 0) { + for (i = o = 0; o <= 31; i = o += 1) { + if (dpi >= 512) { + break; + } + this.buffer[index] += (-1 + BLIP[dpi] * (1 - a) + BLIP[dpi + 1] * a) * mod; + dpi += 16; + index = (index + 1) % this.buffer.length; + } + } + } + } else { + sig = mod - 2 * (this.phase - .5) * slope; + if (this.phase >= 1) { + this.jumped = false; + dp = Math.max(0, Math.min(1, (this.phase - 1) / dphase)); + dp *= 16; + dpi = Math.floor(dp); + a = dp - dpi; + index = this.index; + this.phase -= 1; + sig = 1 - 2 * this.phase * slope; + this.analog_tune = this.sync ? this.tune : this.tune * (1 + (Math.random() - .5) * .002); + for (i = u = 0; u <= 31; i = u += 1) { + if (dpi >= 512) { + break; + } + this.buffer[index] += -1 + BLIP[dpi] * (1 - a) + BLIP[dpi + 1] * a; + dpi += 16; + index = (index + 1) % this.buffer.length; + } + } + } + sig += this.buffer[this.index]; + this.buffer[this.index] = 0; + this.index = (this.index + 1) % this.buffer.length; + offset = 16 * 2 * dphase * slope; + return sig + offset; + }; + + return SawOscillator; + +})(); + +SineOscillator = (function() { + function SineOscillator(voice1, osc) { + this.voice = voice1; + this.sampleRate = this.voice.engine.sampleRate; + this.invSampleRate = 1 / this.voice.engine.sampleRate; + this.maxRatio = this.sampleRate / Math.PI / 5 / (2 * Math.PI); + this.tune = 1; + if (osc != null) { + this.init(osc); + } + } + + SineOscillator.prototype.init = function(osc, sync1) { + this.sync = sync1; + this.phase = this.sync ? .25 : Math.random(); + return this.update(osc); + }; + + SineOscillator.prototype.update = function(osc, sync1) { + var c, fine; + this.sync = sync1; + c = Math.round(osc.coarse * 48) / 48; + fine = osc.tune * 2 - 1; + fine = fine * (1 + fine * fine) * .5; + this.tune = 1 * Math.pow(2, c * 4) * .25 * Math.pow(Math.pow(2, 1 / 12), fine); + this.modnorm = 25 / Math.sqrt(this.tune * this.voice.freq); + return this.dphase = this.tune * this.invSampleRate; + }; + + SineOscillator.prototype.sinWave = function(x) { + var ax, ix; + x = (x - Math.floor(x)) * 10000; + ix = Math.floor(x); + ax = x - ix; + return SIN_TABLE[ix] * (1 - ax) + SIN_TABLE[ix + 1] * ax; + }; + + SineOscillator.prototype.sinWave2 = function(x) { + x = 2 * (x - Math.floor(x)); + if (x > 1) { + x = 2 - x; + } + return x * x * (3 - 2 * x) * 2 - 1; + }; + + SineOscillator.prototype.process = function(freq, mod) { + var m1, m2, p; + this.phase = this.phase + freq * this.dphase; + if (this.phase >= 1) { + this.phase -= 1; + this.analog_tune = this.sync ? this.tune : this.tune * (1 + (Math.random() - .5) * .002); + this.dphase = this.analog_tune * this.invSampleRate; + } + m1 = mod * this.modnorm; + m2 = mod * m1; + p = this.phase; + return this.sinWave2(p + m1 * this.sinWave2(p + m2 * this.sinWave2(p))); + }; + + return SineOscillator; + +})(); + +VoiceOscillator = (function() { + function VoiceOscillator(voice1, osc) { + this.voice = voice1; + this.sampleRate = this.voice.engine.sampleRate; + this.invSampleRate = 1 / this.voice.engine.sampleRate; + this.tune = 1; + if (osc != null) { + this.init(osc); + } + this.f1 = [320, 500, 700, 1000, 500, 320, 700, 500, 320, 320]; + this.f2 = [800, 1000, 1150, 1400, 1500, 1650, 1800, 2300, 3200, 3200]; + } + + VoiceOscillator.prototype.init = function(osc) { + this.phase = 0; + this.grain1_p1 = .25; + this.grain1_p2 = .25; + this.grain2_p1 = .25; + this.grain2_p2 = .25; + return this.update(osc); + }; + + VoiceOscillator.prototype.update = function(osc) { + var c, fine; + c = Math.round(osc.coarse * 48) / 48; + fine = osc.tune * 2 - 1; + fine = fine * (1 + fine * fine) * .5; + this.tune = 1 * Math.pow(2, c * 4) * .25 * Math.pow(Math.pow(2, 1 / 12), fine); + this.modnorm = 25 / Math.sqrt(this.tune * this.voice.freq); + return this.dphase = this.tune * this.invSampleRate; + }; + + VoiceOscillator.prototype.sinWave2 = function(x) { + x = 2 * (x - Math.floor(x)); + if (x > 1) { + x = 2 - x; + } + return x * x * (3 - 2 * x) * 2 - 1; + }; + + VoiceOscillator.prototype.process = function(freq, mod) { + var am, f1, f2, im, m, p1, sig, vol, x; + p1 = this.phase < 1; + this.phase = this.phase + freq * this.dphase; + m = mod * (this.f1.length - 2); + im = Math.floor(m); + am = m - im; + f1 = this.f1[im] * (1 - am) + this.f1[im + 1] * am; + f2 = this.f2[im] * (1 - am) + this.f2[im + 1] * am; + if (p1 && this.phase >= 1) { + this.grain2_p1 = .25; + this.grain2_p2 = .25; + } + if (this.phase >= 2) { + this.phase -= 2; + this.grain1_p1 = .25; + this.grain1_p2 = .25; + } + x = this.phase - 1; + x *= x * x; + vol = 1 - Math.abs(1 - this.phase); + sig = vol * (this.sinWave2(this.grain1_p1) * .25 + .125 * this.sinWave2(this.grain1_p2)); + this.grain1_p1 += f1 * this.invSampleRate; + this.grain1_p2 += f2 * this.invSampleRate; + x = ((this.phase + 1) % 2) - 1; + x *= x * x; + vol = this.phase < 1 ? 1 - this.phase : this.phase - 1; + sig += vol * (this.sinWave2(this.grain2_p1) * .25 + .125 * this.sinWave2(this.grain2_p2)); + sig += this.sinWave2(this.phase + .25); + this.grain2_p1 += f1 * this.invSampleRate; + this.grain2_p2 += f2 * this.invSampleRate; + return sig; + }; + + return VoiceOscillator; + +})(); + +StringOscillator = (function() { + function StringOscillator(voice1, osc) { + this.voice = voice1; + this.sampleRate = this.voice.engine.sampleRate; + this.invSampleRate = 1 / this.voice.engine.sampleRate; + this.maxRatio = this.sampleRate / Math.PI / 5 / (2 * Math.PI); + this.tune = 1; + if (osc != null) { + this.init(osc); + } + } + + StringOscillator.prototype.init = function(osc) { + this.index = 0; + this.buffer = new Float64Array(this.sampleRate / 10); + this.update(osc); + this.prev = 0; + return this.power = 1; + }; + + StringOscillator.prototype.update = function(osc) { + var c, fine; + c = Math.round(osc.coarse * 48) / 48; + fine = osc.tune * 2 - 1; + fine = fine * (1 + fine * fine) * .5; + return this.tune = 1 * Math.pow(2, c * 4) * .25 * Math.pow(Math.pow(2, 1 / 12), fine); + }; + + StringOscillator.prototype.process = function(freq, mod) { + var a, ix, m, n, period, r, reflection, sig, x; + period = this.sampleRate / (freq * this.tune); + x = (this.index - period + this.buffer.length) % this.buffer.length; + ix = Math.floor(x); + a = x - ix; + reflection = this.buffer[ix] * (1 - a) + this.buffer[(ix + 1) % this.buffer.length] * a; + m = Math.exp(Math.log(0.99) / freq); + m = 1; + r = reflection * m + this.prev * (1 - m); + this.prev = r; + n = Math.random() * 2 - 1; + m = mod * .5; + m = m * m * .999 + .001; + sig = n * m + r; + this.power = Math.max(Math.abs(sig) * .1 + this.power * .9, this.power * .999); + sig /= this.power + .0001; + this.buffer[this.index] = sig; + this.index += 1; + if (this.index >= this.buffer.length) { + this.index = 0; + } + return sig; + }; + + StringOscillator.prototype.processOld = function(freq, mod) { + var a, ix, m, period, r, reflection, sig, x; + period = this.sampleRate / (freq * this.tune); + x = (this.index - period + this.buffer.length) % this.buffer.length; + ix = Math.floor(x); + a = x - ix; + reflection = this.buffer[ix] * (1 - a) + this.buffer[(ix + 1) % this.buffer.length] * a; + m = mod; + r = reflection * m + this.prev * (1 - m); + this.prev = r; + sig = (Math.random() * 2 - 1) * .01 + r * .99; + this.buffer[this.index] = sig / this.power; + this.power = Math.abs(sig) * .001 + this.power * .999; + this.index += 1; + if (this.index >= this.buffer.length) { + this.index = 0; + } + return sig; + }; + + return StringOscillator; + +})(); + +Noise = (function() { + function Noise(voice1) { + this.voice = voice1; + } + + Noise.prototype.init = function() { + this.phase = 0; + this.seed = 1382; + return this.n = 0; + }; + + Noise.prototype.process = function(mod) { + var pink, white; + this.seed = (this.seed * 13907 + 12345) & 0x7FFFFFFF; + white = (this.seed / 0x80000000) * 2 - 1; + this.n = this.n * .99 + white * .01; + pink = this.n * 6; + return white * (1 - mod) + pink * mod; + }; + + return Noise; + +})(); + +ModEnvelope = (function() { + function ModEnvelope(voice1) { + this.voice = voice1; + this.sampleRate = this.voice.engine.sampleRate; + } + + ModEnvelope.prototype.init = function(params) { + this.params = params; + this.phase = 0; + return this.update(); + }; + + ModEnvelope.prototype.update = function(a) { + if (a == null) { + a = this.params.a; + } + this.a = 1 / (this.sampleRate * 20 * Math.pow(a, 3) + 1); + this.d = 1 / (this.sampleRate * 20 * Math.pow(this.params.d, 3) + 1); + this.s = this.params.s; + return this.r = 1 / (this.sampleRate * 20 * Math.pow(this.params.r, 3) + 1); + }; + + ModEnvelope.prototype.process = function(noteon) { + var sig; + if (this.phase < 1) { + sig = this.sig = this.phase = Math.min(1, this.phase + this.a); + } else if (this.phase < 2) { + this.phase = Math.min(2, this.phase + this.d); + sig = this.sig = 1 - (this.phase - 1) * (1 - this.s); + } else if (this.phase < 3) { + sig = this.sig = this.s; + } else { + this.phase = this.phase + this.r; + sig = Math.max(0, this.sig * (1 - (this.phase - 3))); + } + if (this.phase < 3 && !noteon) { + this.phase = 3; + } + return sig; + }; + + return ModEnvelope; + +})(); + +AmpEnvelope = (function() { + function AmpEnvelope(voice1) { + this.voice = voice1; + this.sampleRate = this.voice.engine.sampleRate; + this.sig = 0; + } + + AmpEnvelope.prototype.init = function(params) { + this.params = params; + this.phase = 0; + this.sig = 0; + return this.update(); + }; + + AmpEnvelope.prototype.update = function(a) { + if (a == null) { + a = this.params.a; + } + this.a2 = 1 / (this.sampleRate * (20 * Math.pow(a, 3) + .00025)); + this.d = Math.exp(Math.log(0.5) / (this.sampleRate * (10 * Math.pow(this.params.d, 3) + 0.001))); + this.s = DBSCALE(this.params.s, 2); + console.info("sustain " + this.s); + return this.r = Math.exp(Math.log(0.5) / (this.sampleRate * (10 * Math.pow(this.params.r, 3) + 0.001))); + }; + + AmpEnvelope.prototype.process = function(noteon) { + var sig; + if (this.phase < 1) { + this.phase += this.a2; + sig = this.sig = (this.phase * .75 + .25) * this.phase; + if (this.phase >= 1) { + sig = this.sig = 1; + this.phase = 1; + } + } else if (this.phase === 1) { + sig = this.sig = this.sig * this.d; + if (sig <= this.s) { + sig = this.sig = this.s; + this.phase = 2; + } + } else if (this.phase < 3) { + sig = this.sig = this.s; + } else { + sig = this.sig = this.sig * this.r; + } + if (this.phase < 3 && !noteon) { + this.phase = 3; + } + return sig; + }; + + return AmpEnvelope; + +})(); + +LFO = (function() { + function LFO(voice1) { + this.voice = voice1; + this.invSampleRate = 1 / this.voice.engine.sampleRate; + } + + LFO.prototype.init = function(params, sync) { + var rate, t; + this.params = params; + if (sync) { + rate = this.params.rate; + rate = .1 + rate * rate * rate * (100 - .1); + t = this.voice.engine.getTime() * rate; + this.phase = t % 1; + } else { + this.phase = Math.random(); + } + this.process = this.processSine; + this.update(); + this.r1 = Math.random() * 2 - 1; + this.r2 = Math.random() * 2 - 1; + return this.out = 0; + }; + + LFO.prototype.update = function() { + switch (this.params.type) { + case 0: + this.process = this.processSaw; + break; + case 1: + this.process = this.processSquare; + break; + case 2: + this.process = this.processSine; + break; + case 3: + this.process = this.processTriangle; + break; + case 4: + this.process = this.processRandom; + break; + case 5: + this.process = this.processRandomStep; + } + return this.audio_freq = 440 * Math.pow(Math.pow(2, 1 / 12), this.voice.key - 57); + }; + + LFO.prototype.processSine = function(rate) { + var p, r; + if (this.params.audio) { + r = rate < .5 ? .25 + rate * rate / .25 * .75 : rate * rate * 4; + rate = this.audio_freq * r; + } else { + rate = .01 + rate * rate * 20; + } + this.phase = this.phase + rate * this.invSampleRate; + if (this.phase >= 1) { + this.phase -= 1; + } + p = this.phase * 2; + if (p < 1) { + return p * p * (3 - 2 * p) * 2 - 1; + } else { + p -= 1; + return 1 - p * p * (3 - 2 * p) * 2; + } + }; + + LFO.prototype.processTriangle = function(rate) { + rate *= rate; + rate *= rate; + rate = .05 + rate * rate * 10000; + this.phase = this.phase + rate * this.invSampleRate; + if (this.phase >= 1) { + this.phase -= 1; + } + return 1 - 4 * Math.abs(this.phase - .5); + }; + + LFO.prototype.processSaw = function(rate) { + var out; + rate *= rate; + rate *= rate; + rate = .05 + rate * rate * 10000; + this.phase = this.phase + rate * this.invSampleRate; + if (this.phase >= 1) { + this.phase -= 1; + } + out = 1 - this.phase * 2; + return this.out = this.out * .97 + out * .03; + }; + + LFO.prototype.processSquare = function(rate) { + var out; + rate *= rate; + rate *= rate; + rate = .05 + rate * rate * 10000; + this.phase = this.phase + rate * this.invSampleRate; + if (this.phase >= 1) { + this.phase -= 1; + } + out = this.phase < .5 ? 1 : -1; + return this.out = this.out * .97 + out * .03; + }; + + LFO.prototype.processRandom = function(rate) { + rate *= rate; + rate *= rate; + rate = .05 + rate * rate * 10000; + this.phase = this.phase + rate * this.invSampleRate; + if (this.phase >= 1) { + this.phase -= 1; + this.r1 = this.r2; + this.r2 = Math.random() * 2 - 1; + } + return this.r1 * (1 - this.phase) + this.r2 * this.phase; + }; + + LFO.prototype.processRandomStep = function(rate) { + rate *= rate; + rate *= rate; + rate = .05 + rate * rate * 10000; + this.phase = this.phase + rate * this.invSampleRate; + if (this.phase >= 1) { + this.phase -= 1; + this.r1 = Math.random() * 2 - 1; + } + return this.out = this.out * .97 + this.r1 * .03; + }; + + LFO.prototype.processDiscreteRandom = function() {}; + + LFO.prototype.processSmoothRandom = function() {}; + + return LFO; + +})(); + +Filter = (function() { + function Filter(voice1) { + this.voice = voice1; + this.sampleRate = this.voice.engine.sampleRate; + this.invSampleRate = 1 / this.voice.engine.sampleRate; + this.halfSampleRate = this.sampleRate * .5; + } + + Filter.prototype.init = function(layer1) { + this.layer = layer1; + this.fm00 = 0; + this.fm01 = 0; + this.fm10 = 0; + this.fm11 = 0; + return this.update(); + }; + + Filter.prototype.update = function() { + switch (this.layer.inputs.filter.type) { + case 0: + return this.process = this.processLowPass; + case 1: + return this.process = this.processBandPass; + case 2: + return this.process = this.processHighPass; + } + }; + + Filter.prototype.processHighPass = function(sig, cutoff, q) { + var a0, a1, alpha, aw0, b0, b1, b2, cosw0, invOnePlusAlpha, iw0, onePlusCosw0, sinw0, w, w0; + w0 = Math.max(0, Math.min(this.halfSampleRate, cutoff)) * this.invSampleRate; + w0 *= 10000; + iw0 = Math.floor(w0); + aw0 = w0 - iw0; + cosw0 = (1 - aw0) * SIN_TABLE[iw0 + 2500] + aw0 * SIN_TABLE[iw0 + 2501]; + sinw0 = (1 - aw0) * SIN_TABLE[iw0] + aw0 * SIN_TABLE[iw0 + 1]; + alpha = sinw0 / (2 * q); + invOnePlusAlpha = 1 / (1 + alpha); + a0 = (-2 * cosw0) * invOnePlusAlpha; + a1 = (1 - alpha) * invOnePlusAlpha; + onePlusCosw0 = 1 + cosw0; + b0 = onePlusCosw0 * .5 * invOnePlusAlpha; + b1 = -onePlusCosw0 * invOnePlusAlpha; + b2 = b0; + w = sig - a0 * this.fm00 - a1 * this.fm01; + sig = b0 * w + b1 * this.fm00 + b2 * this.fm01; + this.fm01 = this.fm00; + this.fm00 = w; + if (this.layer.inputs.filter.slope) { + w = sig - a0 * this.fm10 - a1 * this.fm11; + sig = b0 * w + b1 * this.fm10 + b2 * this.fm11; + this.fm11 = this.fm10; + this.fm10 = w; + } + return sig; + }; + + Filter.prototype.processBandPass = function(sig, cutoff, q) { + var a0, a1, alpha, aw0, b0, b1, b2, cosw0, invOnePlusAlpha, iw0, oneLessCosw0, sinw0, w, w0; + w0 = Math.max(0, Math.min(this.halfSampleRate, cutoff)) * this.invSampleRate; + w0 *= 10000; + iw0 = Math.floor(w0); + aw0 = w0 - iw0; + cosw0 = (1 - aw0) * SIN_TABLE[iw0 + 2500] + aw0 * SIN_TABLE[iw0 + 2501]; + sinw0 = (1 - aw0) * SIN_TABLE[iw0] + aw0 * SIN_TABLE[iw0 + 1]; + alpha = sinw0 / (2 * q); + invOnePlusAlpha = 1 / (1 + alpha); + oneLessCosw0 = 1 - cosw0; + a0 = (-2 * cosw0) * invOnePlusAlpha; + a1 = (1 - alpha) * invOnePlusAlpha; + b0 = q * alpha * invOnePlusAlpha; + b1 = 0; + b2 = -b0; + w = sig - a0 * this.fm00 - a1 * this.fm01; + sig = b0 * w + b1 * this.fm00 + b2 * this.fm01; + this.fm01 = this.fm00; + this.fm00 = w; + if (this.layer.inputs.filter.slope) { + w = sig - a0 * this.fm10 - a1 * this.fm11; + sig = b0 * w + b1 * this.fm10 + b2 * this.fm11; + this.fm11 = this.fm10; + this.fm10 = w; + } + return sig; + }; + + Filter.prototype.processLowPass = function(sig, cutoff, q) { + var a0, a1, alpha, aw0, b0, b1, b2, cosw0, invOnePlusAlpha, iw0, oneLessCosw0, sinw0, w, w0; + w0 = Math.max(0, Math.min(this.halfSampleRate, cutoff)) * this.invSampleRate; + w0 *= 10000; + iw0 = Math.floor(w0); + aw0 = w0 - iw0; + cosw0 = (1 - aw0) * SIN_TABLE[iw0 + 2500] + aw0 * SIN_TABLE[iw0 + 2501]; + sinw0 = (1 - aw0) * SIN_TABLE[iw0] + aw0 * SIN_TABLE[iw0 + 1]; + alpha = sinw0 / (2 * q); + invOnePlusAlpha = 1 / (1 + alpha); + oneLessCosw0 = 1 - cosw0; + b1 = oneLessCosw0 * invOnePlusAlpha; + b0 = b1 * .5; + b2 = b0; + a0 = (-2 * cosw0) * invOnePlusAlpha; + a1 = (1 - alpha) * invOnePlusAlpha; + w = sig - a0 * this.fm00 - a1 * this.fm01; + sig = b0 * w + b1 * this.fm00 + b2 * this.fm01; + this.fm01 = this.fm00; + this.fm00 = w; + if (this.layer.inputs.filter.slope) { + w = sig - a0 * this.fm10 - a1 * this.fm11; + sig = b0 * w + b1 * this.fm10 + b2 * this.fm11; + this.fm11 = this.fm10; + this.fm10 = w; + } + return sig; + }; + + return Filter; + +})(); + +Distortion = (function() { + function Distortion() { + this.amount = .5; + this.rate = .5; + } + + Distortion.prototype.update = function(data) { + this.amount = data.amount; + return this.rate = data.rate; + }; + + Distortion.prototype.process = function(buffer, length) { + var disto, i, o, ref, s, sig; + for (i = o = 0, ref = length - 1; o <= ref; i = o += 1) { + sig = buffer[0][i]; + s = sig * (1 + this.rate * this.rate * 99); + disto = s < 0 ? -1 + Math.exp(s) : 1 - Math.exp(-s); + buffer[0][i] = (1 - this.amount) * sig + this.amount * disto; + sig = buffer[1][i]; + s = sig * (1 + this.rate * this.rate * 99); + disto = s < 0 ? -1 + Math.exp(s) : 1 - Math.exp(-s); + buffer[1][i] = (1 - this.amount) * sig + this.amount * disto; + } + }; + + return Distortion; + +})(); + +BitCrusher = (function() { + function BitCrusher() { + this.phase = 0; + this.left = 0; + this.right = 0; + } + + BitCrusher.prototype.update = function(data) { + this.amount = Math.pow(2, data.amount * 8); + return this.rate = Math.pow(2, (1 - data.rate) * 16) * 2; + }; + + BitCrusher.prototype.process = function(buffer, length) { + var crush, i, left, o, r, ref, right; + r = 1 - this.rate; + crush = 1 + 15 * r * r; + for (i = o = 0, ref = length - 1; o <= ref; i = o += 1) { + left = buffer[0][i]; + right = buffer[1][i]; + this.phase += 1; + if (this.phase > this.amount) { + this.phase -= this.amount; + this.left = left > 0 ? Math.ceil(left * this.rate) / this.rate : Math.floor(left * this.rate) / this.rate; + this.right = right > 0 ? Math.ceil(right * this.rate) / this.rate : Math.floor(right * this.rate) / this.rate; + } + buffer[0][i] = this.left; + buffer[1][i] = this.right; + } + }; + + return BitCrusher; + +})(); + +Chorus = (function() { + function Chorus(engine) { + this.engine = engine; + this.sampleRate = this.engine.sampleRate; + this.left_buffer = new Float64Array(this.sampleRate); + this.right_buffer = new Float64Array(this.sampleRate); + this.phase1 = Math.random(); + this.phase2 = Math.random(); + this.phase3 = Math.random(); + this.f1 = 1.031 / this.sampleRate; + this.f2 = 1.2713 / this.sampleRate; + this.f3 = 0.9317 / this.sampleRate; + this.index = 0; + } + + Chorus.prototype.update = function(data) { + this.amount = Math.pow(data.amount, .5); + return this.rate = data.rate; + }; + + Chorus.prototype.read = function(buffer, pos) { + var a, i; + if (pos < 0) { + pos += buffer.length; + } + i = Math.floor(pos); + a = pos - i; + return buffer[i] * (1 - a) + buffer[(i + 1) % buffer.length] * a; + }; + + Chorus.prototype.process = function(buffer, length) { + var i, left, o, p1, p2, p3, pleft, pright, ref, right, s1, s2, s3; + for (i = o = 0, ref = length - 1; o <= ref; i = o += 1) { + left = this.left_buffer[this.index] = buffer[0][i]; + right = this.right_buffer[this.index] = buffer[1][i]; + this.phase1 += this.f1 * (.5 + .5 * this.rate); + this.phase2 += this.f2 * (.5 + .5 * this.rate); + this.phase3 += this.f3 * (.5 + .5 * this.rate); + p1 = (1 + Math.sin(this.phase1 * Math.PI * 2)) * this.left_buffer.length * .002; + p2 = (1 + Math.sin(this.phase2 * Math.PI * 2)) * this.left_buffer.length * .002; + p3 = (1 + Math.sin(this.phase3 * Math.PI * 2)) * this.left_buffer.length * .002; + if (this.phase1 >= 1) { + this.phase1 -= 1; + } + if (this.phase2 >= 1) { + this.phase2 -= 1; + } + if (this.phase3 >= 1) { + this.phase3 -= 1; + } + s1 = this.read(this.left_buffer, this.index - p1); + s2 = this.read(this.right_buffer, this.index - p2); + s3 = this.read(this.right_buffer, this.index - p3); + pleft = this.amount * (s1 * .2 + s2 * .7 + s3 * .1); + pright = this.amount * (s1 * .6 + s2 * .2 + s3 * .2); + left += pleft; + right += pright; + this.left_buffer[this.index] += pleft * .5 * this.amount; + this.right_buffer[this.index] += pleft * .5 * this.amount; + this.index += 1; + if (this.index >= this.left_buffer.length) { + this.index = 0; + } + buffer[0][i] = left; + buffer[1][i] = right; + } + }; + + return Chorus; + +})(); + +Phaser = (function() { + function Phaser(engine) { + this.engine = engine; + this.sampleRate = this.engine.sampleRate; + this.left_buffer = new Float64Array(this.sampleRate); + this.right_buffer = new Float64Array(this.sampleRate); + this.phase1 = Math.random(); + this.phase2 = Math.random(); + this.f1 = .0573 / this.sampleRate; + this.f2 = .0497 / this.sampleRate; + this.index = 0; + } + + Phaser.prototype.update = function(data) { + this.amount = data.amount; + return this.rate = data.rate; + }; + + Phaser.prototype.read = function(buffer, pos) { + var a, i; + if (pos < 0) { + pos += buffer.length; + } + i = Math.floor(pos); + a = pos - i; + return buffer[i] * (1 - a) + buffer[(i + 1) % buffer.length] * a; + }; + + Phaser.prototype.process = function(buffer, length) { + var i, left, o, o1, o2, p1, p2, ref, right, s1, s2; + for (i = o = 0, ref = length - 1; o <= ref; i = o += 1) { + left = buffer[0][i]; + right = buffer[1][i]; + this.phase1 += this.f1 * (.5 + .5 * this.rate); + this.phase2 += this.f2 * (.5 + .5 * this.rate); + o1 = (1 + Math.sin(this.phase1 * Math.PI * 2)) / 2; + p1 = this.sampleRate * (.0001 + .05 * o1); + o2 = (1 + Math.sin(this.phase2 * Math.PI * 2)) / 2; + p2 = this.sampleRate * (.0001 + .05 * o2); + if (this.phase1 >= 1) { + this.phase1 -= 1; + } + if (this.phase2 >= 1) { + this.phase2 -= 1; + } + this.left_buffer[this.index] = left; + this.right_buffer[this.index] = right; + s1 = this.read(this.left_buffer, this.index - p1); + s2 = this.read(this.right_buffer, this.index - p2); + this.left_buffer[this.index] += s1 * this.rate * .9; + this.right_buffer[this.index] += s2 * this.rate * .9; + buffer[0][i] = s1 * this.amount - left; + buffer[1][i] = s2 * this.amount - right; + this.index += 1; + if (this.index >= this.left_buffer.length) { + this.index = 0; + } + } + }; + + return Phaser; + +})(); + +Flanger = (function() { + function Flanger(engine) { + this.engine = engine; + this.sampleRate = this.engine.sampleRate; + this.left_buffer = new Float64Array(this.sampleRate); + this.right_buffer = new Float64Array(this.sampleRate); + this.phase1 = 0; + this.phase2 = 0; + this.f1 = .0573 / this.sampleRate; + this.f2 = .0497 / this.sampleRate; + this.index = 0; + } + + Flanger.prototype.update = function(data) { + this.amount = data.amount; + return this.rate = data.rate; + }; + + Flanger.prototype.read = function(buffer, pos) { + var a, i; + if (pos < 0) { + pos += buffer.length; + } + i = Math.floor(pos); + a = pos - i; + return buffer[i] * (1 - a) + buffer[(i + 1) % buffer.length] * a; + }; + + Flanger.prototype.process = function(buffer, length) { + var i, left, o, o1, o2, p1, p2, ref, right, s1, s2; + for (i = o = 0, ref = length - 1; o <= ref; i = o += 1) { + left = buffer[0][i]; + right = buffer[1][i]; + this.phase1 += this.f1; + this.phase2 += this.f2; + o1 = (1 + Math.sin(this.phase1 * Math.PI * 2)) / 2; + p1 = this.sampleRate * (.0001 + .05 * o1); + o2 = (1 + Math.sin(this.phase2 * Math.PI * 2)) / 2; + p2 = this.sampleRate * (.0001 + .05 * o2); + if (this.phase1 >= 1) { + this.phase1 -= 1; + } + if (this.phase2 >= 1) { + this.phase2 -= 1; + } + this.left_buffer[this.index] = left; + this.right_buffer[this.index] = right; + s1 = this.read(this.left_buffer, this.index - p1); + s2 = this.read(this.right_buffer, this.index - p2); + this.left_buffer[this.index] += s1 * this.rate * .9; + this.right_buffer[this.index] += s2 * this.rate * .9; + buffer[0][i] = s1 * this.amount + left; + buffer[1][i] = s2 * this.amount + right; + this.index += 1; + if (this.index >= this.left_buffer.length) { + this.index = 0; + } + } + }; + + return Flanger; + +})(); + +Delay = (function() { + function Delay(engine) { + this.engine = engine; + this.sampleRate = this.engine.sampleRate; + this.left_buffer = new Float64Array(this.sampleRate * 3); + this.right_buffer = new Float64Array(this.sampleRate * 3); + this.index = 0; + } + + Delay.prototype.update = function(data) { + var tempo, tick; + this.amount = data.amount; + this.rate = data.rate; + tempo = (30 + Math.pow(this.rate, 2) * 170 * 4) * 4; + tick = this.sampleRate / (tempo / 60); + this.L = Math.round(tick * 4); + this.R = Math.round(tick * 4 + this.sampleRate * 0.00075); + return this.fb = this.amount * .95; + }; + + Delay.prototype.process = function(buffer, length) { + var i, left, o, ref, right; + for (i = o = 0, ref = length - 1; o <= ref; i = o += 1) { + left = buffer[0][i]; + right = buffer[1][i]; + left += this.right_buffer[(this.index + this.left_buffer.length - this.L) % this.left_buffer.length] * this.fb; + right += this.left_buffer[(this.index + this.right_buffer.length - this.R) % this.right_buffer.length] * this.fb; + buffer[0][i] = this.left_buffer[this.index] = left; + buffer[1][i] = this.right_buffer[this.index] = right; + this.index += 1; + if (this.index >= this.left_buffer.length) { + this.index = 0; + } + } + }; + + return Delay; + +})(); + +Spatializer = (function() { + function Spatializer(engine) { + this.engine = engine; + this.sampleRate = this.engine.sampleRate; + this.left_buffer = new Float64Array(this.sampleRate / 10); + this.right_buffer = new Float64Array(this.sampleRate / 10); + this.index = 0; + this.left_delay1 = 0; + this.left_delay2 = 0; + this.right_delay1 = 0; + this.right_delay2 = 0; + this.left = 0; + this.right = 0; + this.left_delay1 = Math.round(this.sampleRate * 9.7913 / 340); + this.right_delay1 = Math.round(this.sampleRate * 11.1379 / 340); + this.left_delay2 = Math.round(this.sampleRate * 11.3179 / 340); + this.right_delay2 = Math.round(this.sampleRate * 12.7913 / 340); + } + + Spatializer.prototype.process = function(buffer, length, spatialize, pan) { + var i, left, left_buffer, left_pan, mnt, mnt2, o, ref, right, right_buffer, right_pan; + mnt = spatialize; + mnt2 = mnt * mnt; + left_pan = Math.cos(pan * Math.PI / 2) / (1 + spatialize); + right_pan = Math.sin(pan * Math.PI / 2) / (1 + spatialize); + left_buffer = buffer[0]; + right_buffer = buffer[1]; + for (i = o = 0, ref = length - 1; o <= ref; i = o += 1) { + left = left_buffer[i]; + right = right_buffer[i]; + this.left = left * .5 + this.left * .5; + this.right = right * .5 + this.right * .5; + this.left_buffer[this.index] = this.left; + this.right_buffer[this.index] = this.right; + left_buffer[i] = (left - mnt * this.right_buffer[(this.index + this.right_buffer.length - this.left_delay1) % this.right_buffer.length]) * left_pan; + right_buffer[i] = (right - mnt * this.left_buffer[(this.index + this.right_buffer.length - this.right_delay1) % this.right_buffer.length]) * right_pan; + this.index += 1; + if (this.index >= this.left_buffer.length) { + this.index = 0; + } + } + }; + + return Spatializer; + +})(); + +EQ = (function() { + function EQ(engine) { + var alpha, cosw0, invOnePlusAlpha, oneLessCosw0, q, sinw0, w0; + this.engine = engine; + this.sampleRate = this.engine.sampleRate; + this.mid = 900; + q = .5; + w0 = 2 * Math.PI * this.mid / this.sampleRate; + cosw0 = Math.cos(w0); + sinw0 = Math.sin(w0); + alpha = sinw0 / (2 * q); + invOnePlusAlpha = 1 / (1 + alpha); + oneLessCosw0 = 1 - cosw0; + this.a0 = (-2 * cosw0) * invOnePlusAlpha; + this.a1 = (1 - alpha) * invOnePlusAlpha; + this.b0 = q * alpha * invOnePlusAlpha; + this.b1 = 0; + this.b2 = -this.b0; + this.llow = 0; + this.rlow = 0; + this.lfm0 = 0; + this.lfm1 = 0; + this.rfm0 = 0; + this.rfm1 = 0; + this.low = 1; + this.mid = 1; + this.high = 1; + } + + EQ.prototype.update = function(data) { + this.low = data.low * 2; + this.mid = data.mid * 2; + return this.high = data.high * 2; + }; + + EQ.prototype.processBandPass = function(sig, cutoff, q) { + var w; + w = sig - a0 * this.fm0 - a1 * this.fm1; + sig = b0 * w + b1 * this.fm0 + b2 * this.fm1; + this.fm1 = this.fm0; + this.fm0 = w; + return sig; + }; + + EQ.prototype.process = function(buffer, length) { + var i, left, lhigh, lmid, lw, o, ref, rhigh, right, rmid, rw; + for (i = o = 0, ref = length - 1; o <= ref; i = o += 1) { + left = buffer[0][i]; + right = buffer[1][i]; + lw = left - this.a0 * this.lfm0 - this.a1 * this.lfm1; + lmid = this.b0 * lw + this.b1 * this.lfm0 + this.b2 * this.lfm1; + this.lfm1 = this.lfm0; + this.lfm0 = lw; + left -= lmid; + this.llow = left * .1 + this.llow * .9; + lhigh = left - this.llow; + buffer[0][i] = this.llow * this.low + lmid * this.mid + lhigh * this.high; + rw = right - this.a0 * this.rfm0 - this.a1 * this.rfm1; + rmid = this.b0 * rw + this.b1 * this.rfm0 + this.b2 * this.rfm1; + this.rfm1 = this.rfm0; + this.rfm0 = rw; + right -= rmid; + this.rlow = right * .1 + this.rlow * .9; + rhigh = right - this.rlow; + buffer[1][i] = this.rlow * this.low + rmid * this.mid + rhigh * this.high; + } + }; + + return EQ; + +})(); + +CombFilter = (function() { + function CombFilter(length1, feedback1) { + this.length = length1; + this.feedback = feedback1 != null ? feedback1 : .5; + this.buffer = new Float64Array(this.length); + this.index = 0; + this.store = 0; + this.damp = .2; + } + + CombFilter.prototype.process = function(input) { + var output; + output = this.buffer[this.index]; + this.store = output * (1 - this.damp) + this.store * this.damp; + this.buffer[this.index++] = input + this.store * this.feedback; + if (this.index >= this.length) { + this.index = 0; + } + return output; + }; + + return CombFilter; + +})(); + +AllpassFilter = (function() { + function AllpassFilter(length1, feedback1) { + this.length = length1; + this.feedback = feedback1 != null ? feedback1 : .5; + this.buffer = new Float64Array(this.length); + this.index = 0; + } + + AllpassFilter.prototype.process = function(input) { + var bufout, output; + bufout = this.buffer[this.index]; + output = -input + bufout; + this.buffer[this.index++] = input + bufout * this.feedback; + if (this.index >= this.length) { + this.index = 0; + } + return output; + }; + + return AllpassFilter; + +})(); + +Reverb = (function() { + function Reverb(engine) { + var a, allpasstuning, c, combtuning, len, len1, o, stereospread, u; + this.engine = engine; + this.sampleRate = this.engine.sampleRate; + combtuning = [1116, 1188, 1277, 1356, 1422, 1491, 1557, 1617]; + allpasstuning = [556, 441, 341, 225]; + stereospread = 23; + this.left_combs = []; + this.right_combs = []; + this.left_allpass = []; + this.right_allpass = []; + this.res = [0, 0]; + this.spread = .25; + this.wet = .025; + for (o = 0, len = combtuning.length; o < len; o++) { + c = combtuning[o]; + this.left_combs.push(new CombFilter(c, .9)); + this.right_combs.push(new CombFilter(c + stereospread, .9)); + } + for (u = 0, len1 = allpasstuning.length; u < len1; u++) { + a = allpasstuning[u]; + this.left_allpass.push(new AllpassFilter(a, .5)); + this.right_allpass.push(new AllpassFilter(a + stereospread, .5)); + } + } + + Reverb.prototype.update = function(data) { + var damp, feedback, i, o, ref; + this.wet = data.amount * .05; + feedback = .7 + Math.pow(data.rate, .25) * .29; + damp = .2; + for (i = o = 0, ref = this.left_combs.length - 1; o <= ref; i = o += 1) { + this.left_combs[i].feedback = feedback; + this.right_combs[i].feedback = feedback; + this.left_combs[i].damp = damp; + this.right_combs[i].damp = damp; + } + return this.spread = .5 - data.rate * .5; + }; + + Reverb.prototype.process = function(buffer, length) { + var i, input, left, o, outL, outR, ref, ref1, ref2, right, s, u, y; + for (s = o = 0, ref = length - 1; o <= ref; s = o += 1) { + outL = 0; + outR = 0; + left = buffer[0][s]; + right = buffer[1][s]; + input = (left + right) * .5; + for (i = u = 0, ref1 = this.left_combs.length - 1; 0 <= ref1 ? u <= ref1 : u >= ref1; i = 0 <= ref1 ? ++u : --u) { + outL += this.left_combs[i].process(input); + outR += this.right_combs[i].process(input); + } + for (i = y = 0, ref2 = this.left_allpass.length - 1; 0 <= ref2 ? y <= ref2 : y >= ref2; i = 0 <= ref2 ? ++y : --y) { + outL = this.left_allpass[i].process(outL); + outR = this.right_allpass[i].process(outR); + } + buffer[0][s] = (outL * (1 - this.spread) + outR * this.spread) * this.wet + left * (1 - this.wet); + buffer[1][s] = (outR * (1 - this.spread) + outL * this.spread) * this.wet + right * (1 - this.wet); + } + }; + + return Reverb; + +})(); + +Voice = (function() { + Voice.oscillators = [SawOscillator, SquareOscillator, SineOscillator, VoiceOscillator, StringOscillator]; + + function Voice(engine) { + this.engine = engine; + this.osc1 = new SineOscillator(this); + this.osc2 = new SineOscillator(this); + this.noise = new Noise(this); + this.lfo1 = new LFO(this); + this.lfo2 = new LFO(this); + this.filter = new Filter(this); + this.env1 = new AmpEnvelope(this); + this.env2 = new ModEnvelope(this); + this.filter_increment = 0.0005 * 44100 / this.engine.sampleRate; + this.modulation = 0; + this.noteon_time = 0; + } + + Voice.prototype.init = function(layer) { + if (!(this.osc1 instanceof Voice.oscillators[layer.inputs.osc1.type])) { + this.osc1 = new Voice.oscillators[layer.inputs.osc1.type](this); + } + if (!(this.osc2 instanceof Voice.oscillators[layer.inputs.osc2.type])) { + this.osc2 = new Voice.oscillators[layer.inputs.osc2.type](this); + } + this.osc1.init(layer.inputs.osc1, layer.inputs.sync); + this.osc2.init(layer.inputs.osc2, layer.inputs.sync); + this.noise.init(layer, layer.inputs.sync); + this.lfo1.init(layer.inputs.lfo1, layer.inputs.sync); + this.lfo2.init(layer.inputs.lfo2, layer.inputs.sync); + this.filter.init(layer); + this.env1.init(layer.inputs.env1); + this.env2.init(layer.inputs.env2); + return this.updateConstantMods(); + }; + + Voice.prototype.update = function() { + if (this.layer == null) { + return; + } + if (!(this.osc1 instanceof Voice.oscillators[this.layer.inputs.osc1.type])) { + this.osc1 = new Voice.oscillators[this.layer.inputs.osc1.type](this, this.layer.inputs.osc1); + } + if (!(this.osc2 instanceof Voice.oscillators[this.layer.inputs.osc2.type])) { + this.osc2 = new Voice.oscillators[this.layer.inputs.osc2.type](this, this.layer.inputs.osc2); + } + this.osc1.update(this.layer.inputs.osc1, this.layer.inputs.sync); + this.osc2.update(this.layer.inputs.osc2, this.layer.inputs.sync); + this.env1.update(); + this.env2.update(); + this.lfo1.update(); + this.lfo2.update(); + this.filter.update(); + return this.updateConstantMods(); + }; + + Voice.prototype.updateConstantMods = function() { + var a, amp, c, mod, norm, p; + this.osc1_amp = DBSCALE(this.inputs.osc1.amp, 3); + this.osc2_amp = DBSCALE(this.inputs.osc2.amp, 3); + this.osc1_mod = this.inputs.osc1.mod; + this.osc2_mod = this.inputs.osc2.mod; + this.noise_amp = DBSCALE(this.inputs.noise.amp, 3); + this.noise_mod = this.inputs.noise.mod; + if (this.inputs.velocity.amp > 0) { + p = this.inputs.velocity.amp; + p *= p; + p *= 4; + norm = this.inputs.velocity.amp * 4; + amp = Math.exp(this.velocity * norm) / Math.exp(norm); + } else { + amp = 1; + } + this.osc1_amp *= amp; + this.osc2_amp *= amp; + this.noise_amp *= amp; + c = Math.log(1024 * this.freq / 22000) / (10 * Math.log(2)); + this.cutoff_keymod = (c - .5) * this.inputs.filter.follow; + this.cutoff_base = this.inputs.filter.cutoff + this.cutoff_keymod; + this.env2_amount = this.inputs.env2.amount; + this.lfo1_rate = this.inputs.lfo1.rate; + this.lfo1_amount = this.inputs.lfo1.amount; + this.lfo2_rate = this.inputs.lfo2.rate; + this.lfo2_amount = this.inputs.lfo2.amount; + switch (this.inputs.velocity.out) { + case 0: + mod = this.velocity * (this.inputs.velocity.amount - .5) * 2; + this.osc1_mod = Math.min(1, Math.max(0, this.osc1_mod + mod)); + this.osc2_mod = Math.min(1, Math.max(0, this.osc2_mod + mod)); + break; + case 1: + mod = this.velocity * (this.inputs.velocity.amount - .5) * 2; + this.cutoff_base += mod; + break; + case 3: + mod = this.velocity * (this.inputs.velocity.amount - .5) * 2; + this.osc1_mod = Math.min(1, Math.max(0, this.osc1_mod + mod)); + break; + case 4: + mod = this.velocity * (this.inputs.velocity.amount - .5) * 2; + this.osc1_amp = Math.max(0, this.osc1_amp + mod); + break; + case 5: + mod = this.velocity * (this.inputs.velocity.amount - .5) * 2; + this.osc2_mod = Math.min(1, Math.max(0, this.osc2_mod + mod)); + break; + case 6: + mod = this.velocity * (this.inputs.velocity.amount - .5) * 2; + this.osc2_amp = Math.max(0, this.osc2_amp + mod); + break; + case 7: + mod = this.velocity * (this.inputs.velocity.amount - .5) * 2; + this.noise_amp = Math.max(0, this.noise_amp + mod); + break; + case 8: + mod = this.velocity * (this.inputs.velocity.amount - .5) * 2; + this.noise_mod = Math.min(1, Math.max(0, this.noise_mod + mod)); + break; + case 9: + mod = this.velocity * (this.inputs.velocity.amount - .5) * 2; + a = Math.max(0, Math.min(this.inputs.env1.a + mod, 1)); + this.env1.update(a); + break; + case 10: + mod = this.velocity * (this.inputs.velocity.amount - .5) * 2; + a = Math.max(0, Math.min(this.inputs.env2.a + mod, 1)); + this.env2.update(a); + break; + case 11: + mod = this.velocity * (this.inputs.velocity.amount - .5) * 2; + this.env2_amount = Math.max(0, Math.min(1, this.env2_amount + mod)); + break; + case 12: + mod = this.velocity * (this.inputs.velocity.amount - .5) * 2; + this.lfo1_amount = Math.max(0, this.lfo1_amount + mod); + break; + case 13: + mod = this.velocity * (this.inputs.velocity.amount - .5) * 2; + this.lfo1_rate = Math.min(1, Math.max(0, this.lfo1_rate + mod)); + break; + case 14: + mod = this.velocity * (this.inputs.velocity.amount - .5) * 2; + this.lfo2_amount = Math.max(0, this.lfo2_amount + mod); + break; + case 15: + mod = this.velocity * (this.inputs.velocity.amount - .5) * 2; + this.lfo2_rate = Math.min(1, Math.max(0, this.lfo2_rate + mod)); + } + if (this.freq != null) { + c = Math.log(1024 * this.freq / 22000) / (10 * Math.log(2)); + return this.cutoff_keymod = (c - .5) * this.inputs.filter.follow; + } + }; + + Voice.prototype.noteOn = function(layer, key1, velocity, legato) { + var glide_time; + this.key = key1; + if (legato == null) { + legato = false; + } + this.velocity = velocity / 127; + if (this.layer != null) { + this.layer.removeVoice(this); + } + this.layer = layer; + this.inputs = this.layer.inputs; + if (legato && this.on) { + this.freq = 440 * Math.pow(Math.pow(2, 1 / 12), this.key - 57); + if (layer.last_key != null) { + this.glide_from = layer.last_key; + this.glide = true; + this.glide_phase = 0; + glide_time = (this.inputs.glide * .025 + Math.pow(this.inputs.glide, 16) * .975) * 10; + this.glide_inc = 1 / (glide_time * this.engine.sampleRate + 1); + } + } else { + this.freq = 440 * Math.pow(Math.pow(2, 1 / 12), this.key - 57); + this.init(this.layer); + this.on = true; + this.cutoff = 0; + this.modulation = this.layer.instrument.modulation; + this.pitch_bend = this.layer.instrument.pitch_bend; + this.modulation_v = 0; + this.pitch_bend_v = 0; + } + return this.noteon_time = Date.now(); + }; + + Voice.prototype.noteOff = function() { + return this.on = false; + }; + + Voice.prototype.process = function() { + var cutoff, f, k, lfo1_amount, lfo1_rate, lfo2_amount, lfo2_rate, mod, noise_amp, noise_mod, osc1_amp, osc1_freq, osc1_mod, osc2_amp, osc2_freq, osc2_mod, p, q, s1, s2, sig; + osc1_mod = this.osc1_mod; + osc2_mod = this.osc2_mod; + osc1_amp = this.osc1_amp; + osc2_amp = this.osc2_amp; + if (this.glide) { + k = this.glide_from * (1 - this.glide_phase) + this.key * this.glide_phase; + osc1_freq = osc2_freq = 440 * Math.pow(Math.pow(2, 1 / 12), k - 57); + this.glide_phase += this.glide_inc; + if (this.glide_phase >= 1) { + this.glide = false; + } + } else { + osc1_freq = osc2_freq = this.freq; + } + if (Math.abs(this.pitch_bend - this.layer.instrument.pitch_bend) > .0001) { + this.pitch_bend_v += .001 * (this.layer.instrument.pitch_bend - this.pitch_bend); + this.pitch_bend_v *= .5; + this.pitch_bend += this.pitch_bend_v; + } + if (Math.abs(this.pitch_bend - .5) > .0001) { + p = this.pitch_bend * 2 - 1; + p *= 2; + f = Math.pow(Math.pow(2, 1 / 12), p); + osc1_freq *= f; + osc2_freq *= f; + } + noise_amp = this.noise_amp; + noise_mod = this.noise_mod; + lfo1_rate = this.lfo1_rate; + lfo1_amount = this.lfo1_amount; + lfo2_rate = this.lfo2_rate; + lfo2_amount = this.lfo2_amount; + cutoff = this.cutoff_base; + q = this.inputs.filter.resonance; + if (Math.abs(this.modulation - this.layer.instrument.modulation) > .0001) { + this.modulation_v += .001 * (this.layer.instrument.modulation - this.modulation); + this.modulation_v *= .5; + this.modulation += this.modulation_v; + } + switch (this.inputs.modulation.out) { + case 0: + mod = (this.inputs.modulation.amount - .5) * 2 * this.modulation; + osc1_amp = Math.max(0, osc1_amp + mod); + osc2_amp = Math.max(0, osc2_amp + mod); + noise_amp = Math.max(0, noise_amp * (1 + mod)); + break; + case 1: + mod = (this.inputs.modulation.amount - .5) * 2 * this.modulation; + osc1_mod = Math.min(1, Math.max(0, osc1_mod + mod)); + osc2_mod = Math.min(1, Math.max(0, osc2_mod + mod)); + break; + case 2: + mod = (this.inputs.modulation.amount - .5) * 2 * this.modulation; + osc1_amp = Math.max(0, osc1_amp + mod); + break; + case 3: + mod = (this.inputs.modulation.amount - .5) * 2 * this.modulation; + osc1_mod = Math.min(1, Math.max(0, osc1_mod + mod)); + break; + case 4: + mod = (this.inputs.modulation.amount - .5) * 2 * this.modulation; + osc2_amp = Math.max(0, osc2_amp + mod); + break; + case 5: + mod = (this.inputs.modulation.amount - .5) * 2 * this.modulation; + osc2_mod = Math.min(1, Math.max(0, osc2_mod + mod)); + break; + case 6: + mod = (this.inputs.modulation.amount - .5) * 2 * this.modulation; + noise_amp = Math.max(0, noise_amp + mod); + break; + case 7: + mod = (this.inputs.modulation.amount - .5) * 2 * this.modulation; + noise_mod = Math.min(1, Math.max(0, noise_mod + mod)); + break; + case 8: + mod = (this.inputs.modulation.amount - .5) * 2 * this.modulation; + cutoff += mod; + break; + case 9: + mod = (this.inputs.modulation.amount - .5) * 2 * this.modulation; + q = Math.max(0, Math.min(1, q + mod)); + break; + case 10: + mod = (this.inputs.modulation.amount - .5) * 2 * this.modulation; + lfo1_amount = Math.max(0, Math.min(1, lfo1_amount + mod)); + break; + case 11: + mod = (this.inputs.modulation.amount - .5) * 2 * this.modulation; + lfo1_rate = Math.max(0, Math.min(1, lfo1_rate + mod)); + break; + case 12: + mod = (this.inputs.modulation.amount - .5) * 2 * this.modulation; + lfo2_amount = Math.max(0, Math.min(1, lfo2_amount + mod)); + break; + case 13: + mod = (this.inputs.modulation.amount - .5) * 2 * this.modulation; + lfo2_rate = Math.max(0, Math.min(1, lfo2_rate + mod)); + } + switch (this.inputs.env2.out) { + case 0: + cutoff += this.env2.process(this.on) * (this.env2_amount * 2 - 1); + break; + case 1: + q = Math.max(0, Math.min(1, this.env2.process(this.on) * (this.env2_amount * 2 - 1))); + break; + case 2: + mod = this.env2_amount * 2 - 1; + mod *= this.env2.process(this.on); + mod = 1 + mod; + osc1_freq *= mod; + osc2_freq *= mod; + break; + case 3: + mod = this.env2.process(this.on) * (this.env2_amount * 2 - 1); + osc1_mod = Math.min(1, Math.max(0, osc1_mod + mod)); + osc2_mod = Math.min(1, Math.max(0, osc2_mod + mod)); + break; + case 4: + mod = this.env2.process(this.on) * (this.env2_amount * 2 - 1); + osc1_amp = Math.max(0, osc1_amp + mod); + osc2_amp = Math.max(0, osc2_amp + mod); + noise_amp = Math.max(0, noise_amp * (1 + mod)); + break; + case 5: + mod = this.env2_amount * 2 - 1; + mod *= this.env2.process(this.on); + osc1_freq *= 1 + mod; + break; + case 6: + mod = this.env2.process(this.on) * (this.env2_amount * 2 - 1); + osc1_mod = Math.min(1, Math.max(0, osc1_mod + mod)); + break; + case 7: + mod = this.env2.process(this.on) * (this.env2_amount * 2 - 1); + osc1_amp = Math.max(0, osc1_amp + mod); + break; + case 8: + mod = this.env2_amount * 2 - 1; + mod *= this.env2.process(this.on); + osc1_freq *= 1 + mod; + break; + case 9: + mod = this.env2.process(this.on) * (this.env2_amount * 2 - 1); + osc2_mod = Math.min(1, Math.max(0, osc2_mod + mod)); + break; + case 10: + mod = this.env2.process(this.on) * (this.env2_amount * 2 - 1); + osc2_amp = Math.max(0, osc2_amp + mod); + break; + case 11: + mod = this.env2.process(this.on) * (this.env2_amount * 2 - 1); + noise_amp = Math.max(0, noise_amp + mod); + break; + case 12: + mod = this.env2.process(this.on) * (this.env2_amount * 2 - 1); + noise_mod = Math.min(1, Math.max(0, noise_mod + mod)); + break; + case 13: + mod = this.env2.process(this.on) * (this.env2_amount * 2 - 1); + lfo1_amount = Math.min(1, Math.max(0, lfo1_amount + mod)); + break; + case 14: + mod = this.env2.process(this.on) * (this.env2_amount * 2 - 1); + lfo1_rate = Math.min(1, Math.max(0, lfo1_rate + mod)); + break; + case 15: + mod = this.env2.process(this.on) * (this.env2_amount * 2 - 1); + lfo2_amount = Math.min(1, Math.max(0, lfo2_amount + mod)); + break; + case 16: + mod = this.env2.process(this.on) * (this.env2_amount * 2 - 1); + lfo2_rate = Math.min(1, Math.max(0, lfo2_rate + mod)); + } + switch (this.inputs.lfo1.out) { + case 0: + mod = lfo1_amount; + if (this.inputs.lfo1.audio) { + mod = 1 + mod * mod * this.lfo1.process(lfo1_rate) * 16; + } else { + mod = 1 + mod * mod * this.lfo1.process(lfo1_rate); + } + osc1_freq *= mod; + osc2_freq *= mod; + break; + case 1: + mod = this.lfo1.process(lfo1_rate) * lfo1_amount; + osc1_mod = Math.min(1, Math.max(0, osc1_mod + mod)); + osc2_mod = Math.min(1, Math.max(0, osc2_mod + mod)); + break; + case 2: + mod = this.lfo1.process(lfo1_rate) * lfo1_amount; + osc1_amp = Math.max(0, osc1_amp + mod); + osc2_amp = Math.max(0, osc2_amp + mod); + noise_amp = Math.max(0, noise_amp * (1 + mod)); + break; + case 3: + mod = lfo1_amount; + mod = 1 + mod * mod * this.lfo1.process(lfo1_rate); + osc1_freq *= mod; + break; + case 4: + mod = this.lfo1.process(lfo1_rate) * lfo1_amount; + osc1_mod = Math.min(1, Math.max(0, osc1_mod + mod)); + break; + case 5: + mod = this.lfo1.process(lfo1_rate) * lfo1_amount; + osc1_amp = Math.max(0, osc1_amp + mod); + break; + case 6: + mod = lfo1_amount; + mod = 1 + mod * mod * this.lfo1.process(lfo1_rate); + osc2_freq *= mod; + break; + case 7: + mod = this.lfo1.process(lfo1_rate) * lfo1_amount; + osc2_mod = Math.min(1, Math.max(0, osc2_mod + mod)); + break; + case 8: + mod = this.lfo1.process(lfo1_rate) * lfo1_amount; + osc2_amp = Math.max(0, osc2_amp + mod); + break; + case 9: + mod = this.lfo1.process(lfo1_rate) * lfo1_amount; + noise_amp = Math.max(0, noise_amp + mod); + break; + case 10: + mod = this.lfo1.process(lfo1_rate) * lfo1_amount; + noise_mod = Math.min(1, Math.max(0, noise_mod + mod)); + break; + case 11: + cutoff += this.lfo1.process(lfo1_rate) * lfo1_amount; + break; + case 12: + q = Math.max(0, Math.min(1, this.lfo1.process(lfo1_rate) * lfo1_amount)); + break; + case 13: + mod = this.lfo1.process(lfo1_rate) * lfo1_amount; + lfo2_amount = Math.min(1, Math.max(0, lfo2_amount + mod)); + break; + case 14: + mod = this.lfo1.process(lfo1_rate) * lfo1_amount; + lfo2_rate = Math.min(1, Math.max(0, lfo2_rate + mod)); + } + switch (this.inputs.lfo2.out) { + case 0: + mod = lfo2_amount; + if (this.inputs.lfo2.audio) { + mod = 1 + mod * mod * this.lfo2.process(lfo2_rate) * 16; + } else { + mod = 1 + mod * mod * this.lfo2.process(lfo2_rate); + } + osc1_freq *= mod; + osc2_freq *= mod; + break; + case 1: + mod = this.lfo2.process(lfo2_rate) * lfo2_amount; + osc1_mod = Math.min(1, Math.max(0, osc1_mod + mod)); + osc2_mod = Math.min(1, Math.max(0, osc2_mod + mod)); + break; + case 2: + mod = this.lfo2.process(lfo2_rate) * lfo2_amount; + osc1_amp = Math.max(0, osc1_amp + mod); + osc2_amp = Math.max(0, osc2_amp + mod); + noise_amp = Math.max(0, noise_amp * (1 + mod)); + break; + case 3: + mod = lfo2_amount; + mod = 1 + mod * mod * this.lfo2.process(lfo2_rate); + osc1_freq *= mod; + break; + case 4: + mod = this.lfo2.process(lfo2_rate) * lfo2_amount; + osc1_mod = Math.min(1, Math.max(0, osc1_mod + mod)); + break; + case 5: + mod = this.lfo2.process(lfo2_rate) * lfo2_amount; + osc1_amp = Math.max(0, osc1_amp + mod); + break; + case 6: + mod = lfo2_amount; + mod = 1 + mod * mod * this.lfo2.process(lfo2_rate); + osc2_freq *= mod; + break; + case 7: + mod = this.lfo2.process(lfo2_rate) * lfo2_amount; + osc2_mod = Math.min(1, Math.max(0, osc2_mod + mod)); + break; + case 8: + mod = this.lfo2.process(lfo2_rate) * lfo2_amount; + osc2_amp = Math.max(0, osc2_amp + mod); + break; + case 9: + mod = this.lfo2.process(lfo2_rate) * lfo2_amount; + noise_amp = Math.max(0, noise_amp + mod); + break; + case 10: + mod = this.lfo2.process(lfo2_rate) * lfo2_amount; + noise_mod = Math.min(1, Math.max(0, noise_mod + mod)); + break; + case 11: + cutoff += this.lfo2.process(lfo2_rate) * lfo2_amount; + break; + case 12: + q = Math.max(0, Math.min(1, this.lfo2.process(lfo2_rate) * lfo2_amount)); + } + switch (this.inputs.combine) { + case 1: + s1 = this.osc1.process(osc1_freq, osc1_mod) * osc1_amp; + s2 = this.osc2.process(osc2_freq, osc2_mod) * osc2_amp; + sig = (s1 + s2) * (s1 + s2); + break; + case 2: + sig = this.osc2.process(osc2_freq * Math.max(0, 1 - this.osc1.process(osc1_freq, osc1_mod) * osc1_amp), osc2_mod) * osc2_amp; + break; + default: + sig = this.osc1.process(osc1_freq, osc1_mod) * osc1_amp + this.osc2.process(osc2_freq, osc2_mod) * osc2_amp; + } + if (noise_amp > 0) { + sig += this.noise.process(noise_mod) * noise_amp; + } + mod = this.env2.process(this.on); + if (!this.cutoff) { + this.cutoff = cutoff; + } else { + if (this.cutoff < cutoff) { + this.cutoff += Math.min(cutoff - this.cutoff, this.filter_increment); + } else if (this.cutoff > cutoff) { + this.cutoff += Math.max(cutoff - this.cutoff, -this.filter_increment); + } + cutoff = this.cutoff; + } + cutoff = Math.pow(2, Math.max(0, Math.min(cutoff, 1)) * 10) * 22000 / 1024; + sig *= this.env1.process(this.on); + return sig = this.filter.process(sig, cutoff, q * q * 9.5 + .5); + }; + + return Voice; + +})(); + +Instrument = (function() { + function Instrument(engine) { + this.engine = engine; + this.layers = []; + this.layers.push(new Layer(this)); + this.modulation = 0; + this.pitch_bend = .5; + } + + Instrument.prototype.noteOn = function(key, velocity) { + var l, len, o, ref; + ref = this.layers; + for (o = 0, len = ref.length; o < len; o++) { + l = ref[o]; + l.noteOn(key, velocity); + } + }; + + Instrument.prototype.noteOff = function(key) { + var l, len, o, ref; + ref = this.layers; + for (o = 0, len = ref.length; o < len; o++) { + l = ref[o]; + l.noteOff(key); + } + }; + + Instrument.prototype.setModulation = function(modulation) { + this.modulation = modulation; + }; + + Instrument.prototype.setPitchBend = function(pitch_bend) { + this.pitch_bend = pitch_bend; + }; + + Instrument.prototype.process = function(length) { + var i, l, left, len, len1, o, ref, ref1, ref2, right, u, y; + if (false) { + this.layers[0].process(length); + return this.output = this.layers[0].output; + } else { + if ((this.output == null) || this.output[0].length < length) { + this.output = [new Float64Array(length), new Float64Array(length)]; + } + ref = this.layers; + for (o = 0, len = ref.length; o < len; o++) { + l = ref[o]; + l.process(length); + } + for (i = u = 0, ref1 = length - 1; u <= ref1; i = u += 1) { + left = 0; + right = 0; + ref2 = this.layers; + for (y = 0, len1 = ref2.length; y < len1; y++) { + l = ref2[y]; + left += l.output[0][i]; + right += l.output[1][i]; + } + this.output[0][i] = left; + this.output[1][i] = right; + } + } + }; + + return Instrument; + +})(); + +FX1 = [Distortion, BitCrusher, Chorus, Flanger, Phaser, Delay]; + +FX2 = [Delay, Reverb, Chorus, Flanger, Phaser]; + +Layer = (function() { + function Layer(instrument) { + this.instrument = instrument; + this.engine = this.instrument.engine; + this.voices = []; + this.eq = new EQ(this.engine); + this.spatializer = new Spatializer(this.engine); + this.inputs = { + osc1: { + type: 0, + tune: .5, + coarse: .5, + amp: .5, + mod: 0 + }, + osc2: { + type: 0, + tune: .5, + coarse: .5, + amp: .5, + mod: 0 + }, + combine: 0, + noise: { + amp: 0, + mod: 0 + }, + filter: { + cutoff: 1, + resonance: 0, + type: 0, + slope: 1, + follow: 0 + }, + disto: { + wet: 0, + drive: 0 + }, + bitcrusher: { + wet: 0, + drive: 0, + crush: 0 + }, + env1: { + a: 0, + d: 0, + s: 1, + r: 0 + }, + env2: { + a: .1, + d: .1, + s: .5, + r: .1, + out: 0, + amount: .5 + }, + lfo1: { + type: 0, + amount: 0, + rate: .5, + out: 0 + }, + lfo2: { + type: 0, + amount: 0, + rate: .5, + out: 0 + }, + fx1: { + type: -1, + amount: 0, + rate: 0 + }, + fx2: { + type: -1, + amount: 0, + rate: 0 + }, + eq: { + low: .5, + mid: .5, + high: .5 + }, + spatialize: .5, + pan: .5, + polyphony: 1, + glide: .5, + sync: 1, + velocity: { + out: 0, + amount: .5, + amp: .5 + }, + modulation: { + out: 0, + amount: .5 + } + }; + } + + Layer.prototype.noteOn = function(key, velocity) { + var voice; + if (this.inputs.polyphony === 1 && (this.last_voice != null) && this.last_voice.on) { + voice = this.last_voice; + voice.noteOn(this, key, velocity, true); + this.voices.push(voice); + } else { + voice = this.engine.getVoice(); + voice.noteOn(this, key, velocity); + this.voices.push(voice); + this.last_voice = voice; + } + return this.last_key = key; + }; + + Layer.prototype.removeVoice = function(voice) { + var index; + index = this.voices.indexOf(voice); + if (index >= 0) { + return this.voices.splice(index, 1); + } + }; + + Layer.prototype.noteOff = function(key) { + var len, o, ref, v; + ref = this.voices; + for (o = 0, len = ref.length; o < len; o++) { + v = ref[o]; + if (v.key === key) { + v.noteOff(); + } + } + }; + + Layer.prototype.update = function() { + if (this.inputs.fx1.type >= 0) { + if ((this.fx1 == null) || !(this.fx1 instanceof FX1[this.inputs.fx1.type])) { + this.fx1 = new FX1[this.inputs.fx1.type](this.engine); + } + } else { + this.fx1 = null; + } + if (this.inputs.fx2.type >= 0) { + if ((this.fx2 == null) || !(this.fx2 instanceof FX2[this.inputs.fx2.type])) { + this.fx2 = new FX2[this.inputs.fx2.type](this.engine); + } + } else { + this.fx2 = null; + } + if (this.fx1 != null) { + this.fx1.update(this.inputs.fx1); + } + if (this.fx2 != null) { + this.fx2.update(this.inputs.fx2); + } + return this.eq.update(this.inputs.eq); + }; + + Layer.prototype.process = function(length) { + var i, len, o, ref, ref1, ref2, sig, u, v, y; + if ((this.output == null) || this.output[0].length < length) { + this.output = [new Float64Array(length), new Float64Array(length)]; + } + for (i = o = ref = this.voices.length - 1; o >= 0; i = o += -1) { + v = this.voices[i]; + if (!v.on && v.env1.sig < .00001) { + v.env1.sig = 0; + this.removeVoice(v); + } + } + for (i = u = 0, ref1 = length - 1; u <= ref1; i = u += 1) { + sig = 0; + ref2 = this.voices; + for (y = 0, len = ref2.length; y < len; y++) { + v = ref2[y]; + sig += v.process(); + } + this.output[0][i] = sig; + this.output[1][i] = sig; + } + this.spatializer.process(this.output, length, this.inputs.spatialize, this.inputs.pan); + if (this.fx1 != null) { + this.fx1.process(this.output, length); + } + if (this.fx2 != null) { + this.fx2.process(this.output, length); + } + this.eq.process(this.output, length); + }; + + return Layer; + +})(); + +AudioEngine = (function() { + function AudioEngine(sampleRate) { + var i, o, ref; + this.sampleRate = sampleRate; + this.voices = []; + this.voice_index = 0; + this.num_voices = 8; + for (i = o = 0, ref = this.num_voices - 1; 0 <= ref ? o <= ref : o >= ref; i = 0 <= ref ? ++o : --o) { + this.voices[i] = new Voice(this); + } + this.instruments = []; + this.instruments.push(new Instrument(this)); + this.avg = 0; + this.samples = 0; + this.time = 0; + this.layer = { + inputs: this.inputs + }; + this.start = Date.now(); + } + + AudioEngine.prototype.event = function(data) { + var v; + if (data[0] === 144 && data[2] > 0) { + this.instruments[0].noteOn(data[1], data[2]); + } else if (data[0] === 128 || (data[0] === 144 && data[2] === 0)) { + this.instruments[0].noteOff(data[1]); + } else if (data[0] >= 176 && data[0] < 192 && data[1] === 1) { + this.instruments[0].setModulation(data[2] / 127); + } else if (data[0] >= 224 && data[0] < 240) { + v = data[1] + 128 * data[2]; + console.info("PB value=" + v); + if (v >= 8192) { + v = (v - 8192) / (16383 - 8192); + v = .5 + .5 * v; + } else { + v = .5 * v / 8192; + } + console.info("Pitch Bend = " + v); + this.instruments[0].setPitchBend(v); + } + }; + + AudioEngine.prototype.getTime = function() { + return (Date.now() - this.start) / 1000; + }; + + AudioEngine.prototype.getVoice = function() { + var best, i, o, ref, v; + best = this.voices[0]; + for (i = o = 1, ref = this.voices.length - 1; 1 <= ref ? o <= ref : o >= ref; i = 1 <= ref ? ++o : --o) { + v = this.voices[i]; + if (best.on) { + if (v.on) { + if (v.noteon_time < best.noteon_time) { + best = v; + } + } else { + best = v; + } + } else { + if (!v.on && v.env1.sig < best.env1.sig) { + best = v; + } + } + } + return best; + }; + + AudioEngine.prototype.updateVoices = function() { + var l, len, len1, o, ref, ref1, u, v; + ref = this.voices; + for (o = 0, len = ref.length; o < len; o++) { + v = ref[o]; + v.update(); + } + ref1 = this.instruments[0].layers; + for (u = 0, len1 = ref1.length; u < len1; u++) { + l = ref1[u]; + l.update(); + } + }; + + AudioEngine.prototype.process = function(inputs, outputs, parameters) { + var channel, i, inst, j, len, len1, len2, o, output, ref, ref1, ref2, res, sig, time, u, y, z; + output = outputs[0]; + time = Date.now(); + res = [0, 0]; + ref = this.instruments; + for (o = 0, len = ref.length; o < len; o++) { + inst = ref[o]; + inst.process(output[0].length); + } + for (i = u = 0, len1 = output.length; u < len1; i = ++u) { + channel = output[i]; + if (i < 2) { + for (j = y = 0, ref1 = channel.length - 1; y <= ref1; j = y += 1) { + sig = 0; + ref2 = this.instruments; + for (z = 0, len2 = ref2.length; z < len2; z++) { + inst = ref2[z]; + sig += inst.output[i][j]; + } + sig *= .125; + sig = sig < 0 ? -(1 - Math.exp(sig)) : 1 - Math.exp(-sig); + channel[j] = sig; + } + } + } + this.time += Date.now() - time; + this.samples += channel.length; + if (this.samples >= this.sampleRate) { + this.samples -= this.sampleRate; + console.info(this.time + " ms ; buffer size = " + channel.length); + this.time = 0; + } + }; + + return AudioEngine; + +})(); + +Blip = (function() { + function Blip() { + var i, norm, o, p, ref, ref1, u, x, y; + this.size = 512; + this.samples = new Float64Array(this.size + 1); + for (p = o = 1; o <= 31; p = o += 2) { + for (i = u = 0, ref = this.size; u <= ref; i = u += 1) { + x = (i / this.size - .5) * .5; + this.samples[i] += Math.sin(x * 2 * Math.PI * p) / p; + } + } + norm = this.samples[this.size]; + for (i = y = 0, ref1 = this.size; y <= ref1; i = y += 1) { + this.samples[i] /= norm; + } + } + + return Blip; + +})(); + + +class MyWorkletProcessor extends AudioWorkletProcessor { + constructor() { + super() + this.synth = new AudioEngine(sampleRate) + this.port.onmessage = (e) => { + console.info(e) + var data = JSON.parse(e.data) + if (data.name == "note") + { + this.synth.event(data.data) + } + else if (data.name == "param") + { + var value = data.value + var s = data.id.split(".") + data = this.synth.instruments[0].layers[0].inputs + while (s.length>1) + { + data = data[s.splice(0,1)[0]] + } + data[s[0]] = value + this.synth.updateVoices() + } + } + } + + process(inputs, outputs, parameters) { + this.synth.process(inputs,outputs,parameters) + return true + } +} + +registerProcessor('my-worklet-processor', MyWorkletProcessor) +; diff --git a/static/js/sound/synth/oscillator.coffee b/static/js/sound/synth/oscillator.coffee new file mode 100644 index 00000000..c85d2ea4 --- /dev/null +++ b/static/js/sound/synth/oscillator.coffee @@ -0,0 +1,31 @@ +class @Oscillator + constructor:()-> + @inputs = [{ + type: "select" + options: ["saw","square","sine","noise"] + value: "saw" + },{ + type: "signal" + } + ] + + getState:()-> + phase: 0 + + getProcessor:()-> + (context,type,freq,state,output)-> + switch type + when "saw" + sr = context.sampleRate + for i in [0..output.length-1] + output[i] = 1-2*state.phase + state.phase += freq[i]/sr + state.phase -= 1 if state.phase>=1 + + when "square" + sr = context.sampleRate + for i in [0..output.length-1] + output[i] = 1-2*state.phase + state.phase += freq[i]/sr + state.phase -= 1 if state.phase>=1 + return diff --git a/static/js/sound/synth/oscillator.js b/static/js/sound/synth/oscillator.js new file mode 100644 index 00000000..707177f9 --- /dev/null +++ b/static/js/sound/synth/oscillator.js @@ -0,0 +1,49 @@ +this.Oscillator = (function() { + function Oscillator() { + this.inputs = [ + { + type: "select", + options: ["saw", "square", "sine", "noise"], + value: "saw" + }, { + type: "signal" + } + ]; + } + + Oscillator.prototype.getState = function() { + return { + phase: 0 + }; + }; + + Oscillator.prototype.getProcessor = function() { + return function(context, type, freq, state, output) { + var i, j, k, ref, ref1, sr; + switch (type) { + case "saw": + sr = context.sampleRate; + for (i = j = 0, ref = output.length - 1; 0 <= ref ? j <= ref : j >= ref; i = 0 <= ref ? ++j : --j) { + output[i] = 1 - 2 * state.phase; + state.phase += freq[i] / sr; + if (state.phase >= 1) { + state.phase -= 1; + } + } + break; + case "square": + sr = context.sampleRate; + for (i = k = 0, ref1 = output.length - 1; 0 <= ref1 ? k <= ref1 : k >= ref1; i = 0 <= ref1 ? ++k : --k) { + output[i] = 1 - 2 * state.phase; + state.phase += freq[i] / sr; + if (state.phase >= 1) { + state.phase -= 1; + } + } + } + }; + }; + + return Oscillator; + +})(); diff --git a/static/js/sound/synth/synthtest.coffee b/static/js/sound/synth/synthtest.coffee new file mode 100644 index 00000000..2c3b9616 --- /dev/null +++ b/static/js/sound/synth/synthtest.coffee @@ -0,0 +1,553 @@ +class Synth + constructor:()-> + @notes = [] + @blip = new Blip() + @reverb = new Reverb() + @inputs = + osc1_tune: .52 + osc1_coarse: .5 + osc1_amp: .5 + + osc2_tune: .47 + osc2_coarse: 0 + osc2_amp: .5 + + noise: 0 + + filter_cutoff: 1 + filter_resonance: .5 + filter_type: 0 + filter_env_amount: .5 + filter_slope: 1 + + disto: + wet:0 + drive:0 + + bitcrusher: + wet: 0 + drive: 0 + crush: 0 + + env1: + a: 0 + d: 0 + s: 1 + r: 0 + + env2: + a: .1 + d: .1 + s: .5 + r: .1 + + lfo1: + form: 0 + amp: 0 + rate: .5 + + @avg = 0 + @samples = 0 + @time = 0 + @sampleRate = 44100 + @sin_table = [] + for i in [0..10000] + @sin_table[i] = Math.sin(i/10000*Math.PI*2) + + @noise_table = [] + for i in [0..44099] + @noise_table[i] = 2*Math.random()-1 + + event:(data)-> + if data[0] == 144 and data[2]>0 + note = + note: data[1] + freq: 440*Math.pow(Math.pow(2,1/12),data[1]-57) + phase1: Math.random() + phase2: Math.random() + sig1: 1 + sig2: 1 + velocity: data[2] + fm00: 0 + fm01: 0 + fm10: 0 + fm11: 0 + tick: 0 + env1: + phase: 0 + sig: 0 + amp: 0 + out_sig: [] + out_amp: [] + env2: + phase: 0 + sig: 0 + amp: 0 + out_sig: [] + out_amp: [] + + osc_out: [] + noise_out: [] + + noise: 0 + + lfo1: + phase: 0 + rnd1: Math.random()*2-1 + rnd2: Math.random()*2-1 + out:[] + + note.osc1 = + buffer: (0 for i in [0..31] by 1) + index: 0 + + note.osc2 = + buffer: (0 for i in [0..31] by 1) + index: 0 + + @notes.push note + console.info "note on: #{data[1]}" + else if data[0] == 128 or (data[0] == 144 and data[2] == 0) + console.info "note off: #{data[1]}" + for i in [@notes.length-1..0] by -1 + if @notes[i].note == data[1] + @notes[i].env1.phase = 3 + @notes[i].env2.phase = 3 + @notes[i].env1.release_sig = @notes[i].env1.sig + @notes[i].env2.release_sig = @notes[i].env2.sig + + noise:(note,length)-> + noise = note.noise + for i in [0..length-1] by 1 + r = 2*Math.random()-1 + r = ((r*r*r)+r)*.5 + if Math.abs(noise+r)<=1 + noise += r + else + noise -= r + note.noise_out[i] = noise + + note.noise = @noise_table[note.tick++%@noise_table.length] + + lfo:(params,mem,length)-> + # waveform: sine, triangle, saw1, square, saw2, smoothed random, random + for i in [0..length-1] by 1 + form = params.form*6 + iform = Math.floor(form) + aform = form-iform + switch iform + when 0 + p = mem.phase*10000 + pi = Math.floor(p) + pa = p-pi + sig = (@sin_table[pi]*(1-pa)+@sin_table[pi+1]*pa)*(1-aform)+aform*(if mem.phase<.5 then 1-Math.abs(1-mem.phase*4) else -(1-Math.abs(3-mem.phase*4))) + + when 1 + sig = (1-aform)*(if mem.phase<.5 then 1-Math.abs(1-mem.phase*4) else -(1-Math.abs(3-mem.phase*4)))+aform*(1-2*mem.phase) + + when 2 + sig = (1-aform)*(1-2*mem.phase)+aform*(if mem.phase<.5 then 1 else -1) + + when 3 + sig = (1-aform)*(if mem.phase<.5 then 1 else -1)+aform*(2*mem.phase-1) + + when 4 + a = mem.phase + a = a*(3*a-2*a*a) + sig = (1-aform)*(2*mem.phase-1)+aform*((1-a)*mem.rnd1+a*mem.rnd2) + + when 5 + a = mem.phase + a = a*(3*a-2*a*a) + sig = (1-aform)*((1-a)*mem.rnd1+a*mem.rnd2)+aform*mem.rnd1 + + when 6 + sig = mem.rnd1 + + rate = .1+params.rate*params.rate*20 + mem.phase += rate/@sampleRate + if mem.phase>=1 + mem.phase -= 1 + mem.rnd1 = mem.rnd2 + mem.rnd2 = Math.random()*2-1 + + mem.out[i] = sig*params.amp + return + + envelope:(env,note,length)-> + phase = note.phase + sig = note.sig + amp = note.amp + for i in [0..length-1] by 1 + if phase<1 + a = 1/(441000*env.a*env.a*env.a+1) + phase = Math.min(1,phase+a) + sig = phase + amp = (Math.exp(phase*2)-1)/(Math.exp(2)-1) + else if phase<2 + len = 1+env.d*env.d*env.d*44100*10 + a = 1/len + phase = Math.min(2,phase+a) + delta = phase-1 + sig = (1-delta)+delta*env.s + amp = env.s+(1-env.s)*(Math.exp(-delta*0.693147)-.5)*2 + else if phase>=3 + len = 1+env.r*env.r*env.r*44100*10 + f = Math.exp(Math.log(.5)/len) + amp *= f + a = 1/len + phase = Math.min(4,phase+a) + delta = phase-3 + sig = note.release_sig*(1-delta) + + note.out_sig[i] = sig + note.out_amp[i] = amp + + if phase>=3 and amp<.00001 + note.kill = true + + note.sig = sig + note.amp = amp + note.phase = phase + return + + oscillators:(note,length)-> + c1 = Math.round(@inputs.osc1_coarse*24)/24 + c2 = Math.round(@inputs.osc2_coarse*24)/24 + freq1 = note.freq*Math.pow(2,c1*2)*.5*Math.pow(Math.pow(2,1/12),@inputs.osc1_tune*2-1) + freq2 = note.freq*Math.pow(2,c2*2)*.5*Math.pow(Math.pow(2,1/12),@inputs.osc2_tune*2-1) + + for s in [0..length-1] by 1 + dp = freq1/@sampleRate*Math.pow(2,note.lfo1.out[s]) + note.phase1 += dp + if note.phase1>=1 + dp = (note.phase1-1)/dp + note.phase1 -= 1 + dp *= 16 + dpi = Math.floor(dp) + a = dp-dpi + index = note.osc1.index + for i in [0..31] by 1 + break if dpi>=512 + note.osc1.buffer[index] += -1+@blip.samples[dpi]*(1-a)+@blip.samples[dpi+1]*a + dpi += 16 + index = (index+1)%note.osc1.buffer.length + + sig1 = 1-2*note.phase1 + sig1 += note.osc1.buffer[note.osc1.index] + note.osc1.buffer[note.osc1.index] = 0 + note.osc1.index = (note.osc1.index+1)%note.osc1.buffer.length + + dp = freq2/@sampleRate*Math.pow(2,note.lfo1.out[s]) + note.phase2 += dp + if note.phase2>=1 + dp = (note.phase2-1)/dp + note.phase2 -= 1 + dp *= 16 + dpi = Math.floor(dp) + a = dp-dpi + index = note.osc2.index + for i in [0..31] by 1 + break if dpi>=512 + note.osc2.buffer[index] += -1+@blip.samples[dpi]*(1-a)+@blip.samples[dpi+1]*a + dpi += 16 + index = (index+1)%note.osc2.buffer.length + + sig2 = 1-2*note.phase2 + sig2 += note.osc2.buffer[note.osc2.index] + note.osc2.buffer[note.osc2.index] = 0 + note.osc2.index = (note.osc2.index+1)%note.osc2.buffer.length + + note.osc_out[s] = sig1*@inputs.osc1_amp+sig2*@inputs.osc2_amp + return + + processNoteBuffer:(note,buffer)-> + @envelope(@inputs.env1,note.env1,buffer.length) + @envelope(@inputs.env2,note.env2,buffer.length) + @lfo(@inputs.lfo1,note.lfo1,buffer.length) + @oscillators(note,buffer.length) + @noise(note,buffer.length) + + q = .5+Math.pow(@inputs.filter_resonance,2)*9.5 + + for i in [0..buffer.length-1] by 1 + sig = note.osc_out[i] + sig += note.noise_out[i]*@inputs.noise + sig *= note.env1.out_amp[i] + + # VCF + c = Math.max(0,Math.min(1,@inputs.filter_cutoff+(@inputs.filter_env_amount*2-1)*note.env2.out_sig[i])) + cutoff = Math.pow(2,c*10)*22000/1024 + + #w0 = 2*Math.PI*cutoff/44100 + #cosw0 = Math.cos(w0) + #sinw0 = Math.sin(w0) + + w0 = cutoff/44100 + w0 *= 10000 + iw0 = Math.floor(w0) + aw0 = w0-iw0 + cosw0 = (1-aw0)*@sin_table[iw0+2500]+aw0*@sin_table[iw0+2501] + sinw0 = (1-aw0)*@sin_table[iw0]+aw0*@sin_table[iw0+1] + + alpha = sinw0 / (2 * q) + onePlusAlpha = 1 + alpha + + oneLessCosw0 = 1 - cosw0 + lb1 = oneLessCosw0 / onePlusAlpha + lb0 = lb1*.5 + lb2 = lb0 + a0 = (-2 * cosw0) / onePlusAlpha + a1 = (1 - alpha) / onePlusAlpha + + qByAlpha = q * alpha + bb0 = qByAlpha / onePlusAlpha + bb1 = 0 + bb2 = -bb0 + + onePlusCosw0 = 1 + cosw0 + hb0 = onePlusCosw0*.5/onePlusAlpha + hb1 = -onePlusCosw0/onePlusAlpha + hb2 = hb0 + + a = @inputs.filter_type + if a<.5 + a *= 2 + b0 = lb0*(1-a)+a*bb0 + b1 = lb1*(1-a)+a*bb1 + b2 = lb2*(1-a)+a*bb2 + else + a = (a-.5)*2 + b0 = bb0*(1-a)+a*hb0 + b1 = bb1*(1-a)+a*hb1 + b2 = bb2*(1-a)+a*hb2 + + w = sig - a0*note.fm00 - a1*note.fm01 + sig1 = b0*w + b1*note.fm00 + b2*note.fm01 + + note.fm01 = note.fm00 + note.fm00 = w + + w = sig1 - a0*note.fm10 - a1*note.fm11 + sig2 = b0*w + b1*note.fm10 + b2*note.fm11 + + note.fm11 = note.fm10 + note.fm10 = w + + slope = @inputs.filter_slope + buffer[i] += sig1*(1-slope)+sig2*slope + return + + process:(inputs,outputs,parameters)-> + output = outputs[0] + time = Date.now() + res = [0,0] + for channel,i in output + if i==0 + for j in [0..channel.length-1] by 1 + channel[j] = 0 + + for n in @notes + @processNoteBuffer n,channel + + for j in [0..channel.length-1] by 1 + sum = channel[j] + sum *= .25 + @avg = @avg*.999+sum*.001 + sum -= @avg + + #disto + source = sum*(1+@inputs.disto.drive*9) + disto = if source<0 then -1+Math.exp(source) else 1-Math.exp(-source) + sum = (1-@inputs.disto.wet)*sum+@inputs.disto.wet*disto + + #bit crusher + source = sum*(1+@inputs.bitcrusher.drive*9) + source = (1-Math.exp(-source))/(1+Math.exp(-source)) + crush = 4-3*@inputs.bitcrusher.crush + sum = (1-@inputs.bitcrusher.wet)*sum+@inputs.bitcrusher.wet*Math.round(source*crush)/crush + + #sum = Math.round(sum*2)/2 # bit crusher + @reverb.get(res) + @reverb.push(sum) + s1 = sum #+res[0] + s2 = sum #+res[1] + + s1 = if s1<0 then -1+Math.exp(s1) else 1-Math.exp(-s1) #(1-Math.exp(-s1))/(1+Math.exp(-s1)) # disto / soft clipping + s2 = if s2<0 then -1+Math.exp(s2) else 1-Math.exp(-s2) #(1-Math.exp(-s2))/(1+Math.exp(-s2)) # disto / soft clipping + + channel[j] = s1 + output[1][j] = s2 + + @time += Date.now()-time + @samples += channel.length + if @samples >= 44100 + @samples = 0 + console.info @time+" ms ; buffer size = "+channel.length + @time = 0 + + for i in [@notes.length-1..0] by -1 + if @notes[i].env1.kill + @notes.splice(i,1) + return + +class Blip + constructor:()-> + @size = 512 + + @samples = [] + + for i in [0..@size] by 1 + @samples[i] = 0 + + for p in [1..31] by 2 + for i in [0..@size] by 1 + x = (i/@size-.5)*.5 + @samples[i] += Math.sin(x*2*Math.PI*p)/p + + norm = @samples[@size] + + for i in [0..@size] by 1 + @samples[i] /= norm + +class DelayLine + constructor:(@size,@damping=0)-> + @buffer = new Float64Array(@size) + @index = 0 + @last = 0 + @k = .3 + @damping = .5 + @v = 0 + @avg = 0 + @queue = [] + + cutoff = 300+3000*Math.random() + q = .1 + + w0 = cutoff/44100 + cosw0 = Math.cos(w0*Math.PI*2) + sinw0 = Math.sin(w0*Math.PI*2) + + alpha = sinw0 / (2 * q) + onePlusAlpha = 1 + alpha + + oneLessCosw0 = 1 - cosw0 + @a0 = (-2 * cosw0) / onePlusAlpha + @a1 = (1 - alpha) / onePlusAlpha + + qByAlpha = q * alpha + @b0 = qByAlpha / onePlusAlpha + @b1 = 0 + @b2 = -@b0 + + @m0 = 0 + @m1 = 0 + + push:(value)-> + w = value - @a0*@m0 - @a1*@m1 + sig = @b0*w + @b1*@m0 + @b2*@m1 + + @m1 = @m0 + @m0 = w + @buffer[@index] = sig*10 + @index = (@index+1)%@size + + pushOld:(value)-> + @v += (value-@last)*@k + @v *= @damping + @last += @v + @buffer[@index] = v = @last-@avg + @avg = @avg*.9+@last*.1 + @index = (@index+1)%@size + return + + get:()-> + @buffer[@index] + +class Reverb + constructor:()-> + @delays = [] + @delays.push new DelayLine(47*44+1,.0) + @delays.push new DelayLine(113*44+17,.0) + @delays.push new DelayLine(131*44+23,.0) + @delays.push new DelayLine(173*44+31,.0) + @delays.push new DelayLine(193*44+41,.0) + @delays.push new DelayLine(217*44+47,.0) + @delays.push new DelayLine(233*44+41,.0) + @delays.push new DelayLine(293*44+47,.0) + @avg = 0 + + get:(result)-> + d0 = @delays[0].get() + d1 = @delays[1].get() + d2 = @delays[2].get() + d3 = @delays[3].get() + d4 = @delays[4].get() + d5 = @delays[5].get() + d6 = @delays[6].get() + d7 = @delays[7].get() + + #result[0] = result[1] = d0+d1+d2+d3+d4+d5 + + result[0] = (d0*.23+d1*.13+d2*.41+d3*.13+d4*.37+d5*.11+d6*.22+d7*.05)*2 + result[1] = (d0*.07+d1*.29+d2*.05+d3*.43+d4*.19+d5*.29+d6*.04+d7*.24)*2 + + push:(value)-> + @avg = @avg*.8+value*.2 + value -= @avg + + d0 = @delays[0].get() + d1 = @delays[1].get() + d2 = @delays[2].get() + d3 = @delays[3].get() + d4 = @delays[4].get() + d5 = @delays[5].get() + d6 = @delays[6].get() + d7 = @delays[7].get() + + d = (d0+d1+d2+d3+d4+d5+d6+d7)/8*.1 + + @delays[0].push value+d+d0*.9 + @delays[1].push value+d+d1*.9 + @delays[2].push value+d+d2*.9 + @delays[3].push value+d+d3*.9 + @delays[4].push value+d+d4*.9 + @delays[5].push value+d+d5*.9 + @delays[6].push value+d+d6*.9 + @delays[7].push value+d+d7*.9 + return + +` +class MyWorkletProcessor extends AudioWorkletProcessor { + constructor() { + super() + this.synth = new Synth() + this.port.onmessage = (e) => { + console.info(e) + var data = JSON.parse(e.data) + if (data.name == "note") + { + this.synth.event(data.data) + } + else if (data.name == "param") + { + var value = data.value + var s = data.id.split(".") + data = this.synth.inputs + while (s.length>1) + { + data = data[s.splice(0,1)[0]] + } + data[s[0]] = value + } + } + } + + process(inputs, outputs, parameters) { + this.synth.process(inputs,outputs,parameters) + return true + } +} + +registerProcessor('my-worklet-processor', MyWorkletProcessor) +` diff --git a/static/js/sound/synth/synthtest.js b/static/js/sound/synth/synthtest.js new file mode 100644 index 00000000..37570533 --- /dev/null +++ b/static/js/sound/synth/synthtest.js @@ -0,0 +1,588 @@ +var Blip, DelayLine, Reverb, Synth; + +Synth = (function() { + function Synth() { + var i, k, l; + this.notes = []; + this.blip = new Blip(); + this.reverb = new Reverb(); + this.inputs = { + osc1_tune: .52, + osc1_coarse: .5, + osc1_amp: .5, + osc2_tune: .47, + osc2_coarse: 0, + osc2_amp: .5, + noise: 0, + filter_cutoff: 1, + filter_resonance: .5, + filter_type: 0, + filter_env_amount: .5, + filter_slope: 1, + disto: { + wet: 0, + drive: 0 + }, + bitcrusher: { + wet: 0, + drive: 0, + crush: 0 + }, + env1: { + a: 0, + d: 0, + s: 1, + r: 0 + }, + env2: { + a: .1, + d: .1, + s: .5, + r: .1 + }, + lfo1: { + form: 0, + amp: 0, + rate: .5 + } + }; + this.avg = 0; + this.samples = 0; + this.time = 0; + this.sampleRate = 44100; + this.sin_table = []; + for (i = k = 0; k <= 10000; i = ++k) { + this.sin_table[i] = Math.sin(i / 10000 * Math.PI * 2); + } + this.noise_table = []; + for (i = l = 0; l <= 44099; i = ++l) { + this.noise_table[i] = 2 * Math.random() - 1; + } + } + + Synth.prototype.event = function(data) { + var i, k, note, ref, results; + if (data[0] === 144 && data[2] > 0) { + note = { + note: data[1], + freq: 440 * Math.pow(Math.pow(2, 1 / 12), data[1] - 57), + phase1: Math.random(), + phase2: Math.random(), + sig1: 1, + sig2: 1, + velocity: data[2], + fm00: 0, + fm01: 0, + fm10: 0, + fm11: 0, + tick: 0, + env1: { + phase: 0, + sig: 0, + amp: 0, + out_sig: [], + out_amp: [] + }, + env2: { + phase: 0, + sig: 0, + amp: 0, + out_sig: [], + out_amp: [] + }, + osc_out: [], + noise_out: [], + noise: 0, + lfo1: { + phase: 0, + rnd1: Math.random() * 2 - 1, + rnd2: Math.random() * 2 - 1, + out: [] + } + }; + note.osc1 = { + buffer: (function() { + var k, results; + results = []; + for (i = k = 0; k <= 31; i = k += 1) { + results.push(0); + } + return results; + })(), + index: 0 + }; + note.osc2 = { + buffer: (function() { + var k, results; + results = []; + for (i = k = 0; k <= 31; i = k += 1) { + results.push(0); + } + return results; + })(), + index: 0 + }; + this.notes.push(note); + return console.info("note on: " + data[1]); + } else if (data[0] === 128 || (data[0] === 144 && data[2] === 0)) { + console.info("note off: " + data[1]); + results = []; + for (i = k = ref = this.notes.length - 1; k >= 0; i = k += -1) { + if (this.notes[i].note === data[1]) { + this.notes[i].env1.phase = 3; + this.notes[i].env2.phase = 3; + this.notes[i].env1.release_sig = this.notes[i].env1.sig; + results.push(this.notes[i].env2.release_sig = this.notes[i].env2.sig); + } else { + results.push(void 0); + } + } + return results; + } + }; + + Synth.prototype.noise = function(note, length) { + var i, k, noise, r, ref; + noise = note.noise; + for (i = k = 0, ref = length - 1; k <= ref; i = k += 1) { + r = 2 * Math.random() - 1; + r = ((r * r * r) + r) * .5; + if (Math.abs(noise + r) <= 1) { + noise += r; + } else { + noise -= r; + } + note.noise_out[i] = noise; + } + return note.noise = this.noise_table[note.tick++ % this.noise_table.length]; + }; + + Synth.prototype.lfo = function(params, mem, length) { + var a, aform, form, i, iform, k, p, pa, pi, rate, ref, sig; + for (i = k = 0, ref = length - 1; k <= ref; i = k += 1) { + form = params.form * 6; + iform = Math.floor(form); + aform = form - iform; + switch (iform) { + case 0: + p = mem.phase * 10000; + pi = Math.floor(p); + pa = p - pi; + sig = (this.sin_table[pi] * (1 - pa) + this.sin_table[pi + 1] * pa) * (1 - aform) + aform * (mem.phase < .5 ? 1 - Math.abs(1 - mem.phase * 4) : -(1 - Math.abs(3 - mem.phase * 4))); + break; + case 1: + sig = (1 - aform) * (mem.phase < .5 ? 1 - Math.abs(1 - mem.phase * 4) : -(1 - Math.abs(3 - mem.phase * 4))) + aform * (1 - 2 * mem.phase); + break; + case 2: + sig = (1 - aform) * (1 - 2 * mem.phase) + aform * (mem.phase < .5 ? 1 : -1); + break; + case 3: + sig = (1 - aform) * (mem.phase < .5 ? 1 : -1) + aform * (2 * mem.phase - 1); + break; + case 4: + a = mem.phase; + a = a * (3 * a - 2 * a * a); + sig = (1 - aform) * (2 * mem.phase - 1) + aform * ((1 - a) * mem.rnd1 + a * mem.rnd2); + break; + case 5: + a = mem.phase; + a = a * (3 * a - 2 * a * a); + sig = (1 - aform) * ((1 - a) * mem.rnd1 + a * mem.rnd2) + aform * mem.rnd1; + break; + case 6: + sig = mem.rnd1; + } + rate = .1 + params.rate * params.rate * 20; + mem.phase += rate / this.sampleRate; + if (mem.phase >= 1) { + mem.phase -= 1; + mem.rnd1 = mem.rnd2; + mem.rnd2 = Math.random() * 2 - 1; + } + mem.out[i] = sig * params.amp; + } + }; + + Synth.prototype.envelope = function(env, note, length) { + var a, amp, delta, f, i, k, len, phase, ref, sig; + phase = note.phase; + sig = note.sig; + amp = note.amp; + for (i = k = 0, ref = length - 1; k <= ref; i = k += 1) { + if (phase < 1) { + a = 1 / (441000 * env.a * env.a * env.a + 1); + phase = Math.min(1, phase + a); + sig = phase; + amp = (Math.exp(phase * 2) - 1) / (Math.exp(2) - 1); + } else if (phase < 2) { + len = 1 + env.d * env.d * env.d * 44100 * 10; + a = 1 / len; + phase = Math.min(2, phase + a); + delta = phase - 1; + sig = (1 - delta) + delta * env.s; + amp = env.s + (1 - env.s) * (Math.exp(-delta * 0.693147) - .5) * 2; + } else if (phase >= 3) { + len = 1 + env.r * env.r * env.r * 44100 * 10; + f = Math.exp(Math.log(.5) / len); + amp *= f; + a = 1 / len; + phase = Math.min(4, phase + a); + delta = phase - 3; + sig = note.release_sig * (1 - delta); + } + note.out_sig[i] = sig; + note.out_amp[i] = amp; + } + if (phase >= 3 && amp < .00001) { + note.kill = true; + } + note.sig = sig; + note.amp = amp; + note.phase = phase; + }; + + Synth.prototype.oscillators = function(note, length) { + var a, c1, c2, dp, dpi, freq1, freq2, i, index, k, l, m, ref, s, sig1, sig2; + c1 = Math.round(this.inputs.osc1_coarse * 24) / 24; + c2 = Math.round(this.inputs.osc2_coarse * 24) / 24; + freq1 = note.freq * Math.pow(2, c1 * 2) * .5 * Math.pow(Math.pow(2, 1 / 12), this.inputs.osc1_tune * 2 - 1); + freq2 = note.freq * Math.pow(2, c2 * 2) * .5 * Math.pow(Math.pow(2, 1 / 12), this.inputs.osc2_tune * 2 - 1); + for (s = k = 0, ref = length - 1; k <= ref; s = k += 1) { + dp = freq1 / this.sampleRate * Math.pow(2, note.lfo1.out[s]); + note.phase1 += dp; + if (note.phase1 >= 1) { + dp = (note.phase1 - 1) / dp; + note.phase1 -= 1; + dp *= 16; + dpi = Math.floor(dp); + a = dp - dpi; + index = note.osc1.index; + for (i = l = 0; l <= 31; i = l += 1) { + if (dpi >= 512) { + break; + } + note.osc1.buffer[index] += -1 + this.blip.samples[dpi] * (1 - a) + this.blip.samples[dpi + 1] * a; + dpi += 16; + index = (index + 1) % note.osc1.buffer.length; + } + } + sig1 = 1 - 2 * note.phase1; + sig1 += note.osc1.buffer[note.osc1.index]; + note.osc1.buffer[note.osc1.index] = 0; + note.osc1.index = (note.osc1.index + 1) % note.osc1.buffer.length; + dp = freq2 / this.sampleRate * Math.pow(2, note.lfo1.out[s]); + note.phase2 += dp; + if (note.phase2 >= 1) { + dp = (note.phase2 - 1) / dp; + note.phase2 -= 1; + dp *= 16; + dpi = Math.floor(dp); + a = dp - dpi; + index = note.osc2.index; + for (i = m = 0; m <= 31; i = m += 1) { + if (dpi >= 512) { + break; + } + note.osc2.buffer[index] += -1 + this.blip.samples[dpi] * (1 - a) + this.blip.samples[dpi + 1] * a; + dpi += 16; + index = (index + 1) % note.osc2.buffer.length; + } + } + sig2 = 1 - 2 * note.phase2; + sig2 += note.osc2.buffer[note.osc2.index]; + note.osc2.buffer[note.osc2.index] = 0; + note.osc2.index = (note.osc2.index + 1) % note.osc2.buffer.length; + note.osc_out[s] = sig1 * this.inputs.osc1_amp + sig2 * this.inputs.osc2_amp; + } + }; + + Synth.prototype.processNoteBuffer = function(note, buffer) { + var a, a0, a1, alpha, aw0, b0, b1, b2, bb0, bb1, bb2, c, cosw0, cutoff, hb0, hb1, hb2, i, iw0, k, lb0, lb1, lb2, oneLessCosw0, onePlusAlpha, onePlusCosw0, q, qByAlpha, ref, sig, sig1, sig2, sinw0, slope, w, w0; + this.envelope(this.inputs.env1, note.env1, buffer.length); + this.envelope(this.inputs.env2, note.env2, buffer.length); + this.lfo(this.inputs.lfo1, note.lfo1, buffer.length); + this.oscillators(note, buffer.length); + this.noise(note, buffer.length); + q = .5 + Math.pow(this.inputs.filter_resonance, 2) * 9.5; + for (i = k = 0, ref = buffer.length - 1; k <= ref; i = k += 1) { + sig = note.osc_out[i]; + sig += note.noise_out[i] * this.inputs.noise; + sig *= note.env1.out_amp[i]; + c = Math.max(0, Math.min(1, this.inputs.filter_cutoff + (this.inputs.filter_env_amount * 2 - 1) * note.env2.out_sig[i])); + cutoff = Math.pow(2, c * 10) * 22000 / 1024; + w0 = cutoff / 44100; + w0 *= 10000; + iw0 = Math.floor(w0); + aw0 = w0 - iw0; + cosw0 = (1 - aw0) * this.sin_table[iw0 + 2500] + aw0 * this.sin_table[iw0 + 2501]; + sinw0 = (1 - aw0) * this.sin_table[iw0] + aw0 * this.sin_table[iw0 + 1]; + alpha = sinw0 / (2 * q); + onePlusAlpha = 1 + alpha; + oneLessCosw0 = 1 - cosw0; + lb1 = oneLessCosw0 / onePlusAlpha; + lb0 = lb1 * .5; + lb2 = lb0; + a0 = (-2 * cosw0) / onePlusAlpha; + a1 = (1 - alpha) / onePlusAlpha; + qByAlpha = q * alpha; + bb0 = qByAlpha / onePlusAlpha; + bb1 = 0; + bb2 = -bb0; + onePlusCosw0 = 1 + cosw0; + hb0 = onePlusCosw0 * .5 / onePlusAlpha; + hb1 = -onePlusCosw0 / onePlusAlpha; + hb2 = hb0; + a = this.inputs.filter_type; + if (a < .5) { + a *= 2; + b0 = lb0 * (1 - a) + a * bb0; + b1 = lb1 * (1 - a) + a * bb1; + b2 = lb2 * (1 - a) + a * bb2; + } else { + a = (a - .5) * 2; + b0 = bb0 * (1 - a) + a * hb0; + b1 = bb1 * (1 - a) + a * hb1; + b2 = bb2 * (1 - a) + a * hb2; + } + w = sig - a0 * note.fm00 - a1 * note.fm01; + sig1 = b0 * w + b1 * note.fm00 + b2 * note.fm01; + note.fm01 = note.fm00; + note.fm00 = w; + w = sig1 - a0 * note.fm10 - a1 * note.fm11; + sig2 = b0 * w + b1 * note.fm10 + b2 * note.fm11; + note.fm11 = note.fm10; + note.fm10 = w; + slope = this.inputs.filter_slope; + buffer[i] += sig1 * (1 - slope) + sig2 * slope; + } + }; + + Synth.prototype.process = function(inputs, outputs, parameters) { + var channel, crush, disto, i, j, k, l, len1, len2, m, n, o, output, ref, ref1, ref2, ref3, res, s1, s2, source, sum, t, time; + output = outputs[0]; + time = Date.now(); + res = [0, 0]; + for (i = k = 0, len1 = output.length; k < len1; i = ++k) { + channel = output[i]; + if (i === 0) { + for (j = l = 0, ref = channel.length - 1; l <= ref; j = l += 1) { + channel[j] = 0; + } + ref1 = this.notes; + for (m = 0, len2 = ref1.length; m < len2; m++) { + n = ref1[m]; + this.processNoteBuffer(n, channel); + } + for (j = o = 0, ref2 = channel.length - 1; o <= ref2; j = o += 1) { + sum = channel[j]; + sum *= .25; + this.avg = this.avg * .999 + sum * .001; + sum -= this.avg; + source = sum * (1 + this.inputs.disto.drive * 9); + disto = source < 0 ? -1 + Math.exp(source) : 1 - Math.exp(-source); + sum = (1 - this.inputs.disto.wet) * sum + this.inputs.disto.wet * disto; + source = sum * (1 + this.inputs.bitcrusher.drive * 9); + source = (1 - Math.exp(-source)) / (1 + Math.exp(-source)); + crush = 4 - 3 * this.inputs.bitcrusher.crush; + sum = (1 - this.inputs.bitcrusher.wet) * sum + this.inputs.bitcrusher.wet * Math.round(source * crush) / crush; + this.reverb.get(res); + this.reverb.push(sum); + s1 = sum; + s2 = sum; + s1 = s1 < 0 ? -1 + Math.exp(s1) : 1 - Math.exp(-s1); + s2 = s2 < 0 ? -1 + Math.exp(s2) : 1 - Math.exp(-s2); + channel[j] = s1; + output[1][j] = s2; + } + } + } + this.time += Date.now() - time; + this.samples += channel.length; + if (this.samples >= 44100) { + this.samples = 0; + console.info(this.time + " ms ; buffer size = " + channel.length); + this.time = 0; + } + for (i = t = ref3 = this.notes.length - 1; t >= 0; i = t += -1) { + if (this.notes[i].env1.kill) { + this.notes.splice(i, 1); + } + } + }; + + return Synth; + +})(); + +Blip = (function() { + function Blip() { + var i, k, l, m, norm, o, p, ref, ref1, ref2, x; + this.size = 512; + this.samples = []; + for (i = k = 0, ref = this.size; k <= ref; i = k += 1) { + this.samples[i] = 0; + } + for (p = l = 1; l <= 31; p = l += 2) { + for (i = m = 0, ref1 = this.size; m <= ref1; i = m += 1) { + x = (i / this.size - .5) * .5; + this.samples[i] += Math.sin(x * 2 * Math.PI * p) / p; + } + } + norm = this.samples[this.size]; + for (i = o = 0, ref2 = this.size; o <= ref2; i = o += 1) { + this.samples[i] /= norm; + } + } + + return Blip; + +})(); + +DelayLine = (function() { + function DelayLine(size, damping) { + var alpha, cosw0, cutoff, oneLessCosw0, onePlusAlpha, q, qByAlpha, sinw0, w0; + this.size = size; + this.damping = damping != null ? damping : 0; + this.buffer = new Float64Array(this.size); + this.index = 0; + this.last = 0; + this.k = .3; + this.damping = .5; + this.v = 0; + this.avg = 0; + this.queue = []; + cutoff = 300 + 3000 * Math.random(); + q = .1; + w0 = cutoff / 44100; + cosw0 = Math.cos(w0 * Math.PI * 2); + sinw0 = Math.sin(w0 * Math.PI * 2); + alpha = sinw0 / (2 * q); + onePlusAlpha = 1 + alpha; + oneLessCosw0 = 1 - cosw0; + this.a0 = (-2 * cosw0) / onePlusAlpha; + this.a1 = (1 - alpha) / onePlusAlpha; + qByAlpha = q * alpha; + this.b0 = qByAlpha / onePlusAlpha; + this.b1 = 0; + this.b2 = -this.b0; + this.m0 = 0; + this.m1 = 0; + } + + DelayLine.prototype.push = function(value) { + var sig, w; + w = value - this.a0 * this.m0 - this.a1 * this.m1; + sig = this.b0 * w + this.b1 * this.m0 + this.b2 * this.m1; + this.m1 = this.m0; + this.m0 = w; + this.buffer[this.index] = sig * 10; + return this.index = (this.index + 1) % this.size; + }; + + DelayLine.prototype.pushOld = function(value) { + var v; + this.v += (value - this.last) * this.k; + this.v *= this.damping; + this.last += this.v; + this.buffer[this.index] = v = this.last - this.avg; + this.avg = this.avg * .9 + this.last * .1; + this.index = (this.index + 1) % this.size; + }; + + DelayLine.prototype.get = function() { + return this.buffer[this.index]; + }; + + return DelayLine; + +})(); + +Reverb = (function() { + function Reverb() { + this.delays = []; + this.delays.push(new DelayLine(47 * 44 + 1, .0)); + this.delays.push(new DelayLine(113 * 44 + 17, .0)); + this.delays.push(new DelayLine(131 * 44 + 23, .0)); + this.delays.push(new DelayLine(173 * 44 + 31, .0)); + this.delays.push(new DelayLine(193 * 44 + 41, .0)); + this.delays.push(new DelayLine(217 * 44 + 47, .0)); + this.delays.push(new DelayLine(233 * 44 + 41, .0)); + this.delays.push(new DelayLine(293 * 44 + 47, .0)); + this.avg = 0; + } + + Reverb.prototype.get = function(result) { + var d0, d1, d2, d3, d4, d5, d6, d7; + d0 = this.delays[0].get(); + d1 = this.delays[1].get(); + d2 = this.delays[2].get(); + d3 = this.delays[3].get(); + d4 = this.delays[4].get(); + d5 = this.delays[5].get(); + d6 = this.delays[6].get(); + d7 = this.delays[7].get(); + result[0] = (d0 * .23 + d1 * .13 + d2 * .41 + d3 * .13 + d4 * .37 + d5 * .11 + d6 * .22 + d7 * .05) * 2; + return result[1] = (d0 * .07 + d1 * .29 + d2 * .05 + d3 * .43 + d4 * .19 + d5 * .29 + d6 * .04 + d7 * .24) * 2; + }; + + Reverb.prototype.push = function(value) { + var d, d0, d1, d2, d3, d4, d5, d6, d7; + this.avg = this.avg * .8 + value * .2; + value -= this.avg; + d0 = this.delays[0].get(); + d1 = this.delays[1].get(); + d2 = this.delays[2].get(); + d3 = this.delays[3].get(); + d4 = this.delays[4].get(); + d5 = this.delays[5].get(); + d6 = this.delays[6].get(); + d7 = this.delays[7].get(); + d = (d0 + d1 + d2 + d3 + d4 + d5 + d6 + d7) / 8 * .1; + this.delays[0].push(value + d + d0 * .9); + this.delays[1].push(value + d + d1 * .9); + this.delays[2].push(value + d + d2 * .9); + this.delays[3].push(value + d + d3 * .9); + this.delays[4].push(value + d + d4 * .9); + this.delays[5].push(value + d + d5 * .9); + this.delays[6].push(value + d + d6 * .9); + this.delays[7].push(value + d + d7 * .9); + }; + + return Reverb; + +})(); + + +class MyWorkletProcessor extends AudioWorkletProcessor { + constructor() { + super() + this.synth = new Synth() + this.port.onmessage = (e) => { + console.info(e) + var data = JSON.parse(e.data) + if (data.name == "note") + { + this.synth.event(data.data) + } + else if (data.name == "param") + { + var value = data.value + var s = data.id.split(".") + data = this.synth.inputs + while (s.length>1) + { + data = data[s.splice(0,1)[0]] + } + data[s[0]] = value + } + } + } + + process(inputs, outputs, parameters) { + this.synth.process(inputs,outputs,parameters) + return true + } +} + +registerProcessor('my-worklet-processor', MyWorkletProcessor) +; diff --git a/static/js/sound/synthwheel.coffee b/static/js/sound/synthwheel.coffee new file mode 100644 index 00000000..45a9012b --- /dev/null +++ b/static/js/sound/synthwheel.coffee @@ -0,0 +1,32 @@ +class @SynthWheel + constructor:(@canvas,@listener)-> + @update() + + update:()-> + h = @canvas.height-20 + w = @canvas.width-20 + context = @canvas.getContext "2d" + context.globalAlpha = 1 + context.shadowBlur = 10 + context.shadowOpacity = 1 + context.shadowColor = "#000" + context.fillStyle = "#000" + context.fillRect 9,9,w+2,h+2 + context.beginPath() + context.ellipse @canvas.width/2,@canvas.height/2,w/2+5,h/2,0,0,Math.PI*2 + context.fill() + context.shadowBlur = 0 + context.shadowOpacity = 0 + context.fillStyle = "#FFF" + for i in [0..h] + y = (i-h/2)/h*Math.PI/2 + a = Math.asin(y) + p = Math.cos(a)*Math.cos(-Math.PI/8)+Math.sin(a)*Math.sin(-Math.PI/8) + context.globalAlpha = p + context.fillRect(11,10+i,w-2,1) + + for i in [-Math.PI/4..Math.PI/4] by .05 + y = @canvas.height/2+Math.sin(i)*h/2/Math.sin(Math.PI/4) + context.fillStyle = "rgba(0,0,0,.3)" + context.globalAlpha = 1 + context.fillRect(11,y,w-2,Math.abs(Math.cos(i))) diff --git a/static/js/sound/synthwheel.js b/static/js/sound/synthwheel.js new file mode 100644 index 00000000..92646ce1 --- /dev/null +++ b/static/js/sound/synthwheel.js @@ -0,0 +1,44 @@ +this.SynthWheel = (function() { + function SynthWheel(canvas, listener) { + this.canvas = canvas; + this.listener = listener; + this.update(); + } + + SynthWheel.prototype.update = function() { + var a, context, h, i, j, k, p, ref, ref1, ref2, results, w, y; + h = this.canvas.height - 20; + w = this.canvas.width - 20; + context = this.canvas.getContext("2d"); + context.globalAlpha = 1; + context.shadowBlur = 10; + context.shadowOpacity = 1; + context.shadowColor = "#000"; + context.fillStyle = "#000"; + context.fillRect(9, 9, w + 2, h + 2); + context.beginPath(); + context.ellipse(this.canvas.width / 2, this.canvas.height / 2, w / 2 + 5, h / 2, 0, 0, Math.PI * 2); + context.fill(); + context.shadowBlur = 0; + context.shadowOpacity = 0; + context.fillStyle = "#FFF"; + for (i = j = 0, ref = h; 0 <= ref ? j <= ref : j >= ref; i = 0 <= ref ? ++j : --j) { + y = (i - h / 2) / h * Math.PI / 2; + a = Math.asin(y); + p = Math.cos(a) * Math.cos(-Math.PI / 8) + Math.sin(a) * Math.sin(-Math.PI / 8); + context.globalAlpha = p; + context.fillRect(11, 10 + i, w - 2, 1); + } + results = []; + for (i = k = ref1 = -Math.PI / 4, ref2 = Math.PI / 4; k <= ref2; i = k += .05) { + y = this.canvas.height / 2 + Math.sin(i) * h / 2 / Math.sin(Math.PI / 4); + context.fillStyle = "rgba(0,0,0,.3)"; + context.globalAlpha = 1; + results.push(context.fillRect(11, y, w - 2, Math.abs(Math.cos(i)))); + } + return results; + }; + + return SynthWheel; + +})(); diff --git a/static/js/sound/test.html b/static/js/sound/test.html new file mode 100644 index 00000000..e5cf01d6 --- /dev/null +++ b/static/js/sound/test.html @@ -0,0 +1,29 @@ + + + + + + + + + diff --git a/static/js/spriteeditor/animationpanel.coffee b/static/js/spriteeditor/animationpanel.coffee new file mode 100644 index 00000000..46cbdcfb --- /dev/null +++ b/static/js/spriteeditor/animationpanel.coffee @@ -0,0 +1,248 @@ +class @AnimationPanel + constructor:(@sprite_editor)-> + @panel_shown = false + @animation_preview = new AnimationPreview(@sprite_editor) + + document.querySelector("#sprite-animation-title").addEventListener "click",()=>@togglePanel() + document.querySelector("#add-frame-button").addEventListener "click",()=> + @addFrame() + document.querySelector("#add-frame-button").scrollIntoView() + + hidePanel:()-> + @panel_shown = false + document.querySelector("#sprite-animation-title").classList.add "collapsed" + document.querySelector("#sprite-animation-panel").classList.add "collapsed" + document.querySelector("#sprite-animation-title i").classList.add "fa-caret-right" + document.querySelector("#sprite-animation-title i").classList.remove "fa-caret-down" + document.querySelector("#spriteeditorcontainer").classList.add "expanded" + @sprite_editor.spriteview.windowResized() + + showPanel:()-> + return if @sprite_editor.selected_sprite == "icon" + @panel_shown = true + document.querySelector("#sprite-animation-title").classList.remove "collapsed" + document.querySelector("#sprite-animation-panel").classList.remove "collapsed" + document.querySelector("#sprite-animation-title i").classList.remove "fa-caret-right" + document.querySelector("#sprite-animation-title i").classList.add "fa-caret-down" + document.querySelector("#spriteeditorcontainer").classList.remove "expanded" + @sprite_editor.spriteview.windowResized() + + togglePanel:()-> + if @panel_shown + @hidePanel() + else + @showPanel() + + spriteChanged:()-> + if @sprite_editor.selected_sprite == "icon" or not @sprite_editor.selected_sprite? + document.querySelector("#sprite-animation-title").style.display = "none" + @hidePanel() + else + document.querySelector("#sprite-animation-title").style.display = "block" + + if @sprite_editor.spriteview.sprite.frames.length>1 + @showPanel() + else + @hidePanel() + + @animation_preview.fps = @sprite_editor.spriteview.sprite.fps + @animation_preview.setSlider() + + @updateFrames() + + addFrame:()-> + sprite = @sprite_editor.spriteview.sprite + sprite.undo = new Undo() if not sprite.undo? + sprite.undo.pushState sprite.clone() if sprite.undo.empty() + + @sprite_editor.spriteview.sprite.addFrame() + @updateFrames() + @sprite_editor.spriteview.setCurrentFrame(@sprite_editor.spriteview.sprite.frames.length-1) + @sprite_editor.spriteview.update() + @updateSelection() + sprite.undo.pushState sprite.clone() + + updateFrames:()-> + s = @sprite_editor.spriteview.sprite + list = document.querySelector "#sprite-animation-list" + list.innerHTML = "" + @frames = [] + for frame,i in s.frames + list.appendChild @createFrameView(frame,i) + return + + updateSelection:()-> + for c,i in document.getElementById("sprite-animation-list").children + if i == @sprite_editor.spriteview.sprite.current_frame + c.classList.add "selected" + else + c.classList.remove "selected" + + createFrameView:(frame,index)-> + div = document.createElement "div" + div.classList.add "sprite-animation-frame" + if index == @sprite_editor.spriteview.sprite.current_frame + div.classList.add "selected" + canvas = document.createElement "canvas" + canvas.width = 80 + canvas.height = 80 + context = canvas.getContext "2d" + context.imageSmoothingEnabled = false + r = Math.min(80/frame.width,80/frame.height) + w = r*frame.width + h = r*frame.height + context.drawImage frame.getCanvas(),40-w/2,40-h/2,w,h + div.appendChild canvas + canvas.addEventListener "click",()=> + @doFrameOption(index,"select") + div.appendChild @createFrameOption "clone","clone","Duplicate Frame",index + if @sprite_editor.spriteview.sprite.frames.length>1 + div.appendChild @createFrameOption "remove","times","Delete Frame",index + + div.appendChild @createFrameOption "moveleft","arrow-left","Move Left",index + div.appendChild @createFrameOption "moveright","arrow-right","Move Right",index + span = document.createElement "span" + span.innerHTML = "#{index}" + div.appendChild span + @frames[index] = canvas + div + + createFrameOption:(option,icon,text,index)-> + i = document.createElement "i" + i.classList.add option + i.classList.add "fa" + i.classList.add "fa-#{icon}" + i.title = @sprite_editor.app.translator.get(text) + i.addEventListener "click",()=> + @doFrameOption(index,option) + i + + doFrameOption:(index,option)-> + switch option + when "select" + @sprite_editor.spriteview.setCurrentFrame(index) + @sprite_editor.spriteview.update() + @updateSelection() + + when "clone" + sprite = @sprite_editor.spriteview.sprite + sprite.undo = new Undo() if not sprite.undo? + sprite.undo.pushState sprite.clone() if sprite.undo.empty() + + f = @sprite_editor.spriteview.sprite.frames[index] + frame = f.clone() + @sprite_editor.spriteview.sprite.frames.splice(index,0,frame) + @updateFrames() + @doFrameOption(index+1,"select") + + @sprite_editor.spriteChanged() + sprite.undo.pushState sprite.clone() + + when "remove" + if @sprite_editor.spriteview.sprite.frames.length>1 + sprite = @sprite_editor.spriteview.sprite + sprite.undo = new Undo() if not sprite.undo? + sprite.undo.pushState sprite.clone() if sprite.undo.empty() + + @sprite_editor.spriteview.sprite.frames.splice(index,1) + @updateFrames() + @doFrameOption(Math.max(0,index-1),"select") + + @sprite_editor.spriteChanged() + sprite.undo.pushState sprite.clone() + + when "moveleft" + if index>0 + sprite = @sprite_editor.spriteview.sprite + sprite.undo = new Undo() if not sprite.undo? + sprite.undo.pushState sprite.clone() if sprite.undo.empty() + + frame = @sprite_editor.spriteview.sprite.frames.splice(index,1)[0] + @sprite_editor.spriteview.sprite.frames.splice(index-1,0,frame) + @updateFrames() + @doFrameOption(index-1,"select") + + @sprite_editor.spriteChanged() + sprite.undo.pushState sprite.clone() + + when "moveright" + if index<@sprite_editor.spriteview.sprite.frames.length-1 + sprite = @sprite_editor.spriteview.sprite + sprite.undo = new Undo() if not sprite.undo? + sprite.undo.pushState sprite.clone() if sprite.undo.empty() + + frame = @sprite_editor.spriteview.sprite.frames.splice(index,1)[0] + @sprite_editor.spriteview.sprite.frames.splice(index+1,0,frame) + @updateFrames() + @doFrameOption(index+1,"select") + + @sprite_editor.spriteChanged() + sprite.undo.pushState sprite.clone() + + frameUpdated:()-> + for index in [0..@frames.length-1] by 1 + context = @frames[index].getContext "2d" + context.clearRect 0,0,80,80 + frame = @sprite_editor.spriteview.sprite.frames[index] + if frame? + r = Math.min(80/frame.width,80/frame.height) + w = r*frame.width + h = r*frame.height + context.drawImage frame.getCanvas(),40-w/2,40-h/2,w,h + +class @AnimationPreview + constructor:(@sprite_editor)-> + @canvas = document.querySelector("#sprite-animation-preview canvas") + @context = @canvas.getContext "2d" + @context.imageSmoothingEnabled = false + @frame = 0 + @last = Date.now() + @fps = 5 + @update() + @input = document.querySelector("#sprite-animation-preview input") + @input.addEventListener "input",()=> + @fps = 1+Math.round(Math.pow(@input.value/100,2)*59) + @sprite_editor.spriteview.sprite.fps = @fps + @sprite_editor.spriteChanged() + @last = 0 + + @input.addEventListener "mouseenter",()=> + @fps_change = true + @last = 0 + + @input.addEventListener "mouseout",()=> + @fps_change = false + @last = 0 + + @setSlider() + + setSlider:()-> + @input.value = Math.pow((@fps-1)/59,1/2)*100 + + update:()-> + requestAnimationFrame ()=>@update() + if @sprite_editor.spriteview.sprite? + time = Date.now() + if time>@last+1000/@fps + @last += 1000/@fps + @last = time if @last 1) { + this.showPanel(); + } else { + this.hidePanel(); + } + this.animation_preview.fps = this.sprite_editor.spriteview.sprite.fps; + this.animation_preview.setSlider(); + } + return this.updateFrames(); + }; + + AnimationPanel.prototype.addFrame = function() { + var sprite; + sprite = this.sprite_editor.spriteview.sprite; + if (sprite.undo == null) { + sprite.undo = new Undo(); + } + if (sprite.undo.empty()) { + sprite.undo.pushState(sprite.clone()); + } + this.sprite_editor.spriteview.sprite.addFrame(); + this.updateFrames(); + this.sprite_editor.spriteview.setCurrentFrame(this.sprite_editor.spriteview.sprite.frames.length - 1); + this.sprite_editor.spriteview.update(); + this.updateSelection(); + return sprite.undo.pushState(sprite.clone()); + }; + + AnimationPanel.prototype.updateFrames = function() { + var frame, i, j, len, list, ref, s; + s = this.sprite_editor.spriteview.sprite; + list = document.querySelector("#sprite-animation-list"); + list.innerHTML = ""; + this.frames = []; + ref = s.frames; + for (i = j = 0, len = ref.length; j < len; i = ++j) { + frame = ref[i]; + list.appendChild(this.createFrameView(frame, i)); + } + }; + + AnimationPanel.prototype.updateSelection = function() { + var c, i, j, len, ref, results; + ref = document.getElementById("sprite-animation-list").children; + results = []; + for (i = j = 0, len = ref.length; j < len; i = ++j) { + c = ref[i]; + if (i === this.sprite_editor.spriteview.sprite.current_frame) { + results.push(c.classList.add("selected")); + } else { + results.push(c.classList.remove("selected")); + } + } + return results; + }; + + AnimationPanel.prototype.createFrameView = function(frame, index) { + var canvas, context, div, h, r, span, w; + div = document.createElement("div"); + div.classList.add("sprite-animation-frame"); + if (index === this.sprite_editor.spriteview.sprite.current_frame) { + div.classList.add("selected"); + } + canvas = document.createElement("canvas"); + canvas.width = 80; + canvas.height = 80; + context = canvas.getContext("2d"); + context.imageSmoothingEnabled = false; + r = Math.min(80 / frame.width, 80 / frame.height); + w = r * frame.width; + h = r * frame.height; + context.drawImage(frame.getCanvas(), 40 - w / 2, 40 - h / 2, w, h); + div.appendChild(canvas); + canvas.addEventListener("click", (function(_this) { + return function() { + return _this.doFrameOption(index, "select"); + }; + })(this)); + div.appendChild(this.createFrameOption("clone", "clone", "Duplicate Frame", index)); + if (this.sprite_editor.spriteview.sprite.frames.length > 1) { + div.appendChild(this.createFrameOption("remove", "times", "Delete Frame", index)); + } + div.appendChild(this.createFrameOption("moveleft", "arrow-left", "Move Left", index)); + div.appendChild(this.createFrameOption("moveright", "arrow-right", "Move Right", index)); + span = document.createElement("span"); + span.innerHTML = "" + index; + div.appendChild(span); + this.frames[index] = canvas; + return div; + }; + + AnimationPanel.prototype.createFrameOption = function(option, icon, text, index) { + var i; + i = document.createElement("i"); + i.classList.add(option); + i.classList.add("fa"); + i.classList.add("fa-" + icon); + i.title = this.sprite_editor.app.translator.get(text); + i.addEventListener("click", (function(_this) { + return function() { + return _this.doFrameOption(index, option); + }; + })(this)); + return i; + }; + + AnimationPanel.prototype.doFrameOption = function(index, option) { + var f, frame, sprite; + switch (option) { + case "select": + this.sprite_editor.spriteview.setCurrentFrame(index); + this.sprite_editor.spriteview.update(); + return this.updateSelection(); + case "clone": + sprite = this.sprite_editor.spriteview.sprite; + if (sprite.undo == null) { + sprite.undo = new Undo(); + } + if (sprite.undo.empty()) { + sprite.undo.pushState(sprite.clone()); + } + f = this.sprite_editor.spriteview.sprite.frames[index]; + frame = f.clone(); + this.sprite_editor.spriteview.sprite.frames.splice(index, 0, frame); + this.updateFrames(); + this.doFrameOption(index + 1, "select"); + this.sprite_editor.spriteChanged(); + return sprite.undo.pushState(sprite.clone()); + case "remove": + if (this.sprite_editor.spriteview.sprite.frames.length > 1) { + sprite = this.sprite_editor.spriteview.sprite; + if (sprite.undo == null) { + sprite.undo = new Undo(); + } + if (sprite.undo.empty()) { + sprite.undo.pushState(sprite.clone()); + } + this.sprite_editor.spriteview.sprite.frames.splice(index, 1); + this.updateFrames(); + this.doFrameOption(Math.max(0, index - 1), "select"); + this.sprite_editor.spriteChanged(); + return sprite.undo.pushState(sprite.clone()); + } + break; + case "moveleft": + if (index > 0) { + sprite = this.sprite_editor.spriteview.sprite; + if (sprite.undo == null) { + sprite.undo = new Undo(); + } + if (sprite.undo.empty()) { + sprite.undo.pushState(sprite.clone()); + } + frame = this.sprite_editor.spriteview.sprite.frames.splice(index, 1)[0]; + this.sprite_editor.spriteview.sprite.frames.splice(index - 1, 0, frame); + this.updateFrames(); + this.doFrameOption(index - 1, "select"); + this.sprite_editor.spriteChanged(); + return sprite.undo.pushState(sprite.clone()); + } + break; + case "moveright": + if (index < this.sprite_editor.spriteview.sprite.frames.length - 1) { + sprite = this.sprite_editor.spriteview.sprite; + if (sprite.undo == null) { + sprite.undo = new Undo(); + } + if (sprite.undo.empty()) { + sprite.undo.pushState(sprite.clone()); + } + frame = this.sprite_editor.spriteview.sprite.frames.splice(index, 1)[0]; + this.sprite_editor.spriteview.sprite.frames.splice(index + 1, 0, frame); + this.updateFrames(); + this.doFrameOption(index + 1, "select"); + this.sprite_editor.spriteChanged(); + return sprite.undo.pushState(sprite.clone()); + } + } + }; + + AnimationPanel.prototype.frameUpdated = function() { + var context, frame, h, index, j, r, ref, results, w; + results = []; + for (index = j = 0, ref = this.frames.length - 1; j <= ref; index = j += 1) { + context = this.frames[index].getContext("2d"); + context.clearRect(0, 0, 80, 80); + frame = this.sprite_editor.spriteview.sprite.frames[index]; + if (frame != null) { + r = Math.min(80 / frame.width, 80 / frame.height); + w = r * frame.width; + h = r * frame.height; + results.push(context.drawImage(frame.getCanvas(), 40 - w / 2, 40 - h / 2, w, h)); + } else { + results.push(void 0); + } + } + return results; + }; + + return AnimationPanel; + +})(); + +this.AnimationPreview = (function() { + function AnimationPreview(sprite_editor) { + this.sprite_editor = sprite_editor; + this.canvas = document.querySelector("#sprite-animation-preview canvas"); + this.context = this.canvas.getContext("2d"); + this.context.imageSmoothingEnabled = false; + this.frame = 0; + this.last = Date.now(); + this.fps = 5; + this.update(); + this.input = document.querySelector("#sprite-animation-preview input"); + this.input.addEventListener("input", (function(_this) { + return function() { + _this.fps = 1 + Math.round(Math.pow(_this.input.value / 100, 2) * 59); + _this.sprite_editor.spriteview.sprite.fps = _this.fps; + _this.sprite_editor.spriteChanged(); + return _this.last = 0; + }; + })(this)); + this.input.addEventListener("mouseenter", (function(_this) { + return function() { + _this.fps_change = true; + return _this.last = 0; + }; + })(this)); + this.input.addEventListener("mouseout", (function(_this) { + return function() { + _this.fps_change = false; + return _this.last = 0; + }; + })(this)); + this.setSlider(); + } + + AnimationPreview.prototype.setSlider = function() { + return this.input.value = Math.pow((this.fps - 1) / 59, 1 / 2) * 100; + }; + + AnimationPreview.prototype.update = function() { + var frame, h, r, time, w; + requestAnimationFrame((function(_this) { + return function() { + return _this.update(); + }; + })(this)); + if (this.sprite_editor.spriteview.sprite != null) { + time = Date.now(); + if (time > this.last + 1000 / this.fps) { + this.last += 1000 / this.fps; + if (this.last < time - 1000) { + this.last = time; + } + this.frame = (this.frame + 1) % this.sprite_editor.spriteview.sprite.frames.length; + frame = this.sprite_editor.spriteview.sprite.frames[this.frame]; + r = Math.min(80 / frame.width, 80 / frame.height); + w = r * frame.width; + h = r * frame.height; + this.context.clearRect(0, 0, 80, 80); + this.context.drawImage(frame.getCanvas(), 40 - w / 2, 40 - h / 2, w, h); + if (this.fps_change) { + this.context.font = "12pt Ubuntu Mono"; + this.context.shadowBlur = 2; + this.context.shadowOpacity = 1; + this.context.shadowColor = "#000"; + this.context.fillStyle = "#FFF"; + this.context.textAlign = "center"; + this.context.textBaseline = "middle"; + this.context.fillText(this.fps + " FPS", 40, 70); + this.context.shadowBlur = 0; + return this.context.shadowOpacity = 0; + } + } + } + }; + + return AnimationPreview; + +})(); diff --git a/static/js/spriteeditor/autopalette.coffee b/static/js/spriteeditor/autopalette.coffee new file mode 100644 index 00000000..f9090e73 --- /dev/null +++ b/static/js/spriteeditor/autopalette.coffee @@ -0,0 +1,124 @@ +class @AutoPalette + constructor:(@spriteeditor)-> + @locked = false + @lock = document.getElementById("auto-palette-lock") + @list = document.getElementById("auto-palette-list") + setInterval (()=>@process()),16 + @palette = {} + @lock.addEventListener "click",()=> + if @locked + @locked = false + @lock.classList.remove "locked" + @lock.classList.remove "fa-lock" + @lock.classList.add "fa-lock-open" + @lock.title = @spriteeditor.app.translator.get("Lock palette") + else + @locked = true + @lock.classList.add "locked" + @lock.classList.add "fa-lock" + @lock.classList.remove "fa-lock-open" + @lock.title = @spriteeditor.app.translator.get("Unlock palette") + + update:()-> + @current_sprite = @spriteeditor.spriteview.sprite + @current_frame = 0 + @current_line = 0 + + @colors = {} + + setColor:(col)-> + c = [col.color>>16,(col.color>>8)&0xFF,col.color&0xFF] + @spriteeditor.colorpicker.colorPicked(c) + + colorPicked:(color)-> + for c in @list.childNodes + if c == @palette[color] + c.classList.add "selected" + else + c.classList.remove "selected" + + return + + process:()-> + return if @locked + return if not @current_sprite + + if @current_frame>=@current_sprite.frames.length + c = [] + for key,value of @colors + c.push + color: value.color + count: value.count + hsl: @rgbToHsl(value.color) + + if c.length>32 + c.sort (a,b)->b.count-a.count + c.splice(32,c.length-31) + + @current_sprite = null + + score = (col)-> + Math.round(col.hsl[0]*16)/16*100+col.hsl[2]+if col.hsl[1] < .1 or col.hsl[2] >.9 then -1000 else 0 + + c.sort (a,b)-> score(a)-score(b) + + @list.innerHTML = "" + @palette = {} + for col in c + do (col)=> + div = document.createElement "div" + rgb = "rgb(#{col.color>>16},#{(col.color>>8)&0xFF},#{col.color&0xFF})" + div.style.background = rgb + div.addEventListener "click",()=>@setColor(col) + @list.appendChild div + @palette[rgb] = div + else + time = Date.now() + + while Date.now() + r = (c>>16)/255 + g = ((c>>8)&0xFF)/255 + b = (c&0xFF)/255 + max = Math.max(r, g, b) + min = Math.min(r, g, b) + + l = (max + min) / 2 + if max == min + h = s = 0 + else + d = max - min + s = if l > 0.5 then d / (2 - max - min) else d / (max + min) + switch max + when r + h = (g - b) / d + (if g < b then 6 else 0) + when g + h = (b - r) / d + 2 + when b + h = (r - g) / d + 4 + h /= 6 + [ + h + s + l + ] diff --git a/static/js/spriteeditor/autopalette.js b/static/js/spriteeditor/autopalette.js new file mode 100644 index 00000000..78d575a2 --- /dev/null +++ b/static/js/spriteeditor/autopalette.js @@ -0,0 +1,171 @@ +this.AutoPalette = (function() { + function AutoPalette(spriteeditor) { + this.spriteeditor = spriteeditor; + this.locked = false; + this.lock = document.getElementById("auto-palette-lock"); + this.list = document.getElementById("auto-palette-list"); + setInterval(((function(_this) { + return function() { + return _this.process(); + }; + })(this)), 16); + this.palette = {}; + this.lock.addEventListener("click", (function(_this) { + return function() { + if (_this.locked) { + _this.locked = false; + _this.lock.classList.remove("locked"); + _this.lock.classList.remove("fa-lock"); + _this.lock.classList.add("fa-lock-open"); + return _this.lock.title = _this.spriteeditor.app.translator.get("Lock palette"); + } else { + _this.locked = true; + _this.lock.classList.add("locked"); + _this.lock.classList.add("fa-lock"); + _this.lock.classList.remove("fa-lock-open"); + return _this.lock.title = _this.spriteeditor.app.translator.get("Unlock palette"); + } + }; + })(this)); + } + + AutoPalette.prototype.update = function() { + this.current_sprite = this.spriteeditor.spriteview.sprite; + this.current_frame = 0; + this.current_line = 0; + return this.colors = {}; + }; + + AutoPalette.prototype.setColor = function(col) { + var c; + c = [col.color >> 16, (col.color >> 8) & 0xFF, col.color & 0xFF]; + return this.spriteeditor.colorpicker.colorPicked(c); + }; + + AutoPalette.prototype.colorPicked = function(color) { + var c, j, len, ref; + ref = this.list.childNodes; + for (j = 0, len = ref.length; j < len; j++) { + c = ref[j]; + if (c === this.palette[color]) { + c.classList.add("selected"); + } else { + c.classList.remove("selected"); + } + } + }; + + AutoPalette.prototype.process = function() { + var c, col, data, fn, frame, i, j, k, key, len, ref, ref1, score, time, value; + if (this.locked) { + return; + } + if (!this.current_sprite) { + return; + } + if (this.current_frame >= this.current_sprite.frames.length) { + c = []; + ref = this.colors; + for (key in ref) { + value = ref[key]; + c.push({ + color: value.color, + count: value.count, + hsl: this.rgbToHsl(value.color) + }); + } + if (c.length > 32) { + c.sort(function(a, b) { + return b.count - a.count; + }); + c.splice(32, c.length - 31); + } + this.current_sprite = null; + score = function(col) { + return Math.round(col.hsl[0] * 16) / 16 * 100 + col.hsl[2] + (col.hsl[1] < .1 || col.hsl[2] > .9 ? -1000 : 0); + }; + c.sort(function(a, b) { + return score(a) - score(b); + }); + this.list.innerHTML = ""; + this.palette = {}; + fn = (function(_this) { + return function(col) { + var div, rgb; + div = document.createElement("div"); + rgb = "rgb(" + (col.color >> 16) + "," + ((col.color >> 8) & 0xFF) + "," + (col.color & 0xFF) + ")"; + div.style.background = rgb; + div.addEventListener("click", function() { + return _this.setColor(col); + }); + _this.list.appendChild(div); + return _this.palette[rgb] = div; + }; + })(this); + for (j = 0, len = c.length; j < len; j++) { + col = c[j]; + fn(col); + } + } else { + time = Date.now(); + while (Date.now() < time + 2 && this.current_frame < this.current_sprite.frames.length) { + frame = this.current_sprite.frames[this.current_frame]; + if (frame == null) { + return; + } + if (this.current_line < frame.getCanvas().height) { + data = frame.getContext().getImageData(0, this.current_line, frame.width, 1); + for (i = k = 0, ref1 = frame.width - 1; k <= ref1; i = k += 1) { + if (data.data[i * 4 + 3] < 128) { + continue; + } + col = (Math.floor(data.data[i * 4] / 16) << 8) + (Math.floor(data.data[i * 4 + 1] / 16) << 4) + Math.floor(data.data[i * 4 + 2] / 16); + if (this.colors[col] == null) { + this.colors[col] = { + color: (data.data[i * 4] << 16) + (data.data[i * 4 + 1] << 8) + data.data[i * 4 + 2], + count: 1 + }; + } else { + this.colors[col].count += 1; + } + } + this.current_line += 1; + } else { + this.current_line = 0; + this.current_frame += 1; + } + } + } + }; + + AutoPalette.prototype.rgbToHsl = function(c) { + var b, d, g, h, l, max, min, r, s; + r = (c >> 16) / 255; + g = ((c >> 8) & 0xFF) / 255; + b = (c & 0xFF) / 255; + max = Math.max(r, g, b); + min = Math.min(r, g, b); + l = (max + min) / 2; + if (max === min) { + h = s = 0; + } else { + d = max - min; + s = l > 0.5 ? d / (2 - max - min) : d / (max + min); + switch (max) { + case r: + h = (g - b) / d + (g < b ? 6 : 0); + break; + case g: + h = (b - r) / d + 2; + break; + case b: + h = (r - g) / d + 4; + } + h /= 6; + } + return [h, s, l]; + }; + + return AutoPalette; + +})(); diff --git a/static/js/spriteeditor/colorpicker.coffee b/static/js/spriteeditor/colorpicker.coffee new file mode 100644 index 00000000..ab813fa7 --- /dev/null +++ b/static/js/spriteeditor/colorpicker.coffee @@ -0,0 +1,223 @@ +class @ColorPicker + constructor:(@editor)-> + @canvas = document.createElement "canvas" + @canvas.width = 146 + + @num_blocks = 9 + @block = @canvas.width/@num_blocks + @canvas.height = @block*(2+1+1+1+1+1+@num_blocks) + + @hue = 0 + @type = "color" + @saturation = @num_blocks-1 + @lightness = @num_blocks-1 + @updateColor() + @update() + @canvas.addEventListener "mousedown", (event) => @mouseDown(event) + @canvas.addEventListener "mousemove", (event) => @mouseMove(event) + document.addEventListener "mouseup", (event) => @mouseUp(event) + + colorPicked:(c)-> + @color = "rgb(#{c[0]},#{c[1]},#{c[2]})" + @editor.setColor(@color) + col = @RGBtoHSV(c[0],c[1],c[2]) + @hue = Math.floor(col.h*@num_blocks*2)%(@num_blocks*2) + @saturation = Math.round(col.s*@num_blocks) + @lightness = Math.round(col.v*@num_blocks) + if @saturation == 0 or @lightness == 0 + @type = "gray" + @lightness = Math.round(col.v*(2*@num_blocks-1))/2 + else + @type = "color" + @saturation -= 1 + @lightness -= 1 + @update() + + updateColor:()-> + if @type == "gray" + v = Math.floor(255*@lightness*2/(@num_blocks*2-1)) + @color = "rgb(#{v},#{v},#{v})" + else + h = @hue/(@num_blocks*2) + s = (@saturation+1)/@num_blocks + v = (@lightness+1)/@num_blocks + col = @HSVtoRGB(h,s,v) + @color = "rgb(#{col.r},#{col.g},#{col.b})" + @editor.setColor @color + + update:()-> + context = @canvas.getContext "2d" + context.clearRect(0,0,@canvas.width,@canvas.height) + + context.fillStyle = "#888" + context.fillRoundRect 0,0,@canvas.width,@block*2,5 + context.fillStyle = @color + context.fillRoundRect 1,1,@canvas.width-2,@block*2-2,5 + + grd = context.createLinearGradient 0,@canvas.height-@block*2,0,@canvas.height,0 + grd.addColorStop 0,'rgba(255,255,255,0)' + grd.addColorStop 1,"rgba(255,255,255,.5)" + context.fillStyle = grd + #context.fillRect 0,@canvas.height-@block*2,@canvas.width,@block*2 + #context.fillRect 0,@block*7,@canvas.width,@block*@num_blocks + + for light in [0..@num_blocks*2-1] by 1 + if @type == "gray" and @lightness*2 == light + context.fillStyle = "#FFF" + context.fillRoundRect light*@block*.5-1,3*@block-1,@block*.5+2,@block+2,1 + context.fillStyle = "hsl(0,0%,#{light/(@num_blocks*2-1)*100}%)" + context.fillRoundRect light*@block*.5+1,3*@block+1,@block*.5-2,@block-2,1 + + for hue in [0..@num_blocks*2-1] by 1 + if @type == "color" and hue == @hue + context.fillStyle = "#FFF" + context.fillRoundRect hue*@block*.5-1,5*@block-1,@block*.5+2,@block+2,1 + context.fillStyle = "hsl(#{hue/@num_blocks*180},100%,50%)" + context.fillRoundRect hue*@block*.5+1,5*@block+1,@block*.5-2,@block-2,1 + + for y in [0..@num_blocks-1] by 1 + for x in [0..@num_blocks-1] by 1 + ay = @num_blocks-1-y + if @type == "color" and ay == @lightness and x == @saturation and @hue>=0 + context.fillStyle = "#FFF" + context.fillRoundRect x*@block-1,(y+7)*@block-1,@block+2,@block+2,2 + + h = @hue/(@num_blocks*2) + s = (x+1)/@num_blocks + v = (ay+1)/@num_blocks + + col = @HSVtoRGB(h,s,v) + context.fillStyle = "rgb(#{col.r},#{col.g},#{col.b})" + l = @num_blocks-1-light + context.fillRoundRect x*@block+1,(y+7)*@block+1,@block-2,@block-2,2 + + return + + mouseDown:(event)-> + @mousepressed = true + @mouseMove(event) + + mouseMove:(event)-> + if @mousepressed + b = @canvas.getBoundingClientRect() + min = Math.min @canvas.clientWidth,@canvas.clientHeight + x = (event.clientX-b.left) + y = (event.clientY-b.top) + + y = Math.floor(y/@block) + if y == 3 + lightness = Math.floor(x/@canvas.width*@num_blocks*2)/2 + if lightness != @lightness or @type != "gray" + @type = "gray" + @lightness = lightness + @updateColor() + @update() + else if y == 5 + hue = Math.max(0,Math.floor(x/@canvas.width*@num_blocks*2)) + if hue != @hue or @type != @color + @type = "color" + @lightness = Math.floor(@lightness) + @hue = hue + @updateColor() + @update() + else if y >= 7 + x = Math.floor(x/@canvas.width*@num_blocks) + saturation = x + lightness = @num_blocks-1-(y-7) + if lightness != @lightness or saturation != @saturation or @type != "color" + @type = "color" + @lightness = lightness + @saturation = saturation + @updateColor() + @update() + false + + mouseUp:(event)-> + @mousepressed = false + + rgbToHsl:(r, g, b)-> + r /= 255 + g /= 255 + b /= 255 + max = Math.max(r, g, b) + min = Math.min(r, g, b) + h = undefined + s = undefined + l = (max + min) / 2 + if max == min + h = s = 0 + # achromatic + else + d = max - min + s = if l > 0.5 then d / (2 - max - min) else d / (max + min) + switch max + when r + h = (g - b) / d + (if g < b then 6 else 0) + when g + h = (b - r) / d + 2 + when b + h = (r - g) / d + 4 + h /= 6 + [h,s,l] + + HSVtoRGB: (h, s, v) -> + i = Math.floor(h * 6) + f = h * 6 - i + p = v * (1 - s) + q = v * (1 - (f * s)) + t = v * (1 - ((1 - f) * s)) + switch i % 6 + when 0 + r = v + g = t + b = p + when 1 + r = q + g = v + b = p + when 2 + r = p + g = v + b = t + when 3 + r = p + g = q + b = v + when 4 + r = t + g = p + b = v + when 5 + r = v + g = p + b = q + { + r: Math.round(r * 255) + g: Math.round(g * 255) + b: Math.round(b * 255) + } + + RGBtoHSV: (r, g, b) -> + max = Math.max(r, g, b) + min = Math.min(r, g, b) + d = max - min + h = undefined + s = if max == 0 then 0 else d / max + v = max / 255 + switch max + when min + h = 0 + when r + h = g - b + d * (if g < b then 6 else 0) + h /= 6 * d + when g + h = b - r + d * 2 + h /= 6 * d + when b + h = r - g + d * 4 + h /= 6 * d + { + h: h + s: s + v: v + } diff --git a/static/js/spriteeditor/colorpicker.js b/static/js/spriteeditor/colorpicker.js new file mode 100644 index 00000000..81b858bf --- /dev/null +++ b/static/js/spriteeditor/colorpicker.js @@ -0,0 +1,270 @@ +this.ColorPicker = (function() { + function ColorPicker(editor) { + this.editor = editor; + this.canvas = document.createElement("canvas"); + this.canvas.width = 146; + this.num_blocks = 9; + this.block = this.canvas.width / this.num_blocks; + this.canvas.height = this.block * (2 + 1 + 1 + 1 + 1 + 1 + this.num_blocks); + this.hue = 0; + this.type = "color"; + this.saturation = this.num_blocks - 1; + this.lightness = this.num_blocks - 1; + this.updateColor(); + this.update(); + this.canvas.addEventListener("mousedown", (function(_this) { + return function(event) { + return _this.mouseDown(event); + }; + })(this)); + this.canvas.addEventListener("mousemove", (function(_this) { + return function(event) { + return _this.mouseMove(event); + }; + })(this)); + document.addEventListener("mouseup", (function(_this) { + return function(event) { + return _this.mouseUp(event); + }; + })(this)); + } + + ColorPicker.prototype.colorPicked = function(c) { + var col; + this.color = "rgb(" + c[0] + "," + c[1] + "," + c[2] + ")"; + this.editor.setColor(this.color); + col = this.RGBtoHSV(c[0], c[1], c[2]); + this.hue = Math.floor(col.h * this.num_blocks * 2) % (this.num_blocks * 2); + this.saturation = Math.round(col.s * this.num_blocks); + this.lightness = Math.round(col.v * this.num_blocks); + if (this.saturation === 0 || this.lightness === 0) { + this.type = "gray"; + this.lightness = Math.round(col.v * (2 * this.num_blocks - 1)) / 2; + } else { + this.type = "color"; + this.saturation -= 1; + this.lightness -= 1; + } + return this.update(); + }; + + ColorPicker.prototype.updateColor = function() { + var col, h, s, v; + if (this.type === "gray") { + v = Math.floor(255 * this.lightness * 2 / (this.num_blocks * 2 - 1)); + this.color = "rgb(" + v + "," + v + "," + v + ")"; + } else { + h = this.hue / (this.num_blocks * 2); + s = (this.saturation + 1) / this.num_blocks; + v = (this.lightness + 1) / this.num_blocks; + col = this.HSVtoRGB(h, s, v); + this.color = "rgb(" + col.r + "," + col.g + "," + col.b + ")"; + } + return this.editor.setColor(this.color); + }; + + ColorPicker.prototype.update = function() { + var ay, col, context, grd, h, hue, j, k, l, light, m, n, ref, ref1, ref2, ref3, s, v, x, y; + context = this.canvas.getContext("2d"); + context.clearRect(0, 0, this.canvas.width, this.canvas.height); + context.fillStyle = "#888"; + context.fillRoundRect(0, 0, this.canvas.width, this.block * 2, 5); + context.fillStyle = this.color; + context.fillRoundRect(1, 1, this.canvas.width - 2, this.block * 2 - 2, 5); + grd = context.createLinearGradient(0, this.canvas.height - this.block * 2, 0, this.canvas.height, 0); + grd.addColorStop(0, 'rgba(255,255,255,0)'); + grd.addColorStop(1, "rgba(255,255,255,.5)"); + context.fillStyle = grd; + for (light = j = 0, ref = this.num_blocks * 2 - 1; j <= ref; light = j += 1) { + if (this.type === "gray" && this.lightness * 2 === light) { + context.fillStyle = "#FFF"; + context.fillRoundRect(light * this.block * .5 - 1, 3 * this.block - 1, this.block * .5 + 2, this.block + 2, 1); + } + context.fillStyle = "hsl(0,0%," + (light / (this.num_blocks * 2 - 1) * 100) + "%)"; + context.fillRoundRect(light * this.block * .5 + 1, 3 * this.block + 1, this.block * .5 - 2, this.block - 2, 1); + } + for (hue = k = 0, ref1 = this.num_blocks * 2 - 1; k <= ref1; hue = k += 1) { + if (this.type === "color" && hue === this.hue) { + context.fillStyle = "#FFF"; + context.fillRoundRect(hue * this.block * .5 - 1, 5 * this.block - 1, this.block * .5 + 2, this.block + 2, 1); + } + context.fillStyle = "hsl(" + (hue / this.num_blocks * 180) + ",100%,50%)"; + context.fillRoundRect(hue * this.block * .5 + 1, 5 * this.block + 1, this.block * .5 - 2, this.block - 2, 1); + } + for (y = m = 0, ref2 = this.num_blocks - 1; m <= ref2; y = m += 1) { + for (x = n = 0, ref3 = this.num_blocks - 1; n <= ref3; x = n += 1) { + ay = this.num_blocks - 1 - y; + if (this.type === "color" && ay === this.lightness && x === this.saturation && this.hue >= 0) { + context.fillStyle = "#FFF"; + context.fillRoundRect(x * this.block - 1, (y + 7) * this.block - 1, this.block + 2, this.block + 2, 2); + } + h = this.hue / (this.num_blocks * 2); + s = (x + 1) / this.num_blocks; + v = (ay + 1) / this.num_blocks; + col = this.HSVtoRGB(h, s, v); + context.fillStyle = "rgb(" + col.r + "," + col.g + "," + col.b + ")"; + l = this.num_blocks - 1 - light; + context.fillRoundRect(x * this.block + 1, (y + 7) * this.block + 1, this.block - 2, this.block - 2, 2); + } + } + }; + + ColorPicker.prototype.mouseDown = function(event) { + this.mousepressed = true; + return this.mouseMove(event); + }; + + ColorPicker.prototype.mouseMove = function(event) { + var b, hue, lightness, min, saturation, x, y; + if (this.mousepressed) { + b = this.canvas.getBoundingClientRect(); + min = Math.min(this.canvas.clientWidth, this.canvas.clientHeight); + x = event.clientX - b.left; + y = event.clientY - b.top; + y = Math.floor(y / this.block); + if (y === 3) { + lightness = Math.floor(x / this.canvas.width * this.num_blocks * 2) / 2; + if (lightness !== this.lightness || this.type !== "gray") { + this.type = "gray"; + this.lightness = lightness; + this.updateColor(); + this.update(); + } + } else if (y === 5) { + hue = Math.max(0, Math.floor(x / this.canvas.width * this.num_blocks * 2)); + if (hue !== this.hue || this.type !== this.color) { + this.type = "color"; + this.lightness = Math.floor(this.lightness); + this.hue = hue; + this.updateColor(); + this.update(); + } + } else if (y >= 7) { + x = Math.floor(x / this.canvas.width * this.num_blocks); + saturation = x; + lightness = this.num_blocks - 1 - (y - 7); + if (lightness !== this.lightness || saturation !== this.saturation || this.type !== "color") { + this.type = "color"; + this.lightness = lightness; + this.saturation = saturation; + this.updateColor(); + this.update(); + } + } + } + return false; + }; + + ColorPicker.prototype.mouseUp = function(event) { + return this.mousepressed = false; + }; + + ColorPicker.prototype.rgbToHsl = function(r, g, b) { + var d, h, l, max, min, s; + r /= 255; + g /= 255; + b /= 255; + max = Math.max(r, g, b); + min = Math.min(r, g, b); + h = void 0; + s = void 0; + l = (max + min) / 2; + if (max === min) { + h = s = 0; + } else { + d = max - min; + s = l > 0.5 ? d / (2 - max - min) : d / (max + min); + switch (max) { + case r: + h = (g - b) / d + (g < b ? 6 : 0); + break; + case g: + h = (b - r) / d + 2; + break; + case b: + h = (r - g) / d + 4; + } + h /= 6; + } + return [h, s, l]; + }; + + ColorPicker.prototype.HSVtoRGB = function(h, s, v) { + var b, f, g, i, p, q, r, t; + i = Math.floor(h * 6); + f = h * 6 - i; + p = v * (1 - s); + q = v * (1 - (f * s)); + t = v * (1 - ((1 - f) * s)); + switch (i % 6) { + case 0: + r = v; + g = t; + b = p; + break; + case 1: + r = q; + g = v; + b = p; + break; + case 2: + r = p; + g = v; + b = t; + break; + case 3: + r = p; + g = q; + b = v; + break; + case 4: + r = t; + g = p; + b = v; + break; + case 5: + r = v; + g = p; + b = q; + } + return { + r: Math.round(r * 255), + g: Math.round(g * 255), + b: Math.round(b * 255) + }; + }; + + ColorPicker.prototype.RGBtoHSV = function(r, g, b) { + var d, h, max, min, s, v; + max = Math.max(r, g, b); + min = Math.min(r, g, b); + d = max - min; + h = void 0; + s = max === 0 ? 0 : d / max; + v = max / 255; + switch (max) { + case min: + h = 0; + break; + case r: + h = g - b + d * (g < b ? 6 : 0); + h /= 6 * d; + break; + case g: + h = b - r + d * 2; + h /= 6 * d; + break; + case b: + h = r - g + d * 4; + h /= 6 * d; + } + return { + h: h, + s: s, + v: v + }; + }; + + return ColorPicker; + +})(); diff --git a/static/js/spriteeditor/drawtool.coffee b/static/js/spriteeditor/drawtool.coffee new file mode 100644 index 00000000..4cf0e967 --- /dev/null +++ b/static/js/spriteeditor/drawtool.coffee @@ -0,0 +1,343 @@ +class @DrawTool + constructor:(@name,@icon)-> + @parameters = {} + @hsymmetry = false + @vsymmetry = false + @tile = true + + getSize:(sprite)-> + if @parameters["Size"] + size = Math.min(sprite.width,sprite.height)*@parameters["Size"].value/100/2 + size = 1+Math.floor(size/2)*2 + else + 1 + + start:(sprite,x,y,button)-> + @pixels = {} + @move(sprite,x,y,button) + + move:(sprite,x,y,button,pass=0)-> + if pass<1 and @vsymmetry + nx = sprite.width-1-x + @move(sprite,nx,y,button,1) + if pass<2 and @hsymmetry + ny = sprite.height-1-y + @move(sprite,x,ny,button,2) + + size = @getSize(sprite) + d = (size-1)/2 + for i in [-d..d] by 1 + for j in [-d..d] by 1 + xx = x+i + yy = y+j + if @tile + for ii in [-1..1] by 1 + for jj in [-1..1] by 1 + @preprocessPixel(sprite,xx+ii*sprite.width,yy+jj*sprite.height,button) + else + @preprocessPixel(sprite,xx,yy,button) + + preprocessPixel:(sprite,x,y,button)-> + return if x<0 or x>=sprite.width or y<0 or y>=sprite.height + if not @pixels["#{x}-#{y}"]? + @pixels["#{x}-#{y}"] = true + @processPixel(sprite,x,y,button) + + processPixel:(sprite,x,y)-> + + @tools: [] + +class @PencilTool extends @DrawTool + constructor:()-> + super("Pencil","fa-pencil-alt") + + @parameters["Size"] = + type: "range" + value: 0 + + @parameters["Opacity"] = + type: "range" + value: 100 + + @parameters["Color"] = + type: "color" + value: "#FFF" + + processPixel:(sprite,x,y,button)-> + if button == 2 + sprite.erasePixel x,y,@parameters["Opacity"].value/100 + else + c = sprite.getContext() + c.globalAlpha = @parameters["Opacity"].value/100 + c.fillStyle = @parameters["Color"].value + c.fillRect x,y,1,1 + c.globalAlpha = 1 + +@DrawTool.tools.push new @PencilTool() + +class @EraserTool extends @DrawTool + constructor:()-> + super("Eraser","fa-eraser") + + @parameters["Size"] = + type: "range" + value: 0 + + @parameters["Opacity"] = + type: "range" + value: 100 + + processPixel:(sprite,x,y)-> + sprite.erasePixel x,y,@parameters["Opacity"].value/100 + +@DrawTool.tools.push new @EraserTool() + +class @FillTool extends @DrawTool + constructor:()-> + super("Fill","fa-fill-drip") + + @parameters["Threshold"] = + type: "range" + value: 0 + + @parameters["Opacity"] = + type: "range" + value: 100 + + @parameters["Color"] = + type: "color" + value: "#FFF" + + start:(sprite,x,y)-> + return if x<0 or y<0 or x>=sprite.width or y>=sprite.height + threshold = @parameters["Threshold"].value/100*255+1 + alpha = @parameters["Opacity"].value/100 + + c = sprite.getContext() + ref = c.getImageData(x,y,1,1) + + data = c.getImageData(0,0,sprite.width,sprite.height) + c.clearRect(x,y,1,1) + c.globalAlpha = alpha + c.fillStyle = @parameters["Color"].value + c.fillRect(x,y,1,1) + c.globalAlpha = 1 + + fill = c.getImageData(x,y,1,1) + + list = [[x,y]] + table = {} + table["#{x}-#{y}"] = true + check = (x,y)-> + return false if x<0 or y<0 or x>=sprite.width or y>=sprite.height + return false if table["#{x}-#{y}"] + index = 4*(x+y*sprite.width) + dr = Math.abs(data.data[index]-ref.data[0]) + dg = Math.abs(data.data[index+1]-ref.data[1]) + db = Math.abs(data.data[index+2]-ref.data[2]) + da = Math.abs(data.data[index+3]-ref.data[3]) + Math.max(dr,dg,db,da)0 + p = list.splice(0,1)[0] + x = p[0] + y = p[1] + index = 4*(x+y*sprite.width) + data.data[index] = fill.data[0] + data.data[index+1] = fill.data[1] + data.data[index+2] = fill.data[2] + data.data[index+3] = fill.data[3] + + if check(x-1,y) + table["#{x-1}-#{y}"] = true + list.push [x-1,y] + if check(x+1,y) + table["#{x+1}-#{y}"] = true + list.push [x+1,y] + if check(x,y-1) + table["#{x}-#{y-1}"] = true + list.push [x,y-1] + if check(x,y+1) + table["#{x}-#{y+1}"] = true + list.push [x,y+1] + + c.putImageData(data,0,0) + return + + move:(sprite,x,y)-> + +@DrawTool.tools.push new @FillTool() + + +class @BrightenTool extends @DrawTool + constructor:(parent)-> + super("Brighten","fa-sun") + + @parameters = parent.parameters + + processPixel:(sprite,x,y)-> + amount = @parameters["Amount"].value/100 + c = sprite.getContext() + data = c.getImageData(x,y,1,1) + r = data.data[0] + g = data.data[1] + b = data.data[2] + v = (r+g+b)/3 + dr = r-v + dg = g-v + db = b-v + v = v*(1+amount*.5) + data.data[0] = Math.min(255,v+dr) + data.data[1] = Math.min(255,v+dg) + data.data[2] = Math.min(255,v+db) + c.putImageData data,x,y + +#@DrawTool.tools.push new @BrightenTool() + +class @DarkenTool extends @DrawTool + constructor:(parent)-> + super("Darken","fa-moon") + + @parameters = parent.parameters + + processPixel:(sprite,x,y)-> + amount = @parameters["Amount"].value/100 + c = sprite.getContext() + data = c.getImageData(x,y,1,1) + r = data.data[0] + g = data.data[1] + b = data.data[2] + v = (r+g+b)/3 + dr = r-v + dg = g-v + db = b-v + v = v*(1-amount*.5) + data.data[0] = Math.max(0,v+dr) + data.data[1] = Math.max(0,v+dg) + data.data[2] = Math.max(0,v+db) + c.putImageData data,x,y + +#@DrawTool.tools.push new @DarkenTool() + +class @SmoothenTool extends @DrawTool + constructor:(parent)-> + super("Smoothen","fa-brush") + + @parameters = parent.parameters + + processPixel:(sprite,x,y)-> + amount = @parameters["Amount"].value/100 + c = sprite.getContext() + sum = [0,0,0,0] + coef = 0 + ref = c.getImageData(x,y,1,1) + for i in [-1..1] by 1 + for j in [-1..1] by 1 + xx = x+i + yy = y+j + continue if xx<0 or yy<0 or xx>=sprite.width or yy>=sprite.height + data = c.getImageData xx,yy,1,1 + co = 1/(1+i*i+j*j)*(1+data.data[3]) + coef += co + sum[0] += data.data[0]*co + sum[1] += data.data[1]*co + sum[2] += data.data[2]*co + sum[3] += data.data[3]*co + + data.data[0] = sum[0]/coef*amount+(1-amount)*ref.data[0] + data.data[1] = sum[1]/coef*amount+(1-amount)*ref.data[1] + data.data[2] = sum[2]/coef*amount+(1-amount)*ref.data[2] + data.data[3] = sum[3]/coef*amount+(1-amount)*ref.data[3] + c.putImageData data,x,y + +#@DrawTool.tools.push new @SmoothenTool() + +class @SharpenTool extends @DrawTool + constructor:(parent)-> + super("Sharpen","fa-adjust") + + @parameters = parent.parameters + + processPixel:(sprite,x,y)-> + amount = @parameters["Amount"].value/100 + c = sprite.getContext() + sum = [0,0,0,0] + coef = 0 + ref = c.getImageData(x,y,1,1) + for i in [-1..1] by 1 + for j in [-1..1] by 1 + continue if i==0 and j==0 + xx = x+i + yy = y+j + continue if xx<0 or yy<0 or xx>=sprite.width or yy>=sprite.height + data = c.getImageData xx,yy,1,1 + co = 1/(1+i*i+j*j)*(1+data.data[3]) + coef += co + sum[0] += (ref.data[0]-data.data[0])*co + sum[1] += (ref.data[1]-data.data[1])*co + sum[2] += (ref.data[2]-data.data[2])*co + sum[3] += (ref.data[3]-data.data[3])*co + + ref.data[0] += sum[0]/coef*amount + ref.data[1] += sum[1]/coef*amount + ref.data[2] += sum[2]/coef*amount + ref.data[3] += sum[3]/coef*amount + c.putImageData ref,x,y + +#@DrawTool.tools.push new @EnhanceTool() + +class @SaturationTool extends @DrawTool + constructor:(parent)-> + super("Saturation","fa-palette") + + @parameters = parent.parameters + + processPixel:(sprite,x,y)-> + amount = @parameters["Amount"].value/100 + amount = if amount>.5 then amount*2 else .5+amount + c = sprite.getContext() + data = c.getImageData(x,y,1,1) + r = data.data[0] + g = data.data[1] + b = data.data[2] + v = (r+g+b)/3 + dr = r-v + dg = g-v + db = b-v + data.data[0] = Math.max(0,v+dr*amount) + data.data[1] = Math.max(0,v+dg*amount) + data.data[2] = Math.max(0,v+db*amount) + c.putImageData data,x,y + +#@DrawTool.tools.push new @SaturationTool() + +class @EnhanceTool extends @DrawTool + constructor:()-> + super("Enhance","fa-magic") + + @parameters["Tool"] = + type: "tool" + set: [new BrightenTool(@),new DarkenTool(@),new SmoothenTool(@),new SharpenTool(@),new SaturationTool(@)] + value: 0 + + @parameters["Size"] = + type: "range" + value: 0 + + @parameters["Amount"] = + type: "range" + value: 50 + + processPixel:(sprite,x,y)-> + @parameters.Tool.set[@parameters.Tool.value].processPixel(sprite,x,y) + +@DrawTool.tools.push new @EnhanceTool() + +class @SelectTool extends @DrawTool + constructor:()-> + super("Select","fa-vector-square") + @selectiontool = true + + processPixel:(sprite,x,y)-> + +@DrawTool.tools.push new @SelectTool() diff --git a/static/js/spriteeditor/drawtool.js b/static/js/spriteeditor/drawtool.js new file mode 100644 index 00000000..b2d168f6 --- /dev/null +++ b/static/js/spriteeditor/drawtool.js @@ -0,0 +1,473 @@ +var extend = function(child, parent) { for (var key in parent) { if (hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor(); child.__super__ = parent.prototype; return child; }, + hasProp = {}.hasOwnProperty; + +this.DrawTool = (function() { + function DrawTool(name, icon) { + this.name = name; + this.icon = icon; + this.parameters = {}; + this.hsymmetry = false; + this.vsymmetry = false; + this.tile = true; + } + + DrawTool.prototype.getSize = function(sprite) { + var size; + if (this.parameters["Size"]) { + size = Math.min(sprite.width, sprite.height) * this.parameters["Size"].value / 100 / 2; + return size = 1 + Math.floor(size / 2) * 2; + } else { + return 1; + } + }; + + DrawTool.prototype.start = function(sprite, x, y, button) { + this.pixels = {}; + return this.move(sprite, x, y, button); + }; + + DrawTool.prototype.move = function(sprite, x, y, button, pass) { + var d, i, ii, j, jj, k, nx, ny, ref1, ref2, results, size, xx, yy; + if (pass == null) { + pass = 0; + } + if (pass < 1 && this.vsymmetry) { + nx = sprite.width - 1 - x; + this.move(sprite, nx, y, button, 1); + } + if (pass < 2 && this.hsymmetry) { + ny = sprite.height - 1 - y; + this.move(sprite, x, ny, button, 2); + } + size = this.getSize(sprite); + d = (size - 1) / 2; + results = []; + for (i = k = ref1 = -d, ref2 = d; k <= ref2; i = k += 1) { + results.push((function() { + var l, ref3, ref4, results1; + results1 = []; + for (j = l = ref3 = -d, ref4 = d; l <= ref4; j = l += 1) { + xx = x + i; + yy = y + j; + if (this.tile) { + results1.push((function() { + var m, results2; + results2 = []; + for (ii = m = -1; m <= 1; ii = m += 1) { + results2.push((function() { + var n, results3; + results3 = []; + for (jj = n = -1; n <= 1; jj = n += 1) { + results3.push(this.preprocessPixel(sprite, xx + ii * sprite.width, yy + jj * sprite.height, button)); + } + return results3; + }).call(this)); + } + return results2; + }).call(this)); + } else { + results1.push(this.preprocessPixel(sprite, xx, yy, button)); + } + } + return results1; + }).call(this)); + } + return results; + }; + + DrawTool.prototype.preprocessPixel = function(sprite, x, y, button) { + if (x < 0 || x >= sprite.width || y < 0 || y >= sprite.height) { + return; + } + if (this.pixels[x + "-" + y] == null) { + this.pixels[x + "-" + y] = true; + return this.processPixel(sprite, x, y, button); + } + }; + + DrawTool.prototype.processPixel = function(sprite, x, y) {}; + + DrawTool.tools = []; + + return DrawTool; + +})(); + +this.PencilTool = (function(superClass) { + extend(PencilTool, superClass); + + function PencilTool() { + PencilTool.__super__.constructor.call(this, "Pencil", "fa-pencil-alt"); + this.parameters["Size"] = { + type: "range", + value: 0 + }; + this.parameters["Opacity"] = { + type: "range", + value: 100 + }; + this.parameters["Color"] = { + type: "color", + value: "#FFF" + }; + } + + PencilTool.prototype.processPixel = function(sprite, x, y, button) { + var c; + if (button === 2) { + return sprite.erasePixel(x, y, this.parameters["Opacity"].value / 100); + } else { + c = sprite.getContext(); + c.globalAlpha = this.parameters["Opacity"].value / 100; + c.fillStyle = this.parameters["Color"].value; + c.fillRect(x, y, 1, 1); + return c.globalAlpha = 1; + } + }; + + return PencilTool; + +})(this.DrawTool); + +this.DrawTool.tools.push(new this.PencilTool()); + +this.EraserTool = (function(superClass) { + extend(EraserTool, superClass); + + function EraserTool() { + EraserTool.__super__.constructor.call(this, "Eraser", "fa-eraser"); + this.parameters["Size"] = { + type: "range", + value: 0 + }; + this.parameters["Opacity"] = { + type: "range", + value: 100 + }; + } + + EraserTool.prototype.processPixel = function(sprite, x, y) { + return sprite.erasePixel(x, y, this.parameters["Opacity"].value / 100); + }; + + return EraserTool; + +})(this.DrawTool); + +this.DrawTool.tools.push(new this.EraserTool()); + +this.FillTool = (function(superClass) { + extend(FillTool, superClass); + + function FillTool() { + FillTool.__super__.constructor.call(this, "Fill", "fa-fill-drip"); + this.parameters["Threshold"] = { + type: "range", + value: 0 + }; + this.parameters["Opacity"] = { + type: "range", + value: 100 + }; + this.parameters["Color"] = { + type: "color", + value: "#FFF" + }; + } + + FillTool.prototype.start = function(sprite, x, y) { + var alpha, c, check, data, fill, index, list, p, ref, table, threshold; + if (x < 0 || y < 0 || x >= sprite.width || y >= sprite.height) { + return; + } + threshold = this.parameters["Threshold"].value / 100 * 255 + 1; + alpha = this.parameters["Opacity"].value / 100; + c = sprite.getContext(); + ref = c.getImageData(x, y, 1, 1); + data = c.getImageData(0, 0, sprite.width, sprite.height); + c.clearRect(x, y, 1, 1); + c.globalAlpha = alpha; + c.fillStyle = this.parameters["Color"].value; + c.fillRect(x, y, 1, 1); + c.globalAlpha = 1; + fill = c.getImageData(x, y, 1, 1); + list = [[x, y]]; + table = {}; + table[x + "-" + y] = true; + check = function(x, y) { + var da, db, dg, dr, index; + if (x < 0 || y < 0 || x >= sprite.width || y >= sprite.height) { + return false; + } + if (table[x + "-" + y]) { + return false; + } + index = 4 * (x + y * sprite.width); + dr = Math.abs(data.data[index] - ref.data[0]); + dg = Math.abs(data.data[index + 1] - ref.data[1]); + db = Math.abs(data.data[index + 2] - ref.data[2]); + da = Math.abs(data.data[index + 3] - ref.data[3]); + return Math.max(dr, dg, db, da) < threshold; + }; + while (list.length > 0) { + p = list.splice(0, 1)[0]; + x = p[0]; + y = p[1]; + index = 4 * (x + y * sprite.width); + data.data[index] = fill.data[0]; + data.data[index + 1] = fill.data[1]; + data.data[index + 2] = fill.data[2]; + data.data[index + 3] = fill.data[3]; + if (check(x - 1, y)) { + table[(x - 1) + "-" + y] = true; + list.push([x - 1, y]); + } + if (check(x + 1, y)) { + table[(x + 1) + "-" + y] = true; + list.push([x + 1, y]); + } + if (check(x, y - 1)) { + table[x + "-" + (y - 1)] = true; + list.push([x, y - 1]); + } + if (check(x, y + 1)) { + table[x + "-" + (y + 1)] = true; + list.push([x, y + 1]); + } + } + c.putImageData(data, 0, 0); + }; + + FillTool.prototype.move = function(sprite, x, y) {}; + + return FillTool; + +})(this.DrawTool); + +this.DrawTool.tools.push(new this.FillTool()); + +this.BrightenTool = (function(superClass) { + extend(BrightenTool, superClass); + + function BrightenTool(parent) { + BrightenTool.__super__.constructor.call(this, "Brighten", "fa-sun"); + this.parameters = parent.parameters; + } + + BrightenTool.prototype.processPixel = function(sprite, x, y) { + var amount, b, c, data, db, dg, dr, g, r, v; + amount = this.parameters["Amount"].value / 100; + c = sprite.getContext(); + data = c.getImageData(x, y, 1, 1); + r = data.data[0]; + g = data.data[1]; + b = data.data[2]; + v = (r + g + b) / 3; + dr = r - v; + dg = g - v; + db = b - v; + v = v * (1 + amount * .5); + data.data[0] = Math.min(255, v + dr); + data.data[1] = Math.min(255, v + dg); + data.data[2] = Math.min(255, v + db); + return c.putImageData(data, x, y); + }; + + return BrightenTool; + +})(this.DrawTool); + +this.DarkenTool = (function(superClass) { + extend(DarkenTool, superClass); + + function DarkenTool(parent) { + DarkenTool.__super__.constructor.call(this, "Darken", "fa-moon"); + this.parameters = parent.parameters; + } + + DarkenTool.prototype.processPixel = function(sprite, x, y) { + var amount, b, c, data, db, dg, dr, g, r, v; + amount = this.parameters["Amount"].value / 100; + c = sprite.getContext(); + data = c.getImageData(x, y, 1, 1); + r = data.data[0]; + g = data.data[1]; + b = data.data[2]; + v = (r + g + b) / 3; + dr = r - v; + dg = g - v; + db = b - v; + v = v * (1 - amount * .5); + data.data[0] = Math.max(0, v + dr); + data.data[1] = Math.max(0, v + dg); + data.data[2] = Math.max(0, v + db); + return c.putImageData(data, x, y); + }; + + return DarkenTool; + +})(this.DrawTool); + +this.SmoothenTool = (function(superClass) { + extend(SmoothenTool, superClass); + + function SmoothenTool(parent) { + SmoothenTool.__super__.constructor.call(this, "Smoothen", "fa-brush"); + this.parameters = parent.parameters; + } + + SmoothenTool.prototype.processPixel = function(sprite, x, y) { + var amount, c, co, coef, data, i, j, k, l, ref, sum, xx, yy; + amount = this.parameters["Amount"].value / 100; + c = sprite.getContext(); + sum = [0, 0, 0, 0]; + coef = 0; + ref = c.getImageData(x, y, 1, 1); + for (i = k = -1; k <= 1; i = k += 1) { + for (j = l = -1; l <= 1; j = l += 1) { + xx = x + i; + yy = y + j; + if (xx < 0 || yy < 0 || xx >= sprite.width || yy >= sprite.height) { + continue; + } + data = c.getImageData(xx, yy, 1, 1); + co = 1 / (1 + i * i + j * j) * (1 + data.data[3]); + coef += co; + sum[0] += data.data[0] * co; + sum[1] += data.data[1] * co; + sum[2] += data.data[2] * co; + sum[3] += data.data[3] * co; + } + } + data.data[0] = sum[0] / coef * amount + (1 - amount) * ref.data[0]; + data.data[1] = sum[1] / coef * amount + (1 - amount) * ref.data[1]; + data.data[2] = sum[2] / coef * amount + (1 - amount) * ref.data[2]; + data.data[3] = sum[3] / coef * amount + (1 - amount) * ref.data[3]; + return c.putImageData(data, x, y); + }; + + return SmoothenTool; + +})(this.DrawTool); + +this.SharpenTool = (function(superClass) { + extend(SharpenTool, superClass); + + function SharpenTool(parent) { + SharpenTool.__super__.constructor.call(this, "Sharpen", "fa-adjust"); + this.parameters = parent.parameters; + } + + SharpenTool.prototype.processPixel = function(sprite, x, y) { + var amount, c, co, coef, data, i, j, k, l, ref, sum, xx, yy; + amount = this.parameters["Amount"].value / 100; + c = sprite.getContext(); + sum = [0, 0, 0, 0]; + coef = 0; + ref = c.getImageData(x, y, 1, 1); + for (i = k = -1; k <= 1; i = k += 1) { + for (j = l = -1; l <= 1; j = l += 1) { + if (i === 0 && j === 0) { + continue; + } + xx = x + i; + yy = y + j; + if (xx < 0 || yy < 0 || xx >= sprite.width || yy >= sprite.height) { + continue; + } + data = c.getImageData(xx, yy, 1, 1); + co = 1 / (1 + i * i + j * j) * (1 + data.data[3]); + coef += co; + sum[0] += (ref.data[0] - data.data[0]) * co; + sum[1] += (ref.data[1] - data.data[1]) * co; + sum[2] += (ref.data[2] - data.data[2]) * co; + sum[3] += (ref.data[3] - data.data[3]) * co; + } + } + ref.data[0] += sum[0] / coef * amount; + ref.data[1] += sum[1] / coef * amount; + ref.data[2] += sum[2] / coef * amount; + ref.data[3] += sum[3] / coef * amount; + return c.putImageData(ref, x, y); + }; + + return SharpenTool; + +})(this.DrawTool); + +this.SaturationTool = (function(superClass) { + extend(SaturationTool, superClass); + + function SaturationTool(parent) { + SaturationTool.__super__.constructor.call(this, "Saturation", "fa-palette"); + this.parameters = parent.parameters; + } + + SaturationTool.prototype.processPixel = function(sprite, x, y) { + var amount, b, c, data, db, dg, dr, g, r, v; + amount = this.parameters["Amount"].value / 100; + amount = amount > .5 ? amount * 2 : .5 + amount; + c = sprite.getContext(); + data = c.getImageData(x, y, 1, 1); + r = data.data[0]; + g = data.data[1]; + b = data.data[2]; + v = (r + g + b) / 3; + dr = r - v; + dg = g - v; + db = b - v; + data.data[0] = Math.max(0, v + dr * amount); + data.data[1] = Math.max(0, v + dg * amount); + data.data[2] = Math.max(0, v + db * amount); + return c.putImageData(data, x, y); + }; + + return SaturationTool; + +})(this.DrawTool); + +this.EnhanceTool = (function(superClass) { + extend(EnhanceTool, superClass); + + function EnhanceTool() { + EnhanceTool.__super__.constructor.call(this, "Enhance", "fa-magic"); + this.parameters["Tool"] = { + type: "tool", + set: [new BrightenTool(this), new DarkenTool(this), new SmoothenTool(this), new SharpenTool(this), new SaturationTool(this)], + value: 0 + }; + this.parameters["Size"] = { + type: "range", + value: 0 + }; + this.parameters["Amount"] = { + type: "range", + value: 50 + }; + } + + EnhanceTool.prototype.processPixel = function(sprite, x, y) { + return this.parameters.Tool.set[this.parameters.Tool.value].processPixel(sprite, x, y); + }; + + return EnhanceTool; + +})(this.DrawTool); + +this.DrawTool.tools.push(new this.EnhanceTool()); + +this.SelectTool = (function(superClass) { + extend(SelectTool, superClass); + + function SelectTool() { + SelectTool.__super__.constructor.call(this, "Select", "fa-vector-square"); + this.selectiontool = true; + } + + SelectTool.prototype.processPixel = function(sprite, x, y) {}; + + return SelectTool; + +})(this.DrawTool); + +this.DrawTool.tools.push(new this.SelectTool()); diff --git a/static/js/spriteeditor/spriteeditor.coffee b/static/js/spriteeditor/spriteeditor.coffee new file mode 100644 index 00000000..4f84b355 --- /dev/null +++ b/static/js/spriteeditor/spriteeditor.coffee @@ -0,0 +1,711 @@ +class @SpriteEditor + constructor:(@app)-> + @spriteview = new SpriteView @ + + @auto_palette = new AutoPalette @ + @colorpicker = new ColorPicker @ + document.getElementById("colorpicker").appendChild @colorpicker.canvas + + @animation_panel = new AnimationPanel @ + + @save_delay = 1000 + @save_time = 0 + setInterval (()=>@checkSave()),@save_delay/2 + + @app.appui.setAction "create-sprite-button",()=>@createSprite() + #document.getElementById("sprite-name").addEventListener "input",(event)=>@spriteNameChanged() + + @sprite_name_validator = new InputValidator document.getElementById("sprite-name"), + document.getElementById("sprite-name-button"), + null, + (value)=> + return if @app.project.isLocked("sprites/#{@selected_sprite}.png") + @app.project.lockFile("sprites/#{@selected_sprite}.png") + name = value[0].toLowerCase() + name = RegexLib.fixFilename(name) + document.getElementById("sprite-name").value = name + @sprite_name_validator.update() + if @selected_sprite != "icon" and name != @selected_sprite and RegexLib.filename.test(name) and not @app.project.getSprite(name)? + old = @selected_sprite + @selected_sprite = name + @spriteview.sprite.rename name + @app.project.changeSpriteName(old,name) + @app.project.lockFile("sprites/#{@selected_sprite}.png") + @saveSprite ()=> + @app.client.sendRequest { + name: "delete_project_file" + project: @app.project.id + file: "sprites/#{old}.png" + },(msg)=> + @app.project.updateSpriteList() + + @sprite_name_validator.regex = RegexLib.filename + + document.getElementById("sprite-width").addEventListener "input",(event)=>@spriteDimensionChanged("width") + document.getElementById("sprite-height").addEventListener "input",(event)=>@spriteDimensionChanged("height") + + @sprite_size_validator = new InputValidator [document.getElementById("sprite-width"),document.getElementById("sprite-height")], + document.getElementById("sprite-size-button"), + null, + (value)=> + @saveDimensionChange(value) + + @selected_sprite = null + + @app.appui.setAction "undo-sprite",()=>@undo() + @app.appui.setAction "redo-sprite",()=>@redo() + @app.appui.setAction "copy-sprite",()=>@copy() + @app.appui.setAction "cut-sprite",()=>@cut() + @app.appui.setAction "paste-sprite",()=>@paste() + @app.appui.setAction "delete-sprite",()=>@deleteSprite() + + @app.appui.setAction "sprite-helper-tile",()=>@toggleTile() + @app.appui.setAction "sprite-helper-vsymmetry",()=>@toggleVSymmetry() + @app.appui.setAction "sprite-helper-hsymmetry",()=>@toggleHSymmetry() + + @app.appui.setAction "selection-operation-film",()=>@stripToAnimation() + + document.addEventListener "keydown",(event)=> + return if not document.getElementById("spriteeditor").offsetParent? + #console.info event + if event.key == "Alt" and not @tool.selectiontool + @setColorPicker(true) + @alt_pressed = true + + if event.metaKey or event.ctrlKey + switch event.key + when "z" then @undo() + when "Z" then @redo() + when "c" then @copy() + when "x" then @cut() + when "v" then @paste() + + #console.info event + + document.addEventListener "keyup",(event)=> + if event.key == "Alt" and not @tool.selectiontool + @setColorPicker(false) + @alt_pressed = false + + document.getElementById("eyedropper").addEventListener "click",()=> + @setColorPicker not @spriteview.colorpicker + + for tool,i in DrawTool.tools + @createToolButton tool + @createToolOptions tool + @setSelectedTool DrawTool.tools[0].icon + + document.getElementById("spritelist").addEventListener "dragover",(event)=> + event.preventDefault() + #console.info event + + document.getElementById("spritelist").addEventListener "drop",(event)=> + event.preventDefault() + try + list = [] + for i in event.dataTransfer.items + list.push i.getAsFile() + + funk = ()=> + if list.length>0 + file = list.splice(0,1)[0] + console.info "processing #{file.name}" + img = new Image + reader = new FileReader() + reader.addEventListener "load",()=> + img.src = reader.result + + reader.readAsDataURL(file) + #url = "data:application/javascript;base64,"+btoa(Audio.processor) + + img.onload = ()=> + if img.complete and img.width>0 and img.height>0 and img.width<=2048 and img.height<=2048 + @createSprite RegexLib.fixFilename(file.name.split(".")[0]),img,()=>funk() + funk() + + catch err + console.error err + + createToolButton:(tool)-> + parent = document.getElementById("spritetools") + + div = document.createElement "div" + div.classList.add "spritetoolbutton" + div.title = tool.name + + div.innerHTML = "
#{@app.translator.get(tool.name)}" + div.addEventListener "click",()=> + @setSelectedTool(tool.icon) + + div.id = "spritetoolbutton-#{tool.icon}" + + parent.appendChild div + + createToolOptions:(tool)-> + parent = document.getElementById("spritetooloptionslist") + + div = document.createElement "div" + + for key,p of tool.parameters + if p.type == "range" + do (p,key)=> + label = document.createElement "label" + label.innerText = key + div.appendChild label + input = document.createElement "input" + input.type = "range" + input.min = "0" + input.max = "100" + input.value = p.value + input.addEventListener "input",(event)=> + p.value = input.value + if key == "Size" + @spriteview.showBrushSize() + div.appendChild input + else if p.type == "tool" + toolbox = document.createElement "div" + toolbox.classList.add "toolbox" + div.appendChild toolbox + + for t,k in p.set + button = document.createElement "div" + button.classList.add "spritetoolbutton" + if k==0 + button.classList.add "selected" + button.title = t.name + button.id = "spritetoolbutton-#{t.icon}" + i = document.createElement "i" + i.classList.add "fa" + i.classList.add t.icon + button.appendChild i + button.appendChild document.createElement "br" + button.appendChild document.createTextNode t.name + toolbox.appendChild button + t.button = button + + do (p,k)=> + button.addEventListener "click",()=> + p.value = k + for t,i in p.set + if i == k + t.button.classList.add "selected" + else + t.button.classList.remove "selected" + + div.id = "spritetooloptions-#{tool.icon}" + parent.appendChild div + + setSelectedTool:(id)-> + for tool in DrawTool.tools + e = document.getElementById "spritetoolbutton-#{tool.icon}" + if tool.icon == id + @tool = tool + e.classList.add "selected" + else + e.classList.remove "selected" + + e = document.getElementById "spritetooloptions-#{tool.icon}" + if tool.icon == id + e.style.display = "block" + else + e.style.display = "none" + + document.getElementById("colorpicker-group").style.display = if @tool.parameters["Color"]? then "block" else "none" + @spriteview.update() + @updateSelectionHints() + + toggleTile:()-> + @spriteview.tile = not @spriteview.tile + @spriteview.update() + if @spriteview.tile + document.getElementById("sprite-helper-tile").classList.add "selected" + else + document.getElementById("sprite-helper-tile").classList.remove "selected" + + toggleVSymmetry:()-> + @spriteview.vsymmetry = not @spriteview.vsymmetry + @spriteview.update() + if @spriteview.vsymmetry + document.getElementById("sprite-helper-vsymmetry").classList.add "selected" + else + document.getElementById("sprite-helper-vsymmetry").classList.remove "selected" + + toggleHSymmetry:()-> + @spriteview.hsymmetry = not @spriteview.hsymmetry + @spriteview.update() + if @spriteview.hsymmetry + document.getElementById("sprite-helper-hsymmetry").classList.add "selected" + else + document.getElementById("sprite-helper-hsymmetry").classList.remove "selected" + + spriteChanged:()-> + return if @ignore_changes + @app.project.lockFile("sprites/#{@selected_sprite}.png") + @save_time = Date.now() + s = @app.project.getSprite @selected_sprite + s.updated(@spriteview.sprite.saveData()) if s? + # s.loaded() # triggers update of all maps + @app.project.addPendingChange @ + @animation_panel.frameUpdated() + @auto_palette.update() + @app.project.notifyListeners s + @app.runwindow.updateSprite @selected_sprite + #@updateLocalSprites() + + checkSave:(immediate=false,callback)-> + if @save_time>0 and (immediate or Date.now()>@save_time+@save_delay) + @saveSprite(callback) + @save_time = 0 + else + callback() if callback? + + forceSave:(callback)-> + @checkSave(true,callback) + + projectOpened:()-> + @app.project.addListener @ + @setSelectedSprite null + + projectUpdate:(change)-> + if change == "spritelist" + @rebuildSpriteList() + else if change == "locks" + @updateCurrentFileLock() + @updateActiveUsers() + else if change instanceof ProjectSprite + name = change.name + c = document.querySelector "#sprite-image-#{name}" + sprite = change + if c? and c.updateSprite? + c.updateSprite() + + updateCurrentFileLock:()-> + if @selected_sprite? + @spriteview.editable = not @app.project.isLocked("sprites/#{@selected_sprite}.png") + + lock = document.getElementById("sprite-editor-locked") + if @selected_sprite? and @app.project.isLocked("sprites/#{@selected_sprite}.png") + user = @app.project.isLocked("sprites/#{@selected_sprite}.png").user + lock.style = "display: block; background: #{@app.appui.createFriendColor(user)}" + lock.innerHTML = " Locked by #{user}" + else + lock.style = "display: none" + + saveSprite:(callback)-> + return if not @selected_sprite? or not @spriteview.sprite + data = @spriteview.sprite.saveData().split(",")[1] + sprite = @spriteview.sprite + saved = false + @app.client.sendRequest { + name: "write_project_file" + project: @app.project.id + file: "sprites/#{@selected_sprite}.png" + properties: + frames: @spriteview.sprite.frames.length + fps: @spriteview.sprite.fps + content: data + },(msg)=> + saved = true + @app.project.removePendingChange(@) if @save_time == 0 + sprite.size = msg.size + callback() if callback? + + setTimeout (()=> + if not saved + @save_time = Date.now() + console.info("retrying sprite save...") + ),10000 + + createSprite:(name,img,callback)-> + @checkSave true,()=> + if img? + width = img.width + height = img.height + else if @spriteview.selection? + width = Math.max(8,@spriteview.selection.w) + height = Math.max(8,@spriteview.selection.h) + else + width = Math.max(8,@spriteview.sprite.width) + height = Math.max(8,@spriteview.sprite.height) + + sprite = @app.project.createSprite(width,height,name) + @spriteview.setSprite sprite + @animation_panel.spriteChanged() + if img? + @spriteview.getFrame().getContext().drawImage img,0,0 + + @spriteview.update() + @setSelectedSprite(sprite.name) + @spriteview.editable = true + @saveSprite ()=> + @rebuildSpriteList() + callback() if callback? + + setSelectedSprite:(sprite)-> + @selected_sprite = sprite + @animation_panel.spriteChanged() + list = document.getElementById("sprite-list").childNodes + + if @selected_sprite? + for e in list + if e.getAttribute("id") == "project-sprite-#{sprite}" + e.classList.add("selected") + else + e.classList.remove("selected") + + document.getElementById("sprite-name").value = sprite + @sprite_name_validator.update() + document.getElementById("sprite-name").disabled = @selected_sprite == "icon" + if @spriteview.sprite? + document.getElementById("sprite-width").value = @spriteview.sprite.width + document.getElementById("sprite-height").value = @spriteview.sprite.height + @sprite_size_validator.update() + + document.getElementById("sprite-width").disabled = false + document.getElementById("sprite-height").disabled = false + + e = document.getElementById("spriteeditor") + if e.firstChild? + e.firstChild.style.display = "inline-block" + @spriteview.windowResized() + else + for e in list + e.classList.remove("selected") + document.getElementById("sprite-name").value = "" + document.getElementById("sprite-name").disabled = true + document.getElementById("sprite-width").disabled = true + document.getElementById("sprite-height").disabled = true + e = document.getElementById("spriteeditor") + if e.firstChild? + e.firstChild.style.display = "none" + + @updateCurrentFileLock() + @updateSelectionHints() + @auto_palette.update() + + setSprite:(data)-> + data = "data:image/png;base64,"+data + @ignore_changes = true + img = new Image + img.src = data + img.crossOrigin = "Anonymous" + img.onload = ()=> + @spriteview.sprite.load(img) + @spriteview.windowResized() + @spriteview.update() + @spriteview.editable = true + @ignore_changes = false + @spriteview.windowResized() + document.getElementById("sprite-width").value = @spriteview.sprite.width + document.getElementById("sprite-height").value = @spriteview.sprite.height + @sprite_size_validator.update() + + setColor:(@color)-> + @spriteview.setColor @color + @auto_palette.colorPicked @color + + rebuildSpriteList:()-> + list = document.getElementById "sprite-list" + list.innerHTML = "" + + for s in @app.project.sprite_list + element = @createSpriteBox s + list.appendChild element + + @updateActiveUsers() + if @selected_sprite? and not @app.project.getSprite(@selected_sprite)? + @setSelectedSprite null + return + + openSprite:(s)-> + @checkSave(true) + @spriteview.setSprite @app.project.getSprite s + @spriteview.windowResized() + @spriteview.update() + @spriteview.editable = true + @setSelectedSprite(s) + + updateActiveUsers:()-> + list = document.getElementById("sprite-list").childNodes + for e in list + file = e.id.split("-")[2] + lock = @app.project.isLocked("sprites/#{file}.png") + if lock? and Date.now() + element = document.createElement "div" + element.classList.add "sprite-box" + element.setAttribute "id","project-sprite-#{sprite.name}" + element.setAttribute "title",sprite.name + if sprite.name == @selected_sprite + element.classList.add "selected" + + iconbox = document.createElement "div" + iconbox.classList.add("icon-box") + + icon = @createSpriteThumb(sprite) + icon.setAttribute "id","sprite-image-#{sprite.name}" + iconbox.appendChild icon + element.appendChild iconbox + + sprite.addImage icon,64 + + element.appendChild document.createElement "br" + + text = document.createElement "span" + text.innerHTML = sprite.name + + element.appendChild text + + element.addEventListener "click",()=> + @openSprite sprite.name + + activeuser = document.createElement "i" + activeuser.classList.add "active-user" + activeuser.classList.add "fa" + activeuser.classList.add "fa-user" + element.appendChild activeuser + element + + createSpriteThumb:(sprite)-> + canvas = document.createElement "canvas" + canvas.width = 64 + canvas.height = 64 + sprite.addLoadListener ()=> + context = canvas.getContext "2d" + frame = sprite.frames[0].getCanvas() + r = Math.min(64/frame.width,64/frame.height) + context.imageSmoothingEnabled = false + w = r*frame.width + h = r*frame.height + context.drawImage frame,32-w/2,32-h/2,w,h + + mouseover = false + update = ()=> + if mouseover and sprite.frames.length>1 + requestAnimationFrame ()=>update() + + dt = 1000/sprite.fps + t = Date.now() + frame = if mouseover then Math.floor(t/dt)%sprite.frames.length else 0 + context = canvas.getContext "2d" + context.imageSmoothingEnabled = false + context.clearRect 0,0,64,64 + frame = sprite.frames[frame].getCanvas() + r = Math.min(64/frame.width,64/frame.height) + w = r*frame.width + h = r*frame.height + context.drawImage frame,32-w/2,32-h/2,w,h + + canvas.addEventListener "mouseenter",()=> + mouseover = true + update() + + canvas.addEventListener "mouseout",()=> + mouseover = false + + canvas.updateSprite = update + + canvas + + spriteDimensionChanged:(dim)-> + if @selected_sprite == "icon" + if dim == "width" + document.getElementById("sprite-height").value = document.getElementById("sprite-width").value + else + document.getElementById("sprite-width").value = document.getElementById("sprite-height").value + + saveDimensionChange:(value)-> + return if @app.project.isLocked("sprites/#{@selected_sprite}.png") + @app.project.lockFile("sprites/#{@selected_sprite}.png") + w = value[0] + h = value[1] + + try + w = Number.parseFloat(w) + h = Number.parseFloat(h) + catch err + + if (@selected_sprite != "icon" or w == h) and Number.isInteger(w) and Number.isInteger(h) and w>0 and h>0 and w<257 and h<257 and @selected_sprite? and (w != @spriteview.sprite.width or h != @spriteview.sprite.height) + @spriteview.sprite.undo = new Undo() if not @spriteview.sprite.undo? + @spriteview.sprite.undo.pushState @spriteview.sprite.clone() if @spriteview.sprite.undo.empty() + @spriteview.sprite.resize(w,h) + @spriteview.sprite.undo.pushState @spriteview.sprite.clone() + @spriteview.windowResized() + @spriteview.update() + @spriteChanged() + @checkSave(true) + document.getElementById("sprite-width").value = @spriteview.sprite.width + document.getElementById("sprite-height").value = @spriteview.sprite.height + @sprite_size_validator.update() + else + document.getElementById("sprite-width").value = @spriteview.sprite.width + document.getElementById("sprite-height").value = @spriteview.sprite.height + @sprite_size_validator.update() + + deleteSprite:()-> + return if @app.project.isLocked("sprites/#{@selected_sprite}.png") + @app.project.lockFile("sprites/#{@selected_sprite}.png") + if @selected_sprite? and @selected_sprite != "icon" + if confirm "Really delete #{@selected_sprite}?" + @app.client.sendRequest { + name: "delete_project_file" + project: @app.project.id + file: "sprites/#{@selected_sprite}.png" + },(msg)=> + @app.project.updateSpriteList() + @spriteview.sprite = new Sprite(16,16) + @spriteview.update() + @spriteview.editable = false + @setSelectedSprite null + + undo:()-> + return if @app.project.isLocked("sprites/#{@selected_sprite}.png") + @app.project.lockFile("sprites/#{@selected_sprite}.png") + if @spriteview.sprite and @spriteview.sprite.undo? + s = @spriteview.sprite.undo.undo ()=>@spriteview.sprite.clone() + @spriteview.selection = null + if s? + @spriteview.sprite.copyFrom s + @spriteview.update() + document.getElementById("sprite-width").value = @spriteview.sprite.width + document.getElementById("sprite-height").value = @spriteview.sprite.height + @sprite_size_validator.update() + @spriteview.windowResized() + @spriteChanged() + @animation_panel.updateFrames() + + redo:()-> + return if @app.project.isLocked("sprites/#{@selected_sprite}.png") + @app.project.lockFile("sprites/#{@selected_sprite}.png") + if @spriteview.sprite and @spriteview.sprite.undo? + s = @spriteview.sprite.undo.redo() + @spriteview.selection = null + if s? + @spriteview.sprite.copyFrom s + @spriteview.update() + document.getElementById("sprite-width").value = @spriteview.sprite.width + document.getElementById("sprite-height").value = @spriteview.sprite.height + @sprite_size_validator.update() + @spriteview.windowResized() + @spriteChanged() + @animation_panel.updateFrames() + + copy:()-> + if @tool.selectiontool and @spriteview.selection? + @clipboard = new Sprite(@spriteview.selection.w,@spriteview.selection.h) + @clipboard.frames[0].getContext().drawImage @spriteview.getFrame().canvas,-@spriteview.selection.x,-@spriteview.selection.y + @clipboard.partial = true + else + @clipboard = @spriteview.sprite.clone() + + cut:()-> + return if @app.project.isLocked("sprites/#{@selected_sprite}.png") + @app.project.lockFile("sprites/#{@selected_sprite}.png") + + @spriteview.sprite.undo = new Undo() if not @spriteview.sprite.undo? + @spriteview.sprite.undo.pushState @spriteview.sprite.clone() if @spriteview.sprite.undo.empty() + + if @tool.selectiontool and @spriteview.selection? + @clipboard = new Sprite(@spriteview.selection.w,@spriteview.selection.h) + @clipboard.frames[0].getContext().drawImage @spriteview.getFrame().canvas,-@spriteview.selection.x,-@spriteview.selection.y + @clipboard.partial = true + sel = @spriteview.selection + @spriteview.getFrame().getContext().clearRect sel.x,sel.y,sel.w,sel.h + else + @clipboard = @spriteview.sprite.clone() + @spriteview.sprite.clear() + + @spriteview.sprite.undo.pushState @spriteview.sprite.clone() + @currentSpriteUpdated() + @spriteChanged() + + paste:()-> + return if @app.project.isLocked("sprites/#{@selected_sprite}.png") + @app.project.lockFile("sprites/#{@selected_sprite}.png") + if @clipboard? + @spriteview.sprite.undo = new Undo() if not @spriteview.sprite.undo? + @spriteview.sprite.undo.pushState @spriteview.sprite.clone() if @spriteview.sprite.undo.empty() + + if @clipboard.partial + x = 0 + y = 0 + + x = Math.max(0,Math.min(@spriteview.sprite.width-@clipboard.width,@spriteview.mouse_x)) + y = Math.max(0,Math.min(@spriteview.sprite.height-@clipboard.height,@spriteview.mouse_y)) + + @spriteview.floating_selection = + bg: @spriteview.getFrame().clone().getCanvas() + fg: @clipboard.frames[0].getCanvas() + @spriteview.selection = + x: x + y: y + w: @clipboard.frames[0].canvas.width + h: @clipboard.frames[0].canvas.height + + @spriteview.getFrame().getContext().drawImage @clipboard.frames[0].getCanvas(),x,y + @setSelectedTool("fa-vector-square") + else + @spriteview.sprite.copyFrom(@clipboard) + + @spriteview.sprite.undo.pushState @spriteview.sprite.clone() + @currentSpriteUpdated() + @spriteChanged() + + currentSpriteUpdated:()-> + @spriteview.update() + document.getElementById("sprite-width").value = @spriteview.sprite.width + document.getElementById("sprite-height").value = @spriteview.sprite.height + @sprite_size_validator.update() + @spriteview.windowResized() + + setColorPicker:(picker)-> + @spriteview.colorpicker = picker + + if picker + #@spriteview.canvas.classList.add "colorpicker" + @spriteview.canvas.style.cursor = "url( '/img/eyedropper.svg' ) 0 24, pointer" + document.getElementById("eyedropper").classList.add "selected" + else + #@spriteview.canvas.classList.remove "colorpicker" + @spriteview.canvas.style.cursor = "crosshair" + document.getElementById("eyedropper").classList.remove "selected" + + updateSelectionHints:()-> + if @spriteview.selection? and @tool.selectiontool + document.getElementById("selection-group").style.display = "block" + w = @spriteview.selection.w + h = @spriteview.selection.h + if @spriteview.sprite.frames.length == 1 and (@spriteview.sprite.width/w)%1 == 0 and (@spriteview.sprite.height/h)%1 == 0 and (@spriteview.sprite.width/w >=2 or @spriteview.sprite.height/h >= 2) + document.getElementById("selection-operation-film").style.display = "block" + else + document.getElementById("selection-operation-film").style.display = "none" + else + document.getElementById("selection-group").style.display = "none" + + stripToAnimation:()-> + w = @spriteview.selection.w + h = @spriteview.selection.h + if @spriteview.sprite.frames.length == 1 and (@spriteview.sprite.width/w)%1 == 0 and (@spriteview.sprite.height/h)%1 == 0 and (@spriteview.sprite.width/w >=2 or @spriteview.sprite.height/h >= 2) + @spriteview.sprite.undo = new Undo() if not @spriteview.sprite.undo? + @spriteview.sprite.undo.pushState @spriteview.sprite.clone() if @spriteview.sprite.undo.empty() + + n = @spriteview.sprite.width/w + m = @spriteview.sprite.height/h + sprite = new Sprite(w,h) + index = 0 + for j in [0..m-1] by 1 + for i in [0..n-1] by 1 + sprite.frames[index] = new SpriteFrame(sprite,w,h) + sprite.frames[index].getContext().drawImage(@spriteview.sprite.frames[0].getCanvas(),-i*w,-j*h) + index++ + + @spriteview.sprite.copyFrom sprite + @spriteview.sprite.undo.pushState @spriteview.sprite.clone() + @currentSpriteUpdated() + @spriteChanged() + @animation_panel.spriteChanged() diff --git a/static/js/spriteeditor/spriteeditor.js b/static/js/spriteeditor/spriteeditor.js new file mode 100644 index 00000000..973caa23 --- /dev/null +++ b/static/js/spriteeditor/spriteeditor.js @@ -0,0 +1,965 @@ +this.SpriteEditor = (function() { + function SpriteEditor(app) { + var i, l, len, ref, tool; + this.app = app; + this.spriteview = new SpriteView(this); + this.auto_palette = new AutoPalette(this); + this.colorpicker = new ColorPicker(this); + document.getElementById("colorpicker").appendChild(this.colorpicker.canvas); + this.animation_panel = new AnimationPanel(this); + this.save_delay = 1000; + this.save_time = 0; + setInterval(((function(_this) { + return function() { + return _this.checkSave(); + }; + })(this)), this.save_delay / 2); + this.app.appui.setAction("create-sprite-button", (function(_this) { + return function() { + return _this.createSprite(); + }; + })(this)); + this.sprite_name_validator = new InputValidator(document.getElementById("sprite-name"), document.getElementById("sprite-name-button"), null, (function(_this) { + return function(value) { + var name, old; + if (_this.app.project.isLocked("sprites/" + _this.selected_sprite + ".png")) { + return; + } + _this.app.project.lockFile("sprites/" + _this.selected_sprite + ".png"); + name = value[0].toLowerCase(); + name = RegexLib.fixFilename(name); + document.getElementById("sprite-name").value = name; + _this.sprite_name_validator.update(); + if (_this.selected_sprite !== "icon" && name !== _this.selected_sprite && RegexLib.filename.test(name) && (_this.app.project.getSprite(name) == null)) { + old = _this.selected_sprite; + _this.selected_sprite = name; + _this.spriteview.sprite.rename(name); + _this.app.project.changeSpriteName(old, name); + _this.app.project.lockFile("sprites/" + _this.selected_sprite + ".png"); + return _this.saveSprite(function() { + return _this.app.client.sendRequest({ + name: "delete_project_file", + project: _this.app.project.id, + file: "sprites/" + old + ".png" + }, function(msg) { + return _this.app.project.updateSpriteList(); + }); + }); + } + }; + })(this)); + this.sprite_name_validator.regex = RegexLib.filename; + document.getElementById("sprite-width").addEventListener("input", (function(_this) { + return function(event) { + return _this.spriteDimensionChanged("width"); + }; + })(this)); + document.getElementById("sprite-height").addEventListener("input", (function(_this) { + return function(event) { + return _this.spriteDimensionChanged("height"); + }; + })(this)); + this.sprite_size_validator = new InputValidator([document.getElementById("sprite-width"), document.getElementById("sprite-height")], document.getElementById("sprite-size-button"), null, (function(_this) { + return function(value) { + return _this.saveDimensionChange(value); + }; + })(this)); + this.selected_sprite = null; + this.app.appui.setAction("undo-sprite", (function(_this) { + return function() { + return _this.undo(); + }; + })(this)); + this.app.appui.setAction("redo-sprite", (function(_this) { + return function() { + return _this.redo(); + }; + })(this)); + this.app.appui.setAction("copy-sprite", (function(_this) { + return function() { + return _this.copy(); + }; + })(this)); + this.app.appui.setAction("cut-sprite", (function(_this) { + return function() { + return _this.cut(); + }; + })(this)); + this.app.appui.setAction("paste-sprite", (function(_this) { + return function() { + return _this.paste(); + }; + })(this)); + this.app.appui.setAction("delete-sprite", (function(_this) { + return function() { + return _this.deleteSprite(); + }; + })(this)); + this.app.appui.setAction("sprite-helper-tile", (function(_this) { + return function() { + return _this.toggleTile(); + }; + })(this)); + this.app.appui.setAction("sprite-helper-vsymmetry", (function(_this) { + return function() { + return _this.toggleVSymmetry(); + }; + })(this)); + this.app.appui.setAction("sprite-helper-hsymmetry", (function(_this) { + return function() { + return _this.toggleHSymmetry(); + }; + })(this)); + this.app.appui.setAction("selection-operation-film", (function(_this) { + return function() { + return _this.stripToAnimation(); + }; + })(this)); + document.addEventListener("keydown", (function(_this) { + return function(event) { + if (document.getElementById("spriteeditor").offsetParent == null) { + return; + } + if (event.key === "Alt" && !_this.tool.selectiontool) { + _this.setColorPicker(true); + _this.alt_pressed = true; + } + if (event.metaKey || event.ctrlKey) { + switch (event.key) { + case "z": + return _this.undo(); + case "Z": + return _this.redo(); + case "c": + return _this.copy(); + case "x": + return _this.cut(); + case "v": + return _this.paste(); + } + } + }; + })(this)); + document.addEventListener("keyup", (function(_this) { + return function(event) { + if (event.key === "Alt" && !_this.tool.selectiontool) { + _this.setColorPicker(false); + return _this.alt_pressed = false; + } + }; + })(this)); + document.getElementById("eyedropper").addEventListener("click", (function(_this) { + return function() { + return _this.setColorPicker(!_this.spriteview.colorpicker); + }; + })(this)); + ref = DrawTool.tools; + for (i = l = 0, len = ref.length; l < len; i = ++l) { + tool = ref[i]; + this.createToolButton(tool); + this.createToolOptions(tool); + } + this.setSelectedTool(DrawTool.tools[0].icon); + document.getElementById("spritelist").addEventListener("dragover", (function(_this) { + return function(event) { + return event.preventDefault(); + }; + })(this)); + document.getElementById("spritelist").addEventListener("drop", (function(_this) { + return function(event) { + var err, funk, len1, list, o, ref1; + event.preventDefault(); + try { + list = []; + ref1 = event.dataTransfer.items; + for (o = 0, len1 = ref1.length; o < len1; o++) { + i = ref1[o]; + list.push(i.getAsFile()); + } + funk = function() { + var file, img, reader; + if (list.length > 0) { + file = list.splice(0, 1)[0]; + console.info("processing " + file.name); + img = new Image; + reader = new FileReader(); + reader.addEventListener("load", function() { + return img.src = reader.result; + }); + reader.readAsDataURL(file); + return img.onload = function() { + if (img.complete && img.width > 0 && img.height > 0 && img.width <= 2048 && img.height <= 2048) { + return _this.createSprite(RegexLib.fixFilename(file.name.split(".")[0]), img, function() { + return funk(); + }); + } + }; + } + }; + return funk(); + } catch (error) { + err = error; + return console.error(err); + } + }; + })(this)); + } + + SpriteEditor.prototype.createToolButton = function(tool) { + var div, parent; + parent = document.getElementById("spritetools"); + div = document.createElement("div"); + div.classList.add("spritetoolbutton"); + div.title = tool.name; + div.innerHTML = "
" + (this.app.translator.get(tool.name)); + div.addEventListener("click", (function(_this) { + return function() { + return _this.setSelectedTool(tool.icon); + }; + })(this)); + div.id = "spritetoolbutton-" + tool.icon; + return parent.appendChild(div); + }; + + SpriteEditor.prototype.createToolOptions = function(tool) { + var button, div, fn, i, k, key, l, len, p, parent, ref, ref1, t, toolbox; + parent = document.getElementById("spritetooloptionslist"); + div = document.createElement("div"); + ref = tool.parameters; + for (key in ref) { + p = ref[key]; + if (p.type === "range") { + (function(_this) { + return (function(p, key) { + var input, label; + label = document.createElement("label"); + label.innerText = key; + div.appendChild(label); + input = document.createElement("input"); + input.type = "range"; + input.min = "0"; + input.max = "100"; + input.value = p.value; + input.addEventListener("input", function(event) { + p.value = input.value; + if (key === "Size") { + return _this.spriteview.showBrushSize(); + } + }); + return div.appendChild(input); + }); + })(this)(p, key); + } else if (p.type === "tool") { + toolbox = document.createElement("div"); + toolbox.classList.add("toolbox"); + div.appendChild(toolbox); + ref1 = p.set; + fn = (function(_this) { + return function(p, k) { + return button.addEventListener("click", function() { + var i, len1, o, ref2, results; + p.value = k; + ref2 = p.set; + results = []; + for (i = o = 0, len1 = ref2.length; o < len1; i = ++o) { + t = ref2[i]; + if (i === k) { + results.push(t.button.classList.add("selected")); + } else { + results.push(t.button.classList.remove("selected")); + } + } + return results; + }); + }; + })(this); + for (k = l = 0, len = ref1.length; l < len; k = ++l) { + t = ref1[k]; + button = document.createElement("div"); + button.classList.add("spritetoolbutton"); + if (k === 0) { + button.classList.add("selected"); + } + button.title = t.name; + button.id = "spritetoolbutton-" + t.icon; + i = document.createElement("i"); + i.classList.add("fa"); + i.classList.add(t.icon); + button.appendChild(i); + button.appendChild(document.createElement("br")); + button.appendChild(document.createTextNode(t.name)); + toolbox.appendChild(button); + t.button = button; + fn(p, k); + } + } + } + div.id = "spritetooloptions-" + tool.icon; + return parent.appendChild(div); + }; + + SpriteEditor.prototype.setSelectedTool = function(id) { + var e, l, len, ref, tool; + ref = DrawTool.tools; + for (l = 0, len = ref.length; l < len; l++) { + tool = ref[l]; + e = document.getElementById("spritetoolbutton-" + tool.icon); + if (tool.icon === id) { + this.tool = tool; + e.classList.add("selected"); + } else { + e.classList.remove("selected"); + } + e = document.getElementById("spritetooloptions-" + tool.icon); + if (tool.icon === id) { + e.style.display = "block"; + } else { + e.style.display = "none"; + } + } + document.getElementById("colorpicker-group").style.display = this.tool.parameters["Color"] != null ? "block" : "none"; + this.spriteview.update(); + return this.updateSelectionHints(); + }; + + SpriteEditor.prototype.toggleTile = function() { + this.spriteview.tile = !this.spriteview.tile; + this.spriteview.update(); + if (this.spriteview.tile) { + return document.getElementById("sprite-helper-tile").classList.add("selected"); + } else { + return document.getElementById("sprite-helper-tile").classList.remove("selected"); + } + }; + + SpriteEditor.prototype.toggleVSymmetry = function() { + this.spriteview.vsymmetry = !this.spriteview.vsymmetry; + this.spriteview.update(); + if (this.spriteview.vsymmetry) { + return document.getElementById("sprite-helper-vsymmetry").classList.add("selected"); + } else { + return document.getElementById("sprite-helper-vsymmetry").classList.remove("selected"); + } + }; + + SpriteEditor.prototype.toggleHSymmetry = function() { + this.spriteview.hsymmetry = !this.spriteview.hsymmetry; + this.spriteview.update(); + if (this.spriteview.hsymmetry) { + return document.getElementById("sprite-helper-hsymmetry").classList.add("selected"); + } else { + return document.getElementById("sprite-helper-hsymmetry").classList.remove("selected"); + } + }; + + SpriteEditor.prototype.spriteChanged = function() { + var s; + if (this.ignore_changes) { + return; + } + this.app.project.lockFile("sprites/" + this.selected_sprite + ".png"); + this.save_time = Date.now(); + s = this.app.project.getSprite(this.selected_sprite); + if (s != null) { + s.updated(this.spriteview.sprite.saveData()); + } + this.app.project.addPendingChange(this); + this.animation_panel.frameUpdated(); + this.auto_palette.update(); + this.app.project.notifyListeners(s); + return this.app.runwindow.updateSprite(this.selected_sprite); + }; + + SpriteEditor.prototype.checkSave = function(immediate, callback) { + if (immediate == null) { + immediate = false; + } + if (this.save_time > 0 && (immediate || Date.now() > this.save_time + this.save_delay)) { + this.saveSprite(callback); + return this.save_time = 0; + } else { + if (callback != null) { + return callback(); + } + } + }; + + SpriteEditor.prototype.forceSave = function(callback) { + return this.checkSave(true, callback); + }; + + SpriteEditor.prototype.projectOpened = function() { + this.app.project.addListener(this); + return this.setSelectedSprite(null); + }; + + SpriteEditor.prototype.projectUpdate = function(change) { + var c, name, sprite; + if (change === "spritelist") { + return this.rebuildSpriteList(); + } else if (change === "locks") { + this.updateCurrentFileLock(); + return this.updateActiveUsers(); + } else if (change instanceof ProjectSprite) { + name = change.name; + c = document.querySelector("#sprite-image-" + name); + sprite = change; + if ((c != null) && (c.updateSprite != null)) { + return c.updateSprite(); + } + } + }; + + SpriteEditor.prototype.updateCurrentFileLock = function() { + var lock, user; + if (this.selected_sprite != null) { + this.spriteview.editable = !this.app.project.isLocked("sprites/" + this.selected_sprite + ".png"); + } + lock = document.getElementById("sprite-editor-locked"); + if ((this.selected_sprite != null) && this.app.project.isLocked("sprites/" + this.selected_sprite + ".png")) { + user = this.app.project.isLocked("sprites/" + this.selected_sprite + ".png").user; + lock.style = "display: block; background: " + (this.app.appui.createFriendColor(user)); + return lock.innerHTML = " Locked by " + user; + } else { + return lock.style = "display: none"; + } + }; + + SpriteEditor.prototype.saveSprite = function(callback) { + var data, saved, sprite; + if ((this.selected_sprite == null) || !this.spriteview.sprite) { + return; + } + data = this.spriteview.sprite.saveData().split(",")[1]; + sprite = this.spriteview.sprite; + saved = false; + this.app.client.sendRequest({ + name: "write_project_file", + project: this.app.project.id, + file: "sprites/" + this.selected_sprite + ".png", + properties: { + frames: this.spriteview.sprite.frames.length, + fps: this.spriteview.sprite.fps + }, + content: data + }, (function(_this) { + return function(msg) { + saved = true; + if (_this.save_time === 0) { + _this.app.project.removePendingChange(_this); + } + sprite.size = msg.size; + if (callback != null) { + return callback(); + } + }; + })(this)); + return setTimeout(((function(_this) { + return function() { + if (!saved) { + _this.save_time = Date.now(); + return console.info("retrying sprite save..."); + } + }; + })(this)), 10000); + }; + + SpriteEditor.prototype.createSprite = function(name, img, callback) { + return this.checkSave(true, (function(_this) { + return function() { + var height, sprite, width; + if (img != null) { + width = img.width; + height = img.height; + } else if (_this.spriteview.selection != null) { + width = Math.max(8, _this.spriteview.selection.w); + height = Math.max(8, _this.spriteview.selection.h); + } else { + width = Math.max(8, _this.spriteview.sprite.width); + height = Math.max(8, _this.spriteview.sprite.height); + } + sprite = _this.app.project.createSprite(width, height, name); + _this.spriteview.setSprite(sprite); + _this.animation_panel.spriteChanged(); + if (img != null) { + _this.spriteview.getFrame().getContext().drawImage(img, 0, 0); + } + _this.spriteview.update(); + _this.setSelectedSprite(sprite.name); + _this.spriteview.editable = true; + return _this.saveSprite(function() { + _this.rebuildSpriteList(); + if (callback != null) { + return callback(); + } + }); + }; + })(this)); + }; + + SpriteEditor.prototype.setSelectedSprite = function(sprite) { + var e, l, len, len1, list, o; + this.selected_sprite = sprite; + this.animation_panel.spriteChanged(); + list = document.getElementById("sprite-list").childNodes; + if (this.selected_sprite != null) { + for (l = 0, len = list.length; l < len; l++) { + e = list[l]; + if (e.getAttribute("id") === ("project-sprite-" + sprite)) { + e.classList.add("selected"); + } else { + e.classList.remove("selected"); + } + } + document.getElementById("sprite-name").value = sprite; + this.sprite_name_validator.update(); + document.getElementById("sprite-name").disabled = this.selected_sprite === "icon"; + if (this.spriteview.sprite != null) { + document.getElementById("sprite-width").value = this.spriteview.sprite.width; + document.getElementById("sprite-height").value = this.spriteview.sprite.height; + this.sprite_size_validator.update(); + } + document.getElementById("sprite-width").disabled = false; + document.getElementById("sprite-height").disabled = false; + e = document.getElementById("spriteeditor"); + if (e.firstChild != null) { + e.firstChild.style.display = "inline-block"; + } + this.spriteview.windowResized(); + } else { + for (o = 0, len1 = list.length; o < len1; o++) { + e = list[o]; + e.classList.remove("selected"); + } + document.getElementById("sprite-name").value = ""; + document.getElementById("sprite-name").disabled = true; + document.getElementById("sprite-width").disabled = true; + document.getElementById("sprite-height").disabled = true; + e = document.getElementById("spriteeditor"); + if (e.firstChild != null) { + e.firstChild.style.display = "none"; + } + } + this.updateCurrentFileLock(); + this.updateSelectionHints(); + return this.auto_palette.update(); + }; + + SpriteEditor.prototype.setSprite = function(data) { + var img; + data = "data:image/png;base64," + data; + this.ignore_changes = true; + img = new Image; + img.src = data; + img.crossOrigin = "Anonymous"; + return img.onload = (function(_this) { + return function() { + _this.spriteview.sprite.load(img); + _this.spriteview.windowResized(); + _this.spriteview.update(); + _this.spriteview.editable = true; + _this.ignore_changes = false; + _this.spriteview.windowResized(); + document.getElementById("sprite-width").value = _this.spriteview.sprite.width; + document.getElementById("sprite-height").value = _this.spriteview.sprite.height; + return _this.sprite_size_validator.update(); + }; + })(this); + }; + + SpriteEditor.prototype.setColor = function(color) { + this.color = color; + this.spriteview.setColor(this.color); + return this.auto_palette.colorPicked(this.color); + }; + + SpriteEditor.prototype.rebuildSpriteList = function() { + var element, l, len, list, ref, s; + list = document.getElementById("sprite-list"); + list.innerHTML = ""; + ref = this.app.project.sprite_list; + for (l = 0, len = ref.length; l < len; l++) { + s = ref[l]; + element = this.createSpriteBox(s); + list.appendChild(element); + } + this.updateActiveUsers(); + if ((this.selected_sprite != null) && (this.app.project.getSprite(this.selected_sprite) == null)) { + this.setSelectedSprite(null); + } + }; + + SpriteEditor.prototype.openSprite = function(s) { + this.checkSave(true); + this.spriteview.setSprite(this.app.project.getSprite(s)); + this.spriteview.windowResized(); + this.spriteview.update(); + this.spriteview.editable = true; + return this.setSelectedSprite(s); + }; + + SpriteEditor.prototype.updateActiveUsers = function() { + var e, file, l, len, list, lock; + list = document.getElementById("sprite-list").childNodes; + for (l = 0, len = list.length; l < len; l++) { + e = list[l]; + file = e.id.split("-")[2]; + lock = this.app.project.isLocked("sprites/" + file + ".png"); + if ((lock != null) && Date.now() < lock.time) { + e.querySelector(".active-user").style = "display: block; background: " + (this.app.appui.createFriendColor(lock.user)) + ";"; + } else { + e.querySelector(".active-user").style = "display: none;"; + } + } + }; + + SpriteEditor.prototype.createSpriteBox = function(sprite) { + var activeuser, element, icon, iconbox, text; + element = document.createElement("div"); + element.classList.add("sprite-box"); + element.setAttribute("id", "project-sprite-" + sprite.name); + element.setAttribute("title", sprite.name); + if (sprite.name === this.selected_sprite) { + element.classList.add("selected"); + } + iconbox = document.createElement("div"); + iconbox.classList.add("icon-box"); + icon = this.createSpriteThumb(sprite); + icon.setAttribute("id", "sprite-image-" + sprite.name); + iconbox.appendChild(icon); + element.appendChild(iconbox); + sprite.addImage(icon, 64); + element.appendChild(document.createElement("br")); + text = document.createElement("span"); + text.innerHTML = sprite.name; + element.appendChild(text); + element.addEventListener("click", (function(_this) { + return function() { + return _this.openSprite(sprite.name); + }; + })(this)); + activeuser = document.createElement("i"); + activeuser.classList.add("active-user"); + activeuser.classList.add("fa"); + activeuser.classList.add("fa-user"); + element.appendChild(activeuser); + return element; + }; + + SpriteEditor.prototype.createSpriteThumb = function(sprite) { + var canvas, mouseover, update; + canvas = document.createElement("canvas"); + canvas.width = 64; + canvas.height = 64; + sprite.addLoadListener((function(_this) { + return function() { + var context, frame, h, r, w; + context = canvas.getContext("2d"); + frame = sprite.frames[0].getCanvas(); + r = Math.min(64 / frame.width, 64 / frame.height); + context.imageSmoothingEnabled = false; + w = r * frame.width; + h = r * frame.height; + return context.drawImage(frame, 32 - w / 2, 32 - h / 2, w, h); + }; + })(this)); + mouseover = false; + update = (function(_this) { + return function() { + var context, dt, frame, h, r, t, w; + if (mouseover && sprite.frames.length > 1) { + requestAnimationFrame(function() { + return update(); + }); + } + dt = 1000 / sprite.fps; + t = Date.now(); + frame = mouseover ? Math.floor(t / dt) % sprite.frames.length : 0; + context = canvas.getContext("2d"); + context.imageSmoothingEnabled = false; + context.clearRect(0, 0, 64, 64); + frame = sprite.frames[frame].getCanvas(); + r = Math.min(64 / frame.width, 64 / frame.height); + w = r * frame.width; + h = r * frame.height; + return context.drawImage(frame, 32 - w / 2, 32 - h / 2, w, h); + }; + })(this); + canvas.addEventListener("mouseenter", (function(_this) { + return function() { + mouseover = true; + return update(); + }; + })(this)); + canvas.addEventListener("mouseout", (function(_this) { + return function() { + return mouseover = false; + }; + })(this)); + canvas.updateSprite = update; + return canvas; + }; + + SpriteEditor.prototype.spriteDimensionChanged = function(dim) { + if (this.selected_sprite === "icon") { + if (dim === "width") { + return document.getElementById("sprite-height").value = document.getElementById("sprite-width").value; + } else { + return document.getElementById("sprite-width").value = document.getElementById("sprite-height").value; + } + } + }; + + SpriteEditor.prototype.saveDimensionChange = function(value) { + var err, h, w; + if (this.app.project.isLocked("sprites/" + this.selected_sprite + ".png")) { + return; + } + this.app.project.lockFile("sprites/" + this.selected_sprite + ".png"); + w = value[0]; + h = value[1]; + try { + w = Number.parseFloat(w); + h = Number.parseFloat(h); + } catch (error) { + err = error; + } + if ((this.selected_sprite !== "icon" || w === h) && Number.isInteger(w) && Number.isInteger(h) && w > 0 && h > 0 && w < 257 && h < 257 && (this.selected_sprite != null) && (w !== this.spriteview.sprite.width || h !== this.spriteview.sprite.height)) { + if (this.spriteview.sprite.undo == null) { + this.spriteview.sprite.undo = new Undo(); + } + if (this.spriteview.sprite.undo.empty()) { + this.spriteview.sprite.undo.pushState(this.spriteview.sprite.clone()); + } + this.spriteview.sprite.resize(w, h); + this.spriteview.sprite.undo.pushState(this.spriteview.sprite.clone()); + this.spriteview.windowResized(); + this.spriteview.update(); + this.spriteChanged(); + this.checkSave(true); + document.getElementById("sprite-width").value = this.spriteview.sprite.width; + document.getElementById("sprite-height").value = this.spriteview.sprite.height; + return this.sprite_size_validator.update(); + } else { + document.getElementById("sprite-width").value = this.spriteview.sprite.width; + document.getElementById("sprite-height").value = this.spriteview.sprite.height; + return this.sprite_size_validator.update(); + } + }; + + SpriteEditor.prototype.deleteSprite = function() { + if (this.app.project.isLocked("sprites/" + this.selected_sprite + ".png")) { + return; + } + this.app.project.lockFile("sprites/" + this.selected_sprite + ".png"); + if ((this.selected_sprite != null) && this.selected_sprite !== "icon") { + if (confirm("Really delete " + this.selected_sprite + "?")) { + return this.app.client.sendRequest({ + name: "delete_project_file", + project: this.app.project.id, + file: "sprites/" + this.selected_sprite + ".png" + }, (function(_this) { + return function(msg) { + _this.app.project.updateSpriteList(); + _this.spriteview.sprite = new Sprite(16, 16); + _this.spriteview.update(); + _this.spriteview.editable = false; + return _this.setSelectedSprite(null); + }; + })(this)); + } + } + }; + + SpriteEditor.prototype.undo = function() { + var s; + if (this.app.project.isLocked("sprites/" + this.selected_sprite + ".png")) { + return; + } + this.app.project.lockFile("sprites/" + this.selected_sprite + ".png"); + if (this.spriteview.sprite && (this.spriteview.sprite.undo != null)) { + s = this.spriteview.sprite.undo.undo((function(_this) { + return function() { + return _this.spriteview.sprite.clone(); + }; + })(this)); + this.spriteview.selection = null; + if (s != null) { + this.spriteview.sprite.copyFrom(s); + this.spriteview.update(); + document.getElementById("sprite-width").value = this.spriteview.sprite.width; + document.getElementById("sprite-height").value = this.spriteview.sprite.height; + this.sprite_size_validator.update(); + this.spriteview.windowResized(); + this.spriteChanged(); + return this.animation_panel.updateFrames(); + } + } + }; + + SpriteEditor.prototype.redo = function() { + var s; + if (this.app.project.isLocked("sprites/" + this.selected_sprite + ".png")) { + return; + } + this.app.project.lockFile("sprites/" + this.selected_sprite + ".png"); + if (this.spriteview.sprite && (this.spriteview.sprite.undo != null)) { + s = this.spriteview.sprite.undo.redo(); + this.spriteview.selection = null; + if (s != null) { + this.spriteview.sprite.copyFrom(s); + this.spriteview.update(); + document.getElementById("sprite-width").value = this.spriteview.sprite.width; + document.getElementById("sprite-height").value = this.spriteview.sprite.height; + this.sprite_size_validator.update(); + this.spriteview.windowResized(); + this.spriteChanged(); + return this.animation_panel.updateFrames(); + } + } + }; + + SpriteEditor.prototype.copy = function() { + if (this.tool.selectiontool && (this.spriteview.selection != null)) { + this.clipboard = new Sprite(this.spriteview.selection.w, this.spriteview.selection.h); + this.clipboard.frames[0].getContext().drawImage(this.spriteview.getFrame().canvas, -this.spriteview.selection.x, -this.spriteview.selection.y); + return this.clipboard.partial = true; + } else { + return this.clipboard = this.spriteview.sprite.clone(); + } + }; + + SpriteEditor.prototype.cut = function() { + var sel; + if (this.app.project.isLocked("sprites/" + this.selected_sprite + ".png")) { + return; + } + this.app.project.lockFile("sprites/" + this.selected_sprite + ".png"); + if (this.spriteview.sprite.undo == null) { + this.spriteview.sprite.undo = new Undo(); + } + if (this.spriteview.sprite.undo.empty()) { + this.spriteview.sprite.undo.pushState(this.spriteview.sprite.clone()); + } + if (this.tool.selectiontool && (this.spriteview.selection != null)) { + this.clipboard = new Sprite(this.spriteview.selection.w, this.spriteview.selection.h); + this.clipboard.frames[0].getContext().drawImage(this.spriteview.getFrame().canvas, -this.spriteview.selection.x, -this.spriteview.selection.y); + this.clipboard.partial = true; + sel = this.spriteview.selection; + this.spriteview.getFrame().getContext().clearRect(sel.x, sel.y, sel.w, sel.h); + } else { + this.clipboard = this.spriteview.sprite.clone(); + this.spriteview.sprite.clear(); + } + this.spriteview.sprite.undo.pushState(this.spriteview.sprite.clone()); + this.currentSpriteUpdated(); + return this.spriteChanged(); + }; + + SpriteEditor.prototype.paste = function() { + var x, y; + if (this.app.project.isLocked("sprites/" + this.selected_sprite + ".png")) { + return; + } + this.app.project.lockFile("sprites/" + this.selected_sprite + ".png"); + if (this.clipboard != null) { + if (this.spriteview.sprite.undo == null) { + this.spriteview.sprite.undo = new Undo(); + } + if (this.spriteview.sprite.undo.empty()) { + this.spriteview.sprite.undo.pushState(this.spriteview.sprite.clone()); + } + if (this.clipboard.partial) { + x = 0; + y = 0; + x = Math.max(0, Math.min(this.spriteview.sprite.width - this.clipboard.width, this.spriteview.mouse_x)); + y = Math.max(0, Math.min(this.spriteview.sprite.height - this.clipboard.height, this.spriteview.mouse_y)); + this.spriteview.floating_selection = { + bg: this.spriteview.getFrame().clone().getCanvas(), + fg: this.clipboard.frames[0].getCanvas() + }; + this.spriteview.selection = { + x: x, + y: y, + w: this.clipboard.frames[0].canvas.width, + h: this.clipboard.frames[0].canvas.height + }; + this.spriteview.getFrame().getContext().drawImage(this.clipboard.frames[0].getCanvas(), x, y); + this.setSelectedTool("fa-vector-square"); + } else { + this.spriteview.sprite.copyFrom(this.clipboard); + } + this.spriteview.sprite.undo.pushState(this.spriteview.sprite.clone()); + this.currentSpriteUpdated(); + return this.spriteChanged(); + } + }; + + SpriteEditor.prototype.currentSpriteUpdated = function() { + this.spriteview.update(); + document.getElementById("sprite-width").value = this.spriteview.sprite.width; + document.getElementById("sprite-height").value = this.spriteview.sprite.height; + this.sprite_size_validator.update(); + return this.spriteview.windowResized(); + }; + + SpriteEditor.prototype.setColorPicker = function(picker) { + this.spriteview.colorpicker = picker; + if (picker) { + this.spriteview.canvas.style.cursor = "url( '/img/eyedropper.svg' ) 0 24, pointer"; + return document.getElementById("eyedropper").classList.add("selected"); + } else { + this.spriteview.canvas.style.cursor = "crosshair"; + return document.getElementById("eyedropper").classList.remove("selected"); + } + }; + + SpriteEditor.prototype.updateSelectionHints = function() { + var h, w; + if ((this.spriteview.selection != null) && this.tool.selectiontool) { + document.getElementById("selection-group").style.display = "block"; + w = this.spriteview.selection.w; + h = this.spriteview.selection.h; + if (this.spriteview.sprite.frames.length === 1 && (this.spriteview.sprite.width / w) % 1 === 0 && (this.spriteview.sprite.height / h) % 1 === 0 && (this.spriteview.sprite.width / w >= 2 || this.spriteview.sprite.height / h >= 2)) { + return document.getElementById("selection-operation-film").style.display = "block"; + } else { + return document.getElementById("selection-operation-film").style.display = "none"; + } + } else { + return document.getElementById("selection-group").style.display = "none"; + } + }; + + SpriteEditor.prototype.stripToAnimation = function() { + var h, i, index, j, l, m, n, o, ref, ref1, sprite, w; + w = this.spriteview.selection.w; + h = this.spriteview.selection.h; + if (this.spriteview.sprite.frames.length === 1 && (this.spriteview.sprite.width / w) % 1 === 0 && (this.spriteview.sprite.height / h) % 1 === 0 && (this.spriteview.sprite.width / w >= 2 || this.spriteview.sprite.height / h >= 2)) { + if (this.spriteview.sprite.undo == null) { + this.spriteview.sprite.undo = new Undo(); + } + if (this.spriteview.sprite.undo.empty()) { + this.spriteview.sprite.undo.pushState(this.spriteview.sprite.clone()); + } + n = this.spriteview.sprite.width / w; + m = this.spriteview.sprite.height / h; + sprite = new Sprite(w, h); + index = 0; + for (j = l = 0, ref = m - 1; l <= ref; j = l += 1) { + for (i = o = 0, ref1 = n - 1; o <= ref1; i = o += 1) { + sprite.frames[index] = new SpriteFrame(sprite, w, h); + sprite.frames[index].getContext().drawImage(this.spriteview.sprite.frames[0].getCanvas(), -i * w, -j * h); + index++; + } + } + this.spriteview.sprite.copyFrom(sprite); + this.spriteview.sprite.undo.pushState(this.spriteview.sprite.clone()); + this.currentSpriteUpdated(); + this.spriteChanged(); + return this.animation_panel.spriteChanged(); + } + }; + + return SpriteEditor; + +})(); diff --git a/static/js/spriteeditor/spritelist.coffee b/static/js/spriteeditor/spritelist.coffee new file mode 100644 index 00000000..1543a822 --- /dev/null +++ b/static/js/spriteeditor/spritelist.coffee @@ -0,0 +1,69 @@ +class @SpriteList + constructor:(@app)-> + @list = [] + @table = {} + @listeners = [] + + add:(sprite)-> + if @list.indexOf(sprite)<0 + @list.push sprite + @table[sprite.name] = sprite + for lis in @listeners + lis.spriteListChanged() + return + + + rename:(old_name,new_name)-> + if @table[old_name]? + @table[new_name] = @table[old_name] + delete @table[old_name] + + for lis in @listeners + lis.spriteListChanged() + + return + + get:(name)-> + @table[name] + + delete:(sprite)-> + index = @list.indexOf sprite + if index>=0 + @list.splice index,1 + delete @table[sprite.name] + for lis in @listeners + lis.spriteListChanged() + return + + addListener:(listener)-> + @listeners.push listener + + clear:()-> + @list.length = 0 + for key of @table + delete @table[key] + + for lis in @listeners + lis.spriteListChanged() + return + + listFiles:()-> + @list[i].name+".png" for i in [0..@list.length-1] by 1 + + update:(list)-> + for i in [@list.length-1..0] by -1 + s = @list[i] + if list.indexOf(s.name+".png")<0 + @list.splice i,1 + + url = location.origin+"/#{@app.nick}/#{app.project.slug}/" + for s in list + + u = url+s + name = s.substring(0,s.length-4) + s = new Sprite u + s.name = name + @add s + for lis in @listeners + lis.spriteListChanged() + return diff --git a/static/js/spriteeditor/spritelist.js b/static/js/spriteeditor/spritelist.js new file mode 100644 index 00000000..ee274913 --- /dev/null +++ b/static/js/spriteeditor/spritelist.js @@ -0,0 +1,105 @@ +this.SpriteList = (function() { + function SpriteList(app1) { + this.app = app1; + this.list = []; + this.table = {}; + this.listeners = []; + } + + SpriteList.prototype.add = function(sprite) { + var j, len, lis, ref; + if (this.list.indexOf(sprite) < 0) { + this.list.push(sprite); + } + this.table[sprite.name] = sprite; + ref = this.listeners; + for (j = 0, len = ref.length; j < len; j++) { + lis = ref[j]; + lis.spriteListChanged(); + } + }; + + SpriteList.prototype.rename = function(old_name, new_name) { + var j, len, lis, ref; + if (this.table[old_name] != null) { + this.table[new_name] = this.table[old_name]; + delete this.table[old_name]; + } + ref = this.listeners; + for (j = 0, len = ref.length; j < len; j++) { + lis = ref[j]; + lis.spriteListChanged(); + } + }; + + SpriteList.prototype.get = function(name) { + return this.table[name]; + }; + + SpriteList.prototype["delete"] = function(sprite) { + var index, j, len, lis, ref; + index = this.list.indexOf(sprite); + if (index >= 0) { + this.list.splice(index, 1); + } + delete this.table[sprite.name]; + ref = this.listeners; + for (j = 0, len = ref.length; j < len; j++) { + lis = ref[j]; + lis.spriteListChanged(); + } + }; + + SpriteList.prototype.addListener = function(listener) { + return this.listeners.push(listener); + }; + + SpriteList.prototype.clear = function() { + var j, key, len, lis, ref; + this.list.length = 0; + for (key in this.table) { + delete this.table[key]; + } + ref = this.listeners; + for (j = 0, len = ref.length; j < len; j++) { + lis = ref[j]; + lis.spriteListChanged(); + } + }; + + SpriteList.prototype.listFiles = function() { + var i, j, ref, results; + results = []; + for (i = j = 0, ref = this.list.length - 1; j <= ref; i = j += 1) { + results.push(this.list[i].name + ".png"); + } + return results; + }; + + SpriteList.prototype.update = function(list) { + var i, j, k, l, len, len1, lis, name, ref, ref1, s, u, url; + for (i = j = ref = this.list.length - 1; j >= 0; i = j += -1) { + s = this.list[i]; + if (list.indexOf(s.name + ".png") < 0) { + this.list.splice(i, 1); + } + } + url = location.origin + ("/" + this.app.nick + "/" + app.project.slug + "/"); + for (k = 0, len = list.length; k < len; k++) { + s = list[k]; + u = url + s; + name = s.substring(0, s.length - 4); + s = new Sprite(u); + s.name = name; + this.add(s); + } + ref1 = this.listeners; + for (l = 0, len1 = ref1.length; l < len1; l++) { + lis = ref1[l]; + lis.spriteListChanged(); + } + }; + + return SpriteList; + +})(); diff --git a/static/js/spriteeditor/spriteview.coffee b/static/js/spriteeditor/spriteview.coffee new file mode 100644 index 00000000..ac820d5f --- /dev/null +++ b/static/js/spriteeditor/spriteview.coffee @@ -0,0 +1,584 @@ +class @SpriteView + constructor:(@editor)-> + @canvas = document.querySelector "#spriteeditor canvas" + @canvas.width = 400 + @canvas.height = 400 + @sprite = new Sprite(32,32) + + @canvas.addEventListener "mousedown", (event) => @mouseDown(event) + document.addEventListener "mousemove", (event) => @mouseMove(event) + @canvas.addEventListener "mouseout", (event) => @mouseOut(event) + document.addEventListener "mouseup", (event) => @mouseUp(event) + @canvas.addEventListener "contextmenu",(event)=>event.preventDefault() + + @canvas.addEventListener "mouseenter", (event) => @mouseEnter(event) + + @brush_opacity = 1 + @brush_type = "paint" + @brush_size = 1 + + @mouse_over = false + @mouse_x = 0 + @mouse_y = 0 + + window.addEventListener "resize",()=> + @windowResized() + + @editable = false + @tile = false + @vsymmetry = false + @hsymmetry = false + @zoom = 1 + document.getElementById("spriteeditor").addEventListener("mousewheel", ((e)=>@mouseWheel(e)), false) + document.getElementById("spriteeditor").addEventListener("DOMMouseScroll", ((e)=>@mouseWheel(e)), false) + + document.getElementById("spriteeditor").addEventListener "mousedown",()=> + if @selection? + @selection = null + @update() + + document.getElementById("spriteeditor").addEventListener "keydown",(e)=> + if e.keyCode == 32 + @space_pressed = true + @canvas.style.cursor = "grab" + e.preventDefault() + document.getElementById("sprite-grab-info").classList.add "active" + else if e.keyCode == 18 # Alt + document.getElementById("selection-hint-clone").classList.add "active" + else if e.keyCode == 16 # Shift + document.getElementById("selection-hint-move").classList.add "active" + + document.addEventListener "keyup",(e)=> + if e.keyCode == 32 + @space_pressed = false + @canvas.style.cursor = "crosshair" + document.getElementById("sprite-grab-info").classList.remove "active" + else if e.keyCode == 18 # Alt + document.getElementById("selection-hint-clone").classList.remove "active" + else if e.keyCode == 16 # Shift + document.getElementById("selection-hint-move").classList.remove "active" + + document.getElementById("sprite-zoom-plus").addEventListener "click",()=> + @scaleZoom(1.1) + + document.getElementById("sprite-zoom-minus").addEventListener "click",()=> + @scaleZoom(1/1.100001) + + setSprite:(sprite)-> + if sprite != @sprite + if @sprite? + @saveZoom() + @sprite.selection = @selection + + @sprite = sprite + if @sprite.zoom? + @restoreZoom() + else + @scaleZoom(.5/@zoom) + + @selection = @sprite.selection or null + @floating_selection = null + + setCurrentFrame:(index)-> + if index != @sprite.current_frame + @sprite.setCurrentFrame(index) + @selection = null + + getFrame:()-> + return @sprite.frames[@sprite.current_frame] + + mouseWheel:(e)-> + e.preventDefault() + @next_wheel_action = Date.now() if not @next_wheel_action? + + return if Date.now()<@next_wheel_action + @next_wheel_action = Date.now()+50 + + if e.wheelDelta < 0 or e.detail > 0 + @scaleZoom(1/1.100001,e) + else + @scaleZoom(1.1,e) + + scaleZoom:(scale,e)-> + view = document.getElementById("spriteeditor").getBoundingClientRect() + max_zoom = 4096/Math.max(view.width,view.height) + @zoom = Math.max(1,Math.min(max_zoom,@zoom*scale)) + + if e? + b = @canvas.getBoundingClientRect() + x = (e.clientX-b.left)/@canvas.width + y = (e.clientY-b.top)/@canvas.height + + @windowResized() + + if e? + view = document.getElementById("spriteeditor").getBoundingClientRect() + scroll_x = view.x+@canvas.width*x-e.clientX+40 + scroll_y = view.y+@canvas.height*y-e.clientY+40 + document.getElementById("spriteeditor").scrollTo(scroll_x,scroll_y) + @mouseMove(e) if e? + + if @zoom>1 + document.getElementById("sprite-grab-info").style.display = "inline-block" + else + document.getElementById("sprite-grab-info").style.display = "none" + + saveZoom:()-> + if @sprite? + view = document.getElementById("spriteeditor") + @sprite.zoom = + zoom: @zoom + left:view.scrollLeft + top: view.scrollTop + + restoreZoom:()-> + if @sprite.zoom? + view = document.getElementById("spriteeditor") + @scaleZoom(@sprite.zoom.zoom/@zoom) + view.scrollTo(@sprite.zoom.left,@sprite.zoom.top) + + getPattern:()-> + return @pattern if @pattern? + c = document.createElement "canvas" + c.width = 64 + c.height = 64 + context = c.getContext "2d" + data = context.getImageData 0,0,c.width,1 + for line in [0..c.height-1] by 1 + for i in [0..c.width-1] by 1 + value = 32+Math.random()*32 + data.data[i*4] = value + data.data[i*4+1] = value + data.data[i*4+2] = value + data.data[i*4+3] = 255 + + context.putImageData data,0,line + + @pattern = c + document.querySelector(".spriteeditor canvas").style["background-image"] = "url(#{c.toDataURL()})" + document.querySelector(".spriteeditor canvas").style["background-repeat"] = "repeat" + + setColor:(@color)-> + + windowResized: ()-> + c = @canvas.parentElement + return if not c? + + return if c.clientWidth<= 0 + + w = c.clientWidth-80 + h = c.clientHeight-80 + ratio = Math.min(w/@sprite.width,h/@sprite.height) + w = Math.floor(ratio*@sprite.width*@zoom) + h = Math.floor(ratio*@sprite.height*@zoom) + if w != @canvas.width or h != @canvas.height + @canvas.width = w + @canvas.height = h + @update() + + h = Math.max(40,(c.clientHeight-h)/2) + @canvas.style["margin-top"] = h+"px" + + showBrushSize:()-> + @show_brush_size = Date.now()+2000 + @update() + + drawGrid:(ctx)-> + if not @grid_buffer? or @grid_buffer.width != @canvas.width or @grid_buffer.height != @canvas.height or @tile != @grid_tile or @grid_sw != @sprite.width or @grid_sh != @sprite.height + if not @grid_buffer? + @grid_buffer = document.createElement "canvas" + @grid_buffer.width = @canvas.width + @grid_buffer.height = @canvas.height + @grid_tile = @tile + @grid_sw = @sprite.width + @grid_sh = @sprite.height + console.info "updating grid" + + wblock = @canvas.width/@sprite.width + hblock = @canvas.height/@sprite.height + if @tile + wblock /= 2 + hblock /= 2 + context = @grid_buffer.getContext "2d" + + context.lineWidth = 1 + context.strokeStyle = "rgba(0,0,0,.1)" + woffset = if @tile and @sprite.width%2>0 then wblock*.5 else 0 + hoffset = if @tile and @sprite.height%2>0 then hblock*.5 else 0 + + modulo = if @sprite.width%8 == 0 then 8 else 10 + return if wblock<3 + + for i in [0..@canvas.width] by wblock + lw = if Math.round(i/hblock)%modulo ==0 then 2 else 1 + context.lineWidth = lw + context.beginPath() + context.moveTo i+.25*lw+woffset,0 + context.lineTo i+.25*lw+woffset,@canvas.height + context.stroke() + + for i in [0..@canvas.height] by hblock + lw = if Math.round(i/hblock)%modulo ==0 then 2 else 1 + context.lineWidth = lw + context.beginPath() + context.moveTo 0,i+.25*lw+hoffset + context.lineTo @canvas.width,i+.25*lw+hoffset + context.stroke() + + context.strokeStyle = "rgba(255,255,255,.1)" + for i in [0..@canvas.width] by wblock + lw = if Math.round(i/hblock)%modulo ==0 then 2 else 1 + context.lineWidth = lw + context.beginPath() + context.moveTo i-.25*lw+woffset,0 + context.lineTo i-.25*lw+woffset,@canvas.height + context.stroke() + + for i in [0..@canvas.height] by hblock + lw = if Math.round(i/hblock)%modulo ==0 then 2 else 1 + context.lineWidth = lw + context.beginPath() + context.moveTo 0,i-.25*lw+hoffset + context.lineTo @canvas.width,i-.25*lw+hoffset + context.stroke() + + ctx.drawImage @grid_buffer,0,0 + + update:()-> + @brush_size = @editor.tool.getSize(@sprite) + context = @canvas.getContext "2d" + context.clearRect 0,0,@canvas.width,@canvas.height + context.imageSmoothingEnabled = false + p = @getPattern() + #size = @canvas.width/@sprite.width*p.width/4 + #w = Math.floor(@canvas.width/size) + #h = Math.floor(@canvas.height/size) + #for i in [0..w] by 1 + # for j in [0..h] by 1 + # context.drawImage p,i*size,j*size,size,size + + if @sprite.frames.length>1 + f = @sprite.frames[(@sprite.current_frame+@sprite.frames.length-1)%@sprite.frames.length] + context.globalAlpha = .2 + if @tile + w = @canvas.width + h = @canvas.height + for i in [0..2] by 1 + for j in [0..2] by 1 + if f.canvas? + context.drawImage f.canvas,w*(-.25+i*.5),h*(-.25+j*.5),w*.5,h*.5 + else + if f.canvas? + context.drawImage f.canvas,0,0,@canvas.width,@canvas.height + context.globalAlpha = 1 + + if @tile + w = @canvas.width + h = @canvas.height + for i in [0..2] by 1 + for j in [0..2] by 1 + if @getFrame().canvas? + context.drawImage @getFrame().canvas,w*(-.25+i*.5),h*(-.25+j*.5),w*.5,h*.5 + else + if @getFrame().canvas? + context.drawImage @getFrame().canvas,0,0,@canvas.width,@canvas.height + + wblock = @canvas.width/@sprite.width + hblock = @canvas.height/@sprite.height + if @tile + wblock /= 2 + hblock /= 2 + context.lineWidth = 1 + context.strokeStyle = "rgba(0,0,0,.1)" + woffset = if @tile and @sprite.width%2>0 then wblock*.5 else 0 + hoffset = if @tile and @sprite.height%2>0 then hblock*.5 else 0 + + @drawGrid(context) + + if (@mouse_over or Date.now()<@show_brush_size) and @canvas.style.cursor != "move" + if Date.now()<@show_brush_size + mx = Math.floor(@sprite.width/2)*if @tile then 2 else 1 + my = Math.floor(@sprite.height/2)*if @tile then 2 else 1 + else + mx = @mouse_x + my = @mouse_y + bs = (@brush_size-1)/2 + context.strokeStyle = "#000" + context.lineWidth = 4 + context.beginPath() + context.rect (mx-bs)*wblock-woffset,(my-bs)*hblock-hoffset,wblock*@brush_size,hblock*@brush_size + context.stroke() + context.strokeStyle = "#FFF" + context.lineWidth = 3 + context.stroke() + + if @tile + grd = context.createLinearGradient(0,0,@canvas.width,0) + grd.addColorStop(0,"rgba(0,0,0,1)") + grd.addColorStop(.25,"rgba(0,0,0,0)") + grd.addColorStop(.75,"rgba(0,0,0,0)") + grd.addColorStop(1,"rgba(0,0,0,1)") + context.fillStyle = grd + context.fillRect(0,0,@canvas.width,@canvas.height) + + grd = context.createLinearGradient(0,0,0,@canvas.height) + grd.addColorStop(0,"rgba(0,0,0,1)") + grd.addColorStop(.25,"rgba(0,0,0,0)") + grd.addColorStop(.75,"rgba(0,0,0,0)") + grd.addColorStop(1,"rgba(0,0,0,1)") + context.fillStyle = grd + context.fillRect(0,0,@canvas.width,@canvas.height) + + w = @canvas.width + h = @canvas.height + context.strokeStyle = "rgba(0,0,0,.5)" + context.strokeRect w*.25+.5,h*.25+.5,w*.5,h*.5 + context.strokeStyle = "rgba(255,255,255,.5)" + context.strokeRect w*.25-.5,h*.25-.5,w*.5,h*.5 + + if @hsymmetry + w = @canvas.width + h = @canvas.height + context.fillStyle = "rgba(0,0,0,.5)" + context.fillRect 0,h/2-2,w,4 + context.fillStyle = "rgba(255,255,255,.5)" + context.fillRect 0,h/2-1.5,w,3 + + if @vsymmetry + w = @canvas.width + h = @canvas.height + context.fillStyle = "rgba(0,0,0,.5)" + context.fillRect w/2-2,0,4,h + context.fillStyle = "rgba(255,255,255,.5)" + context.fillRect w/2-1.5,0,3,h + + if @selection? and @editor.tool.selectiontool + context.save() + if @tile + context.translate @canvas.width*.25,@canvas.width*.25 + context.strokeStyle = "rgba(0,0,0,.5)" + context.lineWidth = 3 + context.strokeRect(@selection.x*wblock,@selection.y*hblock,@selection.w*wblock,@selection.h*hblock) + context.setLineDash([4,4]) + context.strokeStyle = if @floating_selection then "#FA0" else "#FFF" + context.lineWidth = 2 + context.strokeRect(@selection.x*wblock,@selection.y*hblock,@selection.w*wblock,@selection.h*hblock) + context.setLineDash([]) + if @selection.w>1 or @selection.h>1 + context.font = "#{Math.max(12,wblock)}pt Ubuntu Mono" + context.fillStyle = "#FFF" + context.shadowBlur = 2 + context.shadowColor = "#000" + context.shadowOpacity = 1 + context.textAlign = "center" + context.textBaseline = "middle" + context.fillText "#{@selection.w} x #{@selection.h}",(@selection.x+@selection.w/2)*wblock,(@selection.y+@selection.h/2)*hblock + context.shadowBlur = 0 + context.restore() + + mouseDown:(event)-> + event.stopPropagation() + return if not @editable or not @sprite? + @mousepressed = true + b = @canvas.getBoundingClientRect() + min = Math.min @canvas.clientWidth,@canvas.clientHeight + x = (event.clientX-b.left) + y = (event.clientY-b.top) + + if @tile + x = Math.floor(x/@canvas.width*@sprite.width*2) + y = Math.floor(y/@canvas.height*@sprite.height*2) + x = Math.floor((x+@sprite.width/2)%@sprite.width) + y = Math.floor((y+@sprite.height/2)%@sprite.height) + else + x = Math.floor(x/@canvas.width*@sprite.width) + y = Math.floor(y/@canvas.height*@sprite.height) + + if @space_pressed + @grab_x = event.clientX + @grab_y = event.clientY + @grabbing = true + return + + if @colorpicker and not @editor.tool.selectiontool + c = @getFrame().getRGB(x,y) + @editor.colorpicker.colorPicked(c) + @mousepressed = false + if not @editor.alt_pressed + @editor.setColorPicker(false) + return + + if @editor.tool.selectiontool + if @selection? and x>=@selection.x and y>=@selection.y and x<@selection.x+@selection.w and y<@selection.y+@selection.h + if not @floating_selection? and (event.shiftKey or event.altKey) + bg = document.createElement "canvas" + bg.width = @getFrame().canvas.width + bg.height = @getFrame().canvas.height + context = bg.getContext "2d" + context.drawImage @getFrame().canvas,0,0 + if not event.altKey + context.clearRect @selection.x,@selection.y,@selection.w,@selection.h + fg = document.createElement "canvas" + fg.width = @selection.w + fg.height = @selection.h + context = fg.getContext "2d" + context.drawImage @getFrame().canvas,-@selection.x,-@selection.y + + @floating_selection = + bg: bg + fg: fg + else if event.altKey + @floating_selection.bg.getContext("2d").drawImage @floating_selection.fg,@selection.x,@selection.y + + @moving_selection = true + @moved_once = false + @moving_start_x = x + @moving_start_y = y + @update() + return + else + @floating_selection = null + @selection_start_x = x + @selection_start_y = y + @selection_moved = false + @selection = + x: x + y: y + w: 1 + h: 1 + @update() + return + + @sprite.undo = new Undo() if not @sprite.undo? + @sprite.undo.pushState @sprite.clone() if @sprite.undo.empty() + + if @editor.tool.parameters["Color"]? + @editor.tool.parameters["Color"].value = @color + @editor.tool.tile = @tile + @editor.tool.vsymmetry = @vsymmetry + @editor.tool.hsymmetry = @hsymmetry + @editor.tool.start(@getFrame(),x,y,event.button) + @mouse_x = x + @mouse_y = y + @update() + @editor.spriteChanged() + @floating_selection = null + + mouseEnter:(event)-> + document.getElementById("spriteeditor").focus() + + mouseMove:(event)-> + if @grabbing + dx = event.clientX-@grab_x + dy = event.clientY-@grab_y + @grab_x = event.clientX + @grab_y = event.clientY + view = document.getElementById("spriteeditor") + view.scrollTo(view.scrollLeft-dx,view.scrollTop-dy) + return + + return if not @editable + b = @canvas.getBoundingClientRect() + min = Math.min @canvas.clientWidth,@canvas.clientHeight + x = (event.clientX-b.left) + y = (event.clientY-b.top) + + if x>=0 and y>=0 and x=@sprite.width*2 or y>=@sprite.height*2) + else + x = Math.floor(x/@canvas.width*@sprite.width) + y = Math.floor(y/@canvas.height*@sprite.height) + return if not @mousepressed and (x<0 or y<0 or x>=@sprite.width or y>=@sprite.height) + + if @mousepressed and @moving_selection + if @floating_selection? + if x != @mouse_x or y != @mouse_y + if not @moved_once + @moved_once = true + @sprite.undo = new Undo() if not @sprite.undo? + @sprite.undo.pushState @sprite.clone() if @sprite.undo.empty() + + @selection.x += x-@mouse_x + @selection.y += y-@mouse_y + @mouse_x = x + @mouse_y = y + context = @getFrame().getContext() + context.clearRect 0,0,@getFrame().canvas.width,@getFrame().canvas.height + context.drawImage @floating_selection.bg,0,0 + context.drawImage @floating_selection.fg,@selection.x,@selection.y + @editor.spriteChanged() + @update() + else + @selection.x += x-@mouse_x + @selection.y += y-@mouse_y + @selection.x = Math.max(0,Math.min(@sprite.width-@selection.w,@selection.x)) + @selection.y = Math.max(0,Math.min(@sprite.height-@selection.h,@selection.y)) + @mouse_x = x + @mouse_y = y + @update() + + return + + @mouse_over = true + if @mousepressed and @editor.tool.selectiontool + @selection_moved = true + + if x != @mouse_x or y != @mouse_y + @mouse_x = x + @mouse_y = y + + if @mousepressed + if @tile + x = Math.floor((x+@sprite.width/2)%@sprite.width) + y = Math.floor((y+@sprite.height/2)%@sprite.height) + + if @editor.tool.selectiontool and @selection + @selection.x = Math.max(0,Math.min(@selection_start_x,x)) + @selection.y = Math.max(0,Math.min(@selection_start_y,y)) + @selection.w = Math.min @sprite.width-@selection.x,Math.max(@selection_start_x,x)-Math.min(@selection_start_x,Math.max(0,x))+1 + @selection.h = Math.min @sprite.height-@selection.y,Math.max(@selection_start_y,y)-Math.min(@selection_start_y,Math.max(0,y))+1 + + @update() + return + + @editor.tool.move(@getFrame(),x,y,event.buttons) + @update() + @editor.spriteChanged() + else + if @selection? and @editor.tool.selectiontool and x>=@selection.x and y>=@selection.y and x<@selection.x+@selection.w and y<@selection.y+@selection.h + @canvas.style.cursor = "move" + else if @colorpicker and not @editor.tool.selectiontool + @canvas.style.cursor = "url( '/img/eyedropper.svg' ) 0 24, pointer" + else + @canvas.style.cursor = "crosshair" + @update() + + false + + mouseUp:(event)-> + if @grabbing + @grabbing = false + else if @mousepressed and not @editor.tool.selectiontool + @sprite.undo.pushState @sprite.clone() + + if @editor.tool.selectiontool and not @selection_moved + @selection = null + @update() + + if @moving_selection + @moving_selection = false + if @moved_once and @floating_selection? + @sprite.undo.pushState @sprite.clone() + @moved_once = false + + @mousepressed = false + @editor.updateSelectionHints() + + mouseOut:(event)-> + @mouse_over = false + @update() diff --git a/static/js/spriteeditor/spriteview.js b/static/js/spriteeditor/spriteview.js new file mode 100644 index 00000000..86d1a601 --- /dev/null +++ b/static/js/spriteeditor/spriteview.js @@ -0,0 +1,697 @@ +this.SpriteView = (function() { + function SpriteView(editor) { + this.editor = editor; + this.canvas = document.querySelector("#spriteeditor canvas"); + this.canvas.width = 400; + this.canvas.height = 400; + this.sprite = new Sprite(32, 32); + this.canvas.addEventListener("mousedown", (function(_this) { + return function(event) { + return _this.mouseDown(event); + }; + })(this)); + document.addEventListener("mousemove", (function(_this) { + return function(event) { + return _this.mouseMove(event); + }; + })(this)); + this.canvas.addEventListener("mouseout", (function(_this) { + return function(event) { + return _this.mouseOut(event); + }; + })(this)); + document.addEventListener("mouseup", (function(_this) { + return function(event) { + return _this.mouseUp(event); + }; + })(this)); + this.canvas.addEventListener("contextmenu", (function(_this) { + return function(event) { + return event.preventDefault(); + }; + })(this)); + this.canvas.addEventListener("mouseenter", (function(_this) { + return function(event) { + return _this.mouseEnter(event); + }; + })(this)); + this.brush_opacity = 1; + this.brush_type = "paint"; + this.brush_size = 1; + this.mouse_over = false; + this.mouse_x = 0; + this.mouse_y = 0; + window.addEventListener("resize", (function(_this) { + return function() { + return _this.windowResized(); + }; + })(this)); + this.editable = false; + this.tile = false; + this.vsymmetry = false; + this.hsymmetry = false; + this.zoom = 1; + document.getElementById("spriteeditor").addEventListener("mousewheel", ((function(_this) { + return function(e) { + return _this.mouseWheel(e); + }; + })(this)), false); + document.getElementById("spriteeditor").addEventListener("DOMMouseScroll", ((function(_this) { + return function(e) { + return _this.mouseWheel(e); + }; + })(this)), false); + document.getElementById("spriteeditor").addEventListener("mousedown", (function(_this) { + return function() { + if (_this.selection != null) { + _this.selection = null; + return _this.update(); + } + }; + })(this)); + document.getElementById("spriteeditor").addEventListener("keydown", (function(_this) { + return function(e) { + if (e.keyCode === 32) { + _this.space_pressed = true; + _this.canvas.style.cursor = "grab"; + e.preventDefault(); + return document.getElementById("sprite-grab-info").classList.add("active"); + } else if (e.keyCode === 18) { + return document.getElementById("selection-hint-clone").classList.add("active"); + } else if (e.keyCode === 16) { + return document.getElementById("selection-hint-move").classList.add("active"); + } + }; + })(this)); + document.addEventListener("keyup", (function(_this) { + return function(e) { + if (e.keyCode === 32) { + _this.space_pressed = false; + _this.canvas.style.cursor = "crosshair"; + return document.getElementById("sprite-grab-info").classList.remove("active"); + } else if (e.keyCode === 18) { + return document.getElementById("selection-hint-clone").classList.remove("active"); + } else if (e.keyCode === 16) { + return document.getElementById("selection-hint-move").classList.remove("active"); + } + }; + })(this)); + document.getElementById("sprite-zoom-plus").addEventListener("click", (function(_this) { + return function() { + return _this.scaleZoom(1.1); + }; + })(this)); + document.getElementById("sprite-zoom-minus").addEventListener("click", (function(_this) { + return function() { + return _this.scaleZoom(1 / 1.100001); + }; + })(this)); + } + + SpriteView.prototype.setSprite = function(sprite) { + if (sprite !== this.sprite) { + if (this.sprite != null) { + this.saveZoom(); + this.sprite.selection = this.selection; + } + this.sprite = sprite; + if (this.sprite.zoom != null) { + this.restoreZoom(); + } else { + this.scaleZoom(.5 / this.zoom); + } + this.selection = this.sprite.selection || null; + return this.floating_selection = null; + } + }; + + SpriteView.prototype.setCurrentFrame = function(index) { + if (index !== this.sprite.current_frame) { + this.sprite.setCurrentFrame(index); + return this.selection = null; + } + }; + + SpriteView.prototype.getFrame = function() { + return this.sprite.frames[this.sprite.current_frame]; + }; + + SpriteView.prototype.mouseWheel = function(e) { + e.preventDefault(); + if (this.next_wheel_action == null) { + this.next_wheel_action = Date.now(); + } + if (Date.now() < this.next_wheel_action) { + return; + } + this.next_wheel_action = Date.now() + 50; + if (e.wheelDelta < 0 || e.detail > 0) { + return this.scaleZoom(1 / 1.100001, e); + } else { + return this.scaleZoom(1.1, e); + } + }; + + SpriteView.prototype.scaleZoom = function(scale, e) { + var b, max_zoom, scroll_x, scroll_y, view, x, y; + view = document.getElementById("spriteeditor").getBoundingClientRect(); + max_zoom = 4096 / Math.max(view.width, view.height); + this.zoom = Math.max(1, Math.min(max_zoom, this.zoom * scale)); + if (e != null) { + b = this.canvas.getBoundingClientRect(); + x = (e.clientX - b.left) / this.canvas.width; + y = (e.clientY - b.top) / this.canvas.height; + } + this.windowResized(); + if (e != null) { + view = document.getElementById("spriteeditor").getBoundingClientRect(); + scroll_x = view.x + this.canvas.width * x - e.clientX + 40; + scroll_y = view.y + this.canvas.height * y - e.clientY + 40; + document.getElementById("spriteeditor").scrollTo(scroll_x, scroll_y); + } + if (e != null) { + this.mouseMove(e); + } + if (this.zoom > 1) { + return document.getElementById("sprite-grab-info").style.display = "inline-block"; + } else { + return document.getElementById("sprite-grab-info").style.display = "none"; + } + }; + + SpriteView.prototype.saveZoom = function() { + var view; + if (this.sprite != null) { + view = document.getElementById("spriteeditor"); + return this.sprite.zoom = { + zoom: this.zoom, + left: view.scrollLeft, + top: view.scrollTop + }; + } + }; + + SpriteView.prototype.restoreZoom = function() { + var view; + if (this.sprite.zoom != null) { + view = document.getElementById("spriteeditor"); + this.scaleZoom(this.sprite.zoom.zoom / this.zoom); + return view.scrollTo(this.sprite.zoom.left, this.sprite.zoom.top); + } + }; + + SpriteView.prototype.getPattern = function() { + var c, context, data, i, k, l, line, ref, ref1, value; + if (this.pattern != null) { + return this.pattern; + } + c = document.createElement("canvas"); + c.width = 64; + c.height = 64; + context = c.getContext("2d"); + data = context.getImageData(0, 0, c.width, 1); + for (line = k = 0, ref = c.height - 1; k <= ref; line = k += 1) { + for (i = l = 0, ref1 = c.width - 1; l <= ref1; i = l += 1) { + value = 32 + Math.random() * 32; + data.data[i * 4] = value; + data.data[i * 4 + 1] = value; + data.data[i * 4 + 2] = value; + data.data[i * 4 + 3] = 255; + } + context.putImageData(data, 0, line); + } + this.pattern = c; + document.querySelector(".spriteeditor canvas").style["background-image"] = "url(" + (c.toDataURL()) + ")"; + return document.querySelector(".spriteeditor canvas").style["background-repeat"] = "repeat"; + }; + + SpriteView.prototype.setColor = function(color) { + this.color = color; + }; + + SpriteView.prototype.windowResized = function() { + var c, h, ratio, w; + c = this.canvas.parentElement; + if (c == null) { + return; + } + if (c.clientWidth <= 0) { + return; + } + w = c.clientWidth - 80; + h = c.clientHeight - 80; + ratio = Math.min(w / this.sprite.width, h / this.sprite.height); + w = Math.floor(ratio * this.sprite.width * this.zoom); + h = Math.floor(ratio * this.sprite.height * this.zoom); + if (w !== this.canvas.width || h !== this.canvas.height) { + this.canvas.width = w; + this.canvas.height = h; + this.update(); + } + h = Math.max(40, (c.clientHeight - h) / 2); + return this.canvas.style["margin-top"] = h + "px"; + }; + + SpriteView.prototype.showBrushSize = function() { + this.show_brush_size = Date.now() + 2000; + return this.update(); + }; + + SpriteView.prototype.drawGrid = function(ctx) { + var context, hblock, hoffset, i, k, l, lw, m, modulo, n, ref, ref1, ref2, ref3, ref4, ref5, ref6, ref7, wblock, woffset; + if ((this.grid_buffer == null) || this.grid_buffer.width !== this.canvas.width || this.grid_buffer.height !== this.canvas.height || this.tile !== this.grid_tile || this.grid_sw !== this.sprite.width || this.grid_sh !== this.sprite.height) { + if (this.grid_buffer == null) { + this.grid_buffer = document.createElement("canvas"); + } + this.grid_buffer.width = this.canvas.width; + this.grid_buffer.height = this.canvas.height; + this.grid_tile = this.tile; + this.grid_sw = this.sprite.width; + this.grid_sh = this.sprite.height; + console.info("updating grid"); + wblock = this.canvas.width / this.sprite.width; + hblock = this.canvas.height / this.sprite.height; + if (this.tile) { + wblock /= 2; + hblock /= 2; + } + context = this.grid_buffer.getContext("2d"); + context.lineWidth = 1; + context.strokeStyle = "rgba(0,0,0,.1)"; + woffset = this.tile && this.sprite.width % 2 > 0 ? wblock * .5 : 0; + hoffset = this.tile && this.sprite.height % 2 > 0 ? hblock * .5 : 0; + modulo = this.sprite.width % 8 === 0 ? 8 : 10; + if (wblock < 3) { + return; + } + for (i = k = 0, ref = this.canvas.width, ref1 = wblock; ref1 > 0 ? k <= ref : k >= ref; i = k += ref1) { + lw = Math.round(i / hblock) % modulo === 0 ? 2 : 1; + context.lineWidth = lw; + context.beginPath(); + context.moveTo(i + .25 * lw + woffset, 0); + context.lineTo(i + .25 * lw + woffset, this.canvas.height); + context.stroke(); + } + for (i = l = 0, ref2 = this.canvas.height, ref3 = hblock; ref3 > 0 ? l <= ref2 : l >= ref2; i = l += ref3) { + lw = Math.round(i / hblock) % modulo === 0 ? 2 : 1; + context.lineWidth = lw; + context.beginPath(); + context.moveTo(0, i + .25 * lw + hoffset); + context.lineTo(this.canvas.width, i + .25 * lw + hoffset); + context.stroke(); + } + context.strokeStyle = "rgba(255,255,255,.1)"; + for (i = m = 0, ref4 = this.canvas.width, ref5 = wblock; ref5 > 0 ? m <= ref4 : m >= ref4; i = m += ref5) { + lw = Math.round(i / hblock) % modulo === 0 ? 2 : 1; + context.lineWidth = lw; + context.beginPath(); + context.moveTo(i - .25 * lw + woffset, 0); + context.lineTo(i - .25 * lw + woffset, this.canvas.height); + context.stroke(); + } + for (i = n = 0, ref6 = this.canvas.height, ref7 = hblock; ref7 > 0 ? n <= ref6 : n >= ref6; i = n += ref7) { + lw = Math.round(i / hblock) % modulo === 0 ? 2 : 1; + context.lineWidth = lw; + context.beginPath(); + context.moveTo(0, i - .25 * lw + hoffset); + context.lineTo(this.canvas.width, i - .25 * lw + hoffset); + context.stroke(); + } + } + return ctx.drawImage(this.grid_buffer, 0, 0); + }; + + SpriteView.prototype.update = function() { + var bs, context, f, grd, h, hblock, hoffset, i, j, k, l, m, mx, my, n, p, w, wblock, woffset; + this.brush_size = this.editor.tool.getSize(this.sprite); + context = this.canvas.getContext("2d"); + context.clearRect(0, 0, this.canvas.width, this.canvas.height); + context.imageSmoothingEnabled = false; + p = this.getPattern(); + if (this.sprite.frames.length > 1) { + f = this.sprite.frames[(this.sprite.current_frame + this.sprite.frames.length - 1) % this.sprite.frames.length]; + context.globalAlpha = .2; + if (this.tile) { + w = this.canvas.width; + h = this.canvas.height; + for (i = k = 0; k <= 2; i = k += 1) { + for (j = l = 0; l <= 2; j = l += 1) { + if (f.canvas != null) { + context.drawImage(f.canvas, w * (-.25 + i * .5), h * (-.25 + j * .5), w * .5, h * .5); + } + } + } + } else { + if (f.canvas != null) { + context.drawImage(f.canvas, 0, 0, this.canvas.width, this.canvas.height); + } + } + context.globalAlpha = 1; + } + if (this.tile) { + w = this.canvas.width; + h = this.canvas.height; + for (i = m = 0; m <= 2; i = m += 1) { + for (j = n = 0; n <= 2; j = n += 1) { + if (this.getFrame().canvas != null) { + context.drawImage(this.getFrame().canvas, w * (-.25 + i * .5), h * (-.25 + j * .5), w * .5, h * .5); + } + } + } + } else { + if (this.getFrame().canvas != null) { + context.drawImage(this.getFrame().canvas, 0, 0, this.canvas.width, this.canvas.height); + } + } + wblock = this.canvas.width / this.sprite.width; + hblock = this.canvas.height / this.sprite.height; + if (this.tile) { + wblock /= 2; + hblock /= 2; + } + context.lineWidth = 1; + context.strokeStyle = "rgba(0,0,0,.1)"; + woffset = this.tile && this.sprite.width % 2 > 0 ? wblock * .5 : 0; + hoffset = this.tile && this.sprite.height % 2 > 0 ? hblock * .5 : 0; + this.drawGrid(context); + if ((this.mouse_over || Date.now() < this.show_brush_size) && this.canvas.style.cursor !== "move") { + if (Date.now() < this.show_brush_size) { + mx = Math.floor(this.sprite.width / 2) * (this.tile ? 2 : 1); + my = Math.floor(this.sprite.height / 2) * (this.tile ? 2 : 1); + } else { + mx = this.mouse_x; + my = this.mouse_y; + } + bs = (this.brush_size - 1) / 2; + context.strokeStyle = "#000"; + context.lineWidth = 4; + context.beginPath(); + context.rect((mx - bs) * wblock - woffset, (my - bs) * hblock - hoffset, wblock * this.brush_size, hblock * this.brush_size); + context.stroke(); + context.strokeStyle = "#FFF"; + context.lineWidth = 3; + context.stroke(); + } + if (this.tile) { + grd = context.createLinearGradient(0, 0, this.canvas.width, 0); + grd.addColorStop(0, "rgba(0,0,0,1)"); + grd.addColorStop(.25, "rgba(0,0,0,0)"); + grd.addColorStop(.75, "rgba(0,0,0,0)"); + grd.addColorStop(1, "rgba(0,0,0,1)"); + context.fillStyle = grd; + context.fillRect(0, 0, this.canvas.width, this.canvas.height); + grd = context.createLinearGradient(0, 0, 0, this.canvas.height); + grd.addColorStop(0, "rgba(0,0,0,1)"); + grd.addColorStop(.25, "rgba(0,0,0,0)"); + grd.addColorStop(.75, "rgba(0,0,0,0)"); + grd.addColorStop(1, "rgba(0,0,0,1)"); + context.fillStyle = grd; + context.fillRect(0, 0, this.canvas.width, this.canvas.height); + w = this.canvas.width; + h = this.canvas.height; + context.strokeStyle = "rgba(0,0,0,.5)"; + context.strokeRect(w * .25 + .5, h * .25 + .5, w * .5, h * .5); + context.strokeStyle = "rgba(255,255,255,.5)"; + context.strokeRect(w * .25 - .5, h * .25 - .5, w * .5, h * .5); + } + if (this.hsymmetry) { + w = this.canvas.width; + h = this.canvas.height; + context.fillStyle = "rgba(0,0,0,.5)"; + context.fillRect(0, h / 2 - 2, w, 4); + context.fillStyle = "rgba(255,255,255,.5)"; + context.fillRect(0, h / 2 - 1.5, w, 3); + } + if (this.vsymmetry) { + w = this.canvas.width; + h = this.canvas.height; + context.fillStyle = "rgba(0,0,0,.5)"; + context.fillRect(w / 2 - 2, 0, 4, h); + context.fillStyle = "rgba(255,255,255,.5)"; + context.fillRect(w / 2 - 1.5, 0, 3, h); + } + if ((this.selection != null) && this.editor.tool.selectiontool) { + context.save(); + if (this.tile) { + context.translate(this.canvas.width * .25, this.canvas.width * .25); + } + context.strokeStyle = "rgba(0,0,0,.5)"; + context.lineWidth = 3; + context.strokeRect(this.selection.x * wblock, this.selection.y * hblock, this.selection.w * wblock, this.selection.h * hblock); + context.setLineDash([4, 4]); + context.strokeStyle = this.floating_selection ? "#FA0" : "#FFF"; + context.lineWidth = 2; + context.strokeRect(this.selection.x * wblock, this.selection.y * hblock, this.selection.w * wblock, this.selection.h * hblock); + context.setLineDash([]); + if (this.selection.w > 1 || this.selection.h > 1) { + context.font = (Math.max(12, wblock)) + "pt Ubuntu Mono"; + context.fillStyle = "#FFF"; + context.shadowBlur = 2; + context.shadowColor = "#000"; + context.shadowOpacity = 1; + context.textAlign = "center"; + context.textBaseline = "middle"; + context.fillText(this.selection.w + " x " + this.selection.h, (this.selection.x + this.selection.w / 2) * wblock, (this.selection.y + this.selection.h / 2) * hblock); + context.shadowBlur = 0; + } + return context.restore(); + } + }; + + SpriteView.prototype.mouseDown = function(event) { + var b, bg, c, context, fg, min, x, y; + event.stopPropagation(); + if (!this.editable || (this.sprite == null)) { + return; + } + this.mousepressed = true; + b = this.canvas.getBoundingClientRect(); + min = Math.min(this.canvas.clientWidth, this.canvas.clientHeight); + x = event.clientX - b.left; + y = event.clientY - b.top; + if (this.tile) { + x = Math.floor(x / this.canvas.width * this.sprite.width * 2); + y = Math.floor(y / this.canvas.height * this.sprite.height * 2); + x = Math.floor((x + this.sprite.width / 2) % this.sprite.width); + y = Math.floor((y + this.sprite.height / 2) % this.sprite.height); + } else { + x = Math.floor(x / this.canvas.width * this.sprite.width); + y = Math.floor(y / this.canvas.height * this.sprite.height); + } + if (this.space_pressed) { + this.grab_x = event.clientX; + this.grab_y = event.clientY; + this.grabbing = true; + return; + } + if (this.colorpicker && !this.editor.tool.selectiontool) { + c = this.getFrame().getRGB(x, y); + this.editor.colorpicker.colorPicked(c); + this.mousepressed = false; + if (!this.editor.alt_pressed) { + this.editor.setColorPicker(false); + } + return; + } + if (this.editor.tool.selectiontool) { + if ((this.selection != null) && x >= this.selection.x && y >= this.selection.y && x < this.selection.x + this.selection.w && y < this.selection.y + this.selection.h) { + if ((this.floating_selection == null) && (event.shiftKey || event.altKey)) { + bg = document.createElement("canvas"); + bg.width = this.getFrame().canvas.width; + bg.height = this.getFrame().canvas.height; + context = bg.getContext("2d"); + context.drawImage(this.getFrame().canvas, 0, 0); + if (!event.altKey) { + context.clearRect(this.selection.x, this.selection.y, this.selection.w, this.selection.h); + } + fg = document.createElement("canvas"); + fg.width = this.selection.w; + fg.height = this.selection.h; + context = fg.getContext("2d"); + context.drawImage(this.getFrame().canvas, -this.selection.x, -this.selection.y); + this.floating_selection = { + bg: bg, + fg: fg + }; + } else if (event.altKey) { + this.floating_selection.bg.getContext("2d").drawImage(this.floating_selection.fg, this.selection.x, this.selection.y); + } + this.moving_selection = true; + this.moved_once = false; + this.moving_start_x = x; + this.moving_start_y = y; + this.update(); + return; + } else { + this.floating_selection = null; + this.selection_start_x = x; + this.selection_start_y = y; + this.selection_moved = false; + this.selection = { + x: x, + y: y, + w: 1, + h: 1 + }; + } + this.update(); + return; + } + if (this.sprite.undo == null) { + this.sprite.undo = new Undo(); + } + if (this.sprite.undo.empty()) { + this.sprite.undo.pushState(this.sprite.clone()); + } + if (this.editor.tool.parameters["Color"] != null) { + this.editor.tool.parameters["Color"].value = this.color; + } + this.editor.tool.tile = this.tile; + this.editor.tool.vsymmetry = this.vsymmetry; + this.editor.tool.hsymmetry = this.hsymmetry; + this.editor.tool.start(this.getFrame(), x, y, event.button); + this.mouse_x = x; + this.mouse_y = y; + this.update(); + this.editor.spriteChanged(); + return this.floating_selection = null; + }; + + SpriteView.prototype.mouseEnter = function(event) { + return document.getElementById("spriteeditor").focus(); + }; + + SpriteView.prototype.mouseMove = function(event) { + var b, context, dx, dy, min, view, x, y; + if (this.grabbing) { + dx = event.clientX - this.grab_x; + dy = event.clientY - this.grab_y; + this.grab_x = event.clientX; + this.grab_y = event.clientY; + view = document.getElementById("spriteeditor"); + view.scrollTo(view.scrollLeft - dx, view.scrollTop - dy); + return; + } + if (!this.editable) { + return; + } + b = this.canvas.getBoundingClientRect(); + min = Math.min(this.canvas.clientWidth, this.canvas.clientHeight); + x = event.clientX - b.left; + y = event.clientY - b.top; + if (x >= 0 && y >= 0 && x < b.right - b.left && y < b.bottom - b.top) { + this.show_brush_size = 0; + } + if (this.tile) { + x = Math.floor(x / this.canvas.width * this.sprite.width * 2); + y = Math.floor(y / this.canvas.height * this.sprite.height * 2); + if (!this.mousepressed && (x < 0 || y < 0 || x >= this.sprite.width * 2 || y >= this.sprite.height * 2)) { + return; + } + } else { + x = Math.floor(x / this.canvas.width * this.sprite.width); + y = Math.floor(y / this.canvas.height * this.sprite.height); + if (!this.mousepressed && (x < 0 || y < 0 || x >= this.sprite.width || y >= this.sprite.height)) { + return; + } + } + if (this.mousepressed && this.moving_selection) { + if (this.floating_selection != null) { + if (x !== this.mouse_x || y !== this.mouse_y) { + if (!this.moved_once) { + this.moved_once = true; + if (this.sprite.undo == null) { + this.sprite.undo = new Undo(); + } + if (this.sprite.undo.empty()) { + this.sprite.undo.pushState(this.sprite.clone()); + } + } + this.selection.x += x - this.mouse_x; + this.selection.y += y - this.mouse_y; + this.mouse_x = x; + this.mouse_y = y; + context = this.getFrame().getContext(); + context.clearRect(0, 0, this.getFrame().canvas.width, this.getFrame().canvas.height); + context.drawImage(this.floating_selection.bg, 0, 0); + context.drawImage(this.floating_selection.fg, this.selection.x, this.selection.y); + this.editor.spriteChanged(); + this.update(); + } + } else { + this.selection.x += x - this.mouse_x; + this.selection.y += y - this.mouse_y; + this.selection.x = Math.max(0, Math.min(this.sprite.width - this.selection.w, this.selection.x)); + this.selection.y = Math.max(0, Math.min(this.sprite.height - this.selection.h, this.selection.y)); + this.mouse_x = x; + this.mouse_y = y; + this.update(); + } + return; + } + this.mouse_over = true; + if (this.mousepressed && this.editor.tool.selectiontool) { + this.selection_moved = true; + } + if (x !== this.mouse_x || y !== this.mouse_y) { + this.mouse_x = x; + this.mouse_y = y; + if (this.mousepressed) { + if (this.tile) { + x = Math.floor((x + this.sprite.width / 2) % this.sprite.width); + y = Math.floor((y + this.sprite.height / 2) % this.sprite.height); + } + if (this.editor.tool.selectiontool && this.selection) { + this.selection.x = Math.max(0, Math.min(this.selection_start_x, x)); + this.selection.y = Math.max(0, Math.min(this.selection_start_y, y)); + this.selection.w = Math.min(this.sprite.width - this.selection.x, Math.max(this.selection_start_x, x) - Math.min(this.selection_start_x, Math.max(0, x)) + 1); + this.selection.h = Math.min(this.sprite.height - this.selection.y, Math.max(this.selection_start_y, y) - Math.min(this.selection_start_y, Math.max(0, y)) + 1); + this.update(); + return; + } + this.editor.tool.move(this.getFrame(), x, y, event.buttons); + this.update(); + this.editor.spriteChanged(); + } else { + if ((this.selection != null) && this.editor.tool.selectiontool && x >= this.selection.x && y >= this.selection.y && x < this.selection.x + this.selection.w && y < this.selection.y + this.selection.h) { + this.canvas.style.cursor = "move"; + } else if (this.colorpicker && !this.editor.tool.selectiontool) { + this.canvas.style.cursor = "url( '/img/eyedropper.svg' ) 0 24, pointer"; + } else { + this.canvas.style.cursor = "crosshair"; + } + this.update(); + } + } + return false; + }; + + SpriteView.prototype.mouseUp = function(event) { + if (this.grabbing) { + this.grabbing = false; + } else if (this.mousepressed && !this.editor.tool.selectiontool) { + this.sprite.undo.pushState(this.sprite.clone()); + } + if (this.editor.tool.selectiontool && !this.selection_moved) { + this.selection = null; + this.update(); + } + if (this.moving_selection) { + this.moving_selection = false; + if (this.moved_once && (this.floating_selection != null)) { + this.sprite.undo.pushState(this.sprite.clone()); + } + this.moved_once = false; + } + this.mousepressed = false; + return this.editor.updateSelectionHints(); + }; + + SpriteView.prototype.mouseOut = function(event) { + this.mouse_over = false; + return this.update(); + }; + + return SpriteView; + +})(); diff --git a/static/js/terminal/terminal.coffee b/static/js/terminal/terminal.coffee new file mode 100644 index 00000000..9c45a329 --- /dev/null +++ b/static/js/terminal/terminal.coffee @@ -0,0 +1,147 @@ +class @Terminal + constructor:(@runwindow)-> + @commands = + clear: ()=>@clear() + + @loadHistory() + + loadHistory:()-> + @history = [] + try + if localStorage.getItem("console_history")? + @history = JSON.parse localStorage.getItem("console_history") + catch err + + saveHistory:()-> + localStorage.setItem("console_history",JSON.stringify(@history)) + + start:()-> + return if @started + @started = true + document.getElementById("terminal").addEventListener "mousedown",(event)=> + @pressed = true + @moved = false + true + + document.getElementById("terminal").addEventListener "mousemove",(event)=> + if @pressed + @moved = true + true + + document.getElementById("terminal").addEventListener "mouseup",(event)=> + if not @moved + document.getElementById("terminal-input").focus() + + @moved = false + @pressed = false + true + + document.getElementById("terminal-input").addEventListener "paste",(event)=> + event.preventDefault() + text = event.clipboardData.getData("text/plain") + s = text.split("\n") + if s.length>1 + for line in s + document.getElementById("terminal-input").value = "" + @validateLine line + return + + document.getElementById("terminal-input").value = s[0] + + document.getElementById("terminal-input").addEventListener "keydown",(event)=> + # console.info event.key + if event.key == "Enter" + v = document.getElementById("terminal-input").value + document.getElementById("terminal-input").value = "" + @validateLine v + + else if event.key == "ArrowUp" + if not @history_index? + @history_index = @history.length-1 + @current_input = document.getElementById("terminal-input").value + else + @history_index = Math.max(0,@history_index-1) + + if @history_index == @history.length-1 + @current_input = document.getElementById("terminal-input").value + + if @history_index>=0 and @history_index<@history.length + document.getElementById("terminal-input").value = @history[@history_index] + @setTrailingCaret() + + else if event.key == "ArrowDown" + return if @history_index == @history.length + if @history_index? + @history_index = Math.min(@history.length,@history_index+1) + + if @history_index>=0 and @history_index<@history.length + document.getElementById("terminal-input").value = @history[@history_index] + @setTrailingCaret() + else if @history_index == @history.length + document.getElementById("terminal-input").value = @current_input + @setTrailingCaret() + + validateLine:(v)-> + @history_index = null + + if v.trim().length>0 and v != @history[@history.length-1] + @history.push v + if @history.length>1000 + @history.splice(0,1) + @saveHistory() + + @echo("#{v}",true,"input") + if @commands[v.trim()]? + @commands[v.trim()]() + else + @runwindow.runCommand v + if @runwindow.multiline + document.querySelector("#terminal-input-gt i").classList.add("fa-ellipsis-v") + for i in [0..@runwindow.nesting*2-1] by 1 + document.getElementById("terminal-input").value += " " + @setTrailingCaret() + else + document.querySelector("#terminal-input-gt i").classList.remove("fa-ellipsis-v") + + setTrailingCaret:()-> + setTimeout (()=> + val = document.getElementById("terminal-input").value + document.getElementById("terminal-input").setSelectionRange(val.length,val.length) + ),0 + + echo:(text,scroll=false,classname)-> + if not scroll + e = document.getElementById("terminal-view") + if Math.abs(e.getBoundingClientRect().height+e.scrollTop-e.scrollHeight)<10 + scroll = true + + div = document.createElement("div") + if classname == "input" + d = document.createTextNode(" "+text) + i = document.createElement("i") + i.classList.add("fa") + i.classList.add("fa-angle-right") + div.appendChild i + div.appendChild d + else + div.innerText = text + + if classname? + div.classList.add classname + document.getElementById("terminal-lines").appendChild div + if scroll + div.scrollIntoView() + @truncate() + + error:(text,scroll=false)-> + @echo(text,scroll,"error") + + truncate:()-> + e = document.getElementById("terminal-lines") + while e.childElementCount>1000 + e.removeChild e.firstChild + return + + clear:()-> + document.getElementById("terminal-lines").innerHTML = "" + delete @runwindow.multiline diff --git a/static/js/terminal/terminal.js b/static/js/terminal/terminal.js new file mode 100644 index 00000000..433f2150 --- /dev/null +++ b/static/js/terminal/terminal.js @@ -0,0 +1,208 @@ +this.Terminal = (function() { + function Terminal(runwindow) { + this.runwindow = runwindow; + this.commands = { + clear: (function(_this) { + return function() { + return _this.clear(); + }; + })(this) + }; + this.loadHistory(); + } + + Terminal.prototype.loadHistory = function() { + var err; + this.history = []; + try { + if (localStorage.getItem("console_history") != null) { + return this.history = JSON.parse(localStorage.getItem("console_history")); + } + } catch (error) { + err = error; + } + }; + + Terminal.prototype.saveHistory = function() { + return localStorage.setItem("console_history", JSON.stringify(this.history)); + }; + + Terminal.prototype.start = function() { + if (this.started) { + return; + } + this.started = true; + document.getElementById("terminal").addEventListener("mousedown", (function(_this) { + return function(event) { + _this.pressed = true; + _this.moved = false; + return true; + }; + })(this)); + document.getElementById("terminal").addEventListener("mousemove", (function(_this) { + return function(event) { + if (_this.pressed) { + _this.moved = true; + } + return true; + }; + })(this)); + document.getElementById("terminal").addEventListener("mouseup", (function(_this) { + return function(event) { + if (!_this.moved) { + document.getElementById("terminal-input").focus(); + } + _this.moved = false; + _this.pressed = false; + return true; + }; + })(this)); + document.getElementById("terminal-input").addEventListener("paste", (function(_this) { + return function(event) { + var j, len, line, s, text; + event.preventDefault(); + text = event.clipboardData.getData("text/plain"); + s = text.split("\n"); + if (s.length > 1) { + for (j = 0, len = s.length; j < len; j++) { + line = s[j]; + document.getElementById("terminal-input").value = ""; + _this.validateLine(line); + } + return; + } + return document.getElementById("terminal-input").value = s[0]; + }; + })(this)); + return document.getElementById("terminal-input").addEventListener("keydown", (function(_this) { + return function(event) { + var v; + if (event.key === "Enter") { + v = document.getElementById("terminal-input").value; + document.getElementById("terminal-input").value = ""; + return _this.validateLine(v); + } else if (event.key === "ArrowUp") { + if (_this.history_index == null) { + _this.history_index = _this.history.length - 1; + _this.current_input = document.getElementById("terminal-input").value; + } else { + _this.history_index = Math.max(0, _this.history_index - 1); + } + if (_this.history_index === _this.history.length - 1) { + _this.current_input = document.getElementById("terminal-input").value; + } + if (_this.history_index >= 0 && _this.history_index < _this.history.length) { + document.getElementById("terminal-input").value = _this.history[_this.history_index]; + return _this.setTrailingCaret(); + } + } else if (event.key === "ArrowDown") { + if (_this.history_index === _this.history.length) { + return; + } + if (_this.history_index != null) { + _this.history_index = Math.min(_this.history.length, _this.history_index + 1); + } + if (_this.history_index >= 0 && _this.history_index < _this.history.length) { + document.getElementById("terminal-input").value = _this.history[_this.history_index]; + return _this.setTrailingCaret(); + } else if (_this.history_index === _this.history.length) { + document.getElementById("terminal-input").value = _this.current_input; + return _this.setTrailingCaret(); + } + } + }; + })(this)); + }; + + Terminal.prototype.validateLine = function(v) { + var i, j, ref; + this.history_index = null; + if (v.trim().length > 0 && v !== this.history[this.history.length - 1]) { + this.history.push(v); + if (this.history.length > 1000) { + this.history.splice(0, 1); + } + this.saveHistory(); + } + this.echo("" + v, true, "input"); + if (this.commands[v.trim()] != null) { + return this.commands[v.trim()](); + } else { + this.runwindow.runCommand(v); + if (this.runwindow.multiline) { + document.querySelector("#terminal-input-gt i").classList.add("fa-ellipsis-v"); + for (i = j = 0, ref = this.runwindow.nesting * 2 - 1; j <= ref; i = j += 1) { + document.getElementById("terminal-input").value += " "; + } + return this.setTrailingCaret(); + } else { + return document.querySelector("#terminal-input-gt i").classList.remove("fa-ellipsis-v"); + } + } + }; + + Terminal.prototype.setTrailingCaret = function() { + return setTimeout(((function(_this) { + return function() { + var val; + val = document.getElementById("terminal-input").value; + return document.getElementById("terminal-input").setSelectionRange(val.length, val.length); + }; + })(this)), 0); + }; + + Terminal.prototype.echo = function(text, scroll, classname) { + var d, div, e, i; + if (scroll == null) { + scroll = false; + } + if (!scroll) { + e = document.getElementById("terminal-view"); + if (Math.abs(e.getBoundingClientRect().height + e.scrollTop - e.scrollHeight) < 10) { + scroll = true; + } + } + div = document.createElement("div"); + if (classname === "input") { + d = document.createTextNode(" " + text); + i = document.createElement("i"); + i.classList.add("fa"); + i.classList.add("fa-angle-right"); + div.appendChild(i); + div.appendChild(d); + } else { + div.innerText = text; + } + if (classname != null) { + div.classList.add(classname); + } + document.getElementById("terminal-lines").appendChild(div); + if (scroll) { + div.scrollIntoView(); + } + return this.truncate(); + }; + + Terminal.prototype.error = function(text, scroll) { + if (scroll == null) { + scroll = false; + } + return this.echo(text, scroll, "error"); + }; + + Terminal.prototype.truncate = function() { + var e; + e = document.getElementById("terminal-lines"); + while (e.childElementCount > 1000) { + e.removeChild(e.firstChild); + } + }; + + Terminal.prototype.clear = function() { + document.getElementById("terminal-lines").innerHTML = ""; + return delete this.runwindow.multiline; + }; + + return Terminal; + +})(); diff --git a/static/js/tutorial/highlighter.coffee b/static/js/tutorial/highlighter.coffee new file mode 100644 index 00000000..e33a45ae --- /dev/null +++ b/static/js/tutorial/highlighter.coffee @@ -0,0 +1,99 @@ +class @Highlighter + constructor:(@tutorial)-> + @shown = false + @canvas = document.getElementById("highlighter") + @arrow = document.getElementById("highlighter-arrow") + + highlight:(ref,auto)-> + element = document.querySelector(ref) if ref? + if element? + @highlighted = element + rect = element.getBoundingClientRect() + if rect.width == 0 + @hide() + return + + x = rect.x+rect.width*.5 + y = rect.y+rect.height*.5 + w = Math.floor(rect.width*1)+40 + h = Math.floor(rect.height*1)+40 + @canvas.width = w + @canvas.height = h + @canvas.style.width = "#{w}px" + @canvas.style.height = "#{h}px" + @canvas.style.top = "#{Math.round(y-h/2)}px" + @canvas.style.left = "#{Math.round(x-w/2)}px" + @canvas.style.display = "block" + @shown = true + @updateCanvas() + clearTimeout(@timeout) if @timeout? + @timeout = setTimeout (()=> @justHide()),6000 + if auto + @setAuto(element) + else + @remove_event_listener() if @remove_event_listener? + else + @hide() + + setAuto:(element)-> + if element.tagName == "INPUT" and element.type == "text" + f = (event)=> + if event.key == "Enter" + @tutorial.nextStep() + element.removeEventListener "keydown",f + element.addEventListener "keydown",f + @remove_event_listener() if @remove_event_listener? + @remove_event_listener = ()=> + element.removeEventListener "keydown",f + else + f = ()=> + @tutorial.nextStep() + element.removeEventListener "click",f + element.addEventListener "click",f + + @remove_event_listener() if @remove_event_listener? + @remove_event_listener = ()=> + element.removeEventListener "click",f + + hide:()-> + @shown = false + @canvas.style.display = "none" + @remove_event_listener() if @remove_event_listener? + + justHide:()-> + @shown = false + @canvas.style.display = "none" + + updateCanvas:()-> + return if not @shown + requestAnimationFrame ()=>@updateCanvas() + context = @canvas.getContext "2d" + context.clearRect(0,0,@canvas.width,@canvas.height) + return if @highlighted.getBoundingClientRect().width == 0 + context.shadowOpacity = 1 + context.shadowBlur = 5 + context.shadowColor = "#000" + + grd = context.createLinearGradient(0,0,@canvas.width,@canvas.height) + grd.addColorStop 0,"hsl(0,50%,60%)" + grd.addColorStop 1,"hsl(60,50%,60%)" + context.strokeStyle = "hsl(30,100%,70%)" + context.lineWidth = 3 + context.lineCap = "round" + w = @canvas.width/2-5 + h = @canvas.height/2-5 + amount = (Date.now()%1000)/500 + + context.globalAlpha = Math.min(1,2-amount) + amount = Math.min(1,amount) + + context.beginPath() + context.moveTo(@canvas.width/2+w,@canvas.height/2) + for i in [0..amount] by .01 + x = Math.cos(i*2*Math.PI) + y = Math.sin(i*2*Math.PI) + x = if x>0 then Math.sqrt(x) else -Math.sqrt(-x) + y = if y>0 then Math.sqrt(y) else -Math.sqrt(-y) + context.lineTo(@canvas.width/2+x*w,@canvas.height/2+y*h) + #context.ellipse(@canvas.width/2,@canvas.height/2,w,h,Math.PI,Math.PI*2*Math.min(1,amount),0,true) + context.stroke() diff --git a/static/js/tutorial/highlighter.js b/static/js/tutorial/highlighter.js new file mode 100644 index 00000000..c9708404 --- /dev/null +++ b/static/js/tutorial/highlighter.js @@ -0,0 +1,149 @@ +this.Highlighter = (function() { + function Highlighter(tutorial) { + this.tutorial = tutorial; + this.shown = false; + this.canvas = document.getElementById("highlighter"); + this.arrow = document.getElementById("highlighter-arrow"); + } + + Highlighter.prototype.highlight = function(ref, auto) { + var element, h, rect, w, x, y; + if (ref != null) { + element = document.querySelector(ref); + } + if (element != null) { + this.highlighted = element; + rect = element.getBoundingClientRect(); + if (rect.width === 0) { + this.hide(); + return; + } + x = rect.x + rect.width * .5; + y = rect.y + rect.height * .5; + w = Math.floor(rect.width * 1) + 40; + h = Math.floor(rect.height * 1) + 40; + this.canvas.width = w; + this.canvas.height = h; + this.canvas.style.width = w + "px"; + this.canvas.style.height = h + "px"; + this.canvas.style.top = (Math.round(y - h / 2)) + "px"; + this.canvas.style.left = (Math.round(x - w / 2)) + "px"; + this.canvas.style.display = "block"; + this.shown = true; + this.updateCanvas(); + if (this.timeout != null) { + clearTimeout(this.timeout); + } + this.timeout = setTimeout(((function(_this) { + return function() { + return _this.justHide(); + }; + })(this)), 6000); + if (auto) { + return this.setAuto(element); + } else { + if (this.remove_event_listener != null) { + return this.remove_event_listener(); + } + } + } else { + return this.hide(); + } + }; + + Highlighter.prototype.setAuto = function(element) { + var f; + if (element.tagName === "INPUT" && element.type === "text") { + f = (function(_this) { + return function(event) { + if (event.key === "Enter") { + _this.tutorial.nextStep(); + return element.removeEventListener("keydown", f); + } + }; + })(this); + element.addEventListener("keydown", f); + if (this.remove_event_listener != null) { + this.remove_event_listener(); + } + return this.remove_event_listener = (function(_this) { + return function() { + return element.removeEventListener("keydown", f); + }; + })(this); + } else { + f = (function(_this) { + return function() { + _this.tutorial.nextStep(); + return element.removeEventListener("click", f); + }; + })(this); + element.addEventListener("click", f); + if (this.remove_event_listener != null) { + this.remove_event_listener(); + } + return this.remove_event_listener = (function(_this) { + return function() { + return element.removeEventListener("click", f); + }; + })(this); + } + }; + + Highlighter.prototype.hide = function() { + this.shown = false; + this.canvas.style.display = "none"; + if (this.remove_event_listener != null) { + return this.remove_event_listener(); + } + }; + + Highlighter.prototype.justHide = function() { + this.shown = false; + return this.canvas.style.display = "none"; + }; + + Highlighter.prototype.updateCanvas = function() { + var amount, context, grd, h, i, j, ref1, w, x, y; + if (!this.shown) { + return; + } + requestAnimationFrame((function(_this) { + return function() { + return _this.updateCanvas(); + }; + })(this)); + context = this.canvas.getContext("2d"); + context.clearRect(0, 0, this.canvas.width, this.canvas.height); + if (this.highlighted.getBoundingClientRect().width === 0) { + return; + } + context.shadowOpacity = 1; + context.shadowBlur = 5; + context.shadowColor = "#000"; + grd = context.createLinearGradient(0, 0, this.canvas.width, this.canvas.height); + grd.addColorStop(0, "hsl(0,50%,60%)"); + grd.addColorStop(1, "hsl(60,50%,60%)"); + context.strokeStyle = "hsl(30,100%,70%)"; + context.lineWidth = 3; + context.lineCap = "round"; + w = this.canvas.width / 2 - 5; + h = this.canvas.height / 2 - 5; + amount = (Date.now() % 1000) / 500; + context.globalAlpha = Math.min(1, 2 - amount); + amount = Math.min(1, amount); + context.beginPath(); + context.moveTo(this.canvas.width / 2 + w, this.canvas.height / 2); + for (i = j = 0, ref1 = amount; j <= ref1; i = j += .01) { + x = Math.cos(i * 2 * Math.PI); + y = Math.sin(i * 2 * Math.PI); + x = x > 0 ? Math.sqrt(x) : -Math.sqrt(-x); + y = y > 0 ? Math.sqrt(y) : -Math.sqrt(-y); + context.lineTo(this.canvas.width / 2 + x * w, this.canvas.height / 2 + y * h); + } + return context.stroke(); + }; + + return Highlighter; + +})(); diff --git a/static/js/tutorial/tutorial.coffee b/static/js/tutorial/tutorial.coffee new file mode 100644 index 00000000..7f96f125 --- /dev/null +++ b/static/js/tutorial/tutorial.coffee @@ -0,0 +1,94 @@ +class @Tutorial + constructor:(@link,@back_to_tutorials=true)-> + @title = "" + + load:(callback,error)-> + req = new XMLHttpRequest() + req.onreadystatechange = (event) => + if req.readyState == XMLHttpRequest.DONE + if req.status == 200 + @update(req.responseText,callback) + else if req.status >= 400 + if error? then error(req.status) + + req.open "GET",@link + req.send() + + update:(doc,callback)-> + element = document.createElement("div") + element.innerHTML = DOMPurify.sanitize marked doc + + @steps = [] + + if element.hasChildNodes() + alist = element.getElementsByTagName "a" + if alist and alist.length>0 + for a in alist + a.target = "_blank" + + for e in element.childNodes + switch e.tagName + when "H1" + @title = e.innerText + + when "H2" + step = + title: e.innerText + content: [] + + @steps.push(step) + + else + if step? + if e.tagName == "P" and e.textContent.startsWith ":" + line = e.textContent + line = line.substring(1,line.length) + s = line.split(" ") + switch s[0] + when "highlight" + s.splice(0,1) + step.highlight = s.join(" ").trim() + when "navigate" + s.splice(0,1) + step.navigate = s.join(" ").trim() + when "position" + s.splice(0,1) + step.position = s.join(" ").trim() + when "overlay" + step.overlay = true + when "auto" + s.splice(0,1) + step.auto = s.join(" ").trim() or true + else + if e.tagName == "PRE" + text = e.firstChild.textContent + button = document.createElement "div" + button.classList.add "copy-button" + button.innerText = app.translator.get "Copy" + + e.appendChild button + + do (text,button)=> + button.addEventListener "click",()=> + console.info text + navigator.clipboard.writeText text + button.innerText = app.translator.get "Copied!" + button.classList.add "copied" + # navigator.permissions.query({name: "clipboard-write"}).then (result)=> + # if result.state == "granted" + # navigator.clipboard.writeText(text).then ()=> + # button.innerText = app.translator.get "Copied!" + # button.classList.add "copied" + # console.info "copied" + + step.content.push e + + else if e.tagName == "P" and e.textContent.startsWith ":" + line = e.textContent + line = line.substring(1,line.length) + s = line.split(" ") + if s.splice(0,1)[0] == "project" + @project_title = s.join(" ") + + console.info @steps + callback() if callback? diff --git a/static/js/tutorial/tutorial.js b/static/js/tutorial/tutorial.js new file mode 100644 index 00000000..8ec211de --- /dev/null +++ b/static/js/tutorial/tutorial.js @@ -0,0 +1,120 @@ +this.Tutorial = (function() { + function Tutorial(link, back_to_tutorials) { + this.link = link; + this.back_to_tutorials = back_to_tutorials != null ? back_to_tutorials : true; + this.title = ""; + } + + Tutorial.prototype.load = function(callback, error) { + var req; + req = new XMLHttpRequest(); + req.onreadystatechange = (function(_this) { + return function(event) { + if (req.readyState === XMLHttpRequest.DONE) { + if (req.status === 200) { + return _this.update(req.responseText, callback); + } else if (req.status >= 400) { + if (error != null) { + return error(req.status); + } + } + } + }; + })(this); + req.open("GET", this.link); + return req.send(); + }; + + Tutorial.prototype.update = function(doc, callback) { + var a, alist, button, e, element, i, j, len, len1, line, ref, s, step, text; + element = document.createElement("div"); + element.innerHTML = DOMPurify.sanitize(marked(doc)); + this.steps = []; + if (element.hasChildNodes()) { + alist = element.getElementsByTagName("a"); + if (alist && alist.length > 0) { + for (i = 0, len = alist.length; i < len; i++) { + a = alist[i]; + a.target = "_blank"; + } + } + ref = element.childNodes; + for (j = 0, len1 = ref.length; j < len1; j++) { + e = ref[j]; + switch (e.tagName) { + case "H1": + this.title = e.innerText; + break; + case "H2": + step = { + title: e.innerText, + content: [] + }; + this.steps.push(step); + break; + default: + if (step != null) { + if (e.tagName === "P" && e.textContent.startsWith(":")) { + line = e.textContent; + line = line.substring(1, line.length); + s = line.split(" "); + switch (s[0]) { + case "highlight": + s.splice(0, 1); + step.highlight = s.join(" ").trim(); + break; + case "navigate": + s.splice(0, 1); + step.navigate = s.join(" ").trim(); + break; + case "position": + s.splice(0, 1); + step.position = s.join(" ").trim(); + break; + case "overlay": + step.overlay = true; + break; + case "auto": + s.splice(0, 1); + step.auto = s.join(" ").trim() || true; + } + } else { + if (e.tagName === "PRE") { + text = e.firstChild.textContent; + button = document.createElement("div"); + button.classList.add("copy-button"); + button.innerText = app.translator.get("Copy"); + e.appendChild(button); + (function(_this) { + return (function(text, button) { + return button.addEventListener("click", function() { + console.info(text); + navigator.clipboard.writeText(text); + button.innerText = app.translator.get("Copied!"); + return button.classList.add("copied"); + }); + }); + })(this)(text, button); + } + step.content.push(e); + } + } else if (e.tagName === "P" && e.textContent.startsWith(":")) { + line = e.textContent; + line = line.substring(1, line.length); + s = line.split(" "); + if (s.splice(0, 1)[0] === "project") { + this.project_title = s.join(" "); + } + } + } + } + } + console.info(this.steps); + if (callback != null) { + return callback(); + } + }; + + return Tutorial; + +})(); diff --git a/static/js/tutorial/tutorials.coffee b/static/js/tutorial/tutorials.coffee new file mode 100644 index 00000000..05f231e7 --- /dev/null +++ b/static/js/tutorial/tutorials.coffee @@ -0,0 +1,118 @@ +class @Tutorials + constructor:(@app)-> + + load:()-> + req = new XMLHttpRequest() + req.onreadystatechange = (event) => + if req.readyState == XMLHttpRequest.DONE + if req.status == 200 + @update(req.responseText) + + switch @app.translator.lang + when "fr" + req.open "GET",location.origin+"/tutorials/tutorials_fr/list/doc/doc.md" + else + req.open "GET",location.origin+"/tutorials/tutorials_en/list/doc/doc.md" + + req.send() + + update:(doc)-> + element = document.createElement("div") + element.innerHTML = DOMPurify.sanitize marked doc + + @tutorials = [] + + if element.hasChildNodes() + for e in element.childNodes + #console.info e + switch e.tagName + when "H2" + list = + title: e.innerText + description: "" + list: [] + + @tutorials.push(list) + + when "P" + if e.hasChildNodes() + for e2 in e.childNodes + switch e2.tagName + when "A" + if list? + list.list.push + title: e2.textContent + link: e2.href + when undefined + list.description = e2.textContent + + #console.info @tutorials + + @build() + return + + build:()-> + document.getElementById("tutorials-content").innerHTML = "" + for t in @tutorials + @buildCourse(t) + return + + buildCourse:(course)-> + div = document.createElement "div" + div.classList.add "course" + h2 = document.createElement "h2" + h2.innerText = course.title + div.appendChild h2 + + p = document.createElement "p" + div.appendChild p + p.innerText = course.description + + ul = document.createElement "ul" + div.appendChild ul + + for t in course.list + div.appendChild @buildTutorial t + + document.getElementById("tutorials-content").appendChild div + + buildTutorial:(t)-> + li = document.createElement "li" + li.innerHTML = " #{t.title}" + + li.addEventListener "click",()=> + @startTutorial(t) + + progress = @app.getTutorialProgress(t.link) + if progress>0 + li.style.background = "linear-gradient(90deg,hsl(160,50%,70%) 0%,hsl(160,50%,70%) #{progress}%,rgba(0,0,0,.1) #{progress}%)" + li.addEventListener "mouseover",()-> + li.style.background = "hsl(200,50%,70%)" + + li.addEventListener "mouseout",()-> + li.style.background = "linear-gradient(90deg,hsl(160,50%,70%) 0%,hsl(160,50%,70%) #{progress}%,rgba(0,0,0,.1) #{progress}%)" + + if progress == 100 + li.firstChild.classList.remove "fa-play" + li.firstChild.classList.add "fa-check" + + a = document.createElement "a" + a.href = t.link + a.target = "_blank" + a.title = @app.translator.get("View tutorial source code") + + code = document.createElement "i" + code.classList.add "fas" + code.classList.add "fa-file" + a.appendChild code + li.appendChild a + + a.addEventListener "click",(event)=> + event.stopPropagation() + + li + + startTutorial:(t)-> + tuto = new Tutorial(t.link.replace("https://microstudio.dev",location.origin)) + tuto.load ()=> + @app.tutorial.start(tuto) diff --git a/static/js/tutorial/tutorials.js b/static/js/tutorial/tutorials.js new file mode 100644 index 00000000..d3337c6e --- /dev/null +++ b/static/js/tutorial/tutorials.js @@ -0,0 +1,153 @@ +this.Tutorials = (function() { + function Tutorials(app) { + this.app = app; + } + + Tutorials.prototype.load = function() { + var req; + req = new XMLHttpRequest(); + req.onreadystatechange = (function(_this) { + return function(event) { + if (req.readyState === XMLHttpRequest.DONE) { + if (req.status === 200) { + return _this.update(req.responseText); + } + } + }; + })(this); + switch (this.app.translator.lang) { + case "fr": + req.open("GET", location.origin + "/tutorials/tutorials_fr/list/doc/doc.md"); + break; + default: + req.open("GET", location.origin + "/tutorials/tutorials_en/list/doc/doc.md"); + } + return req.send(); + }; + + Tutorials.prototype.update = function(doc) { + var e, e2, element, i, j, len, len1, list, ref, ref1; + element = document.createElement("div"); + element.innerHTML = DOMPurify.sanitize(marked(doc)); + this.tutorials = []; + if (element.hasChildNodes()) { + ref = element.childNodes; + for (i = 0, len = ref.length; i < len; i++) { + e = ref[i]; + switch (e.tagName) { + case "H2": + list = { + title: e.innerText, + description: "", + list: [] + }; + this.tutorials.push(list); + break; + case "P": + if (e.hasChildNodes()) { + ref1 = e.childNodes; + for (j = 0, len1 = ref1.length; j < len1; j++) { + e2 = ref1[j]; + switch (e2.tagName) { + case "A": + if (list != null) { + list.list.push({ + title: e2.textContent, + link: e2.href + }); + } + break; + case void 0: + list.description = e2.textContent; + } + } + } + } + } + } + this.build(); + }; + + Tutorials.prototype.build = function() { + var i, len, ref, t; + document.getElementById("tutorials-content").innerHTML = ""; + ref = this.tutorials; + for (i = 0, len = ref.length; i < len; i++) { + t = ref[i]; + this.buildCourse(t); + } + }; + + Tutorials.prototype.buildCourse = function(course) { + var div, h2, i, len, p, ref, t, ul; + div = document.createElement("div"); + div.classList.add("course"); + h2 = document.createElement("h2"); + h2.innerText = course.title; + div.appendChild(h2); + p = document.createElement("p"); + div.appendChild(p); + p.innerText = course.description; + ul = document.createElement("ul"); + div.appendChild(ul); + ref = course.list; + for (i = 0, len = ref.length; i < len; i++) { + t = ref[i]; + div.appendChild(this.buildTutorial(t)); + } + return document.getElementById("tutorials-content").appendChild(div); + }; + + Tutorials.prototype.buildTutorial = function(t) { + var a, code, li, progress; + li = document.createElement("li"); + li.innerHTML = " " + t.title; + li.addEventListener("click", (function(_this) { + return function() { + return _this.startTutorial(t); + }; + })(this)); + progress = this.app.getTutorialProgress(t.link); + if (progress > 0) { + li.style.background = "linear-gradient(90deg,hsl(160,50%,70%) 0%,hsl(160,50%,70%) " + progress + "%,rgba(0,0,0,.1) " + progress + "%)"; + li.addEventListener("mouseover", function() { + return li.style.background = "hsl(200,50%,70%)"; + }); + li.addEventListener("mouseout", function() { + return li.style.background = "linear-gradient(90deg,hsl(160,50%,70%) 0%,hsl(160,50%,70%) " + progress + "%,rgba(0,0,0,.1) " + progress + "%)"; + }); + } + if (progress === 100) { + li.firstChild.classList.remove("fa-play"); + li.firstChild.classList.add("fa-check"); + } + a = document.createElement("a"); + a.href = t.link; + a.target = "_blank"; + a.title = this.app.translator.get("View tutorial source code"); + code = document.createElement("i"); + code.classList.add("fas"); + code.classList.add("fa-file"); + a.appendChild(code); + li.appendChild(a); + a.addEventListener("click", (function(_this) { + return function(event) { + return event.stopPropagation(); + }; + })(this)); + return li; + }; + + Tutorials.prototype.startTutorial = function(t) { + var tuto; + tuto = new Tutorial(t.link.replace("https://microstudio.dev", location.origin)); + return tuto.load((function(_this) { + return function() { + return _this.app.tutorial.start(tuto); + }; + })(this)); + }; + + return Tutorials; + +})(); diff --git a/static/js/tutorial/tutorialwindow.coffee b/static/js/tutorial/tutorialwindow.coffee new file mode 100644 index 00000000..0afbadab --- /dev/null +++ b/static/js/tutorial/tutorialwindow.coffee @@ -0,0 +1,234 @@ +class @TutorialWindow + constructor:(@app)-> + @window = document.getElementById("tutorial-window") + + + document.querySelector("#tutorial-window").addEventListener "mousedown",(event)=>@moveToFront() + + document.querySelector("#tutorial-window .titlebar").addEventListener "click",(event)=>@uncollapse() + + document.querySelector("#tutorial-window .titlebar").addEventListener "mousedown",(event)=>@startMove(event) + document.querySelector("#tutorial-window .navigation .resize").addEventListener "mousedown",(event)=>@startResize(event) + document.addEventListener "mousemove",(event)=>@mouseMove(event) + document.addEventListener "mouseup",(event)=>@mouseUp(event) + window.addEventListener "resize",()=> + b = @window.getBoundingClientRect() + @setPosition b.x,b.y + + document.querySelector("#tutorial-window .navigation .previous").addEventListener "click",()=>@previousStep() + document.querySelector("#tutorial-window .navigation .next").addEventListener "click",()=>@nextStep() + document.querySelector("#tutorial-window .titlebar .minify").addEventListener "click",()=>@close() + + @highlighter = new Highlighter @ + @max_ratio = .75 + + + moveToFront:()-> + list = document.getElementsByClassName "floating-window" + for e in list + if e.id == "tutorial-window" + e.style["z-index"] = 11 + else + e.style["z-index"] = 10 + return + + start:(@tutorial)-> + @shown = true + @uncollapse() + if @tutorial.project_title? and not @app.user? + @app.appui.accountRequired ()=> + @start @tutorial + return + + @openProject() + + document.getElementById("tutorial-window").style.display = "block" + document.querySelector("#tutorial-window .title").innerText = @tutorial.title + + progress = @app.getTutorialProgress(@tutorial.link) + @current_step = Math.round(progress/100*(@tutorial.steps.length-1)) + if @current_step == @tutorial.steps.length-1 + @current_step = 0 + @setStep(@current_step) + + openProject:()-> + if @tutorial.project_title? + slug = RegexLib.slugify @tutorial.project_title + project = null + for p in @app.projects + if p.slug == slug + if not @app.project? or @app.project.id != p.id + @app.openProject p + project = p + break + + if not project? + @app.createProject @tutorial.project_title,slug,()=> + @start @tutorial + return + + @app.setProjectTutorial(slug,@tutorial.link) + @app.appui.setMainSection("projects") + + update:()-> + if @tutorial? + @setStep(@current_step) + + setStep:(index)-> + if @tutorial? + index = Math.max(0,Math.min(@tutorial.steps.length-1,index)) + @current_step = index + step = @tutorial.steps[index] + e = document.querySelector("#tutorial-window .content") + e.innerHTML = "" + for c in step.content + e.appendChild c + + e.scrollTo 0,0 + + document.querySelector("#tutorial-window .navigation .step").innerText = (index+1)+" / "+@tutorial.steps.length + + percent = Math.round(@current_step/(@tutorial.steps.length-1)*100) + + document.querySelector("#tutorial-window .navigation .step").style.background = "linear-gradient(90deg,hsl(200,50%,80%) 0%,hsl(200,50%,80%) #{percent}%,transparent #{percent}%)" + + if step.navigate? + s = step.navigate.split(".") + @app.appui.setMainSection s[0] + if s[1]? + @app.appui.setSection s[1] + switch s[2] + when "console" + @app.appui.code_splitbar.setPosition(0) + @app.appui.runtime_splitbar.setPosition(0) + + if step.position? + s = step.position.split(",") + if s.length == 4 + w = Math.floor Math.max(200,Math.min(window.innerWidth*s[2]/100)) + h = Math.floor Math.max(200,Math.min(window.innerHeight*s[3]/100)) + @window.style.width = "#{w}px" + @window.style.height = "#{h}px" + @setPosition(s[0]*window.innerWidth/100,s[1]*window.innerHeight/100) + + if step.highlight? + @highlighter.highlight step.highlight,step.auto == true + else + @highlighter.hide() + + if step.auto? and step.auto != true + element = document.querySelector(step.auto) + if element? + @highlighter.setAuto(element) + + if step.overlay + document.getElementById("tutorial-overlay").style.display = "block" + else + document.getElementById("tutorial-overlay").style.display = "none" + + #if @current_step>0 + progress = @app.getTutorialProgress(@tutorial.link) + percent = Math.round(@current_step/(@tutorial.steps.length-1)*100) + #if percent>progress + @app.setTutorialProgress(@tutorial.link,percent) + + if @current_step == @tutorial.steps.length-1 + document.querySelector("#tutorial-window .navigation .next").classList.add "fa-check" + document.querySelector("#tutorial-window .navigation .next").classList.remove "fa-arrow-right" + else + document.querySelector("#tutorial-window .navigation .next").classList.remove "fa-check" + document.querySelector("#tutorial-window .navigation .next").classList.add "fa-arrow-right" + + return + + nextStep:()-> + if @current_step == @tutorial.steps.length-1 + @close() + else + @setStep(@current_step+1) + + previousStep:()-> + @setStep(@current_step-1) + + close:()-> + if @current_step == @tutorial.steps.length-1 + @shown = false + if @tutorial.back_to_tutorials + @app.appui.setMainSection("tutorials") + document.getElementById("tutorial-window").style.display = "none" + @highlighter.hide() + document.getElementById("tutorial-overlay").style.display = "none" + else + @highlighter.hide() + document.getElementById("tutorial-overlay").style.display = "none" + @pos_top = @window.style.top + @pos_left = @window.style.left + @pos_width = @window.style.width + @pos_height = @window.style.height + + button = document.getElementById "menu-tutorials" + b = button.getBoundingClientRect() + @window.classList.add "minimized" + + setTimeout (()=> + @window.style.top = (b.y+b.height-10)+"px" + @window.style.left = (b.x+b.width/2+20)+"px" + @window.style.width = "30px" + @window.style.height = "30px" + @collapsed = true + ),100 + + uncollapse:()-> + if @collapsed + @collapsed = false + @openProject() + @window.style.top = @pos_top + @window.style.left = @pos_left + @window.style.width = @pos_width + @window.style.height = @pos_height + setTimeout (()=> + @window.classList.remove "minimized" + @setStep @current_step + ),100 + + startMove:(event)-> + @moving = true + @drag_start_x = event.clientX + @drag_start_y = event.clientY + @drag_pos_x = @window.getBoundingClientRect().x + @drag_pos_y = @window.getBoundingClientRect().y + + startResize:(event)-> + @resizing = true + @drag_start_x = event.clientX + @drag_start_y = event.clientY + @drag_size_w = @window.getBoundingClientRect().width + @drag_size_h = @window.getBoundingClientRect().height + + mouseMove:(event)-> + if @moving + dx = event.clientX-@drag_start_x + dy = event.clientY-@drag_start_y + @setPosition(@drag_pos_x+dx,@drag_pos_y+dy) + + if @resizing + dx = event.clientX-@drag_start_x + dy = event.clientY-@drag_start_y + w = Math.floor Math.max(200,Math.min(window.innerWidth*@max_ratio,@drag_size_w+dx)) + h = Math.floor Math.max(200,Math.min(window.innerHeight*@max_ratio,@drag_size_h+dy)) + @window.style.width = "#{w}px" + @window.style.height = "#{h}px" + b = @window.getBoundingClientRect() + if w>window.innerWidth-b.x or h>window.innerHeight-b.y + @setPosition(Math.min(b.x,window.innerWidth-w-4),Math.min(b.y,window.innerHeight-h-4)) + + mouseUp:(event)-> + @moving = false + @resizing = false + + setPosition:(x,y)-> + b = @window.getBoundingClientRect() + x = Math.max(4,Math.min(window.innerWidth-b.width-4,x)) + y = Math.max(4,Math.min(window.innerHeight-b.height-4,y)) + @window.style.top = y+"px" + @window.style.left = x+"px" diff --git a/static/js/tutorial/tutorialwindow.js b/static/js/tutorial/tutorialwindow.js new file mode 100644 index 00000000..8db4b558 --- /dev/null +++ b/static/js/tutorial/tutorialwindow.js @@ -0,0 +1,315 @@ +this.TutorialWindow = (function() { + function TutorialWindow(app) { + this.app = app; + this.window = document.getElementById("tutorial-window"); + document.querySelector("#tutorial-window").addEventListener("mousedown", (function(_this) { + return function(event) { + return _this.moveToFront(); + }; + })(this)); + document.querySelector("#tutorial-window .titlebar").addEventListener("click", (function(_this) { + return function(event) { + return _this.uncollapse(); + }; + })(this)); + document.querySelector("#tutorial-window .titlebar").addEventListener("mousedown", (function(_this) { + return function(event) { + return _this.startMove(event); + }; + })(this)); + document.querySelector("#tutorial-window .navigation .resize").addEventListener("mousedown", (function(_this) { + return function(event) { + return _this.startResize(event); + }; + })(this)); + document.addEventListener("mousemove", (function(_this) { + return function(event) { + return _this.mouseMove(event); + }; + })(this)); + document.addEventListener("mouseup", (function(_this) { + return function(event) { + return _this.mouseUp(event); + }; + })(this)); + window.addEventListener("resize", (function(_this) { + return function() { + var b; + b = _this.window.getBoundingClientRect(); + return _this.setPosition(b.x, b.y); + }; + })(this)); + document.querySelector("#tutorial-window .navigation .previous").addEventListener("click", (function(_this) { + return function() { + return _this.previousStep(); + }; + })(this)); + document.querySelector("#tutorial-window .navigation .next").addEventListener("click", (function(_this) { + return function() { + return _this.nextStep(); + }; + })(this)); + document.querySelector("#tutorial-window .titlebar .minify").addEventListener("click", (function(_this) { + return function() { + return _this.close(); + }; + })(this)); + this.highlighter = new Highlighter(this); + this.max_ratio = .75; + } + + TutorialWindow.prototype.moveToFront = function() { + var e, i, len, list; + list = document.getElementsByClassName("floating-window"); + for (i = 0, len = list.length; i < len; i++) { + e = list[i]; + if (e.id === "tutorial-window") { + e.style["z-index"] = 11; + } else { + e.style["z-index"] = 10; + } + } + }; + + TutorialWindow.prototype.start = function(tutorial) { + var progress; + this.tutorial = tutorial; + this.shown = true; + this.uncollapse(); + if ((this.tutorial.project_title != null) && (this.app.user == null)) { + this.app.appui.accountRequired((function(_this) { + return function() { + return _this.start(_this.tutorial); + }; + })(this)); + return; + } + this.openProject(); + document.getElementById("tutorial-window").style.display = "block"; + document.querySelector("#tutorial-window .title").innerText = this.tutorial.title; + progress = this.app.getTutorialProgress(this.tutorial.link); + this.current_step = Math.round(progress / 100 * (this.tutorial.steps.length - 1)); + if (this.current_step === this.tutorial.steps.length - 1) { + this.current_step = 0; + } + return this.setStep(this.current_step); + }; + + TutorialWindow.prototype.openProject = function() { + var i, len, p, project, ref, slug; + if (this.tutorial.project_title != null) { + slug = RegexLib.slugify(this.tutorial.project_title); + project = null; + ref = this.app.projects; + for (i = 0, len = ref.length; i < len; i++) { + p = ref[i]; + if (p.slug === slug) { + if ((this.app.project == null) || this.app.project.id !== p.id) { + this.app.openProject(p); + } + project = p; + break; + } + } + if (project == null) { + this.app.createProject(this.tutorial.project_title, slug, (function(_this) { + return function() { + return _this.start(_this.tutorial); + }; + })(this)); + return; + } + this.app.setProjectTutorial(slug, this.tutorial.link); + return this.app.appui.setMainSection("projects"); + } + }; + + TutorialWindow.prototype.update = function() { + if (this.tutorial != null) { + return this.setStep(this.current_step); + } + }; + + TutorialWindow.prototype.setStep = function(index) { + var c, e, element, h, i, len, percent, progress, ref, s, step, w; + if (this.tutorial != null) { + index = Math.max(0, Math.min(this.tutorial.steps.length - 1, index)); + this.current_step = index; + step = this.tutorial.steps[index]; + e = document.querySelector("#tutorial-window .content"); + e.innerHTML = ""; + ref = step.content; + for (i = 0, len = ref.length; i < len; i++) { + c = ref[i]; + e.appendChild(c); + } + e.scrollTo(0, 0); + document.querySelector("#tutorial-window .navigation .step").innerText = (index + 1) + " / " + this.tutorial.steps.length; + percent = Math.round(this.current_step / (this.tutorial.steps.length - 1) * 100); + document.querySelector("#tutorial-window .navigation .step").style.background = "linear-gradient(90deg,hsl(200,50%,80%) 0%,hsl(200,50%,80%) " + percent + "%,transparent " + percent + "%)"; + if (step.navigate != null) { + s = step.navigate.split("."); + this.app.appui.setMainSection(s[0]); + if (s[1] != null) { + this.app.appui.setSection(s[1]); + } + switch (s[2]) { + case "console": + this.app.appui.code_splitbar.setPosition(0); + this.app.appui.runtime_splitbar.setPosition(0); + } + } + if (step.position != null) { + s = step.position.split(","); + if (s.length === 4) { + w = Math.floor(Math.max(200, Math.min(window.innerWidth * s[2] / 100))); + h = Math.floor(Math.max(200, Math.min(window.innerHeight * s[3] / 100))); + this.window.style.width = w + "px"; + this.window.style.height = h + "px"; + this.setPosition(s[0] * window.innerWidth / 100, s[1] * window.innerHeight / 100); + } + } + if (step.highlight != null) { + this.highlighter.highlight(step.highlight, step.auto === true); + } else { + this.highlighter.hide(); + } + if ((step.auto != null) && step.auto !== true) { + element = document.querySelector(step.auto); + if (element != null) { + this.highlighter.setAuto(element); + } + } + if (step.overlay) { + document.getElementById("tutorial-overlay").style.display = "block"; + } else { + document.getElementById("tutorial-overlay").style.display = "none"; + } + } + progress = this.app.getTutorialProgress(this.tutorial.link); + percent = Math.round(this.current_step / (this.tutorial.steps.length - 1) * 100); + this.app.setTutorialProgress(this.tutorial.link, percent); + if (this.current_step === this.tutorial.steps.length - 1) { + document.querySelector("#tutorial-window .navigation .next").classList.add("fa-check"); + document.querySelector("#tutorial-window .navigation .next").classList.remove("fa-arrow-right"); + } else { + document.querySelector("#tutorial-window .navigation .next").classList.remove("fa-check"); + document.querySelector("#tutorial-window .navigation .next").classList.add("fa-arrow-right"); + } + }; + + TutorialWindow.prototype.nextStep = function() { + if (this.current_step === this.tutorial.steps.length - 1) { + return this.close(); + } else { + return this.setStep(this.current_step + 1); + } + }; + + TutorialWindow.prototype.previousStep = function() { + return this.setStep(this.current_step - 1); + }; + + TutorialWindow.prototype.close = function() { + var b, button; + if (this.current_step === this.tutorial.steps.length - 1) { + this.shown = false; + if (this.tutorial.back_to_tutorials) { + this.app.appui.setMainSection("tutorials"); + } + document.getElementById("tutorial-window").style.display = "none"; + this.highlighter.hide(); + return document.getElementById("tutorial-overlay").style.display = "none"; + } else { + this.highlighter.hide(); + document.getElementById("tutorial-overlay").style.display = "none"; + this.pos_top = this.window.style.top; + this.pos_left = this.window.style.left; + this.pos_width = this.window.style.width; + this.pos_height = this.window.style.height; + button = document.getElementById("menu-tutorials"); + b = button.getBoundingClientRect(); + this.window.classList.add("minimized"); + return setTimeout(((function(_this) { + return function() { + _this.window.style.top = (b.y + b.height - 10) + "px"; + _this.window.style.left = (b.x + b.width / 2 + 20) + "px"; + _this.window.style.width = "30px"; + _this.window.style.height = "30px"; + return _this.collapsed = true; + }; + })(this)), 100); + } + }; + + TutorialWindow.prototype.uncollapse = function() { + if (this.collapsed) { + this.collapsed = false; + this.openProject(); + this.window.style.top = this.pos_top; + this.window.style.left = this.pos_left; + this.window.style.width = this.pos_width; + this.window.style.height = this.pos_height; + return setTimeout(((function(_this) { + return function() { + _this.window.classList.remove("minimized"); + return _this.setStep(_this.current_step); + }; + })(this)), 100); + } + }; + + TutorialWindow.prototype.startMove = function(event) { + this.moving = true; + this.drag_start_x = event.clientX; + this.drag_start_y = event.clientY; + this.drag_pos_x = this.window.getBoundingClientRect().x; + return this.drag_pos_y = this.window.getBoundingClientRect().y; + }; + + TutorialWindow.prototype.startResize = function(event) { + this.resizing = true; + this.drag_start_x = event.clientX; + this.drag_start_y = event.clientY; + this.drag_size_w = this.window.getBoundingClientRect().width; + return this.drag_size_h = this.window.getBoundingClientRect().height; + }; + + TutorialWindow.prototype.mouseMove = function(event) { + var b, dx, dy, h, w; + if (this.moving) { + dx = event.clientX - this.drag_start_x; + dy = event.clientY - this.drag_start_y; + this.setPosition(this.drag_pos_x + dx, this.drag_pos_y + dy); + } + if (this.resizing) { + dx = event.clientX - this.drag_start_x; + dy = event.clientY - this.drag_start_y; + w = Math.floor(Math.max(200, Math.min(window.innerWidth * this.max_ratio, this.drag_size_w + dx))); + h = Math.floor(Math.max(200, Math.min(window.innerHeight * this.max_ratio, this.drag_size_h + dy))); + this.window.style.width = w + "px"; + this.window.style.height = h + "px"; + b = this.window.getBoundingClientRect(); + if (w > window.innerWidth - b.x || h > window.innerHeight - b.y) { + return this.setPosition(Math.min(b.x, window.innerWidth - w - 4), Math.min(b.y, window.innerHeight - h - 4)); + } + } + }; + + TutorialWindow.prototype.mouseUp = function(event) { + this.moving = false; + return this.resizing = false; + }; + + TutorialWindow.prototype.setPosition = function(x, y) { + var b; + b = this.window.getBoundingClientRect(); + x = Math.max(4, Math.min(window.innerWidth - b.width - 4, x)); + y = Math.max(4, Math.min(window.innerHeight - b.height - 4, y)); + this.window.style.top = y + "px"; + return this.window.style.left = x + "px"; + }; + + return TutorialWindow; + +})(); diff --git a/static/js/user/translationapp.coffee b/static/js/user/translationapp.coffee new file mode 100644 index 00000000..84d827e2 --- /dev/null +++ b/static/js/user/translationapp.coffee @@ -0,0 +1,60 @@ +class @TranslationApp + constructor:(@app)-> + @update() + @edits = {} + @last_edit = 0 + setInterval (()=>@check()),100 + + update:()-> + for key,value of @app.user.flags + if key.startsWith("translator_") and value + @lang = key.split("_")[1] + + @app.client.sendRequest { + name: "get_translation_list" + },(msg)=> + @list = msg.list + @refresh() + + @app.client.sendRequest { + name: "get_language" + language: @lang + },(msg)=> + @language = JSON.parse msg.language + @refresh() + + refresh:()-> + if @list? and @language? + document.getElementById("translation-app-contents").innerHTML = "" + for text of @list + @add(text,@language[text]) + + return + + add:(text,translation)-> + div = document.createElement "div" + i1 = document.createElement "input" + i1.value = text + i1.readOnly = true + div.appendChild i1 + i2 = document.createElement "input" + i2.value = translation or "" + div.appendChild i2 + i2.addEventListener "input",()=> + trans = i2.value + @edits[text] = trans + @last_edit = Date.now() + + document.getElementById("translation-app-contents").appendChild div + + check:()-> + if @last_edit>0 and Date.now()>@last_edit+2000 + @last_edit = 0 + for key,value of @edits + delete @edits[key] + @app.client.sendRequest + name: "set_translation" + language: @lang + source: key + translation: value + return diff --git a/static/js/user/translationapp.js b/static/js/user/translationapp.js new file mode 100644 index 00000000..a111a268 --- /dev/null +++ b/static/js/user/translationapp.js @@ -0,0 +1,93 @@ +this.TranslationApp = (function() { + function TranslationApp(app) { + this.app = app; + this.update(); + this.edits = {}; + this.last_edit = 0; + setInterval(((function(_this) { + return function() { + return _this.check(); + }; + })(this)), 100); + } + + TranslationApp.prototype.update = function() { + var key, ref, value; + ref = this.app.user.flags; + for (key in ref) { + value = ref[key]; + if (key.startsWith("translator_") && value) { + this.lang = key.split("_")[1]; + } + } + this.app.client.sendRequest({ + name: "get_translation_list" + }, (function(_this) { + return function(msg) { + _this.list = msg.list; + return _this.refresh(); + }; + })(this)); + return this.app.client.sendRequest({ + name: "get_language", + language: this.lang + }, (function(_this) { + return function(msg) { + _this.language = JSON.parse(msg.language); + return _this.refresh(); + }; + })(this)); + }; + + TranslationApp.prototype.refresh = function() { + var text; + if ((this.list != null) && (this.language != null)) { + document.getElementById("translation-app-contents").innerHTML = ""; + for (text in this.list) { + this.add(text, this.language[text]); + } + } + }; + + TranslationApp.prototype.add = function(text, translation) { + var div, i1, i2; + div = document.createElement("div"); + i1 = document.createElement("input"); + i1.value = text; + i1.readOnly = true; + div.appendChild(i1); + i2 = document.createElement("input"); + i2.value = translation || ""; + div.appendChild(i2); + i2.addEventListener("input", (function(_this) { + return function() { + var trans; + trans = i2.value; + _this.edits[text] = trans; + return _this.last_edit = Date.now(); + }; + })(this)); + return document.getElementById("translation-app-contents").appendChild(div); + }; + + TranslationApp.prototype.check = function() { + var key, ref, value; + if (this.last_edit > 0 && Date.now() > this.last_edit + 2000) { + this.last_edit = 0; + ref = this.edits; + for (key in ref) { + value = ref[key]; + delete this.edits[key]; + this.app.client.sendRequest({ + name: "set_translation", + language: this.lang, + source: key, + translation: value + }); + } + } + }; + + return TranslationApp; + +})(); diff --git a/static/js/user/usersettings.coffee b/static/js/user/usersettings.coffee new file mode 100644 index 00000000..c4876b5e --- /dev/null +++ b/static/js/user/usersettings.coffee @@ -0,0 +1,266 @@ +class @UserSettings + constructor:(@app)-> + document.getElementById("resend-validation-email").addEventListener "click",()=>@resendValidationEMail() + #document.getElementById("change-password").addEventListener "click",()=>@changePassword() + document.getElementById("subscribe-newsletter").addEventListener "change",()=>@newsletterChange() + + document.getElementById("usersetting-email").addEventListener "input",()=>@emailChange() + + document.getElementById("open-translation-app").addEventListener "click",()=>@openTranslationApp() + document.getElementById("translation-app-back-button").addEventListener "click",()=>@closeTranslationApp() + + @nick_validator = new InputValidator document.getElementById("usersetting-nick"), + document.getElementById("usersetting-nick-button"), + document.getElementById("usersetting-nick-error"), + (value)=> + nick = value[0] + @app.client.sendRequest { + name: "change_nick" + nick: nick + },(msg)=> + if msg.name == "error" and msg.value? + @nick_validator.reset() + @nick_validator.showError(@app.translator.get(msg.value)) + else + @nick_validator.set(nick) + @app.user.nick = nick + document.getElementById("user-nick").innerText = nick + @nickUpdated() + + @nick_validator.regex = RegexLib.nick + + @email_validator = new InputValidator document.getElementById("usersetting-email"), + document.getElementById("usersetting-email-button"), + document.getElementById("usersetting-email-error"), + (value)=> + email = value[0] + @app.client.sendRequest { + name: "change_email" + email: email + },(msg)=> + if msg.name == "error" and msg.value? + @email_validator.reset() + @email_validator.showError(@app.translator.get(msg.value)) + else + @email_validator.set(email) + @app.user.email = email + + @email_validator.regex = RegexLib.email + + @sections = ["settings","profile"] #,"progress"] + @initSections() + + document.getElementById("usersettings-profile").addEventListener "dragover",(event)=> + event.preventDefault() + document.querySelector("#usersettings-profile-image .fa-user-circle").classList.add "dragover" + #console.info event + + document.getElementById("usersettings-profile").addEventListener "dragleave",(event)=> + event.preventDefault() + document.querySelector("#usersettings-profile-image .fa-user-circle").classList.remove "dragover" + #console.info event + + document.getElementById("usersettings-profile").addEventListener "drop",(event)=> + event.preventDefault() + document.querySelector("#usersettings-profile-image .fa-user-circle").classList.remove "dragover" + try + list = [] + for i in event.dataTransfer.items + list.push i.getAsFile() + + if list.length>0 + file = list[0] + @profileImageDropped(file) + catch err + console.error err + + + document.querySelector("#usersettings-profile-image").addEventListener "click",()=> + input = document.createElement "input" + input.type = "file" + input.addEventListener "change",(event)=> + files = event.target.files + if files.length>=1 + f = files[0] + @profileImageDropped f + + input.click() + + + document.querySelector("#usersettings-profile .fa-times-circle").addEventListener "click",()=> + @removeProfileImage() + + document.getElementById("usersettings-profile-description").addEventListener "input",()=> + @profileDescriptionChanged() + + initSections:()-> + for s in @sections + do (s)=> + document.getElementById("usersettings-menu-#{s}").addEventListener "click",()=> + @setSection(s) + + setSection:(section)-> + @current = section + for s in @sections + if s == section + document.getElementById("usersettings-menu-#{s}").classList.add "selected" + document.getElementById("usersettings-#{s}").style.display = "block" + else + document.getElementById("usersettings-menu-#{s}").classList.remove "selected" + document.getElementById("usersettings-#{s}").style.display = "none" + return + + update:()-> + document.getElementById("subscribe-newsletter").checked = @app.user.flags["newsletter"] == true + if @app.user.flags["validated"] == true + document.getElementById("email-not-validated").style.display = "none" + else + document.getElementById("email-not-validated").style.display = "block" + + @nick_validator.set(@app.user.nick) + @email_validator.set(@app.user.email) + + translator = false + for key,value of @app.user.flags + if key.startsWith("translator_") and value + translator = true + + document.getElementById("open-translation-app").style.display = if translator then "inline-block" else "none" + @nickUpdated() + + account_type = "Standard" + if @app.user.flags.guest + account_type = @app.translator.get "Guest" + else + account_type = @app.getTierName @app.user.flags.tier + + if @app.user.flags.tier + icon = PixelatedImage.create location.origin+"/microstudio/patreon/badges/sprites/#{@app.user.flags.tier}.png",32 + icon.style = "vertical-align: middle ; margin-right: 5px" + div = document.getElementById("usersettings-account-type") + div.innerHTML = "" + div.appendChild icon + span = document.createElement "span" + span.innerText = account_type + div.appendChild span + else + document.getElementById("usersettings-account-type").innerText = account_type + + @updateStorage() + + @updateProfileImage() + + document.getElementById("usersettings-profile-description").value = @app.user.info.description + + updateStorage:()-> + percent = Math.floor(@app.user.info.size/@app.user.info.max_storage*100) + str = @app.translator.get "[STORAGE] used of [MAX_STORAGE] ([PERCENT] %)" + str = str.replace("[STORAGE]",@app.appui.displayByteSize(@app.user.info.size)) + str = str.replace("[MAX_STORAGE]",@app.appui.displayByteSize(@app.user.info.max_storage)) + str = str.replace("[PERCENT]",percent) + + document.getElementById("usersettings-storage").innerText = str + + nickUpdated:()-> + document.getElementById("user-public-page").href = location.origin.replace(".dev",".io")+"/#{@app.user.nick}/" + document.getElementById("user-public-page").innerHTML = location.host.replace(".dev",".io")+"/#{@app.user.nick} " + + resendValidationEMail:()-> + @app.client.sendRequest { + name: "send_validation_mail" + },(msg)-> + document.getElementById("resend-validation-email").style.display = "none" + document.getElementById("validation-email-resent").style.display = "block" + + changePassword:()-> + + newsletterChange:()-> + checked = document.getElementById("subscribe-newsletter").checked + @app.user.flags.newsletter = checked + @app.client.sendRequest + name: "change_newsletter" + newsletter: checked + + nickChange:()-> + + emailChange:()-> + + openTranslationApp:()-> + document.getElementById("usersettings").style.display = "none" + document.getElementById("translation-app").style.display = "block" + + if @translation_app? + @translation_app.update() + else + @translation_app = new TranslationApp(@app) + + closeTranslationApp:()-> + document.getElementById("usersettings").style.display = "block" + document.getElementById("translation-app").style.display = "none" + + profileImageDropped:(file)-> + reader = new FileReader() + img = new Image + reader.addEventListener "load",()=> + img.src = reader.result + + reader.readAsDataURL(file) + #url = "data:application/javascript;base64,"+btoa(Audio.processor) + + img.onload = ()=> + if img.complete and img.width>0 and img.height>0 + canvas = document.createElement "canvas" + canvas.width = 128 + canvas.height = 128 + context = canvas.getContext "2d" + if img.width<128 and img.height<128 + context.imageSmoothingEnabled = false + w = img.width + h = img.height + r = Math.max(128/w,128/h) + w *= r + h *= r + context.drawImage img,64-w/2,64-h/2,w,h + document.querySelector("#usersettings-profile-image img").src = canvas.toDataURL() + document.querySelector("#usersettings-profile-image img").style.display = "block" + @app.client.sendRequest { + name: "set_user_profile" + image: canvas.toDataURL().split(",")[1] + },()=> + @app.user.flags.profile_image = true + @updateProfileImage() + + removeProfileImage:()-> + @app.client.sendRequest { + name: "set_user_profile" + image: 0 + },(msg)=> + @app.user.flags.profile_image = false + document.querySelector("#usersettings-profile-image img").style.display = "none" + document.querySelector("#usersettings-profile-image img").src = "" + @updateProfileImage() + + profileDescriptionChanged:()-> + if @description_timeout? + clearTimeout @description_timeout + + @description_timeout = setTimeout (()=>@saveProfileDescription()),2000 + + saveProfileDescription:()-> + @app.client.sendRequest { + name: "set_user_profile" + description: document.getElementById("usersettings-profile-description").value + },(msg)=> + + updateProfileImage:()-> + if @app.user.flags.profile_image + document.querySelector("#login-info img").style.display = "inline-block" + document.querySelector("#login-info img").src = "/#{@app.user.nick}.png?v=#{Date.now()}" + document.querySelector("#login-info i").style.display = "none" + document.querySelector("#usersettings-profile-image img").src = "/#{@app.user.nick}.png?v=#{Date.now()}" + document.querySelector("#usersettings-profile-image img").style.display = "block" + document.querySelector("#usersettings-profile-image .fa-times-circle").style.display = "block" + else + document.querySelector("#login-info img").style.display = "none" + document.querySelector("#login-info i").style.display = "inline-block" + document.querySelector("#usersettings-profile-image .fa-times-circle").style.display = "none" diff --git a/static/js/user/usersettings.js b/static/js/user/usersettings.js new file mode 100644 index 00000000..9b7b47c3 --- /dev/null +++ b/static/js/user/usersettings.js @@ -0,0 +1,356 @@ +this.UserSettings = (function() { + function UserSettings(app) { + this.app = app; + document.getElementById("resend-validation-email").addEventListener("click", (function(_this) { + return function() { + return _this.resendValidationEMail(); + }; + })(this)); + document.getElementById("subscribe-newsletter").addEventListener("change", (function(_this) { + return function() { + return _this.newsletterChange(); + }; + })(this)); + document.getElementById("usersetting-email").addEventListener("input", (function(_this) { + return function() { + return _this.emailChange(); + }; + })(this)); + document.getElementById("open-translation-app").addEventListener("click", (function(_this) { + return function() { + return _this.openTranslationApp(); + }; + })(this)); + document.getElementById("translation-app-back-button").addEventListener("click", (function(_this) { + return function() { + return _this.closeTranslationApp(); + }; + })(this)); + this.nick_validator = new InputValidator(document.getElementById("usersetting-nick"), document.getElementById("usersetting-nick-button"), document.getElementById("usersetting-nick-error"), (function(_this) { + return function(value) { + var nick; + nick = value[0]; + return _this.app.client.sendRequest({ + name: "change_nick", + nick: nick + }, function(msg) { + if (msg.name === "error" && (msg.value != null)) { + _this.nick_validator.reset(); + return _this.nick_validator.showError(_this.app.translator.get(msg.value)); + } else { + _this.nick_validator.set(nick); + _this.app.user.nick = nick; + document.getElementById("user-nick").innerText = nick; + return _this.nickUpdated(); + } + }); + }; + })(this)); + this.nick_validator.regex = RegexLib.nick; + this.email_validator = new InputValidator(document.getElementById("usersetting-email"), document.getElementById("usersetting-email-button"), document.getElementById("usersetting-email-error"), (function(_this) { + return function(value) { + var email; + email = value[0]; + return _this.app.client.sendRequest({ + name: "change_email", + email: email + }, function(msg) { + if (msg.name === "error" && (msg.value != null)) { + _this.email_validator.reset(); + return _this.email_validator.showError(_this.app.translator.get(msg.value)); + } else { + _this.email_validator.set(email); + return _this.app.user.email = email; + } + }); + }; + })(this)); + this.email_validator.regex = RegexLib.email; + this.sections = ["settings", "profile"]; + this.initSections(); + document.getElementById("usersettings-profile").addEventListener("dragover", (function(_this) { + return function(event) { + event.preventDefault(); + return document.querySelector("#usersettings-profile-image .fa-user-circle").classList.add("dragover"); + }; + })(this)); + document.getElementById("usersettings-profile").addEventListener("dragleave", (function(_this) { + return function(event) { + event.preventDefault(); + return document.querySelector("#usersettings-profile-image .fa-user-circle").classList.remove("dragover"); + }; + })(this)); + document.getElementById("usersettings-profile").addEventListener("drop", (function(_this) { + return function(event) { + var err, file, i, j, len, list, ref; + event.preventDefault(); + document.querySelector("#usersettings-profile-image .fa-user-circle").classList.remove("dragover"); + try { + list = []; + ref = event.dataTransfer.items; + for (j = 0, len = ref.length; j < len; j++) { + i = ref[j]; + list.push(i.getAsFile()); + } + if (list.length > 0) { + file = list[0]; + return _this.profileImageDropped(file); + } + } catch (error) { + err = error; + return console.error(err); + } + }; + })(this)); + document.querySelector("#usersettings-profile-image").addEventListener("click", (function(_this) { + return function() { + var input; + input = document.createElement("input"); + input.type = "file"; + input.addEventListener("change", function(event) { + var f, files; + files = event.target.files; + if (files.length >= 1) { + f = files[0]; + return _this.profileImageDropped(f); + } + }); + return input.click(); + }; + })(this)); + document.querySelector("#usersettings-profile .fa-times-circle").addEventListener("click", (function(_this) { + return function() { + return _this.removeProfileImage(); + }; + })(this)); + document.getElementById("usersettings-profile-description").addEventListener("input", (function(_this) { + return function() { + return _this.profileDescriptionChanged(); + }; + })(this)); + } + + UserSettings.prototype.initSections = function() { + var j, len, ref, results, s; + ref = this.sections; + results = []; + for (j = 0, len = ref.length; j < len; j++) { + s = ref[j]; + results.push((function(_this) { + return function(s) { + return document.getElementById("usersettings-menu-" + s).addEventListener("click", function() { + return _this.setSection(s); + }); + }; + })(this)(s)); + } + return results; + }; + + UserSettings.prototype.setSection = function(section) { + var j, len, ref, s; + this.current = section; + ref = this.sections; + for (j = 0, len = ref.length; j < len; j++) { + s = ref[j]; + if (s === section) { + document.getElementById("usersettings-menu-" + s).classList.add("selected"); + document.getElementById("usersettings-" + s).style.display = "block"; + } else { + document.getElementById("usersettings-menu-" + s).classList.remove("selected"); + document.getElementById("usersettings-" + s).style.display = "none"; + } + } + }; + + UserSettings.prototype.update = function() { + var account_type, div, icon, key, ref, span, translator, value; + document.getElementById("subscribe-newsletter").checked = this.app.user.flags["newsletter"] === true; + if (this.app.user.flags["validated"] === true) { + document.getElementById("email-not-validated").style.display = "none"; + } else { + document.getElementById("email-not-validated").style.display = "block"; + } + this.nick_validator.set(this.app.user.nick); + this.email_validator.set(this.app.user.email); + translator = false; + ref = this.app.user.flags; + for (key in ref) { + value = ref[key]; + if (key.startsWith("translator_") && value) { + translator = true; + } + } + document.getElementById("open-translation-app").style.display = translator ? "inline-block" : "none"; + this.nickUpdated(); + account_type = "Standard"; + if (this.app.user.flags.guest) { + account_type = this.app.translator.get("Guest"); + } else { + account_type = this.app.getTierName(this.app.user.flags.tier); + } + if (this.app.user.flags.tier) { + icon = PixelatedImage.create(location.origin + ("/microstudio/patreon/badges/sprites/" + this.app.user.flags.tier + ".png"), 32); + icon.style = "vertical-align: middle ; margin-right: 5px"; + div = document.getElementById("usersettings-account-type"); + div.innerHTML = ""; + div.appendChild(icon); + span = document.createElement("span"); + span.innerText = account_type; + div.appendChild(span); + } else { + document.getElementById("usersettings-account-type").innerText = account_type; + } + this.updateStorage(); + this.updateProfileImage(); + return document.getElementById("usersettings-profile-description").value = this.app.user.info.description; + }; + + UserSettings.prototype.updateStorage = function() { + var percent, str; + percent = Math.floor(this.app.user.info.size / this.app.user.info.max_storage * 100); + str = this.app.translator.get("[STORAGE] used of [MAX_STORAGE] ([PERCENT] %)"); + str = str.replace("[STORAGE]", this.app.appui.displayByteSize(this.app.user.info.size)); + str = str.replace("[MAX_STORAGE]", this.app.appui.displayByteSize(this.app.user.info.max_storage)); + str = str.replace("[PERCENT]", percent); + return document.getElementById("usersettings-storage").innerText = str; + }; + + UserSettings.prototype.nickUpdated = function() { + document.getElementById("user-public-page").href = location.origin.replace(".dev", ".io") + ("/" + this.app.user.nick + "/"); + return document.getElementById("user-public-page").innerHTML = location.host.replace(".dev", ".io") + ("/" + this.app.user.nick + " "); + }; + + UserSettings.prototype.resendValidationEMail = function() { + return this.app.client.sendRequest({ + name: "send_validation_mail" + }, function(msg) { + document.getElementById("resend-validation-email").style.display = "none"; + return document.getElementById("validation-email-resent").style.display = "block"; + }); + }; + + UserSettings.prototype.changePassword = function() {}; + + UserSettings.prototype.newsletterChange = function() { + var checked; + checked = document.getElementById("subscribe-newsletter").checked; + this.app.user.flags.newsletter = checked; + return this.app.client.sendRequest({ + name: "change_newsletter", + newsletter: checked + }); + }; + + UserSettings.prototype.nickChange = function() {}; + + UserSettings.prototype.emailChange = function() {}; + + UserSettings.prototype.openTranslationApp = function() { + document.getElementById("usersettings").style.display = "none"; + document.getElementById("translation-app").style.display = "block"; + if (this.translation_app != null) { + return this.translation_app.update(); + } else { + return this.translation_app = new TranslationApp(this.app); + } + }; + + UserSettings.prototype.closeTranslationApp = function() { + document.getElementById("usersettings").style.display = "block"; + return document.getElementById("translation-app").style.display = "none"; + }; + + UserSettings.prototype.profileImageDropped = function(file) { + var img, reader; + reader = new FileReader(); + img = new Image; + reader.addEventListener("load", (function(_this) { + return function() { + return img.src = reader.result; + }; + })(this)); + reader.readAsDataURL(file); + return img.onload = (function(_this) { + return function() { + var canvas, context, h, r, w; + if (img.complete && img.width > 0 && img.height > 0) { + canvas = document.createElement("canvas"); + canvas.width = 128; + canvas.height = 128; + context = canvas.getContext("2d"); + if (img.width < 128 && img.height < 128) { + context.imageSmoothingEnabled = false; + } + w = img.width; + h = img.height; + r = Math.max(128 / w, 128 / h); + w *= r; + h *= r; + context.drawImage(img, 64 - w / 2, 64 - h / 2, w, h); + document.querySelector("#usersettings-profile-image img").src = canvas.toDataURL(); + document.querySelector("#usersettings-profile-image img").style.display = "block"; + return _this.app.client.sendRequest({ + name: "set_user_profile", + image: canvas.toDataURL().split(",")[1] + }, function() { + _this.app.user.flags.profile_image = true; + return _this.updateProfileImage(); + }); + } + }; + })(this); + }; + + UserSettings.prototype.removeProfileImage = function() { + return this.app.client.sendRequest({ + name: "set_user_profile", + image: 0 + }, (function(_this) { + return function(msg) { + _this.app.user.flags.profile_image = false; + document.querySelector("#usersettings-profile-image img").style.display = "none"; + document.querySelector("#usersettings-profile-image img").src = ""; + return _this.updateProfileImage(); + }; + })(this)); + }; + + UserSettings.prototype.profileDescriptionChanged = function() { + if (this.description_timeout != null) { + clearTimeout(this.description_timeout); + } + return this.description_timeout = setTimeout(((function(_this) { + return function() { + return _this.saveProfileDescription(); + }; + })(this)), 2000); + }; + + UserSettings.prototype.saveProfileDescription = function() { + return this.app.client.sendRequest({ + name: "set_user_profile", + description: document.getElementById("usersettings-profile-description").value + }, (function(_this) { + return function(msg) {}; + })(this)); + }; + + UserSettings.prototype.updateProfileImage = function() { + if (this.app.user.flags.profile_image) { + document.querySelector("#login-info img").style.display = "inline-block"; + document.querySelector("#login-info img").src = "/" + this.app.user.nick + ".png?v=" + (Date.now()); + document.querySelector("#login-info i").style.display = "none"; + document.querySelector("#usersettings-profile-image img").src = "/" + this.app.user.nick + ".png?v=" + (Date.now()); + document.querySelector("#usersettings-profile-image img").style.display = "block"; + return document.querySelector("#usersettings-profile-image .fa-times-circle").style.display = "block"; + } else { + document.querySelector("#login-info img").style.display = "none"; + document.querySelector("#login-info i").style.display = "inline-block"; + return document.querySelector("#usersettings-profile-image .fa-times-circle").style.display = "none"; + } + }; + + return UserSettings; + +})(); diff --git a/static/js/util/canvas2d.coffee b/static/js/util/canvas2d.coffee new file mode 100644 index 00000000..433bc86a --- /dev/null +++ b/static/js/util/canvas2d.coffee @@ -0,0 +1,18 @@ +CanvasRenderingContext2D.prototype.roundRect = (x, y, w, h, r)-> + r = w / 2 if (w < 2 * r) + r = h / 2 if (h < 2 * r) + @beginPath() + @moveTo(x+r, y) + @arcTo(x+w, y, x+w, y+h, r) + @arcTo(x+w, y+h, x, y+h, r) + @arcTo(x, y+h, x, y, r) + @arcTo(x, y, x+w, y, r) + @closePath() + +CanvasRenderingContext2D.prototype.fillRoundRect = (x, y, w, h, r)-> + @roundRect x,y,w,h,r + @fill() + +CanvasRenderingContext2D.prototype.strokeRoundRect = (x, y, w, h, r)-> + @roundRect x,y,w,h,r + @stroke() diff --git a/static/js/util/canvas2d.js b/static/js/util/canvas2d.js new file mode 100644 index 00000000..e2ef8243 --- /dev/null +++ b/static/js/util/canvas2d.js @@ -0,0 +1,25 @@ +CanvasRenderingContext2D.prototype.roundRect = function(x, y, w, h, r) { + if (w < 2 * r) { + r = w / 2; + } + if (h < 2 * r) { + r = h / 2; + } + this.beginPath(); + this.moveTo(x + r, y); + this.arcTo(x + w, y, x + w, y + h, r); + this.arcTo(x + w, y + h, x, y + h, r); + this.arcTo(x, y + h, x, y, r); + this.arcTo(x, y, x + w, y, r); + return this.closePath(); +}; + +CanvasRenderingContext2D.prototype.fillRoundRect = function(x, y, w, h, r) { + this.roundRect(x, y, w, h, r); + return this.fill(); +}; + +CanvasRenderingContext2D.prototype.strokeRoundRect = function(x, y, w, h, r) { + this.roundRect(x, y, w, h, r); + return this.stroke(); +}; diff --git a/static/js/util/inputvalidator.coffee b/static/js/util/inputvalidator.coffee new file mode 100644 index 00000000..b04ea997 --- /dev/null +++ b/static/js/util/inputvalidator.coffee @@ -0,0 +1,86 @@ +class @InputValidator + constructor:(@fields,@button,@error,@callback)-> + @fields = [@fields] if not Array.isArray @fields + + @initial = [] + for f in @fields + @initial.push f.value + f.addEventListener "input",()=> @change() + f.addEventListener "keydown",(event)=> + if event.key == "Enter" + @validate() + else if event.key == "Escape" and @auto_reset + @reset() + + @button.addEventListener "click",()=> + @validate() + + @button.style.width = 0 + @error.style.width = 0 if @error? + + @error_timeout = null + @change_timeout = null + @accept_initial = false + @auto_reset = true + + set:(values)-> + values = [values] if not Array.isArray values + @initial = [] + for f,i in @fields + @initial.push f.value = values[i] + return + + reset:()-> + for f,i in @fields + f.value = @initial[i] + f.blur() + @button.style.width = "0px" + + update:()-> + for f,i in @fields + @initial[i] = f.value + @button.style.width = "0px" + + check:()-> + return true if not @regex? + for f in @fields + return false if not @regex.test(f.value) + true + + change:()-> + @error.style.width = 0 if @error? + change = @accept_initial + for f,i in @fields + if f.value != @initial[i] + change = true + + if change and @check() + @button.style.removeProperty("width") + clearTimeout @change_timeout if @change_timeout? + if @auto_reset + @change_timeout = setTimeout (()=> + @reset() + @change_timeout = null + ),10000 + else + @button.style.width = "0px" + + cancelChange:()-> + @button.style.width = 0 + + showError:(text)-> + return if not @error? + @error.innerText = text + @error.style.width = "auto" + + if @error_timeout + clearTimeout @error_timeout + @error_timeout = setTimeout (()=> + @error.style.width = "0" + @error_timeout = null + ),5000 + + validate:()-> + clearTimeout @change_timeout if @change_timeout? + @callback(f.value for f in @fields) + @button.style.width = "0px" diff --git a/static/js/util/inputvalidator.js b/static/js/util/inputvalidator.js new file mode 100644 index 00000000..9bf530d7 --- /dev/null +++ b/static/js/util/inputvalidator.js @@ -0,0 +1,167 @@ +this.InputValidator = (function() { + function InputValidator(fields, button, error, callback) { + var f, j, len, ref; + this.fields = fields; + this.button = button; + this.error = error; + this.callback = callback; + if (!Array.isArray(this.fields)) { + this.fields = [this.fields]; + } + this.initial = []; + ref = this.fields; + for (j = 0, len = ref.length; j < len; j++) { + f = ref[j]; + this.initial.push(f.value); + f.addEventListener("input", (function(_this) { + return function() { + return _this.change(); + }; + })(this)); + f.addEventListener("keydown", (function(_this) { + return function(event) { + if (event.key === "Enter") { + return _this.validate(); + } else if (event.key === "Escape" && _this.auto_reset) { + return _this.reset(); + } + }; + })(this)); + } + this.button.addEventListener("click", (function(_this) { + return function() { + return _this.validate(); + }; + })(this)); + this.button.style.width = 0; + if (this.error != null) { + this.error.style.width = 0; + } + this.error_timeout = null; + this.change_timeout = null; + this.accept_initial = false; + this.auto_reset = true; + } + + InputValidator.prototype.set = function(values) { + var f, i, j, len, ref; + if (!Array.isArray(values)) { + values = [values]; + } + this.initial = []; + ref = this.fields; + for (i = j = 0, len = ref.length; j < len; i = ++j) { + f = ref[i]; + this.initial.push(f.value = values[i]); + } + }; + + InputValidator.prototype.reset = function() { + var f, i, j, len, ref; + ref = this.fields; + for (i = j = 0, len = ref.length; j < len; i = ++j) { + f = ref[i]; + f.value = this.initial[i]; + f.blur(); + } + return this.button.style.width = "0px"; + }; + + InputValidator.prototype.update = function() { + var f, i, j, len, ref; + ref = this.fields; + for (i = j = 0, len = ref.length; j < len; i = ++j) { + f = ref[i]; + this.initial[i] = f.value; + } + return this.button.style.width = "0px"; + }; + + InputValidator.prototype.check = function() { + var f, j, len, ref; + if (this.regex == null) { + return true; + } + ref = this.fields; + for (j = 0, len = ref.length; j < len; j++) { + f = ref[j]; + if (!this.regex.test(f.value)) { + return false; + } + } + return true; + }; + + InputValidator.prototype.change = function() { + var change, f, i, j, len, ref; + if (this.error != null) { + this.error.style.width = 0; + } + change = this.accept_initial; + ref = this.fields; + for (i = j = 0, len = ref.length; j < len; i = ++j) { + f = ref[i]; + if (f.value !== this.initial[i]) { + change = true; + } + } + if (change && this.check()) { + this.button.style.removeProperty("width"); + if (this.change_timeout != null) { + clearTimeout(this.change_timeout); + } + if (this.auto_reset) { + return this.change_timeout = setTimeout(((function(_this) { + return function() { + _this.reset(); + return _this.change_timeout = null; + }; + })(this)), 10000); + } + } else { + return this.button.style.width = "0px"; + } + }; + + InputValidator.prototype.cancelChange = function() { + return this.button.style.width = 0; + }; + + InputValidator.prototype.showError = function(text) { + if (this.error == null) { + return; + } + this.error.innerText = text; + this.error.style.width = "auto"; + if (this.error_timeout) { + clearTimeout(this.error_timeout); + } + return this.error_timeout = setTimeout(((function(_this) { + return function() { + _this.error.style.width = "0"; + return _this.error_timeout = null; + }; + })(this)), 5000); + }; + + InputValidator.prototype.validate = function() { + var f; + if (this.change_timeout != null) { + clearTimeout(this.change_timeout); + } + this.callback((function() { + var j, len, ref, results; + ref = this.fields; + results = []; + for (j = 0, len = ref.length; j < len; j++) { + f = ref[j]; + results.push(f.value); + } + return results; + }).call(this)); + return this.button.style.width = "0px"; + }; + + return InputValidator; + +})(); diff --git a/static/js/util/pixelartscaler.coffee b/static/js/util/pixelartscaler.coffee new file mode 100644 index 00000000..ceaac8a9 --- /dev/null +++ b/static/js/util/pixelartscaler.coffee @@ -0,0 +1,148 @@ +class @PixelArtScaler + constructor:()-> + + rescale:(canvas,width,height)-> + if width>canvas.width or height>canvas.height + @rescale @triplePix(canvas),width,height + else + c = document.createElement "canvas" + c.width = width + c.height = height + context = c.getContext("2d") + context.drawImage canvas,0,0,width,height + c + + distance:(data,i1,i2)-> + return 0 if i1*4>data.length or i2*4>data.length or i1<0 or i2<0 + dx = Math.abs(data[i1*4]-data[i2*4]) + dy = Math.abs(data[i1*4+1]-data[i2*4+1]) + dz = Math.abs(data[i1*4+2]-data[i2*4+2]) + dw = Math.abs(data[i1*4+3]-data[i2*4+3]) + Math.max(dx,dy,dz,dw) + + inter:(data,i1,i2,res,inter)-> + data[res*4] = data[i1*4]*(1-inter)+data[i2*4]*inter + data[res*4+1] = data[i1*4+1]*(1-inter)+data[i2*4+1]*inter + data[res*4+2] = data[i1*4+2]*(1-inter)+data[i2*4+2]*inter + data[res*4+3] = data[i1*4+3]*(1-inter)+data[i2*4+3]*inter + + inter4:(data,i1,i2,i3,i4,res,a,b)-> + data[res*4] = (1-b)*(data[i1*4]*(1-a)+data[i2*4]*a)+b*(data[i3*4]*(1-a)+data[i4*4]*a) + data[res*4+1] = (1-b)*(data[i1*4+1]*(1-a)+data[i2*4+1]*a)+b*(data[i3*4+1]*(1-a)+data[i4*4+1]*a) + data[res*4+2] = (1-b)*(data[i1*4+2]*(1-a)+data[i2*4+2]*a)+b*(data[i3*4+2]*(1-a)+data[i4*4+2]*a) + data[res*4+3] = (1-b)*(data[i1*4+3]*(1-a)+data[i2*4+3]*a)+b*(data[i3*4+3]*(1-a)+data[i4*4+3]*a) + + tripleSmoothing:(canvas)-> + context = canvas.getContext("2d") + data = context.getImageData(0,0,canvas.width,canvas.height) + + threshold = 64 + + #gradient pass + for i in [1..canvas.width-4] by 3 + for j in [1..canvas.height-4] by 3 + i1 = i+j*canvas.width + i2 = (i+3)+j*canvas.width + i3 = i+(j+3)*canvas.width + i4 = (i+3)+(j+3)*canvas.width + d = @distance(data.data,i1,i2)+@distance(data.data,i3,i4)+@distance(data.data,i1,i3)+@distance(data.data,i2,i4) + + if d=threshold and diag1 + @inter data.data,i1,i2,(i+2)+(j+1)*canvas.width,.5 + @inter data.data,i1,i2,(i+1)+(j+2)*canvas.width,.5 + @inter data.data,i1,i2,(i+1)+(j+1)*canvas.width,1/3 + @inter data.data,i1,i2,(i+2)+(j+2)*canvas.width,2/3 + if @distance(data.data,i1,i1-3*canvas.width)=threshold and diag2 + @inter data.data,i3,i4,(i+1)+(j+1)*canvas.width,.5 + @inter data.data,i3,i4,(i+2)+(j+2)*canvas.width,.5 + @inter data.data,i3,i4,(i+2)+(j+1)*canvas.width,1/3 + @inter data.data,i3,i4,(i+1)+(j+2)*canvas.width,2/3 + if @distance(data.data,i3,i3-3*canvas.width) + c = document.createElement "canvas" + c.width = (canvas.width+2)*3 + c.height = (canvas.height+2)*3 + context = c.getContext("2d") + context.imageSmoothingEnabled = false + context.drawImage canvas,3,3,c.width-6,c.height-6 + @tripleSmoothing(c) + canvas = document.createElement "canvas" + canvas.width = c.width-6 + canvas.height = c.height-6 + context = canvas.getContext("2d") + context.drawImage c,-3,-3 + canvas diff --git a/static/js/util/pixelartscaler.js b/static/js/util/pixelartscaler.js new file mode 100644 index 00000000..6fd9bc55 --- /dev/null +++ b/static/js/util/pixelartscaler.js @@ -0,0 +1,173 @@ +this.PixelArtScaler = (function() { + function PixelArtScaler() {} + + PixelArtScaler.prototype.rescale = function(canvas, width, height) { + var c, context; + if (width > canvas.width || height > canvas.height) { + return this.rescale(this.triplePix(canvas), width, height); + } else { + c = document.createElement("canvas"); + c.width = width; + c.height = height; + context = c.getContext("2d"); + context.drawImage(canvas, 0, 0, width, height); + return c; + } + }; + + PixelArtScaler.prototype.distance = function(data, i1, i2) { + var dw, dx, dy, dz; + if (i1 * 4 > data.length || i2 * 4 > data.length || i1 < 0 || i2 < 0) { + return 0; + } + dx = Math.abs(data[i1 * 4] - data[i2 * 4]); + dy = Math.abs(data[i1 * 4 + 1] - data[i2 * 4 + 1]); + dz = Math.abs(data[i1 * 4 + 2] - data[i2 * 4 + 2]); + dw = Math.abs(data[i1 * 4 + 3] - data[i2 * 4 + 3]); + return Math.max(dx, dy, dz, dw); + }; + + PixelArtScaler.prototype.inter = function(data, i1, i2, res, inter) { + data[res * 4] = data[i1 * 4] * (1 - inter) + data[i2 * 4] * inter; + data[res * 4 + 1] = data[i1 * 4 + 1] * (1 - inter) + data[i2 * 4 + 1] * inter; + data[res * 4 + 2] = data[i1 * 4 + 2] * (1 - inter) + data[i2 * 4 + 2] * inter; + return data[res * 4 + 3] = data[i1 * 4 + 3] * (1 - inter) + data[i2 * 4 + 3] * inter; + }; + + PixelArtScaler.prototype.inter4 = function(data, i1, i2, i3, i4, res, a, b) { + data[res * 4] = (1 - b) * (data[i1 * 4] * (1 - a) + data[i2 * 4] * a) + b * (data[i3 * 4] * (1 - a) + data[i4 * 4] * a); + data[res * 4 + 1] = (1 - b) * (data[i1 * 4 + 1] * (1 - a) + data[i2 * 4 + 1] * a) + b * (data[i3 * 4 + 1] * (1 - a) + data[i4 * 4 + 1] * a); + data[res * 4 + 2] = (1 - b) * (data[i1 * 4 + 2] * (1 - a) + data[i2 * 4 + 2] * a) + b * (data[i3 * 4 + 2] * (1 - a) + data[i4 * 4 + 2] * a); + return data[res * 4 + 3] = (1 - b) * (data[i1 * 4 + 3] * (1 - a) + data[i2 * 4 + 3] * a) + b * (data[i3 * 4 + 3] * (1 - a) + data[i4 * 4 + 3] * a); + }; + + PixelArtScaler.prototype.tripleSmoothing = function(canvas) { + var context, d, d1, d2, data, dd1, dd2, diag1, diag2, i, i1, i2, i3, i4, j, k, l, m, n, o, p, q, r, ref, ref1, ref2, ref3, threshold; + context = canvas.getContext("2d"); + data = context.getImageData(0, 0, canvas.width, canvas.height); + threshold = 64; + for (i = m = 1, ref = canvas.width - 4; m <= ref; i = m += 3) { + for (j = n = 1, ref1 = canvas.height - 4; n <= ref1; j = n += 3) { + i1 = i + j * canvas.width; + i2 = (i + 3) + j * canvas.width; + i3 = i + (j + 3) * canvas.width; + i4 = (i + 3) + (j + 3) * canvas.width; + d = this.distance(data.data, i1, i2) + this.distance(data.data, i3, i4) + this.distance(data.data, i1, i3) + this.distance(data.data, i2, i4); + if (d < threshold * 2) { + for (k = o = 0; o <= 3; k = o += 1) { + for (l = p = 0; p <= 3; l = p += 1) { + this.inter4(data.data, i1, i2, i3, i4, (i + k) + (j + l) * canvas.width, k / 3, l / 3); + } + } + } + } + } + for (i = q = 1, ref2 = canvas.width - 4; q <= ref2; i = q += 3) { + for (j = r = 1, ref3 = canvas.height - 4; r <= ref3; j = r += 3) { + i1 = i + j * canvas.width; + i2 = (i + 3) + (j + 3) * canvas.width; + i3 = i + 3 + j * canvas.width; + i4 = i + (j + 3) * canvas.width; + d1 = this.distance(data.data, i1, i2); + d2 = this.distance(data.data, i3, i4); + diag1 = !((i === canvas.width - 6 + 1 && j === 1) || (i === 1 && j === canvas.height - 6 + 1)); + diag2 = !((i === 1 && j === 1) || (i === canvas.width - 6 + 1 && j === canvas.height - 6 + 1)); + if (d1 < threshold && d2 >= threshold && diag1) { + this.inter(data.data, i1, i2, (i + 2) + (j + 1) * canvas.width, .5); + this.inter(data.data, i1, i2, (i + 1) + (j + 2) * canvas.width, .5); + this.inter(data.data, i1, i2, (i + 1) + (j + 1) * canvas.width, 1 / 3); + this.inter(data.data, i1, i2, (i + 2) + (j + 2) * canvas.width, 2 / 3); + if (this.distance(data.data, i1, i1 - 3 * canvas.width) < threshold && this.distance(data.data, i3, i3 - 3 * canvas.width) < threshold) { + this.inter(data.data, i1, i2, (i + 2) + j * canvas.width, .5); + } + if (this.distance(data.data, i2, i2 + 3 * canvas.width) < threshold && this.distance(data.data, i4, i4 + 3 * canvas.width) < threshold) { + this.inter(data.data, i1, i2, (i + 1) + (j + 3) * canvas.width, .5); + } + if (this.distance(data.data, i1, i1 - 3) < threshold && this.distance(data.data, i4, i4 - 3) < threshold) { + this.inter(data.data, i1, i2, i + (j + 2) * canvas.width, .5); + } + if (this.distance(data.data, i2, i2 + 3) < threshold && this.distance(data.data, i3, i3 + 3) < threshold) { + this.inter(data.data, i1, i2, (i + 3) + (j + 1) * canvas.width, .5); + } + } else if (d2 < threshold && d1 >= threshold && diag2) { + this.inter(data.data, i3, i4, (i + 1) + (j + 1) * canvas.width, .5); + this.inter(data.data, i3, i4, (i + 2) + (j + 2) * canvas.width, .5); + this.inter(data.data, i3, i4, (i + 2) + (j + 1) * canvas.width, 1 / 3); + this.inter(data.data, i3, i4, (i + 1) + (j + 2) * canvas.width, 2 / 3); + if (this.distance(data.data, i3, i3 - 3 * canvas.width) < threshold && this.distance(data.data, i1, i1 - 3 * canvas.width) < threshold) { + this.inter(data.data, i3, i4, i3 - 2, .5); + } + if (this.distance(data.data, i4, i4 + 3 * canvas.width) < threshold && this.distance(data.data, i2, i2 + 3 * canvas.width) < threshold) { + this.inter(data.data, i3, i4, i4 + 2, .5); + } + if (this.distance(data.data, i3, i3 + 3) < threshold && this.distance(data.data, i2, i2 + 3) < threshold) { + this.inter(data.data, i3, i4, i3 + 2 * canvas.width, .5); + } + if (this.distance(data.data, i4, i4 - 3) < threshold && this.distance(data.data, i1, i1 - 3) < threshold) { + this.inter(data.data, i3, i4, i4 - 2 * canvas.width, .5); + } + } else if (d1 < threshold && d2 < threshold) { + dd1 = this.distance(data.data, i1, (i - 3) + j * canvas.width) + this.distance(data.data, i1, i + (j - 3) * canvas.width) + this.distance(data.data, i1, i + 6 + (j + 3) * canvas.width) + this.distance(data.data, i1, i + 3 + (j + 6) * canvas.width); + dd2 = this.distance(data.data, i3, i - 3 + (j + 3) * canvas.width) + this.distance(data.data, i3, i + (j + 6) * canvas.width) + this.distance(data.data, i3, i + 3 + (j - 3) * canvas.width) + this.distance(data.data, i3, i + 6 + j * canvas.width); + if (dd2 < dd1 && diag1) { + this.inter(data.data, i1, i2, (i + 2) + (j + 1) * canvas.width, .5); + this.inter(data.data, i1, i2, (i + 1) + (j + 2) * canvas.width, .5); + this.inter(data.data, i1, i2, (i + 1) + (j + 1) * canvas.width, 1 / 3); + this.inter(data.data, i1, i2, (i + 2) + (j + 2) * canvas.width, 2 / 3); + if (this.distance(data.data, i1, i1 - 3 * canvas.width) < threshold && this.distance(data.data, i3, i3 - 3 * canvas.width) < threshold) { + this.inter(data.data, i1, i2, (i + 2) + j * canvas.width, .5); + } + if (this.distance(data.data, i2, i2 + 3 * canvas.width) < threshold && this.distance(data.data, i4, i4 + 3 * canvas.width) < threshold) { + this.inter(data.data, i1, i2, (i + 1) + (j + 3) * canvas.width, .5); + } + if (this.distance(data.data, i1, i1 - 3) < threshold && this.distance(data.data, i4, i4 - 3) < threshold) { + this.inter(data.data, i1, i2, i + (j + 2) * canvas.width, .5); + } + if (this.distance(data.data, i2, i2 + 3) < threshold && this.distance(data.data, i3, i3 + 3) < threshold) { + this.inter(data.data, i1, i2, (i + 3) + (j + 1) * canvas.width, .5); + } + } else if (dd1 < dd2 && diag2) { + this.inter(data.data, i3, i4, (i + 1) + (j + 1) * canvas.width, .5); + this.inter(data.data, i3, i4, (i + 2) + (j + 2) * canvas.width, .5); + this.inter(data.data, i3, i4, (i + 2) + (j + 1) * canvas.width, 1 / 3); + this.inter(data.data, i3, i4, (i + 1) + (j + 2) * canvas.width, 2 / 3); + if (this.distance(data.data, i3, i3 - 3 * canvas.width) < threshold && this.distance(data.data, i1, i1 - 3 * canvas.width) < threshold) { + this.inter(data.data, i3, i4, i3 - 2, .5); + } + if (this.distance(data.data, i4, i4 + 3 * canvas.width) < threshold && this.distance(data.data, i2, i2 + 3 * canvas.width) < threshold) { + this.inter(data.data, i3, i4, i4 + 2, .5); + } + if (this.distance(data.data, i3, i3 + 3) < threshold && this.distance(data.data, i2, i2 + 3) < threshold) { + this.inter(data.data, i3, i4, i3 + 2 * canvas.width, .5); + } + if (this.distance(data.data, i4, i4 - 3) < threshold && this.distance(data.data, i1, i1 - 3) < threshold) { + this.inter(data.data, i3, i4, i4 - 2 * canvas.width, .5); + } + } + } + } + } + context.putImageData(data, 0, 0); + return canvas; + }; + + PixelArtScaler.prototype.triplePix = function(canvas) { + var c, context; + c = document.createElement("canvas"); + c.width = (canvas.width + 2) * 3; + c.height = (canvas.height + 2) * 3; + context = c.getContext("2d"); + context.imageSmoothingEnabled = false; + context.drawImage(canvas, 3, 3, c.width - 6, c.height - 6); + this.tripleSmoothing(c); + canvas = document.createElement("canvas"); + canvas.width = c.width - 6; + canvas.height = c.height - 6; + context = canvas.getContext("2d"); + context.drawImage(c, -3, -3); + return canvas; + }; + + return PixelArtScaler; + +})(); diff --git a/static/js/util/pixelatedimage.coffee b/static/js/util/pixelatedimage.coffee new file mode 100644 index 00000000..c1e44cbe --- /dev/null +++ b/static/js/util/pixelatedimage.coffee @@ -0,0 +1,47 @@ +class @PixelatedImage + @create:(source,size)-> + img = new Image + img.crossOrigin = "Anonymous" + + sourceimg = new Image + sourceimg.crossOrigin = "Anonymous" + sourceimg.src = source + sourceimg.onload = ()-> + return if sourceimg.width<=0 or sourceimg.height<=0 + canvas = document.createElement "canvas" + canvas.width = size + canvas.height = size + context = canvas.getContext "2d" + context.imageSmoothingEnabled = false + ratio = Math.min(size/sourceimg.width,size/sourceimg.height) + w = sourceimg.width*ratio + h = sourceimg.height*ratio + context.drawImage sourceimg,size/2-w/2,size/2-h/2,w,h + img.src = canvas.toDataURL() + + return img + + @setURL:(img,source,size)-> + img.crossOrigin = "Anonymous" + + sourceimg = new Image + sourceimg.crossOrigin = "Anonymous" + sourceimg.src = source + sourceimg.onload = ()-> + return if sourceimg.width<=0 or sourceimg.height<=0 + if img instanceof HTMLCanvasElement + canvas = img + else + canvas = document.createElement "canvas" + + canvas.width = size + canvas.height = size + context = canvas.getContext "2d" + context.clearRect(0,0,size,size) + context.imageSmoothingEnabled = false + ratio = Math.min(size/sourceimg.width,size/sourceimg.height) + w = sourceimg.width*ratio + h = sourceimg.height*ratio + context.drawImage sourceimg,size/2-w/2,size/2-h/2,w,h + if img not instanceof HTMLCanvasElement + img.src = canvas.toDataURL() diff --git a/static/js/util/pixelatedimage.js b/static/js/util/pixelatedimage.js new file mode 100644 index 00000000..6a73debf --- /dev/null +++ b/static/js/util/pixelatedimage.js @@ -0,0 +1,63 @@ +this.PixelatedImage = (function() { + function PixelatedImage() {} + + PixelatedImage.create = function(source, size) { + var img, sourceimg; + img = new Image; + img.crossOrigin = "Anonymous"; + sourceimg = new Image; + sourceimg.crossOrigin = "Anonymous"; + sourceimg.src = source; + sourceimg.onload = function() { + var canvas, context, h, ratio, w; + if (sourceimg.width <= 0 || sourceimg.height <= 0) { + return; + } + canvas = document.createElement("canvas"); + canvas.width = size; + canvas.height = size; + context = canvas.getContext("2d"); + context.imageSmoothingEnabled = false; + ratio = Math.min(size / sourceimg.width, size / sourceimg.height); + w = sourceimg.width * ratio; + h = sourceimg.height * ratio; + context.drawImage(sourceimg, size / 2 - w / 2, size / 2 - h / 2, w, h); + return img.src = canvas.toDataURL(); + }; + return img; + }; + + PixelatedImage.setURL = function(img, source, size) { + var sourceimg; + img.crossOrigin = "Anonymous"; + sourceimg = new Image; + sourceimg.crossOrigin = "Anonymous"; + sourceimg.src = source; + return sourceimg.onload = function() { + var canvas, context, h, ratio, w; + if (sourceimg.width <= 0 || sourceimg.height <= 0) { + return; + } + if (img instanceof HTMLCanvasElement) { + canvas = img; + } else { + canvas = document.createElement("canvas"); + } + canvas.width = size; + canvas.height = size; + context = canvas.getContext("2d"); + context.clearRect(0, 0, size, size); + context.imageSmoothingEnabled = false; + ratio = Math.min(size / sourceimg.width, size / sourceimg.height); + w = sourceimg.width * ratio; + h = sourceimg.height * ratio; + context.drawImage(sourceimg, size / 2 - w / 2, size / 2 - h / 2, w, h); + if (!(img instanceof HTMLCanvasElement)) { + return img.src = canvas.toDataURL(); + } + }; + }; + + return PixelatedImage; + +})(); diff --git a/static/js/util/random.coffee b/static/js/util/random.coffee new file mode 100644 index 00000000..78c8815d --- /dev/null +++ b/static/js/util/random.coffee @@ -0,0 +1,27 @@ +class Random + constructor:(@seed=Math.random())-> + @seed *= 1 << 30 if @seed<1 + @a = 13971 + @b = 12345 + @size = 1 << 30 + @mask = @size-1 + @norm = 1/@size + @nextSeed() + @nextSeed() + @nextSeed() + + next:()-> + @seed = (@seed*@a+@b) & @mask + @seed*@norm + + nextInt:(num)-> + Math.floor(@next()*num) + + nextSeed:()-> + @seed = (@seed*@a+@b) & @mask + + setSeed:(@seed)-> + @seed *= 1 << 30 if @seed<1 + @nextSeed() + @nextSeed() + @nextSeed() diff --git a/static/js/util/random.js b/static/js/util/random.js new file mode 100644 index 00000000..9195964f --- /dev/null +++ b/static/js/util/random.js @@ -0,0 +1,44 @@ +var Random; + +Random = (function() { + function Random(seed) { + this.seed = seed != null ? seed : Math.random(); + if (this.seed < 1) { + this.seed *= 1 << 30; + } + this.a = 13971; + this.b = 12345; + this.size = 1 << 30; + this.mask = this.size - 1; + this.norm = 1 / this.size; + this.nextSeed(); + this.nextSeed(); + this.nextSeed(); + } + + Random.prototype.next = function() { + this.seed = (this.seed * this.a + this.b) & this.mask; + return this.seed * this.norm; + }; + + Random.prototype.nextInt = function(num) { + return Math.floor(this.next() * num); + }; + + Random.prototype.nextSeed = function() { + return this.seed = (this.seed * this.a + this.b) & this.mask; + }; + + Random.prototype.setSeed = function(seed) { + this.seed = seed; + if (this.seed < 1) { + this.seed *= 1 << 30; + } + this.nextSeed(); + this.nextSeed(); + return this.nextSeed(); + }; + + return Random; + +})(); diff --git a/static/js/util/regexlib.coffee b/static/js/util/regexlib.coffee new file mode 100644 index 00000000..a6e7edeb --- /dev/null +++ b/static/js/util/regexlib.coffee @@ -0,0 +1,19 @@ +@RegexLib = + email: /^([a-zA-Z0-9_\-\.]+)@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.)|(([a-zA-Z0-9\-]+\.)+))([a-zA-Z]{2,4}|[0-9]{1,3})(\]?)$/ + nick: /^[a-zA-Z0-9_]{5,30}$/ + filename: /^[a-z0-9_]{1,30}$/ + slug: /^[a-zA-Z0-9_]{1,30}$/ + csscolor:/^(#[0-9A-Fa-f]{3,6})|(hsl\(\d+,\d+%,\d+%\))|(rgb\(\d+,\d+,\d+\))$/ + + slugify: (text)-> + text.normalize('NFD').replace(/[^a-zA-Z0-9_]/g, "").toLowerCase() + + fixFilename:(text)-> + text.normalize('NFD').replace(/[^a-zA-Z0-9_]/g, "").toLowerCase() + + fixNick:(text)-> + text.normalize('NFD').replace(/[^a-zA-Z0-9_]/g, "") + + +if module? + module.exports = @RegexLib diff --git a/static/js/util/regexlib.js b/static/js/util/regexlib.js new file mode 100644 index 00000000..5517e75a --- /dev/null +++ b/static/js/util/regexlib.js @@ -0,0 +1,20 @@ +this.RegexLib = { + email: /^([a-zA-Z0-9_\-\.]+)@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.)|(([a-zA-Z0-9\-]+\.)+))([a-zA-Z]{2,4}|[0-9]{1,3})(\]?)$/, + nick: /^[a-zA-Z0-9_]{5,30}$/, + filename: /^[a-z0-9_]{1,30}$/, + slug: /^[a-zA-Z0-9_]{1,30}$/, + csscolor: /^(#[0-9A-Fa-f]{3,6})|(hsl\(\d+,\d+%,\d+%\))|(rgb\(\d+,\d+,\d+\))$/, + slugify: function(text) { + return text.normalize('NFD').replace(/[^a-zA-Z0-9_]/g, "").toLowerCase(); + }, + fixFilename: function(text) { + return text.normalize('NFD').replace(/[^a-zA-Z0-9_]/g, "").toLowerCase(); + }, + fixNick: function(text) { + return text.normalize('NFD').replace(/[^a-zA-Z0-9_]/g, ""); + } +}; + +if (typeof module !== "undefined" && module !== null) { + module.exports = this.RegexLib; +} diff --git a/static/js/util/splitbar.coffee b/static/js/util/splitbar.coffee new file mode 100644 index 00000000..3dfaa5a7 --- /dev/null +++ b/static/js/util/splitbar.coffee @@ -0,0 +1,72 @@ +class @SplitBar + constructor:(id,@type="horizontal")-> + @element = document.getElementById(id) + @side1 = @element.childNodes[0] + @splitbar = @element.childNodes[1] + @side2 = @element.childNodes[2] + @position = 50 + + @splitbar.addEventListener "mousedown", (event)=> + @dragging = true + @drag_start_x = event.clientX + @drag_start_y = event.clientY + @drag_position = @position + list = document.getElementsByTagName("iframe") + for e in list + e.classList.add "ignoreMouseEvents" + return + + document.addEventListener "mousemove", (event)=> + if @dragging + switch @type + when "horizontal" + dx = (event.clientX-@drag_start_x)/(@element.clientWidth-@splitbar.clientWidth)*100 + ns = Math.round(Math.max(0,Math.min(100,@drag_position+dx))) + if ns != @position + @position = ns + window.dispatchEvent(new Event('resize')) + else + dy = (event.clientY-@drag_start_y)/(@element.clientHeight-@splitbar.clientHeight)*100 + ns = Math.round(Math.max(0,Math.min(100,@drag_position+dy))) + if ns != @position + @position = ns + window.dispatchEvent(new Event('resize')) + + document.addEventListener "mouseup", (event)=> + @dragging = false + list = document.getElementsByTagName("iframe") + for e in list + e.classList.remove "ignoreMouseEvents" + return + + window.addEventListener "resize",(event)=> + @update() + + @update() + + setPosition:(@position)-> + @update() + + update:()-> + return if @element.clientWidth == 0 or @element.clientHeight == 0 + switch @type + when "horizontal" + @total_width = w = @element.clientWidth-@splitbar.clientWidth + + w1 = Math.round(@position/100*w) + w2 = w1+@splitbar.clientWidth + w3 = @element.clientWidth-w2 + + @side1.style.width = w1+"px" + @splitbar.style.left = w1+"px" + @side2.style.width = w3+"px" + else + @total_height = h = @element.clientHeight-@splitbar.clientHeight + + h1 = Math.round(@position/100*h) + h2 = h1+@splitbar.clientHeight + h3 = @element.clientHeight-h2 + + @side1.style.height = h1+"px" + @splitbar.style.top = h1+"px" + @side2.style.height = h3+"px" diff --git a/static/js/util/splitbar.js b/static/js/util/splitbar.js new file mode 100644 index 00000000..ddce9952 --- /dev/null +++ b/static/js/util/splitbar.js @@ -0,0 +1,98 @@ +this.SplitBar = (function() { + function SplitBar(id, type) { + this.type = type != null ? type : "horizontal"; + this.element = document.getElementById(id); + this.side1 = this.element.childNodes[0]; + this.splitbar = this.element.childNodes[1]; + this.side2 = this.element.childNodes[2]; + this.position = 50; + this.splitbar.addEventListener("mousedown", (function(_this) { + return function(event) { + var e, i, len, list; + _this.dragging = true; + _this.drag_start_x = event.clientX; + _this.drag_start_y = event.clientY; + _this.drag_position = _this.position; + list = document.getElementsByTagName("iframe"); + for (i = 0, len = list.length; i < len; i++) { + e = list[i]; + e.classList.add("ignoreMouseEvents"); + } + }; + })(this)); + document.addEventListener("mousemove", (function(_this) { + return function(event) { + var dx, dy, ns; + if (_this.dragging) { + switch (_this.type) { + case "horizontal": + dx = (event.clientX - _this.drag_start_x) / (_this.element.clientWidth - _this.splitbar.clientWidth) * 100; + ns = Math.round(Math.max(0, Math.min(100, _this.drag_position + dx))); + if (ns !== _this.position) { + _this.position = ns; + return window.dispatchEvent(new Event('resize')); + } + break; + default: + dy = (event.clientY - _this.drag_start_y) / (_this.element.clientHeight - _this.splitbar.clientHeight) * 100; + ns = Math.round(Math.max(0, Math.min(100, _this.drag_position + dy))); + if (ns !== _this.position) { + _this.position = ns; + return window.dispatchEvent(new Event('resize')); + } + } + } + }; + })(this)); + document.addEventListener("mouseup", (function(_this) { + return function(event) { + var e, i, len, list; + _this.dragging = false; + list = document.getElementsByTagName("iframe"); + for (i = 0, len = list.length; i < len; i++) { + e = list[i]; + e.classList.remove("ignoreMouseEvents"); + } + }; + })(this)); + window.addEventListener("resize", (function(_this) { + return function(event) { + return _this.update(); + }; + })(this)); + this.update(); + } + + SplitBar.prototype.setPosition = function(position) { + this.position = position; + return this.update(); + }; + + SplitBar.prototype.update = function() { + var h, h1, h2, h3, w, w1, w2, w3; + if (this.element.clientWidth === 0 || this.element.clientHeight === 0) { + return; + } + switch (this.type) { + case "horizontal": + this.total_width = w = this.element.clientWidth - this.splitbar.clientWidth; + w1 = Math.round(this.position / 100 * w); + w2 = w1 + this.splitbar.clientWidth; + w3 = this.element.clientWidth - w2; + this.side1.style.width = w1 + "px"; + this.splitbar.style.left = w1 + "px"; + return this.side2.style.width = w3 + "px"; + default: + this.total_height = h = this.element.clientHeight - this.splitbar.clientHeight; + h1 = Math.round(this.position / 100 * h); + h2 = h1 + this.splitbar.clientHeight; + h3 = this.element.clientHeight - h2; + this.side1.style.height = h1 + "px"; + this.splitbar.style.top = h1 + "px"; + return this.side2.style.height = h3 + "px"; + } + }; + + return SplitBar; + +})(); diff --git a/static/js/util/translator.coffee b/static/js/util/translator.coffee new file mode 100644 index 00000000..1a714641 --- /dev/null +++ b/static/js/util/translator.coffee @@ -0,0 +1,58 @@ +class @Translator + constructor:(@app)-> + @lang = document.children[0].lang + @incomplete = {} + if document.cookie? and document.cookie.indexOf("language=")>=0 + index = document.cookie.indexOf("language=")+"language=".length + @lang = document.cookie.substring(index,index+2) + #else if navigator.languages? and navigator.languages[0]? + # @lang = navigator.languages[0].split("-")[0] + + setInterval (()=>@check()),5000 + + load:(callback)-> + return if @language? + @app.client.sendRequest { + name: "get_language" + language: @lang + },(msg)=> + try + @language = JSON.parse msg.language + catch err + callback() if callback? + + get:(text)-> + if @language? + value = @language[text] + if not value? + @incomplete[text] = true + text + else + value + else + text + + check:()-> + if @app.user? and @app.user.flags? and @app.user.flags.admin + if not @list_fetched + @list_fetched = true + @app.client.sendRequest { + name: "get_translation_list" + },(msg)=> + @list = msg.list + + if @list? + for text of @incomplete + if not @list[text]? + @app.client.sendRequest + name: "add_translation" + source: text + @list[text] = true + return + + translatorLanguage:()-> + translator = false + for key,value of @app.user.flags + if key.startsWith("translator_") and value + return key.split("-")[1] + return null diff --git a/static/js/util/translator.js b/static/js/util/translator.js new file mode 100644 index 00000000..5ed14380 --- /dev/null +++ b/static/js/util/translator.js @@ -0,0 +1,97 @@ +this.Translator = (function() { + function Translator(app) { + var index; + this.app = app; + this.lang = document.children[0].lang; + this.incomplete = {}; + if ((document.cookie != null) && document.cookie.indexOf("language=") >= 0) { + index = document.cookie.indexOf("language=") + "language=".length; + this.lang = document.cookie.substring(index, index + 2); + } + setInterval(((function(_this) { + return function() { + return _this.check(); + }; + })(this)), 5000); + } + + Translator.prototype.load = function(callback) { + if (this.language != null) { + return; + } + return this.app.client.sendRequest({ + name: "get_language", + language: this.lang + }, (function(_this) { + return function(msg) { + var err; + try { + _this.language = JSON.parse(msg.language); + } catch (error) { + err = error; + } + if (callback != null) { + return callback(); + } + }; + })(this)); + }; + + Translator.prototype.get = function(text) { + var value; + if (this.language != null) { + value = this.language[text]; + if (value == null) { + this.incomplete[text] = true; + return text; + } else { + return value; + } + } else { + return text; + } + }; + + Translator.prototype.check = function() { + var text; + if ((this.app.user != null) && (this.app.user.flags != null) && this.app.user.flags.admin) { + if (!this.list_fetched) { + this.list_fetched = true; + this.app.client.sendRequest({ + name: "get_translation_list" + }, (function(_this) { + return function(msg) { + return _this.list = msg.list; + }; + })(this)); + } + if (this.list != null) { + for (text in this.incomplete) { + if (this.list[text] == null) { + this.app.client.sendRequest({ + name: "add_translation", + source: text + }); + this.list[text] = true; + } + } + } + } + }; + + Translator.prototype.translatorLanguage = function() { + var key, ref, translator, value; + translator = false; + ref = this.app.user.flags; + for (key in ref) { + value = ref[key]; + if (key.startsWith("translator_") && value) { + return key.split("-")[1]; + } + } + return null; + }; + + return Translator; + +})(); diff --git a/static/js/util/undo.coffee b/static/js/util/undo.coffee new file mode 100644 index 00000000..c5bb1371 --- /dev/null +++ b/static/js/util/undo.coffee @@ -0,0 +1,32 @@ +class @Undo + constructor:(@listener)-> + @states = [] + @next_state = 0 + @max = 30 + + pushState:(state)-> + if @next_state>=@max + @states.splice 0,1 + @next_state -= 1 + + @states[@next_state++] = state + while @states.length>@next_state + @states.splice(@states.length-1,1) + state + + empty:()-> + @states.length == 0 + + undo:()-> + if @next_state-2>=0 and @next_state-2<@states.length + @next_state -= 1 + @states[@next_state-1] + else + null + + redo:()-> + if @next_state>=0 and @next_state<@states.length + @next_state += 1 + @states[@next_state-1] + else + null diff --git a/static/js/util/undo.js b/static/js/util/undo.js new file mode 100644 index 00000000..a8a50242 --- /dev/null +++ b/static/js/util/undo.js @@ -0,0 +1,45 @@ +this.Undo = (function() { + function Undo(listener) { + this.listener = listener; + this.states = []; + this.next_state = 0; + this.max = 30; + } + + Undo.prototype.pushState = function(state) { + if (this.next_state >= this.max) { + this.states.splice(0, 1); + this.next_state -= 1; + } + this.states[this.next_state++] = state; + while (this.states.length > this.next_state) { + this.states.splice(this.states.length - 1, 1); + } + return state; + }; + + Undo.prototype.empty = function() { + return this.states.length === 0; + }; + + Undo.prototype.undo = function() { + if (this.next_state - 2 >= 0 && this.next_state - 2 < this.states.length) { + this.next_state -= 1; + return this.states[this.next_state - 1]; + } else { + return null; + } + }; + + Undo.prototype.redo = function() { + if (this.next_state >= 0 && this.next_state < this.states.length) { + this.next_state += 1; + return this.states[this.next_state - 1]; + } else { + return null; + } + }; + + return Undo; + +})(); diff --git a/static/lib/mode-microscript.js b/static/lib/mode-microscript.js new file mode 100644 index 00000000..1f64293d --- /dev/null +++ b/static/lib/mode-microscript.js @@ -0,0 +1,243 @@ +define("ace/mode/microscript_highlight_rules", ["require", "exports", "module", "ace/lib/oop", "ace/mode/text_highlight_rules"], function(e, t, n) { + "use strict"; + var r = e("../lib/oop"), + i = e("./text_highlight_rules").TextHighlightRules, + s = function() { + var e = "continue|break|else|elsif|end|for|by|function|if|in|to|local|return|then|while|or|and|not|object|class|extends|new|this|super", + t = "true|false", + n = "print|time|type|log|max|PI|pow|random|ceil|round|floor|abs|sqrt|min|exp|sin|atan|concat|sort|cos|sin|tan|acos|asin|atan|atan2|sind|cosd|tand|acosd|asind|atand|atan2d", + r = "screen|system|audio|gamepad|keyboard|touch|mouse", + i = "setn|foreach|foreachi|gcinfo|log10|maxn", + s = this.createKeywordMapper({ + keyword: e, + "support.function": n, + "keyword.deprecated": i, + "constant.library": r, + "constant.language": t, + "variable.language": "this" + }, "identifier"), + o = "(?:(?:[1-9]\\d*)|(?:0))", + u = "(?:0[xX][\\dA-Fa-f]+)", + a = "(?:" + o + "|" + u + ")", + f = "(?:\\.\\d+)", + l = "(?:\\d+)", + c = "(?:(?:" + l + "?" + f + ")|(?:" + l + "\\.))", + h = "(?:" + c + ")"; + this.$rules = { + start: [{ + stateName: "bracketedComment", + onMatch: function(e, t, n) { + return n.unshift(this.next, e.length - 2, t), "comment" + }, + regex: /\-\-\[=*\[/, + next: [{ + onMatch: function(e, t, n) { + return e.length == n[1] ? (n.shift(), n.shift(), this.next = n.shift()) : this.next = "", "comment" + }, + regex: /\]=*\]/, + next: "start" + }, { + defaultToken: "comment" + }] + }, { + token: "comment", + regex: "\\/\\/.*$" + }, { + stateName: "bracketedString", + onMatch: function(e, t, n) { + return n.unshift(this.next, e.length, t), "string.start" + }, + regex: /\[=*\[/, + next: [{ + onMatch: function(e, t, n) { + return e.length == n[1] ? (n.shift(), n.shift(), this.next = n.shift()) : this.next = "", "string.end" + }, + regex: /\]=*\]/, + next: "start" + }, { + defaultToken: "string" + }] + }, { + token: "string", + regex: '"(?:[^\\\\]|\\\\.)*?"' + }, { + token: "string", + regex: "'(?:[^\\\\]|\\\\.)*?'" + }, { + token: "constant.numeric", + regex: h + }, { + token: "constant.numeric", + regex: a + "\\b" + }, { + token: s, + regex: "[a-zA-Z_$][a-zA-Z0-9_$]*\\b" + }, { + token: "keyword.operator", + regex: "\\+|\\-|\\*|\\/|%|\\^|<|>|<=|=>|==|=" + }, { + token: "paren.lparen", + regex: "[\\[\\(\\{]" + }, { + token: "paren.rparen", + regex: "[\\]\\)\\}]" + }, { + token: "text", + regex: "\\s+|\\w+" + }] + }, this.normalizeRules() + }; + r.inherits(s, i), t.MicroscriptHighlightRules = s +}), define("ace/mode/folding/microscript", ["require", "exports", "module", "ace/lib/oop", "ace/mode/folding/fold_mode", "ace/range", "ace/token_iterator"], function(e, t, n) { + "use strict"; + var r = e("../../lib/oop"), + i = e("./fold_mode").FoldMode, + s = e("../../range").Range, + o = e("../../token_iterator").TokenIterator, + u = t.FoldMode = function() {}; + r.inherits(u, i), + function() { + this.foldingStartMarker = /\b(function|then|while|for|repeat)\b|{\s*$|(\[=*\[)/, this.foldingStopMarker = /\bend\b|^\s*}|\]=*\]/, this.getFoldWidget = function(e, t, n) { + var r = e.getLine(n), + i = this.foldingStartMarker.test(r), + s = this.foldingStopMarker.test(r); + if (i && !s) { + var o = r.match(this.foldingStartMarker); + if (o[1] == "then" && /\belseif\b/.test(r)) return; + if (o[1]) { + if (e.getTokenAt(n, o.index + 1).type === "keyword") return "start" + } else { + if (!o[2]) return "start"; + var u = e.bgTokenizer.getState(n) || ""; + if (u[0] == "bracketedComment" || u[0] == "bracketedString") return "start" + } + } + if (t != "markbeginend" || !s || i && s) return ""; + var o = r.match(this.foldingStopMarker); + if (o[0] === "end") { + if (e.getTokenAt(n, o.index + 1).type === "keyword") return "end" + } else { + if (o[0][0] !== "]") return "end"; + var u = e.bgTokenizer.getState(n - 1) || ""; + if (u[0] == "bracketedComment" || u[0] == "bracketedString") return "end" + } + }, this.getFoldWidgetRange = function(e, t, n) { + var r = e.doc.getLine(n), + i = this.foldingStartMarker.exec(r); + if (i) return i[1] ? this.microscriptBlock(e, n, i.index + 1) : i[2] ? e.getCommentFoldRange(n, i.index + 1) : this.openingBracketBlock(e, "{", n, i.index); + var i = this.foldingStopMarker.exec(r); + if (i) return i[0] === "end" && e.getTokenAt(n, i.index + 1).type === "keyword" ? this.microscriptBlock(e, n, i.index + 1) : i[0][0] === "]" ? e.getCommentFoldRange(n, i.index + 1) : this.closingBracketBlock(e, "}", n, i.index + i[0].length) + }, this.microscriptBlock = function(e, t, n, r) { + var i = new o(e, t, n), + u = { + "function": 1, + then: 1, + elsif: -1, + end: -1, + "while": 1, + "for": 1, + "object": 1 + }, + a = i.getCurrentToken(); + if (!a || a.type != "keyword") return; + var f = a.value, + l = [f], + c = u[f]; + if (!c) return; + var h = c === -1 ? i.getCurrentTokenColumn() : e.getLine(t).length, + p = t; + i.step = c === -1 ? i.stepBackward : i.stepForward; + while (a = i.step()) { + if (a.type !== "keyword") continue; + var d = c * u[a.value]; + if (d > 0) l.unshift(a.value); + else if (d <= 0) { + l.shift(); + if (!l.length && a.value != "elsif") break; + d === 0 && l.unshift(a.value) + } + } + if (!a) return null; + if (r) return i.getCurrentTokenRange(); + var t = i.getCurrentTokenRow(); + return c === -1 ? new s(t, e.getLine(t).length, p, h) : new s(p, h, t, i.getCurrentTokenColumn()) + } + }.call(u.prototype) +}), define("ace/mode/microscript", ["require", "exports", "module", "ace/lib/oop", "ace/mode/text", "ace/mode/microscript_highlight_rules", "ace/mode/folding/microscript", "ace/range", "ace/worker/worker_client"], function(e, t, n) { + "use strict"; + var r = e("../lib/oop"), + i = e("./text").Mode, + s = e("./microscript_highlight_rules").MicroscriptHighlightRules, + o = e("./folding/microscript").FoldMode, + u = e("../range").Range, + a = e("../worker/worker_client").WorkerClient, + f = function() { + this.HighlightRules = s, this.foldingRules = new o, this.$behaviour = this.$defaultBehaviour + }; + r.inherits(f, i), + function() { + function n(t) { + var n = 0; + for (var r = 0; r < t.length; r++) { + var i = t[r]; + i.type == "keyword" ? i.value in e && (n += e[i.value]) : i.type == "paren.lparen" ? n += i.value.length : i.type == "paren.rparen" && (n -= i.value.length) + } + return n < 0 ? -1 : n > 0 ? 1 : 0 + } + this.lineCommentStart = "//", this.blockComment = { + start: "//", + end: "\n" + }; + var e = { + "function": 1, + "then": 1, + "else": 1, + "elsif": 1, + "while": 1, + "end": -1, + "for": 1 + }, + t = ["else", "elsif", "end"]; + this.getNextLineIndent = function(e, t, r) { + var i = this.$getIndent(t), + s = 0, + o = this.getTokenizer().getLineTokens(t, e), + u = o.tokens; + return e == "start" && (s = n(u)), s > 0 ? i + r : s < 0 && i.substr(i.length - r.length) == r && !this.checkOutdent(e, t, "\n") ? i.substr(0, i.length - r.length) : i + }, this.checkOutdent = function(e, n, r) { + if (r != "\n" && r != "\r" && r != "\r\n") return !1; + if (n.match(/^\s*[\)\}\]]$/)) return !0; + var i = this.getTokenizer().getLineTokens(n.trim(), e).tokens; + return !i || !i.length ? !1 : i[0].type == "keyword" && t.indexOf(i[0].value) != -1 + }, this.getMatching = function(t, n, r) { + if (n == undefined) { + var i = t.selection.lead; + r = i.column, n = i.row + } + var s = t.getTokenAt(n, r); + if (s && s.value in e) return this.foldingRules.microscriptBlock(t, n, r, !0) + }, this.autoOutdent = function(e, t, n) { + var r = t.getLine(n), + i = r.match(/^\s*/)[0].length; + if (!i || !n) return; + var s = this.getMatching(t, n, i + 1); + if (!s || s.start.row == n) return; + var o = this.$getIndent(t.getLine(s.start.row)); + o.length != i && (t.replace(new u(n, 0, n, i), o), t.outdentRows(new u(n + 1, 0, n + 1, 0))) + }, this.createWorker = function(e) { + var t = new a(["ace"], "ace/mode/microscript_worker", "Worker"); + return t.attachToDocument(e.getDocument()), t.on("annotate", function(t) { + e.setAnnotations(t.data) + }), t.on("terminate", function() { + e.clearAnnotations() + }), t + }, this.$id = "ace/mode/microscript" + }.call(f.prototype), t.Mode = f +}); +(function() { + window.require(["ace/mode/microscript"], function(m) { + if (typeof module == "object" && typeof exports == "object" && module) { + module.exports = m; + } + }); +})(); diff --git a/static/manifest.json b/static/manifest.json new file mode 100644 index 00000000..900db0bf --- /dev/null +++ b/static/manifest.json @@ -0,0 +1,20 @@ +{ + "name": "microStudio", + "short_name": "microStudio", + "start_url": "/", + "icons": [ + { + "src": "/img/favicon192.png", + "sizes": "192x192", + "type": "image/png" + }, + { + "src": "/img/favicon512.png", + "sizes": "512x512", + "type": "image/png" + } + ], + "theme_color": "hsl(200,50%,20%)", + "background_color": "hsl(200,40%,10%)", + "display": "standalone" +} diff --git a/static/robots.txt b/static/robots.txt new file mode 100644 index 00000000..f5e06b4a --- /dev/null +++ b/static/robots.txt @@ -0,0 +1 @@ +Sitemap: https://microstudio.dev/sitemap.xml diff --git a/static/sitemap.xml b/static/sitemap.xml new file mode 100644 index 00000000..2854c037 --- /dev/null +++ b/static/sitemap.xml @@ -0,0 +1,45 @@ + + + + + https://microstudio.dev/ + + + + + + + + https://microstudio.dev/fr/ + + + + + + + + https://microstudio.dev/de/ + + + + + + + + https://microstudio.dev/pl/ + + + + + + + + https://microstudio.dev/community/ + + + + https://microstudio.dev/fr/community/ + + + diff --git a/static/sw.js b/static/sw.js new file mode 100644 index 00000000..f6e4f550 --- /dev/null +++ b/static/sw.js @@ -0,0 +1,45 @@ +var VERSION = 'v2'; + +var cacheFiles = [ +/* "https://cdnjs.cloudflare.com/ajax/libs/engine.io-client/3.2.1/engine.io.min.js", + + self.registration.scope, + self.registration.scope+'js/util/canvas2d.js', + self.registration.scope+'js/runtime/runtime.js', + self.registration.scope+'js/runtime/screen.js', + self.registration.scope+'js/runtime/sprite.js', + self.registration.scope+'js/runtime/audio/audio.js', + self.registration.scope+'js/runtime/audio/beeper.js', + self.registration.scope+'js/play/player.js', + self.registration.scope+'js/play/playerclient.js', + self.registration.scope+'sw.js'*/ +]; + +self.addEventListener('install', event => { + event.waitUntil( + caches.open(VERSION).then(cache => { + return cache.addAll(cacheFiles); + }) + ); +}); + +self.addEventListener('fetch', function(event) { + //console.info(event.request); + if (event.request.method != "GET" || event.request.url.indexOf("/engine.io/")>0) + { + return event.respondWith(fetch(event.request)) ; + } + + /* cache then network with caching */ + event.respondWith( + caches.open(VERSION).then(function(cache) { + return cache.match(event.request).then(function(response) { + var fetchPromise = fetch(event.request).then(function(networkResponse) { + cache.put(event.request, networkResponse.clone()); + return networkResponse; + }) + return response || fetchPromise; + }) + }) + ); +}); diff --git a/templates/404.pug b/templates/404.pug new file mode 100644 index 00000000..e743929f --- /dev/null +++ b/templates/404.pug @@ -0,0 +1,13 @@ +doctype html +html + head + title microStudio + link(rel="stylesheet" type="text/css" href="/css/404.css") + + body + div + img(src="/img/microstudiologo.svg" alt="microStudio") + br + div.e404 404 + div Got lost in the code? + a.button(href="/") Back to microStudio! diff --git a/templates/about.pug b/templates/about.pug new file mode 100644 index 00000000..41110b78 --- /dev/null +++ b/templates/about.pug @@ -0,0 +1,13 @@ +div.about + div#about-menu + div.selected#about-menu-about #{translator.get("About")} + br + div#about-menu-changelog #{translator.get("Changelog")} + br + div#about-menu-terms #{translator.get("Terms of use")} + br + div#about-menu-privacy #{translator.get("Privacy")} + div#about-content + + //div.about-button-div + // div#about-button #{translator.get("Let's go!")} diff --git a/templates/assets.pug b/templates/assets.pug new file mode 100644 index 00000000..74f1c918 --- /dev/null +++ b/templates/assets.pug @@ -0,0 +1,15 @@ +div.create-asset-button#create-asset-button #{translator.get("Add Asset")} + +div.assetlist#assetlist + div.asset-list#asset-list + +div.assetinfo#assetinfo + input(type="text" value="")#asset-name + div.validate-button-container#asset-name-button + div.validate-button #{translator.get("Apply")} + + div.buttons + div#delete-asset.delete(title=translator.get("Delete Asset")) + i.fa.fa-trash + +div.asseteditor#asset-viewer diff --git a/templates/code.pug b/templates/code.pug new file mode 100644 index 00000000..69c9fc63 --- /dev/null +++ b/templates/code.pug @@ -0,0 +1,84 @@ +div.code-editor#code-editor + div.code-toolbar#code-toolbar Source Code + div.source-list-panel#source-list-panel + div.source-list-header#source-list-header + div #{translator.get("Files")} + i(class="fa fa-chevron-circle-right") + div.source-list#source-list + div.create-source-button#create-source-button #{translator.get("Add File")} + div.editor-locked#editor-locked + div.editor-view#editor-view + div.help-window#help-window + div.left-side + i(class="fa fa-question") + div.content + + div.code-tools + div.buttons + div#code-font-minus(title="Decrease font size") A + div#code-font-plus(title="Increase font size") A + div#code-search.selected(title="Search in code") + i.fas.fa-search + + div.value-tool-button#color-value-tool-button + span CTRL + i.fa.fa-palette + + div.value-tool-button#number-value-tool-button + span CTRL + div.slider-track + div.slider-cursor + +div.code-splitbar#code-splitbar + +div.runtime-container#runtime-container + div.runtime#runtime + div.runbar#runbar + span Run + div.buttons + div#run-button(title="Run project") + i.fas.fa-play + div#pause-button.selected(title="Pause execution") + i.fas.fa-pause + div#reload-button.selected(title="Restart project") + i.fa.fa-redo + div#detach-button(title="Detach run window") + i.fas.fa-window-restore + + a#run-link(target="_blank") + + div.devicecontainer + div#device + + div#ruler + canvas#ruler-canvas + + div.runtime-splitbar#runtime-splitbar + span Console + div.buttons + div#clear-button(title="Clear console") + i.fa.fa-trash-alt + div#console-options-button(title="Console options") + i.fa.fa-cogs + div#console-options + input#console-options-warning-undefined(type="checkbox") + label(for="console-options-warning-undefined") #{translator.get("Display warning when using an undefined variable")} + br + input#console-options-warning-nonfunction(type="checkbox") + label(for="console-options-warning-nonfunction") #{translator.get("Display warning when calling something that isn't a function")} + br + input#console-options-warning-assign(type="checkbox") + label(for="console-options-warning-assign") #{translator.get("Display warning when setting property on an undefined variable")} + br + input#console-options-transpiler(type="checkbox") + label(for="console-options-transpiler") #{translator.get("Use transpiler [Caution: alpha version, may not work]")} + + div.runtime-terminal#runtime-terminal + div#terminal + div#terminal-view + div#terminal-lines + div#terminal-input-line + div#terminal-input-gt + i.fa.fa-angle-right + div#terminal-input-container + input#terminal-input(autocomplete="off" name="terminalinput") diff --git a/templates/console/console.pug b/templates/console/console.pug new file mode 100644 index 00000000..67cca040 --- /dev/null +++ b/templates/console/console.pug @@ -0,0 +1,38 @@ +doctype html +html + head + link(rel="stylesheet" type="text/css" href="/css/console.css") + + link(rel="stylesheet" href="/lib/fontlib/fontawesome/css/all.css") + + link(rel="stylesheet" href="/lib/fontlib/ubuntu/index.css") + link(rel="stylesheet" href="/lib/fontlib/ubuntu-mono/index.css") + link(rel="stylesheet" href="/lib/fontlib/source-sans-pro/index.css") + link(rel="stylesheet" href="/lib/fontlib/source-sans-pro/700.css") + + link(rel="icon" type="image/png" href="/img/favicon.png") + + script(src="/js/console/console.js") + + title microStudio BOX + body(oncontextmenu="return false;") + div.intro + + div.content + div.filters + ul + li.selected.focus My Games + li Hot + li New + li Top + + i.poweroff.fa.fa-power-off + + div.list#list + + div.footer + img(src="/img/microstudiologoblack.svg") + span BOX + + script. + var gamelist = !{JSON.stringify(gamelist)} ; diff --git a/templates/doc.pug b/templates/doc.pug new file mode 100644 index 00000000..dec731d5 --- /dev/null +++ b/templates/doc.pug @@ -0,0 +1,7 @@ +div.doc-editor#doc-editor + +div.horizontal-splitbar#doc-splitbar + +div.doc-view#doc-view + div.doc-render#doc-render + div#doceditor-start-tutorial #{translator.get("Start Tutorial")} diff --git a/templates/export/html.pug b/templates/export/html.pug new file mode 100644 index 00000000..29d81cfc --- /dev/null +++ b/templates/export/html.pug @@ -0,0 +1,59 @@ +doctype html +html + head + title #{game.title} + meta(http-equiv='content-type', content='text/html; charset=UTF-8') + meta(name='viewport', content='width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, minimal-ui=1') + meta(charset='UTF-8') + meta(name='mobile-web-app-capable', content='yes') + meta(name='apple-mobile-web-app-capable', content='yes') + meta(name='description', content='') + + link(rel="manifest" href="manifest.json") + link(rel='icon', type='image/png', href="icon64.png") + link(rel="apple-touch-icon" sizes="180x180" href="icon180.png") + link(rel="icon" type="image/png" sizes="32x32" href="icon32.png") + link(rel="icon" type="image/png" sizes="16x16" href="icon16.png") + + style. + html,body { + margin: 0; + padding: 0; + background-color: #000; + overflow:hidden; + font-family: Verdana; + } + .noselect { + -webkit-touch-callout: none; + -webkit-user-select: none; + -khtml-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; + } + #canvaswrapper { + text-align: center ; + } + + each font in fonts + style @font-face { font-family: "#{font}" ; src: url("fonts/#{font}.ttf") format("truetype"); } + + script window.fonts = !{JSON.stringify(fonts)} + + body.noselect.custom-cursor(oncontextmenu='return false;') + #canvaswrapper + + script(type="text/javascript"). + !{game.resources} + + script(type="text/javascript"). + var orientation = '!{game.orientation}' ; + var aspect = '!{game.aspect}' ; + window.skip_service_worker = true; + window.exported_project = true; + + each file in javascript_files + script(src=file) + + script(id="code" type="text/x-microscript"). + !{game.code} diff --git a/templates/forum/forum.pug b/templates/forum/forum.pug new file mode 100644 index 00000000..5c66231c --- /dev/null +++ b/templates/forum/forum.pug @@ -0,0 +1,162 @@ +doctype html +html(lang=language) + head + meta(name="description" content=description) + meta(name="viewport" content="width=device-width, user-scalable=no, initial-scale=.8") + link(rel="alternate" hreflang="fr" href="https://microstudio.dev/fr/community/") + link(rel="alternate" hreflang="en" href="https://microstudio.dev/community/") + title #{name} - microStudio + + link(rel="stylesheet" type="text/css" href="/css/forum/forum.css") + link(rel="stylesheet" type="text/css" href="/css/forum/media.css") + link(rel="stylesheet" type="text/css" href="/css/md.css") + + link(rel="stylesheet" href="/lib/fontlib/fontawesome/css/all.css") + + link(rel="stylesheet" href="/lib/fontlib/ubuntu/index.css") + link(rel="stylesheet" href="/lib/fontlib/ubuntu-mono/index.css") + link(rel="stylesheet" href="/lib/fontlib/source-sans-pro/index.css") + link(rel="stylesheet" href="/lib/fontlib/source-sans-pro/700.css") + + script(src="/js/forum/forum.js") + script(src="/js/forum/client.js") + script(src="/js/util/translator.js") + script(src="/lib/marked/marked.js") + + link(rel="apple-touch-icon" sizes="180x180" href="/img/favicon180.png") + link(rel="icon" type="image/png" sizes="32x32" href="/img/favicon32.png") + link(rel="icon" type="image/png" sizes="16x16" href="/img/favicon16.png") + if community.language == "fr" + link(rel="manifest" href="/community/manifest_fr.json") + else + link(rel="manifest" href="/community/manifest.json") + + script window.community = !{JSON.stringify(community)} ; + + body(class=theme) + header + div.username + i.fa.fa-user + img(style="display: none") + span #{translator.get("Login")} + + a(href="/") + img.logo#logo(src="/img/microstudiologo.svg" alt="microStudio" title="microStudio") + a(href=community.language == "en" ? "/community/" : "/"+community.language+"/community/" title="microStudio Community") + div.community-icon + i.fa.fa-users + span #{translator.get("Community")} + + div.login + div.menu + + div.content + div.topright + div.theme #{translator.get(theme == "dark"? "LIGHT THEME" : "DARK THEME")} + + select.languages(onchange="location = this.value == 'en'? '/community/' : '/'+this.value+'/community/' ;") + each la in community.languages + if la == community.language + option(value=la selected) #{la.toUpperCase()} + else + option(value=la) #{la.toUpperCase()} + + ul.categories + each cat in categories + if cat.slug == selected + li + a.category.selected(href=cat.path style="background: hsl("+cat.hue+",40%,50%)") #{cat.name} + else + li + a.category(href=cat.path style="background: hsl("+cat.hue+",50%,80%)") #{cat.name} + + div.description + div.watch + div.text + div.button.unwatch + if selected != "all" + h1 #{name} + + p #{description} + + div.filtering + div#searchbar + input + i.fa.fa-search + + div#sorting + i.fas.fa-arrows-alt-v + span #{translator.get("Activity")} + + div.posts + each post in posts + if !post.author.flags.censored && !post.author.flags.banned + div.post(data-post-id=post.id data-post-activity=post.activity data-post-likes=post.likes.length data-post-views=post.views data-post-replies=post.replies.length data-post-created=post.date) + if post.author.flags.profile_image + img.author-icon(src="/"+post.author.nick+".png") + else + div.author-icon(title=post.author.nick data-colorize=post.author.nick) + span #{post.author.nick.substring(0,1).toUpperCase()} + + div.post-top + div.post-author #{post.author.nick} + a.title(href=post.getPath()) #{post.title} + + div.post-info + div.post-stats + div.post-likes(data-likes-post=post.id) #{post.likes.length} + if post.replies.length>0 + div.post-replies(data-replies=post.replies.length) #{post.replies.length} + + div.post-views(data-views=post.views) #{post.views || 0} + div.post-activity + a.post-category(href=post.category.path style="background: hsl("+post.category.hue+",40%,50%)") #{post.category.name} + if post.progress>0 + div.post-progress(data-progress=post.progress) #{post.progress}% + if post.status + div.post-status(data-colorize=post.status) •  #{post.status}  â€¢ + div.post-users + each p in post.people + if !p.author.flags.censored && !p.author.flags.banned + if p.author.flags.profile_image + img.user(title=p.author.nick src="/"+p.author.nick+".png") + else + div.user(title=p.author.nick data-colorize=p.author.nick) #{p.author.nick.substring(0,1).toUpperCase()} + + div.create-post-button-container + div.create-post-button-wrapper + div.create-post#create-post-button + i.fas.fa-pencil-alt + span #{translator.get("New Post")} + + div.edit-post#edit-post + div.edit-post-content#edit-post-content + h1 #{translator.get("New Post")} + + select#edit-post-category + option(value="" disabled selected) #{translator.get("Choose category...")} + each cat in categories + if cat.slug != "all" + if cat.slug == selected + option(value=cat.slug selected data-perm=cat.permissions.post) #{cat.name} + else + option(value=cat.slug data-perm=cat.permissions.post) #{cat.name} + br + + input#edit-post-title(placeholder=translator.get("Title of your post")) + br + + textarea#edit-post-text(placeholder=translator.get("Write your text here ; you can use markdown")) + br + + div#edit-post-preview-area.md + + div.edit-post-buttons + div.button.preview#edit-post-preview + i.fas.fa-eye + span #{translator.get("Preview")} + div.button.cancel#edit-post-cancel #{translator.get("Cancel")} + div.button.post#edit-post-post #{translator.get("Post")} + + div#validate-your-email + div #{translator.get("Validate your e-mail address to participate in the community")} diff --git a/templates/forum/post.pug b/templates/forum/post.pug new file mode 100644 index 00000000..02e90dd4 --- /dev/null +++ b/templates/forum/post.pug @@ -0,0 +1,156 @@ +doctype html +html(lang=language) + head + meta(name="description" content=post.text.split("\n")[0]) + meta(name="viewport" content="width=device-width, user-scalable=no, initial-scale=.8") + link(rel="alternate" hreflang="fr" href="https://microstudio.dev/fr/community/") + link(rel="alternate" hreflang="en" href="https://microstudio.dev/community/") + title #{post.title} + + link(rel="stylesheet" type="text/css" href="/css/forum/forum.css") + link(rel="stylesheet" type="text/css" href="/css/forum/media.css") + link(rel="stylesheet" type="text/css" href="/css/md.css") + + link(rel="stylesheet" href="/lib/fontlib/fontawesome/css/all.css") + + link(rel="stylesheet" href="/lib/fontlib/ubuntu/index.css") + link(rel="stylesheet" href="/lib/fontlib/ubuntu-mono/index.css") + link(rel="stylesheet" href="/lib/fontlib/source-sans-pro/index.css") + link(rel="stylesheet" href="/lib/fontlib/source-sans-pro/700.css") + + script(src="/js/forum/forum.js") + script(src="/js/forum/client.js") + script(src="/js/util/translator.js") + script(src="/lib/marked/marked.js") + + script window.post = !{JSON.stringify(post_info)} ; + script window.community = !{JSON.stringify(community)} ; + + body(class=theme) + header + div.username + i.fa.fa-user + img(style="display: none") + span #{translator.get("Login")} + + a(href="/") + img.logo#logo(src="/img/microstudiologo.svg" alt="microStudio" title="microStudio") + a(href=community.language == "en" ? "/community/" : "/"+community.language+"/community/" title="microStudio Community") + div.community-icon + i.fa.fa-users + span #{translator.get("Community")} + + div.login + div.menu + + div.content + div.topright + div.theme #{translator.get(theme == "dark"? "LIGHT THEME" : "DARK THEME")} + + ul.categories + each cat in categories + if cat.slug == selected + li + a.category.selected(href=cat.path style="background: hsl("+cat.hue+",40%,50%)") #{cat.name} + else + li + a.category(href=cat.path style="background: hsl("+cat.hue+",50%,80%)") #{cat.name} + + div.post-container + div.post-content + div.watch + div.text + div.button.unwatch + + i.link.fas.fa-link(title="Copy link" data-link=post_info.url) + h1 #{post.title} + + div.post-content-info + div.post-stats + div.post-likes(data-likes-post=post.id) #{post.likes.length} + div.post-replies(data-replies=post.replies.length) #{post.replies.length} + div.post-views(data-views=post.views) #{post.views} + div.post-activity + + div.post-users + if post.author.flags.profile_image + img.user(src="/"+post.author.nick+".png" title=post.author.nick) + else + div.user(data-colorize=post.author.nick) #{post.author.nick.substring(0,1).toUpperCase()} + + span.post-content-author #{post.author.nick} + a.post-category(href=post.category.path style="background:hsl("+post.category.hue+",40%,50%)") #{post.category.name} + if post.progress>0 + div.post-progress(data-progress=post.progress) #{post.progress}% + + if post.status + div.post-status(data-colorize=post.status) •  #{post.status}  â€¢ + + div.post-text.md(data-author=post.author.nick data-type="post") !{post.getHTMLText()} + + if post.reverse + - var i = post.replies.length-1 + while i>=0 + - var reply = post.replies[i] + if !reply.deleted && !reply.author.flags.censored && !reply.author.flags.banned + div.post-content(id="post-reply-"+i data-reply-id=reply.id) + i.link.fas.fa-link(title="Copy link" data-link=post_info.url+i+"/") + div.post-content-info + div.post-stats + div.post-likes(data-likes-reply=reply.id) #{reply.likes.length} + div.post-activity + + div.post-users + if reply.author.flags.profile_image + img.user(src="/"+reply.author.nick+".png" title=reply.author.nick) + else + div.user( data-colorize=reply.author.nick) #{reply.author.nick.substring(0,1).toUpperCase()} + span #{reply.author.nick} + + div.post-text.md(data-author=reply.author.nick data-type="reply") !{reply.getHTMLText()} + - i -= 1 + else + each reply,i in post.replies + if !reply.deleted && !reply.author.flags.censored && !reply.author.flags.banned + div.post-content(id="post-reply-"+i data-reply-id=reply.id) + i.link.fas.fa-link(title="Copy link" data-link=post_info.url+i+"/") + div.post-content-info + div.post-stats + div.post-likes(data-likes-reply=reply.id) #{reply.likes.length} + div.post-activity + + div.post-users + if reply.author.flags.profile_image + img.user(src="/"+reply.author.nick+".png" title=reply.author.nick) + else + div.user( data-colorize=reply.author.nick) #{reply.author.nick.substring(0,1).toUpperCase()} + span #{reply.author.nick} + + div.post-text.md(data-author=reply.author.nick data-type="reply") !{reply.getHTMLText()} + + div.edit-post-content.edit-reply-content#edit-post-content + h2.reply-title #{translator.get("Post a reply")} + + div#edit-post-reserved + span#edit-post-progress-label #{translator.get("Progress")} + br + input#edit-post-progress(type="range") + br + span #{translator.get("Status")} + br + input#edit-post-status() + + textarea#edit-post-text(placeholder=translator.get("Write your text here ; you can use markdown")) + br + + div#edit-post-preview-area.md + + div.edit-post-buttons + div.button.preview#edit-post-preview + i.fas.fa-eye + span #{translator.get("Preview")} + div.button.cancel#edit-post-cancel #{translator.get("Cancel")} + div.button.post#edit-post-post #{translator.get("Post")} + + div#validate-your-email + div #{translator.get("Validate your e-mail address to participate in the community")} diff --git a/templates/help.pug b/templates/help.pug new file mode 100644 index 00000000..1a71fc5d --- /dev/null +++ b/templates/help.pug @@ -0,0 +1,5 @@ +div.helplist#helplist + div.help-list#help-list + +div.help-view#help-view + div.doc-render.documentation#documentation diff --git a/templates/home.pug b/templates/home.pug new file mode 100644 index 00000000..bf3db5c3 --- /dev/null +++ b/templates/home.pug @@ -0,0 +1,455 @@ +doctype html +html(lang=language) + head + meta(name="description" content=translator.get("Learn programming, create video games - microStudio is a free game engine online.")) + meta(name="viewport" content="width=device-width, user-scalable=no, initial-scale=.6") + link(rel="alternate" hreflang="de" href="https://microstudio.dev/de/") + link(rel="alternate" hreflang="pl" href="https://microstudio.dev/pl/") + link(rel="alternate" hreflang="fr" href="https://microstudio.dev/fr/") + link(rel="alternate" hreflang="en" href="https://microstudio.dev") + title microStudio - #{translator.get("Learn programming, create games")} + + each css in css_files + link(rel="stylesheet" type="text/css" href=css) + + link(rel="stylesheet" href="/lib/fontlib/fontawesome/css/all.css") + + link(rel="stylesheet" href="/lib/fontlib/ubuntu/index.css") + link(rel="stylesheet" href="/lib/fontlib/ubuntu-mono/index.css") + link(rel="stylesheet" href="/lib/fontlib/source-sans-pro/index.css") + link(rel="stylesheet" href="/lib/fontlib/source-sans-pro/700.css") + + link(rel="apple-touch-icon" sizes="180x180" href="/img/favicon180.png") + link(rel="icon" type="image/png" sizes="32x32" href="/img/favicon32.png") + link(rel="icon" type="image/png" sizes="16x16" href="/img/favicon16.png") + link(rel="manifest" href="/manifest.json") + + script(type="application/ld+json"). + { + "@context": "https://schema.org", + "@type": "SoftwareApplication", + "name": "microStudio", + "applicationCategory": "https://schema.org/WebApplication", + "offers": { + "@type": "Offer", + "price": "0", + "priceCurrency": "USD" + }, + "operatingSystem": "Any" + } + + meta(property="og:title" content="microStudio") + meta(property="og:type" content="website") + meta(property="og:url" content="https://microstudio.dev") + meta(property="og:image" content="https://microstudio.dev/img/microstudio.jpg") + meta(property="og:description" content="microStudio is a free game engine online. Learn, create and share with the community. Use the built-in sprite editor, map editor and code editor to create anything.") + + meta(name="twitter:card" content="summary_large_image") + meta(name="twitter:site" content="@microStudio") + meta(name="twitter:title" content="microStudio") + meta(name="twitter:description" content="microStudio is a free game engine online. Learn, create and share with the community. Use the built-in sprite editor, map editor and code editor to create anything.") + meta(name="twitter:image" content="https://microstudio.dev/img/microstudio.jpg") + + script(src="/lib/jquery/jquery.js") + script(src="/lib/jquery-ui/jquery-ui.min.js") + script(src="/lib/ace/ace.js") + script(src="/lib/mode-microscript.js") + script(src="/lib/ace/mode-markdown.js") + script(src="/lib/ace/theme-tomorrow_night_bright.js") + + script(src="/lib/marked/marked.js") + script(src="/lib/dompurify/purify.js") + + each js in javascript_files + script(src=js) + + body.notranslate(translate="no") + header + div(id="create-account-button" title=translator.get("Create My Account")) + #{translator.get("Create My Account")} + + div(id="login-button" title=translator.get("Login")) + #{translator.get("Login")} + + div.login-info#login-info + div.username + img(style="display: none;") + i.fa.fa-user + span#user-nick + + div#language-setting + i.fas.fa-comment + div.lang.notranslate(translate="no") #{language.toUpperCase()} + + div.language-menu#language-menu + div.language-choice#language-choice-en(title="English") + i.fas.fa-comment + div.notranslate(translate="no") EN + + div.language-choice#language-choice-fr(title="Français") + i.fas.fa-comment + div.notranslate(translate="no") FR + + div.language-choice#language-choice-pl(title="Polski") + i.fas.fa-comment + div.notranslate(translate="no") PL + + div.language-choice#language-choice-de(title="Deutsch") + i.fas.fa-comment + div.notranslate(translate="no") DE + + img.logo#logo(src="/img/microstudiologo.svg" alt="microStudio" title="microStudio") + + ul.titlemenu + li#menu-explore.selected(title=translator.get("Explore"))
#{translator.get("Explore")} + li#menu-projects(title=translator.get("Create"))
#{translator.get("Create")} + //- li#menu-play(title=translator.get("Play"))
#{translator.get("Play")} + li#menu-tutorials(title=translator.get("Tutorials"))
#{translator.get("Tutorials")} + li#menu-help(title=translator.get("Documentation"))
#{translator.get("Documentation")} + a(target="_blank" href=language == "fr" ? "/fr/community/" : "/community/") + li#menu-community(title=translator.get("Community"))
#{translator.get("Community")} NEW + li#menu-about(title=translator.get("About"))
#{translator.get("About")} + + div.usermenu(style="height:0px;") + //div.profile #{translator.get("Profile")} + div.settings #{translator.get("Settings")} + div.profile #{translator.get("Profile")} + div.logout #{translator.get("Logout")} + div.create-account #{translator.get("Create Account")} + div.discard-account #{translator.get("Quit and Delete")} + + div.main-container + div.home-section#home-section + div.part.part1 + div.center + img.mainlogo(src="/img/microstudiologo.svg" alt="microStudio" style="width: 250px") + p.p1 #{translator.get("microStudio is a free game engine online.")} + p.p2 #{translator.get("Create games, learn programming, play, share, prototype and jam!")} + + div.center.right + img.screen(src="/img/microstudio.jpg" alt="microStudio view") + + div#home-header-background + + div.part + div.center + img.screen(src="/img/mapeditor.png" alt="microStudio map editor") + div.center + h2 #{translator.get("Online and integrated")} + p #{translator.get("microStudio includes all you need to write code, create sprites and maps for your 2D game.")} + p #{translator.get("All from your web browser. Your project is stored in the cloud, accessible from anywhere.")} + + div.part + div.center + h2 #{translator.get("Get into programming")} + p #{translator.get("Write your game code in microScript, a simple language inspired by Lua.")} + p #{translator.get("The documentation is always there to help. Create cool demos in just a few lines of code.")} + div.center + img.screen(src="/img/codeeditor.jpg" alt="Code Editor") + + div.part + div.center + img.screen(src="/img/spriteeditor.jpg" alt="Sprite Editor") + div.center + h2 #{translator.get("Design")} + p #{translator.get("Create pixel art sprites and maps with the included editors.")} + p #{translator.get("Using your sprites and maps from code is as easy as 1-2-3.")} + + div.part(style="padding-bottom:0") + div.center + h2 #{translator.get("Adjusting is a breeze")} + p #{translator.get("Change anything from your PC while testing your game on your mobile.")} + p #{translator.get("Your changes are immediately reflected in your ongoing playing session.")} + div.center(style="padding:0;line-height: 0;margin-top:-50px") + img.screen(src="/img/smartphone.png" alt="Run on your smartphone" style="box-shadow: none;") + + div.part + div.center + i.fa.fa-users + div.center + h2 #{translator.get("Bring your friends")} + p #{translator.get("You can invite others to your project and work in teams.")} + p #{translator.get("microStudio automagically synchronizes all your project files in real time.")} + + div.part + div.center + h2 #{translator.get("Get your gear ready")} + p #{translator.get("Control your game using the gamepad, touchscreen, keyboard or mouse inputs.")} + p #{translator.get("Export your project in a single click to HTML5, Windows, Linux, macOS. More export options are coming!")} + div.center + img.screen(src="/img/gamepad.svg" alt="Gamepad" style="box-shadow: none; width: 75%") + + div.part + div.center + img.screen(src="/img/community.jpg" alt="Community" style="box-shadow: none;") + div.center + h2 #{translator.get("Join the community")} + p #{translator.get("Browse public games, demos, resources. Pick up what you need for your own project.")} + p #{translator.get("Contribute your best creations and help others.")} + + + div.part.last + img(src="/img/microstudiologo.svg" alt="microStudio" style="width: 250px") + + div.part + h2 #{translator.get("So what now?")} + br + div.button#home-action-explore + i.fa.fa-search + br + span #{translator.get("Explore public projects")} + div.button#home-action-create + i.fa.fa-pencil-alt + br + span #{translator.get("Start creating!")} + + div.footer + p #{translator.get("Created in Strasbourg, France. See 'About' for more information")} + p + img(src="/img/faviconlarge.png" alt="microStudio icon" style="border-radius: 8px; width: 48px") + p #{translator.get("You are not tracked here.")}
#{translator.get("microStudio doesn't use any third-party tracking script.")} + p.notranslate(translate="no") + a.notranslate(id="switch-to-fr" href="/fr/" translate="no") microStudio en Français + i.fa.fa-circle + a.notranslate(id="switch-to-en" href="/" translate="no") microStudio in English + br + a.notranslate(id="switch-to-pl" href="/pl/" translate="no") microStudio w języku polskim + i.fa.fa-circle + a.notranslate(id="switch-to-de" href="/de/" translate="no") microStudio auf Deutsch + + div.exploreview#explore-section + div#explore-bar + div#explore-back-button + i(class="fa fa-arrow-left") + span #{translator.get("Back")} + + div#explore-tools + div#explore-sort-button.hot + i.fas.fa-arrows-alt-v + span #{translator.get("Hot")} + + div#explore-tags + + div#explore-search + input#explore-search-input(name="search") + i.fa.fa-search + + div#explore-contents + div#explore-box-list + + div#explore-project-details + include projectdetails.pug + + div.help-section#help-section + include help.pug + + div.about-section#about-section + include about.pug + + div.tutorials-section#tutorials-section + include tutorials.pug + + div.usersettings-section#usersettings-section + include user.pug + + div.projects-section#projects-section + + div.myprojects#myprojects + div.title + div#projects-search + input#projects-search-input(name="search") + i.fa.fa-search + h1 #{translator.get("My Projects")} + div.create-project-button#create-project-button #{translator.get("Create New Project")} + div.project-list#project-list + + div.projectview#projectview + div.projectheader + div.backtoprojects#backtoprojects #{translator.get("Back to Projects")} + img#project-icon + span#project-name Project name + i#save-status(class="fa fa-ellipsis-h") + div#active-project-users + div.active-user + i(class="fa fa-user") + span username + + div.sidemenu + ul + li(id="menuitem-code")
#{translator.get("Code")} + li(id="menuitem-sprites")
#{translator.get("Sprites")} + li(id="menuitem-maps")
#{translator.get("Maps")} + li(id="menuitem-assets")
#{translator.get("Assets")} + li(id="menuitem-sounds")
#{translator.get("Sounds")} + li(id="menuitem-music")
#{translator.get("Music")} + li(id="menuitem-doc")
#{translator.get("Doc")} + // li(id="menuitem-server")
#{translator.get("Server")} + li(id="menuitem-options")
#{translator.get("Settings")} + li(id="menuitem-publish")
#{translator.get("Publish")} + + div.section-container#section-container + div.section#code-section + include code.pug + + div.section#explore-section + + div.section#projects-section + + div.section#sprites-section + include sprites.pug + + div.section#maps-section + include maps.pug + + div.section#assets-section + include assets.pug + + div.section#sounds-section + include sounds.pug + + div.section#music-section + include music.pug + + div.section#doc-section + include doc.pug + + div.section#server-section + include server.pug + + div.section#settings-section. + Settings section here + + div.section#options-section + include projectoptions.pug + + div.section#publish-section + include publish.pug + + div.overlay#login-overlay + div#login-window + form#guest-panel + h2 #{translator.get("Start creating")} + div.button#guest-action-guest + i.fas.fa-user-clock + br + h3 #{translator.get("Create as a guest")} + p #{translator.get("Start using microStudio without creating an account.")} + + div.button#guest-action-create + i.fa.fa-user-plus + br + h3 #{translator.get("Create my account")} + p #{translator.get("Save your projects, work in teams, publish, vote, comment...")} + + div.button#guest-action-login + i.fa.fa-sign-in-alt + br + h3 #{translator.get("Login")} + p #{translator.get("Log in to your existing account.")} + + form#login-panel + h2 #{translator.get("Registered User")} + label #{translator.get("Nickname or e-mail")} + br + input(type="text" id="login_nick" required) + br + label #{translator.get("Password")} + br + input(type="password" id="login_password" required) + br + a#forgot-password-link(href="#") #{translator.get("Forgot password?")} + br + br + button#login-submit #{translator.get("Log in")} + br + br + p #{translator.get("Don't have an account yet?")} + a(href="#" id="switch_to_create_account") #{translator.get("Create my account")} + form#forgot-password-panel + h2 #{translator.get("Password Recovery")} + label #{translator.get("E-mail")} + br + input(type="text" id="forgot_email" required) + br + br + button#forgot-submit #{translator.get("Send password recovery e-mail")} + br + br + a(href="#" id="switch_from_forgot_to_login") #{translator.get("Back to login")} + form#create-account-panel + h2 #{translator.get("New User")} + label #{translator.get("Nickname")} + br + input(type="text" id="create_nick" required) + br + label #{translator.get("E-mail address")} + br + input(type="email" id="create_email" required placeholder="E-mail address") + br + label #{translator.get("Password")} + br + input(type="password" id="create_password" required) + br + div#create-account-terms + div(style="text-align:left") + input(type="checkbox" id="create-account-tos" style="margin-bottom:4px") + label(for="create-account-tos") #{translator.get("Accept the")}  + a#create-account-toggle-terms(href="#") #{translator.get("Terms of Use")} + br + input(type="checkbox" id="create-account-newsletter") + label(for="create-account-newsletter") #{translator.get("Receive news (maximum once per month)")} + button#create-account-submit #{translator.get("Create Account")} + br + br + p #{translator.get("Already registered?")} + a(href="#" id="switch_to_log_in") #{translator.get("Log in to existing account")} + + div.overlay#create-project-overlay + div#create-project-window + form#create-project-form + h2 #{translator.get("Create New Project")} + label #{translator.get("Title")} + br + input#create-project-title(type="text" required) + br + br + button#create-project-submit #{translator.get("Create Project")} + + div#notification-container + div#notification-bubble + i(class="fa fa-comment") + span Example Bubble Text + + div#tutorial-overlay + + div.floating-window#tutorial-window + div.titlebar + i.icon.fa.fa-lightbulb + div.title #{translator.get("Tutorial")} + i.minify.fa.fa-compress + div.content.md + div.navigation + i.previous.fa.fa-arrow-left + span.step 1/15 + i.next.fa.fa-arrow-right + i.resize.fa.fa-grip-horizontal + + div.floating-window#run-window + div.titlebar.runbar + div.title #{translator.get("Run")} + div.buttons + div#run-button-win(title="Run project") + i.fas.fa-play + div#pause-button-win.selected(title="Pause execution") + i.fas.fa-pause + div#reload-button-win.selected(title="Restart project") + i.fa.fa-redo + + i.minify.fas.fa-minus-square(style="background:none;") + div.content + div.navigation + i.resize.fa.fa-grip-horizontal + + canvas#highlighter + + i.fa.fa-arrow-right#highlighter-arrow diff --git a/templates/maps.pug b/templates/maps.pug new file mode 100644 index 00000000..c8cada97 --- /dev/null +++ b/templates/maps.pug @@ -0,0 +1,55 @@ +div.maps-left + div.create-map-button#create-map-button #{translator.get("Add Map")} + div.maplist#maplist + div.map-list#map-list + +div.maps-splitbar + +div.maps-right + div.mapinfo#mapinfo + input(type="text" value="")#map-name + div.validate-button-container#map-name-button + div.validate-button #{translator.get("Apply")} + span     Map Size + input(type="text" value="16")#map-width + span x + input(type="text" value="10")#map-height + div.validate-button-container#map-size-button + div.validate-button #{translator.get("Apply")} + span     Block Size + input(type="text" value="16")#map-block-width + span x + input(type="text" value="16")#map-block-height + div.validate-button-container#map-blocksize-button + div.validate-button #{translator.get("Apply")} + div.buttons + div#undo-map + i.fa.fa-undo + div#redo-map + i.fa.fa-redo + + div#copy-map + i.fa.fa-copy + div#cut-map + i.fa.fa-cut + div#paste-map + i.fa.fa-paste + + div#delete-map + i.fa.fa-trash + + div#map-editor-locked + div#mapeditor-container + div.mapeditor#mapeditor + div#mapeditor-wrapper + div#mapeditor-bottombar + span #{translator.get("Background")} + div#map-background-color + select#map-underlay-select + div#map-coordinates + + div.mapeditor-splitbar + + div.mapbar#mapbar(tabindex="1") + div.map-tilepicker#map-tilepicker + div.map-sprite-list#map-sprite-list diff --git a/templates/music.pug b/templates/music.pug new file mode 100644 index 00000000..a6bb74d5 --- /dev/null +++ b/templates/music.pug @@ -0,0 +1,64 @@ +div.music-left + div.assets-drop + div + div #{translator.get("You can drop MP3 files here")} + + div#create-music-button(style="display:none") #{translator.get("Add Music")} + + div.assetlist#musiclist + div#music-list + +div.horizontal-splitbar#music-splitbar + +div.music-right + div.assetinfo#musicinfo + input(type="text" value="")#music-name + div.validate-button-container#music-name-button + div.validate-button #{translator.get("Apply")} + + div.buttons + div#undo-music(title=translator.get("Undo")) + i.fa.fa-undo + div#redo-music(title=translator.get("Redo")) + i.fa.fa-redo + + div#copy-music(title=translator.get("Copy")) + i.fa.fa-copy + div#cut-music(title=translator.get("Cut")) + i.fa.fa-cut + div#paste-music(title=translator.get("Paste")) + i.fa.fa-paste + + div#delete-music.delete(title=translator.get("Delete Sound")) + i.fa.fa-trash + + div#music-editor-locked + div#music-editor + div.placeholder #{translator.get("Drop your music files on the left. A sleek music creation app will land here in the future!")} + +//- div#daw-transportbar TRANSPORT BAR + +//- div#daw-container +//- div#daw-tracks +//- div#daw-track-headers +//- div.daw-track-header TRACK HEADER 1 +//- div.daw-track-header TRACK HEADER 2 +//- div.daw-track-header TRACK HEADER 3 +//- div.daw-track-header TRACK HEADER 4 +//- div.daw-track-header TRACK HEADER 5 +//- div.daw-track-header TRACK HEADER 6 +//- div.daw-track-header TRACK HEADER 7 +//- div.daw-track-header TRACK HEADER 8 +//- div#daw-track-content +//- div.daw-track TRACK 1 +//- div.daw-track TRACK 2 +//- div.daw-track TRACK 3 +//- div.daw-track TRACK 4 +//- div.daw-track TRACK 5 +//- div.daw-track TRACK 6 +//- div.daw-track TRACK 7 +//- div.daw-track TRACK 8 + +//- div#daw-splitbar + +//- div#daw-pianoroll PIANO ROLL diff --git a/templates/password/reset1.pug b/templates/password/reset1.pug new file mode 100644 index 00000000..dc625689 --- /dev/null +++ b/templates/password/reset1.pug @@ -0,0 +1,42 @@ +doctype html +html + head + style. + body { + font-family: Verdana; + text-align: center ; + background: #111 ; + color: #FFF ; + } + img { + width: 200px ; + } + input,button { + font-size: 18px ; + } + input { + background: rgba(255,255,255,.1); + color: #FFF ; + padding: 4px 8px ; + border-radius: 5px ; + text-align: center ; + } + * { + margin: 10px ; + } + body() + img(src="/img/microstudiologo.svg") + p Choose your new password + input#password(type="password") + br + button(onclick="submit()") Submit + + script. + var submit = function() { + var token = "#{token}" ; + var userid = #{userid} ; + var pass = document.getElementById("password").value ; + window.location.replace(location.origin+"/pwd/"+userid+"/"+token+"/"+pass+"/") ; + } + + diff --git a/templates/password/reset2.pug b/templates/password/reset2.pug new file mode 100644 index 00000000..3b18d74d --- /dev/null +++ b/templates/password/reset2.pug @@ -0,0 +1,35 @@ +doctype html +html + head + style. + body { + font-family: Verdana; + text-align: center ; + background: #111 ; + color: #FFF ; + } + img { + width: 200px ; + } + input,button { + font-size: 18px ; + } + input { + background: rgba(255,255,255,.1); + color: #FFF ; + padding: 4px 8px ; + border-radius: 5px ; + text-align: center ; + } + * { + margin: 10px ; + } + body() + img(src="/img/microstudiologo.svg") + p Your password has been reset. + + script. + setTimeout(function(){ + window.location.replace(location.origin) ; + },5000) ; + diff --git a/templates/play/manifest.json b/templates/play/manifest.json new file mode 100644 index 00000000..7b8dc662 --- /dev/null +++ b/templates/play/manifest.json @@ -0,0 +1,33 @@ +{ + "name": "APPNAME", + "short_name": "APPSHORTNAME", + "theme_color": "#000000", + "background_color": "#000000", + "display": "fullscreen", + "orientation": "ORIENTATION", + "scope": "SCOPE", + "start_url": "START_URL", + "icons": [ + { + "src": "SCOPEicon192.png?v=ICONVERSION", + "type": "image/png", + "sizes": "192x192" + }, + { + "src": "SCOPEicon196.png?v=ICONVERSION", + "type": "image/png", + "sizes": "196x196", + "purpose": "maskable" + }, + { + "src": "SCOPEicon512.png?v=ICONVERSION", + "type": "image/png", + "sizes": "512x512" + }, + { + "src": "SCOPEicon1024.png?v=ICONVERSION", + "type": "image/png", + "sizes": "1024x1024" + } + ] +} diff --git a/templates/play/play.pug b/templates/play/play.pug new file mode 100644 index 00000000..2af5640f --- /dev/null +++ b/templates/play/play.pug @@ -0,0 +1,70 @@ +doctype html +html + head + title #{game.title} + meta(http-equiv='content-type', content='text/html; charset=UTF-8') + meta(name='viewport', content='width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, minimal-ui=1') + meta(charset='UTF-8') + meta(name='mobile-web-app-capable', content='yes') + meta(name='apple-mobile-web-app-capable', content='yes') + meta(name='description', content='') + + meta(property="og:title" content=game.title) + meta(property="og:site_name" content="microStudio") + meta(property="og:url" content="https://microstudio.io/"+game.author+"/"+game.name+"/") + meta(property="og:image" content="https://microstudio.io/"+game.author+"/"+game.name+"/icon512.png") + meta(property="og:image:type" content="image/png") + meta(property="og:description" content=game.title+" by "+game.author) + + link(rel="manifest" href="manifest.json") + link(rel='icon', type='image/png', href="/"+game.author+"/"+game.name+"/icon64.png") + link(rel="apple-touch-icon" sizes="180x180" href="/"+game.author+"/"+game.name+"/icon180.png") + link(rel="icon" type="image/png" sizes="32x32" href="/"+game.author+"/"+game.name+"/icon32.png") + link(rel="icon" type="image/png" sizes="16x16" href="/"+game.author+"/"+game.name+"/icon16.png") + + style. + html,body { + margin: 0; + padding: 0; + background-color: #000; + overflow:hidden; + font-family: Verdana; + } + .noselect { + -webkit-touch-callout: none; + -webkit-user-select: none; + -khtml-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; + } + #canvaswrapper { + text-align: center ; + } + + style. + @font-face { + font-family: "Bit Cell"; + src: url("/fonts/bit_cell.ttf") format("truetype"); + } + each font in fonts + style @font-face { font-family: "#{font}" ; src: url("/fonts/#{font}.ttf") format("truetype"); } + + script window.fonts = !{JSON.stringify(fonts)} + + body.noselect.custom-cursor(oncontextmenu='return false;') + #canvaswrapper + + script(type="text/javascript"). + !{game.resources} + + script(type="text/javascript"). + var orientation = '!{game.orientation}' ; + var aspect = '!{game.aspect}' ; + var graphics = '!{game.graphics}' ; + + each file in javascript_files + script(src=file) + + script(id="code" type="text/x-project-code"). + !{game.code} diff --git a/templates/play/userpage.pug b/templates/play/userpage.pug new file mode 100644 index 00000000..ec4f07bb --- /dev/null +++ b/templates/play/userpage.pug @@ -0,0 +1,55 @@ +doctype html +html(lang='en') + head + title #{user} - microStudio + meta(http-equiv='content-type', content='text/html; charset=UTF-8') + meta(name="viewport" content="width=device-width, user-scalable=no, initial-scale=.6") + meta(charset='UTF-8') + meta(name='mobile-web-app-capable', content='yes') + meta(name='apple-mobile-web-app-capable', content='yes') + meta(name='description', content=user+" is on microStudio") + + link(rel="stylesheet" href="/lib/fontlib/fontawesome/css/all.css") + + link(rel="stylesheet" href="/lib/fontlib/ubuntu/index.css") + link(rel="stylesheet" href="/lib/fontlib/ubuntu-mono/index.css") + link(rel="stylesheet" href="/lib/fontlib/source-sans-pro/index.css") + link(rel="stylesheet" href="/lib/fontlib/source-sans-pro/700.css") + + link(rel="stylesheet" type="text/css" href="/css/userpage.css") + link(rel="stylesheet" type="text/css" href="/css/md.css") + if profile_image + link(rel="icon" type="image/png" href="/"+user+".png") + else + link(rel="icon" type="image/png" href="/img/favicon.png") + + body.noselect.custom-cursor(oncontextmenu='return false;') + header + div.headerdiv + a(href="/") + img.logo(src="/img/microstudiologo.svg") + + div.content + if profile_image + img.profile_image(src="/"+user+".png") + + div.username#user-nick #{user} + div.description.dark.md !{description} + + div.sections + div.selected#section-projects Public Projects + //div#section-progress Stats & Achievements + //div#section-forum Forum interactions + + div.projects#projects + each project in projects + -var path = "/"+user+"/"+project.slug+"/" + -var img = "/"+user+"/"+project.slug+"/icon72.png" + a(href=path) + div.projectbox(id="#{project.slug}") + img(src=img) + div.title #{project.title} + + + + diff --git a/templates/projectdetails.pug b/templates/projectdetails.pug new file mode 100644 index 00000000..12f3b064 --- /dev/null +++ b/templates/projectdetails.pug @@ -0,0 +1,91 @@ +div#project-details-info + div.inside + div.project-details-right + img#project-details-image + br + br + br + a#project-details-runbutton(target="_blank") + i(class="fa fa-play") + span #{translator.get("Run")} + br + br + a#project-details-clonebutton(href="#") + i(class="fa fa-copy") + span #{translator.get("Clone")} + div.project-details-summary + div#project-details-title Title + div#project-details-author + i(class="fa fa-user") + span author + div#project-details-likes likes + div#project-details-tags + div.tag tag1 + div.tag tag2 + div.dark.md#project-details-description Description + + div.project-details-comments + h3 #{translator.get("Comments")} + div#project-comment-list + div.comment + div.author + i.fa.fa-user + span author + div.contents comment contents + + div#post-project-comment + textarea(rows=4) + div.right + div.greenbutton#post-project-comment-button + i.fa.fa-envelope + span #{translator.get("Post")} + + div.redbutton#login-to-post-comment + i.fa.fa-user + span #{translator.get("Log in to post comments")} + + div.redbutton#validate-to-post-comment + i.fa.fa-check + span #{translator.get("Validate your email to post comments")} + +div.horizontal-splitbar + +div#project-details-contents + div.inside + div#project-contents-menu + li#project-contents-menu-code
#{translator.get("Code")} + li#project-contents-menu-sprites
#{translator.get("Sprites")} + li#project-contents-menu-sounds
#{translator.get("Sounds")} + li#project-contents-menu-music
#{translator.get("Music")} + //- li#project-contents-menu-maps
#{translator.get("Maps")} + li#project-contents-menu-doc
#{translator.get("Doc")} + + div#project-contents-view + div.code + div.code-list + div.import-button#project-contents-source-import + i(class="fa fa-download") + span Import source file + div#project-contents-view-editor + + div.sprites + div.item-list-container + div.sprite-list + div.export-panel + a + i.fa.fa-download + span #{translator.get("Export all in a ZIP file")} + + div.sounds + div.item-list-container + div.sound-list + + div.music + div.item-list-container + div.music-list + + div.doc + div.doc-render + div.import-button#project-contents-doc-import + i(class="fa fa-download") + span Import doc diff --git a/templates/projectoptions.pug b/templates/projectoptions.pug new file mode 100644 index 00000000..cd504c24 --- /dev/null +++ b/templates/projectoptions.pug @@ -0,0 +1,77 @@ +div.projectoptions#projectoptions + div#projectoptions-left + img#projectoptions-icon + + div#projectoptions-right + div#projectoptions-general + div#projectoptions-general-content + div.projectoption + h3 #{translator.get("Project Title")} + input#projectoption-name(type="text") + br + //span.tip #{translator.get("The display name of your project")} + + div.projectoption + h3 #{translator.get("Project Slug")} + span#projectoption-slugprefix microstudio.io/username/ + input.sluginput#projectoption-slug(type="text") + div.validate-button-container#project-slug-button + div.validate-button #{translator.get("Apply")} + + span / + br + // span.tip #{translator.get("The identifier used to build the URL of your project")} + + + div.projectoption + h3 #{translator.get("Project Secret Code")} + span.tip #{translator.get("Secret URL for testing your project when not set to public")} + br + span#projectoption-codeprefix microstudio.io/username/ + input.sluginput#projectoption-code(type="text") + div.validate-button-container#project-code-button + div.validate-button #{translator.get("Apply")} + + span / + br + + div.projectoption + h3 #{translator.get("Orientation")} + select#projectoption-orientation + option(value="any") #{translator.get("Any")} + option(value="portrait") #{translator.get("Portrait")} + option(value="landscape") #{translator.get("Landscape")} + + div.projectoption + h3 #{translator.get("Aspect ratio")} + select#projectoption-aspect + option(value="free") #{translator.get("Free")} + option(value="2x1") #{translator.get("Force 2:1")} + option(value="16x9") #{translator.get("Force 16:9")} + option(value="4x3") #{translator.get("Force 4:3")} + option(value="1x1") #{translator.get("Force 1:1 (square)")} + option(value=">2x1") #{translator.get("2:1 or above")} + option(value=">16x9") #{translator.get("16:9 or above")} + option(value=">4x3") #{translator.get("4:3 or above")} + option(value=">1x1") #{translator.get("1:1 or above")} + + + div.projectoption#project-option-graphics + h3 #{translator.get("Graphics library")} + select#projectoption-graphics + option(value="M1") Basic + option(value="M3D") micro3D + + div.projectoption#project-option-storage + h3 #{translator.get("Storage used")} + span#projectoption-storage-used + + div#projectoptions-users + div#projectoptions-users-content + h3 #{translator.get("Users")} + p #{translator.get("Here is where your grant other users access to edit your project. Users added here are granted full control on your project, except for adding or deleting other users.")} + div.userlist#project-user-list + + div.adduser + input.username#add-project-user-nick(placeholder=translator.get("nickname")) + div.addbutton#add-project-user #{translator.get("Add User")} diff --git a/templates/publish.pug b/templates/publish.pug new file mode 100644 index 00000000..1012f08c --- /dev/null +++ b/templates/publish.pug @@ -0,0 +1,108 @@ +div.publish-box + div.action-box#publish-box + div.publish-button#publish-button + i.fa.fa-eye + | #{translator.get("Make public")} + + div#publish-validate-first #{translator.get("You must validate your e-mail address before you can make your project public.")} + div.publish-text #{translator.get("This project is currently private.")} + + div.action-box#unpublish-box + div.publish-button#unpublish-button + i.fa.fa-eye-slash + | #{translator.get("Make private again")} + div.publish-text #{translator.get("This project is currently public.")} + + h2 #{translator.get("Make public on microStudio")} + p #{translator.get("Making your project public will allow it to be listed in the Explore section of the site. Public projects can be run and used by anyone. Making a project public allows anyone to view and possibly reuse the source code.")} + + h3 #{translator.get("Description")} + textarea#publish-box-textarea(rows=5 cols=60) + + h3 #{translator.get("Tags")} + input#publish-add-tags(type="text") + div.validate-button-container#publish-add-tags-button + div.validate-button #{translator.get("Add")} + + div#publish-tag-list + +div.publish-box + div.action-box#html-export + div.publish-button + i.fa.fa-download + | #{translator.get("Export to HTML5")} + + h2 #{translator.get("Export to HTML5")} + p #{translator.get("Exporting to HTML5 allows you to distribute your game as a web app or a standalone HTML app. Unzip contents, double-click index.html to start the game, or upload the contents to a subfolder of your existing web site.")} + p #{translator.get("You can also easily publish your HTML5 game on a number of platforms supporting HTML5 games. We have listed a few popular ones below, which you can check out:")} + + div.logos + a(href="https://itch.io" target="_blank") + img.logo(src="/img/itchio.svg" alt="itch.io" title="itch.io") + a(href="https://gamejolt.com" target="_blank") + img.logo(src="/img/gamejolt.png" alt="Gamejolt" title="Gamejolt") + a(href="https://gamedistribution.com" target="_blank") + img.logo(src="/img/gamedistribution.svg" alt="Game Distribution" title="Game Distribution") + a(href="https://kongregate.com" target="_blank") + img.logo(src="/img/kongregate.svg" alt="Kongregate" title="Kongregate") + a(href="https://gamepix.com" target="_blank") + img.logo(src="/img/gamepix.png" alt="Gamepix" title="Gamepix") + a(href="https://crazygames.com" target="_blank") + img.logo(src="/img/crazygames.svg" alt="CrazyGames" title="CrazyGames") + + +div.publish-box#publish-box-android + img.title(src="/img/android_robot.svg") + div.action-box#android-export + div.publish-button + i.fa.fa-wrench + | #{translator.get("Build for Android")} + + h2 Build for Android beta + p #{translator.get("This will build an APK file of your game. An APK file is what you need to publish your game on the Google Play Store, for Android devices.")} + div.clear + +div.publish-box#publish-box-windows + img.title(src="/img/windows.svg") + div.action-box#windows-export + div.publish-button + i.fa.fa-wrench + | #{translator.get("Build for Windows")} + + h2 Build for Windows beta + p #{translator.get("This will build your game as an executable for Windows (.EXE).")} + div.clear + + +div.publish-box#publish-box-macos + img.title(src="/img/macos.svg") + div.action-box#macos-export + div.publish-button + i.fa.fa-wrench + | #{translator.get("Build for macOS")} + + h2 Build for macOS beta + p #{translator.get("This will build your game as an executable for macOS.")} + div.clear + +div.publish-box#publish-box-linux + img.title(src="/img/ubuntu.svg") + div.action-box#linux-export + div.publish-button + i.fa.fa-wrench + | #{translator.get("Build for Linux")} + + h2 Build for Linux beta + p #{translator.get("This will build your game as an executable for Linux (Ubuntu, x86).")} + div.clear + +div.publish-box#publish-box-raspbian + img.title(src="/img/raspberry.svg") + div.action-box#raspbian-export + div.publish-button + i.fa.fa-wrench + | #{translator.get("Build for Raspberry Pi")} + + h2 Build for Raspberry Pi beta + p #{translator.get("This will build your game as an executable for Raspberry Pi (Raspbian).")} + div.clear diff --git a/templates/server.pug b/templates/server.pug new file mode 100644 index 00000000..e69de29b diff --git a/templates/sounds.pug b/templates/sounds.pug new file mode 100644 index 00000000..891be239 --- /dev/null +++ b/templates/sounds.pug @@ -0,0 +1,54 @@ +div.sounds-left + div.assets-drop + div + div #{translator.get("You can drop WAV files here")} + + div.create-asset-button#create-sound-button(style="display:none") #{translator.get("Add Sound")} + + div.assetlist#soundlist + div.asset-list#sound-list + +div.horizontal-splitbar#sounds-splitbar + +div.sounds-right + div.assetinfo#soundinfo + input(type="text" value="")#sound-name + div.validate-button-container#sound-name-button + div.validate-button #{translator.get("Apply")} + + div.buttons + div#undo-sound(title=translator.get("Undo")) + i.fa.fa-undo + div#redo-sound(title=translator.get("Redo")) + i.fa.fa-redo + + div#copy-sound(title=translator.get("Copy")) + i.fa.fa-copy + div#cut-sound(title=translator.get("Cut")) + i.fa.fa-cut + div#paste-sound(title=translator.get("Paste")) + i.fa.fa-paste + + div#delete-sound.delete(title=translator.get("Delete Sound")) + i.fa.fa-trash + + div#sound-editor-locked + div#sample-editor + div.placeholder #{translator.get("Drop your sounds on the left. A sleek sound creation app will land here in the future!")} + //- div#sample-transport-bar + //- div#sample-view + //- div#sample-zoom + //- i.fa.fa-search-plus#sample-zoom-plus(title=translator.get("Zoom in")) + //- br + //- i.fa.fa-search-minus#sample-zoom-minus(title=translator.get("Zoom out")) + + div#sample-tools + + div.floating-window#synth-window(style="display:none") + div.titlebar + div.title #{translator.get("Synthesizer")} + i.minify.fa.fa-compress + div.content + include synth.pug + //- div.navigation + //- i.resize.fa.fa-grip-horizontal diff --git a/templates/sprites.pug b/templates/sprites.pug new file mode 100644 index 00000000..18e1f557 --- /dev/null +++ b/templates/sprites.pug @@ -0,0 +1,102 @@ +div.sprites-left + div.assets-drop + div + div #{translator.get("You can drop PNG or JPEG files here")} + + div.create-sprite-button#create-sprite-button #{translator.get("Add Sprite")} + div.spritelist#spritelist + div.sprite-list#sprite-list + +div.sprites-splitbar + +div.sprites-right + div.spriteinfo#spriteinfo + input(type="text" value="")#sprite-name + div.validate-button-container#sprite-name-button + div.validate-button #{translator.get("Apply")} + input(type="text" value="32")#sprite-width + span x + input(type="text" value="32")#sprite-height + div.validate-button-container#sprite-size-button + div.validate-button #{translator.get("Apply")} + div.buttons + div#undo-sprite(title=translator.get("Undo")) + i.fa.fa-undo + div#redo-sprite(title=translator.get("Redo")) + i.fa.fa-redo + + div#copy-sprite(title=translator.get("Copy")) + i.fa.fa-copy + div#cut-sprite(title=translator.get("Cut")) + i.fa.fa-cut + div#paste-sprite(title=translator.get("Paste")) + i.fa.fa-paste + + div#delete-sprite(title=translator.get("Delete Sprite")) + i.fa.fa-trash + + div#sprite-editor-locked + div#spriteeditorcontainer.expanded + div.spriteeditor#spriteeditor(tabindex="0") + canvas + div#sprite-grab-info-container + div#sprite-grab-info + i.fa.fa-hand-paper + span #{translator.get("Hold down space bar to move view")} + div#sprite-zoom + i.fa.fa-search-plus#sprite-zoom-plus(title=translator.get("Zoom in")) + br + i.fa.fa-search-minus#sprite-zoom-minus(title=translator.get("Zoom out")) + + div#sprite-animation-title.collapsed + i.fa.fa-caret-down + span #{translator.get("Animation")} + + div#sprite-animation-panel.collapsed + div#sprite-animation-preview + canvas(width="80" height="80") + input(type="range") + + div#sprite-animation-steps + div#sprite-animation-list + + div.button#add-frame-button(title=translator.get("Add new Frame")) + i.fa.fa-plus-square + + div.spritebar#spritebar + div.spritetools#spritetools + div.spritetooloptions#spritetooloptions + div.spritetooloptionslist#spritetooloptionslist + + //h3 Opacity + //input(type="range" min="0" max="100" value="100")#sprite-tool-opacity + div#colorpicker-group + div.colorpicker#colorpicker + div.spritetoolbutton#eyedropper + i(class="fa fa-eye-dropper") + br + span #{translator.get("Eyedropper tool")} + div.auto-palette + div.auto-palette-title Palette + div#auto-palette-list + + div#selection-group + div.selection-hint#selection-hint-move ⇧ + #{translator.get("Move")} + div.selection-hint#selection-hint-clone Alt + #{translator.get("Clone")} + div.selection-operation#selection-operation-film
#{translator.get("Strip to animation")} + + div.spritehelpers + div.spritehelper#sprite-helper-tile + i(class="fa fa-th-large") + br + span Tile + + div.spritehelper#sprite-helper-vsymmetry + i(class="fa fa-arrows-alt-h") + br + span V Symmetry + + div.spritehelper#sprite-helper-hsymmetry + i(class="fa fa-arrows-alt-v") + br + span H Symmetry diff --git a/templates/synth.pug b/templates/synth.pug new file mode 100644 index 00000000..33aeb1ce --- /dev/null +++ b/templates/synth.pug @@ -0,0 +1,359 @@ +div.synth + div.modules + div.module.oscillator#oscillator1 + div.label Oscillator 1 + + div.buttongroup.inline + canvas.button#osc1-saw + canvas.button#osc1-square + canvas.button#osc1-sine + //- canvas.button#osc1-voice + //- canvas.button#osc1-string + + div.inline(style="width:10px") + div.inline + div.knobline + div.labelledknob + canvas.knob#osc1-amp + div.knoblabel Amp + + + div.labelledknob + canvas.knob#osc1-mod + div.knoblabel Mod + + div.knobline + div.labelledknob + canvas.knob#osc1-coarse(data-type="centered" data-quantize=48) + div.knoblabel Coarse + + div.labelledknob + canvas.knob#osc1-tune(data-type="centered") + div.knoblabel Tune + + + div.separator + div#combine-button + + + div.module.oscillator#oscillator2 + div.label Oscillator 2 + + div.buttongroup.inline + canvas.button#osc2-saw + canvas.button#osc2-square + canvas.button#osc2-sine + //- canvas.button#osc2-voice + //- canvas.button#osc2-string + + div.inline(style="width:10px") + + div.inline + div.knobline + div.labelledknob + canvas.knob#osc2-amp + div.knoblabel Amp + + + div.labelledknob + canvas.knob#osc2-mod + div.knoblabel Mod + + div.knobline + div.labelledknob + canvas.knob#osc2-coarse(data-type="centered" data-quantize=48) + div.knoblabel Coarse + + div.labelledknob + canvas.knob#osc2-tune(data-type="centered") + div.knoblabel Tune + + div.module#noise + div.label Noise + + div.knobline + div.labelledknob + canvas.knob#noise-amp + div.knoblabel Amp + + br + + div.labelledknob + canvas.knob#noise-mod + div.knoblabel Color + + div.module#filter + div.label Filter + + div.inline + canvas.button#filter-highpass + canvas.button#filter-bandpass + canvas.button#filter-lowpass + br + br + canvas.button#filter-slope + + div.inline(style="width:10px") + + div.inline + div.knobline + div.labelledknob + canvas.knob#filter-cutoff + div.knoblabel Cutoff + + div.labelledknob + canvas.knob#filter-resonance + div.knoblabel Resonance + + div.knobline + div.labelledknob + canvas.knob#filter-follow + div.knoblabel Key follow + + + div.module#envelope1 + div.label Amplitude Envelope + + div.labelledslider + canvas.synth-slider#env1-a + div.knoblabel Attack + + div.labelledslider + canvas.synth-slider#env1-d + div.knoblabel Decay + + div.labelledslider + canvas.synth-slider#env1-s + div.knoblabel Sustain + + div.labelledslider + canvas.synth-slider#env1-r + div.knoblabel Release + + br + + div.module#lfo1 + div.label LFO 1 + + div.buttongroup.inline + canvas.button#lfo1-sine + canvas.button#lfo1-triangle + canvas.button#lfo1-saw + canvas.button#lfo1-invsaw + + div.buttongroup.inline + canvas.button#lfo1-square + canvas.button#lfo1-random + canvas.button#lfo1-randomstep + + div.inline(style="width:21px") + + div.inline + div.center + div.selector#lfo1-out + i.fa.fa-caret-left + div.screen Filter Cutoff + i.fa.fa-caret-right + + div.knobline + div.labelledknob + canvas.knob#lfo1-rate + div.knoblabel Rate + canvas.button#lfo1-audio + + div.labelledknob + canvas.knob#lfo1-amount + div.knoblabel Amount + + + div.module#lfo2 + div.label LFO 2 + + div.buttongroup.inline + canvas.button#lfo2-sine + canvas.button#lfo2-triangle + canvas.button#lfo2-saw + canvas.button#lfo2-invsaw + + div.buttongroup.inline + canvas.button#lfo2-square + canvas.button#lfo2-random + canvas.button#lfo2-randomstep + + div.inline(style="width:21px") + + div.inline + div.center + div.selector#lfo2-out + i.fa.fa-caret-left + div.screen Filter Cutoff + i.fa.fa-caret-right + + div.knobline + div.labelledknob + canvas.knob#lfo2-rate + div.knoblabel Rate + canvas.button#lfo2-audio + + div.labelledknob + canvas.knob#lfo2-amount + div.knoblabel Amount + + + div.module#envelope2 + div.label Modulation Envelope + + div.labelledslider + canvas.synth-slider#env2-a + div.knoblabel Attack + + div.labelledslider + canvas.synth-slider#env2-d + div.knoblabel Decay + + div.labelledslider + canvas.synth-slider#env2-s + div.knoblabel Sustain + + div.labelledslider + canvas.synth-slider#env2-r + div.knoblabel Release + + div.inline(style="width:21px") + + div.inline + div.center + div.selector#env2-out + i.fa.fa-caret-left + div.screen Filter Cutoff + i.fa.fa-caret-right + + br + + div.labelledknob + canvas.knob#env2-amount(data-type="centered") + div.knoblabel Amount + + br + + div.module#polyphony + div.label Polyphony + + div.buttongroup.inline + canvas.button#polyphony-poly + canvas.button#polyphony-mono + br + canvas.button#sync + + div.inline(style="width:13px") + + div.labelledknob + canvas.knob#glide + div.knoblabel Glide + + div.module#velocity + div.label Velocity + + div.center(style="margin-top:10px") + div.selector#velocity-out + i.fa.fa-caret-left + div.screen OFF + i.fa.fa-caret-right + + div.knobline + div.labelledknob + canvas.knob#velocity-amp + div.knoblabel Amp + + div.labelledknob + canvas.knob#velocity-amount(data-type="centered") + div.knoblabel Amount + + + div.module#modulation + div.label Mod Wheel + + div.inline + div.center(style="margin-top:10px") + div.selector#modulation-out + i.fa.fa-caret-left + div.screen OFF + i.fa.fa-caret-right + + br + + div.labelledknob + canvas.knob#modulation-amount(data-type="centered") + div.knoblabel Amount + + div.module#fx1 + div.label FX 1 + + div.center(style="margin-top:10px") + div.selector#fx1-type + i.fa.fa-caret-left + div.screen Distortion + i.fa.fa-caret-right + + div.knobline + div.labelledknob + canvas.knob#fx1-amount + div.knoblabel Amount + + div.labelledknob + canvas.knob#fx1-rate + div.knoblabel Rate + + + div.module#fx2 + div.label FX 2 + + div.center(style="margin-top:10px") + div.selector#fx2-type + i.fa.fa-caret-left + div.screen Distortion + i.fa.fa-caret-right + + div.knobline + div.labelledknob + canvas.knob#fx2-amount + div.knoblabel Amount + + div.labelledknob + canvas.knob#fx2-rate + div.knoblabel Rate + + div.module#eq + div.label Mix + + div.center + div.knobline + div.labelledknob + canvas.knob#spatialize + div.knoblabel Spatialize + + div.labelledknob + canvas.knob#pan(data-type="centered") + div.knoblabel Pan + + div.knobline + div.labelledknob + canvas.knob#eq-low(data-type="centered") + div.knoblabel Low + + div.labelledknob + canvas.knob#eq-mid(data-type="centered") + div.knoblabel Mid + + div.labelledknob + canvas.knob#eq-high(data-type="centered") + div.knoblabel High + + div.synth-keyboard + canvas#synth-pitchwheel(width="40px" height = "100px") + canvas#synth-modwheel(width="40px" height = "100px") + canvas#synth-keyboard(width="700px" height="100px" style="margin-left:10px") + + //- div.layers + //- div.layer-list + //- div.layer LAYER 1 + //- div.add diff --git a/templates/tutorials.pug b/templates/tutorials.pug new file mode 100644 index 00000000..a0d5c2b4 --- /dev/null +++ b/templates/tutorials.pug @@ -0,0 +1,4 @@ +div.tutorials + h1 #{translator.get("Tutorials")} + h3 #{translator.get("Learn the basics of programming and how to create games with microStudio:")} + div#tutorials-content diff --git a/templates/user.pug b/templates/user.pug new file mode 100644 index 00000000..b8a0a5f3 --- /dev/null +++ b/templates/user.pug @@ -0,0 +1,114 @@ +div.usersettings#usersettings + div#usersettings-menu + div.selected#usersettings-menu-settings #{translator.get("Settings")} + i.fa.fa-cogs + br + div#usersettings-menu-profile #{translator.get("Profile")} + i.fas.fa-user-circle + //- br + //- div#usersettings-menu-progress #{translator.get("Stats & Achievements")} + + div.usersettings-page#usersettings-settings + h1 #{translator.get("User Settings")} + div.usersetting + h3 #{translator.get("Nickname")} + input#usersetting-nick(type="text") + div.validate-button-container#usersetting-nick-button + div.validate-button #{translator.get("Change nickname")} + div.error#usersetting-nick-error + div.tip #{translator.get("Your nickname will be displayed publicly.")} + + div.usersetting + h3 #{translator.get("E-mail Address")} + input#usersetting-email(type="text") + div.validate-button-container#usersetting-email-button + div.validate-button #{translator.get("Change e-mail")} + div.error#usersetting-email-error + br + + div.usersetting#email-not-validated + p #{translator.get("Your e-mail address is not validated yet. Check your e-mails and click on the validation link we sent to you. In case you did not receive the validation e-mail yet, use the button below to resend:")} + div.button#resend-validation-email #{translator.get("Resend validation e-mail")} + br + div#validation-email-resent(style="display: none;") #{translator.get("We have just resent a validation e-mail.")} + + //- div.usersetting + //- div.button#change-password #{translator.get("Change password")} + + div.usersetting + h3 Newsletter + input(type="checkbox" id="subscribe-newsletter") + label(for="subscribe-newsletter") #{translator.get("Receive newsletter")} + br + span.tip #{translator.get("We will send you an information email no more than once a month.")} + + div.usersetting + h3 #{translator.get("Account Type")} + div#usersettings-account-type + + div.usersetting + h3 #{translator.get("Storage")} + span#usersettings-storage + + div.usersetting#advanced + div.button#open-translation-app #{translator.get("Open translation tool")} + + + div.usersettings-page#usersettings-profile(style="display: none") + h1 #{translator.get("Your Profile")} + + div.usersetting + h3 #{translator.get("User Description")} + textarea#usersettings-profile-description + div.tip #{translator.get("Tell us more about yourself (only if you wish to). This text will be displayed on your public page.")} + + div.usersetting + h3 #{translator.get("Profile Image")} + div#usersettings-profile-image + i.fas.fa-user-circle + img(style="display:none") + i.fas.fa-times-circle(title=translator.get("Remove Profile Image")) + + div.tip #{translator.get("Drop your image here. This image will be displayed publicly, along with your nickname.")} + + div.usersetting + h3 #{translator.get("Check your public page")} + a#user-public-page(target="_blank") microstudio.io/ + div.tip #{translator.get("Your public page displays your nickname, description, profile image and a list of your public projects.")} + + + //- div.usersettings-page#usersettings-progress(style="display: none") + //- h1 #{translator.get("Your Progress")} + //- + //- h2 #{translator.get("Level")} + //- + //- table + //- tbody + //- tr + //- td Experience Points (XP) + //- td 38583 + //- tr + //- td Level + //- td 4 + //- + //- h2 #{translator.get("Statistics")} + //- + //- table + //- tbody + //- tr + //- td Pixels Drawn + //- td 38583 + //- tr + //- td Code Characters typed + //- td 48923 + //- + //- tr + //- td Code Lines Typed + //- td 1234 + //- + //- h2 #{translator.get("Achievements")} + +div.usersettings#translation-app + div + div#translation-app-back-button #{translator.get("Back")} + div#translation-app-contents