From 9491a1006e8ad2ba3a01d09f88eb9d1983df05ba Mon Sep 17 00:00:00 2001 From: Skyler Lehmkuhl Date: Thu, 14 Nov 2024 19:41:53 -0500 Subject: [PATCH 01/46] Initial commit --- README.md | 3 + package.json | 12 + pnpm-lock.yaml | 125 + src-tauri/Cargo.lock | 4569 +++++++++++++++++++++++++ src-tauri/Cargo.toml | 25 + src-tauri/build.rs | 3 + src-tauri/capabilities/default.json | 10 + src-tauri/icons/128x128.png | Bin 0 -> 3512 bytes src-tauri/icons/128x128@2x.png | Bin 0 -> 7012 bytes src-tauri/icons/32x32.png | Bin 0 -> 974 bytes src-tauri/icons/Square107x107Logo.png | Bin 0 -> 2863 bytes src-tauri/icons/Square142x142Logo.png | Bin 0 -> 3858 bytes src-tauri/icons/Square150x150Logo.png | Bin 0 -> 3966 bytes src-tauri/icons/Square284x284Logo.png | Bin 0 -> 7737 bytes src-tauri/icons/Square30x30Logo.png | Bin 0 -> 903 bytes src-tauri/icons/Square310x310Logo.png | Bin 0 -> 8591 bytes src-tauri/icons/Square44x44Logo.png | Bin 0 -> 1299 bytes src-tauri/icons/Square71x71Logo.png | Bin 0 -> 2011 bytes src-tauri/icons/Square89x89Logo.png | Bin 0 -> 2468 bytes src-tauri/icons/StoreLogo.png | Bin 0 -> 1523 bytes src-tauri/icons/icon.icns | Bin 0 -> 98451 bytes src-tauri/icons/icon.ico | Bin 0 -> 86642 bytes src-tauri/icons/icon.png | Bin 0 -> 14183 bytes src-tauri/src/lib.rs | 14 + src-tauri/src/main.rs | 6 + src-tauri/tauri.conf.json | 33 + src/assets/buttons.svg | 128 + src/assets/javascript.svg | 1 + src/assets/stage.svg | 59 + src/assets/tauri.svg | 6 + src/index.html | 44 + src/main.js | 261 ++ src/styles.css | 194 ++ 33 files changed, 5493 insertions(+) create mode 100644 README.md create mode 100644 package.json create mode 100644 pnpm-lock.yaml create mode 100644 src-tauri/Cargo.lock create mode 100644 src-tauri/Cargo.toml create mode 100644 src-tauri/build.rs create mode 100644 src-tauri/capabilities/default.json create mode 100644 src-tauri/icons/128x128.png create mode 100644 src-tauri/icons/128x128@2x.png create mode 100644 src-tauri/icons/32x32.png create mode 100644 src-tauri/icons/Square107x107Logo.png create mode 100644 src-tauri/icons/Square142x142Logo.png create mode 100644 src-tauri/icons/Square150x150Logo.png create mode 100644 src-tauri/icons/Square284x284Logo.png create mode 100644 src-tauri/icons/Square30x30Logo.png create mode 100644 src-tauri/icons/Square310x310Logo.png create mode 100644 src-tauri/icons/Square44x44Logo.png create mode 100644 src-tauri/icons/Square71x71Logo.png create mode 100644 src-tauri/icons/Square89x89Logo.png create mode 100644 src-tauri/icons/StoreLogo.png create mode 100644 src-tauri/icons/icon.icns create mode 100644 src-tauri/icons/icon.ico create mode 100644 src-tauri/icons/icon.png create mode 100644 src-tauri/src/lib.rs create mode 100644 src-tauri/src/main.rs create mode 100644 src-tauri/tauri.conf.json create mode 100644 src/assets/buttons.svg create mode 100644 src/assets/javascript.svg create mode 100644 src/assets/stage.svg create mode 100644 src/assets/tauri.svg create mode 100644 src/index.html create mode 100644 src/main.js create mode 100644 src/styles.css diff --git a/README.md b/README.md new file mode 100644 index 0000000..18afe19 --- /dev/null +++ b/README.md @@ -0,0 +1,3 @@ +# Lightningbeam 2 + +This README needs content. This is Lightningbeam rewritten with Tauri. \ No newline at end of file diff --git a/package.json b/package.json new file mode 100644 index 0000000..170a296 --- /dev/null +++ b/package.json @@ -0,0 +1,12 @@ +{ + "name": "lightningbeam", + "private": true, + "version": "0.1.0", + "type": "module", + "scripts": { + "tauri": "tauri" + }, + "devDependencies": { + "@tauri-apps/cli": "^2" + } +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml new file mode 100644 index 0000000..0587e78 --- /dev/null +++ b/pnpm-lock.yaml @@ -0,0 +1,125 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + + .: + devDependencies: + '@tauri-apps/cli': + specifier: ^2 + version: 2.0.4 + +packages: + + '@tauri-apps/cli-darwin-arm64@2.0.4': + resolution: {integrity: sha512-siH7rOHobb16rPbc11k64p1mxIpiRCkWmzs2qmL5IX21Gx9K5onI3Tk67Oqpf2uNupbYzItrOttaDT4NHFC7tw==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [darwin] + + '@tauri-apps/cli-darwin-x64@2.0.4': + resolution: {integrity: sha512-zIccfbCoZMfmUpnk6PFCV0keFyfVj1A9XV3Oiiitj/dkTZ9CQvzjhX3XC0XcK4rsTWegfr2PjSrK06aiPAROAw==} + engines: {node: '>= 10'} + cpu: [x64] + os: [darwin] + + '@tauri-apps/cli-linux-arm-gnueabihf@2.0.4': + resolution: {integrity: sha512-fgQqJzefOGWCBNg4yrVA82Rg4s1XQr5K0dc2rCxBhJfa696e8dQ1LDrnWq/AiO5r+uHfVaoQTIUvxxpFicYRSA==} + engines: {node: '>= 10'} + cpu: [arm] + os: [linux] + + '@tauri-apps/cli-linux-arm64-gnu@2.0.4': + resolution: {integrity: sha512-u8wbt5tPA9pI6j+d7jGrfOz9UVCiTp+IYzKNiIqlrDsAjqAUFaNXYHKqOUboeFWEmI4zoCWj6LgpS2OJTQ5FKg==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [linux] + + '@tauri-apps/cli-linux-arm64-musl@2.0.4': + resolution: {integrity: sha512-hntF1V8e3V1hlrESm93PsghDhf3lA5pbvFrRfYxU1c+fVD/jRXGVw8BH3O1lW8MWwhEg1YdhKk01oAgsuHLuig==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [linux] + + '@tauri-apps/cli-linux-x64-gnu@2.0.4': + resolution: {integrity: sha512-Iq1GGJb+oT1T0ZV8izrgf0cBtlzPCJaWcNueRbf1ZXquMf+FSTyQv+/Lo8rq5T6buOIJOH7cAOTuEWWqiCZteg==} + engines: {node: '>= 10'} + cpu: [x64] + os: [linux] + + '@tauri-apps/cli-linux-x64-musl@2.0.4': + resolution: {integrity: sha512-9NTk6Pf0bSwXqCBdAA+PDYts9HeHebZzIo8mbRzRyUbER6QngG5HZb9Ka36Z1QWtJjdRy6uxSb4zb/9NuTeHfA==} + engines: {node: '>= 10'} + cpu: [x64] + os: [linux] + + '@tauri-apps/cli-win32-arm64-msvc@2.0.4': + resolution: {integrity: sha512-OF2e9oxiBFR8A8wVMOhUx9QGN/I1ZkquWC7gVQBnA56nx9PabJlDT08QBy5UD8USqZFVznnfNr2ehlheQahb3g==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [win32] + + '@tauri-apps/cli-win32-ia32-msvc@2.0.4': + resolution: {integrity: sha512-T+hCKB3rFP6q0saHHtR02hm6wr1ZPJ0Mkii3oRTxjPG6BBXoVzHNCYzvdgEGJPTA2sFuAQtJH764NRtNlDMifw==} + engines: {node: '>= 10'} + cpu: [ia32] + os: [win32] + + '@tauri-apps/cli-win32-x64-msvc@2.0.4': + resolution: {integrity: sha512-GVaiI3KWRFLomjJmApHqihhYlkJ+7FqhumhVfBO6Z2tWzZjQyVQgTdNp0kYEuW2WoAYEj0dKY6qd4YM33xYcUA==} + engines: {node: '>= 10'} + cpu: [x64] + os: [win32] + + '@tauri-apps/cli@2.0.4': + resolution: {integrity: sha512-Hl9eFXz+O366+6su9PfaSzu2EJdFe1p8K8ghkWmi40dz8VmSE7vsMTaOStD0I71ckSOkh2ICDX7FQTBgjlpjWw==} + engines: {node: '>= 10'} + hasBin: true + +snapshots: + + '@tauri-apps/cli-darwin-arm64@2.0.4': + optional: true + + '@tauri-apps/cli-darwin-x64@2.0.4': + optional: true + + '@tauri-apps/cli-linux-arm-gnueabihf@2.0.4': + optional: true + + '@tauri-apps/cli-linux-arm64-gnu@2.0.4': + optional: true + + '@tauri-apps/cli-linux-arm64-musl@2.0.4': + optional: true + + '@tauri-apps/cli-linux-x64-gnu@2.0.4': + optional: true + + '@tauri-apps/cli-linux-x64-musl@2.0.4': + optional: true + + '@tauri-apps/cli-win32-arm64-msvc@2.0.4': + optional: true + + '@tauri-apps/cli-win32-ia32-msvc@2.0.4': + optional: true + + '@tauri-apps/cli-win32-x64-msvc@2.0.4': + optional: true + + '@tauri-apps/cli@2.0.4': + optionalDependencies: + '@tauri-apps/cli-darwin-arm64': 2.0.4 + '@tauri-apps/cli-darwin-x64': 2.0.4 + '@tauri-apps/cli-linux-arm-gnueabihf': 2.0.4 + '@tauri-apps/cli-linux-arm64-gnu': 2.0.4 + '@tauri-apps/cli-linux-arm64-musl': 2.0.4 + '@tauri-apps/cli-linux-x64-gnu': 2.0.4 + '@tauri-apps/cli-linux-x64-musl': 2.0.4 + '@tauri-apps/cli-win32-arm64-msvc': 2.0.4 + '@tauri-apps/cli-win32-ia32-msvc': 2.0.4 + '@tauri-apps/cli-win32-x64-msvc': 2.0.4 diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock new file mode 100644 index 0000000..0c418de --- /dev/null +++ b/src-tauri/Cargo.lock @@ -0,0 +1,4569 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "addr2line" +version = "0.24.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dfbe277e56a376000877090da837660b4427aad530e3028d44e0bffe4f89a1c1" +dependencies = [ + "gimli", +] + +[[package]] +name = "adler2" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627" + +[[package]] +name = "aho-corasick" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" +dependencies = [ + "memchr", +] + +[[package]] +name = "alloc-no-stdlib" +version = "2.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc7bb162ec39d46ab1ca8c77bf72e890535becd1751bb45f64c597edb4c8c6b3" + +[[package]] +name = "alloc-stdlib" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94fb8275041c72129eb51b7d0322c29b8387a0386127718b096429201a5d6ece" +dependencies = [ + "alloc-no-stdlib", +] + +[[package]] +name = "android-tzdata" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + +[[package]] +name = "anyhow" +version = "1.0.93" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c95c10ba0b00a02636238b814946408b1322d5ac4760326e6fb8ec956d85775" + +[[package]] +name = "atk" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4af014b17dd80e8af9fa689b2d4a211ddba6eb583c1622f35d0cb543f6b17e4" +dependencies = [ + "atk-sys", + "glib", + "libc", +] + +[[package]] +name = "atk-sys" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "251e0b7d90e33e0ba930891a505a9a35ece37b2dd37a14f3ffc306c13b980009" +dependencies = [ + "glib-sys", + "gobject-sys", + "libc", + "system-deps", +] + +[[package]] +name = "autocfg" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" + +[[package]] +name = "backtrace" +version = "0.3.74" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d82cb332cdfaed17ae235a638438ac4d4839913cc2af585c3c6746e8f8bee1a" +dependencies = [ + "addr2line", + "cfg-if", + "libc", + "miniz_oxide", + "object", + "rustc-demangle", + "windows-targets 0.52.6", +] + +[[package]] +name = "base64" +version = "0.21.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" + +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "bitflags" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de" +dependencies = [ + "serde", +] + +[[package]] +name = "block" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d8c1fef690941d3e7788d328517591fecc684c084084702d6ff1641e993699a" + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "block2" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c132eebf10f5cad5289222520a4a058514204aed6d791f1cf4fe8088b82d15f" +dependencies = [ + "objc2", +] + +[[package]] +name = "brotli" +version = "7.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc97b8f16f944bba54f0433f07e30be199b6dc2bd25937444bbad560bcea29bd" +dependencies = [ + "alloc-no-stdlib", + "alloc-stdlib", + "brotli-decompressor", +] + +[[package]] +name = "brotli-decompressor" +version = "4.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a45bd2e4095a8b518033b128020dd4a55aab1c0a381ba4404a472630f4bc362" +dependencies = [ + "alloc-no-stdlib", + "alloc-stdlib", +] + +[[package]] +name = "bumpalo" +version = "3.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c" + +[[package]] +name = "bytemuck" +version = "1.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8334215b81e418a0a7bdb8ef0849474f40bb10c8b71f1c4ed315cff49f32494d" + +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[package]] +name = "bytes" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ac0150caa2ae65ca5bd83f25c7de183dea78d4d366469f148435e2acfbad0da" +dependencies = [ + "serde", +] + +[[package]] +name = "cairo-rs" +version = "0.18.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ca26ef0159422fb77631dc9d17b102f253b876fe1586b03b803e63a309b4ee2" +dependencies = [ + "bitflags 2.6.0", + "cairo-sys-rs", + "glib", + "libc", + "once_cell", + "thiserror", +] + +[[package]] +name = "cairo-sys-rs" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "685c9fa8e590b8b3d678873528d83411db17242a73fccaed827770ea0fedda51" +dependencies = [ + "glib-sys", + "libc", + "system-deps", +] + +[[package]] +name = "camino" +version = "1.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b96ec4966b5813e2c0507c1f86115c8c5abaadc3980879c3424042a02fd1ad3" +dependencies = [ + "serde", +] + +[[package]] +name = "cargo-platform" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24b1f0365a6c6bb4020cd05806fd0d33c44d38046b8bd7f0e40814b9763cabfc" +dependencies = [ + "serde", +] + +[[package]] +name = "cargo_metadata" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d886547e41f740c616ae73108f6eb70afe6d940c7bc697cb30f13daec073037" +dependencies = [ + "camino", + "cargo-platform", + "semver", + "serde", + "serde_json", + "thiserror", +] + +[[package]] +name = "cargo_toml" +version = "0.17.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a969e13a7589e9e3e4207e153bae624ade2b5622fb4684a4923b23ec3d57719" +dependencies = [ + "serde", + "toml 0.8.2", +] + +[[package]] +name = "cc" +version = "1.1.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40545c26d092346d8a8dab71ee48e7685a7a9cba76e634790c215b41a4a7b4cf" +dependencies = [ + "shlex", +] + +[[package]] +name = "cesu8" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c" + +[[package]] +name = "cfb" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d38f2da7a0a2c4ccf0065be06397cc26a81f4e528be095826eee9d4adbb8c60f" +dependencies = [ + "byteorder", + "fnv", + "uuid", +] + +[[package]] +name = "cfg-expr" +version = "0.15.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d067ad48b8650848b989a59a86c6c36a995d02d2bf778d45c3c5d57bc2718f02" +dependencies = [ + "smallvec", + "target-lexicon", +] + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "cfg_aliases" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" + +[[package]] +name = "chrono" +version = "0.4.38" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a21f936df1771bf62b77f047b726c4625ff2e8aa607c01ec06e5a05bd8463401" +dependencies = [ + "android-tzdata", + "iana-time-zone", + "num-traits", + "serde", + "windows-targets 0.52.6", +] + +[[package]] +name = "cocoa" +version = "0.26.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f79398230a6e2c08f5c9760610eb6924b52aa9e7950a619602baba59dcbbdbb2" +dependencies = [ + "bitflags 2.6.0", + "block", + "cocoa-foundation", + "core-foundation", + "core-graphics", + "foreign-types", + "libc", + "objc", +] + +[[package]] +name = "cocoa-foundation" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e14045fb83be07b5acf1c0884b2180461635b433455fa35d1cd6f17f1450679d" +dependencies = [ + "bitflags 2.6.0", + "block", + "core-foundation", + "core-graphics-types", + "libc", + "objc", +] + +[[package]] +name = "combine" +version = "4.6.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba5a308b75df32fe02788e748662718f03fde005016435c444eea572398219fd" +dependencies = [ + "bytes", + "memchr", +] + +[[package]] +name = "convert_case" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6245d59a3e82a7fc217c5828a6692dbc6dfb63a0c8c90495621f7b9d79704a0e" + +[[package]] +name = "core-foundation" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b55271e5c8c478ad3f38ad24ef34923091e0548492a266d19b3c0b4d82574c63" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "core-graphics" +version = "0.24.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa95a34622365fa5bbf40b20b75dba8dfa8c94c734aea8ac9a5ca38af14316f1" +dependencies = [ + "bitflags 2.6.0", + "core-foundation", + "core-graphics-types", + "foreign-types", + "libc", +] + +[[package]] +name = "core-graphics-types" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d44a101f213f6c4cdc1853d4b78aef6db6bdfa3468798cc1d9912f4735013eb" +dependencies = [ + "bitflags 2.6.0", + "core-foundation", + "libc", +] + +[[package]] +name = "cpufeatures" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "608697df725056feaccfa42cffdaeeec3fccc4ffc38358ecd19b243e716a78e0" +dependencies = [ + "libc", +] + +[[package]] +name = "crc32fast" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a97769d94ddab943e4510d138150169a2758b5ef3eb191a9ee688de3e23ef7b3" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "crossbeam-channel" +version = "0.5.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33480d6946193aa8033910124896ca395333cae7e2d1113d1fef6c3272217df2" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22ec99545bb0ed0ea7bb9b8e1e9122ea386ff8a48c0922e43f36d45ab09e0e80" + +[[package]] +name = "crypto-common" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "cssparser" +version = "0.27.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "754b69d351cdc2d8ee09ae203db831e005560fc6030da058f86ad60c92a9cb0a" +dependencies = [ + "cssparser-macros", + "dtoa-short", + "itoa 0.4.8", + "matches", + "phf 0.8.0", + "proc-macro2", + "quote", + "smallvec", + "syn 1.0.109", +] + +[[package]] +name = "cssparser-macros" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13b588ba4ac1a99f7f2964d24b3d896ddc6bf847ee3855dbd4366f058cfcd331" +dependencies = [ + "quote", + "syn 2.0.87", +] + +[[package]] +name = "ctor" +version = "0.2.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edb49164822f3ee45b17acd4a208cfc1251410cf0cad9a833234c9890774dd9f" +dependencies = [ + "quote", + "syn 2.0.87", +] + +[[package]] +name = "darling" +version = "0.20.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f63b86c8a8826a49b8c21f08a2d07338eec8d900540f8630dc76284be802989" +dependencies = [ + "darling_core", + "darling_macro", +] + +[[package]] +name = "darling_core" +version = "0.20.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95133861a8032aaea082871032f5815eb9e98cef03fa916ab4500513994df9e5" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn 2.0.87", +] + +[[package]] +name = "darling_macro" +version = "0.20.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d336a2a514f6ccccaa3e09b02d41d35330c07ddf03a62165fcec10bb561c7806" +dependencies = [ + "darling_core", + "quote", + "syn 2.0.87", +] + +[[package]] +name = "deranged" +version = "0.3.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b42b6fa04a440b495c8b04d0e71b707c585f83cb9cb28cf8cd0d976c315e31b4" +dependencies = [ + "powerfmt", + "serde", +] + +[[package]] +name = "derive_more" +version = "0.99.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f33878137e4dafd7fa914ad4e259e18a4e8e532b9617a2d0150262bf53abfce" +dependencies = [ + "convert_case", + "proc-macro2", + "quote", + "rustc_version", + "syn 2.0.87", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", +] + +[[package]] +name = "dirs" +version = "5.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44c45a9d03d6676652bcb5e724c7e988de1acad23a711b5217ab9cbecbec2225" +dependencies = [ + "dirs-sys", +] + +[[package]] +name = "dirs-sys" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "520f05a5cbd335fae5a99ff7a6ab8627577660ee5cfd6a94a6a929b52ff0321c" +dependencies = [ + "libc", + "option-ext", + "redox_users", + "windows-sys 0.48.0", +] + +[[package]] +name = "dispatch" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd0c93bb4b0c6d9b77f4435b0ae98c24d17f1c45b2ff844c6151a07256ca923b" + +[[package]] +name = "displaydoc" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.87", +] + +[[package]] +name = "dlopen2" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e1297103d2bbaea85724fcee6294c2d50b1081f9ad47d0f6f6f61eda65315a6" +dependencies = [ + "dlopen2_derive", + "libc", + "once_cell", + "winapi", +] + +[[package]] +name = "dlopen2_derive" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2b99bf03862d7f545ebc28ddd33a665b50865f4dfd84031a393823879bd4c54" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.87", +] + +[[package]] +name = "dpi" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f25c0e292a7ca6d6498557ff1df68f32c99850012b6ea401cf8daf771f22ff53" +dependencies = [ + "serde", +] + +[[package]] +name = "dtoa" +version = "1.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dcbb2bf8e87535c23f7a8a321e364ce21462d0ff10cb6407820e8e96dfff6653" + +[[package]] +name = "dtoa-short" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd1511a7b6a56299bd043a9c167a6d2bfb37bf84a6dfceaba651168adfb43c87" +dependencies = [ + "dtoa", +] + +[[package]] +name = "dunce" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" + +[[package]] +name = "dyn-clone" +version = "1.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d6ef0072f8a535281e4876be788938b528e9a1d43900b82c2569af7da799125" + +[[package]] +name = "embed-resource" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4e24052d7be71f0efb50c201557f6fe7d237cfd5a64fd5bcd7fd8fe32dbbffa" +dependencies = [ + "cc", + "memchr", + "rustc_version", + "toml 0.8.2", + "vswhom", + "winreg", +] + +[[package]] +name = "embed_plist" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ef6b89e5b37196644d8796de5268852ff179b44e96276cf4290264843743bb7" + +[[package]] +name = "encoding_rs" +version = "0.8.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "equivalent" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" + +[[package]] +name = "erased-serde" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24e2389d65ab4fab27dc2a5de7b191e1f6617d1f1c8855c0dc569c94a4cbb18d" +dependencies = [ + "serde", + "typeid", +] + +[[package]] +name = "fdeflate" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07c6f4c64c1d33a3111c4466f7365ebdcc37c5bd1ea0d62aae2e3d722aacbedb" +dependencies = [ + "simd-adler32", +] + +[[package]] +name = "field-offset" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38e2275cc4e4fc009b0669731a1e5ab7ebf11f469eaede2bab9309a5b4d6057f" +dependencies = [ + "memoffset", + "rustc_version", +] + +[[package]] +name = "flate2" +version = "1.0.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1b589b4dc103969ad3cf85c950899926ec64300a1a46d76c03a6072957036f0" +dependencies = [ + "crc32fast", + "miniz_oxide", +] + +[[package]] +name = "fluent-uri" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17c704e9dbe1ddd863da1e6ff3567795087b1eb201ce80d8fa81162e1516500d" +dependencies = [ + "bitflags 1.3.2", +] + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "foreign-types" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d737d9aa519fb7b749cbc3b962edcf310a8dd1f4b67c91c4f83975dbdd17d965" +dependencies = [ + "foreign-types-macros", + "foreign-types-shared", +] + +[[package]] +name = "foreign-types-macros" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a5c6c585bc94aaf2c7b51dd4c2ba22680844aba4c687be581871a6f518c5742" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.87", +] + +[[package]] +name = "foreign-types-shared" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa9a19cbb55df58761df49b23516a86d432839add4af60fc256da840f66ed35b" + +[[package]] +name = "form_urlencoded" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "futf" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df420e2e84819663797d1ec6544b13c5be84629e7bb00dc960d6917db2987843" +dependencies = [ + "mac", + "new_debug_unreachable", +] + +[[package]] +name = "futures-channel" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" +dependencies = [ + "futures-core", +] + +[[package]] +name = "futures-core" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" + +[[package]] +name = "futures-executor" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-io" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" + +[[package]] +name = "futures-macro" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.87", +] + +[[package]] +name = "futures-sink" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" + +[[package]] +name = "futures-task" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" + +[[package]] +name = "futures-util" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" +dependencies = [ + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "pin-utils", + "slab", +] + +[[package]] +name = "fxhash" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c31b6d751ae2c7f11320402d34e41349dd1016f8d5d45e48c4312bc8625af50c" +dependencies = [ + "byteorder", +] + +[[package]] +name = "gdk" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f5ba081bdef3b75ebcdbfc953699ed2d7417d6bd853347a42a37d76406a33646" +dependencies = [ + "cairo-rs", + "gdk-pixbuf", + "gdk-sys", + "gio", + "glib", + "libc", + "pango", +] + +[[package]] +name = "gdk-pixbuf" +version = "0.18.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50e1f5f1b0bfb830d6ccc8066d18db35c487b1b2b1e8589b5dfe9f07e8defaec" +dependencies = [ + "gdk-pixbuf-sys", + "gio", + "glib", + "libc", + "once_cell", +] + +[[package]] +name = "gdk-pixbuf-sys" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9839ea644ed9c97a34d129ad56d38a25e6756f99f3a88e15cd39c20629caf7" +dependencies = [ + "gio-sys", + "glib-sys", + "gobject-sys", + "libc", + "system-deps", +] + +[[package]] +name = "gdk-sys" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "31ff856cb3386dae1703a920f803abafcc580e9b5f711ca62ed1620c25b51ff2" +dependencies = [ + "cairo-sys-rs", + "gdk-pixbuf-sys", + "gio-sys", + "glib-sys", + "gobject-sys", + "libc", + "pango-sys", + "pkg-config", + "system-deps", +] + +[[package]] +name = "gdkwayland-sys" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a90fbf5c033c65d93792192a49a8efb5bb1e640c419682a58bb96f5ae77f3d4a" +dependencies = [ + "gdk-sys", + "glib-sys", + "gobject-sys", + "libc", + "pkg-config", + "system-deps", +] + +[[package]] +name = "gdkx11" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db2ea8a4909d530f79921290389cbd7c34cb9d623bfe970eaae65ca5f9cd9cce" +dependencies = [ + "gdk", + "gdkx11-sys", + "gio", + "glib", + "libc", + "x11", +] + +[[package]] +name = "gdkx11-sys" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fee8f00f4ee46cad2939b8990f5c70c94ff882c3028f3cc5abf950fa4ab53043" +dependencies = [ + "gdk-sys", + "glib-sys", + "libc", + "system-deps", + "x11", +] + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "getrandom" +version = "0.1.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fc3cb4d91f53b50155bdcfd23f6a4c39ae1969c2ae85982b135750cccaf5fce" +dependencies = [ + "cfg-if", + "libc", + "wasi 0.9.0+wasi-snapshot-preview1", +] + +[[package]] +name = "getrandom" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" +dependencies = [ + "cfg-if", + "libc", + "wasi 0.11.0+wasi-snapshot-preview1", +] + +[[package]] +name = "gimli" +version = "0.31.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" + +[[package]] +name = "gio" +version = "0.18.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4fc8f532f87b79cbc51a79748f16a6828fb784be93145a322fa14d06d354c73" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-util", + "gio-sys", + "glib", + "libc", + "once_cell", + "pin-project-lite", + "smallvec", + "thiserror", +] + +[[package]] +name = "gio-sys" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37566df850baf5e4cb0dfb78af2e4b9898d817ed9263d1090a2df958c64737d2" +dependencies = [ + "glib-sys", + "gobject-sys", + "libc", + "system-deps", + "winapi", +] + +[[package]] +name = "glib" +version = "0.18.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "233daaf6e83ae6a12a52055f568f9d7cf4671dabb78ff9560ab6da230ce00ee5" +dependencies = [ + "bitflags 2.6.0", + "futures-channel", + "futures-core", + "futures-executor", + "futures-task", + "futures-util", + "gio-sys", + "glib-macros", + "glib-sys", + "gobject-sys", + "libc", + "memchr", + "once_cell", + "smallvec", + "thiserror", +] + +[[package]] +name = "glib-macros" +version = "0.18.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bb0228f477c0900c880fd78c8759b95c7636dbd7842707f49e132378aa2acdc" +dependencies = [ + "heck 0.4.1", + "proc-macro-crate 2.0.2", + "proc-macro-error", + "proc-macro2", + "quote", + "syn 2.0.87", +] + +[[package]] +name = "glib-sys" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "063ce2eb6a8d0ea93d2bf8ba1957e78dbab6be1c2220dd3daca57d5a9d869898" +dependencies = [ + "libc", + "system-deps", +] + +[[package]] +name = "glob" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b" + +[[package]] +name = "gobject-sys" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0850127b514d1c4a4654ead6dedadb18198999985908e6ffe4436f53c785ce44" +dependencies = [ + "glib-sys", + "libc", + "system-deps", +] + +[[package]] +name = "gtk" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93c4f5e0e20b60e10631a5f06da7fe3dda744b05ad0ea71fee2f47adf865890c" +dependencies = [ + "atk", + "cairo-rs", + "field-offset", + "futures-channel", + "gdk", + "gdk-pixbuf", + "gio", + "glib", + "gtk-sys", + "gtk3-macros", + "libc", + "pango", + "pkg-config", +] + +[[package]] +name = "gtk-sys" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "771437bf1de2c1c0b496c11505bdf748e26066bbe942dfc8f614c9460f6d7722" +dependencies = [ + "atk-sys", + "cairo-sys-rs", + "gdk-pixbuf-sys", + "gdk-sys", + "gio-sys", + "glib-sys", + "gobject-sys", + "libc", + "pango-sys", + "system-deps", +] + +[[package]] +name = "gtk3-macros" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6063efb63db582968fb7df72e1ae68aa6360dcfb0a75143f34fc7d616bad75e" +dependencies = [ + "proc-macro-crate 1.3.1", + "proc-macro-error", + "proc-macro2", + "quote", + "syn 2.0.87", +] + +[[package]] +name = "hashbrown" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" + +[[package]] +name = "hashbrown" +version = "0.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a9bfc1af68b1726ea47d3d5109de126281def866b33970e10fbab11b5dafab3" + +[[package]] +name = "heck" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "hermit-abi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024" + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "html5ever" +version = "0.26.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bea68cab48b8459f17cf1c944c67ddc572d272d9f2b274140f223ecb1da4a3b7" +dependencies = [ + "log", + "mac", + "markup5ever", + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "http" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21b9ddb458710bc376481b842f5da65cdf31522de232c1ca8146abce2a358258" +dependencies = [ + "bytes", + "fnv", + "itoa 1.0.11", +] + +[[package]] +name = "http-body" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" +dependencies = [ + "bytes", + "http", +] + +[[package]] +name = "http-body-util" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "793429d76616a256bcb62c2a2ec2bed781c8307e797e2598c50010f2bee2544f" +dependencies = [ + "bytes", + "futures-util", + "http", + "http-body", + "pin-project-lite", +] + +[[package]] +name = "httparse" +version = "1.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d71d3574edd2771538b901e6549113b4006ece66150fb69c0fb6d9a2adae946" + +[[package]] +name = "hyper" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbbff0a806a4728c99295b254c8838933b5b082d75e3cb70c8dab21fdfbcfa9a" +dependencies = [ + "bytes", + "futures-channel", + "futures-util", + "http", + "http-body", + "httparse", + "itoa 1.0.11", + "pin-project-lite", + "smallvec", + "tokio", + "want", +] + +[[package]] +name = "hyper-util" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df2dcfbe0677734ab2f3ffa7fa7bfd4706bfdc1ef393f2ee30184aed67e631b4" +dependencies = [ + "bytes", + "futures-channel", + "futures-util", + "http", + "http-body", + "hyper", + "pin-project-lite", + "socket2", + "tokio", + "tower-service", + "tracing", +] + +[[package]] +name = "iana-time-zone" +version = "0.1.61" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "235e081f3925a06703c2d0117ea8b91f042756fd6e7a6e5d901e8ca1a996b220" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "wasm-bindgen", + "windows-core 0.52.0", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + +[[package]] +name = "ico" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3804960be0bb5e4edb1e1ad67afd321a9ecfd875c3e65c099468fd2717d7cae" +dependencies = [ + "byteorder", + "png", +] + +[[package]] +name = "icu_collections" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db2fa452206ebee18c4b5c2274dbf1de17008e874b4dc4f0aea9d01ca79e4526" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locid" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13acbb8371917fc971be86fc8057c41a64b521c184808a698c02acc242dbf637" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_locid_transform" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01d11ac35de8e40fdeda00d9e1e9d92525f3f9d887cdd7aa81d727596788b54e" +dependencies = [ + "displaydoc", + "icu_locid", + "icu_locid_transform_data", + "icu_provider", + "tinystr", + "zerovec", +] + +[[package]] +name = "icu_locid_transform_data" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fdc8ff3388f852bede6b579ad4e978ab004f139284d7b28715f773507b946f6e" + +[[package]] +name = "icu_normalizer" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19ce3e0da2ec68599d193c93d088142efd7f9c5d6fc9b803774855747dc6a84f" +dependencies = [ + "displaydoc", + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "utf16_iter", + "utf8_iter", + "write16", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8cafbf7aa791e9b22bec55a167906f9e1215fd475cd22adfcf660e03e989516" + +[[package]] +name = "icu_properties" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93d6020766cfc6302c15dbbc9c8778c37e62c14427cb7f6e601d849e092aeef5" +dependencies = [ + "displaydoc", + "icu_collections", + "icu_locid_transform", + "icu_properties_data", + "icu_provider", + "tinystr", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67a8effbc3dd3e4ba1afa8ad918d5684b8868b3b26500753effea8d2eed19569" + +[[package]] +name = "icu_provider" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ed421c8a8ef78d3e2dbc98a973be2f3770cb42b606e3ab18d6237c4dfde68d9" +dependencies = [ + "displaydoc", + "icu_locid", + "icu_provider_macros", + "stable_deref_trait", + "tinystr", + "writeable", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_provider_macros" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ec89e9337638ecdc08744df490b221a7399bf8d164eb52a665454e60e075ad6" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.87", +] + +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + +[[package]] +name = "idna" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "686f825264d630750a544639377bae737628043f20d38bbc029e8f29ea968a7e" +dependencies = [ + "idna_adapter", + "smallvec", + "utf8_iter", +] + +[[package]] +name = "idna_adapter" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "daca1df1c957320b2cf139ac61e7bd64fed304c5040df000a745aa1de3b4ef71" +dependencies = [ + "icu_normalizer", + "icu_properties", +] + +[[package]] +name = "indexmap" +version = "1.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" +dependencies = [ + "autocfg", + "hashbrown 0.12.3", + "serde", +] + +[[package]] +name = "indexmap" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "707907fe3c25f5424cce2cb7e1cbcafee6bdbe735ca90ef77c29e84591e5b9da" +dependencies = [ + "equivalent", + "hashbrown 0.15.1", + "serde", +] + +[[package]] +name = "infer" +version = "0.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc150e5ce2330295b8616ce0e3f53250e53af31759a9dbedad1621ba29151847" +dependencies = [ + "cfb", +] + +[[package]] +name = "instant" +version = "0.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0242819d153cba4b4b05a5a8f2a7e9bbf97b6055b2a002b395c96b5ff3c0222" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "ipnet" +version = "2.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddc24109865250148c2e0f3d25d4f0f479571723792d3802153c60922a4fb708" + +[[package]] +name = "is-docker" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "928bae27f42bc99b60d9ac7334e3a21d10ad8f1835a4e12ec3ec0464765ed1b3" +dependencies = [ + "once_cell", +] + +[[package]] +name = "is-wsl" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "173609498df190136aa7dea1a91db051746d339e18476eed5ca40521f02d7aa5" +dependencies = [ + "is-docker", + "once_cell", +] + +[[package]] +name = "itoa" +version = "0.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b71991ff56294aa922b450139ee08b3bfc70982c6b2c7562771375cf73542dd4" + +[[package]] +name = "itoa" +version = "1.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b" + +[[package]] +name = "javascriptcore-rs" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca5671e9ffce8ffba57afc24070e906da7fc4b1ba66f2cabebf61bf2ea257fcc" +dependencies = [ + "bitflags 1.3.2", + "glib", + "javascriptcore-rs-sys", +] + +[[package]] +name = "javascriptcore-rs-sys" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af1be78d14ffa4b75b66df31840478fef72b51f8c2465d4ca7c194da9f7a5124" +dependencies = [ + "glib-sys", + "gobject-sys", + "libc", + "system-deps", +] + +[[package]] +name = "jni" +version = "0.21.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a87aa2bb7d2af34197c04845522473242e1aa17c12f4935d5856491a7fb8c97" +dependencies = [ + "cesu8", + "cfg-if", + "combine", + "jni-sys", + "log", + "thiserror", + "walkdir", + "windows-sys 0.45.0", +] + +[[package]] +name = "jni-sys" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130" + +[[package]] +name = "js-sys" +version = "0.3.72" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a88f1bda2bd75b0452a14784937d796722fdebfe50df998aeb3f0b7603019a9" +dependencies = [ + "wasm-bindgen", +] + +[[package]] +name = "json-patch" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b1fb8864823fad91877e6caea0baca82e49e8db50f8e5c9f9a453e27d3330fc" +dependencies = [ + "jsonptr 0.4.7", + "serde", + "serde_json", + "thiserror", +] + +[[package]] +name = "json-patch" +version = "3.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "863726d7afb6bc2590eeff7135d923545e5e964f004c2ccf8716c25e70a86f08" +dependencies = [ + "jsonptr 0.6.3", + "serde", + "serde_json", + "thiserror", +] + +[[package]] +name = "jsonptr" +version = "0.4.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c6e529149475ca0b2820835d3dce8fcc41c6b943ca608d32f35b449255e4627" +dependencies = [ + "fluent-uri", + "serde", + "serde_json", +] + +[[package]] +name = "jsonptr" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5dea2b27dd239b2556ed7a25ba842fe47fd602e7fc7433c2a8d6106d4d9edd70" +dependencies = [ + "serde", + "serde_json", +] + +[[package]] +name = "keyboard-types" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b750dcadc39a09dbadd74e118f6dd6598df77fa01df0cfcdc52c28dece74528a" +dependencies = [ + "bitflags 2.6.0", + "serde", + "unicode-segmentation", +] + +[[package]] +name = "kuchikiki" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f29e4755b7b995046f510a7520c42b2fed58b77bd94d5a87a8eb43d2fd126da8" +dependencies = [ + "cssparser", + "html5ever", + "indexmap 1.9.3", + "matches", + "selectors", +] + +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + +[[package]] +name = "libappindicator" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03589b9607c868cc7ae54c0b2a22c8dc03dd41692d48f2d7df73615c6a95dc0a" +dependencies = [ + "glib", + "gtk", + "gtk-sys", + "libappindicator-sys", + "log", +] + +[[package]] +name = "libappindicator-sys" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e9ec52138abedcc58dc17a7c6c0c00a2bdb4f3427c7f63fa97fd0d859155caf" +dependencies = [ + "gtk-sys", + "libloading", + "once_cell", +] + +[[package]] +name = "libc" +version = "0.2.162" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "18d287de67fe55fd7e1581fe933d965a5a9477b38e949cfa9f8574ef01506398" + +[[package]] +name = "libloading" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b67380fd3b2fbe7527a606e18729d21c6f3951633d0500574c4dc22d2d638b9f" +dependencies = [ + "cfg-if", + "winapi", +] + +[[package]] +name = "libredox" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0ff37bd590ca25063e35af745c343cb7a0271906fb7b37e4813e8f79f00268d" +dependencies = [ + "bitflags 2.6.0", + "libc", +] + +[[package]] +name = "lightningbeam" +version = "0.1.0" +dependencies = [ + "serde", + "serde_json", + "tauri", + "tauri-build", + "tauri-plugin-shell", +] + +[[package]] +name = "litemap" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "643cb0b8d4fcc284004d5fd0d67ccf61dfffadb7f75e1e71bc420f4688a3a704" + +[[package]] +name = "lock_api" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17" +dependencies = [ + "autocfg", + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24" + +[[package]] +name = "mac" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c41e0c4fef86961ac6d6f8a82609f55f31b05e4fce149ac5710e439df7619ba4" + +[[package]] +name = "malloc_buf" +version = "0.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62bb907fe88d54d8d9ce32a3cceab4218ed2f6b7d35617cafe9adf84e43919cb" +dependencies = [ + "libc", +] + +[[package]] +name = "markup5ever" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a2629bb1404f3d34c2e921f21fd34ba00b206124c81f65c50b43b6aaefeb016" +dependencies = [ + "log", + "phf 0.10.1", + "phf_codegen 0.10.0", + "string_cache", + "string_cache_codegen", + "tendril", +] + +[[package]] +name = "matches" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2532096657941c2fea9c289d370a250971c689d4f143798ff67113ec042024a5" + +[[package]] +name = "memchr" +version = "2.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" + +[[package]] +name = "memoffset" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a" +dependencies = [ + "autocfg", +] + +[[package]] +name = "mime" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" + +[[package]] +name = "miniz_oxide" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2d80299ef12ff69b16a84bb182e3b9df68b5a91574d3d4fa6e41b65deec4df1" +dependencies = [ + "adler2", + "simd-adler32", +] + +[[package]] +name = "mio" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "80e04d1dcff3aae0704555fe5fee3bcfaf3d1fdf8a7e521d5b9d2b42acb52cec" +dependencies = [ + "hermit-abi", + "libc", + "wasi 0.11.0+wasi-snapshot-preview1", + "windows-sys 0.52.0", +] + +[[package]] +name = "muda" +version = "0.15.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fdae9c00e61cc0579bcac625e8ad22104c60548a025bfc972dc83868a28e1484" +dependencies = [ + "crossbeam-channel", + "dpi", + "gtk", + "keyboard-types", + "objc2", + "objc2-app-kit", + "objc2-foundation", + "once_cell", + "png", + "serde", + "thiserror", + "windows-sys 0.59.0", +] + +[[package]] +name = "ndk" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3f42e7bbe13d351b6bead8286a43aac9534b82bd3cc43e47037f012ebfd62d4" +dependencies = [ + "bitflags 2.6.0", + "jni-sys", + "log", + "ndk-sys", + "num_enum", + "raw-window-handle", + "thiserror", +] + +[[package]] +name = "ndk-context" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27b02d87554356db9e9a873add8782d4ea6e3e58ea071a9adb9a2e8ddb884a8b" + +[[package]] +name = "ndk-sys" +version = "0.6.0+11769913" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee6cda3051665f1fb8d9e08fc35c96d5a244fb1be711a03b71118828afc9a873" +dependencies = [ + "jni-sys", +] + +[[package]] +name = "new_debug_unreachable" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086" + +[[package]] +name = "nodrop" +version = "0.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72ef4a56884ca558e5ddb05a1d1e7e1bfd9a68d9ed024c21704cc98872dae1bb" + +[[package]] +name = "num-conv" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "num_enum" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e613fc340b2220f734a8595782c551f1250e969d87d3be1ae0579e8d4065179" +dependencies = [ + "num_enum_derive", +] + +[[package]] +name = "num_enum_derive" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af1844ef2428cc3e1cb900be36181049ef3d3193c63e43026cfe202983b27a56" +dependencies = [ + "proc-macro-crate 2.0.2", + "proc-macro2", + "quote", + "syn 2.0.87", +] + +[[package]] +name = "objc" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "915b1b472bc21c53464d6c8461c9d3af805ba1ef837e1cac254428f4a77177b1" +dependencies = [ + "malloc_buf", +] + +[[package]] +name = "objc-sys" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdb91bdd390c7ce1a8607f35f3ca7151b65afc0ff5ff3b34fa350f7d7c7e4310" +dependencies = [ + "cc", +] + +[[package]] +name = "objc2" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46a785d4eeff09c14c487497c162e92766fbb3e4059a71840cecc03d9a50b804" +dependencies = [ + "objc-sys", + "objc2-encode", +] + +[[package]] +name = "objc2-app-kit" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4e89ad9e3d7d297152b17d39ed92cd50ca8063a89a9fa569046d41568891eff" +dependencies = [ + "bitflags 2.6.0", + "block2", + "libc", + "objc2", + "objc2-core-data", + "objc2-core-image", + "objc2-foundation", + "objc2-quartz-core", +] + +[[package]] +name = "objc2-cloud-kit" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74dd3b56391c7a0596a295029734d3c1c5e7e510a4cb30245f8221ccea96b009" +dependencies = [ + "bitflags 2.6.0", + "block2", + "objc2", + "objc2-core-location", + "objc2-foundation", +] + +[[package]] +name = "objc2-contacts" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5ff520e9c33812fd374d8deecef01d4a840e7b41862d849513de77e44aa4889" +dependencies = [ + "block2", + "objc2", + "objc2-foundation", +] + +[[package]] +name = "objc2-core-data" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "617fbf49e071c178c0b24c080767db52958f716d9eabdf0890523aeae54773ef" +dependencies = [ + "bitflags 2.6.0", + "block2", + "objc2", + "objc2-foundation", +] + +[[package]] +name = "objc2-core-image" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55260963a527c99f1819c4f8e3b47fe04f9650694ef348ffd2227e8196d34c80" +dependencies = [ + "block2", + "objc2", + "objc2-foundation", + "objc2-metal", +] + +[[package]] +name = "objc2-core-location" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "000cfee34e683244f284252ee206a27953279d370e309649dc3ee317b37e5781" +dependencies = [ + "block2", + "objc2", + "objc2-contacts", + "objc2-foundation", +] + +[[package]] +name = "objc2-encode" +version = "4.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7891e71393cd1f227313c9379a26a584ff3d7e6e7159e988851f0934c993f0f8" + +[[package]] +name = "objc2-foundation" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ee638a5da3799329310ad4cfa62fbf045d5f56e3ef5ba4149e7452dcf89d5a8" +dependencies = [ + "bitflags 2.6.0", + "block2", + "libc", + "objc2", +] + +[[package]] +name = "objc2-link-presentation" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1a1ae721c5e35be65f01a03b6d2ac13a54cb4fa70d8a5da293d7b0020261398" +dependencies = [ + "block2", + "objc2", + "objc2-app-kit", + "objc2-foundation", +] + +[[package]] +name = "objc2-metal" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd0cba1276f6023976a406a14ffa85e1fdd19df6b0f737b063b95f6c8c7aadd6" +dependencies = [ + "bitflags 2.6.0", + "block2", + "objc2", + "objc2-foundation", +] + +[[package]] +name = "objc2-quartz-core" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e42bee7bff906b14b167da2bac5efe6b6a07e6f7c0a21a7308d40c960242dc7a" +dependencies = [ + "bitflags 2.6.0", + "block2", + "objc2", + "objc2-foundation", + "objc2-metal", +] + +[[package]] +name = "objc2-symbols" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0a684efe3dec1b305badae1a28f6555f6ddd3bb2c2267896782858d5a78404dc" +dependencies = [ + "objc2", + "objc2-foundation", +] + +[[package]] +name = "objc2-ui-kit" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8bb46798b20cd6b91cbd113524c490f1686f4c4e8f49502431415f3512e2b6f" +dependencies = [ + "bitflags 2.6.0", + "block2", + "objc2", + "objc2-cloud-kit", + "objc2-core-data", + "objc2-core-image", + "objc2-core-location", + "objc2-foundation", + "objc2-link-presentation", + "objc2-quartz-core", + "objc2-symbols", + "objc2-uniform-type-identifiers", + "objc2-user-notifications", +] + +[[package]] +name = "objc2-uniform-type-identifiers" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44fa5f9748dbfe1ca6c0b79ad20725a11eca7c2218bceb4b005cb1be26273bfe" +dependencies = [ + "block2", + "objc2", + "objc2-foundation", +] + +[[package]] +name = "objc2-user-notifications" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76cfcbf642358e8689af64cee815d139339f3ed8ad05103ed5eaf73db8d84cb3" +dependencies = [ + "bitflags 2.6.0", + "block2", + "objc2", + "objc2-core-location", + "objc2-foundation", +] + +[[package]] +name = "objc2-web-kit" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68bc69301064cebefc6c4c90ce9cba69225239e4b8ff99d445a2b5563797da65" +dependencies = [ + "bitflags 2.6.0", + "block2", + "objc2", + "objc2-app-kit", + "objc2-foundation", +] + +[[package]] +name = "object" +version = "0.36.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aedf0a2d09c573ed1d8d85b30c119153926a2b36dce0ab28322c09a117a4683e" +dependencies = [ + "memchr", +] + +[[package]] +name = "once_cell" +version = "1.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1261fe7e33c73b354eab43b1273a57c8f967d0391e80353e51f764ac02cf6775" + +[[package]] +name = "open" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61a877bf6abd716642a53ef1b89fb498923a4afca5c754f9050b4d081c05c4b3" +dependencies = [ + "is-wsl", + "libc", + "pathdiff", +] + +[[package]] +name = "option-ext" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" + +[[package]] +name = "os_pipe" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ffd2b0a5634335b135d5728d84c5e0fd726954b87111f7506a61c502280d982" +dependencies = [ + "libc", + "windows-sys 0.59.0", +] + +[[package]] +name = "pango" +version = "0.18.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ca27ec1eb0457ab26f3036ea52229edbdb74dee1edd29063f5b9b010e7ebee4" +dependencies = [ + "gio", + "glib", + "libc", + "once_cell", + "pango-sys", +] + +[[package]] +name = "pango-sys" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "436737e391a843e5933d6d9aa102cb126d501e815b83601365a948a518555dc5" +dependencies = [ + "glib-sys", + "gobject-sys", + "libc", + "system-deps", +] + +[[package]] +name = "parking_lot" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1bf18183cf54e8d6059647fc3063646a1801cf30896933ec2311622cc4b9a27" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-targets 0.52.6", +] + +[[package]] +name = "pathdiff" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d61c5ce1153ab5b689d0c074c4e7fc613e942dfb7dd9eea5ab202d2ad91fe361" + +[[package]] +name = "percent-encoding" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" + +[[package]] +name = "phf" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3dfb61232e34fcb633f43d12c58f83c1df82962dcdfa565a4e866ffc17dafe12" +dependencies = [ + "phf_macros 0.8.0", + "phf_shared 0.8.0", + "proc-macro-hack", +] + +[[package]] +name = "phf" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fabbf1ead8a5bcbc20f5f8b939ee3f5b0f6f281b6ad3468b84656b658b455259" +dependencies = [ + "phf_shared 0.10.0", +] + +[[package]] +name = "phf" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ade2d8b8f33c7333b51bcf0428d37e217e9f32192ae4772156f65063b8ce03dc" +dependencies = [ + "phf_macros 0.11.2", + "phf_shared 0.11.2", +] + +[[package]] +name = "phf_codegen" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cbffee61585b0411840d3ece935cce9cb6321f01c45477d30066498cd5e1a815" +dependencies = [ + "phf_generator 0.8.0", + "phf_shared 0.8.0", +] + +[[package]] +name = "phf_codegen" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fb1c3a8bc4dd4e5cfce29b44ffc14bedd2ee294559a294e2a4d4c9e9a6a13cd" +dependencies = [ + "phf_generator 0.10.0", + "phf_shared 0.10.0", +] + +[[package]] +name = "phf_generator" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17367f0cc86f2d25802b2c26ee58a7b23faeccf78a396094c13dced0d0182526" +dependencies = [ + "phf_shared 0.8.0", + "rand 0.7.3", +] + +[[package]] +name = "phf_generator" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d5285893bb5eb82e6aaf5d59ee909a06a16737a8970984dd7746ba9283498d6" +dependencies = [ + "phf_shared 0.10.0", + "rand 0.8.5", +] + +[[package]] +name = "phf_generator" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48e4cc64c2ad9ebe670cb8fd69dd50ae301650392e81c05f9bfcb2d5bdbc24b0" +dependencies = [ + "phf_shared 0.11.2", + "rand 0.8.5", +] + +[[package]] +name = "phf_macros" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f6fde18ff429ffc8fe78e2bf7f8b7a5a5a6e2a8b58bc5a9ac69198bbda9189c" +dependencies = [ + "phf_generator 0.8.0", + "phf_shared 0.8.0", + "proc-macro-hack", + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "phf_macros" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3444646e286606587e49f3bcf1679b8cef1dc2c5ecc29ddacaffc305180d464b" +dependencies = [ + "phf_generator 0.11.2", + "phf_shared 0.11.2", + "proc-macro2", + "quote", + "syn 2.0.87", +] + +[[package]] +name = "phf_shared" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c00cf8b9eafe68dde5e9eaa2cef8ee84a9336a47d566ec55ca16589633b65af7" +dependencies = [ + "siphasher", +] + +[[package]] +name = "phf_shared" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6796ad771acdc0123d2a88dc428b5e38ef24456743ddb1744ed628f9815c096" +dependencies = [ + "siphasher", +] + +[[package]] +name = "phf_shared" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90fcb95eef784c2ac79119d1dd819e162b5da872ce6f3c3abe1e8ca1c082f72b" +dependencies = [ + "siphasher", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "915a1e146535de9163f3987b8944ed8cf49a18bb0056bcebcdcece385cece4ff" + +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + +[[package]] +name = "pkg-config" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "953ec861398dccce10c670dfeaf3ec4911ca479e9c02154b3a215178c5f566f2" + +[[package]] +name = "plist" +version = "1.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42cf17e9a1800f5f396bc67d193dc9411b59012a5876445ef450d449881e1016" +dependencies = [ + "base64 0.22.1", + "indexmap 2.6.0", + "quick-xml", + "serde", + "time", +] + +[[package]] +name = "png" +version = "0.17.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52f9d46a34a05a6a57566bc2bfae066ef07585a6e3fa30fbbdff5936380623f0" +dependencies = [ + "bitflags 1.3.2", + "crc32fast", + "fdeflate", + "flate2", + "miniz_oxide", +] + +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + +[[package]] +name = "ppv-lite86" +version = "0.2.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77957b295656769bb8ad2b6a6b09d897d94f05c41b069aede1fcdaa675eaea04" +dependencies = [ + "zerocopy", +] + +[[package]] +name = "precomputed-hash" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c" + +[[package]] +name = "proc-macro-crate" +version = "1.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f4c021e1093a56626774e81216a4ce732a735e5bad4868a03f3ed65ca0c3919" +dependencies = [ + "once_cell", + "toml_edit 0.19.15", +] + +[[package]] +name = "proc-macro-crate" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b00f26d3400549137f92511a46ac1cd8ce37cb5598a96d382381458b992a5d24" +dependencies = [ + "toml_datetime", + "toml_edit 0.20.2", +] + +[[package]] +name = "proc-macro-error" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" +dependencies = [ + "proc-macro-error-attr", + "proc-macro2", + "quote", + "syn 1.0.109", + "version_check", +] + +[[package]] +name = "proc-macro-error-attr" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" +dependencies = [ + "proc-macro2", + "quote", + "version_check", +] + +[[package]] +name = "proc-macro-hack" +version = "0.5.20+deprecated" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc375e1527247fe1a97d8b7156678dfe7c1af2fc075c9a4db3690ecd2a148068" + +[[package]] +name = "proc-macro2" +version = "1.0.89" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f139b0662de085916d1fb67d2b4169d1addddda1919e696f3252b740b629986e" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quick-xml" +version = "0.32.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d3a6e5838b60e0e8fa7a43f22ade549a37d61f8bdbe636d0d7816191de969c2" +dependencies = [ + "memchr", +] + +[[package]] +name = "quote" +version = "1.0.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5b9d34b8991d19d98081b46eacdd8eb58c6f2b201139f7c5f643cc155a633af" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "rand" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a6b1679d49b24bbfe0c803429aa1874472f50d9b363131f0e89fc356b544d03" +dependencies = [ + "getrandom 0.1.16", + "libc", + "rand_chacha 0.2.2", + "rand_core 0.5.1", + "rand_hc", + "rand_pcg", +] + +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "libc", + "rand_chacha 0.3.1", + "rand_core 0.6.4", +] + +[[package]] +name = "rand_chacha" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4c8ed856279c9737206bf725bf36935d8666ead7aa69b52be55af369d193402" +dependencies = [ + "ppv-lite86", + "rand_core 0.5.1", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core 0.6.4", +] + +[[package]] +name = "rand_core" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90bde5296fc891b0cef12a6d03ddccc162ce7b2aff54160af9338f8d40df6d19" +dependencies = [ + "getrandom 0.1.16", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom 0.2.15", +] + +[[package]] +name = "rand_hc" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca3129af7b92a17112d59ad498c6f81eaf463253766b90396d39ea7a39d6613c" +dependencies = [ + "rand_core 0.5.1", +] + +[[package]] +name = "rand_pcg" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16abd0c1b639e9eb4d7c50c0b8100b0d0f849be2349829c740fe8e6eb4816429" +dependencies = [ + "rand_core 0.5.1", +] + +[[package]] +name = "raw-window-handle" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20675572f6f24e9e76ef639bc5552774ed45f1c30e2951e1e99c59888861c539" + +[[package]] +name = "redox_syscall" +version = "0.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b6dfecf2c74bce2466cabf93f6664d6998a69eb21e39f4207930065b27b771f" +dependencies = [ + "bitflags 2.6.0", +] + +[[package]] +name = "redox_users" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba009ff324d1fc1b900bd1fdb31564febe58a8ccc8a6fdbb93b543d33b13ca43" +dependencies = [ + "getrandom 0.2.15", + "libredox", + "thiserror", +] + +[[package]] +name = "regex" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "368758f23274712b504848e9d5a6f010445cc8b87a7cdb4d7cbee666c1288da3" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" + +[[package]] +name = "reqwest" +version = "0.12.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a77c62af46e79de0a562e1a9849205ffcb7fc1238876e9bd743357570e04046f" +dependencies = [ + "base64 0.22.1", + "bytes", + "futures-core", + "futures-util", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-util", + "ipnet", + "js-sys", + "log", + "mime", + "once_cell", + "percent-encoding", + "pin-project-lite", + "serde", + "serde_json", + "serde_urlencoded", + "sync_wrapper", + "tokio", + "tokio-util", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "wasm-streams", + "web-sys", + "windows-registry", +] + +[[package]] +name = "rustc-demangle" +version = "0.1.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" + +[[package]] +name = "rustc_version" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" +dependencies = [ + "semver", +] + +[[package]] +name = "ryu" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f" + +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "schemars" +version = "0.8.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09c024468a378b7e36765cd36702b7a90cc3cba11654f6685c8f233408e89e92" +dependencies = [ + "dyn-clone", + "indexmap 1.9.3", + "schemars_derive", + "serde", + "serde_json", + "url", + "uuid", +] + +[[package]] +name = "schemars_derive" +version = "0.8.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1eee588578aff73f856ab961cd2f79e36bc45d7ded33a7562adba4667aecc0e" +dependencies = [ + "proc-macro2", + "quote", + "serde_derive_internals", + "syn 2.0.87", +] + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "selectors" +version = "0.22.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df320f1889ac4ba6bc0cdc9c9af7af4bd64bb927bccdf32d81140dc1f9be12fe" +dependencies = [ + "bitflags 1.3.2", + "cssparser", + "derive_more", + "fxhash", + "log", + "matches", + "phf 0.8.0", + "phf_codegen 0.8.0", + "precomputed-hash", + "servo_arc", + "smallvec", + "thin-slice", +] + +[[package]] +name = "semver" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61697e0a1c7e512e84a621326239844a24d8207b4669b41bc18b32ea5cbf988b" +dependencies = [ + "serde", +] + +[[package]] +name = "serde" +version = "1.0.214" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f55c3193aca71c12ad7890f1785d2b73e1b9f63a0bbc353c08ef26fe03fc56b5" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde-untagged" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2676ba99bd82f75cae5cbd2c8eda6fa0b8760f18978ea840e980dd5567b5c5b6" +dependencies = [ + "erased-serde", + "serde", + "typeid", +] + +[[package]] +name = "serde_derive" +version = "1.0.214" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de523f781f095e28fa605cdce0f8307e451cc0fd14e2eb4cd2e98a355b147766" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.87", +] + +[[package]] +name = "serde_derive_internals" +version = "0.29.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "18d26a20a969b9e3fdf2fc2d9f21eda6c40e2de84c9408bb5d3b05d499aae711" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.87", +] + +[[package]] +name = "serde_json" +version = "1.0.132" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d726bfaff4b320266d395898905d0eba0345aae23b54aee3a737e260fd46db03" +dependencies = [ + "itoa 1.0.11", + "memchr", + "ryu", + "serde", +] + +[[package]] +name = "serde_repr" +version = "0.1.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c64451ba24fc7a6a2d60fc75dd9c83c90903b19028d4eff35e88fc1e86564e9" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.87", +] + +[[package]] +name = "serde_spanned" +version = "0.6.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87607cb1398ed59d48732e575a4c28a7a8ebf2454b964fe3f224f2afc07909e1" +dependencies = [ + "serde", +] + +[[package]] +name = "serde_urlencoded" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" +dependencies = [ + "form_urlencoded", + "itoa 1.0.11", + "ryu", + "serde", +] + +[[package]] +name = "serde_with" +version = "3.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e28bdad6db2b8340e449f7108f020b3b092e8583a9e3fb82713e1d4e71fe817" +dependencies = [ + "base64 0.22.1", + "chrono", + "hex", + "indexmap 1.9.3", + "indexmap 2.6.0", + "serde", + "serde_derive", + "serde_json", + "serde_with_macros", + "time", +] + +[[package]] +name = "serde_with_macros" +version = "3.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d846214a9854ef724f3da161b426242d8de7c1fc7de2f89bb1efcb154dca79d" +dependencies = [ + "darling", + "proc-macro2", + "quote", + "syn 2.0.87", +] + +[[package]] +name = "serialize-to-javascript" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c9823f2d3b6a81d98228151fdeaf848206a7855a7a042bbf9bf870449a66cafb" +dependencies = [ + "serde", + "serde_json", + "serialize-to-javascript-impl", +] + +[[package]] +name = "serialize-to-javascript-impl" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74064874e9f6a15f04c1f3cb627902d0e6b410abbf36668afa873c61889f1763" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "servo_arc" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d98238b800e0d1576d8b6e3de32827c2d74bee68bb97748dcf5071fb53965432" +dependencies = [ + "nodrop", + "stable_deref_trait", +] + +[[package]] +name = "sha2" +version = "0.10.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "793db75ad2bcafc3ffa7c68b215fee268f537982cd901d132f89c6343f3a3dc8" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "shared_child" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09fa9338aed9a1df411814a5b2252f7cd206c55ae9bf2fa763f8de84603aa60c" +dependencies = [ + "libc", + "windows-sys 0.59.0", +] + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "simd-adler32" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe" + +[[package]] +name = "siphasher" +version = "0.3.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38b58827f4464d87d377d175e90bf58eb00fd8716ff0a62f80356b5e61555d0d" + +[[package]] +name = "slab" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" +dependencies = [ + "autocfg", +] + +[[package]] +name = "smallvec" +version = "1.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" + +[[package]] +name = "socket2" +version = "0.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce305eb0b4296696835b71df73eb912e0f1ffd2556a501fcede6e0c50349191c" +dependencies = [ + "libc", + "windows-sys 0.52.0", +] + +[[package]] +name = "softbuffer" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "18051cdd562e792cad055119e0cdb2cfc137e44e3987532e0f9659a77931bb08" +dependencies = [ + "bytemuck", + "cfg_aliases", + "core-graphics", + "foreign-types", + "js-sys", + "log", + "objc2", + "objc2-foundation", + "objc2-quartz-core", + "raw-window-handle", + "redox_syscall", + "wasm-bindgen", + "web-sys", + "windows-sys 0.59.0", +] + +[[package]] +name = "soup3" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "471f924a40f31251afc77450e781cb26d55c0b650842efafc9c6cbd2f7cc4f9f" +dependencies = [ + "futures-channel", + "gio", + "glib", + "libc", + "soup3-sys", +] + +[[package]] +name = "soup3-sys" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ebe8950a680a12f24f15ebe1bf70db7af98ad242d9db43596ad3108aab86c27" +dependencies = [ + "gio-sys", + "glib-sys", + "gobject-sys", + "libc", + "system-deps", +] + +[[package]] +name = "stable_deref_trait" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" + +[[package]] +name = "string_cache" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f91138e76242f575eb1d3b38b4f1362f10d3a43f47d182a5b359af488a02293b" +dependencies = [ + "new_debug_unreachable", + "once_cell", + "parking_lot", + "phf_shared 0.10.0", + "precomputed-hash", + "serde", +] + +[[package]] +name = "string_cache_codegen" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6bb30289b722be4ff74a408c3cc27edeaad656e06cb1fe8fa9231fa59c728988" +dependencies = [ + "phf_generator 0.10.0", + "phf_shared 0.10.0", + "proc-macro2", + "quote", +] + +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[package]] +name = "swift-rs" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4057c98e2e852d51fdcfca832aac7b571f6b351ad159f9eda5db1655f8d0c4d7" +dependencies = [ + "base64 0.21.7", + "serde", + "serde_json", +] + +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "2.0.87" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25aa4ce346d03a6dcd68dd8b4010bcb74e54e62c90c573f394c46eae99aba32d" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "sync_wrapper" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7065abeca94b6a8a577f9bd45aa0867a2238b74e8eb67cf10d492bc39351394" +dependencies = [ + "futures-core", +] + +[[package]] +name = "synstructure" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8af7666ab7b6390ab78131fb5b0fce11d6b7a6951602017c35fa82800708971" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.87", +] + +[[package]] +name = "system-deps" +version = "6.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3e535eb8dded36d55ec13eddacd30dec501792ff23a0b1682c38601b8cf2349" +dependencies = [ + "cfg-expr", + "heck 0.5.0", + "pkg-config", + "toml 0.8.2", + "version-compare", +] + +[[package]] +name = "tao" +version = "0.30.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63f1f6b2017cc33d7f6fc9c6186a2c0f5dfc985899a7b4fe9e64985c17533db3" +dependencies = [ + "bitflags 2.6.0", + "cocoa", + "core-foundation", + "core-graphics", + "crossbeam-channel", + "dispatch", + "dlopen2", + "dpi", + "gdkwayland-sys", + "gdkx11-sys", + "gtk", + "instant", + "jni", + "lazy_static", + "libc", + "log", + "ndk", + "ndk-context", + "ndk-sys", + "objc", + "once_cell", + "parking_lot", + "raw-window-handle", + "scopeguard", + "tao-macros", + "unicode-segmentation", + "url", + "windows", + "windows-core 0.58.0", + "windows-version", + "x11-dl", +] + +[[package]] +name = "tao-macros" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4e16beb8b2ac17db28eab8bca40e62dbfbb34c0fcdc6d9826b11b7b5d047dfd" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.87", +] + +[[package]] +name = "target-lexicon" +version = "0.12.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1" + +[[package]] +name = "tauri" +version = "2.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3889b392db6d32a105d3757230ea0220090b8f94c90d3e60b6c5eb91178ab1b" +dependencies = [ + "anyhow", + "bytes", + "dirs", + "dunce", + "embed_plist", + "futures-util", + "getrandom 0.2.15", + "glob", + "gtk", + "heck 0.5.0", + "http", + "jni", + "libc", + "log", + "mime", + "muda", + "objc2", + "objc2-app-kit", + "objc2-foundation", + "percent-encoding", + "plist", + "raw-window-handle", + "reqwest", + "serde", + "serde_json", + "serde_repr", + "serialize-to-javascript", + "swift-rs", + "tauri-build", + "tauri-macros", + "tauri-runtime", + "tauri-runtime-wry", + "tauri-utils", + "thiserror", + "tokio", + "tray-icon", + "url", + "urlpattern", + "webkit2gtk", + "webview2-com", + "window-vibrancy", + "windows", +] + +[[package]] +name = "tauri-build" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f96827ccfb1aa40d55d0ded79562d18ba18566657a553f992a982d755148376" +dependencies = [ + "anyhow", + "cargo_toml", + "dirs", + "glob", + "heck 0.5.0", + "json-patch 3.0.1", + "schemars", + "semver", + "serde", + "serde_json", + "tauri-utils", + "tauri-winres", + "toml 0.8.2", + "walkdir", +] + +[[package]] +name = "tauri-codegen" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8947f16f47becd9e9cd39b74ee337fd1981574d78819be18e4384d85e5a0b82f" +dependencies = [ + "base64 0.22.1", + "brotli", + "ico", + "json-patch 2.0.0", + "plist", + "png", + "proc-macro2", + "quote", + "semver", + "serde", + "serde_json", + "sha2", + "syn 2.0.87", + "tauri-utils", + "thiserror", + "time", + "url", + "uuid", + "walkdir", +] + +[[package]] +name = "tauri-macros" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8bd1c8d4a66799d3438747c3a79705cd665a95d6f24cb5f315413ff7a981fe2a" +dependencies = [ + "heck 0.5.0", + "proc-macro2", + "quote", + "syn 2.0.87", + "tauri-codegen", + "tauri-utils", +] + +[[package]] +name = "tauri-plugin" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6fa4e6c94cb1d635f65a770c69e23de1bc054b0e4c554fa037a7cc7676333d39" +dependencies = [ + "anyhow", + "glob", + "plist", + "schemars", + "serde", + "serde_json", + "tauri-utils", + "toml 0.8.2", + "walkdir", +] + +[[package]] +name = "tauri-plugin-shell" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ad7880c5586b6b2104be451e3d7fc0f3800c84bda69e9ba81c828f87cb34267" +dependencies = [ + "encoding_rs", + "log", + "open", + "os_pipe", + "regex", + "schemars", + "serde", + "serde_json", + "shared_child", + "tauri", + "tauri-plugin", + "thiserror", + "tokio", +] + +[[package]] +name = "tauri-runtime" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1ef7363e7229ac8d04e8a5d405670dbd43dde8fc4bc3bc56105c35452d03784" +dependencies = [ + "dpi", + "gtk", + "http", + "jni", + "raw-window-handle", + "serde", + "serde_json", + "tauri-utils", + "thiserror", + "url", + "windows", +] + +[[package]] +name = "tauri-runtime-wry" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62fa2068e8498ad007b54d5773d03d57c3ff6dd96f8c8ce58beff44d0d5e0d30" +dependencies = [ + "gtk", + "http", + "jni", + "log", + "objc2", + "objc2-app-kit", + "objc2-foundation", + "percent-encoding", + "raw-window-handle", + "softbuffer", + "tao", + "tauri-runtime", + "tauri-utils", + "url", + "webkit2gtk", + "webview2-com", + "windows", + "wry", +] + +[[package]] +name = "tauri-utils" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fc65d6f5c54e56b66258948a6d9e47a82ea41f4b5a7612bfbdd1634c2913ed0" +dependencies = [ + "brotli", + "cargo_metadata", + "ctor", + "dunce", + "glob", + "html5ever", + "infer", + "json-patch 2.0.0", + "kuchikiki", + "log", + "memchr", + "phf 0.11.2", + "proc-macro2", + "quote", + "regex", + "schemars", + "semver", + "serde", + "serde-untagged", + "serde_json", + "serde_with", + "swift-rs", + "thiserror", + "toml 0.8.2", + "url", + "urlpattern", + "uuid", + "walkdir", +] + +[[package]] +name = "tauri-winres" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5993dc129e544393574288923d1ec447c857f3f644187f4fbf7d9a875fbfc4fb" +dependencies = [ + "embed-resource", + "toml 0.7.8", +] + +[[package]] +name = "tendril" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d24a120c5fc464a3458240ee02c299ebcb9d67b5249c8848b09d639dca8d7bb0" +dependencies = [ + "futf", + "mac", + "utf-8", +] + +[[package]] +name = "thin-slice" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8eaa81235c7058867fa8c0e7314f33dcce9c215f535d1913822a2b3f5e289f3c" + +[[package]] +name = "thiserror" +version = "1.0.68" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02dd99dc800bbb97186339685293e1cc5d9df1f8fae2d0aecd9ff1c77efea892" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.68" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7c61ec9a6f64d2793d8a45faba21efbe3ced62a886d44c36a009b2b519b4c7e" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.87", +] + +[[package]] +name = "time" +version = "0.3.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5dfd88e563464686c916c7e46e623e520ddc6d79fa6641390f2e3fa86e83e885" +dependencies = [ + "deranged", + "itoa 1.0.11", + "num-conv", + "powerfmt", + "serde", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3" + +[[package]] +name = "time-macros" +version = "0.2.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f252a68540fde3a3877aeea552b832b40ab9a69e318efd078774a01ddee1ccf" +dependencies = [ + "num-conv", + "time-core", +] + +[[package]] +name = "tinystr" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9117f5d4db391c1cf6927e7bea3db74b9a1c1add8f7eda9ffd5364f40f57b82f" +dependencies = [ + "displaydoc", + "zerovec", +] + +[[package]] +name = "tokio" +version = "1.41.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22cfb5bee7a6a52939ca9224d6ac897bb669134078daa8735560897f69de4d33" +dependencies = [ + "backtrace", + "bytes", + "libc", + "mio", + "pin-project-lite", + "socket2", + "windows-sys 0.52.0", +] + +[[package]] +name = "tokio-util" +version = "0.7.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61e7c3654c13bcd040d4a03abee2c75b1d14a37b423cf5a813ceae1cc903ec6a" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "toml" +version = "0.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd79e69d3b627db300ff956027cc6c3798cef26d22526befdfcd12feeb6d2257" +dependencies = [ + "serde", + "serde_spanned", + "toml_datetime", + "toml_edit 0.19.15", +] + +[[package]] +name = "toml" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "185d8ab0dfbb35cf1399a6344d8484209c088f75f8f68230da55d48d95d43e3d" +dependencies = [ + "serde", + "serde_spanned", + "toml_datetime", + "toml_edit 0.20.2", +] + +[[package]] +name = "toml_datetime" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cda73e2f1397b1262d6dfdcef8aafae14d1de7748d66822d3bfeeb6d03e5e4b" +dependencies = [ + "serde", +] + +[[package]] +name = "toml_edit" +version = "0.19.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b5bb770da30e5cbfde35a2d7b9b8a2c4b8ef89548a7a6aeab5c9a576e3e7421" +dependencies = [ + "indexmap 2.6.0", + "serde", + "serde_spanned", + "toml_datetime", + "winnow", +] + +[[package]] +name = "toml_edit" +version = "0.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "396e4d48bbb2b7554c944bde63101b5ae446cff6ec4a24227428f15eb72ef338" +dependencies = [ + "indexmap 2.6.0", + "serde", + "serde_spanned", + "toml_datetime", + "winnow", +] + +[[package]] +name = "tower-service" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" + +[[package]] +name = "tracing" +version = "0.1.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3523ab5a71916ccf420eebdf5521fcef02141234bbc0b8a49f2fdc4544364ef" +dependencies = [ + "pin-project-lite", + "tracing-core", +] + +[[package]] +name = "tracing-core" +version = "0.1.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c06d3da6113f116aaee68e4d601191614c9053067f9ab7f6edbcb161237daa54" +dependencies = [ + "once_cell", +] + +[[package]] +name = "tray-icon" +version = "0.19.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c92af36a182b46206723bdf8a7942e20838cde1cf062e5b97854d57eb01763b" +dependencies = [ + "core-graphics", + "crossbeam-channel", + "dirs", + "libappindicator", + "muda", + "objc2", + "objc2-app-kit", + "objc2-foundation", + "once_cell", + "png", + "serde", + "thiserror", + "windows-sys 0.59.0", +] + +[[package]] +name = "try-lock" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" + +[[package]] +name = "typeid" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e13db2e0ccd5e14a544e8a246ba2312cd25223f616442d7f2cb0e3db614236e" + +[[package]] +name = "typenum" +version = "1.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" + +[[package]] +name = "unic-char-property" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8c57a407d9b6fa02b4795eb81c5b6652060a15a7903ea981f3d723e6c0be221" +dependencies = [ + "unic-char-range", +] + +[[package]] +name = "unic-char-range" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0398022d5f700414f6b899e10b8348231abf9173fa93144cbc1a43b9793c1fbc" + +[[package]] +name = "unic-common" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "80d7ff825a6a654ee85a63e80f92f054f904f21e7d12da4e22f9834a4aaa35bc" + +[[package]] +name = "unic-ucd-ident" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e230a37c0381caa9219d67cf063aa3a375ffed5bf541a452db16e744bdab6987" +dependencies = [ + "unic-char-property", + "unic-char-range", + "unic-ucd-version", +] + +[[package]] +name = "unic-ucd-version" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96bd2f2237fe450fcd0a1d2f5f4e91711124f7857ba2e964247776ebeeb7b0c4" +dependencies = [ + "unic-common", +] + +[[package]] +name = "unicode-ident" +version = "1.0.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e91b56cd4cadaeb79bbf1a5645f6b4f8dc5bde8834ad5894a8db35fda9efa1fe" + +[[package]] +name = "unicode-segmentation" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" + +[[package]] +name = "url" +version = "2.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d157f1b96d14500ffdc1f10ba712e780825526c03d9a49b4d0324b0d9113ada" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", + "serde", +] + +[[package]] +name = "urlpattern" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70acd30e3aa1450bc2eece896ce2ad0d178e9c079493819301573dae3c37ba6d" +dependencies = [ + "regex", + "serde", + "unic-ucd-ident", + "url", +] + +[[package]] +name = "utf-8" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" + +[[package]] +name = "utf16_iter" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8232dd3cdaed5356e0f716d285e4b40b932ac434100fe9b7e0e8e935b9e6246" + +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + +[[package]] +name = "uuid" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8c5f0a0af699448548ad1a2fbf920fb4bee257eae39953ba95cb84891a0446a" +dependencies = [ + "getrandom 0.2.15", + "serde", +] + +[[package]] +name = "version-compare" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "852e951cb7832cb45cb1169900d19760cfa39b82bc0ea9c0e5a14ae88411c98b" + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "vswhom" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be979b7f07507105799e854203b470ff7c78a1639e330a58f183b5fea574608b" +dependencies = [ + "libc", + "vswhom-sys", +] + +[[package]] +name = "vswhom-sys" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3b17ae1f6c8a2b28506cd96d412eebf83b4a0ff2cbefeeb952f2f9dfa44ba18" +dependencies = [ + "cc", + "libc", +] + +[[package]] +name = "walkdir" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" +dependencies = [ + "same-file", + "winapi-util", +] + +[[package]] +name = "want" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" +dependencies = [ + "try-lock", +] + +[[package]] +name = "wasi" +version = "0.9.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cccddf32554fecc6acb585f82a32a72e28b48f8c4c1883ddfeeeaa96f7d8e519" + +[[package]] +name = "wasi" +version = "0.11.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" + +[[package]] +name = "wasm-bindgen" +version = "0.2.95" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "128d1e363af62632b8eb57219c8fd7877144af57558fb2ef0368d0087bddeb2e" +dependencies = [ + "cfg-if", + "once_cell", + "wasm-bindgen-macro", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.95" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb6dd4d3ca0ddffd1dd1c9c04f94b868c37ff5fac97c30b97cff2d74fce3a358" +dependencies = [ + "bumpalo", + "log", + "once_cell", + "proc-macro2", + "quote", + "syn 2.0.87", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc7ec4f8827a71586374db3e87abdb5a2bb3a15afed140221307c3ec06b1f63b" +dependencies = [ + "cfg-if", + "js-sys", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.95" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e79384be7f8f5a9dd5d7167216f022090cf1f9ec128e6e6a482a2cb5c5422c56" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.95" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26c6ab57572f7a24a4985830b120de1594465e5d500f24afe89e16b4e833ef68" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.87", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.95" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65fc09f10666a9f147042251e0dda9c18f166ff7de300607007e96bdebc1068d" + +[[package]] +name = "wasm-streams" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15053d8d85c7eccdbefef60f06769760a563c7f0a9d6902a13d35c7800b0ad65" +dependencies = [ + "futures-util", + "js-sys", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + +[[package]] +name = "web-sys" +version = "0.3.72" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6488b90108c040df0fe62fa815cbdee25124641df01814dd7282749234c6112" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "webkit2gtk" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76b1bc1e54c581da1e9f179d0b38512ba358fb1af2d634a1affe42e37172361a" +dependencies = [ + "bitflags 1.3.2", + "cairo-rs", + "gdk", + "gdk-sys", + "gio", + "gio-sys", + "glib", + "glib-sys", + "gobject-sys", + "gtk", + "gtk-sys", + "javascriptcore-rs", + "libc", + "once_cell", + "soup3", + "webkit2gtk-sys", +] + +[[package]] +name = "webkit2gtk-sys" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62daa38afc514d1f8f12b8693d30d5993ff77ced33ce30cd04deebc267a6d57c" +dependencies = [ + "bitflags 1.3.2", + "cairo-sys-rs", + "gdk-sys", + "gio-sys", + "glib-sys", + "gobject-sys", + "gtk-sys", + "javascriptcore-rs-sys", + "libc", + "pkg-config", + "soup3-sys", + "system-deps", +] + +[[package]] +name = "webview2-com" +version = "0.33.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f61ff3d9d0ee4efcb461b14eb3acfda2702d10dc329f339303fc3e57215ae2c" +dependencies = [ + "webview2-com-macros", + "webview2-com-sys", + "windows", + "windows-core 0.58.0", + "windows-implement", + "windows-interface", +] + +[[package]] +name = "webview2-com-macros" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d228f15bba3b9d56dde8bddbee66fa24545bd17b48d5128ccf4a8742b18e431" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.87", +] + +[[package]] +name = "webview2-com-sys" +version = "0.33.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3a3e2eeb58f82361c93f9777014668eb3d07e7d174ee4c819575a9208011886" +dependencies = [ + "thiserror", + "windows", + "windows-core 0.58.0", +] + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-util" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb" +dependencies = [ + "windows-sys 0.59.0", +] + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "window-vibrancy" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ea403deff7b51fff19e261330f71608ff2cdef5721d72b64180bb95be7c4150" +dependencies = [ + "objc2", + "objc2-app-kit", + "objc2-foundation", + "raw-window-handle", + "windows-sys 0.59.0", + "windows-version", +] + +[[package]] +name = "windows" +version = "0.58.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd04d41d93c4992d421894c18c8b43496aa748dd4c081bac0dc93eb0489272b6" +dependencies = [ + "windows-core 0.58.0", + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-core" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-core" +version = "0.58.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ba6d44ec8c2591c134257ce647b7ea6b20335bf6379a27dac5f1641fcf59f99" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-result", + "windows-strings", + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-implement" +version = "0.58.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2bbd5b46c938e506ecbce286b6628a02171d56153ba733b6c741fc627ec9579b" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.87", +] + +[[package]] +name = "windows-interface" +version = "0.58.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "053c4c462dc91d3b1504c6fe5a726dd15e216ba718e84a0e46a88fbe5ded3515" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.87", +] + +[[package]] +name = "windows-registry" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e400001bb720a623c1c69032f8e3e4cf09984deec740f007dd2b03ec864804b0" +dependencies = [ + "windows-result", + "windows-strings", + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-result" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d1043d8214f791817bab27572aaa8af63732e11bf84aa21a45a78d6c317ae0e" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-strings" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cd9b125c486025df0eabcb585e62173c6c9eddcec5d117d3b6e8c30e2ee4d10" +dependencies = [ + "windows-result", + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.45.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" +dependencies = [ + "windows-targets 0.42.2", +] + +[[package]] +name = "windows-sys" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" +dependencies = [ + "windows-targets 0.48.5", +] + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-targets" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071" +dependencies = [ + "windows_aarch64_gnullvm 0.42.2", + "windows_aarch64_msvc 0.42.2", + "windows_i686_gnu 0.42.2", + "windows_i686_msvc 0.42.2", + "windows_x86_64_gnu 0.42.2", + "windows_x86_64_gnullvm 0.42.2", + "windows_x86_64_msvc 0.42.2", +] + +[[package]] +name = "windows-targets" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" +dependencies = [ + "windows_aarch64_gnullvm 0.48.5", + "windows_aarch64_msvc 0.48.5", + "windows_i686_gnu 0.48.5", + "windows_i686_msvc 0.48.5", + "windows_x86_64_gnu 0.48.5", + "windows_x86_64_gnullvm 0.48.5", + "windows_x86_64_msvc 0.48.5", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", + "windows_i686_gnullvm", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", +] + +[[package]] +name = "windows-version" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6998aa457c9ba8ff2fb9f13e9d2a930dabcea28f1d0ab94d687d8b3654844515" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_i686_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" + +[[package]] +name = "windows_i686_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" + +[[package]] +name = "windows_i686_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "winnow" +version = "0.5.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f593a95398737aeed53e489c785df13f3618e41dbcd6718c6addbf1395aa6876" +dependencies = [ + "memchr", +] + +[[package]] +name = "winreg" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a277a57398d4bfa075df44f501a17cfdf8542d224f0d36095a2adc7aee4ef0a5" +dependencies = [ + "cfg-if", + "windows-sys 0.48.0", +] + +[[package]] +name = "write16" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1890f4022759daae28ed4fe62859b1236caebfc61ede2f63ed4e695f3f6d936" + +[[package]] +name = "writeable" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e9df38ee2d2c3c5948ea468a8406ff0db0b29ae1ffde1bcf20ef305bcc95c51" + +[[package]] +name = "wry" +version = "0.46.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd5cdf57c66813d97601181349c63b96994b3074fc3d7a31a8cce96e968e3bbd" +dependencies = [ + "base64 0.22.1", + "block2", + "crossbeam-channel", + "dpi", + "dunce", + "gdkx11", + "gtk", + "html5ever", + "http", + "javascriptcore-rs", + "jni", + "kuchikiki", + "libc", + "ndk", + "objc2", + "objc2-app-kit", + "objc2-foundation", + "objc2-ui-kit", + "objc2-web-kit", + "once_cell", + "percent-encoding", + "raw-window-handle", + "sha2", + "soup3", + "tao-macros", + "thiserror", + "webkit2gtk", + "webkit2gtk-sys", + "webview2-com", + "windows", + "windows-core 0.58.0", + "windows-version", + "x11-dl", +] + +[[package]] +name = "x11" +version = "2.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "502da5464ccd04011667b11c435cb992822c2c0dbde1770c988480d312a0db2e" +dependencies = [ + "libc", + "pkg-config", +] + +[[package]] +name = "x11-dl" +version = "2.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38735924fedd5314a6e548792904ed8c6de6636285cb9fec04d5b1db85c1516f" +dependencies = [ + "libc", + "once_cell", + "pkg-config", +] + +[[package]] +name = "yoke" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c5b1314b079b0930c31e3af543d8ee1757b1951ae1e1565ec704403a7240ca5" +dependencies = [ + "serde", + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28cc31741b18cb6f1d5ff12f5b7523e3d6eb0852bbbad19d73905511d9849b95" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.87", + "synstructure", +] + +[[package]] +name = "zerocopy" +version = "0.7.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0" +dependencies = [ + "byteorder", + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.7.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.87", +] + +[[package]] +name = "zerofrom" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91ec111ce797d0e0784a1116d0ddcdbea84322cd79e5d5ad173daeba4f93ab55" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ea7b4a3637ea8669cedf0f1fd5c286a17f3de97b8dd5a70a6c167a1730e63a5" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.87", + "synstructure", +] + +[[package]] +name = "zerovec" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa2b893d79df23bfb12d5461018d408ea19dfafe76c2c7ef6d4eba614f8ff079" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6eafa6dfb17584ea3e2bd6e76e0cc15ad7af12b09abdd1ca55961bed9b1063c6" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.87", +] diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml new file mode 100644 index 0000000..2cbf491 --- /dev/null +++ b/src-tauri/Cargo.toml @@ -0,0 +1,25 @@ +[package] +name = "lightningbeam" +version = "0.1.0" +description = "A Tauri App" +authors = ["you"] +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[lib] +# The `_lib` suffix may seem redundant but it is necessary +# to make the lib name unique and wouldn't conflict with the bin name. +# This seems to be only an issue on Windows, see https://github.com/rust-lang/cargo/issues/8519 +name = "lightningbeam_lib" +crate-type = ["staticlib", "cdylib", "rlib"] + +[build-dependencies] +tauri-build = { version = "2", features = [] } + +[dependencies] +tauri = { version = "2", features = [] } +tauri-plugin-shell = "2" +serde = { version = "1", features = ["derive"] } +serde_json = "1" + diff --git a/src-tauri/build.rs b/src-tauri/build.rs new file mode 100644 index 0000000..d860e1e --- /dev/null +++ b/src-tauri/build.rs @@ -0,0 +1,3 @@ +fn main() { + tauri_build::build() +} diff --git a/src-tauri/capabilities/default.json b/src-tauri/capabilities/default.json new file mode 100644 index 0000000..3bb4cc4 --- /dev/null +++ b/src-tauri/capabilities/default.json @@ -0,0 +1,10 @@ +{ + "$schema": "../gen/schemas/desktop-schema.json", + "identifier": "default", + "description": "Capability for the main window", + "windows": ["main"], + "permissions": [ + "core:default", + "shell:allow-open" + ] +} diff --git a/src-tauri/icons/128x128.png b/src-tauri/icons/128x128.png new file mode 100644 index 0000000000000000000000000000000000000000..6be5e50e9b9ae84d9e2ee433f32ef446495eaf3b GIT binary patch literal 3512 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 literal 0 HcmV?d00001 diff --git a/src-tauri/icons/128x128@2x.png b/src-tauri/icons/128x128@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..e81becee571e96f76aa5667f9324c05e5e7a4479 GIT binary patch literal 7012 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 Sh@|Ye~|0>}wMPLT8jm@Y!H33Sz}5aFI6 zM9Lzqz|;A*0sGs=2A1uU!1nk2dGF7knQwr99SAFen)x(eCO;F8y2C~0FD1YxRTPcy zPWVxkUYmeuz}Tv?7&Fe-!UE{)ZW)Mb;H)^#eHDv$`dkZGguJz@^MA!ZNGAUqt{|0H zpZ7Ch9S`q5!>R%}>}62!+(T^evyO+ImSo2wpu)su4^3nw5(%)KD%gbSev^*HZZ&3( z#&c@Z0gH|}Ck)w6fh0&NBJ62ib%R}(3@$VFl*_#l2W$wQ-~4RmZZAt5O*^2Q5}Xr8Hy@c`#pM?kc?hFWxRXr*mUfUCXf4ka5DD~ zat6d85COB05l#(P9*cQZ3EC8fVdS~?&vN#rce(aF9@xp80O2{{FBvU+{X>Hoh;xI` z{$e^Nw1y*VbO8wv`8|-m?NwNaKGTGaF{P^JLB^DbOYWIbn%eT`*!^C1H36=O8Z-M> zkD~88ry`eSo`tEBN4>w7OWZwUzlh{WM1m8R6zepqGcGMaV7vWY9b?K4b6~|HVG)ec wi>I@ws#sZo7or4_*4M>7;p5{nr2pZ?Uu4>Krr0kU)&Kwi07*qoM6N<$f)&@lf&c&j literal 0 HcmV?d00001 diff --git a/src-tauri/icons/Square107x107Logo.png b/src-tauri/icons/Square107x107Logo.png new file mode 100644 index 0000000000000000000000000000000000000000..0ca4f27198838968bd60ed7d371bfa23496b7fe5 GIT binary patch literal 2863 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 Y2S8W#vM)6=T>t<807*qoM6N<$f*y@n<^TWy literal 0 HcmV?d00001 diff --git a/src-tauri/icons/Square284x284Logo.png b/src-tauri/icons/Square284x284Logo.png new file mode 100644 index 0000000000000000000000000000000000000000..c021d2ba76619c08969ab688db3b27f29257aa6f GIT binary patch literal 7737 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 literal 0 HcmV?d00001 diff --git a/src-tauri/icons/Square30x30Logo.png b/src-tauri/icons/Square30x30Logo.png new file mode 100644 index 0000000000000000000000000000000000000000..621970023096ed9f494ba18ace15421a45cd65fa GIT binary patch literal 903 zcmV;219<$2P)2 z+CUKPMqaqGiH;zb!R4$B-WXS^YzQr=@UH>k4?*L)&R=zYjBrZenKdc9|JlS$SO*RJ zKt8FSTDAdk1g_WPAO!p^V!AuL;Lm;uQyV;zKq)J3i(;q*;k+pD%f3eltU`PYdy9(k0&%` zuWAPcV6|-y?|?7O1W!KSK}pbk8#~!|FA@(VJkt^V@0lio{afoAeo*f&$W2s6${5!1eKvAGD2$GZwSB98L2ZVS- zKn8ENRkZ*sb!@QugOrQNK3(sy1v%J#m|rpB+h|Nkqa3FRT>74xSs{#&saU2Lf!_Iq zKmuKAESh`gs!fneGWn+nf}l?7jE$HW!Af&vE5=G!QU)U2v&HLIBGXKk4nQx{hsHjL zLPMAo5=*uInFbq7(aa`Y2VX5wCmaeqvECOFv)a>0t>ZaEb*cJccER=BB?KFZhV$c^ znL*l8x*UYZv4WK|j?~Jt6~~F%{pk~z5A*>^M`?r5m9@RJ_x|uEtX(6Vk@Y()MVto* z93wr)%3m%|#OZ~srm>zF(JvDuTq*@;d&^>_BJm5hOU`3FjG70L#Vzv9I?`<7$T@

jU?lMi@tgxr7CqX_r3uw^y4tVU3Pm0sw;|1WSUO%?=bG`*Kmz6u4{#ti;T7AWIBAEh!(Y zz>O01&#X?Ds@L)Sb{CkG#Yz4$3o d@96)?#cz^xWoA}>B$xmI002ovPDHLkV1l3&k#zt7 literal 0 HcmV?d00001 diff --git a/src-tauri/icons/Square310x310Logo.png b/src-tauri/icons/Square310x310Logo.png new file mode 100644 index 0000000000000000000000000000000000000000..f9bc04839491e66c07b16ab03743c0c53b4109cc GIT binary patch literal 8591 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 JPDHLkV1lm|RXG3v literal 0 HcmV?d00001 diff --git a/src-tauri/icons/Square71x71Logo.png b/src-tauri/icons/Square71x71Logo.png new file mode 100644 index 0000000000000000000000000000000000000000..63440d7984936a9caa89275928d8dce97e4d033b GIT binary patch literal 2011 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∓t$dRJGIadE?vc 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 Kx8Vjl{{H|h@<;Lj literal 0 HcmV?d00001 diff --git a/src-tauri/icons/icon.ico b/src-tauri/icons/icon.ico new file mode 100644 index 0000000000000000000000000000000000000000..b3636e4b22ba65db9061cd60a77b02c92022dfd6 GIT binary patch literal 86642 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 AKmY&$ literal 0 HcmV?d00001 diff --git a/src-tauri/icons/icon.png b/src-tauri/icons/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..e1cd2619e0b5ec089cbba5ec7b03ddf2b1dfceb6 GIT binary patch literal 14183 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

5l#v3A#xI3*z~1~fvo9Q(N^(==!|_FZ z*duZ=+M1~)8E|otX8KNZlr?qels#x_1Xq@9IIw~@9uAREJVH)Xw^}UclF6327}E42 zT)E&?U%TK?(+K7%R!`H5oX0i)4Qn5??Iw3p5J~6_u+aWehY{DSn}3V2p$bgjnAu?o)v@iC254fXeMv50$9YrpU`N?u@QIWs)T?SP|fa}(|9 zqAX+!7`cx=4)cCBg5h~pu(?@9`)aCr#oyz$ld=#RFxYCNZCZls@4v2~*e-t6PEVvV z&bbK3b3wt(Coc!ufAbXXC<**#HQ%J9k`New6iG<5RjtO4XVO?dCvwxD{kJ#tfQr(X zg^NTwF-FwAeS_{V4bfel8l`~NbfrTR2s!G>WduFWxH(t~aK4q=6rEE^$+Uox>gJO2 z{L<;6Q6nHa5#ZEM>H58not!)z(6*_=^~8}jWf*IG$AUKVWOZ4?)GfF z+BM#*wKKmLFD7E~W3U!$IVm$k_k1f&Kz6WV8@55P?r~bcg-Za-!rvW?ns&)KOGT2~ zlkAyqhQj=P$Eg3w#K~}zH@J5bo-BfHjInKSz$@?+Z)NPD4pHj^_Qxmi`UqoTy=`sV zLVxrXGuBr=QRm|}wg75yetQQK4fY3#P_~J}zEfPnb2C4Wo!E(d*(cA;b?7$g2in<( zPn)ghX}nzJPmb6(3Dpeg_GW~Hc}Lt=lgsSZz z!5QXyz7KaR;D`3Ee}d`af{H>WWZ|Io1QI3~4Ll_`g1(cRnhLK73Ro)7zPCd={1W2x zRp%Xlvv4>!<2@}$hz|!V{T}_eHx2xkLl^hQoZTCnsjCl|W_@5Fx2(+j0ogy&Y+;L- z<)G$*CiN7hOm^s!{U>1F7U=iNk{+u~dAC!eDz%=|glFW0jEZU1&o(G_c#wTxUjnG} z#cg3>jEpUi#Mlq@t?Msg_#geK^Lx@DyHWf7=AS5vVyM7YOjvUVCfcpVR<(+5!H?9- zySI6s>o3m&*zr||=wcPGyBkQV`EWJl@bH8qobjOp+sXL*)=&yX)8aAbf~tGv?a2SN zu^Ddo-z?DWk9h9Yz#5p^NU#x~wYSd?H@w@!2Gb4G)6-utEMV~~M85Br5ff(v5O1|T z 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 String { + format!("Hello, {}! You've been greeted from Rust!", name) +} + +#[cfg_attr(mobile, tauri::mobile_entry_point)] +pub fn run() { + tauri::Builder::default() + .plugin(tauri_plugin_shell::init()) + .invoke_handler(tauri::generate_handler![greet]) + .run(tauri::generate_context!()) + .expect("error while running tauri application"); +} diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs new file mode 100644 index 0000000..dc77732 --- /dev/null +++ b/src-tauri/src/main.rs @@ -0,0 +1,6 @@ +// Prevents additional console window on Windows in release, DO NOT REMOVE!! +#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] + +fn main() { + lightningbeam_lib::run() +} diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json new file mode 100644 index 0000000..8d0291d --- /dev/null +++ b/src-tauri/tauri.conf.json @@ -0,0 +1,33 @@ +{ + "$schema": "https://schema.tauri.app/config/2", + "productName": "lightningbeam", + "version": "0.1.0", + "identifier": "org.lightningbeam.app", + "build": { + "frontendDist": "../src" + }, + "app": { + "withGlobalTauri": true, + "windows": [ + { + "title": "lightningbeam", + "width": 800, + "height": 600 + } + ], + "security": { + "csp": null + } + }, + "bundle": { + "active": true, + "targets": "all", + "icon": [ + "icons/32x32.png", + "icons/128x128.png", + "icons/128x128@2x.png", + "icons/icon.icns", + "icons/icon.ico" + ] + } +} diff --git a/src/assets/buttons.svg b/src/assets/buttons.svg new file mode 100644 index 0000000..02db28c --- /dev/null +++ b/src/assets/buttons.svg @@ -0,0 +1,128 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/assets/javascript.svg b/src/assets/javascript.svg new file mode 100644 index 0000000..f9abb2b --- /dev/null +++ b/src/assets/javascript.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/stage.svg b/src/assets/stage.svg new file mode 100644 index 0000000..04412ea --- /dev/null +++ b/src/assets/stage.svg @@ -0,0 +1,59 @@ + + + + + + + + + + + + diff --git a/src/assets/tauri.svg b/src/assets/tauri.svg new file mode 100644 index 0000000..31b62c9 --- /dev/null +++ b/src/assets/tauri.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/src/index.html b/src/index.html new file mode 100644 index 0000000..10c6e7f --- /dev/null +++ b/src/index.html @@ -0,0 +1,44 @@ + + + + + + + Tauri App + + + + +

+ + + + +
+ + +
+
+ + diff --git a/src/main.js b/src/main.js new file mode 100644 index 0000000..5c26fa9 --- /dev/null +++ b/src/main.js @@ -0,0 +1,261 @@ +// const { invoke } = window.__TAURI__.core; + +let greetInputEl; +let greetMsgEl; +let rootPane; + +let canvases = []; + +let mode = "draw" + +let tools = { + select: { + icon: "/assets/select.png", + + }, + draw: { + icon: "/assets/pen.png" + }, + rectangle: { + icon: "/assets/rectangle.png" + }, + polygon: { + icon: "assets/polygon.png" + } +} + +let mouseEvent; + +let context = {} + +function uuidv4() { + return "10000000-1000-4000-8000-100000000000".replace(/[018]/g, c => + (+c ^ crypto.getRandomValues(new Uint8Array(1))[0] & 15 >> +c / 4).toString(16) + ); +} + +function getMousePos(canvas, evt) { + var rect = canvas.getBoundingClientRect(); + return { + x: evt.clientX - rect.left, + y: evt.clientY - rect.top + }; +} + +class Curve { + constructor(cp1x, cp1y, cp2x, cp2y, x, y) { + this.cp1x = cp1x; + this.cp1y = cp1y; + this.cp2x = cp2x; + this.cp2y = cp2y; + this.x = x; + this.y = y; + } +} + +class Frame { + constructor() { + this.keys = {} + } +} + +class Shape { + constructor(startx, starty, fillStyle, strokeStyle, filled=true, stroked=true) { + this.startx = startx; + this.starty = starty; + this.curves = []; + this.fillStyle = fillStyle; + this.strokeStyle = strokeStyle; + this.filled = filled; + this.stroked = stroked; + } +} + +class GraphicsObject { + constructor() { + this.x = 0; + this.y = 0; + this.rotation = 0; + this.scale = 1; + this.idx = uuidv4() + + this.frames = [new Frame()] + this.currentFrame = 0; + this.children = [] + + this.shapes = [] + } + draw(context) { + let ctx = context.ctx; + if (this.currentFrame>=this.frames.length) { + this.currentFrame = 0; + } + for (let child of this.children) { + let idx = child.idx + child.x = this.frames[this.currentFrame][idx].x; + child.y = this.frames[this.currentFrame][idx].y; + child.rotation = this.frames[this.currentFrame][idx].rotation; + child.scale = this.frames[this.currentFrame][idx].scale; + child.draw(context) + } + for (let shape of this.shapes) { + ctx.beginPath() + ctx.moveTo(shape.startx, shape.starty) + for (let curve of shape.curves) { + ctx.bezierCurveTo(curve.cp1x, curve.cp1y, curve.cp2x, curve.cp2y, curve.x, curve.y) + } + if (shape.filled) { + ctx.fillStyle = shape.fillStyle + ctx.fill() + } + if (shape.stroked) { + ctx.strokeStyle = shape.strokeStyle + ctx.stroke() + } + } + } + addShape(shape) { + this.shapes.push(shape) + } +} + +let root = new GraphicsObject(); +let shp = new Shape(100,100,'blue', 'black') +shp.curves.push(new Curve(150,150,150,150,200,100)) +root.addShape(shp) + +async function greet() { + // Learn more about Tauri commands at https://tauri.app/develop/calling-rust/ + greetMsgEl.textContent = await invoke("greet", { name: greetInputEl.value }); + + // splitPane(rootPane, 50, true) +} + +window.addEventListener("DOMContentLoaded", () => { + // greetInputEl = document.querySelector("#greet-input"); + // greetMsgEl = document.querySelector("#greet-msg"); + rootPane = document.querySelector("#root") + rootPane.appendChild(toolbar()) + rootPane.addEventListener("mousemove", (e) => { + mouseEvent = e; + }) + // document.querySelector("#greet-form").addEventListener("submit", (e) => { + // e.preventDefault(); + // greet(); + // }); + splitPane(rootPane, 10, true) +}); + +function stage() { + let stage = document.createElement("canvas") + let scroller = document.createElement("div") + stage.className = "stage" + stage.width = 1500 + stage.height = 1000 + scroller.className = "scroll" + canvases.push(stage) + scroller.appendChild(stage) + return scroller +} + +function toolbar() { + let tools_scroller = document.createElement("div") + tools_scroller.className = "toolbar" + for (let tool in tools) { + let toolbtn = document.createElement("button") + toolbtn.className = "toolbtn" + let icon = document.createElement("img") + icon.className = "icon" + icon.src = tools[tool].icon + toolbtn.appendChild(icon) + tools_scroller.appendChild(toolbtn) + } + return tools_scroller +} + +function createPane() { + let div = document.createElement("div") + let header = document.createElement("div") + let content = stage() // TODO: change based on type + header.className = "header" + + let button = document.createElement("button") + header.appendChild(button) + let icon = document.createElement("img") + icon.className="icon" + icon.src = "/assets/stage.svg" + button.appendChild(icon) + + + // div.style.display = "grid"; + // div.style.gridTemplateColumns = `var(--lineheight) 1fr` + // div.style.gridTemplateRows = "1fr" + // header.style.gridArea = "1 / 1 / 2 / 2" + // content.style.gridArea = "1 / 2 / 2 / 3" + + div.classList = ["vertical-grid", "pane"] + header.style.flex = "0 0 var(--lineheight)" + content.style.flex = "1 1 100%" + div.appendChild(header) + div.appendChild(content) + return div +} + +function splitPane(div, percent, horiz) { + let content = div.firstElementChild + let div1 = document.createElement("div") + let div2 = document.createElement("div") + + div1.className = "panecontainer" + div2.className = "panecontainer" + + div1.appendChild(content) + div2.appendChild(createPane()) + div.appendChild(div1) + div.appendChild(div2) + + // div.style.display = "grid"; + // if (horiz) { + // div.classList.add("horizontal-grid") + // div.style.gridTemplateColumns = `${percent}% 1fr` + // div1.style.gridArea = "1 / 1 / 2 / 2" + // div2.style.gridArea = "1 / 2 / 2 / 3" + // } else { + // div.classList.add("vertical-grid") + // div.style.gridTemplateRows = `${percent}% 1fr` + // div1.style.gridArea = "1 / 1 / 2 / 2" + // div2.style.gridArea = "2 / 1 / 3 / 2" + // } + if (horiz) { + div.className = "horizontal-grid" + } else { + div.className = "verical-grid" + } + div1.style.flex = `0 0 ${percent}%` + div2.style.flex = `1 1 auto` + updateUI() +} + + + +function updateUI() { + for (let canvas of canvases) { + let ctx = canvas.getContext("2d") + ctx.fillStyle = "white" + ctx.fillRect(0,0,canvas.width,canvas.height) + ctx.fillStyle = "green" + // ctx.fillRect(0,0,200,200) + + context.ctx = ctx; + root.draw(context) + + let mouse; + if (mouseEvent) { + mouse = getMousePos(canvas, mouseEvent); + } else { + mouse = {x: 0, y: 0} + } + ctx.fillRect(mouse.x, mouse.y, 50,50) + } + requestAnimationFrame(updateUI) +} \ No newline at end of file diff --git a/src/styles.css b/src/styles.css new file mode 100644 index 0000000..7c180f3 --- /dev/null +++ b/src/styles.css @@ -0,0 +1,194 @@ +body { + width: 100%; + height: 100%; + overflow: hidden; +} + +.logo.vanilla:hover { + filter: drop-shadow(0 0 2em #ffe21c); +} +:root { + --lineheight: 24px; + font-family: Inter, Avenir, Helvetica, Arial, sans-serif; + font-size: 16px; + line-height: var(--lineheight); + font-weight: 400; + + color: #0f0f0f; + background-color: #f6f6f6; + + font-synthesis: none; + text-rendering: optimizeLegibility; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + -webkit-text-size-adjust: 100%; + height: 100%; +} + +.container { + margin: 0; + padding-top: 10vh; + display: flex; + flex-direction: column; + justify-content: center; + text-align: center; +} + +.logo { + height: 6em; + padding: 1.5em; + will-change: filter; + transition: 0.75s; +} + +.logo.tauri:hover { + filter: drop-shadow(0 0 2em #24c8db); +} + +.row { + display: flex; + justify-content: center; +} + +a { + font-weight: 500; + color: #646cff; + text-decoration: inherit; +} + +a:hover { + color: #535bf2; +} + +h1 { + text-align: center; +} + +input, +button { + border-radius: 8px; + border: 1px solid transparent; + padding: 0.6em 1.2em; + font-size: 1em; + font-weight: 500; + font-family: inherit; + color: #0f0f0f; + background-color: #ffffff; + transition: border-color 0.25s; + box-shadow: 0 2px 2px rgba(0, 0, 0, 0.2); +} + +button { + cursor: pointer; +} + +button:hover { + border-color: #396cd8; +} +button:active { + border-color: #396cd8; + background-color: #e8e8e8; +} + +input, +button { + outline: none; +} + +#greet-input { + margin-right: 5px; +} + +@media (prefers-color-scheme: dark) { + :root { + color: #f6f6f6; + background-color: #2f2f2f; + } + + a:hover { + color: #24c8db; + } + + input, + button { + color: #ffffff; + background-color: #0f0f0f98; + } + button:active { + background-color: #0f0f0f69; + } +} + +.header { + height: 60px; + min-width: 100%; + background-color: #3f3f3f; + text-align: left; + z-index: 1; +} + +.icon { + width: var(--lineheight); + height: var(--lineheight); +} + +.panecontainer { + width: 100%; + height: 100%; +} + +.horizontal-grid { + /* display: grid; + grid-template-rows: 1fr; + grid-column-gap: 0px; + grid-row-gap: 0px; + min-width: 0px; + min-height: 0px; + max-height: 100%; */ + display: flex; + flex-direction: row; + gap: 10px; + background-color: #0f0f0f; + width: 100%; +} +.vertical-grid { + /* display: grid; + grid-template-columns: 1fr; + grid-column-gap: 0px; + grid-row-gap: 0px; + min-width: 0px; + min-height: 0px; + max-height: 100%; */ + display: flex; + flex-direction: column; + gap: 10px; + background-color: #0f0f0f; + height: 100%; +} +.scroll { + overflow: scroll; + width: 100%; + height: 100%; + /* display: block; + border-style: solid; + border-width: 1px; + border-color: rgba(0, 0, 0, 0.2); + font-family: Helvetica, Arial, sans-serif; + overflow: scroll; */ +} +.stage { + width: 1500px; + height: 1000px; + overflow: scroll; +} +.toolbar { + display: flex; + flex-direction: row; + gap: 10px; + flex-wrap: wrap; +} +.toolbtn { + /* width: var(--lineheight); + height: var(--lineheight); */ + background-color: #2f2f2f; +} \ No newline at end of file From 36f8c99dd31a39c6f9513e5179a28a1bf8490e91 Mon Sep 17 00:00:00 2001 From: Skyler Lehmkuhl Date: Fri, 15 Nov 2024 00:15:58 -0500 Subject: [PATCH 02/46] basic drawing of closed shapes --- src/assets/draw.svg | 142 +++++ src/assets/select.svg | 139 +++++ src/assets/transform.svg | 200 ++++++ src/coloris.css | 577 +++++++++++++++++ src/coloris.js | 1263 ++++++++++++++++++++++++++++++++++++++ src/index.html | 2 + src/main.js | 154 ++++- src/styles.css | 7 + 8 files changed, 2470 insertions(+), 14 deletions(-) create mode 100644 src/assets/draw.svg create mode 100644 src/assets/select.svg create mode 100644 src/assets/transform.svg create mode 100644 src/coloris.css create mode 100644 src/coloris.js diff --git a/src/assets/draw.svg b/src/assets/draw.svg new file mode 100644 index 0000000..e1a096a --- /dev/null +++ b/src/assets/draw.svg @@ -0,0 +1,142 @@ + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + Klaus Staedtler + + + + + + + + + + + + + diff --git a/src/assets/select.svg b/src/assets/select.svg new file mode 100644 index 0000000..de109e2 --- /dev/null +++ b/src/assets/select.svg @@ -0,0 +1,139 @@ + + + + + + + + + image/svg+xml + + + + + Barbara Muraus, Jakub Steiner, Klaus Staedtler + + + Images originally created as the "Art Libre" icon set. Extended and adopted for GIMP + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/assets/transform.svg b/src/assets/transform.svg new file mode 100644 index 0000000..afc157b --- /dev/null +++ b/src/assets/transform.svg @@ -0,0 +1,200 @@ + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + Klaus Staedtler + + + + + + + + + + + + + + + + diff --git a/src/coloris.css b/src/coloris.css new file mode 100644 index 0000000..41ddad2 --- /dev/null +++ b/src/coloris.css @@ -0,0 +1,577 @@ +.clr-picker { + display: none; + flex-wrap: wrap; + position: absolute; + width: 200px; + z-index: 1000; + border-radius: 10px; + background-color: #fff; + justify-content: flex-end; + direction: ltr; + box-shadow: 0 0 5px rgba(0,0,0,.05), 0 5px 20px rgba(0,0,0,.1); + -moz-user-select: none; + -webkit-user-select: none; + user-select: none; +} + +.clr-picker.clr-open, +.clr-picker[data-inline="true"] { + display: flex; +} + +.clr-picker[data-inline="true"] { + position: relative; +} + +.clr-gradient { + position: relative; + width: 100%; + height: 100px; + margin-bottom: 15px; + border-radius: 3px 3px 0 0; + background-image: linear-gradient(rgba(0,0,0,0), #000), linear-gradient(90deg, #fff, currentColor); + cursor: pointer; +} + +.clr-marker { + position: absolute; + width: 12px; + height: 12px; + margin: -6px 0 0 -6px; + border: 1px solid #fff; + border-radius: 50%; + background-color: currentColor; + cursor: pointer; +} + +.clr-picker input[type="range"]::-webkit-slider-runnable-track { + width: 100%; + height: 16px; +} + +.clr-picker input[type="range"]::-webkit-slider-thumb { + width: 16px; + height: 16px; + -webkit-appearance: none; +} + +.clr-picker input[type="range"]::-moz-range-track { + width: 100%; + height: 16px; + border: 0; +} + +.clr-picker input[type="range"]::-moz-range-thumb { + width: 16px; + height: 16px; + border: 0; +} + +.clr-hue { + background-image: linear-gradient(to right, #f00 0%, #ff0 16.66%, #0f0 33.33%, #0ff 50%, #00f 66.66%, #f0f 83.33%, #f00 100%); +} + +.clr-hue, +.clr-alpha { + position: relative; + width: calc(100% - 40px); + height: 8px; + margin: 5px 20px; + border-radius: 4px; +} + +.clr-alpha span { + display: block; + height: 100%; + width: 100%; + border-radius: inherit; + background-image: linear-gradient(90deg, rgba(0,0,0,0), currentColor); +} + +.clr-hue input[type="range"], +.clr-alpha input[type="range"] { + position: absolute; + width: calc(100% + 32px); + height: 16px; + left: -16px; + top: -4px; + margin: 0; + background-color: transparent; + opacity: 0; + cursor: pointer; + appearance: none; + -webkit-appearance: none; +} + +.clr-hue div, +.clr-alpha div { + position: absolute; + width: 16px; + height: 16px; + left: 0; + top: 50%; + margin-left: -8px; + transform: translateY(-50%); + border: 2px solid #fff; + border-radius: 50%; + background-color: currentColor; + box-shadow: 0 0 1px #888; + pointer-events: none; +} + +.clr-alpha div:before { + content: ''; + position: absolute; + height: 100%; + width: 100%; + left: 0; + top: 0; + border-radius: 50%; + background-color: currentColor; +} + +.clr-format { + display: none; + order: 1; + width: calc(100% - 40px); + margin: 0 20px 20px; +} + +.clr-segmented { + display: flex; + position: relative; + width: 100%; + margin: 0; + padding: 0; + border: 1px solid #ddd; + border-radius: 15px; + box-sizing: border-box; + color: #999; + font-size: 12px; +} + +.clr-segmented input, +.clr-segmented legend { + position: absolute; + width: 100%; + height: 100%; + margin: 0; + padding: 0; + border: 0; + left: 0; + top: 0; + opacity: 0; + pointer-events: none; +} + +.clr-segmented label { + flex-grow: 1; + margin: 0; + padding: 4px 0; + font-size: inherit; + font-weight: normal; + line-height: initial; + text-align: center; + cursor: pointer; +} + +.clr-segmented label:first-of-type { + border-radius: 10px 0 0 10px; +} + +.clr-segmented label:last-of-type { + border-radius: 0 10px 10px 0; +} + +.clr-segmented input:checked + label { + color: #fff; + background-color: #666; +} + +.clr-swatches { + order: 2; + width: calc(100% - 32px); + margin: 0 16px; +} + +.clr-swatches div { + display: flex; + flex-wrap: wrap; + padding-bottom: 12px; + justify-content: center; +} + +.clr-swatches button { + position: relative; + width: 20px; + height: 20px; + margin: 0 4px 6px 4px; + padding: 0; + border: 0; + border-radius: 50%; + color: inherit; + text-indent: -1000px; + white-space: nowrap; + overflow: hidden; + cursor: pointer; +} + +.clr-swatches button:after { + content: ''; + display: block; + position: absolute; + width: 100%; + height: 100%; + left: 0; + top: 0; + border-radius: inherit; + background-color: currentColor; + box-shadow: inset 0 0 0 1px rgba(0,0,0,.1); +} + +input.clr-color { + order: 1; + width: calc(100% - 80px); + height: 32px; + margin: 15px 20px 20px auto; + padding: 0 10px; + border: 1px solid #ddd; + border-radius: 16px; + color: #444; + background-color: #fff; + font-family: sans-serif; + font-size: 14px; + text-align: center; + box-shadow: none; +} + +input.clr-color:focus { + outline: none; + border: 1px solid #1e90ff; +} + +.clr-close, +.clr-clear { + display: none; + order: 2; + height: 24px; + margin: 0 20px 20px; + padding: 0 20px; + border: 0; + border-radius: 12px; + color: #fff; + background-color: #666; + font-family: inherit; + font-size: 12px; + font-weight: 400; + cursor: pointer; +} + +.clr-close { + display: block; + margin: 0 20px 20px auto; +} + +.clr-preview { + position: relative; + width: 32px; + height: 32px; + margin: 15px 0 20px 20px; + border-radius: 50%; + overflow: hidden; +} + +.clr-preview:before, +.clr-preview:after { + content: ''; + position: absolute; + height: 100%; + width: 100%; + left: 0; + top: 0; + border: 1px solid #fff; + border-radius: 50%; +} + +.clr-preview:after { + border: 0; + background-color: currentColor; + box-shadow: inset 0 0 0 1px rgba(0,0,0,.1); +} + +.clr-preview button { + position: absolute; + width: 100%; + height: 100%; + z-index: 1; + margin: 0; + padding: 0; + border: 0; + border-radius: 50%; + outline-offset: -2px; + background-color: transparent; + text-indent: -9999px; + cursor: pointer; + overflow: hidden; +} + +.clr-marker, +.clr-hue div, +.clr-alpha div, +.clr-color { + box-sizing: border-box; +} + +.clr-field { + display: inline-block; + position: relative; + color: transparent; +} + +.clr-field input { + margin: 0; + direction: ltr; +} + +.clr-field.clr-rtl input { + text-align: right; +} + +.clr-field button { + position: absolute; + width: 30px; + height: 100%; + right: 0; + top: 50%; + transform: translateY(-50%); + margin: 0; + padding: 0; + border: 0; + color: inherit; + text-indent: -1000px; + white-space: nowrap; + overflow: hidden; + pointer-events: none; +} + +.clr-field.clr-rtl button { + right: auto; + left: 0; +} + +.clr-field button:after { + content: ''; + display: block; + position: absolute; + width: 100%; + height: 100%; + left: 0; + top: 0; + border-radius: inherit; + background-color: currentColor; + box-shadow: inset 0 0 1px rgba(0,0,0,.5); +} + +.clr-alpha, +.clr-alpha div, +.clr-swatches button, +.clr-preview:before, +.clr-field button { + background-image: repeating-linear-gradient(45deg, #aaa 25%, transparent 25%, transparent 75%, #aaa 75%, #aaa), repeating-linear-gradient(45deg, #aaa 25%, #fff 25%, #fff 75%, #aaa 75%, #aaa); + background-position: 0 0, 4px 4px; + background-size: 8px 8px; +} + +.clr-marker:focus { + outline: none; +} + +.clr-keyboard-nav .clr-marker:focus, +.clr-keyboard-nav .clr-hue input:focus + div, +.clr-keyboard-nav .clr-alpha input:focus + div, +.clr-keyboard-nav .clr-segmented input:focus + label { + outline: none; + box-shadow: 0 0 0 2px #1e90ff, 0 0 2px 2px #fff; +} + +.clr-picker[data-alpha="false"] .clr-alpha { + display: none; +} + +.clr-picker[data-minimal="true"] { + padding-top: 16px; +} + +.clr-picker[data-minimal="true"] .clr-gradient, +.clr-picker[data-minimal="true"] .clr-hue, +.clr-picker[data-minimal="true"] .clr-alpha, +.clr-picker[data-minimal="true"] .clr-color, +.clr-picker[data-minimal="true"] .clr-preview { + display: none; +} + +/** Dark theme **/ + +.clr-dark { + background-color: #444; +} + +.clr-dark .clr-segmented { + border-color: #777; +} + +.clr-dark .clr-swatches button:after { + box-shadow: inset 0 0 0 1px rgba(255,255,255,.3); +} + +.clr-dark input.clr-color { + color: #fff; + border-color: #777; + background-color: #555; +} + +.clr-dark input.clr-color:focus { + border-color: #1e90ff; +} + +.clr-dark .clr-preview:after { + box-shadow: inset 0 0 0 1px rgba(255,255,255,.5); +} + +.clr-dark .clr-alpha, +.clr-dark .clr-alpha div, +.clr-dark .clr-swatches button, +.clr-dark .clr-preview:before { + background-image: repeating-linear-gradient(45deg, #666 25%, transparent 25%, transparent 75%, #888 75%, #888), repeating-linear-gradient(45deg, #888 25%, #444 25%, #444 75%, #888 75%, #888); +} + +/** Polaroid theme **/ + +.clr-picker.clr-polaroid { + border-radius: 6px; + box-shadow: 0 0 5px rgba(0,0,0,.1), 0 5px 30px rgba(0,0,0,.2); +} + +.clr-picker.clr-polaroid:before { + content: ''; + display: block; + position: absolute; + width: 16px; + height: 10px; + left: 20px; + top: -10px; + border: solid transparent; + border-width: 0 8px 10px 8px; + border-bottom-color: currentColor; + box-sizing: border-box; + color: #fff; + filter: drop-shadow(0 -4px 3px rgba(0,0,0,.1)); + pointer-events: none; +} + +.clr-picker.clr-polaroid.clr-dark:before { + color: #444; +} + +.clr-picker.clr-polaroid.clr-left:before { + left: auto; + right: 20px; +} + +.clr-picker.clr-polaroid.clr-top:before { + top: auto; + bottom: -10px; + transform: rotateZ(180deg); +} + +.clr-polaroid .clr-gradient { + width: calc(100% - 20px); + height: 120px; + margin: 10px; + border-radius: 3px; +} + +.clr-polaroid .clr-hue, +.clr-polaroid .clr-alpha { + width: calc(100% - 30px); + height: 10px; + margin: 6px 15px; + border-radius: 5px; +} + +.clr-polaroid .clr-hue div, +.clr-polaroid .clr-alpha div { + box-shadow: 0 0 5px rgba(0,0,0,.2); +} + +.clr-polaroid .clr-format { + width: calc(100% - 20px); + margin: 0 10px 15px; +} + +.clr-polaroid .clr-swatches { + width: calc(100% - 12px); + margin: 0 6px; +} +.clr-polaroid .clr-swatches div { + padding-bottom: 10px; +} + +.clr-polaroid .clr-swatches button { + width: 22px; + height: 22px; +} + +.clr-polaroid input.clr-color { + width: calc(100% - 60px); + margin: 10px 10px 15px auto; +} + +.clr-polaroid .clr-clear { + margin: 0 10px 15px 10px; +} + +.clr-polaroid .clr-close { + margin: 0 10px 15px auto; +} + +.clr-polaroid .clr-preview { + margin: 10px 0 15px 10px; +} + +/** Large theme **/ + +.clr-picker.clr-large { + width: 275px; +} + +.clr-large .clr-gradient { + height: 150px; +} + +.clr-large .clr-swatches button { + width: 22px; + height: 22px; +} + +/** Pill (horizontal) theme **/ + +.clr-picker.clr-pill { + width: 380px; + padding-left: 180px; + box-sizing: border-box; +} + +.clr-pill .clr-gradient { + position: absolute; + width: 180px; + height: 100%; + left: 0; + top: 0; + margin-bottom: 0; + border-radius: 3px 0 0 3px; +} + +.clr-pill .clr-hue { + margin-top: 20px; +} \ No newline at end of file diff --git a/src/coloris.js b/src/coloris.js new file mode 100644 index 0000000..1aa88b0 --- /dev/null +++ b/src/coloris.js @@ -0,0 +1,1263 @@ + /*! + * Copyright (c) 2021 Momo Bassit. + * Licensed under the MIT License (MIT) + * https://github.com/mdbassit/Coloris + */ + +(function (window, document, Math, undefined) { + var ctx = document.createElement('canvas').getContext('2d'); + var currentColor = { r: 0, g: 0, b: 0, h: 0, s: 0, v: 0, a: 1 }; + var container,picker,colorArea,colorMarker,colorPreview,colorValue,clearButton,closeButton, + hueSlider,hueMarker,alphaSlider,alphaMarker,currentEl,currentFormat,oldColor,keyboardNav, + colorAreaDims = {}; + + // Default settings + var settings = { + el: '[data-coloris]', + parent: 'body', + theme: 'default', + themeMode: 'light', + rtl: false, + wrap: true, + margin: 2, + format: 'hex', + formatToggle: false, + swatches: [], + swatchesOnly: false, + alpha: true, + forceAlpha: false, + focusInput: true, + selectInput: false, + inline: false, + defaultColor: '#000000', + clearButton: false, + clearLabel: 'Clear', + closeButton: false, + closeLabel: 'Close', + onChange: function onChange() {return undefined;}, + a11y: { + open: 'Open color picker', + close: 'Close color picker', + clear: 'Clear the selected color', + marker: 'Saturation: {s}. Brightness: {v}.', + hueSlider: 'Hue slider', + alphaSlider: 'Opacity slider', + input: 'Color value field', + format: 'Color format', + swatch: 'Color swatch', + instruction: 'Saturation and brightness selector. Use up, down, left and right arrow keys to select.' } }; + + + + // Virtual instances cache + var instances = {}; + var currentInstanceId = ''; + var defaultInstance = {}; + var hasInstance = false; + + /** + * Configure the color picker. + * @param {object} options Configuration options. + */ + function configure(options) { + if (typeof options !== 'object') { + return; + } + + for (var key in options) { + switch (key) { + case 'el': + bindFields(options.el); + if (options.wrap !== false) { + wrapFields(options.el); + } + break; + case 'parent': + container = options.parent instanceof HTMLElement ? options.parent : document.querySelector(options.parent); + if (container) { + container.appendChild(picker); + settings.parent = options.parent; + + // document.body is special + if (container === document.body) { + container = undefined; + } + } + break; + case 'themeMode': + settings.themeMode = options.themeMode; + if (options.themeMode === 'auto' && window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches) { + settings.themeMode = 'dark'; + } + // The lack of a break statement is intentional + case 'theme': + if (options.theme) { + settings.theme = options.theme; + } + + // Set the theme and color scheme + picker.className = "clr-picker clr-" + settings.theme + " clr-" + settings.themeMode; + + // Update the color picker's position if inline mode is in use + if (settings.inline) { + updatePickerPosition(); + } + break; + case 'rtl': + settings.rtl = !!options.rtl; + Array.from(document.getElementsByClassName('clr-field')).forEach(function (field) {return field.classList.toggle('clr-rtl', settings.rtl);}); + break; + case 'margin': + options.margin *= 1; + settings.margin = !isNaN(options.margin) ? options.margin : settings.margin; + break; + case 'wrap': + if (options.el && options.wrap) { + wrapFields(options.el); + } + break; + case 'formatToggle': + settings.formatToggle = !!options.formatToggle; + getEl('clr-format').style.display = settings.formatToggle ? 'block' : 'none'; + if (settings.formatToggle) { + settings.format = 'auto'; + } + break; + case 'swatches': + if (Array.isArray(options.swatches)) {(function () { + var swatchesContainer = getEl('clr-swatches'); + var swatches = document.createElement('div'); + + // Clear current swatches + swatchesContainer.textContent = ''; + + // Build new swatches + options.swatches.forEach(function (swatch, i) { + var button = document.createElement('button'); + + button.setAttribute('type', "button"); + button.setAttribute('id', "clr-swatch-" + i); + button.setAttribute('aria-labelledby', "clr-swatch-label clr-swatch-" + i); + button.style.color = swatch; + button.textContent = swatch; + + swatches.appendChild(button); + }); + + // Append new swatches if any + if (options.swatches.length) { + swatchesContainer.appendChild(swatches); + } + + settings.swatches = options.swatches.slice();})(); + } + break; + case 'swatchesOnly': + settings.swatchesOnly = !!options.swatchesOnly; + picker.setAttribute('data-minimal', settings.swatchesOnly); + break; + case 'alpha': + settings.alpha = !!options.alpha; + picker.setAttribute('data-alpha', settings.alpha); + break; + case 'inline': + settings.inline = !!options.inline; + picker.setAttribute('data-inline', settings.inline); + + if (settings.inline) { + var defaultColor = options.defaultColor || settings.defaultColor; + + currentFormat = getColorFormatFromStr(defaultColor); + updatePickerPosition(); + setColorFromStr(defaultColor); + } + break; + case 'clearButton': + // Backward compatibility + if (typeof options.clearButton === 'object') { + if (options.clearButton.label) { + settings.clearLabel = options.clearButton.label; + clearButton.innerHTML = settings.clearLabel; + } + + options.clearButton = options.clearButton.show; + } + + settings.clearButton = !!options.clearButton; + clearButton.style.display = settings.clearButton ? 'block' : 'none'; + break; + case 'clearLabel': + settings.clearLabel = options.clearLabel; + clearButton.innerHTML = settings.clearLabel; + break; + case 'closeButton': + settings.closeButton = !!options.closeButton; + + if (settings.closeButton) { + picker.insertBefore(closeButton, colorPreview); + } else { + colorPreview.appendChild(closeButton); + } + + break; + case 'closeLabel': + settings.closeLabel = options.closeLabel; + closeButton.innerHTML = settings.closeLabel; + break; + case 'a11y': + var labels = options.a11y; + var update = false; + + if (typeof labels === 'object') { + for (var label in labels) { + if (labels[label] && settings.a11y[label]) { + settings.a11y[label] = labels[label]; + update = true; + } + } + } + + if (update) { + var openLabel = getEl('clr-open-label'); + var swatchLabel = getEl('clr-swatch-label'); + + openLabel.innerHTML = settings.a11y.open; + swatchLabel.innerHTML = settings.a11y.swatch; + closeButton.setAttribute('aria-label', settings.a11y.close); + clearButton.setAttribute('aria-label', settings.a11y.clear); + hueSlider.setAttribute('aria-label', settings.a11y.hueSlider); + alphaSlider.setAttribute('aria-label', settings.a11y.alphaSlider); + colorValue.setAttribute('aria-label', settings.a11y.input); + colorArea.setAttribute('aria-label', settings.a11y.instruction); + } + break; + default: + settings[key] = options[key];} + + } + } + + /** + * Add or update a virtual instance. + * @param {String} selector The CSS selector of the elements to which the instance is attached. + * @param {Object} options Per-instance options to apply. + */ + function setVirtualInstance(selector, options) { + if (typeof selector === 'string' && typeof options === 'object') { + instances[selector] = options; + hasInstance = true; + } + } + + /** + * Remove a virtual instance. + * @param {String} selector The CSS selector of the elements to which the instance is attached. + */ + function removeVirtualInstance(selector) { + delete instances[selector]; + + if (Object.keys(instances).length === 0) { + hasInstance = false; + + if (selector === currentInstanceId) { + resetVirtualInstance(); + } + } + } + + /** + * Attach a virtual instance to an element if it matches a selector. + * @param {Object} element Target element that will receive a virtual instance if applicable. + */ + function attachVirtualInstance(element) { + if (hasInstance) { + // These options can only be set globally, not per instance + var unsupportedOptions = ['el', 'wrap', 'rtl', 'inline', 'defaultColor', 'a11y'];var _loop = function _loop( + + selector) { + var options = instances[selector]; + + // If the element matches an instance's CSS selector + if (element.matches(selector)) { + currentInstanceId = selector; + defaultInstance = {}; + + // Delete unsupported options + unsupportedOptions.forEach(function (option) {return delete options[option];}); + + // Back up the default options so we can restore them later + for (var option in options) { + defaultInstance[option] = Array.isArray(settings[option]) ? settings[option].slice() : settings[option]; + } + + // Set the instance's options + configure(options); + return "break"; + }};for (var selector in instances) {var _ret = _loop(selector);if (_ret === "break") break; + } + } + } + + /** + * Revert any per-instance options that were previously applied. + */ + function resetVirtualInstance() { + if (Object.keys(defaultInstance).length > 0) { + configure(defaultInstance); + currentInstanceId = ''; + defaultInstance = {}; + } + } + + /** + * Bind the color picker to input fields that match the selector. + * @param {(string|HTMLElement|HTMLElement[])} selector A CSS selector string, a DOM element or a list of DOM elements. + */ + function bindFields(selector) { + if (selector instanceof HTMLElement) { + selector = [selector]; + } + + if (Array.isArray(selector)) { + selector.forEach(function (field) { + addListener(field, 'click', openPicker); + addListener(field, 'input', updateColorPreview); + }); + } else { + addListener(document, 'click', selector, openPicker); + addListener(document, 'input', selector, updateColorPreview); + } + } + + /** + * Open the color picker. + * @param {object} event The event that opens the color picker. + */ + function openPicker(event) { + // Skip if inline mode is in use + if (settings.inline) { + return; + } + + // Apply any per-instance options first + attachVirtualInstance(event.target); + + currentEl = event.target; + oldColor = currentEl.value; + currentFormat = getColorFormatFromStr(oldColor); + picker.classList.add('clr-open'); + + updatePickerPosition(); + setColorFromStr(oldColor); + + if (settings.focusInput || settings.selectInput) { + colorValue.focus({ preventScroll: true }); + colorValue.setSelectionRange(currentEl.selectionStart, currentEl.selectionEnd); + } + + if (settings.selectInput) { + colorValue.select(); + } + + // Always focus the first element when using keyboard navigation + if (keyboardNav || settings.swatchesOnly) { + getFocusableElements().shift().focus(); + } + + // Trigger an "open" event + currentEl.dispatchEvent(new Event('open', { bubbles: true })); + } + + /** + * Update the color picker's position and the color gradient's offset + */ + function updatePickerPosition() { + var parent = container; + var scrollY = window.scrollY; + var pickerWidth = picker.offsetWidth; + var pickerHeight = picker.offsetHeight; + var reposition = { left: false, top: false }; + var parentStyle, parentMarginTop, parentBorderTop; + var offset = { x: 0, y: 0 }; + + if (parent) { + parentStyle = window.getComputedStyle(parent); + parentMarginTop = parseFloat(parentStyle.marginTop); + parentBorderTop = parseFloat(parentStyle.borderTopWidth); + + offset = parent.getBoundingClientRect(); + offset.y += parentBorderTop + scrollY; + } + + if (!settings.inline) { + var coords = currentEl.getBoundingClientRect(); + var left = coords.x; + var top = scrollY + coords.y + coords.height + settings.margin; + + // If the color picker is inside a custom container + // set the position relative to it + if (parent) { + left -= offset.x; + top -= offset.y; + + if (left + pickerWidth > parent.clientWidth) { + left += coords.width - pickerWidth; + reposition.left = true; + } + + if (top + pickerHeight > parent.clientHeight - parentMarginTop) { + if (pickerHeight + settings.margin <= coords.top - (offset.y - scrollY)) { + top -= coords.height + pickerHeight + settings.margin * 2; + reposition.top = true; + } + } + + top += parent.scrollTop; + + // Otherwise set the position relative to the whole document + } else { + if (left + pickerWidth > document.documentElement.clientWidth) { + left += coords.width - pickerWidth; + reposition.left = true; + } + + if (top + pickerHeight - scrollY > document.documentElement.clientHeight) { + if (pickerHeight + settings.margin <= coords.top) { + top = scrollY + coords.y - pickerHeight - settings.margin; + reposition.top = true; + } + } + } + + picker.classList.toggle('clr-left', reposition.left); + picker.classList.toggle('clr-top', reposition.top); + picker.style.left = left + "px"; + picker.style.top = top + "px"; + offset.x += picker.offsetLeft; + offset.y += picker.offsetTop; + } + + colorAreaDims = { + width: colorArea.offsetWidth, + height: colorArea.offsetHeight, + x: colorArea.offsetLeft + offset.x, + y: colorArea.offsetTop + offset.y }; + + } + + /** + * Wrap the linked input fields in a div that adds a color preview. + * @param {(string|HTMLElement|HTMLElement[])} selector A CSS selector string, a DOM element or a list of DOM elements. + */ + function wrapFields(selector) { + if (selector instanceof HTMLElement) { + wrapColorField(selector); + } else if (Array.isArray(selector)) { + selector.forEach(wrapColorField); + } else { + document.querySelectorAll(selector).forEach(wrapColorField); + } + } + + /** + * Wrap an input field in a div that adds a color preview. + * @param {object} field The input field. + */ + function wrapColorField(field) { + var parentNode = field.parentNode; + + if (!parentNode.classList.contains('clr-field')) { + var wrapper = document.createElement('div'); + var classes = 'clr-field'; + + if (settings.rtl || field.classList.contains('clr-rtl')) { + classes += ' clr-rtl'; + } + + wrapper.innerHTML = ''; + parentNode.insertBefore(wrapper, field); + wrapper.className = classes; + wrapper.style.color = field.value; + wrapper.appendChild(field); + } + } + + /** + * Update the color preview of an input field + * @param {object} event The "input" event that triggers the color change. + */ + function updateColorPreview(event) { + var parent = event.target.parentNode; + + // Only update the preview if the field has been previously wrapped + if (parent.classList.contains('clr-field')) { + parent.style.color = event.target.value; + } + } + + /** + * Close the color picker. + * @param {boolean} [revert] If true, revert the color to the original value. + */ + function closePicker(revert) { + if (currentEl && !settings.inline) { + var prevEl = currentEl; + + // Revert the color to the original value if needed + if (revert) { + // This will prevent the "change" event on the colorValue input to execute its handler + currentEl = undefined; + + if (oldColor !== prevEl.value) { + prevEl.value = oldColor; + + // Trigger an "input" event to force update the thumbnail next to the input field + prevEl.dispatchEvent(new Event('input', { bubbles: true })); + } + } + + // Trigger a "change" event if needed + setTimeout(function () {// Add this to the end of the event loop + if (oldColor !== prevEl.value) { + prevEl.dispatchEvent(new Event('change', { bubbles: true })); + } + }); + + // Hide the picker dialog + picker.classList.remove('clr-open'); + + // Reset any previously set per-instance options + if (hasInstance) { + resetVirtualInstance(); + } + + // Trigger a "close" event + prevEl.dispatchEvent(new Event('close', { bubbles: true })); + + if (settings.focusInput) { + prevEl.focus({ preventScroll: true }); + } + + // This essentially marks the picker as closed + currentEl = undefined; + } + } + + /** + * Set the active color from a string. + * @param {string} str String representing a color. + */ + function setColorFromStr(str) { + var rgba = strToRGBA(str); + var hsva = RGBAtoHSVA(rgba); + + updateMarkerA11yLabel(hsva.s, hsva.v); + updateColor(rgba, hsva); + + // Update the UI + hueSlider.value = hsva.h; + picker.style.color = "hsl(" + hsva.h + ", 100%, 50%)"; + hueMarker.style.left = hsva.h / 360 * 100 + "%"; + + colorMarker.style.left = colorAreaDims.width * hsva.s / 100 + "px"; + colorMarker.style.top = colorAreaDims.height - colorAreaDims.height * hsva.v / 100 + "px"; + + alphaSlider.value = hsva.a * 100; + alphaMarker.style.left = hsva.a * 100 + "%"; + } + + /** + * Guess the color format from a string. + * @param {string} str String representing a color. + * @return {string} The color format. + */ + function getColorFormatFromStr(str) { + var format = str.substring(0, 3).toLowerCase(); + + if (format === 'rgb' || format === 'hsl') { + return format; + } + + return 'hex'; + } + + /** + * Copy the active color to the linked input field. + * @param {number} [color] Color value to override the active color. + */ + function pickColor(color) { + color = color !== undefined ? color : colorValue.value; + + if (currentEl) { + currentEl.value = color; + currentEl.dispatchEvent(new Event('input', { bubbles: true })); + } + + if (settings.onChange) { + settings.onChange.call(window, color, currentEl); + } + + document.dispatchEvent(new CustomEvent('coloris:pick', { detail: { color: color, currentEl: currentEl } })); + } + + /** + * Set the active color based on a specific point in the color gradient. + * @param {number} x Left position. + * @param {number} y Top position. + */ + function setColorAtPosition(x, y) { + var hsva = { + h: hueSlider.value * 1, + s: x / colorAreaDims.width * 100, + v: 100 - y / colorAreaDims.height * 100, + a: alphaSlider.value / 100 }; + + var rgba = HSVAtoRGBA(hsva); + + updateMarkerA11yLabel(hsva.s, hsva.v); + updateColor(rgba, hsva); + pickColor(); + } + + /** + * Update the color marker's accessibility label. + * @param {number} saturation + * @param {number} value + */ + function updateMarkerA11yLabel(saturation, value) { + var label = settings.a11y.marker; + + saturation = saturation.toFixed(1) * 1; + value = value.toFixed(1) * 1; + label = label.replace('{s}', saturation); + label = label.replace('{v}', value); + colorMarker.setAttribute('aria-label', label); + } + + // + /** + * Get the pageX and pageY positions of the pointer. + * @param {object} event The MouseEvent or TouchEvent object. + * @return {object} The pageX and pageY positions. + */ + function getPointerPosition(event) { + return { + pageX: event.changedTouches ? event.changedTouches[0].pageX : event.pageX, + pageY: event.changedTouches ? event.changedTouches[0].pageY : event.pageY }; + + } + + /** + * Move the color marker when dragged. + * @param {object} event The MouseEvent object. + */ + function moveMarker(event) { + var pointer = getPointerPosition(event); + var x = pointer.pageX - colorAreaDims.x; + var y = pointer.pageY - colorAreaDims.y; + + if (container) { + y += container.scrollTop; + } + + setMarkerPosition(x, y); + + // Prevent scrolling while dragging the marker + event.preventDefault(); + event.stopPropagation(); + } + + /** + * Move the color marker when the arrow keys are pressed. + * @param {number} offsetX The horizontal amount to move. + * @param {number} offsetY The vertical amount to move. + */ + function moveMarkerOnKeydown(offsetX, offsetY) { + var x = colorMarker.style.left.replace('px', '') * 1 + offsetX; + var y = colorMarker.style.top.replace('px', '') * 1 + offsetY; + + setMarkerPosition(x, y); + } + + /** + * Set the color marker's position. + * @param {number} x Left position. + * @param {number} y Top position. + */ + function setMarkerPosition(x, y) { + // Make sure the marker doesn't go out of bounds + x = x < 0 ? 0 : x > colorAreaDims.width ? colorAreaDims.width : x; + y = y < 0 ? 0 : y > colorAreaDims.height ? colorAreaDims.height : y; + + // Set the position + colorMarker.style.left = x + "px"; + colorMarker.style.top = y + "px"; + + // Update the color + setColorAtPosition(x, y); + + // Make sure the marker is focused + colorMarker.focus(); + } + + /** + * Update the color picker's input field and preview thumb. + * @param {Object} rgba Red, green, blue and alpha values. + * @param {Object} [hsva] Hue, saturation, value and alpha values. + */ + function updateColor(rgba, hsva) {if (rgba === void 0) {rgba = {};}if (hsva === void 0) {hsva = {};} + var format = settings.format; + + for (var key in rgba) { + currentColor[key] = rgba[key]; + } + + for (var _key in hsva) { + currentColor[_key] = hsva[_key]; + } + + var hex = RGBAToHex(currentColor); + var opaqueHex = hex.substring(0, 7); + + colorMarker.style.color = opaqueHex; + alphaMarker.parentNode.style.color = opaqueHex; + alphaMarker.style.color = hex; + colorPreview.style.color = hex; + + // Force repaint the color and alpha gradients as a workaround for a Google Chrome bug + colorArea.style.display = 'none'; + colorArea.offsetHeight; + colorArea.style.display = ''; + alphaMarker.nextElementSibling.style.display = 'none'; + alphaMarker.nextElementSibling.offsetHeight; + alphaMarker.nextElementSibling.style.display = ''; + + if (format === 'mixed') { + format = currentColor.a === 1 ? 'hex' : 'rgb'; + } else if (format === 'auto') { + format = currentFormat; + } + + switch (format) { + case 'hex': + colorValue.value = hex; + break; + case 'rgb': + colorValue.value = RGBAToStr(currentColor); + break; + case 'hsl': + colorValue.value = HSLAToStr(HSVAtoHSLA(currentColor)); + break;} + + + // Select the current format in the format switcher + document.querySelector(".clr-format [value=\"" + format + "\"]").checked = true; + } + + /** + * Set the hue when its slider is moved. + */ + function setHue() { + var hue = hueSlider.value * 1; + var x = colorMarker.style.left.replace('px', '') * 1; + var y = colorMarker.style.top.replace('px', '') * 1; + + picker.style.color = "hsl(" + hue + ", 100%, 50%)"; + hueMarker.style.left = hue / 360 * 100 + "%"; + + setColorAtPosition(x, y); + } + + /** + * Set the alpha when its slider is moved. + */ + function setAlpha() { + var alpha = alphaSlider.value / 100; + + alphaMarker.style.left = alpha * 100 + "%"; + updateColor({ a: alpha }); + pickColor(); + } + + /** + * Convert HSVA to RGBA. + * @param {object} hsva Hue, saturation, value and alpha values. + * @return {object} Red, green, blue and alpha values. + */ + function HSVAtoRGBA(hsva) { + var saturation = hsva.s / 100; + var value = hsva.v / 100; + var chroma = saturation * value; + var hueBy60 = hsva.h / 60; + var x = chroma * (1 - Math.abs(hueBy60 % 2 - 1)); + var m = value - chroma; + + chroma = chroma + m; + x = x + m; + + var index = Math.floor(hueBy60) % 6; + var red = [chroma, x, m, m, x, chroma][index]; + var green = [x, chroma, chroma, x, m, m][index]; + var blue = [m, m, x, chroma, chroma, x][index]; + + return { + r: Math.round(red * 255), + g: Math.round(green * 255), + b: Math.round(blue * 255), + a: hsva.a }; + + } + + /** + * Convert HSVA to HSLA. + * @param {object} hsva Hue, saturation, value and alpha values. + * @return {object} Hue, saturation, lightness and alpha values. + */ + function HSVAtoHSLA(hsva) { + var value = hsva.v / 100; + var lightness = value * (1 - hsva.s / 100 / 2); + var saturation; + + if (lightness > 0 && lightness < 1) { + saturation = Math.round((value - lightness) / Math.min(lightness, 1 - lightness) * 100); + } + + return { + h: hsva.h, + s: saturation || 0, + l: Math.round(lightness * 100), + a: hsva.a }; + + } + + /** + * Convert RGBA to HSVA. + * @param {object} rgba Red, green, blue and alpha values. + * @return {object} Hue, saturation, value and alpha values. + */ + function RGBAtoHSVA(rgba) { + var red = rgba.r / 255; + var green = rgba.g / 255; + var blue = rgba.b / 255; + var xmax = Math.max(red, green, blue); + var xmin = Math.min(red, green, blue); + var chroma = xmax - xmin; + var value = xmax; + var hue = 0; + var saturation = 0; + + if (chroma) { + if (xmax === red) {hue = (green - blue) / chroma;} + if (xmax === green) {hue = 2 + (blue - red) / chroma;} + if (xmax === blue) {hue = 4 + (red - green) / chroma;} + if (xmax) {saturation = chroma / xmax;} + } + + hue = Math.floor(hue * 60); + + return { + h: hue < 0 ? hue + 360 : hue, + s: Math.round(saturation * 100), + v: Math.round(value * 100), + a: rgba.a }; + + } + + /** + * Parse a string to RGBA. + * @param {string} str String representing a color. + * @return {object} Red, green, blue and alpha values. + */ + function strToRGBA(str) { + var regex = /^((rgba)|rgb)[\D]+([\d.]+)[\D]+([\d.]+)[\D]+([\d.]+)[\D]*?([\d.]+|$)/i; + var match, rgba; + + // Default to black for invalid color strings + ctx.fillStyle = '#000'; + + // Use canvas to convert the string to a valid color string + ctx.fillStyle = str; + match = regex.exec(ctx.fillStyle); + + if (match) { + rgba = { + r: match[3] * 1, + g: match[4] * 1, + b: match[5] * 1, + a: match[6] * 1 }; + + + } else { + match = ctx.fillStyle.replace('#', '').match(/.{2}/g).map(function (h) {return parseInt(h, 16);}); + rgba = { + r: match[0], + g: match[1], + b: match[2], + a: 1 }; + + } + + return rgba; + } + + /** + * Convert RGBA to Hex. + * @param {object} rgba Red, green, blue and alpha values. + * @return {string} Hex color string. + */ + function RGBAToHex(rgba) { + var R = rgba.r.toString(16); + var G = rgba.g.toString(16); + var B = rgba.b.toString(16); + var A = ''; + + if (rgba.r < 16) { + R = '0' + R; + } + + if (rgba.g < 16) { + G = '0' + G; + } + + if (rgba.b < 16) { + B = '0' + B; + } + + if (settings.alpha && (rgba.a < 1 || settings.forceAlpha)) { + var alpha = rgba.a * 255 | 0; + A = alpha.toString(16); + + if (alpha < 16) { + A = '0' + A; + } + } + + return '#' + R + G + B + A; + } + + /** + * Convert RGBA values to a CSS rgb/rgba string. + * @param {object} rgba Red, green, blue and alpha values. + * @return {string} CSS color string. + */ + function RGBAToStr(rgba) { + if (!settings.alpha || rgba.a === 1 && !settings.forceAlpha) { + return "rgb(" + rgba.r + ", " + rgba.g + ", " + rgba.b + ")"; + } else { + return "rgba(" + rgba.r + ", " + rgba.g + ", " + rgba.b + ", " + rgba.a + ")"; + } + } + + /** + * Convert HSLA values to a CSS hsl/hsla string. + * @param {object} hsla Hue, saturation, lightness and alpha values. + * @return {string} CSS color string. + */ + function HSLAToStr(hsla) { + if (!settings.alpha || hsla.a === 1 && !settings.forceAlpha) { + return "hsl(" + hsla.h + ", " + hsla.s + "%, " + hsla.l + "%)"; + } else { + return "hsla(" + hsla.h + ", " + hsla.s + "%, " + hsla.l + "%, " + hsla.a + ")"; + } + } + + /** + * Init the color picker. + */ + function init() { + // Render the UI + container = undefined; + picker = document.createElement('div'); + picker.setAttribute('id', 'clr-picker'); + picker.className = 'clr-picker'; + picker.innerHTML = + "" + ("
") + + '
' + + '
' + + '
' + ("") + + '
' + + '
' + + '
' + ("") + + '
' + + '' + + '
' + + '
' + + '
' + ("" + + settings.a11y.format + "") + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '
' + + '
' + + '
' + ("") + + '
' + ("") + + '
' + ("") + (""); + + // Append the color picker to the DOM + document.body.appendChild(picker); + + // Reference the UI elements + colorArea = getEl('clr-color-area'); + colorMarker = getEl('clr-color-marker'); + clearButton = getEl('clr-clear'); + closeButton = getEl('clr-close'); + colorPreview = getEl('clr-color-preview'); + colorValue = getEl('clr-color-value'); + hueSlider = getEl('clr-hue-slider'); + hueMarker = getEl('clr-hue-marker'); + alphaSlider = getEl('clr-alpha-slider'); + alphaMarker = getEl('clr-alpha-marker'); + + // Bind the picker to the default selector + bindFields(settings.el); + wrapFields(settings.el); + + addListener(picker, 'mousedown', function (event) { + picker.classList.remove('clr-keyboard-nav'); + event.stopPropagation(); + }); + + addListener(colorArea, 'mousedown', function (event) { + addListener(document, 'mousemove', moveMarker); + }); + + addListener(colorArea, 'contextmenu', function (event) { + event.preventDefault(); + }); + + addListener(colorArea, 'touchstart', function (event) { + document.addEventListener('touchmove', moveMarker, { passive: false }); + }); + + addListener(colorMarker, 'mousedown', function (event) { + addListener(document, 'mousemove', moveMarker); + }); + + addListener(colorMarker, 'touchstart', function (event) { + document.addEventListener('touchmove', moveMarker, { passive: false }); + }); + + addListener(colorValue, 'change', function (event) { + var value = colorValue.value; + + if (currentEl || settings.inline) { + var color = value === '' ? value : setColorFromStr(value); + pickColor(color); + } + }); + + addListener(clearButton, 'click', function (event) { + pickColor(''); + closePicker(); + }); + + addListener(closeButton, 'click', function (event) { + pickColor(); + closePicker(); + }); + + addListener(getEl('clr-format'), 'click', '.clr-format input', function (event) { + currentFormat = event.target.value; + updateColor(); + pickColor(); + }); + + addListener(picker, 'click', '.clr-swatches button', function (event) { + setColorFromStr(event.target.textContent); + pickColor(); + + if (settings.swatchesOnly) { + closePicker(); + } + }); + + addListener(document, 'mouseup', function (event) { + document.removeEventListener('mousemove', moveMarker); + }); + + addListener(document, 'touchend', function (event) { + document.removeEventListener('touchmove', moveMarker); + }); + + addListener(document, 'mousedown', function (event) { + keyboardNav = false; + picker.classList.remove('clr-keyboard-nav'); + closePicker(); + }); + + addListener(document, 'keydown', function (event) { + var key = event.key; + var target = event.target; + var shiftKey = event.shiftKey; + var navKeys = ['Tab', 'ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight']; + + if (key === 'Escape') { + closePicker(true); + + // Display focus rings when using the keyboard + } else if (navKeys.includes(key)) { + keyboardNav = true; + picker.classList.add('clr-keyboard-nav'); + } + + // Trap the focus within the color picker while it's open + if (key === 'Tab' && target.matches('.clr-picker *')) { + var focusables = getFocusableElements(); + var firstFocusable = focusables.shift(); + var lastFocusable = focusables.pop(); + + if (shiftKey && target === firstFocusable) { + lastFocusable.focus(); + event.preventDefault(); + } else if (!shiftKey && target === lastFocusable) { + firstFocusable.focus(); + event.preventDefault(); + } + } + }); + + addListener(document, 'click', '.clr-field button', function (event) { + // Reset any previously set per-instance options + if (hasInstance) { + resetVirtualInstance(); + } + + // Open the color picker + event.target.nextElementSibling.dispatchEvent(new Event('click', { bubbles: true })); + }); + + addListener(colorMarker, 'keydown', function (event) { + var movements = { + ArrowUp: [0, -1], + ArrowDown: [0, 1], + ArrowLeft: [-1, 0], + ArrowRight: [1, 0] }; + + + if (Object.keys(movements).includes(event.key)) { + moveMarkerOnKeydown.apply(void 0, movements[event.key]); + event.preventDefault(); + } + }); + + addListener(colorArea, 'click', moveMarker); + addListener(hueSlider, 'input', setHue); + addListener(alphaSlider, 'input', setAlpha); + } + + /** + * Return a list of focusable elements within the color picker. + * @return {array} The list of focusable DOM elemnts. + */ + function getFocusableElements() { + var controls = Array.from(picker.querySelectorAll('input, button')); + var focusables = controls.filter(function (node) {return !!node.offsetWidth;}); + + return focusables; + } + + /** + * Shortcut for getElementById to optimize the minified JS. + * @param {string} id The element id. + * @return {object} The DOM element with the provided id. + */ + function getEl(id) { + return document.getElementById(id); + } + + /** + * Shortcut for addEventListener to optimize the minified JS. + * @param {object} context The context to which the listener is attached. + * @param {string} type Event type. + * @param {(string|function)} selector Event target if delegation is used, event handler if not. + * @param {function} [fn] Event handler if delegation is used. + */ + function addListener(context, type, selector, fn) { + var matches = Element.prototype.matches || Element.prototype.msMatchesSelector; + + // Delegate event to the target of the selector + if (typeof selector === 'string') { + context.addEventListener(type, function (event) { + if (matches.call(event.target, selector)) { + fn.call(event.target, event); + } + }); + + // If the selector is not a string then it's a function + // in which case we need a regular event listener + } else { + fn = selector; + context.addEventListener(type, fn); + } + } + + /** + * Call a function only when the DOM is ready. + * @param {function} fn The function to call. + * @param {array} [args] Arguments to pass to the function. + */ + function DOMReady(fn, args) { + args = args !== undefined ? args : []; + + if (document.readyState !== 'loading') { + fn.apply(void 0, args); + } else { + document.addEventListener('DOMContentLoaded', function () { + fn.apply(void 0, args); + }); + } + } + + // Polyfill for Nodelist.forEach + if (NodeList !== undefined && NodeList.prototype && !NodeList.prototype.forEach) { + NodeList.prototype.forEach = Array.prototype.forEach; + } + + // Expose the color picker to the global scope + window.Coloris = function () { + var methods = { + set: configure, + wrap: wrapFields, + close: closePicker, + setInstance: setVirtualInstance, + removeInstance: removeVirtualInstance, + updatePosition: updatePickerPosition, + ready: DOMReady }; + + + function Coloris(options) { + DOMReady(function () { + if (options) { + if (typeof options === 'string') { + bindFields(options); + } else { + configure(options); + } + } + }); + }var _loop2 = function _loop2( + + key) { + Coloris[key] = function () {for (var _len = arguments.length, args = new Array(_len), _key2 = 0; _key2 < _len; _key2++) {args[_key2] = arguments[_key2];} + DOMReady(methods[key], args); + };};for (var key in methods) {_loop2(key); + } + + return Coloris; + }(); + + // Init the color picker when the DOM is ready + DOMReady(init); + +})(window, document, Math); \ No newline at end of file diff --git a/src/index.html b/src/index.html index 10c6e7f..30f3b57 100644 --- a/src/index.html +++ b/src/index.html @@ -3,6 +3,8 @@ + + Tauri App diff --git a/src/main.js b/src/main.js index 5c26fa9..5b4a295 100644 --- a/src/main.js +++ b/src/main.js @@ -8,25 +8,43 @@ let canvases = []; let mode = "draw" +let minSegmentSize = 5; + let tools = { select: { - icon: "/assets/select.png", + icon: "/assets/select.svg", + + }, + transform: { + icon: "/assets/transform.svg", }, draw: { - icon: "/assets/pen.png" + icon: "/assets/draw.svg" }, rectangle: { - icon: "/assets/rectangle.png" + icon: "/assets/rectangle.svg" }, polygon: { - icon: "assets/polygon.png" + icon: "assets/polygon.svg" } } let mouseEvent; -let context = {} +let context = { + mouseDown: false, + swatches: [ + "#000000", + "#FFFFFF", + "#FF0000", + "#FFFF00", + "#00FF00", + "#00FFFF", + "#0000FF", + "#FF00FF", + ] +} function uuidv4() { return "10000000-1000-4000-8000-100000000000".replace(/[018]/g, c => @@ -34,6 +52,10 @@ function uuidv4() { ); } +function vectorDist(a, b) { + return Math.sqrt((a.x-b.x)*(a.x-b.x) + (a.y-b.y)*(a.y-b.y)) +} + function getMousePos(canvas, evt) { var rect = canvas.getBoundingClientRect(); return { @@ -69,6 +91,9 @@ class Shape { this.filled = filled; this.stroked = stroked; } + addCurve(curve) { + this.curves.push(curve) + } } class GraphicsObject { @@ -121,8 +146,9 @@ class GraphicsObject { let root = new GraphicsObject(); let shp = new Shape(100,100,'blue', 'black') -shp.curves.push(new Curve(150,150,150,150,200,100)) +shp.addCurve(new Curve(150,150,150,150,200,100)) root.addShape(shp) +context.activeObject = root async function greet() { // Learn more about Tauri commands at https://tauri.app/develop/calling-rust/ @@ -155,6 +181,60 @@ function stage() { scroller.className = "scroll" canvases.push(stage) scroller.appendChild(stage) + stage.addEventListener("mousedown", (e) => { + let mouse = getMousePos(stage, e) + switch (mode) { + case "draw": + context.mouseDown = true + context.activeShape = new Shape(mouse.x, mouse.y, context.fillStyle, context.strokeStyle, true, true) + context.activeObject.addShape(context.activeShape) + context.lastMouse = mouse + console.log(stage) + break; + + default: + break; + } + context.lastMouse = mouse + updateUI() + }) + stage.addEventListener("mouseup", (e) => { + context.mouseDown = false + let mouse = getMousePos(stage, e) + switch (mode) { + case "draw": + if (context.activeShape) { + if (vectorDist(mouse, context.lastMouse) > minSegmentSize) { + let midpoint = {x: (mouse.x+context.lastMouse.x)/2, y: (mouse.y+context.lastMouse.y)/2} + context.activeShape.addCurve(new Curve(midpoint.x, midpoint.y, midpoint.x, midpoint.y, mouse.x, mouse.y)) + } + context.activeShape = undefined + } + break; + + default: + break; + } + context.lastMouse = mouse + updateUI() + }) + stage.addEventListener("mousemove", (e) => { + let mouse = getMousePos(stage, e) + switch (mode) { + case "draw": + if (context.activeShape) { + if (vectorDist(mouse, context.lastMouse) > minSegmentSize) { + let midpoint = {x: (mouse.x+context.lastMouse.x)/2, y: (mouse.y+context.lastMouse.y)/2} + context.activeShape.addCurve(new Curve(midpoint.x, midpoint.y, midpoint.x, midpoint.y, mouse.x, mouse.y)) + } + } + break; + default: + break; + } + context.lastMouse = mouse + updateUI() + }) return scroller } @@ -170,6 +250,52 @@ function toolbar() { toolbtn.appendChild(icon) tools_scroller.appendChild(toolbtn) } + let tools_break = document.createElement("div") + tools_break.className = "horiz_break" + tools_scroller.appendChild(tools_break) + let fillColor = document.createElement("input") + let strokeColor = document.createElement("input") + fillColor.className = "color-field" + strokeColor.className = "color-field" + fillColor.value = "#ffffff" + strokeColor.value = "#000000" + fillColor.addEventListener('click', e => { + Coloris({ + el: ".color-field", + selectInput: true, + focusInput: true, + theme: 'default', + swatches: context.swatches, + defaultColor: '#ffffff', + onChange: (color) => { + context.fillStyle = color; + } + }) + }) + strokeColor.addEventListener('click', e => { + Coloris({ + el: ".color-field", + selectInput: true, + focusInput: true, + theme: 'default', + swatches: context.swatches, + defaultColor: '#000000', + onChange: (color) => { + context.strokeStyle = color; + } + }) + }) + // Fill and stroke colors use the same set of swatches + fillColor.addEventListener("change", e => { + context.swatches.unshift(fillColor.value) + if (context.swatches.length>12) context.swatches.pop(); + }) + strokeColor.addEventListener("change", e => { + context.swatches.unshift(strokeColor.value) + if (context.swatches.length>12) context.swatches.pop(); + }) + tools_scroller.appendChild(fillColor) + tools_scroller.appendChild(strokeColor) return tools_scroller } @@ -249,13 +375,13 @@ function updateUI() { context.ctx = ctx; root.draw(context) - let mouse; - if (mouseEvent) { - mouse = getMousePos(canvas, mouseEvent); - } else { - mouse = {x: 0, y: 0} - } - ctx.fillRect(mouse.x, mouse.y, 50,50) + // let mouse; + // if (mouseEvent) { + // mouse = getMousePos(canvas, mouseEvent); + // } else { + // mouse = {x: 0, y: 0} + // } + // ctx.fillRect(mouse.x, mouse.y, 50,50) } - requestAnimationFrame(updateUI) + // requestAnimationFrame(updateUI) } \ No newline at end of file diff --git a/src/styles.css b/src/styles.css index 7c180f3..b46af30 100644 --- a/src/styles.css +++ b/src/styles.css @@ -190,5 +190,12 @@ button { .toolbtn { /* width: var(--lineheight); height: var(--lineheight); */ + background-color: #2f2f2f; +} + +.horiz_break { + width: 100%; + height: 5px; + background-color: #2f2f2f; } \ No newline at end of file From 11f35466e93f6989697afa7c422b436ef49bdab4 Mon Sep 17 00:00:00 2001 From: Skyler Lehmkuhl Date: Fri, 15 Nov 2024 01:45:50 -0500 Subject: [PATCH 03/46] show colors on page load --- src/main.js | 1 + src/styles.css | 7 ++++++- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/src/main.js b/src/main.js index 5b4a295..e9654bb 100644 --- a/src/main.js +++ b/src/main.js @@ -359,6 +359,7 @@ function splitPane(div, percent, horiz) { } div1.style.flex = `0 0 ${percent}%` div2.style.flex = `1 1 auto` + Coloris({el: ".color-field"}) updateUI() } diff --git a/src/styles.css b/src/styles.css index b46af30..81006b8 100644 --- a/src/styles.css +++ b/src/styles.css @@ -198,4 +198,9 @@ button { height: 5px; background-color: #2f2f2f; -} \ No newline at end of file +} +.clr-field button { + width: 100% !important; + height: 100% !important; + border-radius: 5px; +} From 455e401794e39a385f13945fbc91ea49fdcf5052 Mon Sep 17 00:00:00 2001 From: Skyler Lehmkuhl Date: Fri, 15 Nov 2024 03:05:52 -0500 Subject: [PATCH 04/46] simplify curves --- src/fit-curve.js | 606 +++++++++++++++++++++++++++++++++++++++++++++++ src/index.html | 1 + src/main.js | 68 +++++- 3 files changed, 666 insertions(+), 9 deletions(-) create mode 100644 src/fit-curve.js diff --git a/src/fit-curve.js b/src/fit-curve.js new file mode 100644 index 0000000..d0aa229 --- /dev/null +++ b/src/fit-curve.js @@ -0,0 +1,606 @@ + + +/** + * @preserve JavaScript implementation of + * Algorithm for Automatically Fitting Digitized Curves + * by Philip J. Schneider + * "Graphics Gems", Academic Press, 1990 + * + * The MIT License (MIT) + * + * https://github.com/soswow/fit-curves + */ + +/** + * Fit one or more Bezier curves to a set of points. + * + * @param {Array>} points - Array of digitized points, e.g. [[5,5],[5,50],[110,140],[210,160],[320,110]] + * @param {Number} maxError - Tolerance, squared error between points and fitted curve + * @returns {Array>>} Array of Bezier curves, where each element is [first-point, control-point-1, control-point-2, second-point] and points are [x, y] + */ +export function fitCurve(points, maxError, progressCallback) { + if (!Array.isArray(points)) { + throw new TypeError("First argument should be an array"); + } + points.forEach(function (point) { + if (!Array.isArray(point) || point.some(function (item) { + return typeof item !== 'number'; + }) || point.length !== points[0].length) { + throw Error("Each point should be an array of numbers. Each point should have the same amount of numbers."); + } + }); + + // Remove duplicate points + points = points.filter(function (point, i) { + return i === 0 || !point.every(function (val, j) { + return val === points[i - 1][j]; + }); + }); + + if (points.length < 2) { + return []; + } + + var len = points.length; + var leftTangent = createTangent(points[1], points[0]); + var rightTangent = createTangent(points[len - 2], points[len - 1]); + + return fitCubic(points, leftTangent, rightTangent, maxError, progressCallback); +} + +/** + * Fit a Bezier curve to a (sub)set of digitized points. + * Your code should not call this function directly. Use {@link fitCurve} instead. + * + * @param {Array>} points - Array of digitized points, e.g. [[5,5],[5,50],[110,140],[210,160],[320,110]] + * @param {Array} leftTangent - Unit tangent vector at start point + * @param {Array} rightTangent - Unit tangent vector at end point + * @param {Number} error - Tolerance, squared error between points and fitted curve + * @returns {Array>>} Array of Bezier curves, where each element is [first-point, control-point-1, control-point-2, second-point] and points are [x, y] + */ +function fitCubic(points, leftTangent, rightTangent, error, progressCallback) { + var MaxIterations = 20; //Max times to try iterating (to find an acceptable curve) + + var bezCurve, //Control points of fitted Bezier curve + u, //Parameter values for point + uPrime, //Improved parameter values + maxError, prevErr, //Maximum fitting error + splitPoint, prevSplit, //Point to split point set at if we need more than one curve + centerVector, toCenterTangent, fromCenterTangent, //Unit tangent vector(s) at splitPoint + beziers, //Array of fitted Bezier curves if we need more than one curve + dist, i; + + //console.log('fitCubic, ', points.length); + + //Use heuristic if region only has two points in it + if (points.length === 2) { + dist = maths.vectorLen(maths.subtract(points[0], points[1])) / 3.0; + bezCurve = [points[0], maths.addArrays(points[0], maths.mulItems(leftTangent, dist)), maths.addArrays(points[1], maths.mulItems(rightTangent, dist)), points[1]]; + return [bezCurve]; + } + + //Parameterize points, and attempt to fit curve + u = chordLengthParameterize(points); + + var _generateAndReport = generateAndReport(points, u, u, leftTangent, rightTangent, progressCallback); + + bezCurve = _generateAndReport[0]; + maxError = _generateAndReport[1]; + splitPoint = _generateAndReport[2]; + + + if (maxError === 0 || maxError < error) { + return [bezCurve]; + } + //If error not too large, try some reparameterization and iteration + if (maxError < error * error) { + + uPrime = u; + prevErr = maxError; + prevSplit = splitPoint; + + for (i = 0; i < MaxIterations; i++) { + + uPrime = reparameterize(bezCurve, points, uPrime); + + var _generateAndReport2 = generateAndReport(points, u, uPrime, leftTangent, rightTangent, progressCallback); + + bezCurve = _generateAndReport2[0]; + maxError = _generateAndReport2[1]; + splitPoint = _generateAndReport2[2]; + + + if (maxError < error) { + return [bezCurve]; + } + //If the development of the fitted curve grinds to a halt, + //we abort this attempt (and try a shorter curve): + else if (splitPoint === prevSplit) { + var errChange = maxError / prevErr; + if (errChange > .9999 && errChange < 1.0001) { + break; + } + } + + prevErr = maxError; + prevSplit = splitPoint; + } + } + + //Fitting failed -- split at max error point and fit recursively + beziers = []; + + //To create a smooth transition from one curve segment to the next, we + //calculate the line between the points directly before and after the + //center, and use that as the tangent both to and from the center point. + centerVector = maths.subtract(points[splitPoint - 1], points[splitPoint + 1]); + //However, this won't work if they're the same point, because the line we + //want to use as a tangent would be 0. Instead, we calculate the line from + //that "double-point" to the center point, and use its tangent. + if (centerVector.every(function (val) { + return val === 0; + })) { + //[x,y] -> [-y,x]: http://stackoverflow.com/a/4780141/1869660 + centerVector = maths.subtract(points[splitPoint - 1], points[splitPoint]); + var _ref = [-centerVector[1], centerVector[0]]; + centerVector[0] = _ref[0]; + centerVector[1] = _ref[1]; + } + toCenterTangent = maths.normalize(centerVector); + //To and from need to point in opposite directions: + fromCenterTangent = maths.mulItems(toCenterTangent, -1); + + /* + Note: An alternative to this "divide and conquer" recursion could be to always + let new curve segments start by trying to go all the way to the end, + instead of only to the end of the current subdivided polyline. + That might let many segments fit a few points more, reducing the number of total segments. + However, a few tests have shown that the segment reduction is insignificant + (240 pts, 100 err: 25 curves vs 27 curves. 140 pts, 100 err: 17 curves on both), + and the results take twice as many steps and milliseconds to finish, + without looking any better than what we already have. + */ + beziers = beziers.concat(fitCubic(points.slice(0, splitPoint + 1), leftTangent, toCenterTangent, error, progressCallback)); + beziers = beziers.concat(fitCubic(points.slice(splitPoint), fromCenterTangent, rightTangent, error, progressCallback)); + return beziers; +}; + +function generateAndReport(points, paramsOrig, paramsPrime, leftTangent, rightTangent, progressCallback) { + var bezCurve, maxError, splitPoint; + + bezCurve = generateBezier(points, paramsPrime, leftTangent, rightTangent, progressCallback); + //Find max deviation of points to fitted curve. + //Here we always use the original parameters (from chordLengthParameterize()), + //because we need to compare the current curve to the actual source polyline, + //and not the currently iterated parameters which reparameterize() & generateBezier() use, + //as those have probably drifted far away and may no longer be in ascending order. + + var _computeMaxError = computeMaxError(points, bezCurve, paramsOrig); + + maxError = _computeMaxError[0]; + splitPoint = _computeMaxError[1]; + + + if (progressCallback) { + progressCallback({ + bez: bezCurve, + points: points, + params: paramsOrig, + maxErr: maxError, + maxPoint: splitPoint + }); + } + + return [bezCurve, maxError, splitPoint]; +} + +/** + * Use least-squares method to find Bezier control points for region. + * + * @param {Array>} points - Array of digitized points + * @param {Array} parameters - Parameter values for region + * @param {Array} leftTangent - Unit tangent vector at start point + * @param {Array} rightTangent - Unit tangent vector at end point + * @returns {Array>} Approximated Bezier curve: [first-point, control-point-1, control-point-2, second-point] where points are [x, y] + */ +function generateBezier(points, parameters, leftTangent, rightTangent) { + var bezCurve, + //Bezier curve ctl pts + A, + a, + //Precomputed rhs for eqn + C, + X, + //Matrices C & X + det_C0_C1, + det_C0_X, + det_X_C1, + //Determinants of matrices + alpha_l, + alpha_r, + //Alpha values, left and right + + epsilon, + segLength, + i, + len, + tmp, + u, + ux, + firstPoint = points[0], + lastPoint = points[points.length - 1]; + + bezCurve = [firstPoint, null, null, lastPoint]; + //console.log('gb', parameters.length); + + //Compute the A's + A = maths.zeros_Xx2x2(parameters.length); + for (i = 0, len = parameters.length; i < len; i++) { + u = parameters[i]; + ux = 1 - u; + a = A[i]; + + a[0] = maths.mulItems(leftTangent, 3 * u * (ux * ux)); + a[1] = maths.mulItems(rightTangent, 3 * ux * (u * u)); + } + + //Create the C and X matrices + C = [[0, 0], [0, 0]]; + X = [0, 0]; + for (i = 0, len = points.length; i < len; i++) { + u = parameters[i]; + a = A[i]; + + C[0][0] += maths.dot(a[0], a[0]); + C[0][1] += maths.dot(a[0], a[1]); + C[1][0] += maths.dot(a[0], a[1]); + C[1][1] += maths.dot(a[1], a[1]); + + tmp = maths.subtract(points[i], bezier.q([firstPoint, firstPoint, lastPoint, lastPoint], u)); + + X[0] += maths.dot(a[0], tmp); + X[1] += maths.dot(a[1], tmp); + } + + //Compute the determinants of C and X + det_C0_C1 = C[0][0] * C[1][1] - C[1][0] * C[0][1]; + det_C0_X = C[0][0] * X[1] - C[1][0] * X[0]; + det_X_C1 = X[0] * C[1][1] - X[1] * C[0][1]; + + //Finally, derive alpha values + alpha_l = det_C0_C1 === 0 ? 0 : det_X_C1 / det_C0_C1; + alpha_r = det_C0_C1 === 0 ? 0 : det_C0_X / det_C0_C1; + + //If alpha negative, use the Wu/Barsky heuristic (see text). + //If alpha is 0, you get coincident control points that lead to + //divide by zero in any subsequent NewtonRaphsonRootFind() call. + segLength = maths.vectorLen(maths.subtract(firstPoint, lastPoint)); + epsilon = 1.0e-6 * segLength; + if (alpha_l < epsilon || alpha_r < epsilon) { + //Fall back on standard (probably inaccurate) formula, and subdivide further if needed. + bezCurve[1] = maths.addArrays(firstPoint, maths.mulItems(leftTangent, segLength / 3.0)); + bezCurve[2] = maths.addArrays(lastPoint, maths.mulItems(rightTangent, segLength / 3.0)); + } else { + //First and last control points of the Bezier curve are + //positioned exactly at the first and last data points + //Control points 1 and 2 are positioned an alpha distance out + //on the tangent vectors, left and right, respectively + bezCurve[1] = maths.addArrays(firstPoint, maths.mulItems(leftTangent, alpha_l)); + bezCurve[2] = maths.addArrays(lastPoint, maths.mulItems(rightTangent, alpha_r)); + } + + return bezCurve; +}; + +/** + * Given set of points and their parameterization, try to find a better parameterization. + * + * @param {Array>} bezier - Current fitted curve + * @param {Array>} points - Array of digitized points + * @param {Array} parameters - Current parameter values + * @returns {Array} New parameter values + */ +function reparameterize(bezier, points, parameters) { + /* + var j, len, point, results, u; + results = []; + for (j = 0, len = points.length; j < len; j++) { + point = points[j], u = parameters[j]; + results.push(newtonRaphsonRootFind(bezier, point, u)); + } + return results; + //*/ + return parameters.map(function (p, i) { + return newtonRaphsonRootFind(bezier, points[i], p); + }); +}; + +/** + * Use Newton-Raphson iteration to find better root. + * + * @param {Array>} bez - Current fitted curve + * @param {Array} point - Digitized point + * @param {Number} u - Parameter value for "P" + * @returns {Number} New u + */ +function newtonRaphsonRootFind(bez, point, u) { + /* + Newton's root finding algorithm calculates f(x)=0 by reiterating + x_n+1 = x_n - f(x_n)/f'(x_n) + We are trying to find curve parameter u for some point p that minimizes + the distance from that point to the curve. Distance point to curve is d=q(u)-p. + At minimum distance the point is perpendicular to the curve. + We are solving + f = q(u)-p * q'(u) = 0 + with + f' = q'(u) * q'(u) + q(u)-p * q''(u) + gives + u_n+1 = u_n - |q(u_n)-p * q'(u_n)| / |q'(u_n)**2 + q(u_n)-p * q''(u_n)| + */ + + var d = maths.subtract(bezier.q(bez, u), point), + qprime = bezier.qprime(bez, u), + numerator = maths.mulMatrix(d, qprime), + denominator = maths.sum(maths.squareItems(qprime)) + 2 * maths.mulMatrix(d, bezier.qprimeprime(bez, u)); + + if (denominator === 0) { + return u; + } else { + return u - numerator / denominator; + } +}; + +/** + * Assign parameter values to digitized points using relative distances between points. + * + * @param {Array>} points - Array of digitized points + * @returns {Array} Parameter values + */ +function chordLengthParameterize(points) { + var u = [], + currU, + prevU, + prevP; + + points.forEach(function (p, i) { + currU = i ? prevU + maths.vectorLen(maths.subtract(p, prevP)) : 0; + u.push(currU); + + prevU = currU; + prevP = p; + }); + u = u.map(function (x) { + return x / prevU; + }); + + return u; +}; + +/** + * Find the maximum squared distance of digitized points to fitted curve. + * + * @param {Array>} points - Array of digitized points + * @param {Array>} bez - Fitted curve + * @param {Array} parameters - Parameterization of points + * @returns {Array} Maximum error (squared) and point of max error + */ +function computeMaxError(points, bez, parameters) { + var dist, //Current error + maxDist, //Maximum error + splitPoint, //Point of maximum error + v, //Vector from point to curve + i, count, point, t; + + maxDist = 0; + splitPoint = Math.floor(points.length / 2); + + var t_distMap = mapTtoRelativeDistances(bez, 10); + + for (i = 0, count = points.length; i < count; i++) { + point = points[i]; + //Find 't' for a point on the bez curve that's as close to 'point' as possible: + t = find_t(bez, parameters[i], t_distMap, 10); + + v = maths.subtract(bezier.q(bez, t), point); + dist = v[0] * v[0] + v[1] * v[1]; + + if (dist > maxDist) { + maxDist = dist; + splitPoint = i; + } + } + + return [maxDist, splitPoint]; +}; + +//Sample 't's and map them to relative distances along the curve: +var mapTtoRelativeDistances = function mapTtoRelativeDistances(bez, B_parts) { + var B_t_curr; + var B_t_dist = [0]; + var B_t_prev = bez[0]; + var sumLen = 0; + + for (var i = 1; i <= B_parts; i++) { + B_t_curr = bezier.q(bez, i / B_parts); + + sumLen += maths.vectorLen(maths.subtract(B_t_curr, B_t_prev)); + + B_t_dist.push(sumLen); + B_t_prev = B_t_curr; + } + + //Normalize B_length to the same interval as the parameter distances; 0 to 1: + B_t_dist = B_t_dist.map(function (x) { + return x / sumLen; + }); + return B_t_dist; +}; + +function find_t(bez, param, t_distMap, B_parts) { + if (param < 0) { + return 0; + } + if (param > 1) { + return 1; + } + + /* + 'param' is a value between 0 and 1 telling us the relative position + of a point on the source polyline (linearly from the start (0) to the end (1)). + To see if a given curve - 'bez' - is a close approximation of the polyline, + we compare such a poly-point to the point on the curve that's the same + relative distance along the curve's length. + But finding that curve-point takes a little work: + There is a function "B(t)" to find points along a curve from the parametric parameter 't' + (also relative from 0 to 1: http://stackoverflow.com/a/32841764/1869660 + http://pomax.github.io/bezierinfo/#explanation), + but 't' isn't linear by length (http://gamedev.stackexchange.com/questions/105230). + So, we sample some points along the curve using a handful of values for 't'. + Then, we calculate the length between those samples via plain euclidean distance; + B(t) concentrates the points around sharp turns, so this should give us a good-enough outline of the curve. + Thus, for a given relative distance ('param'), we can now find an upper and lower value + for the corresponding 't' by searching through those sampled distances. + Finally, we just use linear interpolation to find a better value for the exact 't'. + More info: + http://gamedev.stackexchange.com/questions/105230/points-evenly-spaced-along-a-bezier-curve + http://stackoverflow.com/questions/29438398/cheap-way-of-calculating-cubic-bezier-length + http://steve.hollasch.net/cgindex/curves/cbezarclen.html + https://github.com/retuxx/tinyspline + */ + var lenMax, lenMin, tMax, tMin, t; + + //Find the two t-s that the current param distance lies between, + //and then interpolate a somewhat accurate value for the exact t: + for (var i = 1; i <= B_parts; i++) { + + if (param <= t_distMap[i]) { + tMin = (i - 1) / B_parts; + tMax = i / B_parts; + lenMin = t_distMap[i - 1]; + lenMax = t_distMap[i]; + + t = (param - lenMin) / (lenMax - lenMin) * (tMax - tMin) + tMin; + break; + } + } + return t; +} + +/** + * Creates a vector of length 1 which shows the direction from B to A + */ +function createTangent(pointA, pointB) { + return maths.normalize(maths.subtract(pointA, pointB)); +} + +/* + Simplified versions of what we need from math.js + Optimized for our input, which is only numbers and 1x2 arrays (i.e. [x, y] coordinates). +*/ + +var maths = function () { + function maths() { + _classCallCheck(this, maths); + } + + maths.zeros_Xx2x2 = function zeros_Xx2x2(x) { + var zs = []; + while (x--) { + zs.push([0, 0]); + } + return zs; + }; + + maths.mulItems = function mulItems(items, multiplier) { + return items.map(function (x) { + return x * multiplier; + }); + }; + + maths.mulMatrix = function mulMatrix(m1, m2) { + //https://en.wikipedia.org/wiki/Matrix_multiplication#Matrix_product_.28two_matrices.29 + //Simplified to only handle 1-dimensional matrices (i.e. arrays) of equal length: + return m1.reduce(function (sum, x1, i) { + return sum + x1 * m2[i]; + }, 0); + }; + + maths.subtract = function subtract(arr1, arr2) { + return arr1.map(function (x1, i) { + return x1 - arr2[i]; + }); + }; + + maths.addArrays = function addArrays(arr1, arr2) { + return arr1.map(function (x1, i) { + return x1 + arr2[i]; + }); + }; + + maths.addItems = function addItems(items, addition) { + return items.map(function (x) { + return x + addition; + }); + }; + + maths.sum = function sum(items) { + return items.reduce(function (sum, x) { + return sum + x; + }); + }; + + maths.dot = function dot(m1, m2) { + return maths.mulMatrix(m1, m2); + }; + + maths.vectorLen = function vectorLen(v) { + return Math.hypot.apply(Math, v); + }; + + maths.divItems = function divItems(items, divisor) { + return items.map(function (x) { + return x / divisor; + }); + }; + + maths.squareItems = function squareItems(items) { + return items.map(function (x) { + return x * x; + }); + }; + + maths.normalize = function normalize(v) { + return this.divItems(v, this.vectorLen(v)); + }; + + return maths; +}(); + +var bezier = function () { + function bezier() { + _classCallCheck(this, bezier); + } + + bezier.q = function q(ctrlPoly, t) { + var tx = 1.0 - t; + var pA = maths.mulItems(ctrlPoly[0], tx * tx * tx), + pB = maths.mulItems(ctrlPoly[1], 3 * tx * tx * t), + pC = maths.mulItems(ctrlPoly[2], 3 * tx * t * t), + pD = maths.mulItems(ctrlPoly[3], t * t * t); + return maths.addArrays(maths.addArrays(pA, pB), maths.addArrays(pC, pD)); + }; + + bezier.qprime = function qprime(ctrlPoly, t) { + var tx = 1.0 - t; + var pA = maths.mulItems(maths.subtract(ctrlPoly[1], ctrlPoly[0]), 3 * tx * tx), + pB = maths.mulItems(maths.subtract(ctrlPoly[2], ctrlPoly[1]), 6 * tx * t), + pC = maths.mulItems(maths.subtract(ctrlPoly[3], ctrlPoly[2]), 3 * t * t); + return maths.addArrays(maths.addArrays(pA, pB), pC); + }; + + bezier.qprimeprime = function qprimeprime(ctrlPoly, t) { + return maths.addArrays(maths.mulItems(maths.addArrays(maths.subtract(ctrlPoly[2], maths.mulItems(ctrlPoly[1], 2)), ctrlPoly[0]), 6 * (1.0 - t)), maths.mulItems(maths.addArrays(maths.subtract(ctrlPoly[3], maths.mulItems(ctrlPoly[2], 2)), ctrlPoly[1]), 6 * t)); + }; + + return bezier; +}(); diff --git a/src/index.html b/src/index.html index 30f3b57..a1f6360 100644 --- a/src/index.html +++ b/src/index.html @@ -7,6 +7,7 @@ Tauri App + diff --git a/src/main.js b/src/main.js index e9654bb..ba492a8 100644 --- a/src/main.js +++ b/src/main.js @@ -1,4 +1,5 @@ -// const { invoke } = window.__TAURI__.core; +const { invoke } = window.__TAURI__.core; +import * as fitCurve from '/fit-curve.js'; let greetInputEl; let greetMsgEl; @@ -9,6 +10,7 @@ let canvases = []; let mode = "draw" let minSegmentSize = 5; +let maxSmoothAngle = 0.2; let tools = { select: { @@ -31,6 +33,7 @@ let tools = { } let mouseEvent; +console.log(fitCurve) let context = { mouseDown: false, @@ -94,6 +97,47 @@ class Shape { addCurve(curve) { this.curves.push(curve) } + simplify(mode="smooth") { + // Mode can be corners, smooth or auto + // let maxIndex; + // for (let j=0; j<5; j++) { + // if (this.curves.length < 3) return; + // maxIndex = this.curves.length-1; + // for (let i=1; i minSegmentSize) { - let midpoint = {x: (mouse.x+context.lastMouse.x)/2, y: (mouse.y+context.lastMouse.y)/2} - context.activeShape.addCurve(new Curve(midpoint.x, midpoint.y, midpoint.x, midpoint.y, mouse.x, mouse.y)) - } + let midpoint = {x: (mouse.x+context.lastMouse.x)/2, y: (mouse.y+context.lastMouse.y)/2} + context.activeShape.addCurve(new Curve(midpoint.x, midpoint.y, midpoint.x, midpoint.y, mouse.x, mouse.y)) + context.activeShape.simplify() context.activeShape = undefined } break; @@ -226,13 +274,13 @@ function stage() { if (vectorDist(mouse, context.lastMouse) > minSegmentSize) { let midpoint = {x: (mouse.x+context.lastMouse.x)/2, y: (mouse.y+context.lastMouse.y)/2} context.activeShape.addCurve(new Curve(midpoint.x, midpoint.y, midpoint.x, midpoint.y, mouse.x, mouse.y)) + context.lastMouse = mouse } } break; default: break; } - context.lastMouse = mouse updateUI() }) return scroller @@ -259,6 +307,8 @@ function toolbar() { strokeColor.className = "color-field" fillColor.value = "#ffffff" strokeColor.value = "#000000" + context.fillStyle = fillColor.value + context.strokeStyle = strokeColor.value fillColor.addEventListener('click', e => { Coloris({ el: ".color-field", From d85da1c3a3f39a372d7bfd7aab8ba9079ae7965f Mon Sep 17 00:00:00 2001 From: Skyler Lehmkuhl Date: Fri, 15 Nov 2024 05:26:12 -0500 Subject: [PATCH 05/46] add hard edge simplification --- src/main.js | 69 ++++++++++++++++++++++++----------------------------- 1 file changed, 31 insertions(+), 38 deletions(-) diff --git a/src/main.js b/src/main.js index ba492a8..82a5957 100644 --- a/src/main.js +++ b/src/main.js @@ -10,7 +10,7 @@ let canvases = []; let mode = "draw" let minSegmentSize = 5; -let maxSmoothAngle = 0.2; +let maxSmoothAngle = 0.6; let tools = { select: { @@ -99,43 +99,36 @@ class Shape { } simplify(mode="smooth") { // Mode can be corners, smooth or auto - // let maxIndex; - // for (let j=0; j<5; j++) { - // if (this.curves.length < 3) return; - // maxIndex = this.curves.length-1; - // for (let i=1; i 3) { + angles = [2*Math.PI] + for (let i=1; i Date: Sat, 16 Nov 2024 04:14:49 -0500 Subject: [PATCH 06/46] manually relayout ui --- src/index.html | 4 +-- src/main.js | 73 ++++++++++++++++++++++++++++++++++++++++---------- src/styles.css | 60 ++++++++++++++++++++--------------------- 3 files changed, 91 insertions(+), 46 deletions(-) diff --git a/src/index.html b/src/index.html index a1f6360..490bdbc 100644 --- a/src/index.html +++ b/src/index.html @@ -12,7 +12,7 @@ -
+ -
+ diff --git a/src/main.js b/src/main.js index 82a5957..9d5bc7e 100644 --- a/src/main.js +++ b/src/main.js @@ -33,7 +33,6 @@ let tools = { } let mouseEvent; -console.log(fitCurve) let context = { mouseDown: false, @@ -203,7 +202,7 @@ window.addEventListener("DOMContentLoaded", () => { // greetInputEl = document.querySelector("#greet-input"); // greetMsgEl = document.querySelector("#greet-msg"); rootPane = document.querySelector("#root") - rootPane.appendChild(toolbar()) + rootPane.appendChild(createPane(toolbar())) rootPane.addEventListener("mousemove", (e) => { mouseEvent = e; }) @@ -211,9 +210,14 @@ window.addEventListener("DOMContentLoaded", () => { // e.preventDefault(); // greet(); // }); - splitPane(rootPane, 10, true) + let [_toolbar, panel] = splitPane(rootPane, 10, true) + let [_stage, _infopanel] = splitPane(panel, 70, false, createPane(infopanel())) }); +window.addEventListener("resize", () => { + updateLayout(rootPane) +}) + function stage() { let stage = document.createElement("canvas") let scroller = document.createElement("div") @@ -342,10 +346,18 @@ function toolbar() { return tools_scroller } -function createPane() { +function infopanel() { + let panel = document.createElement("div") + + return panel +} + +function createPane(content=undefined) { let div = document.createElement("div") let header = document.createElement("div") - let content = stage() // TODO: change based on type + if (!content) { + content = stage() // TODO: change based on type + } header.className = "header" let button = document.createElement("button") @@ -362,15 +374,15 @@ function createPane() { // header.style.gridArea = "1 / 1 / 2 / 2" // content.style.gridArea = "1 / 2 / 2 / 3" - div.classList = ["vertical-grid", "pane"] - header.style.flex = "0 0 var(--lineheight)" - content.style.flex = "1 1 100%" + div.className = "vertical-grid" + header.style.height = "calc( 2 * var(--lineheight))" + content.style.height = "calc( 100% - 2 * var(--lineheight) )" div.appendChild(header) div.appendChild(content) return div } -function splitPane(div, percent, horiz) { +function splitPane(div, percent, horiz, newPane=undefined) { let content = div.firstElementChild let div1 = document.createElement("div") let div2 = document.createElement("div") @@ -378,8 +390,13 @@ function splitPane(div, percent, horiz) { div1.className = "panecontainer" div2.className = "panecontainer" + console.log(div) div1.appendChild(content) - div2.appendChild(createPane()) + if (newPane) { + div2.appendChild(newPane) + } else { + div2.appendChild(createPane()) + } div.appendChild(div1) div.appendChild(div2) @@ -398,15 +415,43 @@ function splitPane(div, percent, horiz) { if (horiz) { div.className = "horizontal-grid" } else { - div.className = "verical-grid" + div.className = "vertical-grid" } - div1.style.flex = `0 0 ${percent}%` - div2.style.flex = `1 1 auto` + div.setAttribute("lb-percent", percent) // TODO: better attribute name + // div1.style.flex = `0 0 ${percent}%` + // div2.style.flex = `1 1 auto` Coloris({el: ".color-field"}) updateUI() + updateLayout(rootPane) + return [div1, div2] } - +function updateLayout(element) { + let rect = element.getBoundingClientRect() + let percent = element.getAttribute("lb-percent") + percent ||= 50 + let children = element.children + if (children.length != 2) return; + if (element.className == "horizontal-grid") { + console.log(rect) + children[0].style.width = `${rect.width * percent / 100}px` + children[1].style.width = `${rect.width * (100 - percent) / 100}px` + children[0].style.height = `${rect.height}px` + children[1].style.height = `${rect.height}px` + } else if (element.className == "vertical-grid") { + console.log("vert") + children[0].style.height = `${rect.height * percent / 100}px` + children[1].style.height = `${rect.height * (100 - percent) / 100}px` + children[0].style.width = `${rect.width}px` + children[1].style.width = `${rect.width}px` + } + if (children[0].getAttribute("lb-percent")) { + updateLayout(children[0]) + } + if (children[1].getAttribute("lb-percent")) { + updateLayout(children[1]) + } +} function updateUI() { for (let canvas of canvases) { diff --git a/src/styles.css b/src/styles.css index 81006b8..ada3709 100644 --- a/src/styles.css +++ b/src/styles.css @@ -137,44 +137,32 @@ button { height: 100%; } -.horizontal-grid { - /* display: grid; - grid-template-rows: 1fr; - grid-column-gap: 0px; - grid-row-gap: 0px; - min-width: 0px; - min-height: 0px; - max-height: 100%; */ +.horizontal-grid, .vertical-grid { display: flex; - flex-direction: row; gap: 10px; background-color: #0f0f0f; width: 100%; + height: 100%; +} +.horizontal-grid { + flex-direction: row; } .vertical-grid { - /* display: grid; - grid-template-columns: 1fr; - grid-column-gap: 0px; - grid-row-gap: 0px; - min-width: 0px; - min-height: 0px; - max-height: 100%; */ - display: flex; flex-direction: column; - gap: 10px; - background-color: #0f0f0f; - height: 100%; +} +/* I don't fully understand this selector but it works for now */ +.horizontal-grid:hover:not(:has(*:hover)) { + background: red; + cursor: ew-resize; +} +.vertical-grid:hover:not(:has(*:hover)) { + background: red; + cursor: ns-resize } .scroll { overflow: scroll; width: 100%; height: 100%; - /* display: block; - border-style: solid; - border-width: 1px; - border-color: rgba(0, 0, 0, 0.2); - font-family: Helvetica, Arial, sans-serif; - overflow: scroll; */ } .stage { width: 1500px; @@ -188,8 +176,8 @@ button { flex-wrap: wrap; } .toolbtn { - /* width: var(--lineheight); - height: var(--lineheight); */ + width: calc( 3 * var(--lineheight) ); + height: calc( 3 * var(--lineheight) ); background-color: #2f2f2f; } @@ -199,8 +187,20 @@ button { background-color: #2f2f2f; } +.clr-field { + width: 100%; +} .clr-field button { - width: 100% !important; - height: 100% !important; + width: 50% !important; + /* height: 100% !important; */ + /* margin: 100px; */ border-radius: 5px; } +.clr-field input { + width: 50%; +} +.infopanel { + width: 100%; + height: 100%; + background-color: #3f3f3f; +} \ No newline at end of file From caa6aa8cee86ffea63785aac69560bf6902a4cd1 Mon Sep 17 00:00:00 2001 From: Skyler Lehmkuhl Date: Sat, 16 Nov 2024 04:15:14 -0500 Subject: [PATCH 07/46] add test instructions --- README.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 18afe19..36d8496 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,7 @@ # Lightningbeam 2 -This README needs content. This is Lightningbeam rewritten with Tauri. \ No newline at end of file +This README needs content. This is Lightningbeam rewritten with Tauri. + +To test: + +`pnpm tauri dev` \ No newline at end of file From 277aec918c11aea2b70dc4ed41e31f242c90f7fe Mon Sep 17 00:00:00 2001 From: Skyler Lehmkuhl Date: Sat, 16 Nov 2024 05:39:23 -0500 Subject: [PATCH 08/46] use better polyline simplification --- src/index.html | 2 +- src/main.js | 34 +++++++------ src/simplify.js | 123 ++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 140 insertions(+), 19 deletions(-) create mode 100644 src/simplify.js diff --git a/src/index.html b/src/index.html index 490bdbc..ceeba06 100644 --- a/src/index.html +++ b/src/index.html @@ -7,7 +7,7 @@ Tauri App - + diff --git a/src/main.js b/src/main.js index 9d5bc7e..50e421c 100644 --- a/src/main.js +++ b/src/main.js @@ -1,6 +1,8 @@ const { invoke } = window.__TAURI__.core; import * as fitCurve from '/fit-curve.js'; +let simplifyPolyline = simplify + let greetInputEl; let greetMsgEl; let rootPane; @@ -96,27 +98,23 @@ class Shape { addCurve(curve) { this.curves.push(curve) } - simplify(mode="smooth") { + simplify(mode="corners") { // Mode can be corners, smooth or auto if (mode=="corners") { - let angles; - while (this.curves.length > 3) { - angles = [2*Math.PI] - for (let i=1; i 1) { + x = p2.x; + y = p2.y; + + } else if (t > 0) { + x += dx * t; + y += dy * t; + } + } + + dx = p.x - x; + dy = p.y - y; + + return dx * dx + dy * dy; +} +// rest of the code doesn't care about point format + +// basic distance-based simplification +function simplifyRadialDist(points, sqTolerance) { + + var prevPoint = points[0], + newPoints = [prevPoint], + point; + + for (var i = 1, len = points.length; i < len; i++) { + point = points[i]; + + if (getSqDist(point, prevPoint) > sqTolerance) { + newPoints.push(point); + prevPoint = point; + } + } + + if (prevPoint !== point) newPoints.push(point); + + return newPoints; +} + +function simplifyDPStep(points, first, last, sqTolerance, simplified) { + var maxSqDist = sqTolerance, + index; + + for (var i = first + 1; i < last; i++) { + var sqDist = getSqSegDist(points[i], points[first], points[last]); + + if (sqDist > maxSqDist) { + index = i; + maxSqDist = sqDist; + } + } + + if (maxSqDist > sqTolerance) { + if (index - first > 1) simplifyDPStep(points, first, index, sqTolerance, simplified); + simplified.push(points[index]); + if (last - index > 1) simplifyDPStep(points, index, last, sqTolerance, simplified); + } +} + +// simplification using Ramer-Douglas-Peucker algorithm +function simplifyDouglasPeucker(points, sqTolerance) { + var last = points.length - 1; + + var simplified = [points[0]]; + simplifyDPStep(points, 0, last, sqTolerance, simplified); + simplified.push(points[last]); + + return simplified; +} + +// both algorithms combined for awesome performance +function simplify(points, tolerance, highestQuality) { + + if (points.length <= 2) return points; + + var sqTolerance = tolerance !== undefined ? tolerance * tolerance : 1; + + points = highestQuality ? points : simplifyRadialDist(points, sqTolerance); + points = simplifyDouglasPeucker(points, sqTolerance); + + return points; +} + +// export as AMD module / Node module / browser or worker variable +if (typeof define === 'function' && define.amd) define(function() { return simplify; }); +else if (typeof module !== 'undefined') { + module.exports = simplify; + module.exports.default = simplify; +} else if (typeof self !== 'undefined') self.simplify = simplify; +else window.simplify = simplify; + +})(); From f5113d6687312abdb367d262026c3f554c385c0b Mon Sep 17 00:00:00 2001 From: Skyler Lehmkuhl Date: Sun, 17 Nov 2024 05:22:40 -0500 Subject: [PATCH 09/46] place shape objects on frame --- src/main.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/main.js b/src/main.js index 50e421c..8980fe9 100644 --- a/src/main.js +++ b/src/main.js @@ -82,6 +82,7 @@ class Curve { class Frame { constructor() { this.keys = {} + this.shapes = [] } } @@ -157,7 +158,7 @@ class GraphicsObject { child.scale = this.frames[this.currentFrame][idx].scale; child.draw(context) } - for (let shape of this.shapes) { + for (let shape of this.frames[this.currentFrame].shapes) { ctx.beginPath() ctx.moveTo(shape.startx, shape.starty) for (let curve of shape.curves) { @@ -179,7 +180,8 @@ class GraphicsObject { } } addShape(shape) { - this.shapes.push(shape) + // this.shapes.push(shape) + this.frames[this.currentFrame].shapes.push(shape) } } From d8413e5dfb0ae1c49528b35e61533a05ebe15050 Mon Sep 17 00:00:00 2001 From: Skyler Lehmkuhl Date: Sun, 17 Nov 2024 05:23:21 -0500 Subject: [PATCH 10/46] add d3-interpolate-path to project for shape tweening --- src/d3-interpolate-path.js | 782 +++++++++++++++++++++++++++++++++++++ src/index.html | 1 + 2 files changed, 783 insertions(+) create mode 100644 src/d3-interpolate-path.js diff --git a/src/d3-interpolate-path.js b/src/d3-interpolate-path.js new file mode 100644 index 0000000..7d31297 --- /dev/null +++ b/src/d3-interpolate-path.js @@ -0,0 +1,782 @@ +(function (global, factory) { +typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports) : +typeof define === 'function' && define.amd ? define(['exports'], factory) : +(global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory(global.d3 = global.d3 || {})); +}(this, (function (exports) { 'use strict'; + +function ownKeys(object, enumerableOnly) { + var keys = Object.keys(object); + + if (Object.getOwnPropertySymbols) { + var symbols = Object.getOwnPropertySymbols(object); + + if (enumerableOnly) { + symbols = symbols.filter(function (sym) { + return Object.getOwnPropertyDescriptor(object, sym).enumerable; + }); + } + + keys.push.apply(keys, symbols); + } + + return keys; +} + +function _objectSpread2(target) { + for (var i = 1; i < arguments.length; i++) { + var source = arguments[i] != null ? arguments[i] : {}; + + if (i % 2) { + ownKeys(Object(source), true).forEach(function (key) { + _defineProperty(target, key, source[key]); + }); + } else if (Object.getOwnPropertyDescriptors) { + Object.defineProperties(target, Object.getOwnPropertyDescriptors(source)); + } else { + ownKeys(Object(source)).forEach(function (key) { + Object.defineProperty(target, key, Object.getOwnPropertyDescriptor(source, key)); + }); + } + } + + return target; +} + +function _typeof(obj) { + "@babel/helpers - typeof"; + + if (typeof Symbol === "function" && typeof Symbol.iterator === "symbol") { + _typeof = function (obj) { + return typeof obj; + }; + } else { + _typeof = function (obj) { + return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; + }; + } + + return _typeof(obj); +} + +function _defineProperty(obj, key, value) { + if (key in obj) { + Object.defineProperty(obj, key, { + value: value, + enumerable: true, + configurable: true, + writable: true + }); + } else { + obj[key] = value; + } + + return obj; +} + +function _extends() { + _extends = Object.assign || function (target) { + for (var i = 1; i < arguments.length; i++) { + var source = arguments[i]; + + for (var key in source) { + if (Object.prototype.hasOwnProperty.call(source, key)) { + target[key] = source[key]; + } + } + } + + return target; + }; + + return _extends.apply(this, arguments); +} + +function _unsupportedIterableToArray(o, minLen) { + if (!o) return; + if (typeof o === "string") return _arrayLikeToArray(o, minLen); + var n = Object.prototype.toString.call(o).slice(8, -1); + if (n === "Object" && o.constructor) n = o.constructor.name; + if (n === "Map" || n === "Set") return Array.from(o); + if (n === "Arguments" || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n)) return _arrayLikeToArray(o, minLen); +} + +function _arrayLikeToArray(arr, len) { + if (len == null || len > arr.length) len = arr.length; + + for (var i = 0, arr2 = new Array(len); i < len; i++) arr2[i] = arr[i]; + + return arr2; +} + +function _createForOfIteratorHelper(o, allowArrayLike) { + var it = typeof Symbol !== "undefined" && o[Symbol.iterator] || o["@@iterator"]; + + if (!it) { + if (Array.isArray(o) || (it = _unsupportedIterableToArray(o)) || allowArrayLike && o && typeof o.length === "number") { + if (it) o = it; + var i = 0; + + var F = function () {}; + + return { + s: F, + n: function () { + if (i >= o.length) return { + done: true + }; + return { + done: false, + value: o[i++] + }; + }, + e: function (e) { + throw e; + }, + f: F + }; + } + + throw new TypeError("Invalid attempt to iterate non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."); + } + + var normalCompletion = true, + didErr = false, + err; + return { + s: function () { + it = it.call(o); + }, + n: function () { + var step = it.next(); + normalCompletion = step.done; + return step; + }, + e: function (e) { + didErr = true; + err = e; + }, + f: function () { + try { + if (!normalCompletion && it.return != null) it.return(); + } finally { + if (didErr) throw err; + } + } + }; +} + +/** + * de Casteljau's algorithm for drawing and splitting bezier curves. + * Inspired by https://pomax.github.io/bezierinfo/ + * + * @param {Number[][]} points Array of [x,y] points: [start, control1, control2, ..., end] + * The original segment to split. + * @param {Number} t Where to split the curve (value between [0, 1]) + * @return {Object} An object { left, right } where left is the segment from 0..t and + * right is the segment from t..1. + */ +function decasteljau(points, t) { + var left = []; + var right = []; + + function decasteljauRecurse(points, t) { + if (points.length === 1) { + left.push(points[0]); + right.push(points[0]); + } else { + var newPoints = Array(points.length - 1); + + for (var i = 0; i < newPoints.length; i++) { + if (i === 0) { + left.push(points[0]); + } + + if (i === newPoints.length - 1) { + right.push(points[i + 1]); + } + + newPoints[i] = [(1 - t) * points[i][0] + t * points[i + 1][0], (1 - t) * points[i][1] + t * points[i + 1][1]]; + } + + decasteljauRecurse(newPoints, t); + } + } + + if (points.length) { + decasteljauRecurse(points, t); + } + + return { + left: left, + right: right.reverse() + }; +} +/** + * Convert segments represented as points back into a command object + * + * @param {Number[][]} points Array of [x,y] points: [start, control1, control2, ..., end] + * Represents a segment + * @return {Object} A command object representing the segment. + */ + + +function pointsToCommand(points) { + var command = {}; + + if (points.length === 4) { + command.x2 = points[2][0]; + command.y2 = points[2][1]; + } + + if (points.length >= 3) { + command.x1 = points[1][0]; + command.y1 = points[1][1]; + } + + command.x = points[points.length - 1][0]; + command.y = points[points.length - 1][1]; + + if (points.length === 4) { + // start, control1, control2, end + command.type = 'C'; + } else if (points.length === 3) { + // start, control, end + command.type = 'Q'; + } else { + // start, end + command.type = 'L'; + } + + return command; +} +/** + * Runs de Casteljau's algorithm enough times to produce the desired number of segments. + * + * @param {Number[][]} points Array of [x,y] points for de Casteljau (the initial segment to split) + * @param {Number} segmentCount Number of segments to split the original into + * @return {Number[][][]} Array of segments + */ + + +function splitCurveAsPoints(points, segmentCount) { + segmentCount = segmentCount || 2; + var segments = []; + var remainingCurve = points; + var tIncrement = 1 / segmentCount; // x-----x-----x-----x + // t= 0.33 0.66 1 + // x-----o-----------x + // r= 0.33 + // x-----o-----x + // r= 0.5 (0.33 / (1 - 0.33)) === tIncrement / (1 - (tIncrement * (i - 1)) + // x-----x-----x-----x----x + // t= 0.25 0.5 0.75 1 + // x-----o----------------x + // r= 0.25 + // x-----o----------x + // r= 0.33 (0.25 / (1 - 0.25)) + // x-----o----x + // r= 0.5 (0.25 / (1 - 0.5)) + + for (var i = 0; i < segmentCount - 1; i++) { + var tRelative = tIncrement / (1 - tIncrement * i); + var split = decasteljau(remainingCurve, tRelative); + segments.push(split.left); + remainingCurve = split.right; + } // last segment is just to the end from the last point + + + segments.push(remainingCurve); + return segments; +} +/** + * Convert command objects to arrays of points, run de Casteljau's algorithm on it + * to split into to the desired number of segments. + * + * @param {Object} commandStart The start command object + * @param {Object} commandEnd The end command object + * @param {Number} segmentCount The number of segments to create + * @return {Object[]} An array of commands representing the segments in sequence + */ + + +function splitCurve(commandStart, commandEnd, segmentCount) { + var points = [[commandStart.x, commandStart.y]]; + + if (commandEnd.x1 != null) { + points.push([commandEnd.x1, commandEnd.y1]); + } + + if (commandEnd.x2 != null) { + points.push([commandEnd.x2, commandEnd.y2]); + } + + points.push([commandEnd.x, commandEnd.y]); + return splitCurveAsPoints(points, segmentCount).map(pointsToCommand); +} + +var commandTokenRegex = /[MLCSTQAHVZmlcstqahv]|-?[\d.e+-]+/g; +/** + * List of params for each command type in a path `d` attribute + */ + +var typeMap = { + M: ['x', 'y'], + L: ['x', 'y'], + H: ['x'], + V: ['y'], + C: ['x1', 'y1', 'x2', 'y2', 'x', 'y'], + S: ['x2', 'y2', 'x', 'y'], + Q: ['x1', 'y1', 'x', 'y'], + T: ['x', 'y'], + A: ['rx', 'ry', 'xAxisRotation', 'largeArcFlag', 'sweepFlag', 'x', 'y'], + Z: [] +}; // Add lower case entries too matching uppercase (e.g. 'm' == 'M') + +Object.keys(typeMap).forEach(function (key) { + typeMap[key.toLowerCase()] = typeMap[key]; +}); + +function arrayOfLength(length, value) { + var array = Array(length); + + for (var i = 0; i < length; i++) { + array[i] = value; + } + + return array; +} +/** + * Converts a command object to a string to be used in a `d` attribute + * @param {Object} command A command object + * @return {String} The string for the `d` attribute + */ + + +function commandToString(command) { + return "".concat(command.type).concat(typeMap[command.type].map(function (p) { + return command[p]; + }).join(',')); +} +/** + * Converts command A to have the same type as command B. + * + * e.g., L0,5 -> C0,5,0,5,0,5 + * + * Uses these rules: + * x1 <- x + * x2 <- x + * y1 <- y + * y2 <- y + * rx <- 0 + * ry <- 0 + * xAxisRotation <- read from B + * largeArcFlag <- read from B + * sweepflag <- read from B + * + * @param {Object} aCommand Command object from path `d` attribute + * @param {Object} bCommand Command object from path `d` attribute to match against + * @return {Object} aCommand converted to type of bCommand + */ + + +function convertToSameType(aCommand, bCommand) { + var conversionMap = { + x1: 'x', + y1: 'y', + x2: 'x', + y2: 'y' + }; + var readFromBKeys = ['xAxisRotation', 'largeArcFlag', 'sweepFlag']; // convert (but ignore M types) + + if (aCommand.type !== bCommand.type && bCommand.type.toUpperCase() !== 'M') { + var aConverted = {}; + Object.keys(bCommand).forEach(function (bKey) { + var bValue = bCommand[bKey]; // first read from the A command + + var aValue = aCommand[bKey]; // if it is one of these values, read from B no matter what + + if (aValue === undefined) { + if (readFromBKeys.includes(bKey)) { + aValue = bValue; + } else { + // if it wasn't in the A command, see if an equivalent was + if (aValue === undefined && conversionMap[bKey]) { + aValue = aCommand[conversionMap[bKey]]; + } // if it doesn't have a converted value, use 0 + + + if (aValue === undefined) { + aValue = 0; + } + } + } + + aConverted[bKey] = aValue; + }); // update the type to match B + + aConverted.type = bCommand.type; + aCommand = aConverted; + } + + return aCommand; +} +/** + * Interpolate between command objects commandStart and commandEnd segmentCount times. + * If the types are L, Q, or C then the curves are split as per de Casteljau's algorithm. + * Otherwise we just copy commandStart segmentCount - 1 times, finally ending with commandEnd. + * + * @param {Object} commandStart Command object at the beginning of the segment + * @param {Object} commandEnd Command object at the end of the segment + * @param {Number} segmentCount The number of segments to split this into. If only 1 + * Then [commandEnd] is returned. + * @return {Object[]} Array of ~segmentCount command objects between commandStart and + * commandEnd. (Can be segmentCount+1 objects if commandStart is type M). + */ + + +function splitSegment(commandStart, commandEnd, segmentCount) { + var segments = []; // line, quadratic bezier, or cubic bezier + + if (commandEnd.type === 'L' || commandEnd.type === 'Q' || commandEnd.type === 'C') { + segments = segments.concat(splitCurve(commandStart, commandEnd, segmentCount)); // general case - just copy the same point + } else { + var copyCommand = _extends({}, commandStart); // convert M to L + + + if (copyCommand.type === 'M') { + copyCommand.type = 'L'; + } + + segments = segments.concat(arrayOfLength(segmentCount - 1).map(function () { + return copyCommand; + })); + segments.push(commandEnd); + } + + return segments; +} +/** + * Extends an array of commandsToExtend to the length of the referenceCommands by + * splitting segments until the number of commands match. Ensures all the actual + * points of commandsToExtend are in the extended array. + * + * @param {Object[]} commandsToExtend The command object array to extend + * @param {Object[]} referenceCommands The command object array to match in length + * @param {Function} excludeSegment a function that takes a start command object and + * end command object and returns true if the segment should be excluded from splitting. + * @return {Object[]} The extended commandsToExtend array + */ + + +function extend(commandsToExtend, referenceCommands, excludeSegment) { + // compute insertion points: + // number of segments in the path to extend + var numSegmentsToExtend = commandsToExtend.length - 1; // number of segments in the reference path. + + var numReferenceSegments = referenceCommands.length - 1; // this value is always between [0, 1]. + + var segmentRatio = numSegmentsToExtend / numReferenceSegments; // create a map, mapping segments in referenceCommands to how many points + // should be added in that segment (should always be >= 1 since we need each + // point itself). + // 0 = segment 0-1, 1 = segment 1-2, n-1 = last vertex + + var countPointsPerSegment = arrayOfLength(numReferenceSegments).reduce(function (accum, d, i) { + var insertIndex = Math.floor(segmentRatio * i); // handle excluding segments + + if (excludeSegment && insertIndex < commandsToExtend.length - 1 && excludeSegment(commandsToExtend[insertIndex], commandsToExtend[insertIndex + 1])) { + // set the insertIndex to the segment that this point should be added to: + // round the insertIndex essentially so we split half and half on + // neighbouring segments. hence the segmentRatio * i < 0.5 + var addToPriorSegment = segmentRatio * i % 1 < 0.5; // only skip segment if we already have 1 point in it (can't entirely remove a segment) + + if (accum[insertIndex]) { + // TODO - Note this is a naive algorithm that should work for most d3-area use cases + // but if two adjacent segments are supposed to be skipped, this will not perform as + // expected. Could be updated to search for nearest segment to place the point in, but + // will only do that if necessary. + // add to the prior segment + if (addToPriorSegment) { + if (insertIndex > 0) { + insertIndex -= 1; // not possible to add to previous so adding to next + } else if (insertIndex < commandsToExtend.length - 1) { + insertIndex += 1; + } // add to next segment + + } else if (insertIndex < commandsToExtend.length - 1) { + insertIndex += 1; // not possible to add to next so adding to previous + } else if (insertIndex > 0) { + insertIndex -= 1; + } + } + } + + accum[insertIndex] = (accum[insertIndex] || 0) + 1; + return accum; + }, []); // extend each segment to have the correct number of points for a smooth interpolation + + var extended = countPointsPerSegment.reduce(function (extended, segmentCount, i) { + // if last command, just add `segmentCount` number of times + if (i === commandsToExtend.length - 1) { + var lastCommandCopies = arrayOfLength(segmentCount, _extends({}, commandsToExtend[commandsToExtend.length - 1])); // convert M to L + + if (lastCommandCopies[0].type === 'M') { + lastCommandCopies.forEach(function (d) { + d.type = 'L'; + }); + } + + return extended.concat(lastCommandCopies); + } // otherwise, split the segment segmentCount times. + + + return extended.concat(splitSegment(commandsToExtend[i], commandsToExtend[i + 1], segmentCount)); + }, []); // add in the very first point since splitSegment only adds in the ones after it + + extended.unshift(commandsToExtend[0]); + return extended; +} +/** + * Takes a path `d` string and converts it into an array of command + * objects. Drops the `Z` character. + * + * @param {String|null} d A path `d` string + */ + + +function pathCommandsFromString(d) { + // split into valid tokens + var tokens = (d || '').match(commandTokenRegex) || []; + var commands = []; + var commandArgs; + var command; // iterate over each token, checking if we are at a new command + // by presence in the typeMap + + for (var i = 0; i < tokens.length; ++i) { + commandArgs = typeMap[tokens[i]]; // new command found: + + if (commandArgs) { + command = { + type: tokens[i] + }; // add each of the expected args for this command: + + for (var a = 0; a < commandArgs.length; ++a) { + command[commandArgs[a]] = +tokens[i + a + 1]; + } // need to increment our token index appropriately since + // we consumed token args + + + i += commandArgs.length; + commands.push(command); + } + } + + return commands; +} +/** + * Interpolate from A to B by extending A and B during interpolation to have + * the same number of points. This allows for a smooth transition when they + * have a different number of points. + * + * Ignores the `Z` command in paths unless both A and B end with it. + * + * This function works directly with arrays of command objects instead of with + * path `d` strings (see interpolatePath for working with `d` strings). + * + * @param {Object[]} aCommandsInput Array of path commands + * @param {Object[]} bCommandsInput Array of path commands + * @param {(Function|Object)} interpolateOptions + * @param {Function} interpolateOptions.excludeSegment a function that takes a start command object and + * end command object and returns true if the segment should be excluded from splitting. + * @param {Boolean} interpolateOptions.snapEndsToInput a boolean indicating whether end of input should + * be sourced from input argument or computed. + * @returns {Function} Interpolation function that maps t ([0, 1]) to an array of path commands. + */ + +function interpolatePathCommands(aCommandsInput, bCommandsInput, interpolateOptions) { + // make a copy so we don't mess with the input arrays + var aCommands = aCommandsInput == null ? [] : aCommandsInput.slice(); + var bCommands = bCommandsInput == null ? [] : bCommandsInput.slice(); + + var _ref = _typeof(interpolateOptions) === 'object' ? interpolateOptions : { + excludeSegment: interpolateOptions, + snapEndsToInput: true + }, + excludeSegment = _ref.excludeSegment, + snapEndsToInput = _ref.snapEndsToInput; // both input sets are empty, so we don't interpolate + + + if (!aCommands.length && !bCommands.length) { + return function nullInterpolator() { + return []; + }; + } // do we add Z during interpolation? yes if both have it. (we'd expect both to have it or not) + + + var addZ = (aCommands.length === 0 || aCommands[aCommands.length - 1].type === 'Z') && (bCommands.length === 0 || bCommands[bCommands.length - 1].type === 'Z'); // we temporarily remove Z + + if (aCommands.length > 0 && aCommands[aCommands.length - 1].type === 'Z') { + aCommands.pop(); + } + + if (bCommands.length > 0 && bCommands[bCommands.length - 1].type === 'Z') { + bCommands.pop(); + } // if A is empty, treat it as if it used to contain just the first point + // of B. This makes it so the line extends out of from that first point. + + + if (!aCommands.length) { + aCommands.push(bCommands[0]); // otherwise if B is empty, treat it as if it contains the first point + // of A. This makes it so the line retracts into the first point. + } else if (!bCommands.length) { + bCommands.push(aCommands[0]); + } // extend to match equal size + + + var numPointsToExtend = Math.abs(bCommands.length - aCommands.length); + + if (numPointsToExtend !== 0) { + // B has more points than A, so add points to A before interpolating + if (bCommands.length > aCommands.length) { + aCommands = extend(aCommands, bCommands, excludeSegment); // else if A has more points than B, add more points to B + } else if (bCommands.length < aCommands.length) { + bCommands = extend(bCommands, aCommands, excludeSegment); + } + } // commands have same length now. + // convert commands in A to the same type as those in B + + + aCommands = aCommands.map(function (aCommand, i) { + return convertToSameType(aCommand, bCommands[i]); + }); // create mutable interpolated command objects + + var interpolatedCommands = aCommands.map(function (aCommand) { + return _objectSpread2({}, aCommand); + }); + + if (addZ) { + interpolatedCommands.push({ + type: 'Z' + }); + aCommands.push({ + type: 'Z' + }); // required for when returning at t == 0 + } + + return function pathCommandInterpolator(t) { + // at 1 return the final value without the extensions used during interpolation + if (t === 1 && snapEndsToInput) { + return bCommandsInput == null ? [] : bCommandsInput; + } // work with aCommands directly since interpolatedCommands are mutated + + + if (t === 0) { + return aCommands; + } // interpolate the commands using the mutable interpolated command objs + + + for (var i = 0; i < interpolatedCommands.length; ++i) { + // if (interpolatedCommands[i].type === 'Z') continue; + var aCommand = aCommands[i]; + var bCommand = bCommands[i]; + var interpolatedCommand = interpolatedCommands[i]; + + var _iterator = _createForOfIteratorHelper(typeMap[interpolatedCommand.type]), + _step; + + try { + for (_iterator.s(); !(_step = _iterator.n()).done;) { + var arg = _step.value; + interpolatedCommand[arg] = (1 - t) * aCommand[arg] + t * bCommand[arg]; // do not use floats for flags (#27), round to integer + + if (arg === 'largeArcFlag' || arg === 'sweepFlag') { + interpolatedCommand[arg] = Math.round(interpolatedCommand[arg]); + } + } + } catch (err) { + _iterator.e(err); + } finally { + _iterator.f(); + } + } + + return interpolatedCommands; + }; +} +/** @typedef InterpolateOptions */ + +/** + * Interpolate from A to B by extending A and B during interpolation to have + * the same number of points. This allows for a smooth transition when they + * have a different number of points. + * + * Ignores the `Z` character in paths unless both A and B end with it. + * + * @param {String} a The `d` attribute for a path + * @param {String} b The `d` attribute for a path + * @param {((command1, command2) => boolean|{ + * excludeSegment?: (command1, command2) => boolean; + * snapEndsToInput?: boolean + * })} interpolateOptions The excludeSegment function or an options object + * - interpolateOptions.excludeSegment a function that takes a start command object and + * end command object and returns true if the segment should be excluded from splitting. + * - interpolateOptions.snapEndsToInput a boolean indicating whether end of input should + * be sourced from input argument or computed. + * @returns {Function} Interpolation function that maps t ([0, 1]) to a path `d` string. + */ + +function interpolatePath(a, b, interpolateOptions) { + var aCommands = pathCommandsFromString(a); + var bCommands = pathCommandsFromString(b); + + var _ref2 = _typeof(interpolateOptions) === 'object' ? interpolateOptions : { + excludeSegment: interpolateOptions, + snapEndsToInput: true + }, + excludeSegment = _ref2.excludeSegment, + snapEndsToInput = _ref2.snapEndsToInput; + + if (!aCommands.length && !bCommands.length) { + return function nullInterpolator() { + return ''; + }; + } + + var commandInterpolator = interpolatePathCommands(aCommands, bCommands, { + excludeSegment: excludeSegment, + snapEndsToInput: snapEndsToInput + }); + return function pathStringInterpolator(t) { + // at 1 return the final value without the extensions used during interpolation + if (t === 1 && snapEndsToInput) { + return b == null ? '' : b; + } + + var interpolatedCommands = commandInterpolator(t); // convert to a string (fastest concat: https://jsperf.com/join-concat/150) + + var interpolatedString = ''; + + var _iterator2 = _createForOfIteratorHelper(interpolatedCommands), + _step2; + + try { + for (_iterator2.s(); !(_step2 = _iterator2.n()).done;) { + var interpolatedCommand = _step2.value; + interpolatedString += commandToString(interpolatedCommand); + } + } catch (err) { + _iterator2.e(err); + } finally { + _iterator2.f(); + } + + return interpolatedString; + }; +} + +exports.interpolatePath = interpolatePath; +exports.interpolatePathCommands = interpolatePathCommands; +exports.pathCommandsFromString = pathCommandsFromString; + +Object.defineProperty(exports, '__esModule', { value: true }); + +}))); diff --git a/src/index.html b/src/index.html index ceeba06..c2f3171 100644 --- a/src/index.html +++ b/src/index.html @@ -8,6 +8,7 @@ Tauri App + From 380a4a00d950047a4b8a59e247e87c377746c537 Mon Sep 17 00:00:00 2001 From: Skyler Lehmkuhl Date: Sun, 17 Nov 2024 18:08:07 -0500 Subject: [PATCH 11/46] info panel improvements --- src/main.js | 105 ++++++++++++++++++++++++++++++++++++------------- src/styles.css | 25 +++++++++++- 2 files changed, 102 insertions(+), 28 deletions(-) diff --git a/src/main.js b/src/main.js index 8980fe9..28e43ad 100644 --- a/src/main.js +++ b/src/main.js @@ -17,20 +17,31 @@ let maxSmoothAngle = 0.6; let tools = { select: { icon: "/assets/select.svg", + properties: {} }, transform: { icon: "/assets/transform.svg", + properties: {} }, draw: { - icon: "/assets/draw.svg" + icon: "/assets/draw.svg", + properties: { + "lineWidth": { + type: "number", + // default: 5, + label: "Line Width" + } + } }, rectangle: { - icon: "/assets/rectangle.svg" + icon: "/assets/rectangle.svg", + properties: {} }, polygon: { - icon: "assets/polygon.svg" + icon: "assets/polygon.svg", + properties: {} } } @@ -47,7 +58,14 @@ let context = { "#00FFFF", "#0000FF", "#FF00FF", - ] + ], + lineWidth: 5, +} + +let config = { + shortcuts: { + playAnimation: " ", + } } function uuidv4() { @@ -68,6 +86,25 @@ function getMousePos(canvas, evt) { }; } +function getProperty(context, path) { + let pointer = context; + let pathComponents = path.split('.') + for (let component of pathComponents) { + pointer = pointer[component] + } + return pointer +} + +function setProperty(context, path, value) { + let pointer = context; + let pathComponents = path.split('.') + let finalComponent = pathComponents.pop() + for (let component of pathComponents) { + pointer = pointer[component] + } + pointer[finalComponent] = value +} + class Curve { constructor(cp1x, cp1y, cp2x, cp2y, x, y) { this.cp1x = cp1x; @@ -87,12 +124,13 @@ class Frame { } class Shape { - constructor(startx, starty, fillStyle, strokeStyle, filled=true, stroked=true) { + constructor(startx, starty, context, filled=true, stroked=true) { this.startx = startx; this.starty = starty; this.curves = []; - this.fillStyle = fillStyle; - this.strokeStyle = strokeStyle; + this.fillStyle = context.fillStyle; + this.strokeStyle = context.strokeStyle; + this.lineWidth = context.lineWidth this.filled = filled; this.stroked = stroked; } @@ -105,8 +143,6 @@ class Shape { let points = [{x: this.startx, y: this.starty}] points = points.concat(this.curves) let newpoints = simplifyPolyline(points, 10, false) - console.log(points.length) - console.log(newpoints.length) this.curves = [] let lastpoint = newpoints.shift() let midpoint @@ -115,7 +151,6 @@ class Shape { this.curves.push(new Curve(midpoint.x, midpoint.y,midpoint.x,midpoint.y,point.x,point.y)) lastpoint = point } - console.log(this.curves) } else if (mode=="smooth") { let error = 30; let points = [[this.startx, this.starty]] @@ -160,6 +195,7 @@ class GraphicsObject { } for (let shape of this.frames[this.currentFrame].shapes) { ctx.beginPath() + ctx.lineWidth = shape.lineWidth ctx.moveTo(shape.startx, shape.starty) for (let curve of shape.curves) { ctx.bezierCurveTo(curve.cp1x, curve.cp1y, curve.cp2x, curve.cp2y, curve.x, curve.y) @@ -180,36 +216,25 @@ class GraphicsObject { } } addShape(shape) { - // this.shapes.push(shape) this.frames[this.currentFrame].shapes.push(shape) } } let root = new GraphicsObject(); -// let shp = new Shape(100,100,'blue', 'black') -// shp.addCurve(new Curve(150,150,150,150,200,100)) -// root.addShape(shp) context.activeObject = root async function greet() { // Learn more about Tauri commands at https://tauri.app/develop/calling-rust/ greetMsgEl.textContent = await invoke("greet", { name: greetInputEl.value }); - // splitPane(rootPane, 50, true) } window.addEventListener("DOMContentLoaded", () => { - // greetInputEl = document.querySelector("#greet-input"); - // greetMsgEl = document.querySelector("#greet-msg"); rootPane = document.querySelector("#root") rootPane.appendChild(createPane(toolbar())) rootPane.addEventListener("mousemove", (e) => { mouseEvent = e; }) - // document.querySelector("#greet-form").addEventListener("submit", (e) => { - // e.preventDefault(); - // greet(); - // }); let [_toolbar, panel] = splitPane(rootPane, 10, true) let [_stage, _infopanel] = splitPane(panel, 70, false, createPane(infopanel())) }); @@ -218,6 +243,12 @@ window.addEventListener("resize", () => { updateLayout(rootPane) }) +window.addEventListener("keypress", (e) => { + if (e.key == config.shortcuts.playAnimation) { + console.log("Spacebar pressed") + } +}) + function stage() { let stage = document.createElement("canvas") let scroller = document.createElement("div") @@ -232,10 +263,9 @@ function stage() { switch (mode) { case "draw": context.mouseDown = true - context.activeShape = new Shape(mouse.x, mouse.y, context.fillStyle, context.strokeStyle, true, true) + context.activeShape = new Shape(mouse.x, mouse.y, context, true, true) context.activeObject.addShape(context.activeShape) context.lastMouse = mouse - console.log(stage) break; default: @@ -348,7 +378,31 @@ function toolbar() { function infopanel() { let panel = document.createElement("div") - + panel.className = "infopanel" + let input; + let label; + let span; + // for (let i=0; i<10; i++) { + for (let property in tools[mode].properties) { + let prop = tools[mode].properties[property] + label = document.createElement("label") + label.className = "infopanel-field" + span = document.createElement("span") + span.className = "infopanel-label" + span.innerText = prop.label + input = document.createElement("input") + input.className = "infopanel-input" + input.value = getProperty(context, property) + input.addEventListener("input", () => { + console.log(input.value) + if (!isNaN(input.value) && input.value > 0) { + setProperty(context, property, input.value) + } + }) + label.appendChild(span) + label.appendChild(input) + panel.appendChild(label) + } return panel } @@ -390,7 +444,6 @@ function splitPane(div, percent, horiz, newPane=undefined) { div1.className = "panecontainer" div2.className = "panecontainer" - console.log(div) div1.appendChild(content) if (newPane) { div2.appendChild(newPane) @@ -433,13 +486,11 @@ function updateLayout(element) { let children = element.children if (children.length != 2) return; if (element.className == "horizontal-grid") { - console.log(rect) children[0].style.width = `${rect.width * percent / 100}px` children[1].style.width = `${rect.width * (100 - percent) / 100}px` children[0].style.height = `${rect.height}px` children[1].style.height = `${rect.height}px` } else if (element.className == "vertical-grid") { - console.log("vert") children[0].style.height = `${rect.height * percent / 100}px` children[1].style.height = `${rect.height * (100 - percent) / 100}px` children[0].style.width = `${rect.width}px` diff --git a/src/styles.css b/src/styles.css index ada3709..a14e68a 100644 --- a/src/styles.css +++ b/src/styles.css @@ -139,7 +139,7 @@ button { .horizontal-grid, .vertical-grid { display: flex; - gap: 10px; + gap: 5px; background-color: #0f0f0f; width: 100%; height: 100%; @@ -173,7 +173,10 @@ button { display: flex; flex-direction: row; gap: 10px; + padding: 5px; flex-wrap: wrap; + align-content: flex-start; + justify-content: space-around; } .toolbtn { width: calc( 3 * var(--lineheight) ); @@ -203,4 +206,24 @@ button { width: 100%; height: 100%; background-color: #3f3f3f; + display: flex; + box-sizing: border-box; + gap: calc( var(--lineheight) / 2 ); + padding: calc( var(--lineheight) / 2 ); + flex-direction: column; + flex-wrap: wrap; + align-content: flex-start; +} +.infopanel-field { + width: 300px; + height: var(--lineheight); + display: flex; + flex-direction: row; +} +.infopanel-label { + flex: 1 1 50%; +} +.infopanel-input { + flex: 1 1 50%; + width: 50%; } \ No newline at end of file From cd1902bf1bdca379de7b1ea5d8232b24d7d4b11f Mon Sep 17 00:00:00 2001 From: Skyler Lehmkuhl Date: Sun, 17 Nov 2024 21:01:54 -0500 Subject: [PATCH 12/46] handle enum selections --- src/main.js | 47 ++++++++++++++++++++++++++++++++++++++++------- src/styles.css | 1 + 2 files changed, 41 insertions(+), 7 deletions(-) diff --git a/src/main.js b/src/main.js index 28e43ad..a629ae4 100644 --- a/src/main.js +++ b/src/main.js @@ -32,6 +32,11 @@ let tools = { type: "number", // default: 5, label: "Line Width" + }, + "simplifyMode": { + type: "enum", + options: ["corners", "smooth"], // "auto"], + label: "Line Mode" } } }, @@ -60,6 +65,7 @@ let context = { "#FF00FF", ], lineWidth: 5, + simplifyMode: "smooth", } let config = { @@ -267,7 +273,7 @@ function stage() { context.activeObject.addShape(context.activeShape) context.lastMouse = mouse break; - + default: break; } @@ -282,7 +288,7 @@ function stage() { if (context.activeShape) { let midpoint = {x: (mouse.x+context.lastMouse.x)/2, y: (mouse.y+context.lastMouse.y)/2} context.activeShape.addCurve(new Curve(midpoint.x, midpoint.y, midpoint.x, midpoint.y, mouse.x, mouse.y)) - context.activeShape.simplify() + context.activeShape.simplify(context.simplifyMode) context.activeShape = undefined } break; @@ -390,14 +396,41 @@ function infopanel() { span = document.createElement("span") span.className = "infopanel-label" span.innerText = prop.label - input = document.createElement("input") - input.className = "infopanel-input" - input.value = getProperty(context, property) + switch (prop.type) { + case "number": + input = document.createElement("input") + input.className = "infopanel-input" + input.type = "number" + input.value = getProperty(context, property) + break; + case "enum": + input = document.createElement("select") + input.className = "infopanel-input" + let optionEl; + for (let option of prop.options) { + optionEl = document.createElement("option") + optionEl.value = option + optionEl.innerText = option + input.appendChild(optionEl) + } + input.value = getProperty(context, property) + break; + } input.addEventListener("input", () => { console.log(input.value) - if (!isNaN(input.value) && input.value > 0) { - setProperty(context, property, input.value) + switch (prop.type) { + case "number": + if (!isNaN(input.value) && input.value > 0) { + setProperty(context, property, input.value) + } + break; + case "enum": + if (prop.options.indexOf(input.value) >= 0) { + // console.log(input.value) + setProperty(context, property, input.value) + } } + }) label.appendChild(span) label.appendChild(input) diff --git a/src/styles.css b/src/styles.css index a14e68a..9eca3a4 100644 --- a/src/styles.css +++ b/src/styles.css @@ -143,6 +143,7 @@ button { background-color: #0f0f0f; width: 100%; height: 100%; + contain: size; } .horizontal-grid { flex-direction: row; From ef31e71b738d32230f7eb0cfa8b46905baf84eee Mon Sep 17 00:00:00 2001 From: Skyler Lehmkuhl Date: Mon, 18 Nov 2024 02:06:15 -0500 Subject: [PATCH 13/46] update tool icons --- src/assets/ellipse.svg | 114 ++++++++++++++++++++++++++++++++++ src/assets/paint_bucket.svg | 119 ++++++++++++++++++++++++++++++++++++ src/assets/rectangle.svg | 114 ++++++++++++++++++++++++++++++++++ 3 files changed, 347 insertions(+) create mode 100644 src/assets/ellipse.svg create mode 100644 src/assets/paint_bucket.svg create mode 100644 src/assets/rectangle.svg diff --git a/src/assets/ellipse.svg b/src/assets/ellipse.svg new file mode 100644 index 0000000..abb2701 --- /dev/null +++ b/src/assets/ellipse.svg @@ -0,0 +1,114 @@ + + + + + + + + + + + + image/svg+xml + + + + + Klaus Staedtler + + + + + + + + + + + + diff --git a/src/assets/paint_bucket.svg b/src/assets/paint_bucket.svg new file mode 100644 index 0000000..0208168 --- /dev/null +++ b/src/assets/paint_bucket.svg @@ -0,0 +1,119 @@ + + + + + + + + + + + + image/svg+xml + + + + + Klaus Staedtler + + + + + + + + + + + + + + diff --git a/src/assets/rectangle.svg b/src/assets/rectangle.svg new file mode 100644 index 0000000..d52258b --- /dev/null +++ b/src/assets/rectangle.svg @@ -0,0 +1,114 @@ + + + + + + + + + + + + image/svg+xml + + + + + Klaus Staedtler + + + + + + + + + + + + From 02de0b0afa5c37660d226bf7a2285d4e749c0bef Mon Sep 17 00:00:00 2001 From: Skyler Lehmkuhl Date: Mon, 18 Nov 2024 02:06:55 -0500 Subject: [PATCH 14/46] optimize layout --- src/styles.css | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/styles.css b/src/styles.css index 9eca3a4..bf2a01e 100644 --- a/src/styles.css +++ b/src/styles.css @@ -143,7 +143,7 @@ button { background-color: #0f0f0f; width: 100%; height: 100%; - contain: size; + contain: strict; } .horizontal-grid { flex-direction: row; From d37b0b0450d939901df709324f438eebbe5a3e2a Mon Sep 17 00:00:00 2001 From: Skyler Lehmkuhl Date: Mon, 18 Nov 2024 02:07:17 -0500 Subject: [PATCH 15/46] import images via drag&drop --- src/main.js | 124 ++++++++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 111 insertions(+), 13 deletions(-) diff --git a/src/main.js b/src/main.js index a629ae4..3e814aa 100644 --- a/src/main.js +++ b/src/main.js @@ -30,13 +30,16 @@ let tools = { properties: { "lineWidth": { type: "number", - // default: 5, label: "Line Width" }, "simplifyMode": { type: "enum", options: ["corners", "smooth"], // "auto"], label: "Line Mode" + }, + "fillShape": { + type: "boolean", + label: "Fill Shape" } } }, @@ -44,8 +47,12 @@ let tools = { icon: "/assets/rectangle.svg", properties: {} }, - polygon: { - icon: "assets/polygon.svg", + ellipse: { + icon: "assets/ellipse.svg", + properties: {} + }, + paint_bucket: { + icon: "/assets/paint_bucket.svg", properties: {} } } @@ -66,6 +73,7 @@ let context = { ], lineWidth: 5, simplifyMode: "smooth", + fillShape: true, } let config = { @@ -130,19 +138,31 @@ class Frame { } class Shape { - constructor(startx, starty, context, filled=true, stroked=true) { + constructor(startx, starty, context, stroked=true) { this.startx = startx; this.starty = starty; this.curves = []; this.fillStyle = context.fillStyle; + this.fillImage = context.fillImage; this.strokeStyle = context.strokeStyle; this.lineWidth = context.lineWidth - this.filled = filled; + this.filled = context.fillShape; this.stroked = stroked; } addCurve(curve) { this.curves.push(curve) } + addLine(x, y) { + let lastpoint; + if (this.curves.length) { + lastpoint = this.curves[this.curves.length - 1] + } else { + lastpoint = {x: this.startx, y: this.starty} + } + let midpoint = {x: (x + lastpoint.x) / 2, y: (y + lastpoint.y) / 2} + let curve = new Curve(midpoint.x, midpoint.y, midpoint.x, midpoint.y, x, y) + this.curves.push(curve) + } simplify(mode="corners") { // Mode can be corners, smooth or auto if (mode=="corners") { @@ -176,7 +196,7 @@ class GraphicsObject { constructor() { this.x = 0; this.y = 0; - this.rotation = 0; + this.rotation = 0; // in radians this.scale = 1; this.idx = uuidv4() @@ -188,16 +208,22 @@ class GraphicsObject { } draw(context) { let ctx = context.ctx; + ctx.translate(this.x, this.y) + ctx.rotate(this.rotation) if (this.currentFrame>=this.frames.length) { this.currentFrame = 0; } for (let child of this.children) { let idx = child.idx - child.x = this.frames[this.currentFrame][idx].x; - child.y = this.frames[this.currentFrame][idx].y; - child.rotation = this.frames[this.currentFrame][idx].rotation; - child.scale = this.frames[this.currentFrame][idx].scale; - child.draw(context) + if (idx in this.frames[this.currentFrame].keys) { + child.x = this.frames[this.currentFrame].keys[idx].x; + child.y = this.frames[this.currentFrame].keys[idx].y; + child.rotation = this.frames[this.currentFrame].keys[idx].rotation; + child.scale = this.frames[this.currentFrame].keys[idx].scale; + ctx.save() + child.draw(context) + ctx.restore() + } } for (let shape of this.frames[this.currentFrame].shapes) { ctx.beginPath() @@ -212,7 +238,12 @@ class GraphicsObject { // ctx.fill() } if (shape.filled) { - ctx.fillStyle = shape.fillStyle + if (shape.fillImage) { + let pat = ctx.createPattern(shape.fillImage, "no-repeat") + ctx.fillStyle = pat + } else { + ctx.fillStyle = shape.fillStyle + } ctx.fill() } if (shape.stroked) { @@ -224,6 +255,16 @@ class GraphicsObject { addShape(shape) { this.frames[this.currentFrame].shapes.push(shape) } + addObject(object, x=0, y=0) { + this.children.push(object) + let idx = object.idx + this.frames[this.currentFrame].keys[idx] = { + x: x, + y: y, + rotation: 0, + scale: 1, + } + } } let root = new GraphicsObject(); @@ -262,6 +303,51 @@ function stage() { stage.width = 1500 stage.height = 1000 scroller.className = "scroll" + stage.addEventListener("drop", (e) => { + e.preventDefault() + let mouse = getMousePos(stage, e) + const imageTypes = ['image/png', 'image/gif', 'image/avif', 'image/jpeg', + 'image/svg+xml', 'image/webp' + ]; + if (e.dataTransfer.items) { + let i = 0 + for (let item of e.dataTransfer.items) { + if (item.kind == "file") { + let file = item.getAsFile() + if (imageTypes.includes(file.type)) { + let img = new Image() + img.src = window.URL.createObjectURL(file) + img.ix = i + img.onload = function() { + let width = img.width + let height = img.height + let imageObject = new GraphicsObject() + let ct = { + ...context, + fillImage: img, + } + let imageShape = new Shape(0, 0, ct, false) + imageShape.addLine(width, 0) + imageShape.addLine(width, height) + imageShape.addLine(0, height) + imageShape.addLine(0, 0) + imageObject.addShape(imageShape) + context.activeObject.addObject( + imageObject, + mouse.x-width/2 + (20*img.ix), + mouse.y-height/2 + (20*img.ix)) + updateUI() + } + } + i++; + } + } + } else { + } + }) + stage.addEventListener("dragover", (e) => { + e.preventDefault() + }) canvases.push(stage) scroller.appendChild(stage) stage.addEventListener("mousedown", (e) => { @@ -330,6 +416,9 @@ function toolbar() { icon.src = tools[tool].icon toolbtn.appendChild(icon) tools_scroller.appendChild(toolbtn) + toolbtn.addEventListener("click", () => { + console.log(tool) + }) } let tools_break = document.createElement("div") tools_break.className = "horiz_break" @@ -415,6 +504,12 @@ function infopanel() { } input.value = getProperty(context, property) break; + case "boolean": + input = document.createElement("input") + input.className = "infopanel-input" + input.type = "checkbox" + input.checked = getProperty(context, property) + break; } input.addEventListener("input", () => { console.log(input.value) @@ -426,9 +521,11 @@ function infopanel() { break; case "enum": if (prop.options.indexOf(input.value) >= 0) { - // console.log(input.value) setProperty(context, property, input.value) } + break; + case "boolean": + setProperty(context, property, input.checked) } }) @@ -540,6 +637,7 @@ function updateLayout(element) { function updateUI() { for (let canvas of canvases) { let ctx = canvas.getContext("2d") + ctx.reset(); ctx.fillStyle = "white" ctx.fillRect(0,0,canvas.width,canvas.height) ctx.fillStyle = "green" From 3dee1bae1795455397281468c54ac7bb01a1e88d Mon Sep 17 00:00:00 2001 From: Skyler Lehmkuhl Date: Mon, 18 Nov 2024 04:48:17 -0500 Subject: [PATCH 16/46] use bezier library for curve handling --- src/bezier.js | 1973 +++++++++++++++++++++++++++++++++++++++++++++++++ src/main.js | 144 +++- 2 files changed, 2094 insertions(+), 23 deletions(-) create mode 100644 src/bezier.js diff --git a/src/bezier.js b/src/bezier.js new file mode 100644 index 0000000..2d975c6 --- /dev/null +++ b/src/bezier.js @@ -0,0 +1,1973 @@ +// math-inlining. +const { abs, cos, sin, acos, atan2, sqrt, pow } = Math; + +// cube root function yielding real roots +function crt(v) { + return v < 0 ? -pow(-v, 1 / 3) : pow(v, 1 / 3); +} + +// trig constants +const pi = Math.PI, + tau = 2 * pi, + quart = pi / 2, + // float precision significant decimal + epsilon = 0.000001, + // extremas used in bbox calculation and similar algorithms + nMax = Number.MAX_SAFE_INTEGER || 9007199254740991, + nMin = Number.MIN_SAFE_INTEGER || -9007199254740991, + // a zero coordinate, which is surprisingly useful + ZERO = { x: 0, y: 0, z: 0 }; + +// Bezier utility functions +const utils = { + // Legendre-Gauss abscissae with n=24 (x_i values, defined at i=n as the roots of the nth order Legendre polynomial Pn(x)) + Tvalues: [ + -0.0640568928626056260850430826247450385909, + 0.0640568928626056260850430826247450385909, + -0.1911188674736163091586398207570696318404, + 0.1911188674736163091586398207570696318404, + -0.3150426796961633743867932913198102407864, + 0.3150426796961633743867932913198102407864, + -0.4337935076260451384870842319133497124524, + 0.4337935076260451384870842319133497124524, + -0.5454214713888395356583756172183723700107, + 0.5454214713888395356583756172183723700107, + -0.6480936519369755692524957869107476266696, + 0.6480936519369755692524957869107476266696, + -0.7401241915785543642438281030999784255232, + 0.7401241915785543642438281030999784255232, + -0.8200019859739029219539498726697452080761, + 0.8200019859739029219539498726697452080761, + -0.8864155270044010342131543419821967550873, + 0.8864155270044010342131543419821967550873, + -0.9382745520027327585236490017087214496548, + 0.9382745520027327585236490017087214496548, + -0.9747285559713094981983919930081690617411, + 0.9747285559713094981983919930081690617411, + -0.9951872199970213601799974097007368118745, + 0.9951872199970213601799974097007368118745, + ], + + // Legendre-Gauss weights with n=24 (w_i values, defined by a function linked to in the Bezier primer article) + Cvalues: [ + 0.1279381953467521569740561652246953718517, + 0.1279381953467521569740561652246953718517, + 0.1258374563468282961213753825111836887264, + 0.1258374563468282961213753825111836887264, + 0.121670472927803391204463153476262425607, + 0.121670472927803391204463153476262425607, + 0.1155056680537256013533444839067835598622, + 0.1155056680537256013533444839067835598622, + 0.1074442701159656347825773424466062227946, + 0.1074442701159656347825773424466062227946, + 0.0976186521041138882698806644642471544279, + 0.0976186521041138882698806644642471544279, + 0.086190161531953275917185202983742667185, + 0.086190161531953275917185202983742667185, + 0.0733464814110803057340336152531165181193, + 0.0733464814110803057340336152531165181193, + 0.0592985849154367807463677585001085845412, + 0.0592985849154367807463677585001085845412, + 0.0442774388174198061686027482113382288593, + 0.0442774388174198061686027482113382288593, + 0.0285313886289336631813078159518782864491, + 0.0285313886289336631813078159518782864491, + 0.0123412297999871995468056670700372915759, + 0.0123412297999871995468056670700372915759, + ], + + arcfn: function (t, derivativeFn) { + const d = derivativeFn(t); + let l = d.x * d.x + d.y * d.y; + if (typeof d.z !== "undefined") { + l += d.z * d.z; + } + return sqrt(l); + }, + + compute: function (t, points, _3d) { + // shortcuts + if (t === 0) { + points[0].t = 0; + return points[0]; + } + + const order = points.length - 1; + + if (t === 1) { + points[order].t = 1; + return points[order]; + } + + const mt = 1 - t; + let p = points; + + // constant? + if (order === 0) { + points[0].t = t; + return points[0]; + } + + // linear? + if (order === 1) { + const ret = { + x: mt * p[0].x + t * p[1].x, + y: mt * p[0].y + t * p[1].y, + t: t, + }; + if (_3d) { + ret.z = mt * p[0].z + t * p[1].z; + } + return ret; + } + + // quadratic/cubic curve? + if (order < 4) { + let mt2 = mt * mt, + t2 = t * t, + a, + b, + c, + d = 0; + if (order === 2) { + p = [p[0], p[1], p[2], ZERO]; + a = mt2; + b = mt * t * 2; + c = t2; + } else if (order === 3) { + a = mt2 * mt; + b = mt2 * t * 3; + c = mt * t2 * 3; + d = t * t2; + } + const ret = { + x: a * p[0].x + b * p[1].x + c * p[2].x + d * p[3].x, + y: a * p[0].y + b * p[1].y + c * p[2].y + d * p[3].y, + t: t, + }; + if (_3d) { + ret.z = a * p[0].z + b * p[1].z + c * p[2].z + d * p[3].z; + } + return ret; + } + + // higher order curves: use de Casteljau's computation + const dCpts = JSON.parse(JSON.stringify(points)); + while (dCpts.length > 1) { + for (let i = 0; i < dCpts.length - 1; i++) { + dCpts[i] = { + x: dCpts[i].x + (dCpts[i + 1].x - dCpts[i].x) * t, + y: dCpts[i].y + (dCpts[i + 1].y - dCpts[i].y) * t, + }; + if (typeof dCpts[i].z !== "undefined") { + dCpts[i] = dCpts[i].z + (dCpts[i + 1].z - dCpts[i].z) * t; + } + } + dCpts.splice(dCpts.length - 1, 1); + } + dCpts[0].t = t; + return dCpts[0]; + }, + + computeWithRatios: function (t, points, ratios, _3d) { + const mt = 1 - t, + r = ratios, + p = points; + + let f1 = r[0], + f2 = r[1], + f3 = r[2], + f4 = r[3], + d; + + // spec for linear + f1 *= mt; + f2 *= t; + + if (p.length === 2) { + d = f1 + f2; + return { + x: (f1 * p[0].x + f2 * p[1].x) / d, + y: (f1 * p[0].y + f2 * p[1].y) / d, + z: !_3d ? false : (f1 * p[0].z + f2 * p[1].z) / d, + t: t, + }; + } + + // upgrade to quadratic + f1 *= mt; + f2 *= 2 * mt; + f3 *= t * t; + + if (p.length === 3) { + d = f1 + f2 + f3; + return { + x: (f1 * p[0].x + f2 * p[1].x + f3 * p[2].x) / d, + y: (f1 * p[0].y + f2 * p[1].y + f3 * p[2].y) / d, + z: !_3d ? false : (f1 * p[0].z + f2 * p[1].z + f3 * p[2].z) / d, + t: t, + }; + } + + // upgrade to cubic + f1 *= mt; + f2 *= 1.5 * mt; + f3 *= 3 * mt; + f4 *= t * t * t; + + if (p.length === 4) { + d = f1 + f2 + f3 + f4; + return { + x: (f1 * p[0].x + f2 * p[1].x + f3 * p[2].x + f4 * p[3].x) / d, + y: (f1 * p[0].y + f2 * p[1].y + f3 * p[2].y + f4 * p[3].y) / d, + z: !_3d + ? false + : (f1 * p[0].z + f2 * p[1].z + f3 * p[2].z + f4 * p[3].z) / d, + t: t, + }; + } + }, + + derive: function (points, _3d) { + const dpoints = []; + for (let p = points, d = p.length, c = d - 1; d > 1; d--, c--) { + const list = []; + for (let j = 0, dpt; j < c; j++) { + dpt = { + x: c * (p[j + 1].x - p[j].x), + y: c * (p[j + 1].y - p[j].y), + }; + if (_3d) { + dpt.z = c * (p[j + 1].z - p[j].z); + } + list.push(dpt); + } + dpoints.push(list); + p = list; + } + return dpoints; + }, + + between: function (v, m, M) { + return ( + (m <= v && v <= M) || + utils.approximately(v, m) || + utils.approximately(v, M) + ); + }, + + approximately: function (a, b, precision) { + return abs(a - b) <= (precision || epsilon); + }, + + length: function (derivativeFn) { + const z = 0.5, + len = utils.Tvalues.length; + + let sum = 0; + + for (let i = 0, t; i < len; i++) { + t = z * utils.Tvalues[i] + z; + sum += utils.Cvalues[i] * utils.arcfn(t, derivativeFn); + } + return z * sum; + }, + + map: function (v, ds, de, ts, te) { + const d1 = de - ds, + d2 = te - ts, + v2 = v - ds, + r = v2 / d1; + return ts + d2 * r; + }, + + lerp: function (r, v1, v2) { + const ret = { + x: v1.x + r * (v2.x - v1.x), + y: v1.y + r * (v2.y - v1.y), + }; + if (v1.z !== undefined && v2.z !== undefined) { + ret.z = v1.z + r * (v2.z - v1.z); + } + return ret; + }, + + pointToString: function (p) { + let s = p.x + "/" + p.y; + if (typeof p.z !== "undefined") { + s += "/" + p.z; + } + return s; + }, + + pointsToString: function (points) { + return "[" + points.map(utils.pointToString).join(", ") + "]"; + }, + + copy: function (obj) { + return JSON.parse(JSON.stringify(obj)); + }, + + angle: function (o, v1, v2) { + const dx1 = v1.x - o.x, + dy1 = v1.y - o.y, + dx2 = v2.x - o.x, + dy2 = v2.y - o.y, + cross = dx1 * dy2 - dy1 * dx2, + dot = dx1 * dx2 + dy1 * dy2; + return atan2(cross, dot); + }, + + // round as string, to avoid rounding errors + round: function (v, d) { + const s = "" + v; + const pos = s.indexOf("."); + return parseFloat(s.substring(0, pos + 1 + d)); + }, + + dist: function (p1, p2) { + const dx = p1.x - p2.x, + dy = p1.y - p2.y; + return sqrt(dx * dx + dy * dy); + }, + + closest: function (LUT, point) { + let mdist = pow(2, 63), + mpos, + d; + LUT.forEach(function (p, idx) { + d = utils.dist(point, p); + if (d < mdist) { + mdist = d; + mpos = idx; + } + }); + return { mdist: mdist, mpos: mpos }; + }, + + abcratio: function (t, n) { + // see ratio(t) note on http://pomax.github.io/bezierinfo/#abc + if (n !== 2 && n !== 3) { + return false; + } + if (typeof t === "undefined") { + t = 0.5; + } else if (t === 0 || t === 1) { + return t; + } + const bottom = pow(t, n) + pow(1 - t, n), + top = bottom - 1; + return abs(top / bottom); + }, + + projectionratio: function (t, n) { + // see u(t) note on http://pomax.github.io/bezierinfo/#abc + if (n !== 2 && n !== 3) { + return false; + } + if (typeof t === "undefined") { + t = 0.5; + } else if (t === 0 || t === 1) { + return t; + } + const top = pow(1 - t, n), + bottom = pow(t, n) + top; + return top / bottom; + }, + + lli8: function (x1, y1, x2, y2, x3, y3, x4, y4) { + const nx = + (x1 * y2 - y1 * x2) * (x3 - x4) - (x1 - x2) * (x3 * y4 - y3 * x4), + ny = (x1 * y2 - y1 * x2) * (y3 - y4) - (y1 - y2) * (x3 * y4 - y3 * x4), + d = (x1 - x2) * (y3 - y4) - (y1 - y2) * (x3 - x4); + if (d == 0) { + return false; + } + return { x: nx / d, y: ny / d }; + }, + + lli4: function (p1, p2, p3, p4) { + const x1 = p1.x, + y1 = p1.y, + x2 = p2.x, + y2 = p2.y, + x3 = p3.x, + y3 = p3.y, + x4 = p4.x, + y4 = p4.y; + return utils.lli8(x1, y1, x2, y2, x3, y3, x4, y4); + }, + + lli: function (v1, v2) { + return utils.lli4(v1, v1.c, v2, v2.c); + }, + + makeline: function (p1, p2) { + return new Bezier( + p1.x, + p1.y, + (p1.x + p2.x) / 2, + (p1.y + p2.y) / 2, + p2.x, + p2.y + ); + }, + + findbbox: function (sections) { + let mx = nMax, + my = nMax, + MX = nMin, + MY = nMin; + sections.forEach(function (s) { + const bbox = s.bbox(); + if (mx > bbox.x.min) mx = bbox.x.min; + if (my > bbox.y.min) my = bbox.y.min; + if (MX < bbox.x.max) MX = bbox.x.max; + if (MY < bbox.y.max) MY = bbox.y.max; + }); + return { + x: { min: mx, mid: (mx + MX) / 2, max: MX, size: MX - mx }, + y: { min: my, mid: (my + MY) / 2, max: MY, size: MY - my }, + }; + }, + + shapeintersections: function ( + s1, + bbox1, + s2, + bbox2, + curveIntersectionThreshold + ) { + if (!utils.bboxoverlap(bbox1, bbox2)) return []; + const intersections = []; + const a1 = [s1.startcap, s1.forward, s1.back, s1.endcap]; + const a2 = [s2.startcap, s2.forward, s2.back, s2.endcap]; + a1.forEach(function (l1) { + if (l1.virtual) return; + a2.forEach(function (l2) { + if (l2.virtual) return; + const iss = l1.intersects(l2, curveIntersectionThreshold); + if (iss.length > 0) { + iss.c1 = l1; + iss.c2 = l2; + iss.s1 = s1; + iss.s2 = s2; + intersections.push(iss); + } + }); + }); + return intersections; + }, + + makeshape: function (forward, back, curveIntersectionThreshold) { + const bpl = back.points.length; + const fpl = forward.points.length; + const start = utils.makeline(back.points[bpl - 1], forward.points[0]); + const end = utils.makeline(forward.points[fpl - 1], back.points[0]); + const shape = { + startcap: start, + forward: forward, + back: back, + endcap: end, + bbox: utils.findbbox([start, forward, back, end]), + }; + shape.intersections = function (s2) { + return utils.shapeintersections( + shape, + shape.bbox, + s2, + s2.bbox, + curveIntersectionThreshold + ); + }; + return shape; + }, + + getminmax: function (curve, d, list) { + if (!list) return { min: 0, max: 0 }; + let min = nMax, + max = nMin, + t, + c; + if (list.indexOf(0) === -1) { + list = [0].concat(list); + } + if (list.indexOf(1) === -1) { + list.push(1); + } + for (let i = 0, len = list.length; i < len; i++) { + t = list[i]; + c = curve.get(t); + if (c[d] < min) { + min = c[d]; + } + if (c[d] > max) { + max = c[d]; + } + } + return { min: min, mid: (min + max) / 2, max: max, size: max - min }; + }, + + align: function (points, line) { + const tx = line.p1.x, + ty = line.p1.y, + a = -atan2(line.p2.y - ty, line.p2.x - tx), + d = function (v) { + return { + x: (v.x - tx) * cos(a) - (v.y - ty) * sin(a), + y: (v.x - tx) * sin(a) + (v.y - ty) * cos(a), + }; + }; + return points.map(d); + }, + + roots: function (points, line) { + line = line || { p1: { x: 0, y: 0 }, p2: { x: 1, y: 0 } }; + + const order = points.length - 1; + const aligned = utils.align(points, line); + const reduce = function (t) { + return 0 <= t && t <= 1; + }; + + if (order === 2) { + const a = aligned[0].y, + b = aligned[1].y, + c = aligned[2].y, + d = a - 2 * b + c; + if (d !== 0) { + const m1 = -sqrt(b * b - a * c), + m2 = -a + b, + v1 = -(m1 + m2) / d, + v2 = -(-m1 + m2) / d; + return [v1, v2].filter(reduce); + } else if (b !== c && d === 0) { + return [(2 * b - c) / (2 * b - 2 * c)].filter(reduce); + } + return []; + } + + // see http://www.trans4mind.com/personal_development/mathematics/polynomials/cubicAlgebra.htm + const pa = aligned[0].y, + pb = aligned[1].y, + pc = aligned[2].y, + pd = aligned[3].y; + + let d = -pa + 3 * pb - 3 * pc + pd, + a = 3 * pa - 6 * pb + 3 * pc, + b = -3 * pa + 3 * pb, + c = pa; + + if (utils.approximately(d, 0)) { + // this is not a cubic curve. + if (utils.approximately(a, 0)) { + // in fact, this is not a quadratic curve either. + if (utils.approximately(b, 0)) { + // in fact in fact, there are no solutions. + return []; + } + // linear solution: + return [-c / b].filter(reduce); + } + // quadratic solution: + const q = sqrt(b * b - 4 * a * c), + a2 = 2 * a; + return [(q - b) / a2, (-b - q) / a2].filter(reduce); + } + + // at this point, we know we need a cubic solution: + + a /= d; + b /= d; + c /= d; + + const p = (3 * b - a * a) / 3, + p3 = p / 3, + q = (2 * a * a * a - 9 * a * b + 27 * c) / 27, + q2 = q / 2, + discriminant = q2 * q2 + p3 * p3 * p3; + + let u1, v1, x1, x2, x3; + if (discriminant < 0) { + const mp3 = -p / 3, + mp33 = mp3 * mp3 * mp3, + r = sqrt(mp33), + t = -q / (2 * r), + cosphi = t < -1 ? -1 : t > 1 ? 1 : t, + phi = acos(cosphi), + crtr = crt(r), + t1 = 2 * crtr; + x1 = t1 * cos(phi / 3) - a / 3; + x2 = t1 * cos((phi + tau) / 3) - a / 3; + x3 = t1 * cos((phi + 2 * tau) / 3) - a / 3; + return [x1, x2, x3].filter(reduce); + } else if (discriminant === 0) { + u1 = q2 < 0 ? crt(-q2) : -crt(q2); + x1 = 2 * u1 - a / 3; + x2 = -u1 - a / 3; + return [x1, x2].filter(reduce); + } else { + const sd = sqrt(discriminant); + u1 = crt(-q2 + sd); + v1 = crt(q2 + sd); + return [u1 - v1 - a / 3].filter(reduce); + } + }, + + droots: function (p) { + // quadratic roots are easy + if (p.length === 3) { + const a = p[0], + b = p[1], + c = p[2], + d = a - 2 * b + c; + if (d !== 0) { + const m1 = -sqrt(b * b - a * c), + m2 = -a + b, + v1 = -(m1 + m2) / d, + v2 = -(-m1 + m2) / d; + return [v1, v2]; + } else if (b !== c && d === 0) { + return [(2 * b - c) / (2 * (b - c))]; + } + return []; + } + + // linear roots are even easier + if (p.length === 2) { + const a = p[0], + b = p[1]; + if (a !== b) { + return [a / (a - b)]; + } + return []; + } + + return []; + }, + + curvature: function (t, d1, d2, _3d, kOnly) { + let num, + dnm, + adk, + dk, + k = 0, + r = 0; + + // + // We're using the following formula for curvature: + // + // x'y" - y'x" + // k(t) = ------------------ + // (x'² + y'²)^(3/2) + // + // from https://en.wikipedia.org/wiki/Radius_of_curvature#Definition + // + // With it corresponding 3D counterpart: + // + // sqrt( (y'z" - y"z')² + (z'x" - z"x')² + (x'y" - x"y')²) + // k(t) = ------------------------------------------------------- + // (x'² + y'² + z'²)^(3/2) + // + + const d = utils.compute(t, d1); + const dd = utils.compute(t, d2); + const qdsum = d.x * d.x + d.y * d.y; + + if (_3d) { + num = sqrt( + pow(d.y * dd.z - dd.y * d.z, 2) + + pow(d.z * dd.x - dd.z * d.x, 2) + + pow(d.x * dd.y - dd.x * d.y, 2) + ); + dnm = pow(qdsum + d.z * d.z, 3 / 2); + } else { + num = d.x * dd.y - d.y * dd.x; + dnm = pow(qdsum, 3 / 2); + } + + if (num === 0 || dnm === 0) { + return { k: 0, r: 0 }; + } + + k = num / dnm; + r = dnm / num; + + // We're also computing the derivative of kappa, because + // there is value in knowing the rate of change for the + // curvature along the curve. And we're just going to + // ballpark it based on an epsilon. + if (!kOnly) { + // compute k'(t) based on the interval before, and after it, + // to at least try to not introduce forward/backward pass bias. + const pk = utils.curvature(t - 0.001, d1, d2, _3d, true).k; + const nk = utils.curvature(t + 0.001, d1, d2, _3d, true).k; + dk = (nk - k + (k - pk)) / 2; + adk = (abs(nk - k) + abs(k - pk)) / 2; + } + + return { k: k, r: r, dk: dk, adk: adk }; + }, + + inflections: function (points) { + if (points.length < 4) return []; + + // FIXME: TODO: add in inflection abstraction for quartic+ curves? + + const p = utils.align(points, { p1: points[0], p2: points.slice(-1)[0] }), + a = p[2].x * p[1].y, + b = p[3].x * p[1].y, + c = p[1].x * p[2].y, + d = p[3].x * p[2].y, + v1 = 18 * (-3 * a + 2 * b + 3 * c - d), + v2 = 18 * (3 * a - b - 3 * c), + v3 = 18 * (c - a); + + if (utils.approximately(v1, 0)) { + if (!utils.approximately(v2, 0)) { + let t = -v3 / v2; + if (0 <= t && t <= 1) return [t]; + } + return []; + } + + const trm = v2 * v2 - 4 * v1 * v3, + sq = Math.sqrt(trm), + d2 = 2 * v1; + + if (utils.approximately(d2, 0)) return []; + + return [(sq - v2) / d2, -(v2 + sq) / d2].filter(function (r) { + return 0 <= r && r <= 1; + }); + }, + + bboxoverlap: function (b1, b2) { + const dims = ["x", "y"], + len = dims.length; + + for (let i = 0, dim, l, t, d; i < len; i++) { + dim = dims[i]; + l = b1[dim].mid; + t = b2[dim].mid; + d = (b1[dim].size + b2[dim].size) / 2; + if (abs(l - t) >= d) return false; + } + return true; + }, + + expandbox: function (bbox, _bbox) { + if (_bbox.x.min < bbox.x.min) { + bbox.x.min = _bbox.x.min; + } + if (_bbox.y.min < bbox.y.min) { + bbox.y.min = _bbox.y.min; + } + if (_bbox.z && _bbox.z.min < bbox.z.min) { + bbox.z.min = _bbox.z.min; + } + if (_bbox.x.max > bbox.x.max) { + bbox.x.max = _bbox.x.max; + } + if (_bbox.y.max > bbox.y.max) { + bbox.y.max = _bbox.y.max; + } + if (_bbox.z && _bbox.z.max > bbox.z.max) { + bbox.z.max = _bbox.z.max; + } + bbox.x.mid = (bbox.x.min + bbox.x.max) / 2; + bbox.y.mid = (bbox.y.min + bbox.y.max) / 2; + if (bbox.z) { + bbox.z.mid = (bbox.z.min + bbox.z.max) / 2; + } + bbox.x.size = bbox.x.max - bbox.x.min; + bbox.y.size = bbox.y.max - bbox.y.min; + if (bbox.z) { + bbox.z.size = bbox.z.max - bbox.z.min; + } + }, + + pairiteration: function (c1, c2, curveIntersectionThreshold) { + const c1b = c1.bbox(), + c2b = c2.bbox(), + r = 100000, + threshold = curveIntersectionThreshold || 0.5; + + if ( + c1b.x.size + c1b.y.size < threshold && + c2b.x.size + c2b.y.size < threshold + ) { + return [ + (((r * (c1._t1 + c1._t2)) / 2) | 0) / r + + "/" + + (((r * (c2._t1 + c2._t2)) / 2) | 0) / r, + ]; + } + + let cc1 = c1.split(0.5), + cc2 = c2.split(0.5), + pairs = [ + { left: cc1.left, right: cc2.left }, + { left: cc1.left, right: cc2.right }, + { left: cc1.right, right: cc2.right }, + { left: cc1.right, right: cc2.left }, + ]; + + pairs = pairs.filter(function (pair) { + return utils.bboxoverlap(pair.left.bbox(), pair.right.bbox()); + }); + + let results = []; + + if (pairs.length === 0) return results; + + pairs.forEach(function (pair) { + results = results.concat( + utils.pairiteration(pair.left, pair.right, threshold) + ); + }); + + results = results.filter(function (v, i) { + return results.indexOf(v) === i; + }); + + return results; + }, + + getccenter: function (p1, p2, p3) { + const dx1 = p2.x - p1.x, + dy1 = p2.y - p1.y, + dx2 = p3.x - p2.x, + dy2 = p3.y - p2.y, + dx1p = dx1 * cos(quart) - dy1 * sin(quart), + dy1p = dx1 * sin(quart) + dy1 * cos(quart), + dx2p = dx2 * cos(quart) - dy2 * sin(quart), + dy2p = dx2 * sin(quart) + dy2 * cos(quart), + // chord midpoints + mx1 = (p1.x + p2.x) / 2, + my1 = (p1.y + p2.y) / 2, + mx2 = (p2.x + p3.x) / 2, + my2 = (p2.y + p3.y) / 2, + // midpoint offsets + mx1n = mx1 + dx1p, + my1n = my1 + dy1p, + mx2n = mx2 + dx2p, + my2n = my2 + dy2p, + // intersection of these lines: + arc = utils.lli8(mx1, my1, mx1n, my1n, mx2, my2, mx2n, my2n), + r = utils.dist(arc, p1); + + // arc start/end values, over mid point: + let s = atan2(p1.y - arc.y, p1.x - arc.x), + m = atan2(p2.y - arc.y, p2.x - arc.x), + e = atan2(p3.y - arc.y, p3.x - arc.x), + _; + + // determine arc direction (cw/ccw correction) + if (s < e) { + // if s m || m > e) { + s += tau; + } + if (s > e) { + _ = e; + e = s; + s = _; + } + } else { + // if e 4) { + if (arguments.length !== 1) { + throw new Error( + "Only new Bezier(point[]) is accepted for 4th and higher order curves" + ); + } + higher = true; + } + } else { + if (len !== 6 && len !== 8 && len !== 9 && len !== 12) { + if (arguments.length !== 1) { + throw new Error( + "Only new Bezier(point[]) is accepted for 4th and higher order curves" + ); + } + } + } + + const _3d = (this._3d = + (!higher && (len === 9 || len === 12)) || + (coords && coords[0] && typeof coords[0].z !== "undefined")); + + const points = (this.points = []); + for (let idx = 0, step = _3d ? 3 : 2; idx < len; idx += step) { + var point = { + x: args[idx], + y: args[idx + 1], + }; + if (_3d) { + point.z = args[idx + 2]; + } + points.push(point); + } + const order = (this.order = points.length - 1); + + const dims = (this.dims = ["x", "y"]); + if (_3d) dims.push("z"); + this.dimlen = dims.length; + + // is this curve, practically speaking, a straight line? + const aligned = utils.align(points, { p1: points[0], p2: points[order] }); + const baselength = utils.dist(points[0], points[order]); + this._linear = aligned.reduce((t, p) => t + abs$1(p.y), 0) < baselength / 50; + + this._lut = []; + + this._t1 = 0; + this._t2 = 1; + this.update(); + } + + static quadraticFromPoints(p1, p2, p3, t) { + if (typeof t === "undefined") { + t = 0.5; + } + // shortcuts, although they're really dumb + if (t === 0) { + return new Bezier(p2, p2, p3); + } + if (t === 1) { + return new Bezier(p1, p2, p2); + } + // real fitting. + const abc = Bezier.getABC(2, p1, p2, p3, t); + return new Bezier(p1, abc.A, p3); + } + + static cubicFromPoints(S, B, E, t, d1) { + if (typeof t === "undefined") { + t = 0.5; + } + const abc = Bezier.getABC(3, S, B, E, t); + if (typeof d1 === "undefined") { + d1 = utils.dist(B, abc.C); + } + const d2 = (d1 * (1 - t)) / t; + + const selen = utils.dist(S, E), + lx = (E.x - S.x) / selen, + ly = (E.y - S.y) / selen, + bx1 = d1 * lx, + by1 = d1 * ly, + bx2 = d2 * lx, + by2 = d2 * ly; + // derivation of new hull coordinates + const e1 = { x: B.x - bx1, y: B.y - by1 }, + e2 = { x: B.x + bx2, y: B.y + by2 }, + A = abc.A, + v1 = { x: A.x + (e1.x - A.x) / (1 - t), y: A.y + (e1.y - A.y) / (1 - t) }, + v2 = { x: A.x + (e2.x - A.x) / t, y: A.y + (e2.y - A.y) / t }, + nc1 = { x: S.x + (v1.x - S.x) / t, y: S.y + (v1.y - S.y) / t }, + nc2 = { + x: E.x + (v2.x - E.x) / (1 - t), + y: E.y + (v2.y - E.y) / (1 - t), + }; + // ...done + return new Bezier(S, nc1, nc2, E); + } + + static getUtils() { + return utils; + } + + getUtils() { + return Bezier.getUtils(); + } + + static get PolyBezier() { + return PolyBezier; + } + + valueOf() { + return this.toString(); + } + + toString() { + return utils.pointsToString(this.points); + } + + toSVG() { + if (this._3d) return false; + const p = this.points, + x = p[0].x, + y = p[0].y, + s = ["M", x, y, this.order === 2 ? "Q" : "C"]; + for (let i = 1, last = p.length; i < last; i++) { + s.push(p[i].x); + s.push(p[i].y); + } + return s.join(" "); + } + + setRatios(ratios) { + if (ratios.length !== this.points.length) { + throw new Error("incorrect number of ratio values"); + } + this.ratios = ratios; + this._lut = []; // invalidate any precomputed LUT + } + + verify() { + const print = this.coordDigest(); + if (print !== this._print) { + this._print = print; + this.update(); + } + } + + coordDigest() { + return this.points + .map(function (c, pos) { + return "" + pos + c.x + c.y + (c.z ? c.z : 0); + }) + .join(""); + } + + update() { + // invalidate any precomputed LUT + this._lut = []; + this.dpoints = utils.derive(this.points, this._3d); + this.computedirection(); + } + + computedirection() { + const points = this.points; + const angle = utils.angle(points[0], points[this.order], points[1]); + this.clockwise = angle > 0; + } + + length() { + return utils.length(this.derivative.bind(this)); + } + + static getABC(order = 2, S, B, E, t = 0.5) { + const u = utils.projectionratio(t, order), + um = 1 - u, + C = { + x: u * S.x + um * E.x, + y: u * S.y + um * E.y, + }, + s = utils.abcratio(t, order), + A = { + x: B.x + (B.x - C.x) / s, + y: B.y + (B.y - C.y) / s, + }; + return { A, B, C, S, E }; + } + + getABC(t, B) { + B = B || this.get(t); + let S = this.points[0]; + let E = this.points[this.order]; + return Bezier.getABC(this.order, S, B, E, t); + } + + getLUT(steps) { + this.verify(); + steps = steps || 100; + if (this._lut.length === steps) { + return this._lut; + } + this._lut = []; + // n steps means n+1 points + steps++; + this._lut = []; + for (let i = 0, p, t; i < steps; i++) { + t = i / (steps - 1); + p = this.compute(t); + p.t = t; + this._lut.push(p); + } + return this._lut; + } + + on(point, error) { + error = error || 5; + const lut = this.getLUT(), + hits = []; + for (let i = 0, c, t = 0; i < lut.length; i++) { + c = lut[i]; + if (utils.dist(c, point) < error) { + hits.push(c); + t += i / lut.length; + } + } + if (!hits.length) return false; + return (t /= hits.length); + } + + project(point) { + // step 1: coarse check + const LUT = this.getLUT(), + l = LUT.length - 1, + closest = utils.closest(LUT, point), + mpos = closest.mpos, + t1 = (mpos - 1) / l, + t2 = (mpos + 1) / l, + step = 0.1 / l; + + // step 2: fine check + let mdist = closest.mdist, + t = t1, + ft = t, + p; + mdist += 1; + for (let d; t < t2 + step; t += step) { + p = this.compute(t); + d = utils.dist(point, p); + if (d < mdist) { + mdist = d; + ft = t; + } + } + ft = ft < 0 ? 0 : ft > 1 ? 1 : ft; + p = this.compute(ft); + p.t = ft; + p.d = mdist; + return p; + } + + get(t) { + return this.compute(t); + } + + point(idx) { + return this.points[idx]; + } + + compute(t) { + if (this.ratios) { + return utils.computeWithRatios(t, this.points, this.ratios, this._3d); + } + return utils.compute(t, this.points, this._3d, this.ratios); + } + + raise() { + const p = this.points, + np = [p[0]], + k = p.length; + for (let i = 1, pi, pim; i < k; i++) { + pi = p[i]; + pim = p[i - 1]; + np[i] = { + x: ((k - i) / k) * pi.x + (i / k) * pim.x, + y: ((k - i) / k) * pi.y + (i / k) * pim.y, + }; + } + np[k] = p[k - 1]; + return new Bezier(np); + } + + derivative(t) { + return utils.compute(t, this.dpoints[0], this._3d); + } + + dderivative(t) { + return utils.compute(t, this.dpoints[1], this._3d); + } + + align() { + let p = this.points; + return new Bezier(utils.align(p, { p1: p[0], p2: p[p.length - 1] })); + } + + curvature(t) { + return utils.curvature(t, this.dpoints[0], this.dpoints[1], this._3d); + } + + inflections() { + return utils.inflections(this.points); + } + + normal(t) { + return this._3d ? this.__normal3(t) : this.__normal2(t); + } + + __normal2(t) { + const d = this.derivative(t); + const q = sqrt$1(d.x * d.x + d.y * d.y); + return { x: -d.y / q, y: d.x / q }; + } + + __normal3(t) { + // see http://stackoverflow.com/questions/25453159 + const r1 = this.derivative(t), + r2 = this.derivative(t + 0.01), + q1 = sqrt$1(r1.x * r1.x + r1.y * r1.y + r1.z * r1.z), + q2 = sqrt$1(r2.x * r2.x + r2.y * r2.y + r2.z * r2.z); + r1.x /= q1; + r1.y /= q1; + r1.z /= q1; + r2.x /= q2; + r2.y /= q2; + r2.z /= q2; + // cross product + const c = { + x: r2.y * r1.z - r2.z * r1.y, + y: r2.z * r1.x - r2.x * r1.z, + z: r2.x * r1.y - r2.y * r1.x, + }; + const m = sqrt$1(c.x * c.x + c.y * c.y + c.z * c.z); + c.x /= m; + c.y /= m; + c.z /= m; + // rotation matrix + const R = [ + c.x * c.x, + c.x * c.y - c.z, + c.x * c.z + c.y, + c.x * c.y + c.z, + c.y * c.y, + c.y * c.z - c.x, + c.x * c.z - c.y, + c.y * c.z + c.x, + c.z * c.z, + ]; + // normal vector: + const n = { + x: R[0] * r1.x + R[1] * r1.y + R[2] * r1.z, + y: R[3] * r1.x + R[4] * r1.y + R[5] * r1.z, + z: R[6] * r1.x + R[7] * r1.y + R[8] * r1.z, + }; + return n; + } + + hull(t) { + let p = this.points, + _p = [], + q = [], + idx = 0; + q[idx++] = p[0]; + q[idx++] = p[1]; + q[idx++] = p[2]; + if (this.order === 3) { + q[idx++] = p[3]; + } + // we lerp between all points at each iteration, until we have 1 point left. + while (p.length > 1) { + _p = []; + for (let i = 0, pt, l = p.length - 1; i < l; i++) { + pt = utils.lerp(t, p[i], p[i + 1]); + q[idx++] = pt; + _p.push(pt); + } + p = _p; + } + return q; + } + + split(t1, t2) { + // shortcuts + if (t1 === 0 && !!t2) { + return this.split(t2).left; + } + if (t2 === 1) { + return this.split(t1).right; + } + + // no shortcut: use "de Casteljau" iteration. + const q = this.hull(t1); + const result = { + left: + this.order === 2 + ? new Bezier([q[0], q[3], q[5]]) + : new Bezier([q[0], q[4], q[7], q[9]]), + right: + this.order === 2 + ? new Bezier([q[5], q[4], q[2]]) + : new Bezier([q[9], q[8], q[6], q[3]]), + span: q, + }; + + // make sure we bind _t1/_t2 information! + result.left._t1 = utils.map(0, 0, 1, this._t1, this._t2); + result.left._t2 = utils.map(t1, 0, 1, this._t1, this._t2); + result.right._t1 = utils.map(t1, 0, 1, this._t1, this._t2); + result.right._t2 = utils.map(1, 0, 1, this._t1, this._t2); + + // if we have no t2, we're done + if (!t2) { + return result; + } + + // if we have a t2, split again: + t2 = utils.map(t2, t1, 1, 0, 1); + return result.right.split(t2).left; + } + + extrema() { + const result = {}; + let roots = []; + + this.dims.forEach( + function (dim) { + let mfn = function (v) { + return v[dim]; + }; + let p = this.dpoints[0].map(mfn); + result[dim] = utils.droots(p); + if (this.order === 3) { + p = this.dpoints[1].map(mfn); + result[dim] = result[dim].concat(utils.droots(p)); + } + result[dim] = result[dim].filter(function (t) { + return t >= 0 && t <= 1; + }); + roots = roots.concat(result[dim].sort(utils.numberSort)); + }.bind(this) + ); + + result.values = roots.sort(utils.numberSort).filter(function (v, idx) { + return roots.indexOf(v) === idx; + }); + + return result; + } + + bbox() { + const extrema = this.extrema(), + result = {}; + this.dims.forEach( + function (d) { + result[d] = utils.getminmax(this, d, extrema[d]); + }.bind(this) + ); + return result; + } + + overlaps(curve) { + const lbbox = this.bbox(), + tbbox = curve.bbox(); + return utils.bboxoverlap(lbbox, tbbox); + } + + offset(t, d) { + if (typeof d !== "undefined") { + const c = this.get(t), + n = this.normal(t); + const ret = { + c: c, + n: n, + x: c.x + n.x * d, + y: c.y + n.y * d, + }; + if (this._3d) { + ret.z = c.z + n.z * d; + } + return ret; + } + if (this._linear) { + const nv = this.normal(0), + coords = this.points.map(function (p) { + const ret = { + x: p.x + t * nv.x, + y: p.y + t * nv.y, + }; + if (p.z && nv.z) { + ret.z = p.z + t * nv.z; + } + return ret; + }); + return [new Bezier(coords)]; + } + return this.reduce().map(function (s) { + if (s._linear) { + return s.offset(t)[0]; + } + return s.scale(t); + }); + } + + simple() { + if (this.order === 3) { + const a1 = utils.angle(this.points[0], this.points[3], this.points[1]); + const a2 = utils.angle(this.points[0], this.points[3], this.points[2]); + if ((a1 > 0 && a2 < 0) || (a1 < 0 && a2 > 0)) return false; + } + const n1 = this.normal(0); + const n2 = this.normal(1); + let s = n1.x * n2.x + n1.y * n2.y; + if (this._3d) { + s += n1.z * n2.z; + } + return abs$1(acos$1(s)) < pi$1 / 3; + } + + reduce() { + // TODO: examine these var types in more detail... + let i, + t1 = 0, + t2 = 0, + step = 0.01, + segment, + pass1 = [], + pass2 = []; + // first pass: split on extrema + let extrema = this.extrema().values; + if (extrema.indexOf(0) === -1) { + extrema = [0].concat(extrema); + } + if (extrema.indexOf(1) === -1) { + extrema.push(1); + } + + for (t1 = extrema[0], i = 1; i < extrema.length; i++) { + t2 = extrema[i]; + segment = this.split(t1, t2); + segment._t1 = t1; + segment._t2 = t2; + pass1.push(segment); + t1 = t2; + } + + // second pass: further reduce these segments to simple segments + pass1.forEach(function (p1) { + t1 = 0; + t2 = 0; + while (t2 <= 1) { + for (t2 = t1 + step; t2 <= 1 + step; t2 += step) { + segment = p1.split(t1, t2); + if (!segment.simple()) { + t2 -= step; + if (abs$1(t1 - t2) < step) { + // we can never form a reduction + return []; + } + segment = p1.split(t1, t2); + segment._t1 = utils.map(t1, 0, 1, p1._t1, p1._t2); + segment._t2 = utils.map(t2, 0, 1, p1._t1, p1._t2); + pass2.push(segment); + t1 = t2; + break; + } + } + } + if (t1 < 1) { + segment = p1.split(t1, 1); + segment._t1 = utils.map(t1, 0, 1, p1._t1, p1._t2); + segment._t2 = p1._t2; + pass2.push(segment); + } + }); + return pass2; + } + + translate(v, d1, d2) { + d2 = typeof d2 === "number" ? d2 : d1; + + // TODO: make this take curves with control points outside + // of the start-end interval into account + + const o = this.order; + let d = this.points.map((_, i) => (1 - i / o) * d1 + (i / o) * d2); + return new Bezier( + this.points.map((p, i) => ({ + x: p.x + v.x * d[i], + y: p.y + v.y * d[i], + })) + ); + } + + scale(d) { + const order = this.order; + let distanceFn = false; + if (typeof d === "function") { + distanceFn = d; + } + if (distanceFn && order === 2) { + return this.raise().scale(distanceFn); + } + + // TODO: add special handling for non-linear degenerate curves. + + const clockwise = this.clockwise; + const points = this.points; + + if (this._linear) { + return this.translate( + this.normal(0), + distanceFn ? distanceFn(0) : d, + distanceFn ? distanceFn(1) : d + ); + } + + const r1 = distanceFn ? distanceFn(0) : d; + const r2 = distanceFn ? distanceFn(1) : d; + const v = [this.offset(0, 10), this.offset(1, 10)]; + const np = []; + const o = utils.lli4(v[0], v[0].c, v[1], v[1].c); + + if (!o) { + throw new Error("cannot scale this curve. Try reducing it first."); + } + + // move all points by distance 'd' wrt the origin 'o', + // and move end points by fixed distance along normal. + [0, 1].forEach(function (t) { + const p = (np[t * order] = utils.copy(points[t * order])); + p.x += (t ? r2 : r1) * v[t].n.x; + p.y += (t ? r2 : r1) * v[t].n.y; + }); + + if (!distanceFn) { + // move control points to lie on the intersection of the offset + // derivative vector, and the origin-through-control vector + [0, 1].forEach((t) => { + if (order === 2 && !!t) return; + const p = np[t * order]; + const d = this.derivative(t); + const p2 = { x: p.x + d.x, y: p.y + d.y }; + np[t + 1] = utils.lli4(p, p2, o, points[t + 1]); + }); + return new Bezier(np); + } + + // move control points by "however much necessary to + // ensure the correct tangent to endpoint". + [0, 1].forEach(function (t) { + if (order === 2 && !!t) return; + var p = points[t + 1]; + var ov = { + x: p.x - o.x, + y: p.y - o.y, + }; + var rc = distanceFn ? distanceFn((t + 1) / order) : d; + if (distanceFn && !clockwise) rc = -rc; + var m = sqrt$1(ov.x * ov.x + ov.y * ov.y); + ov.x /= m; + ov.y /= m; + np[t + 1] = { + x: p.x + rc * ov.x, + y: p.y + rc * ov.y, + }; + }); + return new Bezier(np); + } + + outline(d1, d2, d3, d4) { + d2 = d2 === undefined ? d1 : d2; + + if (this._linear) { + // TODO: find the actual extrema, because they might + // be before the start, or past the end. + + const n = this.normal(0); + const start = this.points[0]; + const end = this.points[this.points.length - 1]; + let s, mid, e; + + if (d3 === undefined) { + d3 = d1; + d4 = d2; + } + + s = { x: start.x + n.x * d1, y: start.y + n.y * d1 }; + e = { x: end.x + n.x * d3, y: end.y + n.y * d3 }; + mid = { x: (s.x + e.x) / 2, y: (s.y + e.y) / 2 }; + const fline = [s, mid, e]; + + s = { x: start.x - n.x * d2, y: start.y - n.y * d2 }; + e = { x: end.x - n.x * d4, y: end.y - n.y * d4 }; + mid = { x: (s.x + e.x) / 2, y: (s.y + e.y) / 2 }; + const bline = [e, mid, s]; + + const ls = utils.makeline(bline[2], fline[0]); + const le = utils.makeline(fline[2], bline[0]); + const segments = [ls, new Bezier(fline), le, new Bezier(bline)]; + return new PolyBezier(segments); + } + + const reduced = this.reduce(), + len = reduced.length, + fcurves = []; + + let bcurves = [], + p, + alen = 0, + tlen = this.length(); + + const graduated = typeof d3 !== "undefined" && typeof d4 !== "undefined"; + + function linearDistanceFunction(s, e, tlen, alen, slen) { + return function (v) { + const f1 = alen / tlen, + f2 = (alen + slen) / tlen, + d = e - s; + return utils.map(v, 0, 1, s + f1 * d, s + f2 * d); + }; + } + + // form curve oulines + reduced.forEach(function (segment) { + const slen = segment.length(); + if (graduated) { + fcurves.push( + segment.scale(linearDistanceFunction(d1, d3, tlen, alen, slen)) + ); + bcurves.push( + segment.scale(linearDistanceFunction(-d2, -d4, tlen, alen, slen)) + ); + } else { + fcurves.push(segment.scale(d1)); + bcurves.push(segment.scale(-d2)); + } + alen += slen; + }); + + // reverse the "return" outline + bcurves = bcurves + .map(function (s) { + p = s.points; + if (p[3]) { + s.points = [p[3], p[2], p[1], p[0]]; + } else { + s.points = [p[2], p[1], p[0]]; + } + return s; + }) + .reverse(); + + // form the endcaps as lines + const fs = fcurves[0].points[0], + fe = fcurves[len - 1].points[fcurves[len - 1].points.length - 1], + bs = bcurves[len - 1].points[bcurves[len - 1].points.length - 1], + be = bcurves[0].points[0], + ls = utils.makeline(bs, fs), + le = utils.makeline(fe, be), + segments = [ls].concat(fcurves).concat([le]).concat(bcurves); + + return new PolyBezier(segments); + } + + outlineshapes(d1, d2, curveIntersectionThreshold) { + d2 = d2 || d1; + const outline = this.outline(d1, d2).curves; + const shapes = []; + for (let i = 1, len = outline.length; i < len / 2; i++) { + const shape = utils.makeshape( + outline[i], + outline[len - i], + curveIntersectionThreshold + ); + shape.startcap.virtual = i > 1; + shape.endcap.virtual = i < len / 2 - 1; + shapes.push(shape); + } + return shapes; + } + + intersects(curve, curveIntersectionThreshold) { + if (!curve) return this.selfintersects(curveIntersectionThreshold); + if (curve.p1 && curve.p2) { + return this.lineIntersects(curve); + } + if (curve instanceof Bezier) { + curve = curve.reduce(); + } + return this.curveintersects( + this.reduce(), + curve, + curveIntersectionThreshold + ); + } + + lineIntersects(line) { + const mx = min(line.p1.x, line.p2.x), + my = min(line.p1.y, line.p2.y), + MX = max(line.p1.x, line.p2.x), + MY = max(line.p1.y, line.p2.y); + return utils.roots(this.points, line).filter((t) => { + var p = this.get(t); + return utils.between(p.x, mx, MX) && utils.between(p.y, my, MY); + }); + } + + selfintersects(curveIntersectionThreshold) { + // "simple" curves cannot intersect with their direct + // neighbour, so for each segment X we check whether + // it intersects [0:x-2][x+2:last]. + + const reduced = this.reduce(), + len = reduced.length - 2, + results = []; + + for (let i = 0, result, left, right; i < len; i++) { + left = reduced.slice(i, i + 1); + right = reduced.slice(i + 2); + result = this.curveintersects(left, right, curveIntersectionThreshold); + results.push(...result); + } + return results; + } + + curveintersects(c1, c2, curveIntersectionThreshold) { + const pairs = []; + // step 1: pair off any overlapping segments + c1.forEach(function (l) { + c2.forEach(function (r) { + if (l.overlaps(r)) { + pairs.push({ left: l, right: r }); + } + }); + }); + // step 2: for each pairing, run through the convergence algorithm. + let intersections = []; + pairs.forEach(function (pair) { + const result = utils.pairiteration( + pair.left, + pair.right, + curveIntersectionThreshold + ); + if (result.length > 0) { + intersections = intersections.concat(result); + } + }); + return intersections; + } + + arcs(errorThreshold) { + errorThreshold = errorThreshold || 0.5; + return this._iterate(errorThreshold, []); + } + + _error(pc, np1, s, e) { + const q = (e - s) / 4, + c1 = this.get(s + q), + c2 = this.get(e - q), + ref = utils.dist(pc, np1), + d1 = utils.dist(pc, c1), + d2 = utils.dist(pc, c2); + return abs$1(d1 - ref) + abs$1(d2 - ref); + } + + _iterate(errorThreshold, circles) { + let t_s = 0, + t_e = 1, + safety; + // we do a binary search to find the "good `t` closest to no-longer-good" + do { + safety = 0; + + // step 1: start with the maximum possible arc + t_e = 1; + + // points: + let np1 = this.get(t_s), + np2, + np3, + arc, + prev_arc; + + // booleans: + let curr_good = false, + prev_good = false, + done; + + // numbers: + let t_m = t_e, + prev_e = 1; + + // step 2: find the best possible arc + do { + prev_good = curr_good; + prev_arc = arc; + t_m = (t_s + t_e) / 2; + + np2 = this.get(t_m); + np3 = this.get(t_e); + + arc = utils.getccenter(np1, np2, np3); + + //also save the t values + arc.interval = { + start: t_s, + end: t_e, + }; + + let error = this._error(arc, np1, t_s, t_e); + curr_good = error <= errorThreshold; + + done = prev_good && !curr_good; + if (!done) prev_e = t_e; + + // this arc is fine: we can move 'e' up to see if we can find a wider arc + if (curr_good) { + // if e is already at max, then we're done for this arc. + if (t_e >= 1) { + // make sure we cap at t=1 + arc.interval.end = prev_e = 1; + prev_arc = arc; + // if we capped the arc segment to t=1 we also need to make sure that + // the arc's end angle is correct with respect to the bezier end point. + if (t_e > 1) { + let d = { + x: arc.x + arc.r * cos$1(arc.e), + y: arc.y + arc.r * sin$1(arc.e), + }; + arc.e += utils.angle({ x: arc.x, y: arc.y }, d, this.get(1)); + } + break; + } + // if not, move it up by half the iteration distance + t_e = t_e + (t_e - t_s) / 2; + } else { + // this is a bad arc: we need to move 'e' down to find a good arc + t_e = t_m; + } + } while (!done && safety++ < 100); + + if (safety >= 100) { + break; + } + + // console.log("L835: [F] arc found", t_s, prev_e, prev_arc.x, prev_arc.y, prev_arc.s, prev_arc.e); + + prev_arc = prev_arc ? prev_arc : arc; + circles.push(prev_arc); + t_s = prev_e; + } while (t_e < 1); + return circles; + } +} + +export { Bezier }; diff --git a/src/main.js b/src/main.js index 3e814aa..10474a3 100644 --- a/src/main.js +++ b/src/main.js @@ -1,5 +1,7 @@ const { invoke } = window.__TAURI__.core; import * as fitCurve from '/fit-curve.js'; +import { Bezier } from "/bezier.js"; + let simplifyPolyline = simplify @@ -74,6 +76,7 @@ let context = { lineWidth: 5, simplifyMode: "smooth", fillShape: true, + dragging: false, } let config = { @@ -87,7 +90,6 @@ function uuidv4() { (+c ^ crypto.getRandomValues(new Uint8Array(1))[0] & 15 >> +c / 4).toString(16) ); } - function vectorDist(a, b) { return Math.sqrt((a.x-b.x)*(a.x-b.x) + (a.y-b.y)*(a.y-b.y)) } @@ -119,8 +121,35 @@ function setProperty(context, path, value) { pointer[finalComponent] = value } +function selectCurve(context, mouse) { + let mouseTolerance = 15; + for (let shape of context.activeObject.frames[context.activeObject.currentFrame].shapes) { + if (mouse.x > shape.boundingBox.x.min - mouseTolerance && + mouse.x < shape.boundingBox.x.max + mouseTolerance && + mouse.y > shape.boundingBox.y.min - mouseTolerance && + mouse.y < shape.boundingBox.y.max + mouseTolerance) { + let closestDist = mouseTolerance; + let closest = undefined + for (let curve of shape.curves) { + let dist = vectorDist(mouse, curve.project(mouse)) + if (dist <= closestDist ) { + closestDist = dist + closest = curve + } + } + if (closest) { + return closest + } else { + return undefined + } + } + } +} + class Curve { - constructor(cp1x, cp1y, cp2x, cp2y, x, y) { + constructor(startx, starty, cp1x, cp1y, cp2x, cp2y, x, y) { + this.startx = startx + this.starty = starty this.cp1x = cp1x; this.cp1y = cp1y; this.cp2x = cp2x; @@ -148,47 +177,82 @@ class Shape { this.lineWidth = context.lineWidth this.filled = context.fillShape; this.stroked = stroked; + this.boundingBox = { + x: {min: startx, max: starty}, + y: {min: starty, max: starty} + } } addCurve(curve) { this.curves.push(curve) + this.growBoundingBox(curve.bbox()) } addLine(x, y) { let lastpoint; if (this.curves.length) { - lastpoint = this.curves[this.curves.length - 1] + lastpoint = this.curves[this.curves.length - 1].points[3] } else { lastpoint = {x: this.startx, y: this.starty} } let midpoint = {x: (x + lastpoint.x) / 2, y: (y + lastpoint.y) / 2} - let curve = new Curve(midpoint.x, midpoint.y, midpoint.x, midpoint.y, x, y) + let curve = new Bezier(lastpoint.x, lastpoint.y, + midpoint.x, midpoint.y, + midpoint.x, midpoint.y, + x, y) this.curves.push(curve) } + clear() { + this.curves = [] + } + growBoundingBox(bbox) { + this.boundingBox.x.min = Math.min(this.boundingBox.x.min, bbox.x.min) + this.boundingBox.y.min = Math.min(this.boundingBox.y.min, bbox.y.min) + this.boundingBox.x.max = Math.max(this.boundingBox.x.max, bbox.x.max) + this.boundingBox.y.max = Math.max(this.boundingBox.y.max, bbox.y.max) + } + recalculateBoundingBox() { + for (let curve of this.curves) { + this.growBoundingBox(curve.bbox()) + } + } simplify(mode="corners") { // Mode can be corners, smooth or auto if (mode=="corners") { let points = [{x: this.startx, y: this.starty}] - points = points.concat(this.curves) + for (let curve of this.curves) { + points.push(curve.points[3]) + } + // points = points.concat(this.curves) let newpoints = simplifyPolyline(points, 10, false) this.curves = [] let lastpoint = newpoints.shift() let midpoint for (let point of newpoints) { midpoint = {x: (lastpoint.x+point.x)/2, y: (lastpoint.y+point.y)/2} - this.curves.push(new Curve(midpoint.x, midpoint.y,midpoint.x,midpoint.y,point.x,point.y)) + let bezier = new Bezier(lastpoint.x, lastpoint.y, + midpoint.x, midpoint.y, + midpoint.x,midpoint.y, + point.x,point.y) + this.curves.push(bezier) lastpoint = point } } else if (mode=="smooth") { let error = 30; let points = [[this.startx, this.starty]] for (let curve of this.curves) { - points.push([curve.x, curve.y]) + points.push([curve.points[3].x, curve.points[3].y]) } this.curves = [] let curves = fitCurve.fitCurve(points, error) for (let curve of curves) { - this.curves.push(new Curve(curve[1][0],curve[1][1],curve[2][0], curve[2][1], curve[3][0], curve[3][1])) + let bezier = new Bezier(curve[0][0], curve[0][1], + curve[1][0],curve[1][1], + curve[2][0], curve[2][1], + curve[3][0], curve[3][1]) + this.curves.push(bezier) + } } + this.recalculateBoundingBox() } } @@ -230,11 +294,14 @@ class GraphicsObject { ctx.lineWidth = shape.lineWidth ctx.moveTo(shape.startx, shape.starty) for (let curve of shape.curves) { - ctx.bezierCurveTo(curve.cp1x, curve.cp1y, curve.cp2x, curve.cp2y, curve.x, curve.y) + // ctx.moveTo(curve.points[0].x, curve.points[0].y) + ctx.bezierCurveTo(curve.points[1].x, curve.points[1].y, + curve.points[2].x, curve.points[2].y, + curve.points[3].x, curve.points[3].y) // Debug, show curve endpoints // ctx.beginPath() - // ctx.arc(curve.x,curve.y, 3, 0, 2*Math.PI) + // ctx.arc(curve.points[3].x,curve.points[3].y, 3, 0, 2*Math.PI) // ctx.fill() } if (shape.filled) { @@ -251,6 +318,16 @@ class GraphicsObject { ctx.stroke() } } + if (context.activeObject==this && context.activeCurve) { + ctx.strokeStyle = "magenta" + ctx.beginPath() + ctx.moveTo(context.activeCurve.points[0].x, context.activeCurve.points[0].y) + ctx.bezierCurveTo(context.activeCurve.points[1].x, context.activeCurve.points[1].y, + context.activeCurve.points[2].x, context.activeCurve.points[2].y, + context.activeCurve.points[3].x, context.activeCurve.points[3].y + ) + ctx.stroke() + } } addShape(shape) { this.frames[this.currentFrame].shapes.push(shape) @@ -353,13 +430,19 @@ function stage() { stage.addEventListener("mousedown", (e) => { let mouse = getMousePos(stage, e) switch (mode) { + case "rectangle": case "draw": context.mouseDown = true context.activeShape = new Shape(mouse.x, mouse.y, context, true, true) context.activeObject.addShape(context.activeShape) context.lastMouse = mouse break; - + case "select": + let curve = selectCurve(context, mouse) + if (curve) { + console.log("gonna move this") + } + break; default: break; } @@ -372,13 +455,13 @@ function stage() { switch (mode) { case "draw": if (context.activeShape) { - let midpoint = {x: (mouse.x+context.lastMouse.x)/2, y: (mouse.y+context.lastMouse.y)/2} - context.activeShape.addCurve(new Curve(midpoint.x, midpoint.y, midpoint.x, midpoint.y, mouse.x, mouse.y)) + context.activeShape.addLine(mouse.x, mouse.y) context.activeShape.simplify(context.simplifyMode) context.activeShape = undefined } break; - + case "rectangle": + context.activeShape = undefined default: break; } @@ -387,16 +470,29 @@ function stage() { }) stage.addEventListener("mousemove", (e) => { let mouse = getMousePos(stage, e) + context.activeCurve = undefined switch (mode) { case "draw": if (context.activeShape) { if (vectorDist(mouse, context.lastMouse) > minSegmentSize) { - let midpoint = {x: (mouse.x+context.lastMouse.x)/2, y: (mouse.y+context.lastMouse.y)/2} - context.activeShape.addCurve(new Curve(midpoint.x, midpoint.y, midpoint.x, midpoint.y, mouse.x, mouse.y)) + context.activeShape.addLine(mouse.x, mouse.y) context.lastMouse = mouse } } break; + case "rectangle": + if (context.activeShape) { + context.activeShape.clear() + context.activeShape.addLine(mouse.x, context.activeShape.starty) + context.activeShape.addLine(mouse.x, mouse.y) + context.activeShape.addLine(context.activeShape.startx, mouse.y) + context.activeShape.addLine(context.activeShape.startx, context.activeShape.starty) + context.activeShape.recalculateBoundingBox() + } + break; + case "select": + context.activeCurve = selectCurve(context, mouse) + break; default: break; } @@ -417,6 +513,7 @@ function toolbar() { toolbtn.appendChild(icon) tools_scroller.appendChild(toolbtn) toolbtn.addEventListener("click", () => { + mode = tool console.log(tool) }) } @@ -511,21 +608,22 @@ function infopanel() { input.checked = getProperty(context, property) break; } - input.addEventListener("input", () => { - console.log(input.value) + input.addEventListener("input", (e) => { switch (prop.type) { case "number": - if (!isNaN(input.value) && input.value > 0) { - setProperty(context, property, input.value) + if (!isNaN(e.target.value) && e.target.value > 0) { + setProperty(context, property, e.target.value) } break; case "enum": - if (prop.options.indexOf(input.value) >= 0) { - setProperty(context, property, input.value) + console.log(e) + if (prop.options.indexOf(e.target.value) >= 0) { + console.log(setProperty) + setProperty(context, property, e.target.value) } break; case "boolean": - setProperty(context, property, input.checked) + setProperty(context, property, e.target.checked) } }) From 175eb7e484dc03afee093d3552cc1795f63c61b0 Mon Sep 17 00:00:00 2001 From: Skyler Lehmkuhl Date: Mon, 18 Nov 2024 06:14:04 -0500 Subject: [PATCH 17/46] basic curve editing through moving control points --- src/main.js | 21 +++++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) diff --git a/src/main.js b/src/main.js index 10474a3..a018b3b 100644 --- a/src/main.js +++ b/src/main.js @@ -440,6 +440,7 @@ function stage() { case "select": let curve = selectCurve(context, mouse) if (curve) { + context.dragging = true console.log("gonna move this") } break; @@ -451,6 +452,7 @@ function stage() { }) stage.addEventListener("mouseup", (e) => { context.mouseDown = false + context.dragging = false let mouse = getMousePos(stage, e) switch (mode) { case "draw": @@ -470,9 +472,9 @@ function stage() { }) stage.addEventListener("mousemove", (e) => { let mouse = getMousePos(stage, e) - context.activeCurve = undefined switch (mode) { case "draw": + context.activeCurve = undefined if (context.activeShape) { if (vectorDist(mouse, context.lastMouse) > minSegmentSize) { context.activeShape.addLine(mouse.x, mouse.y) @@ -481,6 +483,7 @@ function stage() { } break; case "rectangle": + context.activeCurve = undefined if (context.activeShape) { context.activeShape.clear() context.activeShape.addLine(mouse.x, context.activeShape.starty) @@ -491,7 +494,19 @@ function stage() { } break; case "select": - context.activeCurve = selectCurve(context, mouse) + if (context.dragging) { + let dist = vectorDist(mouse, context.activeCurve.points[1]) + let cpoint = context.activeCurve.points[1] + if (vectorDist(mouse, context.activeCurve.points[2]) < dist) { + cpoint = context.activeCurve.points[2] + } + cpoint.x += (mouse.x - context.lastMouse.x) + cpoint.y += (mouse.y - context.lastMouse.y) + + } else { + context.activeCurve = selectCurve(context, mouse) + } + context.lastMouse = mouse break; default: break; @@ -616,9 +631,7 @@ function infopanel() { } break; case "enum": - console.log(e) if (prop.options.indexOf(e.target.value) >= 0) { - console.log(setProperty) setProperty(context, property, e.target.value) } break; From 863788b906c73992829a136dced85f5aa02ff891 Mon Sep 17 00:00:00 2001 From: Skyler Lehmkuhl Date: Mon, 18 Nov 2024 07:14:31 -0500 Subject: [PATCH 18/46] select objects --- src/main.js | 68 +++++++++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 61 insertions(+), 7 deletions(-) diff --git a/src/main.js b/src/main.js index a018b3b..ccfbaf4 100644 --- a/src/main.js +++ b/src/main.js @@ -77,6 +77,7 @@ let context = { simplifyMode: "smooth", fillShape: true, dragging: false, + selection: [], } let config = { @@ -146,6 +147,13 @@ function selectCurve(context, mouse) { } } +function growBoundingBox(bboxa, bboxb) { + bboxa.x.min = Math.min(bboxa.x.min, bboxb.x.min) + bboxa.y.min = Math.min(bboxa.y.min, bboxb.y.min) + bboxa.x.max = Math.max(bboxa.x.max, bboxb.x.max) + bboxa.y.max = Math.max(bboxa.y.max, bboxb.y.max) +} + class Curve { constructor(startx, starty, cp1x, cp1y, cp2x, cp2y, x, y) { this.startx = startx @@ -203,15 +211,9 @@ class Shape { clear() { this.curves = [] } - growBoundingBox(bbox) { - this.boundingBox.x.min = Math.min(this.boundingBox.x.min, bbox.x.min) - this.boundingBox.y.min = Math.min(this.boundingBox.y.min, bbox.y.min) - this.boundingBox.x.max = Math.max(this.boundingBox.x.max, bbox.x.max) - this.boundingBox.y.max = Math.max(this.boundingBox.y.max, bbox.y.max) - } recalculateBoundingBox() { for (let curve of this.curves) { - this.growBoundingBox(curve.bbox()) + growBoundingBox(this.boundingBox, curve.bbox()) } } simplify(mode="corners") { @@ -270,6 +272,24 @@ class GraphicsObject { this.shapes = [] } + bbox() { + let bbox; + if (this.frames[this.currentFrame].shapes.length > 0) { + bbox = this.frames[this.currentFrame].shapes[0].boundingBox + for (let shape of this.frames[this.currentFrame].shapes) { + growBoundingBox(bbox, shape.boundingBox) + } + } + if (this.children.length > 0) { + if (!bbox) { + bbox = this.children[0].bbox() + } + for (let child of this.children) { + growBoundingBox(bbox, child.bbox()) + } + } + return bbox + } draw(context) { let ctx = context.ctx; ctx.translate(this.x, this.y) @@ -328,6 +348,18 @@ class GraphicsObject { ) ctx.stroke() } + if (this == context.activeObject) { + for (let item of context.selection) { + ctx.save() + ctx.strokeStyle = "#00ffff" + ctx.translate(item.x, item.y) + ctx.beginPath() + let bbox = item.bbox() + ctx.rect(bbox.x.min, bbox.y.min, bbox.x.max, bbox.y.max) + ctx.stroke() + ctx.restore() + } + } } addShape(shape) { this.frames[this.currentFrame].shapes.push(shape) @@ -408,7 +440,9 @@ function stage() { imageShape.addLine(width, height) imageShape.addLine(0, height) imageShape.addLine(0, 0) + imageShape.recalculateBoundingBox() imageObject.addShape(imageShape) + console.log(imageObject.bbox()) context.activeObject.addObject( imageObject, mouse.x-width/2 + (20*img.ix), @@ -442,7 +476,27 @@ function stage() { if (curve) { context.dragging = true console.log("gonna move this") + } else { + let selected = false + let child; + // Have to iterate in reverse order to grab the frontmost object when two overlap + for (let i=context.activeObject.children.length-1; i>=0; i--) { + child = context.activeObject.children[i] + let bbox = child.bbox() + if (mouse.x > bbox.x.min + child.x && + mouse.x < bbox.x.max + child.x && + mouse.y > bbox.y.min + child.y && + mouse.y < bbox.y.max + child.y) { + context.selection = [child] + selected = true + break + } + } + if (!selected) { + context.selection = [] + } } + console.log(context.selection) break; default: break; From 709bd46ab8b9370cb52342a72bf876decab080f0 Mon Sep 17 00:00:00 2001 From: Skyler Lehmkuhl Date: Mon, 18 Nov 2024 17:36:50 -0500 Subject: [PATCH 19/46] implement selecting better --- src/main.js | 85 +++++++++++++++++++++++++++++++++++++++++++---------- 1 file changed, 69 insertions(+), 16 deletions(-) diff --git a/src/main.js b/src/main.js index ccfbaf4..13d5068 100644 --- a/src/main.js +++ b/src/main.js @@ -77,6 +77,7 @@ let context = { simplifyMode: "smooth", fillShape: true, dragging: false, + selectionRect: undefined, selection: [], } @@ -154,6 +155,36 @@ function growBoundingBox(bboxa, bboxb) { bboxa.y.max = Math.max(bboxa.y.max, bboxb.y.max) } +function regionToBbox(region) { + return { + x: {min: Math.min(region.x1, region.x2), max: Math.max(region.x1, region.x2)}, + y: {min: Math.min(region.y1, region.y2), max: Math.max(region.y1, region.y2)} + } +} + +function hitTest(candidate, object) { + let bbox = object.bbox() + if (candidate.x.min) { + // We're checking a bounding box + if (candidate.x.min < bbox.x.max + object.x && candidate.x.max > bbox.x.min + object.x && + candidate.y.min < bbox.y.max + object.y && candidate.y.max > bbox.y.min + object.y) { + return true; + } else { + return false; + } + } else { + // We're checking a point + if (candidate.x > bbox.x.min + object.x && + candidate.x < bbox.x.max + object.x && + candidate.y > bbox.y.min + object.y && + candidate.y < bbox.y.max + object.y) { + return true; + } else { + return false + } + } +} + class Curve { constructor(startx, starty, cp1x, cp1y, cp2x, cp2y, x, y) { this.startx = startx @@ -338,17 +369,17 @@ class GraphicsObject { ctx.stroke() } } - if (context.activeObject==this && context.activeCurve) { - ctx.strokeStyle = "magenta" - ctx.beginPath() - ctx.moveTo(context.activeCurve.points[0].x, context.activeCurve.points[0].y) - ctx.bezierCurveTo(context.activeCurve.points[1].x, context.activeCurve.points[1].y, - context.activeCurve.points[2].x, context.activeCurve.points[2].y, - context.activeCurve.points[3].x, context.activeCurve.points[3].y - ) - ctx.stroke() - } if (this == context.activeObject) { + if (context.activeCurve) { + ctx.strokeStyle = "magenta" + ctx.beginPath() + ctx.moveTo(context.activeCurve.points[0].x, context.activeCurve.points[0].y) + ctx.bezierCurveTo(context.activeCurve.points[1].x, context.activeCurve.points[1].y, + context.activeCurve.points[2].x, context.activeCurve.points[2].y, + context.activeCurve.points[3].x, context.activeCurve.points[3].y + ) + ctx.stroke() + } for (let item of context.selection) { ctx.save() ctx.strokeStyle = "#00ffff" @@ -359,6 +390,18 @@ class GraphicsObject { ctx.stroke() ctx.restore() } + if (context.selectionRect) { + ctx.save() + ctx.strokeStyle = "#00ffff" + ctx.beginPath() + ctx.rect( + context.selectionRect.x1, context.selectionRect.y1, + context.selectionRect.x2 - context.selectionRect.x1, + context.selectionRect.y2 - context.selectionRect.y1 + ) + ctx.stroke() + ctx.restore() + } } } addShape(shape) { @@ -482,11 +525,11 @@ function stage() { // Have to iterate in reverse order to grab the frontmost object when two overlap for (let i=context.activeObject.children.length-1; i>=0; i--) { child = context.activeObject.children[i] - let bbox = child.bbox() - if (mouse.x > bbox.x.min + child.x && - mouse.x < bbox.x.max + child.x && - mouse.y > bbox.y.min + child.y && - mouse.y < bbox.y.max + child.y) { + // let bbox = child.bbox() + if (hitTest(mouse, child)) { + if (context.selection.indexOf(child) != -1) { + // dragging = true + } context.selection = [child] selected = true break @@ -494,6 +537,7 @@ function stage() { } if (!selected) { context.selection = [] + context.selectionRect = {x1: mouse.x, x2: mouse.x, y1: mouse.y, y2:mouse.y} } } console.log(context.selection) @@ -507,6 +551,7 @@ function stage() { stage.addEventListener("mouseup", (e) => { context.mouseDown = false context.dragging = false + context.selectionRect = undefined let mouse = getMousePos(stage, e) switch (mode) { case "draw": @@ -556,7 +601,15 @@ function stage() { } cpoint.x += (mouse.x - context.lastMouse.x) cpoint.y += (mouse.y - context.lastMouse.y) - + } else if (context.selectionRect) { + context.selectionRect.x2 = mouse.x + context.selectionRect.y2 = mouse.y + context.selection = [] + for (let child of context.activeObject.children) { + if (hitTest(regionToBbox(context.selectionRect), child)) { + context.selection.push(child) + } + } } else { context.activeCurve = selectCurve(context, mouse) } From 45a055250bb9b2dca238d939d369ca0f387a7c7c Mon Sep 17 00:00:00 2001 From: Skyler Lehmkuhl Date: Tue, 19 Nov 2024 21:45:47 -0500 Subject: [PATCH 20/46] Initial undo/redo support --- src/assets/preferences.svg | 73 ++++++++++ src/main.js | 265 +++++++++++++++++++++++++++++-------- 2 files changed, 286 insertions(+), 52 deletions(-) create mode 100644 src/assets/preferences.svg diff --git a/src/assets/preferences.svg b/src/assets/preferences.svg new file mode 100644 index 0000000..ef6313c --- /dev/null +++ b/src/assets/preferences.svg @@ -0,0 +1,73 @@ + + + + + + + + + + + + image/svg+xml + + Gnome Symbolic Icon Theme + + + + Gnome Symbolic Icon Theme + + + + + diff --git a/src/main.js b/src/main.js index 13d5068..6743c68 100644 --- a/src/main.js +++ b/src/main.js @@ -16,6 +16,9 @@ let mode = "draw" let minSegmentSize = 5; let maxSmoothAngle = 0.6; +let undoStack = []; +let redoStack = []; + let tools = { select: { icon: "/assets/select.svg", @@ -84,6 +87,55 @@ let context = { let config = { shortcuts: { playAnimation: " ", + // undo: "+z" + undo: "z", + redo: "Z", + } +} + +// Pointers to all objects +let pointerList = {} + +let actions = { + addShape: { + create: (parent, shape) => { + redoStack.length = 0; // Clear redo stack + let serializableCurves = [] + for (let curve of shape.curves) { + serializableCurves.push({ points: curve.points }) + } + let action = { + parent: parent.idx, + curves: serializableCurves, + startx: shape.startx, + starty: shape.starty, + uuid: uuidv4() + } + undoStack.push({name: "addShape", action: action}) + actions.addShape.execute(action) + }, + execute: (action) => { + let object = pointerList[action.parent] + console.log(object) + let curvesList = action.curves + let shape = new Shape(action.startx, action.starty, context, action.uuid) + for (let curve of curvesList) { + shape.addCurve( + new Bezier( + curve.points[0].x, curve.points[0].y, + curve.points[1].x, curve.points[1].y, + curve.points[2].x, curve.points[2].y, + curve.points[3].x, curve.points[3].y + )) + } + object.addShape(shape) + }, + rollback: (action) => { + let object = pointerList[action.parent] + let shape = pointerList[action.uuid] + object.removeShape(shape) + delete pointerList[action.uuid] + } } } @@ -125,7 +177,7 @@ function setProperty(context, path, value) { function selectCurve(context, mouse) { let mouseTolerance = 15; - for (let shape of context.activeObject.frames[context.activeObject.currentFrame].shapes) { + for (let shape of context.activeObject.currentFrame.shapes) { if (mouse.x > shape.boundingBox.x.min - mouseTolerance && mouse.x < shape.boundingBox.x.max + mouseTolerance && mouse.y > shape.boundingBox.y.min - mouseTolerance && @@ -185,6 +237,36 @@ function hitTest(candidate, object) { } } +function pushState() { + // console.log(context) + // let ctx = context.ctx + // context.ctx = undefined + // undoStack.push(window.structuredClone([root,context])) + // context.ctx = ctx +} +function undo() { + let action = undoStack.pop() + if (action) { + actions[action.name].rollback(action.action) + redoStack.push(action) + updateUI() + } else { + console.log("No actions to undo") + } +} + +function redo() { + let action = redoStack.pop() + if (action) { + actions[action.name].execute(action.action) + undoStack.push(action) + updateUI() + } else { + console.log("No actions to redo") + } +} + + class Curve { constructor(startx, starty, cp1x, cp1y, cp2x, cp2y, x, y) { this.startx = startx @@ -199,14 +281,33 @@ class Curve { } class Frame { - constructor() { + constructor(uuid) { this.keys = {} this.shapes = [] + if (!uuid) { + this.idx = uuidv4() + } else { + this.idx = uuid + } + pointerList[this.idx] = this + } +} + +class Layer { + constructor(uuid) { + this.frames = [new Frame()] + this.children = [] + if (!uuid) { + this.idx = uuidv4() + } else { + this.idx = uuid + } + pointerList[this.idx] = this } } class Shape { - constructor(startx, starty, context, stroked=true) { + constructor(startx, starty, context, uuid=undefined) { this.startx = startx; this.starty = starty; this.curves = []; @@ -215,15 +316,21 @@ class Shape { this.strokeStyle = context.strokeStyle; this.lineWidth = context.lineWidth this.filled = context.fillShape; - this.stroked = stroked; + this.stroked = context.strokeShape || true; this.boundingBox = { x: {min: startx, max: starty}, y: {min: starty, max: starty} } + if (!uuid) { + this.idx = uuidv4() + } else { + this.idx = uuid + } + pointerList[this.idx] = this } addCurve(curve) { this.curves.push(curve) - this.growBoundingBox(curve.bbox()) + growBoundingBox(this.boundingBox, curve.bbox()) } addLine(x, y) { let lastpoint; @@ -287,27 +394,79 @@ class Shape { } this.recalculateBoundingBox() } + draw(context) { + let ctx = context.ctx; + ctx.beginPath() + ctx.lineWidth = this.lineWidth + ctx.moveTo(this.startx, this.starty) + for (let curve of this.curves) { + ctx.bezierCurveTo(curve.points[1].x, curve.points[1].y, + curve.points[2].x, curve.points[2].y, + curve.points[3].x, curve.points[3].y) + + // Debug, show curve endpoints + // ctx.beginPath() + // ctx.arc(curve.points[3].x,curve.points[3].y, 3, 0, 2*Math.PI) + // ctx.fill() + } + if (this.filled) { + if (this.fillImage) { + let pat = ctx.createPattern(this.fillImage, "no-repeat") + ctx.fillStyle = pat + } else { + ctx.fillStyle = this.fillStyle + } + ctx.fill() + } + if (this.stroked) { + ctx.strokeStyle = this.strokeStyle + ctx.stroke() + } + + } } class GraphicsObject { - constructor() { + constructor(uuid) { this.x = 0; this.y = 0; this.rotation = 0; // in radians this.scale = 1; - this.idx = uuidv4() + if (!uuid) { + this.idx = uuidv4() + } else { + this.idx = uuid + } + pointerList[this.idx] = this - this.frames = [new Frame()] - this.currentFrame = 0; - this.children = [] + this.currentFrameNum = 0; + this.currentLayer = 0; + this.layers = [new Layer()] + // this.children = [] this.shapes = [] } + get activeLayer() { + return this.layers[this.currentLayer] + } + get children() { + return this.layers[this.currentLayer].children + } + get currentFrame() { + return this.layers[this.currentLayer].frames[this.currentFrameNum] + } + get maxFrame() { + let maxFrames = [] + for (let layer of this.layers) { + maxFrames.push(layer.frames.length) + } + return Math.max(maxFrames) + } bbox() { let bbox; - if (this.frames[this.currentFrame].shapes.length > 0) { - bbox = this.frames[this.currentFrame].shapes[0].boundingBox - for (let shape of this.frames[this.currentFrame].shapes) { + if (this.currentFrame.shapes.length > 0) { + bbox = this.currentFrame.shapes[0].boundingBox + for (let shape of this.currentFrame.shapes) { growBoundingBox(bbox, shape.boundingBox) } } @@ -325,50 +484,24 @@ class GraphicsObject { let ctx = context.ctx; ctx.translate(this.x, this.y) ctx.rotate(this.rotation) - if (this.currentFrame>=this.frames.length) { - this.currentFrame = 0; + if (this.currentFrameNum>=this.maxFrame) { + this.currentFrameNum = 0; + } + for (let shape of this.currentFrame.shapes) { + shape.draw(context) } for (let child of this.children) { let idx = child.idx - if (idx in this.frames[this.currentFrame].keys) { - child.x = this.frames[this.currentFrame].keys[idx].x; - child.y = this.frames[this.currentFrame].keys[idx].y; - child.rotation = this.frames[this.currentFrame].keys[idx].rotation; - child.scale = this.frames[this.currentFrame].keys[idx].scale; + if (idx in this.currentFrame.keys) { + child.x = this.currentFrame.keys[idx].x; + child.y = this.currentFrame.keys[idx].y; + child.rotation = this.currentFrame.keys[idx].rotation; + child.scale = this.currentFrame.keys[idx].scale; ctx.save() child.draw(context) ctx.restore() } } - for (let shape of this.frames[this.currentFrame].shapes) { - ctx.beginPath() - ctx.lineWidth = shape.lineWidth - ctx.moveTo(shape.startx, shape.starty) - for (let curve of shape.curves) { - // ctx.moveTo(curve.points[0].x, curve.points[0].y) - ctx.bezierCurveTo(curve.points[1].x, curve.points[1].y, - curve.points[2].x, curve.points[2].y, - curve.points[3].x, curve.points[3].y) - - // Debug, show curve endpoints - // ctx.beginPath() - // ctx.arc(curve.points[3].x,curve.points[3].y, 3, 0, 2*Math.PI) - // ctx.fill() - } - if (shape.filled) { - if (shape.fillImage) { - let pat = ctx.createPattern(shape.fillImage, "no-repeat") - ctx.fillStyle = pat - } else { - ctx.fillStyle = shape.fillStyle - } - ctx.fill() - } - if (shape.stroked) { - ctx.strokeStyle = shape.strokeStyle - ctx.stroke() - } - } if (this == context.activeObject) { if (context.activeCurve) { ctx.strokeStyle = "magenta" @@ -405,18 +538,28 @@ class GraphicsObject { } } addShape(shape) { - this.frames[this.currentFrame].shapes.push(shape) + this.currentFrame.shapes.push(shape) } addObject(object, x=0, y=0) { this.children.push(object) let idx = object.idx - this.frames[this.currentFrame].keys[idx] = { + this.currentFrame.keys[idx] = { x: x, y: y, rotation: 0, scale: 1, } } + removeShape(shape) { + for (let layer of this.layers) { + for (let frame of layer.frames) { + let shapeIndex = frame.shapes.indexOf(shape) + if (shapeIndex >= 0) { + frame.shapes.splice(shapeIndex, 1) + } + } + } + } } let root = new GraphicsObject(); @@ -443,8 +586,18 @@ window.addEventListener("resize", () => { }) window.addEventListener("keypress", (e) => { + // let shortcuts = {} + // for (let shortcut of config.shortcuts) { + // shortcut = shortcut.split("+") + // TODO + // } + console.log(e) if (e.key == config.shortcuts.playAnimation) { console.log("Spacebar pressed") + } else if (e.key == config.shortcuts.undo && e.ctrlKey == true) { + undo() + } else if (e.key == config.shortcuts.redo && e.ctrlKey == true) { + redo() } }) @@ -509,9 +662,10 @@ function stage() { switch (mode) { case "rectangle": case "draw": + pushState() context.mouseDown = true context.activeShape = new Shape(mouse.x, mouse.y, context, true, true) - context.activeObject.addShape(context.activeShape) + console.log(context.activeObject) context.lastMouse = mouse break; case "select": @@ -558,7 +712,11 @@ function stage() { if (context.activeShape) { context.activeShape.addLine(mouse.x, mouse.y) context.activeShape.simplify(context.simplifyMode) + actions.addShape.create(context.activeObject, context.activeShape) + // context.activeObject.addShape(context.activeShape) context.activeShape = undefined + console.log(pointerList) + console.log(undoStack) } break; case "rectangle": @@ -863,6 +1021,9 @@ function updateUI() { context.ctx = ctx; root.draw(context) + if (context.activeShape) { + context.activeShape.draw(context) + } // let mouse; // if (mouseEvent) { From 380fcf0c32455cc9ac6fca97d2d6abf2f443e828 Mon Sep 17 00:00:00 2001 From: Skyler Lehmkuhl Date: Wed, 20 Nov 2024 00:08:50 -0500 Subject: [PATCH 21/46] Curve editing --- src/bezier.js | 21 ++++ src/main.js | 295 +++++++++++++++++++++++++++++++++++++++++--------- src/vector.js | 74 +++++++++++++ 3 files changed, 337 insertions(+), 53 deletions(-) create mode 100644 src/vector.js diff --git a/src/bezier.js b/src/bezier.js index 2d975c6..d065495 100644 --- a/src/bezier.js +++ b/src/bezier.js @@ -1,3 +1,5 @@ +import { Vector } from "./vector.js"; + // math-inlining. const { abs, cos, sin, acos, atan2, sqrt, pow } = Math; @@ -1968,6 +1970,25 @@ class Bezier { } while (t_e < 1); return circles; } + + getStrutPoints(t) { + const p = this.points.map((p) => new Vector(p)); + const mt = 1 - t; + + let s = 0; + let n = p.length + 1; + while (--n > 1) { + let list = p.slice(s, s + n); + for (let i = 0, e = list.length - 1; i < e; i++) { + let pt = list[i + 1].subtract(list[i + 1].subtract(list[i]).scale(mt)); + p.push(pt); + } + s += n; + } + + return p; + } + } export { Bezier }; diff --git a/src/main.js b/src/main.js index 6743c68..5425517 100644 --- a/src/main.js +++ b/src/main.js @@ -116,7 +116,6 @@ let actions = { }, execute: (action) => { let object = pointerList[action.parent] - console.log(object) let curvesList = action.curves let shape = new Shape(action.startx, action.starty, context, action.uuid) for (let curve of curvesList) { @@ -136,7 +135,68 @@ let actions = { object.removeShape(shape) delete pointerList[action.uuid] } - } + }, + editShape: { + create: (shape, newCurves) => { + let serializableNewCurves = [] + for (let curve of newCurves) { + serializableNewCurves.push({ points: curve.points }) + } + let serializableOldCurves = [] + for (let curve of shape.curves) { + serializableOldCurves.push({ points: curve.points }) + } + let action = { + shape: shape.idx, + oldCurves: serializableOldCurves, + newCurves: serializableNewCurves + } + undoStack.push({name: "editShape", action: action}) + actions.editShape.execute(action) + + }, + execute: (action) => { + let shape = pointerList[action.shape] + let curvesList = action.newCurves + shape.curves = [] + for (let curve of curvesList) { + shape.addCurve( + new Bezier( + curve.points[0].x, curve.points[0].y, + curve.points[1].x, curve.points[1].y, + curve.points[2].x, curve.points[2].y, + curve.points[3].x, curve.points[3].y + )) + } + }, + rollback: (action) => { + let shape = pointerList[action.shape] + let curvesList = action.oldCurves + shape.curves = [] + for (let curve of curvesList) { + shape.addCurve( + new Bezier( + curve.points[0].x, curve.points[0].y, + curve.points[1].x, curve.points[1].y, + curve.points[2].x, curve.points[2].y, + curve.points[3].x, curve.points[3].y + )) + }} + }, + addObject: { + create: () => {}, + execute: (action) => { + + }, + rollback: (action) => {} + }, + editObject: { + create: () => {}, + execute: (action) => { + + }, + rollback: (action) => {} + }, } function uuidv4() { @@ -192,7 +252,7 @@ function selectCurve(context, mouse) { } } if (closest) { - return closest + return {curve:closest, shape:shape} } else { return undefined } @@ -200,6 +260,137 @@ function selectCurve(context, mouse) { } } +function moldCurve(curve, mouse, oldmouse) { + let diff = {x: mouse.x - oldmouse.x, y: mouse.y - oldmouse.y} + let p = curve.project(mouse) + let min_influence = 0.1 + console.log(p.t) + const CP1 = { + x: curve.points[1].x + diff.x*(1-p.t)*2, + y: curve.points[1].y + diff.y*(1-p.t)*2 + } + const CP2 = { + x: curve.points[2].x + diff.x*(p.t)*2, + y: curve.points[2].y + diff.y*(p.t)*2 + } + return new Bezier(curve.points[0], CP1, CP2, curve.points[3]) + // return curve +} + +function moldCurveMath(curve, mouse) { + let interpolated = true + + let p = curve.project({x: mouse.x, y: mouse.y}) + + let t1 = p.t; + let struts = curve.getStrutPoints(t1); + let m = { + t: p.t, + B: p, + e1: struts[7], + e2: struts[8] + }; + m.d1 = { x: m.e1.x - m.B.x, y: m.e1.y - m.B.y}; + m.d2 = { x: m.e2.x - m.B.x, y: m.e2.y - m.B.y}; + + const S = curve.points[0], + E = curve.points[curve.order], + {B, t, e1, e2} = m, + org = curve.getABC(t, B), + nB = mouse, + d1 = { x: e1.x - B.x, y: e1.y - B.y }, + d2 = { x: e2.x - B.x, y: e2.y - B.y }, + ne1 = { x: nB.x + d1.x, y: nB.y + d1.y }, + ne2 = { x: nB.x + d2.x, y: nB.y + d2.y }, + {A, C} = curve.getABC(t, nB), + // The cubic case requires us to derive two control points, + // which we'll do in a separate function to keep the code + // at least somewhat manageable. + {v1, v2, C1, C2} = deriveControlPoints(S, A, E, ne1, ne2, t); + + // if (interpolated) { + // For the last example, we need to show what the "ideal" curve + // looks like, in addition to the one we actually get when we + // rely on the B we picked with the `t` value and e1/e2 points + // that point B had... + const ideal = getIdealisedCurve(S, nB, E); + let idealCurve = new Bezier(ideal.S, ideal.C1, ideal.C2, ideal.E); + // } + let molded = new Bezier(S,C1,C2,E); + + let falloff = 100 + + let d = Bezier.getUtils().dist(ideal.B, p); + let t2 = Math.min(falloff, d) / falloff; + console.log(d) + let iC1 = { + x: (1-t2) * molded.points[1].x + t2 * idealCurve.points[1].x, + y: (1-t2) * molded.points[1].y + t2 * idealCurve.points[1].y + }; + let iC2 = { + x: (1-t2) * molded.points[2].x + t2 * idealCurve.points[2].x, + y: (1-t2) * molded.points[2].y + t2 * idealCurve.points[2].y + }; + let interpolatedCurve = new Bezier(molded.points[0], iC1, iC2, molded.points[3]); + + return interpolatedCurve + // return idealCurve +} + +function deriveControlPoints(S, A, E, e1, e2, t) { + // Deriving the control points is effectively "doing what + // we talk about in the section", in code: + + const v1 = { + x: A.x - (A.x - e1.x)/(1-t), + y: A.y - (A.y - e1.y)/(1-t) + }; + const v2 = { + x: A.x - (A.x - e2.x)/t, + y: A.y - (A.y - e2.y)/t + }; + + const C1 = { + x: S.x + (v1.x - S.x) / t, + y: S.y + (v1.y - S.y) / t + }; + const C2 = { + x: E.x + (v2.x - E.x) / (1-t), + y: E.y + (v2.y - E.y) / (1-t) + }; + + return {v1, v2, C1, C2}; +} + +function getIdealisedCurve(p1, p2, p3) { + // This "reruns" the curve composition, but with a `t` value + // that is unrelated to the actual point B we picked, instead + // using whatever the appropriate `t` value would be if we were + // trying to fit a circular arc, as per earlier in the section. + const utils = Bezier.getUtils() + const c = utils.getccenter(p1, p2, p3), + d1 = utils.dist(p1, p2), + d2 = utils.dist(p3, p2), + t = d1 / (d1 + d2), + { A, B, C, S, E } = Bezier.getABC(3, p1, p2, p3, t), + angle = (Math.atan2(E.y-S.y, E.x-S.x) - Math.atan2(B.y-S.y, B.x-S.x) + utils.TAU) % utils.TAU, + bc = (angle < 0 || angle > utils.PI ? -1 : 1) * utils.dist(S, E)/3, + de1 = t * bc, + de2 = (1-t) * bc, + tangent = [ + { x: B.x - 10 * (B.y-c.y), y: B.y + 10 * (B.x-c.x) }, + { x: B.x + 10 * (B.y-c.y), y: B.y - 10 * (B.x-c.x) } + ], + tlength = utils.dist(tangent[0], tangent[1]), + dx = (tangent[1].x - tangent[0].x)/tlength, + dy = (tangent[1].y - tangent[0].y)/tlength, + e1 = { x: B.x + de1 * dx, y: B.y + de1 * dy}, + e2 = { x: B.x - de2 * dx, y: B.y - de2 * dy }, + {v1, v2, C1, C2} = deriveControlPoints(S, A, E, e1, e2, t); + + return {A,B,C,S,E,e1,e2,v1,v2,C1,C2}; +} + function growBoundingBox(bboxa, bboxb) { bboxa.x.min = Math.min(bboxa.x.min, bboxb.x.min) bboxa.y.min = Math.min(bboxa.y.min, bboxb.y.min) @@ -506,10 +697,10 @@ class GraphicsObject { if (context.activeCurve) { ctx.strokeStyle = "magenta" ctx.beginPath() - ctx.moveTo(context.activeCurve.points[0].x, context.activeCurve.points[0].y) - ctx.bezierCurveTo(context.activeCurve.points[1].x, context.activeCurve.points[1].y, - context.activeCurve.points[2].x, context.activeCurve.points[2].y, - context.activeCurve.points[3].x, context.activeCurve.points[3].y + ctx.moveTo(context.activeCurve.current.points[0].x, context.activeCurve.current.points[0].y) + ctx.bezierCurveTo(context.activeCurve.current.points[1].x, context.activeCurve.current.points[1].y, + context.activeCurve.current.points[2].x, context.activeCurve.current.points[2].y, + context.activeCurve.current.points[3].x, context.activeCurve.current.points[3].y ) ctx.stroke() } @@ -638,7 +829,6 @@ function stage() { imageShape.addLine(0, 0) imageShape.recalculateBoundingBox() imageObject.addShape(imageShape) - console.log(imageObject.bbox()) context.activeObject.addObject( imageObject, mouse.x-width/2 + (20*img.ix), @@ -665,13 +855,18 @@ function stage() { pushState() context.mouseDown = true context.activeShape = new Shape(mouse.x, mouse.y, context, true, true) - console.log(context.activeObject) context.lastMouse = mouse break; case "select": - let curve = selectCurve(context, mouse) - if (curve) { + let selection = selectCurve(context, mouse) + if (selection) { context.dragging = true + context.activeCurve = { + initial: selection.curve, + current: new Bezier(selection.curve.points), + shape: selection.shape, + startmouse: mouse + } console.log("gonna move this") } else { let selected = false @@ -694,7 +889,6 @@ function stage() { context.selectionRect = {x1: mouse.x, x2: mouse.x, y1: mouse.y, y2:mouse.y} } } - console.log(context.selection) break; default: break; @@ -715,16 +909,29 @@ function stage() { actions.addShape.create(context.activeObject, context.activeShape) // context.activeObject.addShape(context.activeShape) context.activeShape = undefined - console.log(pointerList) - console.log(undoStack) } break; case "rectangle": context.activeShape = undefined + break; + case "select": + if (context.activeCurve) { + let newCurves = [] + for (let curve of context.activeCurve.shape.curves) { + if (curve == context.activeCurve.initial) { + newCurves.push(context.activeCurve.current) + } else { + newCurves.push(curve) + } + } + actions.editShape.create(context.activeCurve.shape, newCurves) + } + break; default: break; } context.lastMouse = mouse + context.activeCurve = undefined updateUI() }) stage.addEventListener("mousemove", (e) => { @@ -752,13 +959,16 @@ function stage() { break; case "select": if (context.dragging) { - let dist = vectorDist(mouse, context.activeCurve.points[1]) - let cpoint = context.activeCurve.points[1] - if (vectorDist(mouse, context.activeCurve.points[2]) < dist) { - cpoint = context.activeCurve.points[2] - } - cpoint.x += (mouse.x - context.lastMouse.x) - cpoint.y += (mouse.y - context.lastMouse.y) + // let dist = vectorDist(mouse, context.activeCurve.points[1]) + // let cpoint = context.activeCurve.points[1] + // if (vectorDist(mouse, context.activeCurve.points[2]) < dist) { + // cpoint = context.activeCurve.points[2] + // } + // cpoint.x += (mouse.x - context.lastMouse.x) + // cpoint.y += (mouse.y - context.lastMouse.y) + context.activeCurve.current.points = moldCurve( + context.activeCurve.initial, mouse, context.activeCurve.startmouse + ).points } else if (context.selectionRect) { context.selectionRect.x2 = mouse.x context.selectionRect.y2 = mouse.y @@ -769,7 +979,17 @@ function stage() { } } } else { - context.activeCurve = selectCurve(context, mouse) + let selection = selectCurve(context, mouse) + if (selection) { + context.activeCurve = { + current: selection.curve, + initial: new Bezier(selection.curve.points), + shape: selection.shape, + startmouse: mouse + } + } else { + context.activeCurve = undefined + } } context.lastMouse = mouse break; @@ -927,13 +1147,6 @@ function createPane(content=undefined) { icon.src = "/assets/stage.svg" button.appendChild(icon) - - // div.style.display = "grid"; - // div.style.gridTemplateColumns = `var(--lineheight) 1fr` - // div.style.gridTemplateRows = "1fr" - // header.style.gridArea = "1 / 1 / 2 / 2" - // content.style.gridArea = "1 / 2 / 2 / 3" - div.className = "vertical-grid" header.style.height = "calc( 2 * var(--lineheight))" content.style.height = "calc( 100% - 2 * var(--lineheight) )" @@ -959,26 +1172,12 @@ function splitPane(div, percent, horiz, newPane=undefined) { div.appendChild(div1) div.appendChild(div2) - // div.style.display = "grid"; - // if (horiz) { - // div.classList.add("horizontal-grid") - // div.style.gridTemplateColumns = `${percent}% 1fr` - // div1.style.gridArea = "1 / 1 / 2 / 2" - // div2.style.gridArea = "1 / 2 / 2 / 3" - // } else { - // div.classList.add("vertical-grid") - // div.style.gridTemplateRows = `${percent}% 1fr` - // div1.style.gridArea = "1 / 1 / 2 / 2" - // div2.style.gridArea = "2 / 1 / 3 / 2" - // } if (horiz) { div.className = "horizontal-grid" } else { div.className = "vertical-grid" } div.setAttribute("lb-percent", percent) // TODO: better attribute name - // div1.style.flex = `0 0 ${percent}%` - // div2.style.flex = `1 1 auto` Coloris({el: ".color-field"}) updateUI() updateLayout(rootPane) @@ -1016,8 +1215,6 @@ function updateUI() { ctx.reset(); ctx.fillStyle = "white" ctx.fillRect(0,0,canvas.width,canvas.height) - ctx.fillStyle = "green" - // ctx.fillRect(0,0,200,200) context.ctx = ctx; root.draw(context) @@ -1025,13 +1222,5 @@ function updateUI() { context.activeShape.draw(context) } - // let mouse; - // if (mouseEvent) { - // mouse = getMousePos(canvas, mouseEvent); - // } else { - // mouse = {x: 0, y: 0} - // } - // ctx.fillRect(mouse.x, mouse.y, 50,50) } - // requestAnimationFrame(updateUI) } \ No newline at end of file diff --git a/src/vector.js b/src/vector.js new file mode 100644 index 0000000..704a41f --- /dev/null +++ b/src/vector.js @@ -0,0 +1,74 @@ +class Vector { + constructor(x, y, z) { + if (arguments.length === 1) { + z = x.z; + y = x.y; + x = x.x; + } + this.x = x; + this.y = y; + if (z !== undefined) { + this.z = z; + } + } + dist(other, y, z = 0) { + if (y !== undefined) other = { x: other, y, z }; + let sum = 0; + sum += (this.x - other.x) ** 2; + sum += (this.y - other.y) ** 2; + let z1 = this.z ? this.z : 0; + let z2 = other.z ? other.z : 0; + sum += (z1 - z2) ** 2; + return sum ** 0.5; + } + normalize(f) { + let mag = this.dist(0, 0, 0); + return new Vector((f * this.x) / mag, (f * this.y) / mag, (f * this.z) / mag); + } + getAngle() { + return -Math.atan2(this.y, this.x); + } + reflect(other) { + let p = new Vector(other.x - this.x, other.y - this.y); + if (other.z !== undefined) { + p.z = other.z; + if (this.z !== undefined) { + p.z -= this.z; + } + } + return this.subtract(p); + } + add(other) { + let p = new Vector(this.x + other.x, this.y + other.y); + if (this.z !== undefined) { + p.z = this.z; + if (other.z !== undefined) { + p.z += other.z; + } + } + return p; + } + subtract(other) { + let p = new Vector(this.x - other.x, this.y - other.y); + if (this.z !== undefined) { + p.z = this.z; + if (other.z !== undefined) { + p.z -= other.z; + } + } + return p; + } + scale(f = 1) { + if (f === 0) { + return new Vector(0, 0, this.z === undefined ? undefined : 0); + } + let p = new Vector(this.x * f, this.y * f); + if (this.z !== undefined) { + p.z = this.z * f; + } + return p; + } + } + + export { Vector }; + \ No newline at end of file From 551993bc169042e4946f7984bbe9fe80a620cce9 Mon Sep 17 00:00:00 2001 From: Skyler Lehmkuhl Date: Wed, 20 Nov 2024 02:41:14 -0500 Subject: [PATCH 22/46] move objects --- src/main.js | 212 +++++++++++++++++++++++++++++++++++++++------------- 1 file changed, 160 insertions(+), 52 deletions(-) diff --git a/src/main.js b/src/main.js index 5425517..557f121 100644 --- a/src/main.js +++ b/src/main.js @@ -79,6 +79,7 @@ let context = { lineWidth: 5, simplifyMode: "smooth", fillShape: true, + strokeShape: true, dragging: false, selectionRect: undefined, selection: [], @@ -95,6 +96,8 @@ let config = { // Pointers to all objects let pointerList = {} +// Keeping track of initial values of variables when we edit them continuously +let startProps = {} let actions = { addShape: { @@ -138,6 +141,7 @@ let actions = { }, editShape: { create: (shape, newCurves) => { + redoStack.length = 0; // Clear redo stack let serializableNewCurves = [] for (let curve of newCurves) { serializableNewCurves.push({ points: curve.points }) @@ -183,19 +187,79 @@ let actions = { )) }} }, - addObject: { - create: () => {}, - execute: (action) => { - + addImageObject: { + create: (x, y, img, parent) => { + redoStack.length = 0; // Clear redo stack + let action = { + shapeUuid: uuidv4(), + objectUuid: uuidv4(), + x: x, + y: y, + width: img.width, + height: img.height, + ix: img.ix, + img: img.idx, + parent: parent.idx + } + undoStack.push({name: "addImageObject", action: action}) + actions.addImageObject.execute(action) }, - rollback: (action) => {} + execute: (action) => { + let imageObject = new GraphicsObject(action.objectUuid) + let img = pointerList[action.img] + let ct = { + ...context, + fillImage: img, + strokeShape: false, + } + let imageShape = new Shape(0, 0, ct, action.shapeUuid) + imageShape.addLine(action.width, 0) + imageShape.addLine(action.width, action.height) + imageShape.addLine(0, action.height) + imageShape.addLine(0, 0) + imageShape.recalculateBoundingBox() + imageObject.addShape(imageShape) + let parent = pointerList[action.parent] + parent.addObject( + imageObject, + action.x-action.width/2 + (20*action.ix), + action.y-action.height/2 + (20*action.ix) + ) + }, + rollback: (action) => { + let shape = pointerList[action.shapeUuid] + let object = pointerList[action.objectUuid] + let parent = pointerList[action.parent] + object.removeShape(shape) + delete pointerList[action.shapeUuid] + parent.removeChild(object) + delete pointerList[action.objectUuid] + let selectIndex = context.selection.indexOf(object) + if (selectIndex >= 0) { + context.selection.splice(selectIndex, 1) + } + } }, - editObject: { - create: () => {}, - execute: (action) => { - + editFrame: { + create: (frame) => { + redoStack.length = 0; // Clear redo stack + let action = { + newState: structuredClone(frame.keys), + oldState: startProps[frame.idx], + frame: frame.idx + } + undoStack.push({name: "editFrame", action: action}) + actions.editFrame.execute(action) }, - rollback: (action) => {} + execute: (action) => { + let frame = pointerList[action.frame] + frame.keys = structuredClone(action.newState) + }, + rollback: (action) => { + let frame = pointerList[action.frame] + frame.keys = structuredClone(action.oldState) + console.log(frame) + } }, } @@ -482,6 +546,9 @@ class Frame { } pointerList[this.idx] = this } + saveState() { + startProps[this.idx] = structuredClone(this.keys) + } } class Layer { @@ -507,7 +574,7 @@ class Shape { this.strokeStyle = context.strokeStyle; this.lineWidth = context.lineWidth this.filled = context.fillShape; - this.stroked = context.strokeShape || true; + this.stroked = context.strokeShape; this.boundingBox = { x: {min: startx, max: starty}, y: {min: starty, max: starty} @@ -707,6 +774,7 @@ class GraphicsObject { for (let item of context.selection) { ctx.save() ctx.strokeStyle = "#00ffff" + ctx.lineWidth = 1; ctx.translate(item.x, item.y) ctx.beginPath() let bbox = item.bbox() @@ -717,6 +785,7 @@ class GraphicsObject { if (context.selectionRect) { ctx.save() ctx.strokeStyle = "#00ffff" + ctx.lineWidth = 1; ctx.beginPath() ctx.rect( context.selectionRect.x1, context.selectionRect.y1, @@ -751,6 +820,23 @@ class GraphicsObject { } } } + removeChild(childObject) { + let idx = childObject.idx + for (let layer of this.layers) { + for (let frame of layer.frames) { + delete frame[idx] + } + } + this.children.splice(this.children.indexOf(childObject), 1) + } + saveState() { + startProps[this.idx] = { + x: this.x, + y: this.y, + rotation: this.rotation, + scale: this.scale + } + } } let root = new GraphicsObject(); @@ -814,25 +900,29 @@ function stage() { let img = new Image() img.src = window.URL.createObjectURL(file) img.ix = i + img.idx = uuidv4() + pointerList[img.idx] = img img.onload = function() { - let width = img.width - let height = img.height - let imageObject = new GraphicsObject() - let ct = { - ...context, - fillImage: img, - } - let imageShape = new Shape(0, 0, ct, false) - imageShape.addLine(width, 0) - imageShape.addLine(width, height) - imageShape.addLine(0, height) - imageShape.addLine(0, 0) - imageShape.recalculateBoundingBox() - imageObject.addShape(imageShape) - context.activeObject.addObject( - imageObject, - mouse.x-width/2 + (20*img.ix), - mouse.y-height/2 + (20*img.ix)) + actions.addImageObject.create( + mouse.x, mouse.y, img, context.activeObject) + // let width = img.width + // let height = img.height + // let imageObject = new GraphicsObject() + // let ct = { + // ...context, + // fillImage: img, + // } + // let imageShape = new Shape(0, 0, ct, false) + // imageShape.addLine(width, 0) + // imageShape.addLine(width, height) + // imageShape.addLine(0, height) + // imageShape.addLine(0, 0) + // imageShape.recalculateBoundingBox() + // imageObject.addShape(imageShape) + // context.activeObject.addObject( + // imageObject, + // mouse.x-width/2 + (20*img.ix), + // mouse.y-height/2 + (20*img.ix)) updateUI() } } @@ -871,22 +961,37 @@ function stage() { } else { let selected = false let child; - // Have to iterate in reverse order to grab the frontmost object when two overlap - for (let i=context.activeObject.children.length-1; i>=0; i--) { - child = context.activeObject.children[i] - // let bbox = child.bbox() - if (hitTest(mouse, child)) { - if (context.selection.indexOf(child) != -1) { - // dragging = true - } - context.selection = [child] - selected = true + if (context.selection.length) { + for (child of context.selection) { + if (hitTest(mouse, child)) { + context.dragging = true + context.lastMouse = mouse + context.activeObject.currentFrame.saveState() break + } } } - if (!selected) { - context.selection = [] - context.selectionRect = {x1: mouse.x, x2: mouse.x, y1: mouse.y, y2:mouse.y} + if (!context.dragging) { + // Have to iterate in reverse order to grab the frontmost object when two overlap + for (let i=context.activeObject.children.length-1; i>=0; i--) { + child = context.activeObject.children[i] + // let bbox = child.bbox() + if (hitTest(mouse, child)) { + if (context.selection.indexOf(child) != -1) { + // dragging = true + } + child.saveState() + context.selection = [child] + context.dragging = true + selected = true + context.activeObject.currentFrame.saveState() + break + } + } + if (!selected) { + context.selection = [] + context.selectionRect = {x1: mouse.x, x2: mouse.x, y1: mouse.y, y2:mouse.y} + } } } break; @@ -925,6 +1030,9 @@ function stage() { } } actions.editShape.create(context.activeCurve.shape, newCurves) + } else if (context.selection.length) { + console.log("sopjngf") + actions.editFrame.create(context.activeObject.currentFrame) } break; default: @@ -959,16 +1067,16 @@ function stage() { break; case "select": if (context.dragging) { - // let dist = vectorDist(mouse, context.activeCurve.points[1]) - // let cpoint = context.activeCurve.points[1] - // if (vectorDist(mouse, context.activeCurve.points[2]) < dist) { - // cpoint = context.activeCurve.points[2] - // } - // cpoint.x += (mouse.x - context.lastMouse.x) - // cpoint.y += (mouse.y - context.lastMouse.y) - context.activeCurve.current.points = moldCurve( - context.activeCurve.initial, mouse, context.activeCurve.startmouse - ).points + if (context.activeCurve) { + context.activeCurve.current.points = moldCurve( + context.activeCurve.initial, mouse, context.activeCurve.startmouse + ).points + } else { + for (let child of context.selection) { + context.activeObject.currentFrame.keys[child.idx].x += (mouse.x - context.lastMouse.x) + context.activeObject.currentFrame.keys[child.idx] .y += (mouse.y - context.lastMouse.y) + } + } } else if (context.selectionRect) { context.selectionRect.x2 = mouse.x context.selectionRect.y2 = mouse.y From af2f499412c04f0d3c1f71b46e6836cdf5287c38 Mon Sep 17 00:00:00 2001 From: Skyler Lehmkuhl Date: Wed, 20 Nov 2024 09:38:45 -0500 Subject: [PATCH 23/46] did stuff --- src/main.js | 137 +++++++++++++++++++++++++++++++++++++------------ src/styles.css | 57 ++++++++++++++++++++ 2 files changed, 160 insertions(+), 34 deletions(-) diff --git a/src/main.js b/src/main.js index 557f121..30be141 100644 --- a/src/main.js +++ b/src/main.js @@ -19,6 +19,8 @@ let maxSmoothAngle = 0.6; let undoStack = []; let redoStack = []; +let layoutElements = [] + let tools = { select: { icon: "/assets/select.svg", @@ -398,7 +400,6 @@ function moldCurveMath(curve, mouse) { let interpolatedCurve = new Bezier(molded.points[0], iC1, iC2, molded.points[3]); return interpolatedCurve - // return idealCurve } function deriveControlPoints(S, A, E, e1, e2, t) { @@ -492,13 +493,6 @@ function hitTest(candidate, object) { } } -function pushState() { - // console.log(context) - // let ctx = context.ctx - // context.ctx = undefined - // undoStack.push(window.structuredClone([root,context])) - // context.ctx = ctx -} function undo() { let action = undoStack.pop() if (action) { @@ -650,6 +644,50 @@ class Shape { } } + let epsilon = 0.0001 + // let newCurves = [] + // for (let i=0; i epsilon && t1 < 1-epsilon) { + // let split = remainingCurve.split(t1 / remainingFraction) + // remainingFraction = t1 + // curveBasket.push(split.right) + // remainingCurve = split.left + // } + // if (t2 > epsilon && t2 < 1-epsilon) { + // secondaryIntersects.push(t2) + // } + // } + // curveBasket.reverse() + // for (let curve of curveBasket) { + // newCurves.push(curve) + // } + // curveBasket = [] + // secondaryIntersects.sort() // now sorting with respect to curve 2 + // remainingFraction = 1 + // remainingCurve = this.curves[j] + // for (let t2 of secondaryIntersects) { + // let split = remainingCurve.split(t2 / remainingFraction) + // remainingFraction = t2 + // curveBasket.push(split.right) + // remainingCurve = split.left + // } + // this.curves.splice(j, 1, ...curveBasket) + // } + // } + // } + // this.curves = newCurves this.recalculateBoundingBox() } draw(context) { @@ -854,12 +892,13 @@ window.addEventListener("DOMContentLoaded", () => { rootPane.addEventListener("mousemove", (e) => { mouseEvent = e; }) - let [_toolbar, panel] = splitPane(rootPane, 10, true) - let [_stage, _infopanel] = splitPane(panel, 70, false, createPane(infopanel())) + let [_toolbar, panel] = splitPane(rootPane, 10, true, createPane(timeline())) + let [stageAndTimeline, _infopanel] = splitPane(panel, 70, false, createPane(infopanel())) + let [_timeline, _stage] = splitPane(stageAndTimeline, 30, false, createPane(stage())) }); window.addEventListener("resize", () => { - updateLayout(rootPane) + updateAll() }) window.addEventListener("keypress", (e) => { @@ -905,24 +944,6 @@ function stage() { img.onload = function() { actions.addImageObject.create( mouse.x, mouse.y, img, context.activeObject) - // let width = img.width - // let height = img.height - // let imageObject = new GraphicsObject() - // let ct = { - // ...context, - // fillImage: img, - // } - // let imageShape = new Shape(0, 0, ct, false) - // imageShape.addLine(width, 0) - // imageShape.addLine(width, height) - // imageShape.addLine(0, height) - // imageShape.addLine(0, 0) - // imageShape.recalculateBoundingBox() - // imageObject.addShape(imageShape) - // context.activeObject.addObject( - // imageObject, - // mouse.x-width/2 + (20*img.ix), - // mouse.y-height/2 + (20*img.ix)) updateUI() } } @@ -942,7 +963,6 @@ function stage() { switch (mode) { case "rectangle": case "draw": - pushState() context.mouseDown = true context.activeShape = new Shape(mouse.x, mouse.y, context, true, true) context.lastMouse = mouse @@ -1012,7 +1032,6 @@ function stage() { context.activeShape.addLine(mouse.x, mouse.y) context.activeShape.simplify(context.simplifyMode) actions.addShape.create(context.activeObject, context.activeShape) - // context.activeObject.addShape(context.activeShape) context.activeShape = undefined } break; @@ -1176,6 +1195,22 @@ function toolbar() { return tools_scroller } +function timeline() { + let container = document.createElement("div") + let layerspanel = document.createElement("div") + let framescontainer = document.createElement("div") + container.classList.add("horizontal-grid") + container.classList.add("layers-container") + layerspanel.className = "layers" + framescontainer.className = "frames-container" + container.appendChild(layerspanel) + container.appendChild(framescontainer) + layoutElements.push(container) + container.setAttribute("lb-percent", 20) + + return container +} + function infopanel() { let panel = document.createElement("div") panel.className = "infopanel" @@ -1287,23 +1322,31 @@ function splitPane(div, percent, horiz, newPane=undefined) { } div.setAttribute("lb-percent", percent) // TODO: better attribute name Coloris({el: ".color-field"}) + updateAll() updateUI() - updateLayout(rootPane) + updateLayers() return [div1, div2] } +function updateAll() { + updateLayout(rootPane) + for (let element of layoutElements) { + updateLayout(element) + } +} + function updateLayout(element) { let rect = element.getBoundingClientRect() let percent = element.getAttribute("lb-percent") percent ||= 50 let children = element.children if (children.length != 2) return; - if (element.className == "horizontal-grid") { + if (element.classList.contains("horizontal-grid")) { children[0].style.width = `${rect.width * percent / 100}px` children[1].style.width = `${rect.width * (100 - percent) / 100}px` children[0].style.height = `${rect.height}px` children[1].style.height = `${rect.height}px` - } else if (element.className == "vertical-grid") { + } else if (element.classList.contains("vertical-grid")) { children[0].style.height = `${rect.height * percent / 100}px` children[1].style.height = `${rect.height * (100 - percent) / 100}px` children[0].style.width = `${rect.width}px` @@ -1331,4 +1374,30 @@ function updateUI() { } } +} + +function updateLayers() { + console.log(document.querySelectorAll(".layers-container")) + for (let container of document.querySelectorAll(".layers-container")) { + console.log("?") + let layerspanel = container.querySelectorAll(".layers")[0] + let framescontainer = container.querySelectorAll(".frames-container")[0] + layerspanel.textContent = "" + framescontainer.textContent = "" + for (let layer of context.activeObject.layers) { + // for (let i=0; i<5; i++) { + let layerHeader = document.createElement("div") + layerHeader.className = "layer-header" + layerspanel.appendChild(layerHeader) + let layerTrack = document.createElement("div") + layerTrack.className = "layer-track" + framescontainer.appendChild(layerTrack) + for (let frame of layer.frames) { + // for (let j=0; j<5-i; j++) { + let frameEl = document.createElement("div") + frameEl.className = "frame" + layerTrack.appendChild(frameEl) + } + } + } } \ No newline at end of file diff --git a/src/styles.css b/src/styles.css index bf2a01e..6a08714 100644 --- a/src/styles.css +++ b/src/styles.css @@ -78,6 +78,11 @@ button { box-shadow: 0 2px 2px rgba(0, 0, 0, 0.2); } +div { + /* this should be on everything by default, really */ + box-sizing: border-box; +} + button { cursor: pointer; } @@ -227,4 +232,56 @@ button { .infopanel-input { flex: 1 1 50%; width: 50%; +} +.layers-container { + overflow-y: scroll; +} +.layers { + background-color: #222222; + display: flex; + flex-direction: column; + flex-wrap: nowrap; + min-height: 100%; +} +.frames-container { + background-color: #222222; + display: flex; + flex-direction: column; + flex-wrap: nowrap; + /* overflow-x: scroll; */ + /* overflow-y:inherit; */ + min-height: 100%; +} +.layer-header { + width: 100%; + height: calc( 2 * var(--lineheight)); + background-color: #3f3f3f; + border-top: 1px solid #4f4f4f; + border-bottom: 1px solid #222222; + flex-shrink: 0; +} +.layer-track { + min-width: 100%; + height: calc( 2 * var(--lineheight)); + background: repeating-linear-gradient(to right, transparent, transparent 24px, #3f3f3f 24px, #3f3f3f 25px), + repeating-linear-gradient(to right, #222222, #222222 100px, #151515 100px, #151515 125px); + + display: flex; + flex-direction: row; + border-top: 1px solid #222222; + border-bottom: 1px solid #3f3f3f; + flex-shrink: 0; +} +.frame { + width: 25px; + height: 100%; + + background-color: #4f4f4f; + flex-grow: 0; + flex-shrink: 0; + border-right: 1px solid #3f3f3f; + border-left: 1px solid #555555; +} +.frame:hover { + background-color: #555555; } \ No newline at end of file From 75884e0d7d73b4a8998882dbba7cbf4b7ec2ff5d Mon Sep 17 00:00:00 2001 From: Skyler Lehmkuhl Date: Wed, 20 Nov 2024 10:09:36 -0500 Subject: [PATCH 24/46] split curves on intersections --- src/main.js | 89 +++++++++++++++++++++++++++-------------------------- 1 file changed, 45 insertions(+), 44 deletions(-) diff --git a/src/main.js b/src/main.js index 30be141..e3ad3e6 100644 --- a/src/main.js +++ b/src/main.js @@ -644,50 +644,51 @@ class Shape { } } - let epsilon = 0.0001 - // let newCurves = [] - // for (let i=0; i epsilon && t1 < 1-epsilon) { - // let split = remainingCurve.split(t1 / remainingFraction) - // remainingFraction = t1 - // curveBasket.push(split.right) - // remainingCurve = split.left - // } - // if (t2 > epsilon && t2 < 1-epsilon) { - // secondaryIntersects.push(t2) - // } - // } - // curveBasket.reverse() - // for (let curve of curveBasket) { - // newCurves.push(curve) - // } - // curveBasket = [] - // secondaryIntersects.sort() // now sorting with respect to curve 2 - // remainingFraction = 1 - // remainingCurve = this.curves[j] - // for (let t2 of secondaryIntersects) { - // let split = remainingCurve.split(t2 / remainingFraction) - // remainingFraction = t2 - // curveBasket.push(split.right) - // remainingCurve = split.left - // } - // this.curves.splice(j, 1, ...curveBasket) - // } - // } - // } - // this.curves = newCurves + let epsilon = 0.01 + let newCurves = [] + let intersectMap = {} + for (let i=0; i=0; i--) { + if (i in intersectMap) { + intersectMap[i].sort().reverse() + let remainingFraction = 1 + let remainingCurve = this.curves[i] + for (let t of intersectMap[i]) { + let split = remainingCurve.split(t / remainingFraction) + remainingFraction = t + newCurves.push(split.right) + remainingCurve = split.left + } + newCurves.push(remainingCurve) + + } else { + newCurves.push(this.curves[i]) + } + } + newCurves.reverse() + this.curves = newCurves this.recalculateBoundingBox() } draw(context) { From 78f03c2a1270d42d9fe1d986ad59513bf00b06b2 Mon Sep 17 00:00:00 2001 From: Skyler Lehmkuhl Date: Wed, 20 Nov 2024 10:33:17 -0500 Subject: [PATCH 25/46] Handle curve selections properly for overlapping shapes --- src/main.js | 101 +++++++++++++++++++++++++++------------------------- 1 file changed, 52 insertions(+), 49 deletions(-) diff --git a/src/main.js b/src/main.js index e3ad3e6..01a73ff 100644 --- a/src/main.js +++ b/src/main.js @@ -303,26 +303,28 @@ function setProperty(context, path, value) { function selectCurve(context, mouse) { let mouseTolerance = 15; + let closestDist = mouseTolerance; + let closestCurve = undefined + let closestShape = undefined for (let shape of context.activeObject.currentFrame.shapes) { if (mouse.x > shape.boundingBox.x.min - mouseTolerance && mouse.x < shape.boundingBox.x.max + mouseTolerance && mouse.y > shape.boundingBox.y.min - mouseTolerance && mouse.y < shape.boundingBox.y.max + mouseTolerance) { - let closestDist = mouseTolerance; - let closest = undefined for (let curve of shape.curves) { let dist = vectorDist(mouse, curve.project(mouse)) if (dist <= closestDist ) { closestDist = dist - closest = curve + closestCurve = curve + closestShape = shape } } - if (closest) { - return {curve:closest, shape:shape} - } else { - return undefined } } + if (closestCurve) { + return {curve:closestCurve, shape:closestShape} + } else { + return undefined } } @@ -645,50 +647,50 @@ class Shape { } } let epsilon = 0.01 - let newCurves = [] - let intersectMap = {} - for (let i=0; i=0; i--) { - if (i in intersectMap) { - intersectMap[i].sort().reverse() - let remainingFraction = 1 - let remainingCurve = this.curves[i] - for (let t of intersectMap[i]) { - let split = remainingCurve.split(t / remainingFraction) - remainingFraction = t - newCurves.push(split.right) - remainingCurve = split.left - } - newCurves.push(remainingCurve) + } + } + } + for (let lst in intersectMap) { + for (let i=1; i=0; i--) { + if (i in intersectMap) { + intersectMap[i].sort().reverse() + let remainingFraction = 1 + let remainingCurve = this.curves[i] + for (let t of intersectMap[i]) { + let split = remainingCurve.split(t / remainingFraction) + remainingFraction = t + newCurves.push(split.right) + remainingCurve = split.left + } + newCurves.push(remainingCurve) - } else { - newCurves.push(this.curves[i]) - } - } - newCurves.reverse() - this.curves = newCurves + } else { + newCurves.push(this.curves[i]) + } + } + newCurves.reverse() + this.curves = newCurves this.recalculateBoundingBox() } draw(context) { @@ -1108,6 +1110,7 @@ function stage() { } } else { let selection = selectCurve(context, mouse) + console.log(selection) if (selection) { context.activeCurve = { current: selection.curve, From 47b8df5d3e4353dad6919ea7a57368d3b583f474 Mon Sep 17 00:00:00 2001 From: Skyler Lehmkuhl Date: Thu, 21 Nov 2024 00:33:10 -0500 Subject: [PATCH 26/46] Edit vertices --- src/main.js | 261 ++++++++++++++++++++++++++++++++++++++++++---------- 1 file changed, 212 insertions(+), 49 deletions(-) diff --git a/src/main.js b/src/main.js index 01a73ff..07b5277 100644 --- a/src/main.js +++ b/src/main.js @@ -132,6 +132,7 @@ let actions = { curve.points[3].x, curve.points[3].y )) } + shape.update() object.addShape(shape) }, rollback: (action) => { @@ -174,6 +175,7 @@ let actions = { curve.points[3].x, curve.points[3].y )) } + shape.update() }, rollback: (action) => { let shape = pointerList[action.shape] @@ -187,7 +189,9 @@ let actions = { curve.points[2].x, curve.points[2].y, curve.points[3].x, curve.points[3].y )) - }} + } + shape.update() + } }, addImageObject: { create: (x, y, img, parent) => { @@ -219,7 +223,7 @@ let actions = { imageShape.addLine(action.width, action.height) imageShape.addLine(0, action.height) imageShape.addLine(0, 0) - imageShape.recalculateBoundingBox() + imageShape.update() imageObject.addShape(imageShape) let parent = pointerList[action.parent] parent.addObject( @@ -260,7 +264,6 @@ let actions = { rollback: (action) => { let frame = pointerList[action.frame] frame.keys = structuredClone(action.oldState) - console.log(frame) } }, } @@ -327,12 +330,37 @@ function selectCurve(context, mouse) { return undefined } } +function selectVertex(context, mouse) { + let mouseTolerance = 15; + let closestDist = mouseTolerance; + let closestVertex = undefined + let closestShape = undefined + for (let shape of context.activeObject.currentFrame.shapes) { + if (mouse.x > shape.boundingBox.x.min - mouseTolerance && + mouse.x < shape.boundingBox.x.max + mouseTolerance && + mouse.y > shape.boundingBox.y.min - mouseTolerance && + mouse.y < shape.boundingBox.y.max + mouseTolerance) { + for (let vertex of shape.vertices) { + let dist = vectorDist(mouse, vertex.point) + if (dist <= closestDist ) { + closestDist = dist + closestVertex = vertex + closestShape = shape + } + } + } + } + if (closestVertex) { + return {vertex:closestVertex, shape:closestShape} + } else { + return undefined + } +} function moldCurve(curve, mouse, oldmouse) { let diff = {x: mouse.x - oldmouse.x, y: mouse.y - oldmouse.y} let p = curve.project(mouse) let min_influence = 0.1 - console.log(p.t) const CP1 = { x: curve.points[1].x + diff.x*(1-p.t)*2, y: curve.points[1].y + diff.y*(1-p.t)*2 @@ -390,7 +418,6 @@ function moldCurveMath(curve, mouse) { let d = Bezier.getUtils().dist(ideal.B, p); let t2 = Math.min(falloff, d) / falloff; - console.log(d) let iC1 = { x: (1-t2) * molded.points[1].x + t2 * idealCurve.points[1].x, y: (1-t2) * molded.points[1].y + t2 * idealCurve.points[1].y @@ -565,6 +592,7 @@ class Shape { this.startx = startx; this.starty = starty; this.curves = []; + this.vertices = []; this.fillStyle = context.fillStyle; this.fillImage = context.fillImage; this.strokeStyle = context.strokeStyle; @@ -652,7 +680,6 @@ class Shape { for (let i=0; i=0; i--) { - child = context.activeObject.children[i] - // let bbox = child.bbox() - if (hitTest(mouse, child)) { - if (context.selection.indexOf(child) != -1) { - // dragging = true - } - child.saveState() - context.selection = [child] + console.log("gonna move this") + } else { + let selected = false + let child; + if (context.selection.length) { + for (child of context.selection) { + if (hitTest(mouse, child)) { context.dragging = true - selected = true + context.lastMouse = mouse context.activeObject.currentFrame.saveState() break + } } } - if (!selected) { - context.selection = [] - context.selectionRect = {x1: mouse.x, x2: mouse.x, y1: mouse.y, y2:mouse.y} + if (!context.dragging) { + // Have to iterate in reverse order to grab the frontmost object when two overlap + for (let i=context.activeObject.children.length-1; i>=0; i--) { + child = context.activeObject.children[i] + // let bbox = child.bbox() + if (hitTest(mouse, child)) { + if (context.selection.indexOf(child) != -1) { + // dragging = true + } + child.saveState() + context.selection = [child] + context.dragging = true + selected = true + context.activeObject.currentFrame.saveState() + break + } + } + if (!selected) { + context.selection = [] + context.selectionRect = {x1: mouse.x, x2: mouse.x, y1: mouse.y, y2:mouse.y} + } } } } @@ -1042,7 +1157,19 @@ function stage() { context.activeShape = undefined break; case "select": - if (context.activeCurve) { + if (context.activeVertex) { + let newCurves = [] + for (let i in context.activeVertex.shape.curves) { + if (i in context.activeVertex.current.startCurves) { + newCurves.push(context.activeVertex.current.startCurves[i]) + } else if (i in context.activeVertex.current.endCurves) { + newCurves.push(context.activeVertex.current.endCurves[i]) + } else { + newCurves.push(context.activeVertex.shape.curves[i]) + } + } + actions.editShape.create(context.activeVertex.shape, newCurves) + } else if (context.activeCurve) { let newCurves = [] for (let curve of context.activeCurve.shape.curves) { if (curve == context.activeCurve.initial) { @@ -1053,7 +1180,6 @@ function stage() { } actions.editShape.create(context.activeCurve.shape, newCurves) } else if (context.selection.length) { - console.log("sopjngf") actions.editFrame.create(context.activeObject.currentFrame) } break; @@ -1084,12 +1210,35 @@ function stage() { context.activeShape.addLine(mouse.x, mouse.y) context.activeShape.addLine(context.activeShape.startx, mouse.y) context.activeShape.addLine(context.activeShape.startx, context.activeShape.starty) - context.activeShape.recalculateBoundingBox() + context.activeShape.update() } break; case "select": if (context.dragging) { - if (context.activeCurve) { + if (context.activeVertex) { + let vert = context.activeVertex + let mouseDelta = {x: mouse.x - vert.startmouse.x, y: mouse.y - vert.startmouse.y} + vert.current.point.x = vert.initial.point.x + mouseDelta.x + vert.current.point.y = vert.initial.point.y + mouseDelta.y + for (let i in vert.current.startCurves) { + let curve = vert.current.startCurves[i] + let oldCurve = vert.initial.startCurves[i] + curve.points[0] = vert.current.point + curve.points[1] = { + x: oldCurve.points[1].x + mouseDelta.x, + y: oldCurve.points[1].y + mouseDelta.y + } + } + for (let i in vert.current.endCurves) { + let curve = vert.current.endCurves[i] + let oldCurve = vert.initial.endCurves[i] + curve.points[3] = {x:vert.current.point.x, y:vert.current.point.y} + curve.points[2] = { + x: oldCurve.points[2].x + mouseDelta.x, + y: oldCurve.points[2].y + mouseDelta.y + } + } + } else if (context.activeCurve) { context.activeCurve.current.points = moldCurve( context.activeCurve.initial, mouse, context.activeCurve.startmouse ).points @@ -1109,17 +1258,32 @@ function stage() { } } } else { - let selection = selectCurve(context, mouse) - console.log(selection) + let selection = selectVertex(context, mouse) if (selection) { - context.activeCurve = { - current: selection.curve, - initial: new Bezier(selection.curve.points), + context.activeCurve = undefined + context.activeVertex = { + current: selection.vertex, + initial: { + point: {x: selection.vertex.point.x, y: selection.vertex.point.y}, + startCurves: structuredClone(selection.vertex.startCurves), + endCurves: structuredClone(selection.vertex.endCurves), + }, shape: selection.shape, - startmouse: mouse + startmouse: {x: mouse.x, y: mouse.y} } } else { - context.activeCurve = undefined + context.activeVertex = undefined + selection = selectCurve(context, mouse) + if (selection) { + context.activeCurve = { + current: selection.curve, + initial: new Bezier(selection.curve.points), + shape: selection.shape, + startmouse: mouse + } + } else { + context.activeCurve = undefined + } } } context.lastMouse = mouse @@ -1383,7 +1547,6 @@ function updateUI() { function updateLayers() { console.log(document.querySelectorAll(".layers-container")) for (let container of document.querySelectorAll(".layers-container")) { - console.log("?") let layerspanel = container.querySelectorAll(".layers")[0] let framescontainer = container.querySelectorAll(".frames-container")[0] layerspanel.textContent = "" From 1569a40495e42f4d1f7ae86691c4f842f05d563d Mon Sep 17 00:00:00 2001 From: Skyler Lehmkuhl Date: Thu, 21 Nov 2024 00:40:30 -0500 Subject: [PATCH 27/46] fix rectangles --- src/main.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/main.js b/src/main.js index 07b5277..30c52b0 100644 --- a/src/main.js +++ b/src/main.js @@ -1154,6 +1154,7 @@ function stage() { } break; case "rectangle": + actions.addShape.create(context.activeObject, context.activeShape) context.activeShape = undefined break; case "select": From 265d9564ad01575ae03d9e537b5448fd3789f2a9 Mon Sep 17 00:00:00 2001 From: Skyler Lehmkuhl Date: Thu, 21 Nov 2024 00:58:27 -0500 Subject: [PATCH 28/46] put color on curve objects --- src/bezier.js | 5 ++++ src/main.js | 83 ++++++++++++++++++++++++++++----------------------- 2 files changed, 50 insertions(+), 38 deletions(-) diff --git a/src/bezier.js b/src/bezier.js index d065495..8f703f1 100644 --- a/src/bezier.js +++ b/src/bezier.js @@ -1149,6 +1149,11 @@ class Bezier { this._lut = []; // invalidate any precomputed LUT } + setColor(color) { + this.color = color + return this + } + verify() { const print = this.coordDigest(); if (print !== this._print) { diff --git a/src/main.js b/src/main.js index 30c52b0..58d192a 100644 --- a/src/main.js +++ b/src/main.js @@ -124,13 +124,12 @@ let actions = { let curvesList = action.curves let shape = new Shape(action.startx, action.starty, context, action.uuid) for (let curve of curvesList) { - shape.addCurve( - new Bezier( - curve.points[0].x, curve.points[0].y, - curve.points[1].x, curve.points[1].y, - curve.points[2].x, curve.points[2].y, - curve.points[3].x, curve.points[3].y - )) + shape.addCurve(new Bezier( + curve.points[0].x, curve.points[0].y, + curve.points[1].x, curve.points[1].y, + curve.points[2].x, curve.points[2].y, + curve.points[3].x, curve.points[3].y + ).setColor(curve.color)) } shape.update() object.addShape(shape) @@ -167,13 +166,12 @@ let actions = { let curvesList = action.newCurves shape.curves = [] for (let curve of curvesList) { - shape.addCurve( - new Bezier( - curve.points[0].x, curve.points[0].y, - curve.points[1].x, curve.points[1].y, - curve.points[2].x, curve.points[2].y, - curve.points[3].x, curve.points[3].y - )) + shape.addCurve(new Bezier( + curve.points[0].x, curve.points[0].y, + curve.points[1].x, curve.points[1].y, + curve.points[2].x, curve.points[2].y, + curve.points[3].x, curve.points[3].y + ).setColor(curve.color)) } shape.update() }, @@ -182,13 +180,12 @@ let actions = { let curvesList = action.oldCurves shape.curves = [] for (let curve of curvesList) { - shape.addCurve( - new Bezier( - curve.points[0].x, curve.points[0].y, - curve.points[1].x, curve.points[1].y, - curve.points[2].x, curve.points[2].y, - curve.points[3].x, curve.points[3].y - )) + shape.addCurve(new Bezier( + curve.points[0].x, curve.points[0].y, + curve.points[1].x, curve.points[1].y, + curve.points[2].x, curve.points[2].y, + curve.points[3].x, curve.points[3].y + ).setColor(curve.color)) } shape.update() } @@ -626,6 +623,7 @@ class Shape { midpoint.x, midpoint.y, midpoint.x, midpoint.y, x, y) + curve.color = context.strokeStyle this.curves.push(curve) } clear() { @@ -716,8 +714,11 @@ class Shape { newCurves.push(this.curves[i]) } } + for (let curve of newCurves) { + curve.color = context.strokeStyle + } newCurves.reverse() - this.curves = newCurves + this.curves = newCurves this.update() } update() { @@ -765,20 +766,16 @@ class Shape { } draw(context) { let ctx = context.ctx; - ctx.beginPath() ctx.lineWidth = this.lineWidth - ctx.moveTo(this.startx, this.starty) - for (let curve of this.curves) { - ctx.bezierCurveTo(curve.points[1].x, curve.points[1].y, - curve.points[2].x, curve.points[2].y, - curve.points[3].x, curve.points[3].y) - - // Debug, show curve endpoints - // ctx.beginPath() - // ctx.arc(curve.points[3].x,curve.points[3].y, 3, 0, 2*Math.PI) - // ctx.fill() - } + ctx.lineCap = "round" if (this.filled) { + ctx.beginPath() + ctx.moveTo(this.startx, this.starty) + for (let curve of this.curves) { + ctx.bezierCurveTo(curve.points[1].x, curve.points[1].y, + curve.points[2].x, curve.points[2].y, + curve.points[3].x, curve.points[3].y) + } if (this.fillImage) { let pat = ctx.createPattern(this.fillImage, "no-repeat") ctx.fillStyle = pat @@ -787,9 +784,19 @@ class Shape { } ctx.fill() } - if (this.stroked) { - ctx.strokeStyle = this.strokeStyle + for (let curve of this.curves) { + ctx.strokeStyle = curve.color + ctx.beginPath() + ctx.moveTo(curve.points[0].x, curve.points[0].y) + ctx.bezierCurveTo(curve.points[1].x, curve.points[1].y, + curve.points[2].x, curve.points[2].y, + curve.points[3].x, curve.points[3].y) ctx.stroke() + + // Debug, show curve endpoints + // ctx.beginPath() + // ctx.arc(curve.points[3].x,curve.points[3].y, 3, 0, 2*Math.PI) + // ctx.fill() } } @@ -1090,7 +1097,7 @@ function stage() { context.activeVertex = undefined context.activeCurve = { initial: selection.curve, - current: new Bezier(selection.curve.points), + current: new Bezier(selection.curve.points).setColor(selection.curve.color), shape: selection.shape, startmouse: {x: mouse.x, y: mouse.y} } @@ -1278,7 +1285,7 @@ function stage() { if (selection) { context.activeCurve = { current: selection.curve, - initial: new Bezier(selection.curve.points), + initial: new Bezier(selection.curve.points).setColor(selection.curve.color), shape: selection.shape, startmouse: mouse } From c2903c535e6bda5873e37138db30136e757bd7bf Mon Sep 17 00:00:00 2001 From: Skyler Lehmkuhl Date: Thu, 21 Nov 2024 01:01:26 -0500 Subject: [PATCH 29/46] put color on curve objects but like for real this time --- src/main.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main.js b/src/main.js index 58d192a..53d4667 100644 --- a/src/main.js +++ b/src/main.js @@ -107,7 +107,7 @@ let actions = { redoStack.length = 0; // Clear redo stack let serializableCurves = [] for (let curve of shape.curves) { - serializableCurves.push({ points: curve.points }) + serializableCurves.push({ points: curve.points, color: curve.color }) } let action = { parent: parent.idx, @@ -146,7 +146,7 @@ let actions = { redoStack.length = 0; // Clear redo stack let serializableNewCurves = [] for (let curve of newCurves) { - serializableNewCurves.push({ points: curve.points }) + serializableNewCurves.push({ points: curve.points, color: curve.color }) } let serializableOldCurves = [] for (let curve of shape.curves) { From f3b0a08c19a7285a45cd4240dbb88437114762e7 Mon Sep 17 00:00:00 2001 From: Skyler Lehmkuhl Date: Tue, 26 Nov 2024 07:21:55 -0500 Subject: [PATCH 30/46] glitchy region filling --- src/main.js | 151 +++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 149 insertions(+), 2 deletions(-) diff --git a/src/main.js b/src/main.js index 53d4667..2a98a62 100644 --- a/src/main.js +++ b/src/main.js @@ -1,6 +1,7 @@ const { invoke } = window.__TAURI__.core; import * as fitCurve from '/fit-curve.js'; import { Bezier } from "/bezier.js"; +import earcut from './earcut.js'; let simplifyPolyline = simplify @@ -590,6 +591,8 @@ class Shape { this.starty = starty; this.curves = []; this.vertices = []; + this.triangles = []; + this.regions = []; this.fillStyle = context.fillStyle; this.fillImage = context.fillImage; this.strokeStyle = context.strokeStyle; @@ -714,6 +717,7 @@ class Shape { newCurves.push(this.curves[i]) } } + console.log(structuredClone(newCurves)) for (let curve of newCurves) { curve.color = context.strokeStyle } @@ -729,12 +733,28 @@ class Shape { this.starty = this.curves[0].points[0].y } } + getClockwiseCurves(point, otherPoints) { + // Returns array of {x, y, idx, angle} + + let points = [] + for (let point of otherPoints) { + points.push({...this.vertices[point].point, idx: point}) + } + // Add an angle property to each point using tan(angle) = y/x + const angles = points.map(({ x, y, idx }) => { + return { x, y, idx, angle: Math.atan2(y - point.y, x - point.x) * 180 / Math.PI }; + }); + // Sort your points by angle + const pointsSorted = angles.sort((a, b) => a.angle - b.angle); + return pointsSorted + } updateVertices() { this.vertices = [] let utils = Bezier.getUtils() let epsilon = 1.5 // big epsilon whoa let tooClose; let i = 0; + // Generate vertices for (let curve of this.curves) { for (let index of [0, 3]) { tooClose = false @@ -750,12 +770,12 @@ class Shape { this.vertices.push({ point:curve.points[index], startCurves: {[i]:curve}, - endCurves: [] + endCurves: {} }) } else { this.vertices.push({ point:curve.points[index], - startCurves: [], + startCurves: {}, endCurves: {[i]:curve} }) } @@ -763,6 +783,122 @@ class Shape { } i++; } + // Generate enclosed regions + let graph = {} + let edges = [] + this.vertices.forEach((vertex, i) => { + this.vertices.forEach((otherVertex, j) => { + for (let curve in vertex.startCurves) { + if (curve in otherVertex.endCurves) { + edges.push([i, j]) + if (graph[i]) { + graph[i].push(j) + } else { + graph[i] = [j] + } + } + } + for (let curve in vertex.endCurves) { + if (curve in otherVertex.startCurves) { + edges.push([i, j]) + if (graph[i]) { + graph[i].push(j) + } else { + graph[i] = [j] + } + } + } + }) + }) + // for (let vertex in graph) { + // let node = vertex + // let seenNodes = [] + // for (let i=0; i 1) { + for (let otherPolygon of polygons) { + if (polygon.every((val, idx) => val === otherPolygon[idx])) { + return; + } + } + polygons.push(polygon); + } + return; + } + + visited.add(node); + for (const edge of edges) { + if (edge[0] === node) { + dfs(edge[1], [...path, node]); + } else if (edge[1] === node) { + dfs(edge[0], [...path, node]); + } + } + } + + for (const edge of edges) { + dfs(edge[0], []); + } + + return polygons; + } + + + const polygons = findEnclosedPolygons(edges); + this.regions = [] + + for (let polygon of polygons) { + let region = [] + let firstVertex = undefined + let lastVertex = undefined + for (let vertex of polygon) { + firstVertex ||= vertex + if (lastVertex) { + for (let i in this.vertices[vertex].startCurves) { + let curve = this.vertices[vertex].startCurves[i] + if (i in this.vertices[lastVertex].endCurves) { + region.push(curve) + } + } + for (let i in this.vertices[vertex].endCurves) { + let curve = this.vertices[vertex].endCurves[i] + if (i in this.vertices[lastVertex].startCurves) { + region.push(new Bezier(curve.points.toReversed())) + } + } + } + lastVertex = vertex + } + for (let i in this.vertices[firstVertex].startCurves) { + let curve = this.vertices[firstVertex].startCurves[i] + if (i in this.vertices[lastVertex].endCurves) { + region.push(curve) + } + } + for (let i in this.vertices[firstVertex].endCurves) { + let curve = this.vertices[firstVertex].endCurves[i] + if (i in this.vertices[lastVertex].startCurves) { + region.push(new Bezier(curve.points.toReversed())) + } + } + if (region.length > 1) { + // Filter out single curves + this.regions.push(region) + } + } + console.log("regions") + console.log(this.regions) } draw(context) { let ctx = context.ctx; @@ -798,6 +934,17 @@ class Shape { // ctx.arc(curve.points[3].x,curve.points[3].y, 3, 0, 2*Math.PI) // ctx.fill() } + for (let region of this.regions) { + ctx.fillStyle = `#${Math.random().toString(16).slice(-6)}` + ctx.beginPath() + for (let curve of region) { + ctx.lineTo(curve.points[0].x, curve.points[0].y) + ctx.bezierCurveTo(curve.points[1].x, curve.points[1].y, + curve.points[2].x, curve.points[2].y, + curve.points[3].x, curve.points[3].y) + } + ctx.fill() + } } } From e0c5e13f577f1c0c75b13c7b591177504c64cd72 Mon Sep 17 00:00:00 2001 From: Skyler Lehmkuhl Date: Tue, 26 Nov 2024 08:50:38 -0500 Subject: [PATCH 31/46] finally, paint bucket --- src/bezier.js | 4 ++ src/main.js | 127 ++++++++++++++++++++++++++++++++++++++------------ 2 files changed, 100 insertions(+), 31 deletions(-) diff --git a/src/bezier.js b/src/bezier.js index 8f703f1..6b12ce6 100644 --- a/src/bezier.js +++ b/src/bezier.js @@ -1994,6 +1994,10 @@ class Bezier { return p; } + toString() { + return `<[${this.points[0].x},${this.points[0].y}], [${this.points[1].x},${this.points[1].y}], [${this.points[2].x},${this.points[2].y}], [${this.points[3].x},${this.points[3].y}]` + } + } export { Bezier }; diff --git a/src/main.js b/src/main.js index 2a98a62..9c258b6 100644 --- a/src/main.js +++ b/src/main.js @@ -717,13 +717,11 @@ class Shape { newCurves.push(this.curves[i]) } } - console.log(structuredClone(newCurves)) for (let curve of newCurves) { curve.color = context.strokeStyle } newCurves.reverse() this.curves = newCurves - this.update() } update() { this.recalculateBoundingBox() @@ -754,6 +752,21 @@ class Shape { let epsilon = 1.5 // big epsilon whoa let tooClose; let i = 0; + + + this.regions = [{curves: [], fillStyle: undefined, filled: false}] + for (let curve of this.curves) { + this.regions[0].curves.push(curve) + } + if (this.regions[0].curves.length) { + if (utils.dist( + this.regions[0].curves[0].points[0], + this.regions[0].curves[this.regions[0].curves.length - 1].points[3] + ) < epsilon) { + this.regions[0].filled = true + } + } + // Generate vertices for (let curve of this.curves) { for (let index of [0, 3]) { @@ -783,6 +796,50 @@ class Shape { } i++; } + + this.vertices.forEach((vertex, i) => { + console.log(i) + for (let i=0; i start) { + this.regions.push({ + curves: region.curves.splice(start, end - start), + fillStyle: region.fillStyle, + filled: true + }) + } + } else { + // not sure how to handle vertices with more than 4 curves + console.log(`Unexpected vertex with ${Object.keys(vertexCurves).length} curves!`) + } + } + }) // Generate enclosed regions let graph = {} let edges = [] @@ -854,7 +911,7 @@ class Shape { return polygons; } - + /* const polygons = findEnclosedPolygons(edges); this.regions = [] @@ -896,29 +953,31 @@ class Shape { // Filter out single curves this.regions.push(region) } - } - console.log("regions") - console.log(this.regions) + }*/ } draw(context) { let ctx = context.ctx; ctx.lineWidth = this.lineWidth ctx.lineCap = "round" - if (this.filled) { - ctx.beginPath() - ctx.moveTo(this.startx, this.starty) - for (let curve of this.curves) { - ctx.bezierCurveTo(curve.points[1].x, curve.points[1].y, - curve.points[2].x, curve.points[2].y, - curve.points[3].x, curve.points[3].y) + for (let region of this.regions) { + // if (region.filled) continue; + if (region.fillStyle && region.filled) { + // ctx.fillStyle = region.fill + if (region.fillImage) { + let pat = ctx.createPattern(region.fillImage, "no-repeat") + ctx.fillStyle = pat + } else { + ctx.fillStyle = region.fillStyle + } + ctx.beginPath() + for (let curve of region.curves) { + ctx.lineTo(curve.points[0].x, curve.points[0].y) + ctx.bezierCurveTo(curve.points[1].x, curve.points[1].y, + curve.points[2].x, curve.points[2].y, + curve.points[3].x, curve.points[3].y) + } + ctx.fill() } - if (this.fillImage) { - let pat = ctx.createPattern(this.fillImage, "no-repeat") - ctx.fillStyle = pat - } else { - ctx.fillStyle = this.fillStyle - } - ctx.fill() } for (let curve of this.curves) { ctx.strokeStyle = curve.color @@ -934,17 +993,6 @@ class Shape { // ctx.arc(curve.points[3].x,curve.points[3].y, 3, 0, 2*Math.PI) // ctx.fill() } - for (let region of this.regions) { - ctx.fillStyle = `#${Math.random().toString(16).slice(-6)}` - ctx.beginPath() - for (let curve of region) { - ctx.lineTo(curve.points[0].x, curve.points[0].y) - ctx.bezierCurveTo(curve.points[1].x, curve.points[1].y, - curve.points[2].x, curve.points[2].y, - curve.points[3].x, curve.points[3].y) - } - ctx.fill() - } } } @@ -1287,6 +1335,23 @@ function stage() { } } break; + case "paint_bucket": + let line = {p1: mouse, p2: {x: mouse.x + 3000, y: mouse.y}} + for (let shape of context.activeObject.currentFrame.shapes) { + for (let region of shape.regions) { + + let intersect_count = 0; + for (let curve of region.curves) { + intersect_count += curve.intersects(line).length + } + console.log(region) + console.log(intersect_count) + if (intersect_count%2==1) { + region.fillStyle = context.fillStyle + } + } + } + break; default: break; } From 7ba51940ae1c499fabe30b785fb631dfabf4bed5 Mon Sep 17 00:00:00 2001 From: Skyler Lehmkuhl Date: Tue, 26 Nov 2024 13:18:36 -0500 Subject: [PATCH 32/46] use quadtree to speed up curve intersection checks --- src/quadtree.js | 176 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 176 insertions(+) create mode 100644 src/quadtree.js diff --git a/src/quadtree.js b/src/quadtree.js new file mode 100644 index 0000000..68df8e0 --- /dev/null +++ b/src/quadtree.js @@ -0,0 +1,176 @@ +class Quadtree { + constructor(boundary, capacity) { + // Boundary is the bounding box of the area this quadtree node covers + // Capacity is the maximum number of curves a node can hold before subdividing + this.boundary = boundary; // {x: {min: , max: }, y: {min: , max: }} + this.capacity = capacity; + this.curveIndexes = []; + this.curves = []; + this.divided = false; + + this.nw = null; // Northwest quadrant + this.ne = null; // Northeast quadrant + this.sw = null; // Southwest quadrant + this.se = null; // Southeast quadrant + } + + + // Check if a bounding box intersects with the boundary of this quadtree node + intersects(bbox) { + return !(bbox.x.max < this.boundary.x.min || bbox.x.min > this.boundary.x.max || + bbox.y.max < this.boundary.y.min || bbox.y.min > this.boundary.y.max); + } + + // Subdivide this quadtree node into 4 quadrants + subdivide() { + const xMid = (this.boundary.x.min + this.boundary.x.max) / 2; + const yMid = (this.boundary.y.min + this.boundary.y.max) / 2; + + const nwBoundary = { x: { min: this.boundary.x.min, max: xMid }, y: { min: this.boundary.y.min, max: yMid }}; + const neBoundary = { x: { min: xMid, max: this.boundary.x.max }, y: { min: this.boundary.y.min, max: yMid }}; + const swBoundary = { x: { min: this.boundary.x.min, max: xMid }, y: { min: yMid, max: this.boundary.y.max }}; + const seBoundary = { x: { min: xMid, max: this.boundary.x.max }, y: { min: yMid, max: this.boundary.y.max }}; + + this.nw = new Quadtree(nwBoundary, this.capacity); + this.ne = new Quadtree(neBoundary, this.capacity); + this.sw = new Quadtree(swBoundary, this.capacity); + this.se = new Quadtree(seBoundary, this.capacity); + + this.divided = true; + } + + insert (curve, curveIdx) { + const bbox = curve.bbox() + if (!this.intersects(curve.bbox())) { + let newNode = new Quadtree(this.boundary, this.capacity) + newNode.curveIndexes = this.curveIndexes; + newNode.curves = this.curves; + newNode.divided = this.divided; + + newNode.nw = this.nw; + newNode.ne = this.ne; + newNode.sw = this.sw; + newNode.se = this.se; + + this.curveIndexes = []; + this.curves = []; + this.subdivide() + if (bbox.x.max < this.boundary.x.max) { + if (bbox.y.max < this.boundary.y.max) { + this.boundary.x.min -= this.boundary.x.max - this.boundary.x.min + this.boundary.y.min -= this.boundary.y.max - this.boundary.y.min + this.nw = newNode + } else { + this.boundary.x.min -= this.boundary.x.max - this.boundary.x.min + this.boundary.y.max += this.boundary.y.max - this.boundary.y.min + this.sw = newNode + } + } else { + if (bbox.y.max < this.boundary.y.max) { + this.boundary.x.max += this.boundary.x.max - this.boundary.x.min + this.boundary.y.min -= this.boundary.y.max - this.boundary.y.min + this.ne = newNode + } else { + this.boundary.x.max += this.boundary.x.max - this.boundary.x.min + this.boundary.y.max += this.boundary.y.max - this.boundary.y.min + this.se = newNode + } + } + return this.insert(curve, curveIdx) + } else { + return this._insert(curve, curveIdx) + } + } + + // Insert a curve into the quadtree, subdividing if necessary + _insert(curve, curveIdx) { + // If the curve's bounding box doesn't intersect this node's boundary, do nothing + if (!this.intersects(curve.bbox())) { + return false; + } + + // If the node has space, insert the curve here + if (this.curves.length < this.capacity) { + this.curves.push(curve); + this.curveIndexes.push(curveIdx) + return true; + } + + // Otherwise, subdivide and insert the curve into the appropriate quadrant + if (!this.divided) { + this.subdivide(); + } + + return ( + this.nw._insert(curve, curveIdx) || + this.ne._insert(curve, curveIdx) || + this.sw._insert(curve, curveIdx) || + this.se._insert(curve, curveIdx) + ); + } + + // Query all curves that intersect with a given bounding box + query(range, found = []) { + // If the range doesn't intersect with this node's boundary, return + if (!this.intersects(range)) { + return found; + } + + // Check the curves in this node + for (let i = 0; i < this.curves.length; i++) { + if (this.bboxIntersect(this.curves[i].bbox(), range)) { + found.push(this.curveIndexes[i]); // Return the curve index instead of the curve + } + } + + // If the node is subdivided, check the child quadrants + if (this.divided) { + this.nw.query(range, found); + this.ne.query(range, found); + this.sw.query(range, found); + this.se.query(range, found); + } + + return found; + } + + // Helper method to check if two bounding boxes intersect + bboxIntersect(bbox1, bbox2) { + return !(bbox1.x.max < bbox2.x.min || bbox1.x.min > bbox2.x.max || + bbox1.y.max < bbox2.y.min || bbox1.y.min > bbox2.y.max); + } + + clear() { + this.curveIndexes = []; + this.curves = []; + this.divided = false; + + this.nw = null; // Northwest quadrant + this.ne = null; // Northeast quadrant + this.sw = null; // Southwest quadrant + this.se = null; // Southeast quadrant + } + draw(ctx) { + // Debug visualization + ctx.save() + ctx.strokeStyle = "red" + ctx.lineWidth = 1 + ctx.beginPath() + ctx.rect( + this.boundary.x.min, + this.boundary.y.min, + this.boundary.x.max-this.boundary.x.min, + this.boundary.y.max-this.boundary.y.min + ) + ctx.stroke() + if (this.divided) { + this.nw.draw(ctx) + this.ne.draw(ctx) + this.sw.draw(ctx) + this.se.draw(ctx) + } + ctx.restore() + } + } + + export { Quadtree }; \ No newline at end of file From bdb6b067f2a08423dc63b281e909818a2318f392 Mon Sep 17 00:00:00 2001 From: Skyler Lehmkuhl Date: Tue, 26 Nov 2024 13:18:53 -0500 Subject: [PATCH 33/46] save file --- package.json | 4 + pnpm-lock.yaml | 26 ++ src-tauri/Cargo.lock | 542 +++++++++++++++++++++++++++- src-tauri/Cargo.toml | 2 + src-tauri/capabilities/default.json | 26 +- src-tauri/src/lib.rs | 2 + src-tauri/tauri.conf.json | 5 +- src/main.js | 223 +++++------- 8 files changed, 676 insertions(+), 154 deletions(-) diff --git a/package.json b/package.json index 170a296..2348626 100644 --- a/package.json +++ b/package.json @@ -8,5 +8,9 @@ }, "devDependencies": { "@tauri-apps/cli": "^2" + }, + "dependencies": { + "@tauri-apps/plugin-dialog": "~2", + "@tauri-apps/plugin-fs": "~2" } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 0587e78..9af2e76 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -7,6 +7,13 @@ settings: importers: .: + dependencies: + '@tauri-apps/plugin-dialog': + specifier: ~2 + version: 2.0.1 + '@tauri-apps/plugin-fs': + specifier: ~2 + version: 2.0.2 devDependencies: '@tauri-apps/cli': specifier: ^2 @@ -14,6 +21,9 @@ importers: packages: + '@tauri-apps/api@2.1.1': + resolution: {integrity: sha512-fzUfFFKo4lknXGJq8qrCidkUcKcH2UHhfaaCNt4GzgzGaW2iS26uFOg4tS3H4P8D6ZEeUxtiD5z0nwFF0UN30A==} + '@tauri-apps/cli-darwin-arm64@2.0.4': resolution: {integrity: sha512-siH7rOHobb16rPbc11k64p1mxIpiRCkWmzs2qmL5IX21Gx9K5onI3Tk67Oqpf2uNupbYzItrOttaDT4NHFC7tw==} engines: {node: '>= 10'} @@ -79,8 +89,16 @@ packages: engines: {node: '>= 10'} hasBin: true + '@tauri-apps/plugin-dialog@2.0.1': + resolution: {integrity: sha512-fnUrNr6EfvTqdls/ufusU7h6UbNFzLKvHk/zTuOiBq01R3dTODqwctZlzakdbfSp/7pNwTKvgKTAgl/NAP/Z0Q==} + + '@tauri-apps/plugin-fs@2.0.2': + resolution: {integrity: sha512-4YZaX2j7ta81M5/DL8aN10kTnpUkEpkPo1FTYPT8Dd0ImHe3azM8i8MrtjrDGoyBYLPO3zFv7df/mSCYF8oA0Q==} + snapshots: + '@tauri-apps/api@2.1.1': {} + '@tauri-apps/cli-darwin-arm64@2.0.4': optional: true @@ -123,3 +141,11 @@ snapshots: '@tauri-apps/cli-win32-arm64-msvc': 2.0.4 '@tauri-apps/cli-win32-ia32-msvc': 2.0.4 '@tauri-apps/cli-win32-x64-msvc': 2.0.4 + + '@tauri-apps/plugin-dialog@2.0.1': + dependencies: + '@tauri-apps/api': 2.1.1 + + '@tauri-apps/plugin-fs@2.0.2': + dependencies: + '@tauri-apps/api': 2.1.1 diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index 0c418de..a828459 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -62,6 +62,61 @@ version = "1.0.93" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4c95c10ba0b00a02636238b814946408b1322d5ac4760326e6fb8ec956d85775" +[[package]] +name = "ashpd" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e9c39d707614dbcc6bed00015539f488d8e3fe3e66ed60961efc0c90f4b380b3" +dependencies = [ + "enumflags2", + "futures-channel", + "futures-util", + "rand 0.8.5", + "raw-window-handle", + "serde", + "serde_repr", + "tokio", + "url", + "wayland-backend", + "wayland-client", + "wayland-protocols", + "zbus", +] + +[[package]] +name = "async-broadcast" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20cd0e2e25ea8e5f7e9df04578dc6cf5c83577fd09b1a46aaf5c85e1c33f2a7e" +dependencies = [ + "event-listener", + "event-listener-strategy", + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "async-recursion" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b43422f69d8ff38f95f1b2bb76517c91589a924d1559a0e935d7c8ce0274c11" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.87", +] + +[[package]] +name = "async-trait" +version = "0.1.83" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "721cae7de5c34fbb2acd27e21e6d2cf7b886dce0c27388d46c4e6c47ea4318dd" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.87", +] + [[package]] name = "atk" version = "0.18.0" @@ -373,6 +428,15 @@ dependencies = [ "memchr", ] +[[package]] +name = "concurrent-queue" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973" +dependencies = [ + "crossbeam-utils", +] + [[package]] name = "convert_case" version = "0.4.0" @@ -605,6 +669,15 @@ dependencies = [ "syn 2.0.87", ] +[[package]] +name = "dlib" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "330c60081dcc4c72131f8eb70510f1ac07223e5d4163db481a04a0befcffa412" +dependencies = [ + "libloading", +] + [[package]] name = "dlopen2" version = "0.7.0" @@ -628,6 +701,12 @@ dependencies = [ "syn 2.0.87", ] +[[package]] +name = "downcast-rs" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75b325c5dbd37f80359721ad39aca5a29fb04c89279657cffdda8736d0c0b9d2" + [[package]] name = "dpi" version = "0.1.1" @@ -693,6 +772,33 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "endi" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3d8a32ae18130a3c84dd492d4215c3d913c3b07c6b63c2eb3eb7ff1101ab7bf" + +[[package]] +name = "enumflags2" +version = "0.7.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d232db7f5956f3f14313dc2f87985c58bd2c695ce124c8cdd984e08e15ac133d" +dependencies = [ + "enumflags2_derive", + "serde", +] + +[[package]] +name = "enumflags2_derive" +version = "0.7.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de0d48a183585823424a4ce1aa132d174a6a81bd540895822eb4c8373a8e49e8" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.87", +] + [[package]] name = "equivalent" version = "1.0.1" @@ -709,6 +815,43 @@ dependencies = [ "typeid", ] +[[package]] +name = "errno" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "534c5cf6194dfab3db3242765c03bbe257cf92f22b38f6bc0c58d59108a820ba" +dependencies = [ + "libc", + "windows-sys 0.52.0", +] + +[[package]] +name = "event-listener" +version = "5.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6032be9bd27023a771701cc49f9f053c751055f71efb2e0ae5c15809093675ba" +dependencies = [ + "concurrent-queue", + "parking", + "pin-project-lite", +] + +[[package]] +name = "event-listener-strategy" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f214dc438f977e6d4e3500aaa277f5ad94ca83fbbd9b1a15713ce2344ccc5a1" +dependencies = [ + "event-listener", + "pin-project-lite", +] + +[[package]] +name = "fastrand" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "486f806e73c5707928240ddc295403b1b93c96a02038563881c4a2fd84b81ac4" + [[package]] name = "fdeflate" version = "0.3.6" @@ -1079,7 +1222,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0bb0228f477c0900c880fd78c8759b95c7636dbd7842707f49e132378aa2acdc" dependencies = [ "heck 0.4.1", - "proc-macro-crate 2.0.2", + "proc-macro-crate 2.0.0", "proc-macro-error", "proc-macro2", "quote", @@ -1735,9 +1878,17 @@ dependencies = [ "serde_json", "tauri", "tauri-build", + "tauri-plugin-dialog", + "tauri-plugin-fs", "tauri-plugin-shell", ] +[[package]] +name = "linux-raw-sys" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89" + [[package]] name = "litemap" version = "0.7.3" @@ -1894,6 +2045,19 @@ version = "1.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086" +[[package]] +name = "nix" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71e2746dc3a24dd78b3cfcb7be93368c6de9963d30f43a6a73998a9cf4b17b46" +dependencies = [ + "bitflags 2.6.0", + "cfg-if", + "cfg_aliases", + "libc", + "memoffset", +] + [[package]] name = "nodrop" version = "0.1.14" @@ -1930,7 +2094,7 @@ version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "af1844ef2428cc3e1cb900be36181049ef3d3193c63e43026cfe202983b27a56" dependencies = [ - "proc-macro-crate 2.0.2", + "proc-macro-crate 1.3.1", "proc-macro2", "quote", "syn 2.0.87", @@ -2054,6 +2218,7 @@ checksum = "0ee638a5da3799329310ad4cfa62fbf045d5f56e3ef5ba4149e7452dcf89d5a8" dependencies = [ "bitflags 2.6.0", "block2", + "dispatch", "libc", "objc2", ] @@ -2195,6 +2360,16 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" +[[package]] +name = "ordered-stream" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9aa2b01e1d916879f73a53d01d1d6cee68adbb31d6d9177a8cfce093cced1d50" +dependencies = [ + "futures-core", + "pin-project-lite", +] + [[package]] name = "os_pipe" version = "1.2.1" @@ -2230,6 +2405,12 @@ dependencies = [ "system-deps", ] +[[package]] +name = "parking" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba" + [[package]] name = "parking_lot" version = "0.12.3" @@ -2425,7 +2606,7 @@ checksum = "42cf17e9a1800f5f396bc67d193dc9411b59012a5876445ef450d449881e1016" dependencies = [ "base64 0.22.1", "indexmap 2.6.0", - "quick-xml", + "quick-xml 0.32.0", "serde", "time", ] @@ -2476,14 +2657,22 @@ dependencies = [ [[package]] name = "proc-macro-crate" -version = "2.0.2" +version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b00f26d3400549137f92511a46ac1cd8ce37cb5598a96d382381458b992a5d24" +checksum = "7e8366a6159044a37876a2b9817124296703c586a5c92e2c53751fa06d8d43e8" dependencies = [ - "toml_datetime", "toml_edit 0.20.2", ] +[[package]] +name = "proc-macro-crate" +version = "3.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecf48c7ca261d60b74ab1a7b20da18bede46776b2e55535cb958eb595c5fa7b" +dependencies = [ + "toml_edit 0.22.22", +] + [[package]] name = "proc-macro-error" version = "1.0.4" @@ -2532,6 +2721,15 @@ dependencies = [ "memchr", ] +[[package]] +name = "quick-xml" +version = "0.36.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7649a7b4df05aed9ea7ec6f628c67c9953a43869b8bc50929569b2999d443fe" +dependencies = [ + "memchr", +] + [[package]] name = "quote" version = "1.0.37" @@ -2714,6 +2912,29 @@ dependencies = [ "windows-registry", ] +[[package]] +name = "rfd" +version = "0.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46f6f80a9b882647d9014673ca9925d30ffc9750f2eed2b4490e189eaebd01e8" +dependencies = [ + "ashpd", + "block2", + "glib-sys", + "gobject-sys", + "gtk-sys", + "js-sys", + "log", + "objc2", + "objc2-app-kit", + "objc2-foundation", + "raw-window-handle", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "windows-sys 0.48.0", +] + [[package]] name = "rustc-demangle" version = "0.1.24" @@ -2729,6 +2950,19 @@ dependencies = [ "semver", ] +[[package]] +name = "rustix" +version = "0.38.41" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7f649912bc1495e167a6edee79151c84b1bad49748cb4f1f1167f459f6224f6" +dependencies = [ + "bitflags 2.6.0", + "errno", + "libc", + "linux-raw-sys", + "windows-sys 0.52.0", +] + [[package]] name = "ryu" version = "1.0.18" @@ -2771,6 +3005,12 @@ dependencies = [ "syn 2.0.87", ] +[[package]] +name = "scoped-tls" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1cf6437eb19a8f4a6cc0f7dca544973b0b78843adbfeb3683d1a94a0024a294" + [[package]] name = "scopeguard" version = "1.2.0" @@ -2981,6 +3221,15 @@ version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" +[[package]] +name = "signal-hook-registry" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9e9e0b4211b72e7b8b6e85c807d36c212bdb33ea8587f7569562a84df5465b1" +dependencies = [ + "libc", +] + [[package]] name = "simd-adler32" version = "0.3.7" @@ -3072,6 +3321,12 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" +[[package]] +name = "static_assertions" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" + [[package]] name = "string_cache" version = "0.8.7" @@ -3356,6 +3611,45 @@ dependencies = [ "walkdir", ] +[[package]] +name = "tauri-plugin-dialog" +version = "2.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4307310e1d2c09ab110235834722e7c2b85099b683e1eb7342ab351b0be5ada3" +dependencies = [ + "log", + "raw-window-handle", + "rfd", + "serde", + "serde_json", + "tauri", + "tauri-plugin", + "tauri-plugin-fs", + "thiserror", + "url", +] + +[[package]] +name = "tauri-plugin-fs" +version = "2.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96ba7d46e86db8c830d143ef90ab5a453328365b0cc834c24edea4267b16aba0" +dependencies = [ + "anyhow", + "dunce", + "glob", + "percent-encoding", + "schemars", + "serde", + "serde_json", + "serde_repr", + "tauri", + "tauri-plugin", + "thiserror", + "url", + "uuid", +] + [[package]] name = "tauri-plugin-shell" version = "2.0.2" @@ -3468,6 +3762,19 @@ dependencies = [ "toml 0.7.8", ] +[[package]] +name = "tempfile" +version = "3.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28cce251fcbc87fac86a866eeb0d6c2d536fc16d06f184bb61aeae11aa4cee0c" +dependencies = [ + "cfg-if", + "fastrand", + "once_cell", + "rustix", + "windows-sys 0.59.0", +] + [[package]] name = "tendril" version = "0.4.3" @@ -3557,7 +3864,9 @@ dependencies = [ "libc", "mio", "pin-project-lite", + "signal-hook-registry", "socket2", + "tracing", "windows-sys 0.52.0", ] @@ -3600,9 +3909,9 @@ dependencies = [ [[package]] name = "toml_datetime" -version = "0.6.3" +version = "0.6.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7cda73e2f1397b1262d6dfdcef8aafae14d1de7748d66822d3bfeeb6d03e5e4b" +checksum = "0dd7358ecb8fc2f8d014bf86f6f638ce72ba252a2c3a2572f2a795f1d23efb41" dependencies = [ "serde", ] @@ -3617,7 +3926,7 @@ dependencies = [ "serde", "serde_spanned", "toml_datetime", - "winnow", + "winnow 0.5.40", ] [[package]] @@ -3630,7 +3939,18 @@ dependencies = [ "serde", "serde_spanned", "toml_datetime", - "winnow", + "winnow 0.5.40", +] + +[[package]] +name = "toml_edit" +version = "0.22.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ae48d6208a266e853d946088ed816055e556cc6028c5e8e2b84d9fa5dd7c7f5" +dependencies = [ + "indexmap 2.6.0", + "toml_datetime", + "winnow 0.6.20", ] [[package]] @@ -3646,9 +3966,21 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c3523ab5a71916ccf420eebdf5521fcef02141234bbc0b8a49f2fdc4544364ef" dependencies = [ "pin-project-lite", + "tracing-attributes", "tracing-core", ] +[[package]] +name = "tracing-attributes" +version = "0.1.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.87", +] + [[package]] name = "tracing-core" version = "0.1.32" @@ -3697,6 +4029,17 @@ version = "1.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" +[[package]] +name = "uds_windows" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89daebc3e6fd160ac4aa9fc8b3bf71e1f74fbf92367ae71fb83a037e8bf164b9" +dependencies = [ + "memoffset", + "tempfile", + "winapi", +] + [[package]] name = "unic-char-property" version = "0.9.0" @@ -3945,6 +4288,66 @@ dependencies = [ "web-sys", ] +[[package]] +name = "wayland-backend" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "056535ced7a150d45159d3a8dc30f91a2e2d588ca0b23f70e56033622b8016f6" +dependencies = [ + "cc", + "downcast-rs", + "rustix", + "scoped-tls", + "smallvec", + "wayland-sys", +] + +[[package]] +name = "wayland-client" +version = "0.31.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b66249d3fc69f76fd74c82cc319300faa554e9d865dab1f7cd66cc20db10b280" +dependencies = [ + "bitflags 2.6.0", + "rustix", + "wayland-backend", + "wayland-scanner", +] + +[[package]] +name = "wayland-protocols" +version = "0.32.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cd0ade57c4e6e9a8952741325c30bf82f4246885dca8bf561898b86d0c1f58e" +dependencies = [ + "bitflags 2.6.0", + "wayland-backend", + "wayland-client", + "wayland-scanner", +] + +[[package]] +name = "wayland-scanner" +version = "0.31.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "597f2001b2e5fc1121e3d5b9791d3e78f05ba6bfa4641053846248e3a13661c3" +dependencies = [ + "proc-macro2", + "quick-xml 0.36.2", + "quote", +] + +[[package]] +name = "wayland-sys" +version = "0.31.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "efa8ac0d8e8ed3e3b5c9fc92c7881406a268e11555abe36493efabe649a29e09" +dependencies = [ + "dlib", + "log", + "pkg-config", +] + [[package]] name = "web-sys" version = "0.3.72" @@ -4396,6 +4799,15 @@ dependencies = [ "memchr", ] +[[package]] +name = "winnow" +version = "0.6.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "36c1fec1a2bb5866f07c25f68c26e565c4c200aebb96d7e55710c19d3e8ac49b" +dependencies = [ + "memchr", +] + [[package]] name = "winreg" version = "0.52.0" @@ -4480,6 +4892,16 @@ dependencies = [ "pkg-config", ] +[[package]] +name = "xdg-home" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec1cdab258fb55c0da61328dc52c8764709b249011b2cad0454c72f0bf10a1f6" +dependencies = [ + "libc", + "windows-sys 0.59.0", +] + [[package]] name = "yoke" version = "0.7.4" @@ -4504,6 +4926,63 @@ dependencies = [ "synstructure", ] +[[package]] +name = "zbus" +version = "5.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1162094dc63b1629fcc44150bcceeaa80798cd28bcbe7fa987b65a034c258608" +dependencies = [ + "async-broadcast", + "async-recursion", + "async-trait", + "enumflags2", + "event-listener", + "futures-core", + "futures-util", + "hex", + "nix", + "ordered-stream", + "serde", + "serde_repr", + "static_assertions", + "tokio", + "tracing", + "uds_windows", + "windows-sys 0.59.0", + "winnow 0.6.20", + "xdg-home", + "zbus_macros", + "zbus_names", + "zvariant", +] + +[[package]] +name = "zbus_macros" +version = "5.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2cd2dcdce3e2727f7d74b7e33b5a89539b3cc31049562137faf7ae4eb86cd16d" +dependencies = [ + "proc-macro-crate 3.2.0", + "proc-macro2", + "quote", + "syn 2.0.87", + "zbus_names", + "zvariant", + "zvariant_utils", +] + +[[package]] +name = "zbus_names" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "856b7a38811f71846fd47856ceee8bccaec8399ff53fb370247e66081ace647b" +dependencies = [ + "serde", + "static_assertions", + "winnow 0.6.20", + "zvariant", +] + [[package]] name = "zerocopy" version = "0.7.35" @@ -4567,3 +5046,46 @@ dependencies = [ "quote", "syn 2.0.87", ] + +[[package]] +name = "zvariant" +version = "5.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1200ee6ac32f1e5a312e455a949a4794855515d34f9909f4a3e082d14e1a56f" +dependencies = [ + "endi", + "enumflags2", + "serde", + "static_assertions", + "url", + "winnow 0.6.20", + "zvariant_derive", + "zvariant_utils", +] + +[[package]] +name = "zvariant_derive" +version = "5.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "687e3b97fae6c9104fbbd36c73d27d149abf04fb874e2efbd84838763daa8916" +dependencies = [ + "proc-macro-crate 3.2.0", + "proc-macro2", + "quote", + "syn 2.0.87", + "zvariant_utils", +] + +[[package]] +name = "zvariant_utils" +version = "3.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20d1d011a38f12360e5fcccceeff5e2c42a8eb7f27f0dcba97a0862ede05c9c6" +dependencies = [ + "proc-macro2", + "quote", + "serde", + "static_assertions", + "syn 2.0.87", + "winnow 0.6.20", +] diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 2cbf491..e7d269e 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -22,4 +22,6 @@ tauri = { version = "2", features = [] } tauri-plugin-shell = "2" serde = { version = "1", features = ["derive"] } serde_json = "1" +tauri-plugin-fs = "2" +tauri-plugin-dialog = "2" diff --git a/src-tauri/capabilities/default.json b/src-tauri/capabilities/default.json index 3bb4cc4..f2367cf 100644 --- a/src-tauri/capabilities/default.json +++ b/src-tauri/capabilities/default.json @@ -2,9 +2,29 @@ "$schema": "../gen/schemas/desktop-schema.json", "identifier": "default", "description": "Capability for the main window", - "windows": ["main"], + "windows": [ + "main" + ], "permissions": [ "core:default", - "shell:allow-open" + "shell:allow-open", + "fs:default", + { + "identifier": "fs:allow-exists", + "allow": [ + { + "path": "$DOCUMENT/*" + } + ] + }, + { + "identifier": "fs:allow-app-write-recursive", + "allow": [ + { + "path": "$DOCUMENT/*" + } + ] + }, + "dialog:default" ] -} +} \ No newline at end of file diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index f91b35e..60d6479 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -7,6 +7,8 @@ fn greet(name: &str) -> String { #[cfg_attr(mobile, tauri::mobile_entry_point)] pub fn run() { tauri::Builder::default() + .plugin(tauri_plugin_dialog::init()) + .plugin(tauri_plugin_fs::init()) .plugin(tauri_plugin_shell::init()) .invoke_handler(tauri::generate_handler![greet]) .run(tauri::generate_context!()) diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index 8d0291d..0cd2754 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -6,13 +6,14 @@ "build": { "frontendDist": "../src" }, + "app": { "withGlobalTauri": true, "windows": [ { "title": "lightningbeam", - "width": 800, - "height": 600 + "width": 1500, + "height": 1024 } ], "security": { diff --git a/src/main.js b/src/main.js index 9c258b6..9c2df09 100644 --- a/src/main.js +++ b/src/main.js @@ -1,8 +1,10 @@ const { invoke } = window.__TAURI__.core; import * as fitCurve from '/fit-curve.js'; import { Bezier } from "/bezier.js"; -import earcut from './earcut.js'; - +import { Quadtree } from './quadtree.js'; +const { writeTextFile, BaseDirectory }= window.__TAURI__.fs; +const { save } = window.__TAURI__.dialog; +const { documentDir, join } = window.__TAURI__.path; let simplifyPolyline = simplify @@ -22,6 +24,7 @@ let redoStack = []; let layoutElements = [] + let tools = { select: { icon: "/assets/select.svg", @@ -94,6 +97,7 @@ let config = { // undo: "+z" undo: "z", redo: "Z", + save: "s", } } @@ -191,6 +195,26 @@ let actions = { shape.update() } }, + colorRegion: { + create: (region, color) => { + redoStack.length = 0; // Clear redo stack + let action = { + region: region.idx, + oldColor: region.fillStyle, + newColor: color + } + undoStack.push({name: "colorRegion", action: action}) + actions.colorRegion.execute(action) + }, + execute: (action) => { + let region = pointerList[action.region] + region.fillStyle = action.newColor + }, + rollback: (action) => { + let region = pointerList[action.region] + region.fillStyle = action.oldColor + } + }, addImageObject: { create: (x, y, img, parent) => { redoStack.length = 0; // Clear redo stack @@ -543,19 +567,6 @@ function redo() { } -class Curve { - constructor(startx, starty, cp1x, cp1y, cp2x, cp2y, x, y) { - this.startx = startx - this.starty = starty - this.cp1x = cp1x; - this.cp1y = cp1y; - this.cp2x = cp2x; - this.cp2y = cp2y; - this.x = x; - this.y = y; - } -} - class Frame { constructor(uuid) { this.keys = {} @@ -603,6 +614,7 @@ class Shape { x: {min: startx, max: starty}, y: {min: starty, max: starty} } + this.quadtree = new Quadtree({x: {min: 0, max: 500}, y: {min: 0, max: 500}}, 4) if (!uuid) { this.idx = uuidv4() } else { @@ -612,6 +624,7 @@ class Shape { } addCurve(curve) { this.curves.push(curve) + this.quadtree.insert(curve, this.curves.length - 1) growBoundingBox(this.boundingBox, curve.bbox()) } addLine(x, y) { @@ -638,6 +651,7 @@ class Shape { } } simplify(mode="corners") { + this.quadtree.clear() // Mode can be corners, smooth or auto if (mode=="corners") { let points = [{x: this.startx, y: this.starty}] @@ -656,6 +670,7 @@ class Shape { midpoint.x,midpoint.y, point.x,point.y) this.curves.push(bezier) + this.quadtree.insert(bezier, this.curves.length - 1) lastpoint = point } } else if (mode=="smooth") { @@ -672,6 +687,7 @@ class Shape { curve[2][0], curve[2][1], curve[3][0], curve[3][1]) this.curves.push(bezier) + this.quadtree.insert(bezier, this.curves.length - 1) } } @@ -679,7 +695,10 @@ class Shape { let newCurves = [] let intersectMap = {} for (let i=0; i start) { - this.regions.push({ + let newRegion = { + idx: uuidv4(), // TODO: generate this deterministically so that undo/redo works curves: region.curves.splice(start, end - start), fillStyle: region.fillStyle, filled: true - }) + } + pointerList[newRegion.idx] = newRegion + this.regions.push(newRegion) } } else { // not sure how to handle vertices with more than 4 curves @@ -840,120 +860,6 @@ class Shape { } } }) - // Generate enclosed regions - let graph = {} - let edges = [] - this.vertices.forEach((vertex, i) => { - this.vertices.forEach((otherVertex, j) => { - for (let curve in vertex.startCurves) { - if (curve in otherVertex.endCurves) { - edges.push([i, j]) - if (graph[i]) { - graph[i].push(j) - } else { - graph[i] = [j] - } - } - } - for (let curve in vertex.endCurves) { - if (curve in otherVertex.startCurves) { - edges.push([i, j]) - if (graph[i]) { - graph[i].push(j) - } else { - graph[i] = [j] - } - } - } - }) - }) - // for (let vertex in graph) { - // let node = vertex - // let seenNodes = [] - // for (let i=0; i 1) { - for (let otherPolygon of polygons) { - if (polygon.every((val, idx) => val === otherPolygon[idx])) { - return; - } - } - polygons.push(polygon); - } - return; - } - - visited.add(node); - for (const edge of edges) { - if (edge[0] === node) { - dfs(edge[1], [...path, node]); - } else if (edge[1] === node) { - dfs(edge[0], [...path, node]); - } - } - } - - for (const edge of edges) { - dfs(edge[0], []); - } - - return polygons; - } - - /* - const polygons = findEnclosedPolygons(edges); - this.regions = [] - - for (let polygon of polygons) { - let region = [] - let firstVertex = undefined - let lastVertex = undefined - for (let vertex of polygon) { - firstVertex ||= vertex - if (lastVertex) { - for (let i in this.vertices[vertex].startCurves) { - let curve = this.vertices[vertex].startCurves[i] - if (i in this.vertices[lastVertex].endCurves) { - region.push(curve) - } - } - for (let i in this.vertices[vertex].endCurves) { - let curve = this.vertices[vertex].endCurves[i] - if (i in this.vertices[lastVertex].startCurves) { - region.push(new Bezier(curve.points.toReversed())) - } - } - } - lastVertex = vertex - } - for (let i in this.vertices[firstVertex].startCurves) { - let curve = this.vertices[firstVertex].startCurves[i] - if (i in this.vertices[lastVertex].endCurves) { - region.push(curve) - } - } - for (let i in this.vertices[firstVertex].endCurves) { - let curve = this.vertices[firstVertex].endCurves[i] - if (i in this.vertices[lastVertex].startCurves) { - region.push(new Bezier(curve.points.toReversed())) - } - } - if (region.length > 1) { - // Filter out single curves - this.regions.push(region) - } - }*/ } draw(context) { let ctx = context.ctx; @@ -993,6 +899,8 @@ class Shape { // ctx.arc(curve.points[3].x,curve.points[3].y, 3, 0, 2*Math.PI) // ctx.fill() } + // Debug, show quadtree + // this.quadtree.draw(ctx) } } @@ -1216,9 +1124,46 @@ window.addEventListener("keypress", (e) => { undo() } else if (e.key == config.shortcuts.redo && e.ctrlKey == true) { redo() + } else if (e.key == config.shortcuts.save && e.ctrlKey == true) { + saveFile() } }) +async function saveTextFile() { + console.log(await documentDir()) + const path = await save({ + filters: [ + { + name: 'Lightningbeam files (.beam)', + extensions: ['beam'], + }, + ], + defaultPath: await join(await documentDir(), "untitled.beam") + }); + console.log(path) + // console.log("saving") + // console.log(BaseDirectory) + try { + const fileData = { + version: "1.0", + actions: undoStack + } + const contents = JSON.stringify(fileData ); + await writeTextFile(path, contents)//, { + // baseDir: BaseDirectory.Document, + // }); + + // console.log("Text file saved successfully!"); + } catch (error) { + console.error("Error saving text file:", error); + } +} + +function saveFile() { + console.log("gonna save") + saveTextFile() +} + function stage() { let stage = document.createElement("canvas") let scroller = document.createElement("div") @@ -1339,7 +1284,6 @@ function stage() { let line = {p1: mouse, p2: {x: mouse.x + 3000, y: mouse.y}} for (let shape of context.activeObject.currentFrame.shapes) { for (let region of shape.regions) { - let intersect_count = 0; for (let curve of region.curves) { intersect_count += curve.intersects(line).length @@ -1347,7 +1291,8 @@ function stage() { console.log(region) console.log(intersect_count) if (intersect_count%2==1) { - region.fillStyle = context.fillStyle + // region.fillStyle = context.fillStyle + actions.colorRegion.create(region, context.fillStyle) } } } From 5290fc6b1107169de41097b917d8981a335e593a Mon Sep 17 00:00:00 2001 From: Skyler Lehmkuhl Date: Tue, 26 Nov 2024 14:10:46 -0500 Subject: [PATCH 34/46] open file --- src-tauri/capabilities/default.json | 8 +++ src/main.js | 87 ++++++++++++++++++++++------- 2 files changed, 76 insertions(+), 19 deletions(-) diff --git a/src-tauri/capabilities/default.json b/src-tauri/capabilities/default.json index f2367cf..1efef12 100644 --- a/src-tauri/capabilities/default.json +++ b/src-tauri/capabilities/default.json @@ -25,6 +25,14 @@ } ] }, + { + "identifier": "fs:allow-app-read-recursive", + "allow": [ + { + "path": "$DOCUMENT/*" + } + ] + }, "dialog:default" ] } \ No newline at end of file diff --git a/src/main.js b/src/main.js index 9c2df09..c0aa5b9 100644 --- a/src/main.js +++ b/src/main.js @@ -2,8 +2,8 @@ const { invoke } = window.__TAURI__.core; import * as fitCurve from '/fit-curve.js'; import { Bezier } from "/bezier.js"; import { Quadtree } from './quadtree.js'; -const { writeTextFile, BaseDirectory }= window.__TAURI__.fs; -const { save } = window.__TAURI__.dialog; +const { writeTextFile: writeTextFile, readTextFile: readTextFile }= window.__TAURI__.fs; +const { open: openFileDialog, save: saveFileDialog, message: messageDialog } = window.__TAURI__.dialog; const { documentDir, join } = window.__TAURI__.path; let simplifyPolyline = simplify @@ -24,6 +24,9 @@ let redoStack = []; let layoutElements = [] +let minFileVersion = "1.0" +let maxFileVersion = "2.0" + let tools = { select: { @@ -98,6 +101,7 @@ let config = { undo: "z", redo: "Z", save: "s", + open: "o" } } @@ -1087,7 +1091,7 @@ class GraphicsObject { } } -let root = new GraphicsObject(); +let root = new GraphicsObject("root"); context.activeObject = root async function greet() { @@ -1125,13 +1129,14 @@ window.addEventListener("keypress", (e) => { } else if (e.key == config.shortcuts.redo && e.ctrlKey == true) { redo() } else if (e.key == config.shortcuts.save && e.ctrlKey == true) { - saveFile() + save() + } else if (e.key == config.shortcuts.open && e.ctrlKey == true) { + open() } }) -async function saveTextFile() { - console.log(await documentDir()) - const path = await save({ +async function save() { + const path = await saveFileDialog({ filters: [ { name: 'Lightningbeam files (.beam)', @@ -1140,28 +1145,72 @@ async function saveTextFile() { ], defaultPath: await join(await documentDir(), "untitled.beam") }); - console.log(path) - // console.log("saving") - // console.log(BaseDirectory) try { const fileData = { version: "1.0", actions: undoStack } - const contents = JSON.stringify(fileData ); - await writeTextFile(path, contents)//, { - // baseDir: BaseDirectory.Document, - // }); - - // console.log("Text file saved successfully!"); + const contents = JSON.stringify(fileData ); + await writeTextFile(path, contents) + console.log(`${path} saved successfully!`); } catch (error) { console.error("Error saving text file:", error); } } -function saveFile() { - console.log("gonna save") - saveTextFile() +async function open() { + console.log("gonna open") + const path = await openFileDialog({ + multiple: false, + directory: false, + filters: [ + { + name: 'Lightningbeam files (.beam)', + extensions: ['beam'], + }, + ], + defaultPath: await documentDir(), + }); + if (path) { + try { + const contents = await readTextFile(path) + let file = JSON.parse(contents) + if (file.version == undefined) { + await messageDialog("Could not read file version!", { title: "Load error", kind: 'error' }) + return + } + if (file.version >= minFileVersion) { + if (file.version < maxFileVersion) { + root = new GraphicsObject("root"); + context.activeObject = root + if (file.actions == undefined) { + await messageDialog("File has no content!", {title: "Parse error", kind: 'error'}) + return + } + for (let action of file.actions) { + if (!(action.name in actions)) { + await messageDialog(`Invalid action ${action.name}. File may be corrupt.`, { title: "Error", kind: 'error'}) + return + } + actions[action.name].execute(action.action) + undoStack.push(action) + } + updateUI() + } else { + await messageDialog(`File ${path} was created in a newer version of Lightningbeam and cannot be opened in this version.`, { title: 'File version mismatch', kind: 'error' }); + } + } else { + await messageDialog(`File ${path} is too old to be opened in this version of Lightningbeam.`, { title: 'File version mismatch', kind: 'error' }); + } + } catch (e) { + console.log(e ) + if (e instanceof SyntaxError) { + await messageDialog(`Could not parse ${path}, ${e.message}`, { title: 'Error', kind: 'error' }) + } else if (e.startsWith("failed to read file as text")) { + await messageDialog(`Could not parse ${path}, is it actually a Lightningbeam file?`, { title: 'Error', kind: 'error' }) + } + } + } } function stage() { From 70d8229a32eec85bda9147f8034802b7579de593 Mon Sep 17 00:00:00 2001 From: Skyler Lehmkuhl Date: Tue, 26 Nov 2024 15:24:02 -0500 Subject: [PATCH 35/46] Add app menu --- src/main.js | 166 +++++++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 152 insertions(+), 14 deletions(-) diff --git a/src/main.js b/src/main.js index c0aa5b9..09f8830 100644 --- a/src/main.js +++ b/src/main.js @@ -3,8 +3,18 @@ import * as fitCurve from '/fit-curve.js'; import { Bezier } from "/bezier.js"; import { Quadtree } from './quadtree.js'; const { writeTextFile: writeTextFile, readTextFile: readTextFile }= window.__TAURI__.fs; -const { open: openFileDialog, save: saveFileDialog, message: messageDialog } = window.__TAURI__.dialog; +const { + open: openFileDialog, + save: saveFileDialog, + message: messageDialog, + confirm: confirmDialog, +} = window.__TAURI__.dialog; const { documentDir, join } = window.__TAURI__.path; +const { Menu, MenuItem, Submenu } = window.__TAURI__.menu ; +const { getCurrentWindow } = window.__TAURI__.window; + + +const macOS = navigator.userAgent.includes('Macintosh') let simplifyPolyline = simplify @@ -24,9 +34,12 @@ let redoStack = []; let layoutElements = [] +let appVersion = "0.6.1-alpha" let minFileVersion = "1.0" let maxFileVersion = "2.0" +let filePath = undefined + let tools = { select: { @@ -101,7 +114,9 @@ let config = { undo: "z", redo: "Z", save: "s", - open: "o" + saveAs: "S", + open: "o", + quit: "q", } } @@ -1130,12 +1145,39 @@ window.addEventListener("keypress", (e) => { redo() } else if (e.key == config.shortcuts.save && e.ctrlKey == true) { save() + } else if (e.key == config.shortcuts.saveAs && e.ctrlKey == true) { + saveAs() } else if (e.key == config.shortcuts.open && e.ctrlKey == true) { open() + } else if (e.key == config.shortcuts.quit && e.ctrlKey == true) { + quit() } }) -async function save() { +async function _save(path) { + try { + const fileData = { + version: "1.0", + actions: undoStack + } + const contents = JSON.stringify(fileData ); + await writeTextFile(path, contents) + filePath = path + console.log(`${path} saved successfully!`); + } catch (error) { + console.error("Error saving text file:", error); + } +} + +async function save() { + if (filePath) { + _save(filePath) + } else { + saveAs() + } +} + +async function saveAs() { const path = await saveFileDialog({ filters: [ { @@ -1145,17 +1187,7 @@ async function save() { ], defaultPath: await join(await documentDir(), "untitled.beam") }); - try { - const fileData = { - version: "1.0", - actions: undoStack - } - const contents = JSON.stringify(fileData ); - await writeTextFile(path, contents) - console.log(`${path} saved successfully!`); - } catch (error) { - console.error("Error saving text file:", error); - } + if (path != undefined) _save(path); } async function open() { @@ -1213,6 +1245,16 @@ async function open() { } } +async function quit() { + if (undoStack.length) { + if (await confirmDialog("Are you sure you want to quit?", {title: 'Really quit?', kind: "warning"})) { + getCurrentWindow().close() + } + } else { + getCurrentWindow().close() + } +} + function stage() { let stage = document.createElement("canvas") let scroller = document.createElement("div") @@ -1657,6 +1699,102 @@ function infopanel() { return panel } +async function setupMenu() { + const fileSubmenu = await Submenu.new({ + text: 'File', + items: [ + { + text: 'Save', + enabled: true, + action: save, + }, + { + text: 'Save As...', + enabled: true, + action: saveAs, + }, + { + text: 'Open File...', + enabled: true, + action: open, + }, + { + text: 'Quit', + enabled: true, + action: quit, + }, + ] + }) + + const editSubmenu = await Submenu.new({ + text: "Edit", + items: [ + { + text: "Undo", + enabled: true, + action: undo + }, + { + text: "Redo", + enabled: true, + action: redo + }, + { + text: "Cut", + enabled: true, + action: () => {} + }, + { + text: "Copy", + enabled: true, + action: () => {} + }, + { + text: "Paste", + enabled: true, + action: () => {} + }, + ] + }); + const viewSubmenu = await Submenu.new({ + text: "View", + items: [ + { + text: "Zoom In", + enabled: true, + action: () => {} + }, + { + text: "Zoom Out", + enabled: true, + action: () => {} + }, + ] + }); + const helpSubmenu = await Submenu.new({ + text: "Help", + items: [ + { + text: "About...", + enabled: true, + action: () => { + messageDialog(`Lightningbeam version ${appVersion}\nDeveloped by Skyler Lehmkuhl`, + {title: 'About', kind: "info"} + ) + } + } + ] +}); + + const menu = await Menu.new({ + items: [fileSubmenu, editSubmenu, viewSubmenu, helpSubmenu], + }) + await (macOS ? menu.setAsAppMenu() : menu.setAsWindowMenu()) +} + +// Initialize the menu when the app starts +setupMenu(); + function createPane(content=undefined) { let div = document.createElement("div") let header = document.createElement("div") From 740e728827d996facd8f1061a224890629900f03 Mon Sep 17 00:00:00 2001 From: Skyler Lehmkuhl Date: Tue, 26 Nov 2024 19:07:41 -0500 Subject: [PATCH 36/46] add new file dialog --- src/main.js | 42 +++++++++++++++++++++++-- src/newfile.js | 83 ++++++++++++++++++++++++++++++++++++++++++++++++++ src/styles.css | 58 +++++++++++++++++++++++++++++++++++ 3 files changed, 180 insertions(+), 3 deletions(-) create mode 100644 src/newfile.js diff --git a/src/main.js b/src/main.js index 09f8830..a5fc5a4 100644 --- a/src/main.js +++ b/src/main.js @@ -2,6 +2,7 @@ const { invoke } = window.__TAURI__.core; import * as fitCurve from '/fit-curve.js'; import { Bezier } from "/bezier.js"; import { Quadtree } from './quadtree.js'; +import { createNewFileDialog, showNewFileDialog, closeDialog } from './newfile.js'; const { writeTextFile: writeTextFile, readTextFile: readTextFile }= window.__TAURI__.fs; const { open: openFileDialog, @@ -39,6 +40,8 @@ let minFileVersion = "1.0" let maxFileVersion = "2.0" let filePath = undefined +let fileWidth = 1500 +let fileHeight = 1000 let tools = { @@ -113,6 +116,7 @@ let config = { // undo: "+z" undo: "z", redo: "Z", + new: "n", save: "s", saveAs: "S", open: "o", @@ -1143,6 +1147,8 @@ window.addEventListener("keypress", (e) => { undo() } else if (e.key == config.shortcuts.redo && e.ctrlKey == true) { redo() + } else if (e.key == config.shortcuts.new && e.ctrlKey == true) { + newFile() } else if (e.key == config.shortcuts.save && e.ctrlKey == true) { save() } else if (e.key == config.shortcuts.saveAs && e.ctrlKey == true) { @@ -1154,10 +1160,33 @@ window.addEventListener("keypress", (e) => { } }) +function _newFile(width, height) { + root = new GraphicsObject("root"); + context.activeObject = root + fileWidth = width + fileHeight = height + for (let stage of document.querySelectorAll(".stage")) { + stage.width = width + stage.height = height + stage.style.width = `${width}px` + stage.style.height = `${height}px` + } + updateUI() +} + +async function newFile() { + if (await confirmDialog("Create a new file? Unsaved work will be lost.", {title: "New file", kind: "warning"})) { + showNewFileDialog() + // updateUI() + } +} + async function _save(path) { try { const fileData = { - version: "1.0", + version: "1.1", + width: fileWidth, + height: fileHeight, actions: undoStack } const contents = JSON.stringify(fileData ); @@ -1213,8 +1242,7 @@ async function open() { } if (file.version >= minFileVersion) { if (file.version < maxFileVersion) { - root = new GraphicsObject("root"); - context.activeObject = root + _newFile(file.width, file.height) if (file.actions == undefined) { await messageDialog("File has no content!", {title: "Parse error", kind: 'error'}) return @@ -1703,6 +1731,11 @@ async function setupMenu() { const fileSubmenu = await Submenu.new({ text: 'File', items: [ + { + text: 'New file...', + enabled: true, + action: newFile, + }, { text: 'Save', enabled: true, @@ -1795,6 +1828,9 @@ async function setupMenu() { // Initialize the menu when the app starts setupMenu(); +createNewFileDialog(_newFile); +showNewFileDialog() + function createPane(content=undefined) { let div = document.createElement("div") let header = document.createElement("div") diff --git a/src/newfile.js b/src/newfile.js new file mode 100644 index 0000000..a6acf15 --- /dev/null +++ b/src/newfile.js @@ -0,0 +1,83 @@ +let overlay; +let newFileDialog; + +function createNewFileDialog(callback) { + overlay = document.createElement('div'); + overlay.id = 'overlay'; + document.body.appendChild(overlay); + + newFileDialog = document.createElement('div'); + newFileDialog.id = 'newFileDialog'; + newFileDialog.classList.add('hidden'); + document.body.appendChild(newFileDialog); + + // Create dialog content dynamically + const title = document.createElement('h3'); + title.textContent = 'Create New File'; + newFileDialog.appendChild(title); + + // Create Width input + const widthLabel = document.createElement('label'); + widthLabel.setAttribute('for', 'width'); + widthLabel.classList.add('dialog-label'); + widthLabel.textContent = 'Width:'; + newFileDialog.appendChild(widthLabel); + + const widthInput = document.createElement('input'); + widthInput.type = 'number'; + widthInput.id = 'width'; + widthInput.classList.add('dialog-input'); + widthInput.value = '1500'; // Default value + newFileDialog.appendChild(widthInput); + + // Create Height input + const heightLabel = document.createElement('label'); + heightLabel.setAttribute('for', 'height'); + heightLabel.classList.add('dialog-label'); + heightLabel.textContent = 'Height:'; + newFileDialog.appendChild(heightLabel); + + const heightInput = document.createElement('input'); + heightInput.type = 'number'; + heightInput.id = 'height'; + heightInput.classList.add('dialog-input'); + heightInput.value = '1000'; // Default value + newFileDialog.appendChild(heightInput); + + // Create Create button + const createButton = document.createElement('button'); + createButton.textContent = 'Create'; + createButton.classList.add('dialog-button'); + createButton.onclick = createNewFile; + newFileDialog.appendChild(createButton); + + + // Create the new file (simulation) + function createNewFile() { + const width = document.getElementById('width').value; + const height = document.getElementById('height').value; + console.log(`New file created with width: ${width} and height: ${height}`); + console.log(callback) + callback(width, height) + + // Add any further logic to handle the new file creation here + + closeDialog(); // Close the dialog after file creation + } + + // Close the dialog if the overlay is clicked + overlay.onclick = closeDialog; +} + +// Show the dialog +function showNewFileDialog() { + overlay.style.display = 'block'; + newFileDialog.style.display = 'block'; +} + +// Close the dialog +function closeDialog() { + overlay.style.display = 'none'; + newFileDialog.style.display = 'none'; +} +export { createNewFileDialog, showNewFileDialog, closeDialog }; \ No newline at end of file diff --git a/src/styles.css b/src/styles.css index 6a08714..9489948 100644 --- a/src/styles.css +++ b/src/styles.css @@ -169,6 +169,7 @@ button { overflow: scroll; width: 100%; height: 100%; + background-color: #555; } .stage { width: 1500px; @@ -284,4 +285,61 @@ button { } .frame:hover { background-color: #555555; +} + + +.hidden { + display: none; +} + +#overlay { + display: none; /* Hidden by default */ + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + background: rgba(0, 0, 0, 0.5); + z-index: 999; /* Under the dialog */ +} + +/* Scoped styles for the dialog */ +#newFileDialog { + display: none; /* Hidden by default */ + position: fixed; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + background-color: #444; + border: 1px solid #333; + border-radius: 5px; + box-shadow: 0 0 10px rgba(0, 0, 0, 0.5); + padding: 20px; + width: 300px; + z-index: 1000; /* Make sure it's in front of other elements */ +} + +#newFileDialog .dialog-label { + display: block; + margin: 10px 0 5px; +} + +#newFileDialog .dialog-input { + width: 100%; + padding: 8px; + margin: 5px 0; + border: 1px solid #333; +} + +#newFileDialog .dialog-button { + width: 100%; + padding: 10px; + background-color: #007bff; + color: white; + border: none; + cursor: pointer; +} + +#newFileDialog .dialog-button:hover { + background-color: #0056b3; } \ No newline at end of file From 7f3fc1a4f35c4cb841d3e41274089e0afdf6202f Mon Sep 17 00:00:00 2001 From: Skyler Lehmkuhl Date: Wed, 27 Nov 2024 07:42:58 -0500 Subject: [PATCH 37/46] work on menus --- src-tauri/capabilities/default.json | 1 + src/main.js | 308 ++++++++++++++++++---------- src/styles.css | 26 ++- 3 files changed, 228 insertions(+), 107 deletions(-) diff --git a/src-tauri/capabilities/default.json b/src-tauri/capabilities/default.json index 1efef12..0b6d6a4 100644 --- a/src-tauri/capabilities/default.json +++ b/src-tauri/capabilities/default.json @@ -7,6 +7,7 @@ ], "permissions": [ "core:default", + "core:window:allow-close", "shell:allow-open", "fs:default", { diff --git a/src/main.js b/src/main.js index a5fc5a4..cf77569 100644 --- a/src/main.js +++ b/src/main.js @@ -591,9 +591,10 @@ function redo() { class Frame { - constructor(uuid) { + constructor(frameType="normal", uuid=undefined) { this.keys = {} this.shapes = [] + this.frameType = frameType if (!uuid) { this.idx = uuidv4() } else { @@ -608,7 +609,7 @@ class Frame { class Layer { constructor(uuid) { - this.frames = [new Frame()] + this.frames = [new Frame("keyframe")] this.children = [] if (!uuid) { this.idx = uuidv4() @@ -955,7 +956,21 @@ class GraphicsObject { return this.layers[this.currentLayer].children } get currentFrame() { - return this.layers[this.currentLayer].frames[this.currentFrameNum] + if (this.layers[this.currentLayer].frames[this.currentFrameNum]) { + if (this.layers[this.currentLayer].frames[this.currentFrameNum].frameType == "keyframe") { + return this.layers[this.currentLayer].frames[this.currentFrameNum] + } else if (this.layers[this.currentLayer].frames[this.currentFrameNum].frameType == "motion") { + + } else if (this.layers[this.currentLayer].frames[this.currentFrameNum].frameType == "shape") { + + } else { + for (let i=this.currentFrameNum; i>=0; i--) { + if (this.layers[this.currentLayer].frames[i].frameType == "keyframe") { + return this.layers[this.currentLayer].frames[i] + } + } + } + } } get maxFrame() { let maxFrames = [] @@ -1134,7 +1149,7 @@ window.addEventListener("resize", () => { updateAll() }) -window.addEventListener("keypress", (e) => { +window.addEventListener("keydown", (e) => { // let shortcuts = {} // for (let shortcut of config.shortcuts) { // shortcut = shortcut.split("+") @@ -1158,8 +1173,28 @@ window.addEventListener("keypress", (e) => { } else if (e.key == config.shortcuts.quit && e.ctrlKey == true) { quit() } + else if (e.key == "ArrowRight") { + advanceFrame() + } + else if (e.key == "ArrowLeft") { + decrementFrame() + } }) +function advanceFrame() { + context.activeObject.currentFrameNum += 1 + updateLayers() + updateMenu() +} + +function decrementFrame() { + if (context.activeObject.currentFrameNum > 0) { + context.activeObject.currentFrameNum -= 1 + updateLayers() + updateMenu() + } +} + function _newFile(width, height) { root = new GraphicsObject("root"); context.activeObject = root @@ -1727,106 +1762,6 @@ function infopanel() { return panel } -async function setupMenu() { - const fileSubmenu = await Submenu.new({ - text: 'File', - items: [ - { - text: 'New file...', - enabled: true, - action: newFile, - }, - { - text: 'Save', - enabled: true, - action: save, - }, - { - text: 'Save As...', - enabled: true, - action: saveAs, - }, - { - text: 'Open File...', - enabled: true, - action: open, - }, - { - text: 'Quit', - enabled: true, - action: quit, - }, - ] - }) - - const editSubmenu = await Submenu.new({ - text: "Edit", - items: [ - { - text: "Undo", - enabled: true, - action: undo - }, - { - text: "Redo", - enabled: true, - action: redo - }, - { - text: "Cut", - enabled: true, - action: () => {} - }, - { - text: "Copy", - enabled: true, - action: () => {} - }, - { - text: "Paste", - enabled: true, - action: () => {} - }, - ] - }); - const viewSubmenu = await Submenu.new({ - text: "View", - items: [ - { - text: "Zoom In", - enabled: true, - action: () => {} - }, - { - text: "Zoom Out", - enabled: true, - action: () => {} - }, - ] - }); - const helpSubmenu = await Submenu.new({ - text: "Help", - items: [ - { - text: "About...", - enabled: true, - action: () => { - messageDialog(`Lightningbeam version ${appVersion}\nDeveloped by Skyler Lehmkuhl`, - {title: 'About', kind: "info"} - ) - } - } - ] -}); - - const menu = await Menu.new({ - items: [fileSubmenu, editSubmenu, viewSubmenu, helpSubmenu], - }) - await (macOS ? menu.setAsAppMenu() : menu.setAsWindowMenu()) -} - -// Initialize the menu when the app starts -setupMenu(); createNewFileDialog(_newFile); showNewFileDialog() @@ -1947,12 +1882,173 @@ function updateLayers() { let layerTrack = document.createElement("div") layerTrack.className = "layer-track" framescontainer.appendChild(layerTrack) - for (let frame of layer.frames) { + let highlightedFrame = false + layer.frames.forEach((frame, i) => { // for (let j=0; j<5-i; j++) { let frameEl = document.createElement("div") frameEl.className = "frame" + if (i == context.activeObject.currentFrameNum) { + frameEl.classList.add("active") + highlightedFrame = true + } + console.log(frame.frameType) + if (frame.frameType == "keyframe") { + frameEl.classList.add("keyframe") + } layerTrack.appendChild(frameEl) + }) + if (!highlightedFrame) { + let highlightObj = document.createElement("div") + let frameCount = layer.frames.length + highlightObj.className = "frame-highlight" + highlightObj.style.left = `${(context.activeObject.currentFrameNum - frameCount) * 25}px`; + layerTrack.appendChild(highlightObj) } } } -} \ No newline at end of file +} + +async function updateMenu() { + let activeFrame; + let newFrameMenuItem; + let newKeyframeMenuItem; + let deleteFrameMenuItem; + + + if (context.activeObject.activeLayer.frames[context.activeObject.currentFrameNum]) { + activeFrame = true + } else { + activeFrame = false + } + const fileSubmenu = await Submenu.new({ + text: 'File', + items: [ + { + text: 'New file...', + enabled: true, + action: newFile, + }, + { + text: 'Save', + enabled: true, + action: save, + }, + { + text: 'Save As...', + enabled: true, + action: saveAs, + }, + { + text: 'Open File...', + enabled: true, + action: open, + }, + { + text: 'Quit', + enabled: true, + action: quit, + }, + ] + }) + + const editSubmenu = await Submenu.new({ + text: "Edit", + items: [ + { + text: "Undo", + enabled: true, + action: undo + }, + { + text: "Redo", + enabled: true, + action: redo + }, + { + text: "Cut", + enabled: true, + action: () => {} + }, + { + text: "Copy", + enabled: true, + action: () => {} + }, + { + text: "Paste", + enabled: true, + action: () => {} + }, + ] + }); + + newFrameMenuItem = { + text: "New Frame", + enabled: !activeFrame, + action: () => {} + } + newKeyframeMenuItem = { + text: "New Keyframe", + enabled: !activeFrame, + action: () => {} + } + deleteFrameMenuItem = { + text: "Delete Frame", + enabled: activeFrame, + action: () => {} + } + + const timelineSubmenu = await Submenu.new({ + text: "Timeline", + items: [ + newFrameMenuItem, + newKeyframeMenuItem, + deleteFrameMenuItem, + { + text: "Return to start", + enabled: false, + action: () => {} + }, + { + text: "Play", + enabled: false, + action: () => {} + }, + ] + }); + const viewSubmenu = await Submenu.new({ + text: "View", + items: [ + { + text: "Zoom In", + enabled: false, + action: () => {} + }, + { + text: "Zoom Out", + enabled: false, + action: () => {} + }, + ] + }); + const helpSubmenu = await Submenu.new({ + text: "Help", + items: [ + { + text: "About...", + enabled: true, + action: () => { + messageDialog(`Lightningbeam version ${appVersion}\nDeveloped by Skyler Lehmkuhl`, + {title: 'About', kind: "info"} + ) + } + } + ] +}); + + const menu = await Menu.new({ + items: [fileSubmenu, editSubmenu, timelineSubmenu, viewSubmenu, helpSubmenu], + }) + await (macOS ? menu.setAsAppMenu() : menu.setAsWindowMenu()) +} +updateMenu() \ No newline at end of file diff --git a/src/styles.css b/src/styles.css index 9489948..728f835 100644 --- a/src/styles.css +++ b/src/styles.css @@ -286,7 +286,31 @@ button { .frame:hover { background-color: #555555; } - +.frame.active { + background-color: #666666; +} +.frame.keyframe { + position: relative; +} +.frame.keyframe::before { + content: ''; /* Creates a pseudo-element */ + position: absolute; + bottom: 0; /* Position the circle at the bottom of the div */ + left: 50%; /* Center the circle horizontally */ + transform: translateX(-50%); /* Adjust for perfect centering */ + width: 50%; /* Set the width of the circle to half of the div's width */ + height: 0; /* Initially set to 0 */ + padding-bottom: 50%; /* Set padding-bottom to 50% of the div's width to create a circle */ + border-radius: 50%; /* Make the shape a circle */ + background-color: #222; /* Set the color of the circle (black in this case) */ + margin-bottom: 5px; +} +.frame-highlight { + background-color: red; + width: 25px; + height: calc( 2 * var(--lineheight) - 2px); + position: relative; +} .hidden { display: none; From e0ba8ed8c3475029280b6389215732c3f9e4a282 Mon Sep 17 00:00:00 2001 From: Skyler Lehmkuhl Date: Mon, 2 Dec 2024 11:48:05 -0500 Subject: [PATCH 38/46] save regions --- src/main.js | 108 ++++++++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 96 insertions(+), 12 deletions(-) diff --git a/src/main.js b/src/main.js index cf77569..22877fe 100644 --- a/src/main.js +++ b/src/main.js @@ -645,6 +645,7 @@ class Shape { this.idx = uuid } pointerList[this.idx] = this + this.regionIdx = 0; } addCurve(curve) { this.curves.push(curve) @@ -669,6 +670,34 @@ class Shape { clear() { this.curves = [] } + copy() { + let newShape = new Shape(this.startx, this.starty, {}) + newShape.startx = this.startx; + newShape.starty = this.starty; + for (let curve of this.curves) { + let newCurve = new Bezier( + curve.points[0].x, curve.points[0].y, + curve.points[1].x, curve.points[1].y, + curve.points[2].x, curve.points[2].y, + curve.points[3].x, curve.points[3].y, + ) + newCurve.color = curve.color + newShape.addCurve(newCurve) + } + // TODO + // for (let vertex of this.vertices) { + + // } + newShape.updateVertices() + newShape.fillStyle = this.fillStyle; + newShape.fillImage = this.fillImage; + newShape.strokeStyle = this.strokeStyle; + newShape.lineWidth = this.lineWidth + newShape.filled = this.filled; + newShape.stroked = this.stroked; + + return newShape + } recalculateBoundingBox() { for (let curve of this.curves) { growBoundingBox(this.boundingBox, curve.bbox()) @@ -797,7 +826,7 @@ class Shape { let i = 0; - let region = {idx: uuidv4(), curves: [], fillStyle: undefined, filled: false} + let region = {idx: `${this.idx}-r${this.regionIdx++}`, curves: [], fillStyle: undefined, filled: false} pointerList[region.idx] = region this.regions = [region] for (let curve of this.curves) { @@ -870,7 +899,7 @@ class Shape { let end = region.curves.indexOf(regionVertexCurves[3]) if (end > start) { let newRegion = { - idx: uuidv4(), // TODO: generate this deterministically so that undo/redo works + idx: `${this.idx}-r${this.regionIdx++}`, // TODO: generate this deterministically so that undo/redo works curves: region.curves.splice(start, end - start), fillStyle: region.fillStyle, filled: true @@ -956,15 +985,18 @@ class GraphicsObject { return this.layers[this.currentLayer].children } get currentFrame() { - if (this.layers[this.currentLayer].frames[this.currentFrameNum]) { - if (this.layers[this.currentLayer].frames[this.currentFrameNum].frameType == "keyframe") { - return this.layers[this.currentLayer].frames[this.currentFrameNum] - } else if (this.layers[this.currentLayer].frames[this.currentFrameNum].frameType == "motion") { + return this.getFrame(this.currentFrameNum) + } + getFrame(num) { + if (this.layers[this.currentLayer].frames[num]) { + if (this.layers[this.currentLayer].frames[num].frameType == "keyframe") { + return this.layers[this.currentLayer].frames[num] + } else if (this.layers[this.currentLayer].frames[num].frameType == "motion") { - } else if (this.layers[this.currentLayer].frames[this.currentFrameNum].frameType == "shape") { + } else if (this.layers[this.currentLayer].frames[num].frameType == "shape") { } else { - for (let i=this.currentFrameNum; i>=0; i--) { + for (let i=num; i>=0; i--) { if (this.layers[this.currentLayer].frames[i].frameType == "keyframe") { return this.layers[this.currentLayer].frames[i] } @@ -1185,6 +1217,7 @@ function advanceFrame() { context.activeObject.currentFrameNum += 1 updateLayers() updateMenu() + updateUI() } function decrementFrame() { @@ -1192,6 +1225,7 @@ function decrementFrame() { context.activeObject.currentFrameNum -= 1 updateLayers() updateMenu() + updateUI() } } @@ -1318,6 +1352,39 @@ async function quit() { } } +function addFrame() { + console.log(context.activeObject.currentFrameNum) + if (context.activeObject.currentFrameNum >= context.activeObject.activeLayer.frames.length) { + for (let i=context.activeObject.activeLayer.frames.length; i<=context.activeObject.currentFrameNum; i++) { + context.activeObject.activeLayer.frames.push(new Frame()) + } + console.log(context.activeObject.activeLayer) + updateLayers() + } +} + +function addKeyframe() { + console.log(context.activeObject.currentFrameNum) + let newKeyframe = new Frame("keyframe") + let latestFrame = context.activeObject.getFrame(Math.max(context.activeObject.currentFrameNum-1, 0)) + for (let key in latestFrame.keys) { + newKeyframe.keys[key] = latestFrame.keys[key] + } + for (let shape of latestFrame.shapes) { + newKeyframe.shapes.push(shape.copy()) + } + if (context.activeObject.currentFrameNum >= context.activeObject.activeLayer.frames.length) { + for (let i=context.activeObject.activeLayer.frames.length; i { + console.log(layerTrack.getBoundingClientRect()) + let mouse = getMousePos(layerTrack, e) + let frameNum = parseInt(mouse.x/25) + context.activeObject.currentFrameNum = frameNum + updateLayers() + updateMenu() + }) let highlightedFrame = false layer.frames.forEach((frame, i) => { // for (let j=0; j<5-i; j++) { let frameEl = document.createElement("div") frameEl.className = "frame" + frameEl.setAttribute("frameNum", i) + // frameEl.addEventListener("click", () => { + // context.activeObject.currentFrameNum = frameEl.getAttribute("frameNum") + // updateLayers() + // }) if (i == context.activeObject.currentFrameNum) { frameEl.classList.add("active") highlightedFrame = true @@ -1910,13 +1990,17 @@ function updateLayers() { async function updateMenu() { let activeFrame; + let activeKeyframe; let newFrameMenuItem; let newKeyframeMenuItem; let deleteFrameMenuItem; - + activeKeyframe = false if (context.activeObject.activeLayer.frames[context.activeObject.currentFrameNum]) { activeFrame = true + if (context.activeObject.activeLayer.frames[context.activeObject.currentFrameNum].frameType=="keyframe") { + activeKeyframe = true + } } else { activeFrame = false } @@ -1985,12 +2069,12 @@ async function updateMenu() { newFrameMenuItem = { text: "New Frame", enabled: !activeFrame, - action: () => {} + action: addFrame } newKeyframeMenuItem = { text: "New Keyframe", - enabled: !activeFrame, - action: () => {} + enabled: !activeKeyframe, + action: addKeyframe } deleteFrameMenuItem = { text: "Delete Frame", From 7778107e4dfbbee4fbe63a887a4d25007ac789fc Mon Sep 17 00:00:00 2001 From: Skyler Lehmkuhl Date: Mon, 2 Dec 2024 14:40:40 -0500 Subject: [PATCH 39/46] change pane type --- src/assets/infopanel.svg | 108 ++++++++++++++++++ src/assets/timeline.svg | 143 ++++++++++++++++++++++++ src/assets/{buttons.svg => toolbar.svg} | 0 src/main.js | 135 +++++++++++++++++----- src/styles.css | 30 +++++ src/utils.js | 5 + 6 files changed, 393 insertions(+), 28 deletions(-) create mode 100644 src/assets/infopanel.svg create mode 100644 src/assets/timeline.svg rename src/assets/{buttons.svg => toolbar.svg} (100%) create mode 100644 src/utils.js diff --git a/src/assets/infopanel.svg b/src/assets/infopanel.svg new file mode 100644 index 0000000..6df5109 --- /dev/null +++ b/src/assets/infopanel.svg @@ -0,0 +1,108 @@ + + + + + + + + + image/svg+xml + + + + + Klaus Staedtler + + + + + + + + + + + + i + + + diff --git a/src/assets/timeline.svg b/src/assets/timeline.svg new file mode 100644 index 0000000..776dfce --- /dev/null +++ b/src/assets/timeline.svg @@ -0,0 +1,143 @@ + + + + + + + image/svg+xml + + + + + Barbara Muraus, Jakub Steiner, Klaus Staedtler + + + Images originally created as the "Art Libre" icon set. Extended and adopted for GIMP + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/assets/buttons.svg b/src/assets/toolbar.svg similarity index 100% rename from src/assets/buttons.svg rename to src/assets/toolbar.svg diff --git a/src/main.js b/src/main.js index 22877fe..577c503 100644 --- a/src/main.js +++ b/src/main.js @@ -3,6 +3,7 @@ import * as fitCurve from '/fit-curve.js'; import { Bezier } from "/bezier.js"; import { Quadtree } from './quadtree.js'; import { createNewFileDialog, showNewFileDialog, closeDialog } from './newfile.js'; +import { titleCase } from './utils.js'; const { writeTextFile: writeTextFile, readTextFile: readTextFile }= window.__TAURI__.fs; const { open: openFileDialog, @@ -748,10 +749,9 @@ class Shape { let newCurves = [] let intersectMap = {} for (let i=0; i= j) continue; let intersects = this.curves[i].intersects(this.curves[j]) if (intersects.length) { intersectMap[i] ||= [] @@ -872,7 +872,6 @@ class Shape { } this.vertices.forEach((vertex, i) => { - console.log(i) for (let i=0; i { rootPane = document.querySelector("#root") - rootPane.appendChild(createPane(toolbar())) + rootPane.appendChild(createPane(panes.toolbar)) rootPane.addEventListener("mousemove", (e) => { mouseEvent = e; }) - let [_toolbar, panel] = splitPane(rootPane, 10, true, createPane(timeline())) - let [stageAndTimeline, _infopanel] = splitPane(panel, 70, false, createPane(infopanel())) - let [_timeline, _stage] = splitPane(stageAndTimeline, 30, false, createPane(stage())) + let [_toolbar, panel] = splitPane(rootPane, 10, true, createPane(panes.timeline)) + let [stageAndTimeline, _infopanel] = splitPane(panel, 70, false, createPane(panes.infopanel)) + let [_timeline, _stage] = splitPane(stageAndTimeline, 30, false, createPane(panes.stage)) }); window.addEventListener("resize", () => { updateAll() }) +window.addEventListener("click", function(event) { + const popupMenu = document.getElementById("popupMenu"); + + // If the menu exists and the click is outside the menu and any button with the class 'paneButton', remove the menu + if (popupMenu && !popupMenu.contains(event.target) && !event.target.classList.contains("paneButton")) { + popupMenu.remove(); // Remove the menu from the DOM + } +}) + window.addEventListener("keydown", (e) => { // let shortcuts = {} // for (let shortcut of config.shortcuts) { // shortcut = shortcut.split("+") // TODO // } - console.log(e) + // console.log(e) if (e.key == config.shortcuts.playAnimation) { console.log("Spacebar pressed") } else if (e.key == config.shortcuts.undo && e.ctrlKey == true) { @@ -1289,7 +1297,6 @@ async function saveAs() { } async function open() { - console.log("gonna open") const path = await openFileDialog({ multiple: false, directory: false, @@ -1353,18 +1360,15 @@ async function quit() { } function addFrame() { - console.log(context.activeObject.currentFrameNum) if (context.activeObject.currentFrameNum >= context.activeObject.activeLayer.frames.length) { for (let i=context.activeObject.activeLayer.frames.length; i<=context.activeObject.currentFrameNum; i++) { context.activeObject.activeLayer.frames.push(new Frame()) } - console.log(context.activeObject.activeLayer) updateLayers() } } function addKeyframe() { - console.log(context.activeObject.currentFrameNum) let newKeyframe = new Frame("keyframe") let latestFrame = context.activeObject.getFrame(Math.max(context.activeObject.currentFrameNum-1, 0)) for (let key in latestFrame.keys) { @@ -1381,7 +1385,6 @@ function addKeyframe() { } else if (context.activeObject.activeLayer.frames[context.activeObject.currentFrameNum].frameType != "keyframe") { context.activeObject.activeLayer.frames[context.activeObject.currentFrameNum] = newKeyframe } - console.log(context.activeObject.activeLayer) updateLayers() } @@ -1509,8 +1512,6 @@ function stage() { for (let curve of region.curves) { intersect_count += curve.intersects(line).length } - console.log(region) - console.log(intersect_count) if (intersect_count%2==1) { // region.fillStyle = context.fillStyle actions.colorRegion.create(region, context.fillStyle) @@ -1833,20 +1834,84 @@ function infopanel() { createNewFileDialog(_newFile); showNewFileDialog() -function createPane(content=undefined) { - let div = document.createElement("div") - let header = document.createElement("div") - if (!content) { - content = stage() // TODO: change based on type +function createPaneMenu(div) { + const menuItems = ["Item 1", "Item 2", "Item 3"]; // The items for the menu + + // Get the menu container (create a new div for the menu) + const popupMenu = document.createElement("div"); + popupMenu.id = "popupMenu"; // Set the ID to ensure we can target it later + + // Create a
    element to hold the list items + const ul = document.createElement("ul"); + + // Loop through the menuItems array and create a
  • for each item + for (let pane in panes) { + const li = document.createElement("li"); + // Create the element for the icon + const img = document.createElement("img"); + img.src = `assets/${panes[pane].name}.svg`; // Use the appropriate SVG as the source + // img.style.width = "20px"; // Set the icon size + // img.style.height = "20px"; // Set the icon size + // img.style.marginRight = "10px"; // Add space between the icon and text + + // Append the image to the
  • element + li.appendChild(img); + + // Set the text of the item + li.appendChild(document.createTextNode(titleCase(panes[pane].name))); + li.addEventListener("click", () => { + createPane(panes[pane], div) + updateUI() + updateLayers() + updateAll() + popupMenu.remove() + }) + ul.appendChild(li); // Append the
  • to the
      } + + popupMenu.appendChild(ul); // Append the
        to the popupMenu div + document.body.appendChild(popupMenu); // Append the menu to the body + return popupMenu; // Return the created menu element +} + +function createPane(paneType=undefined, div=undefined) { + if (!div) { + div = document.createElement("div") + } else { + div.textContent = '' + } + let header = document.createElement("div") + if (!paneType) { + paneType = panes.stage // TODO: change based on type + } + let content = paneType.func() header.className = "header" let button = document.createElement("button") header.appendChild(button) let icon = document.createElement("img") icon.className="icon" - icon.src = "/assets/stage.svg" + icon.src = `/assets/${paneType.name}.svg` button.appendChild(icon) + button.addEventListener("click", () => { + let popupMenu = document.getElementById("popupMenu"); + + // If the menu is already in the DOM, remove it + if (popupMenu) { + popupMenu.remove(); // Remove the menu from the DOM + } else { + // Create and append the new menu to the DOM + popupMenu = createPaneMenu(div); + + // Position the menu below the button + const buttonRect = event.target.getBoundingClientRect(); + popupMenu.style.left = `${buttonRect.left}px`; + popupMenu.style.top = `${buttonRect.bottom + window.scrollY}px`; + } + + // Prevent the click event from propagating to the window click listener + event.stopPropagation(); + }) div.className = "vertical-grid" header.style.height = "calc( 2 * var(--lineheight))" @@ -1941,8 +2006,8 @@ function updateLayers() { let framescontainer = container.querySelectorAll(".frames-container")[0] layerspanel.textContent = "" framescontainer.textContent = "" + console.log(context.activeObject) for (let layer of context.activeObject.layers) { - // for (let i=0; i<5; i++) { let layerHeader = document.createElement("div") layerHeader.className = "layer-header" layerspanel.appendChild(layerHeader) @@ -1959,14 +2024,9 @@ function updateLayers() { }) let highlightedFrame = false layer.frames.forEach((frame, i) => { - // for (let j=0; j<5-i; j++) { let frameEl = document.createElement("div") frameEl.className = "frame" frameEl.setAttribute("frameNum", i) - // frameEl.addEventListener("click", () => { - // context.activeObject.currentFrameNum = frameEl.getAttribute("frameNum") - // updateLayers() - // }) if (i == context.activeObject.currentFrameNum) { frameEl.classList.add("active") highlightedFrame = true @@ -2135,4 +2195,23 @@ async function updateMenu() { }) await (macOS ? menu.setAsAppMenu() : menu.setAsWindowMenu()) } -updateMenu() \ No newline at end of file +updateMenu() + +const panes = { + stage: { + name: "stage", + func: stage + }, + toolbar: { + name: "toolbar", + func: toolbar + }, + timeline: { + name: "timeline", + func: timeline + }, + infopanel: { + name: "infopanel", + func: infopanel + }, +} \ No newline at end of file diff --git a/src/styles.css b/src/styles.css index 728f835..c14b2c6 100644 --- a/src/styles.css +++ b/src/styles.css @@ -366,4 +366,34 @@ button { #newFileDialog .dialog-button:hover { background-color: #0056b3; +} + +#popupMenu { + background-color: #222; + box-shadow: 0 4px 8px rgba(0,0,0,0.5); + padding: 20px; + position: absolute; +} +#popupMenu ul { + padding: 0px; + margin: 0px; +} +#popupMenu li { + color: #ccc; + list-style-type: none; + display: flex; + align-items: center; /* Vertically center the image and text */ + padding: 5px 0; /* Add padding for better spacing */ +} +#popupMenu li:hover { + background-color: #444; + cursor:pointer; +} +#popupMenu li:not(:last-child) { + border-bottom: 1px solid #444; /* Horizontal line for all li elements except the last */ +} +#popupMenu li img { + margin-right: 10px; /* Space between the icon and text */ + width: 20px; /* Adjust the width of the icon */ + height: 20px; /* Adjust the height of the icon */ } \ No newline at end of file diff --git a/src/utils.js b/src/utils.js new file mode 100644 index 0000000..2a63efc --- /dev/null +++ b/src/utils.js @@ -0,0 +1,5 @@ +function titleCase(str) { + return str.charAt(0).toUpperCase() + str.slice(1).toLowerCase(); +} + +export { titleCase }; \ No newline at end of file From 19ed1e39decd4048bcbdbb27df564ae72673f902 Mon Sep 17 00:00:00 2001 From: Skyler Lehmkuhl Date: Mon, 2 Dec 2024 14:41:44 -0500 Subject: [PATCH 40/46] styling tweak --- src/styles.css | 1 + 1 file changed, 1 insertion(+) diff --git a/src/styles.css b/src/styles.css index c14b2c6..449f1bd 100644 --- a/src/styles.css +++ b/src/styles.css @@ -372,6 +372,7 @@ button { background-color: #222; box-shadow: 0 4px 8px rgba(0,0,0,0.5); padding: 20px; + border-radius: 5px; position: absolute; } #popupMenu ul { From d162a9599bfe5a17a2774afa6c7ae37dc587256e Mon Sep 17 00:00:00 2001 From: Skyler Lehmkuhl Date: Mon, 2 Dec 2024 15:15:52 -0500 Subject: [PATCH 41/46] drag ui panes --- src/main.js | 26 +++++++++++++++++++++++++- src/styles.css | 4 ++-- src/utils.js | 19 ++++++++++++++++++- 3 files changed, 45 insertions(+), 4 deletions(-) diff --git a/src/main.js b/src/main.js index 577c503..0b4cf2f 100644 --- a/src/main.js +++ b/src/main.js @@ -3,7 +3,7 @@ import * as fitCurve from '/fit-curve.js'; import { Bezier } from "/bezier.js"; import { Quadtree } from './quadtree.js'; import { createNewFileDialog, showNewFileDialog, closeDialog } from './newfile.js'; -import { titleCase } from './utils.js'; +import { titleCase, getMousePositionFraction } from './utils.js'; const { writeTextFile: writeTextFile, readTextFile: readTextFile }= window.__TAURI__.fs; const { open: openFileDialog, @@ -1944,6 +1944,30 @@ function splitPane(div, percent, horiz, newPane=undefined) { div.className = "vertical-grid" } div.setAttribute("lb-percent", percent) // TODO: better attribute name + div.addEventListener('mousedown', function(event) { + // Check if the clicked element is the parent itself and not a child element + if (event.target === event.currentTarget) { + event.currentTarget.setAttribute("dragging", true) + event.currentTarget.style.userSelect = 'none'; + rootPane.style.userSelect = "none"; + } else { + event.currentTarget.setAttribute("dragging", false) + } + }); + div.addEventListener('mousemove', function(event) { + // Check if the clicked element is the parent itself and not a child element + if (event.currentTarget.getAttribute("dragging")=="true") { + const frac = getMousePositionFraction(event, event.currentTarget) + div.setAttribute("lb-percent", frac*100) + updateAll() + console.log(frac); // Ensure the fraction is between 0 and 1 + } + }); + div.addEventListener('mouseup', (event) => { + console.log("mouseup") + event.currentTarget.setAttribute("dragging", false) + event.currentTarget.style.userSelect = 'auto'; + }) Coloris({el: ".color-field"}) updateAll() updateUI() diff --git a/src/styles.css b/src/styles.css index 449f1bd..8b5fb14 100644 --- a/src/styles.css +++ b/src/styles.css @@ -158,11 +158,11 @@ button { } /* I don't fully understand this selector but it works for now */ .horizontal-grid:hover:not(:has(*:hover)) { - background: red; + background: #666; cursor: ew-resize; } .vertical-grid:hover:not(:has(*:hover)) { - background: red; + background: #666; cursor: ns-resize } .scroll { diff --git a/src/utils.js b/src/utils.js index 2a63efc..aad03a0 100644 --- a/src/utils.js +++ b/src/utils.js @@ -2,4 +2,21 @@ function titleCase(str) { return str.charAt(0).toUpperCase() + str.slice(1).toLowerCase(); } -export { titleCase }; \ No newline at end of file +function getMousePositionFraction(event, element) { + const rect = element.getBoundingClientRect(); // Get the element's position and size + + if (element.classList.contains('horizontal-grid')) { + // If the element has the "horizontal-grid" class, calculate the horizontal position (X) + const xPos = event.clientX - rect.left; // Mouse X position relative to the element + const fraction = xPos / rect.width; // Fraction of the width + return Math.min(Math.max(fraction, 0), 1); // Ensure the fraction is between 0 and 1 + } else if (element.classList.contains('vertical-grid')) { + // If the element has the "vertical-grid" class, calculate the vertical position (Y) + const yPos = event.clientY - rect.top; // Mouse Y position relative to the element + const fraction = yPos / rect.height; // Fraction of the height + return Math.min(Math.max(fraction, 0), 1); // Ensure the fraction is between 0 and 1 + } + return 0; // If neither class is present, return 0 (or handle as needed) + } + +export { titleCase, getMousePositionFraction }; \ No newline at end of file From 95834bb0e9eb463123b7c1f924d762a947169584 Mon Sep 17 00:00:00 2001 From: Skyler Lehmkuhl Date: Mon, 2 Dec 2024 20:06:50 -0500 Subject: [PATCH 42/46] motion tween --- src/main.js | 311 ++++++++++++++++++++++++++++++++++++------------- src/styles.css | 15 +++ src/utils.js | 24 +++- 3 files changed, 267 insertions(+), 83 deletions(-) diff --git a/src/main.js b/src/main.js index 0b4cf2f..cba6570 100644 --- a/src/main.js +++ b/src/main.js @@ -3,7 +3,7 @@ import * as fitCurve from '/fit-curve.js'; import { Bezier } from "/bezier.js"; import { Quadtree } from './quadtree.js'; import { createNewFileDialog, showNewFileDialog, closeDialog } from './newfile.js'; -import { titleCase, getMousePositionFraction } from './utils.js'; +import { titleCase, getMousePositionFraction, getKeyframesSurrounding } from './utils.js'; const { writeTextFile: writeTextFile, readTextFile: readTextFile }= window.__TAURI__.fs; const { open: openFileDialog, @@ -240,43 +240,49 @@ let actions = { } }, addImageObject: { - create: (x, y, img, parent) => { + create: (x, y, imgsrc, ix, parent) => { redoStack.length = 0; // Clear redo stack let action = { shapeUuid: uuidv4(), objectUuid: uuidv4(), x: x, y: y, - width: img.width, - height: img.height, - ix: img.ix, - img: img.idx, + src: imgsrc, + ix: ix, parent: parent.idx + } undoStack.push({name: "addImageObject", action: action}) actions.addImageObject.execute(action) }, execute: (action) => { let imageObject = new GraphicsObject(action.objectUuid) - let img = pointerList[action.img] - let ct = { - ...context, - fillImage: img, - strokeShape: false, + // let img = pointerList[action.img] + let img = new Image(); + img.onload = function() { + let ct = { + ...context, + fillImage: img, + strokeShape: false, + } + let imageShape = new Shape(0, 0, ct, action.shapeUuid) + imageShape.addLine(img.width, 0) + imageShape.addLine(img.width, img.height) + imageShape.addLine(0, img.height) + imageShape.addLine(0, 0) + imageShape.update() + imageShape.regions[0].fillImage = img + imageShape.regions[0].filled = true + imageObject.addShape(imageShape) + let parent = pointerList[action.parent] + parent.addObject( + imageObject, + action.x-img.width/2 + (20*action.ix), + action.y-img.height/2 + (20*action.ix) + ) + updateUI(); } - let imageShape = new Shape(0, 0, ct, action.shapeUuid) - imageShape.addLine(action.width, 0) - imageShape.addLine(action.width, action.height) - imageShape.addLine(0, action.height) - imageShape.addLine(0, 0) - imageShape.update() - imageObject.addShape(imageShape) - let parent = pointerList[action.parent] - parent.addObject( - imageObject, - action.x-action.width/2 + (20*action.ix), - action.y-action.height/2 + (20*action.ix) - ) + img.src = action.src }, rollback: (action) => { let shape = pointerList[action.shapeUuid] @@ -312,6 +318,111 @@ let actions = { frame.keys = structuredClone(action.oldState) } }, + addFrame: { + create: () => { + redoStack.length = 0 + let frames = [] + for (let i=context.activeObject.activeLayer.frames.length; i<=context.activeObject.currentFrameNum; i++) { + frames.push(uuidv4()) + } + let action = { + frames: frames, + layer: context.activeObject.activeLayer.idx + } + undoStack.push({name: 'addFrame', action: action}) + actions.addFrame.execute(action) + }, + execute: (action) => { + let layer = pointerList[action.layer] + for (let frame of action.frames) { + layer.frames.push(new Frame("normal", frame)) + } + updateLayers() + }, + rollback: (action) => { + let layer = pointerList[action.layer] + for (let _frame of action.frames) { + layer.frames.pop() + } + updateLayers() + } + }, + addKeyframe: { + create: () => { + let frameNum = context.activeObject.currentFrameNum + let layer = context.activeObject.activeLayer + let formerType; + let addedFrames = 0; + if (frameNum >= layer.frames.length) { + formerType = "none" + addedFrames = frameNum - layer.frames.length + } else if (layer.frames[frameNum].frameType != "keyframe") { + formerType = layer.frames[frameNum].frameType + } else { + console.log("foolish") + return // Already a keyframe, nothing to do + } + redoStack.length = 0 + let action = { + frameNum: frameNum, + object: context.activeObject.idx, + layer: layer.idx, + formerType: formerType, + addedFrames: addedFrames + } + undoStack.push({name: 'addKeyframe', action: action}) + actions.addKeyframe.execute(action) + }, + execute: (action) => { + // your code here + let object = pointerList[action.object] + let layer = pointerList[action.layer] + let latestFrame = object.getFrame(Math.max(action.frameNum-1, 0)) + let newKeyframe = new Frame("keyframe") + for (let key in latestFrame.keys) { + newKeyframe.keys[key] = structuredClone(latestFrame.keys[key]) + } + for (let shape of latestFrame.shapes) { + newKeyframe.shapes.push(shape.copy()) + } + if (action.frameNum >= layer.frames.length) { + for (let i=layer.frames.length; i { + let layer = pointerList[action.layer] + if (action.formerType == "none") { + for (let i=0; i { + redoStack.length = 0 + let action = { + } + undoStack.push({name: 'addMotionTween', action: action}) + actions.addMotionTween.execute(action) + }, + execute: (action) => { + // your code here + }, + rollback: (action) => { + // your code here + } + }, } function uuidv4() { @@ -919,7 +1030,7 @@ class Shape { ctx.lineCap = "round" for (let region of this.regions) { // if (region.filled) continue; - if (region.fillStyle && region.filled) { + if ((region.fillStyle || region.fillImage) && region.filled) { // ctx.fillStyle = region.fill if (region.fillImage) { let pat = ctx.createPattern(region.fillImage, "no-repeat") @@ -937,19 +1048,21 @@ class Shape { ctx.fill() } } - for (let curve of this.curves) { - ctx.strokeStyle = curve.color - ctx.beginPath() - ctx.moveTo(curve.points[0].x, curve.points[0].y) - ctx.bezierCurveTo(curve.points[1].x, curve.points[1].y, - curve.points[2].x, curve.points[2].y, - curve.points[3].x, curve.points[3].y) - ctx.stroke() + if (this.stroked) { + for (let curve of this.curves) { + ctx.strokeStyle = curve.color + ctx.beginPath() + ctx.moveTo(curve.points[0].x, curve.points[0].y) + ctx.bezierCurveTo(curve.points[1].x, curve.points[1].y, + curve.points[2].x, curve.points[2].y, + curve.points[3].x, curve.points[3].y) + ctx.stroke() - // Debug, show curve endpoints - // ctx.beginPath() - // ctx.arc(curve.points[3].x,curve.points[3].y, 3, 0, 2*Math.PI) - // ctx.fill() + // Debug, show curve endpoints + // ctx.beginPath() + // ctx.arc(curve.points[3].x,curve.points[3].y, 3, 0, 2*Math.PI) + // ctx.fill() + } } // Debug, show quadtree // this.quadtree.draw(ctx) @@ -981,26 +1094,46 @@ class GraphicsObject { return this.layers[this.currentLayer] } get children() { - return this.layers[this.currentLayer].children + return this.activeLayer.children } get currentFrame() { return this.getFrame(this.currentFrameNum) } getFrame(num) { - if (this.layers[this.currentLayer].frames[num]) { - if (this.layers[this.currentLayer].frames[num].frameType == "keyframe") { - return this.layers[this.currentLayer].frames[num] - } else if (this.layers[this.currentLayer].frames[num].frameType == "motion") { - - } else if (this.layers[this.currentLayer].frames[num].frameType == "shape") { + if (this.activeLayer.frames[num]) { + if (this.activeLayer.frames[num].frameType == "keyframe") { + return this.activeLayer.frames[num] + } else if (this.activeLayer.frames[num].frameType == "motion") { + let frameKeys = {} + const t = (num - this.activeLayer.frames[num].prevIndex) / (this.activeLayer.frames[num].nextIndex - this.activeLayer.frames[num].prevIndex); + console.log(this.activeLayer.frames[num].prev) + for (let key in this.activeLayer.frames[num].prev.keys) { + frameKeys[key] = {} + let prevKeyDict = this.activeLayer.frames[num].prev.keys[key] + let nextKeyDict = this.activeLayer.frames[num].next.keys[key] + for (let prop in prevKeyDict) { + frameKeys[key][prop] = (1 - t) * prevKeyDict[prop] + t * nextKeyDict[prop]; + } + + } + let frame = new Frame("motion", "temp") + frame.keys = frameKeys + return frame + } else if (this.activeLayer.frames[num].frameType == "shape") { } else { - for (let i=num; i>=0; i--) { - if (this.layers[this.currentLayer].frames[i].frameType == "keyframe") { - return this.layers[this.currentLayer].frames[i] + for (let i=Math.min(num, this.activeLayer.frames.length-1); i>=0; i--) { + if (this.activeLayer.frames[i].frameType == "keyframe") { + return this.activeLayer.frames[i] } } } + } else { + for (let i=Math.min(num, this.activeLayer.frames.length-1); i>=0; i--) { + if (this.activeLayer.frames[i].frameType == "keyframe") { + return this.activeLayer.frames[i] + } + } } } get maxFrame() { @@ -1032,9 +1165,9 @@ class GraphicsObject { let ctx = context.ctx; ctx.translate(this.x, this.y) ctx.rotate(this.rotation) - if (this.currentFrameNum>=this.maxFrame) { - this.currentFrameNum = 0; - } + // if (this.currentFrameNum>=this.maxFrame) { + // this.currentFrameNum = 0; + // } for (let shape of this.currentFrame.shapes) { shape.draw(context) } @@ -1361,31 +1494,30 @@ async function quit() { function addFrame() { if (context.activeObject.currentFrameNum >= context.activeObject.activeLayer.frames.length) { - for (let i=context.activeObject.activeLayer.frames.length; i<=context.activeObject.currentFrameNum; i++) { - context.activeObject.activeLayer.frames.push(new Frame()) - } - updateLayers() + actions.addFrame.create() } } function addKeyframe() { - let newKeyframe = new Frame("keyframe") - let latestFrame = context.activeObject.getFrame(Math.max(context.activeObject.currentFrameNum-1, 0)) - for (let key in latestFrame.keys) { - newKeyframe.keys[key] = latestFrame.keys[key] - } - for (let shape of latestFrame.shapes) { - newKeyframe.shapes.push(shape.copy()) - } - if (context.activeObject.currentFrameNum >= context.activeObject.activeLayer.frames.length) { - for (let i=context.activeObject.activeLayer.frames.length; i { @@ -2055,10 +2199,8 @@ function updateLayers() { frameEl.classList.add("active") highlightedFrame = true } - console.log(frame.frameType) - if (frame.frameType == "keyframe") { - frameEl.classList.add("keyframe") - } + + frameEl.classList.add(frame.frameType) layerTrack.appendChild(frameEl) }) if (!highlightedFrame) { @@ -2172,6 +2314,11 @@ async function updateMenu() { newFrameMenuItem, newKeyframeMenuItem, deleteFrameMenuItem, + { + text: "Add Motion Tween", + enabled: activeFrame && (!activeKeyframe), + action: addMotionTween + }, { text: "Return to start", enabled: false, diff --git a/src/styles.css b/src/styles.css index 8b5fb14..51781e1 100644 --- a/src/styles.css +++ b/src/styles.css @@ -305,6 +305,21 @@ button { background-color: #222; /* Set the color of the circle (black in this case) */ margin-bottom: 5px; } +.frame.motion { + background-color: #7a00b3; + border: none; +} +.frame.motion:hover, .frame.motion.active { + background-color: #530379; + border: none; +} +/* :nth-child(1 of .frame.motion) { + background-color: blue; +} +:nth-last-child(1 of .frame.motion) { + background-color: red; +} */ + .frame-highlight { background-color: red; width: 25px; diff --git a/src/utils.js b/src/utils.js index aad03a0..48c21bd 100644 --- a/src/utils.js +++ b/src/utils.js @@ -19,4 +19,26 @@ function getMousePositionFraction(event, element) { return 0; // If neither class is present, return 0 (or handle as needed) } -export { titleCase, getMousePositionFraction }; \ No newline at end of file +function getKeyframesSurrounding(frames, index) { + let lastKeyframeBefore = undefined; + let firstKeyframeAfter = undefined; + + // Find the last keyframe before the given index + for (let i = index - 1; i >= 0; i--) { + if (frames[i].frameType === "keyframe") { + lastKeyframeBefore = i; + break; + } + } + + // Find the first keyframe after the given index + for (let i = index + 1; i < frames.length; i++) { + if (frames[i].frameType === "keyframe") { + firstKeyframeAfter = i; + break; + } + } + return { lastKeyframeBefore, firstKeyframeAfter }; +} + +export { titleCase, getMousePositionFraction, getKeyframesSurrounding }; \ No newline at end of file From a891475c6216ccae5ddc4c4f8a310bfaee672efc Mon Sep 17 00:00:00 2001 From: Skyler Lehmkuhl Date: Tue, 3 Dec 2024 00:38:01 -0500 Subject: [PATCH 43/46] load frames in opened file --- src/main.js | 57 +++++++++++++++++++++++++++++++++++++------------- src/newfile.js | 18 ++++++++++++++-- src/utils.js | 52 ++++++++++++++++++++++++++++++++++++++++++++- 3 files changed, 109 insertions(+), 18 deletions(-) diff --git a/src/main.js b/src/main.js index cba6570..84c7578 100644 --- a/src/main.js +++ b/src/main.js @@ -3,7 +3,7 @@ import * as fitCurve from '/fit-curve.js'; import { Bezier } from "/bezier.js"; import { Quadtree } from './quadtree.js'; import { createNewFileDialog, showNewFileDialog, closeDialog } from './newfile.js'; -import { titleCase, getMousePositionFraction, getKeyframesSurrounding } from './utils.js'; +import { titleCase, getMousePositionFraction, getKeyframesSurrounding, invertPixels } from './utils.js'; const { writeTextFile: writeTextFile, readTextFile: readTextFile }= window.__TAURI__.fs; const { open: openFileDialog, @@ -43,7 +43,9 @@ let maxFileVersion = "2.0" let filePath = undefined let fileWidth = 1500 let fileHeight = 1000 +let fileFps = 12 +let playing = false let tools = { select: { @@ -311,6 +313,8 @@ let actions = { }, execute: (action) => { let frame = pointerList[action.frame] + console.log(pointerList) + console.log(action.frame) frame.keys = structuredClone(action.newState) }, rollback: (action) => { @@ -352,10 +356,12 @@ let actions = { let frameNum = context.activeObject.currentFrameNum let layer = context.activeObject.activeLayer let formerType; - let addedFrames = 0; + let addedFrames = {}; if (frameNum >= layer.frames.length) { formerType = "none" - addedFrames = frameNum - layer.frames.length + for (let i=layer.frames.length; i<=frameNum; i++) { + addedFrames[i] = uuidv4() + } } else if (layer.frames[frameNum].frameType != "keyframe") { formerType = layer.frames[frameNum].frameType } else { @@ -368,17 +374,17 @@ let actions = { object: context.activeObject.idx, layer: layer.idx, formerType: formerType, - addedFrames: addedFrames + addedFrames: addedFrames, + uuid: uuidv4() } undoStack.push({name: 'addKeyframe', action: action}) actions.addKeyframe.execute(action) }, execute: (action) => { - // your code here let object = pointerList[action.object] let layer = pointerList[action.layer] let latestFrame = object.getFrame(Math.max(action.frameNum-1, 0)) - let newKeyframe = new Frame("keyframe") + let newKeyframe = new Frame("keyframe", action.uuid) for (let key in latestFrame.keys) { newKeyframe.keys[key] = structuredClone(latestFrame.keys[key]) } @@ -386,19 +392,20 @@ let actions = { newKeyframe.shapes.push(shape.copy()) } if (action.frameNum >= layer.frames.length) { - for (let i=layer.frames.length; i { let layer = pointerList[action.layer] if (action.formerType == "none") { - for (let i=0; i { // console.log(e) if (e.key == config.shortcuts.playAnimation) { console.log("Spacebar pressed") + playPause() } else if (e.key == config.shortcuts.undo && e.ctrlKey == true) { undo() } else if (e.key == config.shortcuts.redo && e.ctrlKey == true) { @@ -1354,6 +1371,11 @@ window.addEventListener("keydown", (e) => { } }) +function playPause() { + playing = !playing + updateUI() +} + function advanceFrame() { context.activeObject.currentFrameNum += 1 updateLayers() @@ -1370,11 +1392,12 @@ function decrementFrame() { } } -function _newFile(width, height) { +function _newFile(width, height, fps) { root = new GraphicsObject("root"); context.activeObject = root fileWidth = width fileHeight = height + fileFps = fps for (let stage of document.querySelectorAll(".stage")) { stage.width = width stage.height = height @@ -1397,6 +1420,7 @@ async function _save(path) { version: "1.1", width: fileWidth, height: fileHeight, + fps: fileFps, actions: undoStack } const contents = JSON.stringify(fileData ); @@ -1451,7 +1475,7 @@ async function open() { } if (file.version >= minFileVersion) { if (file.version < maxFileVersion) { - _newFile(file.width, file.height) + _newFile(file.width, file.height, file.fps) if (file.actions == undefined) { await messageDialog("File has no content!", {title: "Parse error", kind: 'error'}) return @@ -2163,6 +2187,9 @@ function updateUI() { } } + if (playing) { + setTimeout(advanceFrame, 1000/fileFps) + } } function updateLayers() { diff --git a/src/newfile.js b/src/newfile.js index a6acf15..0ced18f 100644 --- a/src/newfile.js +++ b/src/newfile.js @@ -44,6 +44,20 @@ function createNewFileDialog(callback) { heightInput.value = '1000'; // Default value newFileDialog.appendChild(heightInput); + // Create FPS input + const fpsLabel = document.createElement('label'); + fpsLabel.setAttribute('for', 'fps'); + fpsLabel.classList.add('dialog-label'); + fpsLabel.textContent = 'Frames per Second:'; + newFileDialog.appendChild(fpsLabel); + + const fpsInput = document.createElement('input'); + fpsInput.type = 'number'; + fpsInput.id = 'fps'; + fpsInput.classList.add('dialog-input'); + fpsInput.value = '12'; // Default value + newFileDialog.appendChild(fpsInput); + // Create Create button const createButton = document.createElement('button'); createButton.textContent = 'Create'; @@ -56,9 +70,9 @@ function createNewFileDialog(callback) { function createNewFile() { const width = document.getElementById('width').value; const height = document.getElementById('height').value; + const fps = document.getElementById('fps').value; console.log(`New file created with width: ${width} and height: ${height}`); - console.log(callback) - callback(width, height) + callback(width, height, fps) // Add any further logic to handle the new file creation here diff --git a/src/utils.js b/src/utils.js index 48c21bd..974df6d 100644 --- a/src/utils.js +++ b/src/utils.js @@ -41,4 +41,54 @@ function getKeyframesSurrounding(frames, index) { return { lastKeyframeBefore, firstKeyframeAfter }; } -export { titleCase, getMousePositionFraction, getKeyframesSurrounding }; \ No newline at end of file +function invertPixels(ctx, width, height) { + // Create an off-screen canvas for the pattern + const patternCanvas = document.createElement('canvas'); + const patternContext = patternCanvas.getContext('2d'); + + // Define the size of the repeating pattern (2x2 pixels) + const patternSize = 2; + patternCanvas.width = patternSize; + patternCanvas.height = patternSize; + + // Create the alternating pattern (regular and inverted pixels) + function createInvertedPattern() { + const patternData = patternContext.createImageData(patternSize, patternSize); + const data = patternData.data; + + // Fill the pattern with alternating colors (inverted every other pixel) + for (let i = 0; i < patternSize; i++) { + for (let j = 0; j < patternSize; j++) { + const index = (i * patternSize + j) * 4; + // Determine if we should invert the color + if ((i + j) % 2 === 0) { + data[index] = 255; // Red + data[index + 1] = 0; // Green + data[index + 2] = 0; // Blue + data[index + 3] = 255; // Alpha + } else { + data[index] = 0; // Red (inverted) + data[index + 1] = 255; // Green (inverted) + data[index + 2] = 255; // Blue (inverted) + data[index + 3] = 255; // Alpha + } + } + } + + // Set the pattern on the off-screen canvas + patternContext.putImageData(patternData, 0, 0); + return patternCanvas; + } + + // Create the pattern using the function + const pattern = ctx.createPattern(createInvertedPattern(), 'repeat'); + + // Draw a rectangle with the pattern + ctx.globalCompositeOperation = "difference" + ctx.fillStyle = pattern; + ctx.fillRect(0, 0, width, height); + + ctx.globalCompositeOperation = "source-over" +} + +export { titleCase, getMousePositionFraction, getKeyframesSurrounding, invertPixels }; \ No newline at end of file From dac0fc3efbc4a7c5b276eb573088eb6301b19de9 Mon Sep 17 00:00:00 2001 From: Skyler Lehmkuhl Date: Tue, 3 Dec 2024 00:50:02 -0500 Subject: [PATCH 44/46] load motion tween --- src/main.js | 46 ++++++++++++++++++++++++++++++---------------- 1 file changed, 30 insertions(+), 16 deletions(-) diff --git a/src/main.js b/src/main.js index 84c7578..365c885 100644 --- a/src/main.js +++ b/src/main.js @@ -418,16 +418,43 @@ let actions = { addMotionTween: { create: () => { redoStack.length = 0 + let frameNum = context.activeObject.currentFrameNum + let layer = context.activeObject.activeLayer + let frames = layer.frames + let {lastKeyframeBefore, firstKeyframeAfter} = getKeyframesSurrounding(frames, frameNum) + let action = { + frameNum: frameNum, + layer: layer.idx, + lastBefore: lastKeyframeBefore, + firstAfter: firstKeyframeAfter, } undoStack.push({name: 'addMotionTween', action: action}) actions.addMotionTween.execute(action) }, execute: (action) => { - // your code here + let layer = pointerList[action.layer] + let frames = layer.frames + if ((action.lastBefore != undefined) && (action.firstAfter != undefined)) { + for (let i=action.lastBefore + 1; i { - // your code here + let layer = pointerList[action.layer] + let frames = layer.frames + for (let i=action.lastBefore + 1; i Date: Tue, 3 Dec 2024 10:45:03 -0500 Subject: [PATCH 45/46] group --- src/main.js | 126 +++++++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 116 insertions(+), 10 deletions(-) diff --git a/src/main.js b/src/main.js index 365c885..36e872f 100644 --- a/src/main.js +++ b/src/main.js @@ -111,6 +111,7 @@ let context = { dragging: false, selectionRect: undefined, selection: [], + shapeselection: [], } let config = { @@ -124,6 +125,7 @@ let config = { saveAs: "S", open: "o", quit: "q", + group: "g", } } @@ -457,6 +459,65 @@ let actions = { updateUI() } }, + group: { + create: () => { + redoStack.length = 0 + let serializableShapes = [] + let serializableObjects = [] + for (let shape of context.shapeselection) { + serializableShapes.push(shape.idx) + } + for (let object of context.selection) { + serializableObjects.push(object.idx) + } + context.shapeselection = [] + context.selection = [] + let action = { + shapes: serializableShapes, + objects: serializableObjects, + groupUuid: uuidv4(), + parent: context.activeObject.idx + } + undoStack.push({name: 'group', action: action}) + actions.group.execute(action) + }, + execute: (action) => { + // your code here + let group = new GraphicsObject(action.groupUuid) + let parent = pointerList[action.parent] + for (let shapeIdx of action.shapes) { + let shape = pointerList[shapeIdx] + group.addShape(shape) + parent.removeShape(shape) + } + for (let objectIdx of action.objects) { + let object = pointerList[objectIdx] + group.addObject(object, object.x, object.y) + parent.removeChild(object) + } + parent.addObject(group) + if (context.activeObject==parent && context.selection.length==0 && context.shapeselection.length==0) { + context.selection.push(group) + } + updateUI() + }, + rollback: (action) => { + let group = pointerList[action.groupUuid] + let parent = pointerList[action.parent] + for (let shapeIdx of action.shapes) { + let shape = pointerList[shapeIdx] + parent.addShape(shape) + group.removeShape(shape) + } + for (let objectIdx of action.objects) { + let object = pointerList[objectIdx] + parent.addObject(object, object.x, object.y) + group.removeChild(object) + } + parent.removeChild(group) + updateUI() + } + }, } function uuidv4() { @@ -691,6 +752,7 @@ function regionToBbox(region) { } function hitTest(candidate, object) { + return hitTestShape(candidate, object) let bbox = object.bbox() if (candidate.x.min) { // We're checking a bounding box @@ -713,6 +775,30 @@ function hitTest(candidate, object) { } } +function hitTestShape(candidate, shape) { + let bbox = shape.bbox() + if (candidate.x.min) { + // We're checking a bounding box + if (candidate.x.min < bbox.x.max && candidate.x.max > bbox.x.min && + candidate.y.min < bbox.y.max && candidate.y.max > bbox.y.min) { + return true; + } else { + return false; + } + } else { + // We're checking a point + if (candidate.x > bbox.x.min && + candidate.x < bbox.x.max && + candidate.y > bbox.y.min && + candidate.y < bbox.y.max) { + return true; + } else { + return false + } + } + +} + function undo() { let action = undoStack.pop() if (action) { @@ -813,6 +899,9 @@ class Shape { curve.color = context.strokeStyle this.curves.push(curve) } + bbox() { + return this.boundingBox + } clear() { this.curves = [] } @@ -1180,19 +1269,24 @@ class GraphicsObject { bbox() { let bbox; if (this.currentFrame.shapes.length > 0) { - bbox = this.currentFrame.shapes[0].boundingBox + bbox = structuredClone(this.currentFrame.shapes[0].boundingBox) for (let shape of this.currentFrame.shapes) { growBoundingBox(bbox, shape.boundingBox) } } if (this.children.length > 0) { if (!bbox) { - bbox = this.children[0].bbox() + bbox = structuredClone(this.children[0].bbox()) } for (let child of this.children) { growBoundingBox(bbox, child.bbox()) } } + bbox.x.min += this.x + bbox.x.max += this.x + bbox.y.min += this.y + bbox.y.max += this.y + console.log(bbox) return bbox } draw(context) { @@ -1203,11 +1297,11 @@ class GraphicsObject { // this.currentFrameNum = 0; // } for (let shape of this.currentFrame.shapes) { - if (false) { + if (context.shapeselection.indexOf(shape) >= 0) { invertPixels(ctx, fileWidth, fileHeight) } shape.draw(context) - if (false) { + if (context.shapeselection.indexOf(shape) >= 0) { invertPixels(ctx, fileWidth, fileHeight) } } @@ -1268,10 +1362,9 @@ class GraphicsObject { ctx.save() ctx.strokeStyle = "#00ffff" ctx.lineWidth = 1; - ctx.translate(item.x, item.y) ctx.beginPath() let bbox = item.bbox() - ctx.rect(bbox.x.min, bbox.y.min, bbox.x.max, bbox.y.max) + ctx.rect(bbox.x.min, bbox.y.min, bbox.x.max - bbox.x.min, bbox.y.max - bbox.y.min) ctx.stroke() ctx.restore() } @@ -1375,10 +1468,6 @@ window.addEventListener("keydown", (e) => { if (e.key == config.shortcuts.playAnimation) { console.log("Spacebar pressed") playPause() - } else if (e.key == config.shortcuts.undo && e.ctrlKey == true) { - undo() - } else if (e.key == config.shortcuts.redo && e.ctrlKey == true) { - redo() } else if (e.key == config.shortcuts.new && e.ctrlKey == true) { newFile() } else if (e.key == config.shortcuts.save && e.ctrlKey == true) { @@ -1389,6 +1478,12 @@ window.addEventListener("keydown", (e) => { open() } else if (e.key == config.shortcuts.quit && e.ctrlKey == true) { quit() + } else if (e.key == config.shortcuts.undo && e.ctrlKey == true) { + undo() + } else if (e.key == config.shortcuts.redo && e.ctrlKey == true) { + redo() + } else if (e.key == config.shortcuts.group && e.ctrlKey == true) { + actions.group.create() } else if (e.key == "ArrowRight") { advanceFrame() @@ -1819,11 +1914,17 @@ function stage() { context.selectionRect.x2 = mouse.x context.selectionRect.y2 = mouse.y context.selection = [] + context.shapeselection = [] for (let child of context.activeObject.children) { if (hitTest(regionToBbox(context.selectionRect), child)) { context.selection.push(child) } } + for (let shape of context.activeObject.currentFrame.shapes) { + if (hitTestShape(regionToBbox(context.selectionRect), shape)) { + context.shapeselection.push(shape) + } + } } else { let selection = selectVertex(context, mouse) if (selection) { @@ -2330,6 +2431,11 @@ async function updateMenu() { enabled: true, action: () => {} }, + { + text: "Group", + enabled: true, + action: actions.group.create + }, ] }); From ba54e4de8b45e1e76a783a87907b0c1592a24801 Mon Sep 17 00:00:00 2001 From: Skyler Lehmkuhl Date: Tue, 3 Dec 2024 10:51:09 -0500 Subject: [PATCH 46/46] consolidate hit tests --- src/main.js | 115 ++-------------------------------------------------- 1 file changed, 3 insertions(+), 112 deletions(-) diff --git a/src/main.js b/src/main.js index 36e872f..705847b 100644 --- a/src/main.js +++ b/src/main.js @@ -625,63 +625,6 @@ function moldCurve(curve, mouse, oldmouse) { // return curve } -function moldCurveMath(curve, mouse) { - let interpolated = true - - let p = curve.project({x: mouse.x, y: mouse.y}) - - let t1 = p.t; - let struts = curve.getStrutPoints(t1); - let m = { - t: p.t, - B: p, - e1: struts[7], - e2: struts[8] - }; - m.d1 = { x: m.e1.x - m.B.x, y: m.e1.y - m.B.y}; - m.d2 = { x: m.e2.x - m.B.x, y: m.e2.y - m.B.y}; - - const S = curve.points[0], - E = curve.points[curve.order], - {B, t, e1, e2} = m, - org = curve.getABC(t, B), - nB = mouse, - d1 = { x: e1.x - B.x, y: e1.y - B.y }, - d2 = { x: e2.x - B.x, y: e2.y - B.y }, - ne1 = { x: nB.x + d1.x, y: nB.y + d1.y }, - ne2 = { x: nB.x + d2.x, y: nB.y + d2.y }, - {A, C} = curve.getABC(t, nB), - // The cubic case requires us to derive two control points, - // which we'll do in a separate function to keep the code - // at least somewhat manageable. - {v1, v2, C1, C2} = deriveControlPoints(S, A, E, ne1, ne2, t); - - // if (interpolated) { - // For the last example, we need to show what the "ideal" curve - // looks like, in addition to the one we actually get when we - // rely on the B we picked with the `t` value and e1/e2 points - // that point B had... - const ideal = getIdealisedCurve(S, nB, E); - let idealCurve = new Bezier(ideal.S, ideal.C1, ideal.C2, ideal.E); - // } - let molded = new Bezier(S,C1,C2,E); - - let falloff = 100 - - let d = Bezier.getUtils().dist(ideal.B, p); - let t2 = Math.min(falloff, d) / falloff; - let iC1 = { - x: (1-t2) * molded.points[1].x + t2 * idealCurve.points[1].x, - y: (1-t2) * molded.points[1].y + t2 * idealCurve.points[1].y - }; - let iC2 = { - x: (1-t2) * molded.points[2].x + t2 * idealCurve.points[2].x, - y: (1-t2) * molded.points[2].y + t2 * idealCurve.points[2].y - }; - let interpolatedCurve = new Bezier(molded.points[0], iC1, iC2, molded.points[3]); - - return interpolatedCurve -} function deriveControlPoints(S, A, E, e1, e2, t) { // Deriving the control points is effectively "doing what @@ -708,34 +651,6 @@ function deriveControlPoints(S, A, E, e1, e2, t) { return {v1, v2, C1, C2}; } -function getIdealisedCurve(p1, p2, p3) { - // This "reruns" the curve composition, but with a `t` value - // that is unrelated to the actual point B we picked, instead - // using whatever the appropriate `t` value would be if we were - // trying to fit a circular arc, as per earlier in the section. - const utils = Bezier.getUtils() - const c = utils.getccenter(p1, p2, p3), - d1 = utils.dist(p1, p2), - d2 = utils.dist(p3, p2), - t = d1 / (d1 + d2), - { A, B, C, S, E } = Bezier.getABC(3, p1, p2, p3, t), - angle = (Math.atan2(E.y-S.y, E.x-S.x) - Math.atan2(B.y-S.y, B.x-S.x) + utils.TAU) % utils.TAU, - bc = (angle < 0 || angle > utils.PI ? -1 : 1) * utils.dist(S, E)/3, - de1 = t * bc, - de2 = (1-t) * bc, - tangent = [ - { x: B.x - 10 * (B.y-c.y), y: B.y + 10 * (B.x-c.x) }, - { x: B.x + 10 * (B.y-c.y), y: B.y - 10 * (B.x-c.x) } - ], - tlength = utils.dist(tangent[0], tangent[1]), - dx = (tangent[1].x - tangent[0].x)/tlength, - dy = (tangent[1].y - tangent[0].y)/tlength, - e1 = { x: B.x + de1 * dx, y: B.y + de1 * dy}, - e2 = { x: B.x - de2 * dx, y: B.y - de2 * dy }, - {v1, v2, C1, C2} = deriveControlPoints(S, A, E, e1, e2, t); - - return {A,B,C,S,E,e1,e2,v1,v2,C1,C2}; -} function growBoundingBox(bboxa, bboxb) { bboxa.x.min = Math.min(bboxa.x.min, bboxb.x.min) @@ -752,31 +667,7 @@ function regionToBbox(region) { } function hitTest(candidate, object) { - return hitTestShape(candidate, object) let bbox = object.bbox() - if (candidate.x.min) { - // We're checking a bounding box - if (candidate.x.min < bbox.x.max + object.x && candidate.x.max > bbox.x.min + object.x && - candidate.y.min < bbox.y.max + object.y && candidate.y.max > bbox.y.min + object.y) { - return true; - } else { - return false; - } - } else { - // We're checking a point - if (candidate.x > bbox.x.min + object.x && - candidate.x < bbox.x.max + object.x && - candidate.y > bbox.y.min + object.y && - candidate.y < bbox.y.max + object.y) { - return true; - } else { - return false - } - } -} - -function hitTestShape(candidate, shape) { - let bbox = shape.bbox() if (candidate.x.min) { // We're checking a bounding box if (candidate.x.min < bbox.x.max && candidate.x.max > bbox.x.min && @@ -1921,7 +1812,7 @@ function stage() { } } for (let shape of context.activeObject.currentFrame.shapes) { - if (hitTestShape(regionToBbox(context.selectionRect), shape)) { + if (hitTest(regionToBbox(context.selectionRect), shape)) { context.shapeselection.push(shape) } } @@ -1987,7 +1878,7 @@ function toolbar() { let strokeColor = document.createElement("input") fillColor.className = "color-field" strokeColor.className = "color-field" - fillColor.value = "#ffffff" + fillColor.value = "#ff0000" strokeColor.value = "#000000" context.fillStyle = fillColor.value context.strokeStyle = strokeColor.value @@ -1998,7 +1889,7 @@ function toolbar() { focusInput: true, theme: 'default', swatches: context.swatches, - defaultColor: '#ffffff', + defaultColor: '#ff0000', onChange: (color) => { context.fillStyle = color; }