├── .editorconfig ├── .eslintrc.cjs ├── .github └── workflows │ ├── build.yml │ └── lint.yml ├── .gitignore ├── .np-config.json ├── .npmrc ├── .prettierignore ├── .prettierrc ├── .typesafe-i18n.json ├── LICENSE ├── README.md ├── api └── library.ts ├── docs └── CONTRIBUTE.md ├── jest.config.cjs ├── package.json ├── pnpm-lock.yaml ├── postcss.config.cjs ├── src-tauri ├── .gitignore ├── Cargo.lock ├── Cargo.toml ├── build.rs ├── icons │ ├── 128x128.png │ ├── 128x128@2x.png │ ├── 32x32.png │ ├── Square107x107Logo.png │ ├── Square142x142Logo.png │ ├── Square150x150Logo.png │ ├── Square284x284Logo.png │ ├── Square30x30Logo.png │ ├── Square310x310Logo.png │ ├── Square44x44Logo.png │ ├── Square71x71Logo.png │ ├── Square89x89Logo.png │ ├── StoreLogo.png │ ├── icon.icns │ ├── icon.ico │ └── icon.png ├── resources │ ├── banner.png │ └── welcome.png ├── src │ └── main.rs └── tauri.conf.json ├── src ├── app.html ├── assets │ ├── BPM.png │ ├── TimeSignature.png │ ├── cursor │ │ ├── grab-cursor.png │ │ ├── move-cursor.png │ │ ├── resize-cursor.png │ │ └── select-cursor.png │ ├── fever_end.png │ ├── fever_start.png │ ├── homepage │ │ ├── editing.png │ │ ├── filesupport.png │ │ ├── funtionality.png │ │ ├── funtionality0.png │ │ ├── funtionality2.png │ │ ├── screenshot.png │ │ └── soundeffects.png │ ├── notes │ │ ├── noteC.png │ │ ├── noteF.png │ │ ├── noteL.png │ │ ├── noteN.png │ │ ├── notes_flick_arrow_01.png │ │ ├── notes_flick_arrow_01_diagonal.png │ │ ├── notes_flick_arrow_02.png │ │ ├── notes_flick_arrow_02_diagonal.png │ │ ├── notes_flick_arrow_03.png │ │ ├── notes_flick_arrow_03_diagonal.png │ │ ├── notes_flick_arrow_04.png │ │ ├── notes_flick_arrow_04_diagonal.png │ │ ├── notes_flick_arrow_05.png │ │ ├── notes_flick_arrow_05_diagonal.png │ │ ├── notes_flick_arrow_06.png │ │ ├── notes_flick_arrow_06_diagonal.png │ │ ├── notes_flick_arrow_crtcl_01.png │ │ ├── notes_flick_arrow_crtcl_01_diagonal.png │ │ ├── notes_flick_arrow_crtcl_02.png │ │ ├── notes_flick_arrow_crtcl_02_diagonal.png │ │ ├── notes_flick_arrow_crtcl_03.png │ │ ├── notes_flick_arrow_crtcl_03_diagonal.png │ │ ├── notes_flick_arrow_crtcl_04.png │ │ ├── notes_flick_arrow_crtcl_04_diagonal.png │ │ ├── notes_flick_arrow_crtcl_05.png │ │ ├── notes_flick_arrow_crtcl_05_diagonal.png │ │ ├── notes_flick_arrow_crtcl_06.png │ │ ├── notes_flick_arrow_crtcl_06_diagonal.png │ │ ├── notes_long_among.png │ │ └── notes_long_among_crtcl.png │ ├── playhead.png │ ├── select.png │ ├── skill.png │ ├── sound │ │ ├── connect.mp3 │ │ ├── connect_critical.mp3 │ │ ├── critical_flick.mp3 │ │ ├── critical_tap.mp3 │ │ ├── critical_tick.mp3 │ │ ├── flick.mp3 │ │ ├── good.mp3 │ │ ├── great.mp3 │ │ ├── perfect.mp3 │ │ ├── stage.mp3 │ │ └── tick.mp3 │ └── vector │ │ ├── curve-in.svg │ │ ├── curve-out.svg │ │ ├── diamond │ │ ├── ignored.svg │ │ ├── invisible.svg │ │ └── visible.svg │ │ └── straight.svg ├── global.d.ts ├── hooks.ts ├── i18n │ ├── en │ │ └── index.ts │ ├── formatters.ts │ ├── i18n-svelte.ts │ ├── i18n-types.ts │ ├── i18n-util.async.ts │ ├── i18n-util.sync.ts │ ├── i18n-util.ts │ ├── ja │ │ └── index.ts │ ├── ko │ │ └── index.ts │ ├── metadata.ts │ └── zh │ │ └── index.ts ├── lib │ ├── Canvas.svelte │ ├── PropertyBox.svelte │ ├── ToolBox.svelte │ ├── ZoomIndicator.svelte │ ├── api │ │ └── library.ts │ ├── audio │ │ ├── AudioManager.svelte │ │ └── scheduler.ts │ ├── basic │ │ ├── collections.ts │ │ ├── debug.ts │ │ ├── file.spec.ts │ │ ├── file.ts │ │ └── math.ts │ ├── colors.ts │ ├── consts.ts │ ├── control │ │ ├── ControlHandler.svelte │ │ ├── keyboard.ts │ │ └── pointer.ts │ ├── database.ts │ ├── dialogs │ │ ├── AboutDialog.svelte │ │ ├── BPMDialog.svelte │ │ ├── CustomSnappingDialog.svelte │ │ ├── ImageDialog.svelte │ │ ├── LibraryDialog.svelte │ │ ├── PreferencesDialog.svelte │ │ ├── ProjectCard.svelte │ │ ├── ProjectsDialog.svelte │ │ ├── TimeSignatureDialog.svelte │ │ └── index.ts │ ├── editing │ │ ├── clipboard.ts │ │ ├── flick.ts │ │ ├── flip.ts │ │ ├── history.ts │ │ ├── modes.ts │ │ ├── moving.ts │ │ ├── mutations.ts │ │ ├── playhead.ts │ │ ├── resizing.ts │ │ ├── scrolling.ts │ │ ├── selection.ts │ │ ├── slides.ts │ │ └── visibility.ts │ ├── menus │ │ └── CanvasContextMenu.svelte │ ├── position.ts │ ├── preferences.ts │ ├── render │ │ ├── BPM.svelte │ │ ├── DraggingSlide.svelte │ │ ├── Fever.svelte │ │ ├── Floating.svelte │ │ ├── FloatingNotes.svelte │ │ ├── Grid.svelte │ │ ├── Minimap.svelte │ │ ├── MovingNotes.svelte │ │ ├── Note.svelte │ │ ├── NoteControl.svelte │ │ ├── NoteError.svelte │ │ ├── PastingNotes.svelte │ │ ├── Playhead.svelte │ │ ├── ResizingNotes.svelte │ │ ├── Scrollbar.svelte │ │ ├── Selection.svelte │ │ ├── Single.svelte │ │ ├── Skill.svelte │ │ ├── Slide.svelte │ │ ├── SlidePath.svelte │ │ ├── SlideSteps.svelte │ │ ├── minimap.ts │ │ ├── note.ts │ │ └── renderer.ts │ ├── score │ │ ├── beatmap.ts │ │ └── susIO.ts │ ├── style.css │ ├── timing.ts │ └── ui │ │ ├── Button.svelte │ │ ├── Checkbox.svelte │ │ ├── ClickableIcon.svelte │ │ ├── DebugInfo.svelte │ │ ├── FileInput.svelte │ │ ├── KeyboardShortcut.svelte │ │ ├── Menu.svelte │ │ ├── MenuDivider.svelte │ │ ├── MenuItem.svelte │ │ ├── MenuTrigger.svelte │ │ ├── MenuWrapper.svelte │ │ ├── Modal.svelte │ │ ├── Select.svelte │ │ ├── TabContent.svelte │ │ ├── TabItem.svelte │ │ ├── TabSelect.svelte │ │ ├── Tabs.svelte │ │ ├── TextInput.svelte │ │ ├── ToolButton.svelte │ │ ├── Tooltip.svelte │ │ ├── UndoToast.svelte │ │ ├── Wrapper.svelte │ │ └── toast.ts └── routes │ ├── __layout.svelte │ ├── edit.svelte │ └── index.svelte ├── static ├── assets │ ├── fonts │ │ ├── Font.fnt │ │ └── Font.png │ └── textures │ │ ├── path.png │ │ ├── path_critical.png │ │ ├── spritesheet.json │ │ └── spritesheet.png └── favicon.png ├── svelte.config.js ├── tsconfig.json └── tsconfig.spec.json /.editorconfig: -------------------------------------------------------------------------------- 1 | # Editor configuration, see http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | indent_style = space 7 | indent_size = 2 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | 11 | [*.md] 12 | max_line_length = off 13 | trim_trailing_whitespace = false 14 | -------------------------------------------------------------------------------- /.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | parser: '@typescript-eslint/parser', 4 | extends: ['eslint:recommended', 'plugin:@typescript-eslint/recommended'], 5 | plugins: ['svelte3', '@typescript-eslint', 'no-array-concat'], 6 | ignorePatterns: ['*.cjs'], 7 | overrides: [{ files: ['*.svelte'], processor: 'svelte3/svelte3' }], 8 | settings: { 9 | 'svelte3/typescript': () => require('typescript'), 10 | }, 11 | parserOptions: { 12 | sourceType: 'module', 13 | ecmaVersion: 2019, 14 | project: './tsconfig.json', 15 | }, 16 | env: { 17 | browser: true, 18 | es2017: true, 19 | node: true, 20 | }, 21 | rules: { 22 | '@typescript-eslint/no-inferrable-types': 'off', 23 | '@typescript-eslint/no-non-null-assertion': 'off', 24 | 'no-restricted-globals': [ 25 | 'error', 26 | 'closed', 27 | 'event', 28 | 'fdescribe', 29 | 'name', 30 | 'length', 31 | 'location', 32 | 'parent', 33 | 'top', 34 | ], 35 | 'no-redeclare': 'off', 36 | 'no-import-assign': 'off', 37 | 'no-console': ['error', { allow: ['warn', 'error'] }], 38 | 'array-callback-return': 'error', 39 | 'no-array-concat/no-array-concat': 'error', 40 | }, 41 | } 42 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build tauri 2 | 3 | on: 4 | push: 5 | tags: 6 | - v* 7 | 8 | jobs: 9 | publish-tauri: 10 | strategy: 11 | fail-fast: false 12 | matrix: 13 | platform: [macos-latest, ubuntu-latest, windows-latest] 14 | 15 | runs-on: ${{ matrix.platform }} 16 | steps: 17 | - uses: actions/checkout@v2 18 | - name: setup node 19 | uses: actions/setup-node@v4 20 | with: 21 | node-version: 18 22 | - name: install Rust stable 23 | uses: actions-rs/toolchain@v1 24 | with: 25 | toolchain: stable 26 | - name: install webkit2gtk (ubuntu only) 27 | if: matrix.platform == 'ubuntu-latest' 28 | run: | 29 | sudo apt-get update 30 | sudo apt-get install -y webkit2gtk-4.0 31 | - name: Setup pnpm 32 | uses: pnpm/action-setup@v2.1.0 33 | with: 34 | version: 6.0.2 35 | run_install: true 36 | - name: install app dependencies and build it 37 | run: pnpm build 38 | - uses: jdukewich/tauri-action@fix-windows-bundling 39 | env: 40 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 41 | with: 42 | tagName: v__VERSION__ # the action automatically replaces \_\_VERSION\_\_ with the app version 43 | releaseName: "PaletteWorks Editor v__VERSION__" 44 | releaseBody: "**Assets** からダウンロードしてインストールしてください。\n請從 **Assets** 文件下載安裝。\n**Assets** 에서 다운로드하여 설치하십시오.\nSee the **Assets** to download this version and install.\n**Assets**\n\n**Windows** .exe / .msi\n**Linux** .AppImage / .deb\n**macOS** .dmg\n\n**[Release Note](https://paletteworks.notion.site/PaletteWorks-Editor-7571ec4cffd4465f95ec0ff406bed54f)**" 45 | releaseDraft: true 46 | prerelease: false 47 | -------------------------------------------------------------------------------- /.github/workflows/lint.yml: -------------------------------------------------------------------------------- 1 | name: ESLint 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | pull_request: 7 | branches: [ main ] 8 | 9 | workflow_dispatch: 10 | 11 | jobs: 12 | lint: 13 | runs-on: ubuntu-latest 14 | steps: 15 | - name: Checkout Repository 16 | uses: actions/checkout@v2 17 | - name: Setup Node.js 18 | uses: actions/setup-node@v2 19 | - name: Setup pnpm 20 | uses: pnpm/action-setup@v2.0.1 21 | with: 22 | version: 6.0.2 23 | run_install: true 24 | - name: Run ESLint 25 | run: pnpm lint 26 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | /.svelte-kit 4 | /package 5 | 6 | temp/* 7 | 8 | .vercel_build_output 9 | .svelte-kit 10 | 11 | .env.local 12 | 13 | .vscode 14 | .vercel 15 | 16 | coverage 17 | 18 | build -------------------------------------------------------------------------------- /.np-config.json: -------------------------------------------------------------------------------- 1 | { 2 | "yarn": false, 3 | "cleanup": false, 4 | "tests": false, 5 | "branch": "main", 6 | "message": "V: Release v%s" 7 | } 8 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | engine-strict=true 2 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | .github/** 2 | .svelte-kit/** 3 | .vercel/** 4 | .vscode/** 5 | .vercel_build_output 6 | static/** 7 | build/** 8 | coverage/** 9 | node_modules/** 10 | static/** 11 | temp/** 12 | src-tauri/** 13 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "semi": false 4 | } 5 | -------------------------------------------------------------------------------- /.typesafe-i18n.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://unpkg.com/typesafe-i18n@4.2.1/schema/typesafe-i18n.json", 3 | "baseLocale": "ja", 4 | "adapter": "svelte", 5 | "outputPath": "./src/i18n", 6 | "runAfterGenerator": "prettier --write src/i18n" 7 | } 8 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 mkpoli 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # PaletteWorks Editor 2 | 3 | > [!IMPORTANT] 4 | > ✋ Hi! I am *mkpoli*, the author of **PaletteWorks Editor**. I'm really sorry for not communicating for such a long time — life took over with various responsibilities, including academic work and some health challenges. 5 | > 6 | > ⏳ This project has been inactive since 2022, but it's not fully abandoned. I may occasionally update it when I am motivated and well enough. 7 | > 8 | > 🙏 I genuinely appreciate everyone who’s shown interest, and I’m always open to feedback, discussions, or pull requests — as long as I’m feeling healthy and capable enough to respond. 9 | > 10 | > 🤝 If someone is motivated to continue developing PaletteWorks Editor, I’d be more than happy and honored to see it live on, and I’d gladly support however I can. 11 | > 12 | > 🔄 If you’re looking for similar tools or more actively maintained projects, there are modern alternative softwares such as [**MikuMikuWorld4CC**](https://github.com/sevenc-nanashi/MikuMikuWorld4CC). 13 | > 14 | > 2025-04-01 15 | 16 | 「プロジェクトセカイ カラフルステージ! feat. 初音ミク」の譜面エディターです。創作譜面をより作りやすくすることを目指しているものであります。 17 | 18 | 本程序爲「世界計劃:繽紛舞臺 feat. 初音未來」的自製譜面編輯器,目標是提供更好的創作體驗。 19 | 20 | “프로젝트 세카이 컬러풀 스테이지! feat.하츠네 미쿠” 의 자작채보 에디터입니다. 채보를 더 만들기 쉽게 하는 것을 목표로 하고 있습니다. 21 | (“프로젝트 세카이 컬러풀 스테이지! feat.하츠네 미쿠” 의 自作採譜 에디터입니다. 採譜를 더 만들기 쉽게 하는 것을 目標로 하고 있습니다.) 22 | 23 | ## Acknowledgements / 謝辞 / 致謝 / 사사(謝辭) 24 | 25 | ### Inspiration / インスピレーション / 啓發 / 영감(靈感) 26 | 27 | - [Ched](https://github.com/paralleltree/Ched) 28 | 29 | ### Special Thanks / スペシャルサンクス / 特別感謝 / 특별히 감사 (特別히 感謝) 30 | 31 | -  [Burrito](https://github.com/NonSpicyBurrito)  32 | -  [紫式部](https://twitter.com/purplepalettech) 33 | -  [お窓](https://github.com/Dosugamea) 34 | -  [名無し。](https://github.com/sevenc-nanashi) 35 | -  [よこしま。](https://www.youtube.com/c/よこしま) 36 | -  [トトロっぽい何か](https://youtube.com/c/トトロっぽい何か) 37 | -  jch 38 | -  Sora 39 | 40 | and many others from 紫式部 / Sonolus Discord ... 41 | 42 | ### Contributors / コントリビューター / 貢獻者 / 기여자(寄與者) 43 | 44 | 45 | 46 | 47 | 48 | Made with [contrib.rocks](https://contrib.rocks). 49 | 50 | ## [Contribution Guide / コントリビュート方法 / 貢獻指南 / 기여하는 방법 (寄與하는 方法)](docs/CONTRIBUTE.md) 51 | 52 | ## LICENSE / ライセンス / 授權 / 사용권(使用權) 53 | 54 | MIT © 2021 mkpoli 55 | -------------------------------------------------------------------------------- /api/library.ts: -------------------------------------------------------------------------------- 1 | import type { VercelRequest, VercelResponse } from '@vercel/node' 2 | 3 | import { createClient } from '@libsql/client/web' 4 | import type { Single, Slide } from '$lib/score/beatmap' 5 | 6 | type LocaleStrings = { [key: string]: string } 7 | 8 | export type Item = { 9 | title: LocaleStrings 10 | description: LocaleStrings 11 | content: { 12 | singles?: Single[] 13 | slides?: Slide[] 14 | } 15 | } 16 | 17 | const client = createClient({ 18 | url: process.env['TURSO_DATABASE_URL']!, 19 | authToken: process.env['TURSO_AUTH_TOKEN']!, 20 | }) 21 | 22 | export async function list(): Promise { 23 | const res = await client.execute( 24 | 'SELECT title_ja, description_ja, content_json FROM items' 25 | ) 26 | 27 | return res.rows.map((row) => ({ 28 | title: { ja: row.title_ja as string }, 29 | description: { ja: row.description_ja as string }, 30 | content: JSON.parse(row.content_json as string), 31 | })) 32 | } 33 | 34 | export async function create(item: Item) { 35 | await client.execute({ 36 | sql: ` 37 | INSERT INTO items ( 38 | id, collection, ts_iso, title_ja, description_ja, content_json 39 | ) VALUES (?, ?, ?, ?, ?, ?) 40 | `, 41 | args: [ 42 | crypto.randomUUID(), 43 | 'items', 44 | new Date().toISOString(), 45 | item.title?.ja ?? null, 46 | item.description?.ja ?? null, 47 | JSON.stringify(item.content), 48 | ], 49 | }) 50 | } 51 | 52 | export default async (request: VercelRequest, response: VercelResponse) => { 53 | try { 54 | if (request.method === 'GET') { 55 | const data = await list() 56 | response.status(200).json(data) 57 | } else if (request.method === 'POST') { 58 | const data = await create(request.body) 59 | response.status(200).json(data) 60 | } else { 61 | response.status(405).end() 62 | } 63 | } catch (error) { 64 | console.error('❌ API error:', error) 65 | response.status(500).json({ message: 'Internal Server Error' }) 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /docs/CONTRIBUTE.md: -------------------------------------------------------------------------------- 1 | # Contribution Guide / コントリビュート方法 / 貢獻指南 / 寄與하는 方法 2 | 3 | ## Developing / 開發 4 | 5 | ```bash 6 | pnpm install 7 | pnpm run dev -- --open 8 | pnpm run typesafe-i18n 9 | ``` 10 | 11 | ### Tauri Build 12 | 13 | ```bash 14 | pnpm run tauri dev 15 | ``` 16 | ## Releasing / 發佈 17 | 18 | 1. Clear current tree (commit changes) 19 | 2. Run pre-release check 20 | ```bash 21 | pnpm run beforeRelease 22 | ``` 23 | 3. Bump version numbers 24 | ```bash 25 | pnpx @rstacruz/bump-cli package.json src-tauri/Cargo.toml src-tauri/tauri.conf.json # -M / -m / -p 26 | cargo update --manifest-path=src-tauri/Cargo.toml 27 | ``` 28 | 3. Commit ``V: Release v`` 29 | ```bash 30 | git add package.json src-tauri/Cargo.toml src-tauri/Cargo.lock src-tauri/tauri.conf.json 31 | git commit -m "V: Release v" 32 | ``` 33 | 5. Tag the commit above 34 | ``` 35 | pnpm run tag 36 | ``` 37 | 5. Push to Remote 38 | ``` 39 | git push --follow-tags 40 | ``` 41 | 42 | ### Commit Message Naming Convention / コミットメッセージ命名規則 / 提交消息命名规则 / 커밋 메시지 명명 규칙 (커밋 메시지 命名 規則) 43 | 44 | ```regex 45 | (F|R|D|S|V|I): 46 | ``` 47 | 48 | - **F** for **F**eature (Additions, Fixes, Ajustments of functionalities, etc.) 49 | - **T** for **T**esting (New tests / specs, Test refactoring, etc.) 50 | - **R** for **R**efactor (Adjustments of code structure, naming, typing, comments, etc.) 51 | - **D** for **D**ocumentation (Documentation, README, etc.) 52 | - **S** for **S**tyle (Styling, Visual Design Adjustments, etc.) 53 | - **V** for **V**ersion (Versioning, Dependencies, Licensing, etc.) 54 | - **C** for **C**onfiguration (Building, Linting, CLI Tooling, etc.) 55 | - **I** for **I**18n (Translation, Localisation, etc.) 56 | -------------------------------------------------------------------------------- /jest.config.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | preset: 'ts-jest', 3 | globals: { 4 | 'ts-jest': { 5 | tsconfig: './tsconfig.spec.json', 6 | }, 7 | }, 8 | testPathIgnorePatterns: [ 9 | '/node_modules/', 10 | '/build/', 11 | '/temp', 12 | ] 13 | } 14 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "paletteworks", 3 | "private": true, 4 | "version": "1.11.3", 5 | "scripts": { 6 | "dev": "svelte-kit dev", 7 | "clean": "rimraf build", 8 | "build:svelte-kit": "svelte-kit build", 9 | "build": "run-s clean build:svelte-kit", 10 | "preview": "svelte-kit preview", 11 | "test": "jest", 12 | "check": "svelte-check --tsconfig ./tsconfig.json", 13 | "check:watch": "svelte-check --tsconfig ./tsconfig.json --watch", 14 | "lint": "eslint --ignore-path .gitignore src", 15 | "typesafe-i18n": "typesafe-i18n", 16 | "beforeRelease": "run-s check lint test", 17 | "tag": "taggit", 18 | "format": "prettier --write --plugin-search-dir=. .", 19 | "tauri": "tauri" 20 | }, 21 | "license": "MIT", 22 | "devDependencies": { 23 | "@iconify/svelte": "2.1.2", 24 | "@mszu/pixi-ssr-shim": "1.0.2", 25 | "@pixi/events": "6.2.2", 26 | "@rstacruz/bump-cli": "2.0.1", 27 | "@sveltejs/adapter-static": "1.0.0-next.28", 28 | "@sveltejs/adapter-vercel": "1.0.0-next.43", 29 | "@sveltejs/kit": "1.0.0-next.278", 30 | "@tauri-apps/cli": "1.0.0", 31 | "@types/core-js": "2.5.5", 32 | "@types/jest": "27.4.1", 33 | "@types/msgpack-lite": "0.1.8", 34 | "@types/throttle-debounce": "2.1.0", 35 | "@typescript-eslint/eslint-plugin": "5.12.1", 36 | "@typescript-eslint/parser": "5.12.1", 37 | "@vercel/node": "1.13.0", 38 | "@zerodevx/svelte-toast": "0.7.0", 39 | "attr-accept": "2.2.2", 40 | "autoprefixer": "10.4.2", 41 | "bpm-detective": "2.0.5", 42 | "core-js": "3.21.1", 43 | "dexie": "3.2.1", 44 | "dexie-export-import": "1.0.3", 45 | "eslint": "8.9.0", 46 | "eslint-plugin-no-array-concat": "0.1.2", 47 | "eslint-plugin-svelte3": "3.4.0", 48 | "filesize": "8.0.7", 49 | "focus-trap": "6.7.3", 50 | "hotkeys-js": "3.8.7", 51 | "jest": "27.5.1", 52 | "msgpack-lite": "0.1.26", 53 | "npm-run-all": "4.1.5", 54 | "pixi.js": "~6.2.2", 55 | "postcss-viewport-height-correction": "1.1.1", 56 | "prettier": "2.6.0", 57 | "prettier-plugin-svelte": "2.6.0", 58 | "rimraf": "3.0.2", 59 | "sus-analyzer": "2.2.4", 60 | "sus-io": "1.1.0", 61 | "svelte": "3.46.4", 62 | "svelte-check": "2.4.5", 63 | "svelte-preprocess": "4.10.3", 64 | "taggit": "2.0.3", 65 | "throttle-debounce": "3.0.1", 66 | "tippy.js": "6.3.7", 67 | "ts-jest": "27.1.3", 68 | "tslib": "2.0.0", 69 | "typesafe-i18n": "4.2.1", 70 | "typescript": "4.5.5" 71 | }, 72 | "type": "module", 73 | "dependencies": { 74 | "@libsql/client": "^0.15.1", 75 | "@sveltejs/vite-plugin-svelte": "1.0.0-next.32", 76 | "@tauri-apps/api": "1.0.0-rc.1" 77 | }, 78 | "packageManager": "pnpm@9.10.0+sha1.216899f511c8dfde183c7cb50b69009c779534a8" 79 | } 80 | -------------------------------------------------------------------------------- /postcss.config.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: [ 3 | require('autoprefixer'), 4 | require('postcss-viewport-height-correction'), 5 | ], 6 | } 7 | -------------------------------------------------------------------------------- /src-tauri/.gitignore: -------------------------------------------------------------------------------- 1 | # Generated by Cargo 2 | # will have compiled files and executables 3 | /target/ 4 | WixTools 5 | -------------------------------------------------------------------------------- /src-tauri/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "paletteworks" 3 | version = "1.11.3" 4 | description = "A score editor for Project Sekai: Colourful Stage feat. Hatsune Miku" 5 | authors = ["mkpoli "] 6 | license = "MIT" 7 | repository = "https://github.com/mkpoli/paletteworks-editor" 8 | default-run = "paletteworks" 9 | edition = "2021" 10 | rust-version = "1.57" 11 | 12 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 13 | 14 | [build-dependencies] 15 | tauri-build = { version = "1.0.0", features = [] } 16 | 17 | [dependencies] 18 | serde_json = "1.0" 19 | serde = { version = "1.0", features = ["derive"] } 20 | tauri = { version = "1.0.0", features = ["dialog-all", "http-all", "shell-open"] } 21 | 22 | [features] 23 | # by default Tauri runs in production mode 24 | # when `tauri dev` runs it is executed with `cargo run --no-default-features` if `devPath` is an URL 25 | default = [ "custom-protocol" ] 26 | # this feature is used used for production builds where `devPath` points to the filesystem 27 | # DO NOT remove this 28 | custom-protocol = [ "tauri/custom-protocol" ] 29 | -------------------------------------------------------------------------------- /src-tauri/build.rs: -------------------------------------------------------------------------------- 1 | fn main() { 2 | tauri_build::build() 3 | } 4 | -------------------------------------------------------------------------------- /src-tauri/icons/128x128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mkpoli/paletteworks-editor/bf268907e8343ce58c61cdced64c75a6fba25c50/src-tauri/icons/128x128.png -------------------------------------------------------------------------------- /src-tauri/icons/128x128@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mkpoli/paletteworks-editor/bf268907e8343ce58c61cdced64c75a6fba25c50/src-tauri/icons/128x128@2x.png -------------------------------------------------------------------------------- /src-tauri/icons/32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mkpoli/paletteworks-editor/bf268907e8343ce58c61cdced64c75a6fba25c50/src-tauri/icons/32x32.png -------------------------------------------------------------------------------- /src-tauri/icons/Square107x107Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mkpoli/paletteworks-editor/bf268907e8343ce58c61cdced64c75a6fba25c50/src-tauri/icons/Square107x107Logo.png -------------------------------------------------------------------------------- /src-tauri/icons/Square142x142Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mkpoli/paletteworks-editor/bf268907e8343ce58c61cdced64c75a6fba25c50/src-tauri/icons/Square142x142Logo.png -------------------------------------------------------------------------------- /src-tauri/icons/Square150x150Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mkpoli/paletteworks-editor/bf268907e8343ce58c61cdced64c75a6fba25c50/src-tauri/icons/Square150x150Logo.png -------------------------------------------------------------------------------- /src-tauri/icons/Square284x284Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mkpoli/paletteworks-editor/bf268907e8343ce58c61cdced64c75a6fba25c50/src-tauri/icons/Square284x284Logo.png -------------------------------------------------------------------------------- /src-tauri/icons/Square30x30Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mkpoli/paletteworks-editor/bf268907e8343ce58c61cdced64c75a6fba25c50/src-tauri/icons/Square30x30Logo.png -------------------------------------------------------------------------------- /src-tauri/icons/Square310x310Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mkpoli/paletteworks-editor/bf268907e8343ce58c61cdced64c75a6fba25c50/src-tauri/icons/Square310x310Logo.png -------------------------------------------------------------------------------- /src-tauri/icons/Square44x44Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mkpoli/paletteworks-editor/bf268907e8343ce58c61cdced64c75a6fba25c50/src-tauri/icons/Square44x44Logo.png -------------------------------------------------------------------------------- /src-tauri/icons/Square71x71Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mkpoli/paletteworks-editor/bf268907e8343ce58c61cdced64c75a6fba25c50/src-tauri/icons/Square71x71Logo.png -------------------------------------------------------------------------------- /src-tauri/icons/Square89x89Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mkpoli/paletteworks-editor/bf268907e8343ce58c61cdced64c75a6fba25c50/src-tauri/icons/Square89x89Logo.png -------------------------------------------------------------------------------- /src-tauri/icons/StoreLogo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mkpoli/paletteworks-editor/bf268907e8343ce58c61cdced64c75a6fba25c50/src-tauri/icons/StoreLogo.png -------------------------------------------------------------------------------- /src-tauri/icons/icon.icns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mkpoli/paletteworks-editor/bf268907e8343ce58c61cdced64c75a6fba25c50/src-tauri/icons/icon.icns -------------------------------------------------------------------------------- /src-tauri/icons/icon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mkpoli/paletteworks-editor/bf268907e8343ce58c61cdced64c75a6fba25c50/src-tauri/icons/icon.ico -------------------------------------------------------------------------------- /src-tauri/icons/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mkpoli/paletteworks-editor/bf268907e8343ce58c61cdced64c75a6fba25c50/src-tauri/icons/icon.png -------------------------------------------------------------------------------- /src-tauri/resources/banner.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mkpoli/paletteworks-editor/bf268907e8343ce58c61cdced64c75a6fba25c50/src-tauri/resources/banner.png -------------------------------------------------------------------------------- /src-tauri/resources/welcome.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mkpoli/paletteworks-editor/bf268907e8343ce58c61cdced64c75a6fba25c50/src-tauri/resources/welcome.png -------------------------------------------------------------------------------- /src-tauri/src/main.rs: -------------------------------------------------------------------------------- 1 | #![cfg_attr( 2 | all(not(debug_assertions), target_os = "windows"), 3 | windows_subsystem = "windows" 4 | )] 5 | 6 | fn main() { 7 | tauri::Builder::default() 8 | .invoke_handler(tauri::generate_handler![write_file]) 9 | .run(tauri::generate_context!()) 10 | .expect("error while running tauri application"); 11 | } 12 | 13 | #[tauri::command] 14 | fn write_file(path: &str, data: Vec) { 15 | println!("writing file {}", path); 16 | std::fs::write(path, data).expect("error while writing file"); 17 | } 18 | -------------------------------------------------------------------------------- /src-tauri/tauri.conf.json: -------------------------------------------------------------------------------- 1 | { 2 | "package": { 3 | "productName": "PaletteWorks Editor", 4 | "version": "1.11.3" 5 | }, 6 | "build": { 7 | "distDir": "../build", 8 | "devPath": "http://localhost:3000", 9 | "beforeDevCommand": "pnpm dev", 10 | "beforeBuildCommand": "pnpm build", 11 | "withGlobalTauri": true 12 | }, 13 | "tauri": { 14 | "bundle": { 15 | "active": true, 16 | "targets": "all", 17 | "identifier": "li.mkpo.paletteworks", 18 | "icon": [ 19 | "icons/32x32.png", 20 | "icons/128x128.png", 21 | "icons/128x128@2x.png", 22 | "icons/icon.icns", 23 | "icons/icon.ico" 24 | ], 25 | "resources": [], 26 | "externalBin": [], 27 | "copyright": "mkpoli 2021-2022", 28 | "category": "Productivity", 29 | "shortDescription": "A score editor for Project Sekai: Colourful Stage feat. Hatsune Miku", 30 | "longDescription": "This is a score editor for Project Sekai: Colorful Stage feat. Hatsune Miku. It aims to provide a better user experience to create custom maps.", 31 | "deb": { 32 | "depends": [] 33 | }, 34 | "macOS": { 35 | "frameworks": [], 36 | "minimumSystemVersion": "", 37 | "exceptionDomain": "", 38 | "signingIdentity": null, 39 | "providerShortName": null, 40 | "entitlements": null 41 | }, 42 | "windows": { 43 | "certificateThumbprint": null, 44 | "digestAlgorithm": "sha256", 45 | "timestampUrl": "", 46 | "wix": { 47 | "bannerPath": "./resources/banner.png", 48 | "dialogImagePath": "./resources/welcome.png", 49 | "language": ["en-US", "zh-CN", "zh-TW", "jp-JP", "ko-KO"] 50 | } 51 | } 52 | }, 53 | "updater": { 54 | "active": false 55 | }, 56 | "allowlist": { 57 | "http": { 58 | "all": true, 59 | "request": true, 60 | "scope": ["https://paletteworks.mkpo.li/*"] 61 | }, 62 | "dialog": { 63 | "all": true 64 | }, 65 | "shell": { 66 | "open": true 67 | } 68 | }, 69 | "windows": [ 70 | { 71 | "url": "/edit.html", 72 | "title": "PaletteWorks Editor", 73 | "width": 1024, 74 | "height": 768, 75 | "resizable": true, 76 | "fullscreen": false, 77 | "fileDropEnabled": false, 78 | "maximized": true 79 | } 80 | ], 81 | "security": { 82 | "csp": null 83 | } 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /src/app.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | %svelte.head% 8 | 9 | 10 | %svelte.body% 11 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /src/assets/BPM.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mkpoli/paletteworks-editor/bf268907e8343ce58c61cdced64c75a6fba25c50/src/assets/BPM.png -------------------------------------------------------------------------------- /src/assets/TimeSignature.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mkpoli/paletteworks-editor/bf268907e8343ce58c61cdced64c75a6fba25c50/src/assets/TimeSignature.png -------------------------------------------------------------------------------- /src/assets/cursor/grab-cursor.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mkpoli/paletteworks-editor/bf268907e8343ce58c61cdced64c75a6fba25c50/src/assets/cursor/grab-cursor.png -------------------------------------------------------------------------------- /src/assets/cursor/move-cursor.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mkpoli/paletteworks-editor/bf268907e8343ce58c61cdced64c75a6fba25c50/src/assets/cursor/move-cursor.png -------------------------------------------------------------------------------- /src/assets/cursor/resize-cursor.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mkpoli/paletteworks-editor/bf268907e8343ce58c61cdced64c75a6fba25c50/src/assets/cursor/resize-cursor.png -------------------------------------------------------------------------------- /src/assets/cursor/select-cursor.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mkpoli/paletteworks-editor/bf268907e8343ce58c61cdced64c75a6fba25c50/src/assets/cursor/select-cursor.png -------------------------------------------------------------------------------- /src/assets/fever_end.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mkpoli/paletteworks-editor/bf268907e8343ce58c61cdced64c75a6fba25c50/src/assets/fever_end.png -------------------------------------------------------------------------------- /src/assets/fever_start.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mkpoli/paletteworks-editor/bf268907e8343ce58c61cdced64c75a6fba25c50/src/assets/fever_start.png -------------------------------------------------------------------------------- /src/assets/homepage/editing.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mkpoli/paletteworks-editor/bf268907e8343ce58c61cdced64c75a6fba25c50/src/assets/homepage/editing.png -------------------------------------------------------------------------------- /src/assets/homepage/filesupport.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mkpoli/paletteworks-editor/bf268907e8343ce58c61cdced64c75a6fba25c50/src/assets/homepage/filesupport.png -------------------------------------------------------------------------------- /src/assets/homepage/funtionality.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mkpoli/paletteworks-editor/bf268907e8343ce58c61cdced64c75a6fba25c50/src/assets/homepage/funtionality.png -------------------------------------------------------------------------------- /src/assets/homepage/funtionality0.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mkpoli/paletteworks-editor/bf268907e8343ce58c61cdced64c75a6fba25c50/src/assets/homepage/funtionality0.png -------------------------------------------------------------------------------- /src/assets/homepage/funtionality2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mkpoli/paletteworks-editor/bf268907e8343ce58c61cdced64c75a6fba25c50/src/assets/homepage/funtionality2.png -------------------------------------------------------------------------------- /src/assets/homepage/screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mkpoli/paletteworks-editor/bf268907e8343ce58c61cdced64c75a6fba25c50/src/assets/homepage/screenshot.png -------------------------------------------------------------------------------- /src/assets/homepage/soundeffects.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mkpoli/paletteworks-editor/bf268907e8343ce58c61cdced64c75a6fba25c50/src/assets/homepage/soundeffects.png -------------------------------------------------------------------------------- /src/assets/notes/noteC.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mkpoli/paletteworks-editor/bf268907e8343ce58c61cdced64c75a6fba25c50/src/assets/notes/noteC.png -------------------------------------------------------------------------------- /src/assets/notes/noteF.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mkpoli/paletteworks-editor/bf268907e8343ce58c61cdced64c75a6fba25c50/src/assets/notes/noteF.png -------------------------------------------------------------------------------- /src/assets/notes/noteL.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mkpoli/paletteworks-editor/bf268907e8343ce58c61cdced64c75a6fba25c50/src/assets/notes/noteL.png -------------------------------------------------------------------------------- /src/assets/notes/noteN.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mkpoli/paletteworks-editor/bf268907e8343ce58c61cdced64c75a6fba25c50/src/assets/notes/noteN.png -------------------------------------------------------------------------------- /src/assets/notes/notes_flick_arrow_01.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mkpoli/paletteworks-editor/bf268907e8343ce58c61cdced64c75a6fba25c50/src/assets/notes/notes_flick_arrow_01.png -------------------------------------------------------------------------------- /src/assets/notes/notes_flick_arrow_01_diagonal.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mkpoli/paletteworks-editor/bf268907e8343ce58c61cdced64c75a6fba25c50/src/assets/notes/notes_flick_arrow_01_diagonal.png -------------------------------------------------------------------------------- /src/assets/notes/notes_flick_arrow_02.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mkpoli/paletteworks-editor/bf268907e8343ce58c61cdced64c75a6fba25c50/src/assets/notes/notes_flick_arrow_02.png -------------------------------------------------------------------------------- /src/assets/notes/notes_flick_arrow_02_diagonal.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mkpoli/paletteworks-editor/bf268907e8343ce58c61cdced64c75a6fba25c50/src/assets/notes/notes_flick_arrow_02_diagonal.png -------------------------------------------------------------------------------- /src/assets/notes/notes_flick_arrow_03.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mkpoli/paletteworks-editor/bf268907e8343ce58c61cdced64c75a6fba25c50/src/assets/notes/notes_flick_arrow_03.png -------------------------------------------------------------------------------- /src/assets/notes/notes_flick_arrow_03_diagonal.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mkpoli/paletteworks-editor/bf268907e8343ce58c61cdced64c75a6fba25c50/src/assets/notes/notes_flick_arrow_03_diagonal.png -------------------------------------------------------------------------------- /src/assets/notes/notes_flick_arrow_04.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mkpoli/paletteworks-editor/bf268907e8343ce58c61cdced64c75a6fba25c50/src/assets/notes/notes_flick_arrow_04.png -------------------------------------------------------------------------------- /src/assets/notes/notes_flick_arrow_04_diagonal.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mkpoli/paletteworks-editor/bf268907e8343ce58c61cdced64c75a6fba25c50/src/assets/notes/notes_flick_arrow_04_diagonal.png -------------------------------------------------------------------------------- /src/assets/notes/notes_flick_arrow_05.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mkpoli/paletteworks-editor/bf268907e8343ce58c61cdced64c75a6fba25c50/src/assets/notes/notes_flick_arrow_05.png -------------------------------------------------------------------------------- /src/assets/notes/notes_flick_arrow_05_diagonal.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mkpoli/paletteworks-editor/bf268907e8343ce58c61cdced64c75a6fba25c50/src/assets/notes/notes_flick_arrow_05_diagonal.png -------------------------------------------------------------------------------- /src/assets/notes/notes_flick_arrow_06.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mkpoli/paletteworks-editor/bf268907e8343ce58c61cdced64c75a6fba25c50/src/assets/notes/notes_flick_arrow_06.png -------------------------------------------------------------------------------- /src/assets/notes/notes_flick_arrow_06_diagonal.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mkpoli/paletteworks-editor/bf268907e8343ce58c61cdced64c75a6fba25c50/src/assets/notes/notes_flick_arrow_06_diagonal.png -------------------------------------------------------------------------------- /src/assets/notes/notes_flick_arrow_crtcl_01.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mkpoli/paletteworks-editor/bf268907e8343ce58c61cdced64c75a6fba25c50/src/assets/notes/notes_flick_arrow_crtcl_01.png -------------------------------------------------------------------------------- /src/assets/notes/notes_flick_arrow_crtcl_01_diagonal.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mkpoli/paletteworks-editor/bf268907e8343ce58c61cdced64c75a6fba25c50/src/assets/notes/notes_flick_arrow_crtcl_01_diagonal.png -------------------------------------------------------------------------------- /src/assets/notes/notes_flick_arrow_crtcl_02.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mkpoli/paletteworks-editor/bf268907e8343ce58c61cdced64c75a6fba25c50/src/assets/notes/notes_flick_arrow_crtcl_02.png -------------------------------------------------------------------------------- /src/assets/notes/notes_flick_arrow_crtcl_02_diagonal.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mkpoli/paletteworks-editor/bf268907e8343ce58c61cdced64c75a6fba25c50/src/assets/notes/notes_flick_arrow_crtcl_02_diagonal.png -------------------------------------------------------------------------------- /src/assets/notes/notes_flick_arrow_crtcl_03.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mkpoli/paletteworks-editor/bf268907e8343ce58c61cdced64c75a6fba25c50/src/assets/notes/notes_flick_arrow_crtcl_03.png -------------------------------------------------------------------------------- /src/assets/notes/notes_flick_arrow_crtcl_03_diagonal.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mkpoli/paletteworks-editor/bf268907e8343ce58c61cdced64c75a6fba25c50/src/assets/notes/notes_flick_arrow_crtcl_03_diagonal.png -------------------------------------------------------------------------------- /src/assets/notes/notes_flick_arrow_crtcl_04.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mkpoli/paletteworks-editor/bf268907e8343ce58c61cdced64c75a6fba25c50/src/assets/notes/notes_flick_arrow_crtcl_04.png -------------------------------------------------------------------------------- /src/assets/notes/notes_flick_arrow_crtcl_04_diagonal.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mkpoli/paletteworks-editor/bf268907e8343ce58c61cdced64c75a6fba25c50/src/assets/notes/notes_flick_arrow_crtcl_04_diagonal.png -------------------------------------------------------------------------------- /src/assets/notes/notes_flick_arrow_crtcl_05.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mkpoli/paletteworks-editor/bf268907e8343ce58c61cdced64c75a6fba25c50/src/assets/notes/notes_flick_arrow_crtcl_05.png -------------------------------------------------------------------------------- /src/assets/notes/notes_flick_arrow_crtcl_05_diagonal.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mkpoli/paletteworks-editor/bf268907e8343ce58c61cdced64c75a6fba25c50/src/assets/notes/notes_flick_arrow_crtcl_05_diagonal.png -------------------------------------------------------------------------------- /src/assets/notes/notes_flick_arrow_crtcl_06.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mkpoli/paletteworks-editor/bf268907e8343ce58c61cdced64c75a6fba25c50/src/assets/notes/notes_flick_arrow_crtcl_06.png -------------------------------------------------------------------------------- /src/assets/notes/notes_flick_arrow_crtcl_06_diagonal.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mkpoli/paletteworks-editor/bf268907e8343ce58c61cdced64c75a6fba25c50/src/assets/notes/notes_flick_arrow_crtcl_06_diagonal.png -------------------------------------------------------------------------------- /src/assets/notes/notes_long_among.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mkpoli/paletteworks-editor/bf268907e8343ce58c61cdced64c75a6fba25c50/src/assets/notes/notes_long_among.png -------------------------------------------------------------------------------- /src/assets/notes/notes_long_among_crtcl.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mkpoli/paletteworks-editor/bf268907e8343ce58c61cdced64c75a6fba25c50/src/assets/notes/notes_long_among_crtcl.png -------------------------------------------------------------------------------- /src/assets/playhead.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mkpoli/paletteworks-editor/bf268907e8343ce58c61cdced64c75a6fba25c50/src/assets/playhead.png -------------------------------------------------------------------------------- /src/assets/select.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mkpoli/paletteworks-editor/bf268907e8343ce58c61cdced64c75a6fba25c50/src/assets/select.png -------------------------------------------------------------------------------- /src/assets/skill.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mkpoli/paletteworks-editor/bf268907e8343ce58c61cdced64c75a6fba25c50/src/assets/skill.png -------------------------------------------------------------------------------- /src/assets/sound/connect.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mkpoli/paletteworks-editor/bf268907e8343ce58c61cdced64c75a6fba25c50/src/assets/sound/connect.mp3 -------------------------------------------------------------------------------- /src/assets/sound/connect_critical.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mkpoli/paletteworks-editor/bf268907e8343ce58c61cdced64c75a6fba25c50/src/assets/sound/connect_critical.mp3 -------------------------------------------------------------------------------- /src/assets/sound/critical_flick.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mkpoli/paletteworks-editor/bf268907e8343ce58c61cdced64c75a6fba25c50/src/assets/sound/critical_flick.mp3 -------------------------------------------------------------------------------- /src/assets/sound/critical_tap.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mkpoli/paletteworks-editor/bf268907e8343ce58c61cdced64c75a6fba25c50/src/assets/sound/critical_tap.mp3 -------------------------------------------------------------------------------- /src/assets/sound/critical_tick.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mkpoli/paletteworks-editor/bf268907e8343ce58c61cdced64c75a6fba25c50/src/assets/sound/critical_tick.mp3 -------------------------------------------------------------------------------- /src/assets/sound/flick.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mkpoli/paletteworks-editor/bf268907e8343ce58c61cdced64c75a6fba25c50/src/assets/sound/flick.mp3 -------------------------------------------------------------------------------- /src/assets/sound/good.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mkpoli/paletteworks-editor/bf268907e8343ce58c61cdced64c75a6fba25c50/src/assets/sound/good.mp3 -------------------------------------------------------------------------------- /src/assets/sound/great.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mkpoli/paletteworks-editor/bf268907e8343ce58c61cdced64c75a6fba25c50/src/assets/sound/great.mp3 -------------------------------------------------------------------------------- /src/assets/sound/perfect.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mkpoli/paletteworks-editor/bf268907e8343ce58c61cdced64c75a6fba25c50/src/assets/sound/perfect.mp3 -------------------------------------------------------------------------------- /src/assets/sound/stage.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mkpoli/paletteworks-editor/bf268907e8343ce58c61cdced64c75a6fba25c50/src/assets/sound/stage.mp3 -------------------------------------------------------------------------------- /src/assets/sound/tick.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mkpoli/paletteworks-editor/bf268907e8343ce58c61cdced64c75a6fba25c50/src/assets/sound/tick.mp3 -------------------------------------------------------------------------------- /src/assets/vector/curve-in.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/assets/vector/curve-out.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/assets/vector/diamond/ignored.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/assets/vector/diamond/invisible.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /src/assets/vector/diamond/visible.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/assets/vector/straight.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/global.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | interface ImportMeta { 4 | env: { 5 | PACKAGE_VERSION: string 6 | DEV: boolean 7 | } 8 | } 9 | 10 | declare interface Window { 11 | __TAURI__ 12 | } 13 | 14 | declare module 'bpm-detective' { 15 | declare function detect(data: AudioBuffer): number 16 | export default detect 17 | } 18 | 19 | declare module 'core-js/actual/array/at.js' { 20 | /** 21 | * Takes an integer value and returns the item at that index, 22 | * allowing for positive and negative integers. 23 | * Negative integers count back from the last item in the array. 24 | */ 25 | declare function at(array: Array, index: number): T | undefined 26 | export default at 27 | } 28 | -------------------------------------------------------------------------------- /src/hooks.ts: -------------------------------------------------------------------------------- 1 | import { detectLocale } from '$i18n/i18n-util' 2 | import type { GetSession, RequestEvent } from '@sveltejs/kit' 3 | import { initAcceptLanguageHeaderDetector } from 'typesafe-i18n/detectors' 4 | 5 | const getHeaders = (event: RequestEvent) => { 6 | const headers: Record = {} 7 | event.request.headers.forEach((value, key) => { headers[key] = value }) 8 | 9 | return headers 10 | } 11 | 12 | export const getSession: GetSession = (event) => { 13 | // detect the preferred language the user has configured in his browser 14 | // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Accept-Language 15 | const headers = getHeaders(event) 16 | const acceptLanguageDetector = initAcceptLanguageHeaderDetector({ headers }) 17 | const locale = detectLocale(acceptLanguageDetector) 18 | 19 | return { 20 | locale, 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/i18n/formatters.ts: -------------------------------------------------------------------------------- 1 | import type { FormattersInitializer } from 'typesafe-i18n' 2 | import type { Locales, Formatters } from './i18n-types' 3 | 4 | export const initFormatters: FormattersInitializer< 5 | Locales, 6 | Formatters 7 | > = async () => { 8 | const formatters: Formatters = { 9 | // add your formatter functions here 10 | } 11 | 12 | return formatters 13 | } 14 | -------------------------------------------------------------------------------- /src/i18n/i18n-svelte.ts: -------------------------------------------------------------------------------- 1 | // This file was auto-generated by 'typesafe-i18n'. Any manual changes will be overwritten. 2 | /* eslint-disable */ 3 | 4 | import { initI18nSvelte } from 'typesafe-i18n/adapters/adapter-svelte' 5 | import type { Locales, Translations, TranslationFunctions, Formatters } from './i18n-types' 6 | import { loadedLocales, loadedFormatters } from './i18n-util' 7 | 8 | const { locale, LL, setLocale } = initI18nSvelte(loadedLocales, loadedFormatters) 9 | 10 | export { locale, LL, setLocale } 11 | 12 | export default LL 13 | -------------------------------------------------------------------------------- /src/i18n/i18n-util.async.ts: -------------------------------------------------------------------------------- 1 | // This file was auto-generated by 'typesafe-i18n'. Any manual changes will be overwritten. 2 | /* eslint-disable */ 3 | 4 | import { initFormatters } from './formatters' 5 | import type { Locales, Translations } from './i18n-types' 6 | import { loadedFormatters, loadedLocales, locales } from './i18n-util' 7 | 8 | const localeTranslationLoaders = { 9 | en: () => import('./en'), 10 | ja: () => import('./ja'), 11 | ko: () => import('./ko'), 12 | zh: () => import('./zh'), 13 | } 14 | 15 | export const loadLocaleAsync = async (locale: Locales) => { 16 | if (loadedLocales[locale]) return 17 | 18 | loadedLocales[locale] = (await (localeTranslationLoaders[locale])()).default as unknown as Translations 19 | loadFormatters(locale) 20 | } 21 | 22 | export const loadAllLocalesAsync = () => Promise.all(locales.map(loadLocaleAsync)) 23 | 24 | export const loadFormatters = (locale: Locales) => { 25 | loadedFormatters[locale] = initFormatters(locale) 26 | } 27 | -------------------------------------------------------------------------------- /src/i18n/i18n-util.sync.ts: -------------------------------------------------------------------------------- 1 | // This file was auto-generated by 'typesafe-i18n'. Any manual changes will be overwritten. 2 | /* eslint-disable */ 3 | 4 | import { initFormatters } from './formatters' 5 | import type { Locales, Translations } from './i18n-types' 6 | import { loadedFormatters, loadedLocales, locales } from './i18n-util' 7 | 8 | import en from './en' 9 | import ja from './ja' 10 | import ko from './ko' 11 | import zh from './zh' 12 | 13 | const localeTranslations = { 14 | en, 15 | ja, 16 | ko, 17 | zh, 18 | } 19 | 20 | export const loadLocale = (locale: Locales) => { 21 | if (loadedLocales[locale]) return 22 | 23 | loadedLocales[locale] = localeTranslations[locale] as unknown as Translations 24 | loadFormatters(locale) 25 | } 26 | 27 | export const loadAllLocales = () => locales.forEach(loadLocale) 28 | 29 | export const loadFormatters = (locale: Locales) => { 30 | loadedFormatters[locale] = initFormatters(locale) 31 | } 32 | -------------------------------------------------------------------------------- /src/i18n/i18n-util.ts: -------------------------------------------------------------------------------- 1 | // This file was auto-generated by 'typesafe-i18n'. Any manual changes will be overwritten. 2 | /* eslint-disable */ 3 | 4 | import { i18n as initI18n, i18nObject as initI18nObject, i18nString as initI18nString } from 'typesafe-i18n' 5 | import type { LocaleDetector } from 'typesafe-i18n/detectors' 6 | import { detectLocale as detectLocaleFn } from 'typesafe-i18n/detectors' 7 | import type { Formatters, Locales, Translations, TranslationFunctions } from './i18n-types' 8 | 9 | export const baseLocale: Locales = 'ja' 10 | 11 | export const locales: Locales[] = [ 12 | 'en', 13 | 'ja', 14 | 'ko', 15 | 'zh' 16 | ] 17 | 18 | export const loadedLocales = {} as Record 19 | 20 | export const loadedFormatters = {} as Record 21 | 22 | export const i18nString = (locale: Locales) => initI18nString(locale, loadedFormatters[locale]) 23 | 24 | export const i18nObject = (locale: Locales) => 25 | initI18nObject(locale, loadedLocales[locale], loadedFormatters[locale]) 26 | 27 | export const i18n = () => initI18n(loadedLocales, loadedFormatters) 28 | 29 | export const detectLocale = (...detectors: LocaleDetector[]) => detectLocaleFn(baseLocale, locales, ...detectors) 30 | -------------------------------------------------------------------------------- /src/i18n/metadata.ts: -------------------------------------------------------------------------------- 1 | export const LANGUAGE_ENDONYMS = { 2 | en: 'English', 3 | ja: '日本語', 4 | zh: '中文', 5 | ko: '한국어', 6 | } 7 | -------------------------------------------------------------------------------- /src/lib/ZoomIndicator.svelte: -------------------------------------------------------------------------------- 1 | 15 | 16 | 17 | { 21 | onclick(event, 1) 22 | }} 23 | /> 24 | 25 | 26 | 27 | { 31 | onclick(event, -1) 32 | }} 33 | /> 34 | 35 | {zoom.toFixed(1)}x 36 | 37 | 38 | 39 | 97 | -------------------------------------------------------------------------------- /src/lib/api/library.ts: -------------------------------------------------------------------------------- 1 | import type { Single, Slide } from '$lib/score/beatmap' 2 | 3 | // Types 4 | export type LocaleStrings = { 5 | [key: string]: string 6 | } 7 | 8 | export type Item = { 9 | title: LocaleStrings 10 | description: LocaleStrings 11 | content: { 12 | singles?: Single[] 13 | slides?: Slide[] 14 | } 15 | } 16 | 17 | // let fetch: ( 18 | // input: RequestInfo, 19 | // init?: RequestInit | undefined 20 | // ) => Promise 21 | 22 | const PREFIX = 'https://paletteworks.mkpo.li/' 23 | const LIBRARY_URL = '/api/library' 24 | 25 | export async function list(): Promise { 26 | if (!window.__TAURI__) { 27 | const res = await fetch(LIBRARY_URL) 28 | if (!res.ok) throw new Error(`${res.status} ${res.statusText}`) 29 | return (await res.json()) as Item[] 30 | } else { 31 | const { http } = await import('@tauri-apps/api') 32 | return (await http.fetch(new URL(LIBRARY_URL, PREFIX).href)).data 33 | } 34 | } 35 | 36 | export async function create(item: Item): Promise { 37 | const res = await fetch(LIBRARY_URL, { 38 | method: 'POST', 39 | headers: { 40 | 'Content-Type': 'application/json', 41 | }, 42 | body: JSON.stringify(item), 43 | }) 44 | if (!res.ok) throw new Error(`${res.status} ${res.statusText}`) 45 | } 46 | -------------------------------------------------------------------------------- /src/lib/audio/scheduler.ts: -------------------------------------------------------------------------------- 1 | import type { EFFECT_SOUNDS } from '$lib/consts' 2 | export type Sound = keyof typeof EFFECT_SOUNDS 3 | export type AudioEvent = { 4 | time: number 5 | sound: Sound | 'bgm' 6 | loopTo?: number 7 | startFrom?: number 8 | } 9 | 10 | type EventCallback = (event: AudioEvent, offset: number) => void 11 | 12 | export type SchedulerOption = { 13 | scheduleInterval?: number 14 | scheduleLookahead?: number 15 | events?: Array 16 | callback?: EventCallback 17 | } 18 | 19 | export class AudioScheduler { 20 | audioContext: AudioContext 21 | audioNodes: AudioBufferSourceNode[] 22 | scheduleInterval: number 23 | scheduleLookahead: number 24 | events: AudioEvent[] 25 | callback: EventCallback 26 | eventsIndexNeedle: number 27 | timerID: number 28 | startTimeOffset: number 29 | 30 | constructor( 31 | audioContext: AudioContext, 32 | audioNodes: AudioBufferSourceNode[], 33 | { 34 | scheduleInterval = 50, // in milliseconds 35 | scheduleLookahead = 100, // in milliseconds 36 | events = [], 37 | callback = () => { 38 | /* empty */ 39 | }, 40 | }: SchedulerOption 41 | ) { 42 | this.audioContext = audioContext 43 | this.audioNodes = audioNodes // keep track of all created audio nodes so they can be stopped 44 | this.scheduleInterval = scheduleInterval // schedule new events at this interval 45 | this.scheduleLookahead = scheduleLookahead // schedule new events this far into the future 46 | this.events = events // an events object that must at least contain the time property for each event 47 | this.callback = callback // a function used to play the events 48 | 49 | this.eventsIndexNeedle = 0 // a needle used to go through the events index by index 50 | this.timerID = -1 // used by clearTimeout() to identify the setTimeout timer 51 | this.startTimeOffset = 0 // the offset between the time at audioContext creation and audioContext.currentTime 52 | } 53 | 54 | private _stopAllAudioNodes() { 55 | for (const node of this.audioNodes) { 56 | node.stop(0) 57 | } 58 | } 59 | 60 | stop() { 61 | if (this.audioContext.state === 'running') { 62 | this.audioContext.suspend() 63 | } 64 | this._stopAllAudioNodes() 65 | clearTimeout(this.timerID) 66 | } 67 | 68 | reset() { 69 | if (this.audioContext.state === 'suspended') { 70 | this.audioContext.resume() 71 | } 72 | this.eventsIndexNeedle = 0 73 | this.timerID = -1 74 | this.startTimeOffset = this.audioContext.currentTime 75 | } 76 | 77 | start() { 78 | this.stop() 79 | this.reset() 80 | this.play() 81 | } 82 | 83 | play() { 84 | while ( 85 | this.eventsIndexNeedle < this.events.length && 86 | typeof this.events[this.eventsIndexNeedle].time === 'number' && 87 | this.events[this.eventsIndexNeedle].time + this.startTimeOffset < 88 | this.audioContext.currentTime + this.scheduleLookahead / 1000 89 | ) { 90 | this.callback(this.events[this.eventsIndexNeedle], this.startTimeOffset) 91 | this.eventsIndexNeedle++ 92 | } 93 | this.timerID = window.setTimeout(() => { 94 | this.play() 95 | }, this.scheduleInterval) 96 | } 97 | 98 | // pause() { 99 | // if (this.audioContext.state === 'running') { 100 | // this.audioContext.suspend() 101 | // } else if(this.audioContext.state === 'suspended') { 102 | // this.audioContext.resume() 103 | // } 104 | // } 105 | } 106 | 107 | export function playOnce( 108 | audioContext: AudioContext, 109 | target: GainNode | AudioDestinationNode, 110 | buffer: AudioBuffer 111 | ) { 112 | const soundSource = audioContext.createBufferSource() 113 | soundSource.buffer = buffer 114 | soundSource.connect(target) 115 | soundSource.start() 116 | } 117 | -------------------------------------------------------------------------------- /src/lib/basic/collections.ts: -------------------------------------------------------------------------------- 1 | export {} 2 | 3 | declare global { 4 | interface Array { 5 | pairwise(): [T, T][] 6 | rotateNext(cur: T): T 7 | rotatePrev(cur: T): T 8 | } 9 | 10 | interface Array { 11 | closest(num: T, smaller?: boolean): T | undefined 12 | } 13 | 14 | interface ReadonlyArray { 15 | pairwise(): [T, T][] 16 | rotateNext(cur: T): T 17 | rotatePrev(cur: T): T 18 | } 19 | } 20 | 21 | Array.prototype.pairwise = function pairwise(): [T, T][] { 22 | return this.slice(1).map((val: T, i: number) => [this[i], val]) 23 | } 24 | 25 | Array.prototype.rotateNext = function rotateNext(cur: T): T { 26 | return this[(this.indexOf(cur) + 1) % this.length] 27 | } 28 | 29 | Array.prototype.rotatePrev = function rotatePrev(cur: T): T { 30 | return this[(this.indexOf(cur) - 1 + this.length) % this.length] 31 | } 32 | 33 | Array.prototype.closest = function closest( 34 | num: number, 35 | smaller = true 36 | ): number | undefined { 37 | const sorted = [...this].sort((a, b) => a - b) 38 | return (smaller ? sorted.reverse() : sorted).find((e) => 39 | smaller ? e <= num : e >= num 40 | ) 41 | } 42 | -------------------------------------------------------------------------------- /src/lib/basic/debug.ts: -------------------------------------------------------------------------------- 1 | import { writable } from 'svelte/store' 2 | 3 | type Value = string | number | boolean | null | undefined 4 | 5 | export type DebugInfo = Map 6 | export const debugInfo = writable(new Map()) 7 | 8 | export function formatPoint(x: number, y: number) { 9 | return `(${x?.toFixed(3)}, ${y?.toFixed(3)})` 10 | } 11 | 12 | export function dbg(title: string, value: Value) { 13 | debugInfo.update((map: DebugInfo) => { 14 | map.set(title, value) 15 | return map 16 | }) 17 | } 18 | -------------------------------------------------------------------------------- /src/lib/basic/file.spec.ts: -------------------------------------------------------------------------------- 1 | import { formatFilename } from './file' 2 | 3 | describe('formatFilename', () => { 4 | beforeAll(() => { 5 | jest.useFakeTimers('modern') 6 | jest.setSystemTime(new Date('2020-01-01').getTime()) 7 | }) 8 | 9 | const formatData = { 10 | project: 'project', 11 | title: 'title', 12 | artist: 'artist', 13 | author: 'author', 14 | } 15 | 16 | it('should replace {keyword} with real data', () => { 17 | expect(formatFilename('{artist}-{title}.txt', formatData)).toBe( 18 | 'artist-title.txt' 19 | ) 20 | }) 21 | 22 | it('should replace {date} with real date', () => { 23 | expect(formatFilename('{project}_{date}.sus', formatData)).toBe( 24 | 'project_2020-01-01.sus' 25 | ) 26 | }) 27 | 28 | it('should replace {datetime} with filename-safe time', () => { 29 | expect(formatFilename('{title}_{datetime}.sus', formatData)).toBe( 30 | 'title_2020-01-01T00-00-00.000Z.sus' 31 | ) 32 | }) 33 | 34 | it('should keep unknown {keyword} as is', () => { 35 | expect(formatFilename('{unknown}.txt', formatData)).toBe('{unknown}.txt') 36 | }) 37 | 38 | it('should keep other strings intact', () => { 39 | expect(formatFilename('Untitled.txt', formatData)).toBe('Untitled.txt') 40 | }) 41 | 42 | afterAll(() => { 43 | jest.useRealTimers() 44 | }) 45 | }) 46 | -------------------------------------------------------------------------------- /src/lib/basic/file.ts: -------------------------------------------------------------------------------- 1 | import { default as accepted } from 'attr-accept' 2 | 3 | export async function download(b: Blob, filename: string): Promise { 4 | if (!window.__TAURI__) { 5 | const a = document.createElement('a') 6 | document.body.append(a) 7 | a.download = filename 8 | a.href = URL.createObjectURL(b) 9 | a.click() 10 | a.remove() 11 | } else { 12 | const { tauri, dialog } = await import('@tauri-apps/api') 13 | const defaultSavePath = window.localStorage.getItem('default-save-path') 14 | const path = await dialog.save({ 15 | defaultPath: defaultSavePath ? defaultSavePath + filename : filename, 16 | }) 17 | await tauri.invoke('write_file', { 18 | path, 19 | data: [...new Uint8Array(await b.arrayBuffer())], 20 | }) 21 | window.localStorage.setItem( 22 | 'default-save-path', 23 | path.slice(0, path.lastIndexOf('/')) 24 | ) 25 | // await fs.writeBinaryFile({ 26 | // contents: , 27 | // path, 28 | // }) 29 | } 30 | } 31 | 32 | export function toBlob(content: string) { 33 | const blob = new Blob([content], { type: 'text/sus+plain' }) 34 | return blob 35 | } 36 | 37 | export function dropHandler( 38 | accept: string, 39 | callback: (file: File) => void | Promise, 40 | onerror: () => void | Promise 41 | ): (event: DragEvent) => Promise { 42 | return async (event: DragEvent) => { 43 | if (!event.dataTransfer || event.dataTransfer.items.length === 0) return 44 | const item = event.dataTransfer.items[0] 45 | if (item.kind !== 'file') return 46 | const file = item.getAsFile() 47 | event.stopPropagation() 48 | if (file && accepted(file, accept)) { 49 | await callback(file) 50 | } else { 51 | await onerror() 52 | } 53 | } 54 | } 55 | 56 | export function dropHandlerMultiple( 57 | handlers: { accept: string; callback: (file: File) => void }[], 58 | onerror: () => void 59 | ): (event: DragEvent) => void { 60 | return (event: DragEvent) => { 61 | if (!event.dataTransfer || event.dataTransfer.items.length === 0) return 62 | const item = event.dataTransfer.items[0] 63 | if (item.kind !== 'file') return 64 | const file = item.getAsFile() 65 | event.stopPropagation() 66 | for (const { accept, callback } of handlers) { 67 | if (file && accepted(file, accept)) { 68 | callback(file) 69 | return 70 | } 71 | } 72 | onerror() 73 | } 74 | } 75 | 76 | export type FormatData = { 77 | project: string 78 | title: string 79 | artist: string 80 | author: string 81 | } 82 | export const SUPPORTED_FORMAT_KEYWORDS = [ 83 | 'project', 84 | 'title', 85 | 'artist', 86 | 'author', 87 | 'datetime', 88 | 'date', 89 | ] 90 | export function formatFilename(format: string, data: FormatData): string { 91 | return format.replace(/{([^}]+)}/g, (match, key: string | undefined) => { 92 | switch (key) { 93 | case 'project': 94 | case 'title': 95 | case 'artist': 96 | case 'author': 97 | return data[key] 98 | case 'datetime': 99 | return new Date().toISOString().replace(/:/g, '-') 100 | case 'date': 101 | return new Date().toISOString().split('T')[0] 102 | default: 103 | return match 104 | } 105 | }) 106 | } 107 | -------------------------------------------------------------------------------- /src/lib/basic/math.ts: -------------------------------------------------------------------------------- 1 | export function clamp(min: number, number: number, max: number): number { 2 | return Math.min(Math.max(number, min), max) 3 | } 4 | 5 | export function gcd(a: number, b: number): number { 6 | if (a < b) return gcd(b, a) 7 | if (b == 0) return a 8 | return gcd(b, a % b) 9 | } 10 | 11 | export function snap(y: number, step: number): number { 12 | return Math.floor(y / step) * step 13 | } 14 | 15 | export function minmax(a: number, b: number): [number, number] { 16 | return [Math.min(a, b), Math.max(a, b)] 17 | } 18 | 19 | export function between( 20 | a: number, 21 | x: number, 22 | b: number, 23 | includeMin = true, 24 | includeMax = true 25 | ): boolean { 26 | const [min, max] = minmax(a, b) 27 | return (includeMin ? x >= min : x > min) && (includeMax ? x <= max : x < max) 28 | } 29 | 30 | // from: https://stackoverflow.com/a/59906630/2719898 31 | type ArrayLengthMutationKeys = 'splice' | 'push' | 'pop' | 'shift' | 'unshift' 32 | type FixedLengthArray]> = Pick< 33 | TObj, 34 | Exclude 35 | > & { 36 | readonly length: L 37 | [I: number]: T 38 | [Symbol.iterator]: () => IterableIterator 39 | } 40 | 41 | /** 42 | * Calculate the average of multiple vectors with the same dimension 43 | * @param values an array of vectors 44 | * @returns average of all vectors 45 | */ 46 | export function average( 47 | values: FixedLengthArray[] 48 | ): number[] { 49 | const total = values.reduce( 50 | (acc, v) => acc.map((a, i) => a + v[i]), 51 | values[0].map(() => 0) 52 | ) 53 | return total.map((v) => v / values.length) 54 | } 55 | 56 | export function lerp(x: number, y: number, a: number): number { 57 | return x * (1 - a) + y * a 58 | } 59 | 60 | export function easeInQuad(x: number): number { 61 | return x * x 62 | } 63 | 64 | export function easeOutQuad(x: number): number { 65 | return 1 - (1 - x) * (1 - x) 66 | } 67 | 68 | export function cartesianProduct(arr: T[][]): T[][] { 69 | if (arr.length === 1) { 70 | return arr[0].map((n) => [n]) 71 | } 72 | const result: T[][] = [] 73 | const rest = cartesianProduct(arr.slice(1)) 74 | for (const n of arr[0]) { 75 | for (const r of rest) { 76 | // eslint-disable-next-line no-array-concat/no-array-concat 77 | result.push([n].concat(r)) 78 | } 79 | } 80 | return result 81 | } 82 | -------------------------------------------------------------------------------- /src/lib/colors.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | COLOR_BPM: 0x03c04a, 3 | COLOR_TIME_SIGNATURE: 0xff8938, 4 | COLOR_BACKGROUND: 0x000000, 5 | COLOR_LANE_PRIMARY: 0xffffff, 6 | COLOR_LANE_SECONDARY: 0x4c3b5c, 7 | COLOR_BAR_PRIMARY: 0xffffff, 8 | COLOR_BAR_SECONDARY: 0xcccccc, 9 | COLOR_PLAYHEAD: 0xff153f, 10 | COLOR_SLIDE_PATH: 0xdafdf0, 11 | COLOR_SLIDE_PATH_CRITICAL: 0xfffccc, 12 | ALPHA_SLIDE_PATH: 0.9, 13 | COLOR_SLIDE_STEP: 0x24e0a1, 14 | ALPHA_SLIDE_STEP: 1, 15 | ALPHA_SLIDE_STEP_FILL: 0.65, 16 | COLOR_SELECTION: 0x007bf8, 17 | ALPHA_SELECTION: 0.25, 18 | COLOR_STACKED: 0xd50f13, 19 | ALPHA_STACKED: 0.5, 20 | COLOR_CORRUPTED: 0x000000, 21 | ALPHA_CORRUPTED: 0.5, // TODO: fix typo 22 | COLOR_WARNING: 0xffb943, 23 | ALPHA_WARNING: 0.4, 24 | COLOR_MULTI_TAP: 0x00e5ea, 25 | ALPHA_MULTI_TAP: 0.3, 26 | COLOR_MOVING_TINT: 0xced3e4, 27 | ALPHA_FLOATING: 0.5, 28 | } 29 | 30 | // next color hue from the source 31 | export function getColor(source: number, i: number) { 32 | const [l, c, h] = convertRGBToLCH(source) 33 | return convertLCHtoRGB([l, c, (h + i * 50) % 360]) 34 | } 35 | 36 | function convertRGBToLCH(color: number) { 37 | const [r, g, b] = [(color >> 16) & 0xff, (color >> 8) & 0xff, color & 0xff] 38 | const l = 0.2126 * r + 0.7152 * g + 0.0722 * b 39 | const c = Math.sqrt(0.299 * r ** 2 + 0.587 * g ** 2 + 0.114 * b ** 2) 40 | const h = (Math.atan2(b, r) * 180) / Math.PI 41 | return [l, c, h] 42 | } 43 | 44 | function convertLCHtoRGB(lch: [number, number, number]): number { 45 | const [l, c, h] = lch 46 | const r = l + c * Math.cos((h * Math.PI) / 180) 47 | const b = l + c * Math.cos(((h + 120) * Math.PI) / 180) 48 | const g = l + c * Math.cos(((h + 240) * Math.PI) / 180) 49 | return ( 50 | (Math.round(r * 255) << 16) + 51 | (Math.round(g * 255) << 8) + 52 | Math.round(b * 255) 53 | ) 54 | } 55 | -------------------------------------------------------------------------------- /src/lib/consts.ts: -------------------------------------------------------------------------------- 1 | export { default as COLORS } from './colors' 2 | 3 | export const RESOLUTION = 1 4 | 5 | export const BEAT_UNIT = 4 6 | export const BEAT_IN_MEASURE = 4 7 | export const TICK_PER_BEAT = 480 8 | export const TICK_PER_MEASURE = TICK_PER_BEAT * BEAT_IN_MEASURE 9 | 10 | export const MARGIN = 100 11 | export const MARGIN_BOTTOM = 30 12 | 13 | export const TEXT_MARGIN = 20 14 | 15 | export const LANE_WIDTH = 30 16 | export const LANE_COUNT_REAL = 12 17 | export const LANE_MIN = 2 18 | export const LANE_MAX = 13 19 | export const LANE_SUS_MIN = 0 20 | export const LANE_SUS_MAX = 16 21 | export const LANE_SIDE_MAX = LANE_MAX + 1 22 | export const LANE_COUNT = LANE_COUNT_REAL + 2 23 | export const LANE_FEVER = 15 24 | export const LANE_SKILL = 0 25 | export const MEASURE_HEIGHT = 300 26 | export const TICK_HEIGHT = 0.15625 27 | export const MAIN_WIDTH = MARGIN * 2 + 30 * LANE_COUNT 28 | export const SCROLLBAR_WIDTH = 13 29 | export const MINIMAP_RESOLUTION = 0.2 30 | export const MINIMAP_WIDTH = MAIN_WIDTH * MINIMAP_RESOLUTION 31 | 32 | export const WIDTH_DEFAULT = 3 33 | 34 | export const SNAPTO_DEFAULT = 8 35 | 36 | export const NOTE_PIVOT = [0.14971751412, 0.5] 37 | export const NOTE_WIDTH = 43 38 | export const NOTE_HEIGHT = 30 39 | 40 | export const DIAMOND_PIVOT = [0.15189873417, 0.5] 41 | export const DIAMOND_WIDTH = 30 42 | export const DIAMOND_HEIGHT = (30 / 158) * 160 43 | 44 | export const ZOOM_MIN = 0.1 45 | export const ZOOM_MAX = 10.0 46 | export const ZOOM_STEP = 0.1 47 | export const ZOOM_DEFAULT = 1 48 | 49 | export const TEXTURE_NAMES = [ 50 | 'noteC.png', 51 | 'noteF.png', 52 | 'noteL.png', 53 | 'noteN.png', 54 | 'notes_flick_arrow_01.png', 55 | 'notes_flick_arrow_01_diagonal.png', 56 | 'notes_flick_arrow_02.png', 57 | 'notes_flick_arrow_02_diagonal.png', 58 | 'notes_flick_arrow_03.png', 59 | 'notes_flick_arrow_03_diagonal.png', 60 | 'notes_flick_arrow_04.png', 61 | 'notes_flick_arrow_04_diagonal.png', 62 | 'notes_flick_arrow_05.png', 63 | 'notes_flick_arrow_05_diagonal.png', 64 | 'notes_flick_arrow_06.png', 65 | 'notes_flick_arrow_06_diagonal.png', 66 | 'notes_flick_arrow_crtcl_01.png', 67 | 'notes_flick_arrow_crtcl_01_diagonal.png', 68 | 'notes_flick_arrow_crtcl_02.png', 69 | 'notes_flick_arrow_crtcl_02_diagonal.png', 70 | 'notes_flick_arrow_crtcl_03.png', 71 | 'notes_flick_arrow_crtcl_03_diagonal.png', 72 | 'notes_flick_arrow_crtcl_04.png', 73 | 'notes_flick_arrow_crtcl_04_diagonal.png', 74 | 'notes_flick_arrow_crtcl_05.png', 75 | 'notes_flick_arrow_crtcl_05_diagonal.png', 76 | 'notes_flick_arrow_crtcl_06.png', 77 | 'notes_flick_arrow_crtcl_06_diagonal.png', 78 | 'notes_long_among.png', 79 | 'notes_long_among_crtcl.png', 80 | ] 81 | 82 | import tapPerfect from '$assets/sound/perfect.mp3' 83 | import tapCritical from '$assets/sound/critical_tap.mp3' 84 | import flickCritical from '$assets/sound/critical_flick.mp3' 85 | import flick from '$assets/sound/flick.mp3' 86 | import tick from '$assets/sound/tick.mp3' 87 | import tickCritical from '$assets/sound/critical_tick.mp3' 88 | import connect from '$assets/sound/connect.mp3' 89 | import connectCritical from '$assets/sound/connect_critical.mp3' 90 | import stage from '$assets/sound/stage.mp3' 91 | 92 | export const EFFECT_SOUNDS = { 93 | tapPerfect, 94 | tapCritical, 95 | flick, 96 | flickCritical, 97 | tick, 98 | tickCritical, 99 | connect, 100 | connectCritical, 101 | stage, 102 | } 103 | 104 | /** 105 | * Z-indicies 106 | **/ 107 | export enum Z_INDEX { 108 | GRID, 109 | BAR, 110 | GAMESCRIPT, 111 | FLOATING_BAR, 112 | PLAYHEAD, 113 | SLIDE_PATH, 114 | STEP, 115 | DIAMOND, 116 | NOTE, 117 | ARROW, 118 | CONTROL, 119 | CONTROL_INTERACTION, 120 | MINIMAP, 121 | ERROR, 122 | SELECTION, 123 | FLOATING_SLIDE_PATH, 124 | FLOATING_STEP, 125 | FLOATING_DIAMOND, 126 | FLOATING_NOTE, 127 | FLOATING_ARROW, 128 | } 129 | -------------------------------------------------------------------------------- /src/lib/control/ControlHandler.svelte: -------------------------------------------------------------------------------- 1 | 75 | 76 | 77 | -------------------------------------------------------------------------------- /src/lib/control/keyboard.ts: -------------------------------------------------------------------------------- 1 | export const KEYBOARD_SHORTCUTS = { 2 | skipstart: [['backspace'], ['home'], ['shift', '`']], 3 | skipback: [['`'], ['\\']], 4 | playpause: [['space']], 5 | duplicate: [['ctrl', 'd']], 6 | flip: [ 7 | ['ctrl', 'h'], 8 | ['shift', 'h'], 9 | ], 10 | vflip: [['shift', 'v']], 11 | copy: [['ctrl', 'c']], 12 | cut: [['ctrl', 'x']], 13 | paste: [['ctrl', 'v']], 14 | flippaste: [['ctrl', 'alt', 'v']], 15 | undo: [['ctrl', 'z']], 16 | redo: [ 17 | ['ctrl', 'y'], 18 | ['ctrl', 'shift', 'z'], 19 | ], 20 | save: [['ctrl', 's']], 21 | export: [ 22 | ['ctrl', 'e'], 23 | ['ctrl', 'shift', 's'], 24 | ], 25 | open: [['ctrl', 'o']], 26 | image: [['ctrl', 'i']], 27 | new: [['ctrl', 'n']], 28 | selectall: [['ctrl', 'a']], 29 | unselectall: [['ctrl', 'shift', 'a']], 30 | delete: [['delete']], 31 | increaseSnapTo: [['alt', '=']], 32 | decreaseSnapTo: [['alt', '-']], 33 | pageup: [['pageup']], 34 | pagedown: [['pagedown']], 35 | gotoup: [['up']], 36 | gotodown: [['down']], 37 | gotoupfast: [['shift', 'up']], 38 | gotodownfast: [['shift', 'down']], 39 | openmainmenu: [['ctrl', 'm']], 40 | } as const 41 | 42 | export type KeyboardAction = keyof typeof KEYBOARD_SHORTCUTS 43 | 44 | import { writable } from 'svelte/store' 45 | 46 | export const shiftKey = writable(false) 47 | export const ctrlKey = writable(false) 48 | export const altKey = writable(false) 49 | -------------------------------------------------------------------------------- /src/lib/control/pointer.ts: -------------------------------------------------------------------------------- 1 | export const MOUSE_BUTTON = { 2 | LEFT: 0, 3 | MIDDLE: 1, 4 | RIGHT: 2, 5 | } 6 | 7 | import moveCursor from '$assets/cursor/move-cursor.png' 8 | import resizeCursor from '$assets/cursor/resize-cursor.png' 9 | import selectCursor from '$assets/cursor/select-cursor.png' 10 | import grabCursor from '$assets/cursor/grab-cursor.png' 11 | 12 | export const CURSOR_STYLES = { 13 | move: `url(${moveCursor}) 16 16, move`, 14 | resize: `url(${resizeCursor}) 16 16, ew-resize`, 15 | select: `url(${selectCursor}) 6 4, default`, 16 | grab: `url(${grabCursor}) 16 16, default`, 17 | } 18 | -------------------------------------------------------------------------------- /src/lib/database.ts: -------------------------------------------------------------------------------- 1 | import type { Metadata, Score } from '$lib/score/beatmap' 2 | import { serialiseScore, deserialiseScore } from '$lib/score/beatmap' 3 | export interface Project { 4 | id?: number 5 | name: string | null 6 | created: Date 7 | updated: Date 8 | metadata: Metadata 9 | score: Score 10 | preview: Blob 11 | music: File | null 12 | } 13 | 14 | export interface Preferences { 15 | key?: string 16 | value: unknown 17 | } 18 | 19 | import { Dexie, liveQuery } from 'dexie' 20 | 21 | class Database extends Dexie { 22 | projects: Dexie.Table 23 | preferences: Dexie.Table 24 | constructor() { 25 | super('PaletteWorks') 26 | this.version(2).stores({ 27 | projects: '++id,name,created,updated,metadata,score,music,preview', 28 | preferences: 'key,value', 29 | }) 30 | this.projects = this.table('projects') 31 | this.preferences = this.table('preferences') 32 | } 33 | } 34 | 35 | export const db = new Database() 36 | 37 | export const projects = liveQuery(async () => 38 | (await db.projects.toArray()).reverse() 39 | ) 40 | export const preferences = liveQuery(async () => 41 | Object.fromEntries( 42 | (await db.preferences.toArray()).map(({ key, value }) => [key, value]) 43 | ) 44 | ) 45 | 46 | import msgpack from 'msgpack-lite' 47 | 48 | export async function seriliseProject(project: Project): Promise { 49 | const { name, created, updated, metadata, score, preview, music } = project 50 | const data = msgpack.encode({ 51 | version: 1, 52 | name, 53 | created: created.getTime(), 54 | updated: updated.getTime(), 55 | metadata, 56 | score: serialiseScore(score), 57 | preview: await preview.arrayBuffer(), 58 | music: music 59 | ? { 60 | data: await music.arrayBuffer(), 61 | name: music.name, 62 | type: music.type, 63 | lastModified: music.lastModified, 64 | } 65 | : null, 66 | }) 67 | return new Blob([data.buffer], { type: 'application/octet-binary' }) 68 | } 69 | 70 | export async function deserialiseProject(blob: Blob): Promise { 71 | const data = msgpack.decode(new Uint8Array(await blob.arrayBuffer())) 72 | const { version, name, created, updated, metadata, score, preview, music } = 73 | data 74 | if (version !== 1) throw new Error('Unsupported version') 75 | return { 76 | name, 77 | created: new Date(created), 78 | updated: new Date(updated), 79 | metadata, 80 | score: deserialiseScore(score), 81 | preview: new Blob([preview], { type: 'application/octet-binary' }), 82 | music: music 83 | ? new File([music.data], music.name, { 84 | type: music.type, 85 | lastModified: music.lastModified, 86 | }) 87 | : null, 88 | } 89 | } 90 | 91 | export const PROJECT_FILE_EXTENSION = '.pws' // PaletteWorks Score File 92 | -------------------------------------------------------------------------------- /src/lib/dialogs/AboutDialog.svelte: -------------------------------------------------------------------------------- 1 | 31 | 32 | 33 | 34 | { 37 | inputElement.focus() 38 | inputElement.select() 39 | }} 40 | > 41 | 42 | {$LL.editor.dialog.about()} 43 | 44 | { 48 | dispatch('cancel') 49 | opened = false 50 | }} 51 | /> 52 | 53 | 54 | v{process.env.PACKAGE_VERSION} 55 | 56 | MIT License © 2021 mkpoli 57 | 58 | 59 | 64 | Homepage (paletteworks.mkpo.li) 65 | 66 | 71 | Github (mkpoli/paletteworks-editor) 72 | 73 | 78 | Twitter (JA @_mkpoli) 79 | 80 | 85 | Twitter (EN @mkpoli_) 86 | 87 | 92 | Discord (PurplePalette#paletteworks) 93 | 94 | 95 | 96 | 97 | { 101 | dispatch('ok') 102 | opened = false 103 | }} 104 | > 105 | {$LL.editor.dialog.ok()} 106 | 107 | 108 | 109 | 110 | 171 | -------------------------------------------------------------------------------- /src/lib/dialogs/BPMDialog.svelte: -------------------------------------------------------------------------------- 1 | 19 | 20 | { 23 | inputElement.focus() 24 | inputElement.select() 25 | }} 26 | > 27 | 28 | 29 | {$LL.editor.dialog.bpmTitle()} 30 | 31 | { 35 | dispatch('cancel') 36 | opened = false 37 | }} 38 | /> 39 | 40 | { 42 | switch (e.key) { 43 | case 'Enter': 44 | dispatch('ok') 45 | opened = false 46 | break 47 | case 'Escape': 48 | dispatch('cancel') 49 | opened = false 50 | break 51 | } 52 | }} 53 | type="number" 54 | bind:inputElement 55 | bind:value 56 | > 57 | 58 | 59 | 60 | 61 | 62 | (BPM) 63 | 64 | 65 | { 69 | dispatch('ok') 70 | opened = false 71 | }} 72 | > 73 | {!exist ? $LL.editor.dialog.append() : $LL.editor.dialog.change()} 74 | 75 | {#if exist} 76 | { 80 | dispatch('delete') 81 | opened = false 82 | }} 83 | > 84 | {$LL.editor.dialog.delete()} 85 | 86 | {/if} 87 | 88 | 89 | 90 | 145 | -------------------------------------------------------------------------------- /src/lib/dialogs/CustomSnappingDialog.svelte: -------------------------------------------------------------------------------- 1 | 18 | 19 | { 22 | inputElement.focus() 23 | inputElement.select() 24 | }} 25 | > 26 | 27 | 28 | {$LL.editor.dialog.customSnappingTitle()} 29 | 30 | { 34 | dispatch('cancel') 35 | opened = false 36 | }} 37 | /> 38 | 39 | { 41 | switch (e.key) { 42 | case 'Enter': 43 | dispatch('ok') 44 | opened = false 45 | break 46 | case 'Escape': 47 | dispatch('cancel') 48 | opened = false 49 | break 50 | } 51 | }} 52 | bind:inputElement 53 | bind:value 54 | > 55 | 56 | 57 | 58 | 59 | 60 | {$LL.editor.snapTo.snapWithRange()} 63 | 64 | 65 | { 69 | dispatch('ok') 70 | opened = false 71 | }} 72 | > 73 | {$LL.editor.dialog.ok()} 74 | 75 | 76 | 77 | 78 | 133 | -------------------------------------------------------------------------------- /src/lib/dialogs/LibraryDialog.svelte: -------------------------------------------------------------------------------- 1 | 29 | 30 | { 33 | loading = true 34 | try { 35 | library = await list() 36 | } catch (err) { 37 | toast.error($LL.editor.messages.library.loadingFailed()) 38 | console.error(err) 39 | } finally { 40 | loading = false 41 | } 42 | }} 43 | > 44 | 45 | {$LL.editor.dialog.libraryTitle()} 46 | 47 | { 51 | opened = false 52 | }} 53 | /> 54 | 55 | 56 | {#if loading} 57 | 63 | {:else} 64 | {#each library as item} 65 | 66 | {item.title.ja} 67 | {item.description.ja} 68 | 69 | { 73 | dispatch('input', item.content) 74 | }} 75 | > 76 | {$LL.editor.dialog.append()} 77 | 78 | 79 | {/each} 80 | {/if} 81 | 82 | 83 | 84 | 85 | 180 | -------------------------------------------------------------------------------- /src/lib/dialogs/TimeSignatureDialog.svelte: -------------------------------------------------------------------------------- 1 | 36 | 37 | { 40 | inputElement.focus() 41 | inputElement.select() 42 | }} 43 | > 44 | 45 | 46 | {$LL.editor.dialog.timeSignatureTitle()} 47 | 48 | { 52 | dispatch('cancel') 53 | opened = false 54 | }} 55 | /> 56 | 57 | 58 | 59 | 60 | 66 | / 67 | 68 | (拍子) 69 | 70 | { 74 | dispatch('ok') 75 | opened = false 76 | }} 77 | > 78 | {!exist ? $LL.editor.dialog.append() : $LL.editor.dialog.change()} 79 | 80 | {#if exist} 81 | { 85 | dispatch('delete') 86 | opened = false 87 | }} 88 | > 89 | {$LL.editor.dialog.delete()} 90 | 91 | {/if} 92 | 93 | 94 | 95 | 183 | -------------------------------------------------------------------------------- /src/lib/dialogs/index.ts: -------------------------------------------------------------------------------- 1 | export async function confirm(message: string): Promise { 2 | if (window.__TAURI__) { 3 | return window.__TAURI__.dialog.confirm(message) 4 | } 5 | return new Promise((resolve) => { 6 | resolve(window.confirm(message)) 7 | }) 8 | } 9 | -------------------------------------------------------------------------------- /src/lib/editing/clipboard.ts: -------------------------------------------------------------------------------- 1 | import type { Note, Single, Slide } from '$lib/score/beatmap' 2 | import type { Cursor } from '$lib/position' 3 | 4 | import { writable, get } from 'svelte/store' 5 | import { flipLane, flipped } from '$lib/editing/flip' 6 | 7 | export const clipboardSlides = writable([]) 8 | export const clipboardSingles = writable([]) 9 | export const clipboardOffsets = writable( 10 | new Map< 11 | Note, 12 | { 13 | lane: number 14 | tick: number 15 | } 16 | >() 17 | ) 18 | 19 | function pastedNote(cursor: Cursor) { 20 | return (note: Note): Note => { 21 | const offset = get(clipboardOffsets).get(note) 22 | if (!offset) throw new Error('Unexpected not found clipboardOffset') 23 | return { 24 | ...note, 25 | lane: cursor.lane - offset.lane, 26 | tick: cursor.tick - offset.tick, 27 | } 28 | } 29 | } 30 | 31 | export function pasted(cursor: Cursor): { singles: Single[]; slides: Slide[] } { 32 | return { 33 | singles: get(clipboardSingles).map(pastedNote(cursor)) as Single[], 34 | slides: get(clipboardSlides).map((slide) => ({ 35 | ...slide, 36 | head: pastedNote(cursor)(slide.head), 37 | tail: pastedNote(cursor)(slide.tail), 38 | steps: slide.steps.map(pastedNote(cursor)), 39 | })) as Slide[], 40 | } 41 | } 42 | 43 | export function flippasted(cursor: Cursor): { 44 | singles: Single[] 45 | slides: Slide[] 46 | } { 47 | const { singles, slides } = pasted({ 48 | ...cursor, 49 | lane: flipLane(cursor.lane, 1), 50 | }) 51 | return flipped(singles, slides) 52 | } 53 | -------------------------------------------------------------------------------- /src/lib/editing/flick.ts: -------------------------------------------------------------------------------- 1 | import '$lib/basic/collections' 2 | import { FLICK_TYPES } from '$lib/score/beatmap' 3 | 4 | import type { Flick } from '$lib/score/beatmap' 5 | 6 | export function rotateFlick(flick: Flick): Flick { 7 | return FLICK_TYPES.rotateNext(flick) 8 | } 9 | 10 | export function flipFlick(flick: Flick): Flick { 11 | switch (flick) { 12 | case 'left': 13 | return 'right' 14 | case 'right': 15 | return 'left' 16 | case 'middle': 17 | return 'middle' 18 | case 'no': 19 | return 'no' 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/lib/editing/flip.ts: -------------------------------------------------------------------------------- 1 | import type { Note, Single, Slide } from '$lib/score/beatmap' 2 | 3 | import { LANE_SUS_MAX } from '$lib/consts' 4 | import { flipFlick } from '$lib/editing/flick' 5 | import { BatchUpdate } from '$lib/editing/mutations' 6 | 7 | export function flipLane(lane: number, width: number): number { 8 | return LANE_SUS_MAX - lane - width 9 | } 10 | 11 | export function flippedNote(note: Note): Note { 12 | return { 13 | ...note, 14 | ...('flick' in note ? { flick: flipFlick(note.flick) } : {}), 15 | lane: flipLane(note.lane, note.width), 16 | } 17 | } 18 | 19 | export function flipped( 20 | singles: Single[], 21 | slides: Slide[] 22 | ): { singles: Single[]; slides: Slide[] } { 23 | return { 24 | singles: singles.map(flippedNote) as Single[], 25 | slides: slides.map((slide) => ({ 26 | ...slide, 27 | head: flippedNote(slide.head), 28 | tail: flippedNote(slide.tail), 29 | steps: slide.steps.map(flippedNote), 30 | })) as Slide[], 31 | } 32 | } 33 | 34 | export function flipNotes( 35 | singles: Single[], 36 | slides: Slide[], 37 | notes: Note[] 38 | ): BatchUpdate { 39 | const pickLaneFlick = (note: Note) => ({ 40 | lane: note.lane, 41 | ...('flick' in note ? { flick: note.flick } : {}), 42 | }) 43 | const flipTargets = new Map( 44 | notes.map((note) => [note, pickLaneFlick(flippedNote(note))]) 45 | ) 46 | const flipOrigins = new Map(notes.map((note) => [note, pickLaneFlick(note)])) 47 | return new BatchUpdate(singles, slides, flipTargets, flipOrigins, 'flip') 48 | } 49 | 50 | export function vflipNotes( 51 | singles: Single[], 52 | slides: Slide[], 53 | notes: Note[] 54 | ): BatchUpdate { 55 | const [minTick, maxTick] = notes.reduce( 56 | ([min, max], note) => { 57 | return [Math.min(min, note.tick), Math.max(max, note.tick)] 58 | }, 59 | [Number.MAX_SAFE_INTEGER, 0] 60 | ) 61 | 62 | const flipTargets = new Map( 63 | notes.map((note) => [note, { tick: maxTick - note.tick + minTick }]) 64 | ) 65 | const flipOrigins = new Map(notes.map((note) => [note, { tick: note.tick }])) 66 | return new BatchUpdate(singles, slides, flipTargets, flipOrigins, 'flip') 67 | } 68 | -------------------------------------------------------------------------------- /src/lib/editing/history.ts: -------------------------------------------------------------------------------- 1 | import { writable } from 'svelte/store' 2 | import type { Mutation } from '$lib/editing/mutations' 3 | 4 | export const mutationHistory = writable[]>([]) 5 | export const undoneHistory = writable[]>([]) 6 | -------------------------------------------------------------------------------- /src/lib/editing/modes.ts: -------------------------------------------------------------------------------- 1 | export const ALLOWED_SNAPPINGS = [4, 8, 12, 16, 24, 32, 48, 64, 96, 128, 192] 2 | export type SnapTo = number 3 | 4 | import select from '$assets/select.png' 5 | import tap from '$assets/notes/noteN.png' 6 | import slide from '$assets/notes/noteL.png' 7 | import mid from '$assets/notes/notes_long_among.png' 8 | import flick from '$assets/notes/notes_flick_arrow_01.png' 9 | import critical from '$assets/notes/noteC.png' 10 | import bpm from '$assets/BPM.png' 11 | import timeSignature from '$assets/TimeSignature.png' 12 | 13 | type ImageSource = string 14 | 15 | export const MODES = [ 16 | 'select', 17 | 'tap', 18 | 'slide', 19 | 'mid', 20 | 'flick', 21 | 'critical', 22 | 'bpm', 23 | 'timeSignature', 24 | ] as const 25 | 26 | export type Mode = typeof MODES[number] 27 | 28 | export const MODE_TEXTURES: Record = { 29 | select, 30 | tap, 31 | slide, 32 | mid, 33 | flick, 34 | critical, 35 | bpm, 36 | timeSignature, 37 | } 38 | 39 | export const MODE_FLOATING_TEXTURES: Record = { 40 | tap: 'noteN.png', 41 | slide: 'noteL.png', 42 | mid: 'notes_long_among.png', 43 | flick: 'noteF.png', 44 | critical: 'noteC.png', 45 | } 46 | 47 | export const MODE_SHORTCUTS = { 48 | select: 'v', 49 | tap: 't', 50 | slide: 's', 51 | mid: 'r', 52 | flick: 'f', 53 | critical: 'c', 54 | bpm: 'b', 55 | timeSignature: 'm', 56 | } 57 | 58 | export const MODE_SHORTCUTS_NUMERAL = { 59 | select: '1', 60 | tap: '2', 61 | slide: '3', 62 | mid: '4', 63 | flick: '5', 64 | critical: '6', 65 | bpm: '7', 66 | timeSignature: '8', 67 | } 68 | -------------------------------------------------------------------------------- /src/lib/editing/moving.ts: -------------------------------------------------------------------------------- 1 | import { writable, get } from 'svelte/store' 2 | import { hasCritical, hasFlick, isSlideHead } from '$lib/score/beatmap' 3 | import type { Note, Single, Slide, INote } from '$lib/score/beatmap' 4 | import { 5 | AddRemoveSlides, 6 | CombinedMutation, 7 | UpdateSingles, 8 | UpdateSlides, 9 | } from '$lib/editing/mutations' 10 | import { combineSlides, getSlideNotes } from '$lib/editing/slides' 11 | 12 | export type MoveEvent = CustomEvent<{ 13 | lane: number 14 | tick: number 15 | note: Note 16 | }> 17 | 18 | import type { LaneTick } from '$lib/position' 19 | 20 | export const moving = writable(false) 21 | export const movingNotes = writable([]) 22 | export const movingTargets = writable(new Map()) 23 | export const movingOrigins = writable(new Map()) 24 | export const movingOffsets = writable(new Map()) 25 | 26 | export function moveNotes( 27 | singles: Single[], 28 | slides: Slide[] 29 | ): CombinedMutation | AddRemoveSlides | null { 30 | const notes = [...get(movingNotes)] 31 | movingNotes.set([]) 32 | const targets: Map = new Map(get(movingTargets)) 33 | 34 | // -- Check if combining -- 35 | if (notes.length === 1) { 36 | const note = notes[0] 37 | const target = { 38 | ...targets.get(notes[0])!, 39 | width: notes[0].width, 40 | } 41 | let slideA 42 | let slideB 43 | 44 | if (hasCritical(note) && hasFlick(note)) { 45 | slideA = slides.find(({ tail }) => tail === note) 46 | slideB = slides.find( 47 | ({ head }) => 48 | head.tick === target.tick && 49 | head.lane === target.lane && 50 | head.width === target.width 51 | ) 52 | } else if (isSlideHead(note)) { 53 | slideA = slides.find( 54 | ({ tail }) => 55 | tail.tick === target.tick && 56 | tail.lane === target.lane && 57 | tail.width === target.width 58 | ) 59 | slideB = slides.find(({ head }) => head === note) 60 | } 61 | 62 | if (slideA && slideB) { 63 | return combineSlides(slides, slideA, slideB, target) 64 | } 65 | } 66 | 67 | // -- Check if anything changes position -- 68 | if ( 69 | [...targets].every( 70 | ([note, target]) => note.lane === target.lane && note.tick === target.tick 71 | ) 72 | ) { 73 | return null 74 | } 75 | 76 | const movingSlides = slides.filter((slide) => 77 | getSlideNotes(slide).some((note) => notes.includes(note)) 78 | ) 79 | 80 | const slideModifications: Map> = new Map( 81 | movingSlides.map((slide) => { 82 | const { head, tail, steps } = { 83 | ...slide, 84 | head: { ...slide.head, ...(targets.get(slide.head) ?? []) }, 85 | tail: { ...slide.tail, ...(targets.get(slide.tail) ?? []) }, 86 | steps: slide.steps 87 | .map((step) => ({ 88 | ...step, 89 | ...(targets.get(step) ?? []), 90 | })) 91 | .sort((a, b) => a.tick - b.tick), 92 | } 93 | 94 | const pickINote = ({ lane, tick, width }: INote) => ({ 95 | lane, 96 | tick, 97 | width, 98 | }) 99 | 100 | return [ 101 | slide, 102 | { 103 | head: head.tick > tail.tick ? { ...head, ...pickINote(tail) } : head, 104 | tail: head.tick > tail.tick ? { ...tail, ...pickINote(head) } : tail, 105 | steps, 106 | }, 107 | ] 108 | }) 109 | ) 110 | const slideOriginaldatas: Map> = new Map( 111 | [...slideModifications].map(([slide]) => [ 112 | slide, 113 | { 114 | head: slide.head, 115 | tail: slide.tail, 116 | steps: slide.steps, 117 | }, 118 | ]) 119 | ) 120 | 121 | return new CombinedMutation( 122 | singles, 123 | slides, 124 | [ 125 | new UpdateSingles( 126 | singles, 127 | new Map( 128 | [...targets].filter(([note]) => singles.includes(note as Single)) as [ 129 | Single, 130 | Partial 131 | ][] 132 | ), 133 | 'move' 134 | ), 135 | new UpdateSlides(slides, slideModifications, slideOriginaldatas), 136 | ], 137 | 'note', 138 | notes.length, 139 | 'move' 140 | ) 141 | } 142 | -------------------------------------------------------------------------------- /src/lib/editing/playhead.ts: -------------------------------------------------------------------------------- 1 | import { writable } from 'svelte/store' 2 | export const dragging = writable(false) 3 | -------------------------------------------------------------------------------- /src/lib/editing/resizing.ts: -------------------------------------------------------------------------------- 1 | import type { Note } from '$lib/score/beatmap' 2 | 3 | import { writable } from 'svelte/store' 4 | import { clamp, minmax } from '$lib/basic/math' 5 | import { WIDTH_DEFAULT } from '$lib/consts' 6 | 7 | export const resizing = writable(false) 8 | export const resizingNotes = writable([]) 9 | export const resizingOriginNote = writable() 10 | export const resizingTargets = writable( 11 | new Map() 12 | ) 13 | export const resizingOrigins = writable( 14 | new Map() 15 | ) 16 | export const resizingOffsets = writable( 17 | new Map() 18 | ) 19 | export const resizingLastWidth = writable(WIDTH_DEFAULT) 20 | 21 | export function calcResized(a: number, b: number): [number, number] { 22 | const [left, right] = minmax(a, b) 23 | const width = right - left 24 | return [left, clamp(1, width, 12)] 25 | } 26 | -------------------------------------------------------------------------------- /src/lib/editing/scrolling.ts: -------------------------------------------------------------------------------- 1 | import { snap } from '$lib/basic/math' 2 | import { MARGIN_BOTTOM, TICK_HEIGHT } from '$lib/consts' 3 | import { position } from '$lib/position' 4 | import { get, writable } from 'svelte/store' 5 | 6 | export const SCROLL_MODES = ['page', 'smooth', 'none'] as const 7 | export type ScrollMode = typeof SCROLL_MODES[number] 8 | 9 | export function calcScrollTick(y: number, zoom: number): number { 10 | return (MARGIN_BOTTOM - y) / (TICK_HEIGHT * zoom) 11 | } 12 | 13 | export function calcScrollY(tick: number, zoom: number): number { 14 | return MARGIN_BOTTOM - TICK_HEIGHT * zoom * tick 15 | } 16 | 17 | export const scrollY = writable(0) 18 | 19 | type ScrollFunction = (currentTick: number) => number 20 | export const SCROLL_FUNCTIONS: Record = 21 | { 22 | page: (currentTick: number) => { 23 | const pos = get(position) 24 | return snap( 25 | currentTick + pos.calcDistanceTicks(MARGIN_BOTTOM), 26 | pos.calcDistanceTicks(pos.containerHeight) * 0.76 27 | ) 28 | }, 29 | smooth: (currentTick: number) => { 30 | const pos = get(position) 31 | return ( 32 | currentTick - 33 | pos.calcDistanceTicks(pos.containerHeight * 0.5) + 34 | pos.calcDistanceTicks(MARGIN_BOTTOM) 35 | ) 36 | // currentTick - innerHeight / measureHeight * TICK_PER_MEASURE * 0.5 + MARGIN_BOTTOM / MEASURE_HEIGHT * TICK_PER_MEASURE 37 | }, 38 | none: undefined, 39 | } as const 40 | -------------------------------------------------------------------------------- /src/lib/editing/selection.ts: -------------------------------------------------------------------------------- 1 | import { writable } from 'svelte/store' 2 | import type { Note } from '$lib/score/beatmap' 3 | 4 | export const selectedNotes = writable([]) 5 | export const hoveringNote = writable() 6 | -------------------------------------------------------------------------------- /src/lib/editing/slides.ts: -------------------------------------------------------------------------------- 1 | import type { INote, Slide, SlideNote } from '$lib/score/beatmap' 2 | import { AddRemoveSlides } from '$lib/editing/mutations' 3 | 4 | export function getSlideNotes({ head, tail, steps }: Slide): SlideNote[] { 5 | return [head, ...steps, tail] 6 | } 7 | 8 | export function combineSlides( 9 | slides: Slide[], 10 | a: Slide, 11 | b: Slide, 12 | target: INote 13 | ): AddRemoveSlides { 14 | const slide = { 15 | head: a.head, 16 | tail: b.tail, 17 | steps: [ 18 | ...a.steps, 19 | { 20 | tick: target.tick, 21 | lane: target.lane, 22 | width: target.width, 23 | diamond: false, 24 | ignored: false, 25 | easeType: b.head.easeType, 26 | }, 27 | ...b.steps, 28 | ], 29 | critical: a.critical || b.critical, 30 | } 31 | return new AddRemoveSlides(slides, [slide], [a, b], 1, 'combine') 32 | } 33 | -------------------------------------------------------------------------------- /src/lib/editing/visibility.ts: -------------------------------------------------------------------------------- 1 | import { writable } from 'svelte/store' 2 | 3 | export const visibilitys = [ 4 | 'taps', 5 | 'flicks', 6 | 'slides', 7 | 'slidesteps', 8 | 'all', 9 | ] as const 10 | 11 | export type VisibilityType = typeof visibilitys[number] 12 | 13 | export const visibility = writable>({ 14 | taps: true, 15 | flicks: true, 16 | slides: true, 17 | slidesteps: true, 18 | all: true, 19 | }) 20 | -------------------------------------------------------------------------------- /src/lib/position.ts: -------------------------------------------------------------------------------- 1 | import { between, clamp, snap } from './basic/math' 2 | import { 3 | LANE_MAX, 4 | LANE_MIN, 5 | LANE_COUNT, 6 | MARGIN, 7 | MARGIN_BOTTOM, 8 | TICK_PER_MEASURE, 9 | TICK_HEIGHT, 10 | } from './consts' 11 | import { writable } from 'svelte/store' 12 | 13 | interface IRect { 14 | top: number 15 | bottom: number 16 | left: number 17 | right: number 18 | height: number 19 | width: number 20 | x: number 21 | y: number 22 | } 23 | 24 | export class PositionManager { 25 | zoom: number 26 | laneWidth: number 27 | laneAreaWidth: number 28 | measureHeight: number 29 | containerHeight: number 30 | containerWidth: number 31 | contanierLeft: number 32 | 33 | constructor( 34 | measureHeight: number, 35 | containerHeight: number, 36 | containerWidth: number, 37 | zoom: number, 38 | laneWidth: number 39 | ) { 40 | this.measureHeight = measureHeight 41 | this.containerHeight = containerHeight 42 | this.containerWidth = containerWidth 43 | this.zoom = zoom 44 | this.laneWidth = laneWidth 45 | this.laneAreaWidth = laneWidth * LANE_COUNT 46 | 47 | this.contanierLeft = 48 | this.containerWidth / 2 - (this.laneAreaWidth + 2 * MARGIN) / 2 49 | } 50 | 51 | calcX(lane: number): number { 52 | return MARGIN + this.contanierLeft + (lane - 1) * this.laneWidth 53 | } 54 | 55 | calcMidX(lane: number, width: number): number { 56 | return this.calcX(lane) + (this.laneWidth * width) / 2 57 | } 58 | 59 | calcFixedX(lane: number): number { 60 | return MARGIN + (lane - 1) * this.laneWidth 61 | } 62 | 63 | calcFixedMidX(lane: number, width: number): number { 64 | return this.calcFixedX(lane) + (this.laneWidth * width) / 2 65 | } 66 | 67 | calcLeft(): number { 68 | return this.calcX(1) 69 | } 70 | 71 | calcDistanceY(ticks: number): number { 72 | return ticks * TICK_HEIGHT * this.zoom 73 | } 74 | 75 | calcDistanceTicks(y: number): number { 76 | return y / (TICK_HEIGHT * this.zoom) 77 | } 78 | 79 | calcY(tick: number): number { 80 | return ( 81 | this.containerHeight - (MARGIN_BOTTOM + tick * TICK_HEIGHT * this.zoom) 82 | ) 83 | } 84 | 85 | calcRawLane(x: number): number { 86 | return (x - MARGIN - this.contanierLeft) / this.laneWidth + 1 87 | } 88 | 89 | calcLane(x: number): number { 90 | return Math.floor(clamp(LANE_MIN, this.calcRawLane(x), LANE_MAX)) 91 | } 92 | 93 | calcLaneSide(x: number): number { 94 | return Math.floor(clamp(LANE_MIN, this.calcRawLane(x), LANE_MAX + 1)) 95 | } 96 | 97 | calcRawTick(y: number): number { 98 | const rawTick = 99 | ((this.containerHeight - y - MARGIN_BOTTOM) / this.measureHeight) * 100 | TICK_PER_MEASURE 101 | return Math.max(0, rawTick) 102 | } 103 | 104 | calcRawTick2(y: number): number { 105 | const rawTick = 106 | ((this.containerHeight - y - 2 * MARGIN_BOTTOM) / this.measureHeight) * 107 | TICK_PER_MEASURE 108 | return Math.max(0, rawTick) 109 | } 110 | 111 | calcScrolledTick(y: number, scrollTick: number): number { 112 | return this.calcRawTick(y) + scrollTick 113 | } 114 | 115 | calcTick(y: number, scrollTick: number, snapTo: number): number { 116 | return snap(this.calcRawTick(y) + scrollTick, TICK_PER_MEASURE / snapTo) 117 | } 118 | 119 | intersectRect( 120 | lane: number, 121 | width: number, 122 | tick: number, 123 | rect: IRect 124 | ): boolean { 125 | const laneR = lane + width 126 | const top = this.calcRawTick(rect.top) 127 | const bottom = this.calcRawTick(rect.bottom) 128 | const left = this.calcRawLane(rect.left) 129 | const right = this.calcRawLane(rect.right) 130 | return lane < right && laneR > left && between(top, tick, bottom) 131 | } 132 | } 133 | 134 | export const position = writable() 135 | 136 | export type LaneTick = { 137 | lane: number 138 | tick: number 139 | } 140 | 141 | export type Point = { 142 | x: number 143 | y: number 144 | } 145 | 146 | export type Cursor = LaneTick & { 147 | laneSide: number 148 | rawTick: number 149 | rawLane: number 150 | } 151 | export const cursor = writable({ 152 | lane: 0, 153 | tick: 0, 154 | laneSide: 0, 155 | rawTick: 0, 156 | rawLane: 0, 157 | }) 158 | export const placing = writable<{ lane: number; width: number }>({ 159 | lane: 0, 160 | width: 0, 161 | }) 162 | export const inside = writable(false) 163 | export const pointer = writable({ x: 0, y: 0 }) 164 | -------------------------------------------------------------------------------- /src/lib/preferences.ts: -------------------------------------------------------------------------------- 1 | import { preferences as preferencesFromDatabase } from '$lib/database' 2 | import { writable } from 'svelte/store' 3 | 4 | // Also add UI in $lib/dialogs/PreferencesDialog.svelte 5 | export const DEFAULT_PREFERENCES = { 6 | autosaveInterval: 10, 7 | scrollSpeed: 1, 8 | noteHeight: 0.85, 9 | minimapEnabled: true, 10 | laneWidth: 26, 11 | multiTapWarningEnabled: true, 12 | fileSaveName: '{project}-{datetime}.sus', 13 | autoDetectBPM: true 14 | } 15 | 16 | export type Preferences = typeof DEFAULT_PREFERENCES 17 | 18 | export const preferences = writable(DEFAULT_PREFERENCES) 19 | 20 | preferencesFromDatabase.subscribe((value) => { 21 | preferences.set({ 22 | ...DEFAULT_PREFERENCES, 23 | ...value, 24 | }) 25 | }) 26 | -------------------------------------------------------------------------------- /src/lib/render/BPM.svelte: -------------------------------------------------------------------------------- 1 | 102 | -------------------------------------------------------------------------------- /src/lib/render/DraggingSlide.svelte: -------------------------------------------------------------------------------- 1 | 6 | 7 | {#if draggingSlide !== null} 8 | 9 | {/if} 10 | -------------------------------------------------------------------------------- /src/lib/render/Fever.svelte: -------------------------------------------------------------------------------- 1 | 63 | -------------------------------------------------------------------------------- /src/lib/render/FloatingNotes.svelte: -------------------------------------------------------------------------------- 1 | 13 | 14 | {#each singles as single} 15 | 16 | {/each} 17 | {#each slides as slide} 18 | 19 | {/each} 20 | -------------------------------------------------------------------------------- /src/lib/render/Grid.svelte: -------------------------------------------------------------------------------- 1 | 134 | -------------------------------------------------------------------------------- /src/lib/render/MovingNotes.svelte: -------------------------------------------------------------------------------- 1 | 46 | 47 | {#if moving} 48 | 49 | {/if} 50 | -------------------------------------------------------------------------------- /src/lib/render/Note.svelte: -------------------------------------------------------------------------------- 1 | 81 | -------------------------------------------------------------------------------- /src/lib/render/PastingNotes.svelte: -------------------------------------------------------------------------------- 1 | 24 | 25 | {#if $ctrlKey && $clipboardOffsets.size > 0} 26 | 27 | {/if} 28 | -------------------------------------------------------------------------------- /src/lib/render/Playhead.svelte: -------------------------------------------------------------------------------- 1 | 67 | -------------------------------------------------------------------------------- /src/lib/render/ResizingNotes.svelte: -------------------------------------------------------------------------------- 1 | 48 | 49 | {#if resizing} 50 | 51 | {/if} 52 | -------------------------------------------------------------------------------- /src/lib/render/Scrollbar.svelte: -------------------------------------------------------------------------------- 1 | 153 | -------------------------------------------------------------------------------- /src/lib/render/Selection.svelte: -------------------------------------------------------------------------------- 1 | 43 | -------------------------------------------------------------------------------- /src/lib/render/Single.svelte: -------------------------------------------------------------------------------- 1 | 22 | 23 | 34 | 35 | {#if !floating} 36 | 48 | {/if} 49 | -------------------------------------------------------------------------------- /src/lib/render/Skill.svelte: -------------------------------------------------------------------------------- 1 | 51 | -------------------------------------------------------------------------------- /src/lib/render/Slide.svelte: -------------------------------------------------------------------------------- 1 | 41 | 42 | 43 | !x.ignored), tail]} 45 | {critical} 46 | {floating} 47 | {moving} 48 | on:click={() => { 49 | dispatch('pathclick', { slide }) 50 | }} 51 | on:rightclick={() => { 52 | dispatch('pathrightclick', { slide }) 53 | }} 54 | on:dblclick={() => { 55 | dispatch('dblclick', { note: slide.head }) 56 | }} 57 | /> 58 | 59 | 60 | { 67 | dispatch('headclick', { note: head }) 68 | }} 69 | on:rightclick 70 | on:dblclick 71 | on:pointerenter={() => { 72 | dispatch('pointerenter', { note: head }) 73 | }} 74 | on:pointerleave={() => { 75 | dispatch('pointerleave') 76 | }} 77 | /> 78 | 79 | 80 | { 88 | onstepclick(event) 89 | }} 90 | /> 91 | 92 | 93 | { 100 | dispatch('tailclick', { note: tail }) 101 | }} 102 | on:dblclick 103 | on:rightclick 104 | on:pointerenter={() => { 105 | dispatch('pointerenter', { note: tail }) 106 | }} 107 | on:pointerleave={() => { 108 | dispatch('pointerleave') 109 | }} 110 | /> 111 | -------------------------------------------------------------------------------- /src/lib/render/minimap.ts: -------------------------------------------------------------------------------- 1 | import type { PositionManager } from '$lib/position' 2 | import type { 3 | ICritical, 4 | IDirectional, 5 | Note, 6 | Single, 7 | Slide, 8 | } from '$lib/score/beatmap' 9 | import { Graphics, Container } from 'pixi.js' 10 | 11 | export abstract class MinimapNoteRendererBase extends Graphics { 12 | arrows: (Note & IDirectional & ICritical)[] = [] 13 | 14 | abstract drawNotes( 15 | position: PositionManager, 16 | singles: Single[], 17 | slides: Slide[] 18 | ): void 19 | } 20 | 21 | export abstract class MinimapRendererBase extends Container { 22 | notes: MinimapNoteRendererBase 23 | grid: Graphics 24 | screenArea: Graphics 25 | constructor( 26 | notes: MinimapNoteRendererBase, 27 | grid: Graphics, 28 | screenArea: Graphics 29 | ) { 30 | super() 31 | this.notes = notes 32 | this.grid = grid 33 | this.screenArea = screenArea 34 | 35 | this.addChild(this.screenArea) 36 | this.addChild(this.notes) 37 | this.addChild(this.grid) 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/lib/render/note.ts: -------------------------------------------------------------------------------- 1 | import { cartesianProduct } from '$lib/basic/math' 2 | 3 | import { SimplePlane } from 'pixi.js' 4 | import type { Texture, Resource } from 'pixi.js' 5 | 6 | const TEXTURE_WIDTH = 354 7 | const TEXTURE_HEIGHT = 186 8 | const SIDE_WIDTH = 53 9 | const SIDE_HEIGHT = 53 10 | const SIDE_RATIO_X = SIDE_WIDTH / TEXTURE_WIDTH 11 | const SIDE_RATIO_Y = SIDE_HEIGHT / TEXTURE_HEIGHT 12 | const SLICE_X = 91 13 | const SLICE_RATIO_X = SLICE_X / TEXTURE_WIDTH 14 | 15 | const SLICE_T = 72 16 | const SLICE_B = 80 17 | const SLICE_RATIO_T = SLICE_T / TEXTURE_HEIGHT 18 | const SLICE_RATIO_B = SLICE_B / TEXTURE_HEIGHT 19 | 20 | import { get } from 'svelte/store' 21 | import { preferences } from '$lib/preferences' 22 | 23 | export function calcNoteHeight(): number { 24 | return get(preferences).noteHeight * 30 25 | } 26 | 27 | export class NotePlane extends SimplePlane { 28 | constructor(texture: Texture, width: number, height: number) { 29 | super(texture, 4, 4) 30 | 31 | this.uvCoords = new Float32Array( 32 | cartesianProduct([ 33 | [0, SLICE_RATIO_X, 1 - SLICE_RATIO_X, 1], 34 | [0, SLICE_RATIO_T, 1 - SLICE_RATIO_B, 1], 35 | ]).flat() 36 | ) 37 | 38 | this.update(0, 0, width, height) 39 | } 40 | 41 | get uvCoords() { 42 | return this.geometry.getBuffer('aTextureCoord').data 43 | } 44 | 45 | set uvCoords(value) { 46 | this.geometry.getBuffer('aTextureCoord').update(value) 47 | } 48 | 49 | get vertices() { 50 | return this.geometry.getBuffer('aVertexPosition').data 51 | } 52 | 53 | set vertices(value) { 54 | this.geometry.getBuffer('aVertexPosition').update(value) 55 | } 56 | 57 | updateTexture(texture: Texture) { 58 | this.texture = texture 59 | } 60 | 61 | update(x: number, y: number, width: number, height: number) { 62 | const borderL = x - width / 2 63 | const borderR = x + width / 2 64 | const borderT = y - height / 2 65 | const borderB = y + height / 2 66 | 67 | const BASE_WIDTH = 130 68 | 69 | const sideX = BASE_WIDTH * SIDE_RATIO_X 70 | const sliceX = BASE_WIDTH * SLICE_RATIO_X 71 | 72 | const middleX = sliceX - sideX 73 | 74 | const BASE_HEIGHT = 60 75 | 76 | const sideY = BASE_HEIGHT * SIDE_RATIO_Y 77 | const sliceT = BASE_HEIGHT * SLICE_RATIO_T 78 | const sliceB = BASE_HEIGHT * SLICE_RATIO_B 79 | 80 | const middleT = sliceT - sideY 81 | const middleB = sliceB - sideY 82 | 83 | this.vertices = new Float32Array( 84 | cartesianProduct([ 85 | [ 86 | borderL - sideX, 87 | borderL + middleX, 88 | borderR - middleX, 89 | borderR + sideX, 90 | ], 91 | [ 92 | borderT - sideY, 93 | borderT + middleT, 94 | borderB - middleB, 95 | borderB + sideY, 96 | ], 97 | ]).flat() 98 | ) 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /src/lib/render/renderer.ts: -------------------------------------------------------------------------------- 1 | // Types 2 | import type PIXI from 'pixi.js' 3 | 4 | // Drawing Functions 5 | export function drawDashedLine( 6 | graphics: PIXI.Graphics, 7 | fromX: number, 8 | fromY: number, 9 | toX: number, 10 | toY: number, 11 | dash = 10, 12 | gap = 8 13 | ) { 14 | graphics.moveTo(fromX, fromY) 15 | const currentPosition = { 16 | x: fromX, 17 | y: fromY, 18 | } 19 | 20 | for ( 21 | ; 22 | Math.abs(currentPosition.x) < toX || Math.abs(currentPosition.y) < toY; 23 | 24 | ) { 25 | currentPosition.x = 26 | Math.abs(currentPosition.x + dash) < toX ? currentPosition.x + dash : toX 27 | currentPosition.y = 28 | Math.abs(currentPosition.y + dash) < toY ? currentPosition.y + dash : toY 29 | 30 | graphics.lineTo(currentPosition.x, currentPosition.y) 31 | 32 | currentPosition.x = 33 | Math.abs(currentPosition.x + gap) < toX ? currentPosition.x + gap : toX 34 | currentPosition.y = 35 | Math.abs(currentPosition.y + gap) < toY ? currentPosition.y + gap : toY 36 | 37 | graphics.moveTo(currentPosition.x, currentPosition.y) 38 | } 39 | } 40 | 41 | export function createGradientCanvas( 42 | width: number, 43 | height: number, 44 | colors: string[] 45 | ) { 46 | const canvas = document.createElement('canvas') 47 | const ctx = canvas.getContext('2d')! 48 | const gradient = ctx.createLinearGradient(0, 0, 0, height) 49 | 50 | canvas.setAttribute('width', `${width}px`) 51 | canvas.setAttribute('height', `${height}px`) 52 | 53 | colors.forEach((color, index) => { 54 | gradient.addColorStop(index / (color.length - 1), color) 55 | }) 56 | 57 | ctx.fillStyle = gradient 58 | ctx.fillRect(0, 0, width, height) 59 | 60 | return canvas 61 | } 62 | -------------------------------------------------------------------------------- /src/lib/score/beatmap.ts: -------------------------------------------------------------------------------- 1 | export interface Metadata { 2 | title: string 3 | artist: string 4 | author: string 5 | offset: number 6 | } 7 | 8 | export interface INote { 9 | tick: number 10 | lane: number 11 | width: number 12 | } 13 | 14 | export const FLICK_TYPES = ['no', 'middle', 'left', 'right'] as const 15 | export type Flick = typeof FLICK_TYPES[number] 16 | 17 | export interface IDirectional { 18 | flick: Flick 19 | } 20 | 21 | export interface ICritical { 22 | critical: boolean 23 | } 24 | 25 | export interface IDiamond { 26 | diamond: boolean 27 | ignored: boolean 28 | } 29 | 30 | export const EASE_TYPES = ['easeIn', 'easeOut', false] 31 | export type EaseType = typeof EASE_TYPES[number] 32 | 33 | export const DIAMOND_TYPES = ['ignored', 'visible', 'invisible'] as const 34 | export type DiamondType = typeof DIAMOND_TYPES[number] 35 | 36 | export function toDiamondType({ diamond, ignored }: IDiamond): DiamondType { 37 | return ignored ? 'ignored' : diamond ? 'visible' : 'invisible' 38 | } 39 | 40 | export function fromDiamondType(diamondType: DiamondType): IDiamond { 41 | switch (diamondType) { 42 | case 'visible': 43 | return { diamond: true, ignored: false } 44 | case 'invisible': 45 | return { diamond: false, ignored: false } 46 | case 'ignored': 47 | return { diamond: true, ignored: true } 48 | } 49 | } 50 | 51 | export interface IEase { 52 | easeType: EaseType 53 | } 54 | 55 | export type Single = INote & IDirectional & ICritical 56 | export type SlideHead = INote & IEase 57 | export type SlideStep = INote & IDiamond & IEase 58 | export type SlideTail = INote & IDirectional & ICritical 59 | export type SlideNote = SlideHead | SlideStep | SlideTail 60 | export type Slide = { 61 | head: SlideHead 62 | tail: SlideTail 63 | steps: SlideStep[] 64 | } & ICritical 65 | 66 | export function hasEaseType(note: Note): note is Note & IEase { 67 | return 'easeType' in note 68 | } 69 | 70 | export function isSlideStep(note: Note): note is SlideStep { 71 | return 'diamond' in note && 'ignored' in note 72 | } 73 | 74 | export function isSlideHead(note: Note): note is SlideHead { 75 | return 'easeType' in note && !('diamond' in note) 76 | } 77 | 78 | export function isSingleOrSlideTail(note: Note): note is Single | SlideTail { 79 | return 'flick' in note && 'critical' in note 80 | } 81 | 82 | export function isSingleNote(singles: Single[], note: Note): note is Single { 83 | return singles.some((single) => single === note) 84 | } 85 | 86 | export function isSlideNote(slides: Slide[], note: Note): note is SlideNote { 87 | return slides.some(({ head, tail, steps }) => head === note || tail === note || steps.includes(note as SlideStep)) 88 | } 89 | 90 | export function hasFlick(note: Note): note is Note & IDirectional { 91 | return 'flick' in note 92 | } 93 | 94 | export function hasCritical(note: Note): note is Note & ICritical { 95 | return 'critical' in note 96 | } 97 | 98 | export type Fever = [startTick: number, endTick: number] | null 99 | export type TimeSignature = [beats: number, beatType: number] 100 | 101 | export type Score = { 102 | singles: Single[] 103 | slides: Slide[] 104 | bpms: Map 105 | fever: Fever 106 | skills: Set 107 | timeSignatures: Map 108 | } 109 | 110 | export interface SerialisedScore { 111 | singles: Single[] 112 | slides: Slide[] 113 | bpms: [number, number][] 114 | fever: Fever 115 | skills: number[] 116 | timeSignatures: [number, TimeSignature][] 117 | } 118 | 119 | export function serialiseScore(score: Score): SerialisedScore { 120 | const { singles, slides, bpms, fever, skills, timeSignatures } = score 121 | return { 122 | singles, 123 | slides, 124 | bpms: [...bpms], 125 | fever, 126 | skills: [...skills], 127 | timeSignatures: [...timeSignatures], 128 | } 129 | } 130 | 131 | export function deserialiseScore(serialisedScore: SerialisedScore): Score { 132 | const { singles, slides, bpms, fever, skills, timeSignatures } = 133 | serialisedScore 134 | return { 135 | singles, 136 | slides, 137 | bpms: new Map(bpms), 138 | fever, 139 | skills: new Set(skills), 140 | timeSignatures: new Map(timeSignatures), 141 | } 142 | } 143 | 144 | export type Beatmap = { 145 | metadata: Metadata 146 | score: Score 147 | } 148 | 149 | export const NOTE_TYPES = ['tap', 'critical', 'flick', 'slide'] as const 150 | export type Type = typeof NOTE_TYPES[number] 151 | export function calcType( 152 | critical: boolean, 153 | flick: Flick, 154 | slide: boolean 155 | ): Type { 156 | return critical 157 | ? 'critical' 158 | : flick !== 'no' 159 | ? 'flick' 160 | : slide 161 | ? 'slide' 162 | : 'tap' 163 | } 164 | 165 | export type Note = Single | SlideNote 166 | -------------------------------------------------------------------------------- /src/lib/style.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --color-background-main: #2e3142; 3 | --color-text-main: #eeeeee; 4 | --color-text-darker: #aaaaaa; 5 | --color-text-lighter: #ffffff; 6 | 7 | --input-border-radius: 10px; 8 | --input-padding-vertical: 0.75em; 9 | --input-padding-horizontal: 1em; 10 | --input-padding: var(--input-padding-vertical) var(--input-padding-horizontal); 11 | 12 | -webkit-font-smoothing: antialiased; 13 | -moz-osx-font-smoothing: grayscale; 14 | 15 | color-scheme: dark; 16 | font-family: 'M PLUS 1p', sans-serif; 17 | overflow: hidden; 18 | 19 | font-size: 14px; 20 | } 21 | 22 | body { 23 | margin: 0; 24 | overflow: hidden; 25 | } 26 | 27 | * { 28 | box-sizing: border-box; 29 | } 30 | 31 | button, 32 | label, 33 | audio, 34 | select, 35 | input { 36 | cursor: inherit; 37 | font-size: inherit; 38 | font-family: inherit; 39 | min-width: 0; 40 | } 41 | 42 | input[type='text'], 43 | input[type='number'] { 44 | appearance: none; 45 | border: none; 46 | border-radius: var(--input-border-radius); 47 | color: inherit; 48 | padding: var(--input-padding); 49 | box-shadow: inset 1px 1px 5px rgba(0, 0, 0, 0.6); 50 | background: rgba(255, 255, 255, 0.1); 51 | } 52 | -------------------------------------------------------------------------------- /src/lib/timing.ts: -------------------------------------------------------------------------------- 1 | import type { TimeSignature } from '$lib/score/beatmap' 2 | 3 | import { writable } from 'svelte/store' 4 | import { snap } from './basic/math' 5 | import { TICK_PER_BEAT } from './consts' 6 | 7 | export const sortedBPMs = writable<[number, number][]>([]) 8 | 9 | export function tick2secs(tick: number, tpb: number, bpm: number): number { 10 | return (tick / tpb / bpm) * 60 11 | } 12 | 13 | export function accumulateDuration( 14 | targetTick: number, 15 | bpms: [tick: number, bpm: number][], 16 | tpb: number 17 | ): number { 18 | return bpms 19 | .filter(([tick]) => tick <= targetTick) 20 | .reduce( 21 | (acc, [tick, bpm], ind, arr) => 22 | acc + 23 | (ind < arr.length - 1 24 | ? tick2secs(arr[ind + 1][0] - tick, tpb, +bpm) 25 | : tick2secs(targetTick - tick, tpb, +bpm)), 26 | 0 27 | ) 28 | } 29 | 30 | export type TimeSignatureInfo = { 31 | measure: number 32 | startTick: number 33 | beatsPerMeasure: number 34 | p: number 35 | q: number 36 | } 37 | 38 | export class TimeSignatureManager { 39 | timeSignatureInfos: TimeSignatureInfo[] 40 | constructor(timeSignatures: Map) { 41 | let accumulatedTicks = 0 42 | this.timeSignatureInfos = [...timeSignatures] 43 | .sort(([a], [b]) => a - b) 44 | .map(([measure, timeSignature], ind, arr) => { 45 | const [nextMeasure] = arr[ind + 1] ?? [Infinity] 46 | const startTick = accumulatedTicks 47 | const [p, q] = timeSignature 48 | const beatsPerMeasure = (p / q) * 4 49 | accumulatedTicks += 50 | (nextMeasure - measure) * beatsPerMeasure * TICK_PER_BEAT 51 | return { measure, beatsPerMeasure, p, q, startTick } 52 | }) 53 | } 54 | 55 | private find(tick: number): TimeSignatureInfo | undefined { 56 | return [...this.timeSignatureInfos] 57 | .reverse() 58 | .find(({ startTick }) => startTick <= tick) 59 | } 60 | 61 | get(tick: number): number { 62 | const { beatsPerMeasure } = this.find(tick) ?? this.timeSignatureInfos[0] 63 | return beatsPerMeasure 64 | } 65 | 66 | tick2measure(tick: number): number { 67 | const { measure, startTick, beatsPerMeasure } = 68 | this.find(tick) ?? this.timeSignatureInfos[0] 69 | return Math.floor( 70 | measure + (tick - startTick) / TICK_PER_BEAT / beatsPerMeasure 71 | ) 72 | } 73 | 74 | has(tick: number): boolean { 75 | return this.timeSignatureInfos.some( 76 | ({ measure }) => this.tick2measure(tick) === measure 77 | ) 78 | } 79 | 80 | snap(tick: number, snapTo: number): number { 81 | const { p, beatsPerMeasure } = this.find(tick) ?? this.timeSignatureInfos[0] 82 | return snap(tick, ((TICK_PER_BEAT * beatsPerMeasure) / p / snapTo) * 4) 83 | } 84 | 85 | getTickRanges(): [number, number][] { 86 | return this.timeSignatureInfos.map(({ startTick }, ind, arr) => [ 87 | startTick, 88 | arr[ind + 1]?.startTick ?? Infinity, 89 | ]) 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /src/lib/ui/Button.svelte: -------------------------------------------------------------------------------- 1 | 25 | 26 | 27 | {#if href === undefined} 28 | 35 | {#if icon} 36 | {#if loading} 37 | 43 | {:else} 44 | 45 | {/if} 46 | {/if} 47 | 48 | 49 | {:else} 50 | 59 | {#if icon} 60 | {#if loading} 61 | 67 | {:else} 68 | 69 | {/if} 70 | {/if} 71 | 72 | 73 | {/if} 74 | 75 | 76 | 131 | -------------------------------------------------------------------------------- /src/lib/ui/Checkbox.svelte: -------------------------------------------------------------------------------- 1 | 6 | 7 | 13 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /src/lib/ui/ClickableIcon.svelte: -------------------------------------------------------------------------------- 1 | 15 | 16 | {#if href} 17 | 18 | 19 | 20 | {:else} 21 | 22 | 23 | 24 | {/if} 25 | 26 | 44 | -------------------------------------------------------------------------------- /src/lib/ui/DebugInfo.svelte: -------------------------------------------------------------------------------- 1 | 15 | 16 | { 20 | hidden = true 21 | }} 22 | on:pointerdown={(e) => { 23 | if (e.button === 0) { 24 | const { left, top } = debugDisplay.getBoundingClientRect() 25 | offsetX = e.clientX - left 26 | offsetY = e.clientY - top 27 | window.addEventListener('pointermove', movediv) 28 | } 29 | }} 30 | on:pointerup={() => { 31 | window.removeEventListener('pointermove', movediv) 32 | }} 33 | bind:this={debugDisplay} 34 | > 35 | {#each [...$debugInfo.entries()] as [title, value]} 36 | {title} 37 | {value} 38 | {/each} 39 | 40 | 41 | 71 | -------------------------------------------------------------------------------- /src/lib/ui/FileInput.svelte: -------------------------------------------------------------------------------- 1 | 49 | 50 | 51 | 52 | { 56 | /* empty */ 57 | }} 58 | on:drop|preventDefault|capture={dropHandler( 59 | accept, 60 | (dropped) => { 61 | file = dropped 62 | dispatch('open', file) 63 | }, 64 | () => { 65 | toast.error($LL.editor.messages.unknownFileType()) 66 | } 67 | )} 68 | > 69 | {#if file} 70 | 71 | 72 | 73 | 74 | 81 | 82 | {:else} 83 | {text} 86 | {/if} 87 | 88 | 89 | 90 | 91 | {#if file} 92 | { 96 | if (file) await download(file, file.name) 97 | }} 98 | /> 99 | 100 | 101 | { 105 | if (await confirm($LL.editor.messages.deleteConfirm())) { 106 | input.value = '' 107 | file = null 108 | } 109 | }} 110 | /> 111 | {/if} 112 | 113 | 114 | 130 | -------------------------------------------------------------------------------- /src/lib/ui/KeyboardShortcut.svelte: -------------------------------------------------------------------------------- 1 | 38 | 39 | 40 | {#each keys as keyCombination} 41 | 42 | {#each keyCombination as subKey} 43 | 44 | {KEYBOARD_DISPLAY_NAMES[subKey] ?? subKey} 45 | 46 | {/each} 47 | 48 | {/each} 49 | 50 | 51 | 78 | -------------------------------------------------------------------------------- /src/lib/ui/Menu.svelte: -------------------------------------------------------------------------------- 1 | 11 | 12 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 40 | -------------------------------------------------------------------------------- /src/lib/ui/MenuDivider.svelte: -------------------------------------------------------------------------------- 1 | 2 | 3 | 10 | -------------------------------------------------------------------------------- /src/lib/ui/MenuTrigger.svelte: -------------------------------------------------------------------------------- 1 | 8 | 9 | 174 | 175 | {#if !contextArea} 176 | 177 | 178 | 179 | {/if} 180 | 181 | 186 | -------------------------------------------------------------------------------- /src/lib/ui/MenuWrapper.svelte: -------------------------------------------------------------------------------- 1 | 8 | 9 | {#if wrap} 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | {:else} 19 | 20 | {/if} 21 | -------------------------------------------------------------------------------- /src/lib/ui/Modal.svelte: -------------------------------------------------------------------------------- 1 | 39 | 40 | 46 | { 50 | opened = false 51 | }} 52 | /> 53 | 54 | {#if opened} 55 | 56 | {/if} 57 | 58 | 59 | 60 | 61 | 62 | 96 | -------------------------------------------------------------------------------- /src/lib/ui/Select.svelte: -------------------------------------------------------------------------------- 1 | 6 | 7 | 8 | {#each [...options] as [v, text]} 9 | {text} 10 | {/each} 11 | 12 | 13 | 27 | -------------------------------------------------------------------------------- /src/lib/ui/TabContent.svelte: -------------------------------------------------------------------------------- 1 | 8 | 9 | 13 | 14 | 15 | 16 | 26 | -------------------------------------------------------------------------------- /src/lib/ui/TabItem.svelte: -------------------------------------------------------------------------------- 1 | 8 | 9 | { 13 | selectTab(tabIndex) 14 | }} 15 | > 16 | 17 | 18 | 19 | 44 | -------------------------------------------------------------------------------- /src/lib/ui/TabSelect.svelte: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 11 | -------------------------------------------------------------------------------- /src/lib/ui/Tabs.svelte: -------------------------------------------------------------------------------- 1 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /src/lib/ui/TextInput.svelte: -------------------------------------------------------------------------------- 1 | 13 | 14 | { 17 | inputElement?.focus() 18 | }} 19 | > 20 | 21 | {#if type === 'number'} 22 | 29 | {:else if type === 'text'} 30 | 37 | {:else if type === 'search'} 38 | 45 | {#if value !== ''} 46 | 47 | {/if} 48 | {/if} 49 | 50 | 51 | 52 | 53 | 97 | -------------------------------------------------------------------------------- /src/lib/ui/ToolButton.svelte: -------------------------------------------------------------------------------- 1 | 29 | 30 | [key] 37 | )} 38 | > 39 | { 41 | currentMode = mode 42 | }} 43 | class:current={currentMode === mode} 44 | bind:this={button} 45 | > 46 | 47 | 48 | 49 | 50 | 97 | -------------------------------------------------------------------------------- /src/lib/ui/Tooltip.svelte: -------------------------------------------------------------------------------- 1 | 32 | 33 | 34 | 35 | 36 | 37 | {#if description !== undefined} 38 | {description} 39 | {/if} 40 | {#if keys} 41 | 42 | {/if} 43 | 44 | 45 | 46 | 70 | -------------------------------------------------------------------------------- /src/lib/ui/UndoToast.svelte: -------------------------------------------------------------------------------- 1 | 17 | 18 | 19 | {#if undone} 20 | {text} 21 | {:else} 22 | {text} 23 | {/if} 24 | 25 | {button} 26 | 27 | 28 | 29 | 44 | -------------------------------------------------------------------------------- /src/lib/ui/Wrapper.svelte: -------------------------------------------------------------------------------- 1 | 6 | 7 | {#if wrap} 8 | 9 | 10 | 11 | {:else} 12 | 13 | {/if} 14 | -------------------------------------------------------------------------------- /src/lib/ui/toast.ts: -------------------------------------------------------------------------------- 1 | import { SvelteToastOptions, toast } from '@zerodevx/svelte-toast' 2 | 3 | import UndoToast from '$lib/ui/UndoToast.svelte' 4 | 5 | import type { SvelteComponent } from 'svelte' 6 | import type { Mutation } from '$lib/editing/mutations' 7 | import type { Writable } from 'svelte/store' 8 | 9 | let lastUndoToastId = 0 10 | 11 | export default { 12 | success: (message: string, options?: SvelteToastOptions) => { 13 | toast.push(message, { 14 | theme: { 15 | '--toastBackground': '#48BB78', 16 | '--toastBarBackground': '#2F855A', 17 | }, 18 | pausable: true, 19 | ...options, 20 | }) 21 | }, 22 | error: (message: string, options?: SvelteToastOptions) => { 23 | toast.push(message, { 24 | theme: { 25 | '--toastBackground': '#F56565', 26 | '--toastBarBackground': '#C53030', 27 | }, 28 | pausable: true, 29 | ...options, 30 | }) 31 | }, 32 | warn: (message: string, options?: SvelteToastOptions) => { 33 | toast.push(message, { 34 | theme: { 35 | '--toastBackground': '#F0B400', 36 | '--toastBarBackground': '#A67C00', 37 | }, 38 | pausable: true, 39 | ...options, 40 | }) 41 | }, 42 | undo: ( 43 | mutation: Mutation, 44 | history: Writable[]>, 45 | button: string, 46 | undo: () => void, 47 | undone: boolean 48 | ) => { 49 | if (lastUndoToastId) { 50 | toast.pop(lastUndoToastId) 51 | } 52 | 53 | setTimeout( 54 | () => { 55 | lastUndoToastId = toast.push({ 56 | component: { 57 | src: UndoToast as unknown as typeof SvelteComponent, 58 | props: { 59 | text: mutation.toString(), 60 | button, 61 | undo, 62 | mutation, 63 | history, 64 | undone, 65 | }, 66 | }, 67 | theme: { 68 | '--toastWidth': '20rem', 69 | }, 70 | duration: 8000, 71 | pausable: true, 72 | }) 73 | }, 74 | lastUndoToastId ? 400 : 0 75 | ) 76 | }, 77 | } 78 | -------------------------------------------------------------------------------- /src/routes/__layout.svelte: -------------------------------------------------------------------------------- 1 | 15 | 16 | 24 | 25 | 26 | 27 | 28 | 29 | 34 | 38 | 39 | -------------------------------------------------------------------------------- /static/assets/fonts/Font.fnt: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /static/assets/fonts/Font.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mkpoli/paletteworks-editor/bf268907e8343ce58c61cdced64c75a6fba25c50/static/assets/fonts/Font.png -------------------------------------------------------------------------------- /static/assets/textures/path.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mkpoli/paletteworks-editor/bf268907e8343ce58c61cdced64c75a6fba25c50/static/assets/textures/path.png -------------------------------------------------------------------------------- /static/assets/textures/path_critical.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mkpoli/paletteworks-editor/bf268907e8343ce58c61cdced64c75a6fba25c50/static/assets/textures/path_critical.png -------------------------------------------------------------------------------- /static/assets/textures/spritesheet.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mkpoli/paletteworks-editor/bf268907e8343ce58c61cdced64c75a6fba25c50/static/assets/textures/spritesheet.png -------------------------------------------------------------------------------- /static/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mkpoli/paletteworks-editor/bf268907e8343ce58c61cdced64c75a6fba25c50/static/favicon.png -------------------------------------------------------------------------------- /svelte.config.js: -------------------------------------------------------------------------------- 1 | import preprocess from 'svelte-preprocess' 2 | import path from 'path' 3 | import vercel from '@sveltejs/adapter-vercel' 4 | import static_ from '@sveltejs/adapter-static' 5 | 6 | /** @type {import('@sveltejs/kit').Config} */ 7 | const config = { 8 | // Consult https://github.com/sveltejs/svelte-preprocess 9 | // for more information about preprocessors 10 | preprocess: preprocess(), 11 | 12 | kit: { 13 | // hydrate the element in src/app.html 14 | adapter: process.env.TAURI_CONFIG ? static_() : vercel(), 15 | vite: { 16 | assetsInclude: ['**/*.fnt'], 17 | resolve: { 18 | alias: { 19 | $assets: path.resolve('./src/assets'), 20 | $i18n: path.resolve('./src/i18n'), 21 | }, 22 | }, 23 | define: { 24 | 'process.env.PACKAGE_VERSION': JSON.stringify( 25 | process.env.npm_package_version 26 | ), 27 | }, 28 | }, 29 | }, 30 | } 31 | 32 | export default config 33 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "moduleResolution": "node", 4 | "module": "es2020", 5 | "lib": ["es2020"], 6 | "target": "es2019", 7 | "strict": true, 8 | "importHelpers": false, 9 | /** 10 | svelte-preprocess cannot figure out whether you have a value or a type, so tell TypeScript 11 | to enforce using \`import type\` instead of \`import\` for Types. 12 | */ 13 | "importsNotUsedAsValues": "error", 14 | "isolatedModules": true, 15 | "resolveJsonModule": true, 16 | /** 17 | To have warnings/errors of the Svelte compiler at the correct position, 18 | enable source maps by default. 19 | */ 20 | "sourceMap": true, 21 | "esModuleInterop": true, 22 | "skipLibCheck": true, 23 | "forceConsistentCasingInFileNames": true, 24 | "baseUrl": "src", 25 | "allowJs": false, 26 | "checkJs": false, 27 | "paths": { 28 | "$lib/*": ["lib/*"], 29 | "$assets/*": ["assets/*"], 30 | "$i18n/*": ["i18n/*"] 31 | }, 32 | "typeRoots": ["node_modules/@types"] 33 | }, 34 | "include": ["src/**/*.d.ts", "src/**/*.ts", "src/**/*.svelte"] 35 | } 36 | -------------------------------------------------------------------------------- /tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | { 2 | "include": ["**/*.spec.ts", "src/global.d.ts"], 3 | } 4 | --------------------------------------------------------------------------------
{item.description.ja}