| author | |
| committer | |
| log | 0d005581890507e46939e22ee0d991887bb3c1b7 |
| tree | 3c307a53c1aa057a8ada6386c7cf63c0e5a9c8d9 |
| parent | 14ffea224c2042a06a245dcbcc1710ce2cfd9df7 |
| signature | Signed by SSH key SHA256:52mNGHRsVFBDED9IAX5pe+LRWUefqTbxEReunq21QvU |
The window follows OneNote's layout in light and dark themes that track
the system appearance: title bar with the app icon and notebook name, two
compact toolbar rows after the Home ribbon, section tabs that lean at 45°
and rise when opened, a section-coloured frame shading down the window,
and page tabs whose open tab joins the page. Sections keep their stored
hue at per-theme saturation and lightness; --ui-font sets the interface
typeface without bundling one.
In the dark theme the paper darkens and automatic text turns light,
resolved at paint time. The 71 background pictures of OneNote 2010's page
templates are recognised by hash and painted from authored SVG
recreations in light and dark, rasterized off the frame thread at the
current zoom.
Large pages stay within a 165 Hz frame: painting skips paragraphs and
highlights outside the view, accessibility updates only what changed and
moves the page by one transform on scroll, keystrokes validate and lay
out only what they touch, scrolling no longer saves, and hover no longer
animates boxes that don't show it.
draw gains filled, stroked and blurred SVG paths, gradients and tinted
icons, and draw::edit holds the text editing the page and ui share; the
renderer sits behind draw's default render feature.
Assisted-by: claude-opus-5.5
Assisted-by: claude-opus-5150 files changed, 11001 insertions(+), 4097 deletions(-)
Cargo.lock+156-2| ... | ... | @@ -400,8 +400,10 @@ dependencies = [ |
| 400 | 400 | "parley", |
| 401 | 401 | "png", |
| 402 | 402 | "pollster", |
| 403 | "resvg", | |
| 403 | 404 | "serde", |
| 404 | 405 | "serde_json", |
| 406 | "sha2", | |
| 405 | 407 | "skrifa", |
| 406 | 408 | "wgpu", |
| 407 | 409 | ] |
| ... | ... | @@ -642,6 +644,12 @@ version = "1.2.0" |
| 642 | 644 | source = "registry+https://github.com/rust-lang/crates.io-index" |
| 643 | 645 | checksum = "f27ae1dd37df86211c42e150270f82743308803d90a6f6e6651cd730d5e1732f" |
| 644 | 646 | |
| 647 | [[package]] | |
| 648 | name = "data-url" | |
| 649 | version = "0.3.2" | |
| 650 | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 651 | checksum = "be1e0bca6c3637f992fc1cc7cbc52a78c1ef6db076dbf1059c4323d6a2048376" | |
| 652 | ||
| 645 | 653 | [[package]] |
| 646 | 654 | name = "dbl" |
| 647 | 655 | version = "0.5.0" |
| ... | ... | @@ -757,6 +765,15 @@ version = "3.4.0" |
| 757 | 765 | source = "registry+https://github.com/rust-lang/crates.io-index" |
| 758 | 766 | checksum = "0b5343afd4a8365a643ac588dab4cf234a190c7f6c88c9f6dd6ffe00837661b7" |
| 759 | 767 | |
| 768 | [[package]] | |
| 769 | name = "euclid" | |
| 770 | version = "0.22.14" | |
| 771 | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 772 | checksum = "f1a05365e3b1c6d1650318537c7460c6923f1abdd272ad6842baa2b509957a06" | |
| 773 | dependencies = [ | |
| 774 | "num-traits", | |
| 775 | ] | |
| 776 | ||
| 760 | 777 | [[package]] |
| 761 | 778 | name = "fallible-iterator" |
| 762 | 779 | version = "0.3.0" |
| ... | ... | @@ -801,6 +818,12 @@ dependencies = [ |
| 801 | 818 | "zlib-rs", |
| 802 | 819 | ] |
| 803 | 820 | |
| 821 | [[package]] | |
| 822 | name = "float-cmp" | |
| 823 | version = "0.9.0" | |
| 824 | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 825 | checksum = "98de4bbd547a563b716d8dfa9aad1cb19bfab00f4fa09a6a4ed21dbcf44ce9c4" | |
| 826 | ||
| 804 | 827 | [[package]] |
| 805 | 828 | name = "foldhash" |
| 806 | 829 | version = "0.2.0" |
| ... | ... | @@ -1210,6 +1233,12 @@ dependencies = [ |
| 1210 | 1233 | "zune-jpeg", |
| 1211 | 1234 | ] |
| 1212 | 1235 | |
| 1236 | [[package]] | |
| 1237 | name = "imagesize" | |
| 1238 | version = "0.15.0" | |
| 1239 | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 1240 | checksum = "65b27460c2c92b037f3f94c538ed9a3342f3fdf923606781629ccb35f82d042a" | |
| 1241 | ||
| 1213 | 1242 | [[package]] |
| 1214 | 1243 | name = "indexmap" |
| 1215 | 1244 | version = "2.14.2" |
| ... | ... | @@ -1332,6 +1361,18 @@ version = "3.1.0" |
| 1332 | 1361 | source = "registry+https://github.com/rust-lang/crates.io-index" |
| 1333 | 1362 | checksum = "e2db585e1d738fc771bf08a151420d3ed193d9d895a36df7f6f8a9456b911ddc" |
| 1334 | 1363 | |
| 1364 | [[package]] | |
| 1365 | name = "kurbo" | |
| 1366 | version = "0.13.1" | |
| 1367 | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 1368 | checksum = "4b60dfc32f652b926df6192e55525b16d186c69d47876c3ead4da5cc9f8450e2" | |
| 1369 | dependencies = [ | |
| 1370 | "arrayvec", | |
| 1371 | "euclid", | |
| 1372 | "polycool", | |
| 1373 | "smallvec", | |
| 1374 | ] | |
| 1375 | ||
| 1335 | 1376 | [[package]] |
| 1336 | 1377 | name = "libc" |
| 1337 | 1378 | version = "0.2.189" |
| ... | ... | @@ -1958,6 +1999,7 @@ version = "0.1.0" |
| 1958 | 1999 | dependencies = [ |
| 1959 | 2000 | "aes", |
| 1960 | 2001 | "base64", |
| 2002 | "bumpalo", | |
| 1961 | 2003 | "cbc", |
| 1962 | 2004 | "getrandom 0.4.3", |
| 1963 | 2005 | "md5", |
| ... | ... | @@ -2081,6 +2123,12 @@ version = "2.3.2" |
| 2081 | 2123 | source = "registry+https://github.com/rust-lang/crates.io-index" |
| 2082 | 2124 | checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" |
| 2083 | 2125 | |
| 2126 | [[package]] | |
| 2127 | name = "pico-args" | |
| 2128 | version = "0.5.0" | |
| 2129 | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 2130 | checksum = "5be167a7af36ee22fe3115051bc51f6e6c7054c9348e28deb4f49bd6f705a315" | |
| 2131 | ||
| 2084 | 2132 | [[package]] |
| 2085 | 2133 | name = "pin-project" |
| 2086 | 2134 | version = "1.1.13" |
| ... | ... | @@ -2152,6 +2200,15 @@ version = "0.4.0" |
| 2152 | 2200 | source = "registry+https://github.com/rust-lang/crates.io-index" |
| 2153 | 2201 | checksum = "2f3a9f18d041e6d0e102a0a46750538147e5e8992d3b4873aaafee2520b00ce3" |
| 2154 | 2202 | |
| 2203 | [[package]] | |
| 2204 | name = "polycool" | |
| 2205 | version = "0.4.0" | |
| 2206 | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 2207 | checksum = "50596ddc09eb5ad5f75cacd40209568e66df71baf86e1499a0e99c4cff12a5a6" | |
| 2208 | dependencies = [ | |
| 2209 | "arrayvec", | |
| 2210 | ] | |
| 2211 | ||
| 2155 | 2212 | [[package]] |
| 2156 | 2213 | name = "polyval" |
| 2157 | 2214 | version = "0.7.3" |
| ... | ... | @@ -2329,6 +2386,30 @@ version = "1.1.0" |
| 2329 | 2386 | source = "registry+https://github.com/rust-lang/crates.io-index" |
| 2330 | 2387 | checksum = "19b30a45b0cd0bcca8037f3d0dc3421eaf95327a17cad11964fb8179b4fc4832" |
| 2331 | 2388 | |
| 2389 | [[package]] | |
| 2390 | name = "resvg" | |
| 2391 | version = "0.48.1" | |
| 2392 | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 2393 | checksum = "67e3803f97b999e80cbf7c6ecdd07a8102204d92e1633cf48783720c521196bd" | |
| 2394 | dependencies = [ | |
| 2395 | "bytemuck", | |
| 2396 | "log", | |
| 2397 | "pico-args", | |
| 2398 | "rgb", | |
| 2399 | "svgtypes", | |
| 2400 | "tiny-skia 0.12.0", | |
| 2401 | "usvg", | |
| 2402 | ] | |
| 2403 | ||
| 2404 | [[package]] | |
| 2405 | name = "rgb" | |
| 2406 | version = "0.8.53" | |
| 2407 | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 2408 | checksum = "47b34b781b31e5d73e9fbc8689c70551fd1ade9a19e3e28cfec8580a79290cc4" | |
| 2409 | dependencies = [ | |
| 2410 | "bytemuck", | |
| 2411 | ] | |
| 2412 | ||
| 2332 | 2413 | [[package]] |
| 2333 | 2414 | name = "roxmltree" |
| 2334 | 2415 | version = "0.21.1" |
| ... | ... | @@ -2441,7 +2522,7 @@ dependencies = [ |
| 2441 | 2522 | "log", |
| 2442 | 2523 | "memmap2", |
| 2443 | 2524 | "smithay-client-toolkit", |
| 2444 | "tiny-skia", | |
| 2525 | "tiny-skia 0.11.4", | |
| 2445 | 2526 | ] |
| 2446 | 2527 | |
| 2447 | 2528 | [[package]] |
| ... | ... | @@ -2543,6 +2624,21 @@ version = "0.1.5" |
| 2543 | 2624 | source = "registry+https://github.com/rust-lang/crates.io-index" |
| 2544 | 2625 | checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e" |
| 2545 | 2626 | |
| 2627 | [[package]] | |
| 2628 | name = "simplecss" | |
| 2629 | version = "0.2.2" | |
| 2630 | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 2631 | checksum = "7a9c6883ca9c3c7c90e888de77b7a5c849c779d25d74a1269b0218b14e8b136c" | |
| 2632 | dependencies = [ | |
| 2633 | "log", | |
| 2634 | ] | |
| 2635 | ||
| 2636 | [[package]] | |
| 2637 | name = "siphasher" | |
| 2638 | version = "1.0.4" | |
| 2639 | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 2640 | checksum = "33f4fe9184a62d842c9ef383018f3306d8ba224fd9d836f56d7288308847c256" | |
| 2641 | ||
| 2546 | 2642 | [[package]] |
| 2547 | 2643 | name = "skrifa" |
| 2548 | 2644 | version = "0.44.0" |
| ... | ... | @@ -2705,6 +2801,9 @@ name = "strict-num" |
| 2705 | 2801 | version = "0.1.1" |
| 2706 | 2802 | source = "registry+https://github.com/rust-lang/crates.io-index" |
| 2707 | 2803 | checksum = "6637bab7722d379c8b41ba849228d680cc12d0a45ba1fa2b48f2a30577a06731" |
| 2804 | dependencies = [ | |
| 2805 | "float-cmp", | |
| 2806 | ] | |
| 2708 | 2807 | |
| 2709 | 2808 | [[package]] |
| 2710 | 2809 | name = "subtle" |
| ... | ... | @@ -2712,6 +2811,16 @@ version = "2.6.1" |
| 2712 | 2811 | source = "registry+https://github.com/rust-lang/crates.io-index" |
| 2713 | 2812 | checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" |
| 2714 | 2813 | |
| 2814 | [[package]] | |
| 2815 | name = "svgtypes" | |
| 2816 | version = "0.16.1" | |
| 2817 | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 2818 | checksum = "695b5790b3131dafa99b3bbfd25a216edb3d216dad9ca208d4657bfb8f2abc3d" | |
| 2819 | dependencies = [ | |
| 2820 | "kurbo", | |
| 2821 | "siphasher", | |
| 2822 | ] | |
| 2823 | ||
| 2715 | 2824 | [[package]] |
| 2716 | 2825 | name = "swash" |
| 2717 | 2826 | version = "0.2.10" |
| ... | ... | @@ -2829,7 +2938,22 @@ dependencies = [ |
| 2829 | 2938 | "bytemuck", |
| 2830 | 2939 | "cfg-if", |
| 2831 | 2940 | "log", |
| 2832 | "tiny-skia-path", | |
| 2941 | "tiny-skia-path 0.11.4", | |
| 2942 | ] | |
| 2943 | ||
| 2944 | [[package]] | |
| 2945 | name = "tiny-skia" | |
| 2946 | version = "0.12.0" | |
| 2947 | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 2948 | checksum = "47ffee5eaaf5527f630fb0e356b90ebdec84d5d18d937c5e440350f88c5a91ea" | |
| 2949 | dependencies = [ | |
| 2950 | "arrayref", | |
| 2951 | "arrayvec", | |
| 2952 | "bytemuck", | |
| 2953 | "cfg-if", | |
| 2954 | "log", | |
| 2955 | "png", | |
| 2956 | "tiny-skia-path 0.12.0", | |
| 2833 | 2957 | ] |
| 2834 | 2958 | |
| 2835 | 2959 | [[package]] |
| ... | ... | @@ -2843,6 +2967,17 @@ dependencies = [ |
| 2843 | 2967 | "strict-num", |
| 2844 | 2968 | ] |
| 2845 | 2969 | |
| 2970 | [[package]] | |
| 2971 | name = "tiny-skia-path" | |
| 2972 | version = "0.12.0" | |
| 2973 | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 2974 | checksum = "edca365c3faccca67d06593c5980fa6c57687de727a03131735bb85f01fdeeb9" | |
| 2975 | dependencies = [ | |
| 2976 | "arrayref", | |
| 2977 | "bytemuck", | |
| 2978 | "strict-num", | |
| 2979 | ] | |
| 2980 | ||
| 2846 | 2981 | [[package]] |
| 2847 | 2982 | name = "tinystr" |
| 2848 | 2983 | version = "0.8.4" |
| ... | ... | @@ -2969,6 +3104,25 @@ dependencies = [ |
| 2969 | 3104 | "ctutils", |
| 2970 | 3105 | ] |
| 2971 | 3106 | |
| 3107 | [[package]] | |
| 3108 | name = "usvg" | |
| 3109 | version = "0.48.1" | |
| 3110 | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 3111 | checksum = "977d0a4abdef933f424a99fe09f95576e089b90aebc6f016a3bc813762493e91" | |
| 3112 | dependencies = [ | |
| 3113 | "data-url", | |
| 3114 | "imagesize", | |
| 3115 | "kurbo", | |
| 3116 | "log", | |
| 3117 | "pico-args", | |
| 3118 | "roxmltree", | |
| 3119 | "simplecss", | |
| 3120 | "siphasher", | |
| 3121 | "strict-num", | |
| 3122 | "svgtypes", | |
| 3123 | "tiny-skia-path 0.12.0", | |
| 3124 | ] | |
| 3125 | ||
| 2972 | 3126 | [[package]] |
| 2973 | 3127 | name = "utf8_iter" |
| 2974 | 3128 | version = "1.0.4" |
Cargo.toml+5| ... | ... | @@ -6,3 +6,8 @@ resolver = "3" |
| 6 | 6 | [workspace.dependencies] |
| 7 | 7 | parley = { git = "https://github.com/linebender/parley", rev = "4f67747d3690bb21d25cc70b674ab0d269595759", features = ["complex-scripts"] } |
| 8 | 8 | wgpu = "30.0.1" |
| 9 | ||
| 10 | # Shaping, rasterization and GPU submission stay optimised in debug builds, where | |
| 11 | # unoptimised dependencies alone drop frames. | |
| 12 | [profile.dev.package."*"] | |
| 13 | opt-level = 3 |
crates/canvas/Cargo.toml+4-2| ... | ... | @@ -5,19 +5,21 @@ edition = "2024" |
| 5 | 5 | publish = false |
| 6 | 6 | |
| 7 | 7 | [features] |
| 8 | gpu = ["dep:draw"] | |
| 8 | gpu = ["draw/render", "dep:resvg", "dep:sha2"] | |
| 9 | 9 | # How a page responds to input and presents itself to assistive technology. |
| 10 | 10 | interaction = ["gpu", "dep:accesskit"] |
| 11 | 11 | |
| 12 | 12 | [dependencies] |
| 13 | draw = { path = "../draw", default-features = false } | |
| 13 | 14 | onestore = { path = "../onestore" } |
| 14 | 15 | parley.workspace = true |
| 15 | 16 | serde = { version = "1.0.229", features = ["derive"] } |
| 16 | 17 | serde_json = "1.0.151" |
| 17 | 18 | skrifa = "0.44.0" |
| 18 | 19 | |
| 19 | draw = { path = "../draw", optional = true } | |
| 20 | 20 | accesskit = { version = "0.25.0", optional = true } |
| 21 | resvg = { version = "0.48.1", default-features = false, optional = true } | |
| 22 | sha2 = { version = "0.11", optional = true } | |
| 21 | 23 | |
| 22 | 24 | [[bin]] |
| 23 | 25 | name = "layout-probe" |
crates/canvas/assets/backgrounds/bamboo.svg created+22| ... | ... | @@ -0,0 +1,22 @@ |
| 1 | <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 604 784" preserveAspectRatio="none"> | |
| 2 | <g fill="none" stroke="#daf2e5" stroke-dasharray="80 2"> | |
| 3 | <path d="M30 740C25.979393757998373 470.0 21.95878751599675 260 21.95878751599675 200" stroke-width="6"/> | |
| 4 | <path d="M58 770C63.12488329402014 415.0 68.24976658804027 120 68.24976658804027 60" stroke-width="7"/> | |
| 5 | <path d="M115 740C109.680064331656 440.0 104.36012866331198 200 104.36012866331198 140" stroke-width="6"/> | |
| 6 | <path d="M167 720C161.9542010893562 375.0 156.90840217871235 90 156.90840217871235 30" stroke-width="7"/> | |
| 7 | <path d="M256 780C254.3030737821959 390.0 252.60614756439176 60 252.60614756439176 0" stroke-width="8"/> | |
| 8 | <path d="M276 760C273.6957191749255 480.0 271.39143834985094 260 271.39143834985094 200" stroke-width="6"/> | |
| 9 | <path d="M333 640C331.79618698083794 340.0 330.59237396167583 100 330.59237396167583 40" stroke-width="7"/> | |
| 10 | <path d="M397 780C393.9527668105711 420.0 390.9055336211422 120 390.9055336211422 60" stroke-width="7"/> | |
| 11 | <path d="M423 700C428.95145102942934 540.0 434.9029020588587 440 434.9029020588587 380" stroke-width="5"/> | |
| 12 | <path d="M506 760C500.9468708015793 420.0 495.89374160315856 140 495.89374160315856 80" stroke-width="7"/> | |
| 13 | <path d="M526 640C529.6070701467601 470.0 533.2141402935202 360 533.2141402935202 300" stroke-width="5"/> | |
| 14 | </g> | |
| 15 | <path d="M22.7 247.6C17 269.4 -4.3 292.6 -18.7 303.7C-11.7 287.2 4.9 260.5 22.7 247.6ZM22.7 247.6C12.2 262.5 -10.6 274.2 -24.6 278.5C-14.6 267.9 5.5 252.3 22.7 247.6ZM22.7 247.6C2.5 259.1 -30 260 -48.5 257.4C-31.3 250.6 0.4 243.7 22.7 247.6ZM22.7 247.6C-2 251.7 -35.2 240.7 -52.8 231.1C-33 230.7 1.6 235.4 22.7 247.6ZM22.7 247.6C3.3 241.9 -17 222.3 -26.6 209.1C-12 215.8 11.6 231.3 22.7 247.6ZM23.8 326.7C21.3 351.9 1.6 381.4 -12.5 396.1C-7.6 376.6 6.3 344 23.8 326.7ZM23.8 326.7C6.6 348.1 -28.7 363.4 -50.3 368.5C-34.3 353.6 -2.4 332.1 23.8 326.7ZM23.8 326.7C0.9 338.4 -35.1 337.9 -55.5 334C-36.1 327.4 -0.7 321.3 23.8 326.7ZM23.8 326.7C-2.4 324.2 -33.2 304 -48.7 289.4C-28.3 294.4 5.7 308.6 23.8 326.7ZM65.9 224.6C45.9 237 13 239.4 -5.9 237.4C11.3 229.9 43.1 221.5 65.9 224.6ZM65.9 224.6C45.7 229.3 17.7 222 2.5 215.1C18.9 213.6 47.8 215.6 65.9 224.6ZM65.9 224.6C46.8 220.2 26.1 202.4 16.2 190.3C30.6 195.9 54.3 209.5 65.9 224.6ZM65.9 224.6C53 213.7 44.2 191.8 41.4 178.6C50.3 188.6 62.9 208.4 65.9 224.6ZM65.9 224.6C53.9 205 51.9 173 53.9 154.6C61.1 171.4 69 202.4 65.9 224.6ZM63.1 414.2C80 400.8 109.8 395.3 127.1 395.1C112.3 403.7 84.2 414.6 63.1 414.2ZM63.1 414.2C82.3 410.9 108.1 419.5 121.8 426.9C106.4 427.2 79.5 423.6 63.1 414.2ZM63.1 414.2C82 417.6 103 434.3 113.3 445.8C98.9 440.9 75.2 428.5 63.1 414.2ZM63.1 414.2C76.8 424.4 86.9 446 90.5 459.3C80.9 449.7 67 430.3 63.1 414.2ZM63.1 414.2C75.5 435.3 76.9 469.7 74.3 489.2C67 471.1 59.3 437.8 63.1 414.2ZM60.2 617.2C61.6 641.1 47.7 671.5 36.8 687.4C38.4 668.5 46.4 636 60.2 617.2ZM60.2 617.2C49.2 637.4 22.3 655.2 5.3 662.6C16.3 648 39.4 625.5 60.2 617.2ZM60.2 617.2C40.1 625.8 9.5 623.2 -7.6 618.7C9.3 614.3 39.7 611.1 60.2 617.2ZM60.2 617.2C37.3 614.9 10.3 597.2 -3.1 584.4C14.7 588.8 44.3 601.3 60.2 617.2ZM106.6 268.9C124.5 259 153.1 258.4 169.4 260.9C154.2 266.7 126.2 272.5 106.6 268.9ZM106.6 268.9C125.8 265.4 151.7 273.5 165.7 280.6C150.2 281.3 123.2 278.1 106.6 268.9ZM106.6 268.9C122.6 273.2 139.6 288.8 147.7 299.4C135.6 294.3 116 282 106.6 268.9ZM106.6 268.9C122.7 280.4 135.1 305.1 139.7 320.3C128.4 309.4 111.6 287.5 106.6 268.9ZM106.6 268.9C116.1 287.9 115.5 317.6 112.1 334.4C106.8 318.3 102 289.1 106.6 268.9ZM108.9 396.1C107.7 417.3 92.2 442.8 80.9 455.7C84.3 439.1 94.8 411.3 108.9 396.1ZM108.9 396.1C95.6 416.7 65.6 433.6 47 440.2C59.8 425.5 86.1 403.4 108.9 396.1ZM108.9 396.1C93.8 405.1 69.1 406.4 55 404.7C67.9 399.3 91.9 393.5 108.9 396.1ZM108.9 396.1C91.3 393.6 71.1 379.1 61 368.8C74.6 372.8 97.2 383.3 108.9 396.1ZM112.5 598C123.9 579.6 150.2 564.3 166.6 558.2C155.5 571.4 132.5 591.3 112.5 598ZM112.5 598C132.3 586.5 164.3 585.2 182.6 587.7C165.7 594.5 134.5 601.6 112.5 598ZM112.5 598C129.6 592.5 154.3 596.8 167.9 601.8C153.8 604.1 128.7 604.4 112.5 598ZM112.5 598C130.8 601 151.4 616.8 161.5 627.8C147.4 623.3 124.3 611.6 112.5 598ZM112.5 598C129.3 608.2 143.5 631.9 149.1 646.7C137.1 636.8 118.8 616.1 112.5 598ZM157.4 62.1C151.6 81.6 131.7 102 118.4 111.5C125.1 96.8 140.9 73.2 157.4 62.1ZM157.4 62.1C138.8 75.9 106.7 80.9 88 80.5C104.2 71.7 134.8 60.9 157.4 62.1ZM157.4 62.1C132.3 66.3 98.6 55.1 80.6 45.4C100.8 44.9 135.9 49.7 157.4 62.1ZM157.4 62.1C139.2 54.5 121.8 33.1 114.1 19.3C127.5 27.4 148.5 45.1 157.4 62.1ZM160.7 292.4C167.8 273.4 189 254.4 202.8 245.7C195.1 259.9 177.9 282.5 160.7 292.4ZM160.7 292.4C176.5 273.5 208.5 260.4 227.8 256.2C213.2 269.3 184.3 288 160.7 292.4ZM160.7 292.4C177 284.7 202.2 285.9 216.4 289.1C202.7 293.2 177.8 296.8 160.7 292.4ZM160.7 292.4C179 290.5 202.8 300.2 215.3 308C200.7 307.4 175.6 302.3 160.7 292.4ZM160.7 292.4C177.5 294.2 197.2 307.3 207 316.7C194 313.4 172.3 304.1 160.7 292.4ZM160.7 292.4C180.5 300.6 199.5 323.6 208 338.5C193.4 329.8 170.5 310.8 160.7 292.4ZM163.8 499.9C155 478 158.4 445.2 163.6 427C168 445.2 170.7 478 163.8 499.9ZM163.8 499.9C163.9 474.8 180.4 443.8 192.9 427.9C189.9 447.6 179.4 481.1 163.8 499.9ZM163.8 499.9C171.6 480.9 193.7 462.4 208.1 454.2C199.8 468.3 181.5 490.4 163.8 499.9ZM163.8 499.9C178.8 485.5 206.9 477.5 223.5 475.8C210 485.4 184 498.4 163.8 499.9ZM163.8 499.9C188.5 492.5 224.1 499.5 243.5 507C223.1 510 187 509.7 163.8 499.9ZM330.8 98.4C346.7 87.7 373.4 84.9 388.8 85.9C375.1 92.5 349.4 100.2 330.8 98.4ZM330.8 98.4C347.9 96.3 370.4 104.9 382.3 112.1C368.6 111.7 344.9 107.4 330.8 98.4ZM330.8 98.4C349.9 101.7 371.4 118.3 382 129.8C367.3 125 343.1 112.7 330.8 98.4ZM330.8 98.4C347.4 108.9 361.1 132.7 366.5 147.6C354.6 137.4 336.8 116.6 330.8 98.4ZM330.8 98.4C338.6 114.7 337.6 140 334.5 154.2C330.3 140.5 326.6 115.5 330.8 98.4ZM331.5 274.5C324.7 291.9 304.9 309 292 316.7C299.3 303.8 315.6 283.3 331.5 274.5ZM331.5 274.5C315.9 287.2 288.2 292.7 271.9 293C285.7 284.8 311.9 274.3 331.5 274.5ZM331.5 274.5C312.2 277.9 286 269.5 272 262.1C287.6 261.6 314.9 265 331.5 274.5ZM331.5 274.5C315.6 268.6 299.7 250.8 292.4 239.1C304.3 245.6 323.2 260.1 331.5 274.5ZM332.4 498C335.4 520.2 324.6 549.6 315.6 565.2C315.8 547.4 320.9 516.6 332.4 498ZM332.4 498C330 517.4 314.4 539.8 303.3 550.9C307.4 536 318.6 511.1 332.4 498ZM332.4 498C320.3 518.3 292 535.3 274.2 542.2C286.1 527.7 310.7 505.7 332.4 498ZM332.4 498C317.8 514.8 288.7 526.1 271 529.5C284.5 517.9 311 501.6 332.4 498ZM332.4 498C311.8 506.5 280.8 503.6 263.5 498.8C280.7 494.5 311.7 491.6 332.4 498ZM332.4 498C308.6 498.5 278.7 483.4 263.3 471.9C282.1 474.3 314.2 483.6 332.4 498ZM434 403.4C436.1 428.6 422.1 461.3 410.9 478.3C412.2 458.2 419.9 423.6 434 403.4ZM434 403.4C428.2 421.9 408.9 441 396 450C402.7 436 418.2 413.7 434 403.4ZM434 403.4C420.4 422.3 391.2 436.8 373.2 442.1C386.1 428.8 412.1 409.1 434 403.4ZM434 403.4C413.1 415 379.7 415.6 360.6 412.7C378.4 405.9 411.1 399.1 434 403.4ZM434 403.4C407.2 408.1 370.9 396.4 351.5 386.1C373.2 385.5 410.9 390.3 434 403.4ZM434 403.4C407.3 399.6 376.5 377.4 361.4 361.8C382 367.9 416.2 383.9 434 403.4ZM498.2 231.9C491.7 215.4 494.4 190.8 498.4 177.1C501.6 190.8 503.5 215.5 498.2 231.9ZM498.2 231.9C496.6 215.7 505.4 194.6 512.4 183.6C511.7 196.5 507.1 218.7 498.2 231.9ZM498.2 231.9C501.4 217.2 515.1 201.1 524.4 193.4C520.1 204.6 509.7 222.8 498.2 231.9ZM498.2 231.9C508.9 215.1 533.2 201.4 548.4 196.1C537.9 208.1 516.6 226 498.2 231.9ZM498.2 231.9C514.8 220.4 542.8 217.1 559 218.1C544.6 225.2 517.7 233.6 498.2 231.9ZM498.2 231.9C512.1 226.8 532.8 229.8 544.2 233.5C532.6 235.9 511.8 236.8 498.2 231.9ZM501.1 431.7C517.5 420.9 545 418.4 560.7 419.6C546.5 426.2 520.2 433.8 501.1 431.7ZM501.1 431.7C520 428.3 545.7 436.5 559.4 443.6C544.1 444.1 517.5 440.9 501.1 431.7ZM501.1 431.7C520.3 434.4 542.3 450.3 553.2 461.6C538.4 457.2 513.9 445.7 501.1 431.7ZM501.1 431.7C519.8 442.7 535.7 468.5 542.1 484.7C528.7 473.9 508.3 451.5 501.1 431.7ZM501.1 431.7C515.1 445.3 523.5 471.3 525.7 486.8C516.3 474.5 503.2 450.6 501.1 431.7ZM501.1 431.7C513.8 452.8 515.8 487.2 513.4 506.9C505.9 488.8 497.6 455.4 501.1 431.7ZM530.3 436.3C513.9 455.6 480.9 468.7 460.9 472.9C476.1 459.6 506 440.6 530.3 436.3ZM530.3 436.3C512.7 447.7 483.5 450.3 466.7 448.9C481.8 441.9 510 434 530.3 436.3ZM530.3 436.3C507.6 443.6 474.6 437.6 456.6 431C475.3 427.9 508.7 427.6 530.3 436.3ZM530.3 436.3C509.5 434.5 484.9 418.7 472.6 407.3C488.8 411.1 515.8 422.1 530.3 436.3ZM530.3 436.3C508.1 424.3 488.4 394.8 480.2 376.1C496.3 388.1 521.1 413.4 530.3 436.3ZM530.3 436.3C510.3 418.7 497.1 383.7 493 362.6C506.8 378.8 526.2 410.6 530.3 436.3ZM527.6 565.6C520.4 545.2 524.9 515.2 530.4 498.7C533.7 515.6 534.9 545.8 527.6 565.6ZM527.6 565.6C531.6 547.4 548.6 527.5 560.1 517.9C554.9 531.8 541.9 554.4 527.6 565.6ZM527.6 565.6C538.6 544.9 565.8 526.5 583.1 518.8C572 533.8 548.7 556.9 527.6 565.6ZM527.6 565.6C549.8 552.5 585.9 550.7 606.6 553.3C587.5 561.1 552.5 569.5 527.6 565.6ZM527.6 565.6C551.5 560.5 584.6 569.8 602.4 578.3C582.9 579.6 548.8 576.6 527.6 565.6Z" fill="#ecf8f1"/> | |
| 16 | <path d="M25.4 430.8C35.9 413.4 60.3 398.8 75.6 393C65.3 405.4 44.1 424.3 25.4 430.8ZM25.4 430.8C43.3 422.5 70.9 424 86.4 427.6C71.3 432.1 44 435.7 25.4 430.8ZM25.4 430.8C46.3 433.2 70.5 449.8 82.6 461.7C66.4 457.4 39.6 445.5 25.4 430.8ZM25.4 430.8C38.9 445 46.2 471.3 47.8 487C38.8 474.3 26.7 449.8 25.4 430.8ZM27.4 565.5C19.4 583.2 -2 599.8 -15.8 607.1C-7.5 594.1 10.5 573.9 27.4 565.5ZM27.4 565.5C12.6 580.3 -15.4 588.8 -32.1 590.9C-18.7 581 7.1 567.4 27.4 565.5ZM27.4 565.5C4.6 573.5 -28.9 568.5 -47.3 562.4C-28.4 558.7 5.3 557.4 27.4 565.5ZM27.4 565.5C8.3 563.2 -13.7 547.9 -24.7 537C-10 541 14.5 552 27.4 565.5ZM27.4 565.5C11.2 556 -2.6 533.6 -8.2 519.6C3.4 528.9 21.1 548.3 27.4 565.5ZM29 673.4C34.1 691 28.9 716.2 23.5 730C21.5 715.5 21.9 689.8 29 673.4ZM29 673.4C24 693.8 4.5 715.7 -8.8 726.2C-2.5 710.7 12.6 685.6 29 673.4ZM29 673.4C16.1 686 -8.2 693.2 -22.6 694.8C-11 686.4 11.5 674.9 29 673.4ZM29 673.4C10.6 679.4 -16.1 674.8 -30.8 669.5C-15.6 666.9 11.4 666.5 29 673.4ZM67.8 93.9C78.3 78.4 101.5 66.2 115.8 61.6C105.7 72.5 85.3 88.8 67.8 93.9ZM67.8 93.9C83.5 85 108.9 84.4 123.3 86.5C109.9 91.7 85.1 97 67.8 93.9ZM67.8 93.9C87.1 95.9 109.8 111.1 121.1 122C106.1 118.1 81.1 107.4 67.8 93.9ZM67.8 93.9C80.1 104.6 88.3 125.9 90.8 138.8C82.4 129 70.4 109.6 67.8 93.9ZM64.5 316.3C57.5 297.8 61 270.1 65.7 254.9C69.1 270.3 70.8 298 64.5 316.3ZM64.5 316.3C66.7 291.5 85.7 262.2 99.3 247.5C94.8 266.8 81.6 299 64.5 316.3ZM64.5 316.3C76 300.2 100.8 287.7 116.1 283.2C105.2 294.6 83.2 311.3 64.5 316.3ZM64.5 316.3C90.1 309.2 126.4 317 146.1 325.1C125.2 327.8 88.2 326.8 64.5 316.3ZM61.9 500.9C64.5 479.6 81.7 455 93.8 442.7C89.3 459.2 77.1 486.5 61.9 500.9ZM61.9 500.9C69.7 485.9 89.2 472.5 101.6 466.8C93.7 477.7 77.1 494.5 61.9 500.9ZM61.9 500.9C73.6 486.2 97.7 475.6 112.5 472.1C101.6 482.3 79.8 497.1 61.9 500.9ZM61.9 500.9C79.4 490 108.2 487.9 124.7 489.6C109.7 496.2 81.8 503.6 61.9 500.9ZM61.9 500.9C78.2 499.3 99.5 507.9 110.7 514.9C97.7 514.4 75.2 509.8 61.9 500.9ZM61.9 500.9C79.8 502.6 100.9 516.3 111.4 526.2C97.5 522.8 74.3 513.3 61.9 500.9ZM105.4 197.2C95.2 209.6 74.5 218.4 61.8 221.3C71.3 212.7 90 200.2 105.4 197.2ZM105.4 197.2C91.4 205.9 68.3 207.6 55.1 206.2C67.1 200.9 89.4 195.1 105.4 197.2ZM105.4 197.2C89.5 203.2 65.9 200.3 52.8 196.3C66 193.4 89.7 191.9 105.4 197.2ZM105.4 197.2C84.9 196.7 60 182.7 47.2 172.3C63.2 175 90.3 184.1 105.4 197.2ZM105.4 197.2C88.7 191 72.1 172.5 64.5 160.4C76.9 167.1 96.6 182.2 105.4 197.2ZM105.4 197.2C90 183.2 80.1 155.9 77.2 139.4C87.7 152.2 102.5 177.1 105.4 197.2ZM110.5 486C107.4 506.7 90.1 530.4 77.9 542C82.7 526.1 95.3 499.7 110.5 486ZM110.5 486C97.5 508.4 66.7 527.7 47.4 535.5C60.2 519.3 86.8 494.8 110.5 486ZM110.5 486C94.7 495.5 68.8 496.9 54 495.2C67.6 489.5 92.7 483.3 110.5 486ZM110.5 486C89.7 490.8 60.9 483.2 45.3 476C62.2 474.6 91.9 476.7 110.5 486ZM110.5 486C91.3 480.5 71.1 461.2 61.5 448.2C76.1 454.7 99.4 470 110.5 486ZM159.2 188.8C151.4 168.7 154.9 138.6 159.9 122C163.8 138.7 165.9 168.8 159.2 188.8ZM159.2 188.8C158.9 170.7 170.2 148.1 178.9 136.5C177.2 150.7 170.2 175 159.2 188.8ZM159.2 188.8C164.9 172.8 182.5 156.8 194 149.4C187.7 161.4 173.4 180.3 159.2 188.8ZM159.2 188.8C169 174 190.7 162.1 204.2 157.5C194.8 168 175.7 183.7 159.2 188.8ZM159.2 188.8C172.4 179.1 195 175.7 208.1 176C196.7 182.1 175.1 189.7 159.2 188.8ZM159.2 188.8C174.3 186.4 194.5 193.2 205.3 199.1C193.2 199.3 172.1 196.3 159.2 188.8ZM162.3 401.5C152.7 416 131.2 427.6 117.9 432.1C127.2 421.7 146.1 406.4 162.3 401.5ZM162.3 401.5C143.2 412.3 112.5 413.2 94.9 410.7C111.2 404.3 141.2 397.8 162.3 401.5ZM162.3 401.5C140.1 403.2 111.5 390.7 96.6 380.8C114.3 382 144.6 389 162.3 401.5ZM162.3 401.5C142.5 398.5 119.9 381.8 108.8 370.2C124 374.8 149.3 386.9 162.3 401.5ZM162.3 401.5C143.6 389.3 128.3 361.9 122.4 344.8C135.8 356.6 155.8 380.7 162.3 401.5ZM164.9 577.8C152.9 589.4 130.5 595.9 117.1 597.3C127.9 589.6 148.7 579.1 164.9 577.8ZM164.9 577.8C147.7 580.7 124.4 573 112 566.3C125.9 566 150.2 569.3 164.9 577.8ZM164.9 577.8C148.2 570.2 132.5 549.7 125.7 536.6C138 544.5 157.1 561.7 164.9 577.8ZM164.9 577.8C153.2 565.2 147 541.9 145.9 528.2C153.6 539.4 164 561.1 164.9 577.8ZM252.7 30C252 12.5 262.6 -9.6 270.8 -21.1C269.4 -7.2 263.1 16.4 252.7 30ZM252.7 30C259.6 9.9 281.3 -10.6 295.7 -20C287.9 -4.9 270.4 19.1 252.7 30ZM252.7 30C265.7 14 292.3 2.8 308.4 -1C296.4 10.1 272.4 26.1 252.7 30ZM252.7 30C266.4 21.6 288.9 20.2 301.7 21.7C290 26.7 268.2 32.2 252.7 30ZM252.7 30C271.4 24.7 298.1 30.5 312.6 36.3C297.2 38.4 270.1 37.7 252.7 30ZM252.7 30C271.7 33.1 293.1 49.3 303.6 60.6C289 56 265.1 44.1 252.7 30ZM253.2 125.8C239.9 149 208.2 169 188.2 177.2C201.4 160.5 228.7 135 253.2 125.8ZM253.2 125.8C235.2 138.8 204.4 143.2 186.5 142.6C202.1 134.4 231.5 124.4 253.2 125.8ZM253.2 125.8C227.4 133.8 190.2 126.8 169.8 119.2C191 115.8 228.8 115.8 253.2 125.8ZM253.2 125.8C234.3 125.8 210.8 113.4 198.8 104.1C213.7 106.3 238.9 114 253.2 125.8ZM253.2 125.8C229.7 120.1 204.3 97.6 192.2 82.4C210 89.6 239 106.9 253.2 125.8ZM253.2 125.8C232.5 110.9 216.6 79 210.8 59.4C225.4 73.4 246.8 101.8 253.2 125.8ZM253.7 255.9C256.7 278.1 245.9 307.6 236.8 323.3C237 305.4 242.2 274.5 253.7 255.9ZM253.7 255.9C249.5 274.9 231.7 295.7 219.6 305.7C225.2 291.2 238.7 267.6 253.7 255.9ZM253.7 255.9C246.3 274.3 225 292.2 211.2 300.2C219.2 286.6 236.7 265.1 253.7 255.9ZM253.7 255.9C239.7 271.4 212.2 281.4 195.6 284.3C208.5 273.7 233.6 258.8 253.7 255.9ZM253.7 255.9C235.5 262.9 208.4 259.8 193.3 255.3C208.5 251.8 235.7 249.9 253.7 255.9ZM253.7 255.9C237.1 258.9 214.5 251.8 202.4 245.6C215.9 245.1 239.3 247.9 253.7 255.9ZM254.1 351.9C236.9 363.8 207.8 367.3 191 366.3C205.9 358.9 233.8 350.2 254.1 351.9ZM254.1 351.9C233.8 355.7 206.1 347.2 191.3 339.5C207.7 338.9 236.5 342.2 254.1 351.9ZM254.1 351.9C238.9 349.7 221.4 337 212.8 328C224.5 331.5 244 340.8 254.1 351.9ZM254.1 351.9C238.1 344.8 223 325.5 216.4 313.1C228.1 320.6 246.5 336.7 254.1 351.9ZM254.1 351.9C240.8 337.2 233.9 310.3 232.7 294.3C241.5 307.4 253.2 332.6 254.1 351.9ZM254.1 351.9C247 334.3 249.8 307.7 254 293C257.6 307.7 259.8 334.2 254.1 351.9ZM254.6 450.1C257.7 468 249.9 492.1 243 505C242.6 490.6 245.8 465.5 254.6 450.1ZM254.6 450.1C251.8 470.6 234.9 494.1 223 505.8C227.5 490 239.8 463.8 254.6 450.1ZM254.6 450.1C241.9 466.3 215.6 478 199.6 482C211.4 470.7 235 454.4 254.6 450.1ZM254.6 450.1C236.6 459 208.6 458.2 192.8 455C207.9 450.1 235.6 445.7 254.6 450.1ZM254.6 450.1C237 450 215.4 438.3 204.3 429.5C218.1 431.6 241.5 439.1 254.6 450.1ZM255 560.7C267.7 545.9 293.1 535.9 308.5 532.7C296.8 542.9 273.8 557.5 255 560.7ZM255 560.7C271.9 555 296.5 559 310 563.7C296.1 566.2 271.3 566.9 255 560.7ZM255 560.7C272 559.5 293.9 569.2 305.3 576.8C291.7 575.8 268.6 570.4 255 560.7ZM255 560.7C270.7 569.4 284.5 590.4 290.1 603.6C278.8 595 261.5 577 255 560.7ZM255 560.7C266.5 573.6 272.2 596.9 273.1 610.7C265.6 599.3 255.7 577.5 255 560.7ZM255.5 675.1C238.7 688.2 209.3 693.2 192.2 693.2C206.9 684.9 234.8 674.5 255.5 675.1ZM255.5 675.1C233.2 674.9 205.6 660.1 191.5 648.9C209.1 651.6 238.9 661.1 255.5 675.1ZM255.5 675.1C235.8 665.9 217.4 641.6 209.4 626C223.9 635.5 246.4 656 255.5 675.1ZM255.5 675.1C248.7 660.5 249.9 637.9 252.7 625.2C256.4 637.5 259.5 659.9 255.5 675.1ZM271.8 245.3C277.8 229.9 295.5 214.8 306.9 208C300.4 219.4 285.9 237.5 271.8 245.3ZM271.8 245.3C285.7 230.8 312.5 222 328.6 219.6C315.9 229.5 291.3 243.1 271.8 245.3ZM271.8 245.3C289 235.2 316.9 233.9 332.8 236C318.1 242 291 248.4 271.8 245.3ZM271.8 245.3C291.2 241.9 317.4 250.3 331.5 257.7C315.8 258.2 288.5 254.8 271.8 245.3ZM271.8 245.3C288 248 306.4 262 315.5 271.7C303 267.7 282.3 257.4 271.8 245.3ZM271.8 245.3C285.8 252 298.7 269.5 304.3 280.7C294.1 273.8 278.1 259.1 271.8 245.3ZM272.6 352.9C256.2 369.2 225.2 378.6 206.7 380.8C221.5 369.9 250.2 354.9 272.6 352.9ZM272.6 352.9C252.9 359.6 224 355.1 208.1 349.6C224.4 346.5 253.6 345.7 272.6 352.9ZM272.6 352.9C247.6 353.3 216.3 337.5 200.1 325.4C219.9 327.9 253.5 337.6 272.6 352.9ZM272.6 352.9C251.1 346.5 228.7 324.6 218.1 309.9C234.3 317.4 260.4 334.7 272.6 352.9ZM272.6 352.9C256.8 340.2 245.5 314.2 241.7 298.4C252.7 310.1 268.6 333.5 272.6 352.9ZM272.6 352.9C258.9 333.9 254.4 301.5 255 282.6C263.6 299.1 274.1 330.1 272.6 352.9ZM273.5 455.4C270.7 439.4 277.7 417.6 283.8 406C284.2 419 281.3 441.6 273.5 455.4ZM273.5 455.4C278.4 439 295.2 421.9 306.4 413.9C300.7 426.2 287.4 446.1 273.5 455.4ZM273.5 455.4C284.6 446.9 303.9 443.7 315.2 443.8C305.5 449.2 287.1 455.9 273.5 455.4ZM273.5 455.4C287.9 453.7 306.8 461 316.8 467C305.3 466.7 285.4 463.1 273.5 455.4ZM274.4 567C282.3 552.8 301.3 540.5 313.3 535.4C305.4 545.6 289.1 561.2 274.4 567ZM274.4 567C289.4 558.4 313.5 557.6 327.3 559.6C314.5 564.6 291 569.8 274.4 567ZM274.4 567C291.6 569 311.7 582.7 321.6 592.5C308.3 589 286.1 579.2 274.4 567ZM274.4 567C290.5 580.2 301.8 607.1 305.5 623.4C294.3 611.2 278.3 586.9 274.4 567ZM275.1 644.9C270.6 622.5 279.9 591.9 288.2 575.4C289 593.6 285.7 625.3 275.1 644.9ZM275.1 644.9C282.8 624.6 305.6 604.6 320.5 595.5C312.1 610.6 293.4 634.4 275.1 644.9ZM275.1 644.9C297.2 631.6 333.4 629.6 354.1 632.1C335.1 640.1 300 648.7 275.1 644.9ZM275.1 644.9C300 644.7 330.9 660.8 346.8 673C327.2 670.3 293.9 660.2 275.1 644.9ZM331.2 186.2C344.5 166.6 374 151 392.2 145.1C379.4 159 353.4 179.7 331.2 186.2ZM331.2 186.2C347 174.7 374.2 170.8 390 171.3C376.2 178.5 350.3 187.4 331.2 186.2ZM331.2 186.2C350.7 182.1 377.5 189.8 391.9 196.9C376.1 197.8 348.4 195.2 331.2 186.2ZM331.2 186.2C353.1 194.3 374.9 218.8 384.9 234.8C368.5 225.8 342.6 205.9 331.2 186.2ZM331.2 186.2C347.1 198 359.2 222.9 363.5 238.3C352.3 227.2 335.9 204.9 331.2 186.2ZM332 394.8C341.2 381.5 361.2 371 373.6 367.1C364.9 376.5 347.1 390.5 332 394.8ZM332 394.8C352.5 385.8 383.7 388.1 401.2 392.5C384 397.2 353 400.7 332 394.8ZM332 394.8C352.6 398.2 375.8 415.9 387.2 428.3C371.4 423.2 345.4 410.1 332 394.8ZM332 394.8C347.9 408.1 358.9 434.9 362.5 451.3C351.5 439 335.7 414.6 332 394.8ZM391.4 119.9C388.3 142.1 369.9 167.7 357 180.3C362 163.1 375.3 134.7 391.4 119.9ZM391.4 119.9C376.5 132.3 349.8 137.9 334.2 138.5C347.4 130.4 372.5 120 391.4 119.9ZM391.4 119.9C367.6 122.9 336.1 111.1 319.4 101.3C338.5 101.6 371.6 107.4 391.4 119.9ZM391.4 119.9C373.4 114.9 354.3 97 345.3 84.9C358.9 90.9 380.9 105 391.4 119.9ZM392 186C384.9 203.6 364.6 220.8 351.4 228.5C359 215.4 375.7 194.8 392 186ZM392 186C376.3 204.9 344.6 218.1 325.3 222.4C339.8 209.3 368.5 190.5 392 186ZM392 186C369.4 196.1 334.8 193.8 315.4 189C334.4 183.7 368.7 179.5 392 186ZM392 186C370.6 184.4 345.1 168.3 332.3 156.7C349 160.5 376.9 171.5 392 186ZM392 186C370.3 178.3 348.4 154.6 338.4 139C354.6 147.6 380.4 166.7 392 186ZM393.1 320.2C381.8 329.9 361.5 334.6 349.5 335.2C359.5 328.8 378.6 320.5 393.1 320.2ZM393.1 320.2C375.2 322.1 351.9 312.7 339.5 305.1C353.8 305.7 378.5 310.5 393.1 320.2ZM393.1 320.2C379.4 312.6 367.5 294.2 362.5 282.6C372.4 290.2 387.5 306 393.1 320.2ZM393.1 320.2C383 303 381.7 275.2 383.7 259.3C389.7 274 396.1 301 393.1 320.2ZM393.9 417.2C375.6 430.7 344 435.3 325.7 434.9C341.7 426.3 371.8 415.9 393.9 417.2ZM393.9 417.2C376.3 423.7 350.3 420.2 335.9 415.6C350.5 412.5 376.7 411.1 393.9 417.2ZM393.9 417.2C373 414.7 348.7 398 336.7 386.2C352.8 390.5 379.7 402.4 393.9 417.2ZM393.9 417.2C378.9 407.9 366.3 386.6 361.3 373.3C372.1 382.3 388.4 400.9 393.9 417.2ZM393.9 417.2C383 404.3 378 381.3 377.4 367.8C384.5 379.1 393.7 400.8 393.9 417.2ZM394.9 531.8C401.7 515.3 421 499.3 433.5 492.2C426.2 504.4 410.3 523.6 394.9 531.8ZM394.9 531.8C408 516.4 434.3 506 450.2 502.8C438.1 513.3 414.3 528.4 394.9 531.8ZM394.9 531.8C413.4 525.8 440.2 530.5 454.9 535.9C439.6 538.5 412.5 538.8 394.9 531.8ZM394.9 531.8C415.1 531.5 440.2 544.4 453.2 554.2C437.3 552.1 410.2 544.1 394.9 531.8ZM394.9 531.8C414.4 543 431.1 569.7 437.9 586.5C423.9 575.4 402.6 552.3 394.9 531.8ZM395.6 612.9C407.4 596.7 432.6 584.3 448.1 579.8C436.9 591.3 414.5 608 395.6 612.9ZM395.6 612.9C413.5 604.7 441.1 606.2 456.6 609.7C441.5 614.2 414.2 617.8 395.6 612.9ZM395.6 612.9C411.9 612.2 432.5 622 443.2 629.6C430.3 628.3 408.3 622.5 395.6 612.9ZM395.6 612.9C411.6 617.6 428.4 634 436.3 644.9C424.2 639.3 404.7 626.4 395.6 612.9ZM395.6 612.9C411.1 626.6 421.3 653.6 424.4 669.9C413.7 657.4 398.8 632.8 395.6 612.9ZM429.3 529.7C413.2 538.3 387.4 538.4 372.9 535.9C386.6 531 411.8 526.1 429.3 529.7ZM429.3 529.7C409.7 535.3 381.6 529.4 366.3 523.3C382.4 521.1 411 521.7 429.3 529.7ZM429.3 529.7C412.7 525.6 394.8 509.6 386.3 498.8C398.9 504 419.4 516.3 429.3 529.7ZM429.3 529.7C414.7 521.8 401.7 502.6 396.3 490.4C406.9 498.2 423.2 514.7 429.3 529.7ZM429.3 529.7C418 517.3 412 494.5 410.9 480.9C418.4 492 428.5 513.3 429.3 529.7ZM429.3 529.7C421.1 512.4 422.2 485.6 425.5 470.5C430 485.1 433.9 511.5 429.3 529.7ZM426.1 617.5C420.9 601.9 424.8 579.3 429.2 566.8C431.4 579.7 431.9 602.6 426.1 617.5ZM426.1 617.5C429.4 601.2 444.2 583.2 454.4 574.4C449.9 586.9 438.7 607.3 426.1 617.5ZM426.1 617.5C437 607.1 457.3 601.4 469.4 600.3C459.6 607.2 440.7 616.5 426.1 617.5ZM426.1 617.5C440.4 614.5 460.1 620.1 470.7 625.3C459.1 626 438.7 624.1 426.1 617.5ZM496.3 108.1C511.2 91.3 540.6 80.2 558.4 76.9C544.8 88.4 517.9 104.7 496.3 108.1ZM496.3 108.1C517.7 96.6 551.7 96.5 571 99.8C552.8 106.3 519.5 112.8 496.3 108.1ZM496.3 108.1C519.2 102.1 551.5 109.7 569 117.3C550.3 119.3 517.2 117.8 496.3 108.1ZM496.3 108.1C519 111.9 544.5 131.5 557.1 145.1C539.7 139.5 511 125 496.3 108.1ZM496.3 108.1C517.2 119.5 535.6 147.3 543.3 164.9C528.1 153.5 504.9 129.7 496.3 108.1ZM496.3 108.1C509.7 123.9 515.9 152.1 516.6 168.8C507.9 154.8 496.6 128.2 496.3 108.1ZM499.9 349.7C484.9 364.9 456.5 373.8 439.5 376C453.1 365.8 479.3 351.8 499.9 349.7ZM499.9 349.7C480.7 360 450.3 360.2 433 357.2C449.3 351.4 479.1 345.6 499.9 349.7ZM499.9 349.7C477.1 351.7 447.6 339.2 432.1 329.2C450.3 330.3 481.5 337.1 499.9 349.7ZM499.9 349.7C478.9 347.2 454.6 330.3 442.6 318.4C458.8 322.8 485.7 334.8 499.9 349.7ZM499.9 349.7C483.5 342.7 467.8 323.2 460.9 310.6C473 318 491.9 334.2 499.9 349.7ZM499.9 349.7C484.2 331.9 476.4 299.4 475.3 280.3C485.6 296.2 499.2 326.5 499.9 349.7ZM502.9 554.5C496 536.6 499 509.9 503.5 495.1C506.9 510 508.8 536.8 502.9 554.5ZM502.9 554.5C502.6 539.6 511.9 520.9 519.1 511.2C517.7 523 511.9 543.1 502.9 554.5ZM502.9 554.5C510.1 535.8 531.3 517.4 545.2 509C537.3 522.9 520 544.9 502.9 554.5ZM502.9 554.5C514.3 537.9 539.3 524.8 554.7 519.8C543.8 531.6 521.8 549.1 502.9 554.5ZM502.9 554.5C515.6 545.6 537.1 542.8 549.6 543.4C538.6 549 518 555.7 502.9 554.5ZM502.9 554.5C521.8 548.2 549.3 552.6 564.4 557.9C548.8 560.7 521.1 561.4 502.9 554.5ZM504.3 643.5C498 627.9 500.4 604.5 504.1 591.5C507.3 604.5 509.2 627.9 504.3 643.5ZM504.3 643.5C504 625.1 515.7 602.1 524.7 590.3C522.8 604.8 515.5 629.5 504.3 643.5ZM504.3 643.5C507.8 623.3 525.4 600.7 537.7 589.6C532.6 605.1 519.5 630.6 504.3 643.5ZM504.3 643.5C516.8 625.9 543.9 612.1 560.6 607.1C548.7 619.6 524.7 638 504.3 643.5ZM504.3 643.5C521.1 628.2 552 620.3 570.3 618.9C555.3 629 526.5 642.5 504.3 643.5ZM504.3 643.5C525 634.3 556.7 636.5 574.4 640.9C557 645.8 525.6 649.5 504.3 643.5ZM532.5 333.5C538.6 358.4 529.8 393.2 521.4 412C519.4 391.7 521.6 356 532.5 333.5ZM532.5 333.5C526.1 356.5 503 380.9 487.5 392.4C495.2 374.9 513.4 346.8 532.5 333.5ZM532.5 333.5C522.5 354.6 496.2 374.3 479.3 382.7C489.7 367.2 511.8 343.2 532.5 333.5ZM532.5 333.5C514.3 352.5 479.4 364.1 458.5 367.2C474.9 354.4 507 336.5 532.5 333.5ZM532.5 333.5C511.8 342.2 480.4 339.5 462.9 334.8C480.2 330.3 511.5 327.2 532.5 333.5ZM532.5 333.5C509.5 335.1 479.9 322 464.5 311.6C482.8 313 514.2 320.4 532.5 333.5Z" fill="#d9f2e5"/> | |
| 17 | <g fill="none" stroke="#00a652" stroke-linecap="round"> | |
| 18 | <path d="M0 15C60 12 180 10 262 6" stroke-width="3.5"/> | |
| 19 | <path d="M22 0C18 80 14 180 8 280" stroke-width="4"/> | |
| 20 | </g> | |
| 21 | <path d="M60 16C68.8 26.1 73.1 44.4 73.7 55.2C68 46.2 60.4 29.1 60 16ZM60 16C67.7 26.5 70.3 44.6 69.9 55.1C65.1 45.9 59.2 28.7 60 16ZM60 16C63.3 27.9 59.5 44.9 55.7 54.1C54.5 44.3 55.1 27 60 16ZM60 16C61.2 32.6 51.9 54 44.4 65.2C45.4 52 50.6 29.3 60 16ZM60 16C58.5 31 46.9 48.5 38.6 57.3C41.4 45.7 49.6 26.3 60 16ZM60 16C53.3 31.5 34.8 46.2 22.8 52.7C29.9 41.3 45.3 23.4 60 16ZM140 18C154.5 21.4 170.2 35.2 177.7 44.5C166.7 40.2 148.8 29.6 140 18ZM140 18C154.2 26.1 166.5 45.5 171.5 57.7C161.3 49.6 145.7 32.9 140 18ZM140 18C154.1 26.9 165.7 47.1 170.3 59.8C160.2 51.1 145.1 33.4 140 18ZM140 18C152 30.9 158.4 54.7 159.7 68.8C151.7 57.3 141 35.1 140 18ZM140 18C146.2 31.6 145 52.4 142.2 64.1C138.9 52.7 136.2 32 140 18ZM140 18C144.5 30.6 141.7 49.2 138.3 59.4C136.3 49 135.5 30.3 140 18ZM140 18C142.2 31.1 136.3 48.8 131.2 58.2C131 47.6 133.5 29.2 140 18ZM215 12C224.3 22.8 228.8 42.3 229.4 53.8C223.3 44.2 215.3 25.9 215 12ZM215 12C221.4 24 221.5 43.1 219.7 54C216 43.8 212.4 25 215 12ZM215 12C216.9 26.2 210 45 204.1 55C204.3 43.6 207.6 23.9 215 12ZM215 12C213.4 27.2 201.5 45 192.9 53.8C195.9 42 204.4 22.4 215 12ZM215 12C211.8 22.7 200.9 33.8 193.6 39C197.3 31 206 18 215 12ZM215 12C208.3 21.9 193.4 29.8 184.2 32.8C190.7 25.7 203.8 15.3 215 12ZM18 60C13.7 72.4 0.2 85 -8.7 90.8C-3.8 81.5 7 66.7 18 60ZM18 60C11 70.7 -4.6 79.5 -14.3 82.8C-7.6 75.2 6.1 63.8 18 60ZM18 60C10 68 -5.1 72.6 -14.2 73.7C-6.9 68.3 7 61 18 60ZM18 60C8.4 64.6 -6.6 64.1 -15 62.3C-6.9 59.7 7.9 57.5 18 60ZM18 60C6.9 61.6 -8 56.3 -15.9 51.9C-6.9 51.9 8.6 54.3 18 60ZM20 150C30 143 47.1 140.9 56.9 141.4C48.2 145.7 31.9 151 20 150ZM20 150C31.7 144.6 49.6 145.7 59.7 148C49.9 150.9 32.1 153.2 20 150ZM20 150C35.1 147.9 55.1 155.2 65.8 161.4C53.6 161.3 32.6 157.8 20 150ZM20 150C33.9 149.8 51.2 158.7 60.1 165.5C49.2 164 30.6 158.5 20 150ZM20 150C35.8 153.4 53 168 61.3 178C49.3 173.5 29.7 162.4 20 150ZM20 150C33.8 156.9 46.4 174.4 51.7 185.6C41.7 178.6 26.1 163.7 20 150ZM20 150C33.5 157.9 45 176.6 49.7 188.3C40 180.5 25.2 164.4 20 150ZM12 220C25.3 214.6 45.3 216.6 56.5 219.7C45.4 222.4 25.4 224.2 12 220ZM12 220C26.2 218.7 44.6 226.4 54.2 232.7C42.9 232 23.5 227.8 12 220ZM12 220C25.5 221.6 41.2 232.5 49.1 240.2C38.6 237.4 21.2 229.6 12 220ZM12 220C22.1 223.9 32.1 235.4 36.5 242.9C29 238.6 17.2 229.2 12 220ZM12 220C24 228.8 33 247.6 36.3 259.1C27.9 250.8 15.5 234.1 12 220ZM12 220C20.2 230.4 23.5 248.7 23.5 259.4C18.3 250.2 11.7 232.9 12 220Z" fill="#00a652"/> | |
| 22 | </svg> |
crates/canvas/assets/backgrounds/binoculars-corner.svg created+20| ... | ... | @@ -0,0 +1,20 @@ |
| 1 | <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 613 144" preserveAspectRatio="none"> | |
| 2 | <defs> | |
| 3 | <linearGradient id="across" x2="1" y2="0"><stop offset="0.25" stop-color="white"/><stop offset="1" stop-color="white" stop-opacity="0"/></linearGradient><mask id="dots-fade"><rect width="613" height="144" fill="url(#across)"/></mask> | |
| 4 | <radialGradient id="reach" cx="60" cy="70" r="1" gradientTransform="translate(60 70) scale(320 110) translate(-60 -70)" gradientUnits="userSpaceOnUse"><stop offset="0.3" stop-color="white"/><stop offset="1" stop-color="white" stop-opacity="0"/></radialGradient><mask id="art"><rect width="613" height="144" fill="url(#reach)"/></mask> | |
| 5 | <linearGradient id="steel" x2="0" y2="1"><stop offset="0" stop-color="#f4f6fa"/><stop offset=".5" stop-color="#b7bfcb"/><stop offset="1" stop-color="#6f7a88"/></linearGradient> | |
| 6 | <radialGradient id="lens"><stop offset="0" stop-color="#1b2029"/><stop offset=".6" stop-color="#4b5462"/><stop offset="1" stop-color="#c8ced8"/></radialGradient> | |
| 7 | <filter id="blur" x="-20%" y="-50%" width="140%" height="200%"><feGaussianBlur stdDeviation="8"/></filter> | |
| 8 | </defs> | |
| 9 | <g fill="#f3f5fb" mask="url(#dots-fade)"><circle cx="180" cy="12" r="8"/><circle cx="180" cy="38" r="8"/><circle cx="180" cy="64" r="8"/><circle cx="180" cy="90" r="8"/><circle cx="180" cy="116" r="8"/><circle cx="180" cy="142" r="8"/><circle cx="206" cy="12" r="8"/><circle cx="206" cy="38" r="8"/><circle cx="206" cy="64" r="8"/><circle cx="206" cy="90" r="8"/><circle cx="206" cy="116" r="8"/><circle cx="206" cy="142" r="8"/><circle cx="232" cy="12" r="8"/><circle cx="232" cy="38" r="8"/><circle cx="232" cy="64" r="8"/><circle cx="232" cy="90" r="8"/><circle cx="232" cy="116" r="8"/><circle cx="232" cy="142" r="8"/><circle cx="258" cy="12" r="8"/><circle cx="258" cy="38" r="8"/><circle cx="258" cy="64" r="8"/><circle cx="258" cy="90" r="8"/><circle cx="258" cy="116" r="8"/><circle cx="258" cy="142" r="8"/><circle cx="284" cy="12" r="8"/><circle cx="284" cy="38" r="8"/><circle cx="284" cy="64" r="8"/><circle cx="284" cy="90" r="8"/><circle cx="284" cy="116" r="8"/><circle cx="284" cy="142" r="8"/><circle cx="310" cy="12" r="8"/><circle cx="310" cy="38" r="8"/><circle cx="310" cy="64" r="8"/><circle cx="310" cy="90" r="8"/><circle cx="310" cy="116" r="8"/><circle cx="310" cy="142" r="8"/><circle cx="336" cy="12" r="8"/><circle cx="336" cy="38" r="8"/><circle cx="336" cy="64" r="8"/><circle cx="336" cy="90" r="8"/><circle cx="336" cy="116" r="8"/><circle cx="336" cy="142" r="8"/><circle cx="362" cy="12" r="8"/><circle cx="362" cy="38" r="8"/><circle cx="362" cy="64" r="8"/><circle cx="362" cy="90" r="8"/><circle cx="362" cy="116" r="8"/><circle cx="362" cy="142" r="8"/><circle cx="388" cy="12" r="8"/><circle cx="388" cy="38" r="8"/><circle cx="388" cy="64" r="8"/><circle cx="388" cy="90" r="8"/><circle cx="388" cy="116" r="8"/><circle cx="388" cy="142" r="8"/><circle cx="414" cy="12" r="8"/><circle cx="414" cy="38" r="8"/><circle cx="414" cy="64" r="8"/><circle cx="414" cy="90" r="8"/><circle cx="414" cy="116" r="8"/><circle cx="414" cy="142" r="8"/><circle cx="440" cy="12" r="8"/><circle cx="440" cy="38" r="8"/><circle cx="440" cy="64" r="8"/><circle cx="440" cy="90" r="8"/><circle cx="440" cy="116" r="8"/><circle cx="440" cy="142" r="8"/><circle cx="466" cy="12" r="8"/><circle cx="466" cy="38" r="8"/><circle cx="466" cy="64" r="8"/><circle cx="466" cy="90" r="8"/><circle cx="466" cy="116" r="8"/><circle cx="466" cy="142" r="8"/><circle cx="492" cy="12" r="8"/><circle cx="492" cy="38" r="8"/><circle cx="492" cy="64" r="8"/><circle cx="492" cy="90" r="8"/><circle cx="492" cy="116" r="8"/><circle cx="492" cy="142" r="8"/><circle cx="518" cy="12" r="8"/><circle cx="518" cy="38" r="8"/><circle cx="518" cy="64" r="8"/><circle cx="518" cy="90" r="8"/><circle cx="518" cy="116" r="8"/><circle cx="518" cy="142" r="8"/><circle cx="544" cy="12" r="8"/><circle cx="544" cy="38" r="8"/><circle cx="544" cy="64" r="8"/><circle cx="544" cy="90" r="8"/><circle cx="544" cy="116" r="8"/><circle cx="544" cy="142" r="8"/><circle cx="570" cy="12" r="8"/><circle cx="570" cy="38" r="8"/><circle cx="570" cy="64" r="8"/><circle cx="570" cy="90" r="8"/><circle cx="570" cy="116" r="8"/><circle cx="570" cy="142" r="8"/><circle cx="596" cy="12" r="8"/><circle cx="596" cy="38" r="8"/><circle cx="596" cy="64" r="8"/><circle cx="596" cy="90" r="8"/><circle cx="596" cy="116" r="8"/><circle cx="596" cy="142" r="8"/></g> | |
| 10 | <g mask="url(#art)"> | |
| 11 | <path d="M40 30L300 40L300 90L50 110Z" fill="#dfe5ee" filter="url(#blur)"/> | |
| 12 | <rect x="30" y="54" width="140" height="56" rx="26" transform="rotate(-8 30 80)" fill="url(#steel)" filter="url(#blur)"/> | |
| 13 | </g> | |
| 14 | <rect x="-10" y="-20" width="60" height="48" rx="20" fill="url(#steel)"/> | |
| 15 | <ellipse cx="10" cy="4" rx="16" ry="18" fill="url(#lens)"/> | |
| 16 | <rect x="20" y="55" width="80" height="70" rx="30" fill="url(#steel)"/> | |
| 17 | <ellipse cx="46" cy="88" rx="22" ry="26" fill="#c3cad6"/> | |
| 18 | <ellipse cx="46" cy="88" rx="15" ry="18" fill="url(#lens)"/> | |
| 19 | <path d="M14 20L40 64" stroke="#9aa4b2" stroke-width="10"/> | |
| 20 | </svg> |
crates/canvas/assets/backgrounds/black-and-green-title.svg created+12| ... | ... | @@ -0,0 +1,12 @@ |
| 1 | <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 814 45" preserveAspectRatio="none"> | |
| 2 | <defs> | |
| 3 | <linearGradient id="across" x2="1"><stop offset=".63" stop-color="white"/><stop offset=".98" stop-color="white" stop-opacity="0"/></linearGradient> | |
| 4 | <mask id="fade"><rect width="814" height="45" fill="url(#across)"/></mask> | |
| 5 | </defs> | |
| 6 | <g mask="url(#fade)"> | |
| 7 | <rect width="814" height="32" fill="#30323f"/> | |
| 8 | <rect y="32" width="814" height="1" fill="#373618"/> | |
| 9 | <rect y="33" width="814" height="6" fill="#b4ba55"/> | |
| 10 | <rect y="39" width="814" height="6" fill="#dadca6"/> | |
| 11 | </g> | |
| 12 | </svg> |
crates/canvas/assets/backgrounds/blue-bubbles-corner-margin.svg created+10| ... | ... | @@ -0,0 +1,10 @@ |
| 1 | <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 105 441" preserveAspectRatio="none"> | |
| 2 | <defs> | |
| 3 | <filter id="glow" x="-50%" y="-50%" width="200%" height="200%"><feGaussianBlur stdDeviation="3"/></filter> | |
| 4 | <linearGradient id="down" x2="0" y2="1"><stop offset="0" stop-color="white"/><stop offset=".75" stop-color="white" stop-opacity="0"/></linearGradient> | |
| 5 | <mask id="fade"><rect width="105" height="441" fill="url(#down)"/></mask> | |
| 6 | </defs> | |
| 7 | <g mask="url(#fade)" fill="none" stroke="#f3f6fb" stroke-width=".8"><path d="M4 0V330"/><path d="M12 0V330"/><path d="M20 0V330"/><path d="M28 0V330"/><path d="M36 0V330"/><path d="M44 0V330"/><path d="M52 0V330"/><path d="M60 0V330"/><path d="M68 0V330"/><path d="M76 0V330"/><path d="M84 0V330"/><path d="M92 0V330"/><path d="M100 0V330"/><path d="M0 4H105"/><path d="M0 12H105"/><path d="M0 20H105"/><path d="M0 28H105"/><path d="M0 36H105"/><path d="M0 44H105"/><path d="M0 52H105"/><path d="M0 60H105"/><path d="M0 68H105"/><path d="M0 76H105"/><path d="M0 84H105"/><path d="M0 92H105"/><path d="M0 100H105"/><path d="M0 108H105"/><path d="M0 116H105"/><path d="M0 124H105"/><path d="M0 132H105"/><path d="M0 140H105"/><path d="M0 148H105"/><path d="M0 156H105"/><path d="M0 164H105"/><path d="M0 172H105"/><path d="M0 180H105"/><path d="M0 188H105"/><path d="M0 196H105"/><path d="M0 204H105"/><path d="M0 212H105"/><path d="M0 220H105"/><path d="M0 228H105"/><path d="M0 236H105"/><path d="M0 244H105"/><path d="M0 252H105"/><path d="M0 260H105"/><path d="M0 268H105"/><path d="M0 276H105"/><path d="M0 284H105"/><path d="M0 292H105"/><path d="M0 300H105"/><path d="M0 308H105"/><path d="M0 316H105"/><path d="M0 324H105"/></g> | |
| 8 | <g fill="#d9e8fc" filter="url(#glow)"><circle cx="27" cy="12" r="34"/><circle cx="43" cy="105" r="20.5"/><circle cx="36" cy="147" r="13"/><circle cx="49" cy="172" r="7.5"/><circle cx="-4" cy="377" r="36"/></g> | |
| 9 | <g fill="#b5d6ff" stroke="#ffffff" stroke-width="2"><circle cx="27" cy="12" r="31"/><circle cx="43" cy="105" r="17.5"/><circle cx="36" cy="147" r="10"/><circle cx="49" cy="172" r="4.5"/><circle cx="-4" cy="377" r="33"/></g> | |
| 10 | </svg> |
crates/canvas/assets/backgrounds/blue-bubbles-corner-top.svg created+5| ... | ... | @@ -0,0 +1,5 @@ |
| 1 | <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 76 97" preserveAspectRatio="none"> | |
| 2 | <defs><filter id="glow" x="-50%" y="-50%" width="200%" height="200%"><feGaussianBlur stdDeviation="3"/></filter></defs> | |
| 3 | <g fill="#d9e8fc" filter="url(#glow)"><circle cx="36" cy="4" r="20"/><circle cx="42" cy="42" r="13"/><circle cx="29" cy="67" r="7.5"/></g> | |
| 4 | <g fill="#b5d6ff" stroke="#ffffff" stroke-width="2"><circle cx="36" cy="4" r="17"/><circle cx="42" cy="42" r="10"/><circle cx="29" cy="67" r="4.5"/></g> | |
| 5 | </svg> |
crates/canvas/assets/backgrounds/blue-clouds.svg created+10| ... | ... | @@ -0,0 +1,10 @@ |
| 1 | <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 570 779" preserveAspectRatio="none"> | |
| 2 | <defs> | |
| 3 | <radialGradient id="sky" cx="0" cy="0" r="1" gradientTransform="scale(560 760)" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#b2c3e5"/><stop offset=".5" stop-color="#bac8e6"/><stop offset=".72" stop-color="#c3cfea"/><stop offset=".93" stop-color="#c3cfea" stop-opacity="0"/></radialGradient> | |
| 4 | <radialGradient id="shade"><stop offset="0" stop-color="#d8d9ee"/><stop offset=".5" stop-color="#d8d9ee" stop-opacity=".8"/><stop offset="1" stop-color="#d8d9ee" stop-opacity="0"/></radialGradient> | |
| 5 | <radialGradient id="billow" fx=".38" fy=".3"><stop offset="0" stop-color="#ffffff"/><stop offset=".6" stop-color="#f3f3fa" stop-opacity=".9"/><stop offset="1" stop-color="#e0e1f1" stop-opacity="0"/></radialGradient> | |
| 6 | </defs> | |
| 7 | <rect width="570" height="779" fill="url(#sky)"/> | |
| 8 | <circle cx="243.4" cy="204.4" r="102.8" fill="url(#shade)" opacity="0.85"/><circle cx="304.1" cy="204.3" r="53.2" fill="url(#shade)" opacity="0.85"/><circle cx="130.5" cy="192.8" r="47" fill="url(#shade)" opacity="0.85"/><circle cx="248.3" cy="211" r="70.6" fill="url(#shade)" opacity="0.85"/><circle cx="214.1" cy="183.6" r="95.8" fill="url(#shade)" opacity="0.85"/><circle cx="234" cy="173.8" r="79.3" fill="url(#shade)" opacity="0.85"/><circle cx="178.1" cy="211.7" r="90.6" fill="url(#shade)" opacity="0.85"/><circle cx="155.3" cy="206.7" r="51.1" fill="url(#shade)" opacity="0.85"/><circle cx="242.3" cy="178.5" r="67.6" fill="url(#shade)" opacity="0.85"/><circle cx="290.6" cy="182" r="48.9" fill="url(#shade)" opacity="0.85"/><circle cx="355" cy="119.1" r="18.9" fill="url(#shade)" opacity="0.85"/><circle cx="352.6" cy="110.9" r="25.6" fill="url(#shade)" opacity="0.85"/><circle cx="266.3" cy="120.8" r="44.5" fill="url(#shade)" opacity="0.85"/><circle cx="353.2" cy="119.6" r="20.6" fill="url(#shade)" opacity="0.85"/><circle cx="110.4" cy="625.1" r="54.5" fill="url(#shade)" opacity="0.34"/><circle cx="26.1" cy="629.4" r="35.8" fill="url(#shade)" opacity="0.41"/><circle cx="8.5" cy="641.3" r="23.1" fill="url(#shade)" opacity="0.35"/><circle cx="95.8" cy="645.7" r="62" fill="url(#shade)" opacity="0.26"/><circle cx="97.5" cy="635.1" r="69.5" fill="url(#shade)" opacity="0.31"/><circle cx="23.7" cy="650.6" r="25" fill="url(#shade)" opacity="0.29"/><circle cx="10.1" cy="621.4" r="21.9" fill="url(#shade)" opacity="0.45"/><circle cx="241.2" cy="135.4" r="49.5" fill="url(#shade)" opacity="0.85"/><circle cx="150.3" cy="129.6" r="31.4" fill="url(#shade)" opacity="0.85"/><circle cx="276.7" cy="152.1" r="40.5" fill="url(#shade)" opacity="0.85"/><circle cx="118.5" cy="151.9" r="25" fill="url(#shade)" opacity="0.85"/><circle cx="206.8" cy="133.5" r="55.8" fill="url(#shade)" opacity="0.85"/><circle cx="310.6" cy="138.9" r="36.2" fill="url(#shade)" opacity="0.85"/><circle cx="120.3" cy="150.1" r="19.5" fill="url(#shade)" opacity="0.85"/><circle cx="228.4" cy="148.2" r="44.7" fill="url(#shade)" opacity="0.85"/><circle cx="226.4" cy="87.7" r="27.9" fill="url(#shade)" opacity="0.85"/><circle cx="232.8" cy="75.9" r="22.8" fill="url(#shade)" opacity="0.85"/><circle cx="160" cy="86.2" r="18.1" fill="url(#shade)" opacity="0.85"/><circle cx="194.7" cy="88.4" r="36.6" fill="url(#shade)" opacity="0.85"/><circle cx="372.4" cy="243.4" r="15.5" fill="url(#shade)" opacity="0.78"/><circle cx="334.9" cy="243.9" r="29.4" fill="url(#shade)" opacity="0.85"/><circle cx="285.4" cy="238.1" r="35.7" fill="url(#shade)" opacity="0.85"/><circle cx="374.2" cy="252.3" r="15.6" fill="url(#shade)" opacity="0.74"/><circle cx="59.9" cy="479.1" r="24.9" fill="url(#shade)" opacity="0.85"/><circle cx="16.7" cy="485.3" r="31.2" fill="url(#shade)" opacity="0.85"/><circle cx="33.7" cy="485.4" r="33.5" fill="url(#shade)" opacity="0.85"/><circle cx="97" cy="483.7" r="31.3" fill="url(#shade)" opacity="0.85"/><circle cx="34.1" cy="484.3" r="26.3" fill="url(#shade)" opacity="0.85"/><circle cx="28.8" cy="429.9" r="22.3" fill="url(#shade)" opacity="0.85"/><circle cx="12.4" cy="431.5" r="19" fill="url(#shade)" opacity="0.85"/><circle cx="80.5" cy="659" r="34.3" fill="url(#shade)" opacity="0.21"/><circle cx="119.3" cy="658.4" r="44.7" fill="url(#shade)" opacity="0.15"/><circle cx="46.8" cy="662.2" r="35.4" fill="url(#shade)" opacity="0.22"/><circle cx="170.9" cy="659.4" r="47.8" fill="url(#shade)" opacity="0.04"/><circle cx="122.3" cy="603.7" r="29.5" fill="url(#shade)" opacity="0.43"/><circle cx="128.8" cy="605.2" r="31.9" fill="url(#shade)" opacity="0.41"/><circle cx="143.5" cy="594.8" r="22.1" fill="url(#shade)" opacity="0.43"/><circle cx="101" cy="603.5" r="26.7" fill="url(#shade)" opacity="0.47"/><circle cx="88.3" cy="259.9" r="16.2" fill="url(#shade)" opacity="0.85"/><circle cx="55.7" cy="255.7" r="18.2" fill="url(#shade)" opacity="0.85"/><circle cx="4.3" cy="256.6" r="12.2" fill="url(#shade)" opacity="0.85"/><circle cx="-0.4" cy="163.2" r="14.4" fill="url(#shade)" opacity="0.85"/><circle cx="12" cy="168.4" r="20.3" fill="url(#shade)" opacity="0.85"/><circle cx="292.6" cy="579.2" r="21" fill="url(#shade)" opacity="0.02"/><circle cx="275.1" cy="578" r="19.2" fill="url(#shade)" opacity="0.10"/><circle cx="263.9" cy="574.6" r="11.9" fill="url(#shade)" opacity="0.16"/> | |
| 9 | <circle cx="130.5" cy="156.1" r="55.6" fill="url(#billow)" opacity="0.90"/><circle cx="155.5" cy="167.6" r="76.4" fill="url(#billow)" opacity="0.90"/><circle cx="173.8" cy="163.4" r="89.1" fill="url(#billow)" opacity="0.90"/><circle cx="185.7" cy="162.3" r="105.3" fill="url(#billow)" opacity="0.90"/><circle cx="215.5" cy="171.2" r="117.6" fill="url(#billow)" opacity="0.90"/><circle cx="228.2" cy="169.4" r="87" fill="url(#billow)" opacity="0.90"/><circle cx="245.3" cy="161.6" r="112.3" fill="url(#billow)" opacity="0.90"/><circle cx="265.7" cy="163.1" r="111.5" fill="url(#billow)" opacity="0.90"/><circle cx="288.5" cy="158.4" r="66.3" fill="url(#billow)" opacity="0.90"/><circle cx="307.5" cy="171.3" r="52.8" fill="url(#billow)" opacity="0.90"/><circle cx="135.5" cy="127.9" r="46.2" fill="url(#billow)" opacity="0.90"/><circle cx="147.1" cy="129.5" r="53.3" fill="url(#billow)" opacity="0.90"/><circle cx="169" cy="113.7" r="82.9" fill="url(#billow)" opacity="0.90"/><circle cx="189.9" cy="112.9" r="77.7" fill="url(#billow)" opacity="0.90"/><circle cx="205.5" cy="120.9" r="80.8" fill="url(#billow)" opacity="0.90"/><circle cx="227.5" cy="124.8" r="96.1" fill="url(#billow)" opacity="0.90"/><circle cx="244.2" cy="118.8" r="72.4" fill="url(#billow)" opacity="0.90"/><circle cx="275.2" cy="124.8" r="58.2" fill="url(#billow)" opacity="0.90"/><circle cx="287.2" cy="109.7" r="70" fill="url(#billow)" opacity="0.90"/><circle cx="307.5" cy="120.6" r="41.4" fill="url(#billow)" opacity="0.90"/><circle cx="257.6" cy="91.5" r="47.4" fill="url(#billow)" opacity="0.90"/><circle cx="278.1" cy="93.6" r="53.8" fill="url(#billow)" opacity="0.90"/><circle cx="315.6" cy="87.7" r="56.5" fill="url(#billow)" opacity="0.90"/><circle cx="345.5" cy="89.5" r="37.3" fill="url(#billow)" opacity="0.90"/><circle cx="259.4" cy="66.8" r="26.5" fill="url(#billow)" opacity="0.90"/><circle cx="280.5" cy="64.9" r="53.6" fill="url(#billow)" opacity="0.90"/><circle cx="322" cy="66.1" r="34.7" fill="url(#billow)" opacity="0.90"/><circle cx="350" cy="64.5" r="27.3" fill="url(#billow)" opacity="0.90"/><circle cx="17" cy="612.4" r="40.6" fill="url(#billow)" opacity="0.53"/><circle cx="47.6" cy="607.9" r="54.1" fill="url(#billow)" opacity="0.54"/><circle cx="73.9" cy="604.5" r="62.2" fill="url(#billow)" opacity="0.53"/><circle cx="100.9" cy="602.2" r="89" fill="url(#billow)" opacity="0.50"/><circle cx="125.9" cy="608.8" r="74.7" fill="url(#billow)" opacity="0.42"/><circle cx="154.6" cy="617.4" r="70.8" fill="url(#billow)" opacity="0.31"/><circle cx="176.9" cy="610.9" r="82.1" fill="url(#billow)" opacity="0.28"/><circle cx="201.8" cy="603.8" r="79.6" fill="url(#billow)" opacity="0.25"/><circle cx="236.9" cy="602.2" r="56.2" fill="url(#billow)" opacity="0.14"/><circle cx="21.8" cy="582.1" r="26.1" fill="url(#billow)" opacity="0.70"/><circle cx="46.5" cy="573.4" r="44.9" fill="url(#billow)" opacity="0.73"/><circle cx="75.6" cy="582.3" r="43.4" fill="url(#billow)" opacity="0.65"/><circle cx="92.1" cy="586" r="47.4" fill="url(#billow)" opacity="0.61"/><circle cx="124.5" cy="577" r="52.5" fill="url(#billow)" opacity="0.60"/><circle cx="152" cy="570.3" r="52.2" fill="url(#billow)" opacity="0.57"/><circle cx="175.3" cy="570.8" r="49.6" fill="url(#billow)" opacity="0.50"/><circle cx="205.8" cy="582.9" r="66.2" fill="url(#billow)" opacity="0.34"/><circle cx="225.8" cy="577.3" r="47.1" fill="url(#billow)" opacity="0.30"/><circle cx="260.7" cy="578.3" r="47.2" fill="url(#billow)" opacity="0.16"/><circle cx="284.3" cy="570.5" r="32.8" fill="url(#billow)" opacity="0.10"/><circle cx="-28.4" cy="691.6" r="27.5" fill="url(#billow)" opacity="0.08"/><circle cx="29.9" cy="690.7" r="55.1" fill="url(#billow)" opacity="0.08"/><circle cx="54.3" cy="690.6" r="49.9" fill="url(#billow)" opacity="0.07"/><circle cx="-27.7" cy="671.6" r="22.3" fill="url(#billow)" opacity="0.19"/><circle cx="10" cy="671" r="28.2" fill="url(#billow)" opacity="0.20"/><circle cx="51" cy="670.1" r="37" fill="url(#billow)" opacity="0.19"/><circle cx="109" cy="668.8" r="32" fill="url(#billow)" opacity="0.12"/><circle cx="147.1" cy="663.4" r="23.2" fill="url(#billow)" opacity="0.08"/><circle cx="107.5" cy="115.1" r="34" fill="url(#billow)" opacity="0.90"/><circle cx="152.3" cy="116.1" r="40.4" fill="url(#billow)" opacity="0.90"/><circle cx="190.5" cy="122.3" r="69.6" fill="url(#billow)" opacity="0.90"/><circle cx="206.7" cy="121.7" r="59.8" fill="url(#billow)" opacity="0.90"/><circle cx="241.1" cy="117.7" r="68.4" fill="url(#billow)" opacity="0.90"/><circle cx="275.8" cy="118.4" r="49.8" fill="url(#billow)" opacity="0.90"/><circle cx="317.7" cy="122.1" r="50.6" fill="url(#billow)" opacity="0.90"/><circle cx="342.5" cy="124.9" r="33.4" fill="url(#billow)" opacity="0.90"/><circle cx="118.1" cy="96.5" r="27.8" fill="url(#billow)" opacity="0.90"/><circle cx="146.7" cy="89.9" r="27.6" fill="url(#billow)" opacity="0.90"/><circle cx="175.4" cy="89.1" r="50.1" fill="url(#billow)" opacity="0.90"/><circle cx="213.4" cy="99.1" r="37.7" fill="url(#billow)" opacity="0.90"/><circle cx="245.7" cy="101" r="51.2" fill="url(#billow)" opacity="0.90"/><circle cx="277.4" cy="95" r="36.4" fill="url(#billow)" opacity="0.90"/><circle cx="304.5" cy="90.7" r="35.4" fill="url(#billow)" opacity="0.90"/><circle cx="341.5" cy="100.8" r="21.3" fill="url(#billow)" opacity="0.90"/><circle cx="150.6" cy="68.2" r="17.4" fill="url(#billow)" opacity="0.90"/><circle cx="181.2" cy="67.9" r="34.4" fill="url(#billow)" opacity="0.90"/><circle cx="206.6" cy="73.4" r="34.9" fill="url(#billow)" opacity="0.90"/><circle cx="252.9" cy="71.5" r="19.8" fill="url(#billow)" opacity="0.90"/><circle cx="154.5" cy="59.6" r="14.1" fill="url(#billow)" opacity="0.90"/><circle cx="188" cy="54.3" r="29.3" fill="url(#billow)" opacity="0.90"/><circle cx="219.1" cy="53.3" r="22.5" fill="url(#billow)" opacity="0.90"/><circle cx="236.4" cy="53.8" r="16.8" fill="url(#billow)" opacity="0.90"/><circle cx="237.6" cy="234.7" r="25.8" fill="url(#billow)" opacity="0.90"/><circle cx="275.5" cy="229.7" r="39.3" fill="url(#billow)" opacity="0.90"/><circle cx="325.6" cy="232.2" r="36.7" fill="url(#billow)" opacity="0.90"/><circle cx="353.8" cy="231.7" r="38.2" fill="url(#billow)" opacity="0.90"/><circle cx="243.5" cy="211.8" r="27.8" fill="url(#billow)" opacity="0.90"/><circle cx="268.2" cy="208.1" r="34.8" fill="url(#billow)" opacity="0.90"/><circle cx="317.8" cy="207.9" r="34.5" fill="url(#billow)" opacity="0.90"/><circle cx="371.8" cy="212.7" r="15.6" fill="url(#billow)" opacity="0.90"/><circle cx="-29.7" cy="466.6" r="20.9" fill="url(#billow)" opacity="0.90"/><circle cx="19.8" cy="473.5" r="26.4" fill="url(#billow)" opacity="0.90"/><circle cx="53.8" cy="466.4" r="35.6" fill="url(#billow)" opacity="0.90"/><circle cx="89.4" cy="468" r="33.5" fill="url(#billow)" opacity="0.90"/><circle cx="135.3" cy="466.4" r="18.2" fill="url(#billow)" opacity="0.90"/><circle cx="-8.7" cy="453.4" r="18.9" fill="url(#billow)" opacity="0.90"/><circle cx="17.7" cy="456.3" r="19.8" fill="url(#billow)" opacity="0.90"/><circle cx="55.5" cy="452.9" r="28.6" fill="url(#billow)" opacity="0.90"/><circle cx="110.1" cy="456.8" r="26.6" fill="url(#billow)" opacity="0.90"/><circle cx="133.1" cy="452.1" r="18" fill="url(#billow)" opacity="0.90"/><circle cx="-0.2" cy="420.4" r="15.7" fill="url(#billow)" opacity="0.90"/><circle cx="32.3" cy="421.2" r="28.7" fill="url(#billow)" opacity="0.90"/><circle cx="56.5" cy="419.7" r="20.9" fill="url(#billow)" opacity="0.90"/><circle cx="-5" cy="408.9" r="9.9" fill="url(#billow)" opacity="0.90"/><circle cx="22.6" cy="412.3" r="20.4" fill="url(#billow)" opacity="0.90"/><circle cx="56.6" cy="409" r="11.1" fill="url(#billow)" opacity="0.90"/><circle cx="-15.4" cy="635.4" r="22.1" fill="url(#billow)" opacity="0.40"/><circle cx="34.9" cy="637.2" r="27.6" fill="url(#billow)" opacity="0.38"/><circle cx="80.1" cy="635" r="37.2" fill="url(#billow)" opacity="0.35"/><circle cx="111.6" cy="638.6" r="38.9" fill="url(#billow)" opacity="0.28"/><circle cx="157" cy="638" r="56.7" fill="url(#billow)" opacity="0.19"/><circle cx="202.4" cy="644.4" r="54.5" fill="url(#billow)" opacity="0.04"/><circle cx="-19.9" cy="617.1" r="14.6" fill="url(#billow)" opacity="0.50"/><circle cx="37.4" cy="618.4" r="21.1" fill="url(#billow)" opacity="0.49"/><circle cx="81.6" cy="621.4" r="25.6" fill="url(#billow)" opacity="0.43"/><circle cx="105.7" cy="616.7" r="32.8" fill="url(#billow)" opacity="0.42"/><circle cx="168.1" cy="625.7" r="40.4" fill="url(#billow)" opacity="0.23"/><circle cx="194.2" cy="620" r="36.1" fill="url(#billow)" opacity="0.19"/><circle cx="238.7" cy="615.4" r="37.7" fill="url(#billow)" opacity="0.06"/><circle cx="40.8" cy="588.5" r="22.5" fill="url(#billow)" opacity="0.65"/><circle cx="89.1" cy="589.5" r="34.6" fill="url(#billow)" opacity="0.59"/><circle cx="136.4" cy="593.5" r="34.9" fill="url(#billow)" opacity="0.48"/><circle cx="181.3" cy="590.8" r="14.9" fill="url(#billow)" opacity="0.38"/><circle cx="57.5" cy="575.4" r="15.7" fill="url(#billow)" opacity="0.71"/><circle cx="101.5" cy="572.3" r="28.8" fill="url(#billow)" opacity="0.67"/><circle cx="129.6" cy="579.1" r="25.5" fill="url(#billow)" opacity="0.57"/><circle cx="163.2" cy="574.2" r="16.8" fill="url(#billow)" opacity="0.52"/><circle cx="8.7" cy="249.1" r="22.1" fill="url(#billow)" opacity="0.90"/><circle cx="51.7" cy="249.1" r="24.7" fill="url(#billow)" opacity="0.90"/><circle cx="128.8" cy="252" r="16.6" fill="url(#billow)" opacity="0.90"/><circle cx="10.4" cy="238.9" r="12.9" fill="url(#billow)" opacity="0.90"/><circle cx="70.6" cy="239.4" r="16.5" fill="url(#billow)" opacity="0.90"/><circle cx="102.8" cy="237.1" r="18.3" fill="url(#billow)" opacity="0.90"/><circle cx="2.3" cy="158.4" r="22.4" fill="url(#billow)" opacity="0.90"/><circle cx="17.5" cy="161" r="28.2" fill="url(#billow)" opacity="0.90"/><circle cx="45.1" cy="161.6" r="14.8" fill="url(#billow)" opacity="0.90"/><circle cx="-10.2" cy="151" r="9.6" fill="url(#billow)" opacity="0.90"/><circle cx="15.6" cy="150.3" r="16.6" fill="url(#billow)" opacity="0.90"/><circle cx="36.7" cy="149.9" r="14.4" fill="url(#billow)" opacity="0.90"/><circle cx="273.2" cy="561.8" r="24.4" fill="url(#billow)" opacity="0.19"/><circle cx="277.8" cy="544.4" r="18.9" fill="url(#billow)" opacity="0.25"/><circle cx="301.7" cy="540.5" r="26.4" fill="url(#billow)" opacity="0.16"/> | |
| 10 | </svg> |
crates/canvas/assets/backgrounds/blue-dots.svg created+58| ... | ... | @@ -0,0 +1,58 @@ |
| 1 | <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 612 792" preserveAspectRatio="none"> | |
| 2 | <defs> | |
| 3 | <linearGradient id="relief" x1="0" y1="0" x2="1" y2="1"><stop offset=".2" stop-color="#c2ccee"/><stop offset=".55" stop-color="#e7ebf7"/><stop offset=".85" stop-color="#ffffff"/></linearGradient> | |
| 4 | <g id="dot"> | |
| 5 | <circle r="15.5" fill="none" stroke="url(#relief)" stroke-width="9"/> | |
| 6 | <circle r="10.5" fill="#dde3f6"/> | |
| 7 | </g> | |
| 8 | <radialGradient id="corner" cx="0" cy="0" r="1" gradientTransform="scale(600 800)" gradientUnits="userSpaceOnUse"><stop offset=".6" stop-color="#e7ebf7"/><stop offset="1" stop-color="#e7ebf7" stop-opacity="0"/></radialGradient> | |
| 9 | </defs> | |
| 10 | <rect width="612" height="792" fill="url(#corner)"/> | |
| 11 | <use href="#dot" x="481" y="39" opacity="0.49"/> | |
| 12 | <use href="#dot" x="74" y="41" opacity="1.00"/> | |
| 13 | <use href="#dot" x="343" y="42" opacity="1.00"/> | |
| 14 | <use href="#dot" x="150" y="54" opacity="1.00"/> | |
| 15 | <use href="#dot" x="256" y="65" opacity="1.00"/> | |
| 16 | <use href="#dot" x="431" y="95" opacity="0.68"/> | |
| 17 | <use href="#dot" x="98" y="114" opacity="1.00"/> | |
| 18 | <use href="#dot" x="175" y="143" opacity="1.00"/> | |
| 19 | <use href="#dot" x="348" y="158" opacity="0.97"/> | |
| 20 | <use href="#dot" x="54" y="162" opacity="1.00"/> | |
| 21 | <use href="#dot" x="264" y="167" opacity="1.00"/> | |
| 22 | <use href="#dot" x="463" y="192" opacity="0.48"/> | |
| 23 | <use href="#dot" x="98" y="216" opacity="1.00"/> | |
| 24 | <use href="#dot" x="181" y="231" opacity="1.00"/> | |
| 25 | <use href="#dot" x="385" y="241" opacity="0.73"/> | |
| 26 | <use href="#dot" x="271" y="251" opacity="1.00"/> | |
| 27 | <use href="#dot" x="47" y="253" opacity="1.00"/> | |
| 28 | <use href="#dot" x="144" y="286" opacity="1.00"/> | |
| 29 | <use href="#dot" x="438" y="297" opacity="0.45"/> | |
| 30 | <use href="#dot" x="309" y="318" opacity="0.87"/> | |
| 31 | <use href="#dot" x="79" y="338" opacity="1.00"/> | |
| 32 | <use href="#dot" x="197" y="345" opacity="1.00"/> | |
| 33 | <use href="#dot" x="462" y="372" opacity="0.25"/> | |
| 34 | <use href="#dot" x="369" y="385" opacity="0.55"/> | |
| 35 | <use href="#dot" x="271" y="388" opacity="0.84"/> | |
| 36 | <use href="#dot" x="161" y="408" opacity="1.00"/> | |
| 37 | <use href="#dot" x="54" y="443" opacity="1.00"/> | |
| 38 | <use href="#dot" x="309" y="446" opacity="0.60"/> | |
| 39 | ||
| 40 | <use href="#dot" x="221" y="469" opacity="0.77"/> | |
| 41 | <use href="#dot" x="417" y="476" opacity="0.21"/> | |
| 42 | <use href="#dot" x="151" y="494" opacity="0.83"/> | |
| 43 | <use href="#dot" x="77" y="522" opacity="0.84"/> | |
| 44 | <use href="#dot" x="346" y="525" opacity="0.32"/> | |
| 45 | <use href="#dot" x="239" y="528" opacity="0.57"/> | |
| 46 | ||
| 47 | ||
| 48 | <use href="#dot" x="128" y="585" opacity="0.60"/> | |
| 49 | <use href="#dot" x="35" y="588" opacity="0.66"/> | |
| 50 | <use href="#dot" x="342" y="590" opacity="0.17"/> | |
| 51 | <use href="#dot" x="270" y="592" opacity="0.33"/> | |
| 52 | <use href="#dot" x="186" y="620" opacity="0.41"/> | |
| 53 | ||
| 54 | <use href="#dot" x="288" y="677" opacity="0.07"/> | |
| 55 | <use href="#dot" x="204" y="696" opacity="0.16"/> | |
| 56 | <use href="#dot" x="133" y="707" opacity="0.22"/> | |
| 57 | <use href="#dot" x="54" y="665" opacity="0.41"/> | |
| 58 | </svg> |
crates/canvas/assets/backgrounds/blue-flow.svg created+18| ... | ... | @@ -0,0 +1,18 @@ |
| 1 | <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 40 623" preserveAspectRatio="none"> | |
| 2 | <clipPath id="band"><rect x="6.5" width="25" height="623"/></clipPath> | |
| 3 | <g clip-path="url(#band)" fill="none" stroke="#deefff" stroke-width="5.6" stroke-miterlimit="80"> | |
| 4 | <path d="M-6 -70L18.8 -270L43.6 -70"/> | |
| 5 | <path d="M-6 6.5L18.8 -193.5L43.6 6.5"/> | |
| 6 | <path d="M-6 83L18.8 -117L43.6 83"/> | |
| 7 | <path d="M-6 159.5L18.8 -40.5L43.6 159.5"/> | |
| 8 | <path d="M-6 236L18.8 36L43.6 236"/> | |
| 9 | <path d="M-6 312.5L18.8 112.5L43.6 312.5"/> | |
| 10 | <path d="M-6 389L18.8 189L43.6 389"/> | |
| 11 | <path d="M-6 465.5L18.8 265.5L43.6 465.5"/> | |
| 12 | <path d="M-6 542L18.8 342L43.6 542"/> | |
| 13 | <path d="M-6 618.5L18.8 418.5L43.6 618.5"/> | |
| 14 | <path d="M-6 695L18.8 495L43.6 695"/> | |
| 15 | <path d="M-6 771.5L18.8 571.5L43.6 771.5"/> | |
| 16 | <path d="M-6 848L18.8 648L43.6 848"/> | |
| 17 | </g> | |
| 18 | </svg> |
crates/canvas/assets/backgrounds/blue-mist-margin.svg created+14| ... | ... | @@ -0,0 +1,14 @@ |
| 1 | <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 69 630" preserveAspectRatio="none"> | |
| 2 | <defs> | |
| 3 | <linearGradient id="down" x2="0" y2="1"><stop offset=".38" stop-color="white"/><stop offset=".99" stop-color="white" stop-opacity="0"/></linearGradient> | |
| 4 | <mask id="fade"><rect width="69" height="630" fill="url(#down)"/></mask> | |
| 5 | </defs> | |
| 6 | <g mask="url(#fade)"> | |
| 7 | <rect width="40" height="630" fill="#698686"/> | |
| 8 | <g fill="#adb7b9"> | |
| 9 | <rect x="43" width="4" height="630"/> | |
| 10 | <rect x="50" width="1" height="630"/> | |
| 11 | <rect x="64.5" width="2.5" height="630"/> | |
| 12 | </g> | |
| 13 | </g> | |
| 14 | </svg> |
crates/canvas/assets/backgrounds/blue-stripe-title.svg created+15| ... | ... | @@ -0,0 +1,15 @@ |
| 1 | <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 814 105" preserveAspectRatio="none"> | |
| 2 | <defs> | |
| 3 | <pattern id="pinstripe" width="8" height="4" patternUnits="userSpaceOnUse"><rect width="8" height="2" fill="#ebf0f3"/></pattern> | |
| 4 | <linearGradient id="across" x2="1"><stop offset=".3" stop-color="white"/><stop offset=".8" stop-color="white" stop-opacity="0"/></linearGradient> | |
| 5 | <mask id="fade"><rect width="814" height="105" fill="url(#across)"/></mask> | |
| 6 | <linearGradient id="bar" x2="1"> | |
| 7 | <stop offset="0" stop-color="#2663a8"/> | |
| 8 | <stop offset=".5" stop-color="#8aabd0"/> | |
| 9 | <stop offset=".74" stop-color="#d0dded"/> | |
| 10 | <stop offset=".8" stop-color="#d0dded" stop-opacity="0"/> | |
| 11 | </linearGradient> | |
| 12 | </defs> | |
| 13 | <rect width="814" height="95" fill="url(#pinstripe)" mask="url(#fade)"/> | |
| 14 | <rect y="96" width="814" height="8" fill="url(#bar)"/> | |
| 15 | </svg> |
crates/canvas/assets/backgrounds/blue-stripes.svg created+12| ... | ... | @@ -0,0 +1,12 @@ |
| 1 | <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 612 792" preserveAspectRatio="none"> | |
| 2 | <defs> | |
| 3 | <linearGradient id="columns" x2="80" gradientUnits="userSpaceOnUse"><stop offset="0.000" stop-color="#a3b5db"/><stop offset="0.075" stop-color="#b8c3df"/><stop offset="0.150" stop-color="#c9d1e6"/><stop offset="0.200" stop-color="#abb9dc"/><stop offset="0.225" stop-color="#a0b2d8"/><stop offset="0.325" stop-color="#bfcbe5"/><stop offset="0.400" stop-color="#d4d9ec"/><stop offset="0.450" stop-color="#abbadb"/><stop offset="0.550" stop-color="#afbcdc"/><stop offset="0.600" stop-color="#bfc9e4"/><stop offset="0.675" stop-color="#ced5e8"/><stop offset="0.725" stop-color="#a1b0d7"/><stop offset="0.775" stop-color="#9aabd7"/><stop offset="0.825" stop-color="#b3c1de"/><stop offset="0.850" stop-color="#c7d2e6"/><stop offset="0.875" stop-color="#8d9cbd"/><stop offset="0.900" stop-color="#9dabc6"/><stop offset="0.950" stop-color="#d5dce6"/><stop offset="1" stop-color="#d5dce6" stop-opacity="0"/></linearGradient> | |
| 4 | <linearGradient id="rows" x2="0" y2="32" gradientUnits="userSpaceOnUse"><stop offset="0.000" stop-color="#a2b1d8"/><stop offset="0.125" stop-color="#b3c1de"/><stop offset="0.188" stop-color="#c2cce5"/><stop offset="0.250" stop-color="#b4c2df"/><stop offset="0.375" stop-color="#afbcdc"/><stop offset="0.562" stop-color="#a4b4d8"/><stop offset="0.656" stop-color="#a4b2d9"/><stop offset="0.688" stop-color="#7e91b2"/><stop offset="0.750" stop-color="#8e9dbc"/><stop offset="0.812" stop-color="#b0bbd1"/><stop offset="0.875" stop-color="#dde1ea"/><stop offset="1" stop-color="#dde1ea" stop-opacity="0"/></linearGradient> | |
| 5 | <linearGradient id="fade-down" x2="0" y2="1"><stop offset="0.6" stop-color="white"/><stop offset="0.9" stop-color="white" stop-opacity="0"/></linearGradient> | |
| 6 | <linearGradient id="fade-across" x2="1"><stop offset="0.5" stop-color="white"/><stop offset="0.79" stop-color="white" stop-opacity="0"/></linearGradient> | |
| 7 | <mask id="columns-fade"><rect width="612" height="792" fill="url(#fade-down)"/></mask> | |
| 8 | <mask id="rows-fade"><rect width="612" height="792" fill="url(#fade-across)"/></mask> | |
| 9 | </defs> | |
| 10 | <rect width="80" height="792" fill="url(#columns)" mask="url(#columns-fade)"/> | |
| 11 | <rect width="612" height="32" fill="url(#rows)" mask="url(#rows-fade)"/> | |
| 12 | </svg> |
crates/canvas/assets/backgrounds/blue-swirls.svg created+102| ... | ... | @@ -0,0 +1,102 @@ |
| 1 | <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 594 773" preserveAspectRatio="none"> | |
| 2 | <defs> | |
| 3 | <radialGradient id="edge" cx="0" cy="0" r="1" gradientTransform="scale(610 800)" gradientUnits="userSpaceOnUse"><stop offset=".6" stop-color="#8fb7db"/><stop offset=".95" stop-color="#8fb7db" stop-opacity="0"/></radialGradient> | |
| 4 | <radialGradient id="sheet-fill" cx="0" cy="0" r="1" gradientTransform="scale(610 800)" gradientUnits="userSpaceOnUse"><stop offset=".6" stop-color="#e6f0f9"/><stop offset=".95" stop-color="#e6f0f9" stop-opacity="0"/></radialGradient> | |
| 5 | <path id="band" d="M72 0C71.9 1 71.6 4 71.2 6C70.9 8 70.5 9.9 70 11.8C69.5 13.8 68.9 15.7 68.3 17.5C67.6 19.4 66.9 21.2 66.1 23C65.3 24.8 64.4 26.5 63.4 28.2C62.5 29.9 61.5 31.6 60.4 33.2C59.3 34.8 58.2 36.4 57 37.9C55.8 39.4 54.5 40.8 53.2 42.2C51.9 43.6 50.6 44.9 49.1 46.1C47.7 47.4 46.3 48.6 44.8 49.7C43.3 50.8 41.7 51.9 40.1 52.9C38.6 53.9 36.9 54.8 35.3 55.6C33.7 56.5 32 57.2 30.3 57.9C28.6 58.6 26.9 59.2 25.1 59.8C23.4 60.3 21.6 60.8 19.9 61.2C18.1 61.6 16.4 61.9 14.6 62.2C12.8 62.4 11 62.6 9.3 62.7C7.5 62.7 5.7 62.8 3.9 62.7C2.2 62.6 0.4 62.5 -1.3 62.3C-3 62.1 -4.8 61.8 -6.5 61.5C-8.2 61.1 -9.8 60.7 -11.5 60.2C-13.1 59.7 -14.8 59.2 -16.3 58.5C-17.9 57.9 -19.5 57.2 -21 56.5C-22.5 55.7 -24 54.9 -25.4 54.1C-26.9 53.2 -28.3 52.3 -29.6 51.3C-31 50.3 -32.3 49.3 -33.5 48.2C-34.8 47.2 -36 46 -37.1 44.9C-38.3 43.7 -39.4 42.5 -40.4 41.2C-41.4 40 -42.4 38.7 -43.3 37.4C-44.2 36.1 -45.1 34.7 -45.9 33.3C-46.7 31.9 -47.4 30.5 -48.1 29.1C-48.7 27.7 -49.3 26.2 -49.9 24.8C-50.4 23.3 -50.9 21.8 -51.3 20.3C-51.7 18.8 -52 17.3 -52.3 15.8C-52.6 14.3 -52.8 12.8 -53 11.3C-53.1 9.7 -53.2 8.2 -53.2 6.7C-53.2 5.2 -53.2 3.7 -53.1 2.2C-53 0.7 -52.8 -0.7 -52.6 -2.2C-52.3 -3.7 -52 -5.1 -51.7 -6.5C-51.3 -8 -50.9 -9.4 -50.5 -10.7C-50 -12.1 -49.5 -13.5 -48.9 -14.8C-48.3 -16.1 -47.7 -17.4 -47 -18.6C-46.4 -19.9 -45.6 -21.1 -44.8 -22.3C-44.1 -23.4 -43.3 -24.6 -42.4 -25.7C-41.5 -26.8 -40.6 -27.8 -39.7 -28.8C-38.7 -29.8 -37.8 -30.8 -36.7 -31.7C-35.7 -32.6 -34.7 -33.5 -33.6 -34.3C-32.5 -35.1 -31.4 -35.9 -30.3 -36.6C-29.2 -37.3 -28 -38 -26.8 -38.6C-25.7 -39.2 -24.5 -39.8 -23.3 -40.3C-22 -40.8 -20.8 -41.2 -19.6 -41.6C-18.3 -42 -17.1 -42.3 -15.9 -42.6C-14.6 -42.9 -13.3 -43.1 -12.1 -43.3C-10.8 -43.5 -9.6 -43.6 -8.3 -43.7C-7.1 -43.7 -5.8 -43.7 -4.6 -43.7C-3.4 -43.7 -2.1 -43.6 -0.9 -43.4C0.3 -43.3 1.5 -43.1 2.7 -42.8C3.9 -42.6 5.1 -42.3 6.2 -42C7.3 -41.6 8.5 -41.2 9.6 -40.8C10.7 -40.4 11.7 -39.9 12.8 -39.4C13.8 -38.9 14.9 -38.3 15.8 -37.7C16.8 -37.1 17.8 -36.5 18.7 -35.8C19.6 -35.1 20.5 -34.4 21.4 -33.7C22.2 -32.9 23 -32.2 23.8 -31.4C24.6 -30.6 25.3 -29.7 26 -28.9C26.7 -28 27.3 -27.1 27.9 -26.2C28.6 -25.4 29.1 -24.4 29.6 -23.5C30.2 -22.6 30.6 -21.6 31.1 -20.7C31.5 -19.7 31.9 -18.7 32.3 -17.7C32.6 -16.8 32.9 -15.8 33.2 -14.8C33.4 -13.8 33.6 -12.8 33.8 -11.8C34 -10.8 34.1 -9.8 34.2 -8.8C34.3 -7.8 34.3 -6.8 34.3 -5.8C34.3 -4.8 34.2 -3.8 34.1 -2.9C34.1 -1.9 33.9 -0.9 33.8 -0C33.6 0.9 33.4 1.9 33.1 2.8C32.9 3.7 32.6 4.6 32.3 5.5C32 6.3 31.6 7.2 31.2 8C30.8 8.8 30.4 9.6 29.9 10.4C29.5 11.2 29 12 28.5 12.7C28 13.4 27.5 14.1 26.9 14.8C26.3 15.5 25.7 16.1 25.1 16.7C24.5 17.3 23.9 17.9 23.3 18.4C22.6 19 21.9 19.5 21.3 20C20.6 20.4 19.9 20.9 19.2 21.3C18.5 21.7 17.7 22.1 17 22.4C16.3 22.8 15.5 23.1 14.8 23.3C14.1 23.6 13.3 23.8 12.6 24C11.8 24.2 11.1 24.4 10.3 24.5C9.6 24.7 8.8 24.8 8.1 24.8C7.3 24.9 6.6 24.9 5.8 24.9C5.1 24.9 4.4 24.9 3.7 24.8C2.9 24.7 2.2 24.6 1.5 24.5C0.8 24.4 0.2 24.2 -0.5 24.1C-1.2 23.9 -1.8 23.7 -2.5 23.4C-3.1 23.2 -3.7 22.9 -4.3 22.6C-4.9 22.3 -5.5 22 -6.1 21.7C-6.6 21.4 -7.2 21 -7.7 20.6C-8.2 20.3 -8.7 19.9 -9.2 19.5C-9.6 19.1 -10.1 18.6 -10.5 18.2C-10.9 17.7 -11.3 17.3 -11.7 16.8C-12.1 16.4 -12.4 15.9 -12.7 15.4C-13.1 14.9 -13.4 14.4 -13.6 13.9C-13.9 13.4 -14.1 12.9 -14.4 12.4C-14.6 11.9 -14.8 11.4 -14.9 10.8C-15.1 10.3 -15.2 9.8 -15.3 9.3C-15.5 8.8 -15.5 8.3 -15.6 7.7C-15.7 7.2 -15.7 6.7 -15.7 6.2C-15.7 5.7 -15.7 5.2 -15.7 4.7C-15.7 4.3 -15.6 3.8 -15.6 3.3C-15.5 2.8 -15.4 2.4 -15.3 1.9C-15.2 1.5 -15 1 -14.9 0.6C-14.7 0.2 -14.5 -0.2 -14.4 -0.6C-14.2 -1 -14 -1.4 -13.8 -1.7C-13.5 -2.1 -13.3 -2.4 -13.1 -2.8C-12.8 -3.1 -12.6 -3.4 -12.3 -3.7C-12 -4 -11.7 -4.3 -11.5 -4.5C-11.2 -4.8 -10.9 -5 -10.6 -5.3C-10.3 -5.5 -10 -5.7 -9.7 -5.9C-9.4 -6 -8.9 -6.3 -8.7 -6.3"/> | |
| 6 | <path id="beads" d="M52 30.4a2.8 2.8 0 1 0-5.6 0a2.8 2.8 0 1 0 5.6 0M43.9 39a2.8 2.8 0 1 0-5.6 0a2.8 2.8 0 1 0 5.6 0M34.5 45.5a2.8 2.8 0 1 0-5.6 0a2.8 2.8 0 1 0 5.6 0M24.1 50a2.8 2.8 0 1 0-5.6 0a2.8 2.8 0 1 0 5.6 0M13.3 52.1a2.8 2.8 0 1 0-5.6 0a2.8 2.8 0 1 0 5.6 0M2.5 52a2.8 2.8 0 1 0-5.6 0a2.8 2.8 0 1 0 5.6 0M-7.8 49.7a2.8 2.8 0 1 0-5.6 0a2.8 2.8 0 1 0 5.6 0M-17.2 45.4a2.8 2.8 0 1 0-5.6 0a2.8 2.8 0 1 0 5.6 0M-25.3 39.5a2.8 2.8 0 1 0-5.6 0a2.8 2.8 0 1 0 5.6 0M-34.5 28.1a2.8 2.8 0 1 0-5.6 0a2.8 2.8 0 1 0 5.6 0M-39.6 14.9a2.8 2.8 0 1 0-5.6 0a2.8 2.8 0 1 0 5.6 0M-40.4 1.3a2.8 2.8 0 1 0-5.6 0a2.8 2.8 0 1 0 5.6 0M-37.1 -11.3a2.8 2.8 0 1 0-5.6 0a2.8 2.8 0 1 0 5.6 0M-30.3 -21.8a2.8 2.8 0 1 0-5.6 0a2.8 2.8 0 1 0 5.6 0M-21.1 -29.5a2.8 2.8 0 1 0-5.6 0a2.8 2.8 0 1 0 5.6 0M-10.4 -33.7a2.8 2.8 0 1 0-5.6 0a2.8 2.8 0 1 0 5.6 0M0.5 -34.3a2.8 2.8 0 1 0-5.6 0a2.8 2.8 0 1 0 5.6 0M10.5 -31.7a2.8 2.8 0 1 0-5.6 0a2.8 2.8 0 1 0 5.6 0M21.1 -24.2a2.8 2.8 0 1 0-5.6 0a2.8 2.8 0 1 0 5.6 0M27.2 -13.7a2.8 2.8 0 1 0-5.6 0a2.8 2.8 0 1 0 5.6 0M28.3 -2.7a2.8 2.8 0 1 0-5.6 0a2.8 2.8 0 1 0 5.6 0M25 7a2.8 2.8 0 1 0-5.6 0a2.8 2.8 0 1 0 5.6 0M16.7 14.8a2.8 2.8 0 1 0-5.6 0a2.8 2.8 0 1 0 5.6 0M7 16.9a2.8 2.8 0 1 0-5.6 0a2.8 2.8 0 1 0 5.6 0M-2.2 13a2.8 2.8 0 1 0-5.6 0a2.8 2.8 0 1 0 5.6 0M-5.8 3.4a2.8 2.8 0 1 0-5.6 0a2.8 2.8 0 1 0 5.6 0"/> | |
| 7 | </defs> | |
| 8 | <rect width="594" height="773" fill="url(#edge)"/> | |
| 9 | <clipPath id="sheet"><path d="M600 800L6.7 800L6 796L7.5 792L6.3 788L5.8 784L7.5 780L10.1 776L9.5 772L9.3 768L6.6 764L8.2 760L6.9 756L6.4 752L6 748L6.6 744L10.1 740L9.6 736L9.5 732L9.5 728L6.5 724L7 720L8.6 716L9.2 712L9.8 708L9.9 704L5.9 700L8.5 696L8.9 692L8 688L6.4 684L7.9 680L5.9 676L10.2 672L9.8 668L8.2 664L7 660L10 656L8.4 652L9.9 648L9.7 644L8 640L7.6 636L8.5 632L7.7 628L6.3 624L7 620L9.6 616L5.7 612L5.7 608L8.6 604L6.9 600L8.2 596L7.9 592L7.2 588L10.5 584L6.5 580L7.6 576L6.5 572L8.7 568L6.9 564L7.3 560L9.2 556L7.1 552L8.3 548L10 544L6 540L5.8 536L6.6 532L9.3 528L8.6 524L6.7 520L7.2 516L6.4 512L7.8 508L5.7 504L9 500L10 496L10.3 492L9.2 488L10.3 484L5.6 480L6.9 476L10.3 472L9.4 468L7.6 464L10.2 460L8.6 456L9.6 452L7 448L6.5 444L7.7 440L6.2 436L7.4 432L10.3 428L7.2 424L5.5 420L5.7 416L6.3 412L9.4 408L7.3 404L7 400L6 396L10.4 392L7.6 388L6.5 384L5.8 380L5.8 376L6.3 372L8.9 368L6.2 364L5.7 360L8 356L6.7 352L10.5 348L6.1 344L8.1 340L9.4 336L7.5 332L10.4 328L7.9 324L6.7 320L7.6 316L5.7 312L7.6 308L6.7 304L9.9 300L9.7 296L8 292L5.7 288L6.8 284L6.7 280L6.5 276L6.7 272L9.8 268L6.2 264L5.8 260L10.1 256L8.3 252L10.5 248L7.5 244L10 240L8.8 236L9.5 232L9.2 228L8 224L6 220L6.6 216L9.9 212L10 208L10.1 204L7.2 200L8.8 196L9.5 192L8.7 188L9.6 184L8.1 180L8.8 176L8.9 172L6.8 168L10.1 164L10.3 160L5.9 156L10.4 152L10.3 148L8.8 144L5.7 140L10 136L6.1 132L10.3 128L8.8 124L5.8 120L6.3 116L8.7 112L8.3 108L9.2 104L10.1 100L6.6 96L5.5 92L10.1 88L5.6 84L9.9 80L6.1 76L9.5 72L9.4 68L9.9 64L8.3 60L9.9 56L6.5 52L8.9 48L7.2 44L10 40L9.4 36L7.9 32L8.1 28L5.6 24L5.7 20L8.5 16L7.9 12L9.8 8L8 7.5L12 5.2L16 6.3L20 8.3L24 7.1L28 4.6L32 8.7L36 8.6L40 4.9L44 7.2L48 6.4L52 8.4L56 6.1L60 5.7L64 6.9L68 9.3L72 5L76 5.1L80 7.6L84 8.9L88 7.1L92 6.7L96 8.8L100 8.4L104 4.8L108 8.9L112 5.5L116 6L120 8.7L124 6.6L128 8.5L132 5.3L136 8.9L140 5.4L144 5.2L148 7L152 6.2L156 7.2L160 9L164 8.1L168 4.5L172 6.1L176 7.2L180 6.9L184 8.1L188 6.9L192 4.9L196 5.7L200 8.7L204 6.3L208 8.3L212 9.4L216 7.6L220 7.9L224 7.5L228 6.1L232 9.1L236 6.8L240 9.1L244 6L248 8.8L252 8.4L256 7.6L260 6.7L264 5.2L268 8.4L272 6.3L276 7.8L280 5.2L284 4.9L288 5.2L292 8.5L296 5.4L300 9L304 6.4L308 7.4L312 6.3L316 7.6L320 5L324 6.5L328 9.2L332 5.4L336 7.8L340 6.1L344 6L348 4.6L352 4.6L356 9.2L360 8.6L364 8.5L368 8.5L372 9.3L376 5.3L380 7.4L384 7L388 7.4L392 9.2L396 8.3L400 9.3L404 5.1L408 7.8L412 7.9L416 8.2L420 7.6L424 8.7L428 6L432 9.1L436 6.5L440 7.5L444 9L448 8L452 6L456 5.7L460 6.1L464 7.6L468 9.5L472 9L476 6.5L480 6.5L484 8.6L488 5.9L492 6.6L496 4.6L500 5.4L504 7.2L508 8L512 7.6L516 6.3L520 9.3L524 7.6L528 5.3L532 4.8L536 9.4L540 9.4L544 9.1L548 7.5L552 6.1L556 5L560 5.8L564 5.6L568 9.1L572 9L576 8.4L580 5.2L584 5.7L588 6L592 9.2L596 5.3L600 7Z"/></clipPath> | |
| 10 | <path d="M600 800L6.7 800L6 796L7.5 792L6.3 788L5.8 784L7.5 780L10.1 776L9.5 772L9.3 768L6.6 764L8.2 760L6.9 756L6.4 752L6 748L6.6 744L10.1 740L9.6 736L9.5 732L9.5 728L6.5 724L7 720L8.6 716L9.2 712L9.8 708L9.9 704L5.9 700L8.5 696L8.9 692L8 688L6.4 684L7.9 680L5.9 676L10.2 672L9.8 668L8.2 664L7 660L10 656L8.4 652L9.9 648L9.7 644L8 640L7.6 636L8.5 632L7.7 628L6.3 624L7 620L9.6 616L5.7 612L5.7 608L8.6 604L6.9 600L8.2 596L7.9 592L7.2 588L10.5 584L6.5 580L7.6 576L6.5 572L8.7 568L6.9 564L7.3 560L9.2 556L7.1 552L8.3 548L10 544L6 540L5.8 536L6.6 532L9.3 528L8.6 524L6.7 520L7.2 516L6.4 512L7.8 508L5.7 504L9 500L10 496L10.3 492L9.2 488L10.3 484L5.6 480L6.9 476L10.3 472L9.4 468L7.6 464L10.2 460L8.6 456L9.6 452L7 448L6.5 444L7.7 440L6.2 436L7.4 432L10.3 428L7.2 424L5.5 420L5.7 416L6.3 412L9.4 408L7.3 404L7 400L6 396L10.4 392L7.6 388L6.5 384L5.8 380L5.8 376L6.3 372L8.9 368L6.2 364L5.7 360L8 356L6.7 352L10.5 348L6.1 344L8.1 340L9.4 336L7.5 332L10.4 328L7.9 324L6.7 320L7.6 316L5.7 312L7.6 308L6.7 304L9.9 300L9.7 296L8 292L5.7 288L6.8 284L6.7 280L6.5 276L6.7 272L9.8 268L6.2 264L5.8 260L10.1 256L8.3 252L10.5 248L7.5 244L10 240L8.8 236L9.5 232L9.2 228L8 224L6 220L6.6 216L9.9 212L10 208L10.1 204L7.2 200L8.8 196L9.5 192L8.7 188L9.6 184L8.1 180L8.8 176L8.9 172L6.8 168L10.1 164L10.3 160L5.9 156L10.4 152L10.3 148L8.8 144L5.7 140L10 136L6.1 132L10.3 128L8.8 124L5.8 120L6.3 116L8.7 112L8.3 108L9.2 104L10.1 100L6.6 96L5.5 92L10.1 88L5.6 84L9.9 80L6.1 76L9.5 72L9.4 68L9.9 64L8.3 60L9.9 56L6.5 52L8.9 48L7.2 44L10 40L9.4 36L7.9 32L8.1 28L5.6 24L5.7 20L8.5 16L7.9 12L9.8 8L8 7.5L12 5.2L16 6.3L20 8.3L24 7.1L28 4.6L32 8.7L36 8.6L40 4.9L44 7.2L48 6.4L52 8.4L56 6.1L60 5.7L64 6.9L68 9.3L72 5L76 5.1L80 7.6L84 8.9L88 7.1L92 6.7L96 8.8L100 8.4L104 4.8L108 8.9L112 5.5L116 6L120 8.7L124 6.6L128 8.5L132 5.3L136 8.9L140 5.4L144 5.2L148 7L152 6.2L156 7.2L160 9L164 8.1L168 4.5L172 6.1L176 7.2L180 6.9L184 8.1L188 6.9L192 4.9L196 5.7L200 8.7L204 6.3L208 8.3L212 9.4L216 7.6L220 7.9L224 7.5L228 6.1L232 9.1L236 6.8L240 9.1L244 6L248 8.8L252 8.4L256 7.6L260 6.7L264 5.2L268 8.4L272 6.3L276 7.8L280 5.2L284 4.9L288 5.2L292 8.5L296 5.4L300 9L304 6.4L308 7.4L312 6.3L316 7.6L320 5L324 6.5L328 9.2L332 5.4L336 7.8L340 6.1L344 6L348 4.6L352 4.6L356 9.2L360 8.6L364 8.5L368 8.5L372 9.3L376 5.3L380 7.4L384 7L388 7.4L392 9.2L396 8.3L400 9.3L404 5.1L408 7.8L412 7.9L416 8.2L420 7.6L424 8.7L428 6L432 9.1L436 6.5L440 7.5L444 9L448 8L452 6L456 5.7L460 6.1L464 7.6L468 9.5L472 9L476 6.5L480 6.5L484 8.6L488 5.9L492 6.6L496 4.6L500 5.4L504 7.2L508 8L512 7.6L516 6.3L520 9.3L524 7.6L528 5.3L532 4.8L536 9.4L540 9.4L544 9.1L548 7.5L552 6.1L556 5L560 5.8L564 5.6L568 9.1L572 9L576 8.4L580 5.2L584 5.7L588 6L592 9.2L596 5.3L600 7Z" fill="url(#sheet-fill)"/> | |
| 11 | <g clip-path="url(#sheet)"> | |
| 12 | <g fill="none" stroke-linecap="round"> | |
| 13 | <g stroke="#cfdff0" stroke-width="14"> | |
| 14 | <use href="#band" transform="translate(1.5 2) translate(-21.7 11.8) rotate(238) scale(1.12 -1.12)" opacity="1.00"/> | |
| 15 | <use href="#band" transform="translate(1.5 2) translate(92.3 12.6) rotate(262) scale(0.91 -0.91)" opacity="1.00"/> | |
| 16 | <use href="#band" transform="translate(1.5 2) translate(208.7 18.8) rotate(155) scale(0.89 0.89)" opacity="1.00"/> | |
| 17 | <use href="#band" transform="translate(1.5 2) translate(301.3 19.3) rotate(21) scale(1.05 -1.05)" opacity="1.00"/> | |
| 18 | <use href="#band" transform="translate(1.5 2) translate(444.7 14.6) rotate(80) scale(1.03 1.03)" opacity="0.30"/> | |
| 19 | <use href="#band" transform="translate(1.5 2) translate(46 108.8) rotate(100) scale(1.03 1.03)" opacity="1.00"/> | |
| 20 | <use href="#band" transform="translate(1.5 2) translate(153.1 114) rotate(234) scale(1.05 -1.05)" opacity="1.00"/> | |
| 21 | <use href="#band" transform="translate(1.5 2) translate(256.6 115.5) rotate(42) scale(1.12 -1.12)" opacity="1.00"/> | |
| 22 | <use href="#band" transform="translate(1.5 2) translate(365.3 105.9) rotate(35) scale(0.94 0.94)" opacity="0.69"/> | |
| 23 | <use href="#band" transform="translate(1.5 2) translate(480.4 124.4) rotate(8) scale(0.97 0.97)" opacity="0.06"/> | |
| 24 | <use href="#band" transform="translate(1.5 2) translate(-22.9 220) rotate(345) scale(0.92 -0.92)" opacity="1.00"/> | |
| 25 | <use href="#band" transform="translate(1.5 2) translate(86.1 212.3) rotate(209) scale(0.87 0.87)" opacity="1.00"/> | |
| 26 | <use href="#band" transform="translate(1.5 2) translate(190.8 224.2) rotate(238) scale(0.86 -0.86)" opacity="1.00"/> | |
| 27 | <use href="#band" transform="translate(1.5 2) translate(304.4 219.8) rotate(97) scale(0.99 0.99)" opacity="0.83"/> | |
| 28 | <use href="#band" transform="translate(1.5 2) translate(425.1 214.5) rotate(108) scale(0.97 0.97)" opacity="0.24"/> | |
| 29 | <use href="#band" transform="translate(1.5 2) translate(25.6 318.8) rotate(328) scale(1.10 1.10)" opacity="1.00"/> | |
| 30 | <use href="#band" transform="translate(1.5 2) translate(131.5 311.4) rotate(4) scale(0.98 -0.98)" opacity="1.00"/> | |
| 31 | <use href="#band" transform="translate(1.5 2) translate(271.9 318.6) rotate(106) scale(0.88 0.88)" opacity="0.74"/> | |
| 32 | <use href="#band" transform="translate(1.5 2) translate(354.6 318.1) rotate(65) scale(1.03 -1.03)" opacity="0.39"/> | |
| 33 | <use href="#band" transform="translate(1.5 2) translate(-11.4 439.2) rotate(198) scale(0.92 -0.92)" opacity="0.90"/> | |
| 34 | <use href="#band" transform="translate(1.5 2) translate(95.7 423.6) rotate(19) scale(0.89 -0.89)" opacity="0.89"/> | |
| 35 | <use href="#band" transform="translate(1.5 2) translate(194.6 432.1) rotate(305) scale(0.93 -0.93)" opacity="0.64"/> | |
| 36 | <use href="#band" transform="translate(1.5 2) translate(308.6 416.3) rotate(35) scale(1.06 1.06)" opacity="0.31"/> | |
| 37 | <use href="#band" transform="translate(1.5 2) translate(27.7 528.4) rotate(90) scale(0.88 -0.88)" opacity="0.53"/> | |
| 38 | <use href="#band" transform="translate(1.5 2) translate(143.3 532.2) rotate(185) scale(0.90 0.90)" opacity="0.38"/> | |
| 39 | <use href="#band" transform="translate(1.5 2) translate(-18.3 641.1) rotate(66) scale(1.11 -1.11)" opacity="0.06"/> | |
| 40 | <use href="#band" transform="translate(1.5 2) translate(86.7 639.6) rotate(213) scale(1.11 1.11)" opacity="0.03"/> | |
| 41 | </g> | |
| 42 | <g stroke="#f7fafd" stroke-width="11"> | |
| 43 | <use href="#band" transform="translate(-21.7 11.8) rotate(238) scale(1.12 -1.12)" opacity="1.00"/> | |
| 44 | <use href="#band" transform="translate(92.3 12.6) rotate(262) scale(0.91 -0.91)" opacity="1.00"/> | |
| 45 | <use href="#band" transform="translate(208.7 18.8) rotate(155) scale(0.89 0.89)" opacity="1.00"/> | |
| 46 | <use href="#band" transform="translate(301.3 19.3) rotate(21) scale(1.05 -1.05)" opacity="1.00"/> | |
| 47 | <use href="#band" transform="translate(444.7 14.6) rotate(80) scale(1.03 1.03)" opacity="0.30"/> | |
| 48 | <use href="#band" transform="translate(46 108.8) rotate(100) scale(1.03 1.03)" opacity="1.00"/> | |
| 49 | <use href="#band" transform="translate(153.1 114) rotate(234) scale(1.05 -1.05)" opacity="1.00"/> | |
| 50 | <use href="#band" transform="translate(256.6 115.5) rotate(42) scale(1.12 -1.12)" opacity="1.00"/> | |
| 51 | <use href="#band" transform="translate(365.3 105.9) rotate(35) scale(0.94 0.94)" opacity="0.69"/> | |
| 52 | <use href="#band" transform="translate(480.4 124.4) rotate(8) scale(0.97 0.97)" opacity="0.06"/> | |
| 53 | <use href="#band" transform="translate(-22.9 220) rotate(345) scale(0.92 -0.92)" opacity="1.00"/> | |
| 54 | <use href="#band" transform="translate(86.1 212.3) rotate(209) scale(0.87 0.87)" opacity="1.00"/> | |
| 55 | <use href="#band" transform="translate(190.8 224.2) rotate(238) scale(0.86 -0.86)" opacity="1.00"/> | |
| 56 | <use href="#band" transform="translate(304.4 219.8) rotate(97) scale(0.99 0.99)" opacity="0.83"/> | |
| 57 | <use href="#band" transform="translate(425.1 214.5) rotate(108) scale(0.97 0.97)" opacity="0.24"/> | |
| 58 | <use href="#band" transform="translate(25.6 318.8) rotate(328) scale(1.10 1.10)" opacity="1.00"/> | |
| 59 | <use href="#band" transform="translate(131.5 311.4) rotate(4) scale(0.98 -0.98)" opacity="1.00"/> | |
| 60 | <use href="#band" transform="translate(271.9 318.6) rotate(106) scale(0.88 0.88)" opacity="0.74"/> | |
| 61 | <use href="#band" transform="translate(354.6 318.1) rotate(65) scale(1.03 -1.03)" opacity="0.39"/> | |
| 62 | <use href="#band" transform="translate(-11.4 439.2) rotate(198) scale(0.92 -0.92)" opacity="0.90"/> | |
| 63 | <use href="#band" transform="translate(95.7 423.6) rotate(19) scale(0.89 -0.89)" opacity="0.89"/> | |
| 64 | <use href="#band" transform="translate(194.6 432.1) rotate(305) scale(0.93 -0.93)" opacity="0.64"/> | |
| 65 | <use href="#band" transform="translate(308.6 416.3) rotate(35) scale(1.06 1.06)" opacity="0.31"/> | |
| 66 | <use href="#band" transform="translate(27.7 528.4) rotate(90) scale(0.88 -0.88)" opacity="0.53"/> | |
| 67 | <use href="#band" transform="translate(143.3 532.2) rotate(185) scale(0.90 0.90)" opacity="0.38"/> | |
| 68 | <use href="#band" transform="translate(-18.3 641.1) rotate(66) scale(1.11 -1.11)" opacity="0.06"/> | |
| 69 | <use href="#band" transform="translate(86.7 639.6) rotate(213) scale(1.11 1.11)" opacity="0.03"/> | |
| 70 | </g> | |
| 71 | </g> | |
| 72 | <g fill="#c4d8ed"> | |
| 73 | <use href="#beads" transform="translate(-21.7 11.8) rotate(238) scale(1.12 -1.12)" opacity="1.00"/> | |
| 74 | <use href="#beads" transform="translate(92.3 12.6) rotate(262) scale(0.91 -0.91)" opacity="1.00"/> | |
| 75 | <use href="#beads" transform="translate(208.7 18.8) rotate(155) scale(0.89 0.89)" opacity="1.00"/> | |
| 76 | <use href="#beads" transform="translate(301.3 19.3) rotate(21) scale(1.05 -1.05)" opacity="1.00"/> | |
| 77 | <use href="#beads" transform="translate(444.7 14.6) rotate(80) scale(1.03 1.03)" opacity="0.30"/> | |
| 78 | <use href="#beads" transform="translate(46 108.8) rotate(100) scale(1.03 1.03)" opacity="1.00"/> | |
| 79 | <use href="#beads" transform="translate(153.1 114) rotate(234) scale(1.05 -1.05)" opacity="1.00"/> | |
| 80 | <use href="#beads" transform="translate(256.6 115.5) rotate(42) scale(1.12 -1.12)" opacity="1.00"/> | |
| 81 | <use href="#beads" transform="translate(365.3 105.9) rotate(35) scale(0.94 0.94)" opacity="0.69"/> | |
| 82 | <use href="#beads" transform="translate(480.4 124.4) rotate(8) scale(0.97 0.97)" opacity="0.06"/> | |
| 83 | <use href="#beads" transform="translate(-22.9 220) rotate(345) scale(0.92 -0.92)" opacity="1.00"/> | |
| 84 | <use href="#beads" transform="translate(86.1 212.3) rotate(209) scale(0.87 0.87)" opacity="1.00"/> | |
| 85 | <use href="#beads" transform="translate(190.8 224.2) rotate(238) scale(0.86 -0.86)" opacity="1.00"/> | |
| 86 | <use href="#beads" transform="translate(304.4 219.8) rotate(97) scale(0.99 0.99)" opacity="0.83"/> | |
| 87 | <use href="#beads" transform="translate(425.1 214.5) rotate(108) scale(0.97 0.97)" opacity="0.24"/> | |
| 88 | <use href="#beads" transform="translate(25.6 318.8) rotate(328) scale(1.10 1.10)" opacity="1.00"/> | |
| 89 | <use href="#beads" transform="translate(131.5 311.4) rotate(4) scale(0.98 -0.98)" opacity="1.00"/> | |
| 90 | <use href="#beads" transform="translate(271.9 318.6) rotate(106) scale(0.88 0.88)" opacity="0.74"/> | |
| 91 | <use href="#beads" transform="translate(354.6 318.1) rotate(65) scale(1.03 -1.03)" opacity="0.39"/> | |
| 92 | <use href="#beads" transform="translate(-11.4 439.2) rotate(198) scale(0.92 -0.92)" opacity="0.90"/> | |
| 93 | <use href="#beads" transform="translate(95.7 423.6) rotate(19) scale(0.89 -0.89)" opacity="0.89"/> | |
| 94 | <use href="#beads" transform="translate(194.6 432.1) rotate(305) scale(0.93 -0.93)" opacity="0.64"/> | |
| 95 | <use href="#beads" transform="translate(308.6 416.3) rotate(35) scale(1.06 1.06)" opacity="0.31"/> | |
| 96 | <use href="#beads" transform="translate(27.7 528.4) rotate(90) scale(0.88 -0.88)" opacity="0.53"/> | |
| 97 | <use href="#beads" transform="translate(143.3 532.2) rotate(185) scale(0.90 0.90)" opacity="0.38"/> | |
| 98 | <use href="#beads" transform="translate(-18.3 641.1) rotate(66) scale(1.11 -1.11)" opacity="0.06"/> | |
| 99 | <use href="#beads" transform="translate(86.7 639.6) rotate(213) scale(1.11 1.11)" opacity="0.03"/> | |
| 100 | </g> | |
| 101 | </g> | |
| 102 | </svg> |
crates/canvas/assets/backgrounds/blue-wave-title.svg created+5| ... | ... | @@ -0,0 +1,5 @@ |
| 1 | <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 728 77" preserveAspectRatio="none"> | |
| 2 | <path d="M-1 -1L0 80C7.1 75.8 24.7 62.4 42.4 54.9C60.1 47.5 81.3 40.3 106 35.3C130.7 30.4 162.5 26.6 190.8 25.2C219.1 23.8 245.6 25.8 275.6 27.1C305.6 28.3 337.4 31.2 371 32.7C404.6 34.2 441.7 35.4 477 36C512.3 36.6 553 37.4 583 36.3C613 35.2 636 32.8 657.2 29.5C678.4 26.1 695.2 20.3 710.2 16.1C725.2 11.9 741.1 6.2 747.3 4.2V-1Z" fill="#d6eef8"/> | |
| 3 | <path d="M-1 -1L0 68C6.9 63.9 24 50.7 41.2 43.5C58.4 36.2 79 29.3 103 24.7C127 20 157.9 16.6 185.4 15.6C212.9 14.6 238.6 16.9 267.8 18.5C297 20.2 327.9 23.4 360.5 25.3C393.1 27.2 429.2 28.9 463.5 30C497.8 31.1 537.3 32.4 566.5 31.7C595.7 31 618 28.8 638.6 25.7C659.2 22.6 675.5 17.1 690.1 13.1C704.7 9 720.1 3.5 726.1 1.6V-1Z" fill="#b8e2f8"/> | |
| 4 | <path d="M-1 -1L0 56C6.7 52 23.3 39 40 32C56.7 25 76.7 18.3 100 14C123.3 9.7 153.3 6.7 180 6C206.7 5.3 231.7 8 260 10C288.3 12 318.3 15.7 350 18C381.7 20.3 416.7 22.5 450 24C483.3 25.5 521.7 27.3 550 27C578.3 26.7 600 24.8 620 22C640 19.2 655.8 13.8 670 10C684.2 6.2 699.2 0.8 705 -1V-1Z" fill="#94d1fd"/> | |
| 5 | </svg> |
crates/canvas/assets/backgrounds/blue-waves.svg created+33| ... | ... | @@ -0,0 +1,33 @@ |
| 1 | <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 598 766" preserveAspectRatio="none"> | |
| 2 | <defs> | |
| 3 | <radialGradient id="wash" cx="0" cy="0" r="1" gradientTransform="scale(520 700)" gradientUnits="userSpaceOnUse"> | |
| 4 | <stop offset="0" stop-color="#e4eaf8"/><stop offset=".6" stop-color="#ebf0fa"/><stop offset="1" stop-color="#e8eef9" stop-opacity="0"/> | |
| 5 | </radialGradient> | |
| 6 | <radialGradient id="ridge-shadow" cx="0" cy="0" r="1" gradientTransform="scale(640 720)" gradientUnits="userSpaceOnUse"><stop offset=".55" stop-color="#d0d9f1" stop-opacity=".55"/><stop offset=".85" stop-color="#d0d9f1" stop-opacity="0"/></radialGradient> | |
| 7 | <radialGradient id="ridge" cx="0" cy="0" r="1" gradientTransform="scale(640 720)" gradientUnits="userSpaceOnUse"><stop offset=".55" stop-color="#ffffff"/><stop offset=".85" stop-color="#ffffff" stop-opacity="0"/></radialGradient> | |
| 8 | </defs> | |
| 9 | <rect width="598" height="766" fill="url(#wash)"/> | |
| 10 | <g fill="none" stroke-linecap="round"> | |
| 11 | <g stroke="url(#ridge-shadow)" stroke-width="11"> | |
| 12 | <path d="M109.6 -10C110 -9.2 112 -6.7 112.1 -5C112.2 -3.3 111.3 -1.7 110.2 0C109 1.7 107 3.3 105.3 5C103.6 6.7 101.4 8.3 99.9 10C98.5 11.7 97.1 13.3 96.6 15C96.2 16.7 96.4 18.3 97.4 20C98.5 21.7 100.5 23.3 103 25C105.6 26.7 109.2 28.3 113 30C116.7 31.7 121.2 33.3 125.3 35C129.5 36.7 134 38.3 137.7 40C141.4 41.7 144.9 43.3 147.5 45C150 46.7 151.9 48.3 153 50C154 51.7 154 53.3 153.6 55C153.1 56.7 151.7 58.3 150.2 60C148.7 61.7 146.5 63.3 144.8 65C143.1 66.7 141.1 68.3 140 70C138.9 71.7 138 73.3 138.2 75C138.3 76.7 139.2 78.3 140.8 80C142.5 81.7 145.2 83.3 148.3 85C151.4 86.7 155.5 88.3 159.5 90C163.5 91.7 168.1 93.3 172.2 95C176.2 96.7 180.5 98.3 183.8 100C187.1 101.7 190.1 103.3 192 105C194 106.7 195.1 108.3 195.5 110C195.9 111.7 195.3 113.3 194.4 115C193.4 116.7 191.6 118.3 189.9 120C188.3 121.7 186 123.3 184.4 125C182.9 126.7 181.2 128.3 180.6 130C179.9 131.7 179.6 133.3 180.4 135C181.2 136.7 182.8 138.3 185.1 140C187.4 141.7 190.7 143.3 194.2 145C197.8 146.7 202.2 148.3 206.3 150C210.4 151.7 215 153.3 218.9 155C222.7 156.7 226.5 158.3 229.4 160C232.2 161.7 234.4 163.3 235.8 165C237.1 166.7 237.5 168.3 237.4 170C237.2 171.7 236 173.3 234.6 175C233.3 176.7 231.2 178.3 229.5 180C227.7 181.7 225.6 183.3 224.3 185C223 186.7 221.9 188.3 221.7 190C221.6 191.7 222.1 193.3 223.4 195C224.8 196.7 227.2 198.3 230 200C232.9 201.7 236.7 203.3 240.6 205C244.5 206.7 249.1 208.3 253.2 210C257.3 211.7 261.7 213.3 265.2 215C268.7 216.7 272 218.3 274.2 220C276.5 221.7 278 223.3 278.8 225C279.5 226.7 279.2 228.3 278.5 230C277.8 231.7 276.1 233.3 274.5 235C272.9 236.7 270.7 238.3 269 240C267.4 241.7 265.5 243.3 264.6 245C263.7 246.7 263.1 248.3 263.6 250C264 251.7 265.3 253.3 267.3 255C269.3 256.7 272.3 258.3 275.6 260C279 261.7 283.2 263.3 287.3 265C291.3 266.7 296 268.3 300 270C303.9 271.7 308 273.3 311 275C314.1 276.7 316.8 278.3 318.4 280C320 281.7 320.8 283.3 320.9 285C321 286.7 320.1 288.3 319 290C317.8 291.7 315.8 293.3 314.1 295C312.4 296.7 310.2 298.3 308.7 300C307.3 301.7 305.9 303.3 305.4 305C305 306.7 305.2 308.3 306.2 310C307.3 311.7 309.3 313.3 311.8 315C314.4 316.7 318 318.3 321.8 320C325.5 321.7 330 323.3 334.1 325C338.3 326.7 342.8 328.3 346.5 330C350.2 331.7 353.7 333.3 356.3 335C358.8 336.7 360.7 338.3 361.8 340C362.8 341.7 362.8 343.3 362.4 345C361.9 346.7 360.5 348.3 359 350C357.5 351.7 355.3 353.3 353.6 355C351.9 356.7 349.9 358.3 348.8 360C347.7 361.7 346.8 363.3 347 365C347.1 366.7 348 368.3 349.6 370C351.3 371.7 354 373.3 357.1 375C360.2 376.7 364.3 378.3 368.3 380C372.3 381.7 376.9 383.3 381 385C385 386.7 389.3 388.3 392.6 390C395.9 391.7 398.9 393.3 400.8 395C402.8 396.7 403.9 398.3 404.3 400C404.7 401.7 404.1 403.3 403.2 405C402.2 406.7 400.4 408.3 398.7 410C397.1 411.7 394.8 413.3 393.2 415C391.7 416.7 390 418.3 389.4 420C388.7 421.7 388.4 423.3 389.2 425C390 426.7 391.6 428.3 393.9 430C396.2 431.7 399.5 433.3 403 435C406.6 436.7 411 438.3 415.1 440C419.2 441.7 423.8 443.3 427.7 445C431.5 446.7 435.3 448.3 438.2 450C441 451.7 443.2 453.3 444.6 455C445.9 456.7 446.3 458.3 446.2 460C446 461.7 444.8 463.3 443.4 465C442.1 466.7 440 468.3 438.3 470C436.5 471.7 434 474.2 433.1 475" transform="translate(-6 5)" opacity="1"/> | |
| 13 | <path d="M52.7 -10C54.6 -9.2 60.8 -6.7 64 -5C67.1 -3.3 69.9 -1.7 71.7 0C73.4 1.7 74.4 3.3 74.6 5C74.8 6.7 74 8.3 72.9 10C71.9 11.7 69.9 13.3 68.2 15C66.5 16.7 64.3 18.3 62.8 20C61.3 21.7 59.8 23.3 59.3 25C58.7 26.7 58.7 28.3 59.7 30C60.6 31.7 62.4 33.3 64.9 35C67.4 36.7 70.9 38.3 74.6 40C78.2 41.7 82.7 43.3 86.8 45C91 46.7 95.5 48.3 99.3 50C103 51.7 106.7 53.3 109.3 55C112 56.7 114.1 58.3 115.2 60C116.3 61.7 116.5 63.3 116.2 65C115.8 66.7 114.5 68.3 113 70C111.6 71.7 109.4 73.3 107.7 75C106 76.7 104 78.3 102.8 80C101.6 81.7 100.6 83.3 100.6 85C100.7 86.7 101.4 88.3 102.9 90C104.5 91.7 107.1 93.3 110.1 95C113.1 96.7 117.1 98.3 121 100C125 101.7 129.6 103.3 133.7 105C137.8 106.7 142.1 108.3 145.5 110C148.9 111.7 151.9 113.3 154 115C156.1 116.7 157.4 118.3 157.9 120C158.4 121.7 157.9 123.3 157.1 125C156.2 126.7 154.4 128.3 152.8 130C151.2 131.7 148.9 133.3 147.3 135C145.7 136.7 144 138.3 143.2 140C142.5 141.7 142.1 143.3 142.7 145C143.4 146.7 144.8 148.3 147 150C149.2 151.7 152.4 153.3 155.9 155C159.3 156.7 163.7 158.3 167.8 160C171.9 161.7 176.5 163.3 180.4 165C184.3 166.7 188.2 168.3 191.1 170C194.1 171.7 196.5 173.3 197.9 175C199.4 176.7 200 178.3 199.9 180C199.8 181.7 198.7 183.3 197.4 185C196.2 186.7 194.1 188.3 192.4 190C190.6 191.7 188.5 193.3 187.1 195C185.8 196.7 184.5 198.3 184.3 200C184 201.7 184.3 203.3 185.6 205C186.9 206.7 189.1 208.3 191.8 210C194.6 211.7 198.3 213.3 202.2 215C206 216.7 210.6 218.3 214.7 220C218.8 221.7 223.2 223.3 226.8 225C230.4 226.7 233.8 228.3 236.2 230C238.5 231.7 240.2 233.3 241.1 235C241.9 236.7 241.7 238.3 241.1 240C240.5 241.7 238.9 243.3 237.4 245C235.8 246.7 233.6 248.3 231.9 250C230.2 251.7 228.3 253.3 227.3 255C226.3 256.7 225.7 258.3 226 260C226.3 261.7 227.4 263.3 229.3 265C231.2 266.7 234.1 268.3 237.3 270C240.6 271.7 244.8 273.3 248.8 275C252.8 276.7 257.5 278.3 261.5 280C265.5 281.7 269.6 283.3 272.8 285C275.9 286.7 278.7 288.3 280.5 290C282.2 291.7 283.2 293.3 283.4 295C283.6 296.7 282.8 298.3 281.7 300C280.7 301.7 278.7 303.3 277 305C275.3 306.7 273.1 308.3 271.6 310C270.1 311.7 268.6 313.3 268.1 315C267.5 316.7 267.5 318.3 268.5 320C269.4 321.7 271.2 323.3 273.7 325C276.2 326.7 279.7 328.3 283.4 330C287 331.7 291.5 333.3 295.6 335C299.8 336.7 304.3 338.3 308.1 340C311.8 341.7 315.5 343.3 318.1 345C320.8 346.7 322.9 348.3 324 350C325.1 351.7 325.3 353.3 325 355C324.6 356.7 323.3 358.3 321.8 360C320.4 361.7 318.2 363.3 316.5 365C314.8 366.7 312.8 368.3 311.6 370C310.4 371.7 309.8 374.2 309.4 375" transform="translate(-6 5)" opacity="0.75"/> | |
| 14 | <path d="M-9.8 -10C-7.9 -9.2 -2.5 -6.7 1.5 -5C5.5 -3.3 10.2 -1.7 14.2 0C18.2 1.7 22.4 3.3 25.7 5C28.9 6.7 31.8 8.3 33.7 10C35.6 11.7 36.7 13.3 37 15C37.3 16.7 36.7 18.3 35.7 20C34.7 21.7 32.8 23.3 31.1 25C29.4 26.7 27.2 28.3 25.6 30C24.1 31.7 22.5 33.3 21.9 35C21.3 36.7 21.1 38.3 21.9 40C22.8 41.7 24.5 43.3 26.8 45C29.2 46.7 32.6 48.3 36.2 50C39.8 51.7 44.2 53.3 48.3 55C52.4 56.7 57 58.3 60.8 60C64.7 61.7 68.4 63.3 71.2 65C73.9 66.7 76.1 68.3 77.4 70C78.7 71.7 79 73.3 78.7 75C78.5 76.7 77.2 78.3 75.9 80C74.5 81.7 72.4 83.3 70.6 85C68.9 86.7 66.8 88.3 65.6 90C64.3 91.7 63.2 93.3 63.1 95C63 96.7 63.6 98.3 65.1 100C66.5 101.7 68.9 103.3 71.9 105C74.8 106.7 78.7 108.3 82.6 110C86.5 111.7 91.1 113.3 95.2 115C99.3 116.7 103.7 118.3 107.1 120C110.6 121.7 113.8 123.3 116 125C118.2 126.7 119.6 128.3 120.3 130C120.9 131.7 120.5 133.3 119.8 135C119 136.7 117.3 138.3 115.7 140C114.1 141.7 111.8 143.3 110.2 145C108.6 146.7 106.8 148.3 105.9 150C105.1 151.7 104.6 153.3 105.1 155C105.6 156.7 106.9 158.3 109 160C111.1 161.7 114.1 163.3 117.5 165C120.9 166.7 125.2 168.3 129.3 170C133.4 171.7 138 173.3 142 175C145.9 176.7 149.9 178.3 152.9 180C155.9 181.7 158.5 183.3 160.1 185C161.6 186.7 162.3 188.3 162.4 190C162.4 191.7 161.4 193.3 160.2 195C159 196.7 157 198.3 155.3 200C153.6 201.7 151.4 203.3 150 205C148.5 206.7 147.2 208.3 146.8 210C146.5 211.7 146.7 213.3 147.8 215C148.9 216.7 151 218.3 153.7 220C156.3 221.7 160 223.3 163.7 225C167.5 226.7 172 228.3 176.2 230C180.3 231.7 184.8 233.3 188.4 235C192.1 236.7 195.6 238.3 198.1 240C200.6 241.7 202.4 243.3 203.3 245C204.3 246.7 204.3 248.3 203.7 250C203.2 251.7 201.7 253.3 200.2 255C198.7 256.7 196.5 258.3 194.8 260C193.1 261.7 191.1 263.3 190.1 265C189 266.7 188.2 268.3 188.4 270C188.6 271.7 189.6 273.3 191.3 275C193.1 276.7 195.9 278.3 199 280C202.2 281.7 206.3 283.3 210.3 285C214.3 286.7 219 288.3 223 290C227 291.7 232.6 294.2 234.5 295" transform="translate(-6 5)" opacity="0.55"/> | |
| 15 | <path d="M-24.3 10C-22.3 10.8 -16 13.3 -12.6 15C-9.3 16.7 -6.3 18.3 -4.3 20C-2.3 21.7 -1 23.3 -0.6 25C-0.1 26.7 -0.7 28.3 -1.6 30C-2.5 31.7 -4.4 33.3 -6 35C-7.7 36.7 -9.9 38.3 -11.5 40C-13.1 41.7 -14.8 43.3 -15.5 45C-16.2 46.7 -16.5 48.3 -15.8 50C-15 51.7 -13.5 53.3 -11.2 55C-9 56.7 -5.7 58.3 -2.2 60C1.3 61.7 5.7 63.3 9.8 65C13.9 66.7 18.5 68.3 22.4 70C26.3 71.7 30.1 73.3 33 75C35.9 76.7 38.2 78.3 39.6 80C40.9 81.7 41.4 83.3 41.3 85C41.1 86.7 40 88.3 38.7 90C37.4 91.7 35.3 93.3 33.5 95C31.8 96.7 29.7 98.3 28.4 100C27 101.7 25.8 103.3 25.6 105C25.5 106.7 25.9 108.3 27.2 110C28.6 111.7 30.8 113.3 33.7 115C36.5 116.7 40.3 118.3 44.1 120C48 121.7 52.6 123.3 56.7 125C60.8 126.7 65.2 128.3 68.8 130C72.3 131.7 75.6 133.3 77.9 135C80.2 136.7 81.8 138.3 82.6 140C83.3 141.7 83.1 143.3 82.4 145C81.8 146.7 80.1 148.3 78.6 150C77 151.7 74.7 153.3 73.1 155C71.4 156.7 69.6 158.3 68.6 160C67.7 161.7 67.1 163.3 67.5 165C67.9 166.7 69 168.3 71 170C72.9 171.7 75.9 173.3 79.2 175C82.5 176.7 86.8 178.3 90.8 180C94.9 181.7 99.5 183.3 103.5 185C107.5 186.7 111.6 188.3 114.7 190C117.8 191.7 120.5 193.3 122.2 195C123.8 196.7 124.7 198.3 124.8 200C125 201.7 124.1 203.3 123 205C121.9 206.7 119.9 208.3 118.2 210C116.5 211.7 113.7 214.2 112.8 215" transform="translate(-6 5)" opacity="0.4"/> | |
| 16 | <path d="M158.3 20C160.4 20.8 166.9 23.3 171 25C175 26.7 179.4 28.3 182.8 30C186.2 31.7 189.3 33.3 191.4 35C193.5 36.7 194.8 38.3 195.4 40C195.9 41.7 195.4 43.3 194.6 45C193.8 46.7 192 48.3 190.3 50C188.7 51.7 186.5 53.3 184.8 55C183.2 56.7 181.5 58.3 180.7 60C179.9 61.7 179.5 63.3 180.2 65C180.8 66.7 182.2 68.3 184.4 70C186.5 71.7 189.7 73.3 193.2 75C196.6 76.7 201 78.3 205.1 80C209.1 81.7 213.8 83.3 217.7 85C221.6 86.7 225.5 88.3 228.5 90C231.4 91.7 233.8 93.3 235.3 95C236.8 96.7 237.4 98.3 237.3 100C237.3 101.7 236.2 103.3 235 105C233.7 106.7 231.6 108.3 229.9 110C228.2 111.7 226 113.3 224.6 115C223.3 116.7 222 118.3 221.7 120C221.4 121.7 221.8 123.3 223 125C224.2 126.7 226.4 128.3 229.1 130C231.9 131.7 235.6 133.3 239.4 135C243.2 136.7 247.8 138.3 251.9 140C256 141.7 260.5 143.3 264.1 145C267.7 146.7 271.1 148.3 273.5 150C275.9 151.7 277.6 153.3 278.5 155C279.3 156.7 279.2 158.3 278.6 160C278 161.7 276.4 163.3 274.9 165C273.4 166.7 271.1 168.3 269.4 170C267.8 171.7 265.8 173.3 264.8 175C263.8 176.7 263.1 178.3 263.4 180C263.7 181.7 264.8 183.3 266.6 185C268.5 186.7 271.4 188.3 274.6 190C277.8 191.7 282 193.3 286.1 195C290.1 196.7 294.7 198.3 298.8 200C302.8 201.7 306.9 203.3 310.1 205C313.3 206.7 316.1 208.3 317.8 210C319.6 211.7 320.6 213.3 320.8 215C321.1 216.7 320.3 218.3 319.2 220C318.2 221.7 316.2 223.3 314.5 225C312.9 226.7 310.6 228.3 309.1 230C307.6 231.7 306.1 233.3 305.5 235C305 236.7 305 238.3 305.9 240C306.8 241.7 308.6 243.3 311.1 245C313.5 246.7 317 248.3 320.6 250C324.3 251.7 328.8 253.3 332.9 255C337 256.7 341.6 258.3 345.4 260C349.1 261.7 352.8 263.3 355.5 265C358.2 266.7 360.2 268.3 361.4 270C362.6 271.7 362.8 273.3 362.5 275C362.1 276.7 360.8 278.3 359.4 280C358 281.7 355.8 283.3 354.1 285C352.4 286.7 350.3 288.3 349.1 290C347.9 291.7 346.9 293.3 346.9 295C346.9 296.7 347.6 298.3 349.1 300C350.7 301.7 353.2 303.3 356.2 305C359.2 306.7 363.2 308.3 367.1 310C371 311.7 375.7 313.3 379.8 315C383.8 316.7 388.2 318.3 391.6 320C395 321.7 398.1 323.3 400.2 325C402.3 326.7 403.6 328.3 404.2 330C404.7 331.7 404.2 333.3 403.4 335C402.6 336.7 400.8 338.3 399.1 340C397.5 341.7 395.3 343.3 393.6 345C392 346.7 390.3 348.3 389.5 350C388.7 351.7 388.3 353.3 389 355C389.6 356.7 391 358.3 393.2 360C395.3 361.7 398.5 363.3 402 365C405.4 366.7 409.8 368.3 413.9 370C417.9 371.7 422.6 373.3 426.5 375C430.4 376.7 434.3 378.3 437.3 380C440.2 381.7 442.6 383.3 444.1 385C445.6 386.7 446.2 388.3 446.1 390C446.1 391.7 445 393.3 443.8 395C442.5 396.7 440.4 398.3 438.7 400C437 401.7 434.8 403.3 433.4 405C432.1 406.7 430.8 408.3 430.5 410C430.2 411.7 431.6 414.2 431.8 415" transform="translate(-6 5)" opacity="0.55"/> | |
| 17 | </g> | |
| 18 | <g stroke="url(#ridge-shadow)" stroke-width="6"> | |
| 19 | <path d="M109.6 -10C110 -9.2 112 -6.7 112.1 -5C112.2 -3.3 111.3 -1.7 110.2 0C109 1.7 107 3.3 105.3 5C103.6 6.7 101.4 8.3 99.9 10C98.5 11.7 97.1 13.3 96.6 15C96.2 16.7 96.4 18.3 97.4 20C98.5 21.7 100.5 23.3 103 25C105.6 26.7 109.2 28.3 113 30C116.7 31.7 121.2 33.3 125.3 35C129.5 36.7 134 38.3 137.7 40C141.4 41.7 144.9 43.3 147.5 45C150 46.7 151.9 48.3 153 50C154 51.7 154 53.3 153.6 55C153.1 56.7 151.7 58.3 150.2 60C148.7 61.7 146.5 63.3 144.8 65C143.1 66.7 141.1 68.3 140 70C138.9 71.7 138 73.3 138.2 75C138.3 76.7 139.2 78.3 140.8 80C142.5 81.7 145.2 83.3 148.3 85C151.4 86.7 155.5 88.3 159.5 90C163.5 91.7 168.1 93.3 172.2 95C176.2 96.7 180.5 98.3 183.8 100C187.1 101.7 190.1 103.3 192 105C194 106.7 195.1 108.3 195.5 110C195.9 111.7 195.3 113.3 194.4 115C193.4 116.7 191.6 118.3 189.9 120C188.3 121.7 186 123.3 184.4 125C182.9 126.7 181.2 128.3 180.6 130C179.9 131.7 179.6 133.3 180.4 135C181.2 136.7 182.8 138.3 185.1 140C187.4 141.7 190.7 143.3 194.2 145C197.8 146.7 202.2 148.3 206.3 150C210.4 151.7 215 153.3 218.9 155C222.7 156.7 226.5 158.3 229.4 160C232.2 161.7 234.4 163.3 235.8 165C237.1 166.7 237.5 168.3 237.4 170C237.2 171.7 236 173.3 234.6 175C233.3 176.7 231.2 178.3 229.5 180C227.7 181.7 225.6 183.3 224.3 185C223 186.7 221.9 188.3 221.7 190C221.6 191.7 222.1 193.3 223.4 195C224.8 196.7 227.2 198.3 230 200C232.9 201.7 236.7 203.3 240.6 205C244.5 206.7 249.1 208.3 253.2 210C257.3 211.7 261.7 213.3 265.2 215C268.7 216.7 272 218.3 274.2 220C276.5 221.7 278 223.3 278.8 225C279.5 226.7 279.2 228.3 278.5 230C277.8 231.7 276.1 233.3 274.5 235C272.9 236.7 270.7 238.3 269 240C267.4 241.7 265.5 243.3 264.6 245C263.7 246.7 263.1 248.3 263.6 250C264 251.7 265.3 253.3 267.3 255C269.3 256.7 272.3 258.3 275.6 260C279 261.7 283.2 263.3 287.3 265C291.3 266.7 296 268.3 300 270C303.9 271.7 308 273.3 311 275C314.1 276.7 316.8 278.3 318.4 280C320 281.7 320.8 283.3 320.9 285C321 286.7 320.1 288.3 319 290C317.8 291.7 315.8 293.3 314.1 295C312.4 296.7 310.2 298.3 308.7 300C307.3 301.7 305.9 303.3 305.4 305C305 306.7 305.2 308.3 306.2 310C307.3 311.7 309.3 313.3 311.8 315C314.4 316.7 318 318.3 321.8 320C325.5 321.7 330 323.3 334.1 325C338.3 326.7 342.8 328.3 346.5 330C350.2 331.7 353.7 333.3 356.3 335C358.8 336.7 360.7 338.3 361.8 340C362.8 341.7 362.8 343.3 362.4 345C361.9 346.7 360.5 348.3 359 350C357.5 351.7 355.3 353.3 353.6 355C351.9 356.7 349.9 358.3 348.8 360C347.7 361.7 346.8 363.3 347 365C347.1 366.7 348 368.3 349.6 370C351.3 371.7 354 373.3 357.1 375C360.2 376.7 364.3 378.3 368.3 380C372.3 381.7 376.9 383.3 381 385C385 386.7 389.3 388.3 392.6 390C395.9 391.7 398.9 393.3 400.8 395C402.8 396.7 403.9 398.3 404.3 400C404.7 401.7 404.1 403.3 403.2 405C402.2 406.7 400.4 408.3 398.7 410C397.1 411.7 394.8 413.3 393.2 415C391.7 416.7 390 418.3 389.4 420C388.7 421.7 388.4 423.3 389.2 425C390 426.7 391.6 428.3 393.9 430C396.2 431.7 399.5 433.3 403 435C406.6 436.7 411 438.3 415.1 440C419.2 441.7 423.8 443.3 427.7 445C431.5 446.7 435.3 448.3 438.2 450C441 451.7 443.2 453.3 444.6 455C445.9 456.7 446.3 458.3 446.2 460C446 461.7 444.8 463.3 443.4 465C442.1 466.7 440 468.3 438.3 470C436.5 471.7 434 474.2 433.1 475" transform="translate(-6 5)" opacity="1"/> | |
| 20 | <path d="M52.7 -10C54.6 -9.2 60.8 -6.7 64 -5C67.1 -3.3 69.9 -1.7 71.7 0C73.4 1.7 74.4 3.3 74.6 5C74.8 6.7 74 8.3 72.9 10C71.9 11.7 69.9 13.3 68.2 15C66.5 16.7 64.3 18.3 62.8 20C61.3 21.7 59.8 23.3 59.3 25C58.7 26.7 58.7 28.3 59.7 30C60.6 31.7 62.4 33.3 64.9 35C67.4 36.7 70.9 38.3 74.6 40C78.2 41.7 82.7 43.3 86.8 45C91 46.7 95.5 48.3 99.3 50C103 51.7 106.7 53.3 109.3 55C112 56.7 114.1 58.3 115.2 60C116.3 61.7 116.5 63.3 116.2 65C115.8 66.7 114.5 68.3 113 70C111.6 71.7 109.4 73.3 107.7 75C106 76.7 104 78.3 102.8 80C101.6 81.7 100.6 83.3 100.6 85C100.7 86.7 101.4 88.3 102.9 90C104.5 91.7 107.1 93.3 110.1 95C113.1 96.7 117.1 98.3 121 100C125 101.7 129.6 103.3 133.7 105C137.8 106.7 142.1 108.3 145.5 110C148.9 111.7 151.9 113.3 154 115C156.1 116.7 157.4 118.3 157.9 120C158.4 121.7 157.9 123.3 157.1 125C156.2 126.7 154.4 128.3 152.8 130C151.2 131.7 148.9 133.3 147.3 135C145.7 136.7 144 138.3 143.2 140C142.5 141.7 142.1 143.3 142.7 145C143.4 146.7 144.8 148.3 147 150C149.2 151.7 152.4 153.3 155.9 155C159.3 156.7 163.7 158.3 167.8 160C171.9 161.7 176.5 163.3 180.4 165C184.3 166.7 188.2 168.3 191.1 170C194.1 171.7 196.5 173.3 197.9 175C199.4 176.7 200 178.3 199.9 180C199.8 181.7 198.7 183.3 197.4 185C196.2 186.7 194.1 188.3 192.4 190C190.6 191.7 188.5 193.3 187.1 195C185.8 196.7 184.5 198.3 184.3 200C184 201.7 184.3 203.3 185.6 205C186.9 206.7 189.1 208.3 191.8 210C194.6 211.7 198.3 213.3 202.2 215C206 216.7 210.6 218.3 214.7 220C218.8 221.7 223.2 223.3 226.8 225C230.4 226.7 233.8 228.3 236.2 230C238.5 231.7 240.2 233.3 241.1 235C241.9 236.7 241.7 238.3 241.1 240C240.5 241.7 238.9 243.3 237.4 245C235.8 246.7 233.6 248.3 231.9 250C230.2 251.7 228.3 253.3 227.3 255C226.3 256.7 225.7 258.3 226 260C226.3 261.7 227.4 263.3 229.3 265C231.2 266.7 234.1 268.3 237.3 270C240.6 271.7 244.8 273.3 248.8 275C252.8 276.7 257.5 278.3 261.5 280C265.5 281.7 269.6 283.3 272.8 285C275.9 286.7 278.7 288.3 280.5 290C282.2 291.7 283.2 293.3 283.4 295C283.6 296.7 282.8 298.3 281.7 300C280.7 301.7 278.7 303.3 277 305C275.3 306.7 273.1 308.3 271.6 310C270.1 311.7 268.6 313.3 268.1 315C267.5 316.7 267.5 318.3 268.5 320C269.4 321.7 271.2 323.3 273.7 325C276.2 326.7 279.7 328.3 283.4 330C287 331.7 291.5 333.3 295.6 335C299.8 336.7 304.3 338.3 308.1 340C311.8 341.7 315.5 343.3 318.1 345C320.8 346.7 322.9 348.3 324 350C325.1 351.7 325.3 353.3 325 355C324.6 356.7 323.3 358.3 321.8 360C320.4 361.7 318.2 363.3 316.5 365C314.8 366.7 312.8 368.3 311.6 370C310.4 371.7 309.8 374.2 309.4 375" transform="translate(-6 5)" opacity="0.75"/> | |
| 21 | <path d="M-9.8 -10C-7.9 -9.2 -2.5 -6.7 1.5 -5C5.5 -3.3 10.2 -1.7 14.2 0C18.2 1.7 22.4 3.3 25.7 5C28.9 6.7 31.8 8.3 33.7 10C35.6 11.7 36.7 13.3 37 15C37.3 16.7 36.7 18.3 35.7 20C34.7 21.7 32.8 23.3 31.1 25C29.4 26.7 27.2 28.3 25.6 30C24.1 31.7 22.5 33.3 21.9 35C21.3 36.7 21.1 38.3 21.9 40C22.8 41.7 24.5 43.3 26.8 45C29.2 46.7 32.6 48.3 36.2 50C39.8 51.7 44.2 53.3 48.3 55C52.4 56.7 57 58.3 60.8 60C64.7 61.7 68.4 63.3 71.2 65C73.9 66.7 76.1 68.3 77.4 70C78.7 71.7 79 73.3 78.7 75C78.5 76.7 77.2 78.3 75.9 80C74.5 81.7 72.4 83.3 70.6 85C68.9 86.7 66.8 88.3 65.6 90C64.3 91.7 63.2 93.3 63.1 95C63 96.7 63.6 98.3 65.1 100C66.5 101.7 68.9 103.3 71.9 105C74.8 106.7 78.7 108.3 82.6 110C86.5 111.7 91.1 113.3 95.2 115C99.3 116.7 103.7 118.3 107.1 120C110.6 121.7 113.8 123.3 116 125C118.2 126.7 119.6 128.3 120.3 130C120.9 131.7 120.5 133.3 119.8 135C119 136.7 117.3 138.3 115.7 140C114.1 141.7 111.8 143.3 110.2 145C108.6 146.7 106.8 148.3 105.9 150C105.1 151.7 104.6 153.3 105.1 155C105.6 156.7 106.9 158.3 109 160C111.1 161.7 114.1 163.3 117.5 165C120.9 166.7 125.2 168.3 129.3 170C133.4 171.7 138 173.3 142 175C145.9 176.7 149.9 178.3 152.9 180C155.9 181.7 158.5 183.3 160.1 185C161.6 186.7 162.3 188.3 162.4 190C162.4 191.7 161.4 193.3 160.2 195C159 196.7 157 198.3 155.3 200C153.6 201.7 151.4 203.3 150 205C148.5 206.7 147.2 208.3 146.8 210C146.5 211.7 146.7 213.3 147.8 215C148.9 216.7 151 218.3 153.7 220C156.3 221.7 160 223.3 163.7 225C167.5 226.7 172 228.3 176.2 230C180.3 231.7 184.8 233.3 188.4 235C192.1 236.7 195.6 238.3 198.1 240C200.6 241.7 202.4 243.3 203.3 245C204.3 246.7 204.3 248.3 203.7 250C203.2 251.7 201.7 253.3 200.2 255C198.7 256.7 196.5 258.3 194.8 260C193.1 261.7 191.1 263.3 190.1 265C189 266.7 188.2 268.3 188.4 270C188.6 271.7 189.6 273.3 191.3 275C193.1 276.7 195.9 278.3 199 280C202.2 281.7 206.3 283.3 210.3 285C214.3 286.7 219 288.3 223 290C227 291.7 232.6 294.2 234.5 295" transform="translate(-6 5)" opacity="0.55"/> | |
| 22 | <path d="M-24.3 10C-22.3 10.8 -16 13.3 -12.6 15C-9.3 16.7 -6.3 18.3 -4.3 20C-2.3 21.7 -1 23.3 -0.6 25C-0.1 26.7 -0.7 28.3 -1.6 30C-2.5 31.7 -4.4 33.3 -6 35C-7.7 36.7 -9.9 38.3 -11.5 40C-13.1 41.7 -14.8 43.3 -15.5 45C-16.2 46.7 -16.5 48.3 -15.8 50C-15 51.7 -13.5 53.3 -11.2 55C-9 56.7 -5.7 58.3 -2.2 60C1.3 61.7 5.7 63.3 9.8 65C13.9 66.7 18.5 68.3 22.4 70C26.3 71.7 30.1 73.3 33 75C35.9 76.7 38.2 78.3 39.6 80C40.9 81.7 41.4 83.3 41.3 85C41.1 86.7 40 88.3 38.7 90C37.4 91.7 35.3 93.3 33.5 95C31.8 96.7 29.7 98.3 28.4 100C27 101.7 25.8 103.3 25.6 105C25.5 106.7 25.9 108.3 27.2 110C28.6 111.7 30.8 113.3 33.7 115C36.5 116.7 40.3 118.3 44.1 120C48 121.7 52.6 123.3 56.7 125C60.8 126.7 65.2 128.3 68.8 130C72.3 131.7 75.6 133.3 77.9 135C80.2 136.7 81.8 138.3 82.6 140C83.3 141.7 83.1 143.3 82.4 145C81.8 146.7 80.1 148.3 78.6 150C77 151.7 74.7 153.3 73.1 155C71.4 156.7 69.6 158.3 68.6 160C67.7 161.7 67.1 163.3 67.5 165C67.9 166.7 69 168.3 71 170C72.9 171.7 75.9 173.3 79.2 175C82.5 176.7 86.8 178.3 90.8 180C94.9 181.7 99.5 183.3 103.5 185C107.5 186.7 111.6 188.3 114.7 190C117.8 191.7 120.5 193.3 122.2 195C123.8 196.7 124.7 198.3 124.8 200C125 201.7 124.1 203.3 123 205C121.9 206.7 119.9 208.3 118.2 210C116.5 211.7 113.7 214.2 112.8 215" transform="translate(-6 5)" opacity="0.4"/> | |
| 23 | <path d="M158.3 20C160.4 20.8 166.9 23.3 171 25C175 26.7 179.4 28.3 182.8 30C186.2 31.7 189.3 33.3 191.4 35C193.5 36.7 194.8 38.3 195.4 40C195.9 41.7 195.4 43.3 194.6 45C193.8 46.7 192 48.3 190.3 50C188.7 51.7 186.5 53.3 184.8 55C183.2 56.7 181.5 58.3 180.7 60C179.9 61.7 179.5 63.3 180.2 65C180.8 66.7 182.2 68.3 184.4 70C186.5 71.7 189.7 73.3 193.2 75C196.6 76.7 201 78.3 205.1 80C209.1 81.7 213.8 83.3 217.7 85C221.6 86.7 225.5 88.3 228.5 90C231.4 91.7 233.8 93.3 235.3 95C236.8 96.7 237.4 98.3 237.3 100C237.3 101.7 236.2 103.3 235 105C233.7 106.7 231.6 108.3 229.9 110C228.2 111.7 226 113.3 224.6 115C223.3 116.7 222 118.3 221.7 120C221.4 121.7 221.8 123.3 223 125C224.2 126.7 226.4 128.3 229.1 130C231.9 131.7 235.6 133.3 239.4 135C243.2 136.7 247.8 138.3 251.9 140C256 141.7 260.5 143.3 264.1 145C267.7 146.7 271.1 148.3 273.5 150C275.9 151.7 277.6 153.3 278.5 155C279.3 156.7 279.2 158.3 278.6 160C278 161.7 276.4 163.3 274.9 165C273.4 166.7 271.1 168.3 269.4 170C267.8 171.7 265.8 173.3 264.8 175C263.8 176.7 263.1 178.3 263.4 180C263.7 181.7 264.8 183.3 266.6 185C268.5 186.7 271.4 188.3 274.6 190C277.8 191.7 282 193.3 286.1 195C290.1 196.7 294.7 198.3 298.8 200C302.8 201.7 306.9 203.3 310.1 205C313.3 206.7 316.1 208.3 317.8 210C319.6 211.7 320.6 213.3 320.8 215C321.1 216.7 320.3 218.3 319.2 220C318.2 221.7 316.2 223.3 314.5 225C312.9 226.7 310.6 228.3 309.1 230C307.6 231.7 306.1 233.3 305.5 235C305 236.7 305 238.3 305.9 240C306.8 241.7 308.6 243.3 311.1 245C313.5 246.7 317 248.3 320.6 250C324.3 251.7 328.8 253.3 332.9 255C337 256.7 341.6 258.3 345.4 260C349.1 261.7 352.8 263.3 355.5 265C358.2 266.7 360.2 268.3 361.4 270C362.6 271.7 362.8 273.3 362.5 275C362.1 276.7 360.8 278.3 359.4 280C358 281.7 355.8 283.3 354.1 285C352.4 286.7 350.3 288.3 349.1 290C347.9 291.7 346.9 293.3 346.9 295C346.9 296.7 347.6 298.3 349.1 300C350.7 301.7 353.2 303.3 356.2 305C359.2 306.7 363.2 308.3 367.1 310C371 311.7 375.7 313.3 379.8 315C383.8 316.7 388.2 318.3 391.6 320C395 321.7 398.1 323.3 400.2 325C402.3 326.7 403.6 328.3 404.2 330C404.7 331.7 404.2 333.3 403.4 335C402.6 336.7 400.8 338.3 399.1 340C397.5 341.7 395.3 343.3 393.6 345C392 346.7 390.3 348.3 389.5 350C388.7 351.7 388.3 353.3 389 355C389.6 356.7 391 358.3 393.2 360C395.3 361.7 398.5 363.3 402 365C405.4 366.7 409.8 368.3 413.9 370C417.9 371.7 422.6 373.3 426.5 375C430.4 376.7 434.3 378.3 437.3 380C440.2 381.7 442.6 383.3 444.1 385C445.6 386.7 446.2 388.3 446.1 390C446.1 391.7 445 393.3 443.8 395C442.5 396.7 440.4 398.3 438.7 400C437 401.7 434.8 403.3 433.4 405C432.1 406.7 430.8 408.3 430.5 410C430.2 411.7 431.6 414.2 431.8 415" transform="translate(-6 5)" opacity="0.55"/> | |
| 24 | </g> | |
| 25 | <g stroke="url(#ridge)" stroke-width="5"> | |
| 26 | <path d="M109.6 -10C110 -9.2 112 -6.7 112.1 -5C112.2 -3.3 111.3 -1.7 110.2 0C109 1.7 107 3.3 105.3 5C103.6 6.7 101.4 8.3 99.9 10C98.5 11.7 97.1 13.3 96.6 15C96.2 16.7 96.4 18.3 97.4 20C98.5 21.7 100.5 23.3 103 25C105.6 26.7 109.2 28.3 113 30C116.7 31.7 121.2 33.3 125.3 35C129.5 36.7 134 38.3 137.7 40C141.4 41.7 144.9 43.3 147.5 45C150 46.7 151.9 48.3 153 50C154 51.7 154 53.3 153.6 55C153.1 56.7 151.7 58.3 150.2 60C148.7 61.7 146.5 63.3 144.8 65C143.1 66.7 141.1 68.3 140 70C138.9 71.7 138 73.3 138.2 75C138.3 76.7 139.2 78.3 140.8 80C142.5 81.7 145.2 83.3 148.3 85C151.4 86.7 155.5 88.3 159.5 90C163.5 91.7 168.1 93.3 172.2 95C176.2 96.7 180.5 98.3 183.8 100C187.1 101.7 190.1 103.3 192 105C194 106.7 195.1 108.3 195.5 110C195.9 111.7 195.3 113.3 194.4 115C193.4 116.7 191.6 118.3 189.9 120C188.3 121.7 186 123.3 184.4 125C182.9 126.7 181.2 128.3 180.6 130C179.9 131.7 179.6 133.3 180.4 135C181.2 136.7 182.8 138.3 185.1 140C187.4 141.7 190.7 143.3 194.2 145C197.8 146.7 202.2 148.3 206.3 150C210.4 151.7 215 153.3 218.9 155C222.7 156.7 226.5 158.3 229.4 160C232.2 161.7 234.4 163.3 235.8 165C237.1 166.7 237.5 168.3 237.4 170C237.2 171.7 236 173.3 234.6 175C233.3 176.7 231.2 178.3 229.5 180C227.7 181.7 225.6 183.3 224.3 185C223 186.7 221.9 188.3 221.7 190C221.6 191.7 222.1 193.3 223.4 195C224.8 196.7 227.2 198.3 230 200C232.9 201.7 236.7 203.3 240.6 205C244.5 206.7 249.1 208.3 253.2 210C257.3 211.7 261.7 213.3 265.2 215C268.7 216.7 272 218.3 274.2 220C276.5 221.7 278 223.3 278.8 225C279.5 226.7 279.2 228.3 278.5 230C277.8 231.7 276.1 233.3 274.5 235C272.9 236.7 270.7 238.3 269 240C267.4 241.7 265.5 243.3 264.6 245C263.7 246.7 263.1 248.3 263.6 250C264 251.7 265.3 253.3 267.3 255C269.3 256.7 272.3 258.3 275.6 260C279 261.7 283.2 263.3 287.3 265C291.3 266.7 296 268.3 300 270C303.9 271.7 308 273.3 311 275C314.1 276.7 316.8 278.3 318.4 280C320 281.7 320.8 283.3 320.9 285C321 286.7 320.1 288.3 319 290C317.8 291.7 315.8 293.3 314.1 295C312.4 296.7 310.2 298.3 308.7 300C307.3 301.7 305.9 303.3 305.4 305C305 306.7 305.2 308.3 306.2 310C307.3 311.7 309.3 313.3 311.8 315C314.4 316.7 318 318.3 321.8 320C325.5 321.7 330 323.3 334.1 325C338.3 326.7 342.8 328.3 346.5 330C350.2 331.7 353.7 333.3 356.3 335C358.8 336.7 360.7 338.3 361.8 340C362.8 341.7 362.8 343.3 362.4 345C361.9 346.7 360.5 348.3 359 350C357.5 351.7 355.3 353.3 353.6 355C351.9 356.7 349.9 358.3 348.8 360C347.7 361.7 346.8 363.3 347 365C347.1 366.7 348 368.3 349.6 370C351.3 371.7 354 373.3 357.1 375C360.2 376.7 364.3 378.3 368.3 380C372.3 381.7 376.9 383.3 381 385C385 386.7 389.3 388.3 392.6 390C395.9 391.7 398.9 393.3 400.8 395C402.8 396.7 403.9 398.3 404.3 400C404.7 401.7 404.1 403.3 403.2 405C402.2 406.7 400.4 408.3 398.7 410C397.1 411.7 394.8 413.3 393.2 415C391.7 416.7 390 418.3 389.4 420C388.7 421.7 388.4 423.3 389.2 425C390 426.7 391.6 428.3 393.9 430C396.2 431.7 399.5 433.3 403 435C406.6 436.7 411 438.3 415.1 440C419.2 441.7 423.8 443.3 427.7 445C431.5 446.7 435.3 448.3 438.2 450C441 451.7 443.2 453.3 444.6 455C445.9 456.7 446.3 458.3 446.2 460C446 461.7 444.8 463.3 443.4 465C442.1 466.7 440 468.3 438.3 470C436.5 471.7 434 474.2 433.1 475" opacity="1"/> | |
| 27 | <path d="M52.7 -10C54.6 -9.2 60.8 -6.7 64 -5C67.1 -3.3 69.9 -1.7 71.7 0C73.4 1.7 74.4 3.3 74.6 5C74.8 6.7 74 8.3 72.9 10C71.9 11.7 69.9 13.3 68.2 15C66.5 16.7 64.3 18.3 62.8 20C61.3 21.7 59.8 23.3 59.3 25C58.7 26.7 58.7 28.3 59.7 30C60.6 31.7 62.4 33.3 64.9 35C67.4 36.7 70.9 38.3 74.6 40C78.2 41.7 82.7 43.3 86.8 45C91 46.7 95.5 48.3 99.3 50C103 51.7 106.7 53.3 109.3 55C112 56.7 114.1 58.3 115.2 60C116.3 61.7 116.5 63.3 116.2 65C115.8 66.7 114.5 68.3 113 70C111.6 71.7 109.4 73.3 107.7 75C106 76.7 104 78.3 102.8 80C101.6 81.7 100.6 83.3 100.6 85C100.7 86.7 101.4 88.3 102.9 90C104.5 91.7 107.1 93.3 110.1 95C113.1 96.7 117.1 98.3 121 100C125 101.7 129.6 103.3 133.7 105C137.8 106.7 142.1 108.3 145.5 110C148.9 111.7 151.9 113.3 154 115C156.1 116.7 157.4 118.3 157.9 120C158.4 121.7 157.9 123.3 157.1 125C156.2 126.7 154.4 128.3 152.8 130C151.2 131.7 148.9 133.3 147.3 135C145.7 136.7 144 138.3 143.2 140C142.5 141.7 142.1 143.3 142.7 145C143.4 146.7 144.8 148.3 147 150C149.2 151.7 152.4 153.3 155.9 155C159.3 156.7 163.7 158.3 167.8 160C171.9 161.7 176.5 163.3 180.4 165C184.3 166.7 188.2 168.3 191.1 170C194.1 171.7 196.5 173.3 197.9 175C199.4 176.7 200 178.3 199.9 180C199.8 181.7 198.7 183.3 197.4 185C196.2 186.7 194.1 188.3 192.4 190C190.6 191.7 188.5 193.3 187.1 195C185.8 196.7 184.5 198.3 184.3 200C184 201.7 184.3 203.3 185.6 205C186.9 206.7 189.1 208.3 191.8 210C194.6 211.7 198.3 213.3 202.2 215C206 216.7 210.6 218.3 214.7 220C218.8 221.7 223.2 223.3 226.8 225C230.4 226.7 233.8 228.3 236.2 230C238.5 231.7 240.2 233.3 241.1 235C241.9 236.7 241.7 238.3 241.1 240C240.5 241.7 238.9 243.3 237.4 245C235.8 246.7 233.6 248.3 231.9 250C230.2 251.7 228.3 253.3 227.3 255C226.3 256.7 225.7 258.3 226 260C226.3 261.7 227.4 263.3 229.3 265C231.2 266.7 234.1 268.3 237.3 270C240.6 271.7 244.8 273.3 248.8 275C252.8 276.7 257.5 278.3 261.5 280C265.5 281.7 269.6 283.3 272.8 285C275.9 286.7 278.7 288.3 280.5 290C282.2 291.7 283.2 293.3 283.4 295C283.6 296.7 282.8 298.3 281.7 300C280.7 301.7 278.7 303.3 277 305C275.3 306.7 273.1 308.3 271.6 310C270.1 311.7 268.6 313.3 268.1 315C267.5 316.7 267.5 318.3 268.5 320C269.4 321.7 271.2 323.3 273.7 325C276.2 326.7 279.7 328.3 283.4 330C287 331.7 291.5 333.3 295.6 335C299.8 336.7 304.3 338.3 308.1 340C311.8 341.7 315.5 343.3 318.1 345C320.8 346.7 322.9 348.3 324 350C325.1 351.7 325.3 353.3 325 355C324.6 356.7 323.3 358.3 321.8 360C320.4 361.7 318.2 363.3 316.5 365C314.8 366.7 312.8 368.3 311.6 370C310.4 371.7 309.8 374.2 309.4 375" opacity="0.75"/> | |
| 28 | <path d="M-9.8 -10C-7.9 -9.2 -2.5 -6.7 1.5 -5C5.5 -3.3 10.2 -1.7 14.2 0C18.2 1.7 22.4 3.3 25.7 5C28.9 6.7 31.8 8.3 33.7 10C35.6 11.7 36.7 13.3 37 15C37.3 16.7 36.7 18.3 35.7 20C34.7 21.7 32.8 23.3 31.1 25C29.4 26.7 27.2 28.3 25.6 30C24.1 31.7 22.5 33.3 21.9 35C21.3 36.7 21.1 38.3 21.9 40C22.8 41.7 24.5 43.3 26.8 45C29.2 46.7 32.6 48.3 36.2 50C39.8 51.7 44.2 53.3 48.3 55C52.4 56.7 57 58.3 60.8 60C64.7 61.7 68.4 63.3 71.2 65C73.9 66.7 76.1 68.3 77.4 70C78.7 71.7 79 73.3 78.7 75C78.5 76.7 77.2 78.3 75.9 80C74.5 81.7 72.4 83.3 70.6 85C68.9 86.7 66.8 88.3 65.6 90C64.3 91.7 63.2 93.3 63.1 95C63 96.7 63.6 98.3 65.1 100C66.5 101.7 68.9 103.3 71.9 105C74.8 106.7 78.7 108.3 82.6 110C86.5 111.7 91.1 113.3 95.2 115C99.3 116.7 103.7 118.3 107.1 120C110.6 121.7 113.8 123.3 116 125C118.2 126.7 119.6 128.3 120.3 130C120.9 131.7 120.5 133.3 119.8 135C119 136.7 117.3 138.3 115.7 140C114.1 141.7 111.8 143.3 110.2 145C108.6 146.7 106.8 148.3 105.9 150C105.1 151.7 104.6 153.3 105.1 155C105.6 156.7 106.9 158.3 109 160C111.1 161.7 114.1 163.3 117.5 165C120.9 166.7 125.2 168.3 129.3 170C133.4 171.7 138 173.3 142 175C145.9 176.7 149.9 178.3 152.9 180C155.9 181.7 158.5 183.3 160.1 185C161.6 186.7 162.3 188.3 162.4 190C162.4 191.7 161.4 193.3 160.2 195C159 196.7 157 198.3 155.3 200C153.6 201.7 151.4 203.3 150 205C148.5 206.7 147.2 208.3 146.8 210C146.5 211.7 146.7 213.3 147.8 215C148.9 216.7 151 218.3 153.7 220C156.3 221.7 160 223.3 163.7 225C167.5 226.7 172 228.3 176.2 230C180.3 231.7 184.8 233.3 188.4 235C192.1 236.7 195.6 238.3 198.1 240C200.6 241.7 202.4 243.3 203.3 245C204.3 246.7 204.3 248.3 203.7 250C203.2 251.7 201.7 253.3 200.2 255C198.7 256.7 196.5 258.3 194.8 260C193.1 261.7 191.1 263.3 190.1 265C189 266.7 188.2 268.3 188.4 270C188.6 271.7 189.6 273.3 191.3 275C193.1 276.7 195.9 278.3 199 280C202.2 281.7 206.3 283.3 210.3 285C214.3 286.7 219 288.3 223 290C227 291.7 232.6 294.2 234.5 295" opacity="0.55"/> | |
| 29 | <path d="M-24.3 10C-22.3 10.8 -16 13.3 -12.6 15C-9.3 16.7 -6.3 18.3 -4.3 20C-2.3 21.7 -1 23.3 -0.6 25C-0.1 26.7 -0.7 28.3 -1.6 30C-2.5 31.7 -4.4 33.3 -6 35C-7.7 36.7 -9.9 38.3 -11.5 40C-13.1 41.7 -14.8 43.3 -15.5 45C-16.2 46.7 -16.5 48.3 -15.8 50C-15 51.7 -13.5 53.3 -11.2 55C-9 56.7 -5.7 58.3 -2.2 60C1.3 61.7 5.7 63.3 9.8 65C13.9 66.7 18.5 68.3 22.4 70C26.3 71.7 30.1 73.3 33 75C35.9 76.7 38.2 78.3 39.6 80C40.9 81.7 41.4 83.3 41.3 85C41.1 86.7 40 88.3 38.7 90C37.4 91.7 35.3 93.3 33.5 95C31.8 96.7 29.7 98.3 28.4 100C27 101.7 25.8 103.3 25.6 105C25.5 106.7 25.9 108.3 27.2 110C28.6 111.7 30.8 113.3 33.7 115C36.5 116.7 40.3 118.3 44.1 120C48 121.7 52.6 123.3 56.7 125C60.8 126.7 65.2 128.3 68.8 130C72.3 131.7 75.6 133.3 77.9 135C80.2 136.7 81.8 138.3 82.6 140C83.3 141.7 83.1 143.3 82.4 145C81.8 146.7 80.1 148.3 78.6 150C77 151.7 74.7 153.3 73.1 155C71.4 156.7 69.6 158.3 68.6 160C67.7 161.7 67.1 163.3 67.5 165C67.9 166.7 69 168.3 71 170C72.9 171.7 75.9 173.3 79.2 175C82.5 176.7 86.8 178.3 90.8 180C94.9 181.7 99.5 183.3 103.5 185C107.5 186.7 111.6 188.3 114.7 190C117.8 191.7 120.5 193.3 122.2 195C123.8 196.7 124.7 198.3 124.8 200C125 201.7 124.1 203.3 123 205C121.9 206.7 119.9 208.3 118.2 210C116.5 211.7 113.7 214.2 112.8 215" opacity="0.4"/> | |
| 30 | <path d="M158.3 20C160.4 20.8 166.9 23.3 171 25C175 26.7 179.4 28.3 182.8 30C186.2 31.7 189.3 33.3 191.4 35C193.5 36.7 194.8 38.3 195.4 40C195.9 41.7 195.4 43.3 194.6 45C193.8 46.7 192 48.3 190.3 50C188.7 51.7 186.5 53.3 184.8 55C183.2 56.7 181.5 58.3 180.7 60C179.9 61.7 179.5 63.3 180.2 65C180.8 66.7 182.2 68.3 184.4 70C186.5 71.7 189.7 73.3 193.2 75C196.6 76.7 201 78.3 205.1 80C209.1 81.7 213.8 83.3 217.7 85C221.6 86.7 225.5 88.3 228.5 90C231.4 91.7 233.8 93.3 235.3 95C236.8 96.7 237.4 98.3 237.3 100C237.3 101.7 236.2 103.3 235 105C233.7 106.7 231.6 108.3 229.9 110C228.2 111.7 226 113.3 224.6 115C223.3 116.7 222 118.3 221.7 120C221.4 121.7 221.8 123.3 223 125C224.2 126.7 226.4 128.3 229.1 130C231.9 131.7 235.6 133.3 239.4 135C243.2 136.7 247.8 138.3 251.9 140C256 141.7 260.5 143.3 264.1 145C267.7 146.7 271.1 148.3 273.5 150C275.9 151.7 277.6 153.3 278.5 155C279.3 156.7 279.2 158.3 278.6 160C278 161.7 276.4 163.3 274.9 165C273.4 166.7 271.1 168.3 269.4 170C267.8 171.7 265.8 173.3 264.8 175C263.8 176.7 263.1 178.3 263.4 180C263.7 181.7 264.8 183.3 266.6 185C268.5 186.7 271.4 188.3 274.6 190C277.8 191.7 282 193.3 286.1 195C290.1 196.7 294.7 198.3 298.8 200C302.8 201.7 306.9 203.3 310.1 205C313.3 206.7 316.1 208.3 317.8 210C319.6 211.7 320.6 213.3 320.8 215C321.1 216.7 320.3 218.3 319.2 220C318.2 221.7 316.2 223.3 314.5 225C312.9 226.7 310.6 228.3 309.1 230C307.6 231.7 306.1 233.3 305.5 235C305 236.7 305 238.3 305.9 240C306.8 241.7 308.6 243.3 311.1 245C313.5 246.7 317 248.3 320.6 250C324.3 251.7 328.8 253.3 332.9 255C337 256.7 341.6 258.3 345.4 260C349.1 261.7 352.8 263.3 355.5 265C358.2 266.7 360.2 268.3 361.4 270C362.6 271.7 362.8 273.3 362.5 275C362.1 276.7 360.8 278.3 359.4 280C358 281.7 355.8 283.3 354.1 285C352.4 286.7 350.3 288.3 349.1 290C347.9 291.7 346.9 293.3 346.9 295C346.9 296.7 347.6 298.3 349.1 300C350.7 301.7 353.2 303.3 356.2 305C359.2 306.7 363.2 308.3 367.1 310C371 311.7 375.7 313.3 379.8 315C383.8 316.7 388.2 318.3 391.6 320C395 321.7 398.1 323.3 400.2 325C402.3 326.7 403.6 328.3 404.2 330C404.7 331.7 404.2 333.3 403.4 335C402.6 336.7 400.8 338.3 399.1 340C397.5 341.7 395.3 343.3 393.6 345C392 346.7 390.3 348.3 389.5 350C388.7 351.7 388.3 353.3 389 355C389.6 356.7 391 358.3 393.2 360C395.3 361.7 398.5 363.3 402 365C405.4 366.7 409.8 368.3 413.9 370C417.9 371.7 422.6 373.3 426.5 375C430.4 376.7 434.3 378.3 437.3 380C440.2 381.7 442.6 383.3 444.1 385C445.6 386.7 446.2 388.3 446.1 390C446.1 391.7 445 393.3 443.8 395C442.5 396.7 440.4 398.3 438.7 400C437 401.7 434.8 403.3 433.4 405C432.1 406.7 430.8 408.3 430.5 410C430.2 411.7 431.6 414.2 431.8 415" opacity="0.55"/> | |
| 31 | </g> | |
| 32 | </g> | |
| 33 | </svg> |
crates/canvas/assets/backgrounds/books.svg created+9| ... | ... | @@ -0,0 +1,9 @@ |
| 1 | <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 221 792" preserveAspectRatio="none"> | |
| 2 | <defs> | |
| 3 | <linearGradient id="down" x2="0" y2="1"><stop offset="0.75" stop-color="white"/><stop offset="1" stop-color="white" stop-opacity="0"/></linearGradient><mask id="rule-down"><rect width="221" height="792" fill="url(#down)"/></mask> | |
| 4 | <filter id="shadow" x="-30%" y="-30%" width="160%" height="160%"><feGaussianBlur stdDeviation="4"/></filter> | |
| 5 | </defs> | |
| 6 | <ellipse cx="80" cy="134.6" rx="70" ry="6" fill="#ece8f0" filter="url(#shadow)"/> | |
| 7 | <rect x="9.4" y="16" width="75.6" height="10.4" rx="1.5" fill="#efe3cf" stroke="#b67358" stroke-width="1.2"/><rect x="12.4" y="26.4" width="78.3" height="10.2" rx="1.5" fill="#f3e7d2" stroke="#9a7b63" stroke-width="1.2"/><rect x="13.9" y="36.6" width="72.9" height="10.5" rx="1.5" fill="#e8dcc6" stroke="#b49f9a" stroke-width="1.2"/><rect x="11.7" y="47" width="81" height="9.8" rx="1.5" fill="#f0e2c9" stroke="#7f5b4f" stroke-width="1.2"/><rect x="12.6" y="56.8" width="70.2" height="11.7" rx="1.5" fill="#f6ead6" stroke="#a58a6c" stroke-width="1.2"/><rect x="14.4" y="68.5" width="83.7" height="13" rx="1.5" fill="#eed7b8" stroke="#8c6c55" stroke-width="1.2"/><rect x="8.5" y="81.5" width="75.6" height="12.7" rx="1.5" fill="#f4e8d6" stroke="#c09a7a" stroke-width="1.2"/><rect x="12.2" y="94.2" width="78.3" height="14.8" rx="1.5" fill="#efd3ae" stroke="#8f705c" stroke-width="1.2"/><rect x="11" y="109" width="72.9" height="11.3" rx="1.5" fill="#f5ecdf" stroke="#b4aaa1" stroke-width="1.2"/><rect x="9.6" y="120.3" width="81" height="10.3" rx="1.5" fill="#e9ded3" stroke="#7f5b4f" stroke-width="1.2"/> | |
| 8 | <rect x="41" y="0" width="1" height="792" fill="#885157" mask="url(#rule-down)"/> | |
| 9 | </svg> |
crates/canvas/assets/backgrounds/bouquet.svg created+21| ... | ... | @@ -0,0 +1,21 @@ |
| 1 | <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 612 792" preserveAspectRatio="none"> | |
| 2 | <defs> | |
| 3 | <radialGradient id="reach" cx="180" cy="0" r="1" gradientTransform="translate(180 0) scale(520 300) translate(-180 0)" gradientUnits="userSpaceOnUse"><stop offset="0.3" stop-color="white"/><stop offset="0.95" stop-color="white" stop-opacity="0"/></radialGradient><mask id="art"><rect width="612" height="792" fill="url(#reach)"/></mask> | |
| 4 | <linearGradient id="across" x2="1" y2="0"><stop offset="0.55" stop-color="white"/><stop offset="0.96" stop-color="white" stop-opacity="0"/></linearGradient><mask id="rule-across"><rect width="612" height="792" fill="url(#across)"/></mask> | |
| 5 | <linearGradient id="down" x2="0" y2="1"><stop offset="0.28" stop-color="white"/><stop offset="0.42" stop-color="white" stop-opacity="0"/></linearGradient><mask id="rule-down"><rect width="612" height="792" fill="url(#down)"/></mask> | |
| 6 | <linearGradient id="meadow" x2="0" y2="1"><stop offset="0" stop-color="#6d924c"/><stop offset=".25" stop-color="#b5b35e"/><stop offset=".45" stop-color="#fbe4d0"/><stop offset=".7" stop-color="#fdf4ec" stop-opacity="0"/></linearGradient> | |
| 7 | <radialGradient id="poppy" fx=".4" fy=".35"><stop offset="0" stop-color="#fbd3dc"/><stop offset=".6" stop-color="#f28aa6"/><stop offset="1" stop-color="#d8567c"/></radialGradient> | |
| 8 | <filter id="bokeh" x="-20%" y="-20%" width="140%" height="140%"><feGaussianBlur stdDeviation="7"/></filter> | |
| 9 | </defs> | |
| 10 | <g mask="url(#art)"> | |
| 11 | <rect width="612" height="400" fill="url(#meadow)"/> | |
| 12 | <g filter="url(#bokeh)" opacity=".85"><circle cx="46.9" cy="-10.8" r="29.2" fill="#fbb248"/><circle cx="195" cy="25.5" r="33.2" fill="#f7813f"/><circle cx="152.8" cy="-17.8" r="32.6" fill="#f0563a"/><circle cx="76.9" cy="-27.8" r="28.3" fill="#fbb248"/><circle cx="482.5" cy="109.9" r="17.2" fill="#e74c64"/><circle cx="547.9" cy="64.4" r="32.4" fill="#9272c9"/><circle cx="3.7" cy="-16.7" r="33.9" fill="#e74c64"/><circle cx="253.3" cy="7.5" r="23" fill="#f7813f"/><circle cx="-17.9" cy="-25.9" r="19.5" fill="#9272c9"/><circle cx="111.3" cy="108.9" r="22.3" fill="#9272c9"/><circle cx="556.1" cy="76.7" r="22.2" fill="#e74c64"/><circle cx="69.9" cy="50.9" r="23.3" fill="#fbb248"/><circle cx="424.4" cy="70" r="14.6" fill="#f7813f"/><circle cx="505.3" cy="37.5" r="17.6" fill="#fbb248"/><circle cx="39.7" cy="52.4" r="15.6" fill="#9272c9"/><circle cx="504.1" cy="50" r="18.3" fill="#f0563a"/><circle cx="412.3" cy="60.9" r="14.5" fill="#e74c64"/><circle cx="288.3" cy="78.8" r="20" fill="#f7813f"/><circle cx="339.3" cy="61" r="22.6" fill="#fbb248"/><circle cx="415.5" cy="1.6" r="26.2" fill="#f0563a"/><circle cx="121.3" cy="44.2" r="25.4" fill="#f0563a"/><circle cx="497.4" cy="97" r="26.2" fill="#f7813f"/><circle cx="284.3" cy="109" r="32.3" fill="#f7813f"/><circle cx="409.5" cy="-15.9" r="32.6" fill="#f7813f"/><circle cx="453.9" cy="86.4" r="28.4" fill="#9272c9"/><circle cx="367" cy="52.7" r="28.3" fill="#9272c9"/></g> | |
| 13 | </g> | |
| 14 | <path d="M28 150C26 190 30 240 26 300" stroke="#8fb36a" stroke-width="3" fill="none" mask="url(#rule-down)"/> | |
| 15 | <g transform="translate(28 110)"> | |
| 16 | <path d="M-26-6C-30-24-16-36 0-34C16-38 30-26 28-8C26 10 14 22 0 22C-14 22-24 10-26-6Z" fill="url(#poppy)"/> | |
| 17 | <path d="M-8 18C-4 8 4 8 8 18Z" fill="#b94668"/> | |
| 18 | </g> | |
| 19 | <rect x="45" y="25" width="567" height="8" fill="#8b58a0" mask="url(#rule-across)"/> | |
| 20 | <rect x="45" y="25" width="1" height="767" fill="#8b58a0" mask="url(#rule-down)"/> | |
| 21 | </svg> |
crates/canvas/assets/backgrounds/boxes-shadow.svg created+16| ... | ... | @@ -0,0 +1,16 @@ |
| 1 | <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 40 617" preserveAspectRatio="none"> | |
| 2 | <rect x="9.5" y="7.5" width="22" height="22" fill="#ffebce" stroke="#ffba8c"/><rect x="4.5" y="12.5" width="22" height="22" fill="none" stroke="#ffba8c"/> | |
| 3 | <rect x="9.5" y="51.6" width="22" height="22" fill="#fffbce" stroke="#ffe384"/><rect x="4.5" y="56.6" width="22" height="22" fill="none" stroke="#ffe384"/> | |
| 4 | <rect x="9.5" y="95.7" width="22" height="22" fill="#ffe7f7" stroke="#e7b6ce"/><rect x="4.5" y="100.7" width="22" height="22" fill="none" stroke="#e7b6ce"/> | |
| 5 | <rect x="9.5" y="139.8" width="22" height="22" fill="#deefff" stroke="#9cb6ef"/><rect x="4.5" y="144.8" width="22" height="22" fill="none" stroke="#9cb6ef"/> | |
| 6 | <rect x="9.5" y="183.9" width="22" height="22" fill="#f7e3ff" stroke="#c6aee7"/><rect x="4.5" y="188.9" width="22" height="22" fill="none" stroke="#c6aee7"/> | |
| 7 | <rect x="9.5" y="228" width="22" height="22" fill="#e7efde" stroke="#c6d3ad"/><rect x="4.5" y="233" width="22" height="22" fill="none" stroke="#c6d3ad"/> | |
| 8 | <rect x="9.5" y="272.1" width="22" height="22" fill="#d6f3ef" stroke="#adcbc6"/><rect x="4.5" y="277.1" width="22" height="22" fill="none" stroke="#adcbc6"/> | |
| 9 | <rect x="9.5" y="316.2" width="22" height="22" fill="#ffe7e7" stroke="#f7a6ad"/><rect x="4.5" y="321.2" width="22" height="22" fill="none" stroke="#f7a6ad"/> | |
| 10 | <rect x="9.5" y="360.3" width="22" height="22" fill="#ffebce" stroke="#ffba8c"/><rect x="4.5" y="365.3" width="22" height="22" fill="none" stroke="#ffba8c"/> | |
| 11 | <rect x="9.5" y="404.4" width="22" height="22" fill="#fffbce" stroke="#ffe384"/><rect x="4.5" y="409.4" width="22" height="22" fill="none" stroke="#ffe384"/> | |
| 12 | <rect x="9.5" y="448.5" width="22" height="22" fill="#ffe7f7" stroke="#e7b6ce"/><rect x="4.5" y="453.5" width="22" height="22" fill="none" stroke="#e7b6ce"/> | |
| 13 | <rect x="9.5" y="492.6" width="22" height="22" fill="#deefff" stroke="#9cb6ef"/><rect x="4.5" y="497.6" width="22" height="22" fill="none" stroke="#9cb6ef"/> | |
| 14 | <rect x="9.5" y="536.7" width="22" height="22" fill="#f7e3ff" stroke="#c6aee7"/><rect x="4.5" y="541.7" width="22" height="22" fill="none" stroke="#c6aee7"/> | |
| 15 | <rect x="9.5" y="580.8" width="22" height="22" fill="#e7efde" stroke="#c6d3ad"/><rect x="4.5" y="585.8" width="22" height="22" fill="none" stroke="#c6d3ad"/> | |
| 16 | </svg> |
crates/canvas/assets/backgrounds/boxes.svg created+3| ... | ... | @@ -0,0 +1,3 @@ |
| 1 | <svg xmlns="http://www.w3.org/2000/svg" width="45pt" height="252pt" viewBox="0 0 60 336" preserveAspectRatio="none"> | |
| 2 | <g fill="none" stroke-width="2"><rect x="7" y="5" width="32" height="32" stroke="#d1e5fc"/><rect x="22" y="27" width="32" height="32" stroke="#fff2ae"/><rect x="7" y="50" width="32" height="32" stroke="#d9e2c7"/><rect x="22" y="73" width="32" height="32" stroke="#ffd4d6"/><rect x="5" y="99" width="32" height="32" stroke="#e0c9ff"/><rect x="23" y="123" width="32" height="32" stroke="#f5dfb4"/><rect x="7" y="180" width="32" height="32" stroke="#d1e5fc"/><rect x="22" y="202" width="32" height="32" stroke="#fff2ae"/><rect x="7" y="225" width="32" height="32" stroke="#d9e2c7"/><rect x="22" y="248" width="32" height="32" stroke="#ffd4d6"/><rect x="5" y="274" width="32" height="32" stroke="#e0c9ff"/><rect x="23" y="298" width="32" height="32" stroke="#f5dfb4"/></g> | |
| 3 | </svg> |
crates/canvas/assets/backgrounds/bubbles-margin-margin.svg created+9| ... | ... | @@ -0,0 +1,9 @@ |
| 1 | <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 95 498" preserveAspectRatio="none"> | |
| 2 | <defs><filter id="glow" x="-30%" y="-30%" width="160%" height="160%"><feGaussianBlur stdDeviation="4"/></filter></defs> | |
| 3 | <g opacity=".85"><g filter="url(#glow)" fill="#e6e0c9"><circle cx="2" cy="335" r="74"/></g><g fill="#e6e0c9" stroke="#c3bca8"><circle cx="2" cy="335" r="74"/></g></g> | |
| 4 | <g opacity=".85"><g filter="url(#glow)" fill="#d3e4d4"><rect x="21" y="9" width="19" height="148" rx="4"/></g><g fill="#d3e4d4" stroke="#9fb8a4"><rect x="21" y="9" width="19" height="148" rx="4"/></g></g> | |
| 5 | <g opacity=".85"><g filter="url(#glow)" fill="#c6dfdd"><rect x="31" y="54" width="19" height="149" rx="4"/></g><g fill="#c6dfdd" stroke="#9ab7bb"><rect x="31" y="54" width="19" height="149" rx="4"/></g></g> | |
| 6 | <g opacity=".85"><g filter="url(#glow)" fill="#f2d3d5"><rect x="21" y="180" width="19" height="149" rx="4"/></g><g fill="#f2d3d5" stroke="#c4959a"><rect x="21" y="180" width="19" height="149" rx="4"/></g></g> | |
| 7 | <g opacity=".85"><g filter="url(#glow)" fill="#f2d3d5"><circle cx="60" cy="437" r="18"/></g><g fill="#f2d3d5" stroke="#c4959a"><circle cx="60" cy="437" r="18"/></g></g> | |
| 8 | <g opacity=".85"><g filter="url(#glow)" fill="#d3e4d4"><circle cx="33" cy="481" r="6.5"/></g><g fill="#d3e4d4" stroke="#9fb8a4"><circle cx="33" cy="481" r="6.5"/></g></g> | |
| 9 | </svg> |
crates/canvas/assets/backgrounds/bubbles-margin-top.svg created+8| ... | ... | @@ -0,0 +1,8 @@ |
| 1 | <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 700 114" preserveAspectRatio="none"> | |
| 2 | <defs><filter id="glow" x="-30%" y="-30%" width="160%" height="160%"><feGaussianBlur stdDeviation="4"/></filter></defs> | |
| 3 | <g opacity=".85"><g filter="url(#glow)" fill="#d3e4d4"><circle cx="0" cy="0" r="103"/></g><g fill="#d3e4d4" stroke="#9fb8a4"><circle cx="0" cy="0" r="103"/></g></g> | |
| 4 | <g opacity=".85"><g filter="url(#glow)" fill="#f2d3d5"><circle cx="125" cy="-33" r="62"/></g><g fill="#f2d3d5" stroke="#c4959a"><circle cx="125" cy="-33" r="62"/></g></g> | |
| 5 | <g opacity=".85"><g filter="url(#glow)" fill="#f2d3d5"><circle cx="671" cy="7" r="18"/></g><g fill="#f2d3d5" stroke="#c4959a"><circle cx="671" cy="7" r="18"/></g></g> | |
| 6 | <g opacity=".85"><g filter="url(#glow)" fill="#d3e4d4"><circle cx="644" cy="41.5" r="7"/></g><g fill="#d3e4d4" stroke="#9fb8a4"><circle cx="644" cy="41.5" r="7"/></g></g> | |
| 7 | <g opacity=".85"><g filter="url(#glow)" fill="#d3e4d4"><circle cx="668" cy="41.5" r="3.5"/></g><g fill="#d3e4d4" stroke="#9fb8a4"><circle cx="668" cy="41.5" r="3.5"/></g></g> | |
| 8 | </svg> |
crates/canvas/assets/backgrounds/bubbles.svg created+25| ... | ... | @@ -0,0 +1,25 @@ |
| 1 | <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 50 600" preserveAspectRatio="none"> | |
| 2 | <circle cx="20" cy="32" r="14" fill="#ffebce"/> | |
| 3 | <circle cx="36" cy="190" r="7.5" fill="#fffbce"/> | |
| 4 | <circle cx="19" cy="293" r="10" fill="#ffe7f7"/> | |
| 5 | <circle cx="25" cy="374" r="11" fill="#f7e3ff"/> | |
| 6 | <circle cx="19" cy="449" r="14.5" fill="#d6f3ef"/> | |
| 7 | <circle cx="28" cy="550" r="11" fill="#ffebce"/> | |
| 8 | <g fill="none" stroke-width="1.2"> | |
| 9 | <circle cx="30" cy="37.5" r="3.7" stroke="#e3d2dc"/> | |
| 10 | <circle cx="31" cy="69" r="7.5" stroke="#fac6cb"/> | |
| 11 | <circle cx="30.5" cy="99.5" r="16" stroke="#9cb6ef"/> | |
| 12 | <circle cx="34" cy="143" r="3.5" stroke="#e3daf6"/> | |
| 13 | <circle cx="26" cy="180" r="13.5" stroke="#ffe384"/> | |
| 14 | <circle cx="26" cy="213" r="9" stroke="#ffc9a5"/> | |
| 15 | <circle cx="15.5" cy="233.5" r="3.5" stroke="#e3daf6"/> | |
| 16 | <circle cx="30" cy="308" r="15.5" stroke="#ffe384"/> | |
| 17 | <circle cx="20" cy="339" r="3.5" stroke="#e3daf6"/> | |
| 18 | <circle cx="19" cy="374" r="8" stroke="#d6dbca"/> | |
| 19 | <circle cx="33" cy="404" r="10" stroke="#adc3f2"/> | |
| 20 | <circle cx="32" cy="439" r="7" stroke="#ffcfb0"/> | |
| 21 | <circle cx="25" cy="470" r="17.5" stroke="#e7b6ce"/> | |
| 22 | <circle cx="20" cy="527" r="9" stroke="#b1c5f2"/> | |
| 23 | <circle cx="30" cy="565" r="17" stroke="#aecbc6"/> | |
| 24 | </g> | |
| 25 | </svg> |
crates/canvas/assets/backgrounds/buildings.svg created+16| ... | ... | @@ -0,0 +1,16 @@ |
| 1 | <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 612 792" preserveAspectRatio="none"> | |
| 2 | <defs> | |
| 3 | <linearGradient id="glass" x2="0" y2="792" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#a8c9ec" stop-opacity="1"/><stop offset="0.4" stop-color="#c9dff4" stop-opacity="1"/><stop offset="0.7" stop-color="#8cb9e0" stop-opacity="1"/><stop offset="0.98" stop-color="#8cb9e0" stop-opacity="0"/></linearGradient> | |
| 4 | <linearGradient id="mullion" x2="0" y2="792" gradientUnits="userSpaceOnUse"><stop offset="0.7" stop-color="#5f7fa6" stop-opacity="1"/><stop offset="0.98" stop-color="#5f7fa6" stop-opacity="0"/></linearGradient> | |
| 5 | <linearGradient id="rule" x2="0" y2="792" gradientUnits="userSpaceOnUse"><stop offset="0.7" stop-color="#1f3f6a" stop-opacity="1"/><stop offset="0.98" stop-color="#1f3f6a" stop-opacity="0"/></linearGradient> | |
| 6 | <linearGradient id="bar" x2="612" y2="0" gradientUnits="userSpaceOnUse"><stop offset="0.5" stop-color="#2e69a5" stop-opacity="1"/><stop offset="0.95" stop-color="#2e69a5" stop-opacity="0"/></linearGradient> | |
| 7 | <radialGradient id="sheen" cx="0" cy="0" r="1" gradientTransform="scale(560 720)" gradientUnits="userSpaceOnUse"><stop offset="0.45" stop-color="#eef3fa"/><stop offset="1" stop-color="#eef3fa" stop-opacity="0"/></radialGradient> | |
| 8 | <radialGradient id="frame" cx="0" cy="0" r="1" gradientTransform="scale(560 720)" gradientUnits="userSpaceOnUse"><stop offset="0.45" stop-color="#d0dcea"/><stop offset="1" stop-color="#d0dcea" stop-opacity="0"/></radialGradient> | |
| 9 | </defs> | |
| 10 | <rect width="24" height="792" fill="url(#glass)"/> | |
| 11 | <path d="M0 392L24 368M0 414L24 390M0 436L24 412M0 458L24 434M0 480L24 456M0 502L24 478M0 524L24 500M0 546L24 522M0 568L24 544M0 590L24 566M0 612L24 588M0 634L24 610M0 656L24 632M-0 678L24 654M0 700L24 676M0 722L24 698M0 744L24 720M0 766L24 742M0 788L24 764M18 792L24 786M0 292L24 316M0 262L24 286M0 232L24 256M0 202L24 226M0 172L24 196M0 142L24 166M0 112L24 136M0 82L24 106M0 52L24 76M0 22L24 46M0 -8L24 16" stroke="url(#mullion)" stroke-width="1.4"/> | |
| 12 | <rect x="26" width="586" height="792" fill="url(#sheen)"/> | |
| 13 | <path d="M26 366L400 -8M26 388L422 -8M26 410L444 -8M26 432L466 -8M26 454L488 -8M26 476L510 -8M26 498L532 -8M26 520L554 -8M26 542L576 -8M26 564L598 -8M26 586L612 0M26 608L612 22M26 630L612 44M26 652L612 66M26 674L612 88M26 696L612 110M26 718L612 132M26 740L612 154M26 762L612 176M26 784L612 198M40 792L612 220M62 792L612 242M84 792L612 264M106 792L612 286M128 792L612 308M150 792L612 330M172 792L612 352M194 792L612 374M216 792L612 396M238 792L612 418M260 792L612 440M282 792L612 462M304 792L612 484M326 792L612 506M348 792L612 528M370 792L612 550M392 792L612 572M414 792L612 594M436 792L612 616M458 792L612 638M480 792L612 660M502 792L612 682M524 792L612 704M546 792L612 726M568 792L612 748M590 792L612 770M26 318L500 792M26 288L530 792M26 258L560 792M26 228L590 792M26 198L612 784M26 168L612 754M26 138L612 724M26 108L612 694M26 78L612 664M26 48L612 634M26 18L612 604M30 -8L612 574M60 -8L612 544M90 -8L612 514M120 -8L612 484M150 -8L612 454M180 -8L612 424M210 -8L612 394M240 -8L612 364M270 -8L612 334M300 -8L612 304M330 -8L612 274M360 -8L612 244M390 -8L612 214M420 -8L612 184M450 -8L612 154M480 -8L612 124M510 -8L612 94M540 -8L612 64M570 -8L612 34M600 -8L612 4" stroke="url(#frame)" stroke-width="1.2"/> | |
| 14 | <rect x="24" y="18" width="2" height="774" fill="url(#rule)"/> | |
| 15 | <rect width="612" height="18" fill="url(#bar)"/> | |
| 16 | </svg> |
crates/canvas/assets/backgrounds/chain-title.svg created+20| ... | ... | @@ -0,0 +1,20 @@ |
| 1 | <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 813 99" preserveAspectRatio="none"> | |
| 2 | <defs> | |
| 3 | <linearGradient id="across" x2="1"><stop offset=".76" stop-color="#f4f5f1"/><stop offset=".99" stop-color="#f4f5f1" stop-opacity="0"/></linearGradient> | |
| 4 | <linearGradient id="rule" x2="1"><stop offset=".76" stop-color="#e09c71"/><stop offset=".98" stop-color="#e09c71" stop-opacity="0"/></linearGradient> | |
| 5 | <linearGradient id="metal" x2="0" y2="1"><stop offset="0" stop-color="#e1e6f5"/><stop offset=".3" stop-color="#98a4cf"/><stop offset=".65" stop-color="#5c6897"/><stop offset="1" stop-color="#aeb9dd"/></linearGradient> | |
| 6 | <filter id="depth" x="-20%" y="-20%" width="140%" height="140%"><feGaussianBlur stdDeviation="1.3"/></filter> | |
| 7 | </defs> | |
| 8 | <rect width="813" height="98" fill="url(#across)"/> | |
| 9 | <rect y="98" width="813" height="1" fill="url(#rule)"/> | |
| 10 | <g opacity=".9"> | |
| 11 | <g filter="url(#depth)" opacity=".75"> | |
| 12 | <rect x="-52" y="-19" width="104" height="38" rx="19" transform="translate(236 30) rotate(58)" fill="none" stroke="url(#metal)" stroke-width="13"/> | |
| 13 | </g> | |
| 14 | <rect x="-44" y="-14" width="88" height="28" rx="14" transform="translate(478 104) rotate(14)" fill="none" stroke="url(#metal)" stroke-width="13"/> | |
| 15 | <rect x="-54" y="-8" width="108" height="16" rx="8" transform="translate(405 76) rotate(27)" fill="url(#metal)"/> | |
| 16 | <path d="M-44 -2H44" transform="translate(405 76) rotate(27)" stroke="#eef1fa" stroke-width="1.6" stroke-linecap="round" stroke-dasharray="14 5"/> | |
| 17 | <rect x="-68" y="-21" width="136" height="42" rx="21" transform="translate(333 46) rotate(14)" fill="none" stroke="url(#metal)" stroke-width="14"/> | |
| 18 | <rect x="-68" y="-21" width="136" height="42" rx="21" transform="translate(332 44) rotate(14)" fill="none" stroke="#f3f5fb" stroke-width="2" opacity=".8"/> | |
| 19 | </g> | |
| 20 | </svg> |
crates/canvas/assets/backgrounds/circles.svg created+9| ... | ... | @@ -0,0 +1,9 @@ |
| 1 | <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 77 627" preserveAspectRatio="none"> | |
| 2 | <circle cx="37" cy="43" r="25" fill="none" stroke="#ffe7f7" stroke-width="11"/> | |
| 3 | <circle cx="37" cy="134" r="30.5" fill="#deefff"/> | |
| 4 | <circle cx="37" cy="210" r="30.5" fill="#e7efde"/> | |
| 5 | <circle cx="37" cy="286" r="25" fill="none" stroke="#d6f3ef" stroke-width="11"/> | |
| 6 | <circle cx="37" cy="425" r="30.5" fill="#ffebce"/> | |
| 7 | <circle cx="37" cy="498" r="25" fill="none" stroke="#fffbce" stroke-width="11"/> | |
| 8 | <circle cx="37" cy="586" r="30.5" fill="#f7e3ff"/> | |
| 9 | </svg> |
crates/canvas/assets/backgrounds/clock.svg created+16| ... | ... | @@ -0,0 +1,16 @@ |
| 1 | <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 612 792" preserveAspectRatio="none"> | |
| 2 | <defs> | |
| 3 | <radialGradient id="reach" cx="0" cy="0" r="1" gradientTransform="translate(0 0) scale(480 520) translate(0 0)" gradientUnits="userSpaceOnUse"><stop offset="0.4" stop-color="white"/><stop offset="1" stop-color="white" stop-opacity="0"/></radialGradient><mask id="art"><rect width="612" height="792" fill="url(#reach)"/></mask> | |
| 4 | <linearGradient id="face" x2="1" y2="1"><stop offset="0" stop-color="#cfe4f6"/><stop offset="1" stop-color="#eef5fb"/></linearGradient> | |
| 5 | </defs> | |
| 6 | <g mask="url(#art)" transform="translate(-20 -60)"> | |
| 7 | <circle cx="560" cy="560" r="680" fill="#2d7ecf"/> | |
| 8 | <circle cx="560" cy="560" r="640" fill="#6aa8e2"/> | |
| 9 | <circle cx="560" cy="560" r="620" fill="#f6f7f9"/> | |
| 10 | <circle cx="560" cy="560" r="580" fill="url(#face)"/> | |
| 11 | <g stroke="#ffffff" stroke-width="3"><path d="M33.8 368.5L62 378.7"/><path d="M56.7 314.5L83.6 327.7"/><path d="M85.1 263.2L110.5 279.1"/><path d="M118.7 215.2L142.4 233.7"/><path d="M157.2 171L178.7 191.8"/><path d="M200 131L219.3 154"/><path d="M246.9 95.7L263.6 120.6"/><path d="M297.1 65.5L311.2 92"/><path d="M350.2 40.8L361.5 68.6"/><path d="M405.6 21.7L413.9 50.5"/><path d="M462.8 8.5L468 38.1"/><path d="M520.9 1.4L523 31.3"/></g> | |
| 12 | <path d="M140 470L320 250" stroke="#f2ecd2" stroke-width="10" stroke-linecap="round"/> | |
| 13 | <path d="M200 560L420 330" stroke="#ffffff" stroke-width="4" stroke-linecap="round"/> | |
| 14 | <rect x="160" y="400" width="26" height="14" fill="#e45b5b" transform="rotate(-40 160 400)"/> | |
| 15 | </g> | |
| 16 | </svg> |
crates/canvas/assets/backgrounds/columns.svg created+33| ... | ... | @@ -0,0 +1,33 @@ |
| 1 | <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 612 792" preserveAspectRatio="none"> | |
| 2 | <defs> | |
| 3 | <linearGradient id="across" x2="1" y2="0"><stop offset="0.05" stop-color="white"/><stop offset="0.55" stop-color="white" stop-opacity="0"/></linearGradient><mask id="page-fade"><rect width="612" height="792" fill="url(#across)"/></mask> | |
| 4 | <linearGradient id="down" x2="0" y2="1"><stop offset="0.75" stop-color="white"/><stop offset="0.98" stop-color="white" stop-opacity="0"/></linearGradient><mask id="strip-fade"><rect width="612" height="792" fill="url(#down)"/></mask> | |
| 5 | <linearGradient id="stone" x2="1"><stop offset="0" stop-color="#6d7198"/><stop offset=".5" stop-color="#9195b8"/><stop offset="1" stop-color="#7c80a5"/></linearGradient> | |
| 6 | <linearGradient id="shaft" x2="1"><stop offset="0" stop-color="#f1f1f6"/><stop offset=".4" stop-color="#e0e0e9"/><stop offset="1" stop-color="#f8f8fb"/></linearGradient> | |
| 7 | </defs> | |
| 8 | <g mask="url(#strip-fade)"> | |
| 9 | <rect width="42" height="792" fill="url(#stone)"/> | |
| 10 | <g stroke="#62688e" stroke-width="1.5"><path d="M6 0V792"/><path d="M12 0V792"/><path d="M18 0V792"/><path d="M24 0V792"/><path d="M30 0V792"/><path d="M36 0V792"/></g> | |
| 11 | <g fill="#b3b6d0" color="#b3b6d0"><g transform="translate(21 30) scale(0.75)"> | |
| 12 | <path d="M-24 0H24V6H-24Z"/><path d="M-20 6C-24 12-26 20-18 26C-10 20-12 12-8 8H8C12 12 10 20 18 26C26 20 24 12 20 6Z"/> | |
| 13 | <path d="M-16 26C-10 34-12 42-6 46H6C12 42 10 34 16 26Z"/><circle cx="-18" cy="16" r="4" fill="none" stroke="currentColor" stroke-width="2"/><circle cx="18" cy="16" r="4" fill="none" stroke="currentColor" stroke-width="2"/> | |
| 14 | <path d="M-12 46H12V52H-12Z"/></g><g transform="translate(21 300) scale(0.75)"> | |
| 15 | <path d="M-24 0H24V6H-24Z"/><path d="M-20 6C-24 12-26 20-18 26C-10 20-12 12-8 8H8C12 12 10 20 18 26C26 20 24 12 20 6Z"/> | |
| 16 | <path d="M-16 26C-10 34-12 42-6 46H6C12 42 10 34 16 26Z"/><circle cx="-18" cy="16" r="4" fill="none" stroke="currentColor" stroke-width="2"/><circle cx="18" cy="16" r="4" fill="none" stroke="currentColor" stroke-width="2"/> | |
| 17 | <path d="M-12 46H12V52H-12Z"/></g><g transform="translate(21 570) scale(0.75)"> | |
| 18 | <path d="M-24 0H24V6H-24Z"/><path d="M-20 6C-24 12-26 20-18 26C-10 20-12 12-8 8H8C12 12 10 20 18 26C26 20 24 12 20 6Z"/> | |
| 19 | <path d="M-16 26C-10 34-12 42-6 46H6C12 42 10 34 16 26Z"/><circle cx="-18" cy="16" r="4" fill="none" stroke="currentColor" stroke-width="2"/><circle cx="18" cy="16" r="4" fill="none" stroke="currentColor" stroke-width="2"/> | |
| 20 | <path d="M-12 46H12V52H-12Z"/></g></g> | |
| 21 | </g> | |
| 22 | <g mask="url(#page-fade)"> | |
| 23 | <g mask="url(#strip-fade)"> | |
| 24 | <rect x="50" y="100" width="110" height="692" fill="url(#shaft)"/> | |
| 25 | <g stroke="#d6d6e2" stroke-width="2"><path d="M62 130V792"/><path d="M76 130V792"/><path d="M90 130V792"/><path d="M104 130V792"/><path d="M118 130V792"/><path d="M132 130V792"/><path d="M146 130V792"/></g> | |
| 26 | <g fill="#d9d9e5" color="#d9d9e5"><g transform="translate(105 -10) scale(2.4)"> | |
| 27 | <path d="M-24 0H24V6H-24Z"/><path d="M-20 6C-24 12-26 20-18 26C-10 20-12 12-8 8H8C12 12 10 20 18 26C26 20 24 12 20 6Z"/> | |
| 28 | <path d="M-16 26C-10 34-12 42-6 46H6C12 42 10 34 16 26Z"/><circle cx="-18" cy="16" r="4" fill="none" stroke="currentColor" stroke-width="2"/><circle cx="18" cy="16" r="4" fill="none" stroke="currentColor" stroke-width="2"/> | |
| 29 | <path d="M-12 46H12V52H-12Z"/></g></g> | |
| 30 | <rect x="150" width="150" height="792" fill="#f4f4f8"/> | |
| 31 | </g> | |
| 32 | </g> | |
| 33 | </svg> |
crates/canvas/assets/backgrounds/computer.svg created+23| ... | ... | @@ -0,0 +1,23 @@ |
| 1 | <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 612 792" preserveAspectRatio="none"> | |
| 2 | <defs> | |
| 3 | <radialGradient id="reach" cx="60" cy="40" r="1" gradientTransform="translate(60 40) scale(330 280) translate(-60 -40)" gradientUnits="userSpaceOnUse"><stop offset="0.35" stop-color="white"/><stop offset="1" stop-color="white" stop-opacity="0"/></radialGradient><mask id="art"><rect width="612" height="792" fill="url(#reach)"/></mask> | |
| 4 | <linearGradient id="across" x2="1" y2="0"><stop offset="0.6" stop-color="white"/><stop offset="0.98" stop-color="white" stop-opacity="0"/></linearGradient><mask id="rule-across"><rect width="612" height="792" fill="url(#across)"/></mask> | |
| 5 | <linearGradient id="down" x2="0" y2="1"><stop offset="0.6" stop-color="white"/><stop offset="0.97" stop-color="white" stop-opacity="0"/></linearGradient><mask id="rule-down"><rect width="612" height="792" fill="url(#down)"/></mask> | |
| 6 | <linearGradient id="screen" x2="0" y2="1"><stop offset="0" stop-color="#89c6e2"/><stop offset="1" stop-color="#c2e2f1"/></linearGradient> | |
| 7 | <linearGradient id="mug" x2="1"><stop offset="0" stop-color="#f7d9c4"/><stop offset=".6" stop-color="#fdf1e8"/><stop offset="1" stop-color="#f3d2bb"/></linearGradient> | |
| 8 | <filter id="soft" x="-10%" y="-10%" width="120%" height="120%"><feGaussianBlur stdDeviation="1.5"/></filter> | |
| 9 | </defs> | |
| 10 | <g mask="url(#art)" filter="url(#soft)"> | |
| 11 | <rect x="-10" y="-10" width="240" height="110" fill="#6f4a36" transform="rotate(-3)"/> | |
| 12 | <rect x="-6" y="-6" width="228" height="98" fill="url(#screen)" transform="rotate(-3)"/> | |
| 13 | <g stroke="#7aa7c2" stroke-width="3"><path d="M80 30V20"/><path d="M87 30V27"/><path d="M94 30V34"/><path d="M101 30V23"/><path d="M108 30V30"/><path d="M115 30V37"/><path d="M122 30V26"/><path d="M129 30V33"/></g> | |
| 14 | <path d="M-20 110L230 104L262 210L-20 222Z" fill="#dfe0e2"/> | |
| 15 | <g fill="#b8bcc2"><rect x="-20" y="128" width="11" height="8" rx="1.5"/><rect x="-6" y="128" width="11" height="8" rx="1.5"/><rect x="8" y="128" width="11" height="8" rx="1.5"/><rect x="22" y="128" width="11" height="8" rx="1.5"/><rect x="36" y="128" width="11" height="8" rx="1.5"/><rect x="50" y="128" width="11" height="8" rx="1.5"/><rect x="64" y="128" width="11" height="8" rx="1.5"/><rect x="78" y="128" width="11" height="8" rx="1.5"/><rect x="92" y="128" width="11" height="8" rx="1.5"/><rect x="106" y="128" width="11" height="8" rx="1.5"/><rect x="120" y="128" width="11" height="8" rx="1.5"/><rect x="134" y="128" width="11" height="8" rx="1.5"/><rect x="148" y="128" width="11" height="8" rx="1.5"/><rect x="162" y="128" width="11" height="8" rx="1.5"/><rect x="176" y="128" width="11" height="8" rx="1.5"/><rect x="190" y="128" width="11" height="8" rx="1.5"/><rect x="-14" y="140" width="11" height="8" rx="1.5"/><rect x="0" y="140" width="11" height="8" rx="1.5"/><rect x="14" y="140" width="11" height="8" rx="1.5"/><rect x="28" y="140" width="11" height="8" rx="1.5"/><rect x="42" y="140" width="11" height="8" rx="1.5"/><rect x="56" y="140" width="11" height="8" rx="1.5"/><rect x="70" y="140" width="11" height="8" rx="1.5"/><rect x="84" y="140" width="11" height="8" rx="1.5"/><rect x="98" y="140" width="11" height="8" rx="1.5"/><rect x="112" y="140" width="11" height="8" rx="1.5"/><rect x="126" y="140" width="11" height="8" rx="1.5"/><rect x="140" y="140" width="11" height="8" rx="1.5"/><rect x="154" y="140" width="11" height="8" rx="1.5"/><rect x="168" y="140" width="11" height="8" rx="1.5"/><rect x="182" y="140" width="11" height="8" rx="1.5"/><rect x="196" y="140" width="11" height="8" rx="1.5"/><rect x="-20" y="152" width="11" height="8" rx="1.5"/><rect x="-6" y="152" width="11" height="8" rx="1.5"/><rect x="8" y="152" width="11" height="8" rx="1.5"/><rect x="22" y="152" width="11" height="8" rx="1.5"/><rect x="36" y="152" width="11" height="8" rx="1.5"/><rect x="50" y="152" width="11" height="8" rx="1.5"/><rect x="64" y="152" width="11" height="8" rx="1.5"/><rect x="78" y="152" width="11" height="8" rx="1.5"/><rect x="92" y="152" width="11" height="8" rx="1.5"/><rect x="106" y="152" width="11" height="8" rx="1.5"/><rect x="120" y="152" width="11" height="8" rx="1.5"/><rect x="134" y="152" width="11" height="8" rx="1.5"/><rect x="148" y="152" width="11" height="8" rx="1.5"/><rect x="162" y="152" width="11" height="8" rx="1.5"/><rect x="176" y="152" width="11" height="8" rx="1.5"/><rect x="190" y="152" width="11" height="8" rx="1.5"/><rect x="-14" y="164" width="11" height="8" rx="1.5"/><rect x="0" y="164" width="11" height="8" rx="1.5"/><rect x="14" y="164" width="11" height="8" rx="1.5"/><rect x="28" y="164" width="11" height="8" rx="1.5"/><rect x="42" y="164" width="11" height="8" rx="1.5"/><rect x="56" y="164" width="11" height="8" rx="1.5"/><rect x="70" y="164" width="11" height="8" rx="1.5"/><rect x="84" y="164" width="11" height="8" rx="1.5"/><rect x="98" y="164" width="11" height="8" rx="1.5"/><rect x="112" y="164" width="11" height="8" rx="1.5"/><rect x="126" y="164" width="11" height="8" rx="1.5"/><rect x="140" y="164" width="11" height="8" rx="1.5"/><rect x="154" y="164" width="11" height="8" rx="1.5"/><rect x="168" y="164" width="11" height="8" rx="1.5"/><rect x="182" y="164" width="11" height="8" rx="1.5"/><rect x="196" y="164" width="11" height="8" rx="1.5"/><rect x="-20" y="176" width="11" height="8" rx="1.5"/><rect x="-6" y="176" width="11" height="8" rx="1.5"/><rect x="8" y="176" width="11" height="8" rx="1.5"/><rect x="22" y="176" width="11" height="8" rx="1.5"/><rect x="36" y="176" width="11" height="8" rx="1.5"/><rect x="50" y="176" width="11" height="8" rx="1.5"/><rect x="64" y="176" width="11" height="8" rx="1.5"/><rect x="78" y="176" width="11" height="8" rx="1.5"/><rect x="92" y="176" width="11" height="8" rx="1.5"/><rect x="106" y="176" width="11" height="8" rx="1.5"/><rect x="120" y="176" width="11" height="8" rx="1.5"/><rect x="134" y="176" width="11" height="8" rx="1.5"/><rect x="148" y="176" width="11" height="8" rx="1.5"/><rect x="162" y="176" width="11" height="8" rx="1.5"/><rect x="176" y="176" width="11" height="8" rx="1.5"/><rect x="190" y="176" width="11" height="8" rx="1.5"/></g> | |
| 16 | <rect x="210" y="-10" width="120" height="40" fill="#f5a577" opacity=".6"/> | |
| 17 | <path d="M80 250C80 200 180 200 180 250V420H80Z" fill="url(#mug)"/> | |
| 18 | <ellipse cx="130" cy="230" rx="50" ry="14" fill="#fbe6d8"/> | |
| 19 | <rect x="-10" y="240" width="40" height="140" fill="#b36235"/> | |
| 20 | </g> | |
| 21 | <rect x="21" y="18" width="591" height="1.5" fill="#39221c" mask="url(#rule-across)"/> | |
| 22 | <rect x="21" y="18" width="1.5" height="774" fill="#39221c" mask="url(#rule-down)"/> | |
| 23 | </svg> |
crates/canvas/assets/backgrounds/cyan-title.svg created+25| ... | ... | @@ -0,0 +1,25 @@ |
| 1 | <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 780 107" preserveAspectRatio="none"> | |
| 2 | <defs> | |
| 3 | <radialGradient id="glow" cx="120" cy="28" r="1" gradientTransform="translate(120 28) scale(290 80) translate(-120 -28)" gradientUnits="userSpaceOnUse"> | |
| 4 | <stop offset="0" stop-color="#b2e6f4"/> | |
| 5 | <stop offset=".45" stop-color="#cdeff8"/> | |
| 6 | <stop offset=".8" stop-color="#eef9fc"/> | |
| 7 | <stop offset="1" stop-color="#eef9fc" stop-opacity="0"/> | |
| 8 | </radialGradient> | |
| 9 | <linearGradient id="line" x2="1"> | |
| 10 | <stop offset="0" stop-color="#4a9ab4"/> | |
| 11 | <stop offset=".5" stop-color="#a9cdd7"/> | |
| 12 | <stop offset=".77" stop-color="#eef5f8"/> | |
| 13 | <stop offset=".82" stop-color="#eef5f8" stop-opacity="0"/> | |
| 14 | </linearGradient> | |
| 15 | <linearGradient id="glint" x2="1"> | |
| 16 | <stop offset="0" stop-color="#c6fbff"/> | |
| 17 | <stop offset=".78" stop-color="#f6feff"/> | |
| 18 | <stop offset=".82" stop-color="#f6feff" stop-opacity="0"/> | |
| 19 | </linearGradient> | |
| 20 | </defs> | |
| 21 | <rect width="780" height="98" fill="url(#glow)"/> | |
| 22 | <rect y="99" width="780" height="7" fill="url(#glint)"/> | |
| 23 | <rect y="100" width="780" height="1" fill="url(#line)"/> | |
| 24 | <rect y="103" width="780" height="2" fill="url(#line)"/> | |
| 25 | </svg> |
crates/canvas/assets/backgrounds/day-planner.svg created+21| ... | ... | @@ -0,0 +1,21 @@ |
| 1 | <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 612 618" preserveAspectRatio="none"> | |
| 2 | <defs> | |
| 3 | <radialGradient id="reach" cx="20" cy="0" r="1" gradientTransform="translate(20 0) scale(460 560) translate(-20 0)" gradientUnits="userSpaceOnUse"><stop offset="0.3" stop-color="white"/><stop offset="0.95" stop-color="white" stop-opacity="0"/></radialGradient><mask id="art"><rect width="612" height="618" fill="url(#reach)"/></mask> | |
| 4 | <linearGradient id="down" x2="0" y2="1"><stop offset="0.6" stop-color="white"/><stop offset="0.95" stop-color="white" stop-opacity="0"/></linearGradient><mask id="spine-fade"><rect width="612" height="618" fill="url(#down)"/></mask> | |
| 5 | <linearGradient id="spine" x2="1"><stop offset="0" stop-color="#fcac2f"/><stop offset=".7" stop-color="#f3a121"/><stop offset="1" stop-color="#e58f16"/></linearGradient> | |
| 6 | <linearGradient id="ring" x2="1"><stop offset="0" stop-color="#7b5a33"/><stop offset=".5" stop-color="#f1e1c0"/><stop offset="1" stop-color="#8b6a3f"/></linearGradient> | |
| 7 | <filter id="soft" x="-10%" y="-10%" width="120%" height="120%"><feGaussianBlur stdDeviation="2"/></filter> | |
| 8 | </defs> | |
| 9 | <g mask="url(#art)" filter="url(#soft)"> | |
| 10 | <rect width="612" height="618" fill="#fbead0"/> | |
| 11 | <path d="M60 20L500 120L420 560L0 520Z" fill="#fdf5e7"/> | |
| 12 | <g stroke="#ecd9b8" stroke-width="1.5"><path d="M80 60L480 150"/><path d="M74 94L474 184"/><path d="M68 128L468 218"/><path d="M62 162L462 252"/><path d="M56 196L456 286"/><path d="M50 230L450 320"/><path d="M44 264L444 354"/><path d="M38 298L438 388"/><path d="M32 332L432 422"/><path d="M26 366L426 456"/></g> | |
| 13 | <circle cx="100" cy="420" r="16" fill="#fff8ea"/> | |
| 14 | </g> | |
| 15 | <g mask="url(#spine-fade)"> | |
| 16 | <rect width="42" height="618" fill="url(#spine)"/> | |
| 17 | <rect x="42" width="4" height="618" fill="#b87a1c" opacity=".5"/> | |
| 18 | <path d="M10 190C60 170 110 185 118 220C124 250 100 262 80 250" fill="none" stroke="url(#ring)" stroke-width="10"/> | |
| 19 | <rect y="170" width="14" height="130" fill="#3a2414" opacity=".8"/> | |
| 20 | </g> | |
| 21 | </svg> |
crates/canvas/assets/backgrounds/fireworks.svg created+25| ... | ... | @@ -0,0 +1,25 @@ |
| 1 | <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 176 513" preserveAspectRatio="none"> | |
| 2 | <defs> | |
| 3 | <g id="bloom"><ellipse cx="4.9" rx="5.1" ry="1.15" transform="rotate(0)"/><ellipse cx="4.9" rx="5.1" ry="1.15" transform="rotate(45)"/><ellipse cx="4.9" rx="5.1" ry="1.15" transform="rotate(90)"/><ellipse cx="4.9" rx="5.1" ry="1.15" transform="rotate(135)"/><ellipse cx="4.9" rx="5.1" ry="1.15" transform="rotate(180)"/><ellipse cx="4.9" rx="5.1" ry="1.15" transform="rotate(225)"/><ellipse cx="4.9" rx="5.1" ry="1.15" transform="rotate(270)"/><ellipse cx="4.9" rx="5.1" ry="1.15" transform="rotate(315)"/></g> | |
| 4 | <g id="spark"><ellipse cx="4.9" rx="5.1" ry="1.25" transform="rotate(0)"/><ellipse cx="4.9" rx="5.1" ry="1.25" transform="rotate(45)"/><ellipse cx="4.9" rx="5.1" ry="1.25" transform="rotate(90)"/><ellipse cx="4.9" rx="5.1" ry="1.25" transform="rotate(135)"/><ellipse cx="4.9" rx="5.1" ry="1.25" transform="rotate(180)"/><ellipse cx="4.9" rx="5.1" ry="1.25" transform="rotate(225)"/><ellipse cx="4.9" rx="5.1" ry="1.25" transform="rotate(270)"/><ellipse cx="4.9" rx="5.1" ry="1.25" transform="rotate(315)"/></g> | |
| 5 | </defs> | |
| 6 | <g fill="#f9c7bf"> | |
| 7 | <use href="#spark" transform="translate(89.5 16.5) scale(1.65)"/> | |
| 8 | <use href="#bloom" transform="translate(80.5 64) scale(3.30)"/> | |
| 9 | <use href="#spark" transform="translate(28 79) scale(1.70)"/> | |
| 10 | <use href="#spark" transform="translate(103.5 113) scale(1.70)"/> | |
| 11 | <use href="#bloom" transform="translate(46 144) scale(4.60)"/> | |
| 12 | <use href="#spark" transform="translate(136 159) scale(1.40)"/> | |
| 13 | <use href="#bloom" transform="translate(109 224.5) scale(6.65)"/> | |
| 14 | <use href="#spark" transform="translate(26.5 206) scale(1.70)"/> | |
| 15 | <use href="#spark" transform="translate(49 248) scale(1.40)"/> | |
| 16 | <use href="#bloom" transform="translate(44 313) scale(4.50)"/> | |
| 17 | <use href="#spark" transform="translate(145 293) scale(1.40)"/> | |
| 18 | <use href="#spark" transform="translate(117 330) scale(1.70)"/> | |
| 19 | <use href="#bloom" transform="translate(133 376) scale(2.70)"/> | |
| 20 | <use href="#bloom" transform="translate(64.5 390) scale(4.05)"/> | |
| 21 | <use href="#spark" transform="translate(130 427.5) scale(1.70)"/> | |
| 22 | <use href="#spark" transform="translate(86.5 452) scale(1.40)"/> | |
| 23 | <use href="#bloom" transform="translate(53 485) scale(2.70)"/> | |
| 24 | </g> | |
| 25 | </svg> |
crates/canvas/assets/backgrounds/flat-squares.svg created+17| ... | ... | @@ -0,0 +1,17 @@ |
| 1 | <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 40 650" preserveAspectRatio="none"> | |
| 2 | <rect x="9.5" y="7.5" width="22" height="22" fill="#ffebce" stroke="#ffba8c"/> | |
| 3 | <rect x="9.5" y="51.6" width="22" height="22" fill="#fffbce" stroke="#ffe384"/> | |
| 4 | <rect x="9.5" y="95.7" width="22" height="22" fill="#ffe7f7" stroke="#e7b6ce"/> | |
| 5 | <rect x="9.5" y="139.8" width="22" height="22" fill="#deefff" stroke="#9cb6ef"/> | |
| 6 | <rect x="9.5" y="183.9" width="22" height="22" fill="#f7e3ff" stroke="#c6aee7"/> | |
| 7 | <rect x="9.5" y="228" width="22" height="22" fill="#e7efde" stroke="#c6d3ad"/> | |
| 8 | <rect x="9.5" y="272.1" width="22" height="22" fill="#d6f3ef" stroke="#adcbc6"/> | |
| 9 | <rect x="9.5" y="316.2" width="22" height="22" fill="#ffe7e7" stroke="#f7a6ad"/> | |
| 10 | <rect x="9.5" y="360.3" width="22" height="22" fill="#ffebce" stroke="#ffba8c"/> | |
| 11 | <rect x="9.5" y="404.4" width="22" height="22" fill="#fffbce" stroke="#ffe384"/> | |
| 12 | <rect x="9.5" y="448.5" width="22" height="22" fill="#ffe7f7" stroke="#e7b6ce"/> | |
| 13 | <rect x="9.5" y="492.6" width="22" height="22" fill="#deefff" stroke="#9cb6ef"/> | |
| 14 | <rect x="9.5" y="536.7" width="22" height="22" fill="#f7e3ff" stroke="#c6aee7"/> | |
| 15 | <rect x="9.5" y="580.8" width="22" height="22" fill="#e7efde" stroke="#c6d3ad"/> | |
| 16 | <rect x="9.5" y="624.9" width="22" height="22" fill="#d6f3ef" stroke="#adcbc6"/> | |
| 17 | </svg> |
crates/canvas/assets/backgrounds/flowers-and-hearts.svg created+36| ... | ... | @@ -0,0 +1,36 @@ |
| 1 | <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 102 792" preserveAspectRatio="none"> | |
| 2 | <defs> | |
| 3 | <linearGradient id="down" x2="0" y2="1"><stop offset=".7" stop-color="white"/><stop offset=".99" stop-color="white" stop-opacity="0"/></linearGradient> | |
| 4 | <mask id="fade"><rect width="102" height="792" fill="url(#down)"/></mask> | |
| 5 | <g id="sprig"> | |
| 6 | <path d="M42 70C36 95 33 125 34 150S31 200 31 240" fill="none" stroke="#7fbf73" stroke-width="1.8"/> | |
| 7 | <path d="M57 205C62 215 64 222 60 232" fill="none" stroke="#7fbf73" stroke-width="1.2"/> | |
| 8 | <path d="M29 80C20 86 17 100 24 110C28 104 31 92 29 80Z" fill="#6cb05f"/> | |
| 9 | <path d="M31 148C20 160 14 185 17 212C26 200 32 175 31 148Z" fill="#6cb05f"/> | |
| 10 | <path d="M31 212C30 190 42 170 66 160C68 168 62 176 56 180C54 194 44 206 31 212Z" fill="#92c886"/> | |
| 11 | <path d="M62 214C55 226 55 242 62 258C68 244 68 228 62 214Z" fill="#8cc580"/> | |
| 12 | <path d="M14 245C18 230 28 222 33 228C31 238 22 245 14 245Z" fill="#8cc580"/> | |
| 13 | <g fill="none" stroke="#e8837b" stroke-width="2.2" stroke-linecap="round"> | |
| 14 | <path d="M31 106C24 110 12 109 6 102C2 95 6 88 13 89C16 84 24 85 25 92C26 98 30 101 31 106"/> | |
| 15 | <path d="M33 224C29 226 27 219 30 216C32 212 37 215 36 218C40 216 43 221 40 224C37 227 33 226 33 224Z"/> | |
| 16 | </g> | |
| 17 | <path d="M44 44L49 50L52 42L56 50L64 46C62 56 60 66 52 70C46 71 41 66 41 58C41 52 42 47 44 44Z" fill="#e8837b"/> | |
| 18 | <path d="M47 50C46 58 48 65 52 68" fill="none" stroke="#d6655e" stroke-width="1.2"/> | |
| 19 | <path d="M62 48C66 49 65 56 61 60" fill="none" stroke="#f3aaa3" stroke-width="1.4"/> | |
| 20 | <g transform="translate(51 133)"> | |
| 21 | <path d="M-28 5C-30-6-22-14-8-14C0-22 16-20 22-12C32-10 36-2 30 6C36 12 32 22 22 22C14 26 0 26-8 22C-20 24-28 16-28 5Z" fill="#f1a09a"/> | |
| 22 | <path d="M-20 -6C-10-10 6-10 16-4C24 0 26 6 22 8C12 2 0 0-14 2C-18 1-21-2-20-6Z" fill="#e07571"/> | |
| 23 | <ellipse cx="-12" cy="9" rx="10" ry="8" fill="#f7cdc8"/> | |
| 24 | <ellipse cx="12" cy="13" rx="11" ry="6" fill="#f5c3be"/> | |
| 25 | <path d="M-26-4C-24-10-16-14-10-12" fill="none" stroke="#f7d3cf" stroke-width="2.4" stroke-linecap="round"/> | |
| 26 | </g> | |
| 27 | </g> | |
| 28 | </defs> | |
| 29 | <g mask="url(#fade)"> | |
| 30 | <rect width="50" height="792" fill="#b6d6b1"/> | |
| 31 | <use href="#sprig"/> | |
| 32 | <use href="#sprig" y="248"/> | |
| 33 | <use href="#sprig" y="496"/> | |
| 34 | <use href="#sprig" y="744"/> | |
| 35 | </g> | |
| 36 | </svg> |
crates/canvas/assets/backgrounds/glasses-corner.svg created+15| ... | ... | @@ -0,0 +1,15 @@ |
| 1 | <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 276 139" preserveAspectRatio="none"> | |
| 2 | <defs> | |
| 3 | <radialGradient id="reach" cx="60" cy="0" r="1" gradientTransform="translate(60 0) scale(260 150) translate(-60 0)" gradientUnits="userSpaceOnUse"><stop offset="0.35" stop-color="white"/><stop offset="1" stop-color="white" stop-opacity="0"/></radialGradient><mask id="art"><rect width="276" height="139" fill="url(#reach)"/></mask> | |
| 4 | </defs> | |
| 5 | <g mask="url(#art)" fill="none" stroke-linecap="round"> | |
| 6 | <path d="M-10 120L250 20L270 50L10 139Z" fill="#f4f3f1" stroke="#c9c6c2"/> | |
| 7 | <g stroke="#cfcac5" stroke-width=".9"><path d="M-40 154.0L0 104.0"/><path d="M-35 152.25L5 102.25"/><path d="M-30 150.5L10 100.5"/><path d="M-25 148.75L15 98.75"/><path d="M-20 147.0L20 97.0"/><path d="M-15 145.25L25 95.25"/><path d="M-10 143.5L30 93.5"/><path d="M-5 141.75L35 91.75"/><path d="M0 140.0L40 90.0"/><path d="M5 138.25L45 88.25"/><path d="M10 136.5L50 86.5"/><path d="M15 134.75L55 84.75"/><path d="M20 133.0L60 83.0"/><path d="M25 131.25L65 81.25"/><path d="M30 129.5L70 79.5"/><path d="M35 127.75L75 77.75"/><path d="M40 126.0L80 76.0"/><path d="M45 124.25L85 74.25"/><path d="M50 122.5L90 72.5"/><path d="M55 120.75L95 70.75"/><path d="M60 119.0L100 69.0"/><path d="M65 117.25L105 67.25"/><path d="M70 115.5L110 65.5"/><path d="M75 113.75L115 63.75"/><path d="M80 112.0L120 62.0"/><path d="M85 110.25L125 60.25"/><path d="M90 108.5L130 58.5"/><path d="M95 106.75L135 56.75"/><path d="M100 105.0L140 55.0"/><path d="M105 103.25L145 53.25"/><path d="M110 101.5L150 51.5"/><path d="M115 99.75L155 49.75"/><path d="M120 98.0L160 48.0"/><path d="M125 96.25L165 46.25"/><path d="M130 94.5L170 44.5"/><path d="M135 92.75L175 42.75"/><path d="M140 91.0L180 41.0"/><path d="M145 89.25L185 39.25"/><path d="M150 87.5L190 37.5"/><path d="M155 85.75L195 35.75"/><path d="M160 84.0L200 34.0"/><path d="M165 82.25L205 32.25000000000001"/><path d="M170 80.5L210 30.500000000000007"/><path d="M175 78.75L215 28.750000000000007"/><path d="M180 77.0L220 27.000000000000007"/><path d="M185 75.25L225 25.25"/><path d="M190 73.5L230 23.5"/><path d="M195 71.75L235 21.75"/><path d="M200 70.0L240 20.0"/><path d="M205 68.25L245 18.25"/><path d="M210 66.5L250 16.5"/><path d="M215 64.75L255 14.75"/><path d="M220 63.0L260 13.0"/><path d="M225 61.25L265 11.25"/><path d="M230 59.5L270 9.5"/><path d="M235 57.75L275 7.75"/><path d="M240 56.0L280 6.0"/><path d="M245 54.25L285 4.25"/><path d="M250 52.5L290 2.5"/><path d="M255 50.75L295 0.75"/><path d="M260 49.0L300 -1.0"/><path d="M265 47.25L305 -2.75"/><path d="M270 45.5L310 -4.5"/><path d="M275 43.75L315 -6.25"/><path d="M280 42.0L320 -8.0"/><path d="M285 40.25L325 -9.75"/><path d="M290 38.5L330 -11.5"/><path d="M295 36.75L335 -13.25"/></g> | |
| 8 | <g stroke="#b8b3ae" stroke-width="1.2"><path d="M40 110l14-5"/><path d="M60 102l14-5"/><path d="M80 94l14-5"/><path d="M100 86l14-5"/><path d="M120 78l14-5"/><path d="M140 70l14-5"/><path d="M160 62l14-5"/><path d="M180 54l14-5"/><path d="M200 46l14-5"/><path d="M60 118l12-4"/><path d="M80 110l12-4"/><path d="M100 102l12-4"/><path d="M120 94l12-4"/><path d="M140 86l12-4"/><path d="M160 78l12-4"/><path d="M180 70l12-4"/><path d="M200 62l12-4"/></g> | |
| 9 | <g stroke="#8c8781" stroke-width="2.4"> | |
| 10 | <ellipse cx="62" cy="22" rx="30" ry="18" transform="rotate(-12 62 22)"/> | |
| 11 | <ellipse cx="134" cy="10" rx="30" ry="18" transform="rotate(-12 134 10)"/> | |
| 12 | <path d="M92 18C98 12 102 12 106 14"/><path d="M33 30L0 40"/> | |
| 13 | </g> | |
| 14 | </g> | |
| 15 | </svg> |
crates/canvas/assets/backgrounds/globe.svg created+18| ... | ... | @@ -0,0 +1,18 @@ |
| 1 | <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 164 641" preserveAspectRatio="none"> | |
| 2 | <defs> | |
| 3 | <linearGradient id="down" x2="0" y2="1"><stop offset="0.3" stop-color="white"/><stop offset="0.62" stop-color="white" stop-opacity="0"/></linearGradient><mask id="fade"><rect width="164" height="641" fill="url(#down)"/></mask> | |
| 4 | <linearGradient id="rule" x2="0" y2="1"><stop offset="0.7" stop-color="white"/><stop offset="0.95" stop-color="white" stop-opacity="0"/></linearGradient><mask id="rule-fade"><rect width="164" height="641" fill="url(#rule)"/></mask> | |
| 5 | <radialGradient id="sea" cx=".35" cy=".35" r=".7"><stop offset="0" stop-color="#f2fbfb"/><stop offset=".6" stop-color="#cde9ec"/><stop offset="1" stop-color="#9cc6c9"/></radialGradient> | |
| 6 | <linearGradient id="brass" x2="1"><stop offset="0" stop-color="#b98a3a"/><stop offset=".5" stop-color="#f3d58c"/><stop offset="1" stop-color="#a0752e"/></linearGradient> | |
| 7 | </defs> | |
| 8 | <g mask="url(#fade)"> | |
| 9 | <circle cx="15" cy="120" r="115" fill="url(#sea)"/> | |
| 10 | <g fill="#d6e1c4" opacity=".8"><path d="M-10 60C10 50 60 70 50 90C40 100 30 120 10 110C0 90 5 70 20 60Z"/><path d="M50 130C70 120 90 140 80 170C65 180 50 170 48 150Z"/></g> | |
| 11 | <path d="M-70 120A90 90 0 0 0 110 150" fill="none" stroke="#c9b27a" stroke-width="4"/> | |
| 12 | <rect x="9" y="235" width="12" height="40" fill="url(#brass)"/> | |
| 13 | <ellipse cx="15" cy="282" rx="40" ry="10" fill="url(#brass)"/> | |
| 14 | <rect x="71" y="0" width="93" height="641" fill="#ffffff" opacity=".75"/> | |
| 15 | </g> | |
| 16 | <rect x="57" width="14" height="641" fill="#c3cba2" mask="url(#rule-fade)"/> | |
| 17 | <rect x="71" width="1" height="641" fill="#5c682a" mask="url(#rule-fade)"/> | |
| 18 | </svg> |
crates/canvas/assets/backgrounds/graph.svg created+18| ... | ... | @@ -0,0 +1,18 @@ |
| 1 | <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 50 556" preserveAspectRatio="none"> | |
| 2 | <g fill="none"> | |
| 3 | <path transform="translate(0 0)" stroke="#b8caf3" d="M8.5 11.5V47.5M20.5 11.5V47.5M32.5 11.5V47.5M44.5 11.5V47.5M8.5 11.5H44.5M8.5 23.5H44.5M8.5 35.5H44.5M8.5 47.5H44.5"/> | |
| 4 | <path transform="translate(0 38)" stroke="#f9bfc4" d="M8.5 11.5V47.5M20.5 11.5V47.5M32.5 11.5V47.5M44.5 11.5V47.5M8.5 11.5H44.5M8.5 23.5H44.5M8.5 35.5H44.5M8.5 47.5H44.5"/> | |
| 5 | <path transform="translate(0 76)" stroke="#c4d9d6" d="M8.5 11.5V47.5M20.5 11.5V47.5M32.5 11.5V47.5M44.5 11.5V47.5M8.5 11.5H44.5M8.5 23.5H44.5M8.5 35.5H44.5M8.5 47.5H44.5"/> | |
| 6 | <path transform="translate(0 114)" stroke="#d6c5ee" d="M8.5 11.5V47.5M20.5 11.5V47.5M32.5 11.5V47.5M44.5 11.5V47.5M8.5 11.5H44.5M8.5 23.5H44.5M8.5 35.5H44.5M8.5 47.5H44.5"/> | |
| 7 | <path transform="translate(0 152)" stroke="#ffeba6" d="M8.5 11.5V47.5M20.5 11.5V47.5M32.5 11.5V47.5M44.5 11.5V47.5M8.5 11.5H44.5M8.5 23.5H44.5M8.5 35.5H44.5M8.5 47.5H44.5"/> | |
| 8 | <path transform="translate(0 190)" stroke="#ffcdac" d="M8.5 11.5V47.5M20.5 11.5V47.5M32.5 11.5V47.5M44.5 11.5V47.5M8.5 11.5H44.5M8.5 23.5H44.5M8.5 35.5H44.5M8.5 47.5H44.5"/> | |
| 9 | <path transform="translate(0 228)" stroke="#eecadc" d="M8.5 11.5V47.5M20.5 11.5V47.5M32.5 11.5V47.5M44.5 11.5V47.5M8.5 11.5H44.5M8.5 23.5H44.5M8.5 35.5H44.5M8.5 47.5H44.5"/> | |
| 10 | <path transform="translate(0 266)" stroke="#b8caf3" d="M8.5 11.5V47.5M20.5 11.5V47.5M32.5 11.5V47.5M44.5 11.5V47.5M8.5 11.5H44.5M8.5 23.5H44.5M8.5 35.5H44.5M8.5 47.5H44.5"/> | |
| 11 | <path transform="translate(0 304)" stroke="#f9bfc4" d="M8.5 11.5V47.5M20.5 11.5V47.5M32.5 11.5V47.5M44.5 11.5V47.5M8.5 11.5H44.5M8.5 23.5H44.5M8.5 35.5H44.5M8.5 47.5H44.5"/> | |
| 12 | <path transform="translate(0 342)" stroke="#c4d9d6" d="M8.5 11.5V47.5M20.5 11.5V47.5M32.5 11.5V47.5M44.5 11.5V47.5M8.5 11.5H44.5M8.5 23.5H44.5M8.5 35.5H44.5M8.5 47.5H44.5"/> | |
| 13 | <path transform="translate(0 380)" stroke="#d6c5ee" d="M8.5 11.5V47.5M20.5 11.5V47.5M32.5 11.5V47.5M44.5 11.5V47.5M8.5 11.5H44.5M8.5 23.5H44.5M8.5 35.5H44.5M8.5 47.5H44.5"/> | |
| 14 | <path transform="translate(0 418)" stroke="#ffeba6" d="M8.5 11.5V47.5M20.5 11.5V47.5M32.5 11.5V47.5M44.5 11.5V47.5M8.5 11.5H44.5M8.5 23.5H44.5M8.5 35.5H44.5M8.5 47.5H44.5"/> | |
| 15 | <path transform="translate(0 456)" stroke="#ffcdac" d="M8.5 11.5V47.5M20.5 11.5V47.5M32.5 11.5V47.5M44.5 11.5V47.5M8.5 11.5H44.5M8.5 23.5H44.5M8.5 35.5H44.5M8.5 47.5H44.5"/> | |
| 16 | <path transform="translate(0 494)" stroke="#eecadc" d="M8.5 11.5V47.5M20.5 11.5V47.5M32.5 11.5V47.5M44.5 11.5V47.5M8.5 11.5H44.5M8.5 23.5H44.5M8.5 35.5H44.5M8.5 47.5H44.5"/> | |
| 17 | </g> | |
| 18 | </svg> |
crates/canvas/assets/backgrounds/green-stripes.svg created+12| ... | ... | @@ -0,0 +1,12 @@ |
| 1 | <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 612 792" preserveAspectRatio="none"> | |
| 2 | <defs> | |
| 3 | <linearGradient id="columns" x2="88" gradientUnits="userSpaceOnUse"><stop offset="0.000" stop-color="#c0cbab"/><stop offset="0.045" stop-color="#c9d3ba"/><stop offset="0.114" stop-color="#d4dcc7"/><stop offset="0.136" stop-color="#dbe2d2"/><stop offset="0.182" stop-color="#c0cdaf"/><stop offset="0.227" stop-color="#bfcdac"/><stop offset="0.273" stop-color="#d2dac3"/><stop offset="0.341" stop-color="#e0e4d6"/><stop offset="0.386" stop-color="#cad4bb"/><stop offset="0.432" stop-color="#c6d3b7"/><stop offset="0.477" stop-color="#c6d0b5"/><stop offset="0.523" stop-color="#d0d9c4"/><stop offset="0.591" stop-color="#dde1d2"/><stop offset="0.636" stop-color="#c1ceb0"/><stop offset="0.682" stop-color="#b9c9a5"/><stop offset="0.727" stop-color="#c3ceb0"/><stop offset="0.773" stop-color="#dce2d4"/><stop offset="0.795" stop-color="#a3ad92"/><stop offset="0.818" stop-color="#b1bba3"/><stop offset="0.864" stop-color="#d1d5c7"/><stop offset="0.909" stop-color="#e8ebe4"/><stop offset="0.955" stop-color="#f8f8f6"/><stop offset="1" stop-color="#f8f8f6" stop-opacity="0"/></linearGradient> | |
| 4 | <linearGradient id="rows" x2="0" y2="30" gradientUnits="userSpaceOnUse"><stop offset="0.000" stop-color="#bbc9a6"/><stop offset="0.133" stop-color="#d4dbc9"/><stop offset="0.200" stop-color="#d0d8c1"/><stop offset="0.333" stop-color="#c1ccae"/><stop offset="0.533" stop-color="#b9c7a4"/><stop offset="0.667" stop-color="#bdcbaa"/><stop offset="0.733" stop-color="#a1ab90"/><stop offset="0.800" stop-color="#bec5b3"/><stop offset="0.867" stop-color="#e5e8e1"/><stop offset="1" stop-color="#e5e8e1" stop-opacity="0"/></linearGradient> | |
| 5 | <linearGradient id="fade-down" x2="0" y2="1"><stop offset=".6" stop-color="white"/><stop offset=".88" stop-color="white" stop-opacity="0"/></linearGradient> | |
| 6 | <linearGradient id="fade-across" x2="1"><stop offset=".44" stop-color="white"/><stop offset=".69" stop-color="white" stop-opacity="0"/></linearGradient> | |
| 7 | <mask id="columns-fade"><rect width="612" height="792" fill="url(#fade-down)"/></mask> | |
| 8 | <mask id="rows-fade"><rect width="612" height="792" fill="url(#fade-across)"/></mask> | |
| 9 | </defs> | |
| 10 | <rect width="88" height="792" fill="url(#columns)" mask="url(#columns-fade)"/> | |
| 11 | <rect width="612" height="30" fill="url(#rows)" mask="url(#rows-fade)"/> | |
| 12 | </svg> |
crates/canvas/assets/backgrounds/hearts.svg created+11| ... | ... | @@ -0,0 +1,11 @@ |
| 1 | <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 171 552" preserveAspectRatio="none"> | |
| 2 | <defs><path id="heart" d="M0 10C-5 6-10.5 1.5-10.5-3.5C-10.5-8-7-10.5-4-10.5C-2-10.5-.5-9.2 0-7.5C.5-9.2 2-10.5 4-10.5C7-10.5 10.5-8 10.5-3.5C10.5 1.5 5 6 0 10Z"/></defs> | |
| 3 | <g transform="translate(70 58) rotate(-35) scale(5.6)"><use href="#heart" fill="#dcbccf"/><use href="#heart" fill="#ddd1d9" transform="translate(-1.2 -1.4) scale(.8)"/></g> | |
| 4 | <g transform="translate(130 108) rotate(90) scale(4.3)"><use href="#heart" fill="#99d3a8"/><use href="#heart" fill="#bee2ac" transform="translate(-1.2 -1.4) scale(.8)"/></g> | |
| 5 | <g transform="translate(62 166) rotate(-70) scale(4.3)"><use href="#heart" fill="#da91ba"/><use href="#heart" fill="#dcbccf" transform="translate(-1.2 -1.4) scale(.8)"/></g> | |
| 6 | <g transform="translate(118 230) rotate(35) scale(4.8)"><use href="#heart" fill="#b0dede"/><use href="#heart" fill="#d2ebe5" transform="translate(-1.2 -1.4) scale(.8)"/></g> | |
| 7 | <g transform="translate(52 322) rotate(-135) scale(4.6)"><use href="#heart" fill="#b0dede"/><use href="#heart" fill="#d2ebe5" transform="translate(-1.2 -1.4) scale(.8)"/></g> | |
| 8 | <g transform="translate(110 382) rotate(110) scale(4.3)"><use href="#heart" fill="#da91ba"/><use href="#heart" fill="#dcbccf" transform="translate(-1.2 -1.4) scale(.8)"/></g> | |
| 9 | <g transform="translate(40 443) rotate(-120) scale(4.3)"><use href="#heart" fill="#99d3a8"/><use href="#heart" fill="#bee2ac" transform="translate(-1.2 -1.4) scale(.8)"/></g> | |
| 10 | <g transform="translate(100 497) rotate(172) scale(5.6)"><use href="#heart" fill="#dcbccf"/><use href="#heart" fill="#ddd1d9" transform="translate(-1.2 -1.4) scale(.8)"/></g> | |
| 11 | </svg> |
crates/canvas/assets/backgrounds/ivy.svg created+158| ... | ... | @@ -0,0 +1,158 @@ |
| 1 | <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 216 792" preserveAspectRatio="none"> | |
| 2 | <defs> | |
| 3 | <clipPath id="strip"><rect x="-10" y="-10" width="43" height="812"/></clipPath> | |
| 4 | <clipPath id="page"><rect x="35" y="-10" width="191" height="812"/></clipPath> | |
| 5 | <linearGradient id="fade" x1="0" y1="0" x2="0" y2="792" gradientUnits="userSpaceOnUse"> | |
| 6 | <stop offset=".93" stop-color="#8fce73"/><stop offset="1" stop-color="#8fce73" stop-opacity="0"/> | |
| 7 | </linearGradient> | |
| 8 | <g id="ivy"> | |
| 9 | <g fill="none" stroke="#c4ac2c" stroke-width=".6"> | |
| 10 | <path d="M35 20C33.7 21.7 29 25.8 27 30C25 34.2 23.2 40 23 45C22.8 50 24.3 55.8 26 60C27.7 64.2 31.8 68.3 33 70"/> | |
| 11 | <path d="M6.1 176.5C5.3 176.6 2.9 176.3 1.6 176.6C0.2 177 -1.1 177.9 -2 178.8C-3 179.8 -3.6 181.1 -3.9 182.3C-4.3 183.5 -4.2 184.9 -3.9 186C-3.6 187.1 -2.8 188.2 -2.1 188.9C-1.3 189.7 -0.2 190.2 0.7 190.5C1.7 190.7 2.8 190.6 3.7 190.4C4.5 190.1 5.4 189.5 5.9 188.9C6.5 188.3 6.8 187.5 7 186.8C7.2 186.1 7 185.2 6.8 184.6C6.6 184 6.2 183.4 5.7 183.1C5.3 182.7 4.7 182.5 4.2 182.4C3.7 182.4 3.2 182.5 2.8 182.7C2.5 182.9 2.1 183.4 2 183.5"/> | |
| 12 | <path d="M33 172C31.2 170.7 25.8 165 22 164C18.2 163 13.3 164 10 166C6.7 168 3.3 174.3 2 176"/> | |
| 13 | <path d="M10 196C11.7 199.2 17.7 207.7 20 215C22.3 222.3 24 231.7 24 240C24 248.3 22.3 257.5 20 265C17.7 272.5 12 278.3 10 285C8 291.7 8.3 301.7 8 305"/> | |
| 14 | <path d="M13.9 315.7C14.9 315.7 18.1 315.3 19.9 315.8C21.7 316.3 23.5 317.5 24.7 318.8C25.9 320 26.9 321.8 27.3 323.4C27.7 325 27.6 326.9 27.2 328.3C26.7 329.8 25.8 331.3 24.8 332.3C23.7 333.3 22.3 334 21 334.3C19.8 334.6 18.3 334.5 17.1 334.1C16 333.8 14.9 333 14.1 332.2C13.4 331.4 12.9 330.3 12.7 329.4C12.5 328.4 12.6 327.3 12.9 326.5C13.2 325.7 13.8 324.9 14.4 324.4C14.9 323.9 15.8 323.7 16.4 323.6C17 323.5 17.7 323.7 18.2 323.9C18.7 324.2 19.2 324.8 19.3 325"/> | |
| 15 | <path d="M8 338C6.8 341 1 349.8 1 356C1 362.2 4.5 370 8 375C11.5 380 17.8 383.2 22 386C26.2 388.8 31.2 391 33 392"/> | |
| 16 | <path d="M33 410C30.5 412.5 22.5 420 18 425C13.5 430 9 435.5 6 440C3 444.5 1 450 0 452"/> | |
| 17 | <path d="M5.7 467C5.8 467.8 6.6 470.4 6.4 471.9C6.3 473.5 5.6 475.1 4.7 476.3C3.9 477.5 2.6 478.5 1.3 479.1C0.1 479.6 -1.5 479.8 -2.8 479.7C-4 479.6 -5.4 479 -6.3 478.3C-7.3 477.6 -8.1 476.5 -8.5 475.5C-9 474.5 -9.1 473.3 -9 472.3C-8.9 471.3 -8.4 470.3 -7.9 469.6C-7.3 468.8 -6.5 468.3 -5.7 468C-5 467.7 -4 467.6 -3.3 467.7C-2.6 467.8 -1.9 468.2 -1.4 468.6C-0.9 469.1 -0.6 469.7 -0.4 470.2C-0.3 470.7 -0.3 471.3 -0.4 471.8C-0.6 472.2 -1.1 472.6 -1.2 472.8"/> | |
| 18 | <path d="M33 543C30.5 545.2 22.7 550.7 18 556C13.3 561.3 8 567.7 5 575C2 582.3 0 591.7 0 600C0 608.3 2 617.5 5 625C8 632.5 14.2 639.2 18 645C21.8 650.8 25.5 656.2 28 660C30.5 663.8 32.2 666.7 33 668"/> | |
| 19 | <path d="M30.6 29.6C31.2 29 32.6 26.7 33.9 25.8C35.2 24.9 36.9 24.4 38.3 24.3C39.8 24.2 41.4 24.6 42.7 25.2C43.9 25.7 45 26.8 45.8 27.9C46.5 28.9 46.9 30.3 47 31.5C47.1 32.7 46.8 34 46.3 35C45.8 36 44.9 36.8 44.1 37.4C43.3 37.9 42.2 38.2 41.3 38.3C40.4 38.3 39.4 38 38.6 37.7C37.9 37.3 37.3 36.6 36.9 36C36.6 35.4 36.4 34.6 36.4 33.9C36.4 33.3 36.7 32.7 37 32.2C37.3 31.7 37.7 31.4 38.2 31.2C38.6 31 39.2 31.1 39.4 31.1"/> | |
| 20 | <path d="M48.1 113.6C49.1 111.5 51.4 104.3 54.3 101C57.1 97.7 61.3 95 65.2 93.7C69.1 92.3 73.7 92.1 77.5 92.8C81.3 93.6 85.1 95.6 87.9 97.9C90.6 100.3 92.7 103.7 93.8 106.9C94.9 110 95 113.8 94.4 116.8C93.8 119.8 92.1 122.8 90.2 124.9C88.4 127 85.7 128.6 83.2 129.4C80.8 130.1 78 130.1 75.7 129.6C73.5 129.1 71.3 127.7 69.9 126.3C68.4 124.9 67.4 122.9 67 121.2C66.5 119.4 66.7 117.5 67.2 116C67.7 114.6 68.7 113.3 69.7 112.5C70.7 111.7 72.6 111.4 73.1 111.2"/> | |
| 21 | <path d="M104 120C105 125 110 139.2 110 150C110 160.8 108.2 174.2 104 185C99.8 195.8 92.3 206.2 85 215C77.7 223.8 68.2 232.2 60 238C51.8 243.8 40 248 36 250"/> | |
| 22 | <path d="M54.2 385.8C54.9 386.9 56.6 390.9 58.4 392.6C60.2 394.4 62.7 395.7 65 396.2C67.2 396.8 69.9 396.7 72 396.1C74.1 395.5 76.2 394.2 77.6 392.7C79.1 391.2 80.1 389.1 80.6 387.3C81 385.5 80.9 383.3 80.4 381.6C79.9 380 78.8 378.3 77.7 377.2C76.5 376.1 74.9 375.4 73.5 375C72 374.7 70.4 374.9 69.2 375.3C67.9 375.7 66.8 376.6 66 377.4C65.2 378.3 64.8 379.5 64.6 380.5C64.4 381.5 64.7 382.6 65 383.4C65.3 384.2 66 384.9 66.6 385.3C67.2 385.7 68.3 385.8 68.6 385.9"/> | |
| 23 | <path d="M60 399C58 402.8 52 414 48 422C44 430 38 442.8 36 447"/> | |
| 24 | <path d="M41.3 584.5C41.4 582.7 40.6 576.9 41.5 573.6C42.5 570.2 44.6 567 46.9 564.7C49.2 562.4 52.5 560.8 55.4 560C58.3 559.3 61.7 559.4 64.4 560.2C67.2 561 69.8 562.7 71.6 564.6C73.5 566.5 74.8 569.1 75.3 571.5C75.9 573.8 75.7 576.5 75.1 578.6C74.5 580.7 73.1 582.8 71.6 584.1C70.1 585.5 68.1 586.4 66.3 586.8C64.6 587.2 62.6 586.9 61.1 586.4C59.6 585.9 58.2 584.7 57.3 583.7C56.4 582.6 55.9 581.1 55.7 579.9C55.6 578.8 55.9 577.5 56.4 576.6C56.8 575.7 58 574.9 58.4 574.5"/> | |
| 25 | <path d="M107.9 646.9C107.9 648.1 107.4 652.1 108.1 654.4C108.7 656.6 110.1 658.8 111.7 660.4C113.3 661.9 115.5 663.1 117.5 663.6C119.5 664.1 121.8 664 123.7 663.5C125.5 662.9 127.3 661.7 128.6 660.5C129.8 659.2 130.7 657.4 131.1 655.8C131.5 654.2 131.4 652.3 130.9 650.9C130.5 649.5 129.5 648.1 128.5 647.1C127.5 646.2 126.1 645.6 124.9 645.3C123.8 645.1 122.4 645.3 121.4 645.6C120.3 646 119.4 646.7 118.8 647.5C118.2 648.2 117.8 649.2 117.7 650C117.6 650.8 117.9 651.7 118.2 652.3C118.5 652.9 119.3 653.4 119.5 653.7"/> | |
| 26 | <path d="M38 598C40.3 605 45.3 628.3 52 640C58.7 651.7 70.8 658 78 668C85.2 678 91.3 689.7 95 700C98.7 710.3 99.7 719.2 100 730C100.3 740.8 97.5 759.2 97 765"/> | |
| 27 | <path d="M108 664C106 670 98 694 96 700"/> | |
| 28 | </g> | |
| 29 | <g fill="#8fce73" stroke="#c4ac2c" stroke-width=".35"> | |
| 30 | <path d="M31 17C30.2 14.3 31.3 11.4 34.1 8.5C34.1 12.4 33.1 15.4 31 17Z"/> | |
| 31 | <path d="M27 12C23.4 10.9 21.1 7.3 20.1 2.2C24.4 5.1 26.9 8.4 27 12Z"/> | |
| 32 | <path d="M28 47C21.9 48.3 15.6 45.2 9.9 38.5C18.4 39.2 24.8 42 28 47Z"/> | |
| 33 | <path d="M29 58C23 62.6 14.6 62.3 5 58C14.6 54.4 23 54.1 29 58Z"/> | |
| 34 | <path d="M24 33C20 32.1 17.2 28.5 15.6 23C20.5 25.8 23.5 29.2 24 33Z"/> | |
| 35 | <path d="M33 86C29.5 86.1 26.5 83.8 24 79.7C28.6 80.8 31.8 83 33 86Z"/> | |
| 36 | <path d="M33 130C27.7 133.9 21.6 132.6 15.3 126.9C23 124.4 29.3 125.2 33 130Z"/> | |
| 37 | <path d="M3.8 176C1 173.2 0.6 168.7 2.2 163.3C4.8 168.2 5.4 172.6 3.8 176Z"/> | |
| 38 | <path d="M-3 178.3C-8.5 178.4 -13.3 174.6 -17.1 167.9C-9.9 169.9 -4.8 173.4 -3 178.3Z"/> | |
| 39 | <path d="M-6 184.7C-8.8 187.8 -13.3 188.5 -18.9 187.1C-14.1 184.2 -9.6 183.2 -6 184.7Z"/> | |
| 40 | <path d="M-3.4 191.4C-3 196.7 -6.3 201.5 -12.6 205.5C-11 198.5 -8 193.4 -3.4 191.4Z"/> | |
| 41 | <path d="M3.3 194C6.7 196.8 7.7 201.5 6.6 207.6C3.2 202.6 1.9 197.9 3.3 194Z"/> | |
| 42 | <path d="M9.7 191C14.7 190.3 19.4 193.2 23.5 198.8C16.8 197.8 11.9 195.2 9.7 191Z"/> | |
| 43 | <path d="M12 184.2C14.7 180.4 19.9 178.9 26.5 179.8C21.3 183.8 16.3 185.4 12 184.2Z"/> | |
| 44 | <path d="M4 184.5C5.3 183.4 7.2 183.5 9.4 184.5C7.2 185.4 5.3 185.5 4 184.5Z"/> | |
| 45 | <path d="M3.5 186C4.6 187.3 4.5 189.2 3.5 191.4C2.6 189.2 2.5 187.3 3.5 186Z"/> | |
| 46 | <path d="M2 185.5C0.7 186.6 -1.2 186.5 -3.4 185.5C-1.2 184.6 0.7 184.5 2 185.5Z"/> | |
| 47 | <path d="M2.5 184C1.4 182.7 1.5 180.8 2.5 178.6C3.4 180.8 3.5 182.7 2.5 184Z"/> | |
| 48 | <path d="M23 225C22.4 220.6 25 216.5 30 212.9C29 218.8 26.7 223.1 23 225Z"/> | |
| 49 | <path d="M21 252C17.4 253.1 13.5 251.6 9.7 247.9C14.9 247.8 18.8 249.2 21 252Z"/> | |
| 50 | <path d="M17 272C15.8 274.9 12.7 276.6 8.3 277C11 273.7 14 271.9 17 272Z"/> | |
| 51 | <path d="M17 315C14.7 311.1 15.4 306.2 18.7 300.8C20.2 306.7 19.7 311.8 17 315Z"/> | |
| 52 | <path d="M25.4 317.5C26.7 311.5 32.1 307.2 40.4 304.8C36.3 312.2 31.2 316.8 25.4 317.5Z"/> | |
| 53 | <path d="M29.8 325.1C33.7 322.4 38.8 322.8 44.6 325.8C38.6 327.7 33.4 327.6 29.8 325.1Z"/> | |
| 54 | <path d="M28 333.6C33.9 334.5 38.4 339.4 41.3 347.2C33.9 343.8 29.1 339.2 28 333.6Z"/> | |
| 55 | <path d="M20.8 338.7C23.9 342.5 23.9 348 21.2 354.4C18.7 348.2 18.4 342.7 20.8 338.7Z"/> | |
| 56 | <path d="M12.1 337.5C11.7 343.1 7.5 347.6 0.4 350.9C3 343.7 7 338.9 12.1 337.5Z"/> | |
| 57 | <path d="M6.6 330.7C2.7 334.3 -3.3 334.8 -10.4 332.4C-3.9 329.1 2.1 328.4 6.6 330.7Z"/> | |
| 58 | <path d="M7.1 321.9C2 321.9 -2.6 318.4 -6 312C0.7 314 5.4 317.3 7.1 321.9Z"/> | |
| 59 | <path d="M18.6 325.7C17.1 323.9 17.2 321.4 18.6 318.5C19.8 321.4 19.9 323.9 18.6 325.7Z"/> | |
| 60 | <path d="M19.3 327.6C21.1 326.1 23.6 326.2 26.5 327.6C23.6 328.8 21.1 328.9 19.3 327.6Z"/> | |
| 61 | <path d="M17.4 328.3C18.9 330.1 18.8 332.6 17.4 335.5C16.2 332.6 16.1 330.1 17.4 328.3Z"/> | |
| 62 | <path d="M16.7 326.4C14.9 327.9 12.4 327.8 9.5 326.4C12.4 325.2 14.9 325.1 16.7 326.4Z"/> | |
| 63 | <path d="M14 444C7.8 437.8 6.7 428.1 10.1 416.3C15.9 426.8 17.5 436.4 14 444Z"/> | |
| 64 | <path d="M33 462C29.3 462.5 25.7 460.3 22.6 456C27.7 456.8 31.4 458.8 33 462Z"/> | |
| 65 | <path d="M20 418C15.8 416.7 13.1 412.6 12 406.5C16.9 409.9 19.8 413.8 20 418Z"/> | |
| 66 | <path d="M6.7 469.4C9.9 466.7 14.7 466.5 20.2 468.7C14.9 471 10.2 471.4 6.7 469.4Z"/> | |
| 67 | <path d="M4.1 479.1C9.6 481 13 486.6 14.2 494.7C7.8 490 4.1 484.6 4.1 479.1Z"/> | |
| 68 | <path d="M-5.6 481.7C-4.8 486 -7.1 490.2 -11.9 494.1C-11.3 488.1 -9.2 483.7 -5.6 481.7Z"/> | |
| 69 | <path d="M-12.7 474.6C-17 478.2 -23.3 478.4 -30.6 475.5C-23.6 472.4 -17.3 471.9 -12.7 474.6Z"/> | |
| 70 | <path d="M-10.1 464.9C-14.5 463.4 -17.2 458.9 -18.2 452.5C-13 456.2 -10.1 460.5 -10.1 464.9Z"/> | |
| 71 | <path d="M-0.4 462.3C-1.4 457.1 1.5 452 7.2 447.3C6.5 454.5 3.9 459.8 -0.4 462.3Z"/> | |
| 72 | <path d="M-1.8 472.3C-0.6 470.8 1.5 470.5 4.1 471.3C1.9 472.7 -0.2 473.1 -1.8 472.3Z"/> | |
| 73 | <path d="M-3.3 473.2C-1.8 474.4 -1.5 476.5 -2.3 479.1C-3.7 476.9 -4.1 474.8 -3.3 473.2Z"/> | |
| 74 | <path d="M-4.2 471.7C-5.4 473.2 -7.5 473.5 -10.1 472.7C-7.9 471.3 -5.8 470.9 -4.2 471.7Z"/> | |
| 75 | <path d="M-2.7 470.8C-4.2 469.6 -4.5 467.5 -3.7 464.9C-2.3 467.1 -1.9 469.2 -2.7 470.8Z"/> | |
| 76 | <path d="M12 532C6.3 528.1 4.1 520.7 5.2 511.1C11.1 518.4 13.7 525.7 12 532Z"/> | |
| 77 | <path d="M33 594C27.9 596.4 21.8 594.9 15.4 590.3C23 589.1 29.2 590.2 33 594Z"/> | |
| 78 | <path d="M7 630C3.9 630.1 1 628 -1.2 624.3C3 625.3 5.9 627.2 7 630Z"/> | |
| 79 | <path d="M36 670C24.6 687.4 5.4 689.3 -18.5 677.7C1.6 662.2 20.5 658.7 36 670Z"/> | |
| 80 | <path d="M-6 778C-7.5 744.6 15.6 720.7 57.6 707.4C46.1 748.2 24.7 773.7 -6 778Z"/> | |
| 81 | <path d="M31.8 27.3C28.8 26.8 26.6 24.2 25.3 20.2C28.9 22.1 31.3 24.5 31.8 27.3Z"/> | |
| 82 | <path d="M39.1 23C37.1 19.5 37.7 14.9 40.7 10C42.1 15.5 41.6 20 39.1 23Z"/> | |
| 83 | <path d="M47.1 25.9C48.1 23 51.1 21.2 55.3 20.6C52.8 24 50 25.9 47.1 25.9Z"/> | |
| 84 | <path d="M50 33.9C53.7 32.5 57.9 33.9 62.1 37.6C56.7 37.9 52.4 36.8 50 33.9Z"/> | |
| 85 | <path d="M45.7 41.2C48.6 42.8 49.9 46.2 49.8 50.8C46.7 47.6 45.2 44.3 45.7 41.2Z"/> | |
| 86 | <path d="M37.4 42.7C38.1 46.3 36.1 50 32 53.3C32.6 48.2 34.4 44.4 37.4 42.7Z"/> | |
| 87 | <path d="M30.9 37.2C28.7 40 24.8 40.8 19.9 39.8C23.9 37.1 27.8 36.1 30.9 37.2Z"/> | |
| 88 | <path d="M39.5 31.9C37.5 31.9 36 30.5 34.9 28.1C37.4 28.8 39 30.1 39.5 31.9Z"/> | |
| 89 | <path d="M41.1 32.5C41.1 30.5 42.5 29 44.9 27.9C44.2 30.4 42.9 32 41.1 32.5Z"/> | |
| 90 | <path d="M40.5 34.1C42.5 34.1 44 35.5 45.1 37.9C42.6 37.2 41 35.9 40.5 34.1Z"/> | |
| 91 | <path d="M38.9 33.5C38.9 35.5 37.5 37 35.1 38.1C35.8 35.6 37.1 34 38.9 33.5Z"/> | |
| 92 | <path d="M49.7 106.4C42.2 107.1 35.2 102.5 29.3 93.7C39.4 95.7 46.7 100 49.7 106.4Z"/> | |
| 93 | <path d="M59.3 93.5C50.2 88.7 45.7 78.1 45.6 63.7C55.6 73.5 60.7 83.9 59.3 93.5Z"/> | |
| 94 | <path d="M74.4 88C70.7 81.3 72.2 72.8 78.1 63.7C80.3 74 79.2 82.6 74.4 88Z"/> | |
| 95 | <path d="M90 91.8C90.8 81.8 98.5 73.8 111.2 68.2C106.3 80.8 99.1 89.3 90 91.8Z"/> | |
| 96 | <path d="M101 103.4C106.1 97 115 94.9 126.4 96.8C117.2 103.3 108.4 105.9 101 103.4Z"/> | |
| 97 | <path d="M103.8 119.3C112.7 116.3 122.4 119.9 132 128.8C119.3 129.2 109.3 126.2 103.8 119.3Z"/> | |
| 98 | <path d="M97.4 134C106.1 136.3 112 144.3 114.9 156.4C104.5 150.1 98.2 142.4 97.4 134Z"/> | |
| 99 | <path d="M84 142.8C89.8 149.2 90.3 158.8 86.3 170.2C81.2 159.6 80.2 150 84 142.8Z"/> | |
| 100 | <path d="M68 142.8C69.4 152.3 64 161.6 53.3 169.7C55.1 156.7 60 147.2 68 142.8Z"/> | |
| 101 | <path d="M54.6 134C51.2 141.2 43.2 145.2 32.1 145.9C39.3 137.7 47 133.4 54.6 134Z"/> | |
| 102 | <path d="M73.8 113.4C68.5 114.8 63.3 112.2 58.6 106.3C65.9 106.6 71.3 108.9 73.8 113.4Z"/> | |
| 103 | <path d="M78.6 113.8C77.2 108.5 79.8 103.3 85.7 98.6C85.4 105.9 83.1 111.3 78.6 113.8Z"/> | |
| 104 | <path d="M78.2 118.6C83.5 117.2 88.7 119.8 93.4 125.7C86.1 125.4 80.7 123.1 78.2 118.6Z"/> | |
| 105 | <path d="M73.4 118.2C74.8 123.5 72.2 128.7 66.3 133.4C66.6 126.1 68.9 120.7 73.4 118.2Z"/> | |
| 106 | <path d="M116 188C130.5 169.2 154.7 165.8 184.9 175.8C159.6 193.3 135.6 198.3 116 188Z"/> | |
| 107 | <path d="M84 222C111.2 218.8 132.1 236.4 145.5 270C111.4 263 89.3 246.9 84 222Z"/> | |
| 108 | <path d="M38 262C71.1 266.3 90.7 293.2 96.5 336.9C58.3 318.5 36.9 293 38 262Z"/> | |
| 109 | <path d="M37 250C47.2 245.2 59.5 248.1 72.2 257.5C57 259.8 44.6 257.6 37 250Z"/> | |
| 110 | <path d="M55.5 389.8C54.2 394.4 49.9 397.4 43.4 398.9C46.8 393.4 51 390.1 55.5 389.8Z"/> | |
| 111 | <path d="M64 397.8C66.9 403.6 65.5 410.8 60.3 418.3C58.7 409.6 59.7 402.3 64 397.8Z"/> | |
| 112 | <path d="M75.7 398C80.3 399.6 83.1 404.3 84.1 411.1C78.7 407.1 75.6 402.6 75.7 398Z"/> | |
| 113 | <path d="M84.3 390.1C90.1 387.7 96.9 389.6 103.8 395.1C95.2 396.1 88.4 394.5 84.3 390.1Z"/> | |
| 114 | <path d="M85.3 378.5C87.5 373.7 92.7 371.1 99.9 370.6C95.3 376 90.3 378.9 85.3 378.5Z"/> | |
| 115 | <path d="M78.2 369.2C76.3 363.6 78.6 357.5 84.2 351.4C84.5 359.5 82.6 365.8 78.2 369.2Z"/> | |
| 116 | <path d="M66.6 367.4C61.7 364.7 59.3 358.8 59.3 350.9C64.7 356.4 67.4 362.1 66.6 367.4Z"/> | |
| 117 | <path d="M56.9 373.8C51.6 375.2 46.1 372.6 40.9 367C48.3 367.3 54 369.5 56.9 373.8Z"/> | |
| 118 | <path d="M68.4 381.9C65.5 380.8 63.9 377.9 63.6 373.6C66.9 376.1 68.7 379 68.4 381.9Z"/> | |
| 119 | <path d="M71.1 381.4C72.2 378.5 75.1 376.9 79.4 376.6C76.9 379.9 74 381.7 71.1 381.4Z"/> | |
| 120 | <path d="M71.6 384.1C74.5 385.2 76.1 388.1 76.4 392.4C73.1 389.9 71.3 387 71.6 384.1Z"/> | |
| 121 | <path d="M68.9 384.6C67.8 387.5 64.9 389.1 60.6 389.4C63.1 386.1 66 384.3 68.9 384.6Z"/> | |
| 122 | <path d="M38 176C35.3 182.4 28.6 186 18.9 187C24.9 179.7 31.5 175.7 38 176Z"/> | |
| 123 | <path d="M40 532C36.4 523.3 39.4 513.2 47.8 503C49.1 515.8 46.6 526 40 532Z"/> | |
| 124 | <path d="M66 534C61.7 526.3 63.3 516.7 69.9 506.3C72.6 518 71.5 527.7 66 534Z"/> | |
| 125 | <path d="M40.1 578.9C34.4 582.2 27.2 581.1 19.4 576.4C28.1 574.3 35.3 574.9 40.1 578.9Z"/> | |
| 126 | <path d="M43.4 565.2C34.6 564 27.7 556.9 23.1 545.2C34.3 550.1 41.6 556.9 43.4 565.2Z"/> | |
| 127 | <path d="M54.5 556.3C49.7 551.6 48.9 544.2 51.5 535.2C55.9 543.2 57.1 550.6 54.5 556.3Z"/> | |
| 128 | <path d="M68.6 556C67.5 547.5 72.4 539.2 82.2 532.1C80.4 543.7 75.9 552.2 68.6 556Z"/> | |
| 129 | <path d="M80 564.4C83.6 558.2 91 555.3 100.9 555.5C93.9 562.2 86.7 565.5 80 564.4Z"/> | |
| 130 | <path d="M84 578C91.4 574.8 100.1 577.3 108.9 584.4C98 585.6 89.2 583.6 84 578Z"/> | |
| 131 | <path d="M78.9 591.1C86.3 593.1 91.4 600 94 610.5C85 605.1 79.5 598.4 78.9 591.1Z"/> | |
| 132 | <path d="M66.8 598.5C71.3 604.4 71.2 612.7 67 622.3C63.3 612.8 63 604.5 66.8 598.5Z"/> | |
| 133 | <path d="M52.7 596.9C52.7 605.3 46.9 612.5 36.7 618.1C39.9 607.2 45.3 599.7 52.7 596.9Z"/> | |
| 134 | <path d="M59.6 575.9C56.3 578.6 51.7 578.5 46.4 575.9C51.7 573.7 56.3 573.5 59.6 575.9Z"/> | |
| 135 | <path d="M63.1 574.6C60.4 571.3 60.5 566.7 63.1 561.4C65.3 566.7 65.5 571.3 63.1 574.6Z"/> | |
| 136 | <path d="M64.4 578.1C67.7 575.4 72.3 575.5 77.6 578.1C72.3 580.3 67.7 580.5 64.4 578.1Z"/> | |
| 137 | <path d="M60.9 579.4C63.6 582.7 63.5 587.3 60.9 592.6C58.7 587.3 58.5 582.7 60.9 579.4Z"/> | |
| 138 | <path d="M107.1 650.7C104 653.7 99.3 654.1 93.6 652.4C98.7 649.6 103.4 648.9 107.1 650.7Z"/> | |
| 139 | <path d="M110.2 661.2C109.9 667 105.5 671.9 98.2 675.4C100.8 667.9 104.9 662.8 110.2 661.2Z"/> | |
| 140 | <path d="M119.6 666.8C122.4 670.2 122.5 675 120.2 680.7C117.9 675.3 117.5 670.4 119.6 666.8Z"/> | |
| 141 | <path d="M130.3 664.5C135.9 665.2 140.3 669.7 143.1 677.1C136.1 674 131.5 669.7 130.3 664.5Z"/> | |
| 142 | <path d="M136.6 655.5C140.4 652.8 145.6 653.1 151.4 655.9C145.4 658 140.2 658 136.6 655.5Z"/> | |
| 143 | <path d="M135.1 644.7C136.1 639.5 140.7 635.7 147.7 633.5C144.4 639.9 140 643.9 135.1 644.7Z"/> | |
| 144 | <path d="M126.6 637.7C124 633.4 124.7 627.8 128.2 621.7C130 628.4 129.6 634 126.6 637.7Z"/> | |
| 145 | <path d="M115.7 638.4C111 637.1 107.8 632.6 106.3 626C111.9 629.5 115.3 633.8 115.7 638.4Z"/> | |
| 146 | <path d="M121.2 650.4C119.4 648.1 119.5 645 121.2 641.4C122.7 645 122.8 648.1 121.2 650.4Z"/> | |
| 147 | <path d="M123.6 651.2C125.9 649.4 129 649.5 132.6 651.2C129 652.7 125.9 652.8 123.6 651.2Z"/> | |
| 148 | <path d="M122.8 653.6C124.6 655.9 124.5 659 122.8 662.6C121.3 659 121.2 655.9 122.8 653.6Z"/> | |
| 149 | <path d="M120.4 652.8C118.1 654.6 115 654.5 111.4 652.8C115 651.3 118.1 651.2 120.4 652.8Z"/> | |
| 150 | <path d="M86 664C74.9 663.6 66.3 655.4 60.7 641.2C74.6 646.1 83.7 653.8 86 664Z"/> | |
| 151 | <path d="M76 700C77.6 714 68.6 724.7 51.4 731.5C55 714 63.2 702.7 76 700Z"/> | |
| 152 | </g> | |
| 153 | </g> | |
| 154 | </defs> | |
| 155 | <use href="#ivy" clip-path="url(#strip)"/> | |
| 156 | <use href="#ivy" clip-path="url(#page)" opacity=".12"/> | |
| 157 | <rect x="32.6" width="1.8" height="792" fill="url(#fade)"/> | |
| 158 | </svg> |
crates/canvas/assets/backgrounds/large-push-pins.svg created+24| ... | ... | @@ -0,0 +1,24 @@ |
| 1 | <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 612 792" preserveAspectRatio="none"> | |
| 2 | <defs> | |
| 3 | <radialGradient id="wash-reach" cx="0" cy="0" r="1" gradientTransform="translate(0 0) scale(520 300) translate(0 0)" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="white"/><stop offset="1" stop-color="white" stop-opacity="0"/></radialGradient><mask id="wash"><rect width="612" height="792" fill="url(#wash-reach)"/></mask> | |
| 4 | <linearGradient id="down" x2="0" y2="1"><stop offset="0.22" stop-color="white"/><stop offset="0.4" stop-color="white" stop-opacity="0"/></linearGradient><mask id="rule-down"><rect width="612" height="792" fill="url(#down)"/></mask> | |
| 5 | <linearGradient id="across" x2="1" y2="0"><stop offset="0.45" stop-color="white"/><stop offset="0.9" stop-color="white" stop-opacity="0"/></linearGradient><mask id="rule-across"><rect width="612" height="792" fill="url(#across)"/></mask> | |
| 6 | <linearGradient id="navy" x2="1"><stop offset="0" stop-color="#1f2d4a"/><stop offset=".45" stop-color="#5d6f92"/><stop offset="1" stop-color="#1f2d4a"/></linearGradient> | |
| 7 | <g id="pin"> | |
| 8 | <path d="M-11-30C-11-36 11-36 11-30L8-12C16-8 18 0 15 4H-15C-18 0-16-8-8-12Z"/> | |
| 9 | <path d="M-1.2 4L0 34L1.2 4Z" fill="#9aa9b8"/> | |
| 10 | </g> | |
| 11 | <filter id="shadow" x="-40%" y="-40%" width="180%" height="180%"><feGaussianBlur stdDeviation="3"/></filter> | |
| 12 | </defs> | |
| 13 | <rect width="612" height="300" fill="#1b7dbe" mask="url(#wash)" opacity=".85"/> | |
| 14 | <rect x="12" y="12" width="600" height="280" fill="#ffffff" mask="url(#wash)" opacity=".7"/> | |
| 15 | <g fill="#1b7dbe"> | |
| 16 | <rect width="8" height="792" mask="url(#rule-down)"/><rect width="612" height="8" mask="url(#rule-across)"/> | |
| 17 | <rect x="11" y="11" width="2" height="781" mask="url(#rule-down)"/><rect x="11" y="11" width="601" height="2" mask="url(#rule-across)"/> | |
| 18 | </g> | |
| 19 | <g fill="#4b8ec4" opacity=".6" filter="url(#shadow)"> | |
| 20 | <ellipse cx="230" cy="92" rx="30" ry="11"/><ellipse cx="320" cy="170" rx="30" ry="12"/> | |
| 21 | </g> | |
| 22 | <use href="#pin" fill="url(#navy)" transform="translate(264 58) rotate(35) scale(1.3)"/> | |
| 23 | <use href="#pin" fill="url(#navy)" transform="translate(350 90) rotate(15) scale(1.3)"/> | |
| 24 | </svg> |
crates/canvas/assets/backgrounds/light-bulb-corner.svg created+15| ... | ... | @@ -0,0 +1,15 @@ |
| 1 | <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 262 277" preserveAspectRatio="none"> | |
| 2 | <defs> | |
| 3 | <radialGradient id="reach" cx="30" cy="60" r="1" gradientTransform="translate(30 60) scale(200 230) translate(-30 -60)" gradientUnits="userSpaceOnUse"><stop offset="0.35" stop-color="white"/><stop offset="1" stop-color="white" stop-opacity="0"/></radialGradient><mask id="art"><rect width="262" height="277" fill="url(#reach)"/></mask> | |
| 4 | <radialGradient id="glass" cx=".4" cy=".35" r=".7"><stop offset="0" stop-color="#fbfaff"/><stop offset=".6" stop-color="#e4e0f6"/><stop offset="1" stop-color="#bdb6e3"/></radialGradient> | |
| 5 | <radialGradient id="warm" cx=".5" cy=".5" r=".5"><stop offset="0" stop-color="#fde2a8"/><stop offset="1" stop-color="#fde2a8" stop-opacity="0"/></radialGradient> | |
| 6 | </defs> | |
| 7 | <g mask="url(#art)"> | |
| 8 | <ellipse cx="120" cy="215" rx="130" ry="55" fill="url(#warm)"/> | |
| 9 | <ellipse cx="130" cy="185" rx="60" ry="30" fill="#f8c9b8" opacity=".5"/> | |
| 10 | <path d="M-30 250L-10 190C-40 160-50 110-20 60C10 10 80-10 130 20C180 50 200 120 170 170C150 200 110 215 70 215L40 280Z" fill="url(#glass)"/> | |
| 11 | <ellipse cx="60" cy="30" rx="30" ry="20" fill="#ffffff" opacity=".6"/> | |
| 12 | <g fill="none" stroke="#5d5774" stroke-width="1.6"><path d="M0 230L40 80L70 60L90 110L20 220"/><path d="M40 80L60 120"/></g> | |
| 13 | <circle cx="70" cy="85" r="6" fill="#ffffff" opacity=".8"/> | |
| 14 | </g> | |
| 15 | </svg> |
crates/canvas/assets/backgrounds/lilies.svg created+15| ... | ... | @@ -0,0 +1,15 @@ |
| 1 | <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 577 757" preserveAspectRatio="none"> | |
| 2 | <defs> | |
| 3 | <linearGradient id="across" x2="1" y2="0"><stop offset="0.45" stop-color="white"/><stop offset="0.72" stop-color="white" stop-opacity="0"/></linearGradient><mask id="fade-across"><rect width="577" height="757" fill="url(#across)"/></mask> | |
| 4 | <linearGradient id="down" x2="0" y2="1"><stop offset="0.7" stop-color="white"/><stop offset="0.98" stop-color="white" stop-opacity="0"/></linearGradient><mask id="fade-down"><rect width="577" height="757" fill="url(#down)"/></mask> | |
| 5 | <radialGradient id="petal" cy=".8" r=".9"><stop offset="0" stop-color="#e9ecf5"/><stop offset=".7" stop-color="#fbfbfd"/><stop offset="1" stop-color="#f0f1f7"/></radialGradient> | |
| 6 | <filter id="soft" x="-20%" y="-20%" width="140%" height="140%"><feGaussianBlur stdDeviation="3"/></filter> | |
| 7 | </defs> | |
| 8 | <g mask="url(#fade-down)"><g mask="url(#fade-across)"> | |
| 9 | <rect width="577" height="757" fill="#f4f6f2"/> | |
| 10 | <g> | |
| 11 | <path d="M90 -10C95 11.7 116.7 75 120 120C123.3 165 108.3 210 110 260C111.7 310 130 370 130 420C130 470 116.7 503.8 110 560C103.3 616.2 93.3 724.2 90 757" fill="none" stroke="#dfe7d2" stroke-width="5"/> | |
| 12 | <g transform="translate(90 110) rotate(10) scale(3.6)"><ellipse cx="0" cy="-22" rx="11" ry="24" transform="rotate(-70)" fill="url(#petal)"/><ellipse cx="0" cy="-22" rx="11" ry="24" transform="rotate(0)" fill="url(#petal)"/><ellipse cx="0" cy="-22" rx="11" ry="24" transform="rotate(70)" fill="url(#petal)"/><ellipse cx="0" cy="-22" rx="11" ry="24" transform="rotate(145)" fill="url(#petal)"/><ellipse cx="0" cy="-22" rx="11" ry="24" transform="rotate(215)" fill="url(#petal)"/><path d="M-9 4C-12 16-4 26 0 28C4 26 12 16 9 4C4 8-4 8-9 4Z" fill="#cdc6e8"/><circle r="4" fill="#f4f1dc"/></g><g transform="translate(240 60) rotate(-30) scale(2.6)"><ellipse cx="0" cy="-22" rx="11" ry="24" transform="rotate(-70)" fill="url(#petal)"/><ellipse cx="0" cy="-22" rx="11" ry="24" transform="rotate(0)" fill="url(#petal)"/><ellipse cx="0" cy="-22" rx="11" ry="24" transform="rotate(70)" fill="url(#petal)"/><ellipse cx="0" cy="-22" rx="11" ry="24" transform="rotate(145)" fill="url(#petal)"/><ellipse cx="0" cy="-22" rx="11" ry="24" transform="rotate(215)" fill="url(#petal)"/><path d="M-9 4C-12 16-4 26 0 28C4 26 12 16 9 4C4 8-4 8-9 4Z" fill="#cdc6e8"/><circle r="4" fill="#f4f1dc"/></g><g transform="translate(40 330) rotate(25) scale(3.0)"><ellipse cx="0" cy="-22" rx="11" ry="24" transform="rotate(-70)" fill="url(#petal)"/><ellipse cx="0" cy="-22" rx="11" ry="24" transform="rotate(0)" fill="url(#petal)"/><ellipse cx="0" cy="-22" rx="11" ry="24" transform="rotate(70)" fill="url(#petal)"/><ellipse cx="0" cy="-22" rx="11" ry="24" transform="rotate(145)" fill="url(#petal)"/><ellipse cx="0" cy="-22" rx="11" ry="24" transform="rotate(215)" fill="url(#petal)"/><path d="M-9 4C-12 16-4 26 0 28C4 26 12 16 9 4C4 8-4 8-9 4Z" fill="#cdc6e8"/><circle r="4" fill="#f4f1dc"/></g><g transform="translate(220 360) rotate(-5) scale(2.4)"><ellipse cx="0" cy="-22" rx="11" ry="24" transform="rotate(-70)" fill="url(#petal)"/><ellipse cx="0" cy="-22" rx="11" ry="24" transform="rotate(0)" fill="url(#petal)"/><ellipse cx="0" cy="-22" rx="11" ry="24" transform="rotate(70)" fill="url(#petal)"/><ellipse cx="0" cy="-22" rx="11" ry="24" transform="rotate(145)" fill="url(#petal)"/><ellipse cx="0" cy="-22" rx="11" ry="24" transform="rotate(215)" fill="url(#petal)"/><path d="M-9 4C-12 16-4 26 0 28C4 26 12 16 9 4C4 8-4 8-9 4Z" fill="#cdc6e8"/><circle r="4" fill="#f4f1dc"/></g><g transform="translate(110 560) rotate(40) scale(2.8)"><ellipse cx="0" cy="-22" rx="11" ry="24" transform="rotate(-70)" fill="url(#petal)"/><ellipse cx="0" cy="-22" rx="11" ry="24" transform="rotate(0)" fill="url(#petal)"/><ellipse cx="0" cy="-22" rx="11" ry="24" transform="rotate(70)" fill="url(#petal)"/><ellipse cx="0" cy="-22" rx="11" ry="24" transform="rotate(145)" fill="url(#petal)"/><ellipse cx="0" cy="-22" rx="11" ry="24" transform="rotate(215)" fill="url(#petal)"/><path d="M-9 4C-12 16-4 26 0 28C4 26 12 16 9 4C4 8-4 8-9 4Z" fill="#cdc6e8"/><circle r="4" fill="#f4f1dc"/></g><g transform="translate(260 620) rotate(10) scale(2.0)"><ellipse cx="0" cy="-22" rx="11" ry="24" transform="rotate(-70)" fill="url(#petal)"/><ellipse cx="0" cy="-22" rx="11" ry="24" transform="rotate(0)" fill="url(#petal)"/><ellipse cx="0" cy="-22" rx="11" ry="24" transform="rotate(70)" fill="url(#petal)"/><ellipse cx="0" cy="-22" rx="11" ry="24" transform="rotate(145)" fill="url(#petal)"/><ellipse cx="0" cy="-22" rx="11" ry="24" transform="rotate(215)" fill="url(#petal)"/><path d="M-9 4C-12 16-4 26 0 28C4 26 12 16 9 4C4 8-4 8-9 4Z" fill="#cdc6e8"/><circle r="4" fill="#f4f1dc"/></g> | |
| 13 | </g> | |
| 14 | </g></g> | |
| 15 | </svg> |
crates/canvas/assets/backgrounds/math-science-class-notes.svg created+17| ... | ... | @@ -0,0 +1,17 @@ |
| 1 | <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 612 792" preserveAspectRatio="none"> | |
| 2 | <defs> | |
| 3 | <radialGradient id="reach" cx="180" cy="0" r="1" gradientTransform="translate(180 0) scale(440 220) translate(-180 0)" gradientUnits="userSpaceOnUse"><stop offset="0.4" stop-color="white"/><stop offset="1" stop-color="white" stop-opacity="0"/></radialGradient><mask id="art"><rect width="612" height="792" fill="url(#reach)"/></mask> | |
| 4 | <linearGradient id="down" x2="0" y2="1"><stop offset="0.55" stop-color="white"/><stop offset="0.75" stop-color="white" stop-opacity="0"/></linearGradient><mask id="rule-down"><rect width="612" height="792" fill="url(#down)"/></mask> | |
| 5 | </defs> | |
| 6 | <g mask="url(#art)"> | |
| 7 | <g transform="rotate(-8 200 100)"> | |
| 8 | <rect x="-40" y="-10" width="540" height="250" fill="#e3e8f3"/> | |
| 9 | <g stroke="#a4b2cc" stroke-width=".9"><path d="M-40 -10V240"/><path d="M-24 -10V240"/><path d="M-8 -10V240"/><path d="M8 -10V240"/><path d="M24 -10V240"/><path d="M40 -10V240"/><path d="M56 -10V240"/><path d="M72 -10V240"/><path d="M88 -10V240"/><path d="M104 -10V240"/><path d="M120 -10V240"/><path d="M136 -10V240"/><path d="M152 -10V240"/><path d="M168 -10V240"/><path d="M184 -10V240"/><path d="M200 -10V240"/><path d="M216 -10V240"/><path d="M232 -10V240"/><path d="M248 -10V240"/><path d="M264 -10V240"/><path d="M280 -10V240"/><path d="M296 -10V240"/><path d="M312 -10V240"/><path d="M328 -10V240"/><path d="M344 -10V240"/><path d="M360 -10V240"/><path d="M376 -10V240"/><path d="M392 -10V240"/><path d="M408 -10V240"/><path d="M424 -10V240"/><path d="M440 -10V240"/><path d="M456 -10V240"/><path d="M472 -10V240"/><path d="M488 -10V240"/><path d="M-40 -10H500"/><path d="M-40 6H500"/><path d="M-40 22H500"/><path d="M-40 38H500"/><path d="M-40 54H500"/><path d="M-40 70H500"/><path d="M-40 86H500"/><path d="M-40 102H500"/><path d="M-40 118H500"/><path d="M-40 134H500"/><path d="M-40 150H500"/><path d="M-40 166H500"/><path d="M-40 182H500"/><path d="M-40 198H500"/><path d="M-40 214H500"/><path d="M-40 230H500"/></g> | |
| 10 | <g stroke="#f6f8fb" stroke-width="18" stroke-linecap="square"> | |
| 11 | <path d="M70 50L150 150M150 50L70 150"/><path d="M230 60H330M230 110H330"/> | |
| 12 | </g> | |
| 13 | </g> | |
| 14 | <path d="M385 70L425 110M425 70L385 110" stroke="#d9dfeb" stroke-width="6"/> | |
| 15 | </g> | |
| 16 | <rect x="0" y="0" width="3" height="792" fill="#576c97" mask="url(#rule-down)"/> | |
| 17 | </svg> |
crates/canvas/assets/backgrounds/math.svg created+15| ... | ... | @@ -0,0 +1,15 @@ |
| 1 | <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 609 675" preserveAspectRatio="none"> | |
| 2 | <defs> | |
| 3 | <radialGradient id="reach" cx="260" cy="0" r="1" gradientTransform="translate(260 0) scale(520 240) translate(-260 0)" gradientUnits="userSpaceOnUse"><stop offset="0.45" stop-color="white"/><stop offset="1" stop-color="white" stop-opacity="0"/></radialGradient><mask id="art"><rect width="609" height="675" fill="url(#reach)"/></mask> | |
| 4 | </defs> | |
| 5 | <g mask="url(#art)"> | |
| 6 | <g transform="rotate(-8 260 100)"> | |
| 7 | <rect x="-40" y="-10" width="680" height="270" fill="#e3e8f3"/> | |
| 8 | <g stroke="#a4b2cc" stroke-width=".9"><path d="M-40 -10V260"/><path d="M-22 -10V260"/><path d="M-4 -10V260"/><path d="M14 -10V260"/><path d="M32 -10V260"/><path d="M50 -10V260"/><path d="M68 -10V260"/><path d="M86 -10V260"/><path d="M104 -10V260"/><path d="M122 -10V260"/><path d="M140 -10V260"/><path d="M158 -10V260"/><path d="M176 -10V260"/><path d="M194 -10V260"/><path d="M212 -10V260"/><path d="M230 -10V260"/><path d="M248 -10V260"/><path d="M266 -10V260"/><path d="M284 -10V260"/><path d="M302 -10V260"/><path d="M320 -10V260"/><path d="M338 -10V260"/><path d="M356 -10V260"/><path d="M374 -10V260"/><path d="M392 -10V260"/><path d="M410 -10V260"/><path d="M428 -10V260"/><path d="M446 -10V260"/><path d="M464 -10V260"/><path d="M482 -10V260"/><path d="M500 -10V260"/><path d="M518 -10V260"/><path d="M536 -10V260"/><path d="M554 -10V260"/><path d="M572 -10V260"/><path d="M590 -10V260"/><path d="M608 -10V260"/><path d="M626 -10V260"/><path d="M-40 -10H640"/><path d="M-40 8H640"/><path d="M-40 26H640"/><path d="M-40 44H640"/><path d="M-40 62H640"/><path d="M-40 80H640"/><path d="M-40 98H640"/><path d="M-40 116H640"/><path d="M-40 134H640"/><path d="M-40 152H640"/><path d="M-40 170H640"/><path d="M-40 188H640"/><path d="M-40 206H640"/><path d="M-40 224H640"/><path d="M-40 242H640"/></g> | |
| 9 | <g stroke="#f6f8fb" stroke-width="20" stroke-linecap="square"> | |
| 10 | <path d="M90 50L180 160M180 50L90 160"/><path d="M280 60H400M280 115H400"/> | |
| 11 | </g> | |
| 12 | </g> | |
| 13 | <path d="M470 70L515 115M515 70L470 115" stroke="#d9dfeb" stroke-width="7"/> | |
| 14 | </g> | |
| 15 | </svg> |
crates/canvas/assets/backgrounds/networking.svg created+33| ... | ... | @@ -0,0 +1,33 @@ |
| 1 | <svg xmlns="http://www.w3.org/2000/svg" width="45.07pt" height="72.71pt" viewBox="0 0 189 305" preserveAspectRatio="none"> | |
| 2 | <circle cx="13.8" cy="13.8" r="14" fill="#c6d3ad"/> | |
| 3 | <circle cx="52.6" cy="13.8" r="14" fill="#c6d3ad"/> | |
| 4 | <circle cx="91.4" cy="13.8" r="14" fill="#c6d3ad"/> | |
| 5 | <circle cx="13.8" cy="52.6" r="14" fill="#c6d3ad"/> | |
| 6 | <circle cx="52.6" cy="52.6" r="14" fill="#c6d3ad"/> | |
| 7 | <circle cx="91.4" cy="52.6" r="14" fill="#c6d3ad"/> | |
| 8 | <circle cx="130.2" cy="52.6" r="14" fill="#9cb6ef"/> | |
| 9 | <circle cx="13.8" cy="91.4" r="14" fill="#c6d3ad"/> | |
| 10 | <circle cx="52.6" cy="91.4" r="14" fill="#c6d3ad"/> | |
| 11 | <circle cx="91.4" cy="91.4" r="14" fill="#9cb6ef"/> | |
| 12 | <circle cx="130.2" cy="91.4" r="14" fill="#9cb6ef"/> | |
| 13 | <circle cx="169.0" cy="91.4" r="14" fill="#e7efde"/> | |
| 14 | <circle cx="13.8" cy="130.2" r="14" fill="#c6d3ad"/> | |
| 15 | <circle cx="52.6" cy="130.2" r="14" fill="#9cb6ef"/> | |
| 16 | <circle cx="91.4" cy="130.2" r="14" fill="#9cb6ef"/> | |
| 17 | <circle cx="130.2" cy="130.2" r="14" fill="#e7efde"/> | |
| 18 | <circle cx="13.8" cy="169.0" r="14" fill="#9cb6ef"/> | |
| 19 | <circle cx="52.6" cy="169.0" r="14" fill="#9cb6ef"/> | |
| 20 | <circle cx="91.4" cy="169.0" r="14" fill="#e7efde"/> | |
| 21 | <circle cx="130.2" cy="169.0" r="14" fill="#e7efde"/> | |
| 22 | <circle cx="169.0" cy="169.0" r="14" fill="#deefff"/> | |
| 23 | <circle cx="13.8" cy="207.8" r="14" fill="#9cb6ef"/> | |
| 24 | <circle cx="52.6" cy="207.8" r="14" fill="#e7efde"/> | |
| 25 | <circle cx="91.4" cy="207.8" r="14" fill="#e7efde"/> | |
| 26 | <circle cx="130.2" cy="207.8" r="14" fill="#deefff"/> | |
| 27 | <circle cx="13.8" cy="246.6" r="14" fill="#e7efde"/> | |
| 28 | <circle cx="52.6" cy="246.6" r="14" fill="#e7efde"/> | |
| 29 | <circle cx="91.4" cy="246.6" r="14" fill="#deefff"/> | |
| 30 | <circle cx="130.2" cy="246.6" r="14" fill="#deefff"/> | |
| 31 | <circle cx="52.6" cy="285.4" r="14" fill="#deefff"/> | |
| 32 | <circle cx="130.2" cy="285.4" r="14" fill="#deefff"/> | |
| 33 | </svg> |
crates/canvas/assets/backgrounds/notebook.svg created+19| ... | ... | @@ -0,0 +1,19 @@ |
| 1 | <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 612 792" preserveAspectRatio="none"> | |
| 2 | <defs> | |
| 3 | <radialGradient id="reach" cx="100" cy="20" r="1" gradientTransform="translate(100 20) scale(360 180) translate(-100 -20)" gradientUnits="userSpaceOnUse"><stop offset="0.35" stop-color="white"/><stop offset="1" stop-color="white" stop-opacity="0"/></radialGradient><mask id="art"><rect width="612" height="792" fill="url(#reach)"/></mask> | |
| 4 | <linearGradient id="down" x2="0" y2="1"><stop offset="0.55" stop-color="white"/><stop offset="0.75" stop-color="white" stop-opacity="0"/></linearGradient><mask id="rule-down"><rect width="612" height="792" fill="url(#down)"/></mask> | |
| 5 | <linearGradient id="across" x2="1" y2="0"><stop offset="0.6" stop-color="white"/><stop offset="0.96" stop-color="white" stop-opacity="0"/></linearGradient><mask id="rule-across"><rect width="612" height="792" fill="url(#across)"/></mask> | |
| 6 | <linearGradient id="pad" x2="0" y2="1"><stop offset="0" stop-color="#cfdcb6"/><stop offset="1" stop-color="#f1f4ea"/></linearGradient> | |
| 7 | </defs> | |
| 8 | <g mask="url(#art)"> | |
| 9 | <rect width="612" height="120" fill="#f4f6ee"/> | |
| 10 | <g transform="rotate(-12 200 100)"> | |
| 11 | <rect x="20" y="50" width="420" height="160" fill="url(#pad)" stroke="#aebd96"/> | |
| 12 | <g stroke="#bfc4b4" stroke-width="1.3"><path d="M60 130H440"/><path d="M60 150H440"/><path d="M60 170H440"/><path d="M60 190H440"/></g> | |
| 13 | <g fill="none" stroke="#b5bda8" stroke-width="3"><path d="M40 58c-10-26 10-34 14-12" /><path d="M70 58c-10-26 10-34 14-12" /><path d="M100 58c-10-26 10-34 14-12" /><path d="M130 58c-10-26 10-34 14-12" /><path d="M160 58c-10-26 10-34 14-12" /><path d="M190 58c-10-26 10-34 14-12" /><path d="M220 58c-10-26 10-34 14-12" /><path d="M250 58c-10-26 10-34 14-12" /><path d="M280 58c-10-26 10-34 14-12" /><path d="M310 58c-10-26 10-34 14-12" /><path d="M340 58c-10-26 10-34 14-12" /><path d="M370 58c-10-26 10-34 14-12" /></g> | |
| 14 | <g fill="#dfe3d8"><circle cx="46" cy="64" r="4"/><circle cx="76" cy="64" r="4"/><circle cx="106" cy="64" r="4"/><circle cx="136" cy="64" r="4"/><circle cx="166" cy="64" r="4"/><circle cx="196" cy="64" r="4"/><circle cx="226" cy="64" r="4"/><circle cx="256" cy="64" r="4"/><circle cx="286" cy="64" r="4"/><circle cx="316" cy="64" r="4"/><circle cx="346" cy="64" r="4"/><circle cx="376" cy="64" r="4"/></g> | |
| 15 | </g> | |
| 16 | </g> | |
| 17 | <rect x="0" y="0" width="3" height="792" fill="#96a088" mask="url(#rule-down)"/> | |
| 18 | <rect x="0" y="0" width="612" height="3" fill="#96a088" mask="url(#rule-across)"/> | |
| 19 | </svg> |
crates/canvas/assets/backgrounds/orange-margin.svg created+16| ... | ... | @@ -0,0 +1,16 @@ |
| 1 | <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 70 626" preserveAspectRatio="none"> | |
| 2 | <defs> | |
| 3 | <linearGradient id="sun" x2="0" y2="1"> | |
| 4 | <stop offset="0" stop-color="#fee09f"/><stop offset=".23" stop-color="#fecc69"/><stop offset=".5" stop-color="#feb630"/><stop offset=".95" stop-color="#ffc860"/> | |
| 5 | </linearGradient> | |
| 6 | <linearGradient id="ink" x2="0" y1="0" y2="626" gradientUnits="userSpaceOnUse"> | |
| 7 | <stop offset=".68" stop-color="#1a1b1f"/><stop offset=".8" stop-color="#9e9e9e"/><stop offset=".88" stop-color="#d4d4d4"/><stop offset=".98" stop-color="#d4d4d4" stop-opacity="0"/> | |
| 8 | </linearGradient> | |
| 9 | <linearGradient id="sky" x2="0" y1="0" y2="626" gradientUnits="userSpaceOnUse"> | |
| 10 | <stop offset=".3" stop-color="#c6eaf6"/><stop offset=".64" stop-color="#d9f0f8"/><stop offset=".85" stop-color="#f3fafd"/><stop offset=".97" stop-color="#f3fafd" stop-opacity="0"/> | |
| 11 | </linearGradient> | |
| 12 | </defs> | |
| 13 | <path d="M41 118L64 520V626H18L21 430Z" fill="url(#sky)"/> | |
| 14 | <path d="M44 0H0V440Z" fill="url(#sun)"/> | |
| 15 | <path d="M32 120L38 118L20 440L19 626H0V440Z" fill="url(#ink)"/> | |
| 16 | </svg> |
crates/canvas/assets/backgrounds/paper.svg created+15| ... | ... | @@ -0,0 +1,15 @@ |
| 1 | <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 612 792" preserveAspectRatio="none"> | |
| 2 | <defs> | |
| 3 | <linearGradient id="fx0" x2="612" gradientUnits="userSpaceOnUse"><stop offset="0.490" stop-color="white"/><stop offset="1.000" stop-color="white" stop-opacity="0"/></linearGradient> | |
| 4 | <linearGradient id="fy0" x2="0" y2="792" gradientUnits="userSpaceOnUse"><stop offset="0.581" stop-color="white"/><stop offset="0.972" stop-color="white" stop-opacity="0"/></linearGradient> | |
| 5 | <mask id="across"><rect width="612" height="792" fill="url(#fx0)"/></mask> | |
| 6 | <mask id="down"><rect width="612" height="792" fill="url(#fy0)"/></mask> | |
| 7 | <linearGradient id="bar" x2="0" y2="1"><stop offset="0" stop-color="#e2e9f5"/><stop offset=".6" stop-color="#c8d5e3"/><stop offset=".85" stop-color="#8d9dad"/><stop offset="1" stop-color="#7f8f9f"/></linearGradient> | |
| 8 | <linearGradient id="post" x2="1"><stop offset="0" stop-color="#c8d4e1"/><stop offset=".5" stop-color="#a6b7c7"/><stop offset=".85" stop-color="#74808e"/><stop offset="1" stop-color="#c7c7c7"/></linearGradient> | |
| 9 | <filter id="soft"><feGaussianBlur stdDeviation="1.5"/></filter> | |
| 10 | </defs> | |
| 11 | <rect width="30" height="792" fill="url(#post)" mask="url(#down)"/> | |
| 12 | <rect width="612" height="55" fill="url(#bar)" mask="url(#across)"/> | |
| 13 | <rect y="55" width="612" height="4" fill="#bcbcbc" mask="url(#across)" filter="url(#soft)"/> | |
| 14 | <g><rect x="22" y="24.0" width="100" height="2" rx="1" fill="#8a8178"/><rect x="25" y="26.4" width="97" height="2" rx="1" fill="#f4f4f2"/><rect x="23" y="28.8" width="99" height="2" rx="1" fill="#bebebe"/><rect x="26" y="31.2" width="96" height="2" rx="1" fill="#fafafa"/><rect x="24" y="33.6" width="98" height="2" rx="1" fill="#6f655c"/><rect x="22" y="36.0" width="100" height="2" rx="1" fill="#e8e8e6"/><rect x="25" y="38.4" width="97" height="2" rx="1" fill="#c4c5c7"/><rect x="23" y="40.8" width="99" height="2" rx="1" fill="#ffffff"/><rect x="24" y="43.2" width="98" height="2" rx="1" fill="#b1b7b5"/></g> | |
| 15 | </svg> |
crates/canvas/assets/backgrounds/paws-cyan.svg created+18| ... | ... | @@ -0,0 +1,18 @@ |
| 1 | <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 50 500" preserveAspectRatio="none"> | |
| 2 | <defs> | |
| 3 | <g id="paw"> | |
| 4 | <path d="M-12 6C-11-2-5-8 0-8S11-2 12 6C12 10 8 10 0 8.5C-8 10-12 10-12 6Z"/> | |
| 5 | <ellipse rx="4.4" ry="5.6" transform="translate(-13.5 -9) rotate(-28)"/> | |
| 6 | <ellipse rx="4" ry="6.5" transform="translate(-7 -19) rotate(-10)"/> | |
| 7 | <ellipse rx="4" ry="6.5" transform="translate(4 -19.5) rotate(10)"/> | |
| 8 | <ellipse rx="4.2" ry="5.6" transform="translate(11 -10) rotate(28)"/> | |
| 9 | </g> | |
| 10 | </defs> | |
| 11 | <use href="#paw" fill="#d6f3ef" transform="translate(21 37)"/> | |
| 12 | <use href="#paw" fill="#d6f3ef" transform="translate(21 175)"/> | |
| 13 | <use href="#paw" fill="#d6f3ef" transform="translate(21 321)"/> | |
| 14 | <use href="#paw" fill="#d6f3ef" transform="translate(21 476)"/> | |
| 15 | <use href="#paw" fill="#adcbc6" transform="translate(25 106) rotate(15)"/> | |
| 16 | <use href="#paw" fill="#adcbc6" transform="translate(25 245) rotate(15)"/> | |
| 17 | <use href="#paw" fill="#adcbc6" transform="translate(25 395) rotate(15)"/> | |
| 18 | </svg> |
crates/canvas/assets/backgrounds/pencil-and-notebook.svg created+17| ... | ... | @@ -0,0 +1,17 @@ |
| 1 | <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 595 450" preserveAspectRatio="none"> | |
| 2 | <defs> | |
| 3 | <radialGradient id="reach" cx="200" cy="0" r="1" gradientTransform="translate(200 0) scale(480 150) translate(-200 0)" gradientUnits="userSpaceOnUse"><stop offset="0.35" stop-color="white"/><stop offset="1" stop-color="white" stop-opacity="0"/></radialGradient><mask id="art"><rect width="595" height="450" fill="url(#reach)"/></mask> | |
| 4 | <linearGradient id="pages" x2="0" y2="1"><stop offset="0" stop-color="#e8d2c3"/><stop offset="1" stop-color="#fbf4ee"/></linearGradient> | |
| 5 | <linearGradient id="ferrule" x2="1"><stop offset="0" stop-color="#8d7263"/><stop offset=".3" stop-color="#f1e6de"/><stop offset=".6" stop-color="#a88a79"/><stop offset="1" stop-color="#e9ddd4"/></linearGradient> | |
| 6 | <linearGradient id="barrel" x2="0" y2="1"><stop offset="0" stop-color="#f7ece4"/><stop offset=".5" stop-color="#e7cdbd"/><stop offset="1" stop-color="#c29c86"/></linearGradient> | |
| 7 | </defs> | |
| 8 | <filter id="soft" x="-10%" y="-10%" width="120%" height="120%"><feGaussianBlur stdDeviation="1.8"/></filter> | |
| 9 | <g mask="url(#art)" filter="url(#soft)" opacity=".85"> | |
| 10 | <path d="M0 60C80 40 150 50 190 70V100C150 85 80 80 0 100Z" fill="#c2a291"/> | |
| 11 | <path d="M0 0H170C190 30 185 60 175 80C120 60 60 58 0 70Z" fill="url(#pages)"/> | |
| 12 | <g stroke="#d3b8a7" stroke-width="1"><path d="M10 76C120 70 250 70 470 80"/><path d="M13 79C120 73 250 72 470 81"/><path d="M16 82C120 76 250 74 470 82"/><path d="M19 85C120 79 250 76 470 83"/><path d="M22 88C120 82 250 78 470 84"/><path d="M25 91C120 85 250 80 470 85"/><path d="M28 94C120 88 250 82 470 86"/><path d="M31 97C120 91 250 84 470 87"/></g> | |
| 13 | <rect x="265" y="0" width="40" height="60" rx="6" fill="#9f7f6f"/> | |
| 14 | <rect x="300" y="0" width="115" height="66" fill="url(#ferrule)"/> | |
| 15 | <rect x="415" y="4" width="180" height="58" fill="url(#barrel)"/> | |
| 16 | </g> | |
| 17 | </svg> |
crates/canvas/assets/backgrounds/pens.svg created+27| ... | ... | @@ -0,0 +1,27 @@ |
| 1 | <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 612 792" preserveAspectRatio="none"> | |
| 2 | <defs> | |
| 3 | <linearGradient id="fx0" x2="612" gradientUnits="userSpaceOnUse"><stop offset="0.392" stop-color="white"/><stop offset="0.915" stop-color="white" stop-opacity="0"/></linearGradient> | |
| 4 | <linearGradient id="fy0" x2="0" y2="792" gradientUnits="userSpaceOnUse"><stop offset="0.556" stop-color="white"/><stop offset="0.960" stop-color="white" stop-opacity="0"/></linearGradient> | |
| 5 | <mask id="across"><rect width="612" height="792" fill="url(#fx0)"/></mask> | |
| 6 | <mask id="down"><rect width="612" height="792" fill="url(#fy0)"/></mask> | |
| 7 | <radialGradient id="corner" cx="0" cy="0" r="1" gradientTransform="scale(300 260)" gradientUnits="userSpaceOnUse"><stop offset=".45" stop-color="white"/><stop offset="1" stop-color="white" stop-opacity="0"/></radialGradient> | |
| 8 | <mask id="art"><rect width="612" height="792" fill="url(#corner)"/></mask> | |
| 9 | </defs> | |
| 10 | <g mask="url(#art)" opacity=".85"> | |
| 11 | <g transform="translate(95 -18) rotate(8)"> | |
| 12 | <rect width="150" height="150" fill="#fff3ea" stroke="#f6c3a2"/> | |
| 13 | <g stroke="#f7c7a8" stroke-width=".8"><path d="M0 8H150"/><path d="M0 15H150"/><path d="M0 22H150"/><path d="M0 29H150"/><path d="M0 36H150"/><path d="M0 43H150"/><path d="M0 50H150"/><path d="M0 57H150"/><path d="M0 64H150"/><path d="M0 71H150"/><path d="M0 78H150"/><path d="M0 85H150"/><path d="M0 92H150"/><path d="M0 99H150"/><path d="M0 106H150"/><path d="M0 113H150"/><path d="M0 120H150"/><path d="M0 127H150"/><path d="M0 134H150"/><path d="M0 141H150"/><path d="M0 148H150"/></g> | |
| 14 | </g> | |
| 15 | <rect x="-10" y="60" width="44" height="130" rx="4" transform="rotate(10)" fill="#fbd4bb" stroke="#f3a578" stroke-width="1.5"/> | |
| 16 | <path d="M152 -10L230 150L222 156L140 0Z" fill="#fbb98e"/> | |
| 17 | <path d="M230 150L236 170L222 156Z" fill="#e89468"/> | |
| 18 | <path d="M0 222L170 140L176 150L6 238Z" fill="#fcc6a3"/> | |
| 19 | <path d="M170 140L192 132L176 150Z" fill="#ef9f73"/> | |
| 20 | </g> | |
| 21 | <g fill="#a2552b"> | |
| 22 | <rect x="23" y="19" width="3" height="773" mask="url(#down)"/> | |
| 23 | <rect x="40" y="36" width="1.5" height="756" mask="url(#down)"/> | |
| 24 | <rect x="23" y="19" width="589" height="3" mask="url(#across)"/> | |
| 25 | <rect x="40" y="36" width="572" height="1.5" mask="url(#across)"/> | |
| 26 | </g> | |
| 27 | </svg> |
crates/canvas/assets/backgrounds/pixel-blends.svg created+39| ... | ... | @@ -0,0 +1,39 @@ |
| 1 | <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 39 600" preserveAspectRatio="none"> | |
| 2 | <defs><linearGradient id="blend" x2="0" y2="1"> | |
| 3 | <stop offset="0" stop-color="#ffffff" stop-opacity="0"/> | |
| 4 | <stop offset="0.046" stop-color="#fff3fb"/> | |
| 5 | <stop offset="0.106" stop-color="#f2d7e4"/> | |
| 6 | <stop offset="0.166" stop-color="#fff5e6"/> | |
| 7 | <stop offset="0.226" stop-color="#ffd9c0"/> | |
| 8 | <stop offset="0.286" stop-color="#fffde6"/> | |
| 9 | <stop offset="0.346" stop-color="#fff0bb"/> | |
| 10 | <stop offset="0.406" stop-color="#eef7ff"/> | |
| 11 | <stop offset="0.466" stop-color="#c9d7f6"/> | |
| 12 | <stop offset="0.526" stop-color="#fbf1ff"/> | |
| 13 | <stop offset="0.586" stop-color="#e0d2f2"/> | |
| 14 | <stop offset="0.646" stop-color="#f3f7ee"/> | |
| 15 | <stop offset="0.706" stop-color="#e0e7d2"/> | |
| 16 | <stop offset="0.766" stop-color="#eaf9f7"/> | |
| 17 | <stop offset="0.826" stop-color="#d2e2e0"/> | |
| 18 | <stop offset="0.886" stop-color="#fff3f3"/> | |
| 19 | <stop offset="0.946" stop-color="#fbced2"/> | |
| 20 | <stop offset=".985" stop-color="#ffffff" stop-opacity="0"/> | |
| 21 | </linearGradient></defs> | |
| 22 | <rect x="15" width="17" height="600" fill="url(#blend)"/> | |
| 23 | <rect x="7" y="19" width="17" height="17" fill="#ffe7f7"/> | |
| 24 | <rect x="7" y="55" width="17" height="17" fill="#e7b6ce"/> | |
| 25 | <rect x="7" y="91" width="17" height="17" fill="#ffebce"/> | |
| 26 | <rect x="7" y="127" width="17" height="17" fill="#ffba8c"/> | |
| 27 | <rect x="7" y="163" width="17" height="17" fill="#fffbce"/> | |
| 28 | <rect x="7" y="199" width="17" height="17" fill="#ffe384"/> | |
| 29 | <rect x="7" y="235" width="17" height="17" fill="#deefff"/> | |
| 30 | <rect x="7" y="271" width="17" height="17" fill="#9cb6ef"/> | |
| 31 | <rect x="7" y="307" width="17" height="17" fill="#f7e3ff"/> | |
| 32 | <rect x="7" y="343" width="17" height="17" fill="#c6aee7"/> | |
| 33 | <rect x="7" y="379" width="17" height="17" fill="#e7efde"/> | |
| 34 | <rect x="7" y="415" width="17" height="17" fill="#c6d3ad"/> | |
| 35 | <rect x="7" y="451" width="17" height="17" fill="#d6f3ef"/> | |
| 36 | <rect x="7" y="487" width="17" height="17" fill="#adcbc6"/> | |
| 37 | <rect x="7" y="523" width="17" height="17" fill="#ffe7e7"/> | |
| 38 | <rect x="7" y="559" width="17" height="17" fill="#f7a6ad"/> | |
| 39 | </svg> |
crates/canvas/assets/backgrounds/pixel.svg created+11| ... | ... | @@ -0,0 +1,11 @@ |
| 1 | <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 3005 184" preserveAspectRatio="none"> | |
| 2 | <defs> | |
| 3 | <linearGradient id="edge" x2="85" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#deefff"/><stop offset="1" stop-color="#deefff" stop-opacity="0"/></linearGradient> | |
| 4 | <linearGradient id="band" x1="225" x2="2700" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#deefff"/><stop offset=".55" stop-color="#f0f8ff"/><stop offset="1" stop-color="#f0f8ff" stop-opacity="0"/></linearGradient> | |
| 5 | </defs> | |
| 6 | <rect width="90" height="175" fill="url(#edge)"/> | |
| 7 | <path d="M180 90H225V45H3005V135H180Z" fill="url(#band)"/> | |
| 8 | <rect x="45" y="45" width="45" height="45" fill="#ffe384"/> | |
| 9 | <g fill="#fffbce"><rect x="90" y="90" width="45" height="45"/><rect x="135" y="45" width="45" height="45"/><rect x="180" width="45" height="45"/></g> | |
| 10 | <g fill="#9cb6ef"><rect x="90" y="135" width="45" height="45"/><rect x="135" y="90" width="45" height="45"/><rect x="180" y="45" width="45" height="45"/></g> | |
| 11 | </svg> |
crates/canvas/assets/backgrounds/plain-and-simple.svg created+12| ... | ... | @@ -0,0 +1,12 @@ |
| 1 | <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 612 792" preserveAspectRatio="none"> | |
| 2 | <defs> | |
| 3 | <linearGradient id="rule-across" x2="1"><stop offset="0" stop-color="#777c99"/><stop offset=".92" stop-color="#777c99" stop-opacity="0"/></linearGradient> | |
| 4 | <linearGradient id="rule-down" x2="0" y2="1"><stop offset="0" stop-color="#777c99"/><stop offset=".72" stop-color="#777c99" stop-opacity="0"/></linearGradient> | |
| 5 | <linearGradient id="band" x2="1"><stop offset=".2" stop-color="#233776"/><stop offset=".9" stop-color="#233776" stop-opacity="0"/></linearGradient> | |
| 6 | <linearGradient id="margin" x2="0" y2="1"><stop offset="0" stop-color="#ececf1"/><stop offset=".38" stop-color="#ececf1" stop-opacity="0"/></linearGradient> | |
| 7 | </defs> | |
| 8 | <rect y="16" width="29" height="776" fill="url(#margin)"/> | |
| 9 | <rect width="612" height="6" fill="url(#rule-across)"/> | |
| 10 | <rect x="29" width="3" height="792" fill="url(#rule-down)"/> | |
| 11 | <rect y="8" width="612" height="8" fill="url(#band)"/> | |
| 12 | </svg> |
crates/canvas/assets/backgrounds/purple-clouds.svg created+61| ... | ... | @@ -0,0 +1,61 @@ |
| 1 | <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 611 163" preserveAspectRatio="none"> | |
| 2 | <defs> | |
| 3 | <linearGradient id="band" x2="1"> | |
| 4 | <stop offset=".25" stop-color="#834a7f"/> | |
| 5 | <stop offset=".33" stop-color="#8f5b8b"/> | |
| 6 | <stop offset=".46" stop-color="#b28aac"/> | |
| 7 | <stop offset=".58" stop-color="#d3b7cd"/> | |
| 8 | <stop offset=".69" stop-color="#ebdcea"/> | |
| 9 | <stop offset=".8" stop-color="#ebdcea" stop-opacity="0"/> | |
| 10 | </linearGradient> | |
| 11 | <linearGradient id="rule" x2="1"> | |
| 12 | <stop offset=".53" stop-color="#5b1e57"/> | |
| 13 | <stop offset=".7" stop-color="#c3aac0"/> | |
| 14 | <stop offset=".8" stop-color="#c3aac0" stop-opacity="0"/> | |
| 15 | </linearGradient> | |
| 16 | <linearGradient id="across" x2="1"><stop offset=".68" stop-color="white"/><stop offset=".86" stop-color="white" stop-opacity="0"/></linearGradient> | |
| 17 | <linearGradient id="down" x2="0" y2="1"><stop offset=".5" stop-color="white"/><stop offset=".82" stop-color="white" stop-opacity="0"/></linearGradient> | |
| 18 | <mask id="fade-across"><rect width="611" height="163" fill="url(#across)"/></mask> | |
| 19 | <mask id="fade-down"><rect width="611" height="163" fill="url(#down)"/></mask> | |
| 20 | <filter id="b1" x="-20%" y="-50%" width="140%" height="200%"><feGaussianBlur stdDeviation="2.2"/></filter> | |
| 21 | <filter id="b2" x="-20%" y="-50%" width="140%" height="200%"><feGaussianBlur stdDeviation="5"/></filter> | |
| 22 | <filter id="haze" x="-20%" y="-50%" width="140%" height="200%"><feGaussianBlur stdDeviation="12"/></filter> | |
| 23 | <linearGradient id="c0" x2="0" y2="1"><stop offset="0" stop-color="#e3cfe3"/><stop offset=".7" stop-color="#cdb1cd"/><stop offset="1" stop-color="#cdb1cd" stop-opacity=".2"/></linearGradient> | |
| 24 | <linearGradient id="c1" x2="0" y2="1"><stop offset="0" stop-color="#c9a3c7"/><stop offset=".7" stop-color="#9e6f9b"/><stop offset="1" stop-color="#9e6f9b" stop-opacity=".2"/></linearGradient> | |
| 25 | <linearGradient id="c2" x2="0" y2="1"><stop offset="0" stop-color="#a172a0"/><stop offset=".7" stop-color="#83547f"/><stop offset="1" stop-color="#83547f" stop-opacity=".2"/></linearGradient> | |
| 26 | <linearGradient id="c3" x2="0" y2="1"><stop offset="0" stop-color="#e0c9e0"/><stop offset=".7" stop-color="#c7a3c5"/><stop offset="1" stop-color="#c7a3c5" stop-opacity=".2"/></linearGradient> | |
| 27 | <linearGradient id="c4" x2="0" y2="1"><stop offset="0" stop-color="#d5b8d5"/><stop offset=".7" stop-color="#b28ab2"/><stop offset="1" stop-color="#b28ab2" stop-opacity=".2"/></linearGradient> | |
| 28 | <linearGradient id="c5" x2="0" y2="1"><stop offset="0" stop-color="#e2d0e2"/><stop offset=".7" stop-color="#cdb2cd"/><stop offset="1" stop-color="#cdb2cd" stop-opacity=".2"/></linearGradient> | |
| 29 | <linearGradient id="c6" x2="0" y2="1"><stop offset="0" stop-color="#e6dfea"/><stop offset=".7" stop-color="#d4ccdc"/><stop offset="1" stop-color="#d4ccdc" stop-opacity=".2"/></linearGradient> | |
| 30 | <linearGradient id="c7" x2="0" y2="1"><stop offset="0" stop-color="#dbc3dc"/><stop offset=".7" stop-color="#c4a3c6"/><stop offset="1" stop-color="#c4a3c6" stop-opacity=".2"/></linearGradient> | |
| 31 | <linearGradient id="c8" x2="0" y2="1"><stop offset="0" stop-color="#caa6cb"/><stop offset=".7" stop-color="#b18bb4"/><stop offset="1" stop-color="#b18bb4" stop-opacity=".2"/></linearGradient> | |
| 32 | <linearGradient id="c9" x2="0" y2="1"><stop offset="0" stop-color="#d4bad6"/><stop offset=".7" stop-color="#bba0c0"/><stop offset="1" stop-color="#bba0c0" stop-opacity=".2"/></linearGradient> | |
| 33 | <linearGradient id="c10" x2="0" y2="1"><stop offset="0" stop-color="#dcd3e2"/><stop offset=".7" stop-color="#c8bdd0"/><stop offset="1" stop-color="#c8bdd0" stop-opacity=".2"/></linearGradient> | |
| 34 | </defs> | |
| 35 | <g mask="url(#fade-across)"> | |
| 36 | <g mask="url(#fade-down)"> | |
| 37 | <rect y="19" width="611" height="125" fill="#f4e1eb"/> | |
| 38 | <g filter="url(#haze)"> | |
| 39 | <ellipse cx="90" cy="84" rx="130" ry="12" fill="#e2cde2"/> | |
| 40 | <ellipse cx="330" cy="28" rx="90" ry="8" fill="#e9d6e8"/> | |
| 41 | <ellipse cx="215" cy="55" rx="28" ry="16" fill="#fff8fb"/> | |
| 42 | <ellipse cx="370" cy="92" rx="140" ry="20" fill="#cbb6d1"/> | |
| 43 | <ellipse cx="100" cy="52" rx="75" ry="18" fill="#cfb0cf"/> | |
| 44 | <ellipse cx="200" cy="100" rx="200" ry="10" fill="#e8e2ec"/> | |
| 45 | </g> | |
| 46 | <g fill="url(#c0)" filter="url(#b2)"><circle cx="9" cy="53.2" r="4.3"/><circle cx="17.4" cy="51.3" r="6.7"/><circle cx="26.7" cy="50.8" r="7.9"/><circle cx="37.5" cy="51.1" r="7.5"/><circle cx="46.9" cy="47.7" r="11"/><circle cx="55.5" cy="45.1" r="10.9"/><circle cx="66.2" cy="45.6" r="10.4"/><circle cx="73.5" cy="45.6" r="12.6"/><circle cx="83.3" cy="48.6" r="9.5"/><circle cx="95.3" cy="47.7" r="9.5"/><circle cx="104" cy="47.5" r="9.9"/><circle cx="111.1" cy="50.2" r="8.1"/><circle cx="122.9" cy="49.4" r="8.7"/><circle cx="131.9" cy="50.4" r="7.7"/><circle cx="142.1" cy="51.1" r="7.1"/><circle cx="150.6" cy="52.1" r="5.1"/><rect x="8" y="52" width="144" height="6" rx="3"/></g> | |
| 47 | <g fill="url(#c1)" filter="url(#b1)"><circle cx="65.9" cy="43.3" r="6"/><circle cx="73.5" cy="40.1" r="9.3"/><circle cx="83.6" cy="40.1" r="11.4"/><circle cx="95.7" cy="36.3" r="13.2"/><circle cx="106.5" cy="38.8" r="10.6"/><circle cx="115.4" cy="40.3" r="9.9"/><circle cx="126.4" cy="42.5" r="7.7"/><rect x="61.4" y="44" width="67.2" height="6.5" rx="3.2"/></g> | |
| 48 | <g fill="url(#c2)" filter="url(#b1)"><circle cx="22.7" cy="49.9" r="2.3"/><circle cx="31.6" cy="46.8" r="4.9"/><circle cx="39.1" cy="47.6" r="4.4"/><circle cx="47.1" cy="48.6" r="4.1"/><circle cx="56.5" cy="49.9" r="2.3"/><rect x="18.4" y="50" width="43.2" height="2.5" rx="1.2"/></g> | |
| 49 | <g fill="url(#c3)" filter="url(#b1)"><circle cx="105.8" cy="37.6" r="4.4"/><circle cx="117.2" cy="35.8" r="6"/><circle cx="124.4" cy="32.7" r="8.1"/><circle cx="134.2" cy="37.5" r="4.4"/><rect x="103.7" y="38" width="32.6" height="4" rx="2"/></g> | |
| 50 | <g fill="url(#c4)" filter="url(#b1)"><circle cx="266.1" cy="39.6" r="6.6"/><circle cx="274.6" cy="40.2" r="7.5"/><circle cx="284.3" cy="36.8" r="9.9"/><circle cx="293.7" cy="39.2" r="8.1"/><circle cx="303.5" cy="37.1" r="8.8"/><circle cx="314.1" cy="41.2" r="5.8"/><rect x="263.1" y="42" width="53.8" height="5.5" rx="2.8"/></g> | |
| 51 | <g fill="url(#c5)" filter="url(#b1)"><circle cx="263.2" cy="35.6" r="2.5"/><circle cx="268.6" cy="32.7" r="5.1"/><circle cx="272.1" cy="34.5" r="4.3"/><circle cx="278" cy="36.2" r="2.5"/><rect x="260.4" y="36" width="19.2" height="2.5" rx="1.2"/></g> | |
| 52 | <g fill="url(#c6)" filter="url(#b2)"><circle cx="426.5" cy="44.9" r="4.1"/><circle cx="435.2" cy="43.9" r="5.4"/><circle cx="444.7" cy="42" r="7.7"/><circle cx="457.2" cy="40.2" r="9.3"/><circle cx="464" cy="40.2" r="8.7"/><circle cx="472.8" cy="36.9" r="10.1"/><circle cx="483.5" cy="41.6" r="8"/><circle cx="491.4" cy="42.8" r="6.3"/><circle cx="503.6" cy="45.3" r="4.1"/><rect x="424.7" y="44" width="80.6" height="5" rx="2.5"/></g> | |
| 53 | <g fill="url(#c7)" filter="url(#b2)"><circle cx="256.4" cy="86.2" r="6.7"/><circle cx="264" cy="85.5" r="6.6"/><circle cx="275" cy="80.7" r="10.7"/><circle cx="281.4" cy="83" r="10"/><circle cx="292.6" cy="79.3" r="11.7"/><circle cx="300.1" cy="79.8" r="11.1"/><circle cx="311.9" cy="75.4" r="15"/><circle cx="320.5" cy="77.8" r="15.2"/><circle cx="328.5" cy="80" r="13.7"/><circle cx="335.8" cy="79.1" r="13.7"/><circle cx="347.7" cy="73.9" r="18"/><circle cx="357.9" cy="70.6" r="18.5"/><circle cx="364.8" cy="78.9" r="14"/><circle cx="373.4" cy="77.3" r="14"/><circle cx="385.4" cy="74.1" r="17.6"/><circle cx="393.7" cy="76.7" r="17.1"/><circle cx="403" cy="72.8" r="17.3"/><circle cx="412.5" cy="78.8" r="14.4"/><circle cx="421.9" cy="77.6" r="13.1"/><circle cx="431.9" cy="79.5" r="12.7"/><circle cx="441" cy="79.7" r="13.5"/><circle cx="447" cy="80.8" r="10"/><circle cx="458.9" cy="82" r="9.1"/><circle cx="468.8" cy="82.5" r="9.9"/><circle cx="476.3" cy="86.2" r="7"/><circle cx="487.3" cy="85.1" r="7"/><rect x="254.8" y="84" width="230.4" height="8.5" rx="4.2"/></g> | |
| 54 | <g fill="url(#c8)" filter="url(#b1)"><circle cx="276.3" cy="76.1" r="5.1"/><circle cx="285.2" cy="75.9" r="6.3"/><circle cx="292.2" cy="73.8" r="7.6"/><circle cx="301.3" cy="73.3" r="9.3"/><circle cx="313.2" cy="71.8" r="9.9"/><circle cx="321.1" cy="69.1" r="12.6"/><circle cx="331.7" cy="70.2" r="11.1"/><circle cx="339.3" cy="72.8" r="8.9"/><circle cx="347.2" cy="72.3" r="8.5"/><circle cx="356.4" cy="71.4" r="9.5"/><circle cx="367.1" cy="73.6" r="7.9"/><circle cx="376.4" cy="74.7" r="7.8"/><circle cx="385.6" cy="77.4" r="4.7"/><rect x="272.4" y="76" width="115.2" height="6" rx="3"/></g> | |
| 55 | <g fill="url(#c9)" filter="url(#b1)"><circle cx="376.1" cy="71.4" r="4.8"/><circle cx="386" cy="68.1" r="8.1"/><circle cx="395.5" cy="67.9" r="8.2"/><circle cx="405.8" cy="68" r="8"/><circle cx="414.9" cy="67.4" r="8.2"/><circle cx="426.5" cy="71.1" r="5.5"/><rect x="371.2" y="72" width="57.6" height="4.5" rx="2.2"/></g> | |
| 56 | <g fill="url(#c10)" filter="url(#b2)"><circle cx="374.9" cy="91.2" r="4.6"/><circle cx="382.4" cy="91.8" r="4.5"/><circle cx="391.4" cy="91.2" r="5.7"/><circle cx="403" cy="87.2" r="8"/><circle cx="410.2" cy="87.2" r="8.5"/><circle cx="419.3" cy="85.3" r="9.4"/><circle cx="428.9" cy="86.6" r="9.8"/><circle cx="439.2" cy="85.3" r="9.7"/><circle cx="447.1" cy="88.5" r="7.6"/><circle cx="457" cy="90.2" r="6.4"/><circle cx="467.8" cy="91" r="5.2"/><circle cx="475.9" cy="92.2" r="4.3"/><circle cx="485.9" cy="92.9" r="3.9"/><rect x="372.4" y="92" width="115.2" height="4.5" rx="2.2"/></g> | |
| 57 | </g> | |
| 58 | </g> | |
| 59 | <rect width="611" height="17" fill="url(#band)"/> | |
| 60 | <rect y="17" width="611" height="2" fill="url(#rule)"/> | |
| 61 | </svg> |
crates/canvas/assets/backgrounds/pushpins-corner.svg created+16| ... | ... | @@ -0,0 +1,16 @@ |
| 1 | <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 177 123" preserveAspectRatio="none"> | |
| 2 | <defs> | |
| 3 | <radialGradient id="reach" cx="40" cy="20" r="1" gradientTransform="translate(40 20) scale(180 120) translate(-40 -20)" gradientUnits="userSpaceOnUse"><stop offset="0.2" stop-color="white"/><stop offset="1" stop-color="white" stop-opacity="0"/></radialGradient><mask id="glow"><rect width="177" height="123" fill="url(#reach)"/></mask> | |
| 4 | <linearGradient id="navy" x2="1"><stop offset="0" stop-color="#1f2d4a"/><stop offset=".45" stop-color="#6d7f9f"/><stop offset="1" stop-color="#1f2d4a"/></linearGradient> | |
| 5 | <g id="pin"> | |
| 6 | <path d="M-11-30C-11-36 11-36 11-30L8-12C16-8 18 0 15 4H-15C-18 0-16-8-8-12Z"/> | |
| 7 | <path d="M-1.2 4L0 34L1.2 4Z" fill="#9aa9b8"/> | |
| 8 | </g> | |
| 9 | <filter id="shadow" x="-40%" y="-40%" width="180%" height="180%"><feGaussianBlur stdDeviation="3"/></filter> | |
| 10 | <filter id="focus" x="-40%" y="-40%" width="180%" height="180%"><feGaussianBlur stdDeviation="1.5"/></filter> | |
| 11 | </defs> | |
| 12 | <rect width="177" height="123" fill="#d4e6f6" mask="url(#glow)"/> | |
| 13 | <g fill="#8fb8dc" filter="url(#shadow)" opacity=".7"><ellipse cx="60" cy="100" rx="18" ry="7"/><ellipse cx="130" cy="40" rx="18" ry="7"/></g> | |
| 14 | <use href="#pin" fill="url(#navy)" transform="translate(45 40) rotate(-15) scale(.85)"/> | |
| 15 | <use href="#pin" fill="url(#navy)" transform="translate(110 12) rotate(-25) scale(.7)" filter="url(#focus)" opacity=".8"/> | |
| 16 | </svg> |
crates/canvas/assets/backgrounds/rainbow-border.svg created+18| ... | ... | @@ -0,0 +1,18 @@ |
| 1 | <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 39 579" preserveAspectRatio="none"> | |
| 2 | <rect x="12" y="10" width="17" height="17" fill="#ffe7f7"/> | |
| 3 | <rect x="12" y="45.9" width="17" height="17" fill="#e7b6ce"/> | |
| 4 | <rect x="12" y="81.7" width="17" height="17" fill="#deefff"/> | |
| 5 | <rect x="12" y="117.6" width="17" height="17" fill="#9cb6ef"/> | |
| 6 | <rect x="12" y="153.5" width="17" height="17" fill="#f7e3ff"/> | |
| 7 | <rect x="12" y="189.3" width="17" height="17" fill="#c6aee7"/> | |
| 8 | <rect x="12" y="225.2" width="17" height="17" fill="#e7efde"/> | |
| 9 | <rect x="12" y="261.1" width="17" height="17" fill="#c6d3ad"/> | |
| 10 | <rect x="12" y="297" width="17" height="17" fill="#d6f3ef"/> | |
| 11 | <rect x="12" y="332.8" width="17" height="17" fill="#adcbc6"/> | |
| 12 | <rect x="12" y="368.7" width="17" height="17" fill="#ffe7e7"/> | |
| 13 | <rect x="12" y="404.6" width="17" height="17" fill="#f7a6ad"/> | |
| 14 | <rect x="12" y="440.4" width="17" height="17" fill="#ffebce"/> | |
| 15 | <rect x="12" y="476.3" width="17" height="17" fill="#ffba8c"/> | |
| 16 | <rect x="12" y="512.2" width="17" height="17" fill="#fffbce"/> | |
| 17 | <rect x="12" y="548" width="17" height="17" fill="#ffe384"/> | |
| 18 | </svg> |
crates/canvas/assets/backgrounds/rainbow-mini-border.svg created+44| ... | ... | @@ -0,0 +1,44 @@ |
| 1 | <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 30 700" preserveAspectRatio="none"> | |
| 2 | <rect x="12" y="8" width="8" height="8" fill="#ffe7f7"/> | |
| 3 | <rect x="12" y="24.6" width="8" height="8" fill="#e7b6ce"/> | |
| 4 | <rect x="12" y="41.2" width="8" height="8" fill="#deefff"/> | |
| 5 | <rect x="12" y="57.8" width="8" height="8" fill="#9cb6ef"/> | |
| 6 | <rect x="12" y="74.4" width="8" height="8" fill="#f7e3ff"/> | |
| 7 | <rect x="12" y="91" width="8" height="8" fill="#c6aee7"/> | |
| 8 | <rect x="12" y="107.6" width="8" height="8" fill="#e7efde"/> | |
| 9 | <rect x="12" y="124.2" width="8" height="8" fill="#c6d3ad"/> | |
| 10 | <rect x="12" y="140.8" width="8" height="8" fill="#d6f3ef"/> | |
| 11 | <rect x="12" y="157.4" width="8" height="8" fill="#adcbc6"/> | |
| 12 | <rect x="12" y="174" width="8" height="8" fill="#ffe7e7"/> | |
| 13 | <rect x="12" y="190.6" width="8" height="8" fill="#f7a6ad"/> | |
| 14 | <rect x="12" y="207.2" width="8" height="8" fill="#ffebce"/> | |
| 15 | <rect x="12" y="223.8" width="8" height="8" fill="#ffba8c"/> | |
| 16 | <rect x="12" y="240.4" width="8" height="8" fill="#fffbce"/> | |
| 17 | <rect x="12" y="257" width="8" height="8" fill="#ffe384"/> | |
| 18 | <rect x="12" y="273.6" width="8" height="8" fill="#ffe7f7"/> | |
| 19 | <rect x="12" y="290.2" width="8" height="8" fill="#e7b6ce"/> | |
| 20 | <rect x="12" y="306.8" width="8" height="8" fill="#deefff"/> | |
| 21 | <rect x="12" y="323.4" width="8" height="8" fill="#9cb6ef"/> | |
| 22 | <rect x="12" y="340" width="8" height="8" fill="#f7e3ff"/> | |
| 23 | <rect x="12" y="356.6" width="8" height="8" fill="#c6aee7"/> | |
| 24 | <rect x="12" y="373.2" width="8" height="8" fill="#e7efde"/> | |
| 25 | <rect x="12" y="389.8" width="8" height="8" fill="#c6d3ad"/> | |
| 26 | <rect x="12" y="406.4" width="8" height="8" fill="#d6f3ef"/> | |
| 27 | <rect x="12" y="423" width="8" height="8" fill="#adcbc6"/> | |
| 28 | <rect x="12" y="439.6" width="8" height="8" fill="#ffe7e7"/> | |
| 29 | <rect x="12" y="456.2" width="8" height="8" fill="#f7a6ad"/> | |
| 30 | <rect x="12" y="472.8" width="8" height="8" fill="#ffebce"/> | |
| 31 | <rect x="12" y="489.4" width="8" height="8" fill="#ffba8c"/> | |
| 32 | <rect x="12" y="506" width="8" height="8" fill="#fffbce"/> | |
| 33 | <rect x="12" y="522.6" width="8" height="8" fill="#ffe384"/> | |
| 34 | <rect x="12" y="539.2" width="8" height="8" fill="#ffe7f7"/> | |
| 35 | <rect x="12" y="555.8" width="8" height="8" fill="#e7b6ce"/> | |
| 36 | <rect x="12" y="572.4" width="8" height="8" fill="#deefff"/> | |
| 37 | <rect x="12" y="589" width="8" height="8" fill="#9cb6ef"/> | |
| 38 | <rect x="12" y="605.6" width="8" height="8" fill="#f7e3ff"/> | |
| 39 | <rect x="12" y="622.2" width="8" height="8" fill="#c6aee7"/> | |
| 40 | <rect x="12" y="638.8" width="8" height="8" fill="#e7efde"/> | |
| 41 | <rect x="12" y="655.4" width="8" height="8" fill="#c6d3ad"/> | |
| 42 | <rect x="12" y="672" width="8" height="8" fill="#d6f3ef"/> | |
| 43 | <rect x="12" y="688.6" width="8" height="8" fill="#adcbc6"/> | |
| 44 | </svg> |
crates/canvas/assets/backgrounds/rainbow.svg created+40| ... | ... | @@ -0,0 +1,40 @@ |
| 1 | <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 612 792" preserveAspectRatio="none"> | |
| 2 | <defs> | |
| 3 | <linearGradient id="sky" x2="0" y2="1"><stop offset=".1" stop-color="#daedfb"/><stop offset=".38" stop-color="#cfe3f6"/><stop offset=".55" stop-color="#bcd6ef"/></linearGradient> | |
| 4 | <linearGradient id="cloud" x2="0" y2="1"><stop offset=".45" stop-color="#ffffff"/><stop offset="1" stop-color="#e3ecf7"/></linearGradient> | |
| 5 | <g id="scene"> | |
| 6 | <rect width="612" height="792" fill="url(#sky)"/> | |
| 7 | <ellipse cx="290" cy="420" rx="200" ry="120" fill="#f1f6fc"/> | |
| 8 | <g fill="none" stroke-width="7.5"> | |
| 9 | <path d="M-4 640C14 300 250-80 425-62C525-52 585 80 616 205" transform="translate(0.0 0.0)" stroke="#f47473"/> | |
| 10 | <path d="M-4 640C14 300 250-80 425-62C525-52 585 80 616 205" transform="translate(6.3 3.15)" stroke="#f9b0a8"/> | |
| 11 | <path d="M-4 640C14 300 250-80 425-62C525-52 585 80 616 205" transform="translate(12.6 6.3)" stroke="#97dd09"/> | |
| 12 | <path d="M-4 640C14 300 250-80 425-62C525-52 585 80 616 205" transform="translate(18.900000000000002 9.450000000000001)" stroke="#fde706"/> | |
| 13 | <path d="M-4 640C14 300 250-80 425-62C525-52 585 80 616 205" transform="translate(25.2 12.6)" stroke="#ffffff"/> | |
| 14 | <path d="M-4 640C14 300 250-80 425-62C525-52 585 80 616 205" transform="translate(31.5 15.75)" stroke="#9fb1f3"/> | |
| 15 | <path d="M-4 640C14 300 250-80 425-62C525-52 585 80 616 205" transform="translate(37.800000000000004 18.900000000000002)" stroke="#ffffff"/> | |
| 16 | </g> | |
| 17 | <g fill="none" stroke-width="3"> | |
| 18 | <path d="M470 505C488 530 502 580 508 625" transform="translate(0.0 0)" stroke="#f47473"/> | |
| 19 | <path d="M470 505C488 530 502 580 508 625" transform="translate(2.8000000000000003 0)" stroke="#f9b0a8"/> | |
| 20 | <path d="M470 505C488 530 502 580 508 625" transform="translate(5.6000000000000005 0)" stroke="#97dd09"/> | |
| 21 | <path d="M470 505C488 530 502 580 508 625" transform="translate(8.4 0)" stroke="#fde706"/> | |
| 22 | <path d="M470 505C488 530 502 580 508 625" transform="translate(11.200000000000001 0)" stroke="#ffffff"/> | |
| 23 | </g> | |
| 24 | <g fill="url(#cloud)"><circle cx="30" cy="200" r="70"/><circle cx="80" cy="150" r="60"/><circle cx="40" cy="260" r="45"/><circle cx="160" cy="130" r="85"/><circle cx="240" cy="200" r="70"/><circle cx="130" cy="230" r="70"/><circle cx="270" cy="260" r="50"/><circle cx="300" cy="300" r="45"/><circle cx="350" cy="330" r="40"/><circle cx="260" cy="320" r="35"/><circle cx="510" cy="410" r="55"/><circle cx="450" cy="430" r="45"/><circle cx="560" cy="440" r="45"/><circle cx="470" cy="480" r="35"/></g> | |
| 25 | <ellipse cx="440" cy="760" rx="360" ry="140" fill="#8fc865"/> | |
| 26 | <ellipse cx="150" cy="800" rx="200" ry="110" fill="#79ba59"/> | |
| 27 | <ellipse cx="-10" cy="720" rx="85" ry="110" fill="#46963f"/> | |
| 28 | </g> | |
| 29 | <clipPath id="strip"><rect width="42" height="792"/></clipPath> | |
| 30 | <clipPath id="page"><rect x="42" width="570" height="792"/></clipPath> | |
| 31 | <linearGradient id="across" x2="1"><stop offset=".88" stop-color="white"/><stop offset="1" stop-color="white" stop-opacity="0"/></linearGradient> | |
| 32 | <linearGradient id="down" x2="0" y2="1"><stop offset=".82" stop-color="white"/><stop offset="1" stop-color="white" stop-opacity="0"/></linearGradient> | |
| 33 | <mask id="fade-across"><rect width="612" height="792" fill="url(#across)"/></mask> | |
| 34 | <mask id="fade-down"><rect width="612" height="792" fill="url(#down)"/></mask> | |
| 35 | </defs> | |
| 36 | <g mask="url(#fade-down)"> | |
| 37 | <use href="#scene" clip-path="url(#strip)"/> | |
| 38 | <g mask="url(#fade-across)"><use href="#scene" clip-path="url(#page)" opacity=".16"/></g> | |
| 39 | </g> | |
| 40 | </svg> |
crates/canvas/assets/backgrounds/recipe.svg created+21| ... | ... | @@ -0,0 +1,21 @@ |
| 1 | <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 612 792" preserveAspectRatio="none"> | |
| 2 | <defs> | |
| 3 | <radialGradient id="reach" cx="180" cy="150" r="1" gradientTransform="translate(180 150) scale(420 560) translate(-180 -150)" gradientUnits="userSpaceOnUse"><stop offset="0.4" stop-color="white"/><stop offset="1" stop-color="white" stop-opacity="0"/></radialGradient><mask id="art"><rect width="612" height="792" fill="url(#reach)"/></mask> | |
| 4 | <linearGradient id="down" x2="0" y2="1"><stop offset="0.8" stop-color="white"/><stop offset="1" stop-color="white" stop-opacity="0"/></linearGradient><mask id="strip-fade"><rect width="612" height="792" fill="url(#down)"/></mask> | |
| 5 | <radialGradient id="bowl" fx=".35" fy=".3"><stop offset="0" stop-color="#f4f4f4"/><stop offset=".6" stop-color="#9c9c9c"/><stop offset="1" stop-color="#5f5f5f"/></radialGradient> | |
| 6 | <linearGradient id="handle" x2="0" y2="1"><stop offset="0" stop-color="#f0f0f0"/><stop offset="1" stop-color="#8a8a8a"/></linearGradient> | |
| 7 | <filter id="soft" x="-10%" y="-10%" width="120%" height="120%"><feGaussianBlur stdDeviation="2"/></filter> | |
| 8 | </defs> | |
| 9 | <g mask="url(#strip-fade)"> | |
| 10 | <rect width="38" height="792" fill="#e6e8e3"/> | |
| 11 | <g fill="#b7beb2"><circle cx="19.8" cy="638.9" r="1.3"/><circle cx="11" cy="606.8" r="1.1"/><circle cx="25.1" cy="87.2" r="0.5"/><circle cx="14.6" cy="591.1" r="0.7"/><circle cx="19.1" cy="251.4" r="1.2"/><circle cx="35.9" cy="318.1" r="1.3"/><circle cx="2.4" cy="643.6" r="1.2"/><circle cx="5.7" cy="558.8" r="0.9"/><circle cx="36.3" cy="166.3" r="0.9"/><circle cx="33.5" cy="505.9" r="0.7"/><circle cx="3.1" cy="744.3" r="0.9"/><circle cx="37.9" cy="112.2" r="1.1"/><circle cx="30.9" cy="728.6" r="0.5"/><circle cx="14.1" cy="40.9" r="1.1"/><circle cx="13.5" cy="759.9" r="0.8"/><circle cx="29.4" cy="687.3" r="0.7"/><circle cx="21.2" cy="758.3" r="0.9"/><circle cx="13.7" cy="211.6" r="1.1"/><circle cx="7.3" cy="478.3" r="0.9"/><circle cx="21" cy="102.6" r="1"/><circle cx="34.8" cy="252.6" r="0.8"/><circle cx="34.5" cy="458" r="0.7"/><circle cx="0.4" cy="596.6" r="0.8"/><circle cx="17.8" cy="215.8" r="1"/><circle cx="27.7" cy="245.4" r="0.6"/><circle cx="37.5" cy="720.8" r="0.5"/><circle cx="21.4" cy="543.3" r="0.8"/><circle cx="27.8" cy="760.2" r="1"/><circle cx="14.9" cy="155" r="0.7"/><circle cx="5.7" cy="87.4" r="0.7"/><circle cx="32" cy="425.3" r="1.1"/><circle cx="35" cy="642.1" r="0.5"/><circle cx="35.2" cy="73.5" r="1.1"/><circle cx="31.9" cy="520.7" r="0.8"/><circle cx="2.9" cy="67.4" r="1"/><circle cx="9.2" cy="421.6" r="0.7"/><circle cx="6.2" cy="233.8" r="1.1"/><circle cx="18.5" cy="311.1" r="0.9"/><circle cx="21.1" cy="408.4" r="0.9"/><circle cx="36.8" cy="595.4" r="0.7"/><circle cx="12.1" cy="149.5" r="0.8"/><circle cx="1.1" cy="589.6" r="1"/><circle cx="16.1" cy="707.6" r="0.8"/><circle cx="22" cy="736.4" r="0.9"/><circle cx="23.3" cy="291.4" r="1.3"/><circle cx="28.7" cy="240.2" r="0.8"/><circle cx="22.5" cy="410.8" r="0.9"/><circle cx="7.3" cy="456" r="1.1"/><circle cx="29.1" cy="355.9" r="0.9"/><circle cx="19.2" cy="424.7" r="0.9"/><circle cx="0.5" cy="208.6" r="1.3"/><circle cx="31.4" cy="370.2" r="1.1"/><circle cx="8" cy="211.6" r="0.9"/><circle cx="26.6" cy="21.8" r="0.9"/><circle cx="34.4" cy="484.9" r="1.3"/><circle cx="3.8" cy="719.6" r="1.2"/><circle cx="1" cy="425" r="1.1"/><circle cx="8.4" cy="514.2" r="1.2"/><circle cx="30.2" cy="339.1" r="0.5"/><circle cx="20.5" cy="149" r="0.9"/><circle cx="10.2" cy="275.1" r="0.9"/><circle cx="31.1" cy="772.5" r="0.8"/><circle cx="0.3" cy="594.1" r="0.8"/><circle cx="28.8" cy="657.8" r="0.5"/><circle cx="27.8" cy="225.6" r="0.8"/><circle cx="12.7" cy="426.4" r="0.7"/><circle cx="18.8" cy="224.4" r="0.6"/><circle cx="9.5" cy="539.3" r="0.7"/><circle cx="7.6" cy="289.5" r="0.7"/><circle cx="37.4" cy="623.5" r="0.6"/><circle cx="11.2" cy="345.2" r="0.9"/><circle cx="14.8" cy="414.5" r="1.1"/><circle cx="5.9" cy="442.9" r="0.6"/><circle cx="20.3" cy="251.1" r="1.1"/><circle cx="33.5" cy="757.7" r="0.8"/><circle cx="23.4" cy="133.6" r="1.1"/><circle cx="29.6" cy="338.2" r="0.8"/><circle cx="28.8" cy="594.3" r="1.1"/><circle cx="11" cy="192.1" r="0.7"/><circle cx="7.3" cy="5.6" r="0.6"/><circle cx="18.7" cy="341" r="1.2"/><circle cx="14.4" cy="134.9" r="1.2"/><circle cx="2.8" cy="269.1" r="1.1"/><circle cx="12.2" cy="763.5" r="1.1"/><circle cx="11.7" cy="683.8" r="1.2"/><circle cx="19.5" cy="113.7" r="0.6"/><circle cx="31.1" cy="703.3" r="1"/><circle cx="29.4" cy="628.6" r="0.7"/><circle cx="8.1" cy="687.2" r="1.2"/><circle cx="16.6" cy="214" r="0.6"/><circle cx="15.4" cy="719.9" r="0.7"/><circle cx="12" cy="27.9" r="1"/><circle cx="2.3" cy="534.5" r="0.9"/><circle cx="16.1" cy="205.4" r="0.7"/><circle cx="33.9" cy="745.1" r="1.1"/><circle cx="8.1" cy="164.8" r="0.8"/><circle cx="33.3" cy="746.7" r="0.7"/><circle cx="24.1" cy="300.8" r="0.9"/><circle cx="7.5" cy="571.5" r="1.1"/><circle cx="35.7" cy="383.5" r="0.9"/><circle cx="24.6" cy="104" r="1.2"/><circle cx="29.2" cy="180.4" r="0.8"/><circle cx="18.5" cy="674.7" r="1.1"/><circle cx="9.1" cy="753.3" r="1.2"/><circle cx="32.6" cy="390.2" r="0.9"/><circle cx="15.4" cy="774.4" r="0.9"/><circle cx="36.5" cy="145.7" r="0.5"/><circle cx="17.9" cy="157.4" r="0.7"/><circle cx="1.4" cy="98.5" r="0.9"/><circle cx="3.3" cy="234.7" r="1"/><circle cx="9.9" cy="246.6" r="0.8"/><circle cx="10" cy="446.2" r="1.1"/><circle cx="1.2" cy="424.9" r="0.9"/><circle cx="34.9" cy="557.6" r="0.7"/><circle cx="23.4" cy="713.3" r="1.3"/><circle cx="29.2" cy="85.2" r="0.8"/><circle cx="18.2" cy="260.8" r="1.1"/><circle cx="18.7" cy="775" r="0.5"/><circle cx="30.7" cy="90" r="0.9"/><circle cx="5.7" cy="302.8" r="1"/><circle cx="36.4" cy="32.4" r="0.8"/><circle cx="4.2" cy="588.4" r="0.6"/><circle cx="7.5" cy="640.6" r="0.5"/><circle cx="33.6" cy="212.4" r="1.3"/><circle cx="4" cy="76.2" r="0.9"/><circle cx="11.7" cy="510.1" r="0.7"/><circle cx="12.6" cy="13.2" r="0.6"/><circle cx="0.1" cy="365.4" r="1.2"/><circle cx="20.4" cy="256.1" r="0.9"/><circle cx="29.1" cy="689.5" r="0.6"/><circle cx="24.6" cy="235.2" r="1.2"/><circle cx="27.5" cy="467.4" r="1"/><circle cx="29" cy="56.2" r="0.8"/><circle cx="1.5" cy="122.1" r="1.2"/><circle cx="21.7" cy="158.7" r="0.7"/><circle cx="15.9" cy="67.6" r="1.2"/><circle cx="14.5" cy="44.6" r="0.9"/><circle cx="15.7" cy="604.3" r="0.7"/><circle cx="9.1" cy="397.3" r="0.8"/><circle cx="4.9" cy="582.5" r="1.2"/><circle cx="31.6" cy="520.9" r="0.9"/><circle cx="30" cy="167.1" r="0.6"/><circle cx="7.8" cy="420.2" r="0.7"/><circle cx="16.6" cy="643.5" r="1"/><circle cx="7.6" cy="96.5" r="0.5"/><circle cx="1.9" cy="207.1" r="1.1"/><circle cx="31.5" cy="306" r="0.5"/><circle cx="21.3" cy="751.1" r="0.5"/><circle cx="23.9" cy="790.5" r="0.8"/><circle cx="22.5" cy="642.4" r="1"/><circle cx="6.6" cy="622.7" r="1"/><circle cx="25.1" cy="474.3" r="1.2"/><circle cx="21.7" cy="731.1" r="0.6"/><circle cx="0.9" cy="109" r="0.9"/><circle cx="27.3" cy="726.7" r="0.5"/><circle cx="0.3" cy="303" r="1"/><circle cx="15.2" cy="221.6" r="0.7"/><circle cx="27.1" cy="480.4" r="1.3"/><circle cx="12.4" cy="377" r="1.3"/><circle cx="26.6" cy="207" r="1.1"/><circle cx="17" cy="71" r="0.6"/><circle cx="18.9" cy="246.6" r="0.7"/><circle cx="27.5" cy="204.1" r="0.9"/><circle cx="19.3" cy="6.3" r="1.1"/><circle cx="10.2" cy="128.1" r="0.9"/><circle cx="8.1" cy="25.6" r="0.6"/><circle cx="1.5" cy="280.6" r="0.8"/><circle cx="8" cy="138.6" r="1.2"/><circle cx="21.9" cy="480.5" r="0.5"/><circle cx="35.1" cy="25.6" r="1.1"/><circle cx="24.8" cy="325.5" r="0.8"/><circle cx="20.6" cy="460.4" r="1.2"/><circle cx="2.2" cy="70.6" r="1"/><circle cx="10.8" cy="459.4" r="0.8"/><circle cx="1.3" cy="652.4" r="0.6"/><circle cx="12.7" cy="588.9" r="1.2"/><circle cx="32.5" cy="736.3" r="0.8"/><circle cx="8.9" cy="363.3" r="1.2"/><circle cx="24.7" cy="495" r="1.1"/><circle cx="8.2" cy="448.9" r="0.8"/><circle cx="33.5" cy="245.3" r="0.9"/><circle cx="24" cy="559" r="1.1"/><circle cx="28.4" cy="105.5" r="1.3"/><circle cx="1.4" cy="579.4" r="0.7"/><circle cx="3.8" cy="287.1" r="0.6"/><circle cx="8" cy="765.8" r="1.1"/><circle cx="14.1" cy="391.2" r="1"/><circle cx="12.1" cy="106.8" r="0.9"/><circle cx="32.1" cy="760.7" r="1"/><circle cx="30.5" cy="331.5" r="1.1"/><circle cx="30.8" cy="714.9" r="0.7"/><circle cx="19.9" cy="58.5" r="1"/><circle cx="18" cy="468.2" r="0.9"/><circle cx="13" cy="111.4" r="0.8"/><circle cx="29.6" cy="218.7" r="1.3"/><circle cx="31.9" cy="208.9" r="0.9"/><circle cx="30.3" cy="7.2" r="1.1"/><circle cx="15.5" cy="534.5" r="0.5"/><circle cx="24.7" cy="45.3" r="0.7"/><circle cx="23.3" cy="502.1" r="0.8"/><circle cx="9.8" cy="289" r="0.7"/><circle cx="8.7" cy="195.9" r="0.9"/><circle cx="28.6" cy="651.3" r="0.8"/><circle cx="35.9" cy="274.8" r="1.2"/><circle cx="30.9" cy="596.4" r="1.2"/><circle cx="22.9" cy="697.8" r="0.7"/><circle cx="19.2" cy="450.7" r="0.9"/><circle cx="2.3" cy="203.8" r="1"/><circle cx="14" cy="555.8" r="1.1"/><circle cx="21" cy="105" r="0.9"/><circle cx="35" cy="249.3" r="0.6"/><circle cx="29.2" cy="290.9" r="0.7"/><circle cx="19.5" cy="724.4" r="0.9"/><circle cx="8" cy="6.8" r="0.8"/><circle cx="10" cy="762.9" r="1.2"/><circle cx="37.1" cy="723.4" r="0.7"/><circle cx="24.1" cy="437" r="0.9"/><circle cx="20.6" cy="172.3" r="0.6"/><circle cx="2.1" cy="546.4" r="0.6"/><circle cx="32.8" cy="513.7" r="0.7"/><circle cx="34" cy="73" r="0.7"/><circle cx="30.2" cy="293.4" r="1.2"/><circle cx="18.7" cy="0.1" r="1.2"/><circle cx="37.5" cy="358.1" r="1.1"/><circle cx="26" cy="80.4" r="1.1"/><circle cx="18.7" cy="549.8" r="0.5"/><circle cx="15.5" cy="156.2" r="1.2"/><circle cx="1.1" cy="20" r="0.8"/><circle cx="31" cy="152.9" r="1"/><circle cx="29.9" cy="471.7" r="0.8"/><circle cx="28.3" cy="94.4" r="0.7"/><circle cx="32.9" cy="170.3" r="0.7"/><circle cx="5.2" cy="422.7" r="1.2"/><circle cx="5.5" cy="406.8" r="1.2"/><circle cx="3.3" cy="383.4" r="0.7"/><circle cx="20.9" cy="746" r="1.2"/><circle cx="12.6" cy="160" r="0.9"/><circle cx="35.8" cy="191.4" r="0.7"/><circle cx="14.7" cy="743.2" r="0.7"/><circle cx="7.3" cy="318" r="0.9"/><circle cx="8.5" cy="613.3" r="0.7"/><circle cx="20.1" cy="283.3" r="1.2"/><circle cx="37.1" cy="368.2" r="0.8"/><circle cx="15.4" cy="496.9" r="0.6"/><circle cx="1.5" cy="210" r="1"/><circle cx="13.1" cy="129.1" r="0.9"/><circle cx="36.5" cy="747.1" r="0.7"/><circle cx="7.3" cy="540.8" r="1.2"/><circle cx="4.7" cy="408.6" r="0.8"/><circle cx="36.6" cy="749.7" r="0.7"/><circle cx="36.4" cy="114.1" r="0.7"/><circle cx="35.7" cy="273" r="1.3"/><circle cx="22" cy="499.1" r="1.2"/><circle cx="27.9" cy="256.6" r="0.8"/><circle cx="13.3" cy="188.9" r="0.8"/><circle cx="26.7" cy="341.7" r="0.5"/><circle cx="30.9" cy="526.5" r="1.2"/><circle cx="21.8" cy="270.4" r="1.1"/><circle cx="26.6" cy="458.5" r="1"/><circle cx="14" cy="220.8" r="0.5"/><circle cx="16.5" cy="403.9" r="0.6"/><circle cx="35.4" cy="724.2" r="0.8"/><circle cx="5.1" cy="62.6" r="1"/><circle cx="28.9" cy="340.1" r="1.1"/><circle cx="9.4" cy="259.9" r="0.7"/><circle cx="0.8" cy="5.7" r="1.2"/><circle cx="5.4" cy="71.5" r="1"/><circle cx="12.5" cy="427.7" r="0.5"/><circle cx="2.2" cy="380.1" r="1.3"/><circle cx="6.6" cy="113.8" r="1.1"/><circle cx="28.6" cy="406.5" r="1.1"/><circle cx="12.8" cy="180.8" r="0.8"/><circle cx="10.6" cy="453.1" r="1.1"/><circle cx="30.7" cy="524.5" r="0.7"/><circle cx="27" cy="195.6" r="0.6"/><circle cx="16.2" cy="787.3" r="0.8"/><circle cx="25.8" cy="616.9" r="1"/><circle cx="6.5" cy="517.5" r="1"/><circle cx="10.3" cy="95.7" r="0.5"/><circle cx="26.4" cy="725.9" r="0.5"/><circle cx="15.6" cy="627.8" r="1.2"/><circle cx="29.2" cy="125.5" r="0.8"/><circle cx="2.1" cy="306.5" r="1.1"/><circle cx="9.9" cy="770.3" r="0.7"/><circle cx="18.7" cy="431.8" r="0.9"/><circle cx="27.1" cy="402.6" r="1.1"/><circle cx="22.1" cy="757.8" r="1.2"/><circle cx="33.8" cy="746.1" r="1"/><circle cx="16.1" cy="142.4" r="0.7"/><circle cx="18.9" cy="601.5" r="0.9"/><circle cx="2" cy="227.6" r="0.9"/><circle cx="6.7" cy="703.6" r="0.7"/><circle cx="4.3" cy="724.2" r="0.9"/><circle cx="7.9" cy="73.5" r="0.9"/><circle cx="35.6" cy="249.7" r="0.8"/><circle cx="32.7" cy="512" r="1.1"/><circle cx="0.1" cy="681.3" r="1.3"/><circle cx="14.6" cy="641.3" r="0.6"/><circle cx="9.6" cy="728.9" r="0.7"/><circle cx="14.4" cy="251.7" r="0.6"/><circle cx="34.4" cy="66.3" r="1.3"/><circle cx="12.9" cy="524.5" r="0.8"/><circle cx="11.1" cy="474.2" r="1.2"/><circle cx="24.1" cy="165" r="1"/><circle cx="25.8" cy="778.2" r="0.8"/><circle cx="34.4" cy="365.2" r="1.3"/><circle cx="21" cy="218.2" r="1.1"/><circle cx="35" cy="115.9" r="0.8"/><circle cx="18.1" cy="729.1" r="1.2"/><circle cx="9.4" cy="510.6" r="1"/><circle cx="21.7" cy="42.1" r="0.8"/><circle cx="34.5" cy="215.2" r="1.3"/><circle cx="29.2" cy="314.6" r="0.7"/><circle cx="7.8" cy="690.5" r="1"/><circle cx="31.4" cy="332.7" r="1"/><circle cx="12.4" cy="127.6" r="0.9"/><circle cx="7.7" cy="376.2" r="0.8"/><circle cx="6.6" cy="242.5" r="1.2"/><circle cx="33.3" cy="687.2" r="0.7"/><circle cx="17.4" cy="530.2" r="0.6"/><circle cx="14.8" cy="608.2" r="1.3"/><circle cx="5.6" cy="400.5" r="0.9"/><circle cx="16.5" cy="695.3" r="0.5"/><circle cx="31.2" cy="659.7" r="0.8"/><circle cx="28.6" cy="613.4" r="0.8"/><circle cx="4.8" cy="179.6" r="1.3"/><circle cx="13.5" cy="619.4" r="0.6"/><circle cx="30.4" cy="216.1" r="1"/><circle cx="11" cy="579.9" r="1.1"/><circle cx="37.4" cy="159.8" r="0.8"/><circle cx="34" cy="500" r="0.6"/><circle cx="3.8" cy="470.1" r="0.6"/><circle cx="2.8" cy="358.8" r="0.7"/><circle cx="18" cy="383" r="1"/><circle cx="6.1" cy="736.2" r="0.6"/><circle cx="30.4" cy="325.9" r="0.9"/><circle cx="22.1" cy="313.9" r="0.9"/><circle cx="37" cy="473.1" r="0.9"/><circle cx="9.9" cy="747.2" r="1"/><circle cx="11.6" cy="764" r="0.7"/><circle cx="36" cy="751.6" r="1.1"/><circle cx="10.9" cy="750.9" r="0.9"/><circle cx="0.4" cy="369.2" r="0.9"/><circle cx="15.7" cy="6.1" r="0.6"/><circle cx="8.5" cy="263.4" r="0.7"/><circle cx="22.5" cy="627.7" r="1"/><circle cx="19.5" cy="180.5" r="0.8"/><circle cx="12" cy="317.3" r="0.7"/><circle cx="37" cy="428.2" r="0.8"/><circle cx="34.9" cy="470.6" r="1.2"/><circle cx="30.8" cy="762.4" r="0.7"/><circle cx="25.4" cy="706.5" r="0.8"/><circle cx="33.9" cy="54.3" r="0.6"/><circle cx="8" cy="264.4" r="0.7"/><circle cx="9.9" cy="420.6" r="1"/><circle cx="11.7" cy="708.5" r="1.3"/><circle cx="37" cy="572.7" r="1.3"/><circle cx="28.3" cy="737.8" r="0.9"/><circle cx="37" cy="15.2" r="1.1"/><circle cx="26.1" cy="94.6" r="1.2"/><circle cx="14.1" cy="720.9" r="1.3"/><circle cx="16.7" cy="781.8" r="0.6"/><circle cx="8.3" cy="686.5" r="1.1"/><circle cx="11.5" cy="328.1" r="0.8"/><circle cx="27.4" cy="29.6" r="1"/><circle cx="25.9" cy="575.1" r="0.6"/><circle cx="9.3" cy="188.1" r="0.8"/><circle cx="25.7" cy="416.9" r="0.8"/><circle cx="16" cy="661.9" r="1.3"/><circle cx="28.6" cy="230.3" r="0.5"/><circle cx="23.6" cy="12.5" r="0.7"/><circle cx="4.3" cy="184.7" r="0.5"/><circle cx="2.3" cy="758.1" r="1.2"/><circle cx="0.6" cy="684.8" r="1.1"/><circle cx="36" cy="629.9" r="0.7"/><circle cx="27.7" cy="69" r="1"/><circle cx="30.9" cy="55" r="0.6"/><circle cx="35.5" cy="354" r="0.7"/><circle cx="14.5" cy="269.2" r="0.5"/><circle cx="31.4" cy="328.1" r="1"/><circle cx="25.3" cy="750.2" r="1.2"/><circle cx="21" cy="292.8" r="1"/><circle cx="8.4" cy="128.2" r="1.1"/><circle cx="36.2" cy="704" r="0.6"/><circle cx="27.5" cy="561.2" r="1.1"/><circle cx="15.1" cy="380.5" r="1.1"/><circle cx="5.3" cy="285.9" r="0.6"/><circle cx="23.7" cy="229.7" r="1.1"/><circle cx="1.3" cy="716.2" r="0.9"/><circle cx="16" cy="60.1" r="0.7"/><circle cx="26" cy="320.3" r="0.6"/><circle cx="4.2" cy="446.6" r="0.5"/><circle cx="21.4" cy="367.5" r="1"/><circle cx="4.1" cy="659.8" r="0.6"/><circle cx="27.2" cy="556" r="1.2"/><circle cx="22.8" cy="496.8" r="1"/><circle cx="13" cy="193.9" r="0.7"/><circle cx="32.5" cy="421" r="0.9"/><circle cx="36.9" cy="608.6" r="1"/><circle cx="25.5" cy="150.7" r="0.9"/><circle cx="7.7" cy="620.4" r="1"/><circle cx="9.1" cy="307.3" r="1.3"/><circle cx="33.2" cy="393.8" r="0.7"/><circle cx="8.5" cy="417.7" r="0.6"/><circle cx="21.1" cy="403.8" r="0.9"/><circle cx="30.1" cy="298.5" r="1.1"/><circle cx="9.3" cy="612.7" r="0.9"/><circle cx="16.4" cy="300.5" r="0.6"/><circle cx="13.1" cy="15.2" r="0.8"/><circle cx="22.2" cy="134.9" r="0.8"/><circle cx="9.5" cy="110" r="1.3"/><circle cx="36.9" cy="589.9" r="0.7"/><circle cx="36.7" cy="165.4" r="0.8"/><circle cx="1.1" cy="784" r="0.6"/><circle cx="33.6" cy="637.2" r="1.1"/><circle cx="33.9" cy="757.5" r="0.9"/><circle cx="26.4" cy="387.7" r="0.8"/><circle cx="14.2" cy="683.4" r="1.2"/><circle cx="11.5" cy="331" r="0.9"/><circle cx="15.1" cy="140.1" r="1"/><circle cx="0.4" cy="790" r="0.7"/><circle cx="25.3" cy="384.9" r="1.1"/><circle cx="32" cy="65.4" r="0.6"/><circle cx="35.9" cy="706.5" r="0.7"/><circle cx="24.2" cy="145.6" r="0.9"/><circle cx="19.1" cy="577.4" r="0.8"/><circle cx="19.7" cy="284" r="0.7"/><circle cx="2.3" cy="285" r="1.1"/><circle cx="35.5" cy="287.7" r="1.2"/><circle cx="5" cy="516.7" r="1.2"/><circle cx="31" cy="760.7" r="1.2"/><circle cx="31.8" cy="139.1" r="1"/><circle cx="29.6" cy="586.1" r="1"/><circle cx="33.9" cy="152.3" r="1.3"/><circle cx="35.8" cy="277.6" r="0.8"/><circle cx="23.9" cy="411.1" r="0.5"/><circle cx="31.9" cy="288.1" r="1"/><circle cx="28.1" cy="775" r="0.8"/><circle cx="23.5" cy="728.7" r="1.1"/><circle cx="11.4" cy="195" r="1"/><circle cx="24.4" cy="324.4" r="1.1"/><circle cx="2.9" cy="453.8" r="1.2"/><circle cx="16.5" cy="219.4" r="1.1"/><circle cx="20.3" cy="168.1" r="0.7"/><circle cx="10.6" cy="123.4" r="0.6"/><circle cx="12" cy="768.8" r="0.9"/><circle cx="1" cy="478.7" r="1.1"/><circle cx="20.6" cy="331.1" r="0.5"/><circle cx="28.2" cy="229.6" r="1.1"/><circle cx="27" cy="513.3" r="1"/><circle cx="26.5" cy="60.2" r="0.9"/><circle cx="36.2" cy="152.1" r="1.1"/><circle cx="1.4" cy="83.1" r="0.5"/><circle cx="33.9" cy="518.9" r="1.2"/><circle cx="16.5" cy="383.7" r="1.3"/><circle cx="23.1" cy="335.8" r="0.8"/><circle cx="2.8" cy="428.1" r="0.6"/><circle cx="19.5" cy="389.5" r="0.9"/><circle cx="26.6" cy="145.9" r="0.6"/><circle cx="12.9" cy="674.2" r="0.8"/><circle cx="27.2" cy="193.3" r="1.2"/><circle cx="20" cy="534.9" r="1"/><circle cx="6.5" cy="198" r="0.6"/><circle cx="23.9" cy="66" r="0.9"/><circle cx="34.5" cy="608.3" r="0.6"/><circle cx="17.2" cy="684.2" r="1.3"/><circle cx="37.4" cy="227.1" r="1"/><circle cx="33.9" cy="75.4" r="0.8"/><circle cx="24.7" cy="506" r="0.9"/><circle cx="20" cy="11.3" r="0.5"/><circle cx="7.9" cy="35.7" r="0.8"/><circle cx="34.6" cy="732.8" r="0.7"/><circle cx="10.6" cy="447.4" r="1.1"/><circle cx="16.5" cy="129.6" r="0.9"/><circle cx="29.9" cy="189.4" r="1.1"/><circle cx="16.5" cy="791.1" r="0.6"/><circle cx="14.8" cy="173.1" r="0.6"/><circle cx="23.8" cy="332.1" r="1.2"/><circle cx="31.1" cy="9.5" r="0.9"/><circle cx="28" cy="101.3" r="1.1"/><circle cx="10" cy="559.8" r="1"/><circle cx="0.1" cy="573" r="0.6"/><circle cx="34.4" cy="531.9" r="0.5"/><circle cx="1.2" cy="195.3" r="1"/><circle cx="9.2" cy="587.2" r="0.5"/><circle cx="12" cy="100.6" r="1.1"/><circle cx="29.6" cy="193.8" r="0.9"/><circle cx="27.1" cy="643.5" r="0.6"/><circle cx="7.4" cy="383.9" r="0.5"/><circle cx="30.3" cy="135.9" r="1"/><circle cx="17.9" cy="575" r="0.7"/><circle cx="19" cy="16.4" r="1.2"/><circle cx="26.6" cy="56.9" r="0.9"/><circle cx="16.9" cy="432.8" r="0.8"/><circle cx="11.1" cy="224.6" r="1.2"/><circle cx="26.2" cy="618.9" r="0.9"/><circle cx="36.2" cy="415.8" r="1.2"/><circle cx="22.3" cy="457" r="1"/><circle cx="31.9" cy="366.9" r="0.5"/><circle cx="32.1" cy="51.8" r="0.8"/><circle cx="14.8" cy="486.7" r="0.6"/><circle cx="23.2" cy="576.8" r="1.2"/><circle cx="13.4" cy="372.4" r="1.1"/><circle cx="15.6" cy="317.8" r="0.6"/><circle cx="29.8" cy="78.2" r="0.9"/><circle cx="4.6" cy="525.7" r="0.7"/><circle cx="25.5" cy="71.3" r="1.2"/><circle cx="11.4" cy="548.4" r="1.3"/><circle cx="15.3" cy="728.9" r="1.2"/><circle cx="20.2" cy="520.8" r="1.1"/><circle cx="35.8" cy="338.1" r="0.6"/><circle cx="12.5" cy="437.1" r="1.2"/><circle cx="1.3" cy="212.1" r="1.3"/><circle cx="28.1" cy="241.9" r="0.8"/><circle cx="6" cy="529.7" r="1.2"/><circle cx="35.8" cy="200.2" r="0.7"/><circle cx="8.3" cy="658.6" r="0.7"/><circle cx="13.9" cy="73.1" r="1.2"/><circle cx="33.2" cy="524.6" r="0.7"/><circle cx="26.7" cy="567.6" r="1.2"/><circle cx="8.8" cy="504.6" r="1"/><circle cx="14.2" cy="337.2" r="0.7"/><circle cx="10.1" cy="730.7" r="0.5"/><circle cx="36.1" cy="139.4" r="1.1"/><circle cx="5.1" cy="260.1" r="0.5"/><circle cx="34" cy="34.7" r="1.3"/><circle cx="7.3" cy="78.1" r="0.6"/><circle cx="26.2" cy="5.5" r="1"/><circle cx="23.6" cy="767.9" r="1"/><circle cx="25.5" cy="136.8" r="1"/><circle cx="21.2" cy="381.9" r="0.7"/><circle cx="15" cy="51.2" r="1.2"/><circle cx="27.4" cy="45.8" r="1.3"/><circle cx="20.1" cy="613.4" r="0.7"/><circle cx="7.3" cy="35.6" r="1"/><circle cx="26.6" cy="147.4" r="0.7"/><circle cx="20.3" cy="301.6" r="0.9"/><circle cx="35.2" cy="57" r="0.5"/><circle cx="26.6" cy="461.4" r="0.6"/><circle cx="18.8" cy="481.3" r="0.8"/><circle cx="14.6" cy="749" r="0.5"/><circle cx="12.5" cy="641" r="0.6"/><circle cx="31.5" cy="626" r="0.6"/><circle cx="5.4" cy="606.9" r="0.5"/><circle cx="32.5" cy="628.4" r="1.1"/><circle cx="3.2" cy="641.2" r="0.7"/><circle cx="33.2" cy="615.9" r="0.5"/><circle cx="17.6" cy="720.2" r="0.6"/><circle cx="14.2" cy="508.2" r="1.2"/><circle cx="32.1" cy="164.3" r="0.7"/><circle cx="14.2" cy="178" r="1.1"/><circle cx="29.7" cy="411.5" r="0.7"/><circle cx="3.8" cy="422.7" r="0.9"/><circle cx="11.6" cy="277.6" r="1"/><circle cx="23" cy="81.5" r="0.6"/><circle cx="14.5" cy="650.5" r="1"/><circle cx="27.7" cy="691.7" r="0.7"/><circle cx="37.6" cy="528.2" r="0.9"/><circle cx="26.3" cy="466.7" r="1.2"/><circle cx="0.2" cy="310.8" r="0.8"/><circle cx="31.6" cy="501.9" r="1"/><circle cx="11.3" cy="161.5" r="1.3"/><circle cx="0.9" cy="373.3" r="1.2"/><circle cx="7.2" cy="6.8" r="0.5"/><circle cx="3.8" cy="264" r="0.6"/><circle cx="20.4" cy="128.1" r="0.6"/><circle cx="4.5" cy="425.2" r="1.2"/><circle cx="31.6" cy="18.1" r="0.6"/><circle cx="6.9" cy="317.2" r="0.6"/><circle cx="3.2" cy="743.7" r="0.9"/><circle cx="21.5" cy="202.3" r="0.5"/><circle cx="27.6" cy="472" r="1.1"/><circle cx="28.5" cy="220.9" r="1.2"/><circle cx="34.9" cy="395.8" r="0.8"/><circle cx="23.8" cy="475.4" r="1"/><circle cx="9.1" cy="111.4" r="1.1"/><circle cx="20" cy="645.7" r="1.1"/><circle cx="28.1" cy="388.9" r="0.7"/><circle cx="37.6" cy="779" r="1.2"/><circle cx="28.4" cy="636" r="0.7"/><circle cx="15.9" cy="462.1" r="0.8"/><circle cx="18.4" cy="721.6" r="1"/><circle cx="33.8" cy="680" r="0.9"/><circle cx="14.2" cy="680.8" r="1.2"/><circle cx="35.1" cy="540.2" r="1.3"/><circle cx="15.2" cy="716.3" r="1"/><circle cx="26.9" cy="206.9" r="0.8"/><circle cx="13.5" cy="427" r="0.9"/><circle cx="0.3" cy="149.9" r="1.1"/><circle cx="10.2" cy="426.6" r="0.7"/><circle cx="30" cy="227.7" r="1.1"/><circle cx="9.2" cy="482.3" r="0.8"/><circle cx="24.4" cy="771.6" r="1"/><circle cx="36.1" cy="671.6" r="0.7"/><circle cx="15.9" cy="746.2" r="1.1"/><circle cx="13.8" cy="123.2" r="1"/><circle cx="36.4" cy="645.3" r="1.1"/><circle cx="6.4" cy="764.5" r="1.2"/><circle cx="7.2" cy="600.2" r="0.7"/><circle cx="23.6" cy="394.2" r="0.7"/><circle cx="22.4" cy="106.2" r="1.2"/><circle cx="10.1" cy="786.6" r="0.8"/><circle cx="17.8" cy="210.1" r="1.1"/><circle cx="25.8" cy="467.9" r="1"/><circle cx="20.5" cy="80.1" r="1.3"/><circle cx="10.5" cy="253.5" r="0.6"/><circle cx="35.7" cy="537.3" r="1.1"/><circle cx="10.1" cy="425.6" r="0.6"/><circle cx="4.3" cy="65.6" r="0.5"/><circle cx="26.7" cy="215.1" r="0.9"/><circle cx="24.3" cy="400.8" r="0.9"/><circle cx="16.1" cy="57" r="0.9"/><circle cx="10.9" cy="251.9" r="1"/><circle cx="1.2" cy="326.1" r="0.8"/><circle cx="4.2" cy="428.5" r="1"/><circle cx="30.5" cy="353.5" r="0.7"/><circle cx="12" cy="207.4" r="0.7"/><circle cx="21.4" cy="775" r="0.7"/><circle cx="22.3" cy="259.2" r="0.7"/><circle cx="25.9" cy="563.6" r="1.2"/><circle cx="7.5" cy="713" r="1.2"/><circle cx="30.7" cy="171.4" r="0.9"/><circle cx="25.4" cy="575" r="0.8"/><circle cx="8.6" cy="250.7" r="0.6"/><circle cx="27.5" cy="153.6" r="0.6"/><circle cx="4.3" cy="692.8" r="1.3"/><circle cx="24.9" cy="262.2" r="0.7"/><circle cx="30.4" cy="750.1" r="0.9"/><circle cx="4.5" cy="616.1" r="1.3"/><circle cx="23.3" cy="602.3" r="0.7"/><circle cx="29.1" cy="259.6" r="0.6"/><circle cx="0.3" cy="392.1" r="1"/><circle cx="26" cy="525.8" r="0.9"/><circle cx="38" cy="507.5" r="1.2"/><circle cx="0.6" cy="401.1" r="1.3"/><circle cx="35.7" cy="159.2" r="0.5"/><circle cx="36.9" cy="295.7" r="0.7"/><circle cx="28" cy="447.8" r="0.9"/><circle cx="34.6" cy="475" r="1"/><circle cx="37.9" cy="80.5" r="1.2"/><circle cx="25.6" cy="479.8" r="1.1"/><circle cx="12.3" cy="691.6" r="0.9"/><circle cx="35.8" cy="111.4" r="1"/><circle cx="15.9" cy="602.7" r="0.7"/><circle cx="9.7" cy="789.2" r="0.8"/><circle cx="4.4" cy="624.3" r="1.1"/><circle cx="0.4" cy="342.8" r="0.7"/><circle cx="28.5" cy="761.2" r="1.2"/><circle cx="1.7" cy="70.5" r="0.7"/><circle cx="3.2" cy="328.6" r="1.1"/><circle cx="21.5" cy="585.1" r="0.9"/><circle cx="17.7" cy="253.3" r="1.1"/><circle cx="25.3" cy="270.8" r="0.6"/><circle cx="31.2" cy="53" r="0.6"/><circle cx="23.2" cy="510.1" r="1.1"/><circle cx="14" cy="299.3" r="1.2"/><circle cx="17.7" cy="748.6" r="1.1"/><circle cx="36.9" cy="324.5" r="0.9"/><circle cx="20" cy="325.4" r="0.7"/><circle cx="9.4" cy="696.7" r="0.6"/><circle cx="29.8" cy="400.2" r="0.7"/><circle cx="23.6" cy="336.2" r="0.9"/><circle cx="5.6" cy="788.2" r="0.9"/><circle cx="37.8" cy="351.3" r="1.3"/><circle cx="13.7" cy="543" r="0.7"/><circle cx="25.4" cy="410.7" r="1.3"/><circle cx="21.9" cy="320.2" r="0.8"/><circle cx="28.9" cy="765.9" r="0.7"/><circle cx="1.4" cy="538.9" r="0.6"/><circle cx="27.6" cy="370.8" r="0.8"/><circle cx="18.3" cy="424.6" r="0.7"/><circle cx="10.5" cy="781.4" r="0.8"/><circle cx="30.6" cy="470.4" r="0.8"/><circle cx="14.4" cy="87.5" r="1.1"/><circle cx="15.4" cy="136.5" r="1.2"/><circle cx="34.4" cy="272.1" r="1.2"/><circle cx="30.2" cy="650.4" r="0.8"/><circle cx="28.2" cy="604.1" r="0.8"/><circle cx="9.8" cy="475.2" r="0.8"/><circle cx="16.9" cy="537.3" r="1.2"/><circle cx="8.8" cy="297.5" r="0.8"/><circle cx="4" cy="690.1" r="1.2"/><circle cx="7.9" cy="694.2" r="1.1"/><circle cx="31.6" cy="372.1" r="0.6"/><circle cx="3.3" cy="409" r="0.7"/><circle cx="1.4" cy="150.9" r="0.5"/><circle cx="35.6" cy="165.7" r="1.1"/><circle cx="0" cy="615.8" r="1.1"/><circle cx="27.7" cy="347.4" r="1"/><circle cx="3.5" cy="92.9" r="0.6"/><circle cx="26" cy="473.3" r="0.5"/><circle cx="37.4" cy="410.4" r="1.2"/><circle cx="13.6" cy="530.9" r="0.8"/><circle cx="23.1" cy="672.8" r="1.1"/><circle cx="7.4" cy="494.8" r="0.7"/><circle cx="8.8" cy="625.7" r="0.8"/><circle cx="6.9" cy="180.2" r="0.7"/><circle cx="30.6" cy="132.9" r="1.3"/><circle cx="13.2" cy="489.1" r="1.2"/><circle cx="22.5" cy="513.5" r="1"/></g> | |
| 12 | <rect x="30" width="8" height="792" fill="#b4c1b0"/> | |
| 13 | </g> | |
| 14 | <g mask="url(#art)"> | |
| 15 | <rect x="45" width="567" height="792" fill="#ececec"/> | |
| 16 | <g filter="url(#soft)" opacity=".5"> | |
| 17 | <path d="M150 -20L420 250L400 270L130 0Z" fill="#dcdcdc"/> | |
| 18 | <g transform="translate(110 250) rotate(-60)"><ellipse rx="70" ry="56.0" fill="url(#bowl)"/><rect x="56.0" y="-11" width="240" height="22" rx="11" fill="url(#handle)"/></g><g transform="translate(260 330) rotate(-50)"><ellipse rx="58" ry="46.400000000000006" fill="url(#bowl)"/><rect x="46.400000000000006" y="-11" width="220" height="22" rx="11" fill="url(#handle)"/></g><g transform="translate(360 470) rotate(-40)"><ellipse rx="46" ry="36.800000000000004" fill="url(#bowl)"/><rect x="36.800000000000004" y="-11" width="200" height="22" rx="11" fill="url(#handle)"/></g><g transform="translate(150 560) rotate(-70)"><ellipse rx="64" ry="51.2" fill="url(#bowl)"/><rect x="51.2" y="-11" width="160" height="22" rx="11" fill="url(#handle)"/></g> | |
| 19 | </g> | |
| 20 | </g> | |
| 21 | </svg> |
crates/canvas/assets/backgrounds/red-and-black-margin.svg created+16| ... | ... | @@ -0,0 +1,16 @@ |
| 1 | <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 17 608" preserveAspectRatio="none"> | |
| 2 | <defs> | |
| 3 | <linearGradient id="red" x2="0" y1="1" y2="45" gradientUnits="userSpaceOnUse"> | |
| 4 | <stop offset="0" stop-color="#f7d8db"/><stop offset=".45" stop-color="#e02433"/><stop offset=".85" stop-color="#eb868c"/><stop offset="1" stop-color="#f6d6d8"/> | |
| 5 | </linearGradient> | |
| 6 | <linearGradient id="black" x2="0" y1="64" y2="606" gradientUnits="userSpaceOnUse"> | |
| 7 | <stop offset="0" stop-color="#a3a3a3"/><stop offset=".06" stop-color="#080808"/><stop offset=".12" stop-color="#8a8a8a"/><stop offset=".14" stop-color="#909090"/><stop offset=".6" stop-color="#b8b8b8"/><stop offset=".9" stop-color="#ececec"/><stop offset="1" stop-color="#ececec" stop-opacity="0"/> | |
| 8 | </linearGradient> | |
| 9 | </defs> | |
| 10 | <rect width="7" y="1" height="44" fill="url(#red)"/> | |
| 11 | <rect x="9" y="1" width="1.6" height="44" fill="url(#red)"/> | |
| 12 | <rect x="13" y="1" width="1" height="44" fill="url(#red)"/> | |
| 13 | <rect width="7" y="64" height="542" fill="url(#black)"/> | |
| 14 | <rect x="9" y="64" width="1.6" height="542" fill="url(#black)"/> | |
| 15 | <rect x="13" y="64" width="1" height="542" fill="url(#black)"/> | |
| 16 | </svg> |
crates/canvas/assets/backgrounds/side-stripes.svg created+11| ... | ... | @@ -0,0 +1,11 @@ |
| 1 | <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 38 792" preserveAspectRatio="none"> | |
| 2 | <defs> | |
| 3 | <linearGradient id="shade" x2="1"><stop offset=".65" stop-color="#c5cfe1"/><stop offset="1" stop-color="#7e8eb2"/></linearGradient> | |
| 4 | <linearGradient id="block-fade" x2="0" y2="1"><stop offset=".48" stop-color="white"/><stop offset=".76" stop-color="white" stop-opacity="0"/></linearGradient> | |
| 5 | <linearGradient id="line-fade" x2="0" y2="1"><stop offset=".42" stop-color="white"/><stop offset=".61" stop-color="white" stop-opacity="0"/></linearGradient> | |
| 6 | <mask id="block"><rect width="38" height="792" fill="url(#block-fade)"/></mask> | |
| 7 | <mask id="lines"><rect width="38" height="792" fill="url(#line-fade)"/></mask> | |
| 8 | </defs> | |
| 9 | <rect width="26" height="792" fill="url(#shade)" mask="url(#block)"/> | |
| 10 | <path d="M0 4.50H26M0 13.55H26M0 22.60H26M0 31.65H26M0 40.70H26M0 49.75H26M0 58.80H26M0 67.85H26M0 76.90H26M0 85.95H26M0 95.00H26M0 104.05H26M0 113.10H26M0 122.15H26M0 131.20H26M0 140.25H26M0 149.30H26M0 158.35H26M0 167.40H26M0 176.45H26M0 185.50H26M0 194.55H26M0 203.60H26M0 212.65H26M0 221.70H26M0 230.75H26M0 239.80H26M0 248.85H26M0 257.90H26M0 266.95H26M0 276.00H26M0 285.05H26M0 294.10H26M0 303.15H26M0 312.20H26M0 321.25H26M0 330.30H26M0 339.35H26M0 348.40H26M0 357.45H26M0 366.50H26M0 375.55H26M0 384.60H26M0 393.65H26M0 402.70H26M0 411.75H26M0 420.80H26M0 429.85H26M0 438.90H26M0 447.95H26M0 457.00H26M0 466.05H26M0 475.10H26" stroke="#203e72" mask="url(#lines)"/> | |
| 11 | </svg> |
crates/canvas/assets/backgrounds/small-push-pins.svg created+22| ... | ... | @@ -0,0 +1,22 @@ |
| 1 | <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 612 792" preserveAspectRatio="none"> | |
| 2 | <defs> | |
| 3 | <linearGradient id="fx0" x2="612" gradientUnits="userSpaceOnUse"><stop offset="0.490" stop-color="white"/><stop offset="0.980" stop-color="white" stop-opacity="0"/></linearGradient> | |
| 4 | <linearGradient id="fy0" x2="0" y2="792" gradientUnits="userSpaceOnUse"><stop offset="0.606" stop-color="white"/><stop offset="0.985" stop-color="white" stop-opacity="0"/></linearGradient> | |
| 5 | <mask id="across"><rect width="612" height="792" fill="url(#fx0)"/></mask> | |
| 6 | <mask id="down"><rect width="612" height="792" fill="url(#fy0)"/></mask> | |
| 7 | <filter id="blur" x="-30%" y="-30%" width="160%" height="160%"><feGaussianBlur stdDeviation="3"/></filter> | |
| 8 | <g id="pin"> | |
| 9 | <path d="M-7-13C-7-17 7-17 7-13L5-3C11 0 12 5 10 7H-10C-12 5-11 0-5-3Z"/> | |
| 10 | <path d="M-.8 7L0 26L.8 7Z" fill="#8f9fc2"/> | |
| 11 | </g> | |
| 12 | </defs> | |
| 13 | <rect x="30" y="33" width="2" height="759" fill="#a2b8e7" mask="url(#down)"/> | |
| 14 | <rect x="30" y="33" width="582" height="2" fill="#a2b8e7" mask="url(#across)"/> | |
| 15 | <g opacity=".75"> | |
| 16 | <ellipse cx="44" cy="52" rx="30" ry="14" fill="#c9d4ef" filter="url(#blur)"/> | |
| 17 | <ellipse cx="94" cy="72" rx="26" ry="9" fill="#d7dff3" filter="url(#blur)"/> | |
| 18 | <use href="#pin" fill="#9fb2e3" transform="translate(52 22) rotate(-40) scale(1.2)"/> | |
| 19 | <use href="#pin" fill="#aebfe8" transform="translate(18 55) rotate(-15) scale(1.1)" filter="url(#blur)"/> | |
| 20 | <use href="#pin" fill="#b9c8ec" transform="translate(80 60) rotate(-60)" opacity=".7"/> | |
| 21 | </g> | |
| 22 | </svg> |
crates/canvas/assets/backgrounds/sparks.svg created+43| ... | ... | @@ -0,0 +1,43 @@ |
| 1 | <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 612 792" preserveAspectRatio="none"> | |
| 2 | <defs> | |
| 3 | <g id="burst"><path d="M18.00 0.00L3.48 0.93L9.66 5.58L2.55 2.55L9.00 15.59L0.93 3.48L0.00 11.16L-0.93 3.48L-9.00 15.59L-2.55 2.55L-9.66 5.58L-3.48 0.93L-18.00 0.00L-3.48 -0.93L-9.66 -5.58L-2.55 -2.55L-9.00 -15.59L-0.93 -3.48L-0.00 -11.16L0.93 -3.48L9.00 -15.59L2.55 -2.55L9.66 -5.58L3.48 -0.93Z"/><circle r="3"/></g> | |
| 4 | <linearGradient id="wash" x2="1"><stop offset=".03" stop-color="#fde8b8"/><stop offset=".3" stop-color="#feefcc"/><stop offset=".62" stop-color="#feefcc" stop-opacity="0"/></linearGradient> | |
| 5 | <linearGradient id="down" x2="0" y2="1"><stop offset=".92" stop-color="white"/><stop offset="1" stop-color="white" stop-opacity="0"/></linearGradient> | |
| 6 | <mask id="fade"><rect width="612" height="792" fill="url(#down)"/></mask> | |
| 7 | <filter id="glow" x="-20%" y="-20%" width="140%" height="140%"><feGaussianBlur stdDeviation=".7"/></filter> | |
| 8 | </defs> | |
| 9 | <g mask="url(#fade)"> | |
| 10 | <rect x="22" width="590" height="792" fill="url(#wash)"/> | |
| 11 | <g fill="#fffcf4"> | |
| 12 | <path d="M225 190L391 190M225 190L366 253M225 190L310 284M225 190L291 394M225 190L213 300M225 190L140 338M225 190L49 318M225 190L108 215M225 190L52 153M225 190L76 82M225 190L168 90M225 190L209 35M225 190L285 5M225 190L335 68M225 190L405 110" stroke="#fffaee" stroke-width="1.6" opacity=".8"/> | |
| 13 | <path d="M226 200C222 300 196 380 190 430C186 470 182 520 160 540C150 600 120 680 104 745" fill="none" stroke="#fffaee" stroke-width="4" stroke-linecap="round" opacity=".75"/> | |
| 14 | <use href="#burst" transform="translate(30 48) scale(2)"/> | |
| 15 | <use href="#burst" transform="translate(95 90) scale(2.5)"/> | |
| 16 | <use href="#burst" transform="translate(152 140) scale(2)"/> | |
| 17 | <use href="#burst" transform="translate(182 62) scale(1.8)"/> | |
| 18 | <use href="#burst" transform="translate(225 190) scale(4.8)"/> | |
| 19 | <use href="#burst" transform="translate(85 220) scale(2.2)"/> | |
| 20 | <use href="#burst" transform="translate(60 350) scale(2)"/> | |
| 21 | <use href="#burst" transform="translate(105 342) scale(3)"/> | |
| 22 | <use href="#burst" transform="translate(132 372) scale(1.8)"/> | |
| 23 | <use href="#burst" transform="translate(268 40) scale(1.4)"/> | |
| 24 | <use href="#burst" transform="translate(40 470) scale(1.6)"/> | |
| 25 | <use href="#burst" transform="translate(120 520) scale(2.2)"/> | |
| 26 | <use href="#burst" transform="translate(95 575) scale(5.2)"/> | |
| 27 | <use href="#burst" transform="translate(240 520) scale(2.4)"/> | |
| 28 | <use href="#burst" transform="translate(60 650) scale(1.8)"/> | |
| 29 | <use href="#burst" transform="translate(170 640) scale(2)"/> | |
| 30 | <use href="#burst" transform="translate(250 420) scale(1.6)"/> | |
| 31 | <use href="#burst" transform="translate(200 300) scale(1.4)"/> | |
| 32 | <use href="#burst" transform="translate(150 20) scale(1.6)"/> | |
| 33 | </g> | |
| 34 | <g fill="#f5d48c"> | |
| 35 | <use href="#burst" transform="translate(6 128) scale(3.4)"/> | |
| 36 | <use href="#burst" transform="translate(12 282) scale(2.4)"/> | |
| 37 | <use href="#burst" transform="translate(4 60) scale(1.8)"/> | |
| 38 | <use href="#burst" transform="translate(6 500) scale(2.2)"/> | |
| 39 | <use href="#burst" transform="translate(10 420) scale(1.4)"/> | |
| 40 | </g> | |
| 41 | <rect x="18" width="4" height="792" fill="#fdcb4e"/> | |
| 42 | </g> | |
| 43 | </svg> |
crates/canvas/assets/backgrounds/stars.svg created+26| ... | ... | @@ -0,0 +1,26 @@ |
| 1 | <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 85 470" preserveAspectRatio="none"> | |
| 2 | <defs> | |
| 3 | <g id="star"><path d="M9.4 3.42L1.41 1.41L3.42 9.4L0 2L-3.42 9.4L-1.41 1.41L-9.4 3.42L-2 0L-9.4 -3.42L-1.41 -1.41L-3.42 -9.4L-0 -2L3.42 -9.4L1.41 -1.41L9.4 -3.42L2 -0Z"/><circle r="2.6"/></g> | |
| 4 | <radialGradient id="teal"><stop offset=".05" stop-color="#ffffff"/><stop offset=".35" stop-color="#a8cfd6"/><stop offset="1" stop-color="#5a9fac"/></radialGradient> | |
| 5 | <radialGradient id="orange"><stop offset=".05" stop-color="#ffffff"/><stop offset=".35" stop-color="#fad0b4"/><stop offset="1" stop-color="#f5a46e"/></radialGradient> | |
| 6 | </defs> | |
| 7 | <use href="#star" fill="url(#orange)" transform="translate(29.5 8.5) scale(0.85)"/> | |
| 8 | <use href="#star" fill="url(#teal)" transform="translate(33 51) scale(3.4)"/> | |
| 9 | <use href="#star" fill="url(#teal)" transform="translate(59 97) scale(1.3)"/> | |
| 10 | <use href="#star" fill="url(#orange)" transform="translate(20 105) scale(1.9)"/> | |
| 11 | <use href="#star" fill="url(#orange)" transform="translate(56.5 139.5) scale(2.55)"/> | |
| 12 | <use href="#star" fill="url(#teal)" transform="translate(21 162) scale(1.9)"/> | |
| 13 | <use href="#star" fill="url(#teal)" transform="translate(65 178) scale(0.9)"/> | |
| 14 | <use href="#star" fill="url(#orange)" transform="translate(28.5 200.5) scale(0.85)"/> | |
| 15 | <use href="#star" fill="url(#orange)" transform="translate(62 213) scale(1.6)"/> | |
| 16 | <use href="#star" fill="url(#teal)" transform="translate(35 238) scale(2)"/> | |
| 17 | <use href="#star" fill="url(#teal)" transform="translate(53 265) scale(0.9)"/> | |
| 18 | <use href="#star" fill="url(#orange)" transform="translate(28 274) scale(0.8)"/> | |
| 19 | <use href="#star" fill="url(#teal)" transform="translate(59.5 291.5) scale(0.95)"/> | |
| 20 | <use href="#star" fill="url(#teal)" transform="translate(24 307.5) scale(1.95)"/> | |
| 21 | <use href="#star" fill="url(#orange)" transform="translate(58.5 330) scale(2.6)"/> | |
| 22 | <use href="#star" fill="url(#orange)" transform="translate(23 365) scale(1.9)"/> | |
| 23 | <use href="#star" fill="url(#teal)" transform="translate(61 373) scale(1.3)"/> | |
| 24 | <use href="#star" fill="url(#teal)" transform="translate(35 418.5) scale(3.35)"/> | |
| 25 | <use href="#star" fill="url(#orange)" transform="translate(32 461) scale(0.8)"/> | |
| 26 | </svg> |
crates/canvas/assets/backgrounds/swirls.svg created+45| ... | ... | @@ -0,0 +1,45 @@ |
| 1 | <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 88 574" preserveAspectRatio="none"> | |
| 2 | <defs> | |
| 3 | <radialGradient id="core" cx=".5" cy=".45" r=".55"> | |
| 4 | <stop offset="0" stop-color="#fbfbfc"/><stop offset=".5" stop-color="#c3c9cf"/><stop offset="1" stop-color="#7c8996"/> | |
| 5 | </radialGradient> | |
| 6 | </defs> | |
| 7 | <circle cx="43.5" cy="41.5" r="20.5" fill="url(#core)"/> | |
| 8 | <g fill="#ffffff"><path d="M63.2 48.7C64.8 47.3 61.1 51.5 59.9 52.7C58.6 53.8 57.2 54.8 55.8 55.5C54.4 56.2 52.8 56.7 51.4 57.1C49.9 57.4 48.4 57.4 47 57.4C45.6 57.3 44.2 57 43 56.6C41.8 56.1 40.6 55.5 39.6 54.8C38.6 54.1 37.7 53.3 37 52.4C36.3 51.6 35.8 50.6 35.4 49.6C35 48.7 34.7 47.7 34.6 46.8C34.5 45.8 34.6 44.9 34.7 44.1C34.9 43.2 35.2 42.4 35.6 41.8C35.9 41.1 36.4 40.5 36.9 40C37.4 39.6 38 39.2 38.5 39C39.1 38.7 39.7 38.6 40.2 38.6C40.7 38.6 41.3 38.7 41.7 38.9C42.1 39 42.6 39.5 42.8 39.6C43 39.7 43 39.8 42.8 39.6C42.7 39.5 42.2 39 41.8 38.8C41.5 38.6 41 38.4 40.5 38.3C40.1 38.2 39.5 38.1 39 38.2C38.5 38.3 37.9 38.4 37.4 38.6C36.8 38.9 36.3 39.2 35.8 39.6C35.3 40 34.8 40.5 34.4 41.1C34 41.7 33.6 42.3 33.3 43.1C33 43.8 32.8 44.6 32.7 45.4C32.6 46.3 32.6 47.2 32.7 48.1C32.7 49 32.9 49.9 33.3 50.9C33.6 51.8 34 52.8 34.6 53.6C35.2 54.5 35.8 55.4 36.6 56.2C37.4 57 38.3 57.8 39.4 58.4C40.4 59.1 41.5 59.7 42.7 60.1C43.9 60.5 45.2 60.9 46.5 61.1C47.8 61.3 47.9 63.3 50.7 61.2C53.5 59.2 61.7 50.1 63.2 48.7Z"/><path d="M23.8 34.3C22.2 35.7 25.9 31.5 27.1 30.3C28.4 29.2 29.8 28.2 31.2 27.5C32.6 26.8 34.2 26.3 35.6 25.9C37.1 25.6 38.6 25.6 40 25.6C41.4 25.7 42.8 26 44 26.4C45.2 26.9 46.4 27.5 47.4 28.2C48.4 28.9 49.3 29.7 50 30.6C50.7 31.4 51.2 32.4 51.6 33.4C52 34.3 52.3 35.3 52.4 36.2C52.5 37.2 52.4 38.1 52.3 38.9C52.1 39.8 51.8 40.6 51.4 41.2C51.1 41.9 50.6 42.5 50.1 43C49.6 43.4 49 43.8 48.5 44C47.9 44.3 47.3 44.4 46.8 44.4C46.3 44.4 45.7 44.3 45.3 44.1C44.9 44 44.4 43.5 44.2 43.4C44 43.3 44 43.2 44.2 43.4C44.3 43.5 44.8 44 45.2 44.2C45.5 44.4 46 44.6 46.5 44.7C46.9 44.8 47.5 44.9 48 44.8C48.5 44.7 49.1 44.6 49.6 44.4C50.2 44.1 50.7 43.8 51.2 43.4C51.7 43 52.2 42.5 52.6 41.9C53 41.3 53.4 40.7 53.7 39.9C54 39.2 54.2 38.4 54.3 37.6C54.4 36.7 54.4 35.8 54.3 34.9C54.3 34 54.1 33.1 53.7 32.1C53.4 31.2 53 30.2 52.4 29.4C51.8 28.5 51.2 27.6 50.4 26.8C49.6 26 48.7 25.2 47.6 24.6C46.6 23.9 45.5 23.3 44.3 22.9C43.1 22.5 41.8 22.1 40.5 21.9C39.2 21.7 39.1 19.7 36.3 21.8C33.5 23.8 25.3 32.9 23.8 34.3Z"/></g> | |
| 9 | <circle cx="43.5" cy="123.1" r="20.5" fill="url(#core)"/> | |
| 10 | <g fill="#ffffff"><path d="M63.2 130.3C64.8 128.9 61.1 133.1 59.9 134.3C58.6 135.4 57.2 136.4 55.8 137.1C54.4 137.8 52.8 138.3 51.4 138.7C49.9 139 48.4 139 47 139C45.6 138.9 44.2 138.6 43 138.2C41.8 137.7 40.6 137.1 39.6 136.4C38.6 135.7 37.7 134.9 37 134C36.3 133.2 35.8 132.2 35.4 131.2C35 130.3 34.7 129.3 34.6 128.4C34.5 127.4 34.6 126.5 34.7 125.7C34.9 124.8 35.2 124 35.6 123.4C35.9 122.7 36.4 122.1 36.9 121.6C37.4 121.2 38 120.8 38.5 120.6C39.1 120.3 39.7 120.2 40.2 120.2C40.7 120.2 41.3 120.3 41.7 120.5C42.1 120.6 42.6 121.1 42.8 121.2C43 121.3 43 121.4 42.8 121.2C42.7 121.1 42.2 120.6 41.8 120.4C41.5 120.2 41 120 40.5 119.9C40.1 119.8 39.5 119.7 39 119.8C38.5 119.9 37.9 120 37.4 120.2C36.8 120.5 36.3 120.8 35.8 121.2C35.3 121.6 34.8 122.1 34.4 122.7C34 123.3 33.6 123.9 33.3 124.7C33 125.4 32.8 126.2 32.7 127C32.6 127.9 32.6 128.8 32.7 129.7C32.7 130.6 32.9 131.5 33.3 132.5C33.6 133.4 34 134.4 34.6 135.2C35.2 136.1 35.8 137 36.6 137.8C37.4 138.6 38.3 139.4 39.4 140C40.4 140.7 41.5 141.3 42.7 141.7C43.9 142.1 45.2 142.5 46.5 142.7C47.8 142.9 47.9 144.9 50.7 142.8C53.5 140.8 61.7 131.7 63.2 130.3Z"/><path d="M23.8 115.9C22.2 117.3 25.9 113.1 27.1 111.9C28.4 110.8 29.8 109.8 31.2 109.1C32.6 108.4 34.2 107.9 35.6 107.5C37.1 107.2 38.6 107.2 40 107.2C41.4 107.3 42.8 107.6 44 108C45.2 108.5 46.4 109.1 47.4 109.8C48.4 110.5 49.3 111.3 50 112.2C50.7 113 51.2 114 51.6 115C52 115.9 52.3 116.9 52.4 117.8C52.5 118.8 52.4 119.7 52.3 120.5C52.1 121.4 51.8 122.2 51.4 122.8C51.1 123.5 50.6 124.1 50.1 124.6C49.6 125 49 125.4 48.5 125.6C47.9 125.9 47.3 126 46.8 126C46.3 126 45.7 125.9 45.3 125.7C44.9 125.6 44.4 125.1 44.2 125C44 124.9 44 124.8 44.2 125C44.3 125.1 44.8 125.6 45.2 125.8C45.5 126 46 126.2 46.5 126.3C46.9 126.4 47.5 126.5 48 126.4C48.5 126.3 49.1 126.2 49.6 126C50.2 125.7 50.7 125.4 51.2 125C51.7 124.6 52.2 124.1 52.6 123.5C53 122.9 53.4 122.3 53.7 121.5C54 120.8 54.2 120 54.3 119.2C54.4 118.3 54.4 117.4 54.3 116.5C54.3 115.6 54.1 114.7 53.7 113.7C53.4 112.8 53 111.8 52.4 111C51.8 110.1 51.2 109.2 50.4 108.4C49.6 107.6 48.7 106.8 47.6 106.2C46.6 105.5 45.5 104.9 44.3 104.5C43.1 104.1 41.8 103.7 40.5 103.5C39.2 103.3 39.1 101.3 36.3 103.4C33.5 105.4 25.3 114.5 23.8 115.9Z"/></g> | |
| 11 | <circle cx="43.5" cy="204.7" r="20.5" fill="url(#core)"/> | |
| 12 | <g fill="#ffffff"><path d="M63.2 211.9C64.8 210.5 61.1 214.7 59.9 215.9C58.6 217 57.2 218 55.8 218.7C54.4 219.4 52.8 219.9 51.4 220.3C49.9 220.6 48.4 220.6 47 220.6C45.6 220.5 44.2 220.2 43 219.8C41.8 219.3 40.6 218.7 39.6 218C38.6 217.3 37.7 216.5 37 215.6C36.3 214.8 35.8 213.8 35.4 212.8C35 211.9 34.7 210.9 34.6 210C34.5 209 34.6 208.1 34.7 207.3C34.9 206.4 35.2 205.6 35.6 205C35.9 204.3 36.4 203.7 36.9 203.2C37.4 202.8 38 202.4 38.5 202.2C39.1 201.9 39.7 201.8 40.2 201.8C40.7 201.8 41.3 201.9 41.7 202.1C42.1 202.2 42.6 202.7 42.8 202.8C43 202.9 43 203 42.8 202.8C42.7 202.7 42.2 202.2 41.8 202C41.5 201.8 41 201.6 40.5 201.5C40.1 201.4 39.5 201.3 39 201.4C38.5 201.5 37.9 201.6 37.4 201.8C36.8 202.1 36.3 202.4 35.8 202.8C35.3 203.2 34.8 203.7 34.4 204.3C34 204.9 33.6 205.5 33.3 206.3C33 207 32.8 207.8 32.7 208.6C32.6 209.5 32.6 210.4 32.7 211.3C32.7 212.2 32.9 213.1 33.3 214.1C33.6 215 34 216 34.6 216.8C35.2 217.7 35.8 218.6 36.6 219.4C37.4 220.2 38.3 221 39.4 221.6C40.4 222.3 41.5 222.9 42.7 223.3C43.9 223.7 45.2 224.1 46.5 224.3C47.8 224.5 47.9 226.5 50.7 224.4C53.5 222.4 61.7 213.3 63.2 211.9Z"/><path d="M23.8 197.5C22.2 198.9 25.9 194.7 27.1 193.5C28.4 192.4 29.8 191.4 31.2 190.7C32.6 190 34.2 189.5 35.6 189.1C37.1 188.8 38.6 188.8 40 188.8C41.4 188.9 42.8 189.2 44 189.6C45.2 190.1 46.4 190.7 47.4 191.4C48.4 192.1 49.3 192.9 50 193.8C50.7 194.6 51.2 195.6 51.6 196.6C52 197.5 52.3 198.5 52.4 199.4C52.5 200.4 52.4 201.3 52.3 202.1C52.1 203 51.8 203.8 51.4 204.4C51.1 205.1 50.6 205.7 50.1 206.2C49.6 206.6 49 207 48.5 207.2C47.9 207.5 47.3 207.6 46.8 207.6C46.3 207.6 45.7 207.5 45.3 207.3C44.9 207.2 44.4 206.7 44.2 206.6C44 206.5 44 206.4 44.2 206.6C44.3 206.7 44.8 207.2 45.2 207.4C45.5 207.6 46 207.8 46.5 207.9C46.9 208 47.5 208.1 48 208C48.5 207.9 49.1 207.8 49.6 207.6C50.2 207.3 50.7 207 51.2 206.6C51.7 206.2 52.2 205.7 52.6 205.1C53 204.5 53.4 203.9 53.7 203.1C54 202.4 54.2 201.6 54.3 200.8C54.4 199.9 54.4 199 54.3 198.1C54.3 197.2 54.1 196.3 53.7 195.3C53.4 194.4 53 193.4 52.4 192.6C51.8 191.7 51.2 190.8 50.4 190C49.6 189.2 48.7 188.4 47.6 187.8C46.6 187.1 45.5 186.5 44.3 186.1C43.1 185.7 41.8 185.3 40.5 185.1C39.2 184.9 39.1 182.9 36.3 185C33.5 187 25.3 196.1 23.8 197.5Z"/></g> | |
| 13 | <circle cx="43.5" cy="286.3" r="20.5" fill="url(#core)"/> | |
| 14 | <g fill="#ffffff"><path d="M63.2 293.5C64.8 292.1 61.1 296.3 59.9 297.5C58.6 298.6 57.2 299.6 55.8 300.3C54.4 301 52.8 301.5 51.4 301.9C49.9 302.2 48.4 302.2 47 302.2C45.6 302.1 44.2 301.8 43 301.4C41.8 300.9 40.6 300.3 39.6 299.6C38.6 298.9 37.7 298.1 37 297.2C36.3 296.4 35.8 295.4 35.4 294.4C35 293.5 34.7 292.5 34.6 291.6C34.5 290.6 34.6 289.7 34.7 288.9C34.9 288 35.2 287.2 35.6 286.6C35.9 285.9 36.4 285.3 36.9 284.8C37.4 284.4 38 284 38.5 283.8C39.1 283.5 39.7 283.4 40.2 283.4C40.7 283.4 41.3 283.5 41.7 283.7C42.1 283.8 42.6 284.3 42.8 284.4C43 284.5 43 284.6 42.8 284.4C42.7 284.3 42.2 283.8 41.8 283.6C41.5 283.4 41 283.2 40.5 283.1C40.1 283 39.5 282.9 39 283C38.5 283.1 37.9 283.2 37.4 283.4C36.8 283.7 36.3 284 35.8 284.4C35.3 284.8 34.8 285.3 34.4 285.9C34 286.5 33.6 287.1 33.3 287.9C33 288.6 32.8 289.4 32.7 290.2C32.6 291.1 32.6 292 32.7 292.9C32.7 293.8 32.9 294.7 33.3 295.7C33.6 296.6 34 297.6 34.6 298.4C35.2 299.3 35.8 300.2 36.6 301C37.4 301.8 38.3 302.6 39.4 303.2C40.4 303.9 41.5 304.5 42.7 304.9C43.9 305.3 45.2 305.7 46.5 305.9C47.8 306.1 47.9 308.1 50.7 306C53.5 304 61.7 294.9 63.2 293.5Z"/><path d="M23.8 279.1C22.2 280.5 25.9 276.3 27.1 275.1C28.4 274 29.8 273 31.2 272.3C32.6 271.6 34.2 271.1 35.6 270.7C37.1 270.4 38.6 270.4 40 270.4C41.4 270.5 42.8 270.8 44 271.2C45.2 271.7 46.4 272.3 47.4 273C48.4 273.7 49.3 274.5 50 275.4C50.7 276.2 51.2 277.2 51.6 278.2C52 279.1 52.3 280.1 52.4 281C52.5 282 52.4 282.9 52.3 283.7C52.1 284.6 51.8 285.4 51.4 286C51.1 286.7 50.6 287.3 50.1 287.8C49.6 288.2 49 288.6 48.5 288.8C47.9 289.1 47.3 289.2 46.8 289.2C46.3 289.2 45.7 289.1 45.3 288.9C44.9 288.8 44.4 288.3 44.2 288.2C44 288.1 44 288 44.2 288.2C44.3 288.3 44.8 288.8 45.2 289C45.5 289.2 46 289.4 46.5 289.5C46.9 289.6 47.5 289.7 48 289.6C48.5 289.5 49.1 289.4 49.6 289.2C50.2 288.9 50.7 288.6 51.2 288.2C51.7 287.8 52.2 287.3 52.6 286.7C53 286.1 53.4 285.5 53.7 284.7C54 284 54.2 283.2 54.3 282.4C54.4 281.5 54.4 280.6 54.3 279.7C54.3 278.8 54.1 277.9 53.7 276.9C53.4 276 53 275 52.4 274.2C51.8 273.3 51.2 272.4 50.4 271.6C49.6 270.8 48.7 270 47.6 269.4C46.6 268.7 45.5 268.1 44.3 267.7C43.1 267.3 41.8 266.9 40.5 266.7C39.2 266.5 39.1 264.5 36.3 266.6C33.5 268.6 25.3 277.7 23.8 279.1Z"/></g> | |
| 15 | <circle cx="43.5" cy="367.9" r="20.5" fill="url(#core)"/> | |
| 16 | <g fill="#ffffff"><path d="M63.2 375.1C64.8 373.7 61.1 377.9 59.9 379.1C58.6 380.2 57.2 381.2 55.8 381.9C54.4 382.6 52.8 383.1 51.4 383.5C49.9 383.8 48.4 383.8 47 383.8C45.6 383.7 44.2 383.4 43 383C41.8 382.5 40.6 381.9 39.6 381.2C38.6 380.5 37.7 379.7 37 378.8C36.3 378 35.8 377 35.4 376C35 375.1 34.7 374.1 34.6 373.2C34.5 372.2 34.6 371.3 34.7 370.5C34.9 369.6 35.2 368.8 35.6 368.2C35.9 367.5 36.4 366.9 36.9 366.4C37.4 366 38 365.6 38.5 365.4C39.1 365.1 39.7 365 40.2 365C40.7 365 41.3 365.1 41.7 365.3C42.1 365.4 42.6 365.9 42.8 366C43 366.1 43 366.2 42.8 366C42.7 365.9 42.2 365.4 41.8 365.2C41.5 365 41 364.8 40.5 364.7C40.1 364.6 39.5 364.5 39 364.6C38.5 364.7 37.9 364.8 37.4 365C36.8 365.3 36.3 365.6 35.8 366C35.3 366.4 34.8 366.9 34.4 367.5C34 368.1 33.6 368.7 33.3 369.5C33 370.2 32.8 371 32.7 371.8C32.6 372.7 32.6 373.6 32.7 374.5C32.7 375.4 32.9 376.3 33.3 377.3C33.6 378.2 34 379.2 34.6 380C35.2 380.9 35.8 381.8 36.6 382.6C37.4 383.4 38.3 384.2 39.4 384.8C40.4 385.5 41.5 386.1 42.7 386.5C43.9 386.9 45.2 387.3 46.5 387.5C47.8 387.7 47.9 389.7 50.7 387.6C53.5 385.6 61.7 376.5 63.2 375.1Z"/><path d="M23.8 360.7C22.2 362.1 25.9 357.9 27.1 356.7C28.4 355.6 29.8 354.6 31.2 353.9C32.6 353.2 34.2 352.7 35.6 352.3C37.1 352 38.6 352 40 352C41.4 352.1 42.8 352.4 44 352.8C45.2 353.3 46.4 353.9 47.4 354.6C48.4 355.3 49.3 356.1 50 357C50.7 357.8 51.2 358.8 51.6 359.8C52 360.7 52.3 361.7 52.4 362.6C52.5 363.6 52.4 364.5 52.3 365.3C52.1 366.2 51.8 367 51.4 367.6C51.1 368.3 50.6 368.9 50.1 369.4C49.6 369.8 49 370.2 48.5 370.4C47.9 370.7 47.3 370.8 46.8 370.8C46.3 370.8 45.7 370.7 45.3 370.5C44.9 370.4 44.4 369.9 44.2 369.8C44 369.7 44 369.6 44.2 369.8C44.3 369.9 44.8 370.4 45.2 370.6C45.5 370.8 46 371 46.5 371.1C46.9 371.2 47.5 371.3 48 371.2C48.5 371.1 49.1 371 49.6 370.8C50.2 370.5 50.7 370.2 51.2 369.8C51.7 369.4 52.2 368.9 52.6 368.3C53 367.7 53.4 367.1 53.7 366.3C54 365.6 54.2 364.8 54.3 364C54.4 363.1 54.4 362.2 54.3 361.3C54.3 360.4 54.1 359.5 53.7 358.5C53.4 357.6 53 356.6 52.4 355.8C51.8 354.9 51.2 354 50.4 353.2C49.6 352.4 48.7 351.6 47.6 351C46.6 350.3 45.5 349.7 44.3 349.3C43.1 348.9 41.8 348.5 40.5 348.3C39.2 348.1 39.1 346.1 36.3 348.2C33.5 350.2 25.3 359.3 23.8 360.7Z"/></g> | |
| 17 | <circle cx="43.5" cy="449.5" r="20.5" fill="url(#core)"/> | |
| 18 | <g fill="#ffffff"><path d="M63.2 456.7C64.8 455.3 61.1 459.5 59.9 460.7C58.6 461.8 57.2 462.8 55.8 463.5C54.4 464.2 52.8 464.7 51.4 465.1C49.9 465.4 48.4 465.4 47 465.4C45.6 465.3 44.2 465 43 464.6C41.8 464.1 40.6 463.5 39.6 462.8C38.6 462.1 37.7 461.3 37 460.4C36.3 459.6 35.8 458.6 35.4 457.6C35 456.7 34.7 455.7 34.6 454.8C34.5 453.8 34.6 452.9 34.7 452.1C34.9 451.2 35.2 450.4 35.6 449.8C35.9 449.1 36.4 448.5 36.9 448C37.4 447.6 38 447.2 38.5 447C39.1 446.7 39.7 446.6 40.2 446.6C40.7 446.6 41.3 446.7 41.7 446.9C42.1 447 42.6 447.5 42.8 447.6C43 447.7 43 447.8 42.8 447.6C42.7 447.5 42.2 447 41.8 446.8C41.5 446.6 41 446.4 40.5 446.3C40.1 446.2 39.5 446.1 39 446.2C38.5 446.3 37.9 446.4 37.4 446.6C36.8 446.9 36.3 447.2 35.8 447.6C35.3 448 34.8 448.5 34.4 449.1C34 449.7 33.6 450.3 33.3 451.1C33 451.8 32.8 452.6 32.7 453.4C32.6 454.3 32.6 455.2 32.7 456.1C32.7 457 32.9 457.9 33.3 458.9C33.6 459.8 34 460.8 34.6 461.6C35.2 462.5 35.8 463.4 36.6 464.2C37.4 465 38.3 465.8 39.4 466.4C40.4 467.1 41.5 467.7 42.7 468.1C43.9 468.5 45.2 468.9 46.5 469.1C47.8 469.3 47.9 471.3 50.7 469.2C53.5 467.2 61.7 458.1 63.2 456.7Z"/><path d="M23.8 442.3C22.2 443.7 25.9 439.5 27.1 438.3C28.4 437.2 29.8 436.2 31.2 435.5C32.6 434.8 34.2 434.3 35.6 433.9C37.1 433.6 38.6 433.6 40 433.6C41.4 433.7 42.8 434 44 434.4C45.2 434.9 46.4 435.5 47.4 436.2C48.4 436.9 49.3 437.7 50 438.6C50.7 439.4 51.2 440.4 51.6 441.4C52 442.3 52.3 443.3 52.4 444.2C52.5 445.2 52.4 446.1 52.3 446.9C52.1 447.8 51.8 448.6 51.4 449.2C51.1 449.9 50.6 450.5 50.1 451C49.6 451.4 49 451.8 48.5 452C47.9 452.3 47.3 452.4 46.8 452.4C46.3 452.4 45.7 452.3 45.3 452.1C44.9 452 44.4 451.5 44.2 451.4C44 451.3 44 451.2 44.2 451.4C44.3 451.5 44.8 452 45.2 452.2C45.5 452.4 46 452.6 46.5 452.7C46.9 452.8 47.5 452.9 48 452.8C48.5 452.7 49.1 452.6 49.6 452.4C50.2 452.1 50.7 451.8 51.2 451.4C51.7 451 52.2 450.5 52.6 449.9C53 449.3 53.4 448.7 53.7 447.9C54 447.2 54.2 446.4 54.3 445.6C54.4 444.7 54.4 443.8 54.3 442.9C54.3 442 54.1 441.1 53.7 440.1C53.4 439.2 53 438.2 52.4 437.4C51.8 436.5 51.2 435.6 50.4 434.8C49.6 434 48.7 433.2 47.6 432.6C46.6 431.9 45.5 431.3 44.3 430.9C43.1 430.5 41.8 430.1 40.5 429.9C39.2 429.7 39.1 427.7 36.3 429.8C33.5 431.8 25.3 440.9 23.8 442.3Z"/></g> | |
| 19 | <circle cx="43.5" cy="531.1" r="20.5" fill="url(#core)"/> | |
| 20 | <g fill="#ffffff"><path d="M63.2 538.3C64.8 536.9 61.1 541.1 59.9 542.3C58.6 543.4 57.2 544.4 55.8 545.1C54.4 545.8 52.8 546.3 51.4 546.7C49.9 547 48.4 547 47 547C45.6 546.9 44.2 546.6 43 546.2C41.8 545.7 40.6 545.1 39.6 544.4C38.6 543.7 37.7 542.9 37 542C36.3 541.2 35.8 540.2 35.4 539.2C35 538.3 34.7 537.3 34.6 536.4C34.5 535.4 34.6 534.5 34.7 533.7C34.9 532.8 35.2 532 35.6 531.4C35.9 530.7 36.4 530.1 36.9 529.6C37.4 529.2 38 528.8 38.5 528.6C39.1 528.3 39.7 528.2 40.2 528.2C40.7 528.2 41.3 528.3 41.7 528.5C42.1 528.6 42.6 529.1 42.8 529.2C43 529.3 43 529.4 42.8 529.2C42.7 529.1 42.2 528.6 41.8 528.4C41.5 528.2 41 528 40.5 527.9C40.1 527.8 39.5 527.7 39 527.8C38.5 527.9 37.9 528 37.4 528.2C36.8 528.5 36.3 528.8 35.8 529.2C35.3 529.6 34.8 530.1 34.4 530.7C34 531.3 33.6 531.9 33.3 532.7C33 533.4 32.8 534.2 32.7 535C32.6 535.9 32.6 536.8 32.7 537.7C32.7 538.6 32.9 539.5 33.3 540.5C33.6 541.4 34 542.4 34.6 543.2C35.2 544.1 35.8 545 36.6 545.8C37.4 546.6 38.3 547.4 39.4 548C40.4 548.7 41.5 549.3 42.7 549.7C43.9 550.1 45.2 550.5 46.5 550.7C47.8 550.9 47.9 552.9 50.7 550.8C53.5 548.8 61.7 539.7 63.2 538.3Z"/><path d="M23.8 523.9C22.2 525.3 25.9 521.1 27.1 519.9C28.4 518.8 29.8 517.8 31.2 517.1C32.6 516.4 34.2 515.9 35.6 515.5C37.1 515.2 38.6 515.2 40 515.2C41.4 515.3 42.8 515.6 44 516C45.2 516.5 46.4 517.1 47.4 517.8C48.4 518.5 49.3 519.3 50 520.2C50.7 521 51.2 522 51.6 523C52 523.9 52.3 524.9 52.4 525.8C52.5 526.8 52.4 527.7 52.3 528.5C52.1 529.4 51.8 530.2 51.4 530.8C51.1 531.5 50.6 532.1 50.1 532.6C49.6 533 49 533.4 48.5 533.6C47.9 533.9 47.3 534 46.8 534C46.3 534 45.7 533.9 45.3 533.7C44.9 533.6 44.4 533.1 44.2 533C44 532.9 44 532.8 44.2 533C44.3 533.1 44.8 533.6 45.2 533.8C45.5 534 46 534.2 46.5 534.3C46.9 534.4 47.5 534.5 48 534.4C48.5 534.3 49.1 534.2 49.6 534C50.2 533.7 50.7 533.4 51.2 533C51.7 532.6 52.2 532.1 52.6 531.5C53 530.9 53.4 530.3 53.7 529.5C54 528.8 54.2 528 54.3 527.2C54.4 526.3 54.4 525.4 54.3 524.5C54.3 523.6 54.1 522.7 53.7 521.7C53.4 520.8 53 519.8 52.4 519C51.8 518.1 51.2 517.2 50.4 516.4C49.6 515.6 48.7 514.8 47.6 514.2C46.6 513.5 45.5 512.9 44.3 512.5C43.1 512.1 41.8 511.7 40.5 511.5C39.2 511.3 39.1 509.3 36.3 511.4C33.5 513.4 25.3 522.5 23.8 523.9Z"/></g> | |
| 21 | <g fill="none" stroke="#5f6e7d" stroke-width=".85" stroke-linecap="round"> | |
| 22 | <path d="M54.2 22.9C53.6 22.5 51.8 21.4 50.5 20.8C49.2 20.2 47.8 19.8 46.4 19.4C45 19.1 43.5 18.9 42 18.9C40.5 18.9 39 19 37.5 19.3C36.1 19.6 34.6 20 33.2 20.5C31.7 21.1 30.3 21.8 29 22.7C27.7 23.5 26.5 24.5 25.4 25.6C24.2 26.7 23.2 27.9 22.3 29.3C21.4 30.6 20.6 32 19.9 33.5C19.3 35 18.8 36.6 18.5 38.2C18.1 39.8 17.9 41.5 17.9 43.2C17.9 44.8 18.1 46.6 18.4 48.2C18.7 49.9 19.2 51.6 19.8 53.2C20.5 54.8 21.3 56.3 22.3 57.8C23.2 59.2 24.4 60.6 25.6 61.9C26.9 63.1 28.3 64.3 29.8 65.3C31.2 66.3 32.9 67.2 34.5 67.9C36.2 68.6 38 69.1 39.8 69.5C41.6 69.9 43.5 70.1 45.4 70.1C47.2 70.1 49.2 69.9 51 69.5C52.9 69.1 54.7 68.6 56.5 67.8C58.3 67.1 60 66.2 61.6 65.1C63.2 64 64.8 62.8 66.1 61.4C67.5 60 68.8 58.4 69.9 56.8C71 55.1 72 53.3 72.7 51.4C73.5 49.6 74.1 47.6 74.5 45.6C74.9 43.6 75.1 41.5 75.1 39.4C75 37.4 74.8 35.3 74.4 33.2C74 31.2 73.4 29.1 72.5 27.2C71.7 25.2 70.7 23.3 69.5 21.6C68.3 19.8 66.9 18.1 65.3 16.6C63.8 15.1 62.1 13.7 60.2 12.5C58.4 11.3 56.4 10.2 54.4 9.4C52.3 8.6 50.2 8 48 7.5C45.8 7.1 43.5 6.9 41.2 6.9C39 7 36.7 7.2 34.4 7.7C32.2 8.2 30 8.9 27.9 9.8C25.7 10.7 23.7 11.8 21.7 13.1C19.8 14.5 18 16 16.3 17.7C14.7 19.4 13.2 21.3 11.9 23.2C10.6 25.2 9.5 27.4 8.6 29.6C7.7 31.9 7 34.3 6.6 36.6C6.1 39 5.9 41.5 6 44C6 46.4 6.3 48.9 6.8 51.3C7.3 53.8 8.1 56.2 9.1 58.5C10.1 60.8 11.3 63 12.8 65.1C14.2 67.2 15.9 69.1 17.7 70.9C19.5 72.7 22.7 74.9 23.8 75.7"/> | |
| 23 | <path d="M32.7 60.1C33.4 60.5 35.2 61.6 36.5 62.2C37.8 62.8 39.2 63.2 40.6 63.6C42 63.9 43.5 64.1 45 64.1C46.5 64.1 48 64 49.5 63.7C50.9 63.4 52.4 63 53.8 62.5C55.3 61.9 56.7 61.2 58 60.3C59.3 59.5 60.5 58.5 61.6 57.4C62.8 56.3 63.8 55.1 64.7 53.7C65.6 52.4 66.4 51 67.1 49.5C67.7 48 68.2 46.4 68.5 44.8C68.9 43.2 69.1 41.5 69.1 39.8C69.1 38.2 68.9 36.4 68.6 34.8C68.3 33.1 67.8 31.4 67.2 29.8C66.5 28.2 65.7 26.7 64.7 25.2C63.8 23.8 62.6 22.4 61.4 21.1C60.1 19.9 58.7 18.7 57.2 17.7C55.8 16.7 54.1 15.8 52.5 15.1C50.8 14.4 49 13.9 47.2 13.5C45.4 13.1 43.5 12.9 41.6 12.9C39.8 12.9 37.8 13.1 36 13.5C34.1 13.9 32.3 14.4 30.5 15.2C28.7 15.9 27 16.8 25.4 17.9C23.8 19 22.2 20.2 20.9 21.6C19.5 23 18.2 24.6 17.1 26.3C16 27.9 15 29.7 14.3 31.6C13.5 33.4 12.9 35.4 12.5 37.4C12.1 39.4 11.9 41.5 11.9 43.6C12 45.6 12.2 47.7 12.6 49.8C13 51.8 13.6 53.9 14.5 55.8C15.3 57.8 16.3 59.7 17.5 61.4C18.7 63.2 20.1 64.9 21.7 66.4C23.2 67.9 24.9 69.3 26.8 70.5C28.6 71.7 30.6 72.8 32.6 73.6C34.7 74.4 36.8 75 39 75.5C41.2 75.9 43.5 76.1 45.8 76.1C48 76 50.3 75.8 52.6 75.3C54.8 74.8 57 74.1 59.1 73.2C61.3 72.3 63.3 71.2 65.3 69.9C67.2 68.5 69 67 70.7 65.3C72.3 63.6 73.8 61.7 75.1 59.8C76.4 57.8 77.5 55.6 78.4 53.4C79.3 51.1 80 48.7 80.4 46.4C80.9 44 81.1 41.5 81 39C81 36.6 80.7 34.1 80.2 31.7C79.7 29.2 78.9 26.8 77.9 24.5C76.9 22.2 75.7 20 74.2 17.9C72.8 15.8 71.1 13.9 69.3 12.1C67.5 10.3 64.3 8.1 63.2 7.3"/> | |
| 24 | <path d="M23.8 75.7C34.2 81.7 52.8 82.9 63.2 88.9"/> | |
| 25 | <path d="M28.5 -12.7C38.9 -6.7 52.8 1.3 63.2 7.3"/> | |
| 26 | <path d="M54.2 104.5C53.6 104.1 51.8 103 50.5 102.4C49.2 101.8 47.8 101.4 46.4 101C45 100.7 43.5 100.5 42 100.5C40.5 100.5 39 100.6 37.5 100.9C36.1 101.2 34.6 101.6 33.2 102.1C31.7 102.7 30.3 103.4 29 104.3C27.7 105.1 26.5 106.1 25.4 107.2C24.2 108.3 23.2 109.5 22.3 110.9C21.4 112.2 20.6 113.6 19.9 115.1C19.3 116.6 18.8 118.2 18.5 119.8C18.1 121.4 17.9 123.1 17.9 124.8C17.9 126.4 18.1 128.2 18.4 129.8C18.7 131.5 19.2 133.2 19.8 134.8C20.5 136.4 21.3 137.9 22.3 139.4C23.2 140.8 24.4 142.2 25.6 143.5C26.9 144.7 28.3 145.9 29.8 146.9C31.2 147.9 32.9 148.8 34.5 149.5C36.2 150.2 38 150.7 39.8 151.1C41.6 151.5 43.5 151.7 45.4 151.7C47.2 151.7 49.2 151.5 51 151.1C52.9 150.7 54.7 150.2 56.5 149.4C58.3 148.7 60 147.8 61.6 146.7C63.2 145.6 64.8 144.4 66.1 143C67.5 141.6 68.8 140 69.9 138.3C71 136.7 72 134.9 72.7 133C73.5 131.2 74.1 129.2 74.5 127.2C74.9 125.2 75.1 123.1 75.1 121C75 119 74.8 116.9 74.4 114.8C74 112.8 73.4 110.7 72.5 108.8C71.7 106.8 70.7 104.9 69.5 103.2C68.3 101.4 66.9 99.7 65.3 98.2C63.8 96.7 62.1 95.3 60.2 94.1C58.4 92.9 56.4 91.8 54.4 91C52.3 90.2 50.2 89.6 48 89.1C45.8 88.7 43.5 88.5 41.2 88.5C39 88.6 36.7 88.8 34.4 89.3C32.2 89.8 30 90.5 27.9 91.4C25.7 92.3 23.7 93.4 21.7 94.7C19.8 96.1 18 97.6 16.3 99.3C14.7 101 13.2 102.9 11.9 104.8C10.6 106.8 9.5 109 8.6 111.2C7.7 113.5 7 115.9 6.6 118.2C6.1 120.6 5.9 123.1 6 125.6C6 128 6.3 130.5 6.8 132.9C7.3 135.4 8.1 137.8 9.1 140.1C10.1 142.4 11.3 144.6 12.8 146.7C14.2 148.8 15.9 150.7 17.7 152.5C19.5 154.3 22.7 156.5 23.8 157.3"/> | |
| 27 | <path d="M32.7 141.7C33.4 142.1 35.2 143.2 36.5 143.8C37.8 144.4 39.2 144.8 40.6 145.2C42 145.5 43.5 145.7 45 145.7C46.5 145.7 48 145.6 49.5 145.3C50.9 145 52.4 144.6 53.8 144.1C55.3 143.5 56.7 142.8 58 141.9C59.3 141.1 60.5 140.1 61.6 139C62.8 137.9 63.8 136.7 64.7 135.3C65.6 134 66.4 132.6 67.1 131.1C67.7 129.6 68.2 128 68.5 126.4C68.9 124.8 69.1 123.1 69.1 121.4C69.1 119.8 68.9 118 68.6 116.4C68.3 114.7 67.8 113 67.2 111.4C66.5 109.8 65.7 108.3 64.7 106.8C63.8 105.4 62.6 104 61.4 102.7C60.1 101.5 58.7 100.3 57.2 99.3C55.8 98.3 54.1 97.4 52.5 96.7C50.8 96 49 95.5 47.2 95.1C45.4 94.7 43.5 94.5 41.6 94.5C39.8 94.5 37.8 94.7 36 95.1C34.1 95.5 32.3 96 30.5 96.8C28.7 97.5 27 98.4 25.4 99.5C23.8 100.6 22.2 101.8 20.9 103.2C19.5 104.6 18.2 106.2 17.1 107.9C16 109.5 15 111.3 14.3 113.2C13.5 115 12.9 117 12.5 119C12.1 121 11.9 123.1 11.9 125.2C12 127.2 12.2 129.3 12.6 131.4C13 133.4 13.6 135.5 14.5 137.4C15.3 139.4 16.3 141.3 17.5 143C18.7 144.8 20.1 146.5 21.7 148C23.2 149.5 24.9 150.9 26.8 152.1C28.6 153.3 30.6 154.4 32.6 155.2C34.7 156 36.8 156.6 39 157.1C41.2 157.5 43.5 157.7 45.8 157.7C48 157.6 50.3 157.4 52.6 156.9C54.8 156.4 57 155.7 59.1 154.8C61.3 153.9 63.3 152.8 65.3 151.5C67.2 150.1 69 148.6 70.7 146.9C72.3 145.2 73.8 143.3 75.1 141.3C76.4 139.4 77.5 137.2 78.4 135C79.3 132.7 80 130.3 80.4 128C80.9 125.6 81.1 123.1 81 120.6C81 118.2 80.7 115.7 80.2 113.3C79.7 110.8 78.9 108.4 77.9 106.1C76.9 103.8 75.7 101.6 74.2 99.5C72.8 97.4 71.1 95.5 69.3 93.7C67.5 91.9 64.3 89.7 63.2 88.9"/> | |
| 28 | <path d="M23.8 157.3C34.2 163.3 52.8 164.5 63.2 170.5"/> | |
| 29 | <path d="M54.2 186.1C53.6 185.7 51.8 184.6 50.5 184C49.2 183.4 47.8 183 46.4 182.6C45 182.3 43.5 182.1 42 182.1C40.5 182.1 39 182.2 37.5 182.5C36.1 182.8 34.6 183.2 33.2 183.7C31.7 184.3 30.3 185 29 185.9C27.7 186.7 26.5 187.7 25.4 188.8C24.2 189.9 23.2 191.1 22.3 192.5C21.4 193.8 20.6 195.2 19.9 196.7C19.3 198.2 18.8 199.8 18.5 201.4C18.1 203 17.9 204.7 17.9 206.4C17.9 208 18.1 209.8 18.4 211.4C18.7 213.1 19.2 214.8 19.8 216.4C20.5 218 21.3 219.5 22.3 221C23.2 222.4 24.4 223.8 25.6 225.1C26.9 226.3 28.3 227.5 29.8 228.5C31.2 229.5 32.9 230.4 34.5 231.1C36.2 231.8 38 232.3 39.8 232.7C41.6 233.1 43.5 233.3 45.4 233.3C47.2 233.3 49.2 233.1 51 232.7C52.9 232.3 54.7 231.8 56.5 231C58.3 230.3 60 229.4 61.6 228.3C63.2 227.2 64.8 226 66.1 224.6C67.5 223.2 68.8 221.6 69.9 219.9C71 218.3 72 216.5 72.7 214.6C73.5 212.8 74.1 210.8 74.5 208.8C74.9 206.8 75.1 204.7 75.1 202.6C75 200.6 74.8 198.5 74.4 196.4C74 194.4 73.4 192.3 72.5 190.4C71.7 188.4 70.7 186.5 69.5 184.8C68.3 183 66.9 181.3 65.3 179.8C63.8 178.3 62.1 176.9 60.2 175.7C58.4 174.5 56.4 173.4 54.4 172.6C52.3 171.8 50.2 171.2 48 170.7C45.8 170.3 43.5 170.1 41.2 170.1C39 170.2 36.7 170.4 34.4 170.9C32.2 171.4 30 172.1 27.9 173C25.7 173.9 23.7 175 21.7 176.3C19.8 177.7 18 179.2 16.3 180.9C14.7 182.6 13.2 184.5 11.9 186.4C10.6 188.4 9.5 190.6 8.6 192.8C7.7 195.1 7 197.5 6.6 199.8C6.1 202.2 5.9 204.7 6 207.2C6 209.6 6.3 212.1 6.8 214.5C7.3 217 8.1 219.4 9.1 221.7C10.1 224 11.3 226.2 12.8 228.3C14.2 230.4 15.9 232.3 17.7 234.1C19.5 235.9 22.7 238.1 23.8 238.9"/> | |
| 30 | <path d="M32.7 223.3C33.4 223.7 35.2 224.8 36.5 225.4C37.8 226 39.2 226.4 40.6 226.8C42 227.1 43.5 227.3 45 227.3C46.5 227.3 48 227.2 49.5 226.9C50.9 226.6 52.4 226.2 53.8 225.7C55.3 225.1 56.7 224.4 58 223.5C59.3 222.7 60.5 221.7 61.6 220.6C62.8 219.5 63.8 218.3 64.7 216.9C65.6 215.6 66.4 214.2 67.1 212.7C67.7 211.2 68.2 209.6 68.5 208C68.9 206.4 69.1 204.7 69.1 203C69.1 201.4 68.9 199.6 68.6 198C68.3 196.3 67.8 194.6 67.2 193C66.5 191.4 65.7 189.9 64.7 188.4C63.8 187 62.6 185.6 61.4 184.3C60.1 183.1 58.7 181.9 57.2 180.9C55.8 179.9 54.1 179 52.5 178.3C50.8 177.6 49 177.1 47.2 176.7C45.4 176.3 43.5 176.1 41.6 176.1C39.8 176.1 37.8 176.3 36 176.7C34.1 177.1 32.3 177.6 30.5 178.4C28.7 179.1 27 180 25.4 181.1C23.8 182.2 22.2 183.4 20.9 184.8C19.5 186.2 18.2 187.8 17.1 189.5C16 191.1 15 192.9 14.3 194.8C13.5 196.6 12.9 198.6 12.5 200.6C12.1 202.6 11.9 204.7 11.9 206.8C12 208.8 12.2 210.9 12.6 213C13 215 13.6 217.1 14.5 219C15.3 221 16.3 222.9 17.5 224.6C18.7 226.4 20.1 228.1 21.7 229.6C23.2 231.1 24.9 232.5 26.8 233.7C28.6 234.9 30.6 236 32.6 236.8C34.7 237.6 36.8 238.2 39 238.7C41.2 239.1 43.5 239.3 45.8 239.3C48 239.2 50.3 239 52.6 238.5C54.8 238 57 237.3 59.1 236.4C61.3 235.5 63.3 234.4 65.3 233.1C67.2 231.7 69 230.2 70.7 228.5C72.3 226.8 73.8 224.9 75.1 222.9C76.4 221 77.5 218.8 78.4 216.6C79.3 214.3 80 211.9 80.4 209.6C80.9 207.2 81.1 204.7 81 202.2C81 199.8 80.7 197.3 80.2 194.9C79.7 192.4 78.9 190 77.9 187.7C76.9 185.4 75.7 183.2 74.2 181.1C72.8 179 71.1 177.1 69.3 175.3C67.5 173.5 64.3 171.3 63.2 170.5"/> | |
| 31 | <path d="M23.8 238.9C34.2 244.9 52.8 246.1 63.2 252.1"/> | |
| 32 | <path d="M54.2 267.7C53.6 267.3 51.8 266.2 50.5 265.6C49.2 265 47.8 264.6 46.4 264.2C45 263.9 43.5 263.7 42 263.7C40.5 263.7 39 263.8 37.5 264.1C36.1 264.4 34.6 264.8 33.2 265.3C31.7 265.9 30.3 266.6 29 267.5C27.7 268.3 26.5 269.3 25.4 270.4C24.2 271.5 23.2 272.7 22.3 274C21.4 275.4 20.6 276.8 19.9 278.3C19.3 279.8 18.8 281.4 18.5 283C18.1 284.6 17.9 286.3 17.9 288C17.9 289.6 18.1 291.4 18.4 293C18.7 294.7 19.2 296.4 19.8 298C20.5 299.6 21.3 301.1 22.3 302.6C23.2 304 24.4 305.4 25.6 306.7C26.9 307.9 28.3 309.1 29.8 310.1C31.2 311.1 32.9 312 34.5 312.7C36.2 313.4 38 313.9 39.8 314.3C41.6 314.7 43.5 314.9 45.4 314.9C47.2 314.9 49.2 314.7 51 314.3C52.9 313.9 54.7 313.4 56.5 312.6C58.3 311.9 60 311 61.6 309.9C63.2 308.8 64.8 307.6 66.1 306.2C67.5 304.8 68.8 303.2 69.9 301.5C71 299.9 72 298.1 72.7 296.2C73.5 294.4 74.1 292.4 74.5 290.4C74.9 288.4 75.1 286.3 75.1 284.2C75 282.2 74.8 280.1 74.4 278C74 276 73.4 273.9 72.5 272C71.7 270 70.7 268.1 69.5 266.4C68.3 264.6 66.9 262.9 65.3 261.4C63.8 259.9 62.1 258.5 60.2 257.3C58.4 256.1 56.4 255 54.4 254.2C52.3 253.4 50.2 252.8 48 252.3C45.8 251.9 43.5 251.7 41.2 251.7C39 251.8 36.7 252 34.4 252.5C32.2 253 30 253.7 27.9 254.6C25.7 255.5 23.7 256.6 21.7 257.9C19.8 259.3 18 260.8 16.3 262.5C14.7 264.2 13.2 266.1 11.9 268C10.6 270 9.5 272.2 8.6 274.4C7.7 276.7 7 279.1 6.6 281.4C6.1 283.8 5.9 286.3 6 288.8C6 291.2 6.3 293.7 6.8 296.1C7.3 298.6 8.1 301 9.1 303.3C10.1 305.6 11.3 307.8 12.8 309.9C14.2 312 15.9 313.9 17.7 315.7C19.5 317.5 22.7 319.7 23.8 320.5"/> | |
| 33 | <path d="M32.7 304.9C33.4 305.3 35.2 306.4 36.5 307C37.8 307.6 39.2 308 40.6 308.4C42 308.7 43.5 308.9 45 308.9C46.5 308.9 48 308.8 49.5 308.5C50.9 308.2 52.4 307.8 53.8 307.3C55.3 306.7 56.7 306 58 305.1C59.3 304.3 60.5 303.3 61.6 302.2C62.8 301.1 63.8 299.9 64.7 298.5C65.6 297.2 66.4 295.8 67.1 294.3C67.7 292.8 68.2 291.2 68.5 289.6C68.9 288 69.1 286.3 69.1 284.6C69.1 283 68.9 281.2 68.6 279.6C68.3 277.9 67.8 276.2 67.2 274.6C66.5 273 65.7 271.5 64.7 270C63.8 268.6 62.6 267.2 61.4 265.9C60.1 264.7 58.7 263.5 57.2 262.5C55.8 261.5 54.1 260.6 52.5 259.9C50.8 259.2 49 258.7 47.2 258.3C45.4 257.9 43.5 257.7 41.6 257.7C39.8 257.7 37.8 257.9 36 258.3C34.1 258.7 32.3 259.2 30.5 260C28.7 260.7 27 261.6 25.4 262.7C23.8 263.8 22.2 265 20.9 266.4C19.5 267.8 18.2 269.4 17.1 271C16 272.7 15 274.5 14.3 276.4C13.5 278.2 12.9 280.2 12.5 282.2C12.1 284.2 11.9 286.3 11.9 288.4C12 290.4 12.2 292.5 12.6 294.6C13 296.6 13.6 298.7 14.5 300.6C15.3 302.6 16.3 304.5 17.5 306.2C18.7 308 20.1 309.7 21.7 311.2C23.2 312.7 24.9 314.1 26.8 315.3C28.6 316.5 30.6 317.6 32.6 318.4C34.7 319.2 36.8 319.8 39 320.3C41.2 320.7 43.5 320.9 45.8 320.9C48 320.8 50.3 320.6 52.6 320.1C54.8 319.6 57 318.9 59.1 318C61.3 317.1 63.3 316 65.3 314.7C67.2 313.3 69 311.8 70.7 310.1C72.3 308.4 73.8 306.5 75.1 304.5C76.4 302.6 77.5 300.4 78.4 298.2C79.3 295.9 80 293.5 80.4 291.2C80.9 288.8 81.1 286.3 81 283.8C81 281.4 80.7 278.9 80.2 276.5C79.7 274 78.9 271.6 77.9 269.3C76.9 267 75.7 264.8 74.2 262.7C72.8 260.6 71.1 258.7 69.3 256.9C67.5 255.1 64.3 252.9 63.2 252.1"/> | |
| 34 | <path d="M23.8 320.5C34.2 326.5 52.8 327.7 63.2 333.7"/> | |
| 35 | <path d="M54.2 349.3C53.6 348.9 51.8 347.8 50.5 347.2C49.2 346.6 47.8 346.2 46.4 345.8C45 345.5 43.5 345.3 42 345.3C40.5 345.3 39 345.4 37.5 345.7C36.1 346 34.6 346.4 33.2 346.9C31.7 347.5 30.3 348.2 29 349.1C27.7 349.9 26.5 350.9 25.4 352C24.2 353.1 23.2 354.3 22.3 355.6C21.4 357 20.6 358.4 19.9 359.9C19.3 361.4 18.8 363 18.5 364.6C18.1 366.2 17.9 367.9 17.9 369.6C17.9 371.2 18.1 373 18.4 374.6C18.7 376.3 19.2 378 19.8 379.6C20.5 381.2 21.3 382.7 22.3 384.2C23.2 385.6 24.4 387 25.6 388.3C26.9 389.5 28.3 390.7 29.8 391.7C31.2 392.7 32.9 393.6 34.5 394.3C36.2 395 38 395.5 39.8 395.9C41.6 396.3 43.5 396.5 45.4 396.5C47.2 396.5 49.2 396.3 51 395.9C52.9 395.5 54.7 395 56.5 394.2C58.3 393.5 60 392.6 61.6 391.5C63.2 390.4 64.8 389.2 66.1 387.8C67.5 386.4 68.8 384.8 69.9 383.1C71 381.5 72 379.7 72.7 377.8C73.5 376 74.1 374 74.5 372C74.9 370 75.1 367.9 75.1 365.8C75 363.8 74.8 361.7 74.4 359.6C74 357.6 73.4 355.5 72.5 353.6C71.7 351.6 70.7 349.7 69.5 348C68.3 346.2 66.9 344.5 65.3 343C63.8 341.5 62.1 340.1 60.2 338.9C58.4 337.7 56.4 336.6 54.4 335.8C52.3 335 50.2 334.4 48 333.9C45.8 333.5 43.5 333.3 41.2 333.3C39 333.4 36.7 333.6 34.4 334.1C32.2 334.6 30 335.3 27.9 336.2C25.7 337.1 23.7 338.2 21.7 339.5C19.8 340.9 18 342.4 16.3 344.1C14.7 345.8 13.2 347.7 11.9 349.6C10.6 351.6 9.5 353.8 8.6 356C7.7 358.3 7 360.7 6.6 363C6.1 365.4 5.9 367.9 6 370.4C6 372.8 6.3 375.3 6.8 377.7C7.3 380.2 8.1 382.6 9.1 384.9C10.1 387.2 11.3 389.4 12.8 391.5C14.2 393.6 15.9 395.5 17.7 397.3C19.5 399.1 22.7 401.3 23.8 402.1"/> | |
| 36 | <path d="M32.7 386.5C33.4 386.9 35.2 388 36.5 388.6C37.8 389.2 39.2 389.6 40.6 390C42 390.3 43.5 390.5 45 390.5C46.5 390.5 48 390.4 49.5 390.1C50.9 389.8 52.4 389.4 53.8 388.9C55.3 388.3 56.7 387.6 58 386.7C59.3 385.9 60.5 384.9 61.6 383.8C62.8 382.7 63.8 381.5 64.7 380.1C65.6 378.8 66.4 377.4 67.1 375.9C67.7 374.4 68.2 372.8 68.5 371.2C68.9 369.6 69.1 367.9 69.1 366.2C69.1 364.6 68.9 362.8 68.6 361.2C68.3 359.5 67.8 357.8 67.2 356.2C66.5 354.6 65.7 353.1 64.7 351.6C63.8 350.2 62.6 348.8 61.4 347.5C60.1 346.3 58.7 345.1 57.2 344.1C55.8 343.1 54.1 342.2 52.5 341.5C50.8 340.8 49 340.3 47.2 339.9C45.4 339.5 43.5 339.3 41.6 339.3C39.8 339.3 37.8 339.5 36 339.9C34.1 340.3 32.3 340.8 30.5 341.6C28.7 342.3 27 343.2 25.4 344.3C23.8 345.4 22.2 346.6 20.9 348C19.5 349.4 18.2 351 17.1 352.6C16 354.3 15 356.1 14.3 358C13.5 359.8 12.9 361.8 12.5 363.8C12.1 365.8 11.9 367.9 11.9 370C12 372 12.2 374.1 12.6 376.2C13 378.2 13.6 380.3 14.5 382.2C15.3 384.2 16.3 386.1 17.5 387.8C18.7 389.6 20.1 391.3 21.7 392.8C23.2 394.3 24.9 395.7 26.8 396.9C28.6 398.1 30.6 399.2 32.6 400C34.7 400.8 36.8 401.4 39 401.9C41.2 402.3 43.5 402.5 45.8 402.5C48 402.4 50.3 402.2 52.6 401.7C54.8 401.2 57 400.5 59.1 399.6C61.3 398.7 63.3 397.6 65.3 396.3C67.2 394.9 69 393.4 70.7 391.7C72.3 390 73.8 388.1 75.1 386.1C76.4 384.2 77.5 382 78.4 379.8C79.3 377.5 80 375.1 80.4 372.8C80.9 370.4 81.1 367.9 81 365.4C81 363 80.7 360.5 80.2 358.1C79.7 355.6 78.9 353.2 77.9 350.9C76.9 348.6 75.7 346.4 74.2 344.3C72.8 342.2 71.1 340.3 69.3 338.5C67.5 336.7 64.3 334.5 63.2 333.7"/> | |
| 37 | <path d="M23.8 402.1C34.2 408.1 52.8 409.3 63.2 415.3"/> | |
| 38 | <path d="M54.2 430.9C53.6 430.5 51.8 429.4 50.5 428.8C49.2 428.2 47.8 427.8 46.4 427.4C45 427.1 43.5 426.9 42 426.9C40.5 426.9 39 427 37.5 427.3C36.1 427.6 34.6 428 33.2 428.5C31.7 429.1 30.3 429.8 29 430.7C27.7 431.5 26.5 432.5 25.4 433.6C24.2 434.7 23.2 435.9 22.3 437.2C21.4 438.6 20.6 440 19.9 441.5C19.3 443 18.8 444.6 18.5 446.2C18.1 447.8 17.9 449.5 17.9 451.2C17.9 452.8 18.1 454.6 18.4 456.2C18.7 457.9 19.2 459.6 19.8 461.2C20.5 462.8 21.3 464.3 22.3 465.8C23.2 467.2 24.4 468.6 25.6 469.9C26.9 471.1 28.3 472.3 29.8 473.3C31.2 474.3 32.9 475.2 34.5 475.9C36.2 476.6 38 477.1 39.8 477.5C41.6 477.9 43.5 478.1 45.4 478.1C47.2 478.1 49.2 477.9 51 477.5C52.9 477.1 54.7 476.6 56.5 475.8C58.3 475.1 60 474.2 61.6 473.1C63.2 472 64.8 470.8 66.1 469.4C67.5 468 68.8 466.4 69.9 464.8C71 463.1 72 461.3 72.7 459.4C73.5 457.6 74.1 455.6 74.5 453.6C74.9 451.6 75.1 449.5 75.1 447.4C75 445.4 74.8 443.3 74.4 441.2C74 439.2 73.4 437.1 72.5 435.2C71.7 433.2 70.7 431.3 69.5 429.6C68.3 427.8 66.9 426.1 65.3 424.6C63.8 423.1 62.1 421.7 60.2 420.5C58.4 419.3 56.4 418.2 54.4 417.4C52.3 416.6 50.2 416 48 415.5C45.8 415.1 43.5 414.9 41.2 414.9C39 415 36.7 415.2 34.4 415.7C32.2 416.2 30 416.9 27.9 417.8C25.7 418.7 23.7 419.8 21.7 421.1C19.8 422.5 18 424 16.3 425.7C14.7 427.4 13.2 429.3 11.9 431.2C10.6 433.2 9.5 435.4 8.6 437.6C7.7 439.9 7 442.3 6.6 444.6C6.1 447 5.9 449.5 6 452C6 454.4 6.3 456.9 6.8 459.3C7.3 461.8 8.1 464.2 9.1 466.5C10.1 468.8 11.3 471 12.8 473.1C14.2 475.2 15.9 477.1 17.7 478.9C19.5 480.7 22.7 482.9 23.8 483.7"/> | |
| 39 | <path d="M32.7 468.1C33.4 468.5 35.2 469.6 36.5 470.2C37.8 470.8 39.2 471.2 40.6 471.6C42 471.9 43.5 472.1 45 472.1C46.5 472.1 48 472 49.5 471.7C50.9 471.4 52.4 471 53.8 470.5C55.3 469.9 56.7 469.2 58 468.3C59.3 467.5 60.5 466.5 61.6 465.4C62.8 464.3 63.8 463.1 64.7 461.8C65.6 460.4 66.4 459 67.1 457.5C67.7 456 68.2 454.4 68.5 452.8C68.9 451.2 69.1 449.5 69.1 447.8C69.1 446.2 68.9 444.4 68.6 442.8C68.3 441.1 67.8 439.4 67.2 437.8C66.5 436.2 65.7 434.7 64.7 433.2C63.8 431.8 62.6 430.4 61.4 429.1C60.1 427.9 58.7 426.7 57.2 425.7C55.8 424.7 54.1 423.8 52.5 423.1C50.8 422.4 49 421.9 47.2 421.5C45.4 421.1 43.5 420.9 41.6 420.9C39.8 420.9 37.8 421.1 36 421.5C34.1 421.9 32.3 422.4 30.5 423.2C28.7 423.9 27 424.8 25.4 425.9C23.8 427 22.2 428.2 20.9 429.6C19.5 431 18.2 432.6 17.1 434.2C16 435.9 15 437.7 14.3 439.6C13.5 441.4 12.9 443.4 12.5 445.4C12.1 447.4 11.9 449.5 11.9 451.6C12 453.6 12.2 455.7 12.6 457.8C13 459.8 13.6 461.9 14.5 463.8C15.3 465.8 16.3 467.7 17.5 469.4C18.7 471.2 20.1 472.9 21.7 474.4C23.2 475.9 24.9 477.3 26.8 478.5C28.6 479.7 30.6 480.8 32.6 481.6C34.7 482.4 36.8 483 39 483.5C41.2 483.9 43.5 484.1 45.8 484.1C48 484 50.3 483.8 52.6 483.3C54.8 482.8 57 482.1 59.1 481.2C61.3 480.3 63.3 479.2 65.3 477.9C67.2 476.5 69 475 70.7 473.3C72.3 471.6 73.8 469.7 75.1 467.8C76.4 465.8 77.5 463.6 78.4 461.4C79.3 459.1 80 456.7 80.4 454.4C80.9 452 81.1 449.5 81 447C81 444.6 80.7 442.1 80.2 439.7C79.7 437.2 78.9 434.8 77.9 432.5C76.9 430.2 75.7 428 74.2 425.9C72.8 423.8 71.1 421.9 69.3 420.1C67.5 418.3 64.3 416.1 63.2 415.3"/> | |
| 40 | <path d="M23.8 483.7C34.2 489.7 52.8 490.9 63.2 496.9"/> | |
| 41 | <path d="M54.2 512.5C53.6 512.1 51.8 511 50.5 510.4C49.2 509.8 47.8 509.4 46.4 509C45 508.7 43.5 508.5 42 508.5C40.5 508.5 39 508.6 37.5 508.9C36.1 509.2 34.6 509.6 33.2 510.1C31.7 510.7 30.3 511.4 29 512.3C27.7 513.1 26.5 514.1 25.4 515.2C24.2 516.3 23.2 517.5 22.3 518.8C21.4 520.2 20.6 521.6 19.9 523.1C19.3 524.6 18.8 526.2 18.5 527.8C18.1 529.4 17.9 531.1 17.9 532.8C17.9 534.4 18.1 536.2 18.4 537.8C18.7 539.5 19.2 541.2 19.8 542.8C20.5 544.4 21.3 545.9 22.3 547.4C23.2 548.8 24.4 550.2 25.6 551.5C26.9 552.7 28.3 553.9 29.8 554.9C31.2 555.9 32.9 556.8 34.5 557.5C36.2 558.2 38 558.7 39.8 559.1C41.6 559.5 43.5 559.7 45.4 559.7C47.2 559.7 49.2 559.5 51 559.1C52.9 558.7 54.7 558.2 56.5 557.4C58.3 556.7 60 555.8 61.6 554.7C63.2 553.6 64.8 552.4 66.1 551C67.5 549.6 68.8 548 69.9 546.3C71 544.7 72 542.9 72.7 541C73.5 539.2 74.1 537.2 74.5 535.2C74.9 533.2 75.1 531.1 75.1 529C75 527 74.8 524.9 74.4 522.8C74 520.8 73.4 518.7 72.5 516.8C71.7 514.8 70.7 512.9 69.5 511.2C68.3 509.4 66.9 507.7 65.3 506.2C63.8 504.7 62.1 503.3 60.2 502.1C58.4 500.9 56.4 499.8 54.4 499C52.3 498.2 50.2 497.6 48 497.1C45.8 496.7 43.5 496.5 41.2 496.5C39 496.6 36.7 496.8 34.4 497.3C32.2 497.8 30 498.5 27.9 499.4C25.7 500.3 23.7 501.4 21.7 502.7C19.8 504.1 18 505.6 16.3 507.3C14.7 509 13.2 510.9 11.9 512.8C10.6 514.8 9.5 517 8.6 519.2C7.7 521.5 7 523.9 6.6 526.2C6.1 528.6 5.9 531.1 6 533.6C6 536 6.3 538.5 6.8 540.9C7.3 543.4 8.1 545.8 9.1 548.1C10.1 550.4 11.3 552.6 12.8 554.7C14.2 556.8 15.9 558.7 17.7 560.5C19.5 562.3 22.7 564.5 23.8 565.3"/> | |
| 42 | <path d="M32.7 549.7C33.4 550.1 35.2 551.2 36.5 551.8C37.8 552.4 39.2 552.8 40.6 553.2C42 553.5 43.5 553.7 45 553.7C46.5 553.7 48 553.6 49.5 553.3C50.9 553 52.4 552.6 53.8 552.1C55.3 551.5 56.7 550.8 58 549.9C59.3 549.1 60.5 548.1 61.6 547C62.8 545.9 63.8 544.7 64.7 543.3C65.6 542 66.4 540.6 67.1 539.1C67.7 537.6 68.2 536 68.5 534.4C68.9 532.8 69.1 531.1 69.1 529.4C69.1 527.8 68.9 526 68.6 524.4C68.3 522.7 67.8 521 67.2 519.4C66.5 517.8 65.7 516.3 64.7 514.8C63.8 513.4 62.6 512 61.4 510.7C60.1 509.5 58.7 508.3 57.2 507.3C55.8 506.3 54.1 505.4 52.5 504.7C50.8 504 49 503.5 47.2 503.1C45.4 502.7 43.5 502.5 41.6 502.5C39.8 502.5 37.8 502.7 36 503.1C34.1 503.5 32.3 504 30.5 504.8C28.7 505.5 27 506.4 25.4 507.5C23.8 508.6 22.2 509.8 20.9 511.2C19.5 512.6 18.2 514.2 17.1 515.8C16 517.5 15 519.3 14.3 521.2C13.5 523 12.9 525 12.5 527C12.1 529 11.9 531.1 11.9 533.2C12 535.2 12.2 537.3 12.6 539.4C13 541.4 13.6 543.5 14.5 545.4C15.3 547.4 16.3 549.3 17.5 551C18.7 552.8 20.1 554.5 21.7 556C23.2 557.5 24.9 558.9 26.8 560.1C28.6 561.3 30.6 562.4 32.6 563.2C34.7 564 36.8 564.6 39 565.1C41.2 565.5 43.5 565.7 45.8 565.7C48 565.6 50.3 565.4 52.6 564.9C54.8 564.4 57 563.7 59.1 562.8C61.3 561.9 63.3 560.8 65.3 559.5C67.2 558.1 69 556.6 70.7 554.9C72.3 553.2 73.8 551.3 75.1 549.3C76.4 547.4 77.5 545.2 78.4 543C79.3 540.7 80 538.3 80.4 536C80.9 533.6 81.1 531.1 81 528.6C81 526.2 80.7 523.7 80.2 521.3C79.7 518.8 78.9 516.4 77.9 514.1C76.9 511.8 75.7 509.6 74.2 507.5C72.8 505.4 71.1 503.5 69.3 501.7C67.5 499.9 64.3 497.7 63.2 496.9"/> | |
| 43 | <path d="M23.8 565.3C34.2 571.3 52.8 572.5 63.2 578.5"/> | |
| 44 | </g> | |
| 45 | </svg> |
crates/canvas/assets/backgrounds/tiles.svg created+389| ... | ... | @@ -0,0 +1,389 @@ |
| 1 | <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 612 792" preserveAspectRatio="none"> | |
| 2 | <defs> | |
| 3 | <path id="tile" d="M0 0Q15 2 30 0Q28 15 30 30Q15 28 0 30Q2 15 0 0Z" stroke="#c9d67a" stroke-width=".8"/> | |
| 4 | <radialGradient id="teal" cx=".45" cy=".42" r=".7"><stop offset="0" stop-color="#ffffff"/><stop offset=".75" stop-color="#5fa3b0"/></radialGradient> | |
| 5 | <radialGradient id="orange" cx=".45" cy=".42" r=".7"><stop offset="0" stop-color="#ffffff"/><stop offset=".75" stop-color="#f6ac78"/></radialGradient> | |
| 6 | <linearGradient id="across" x2="1"><stop offset="0" stop-color="white" stop-opacity=".27"/><stop offset=".96" stop-color="white" stop-opacity="0"/></linearGradient> | |
| 7 | <linearGradient id="down" x2="0" y2="1"><stop offset=".78" stop-color="white"/><stop offset=".98" stop-color="white" stop-opacity="0"/></linearGradient> | |
| 8 | <mask id="pale"><rect width="612" height="792" fill="url(#across)"/></mask> | |
| 9 | <mask id="fade"><rect width="612" height="792" fill="url(#down)"/></mask> | |
| 10 | </defs> | |
| 11 | <g mask="url(#fade)"> | |
| 12 | <use href="#tile" fill="url(#teal)" x="3" y="3"/> | |
| 13 | <use href="#tile" fill="url(#orange)" x="3" y="39"/> | |
| 14 | <use href="#tile" fill="url(#teal)" x="3" y="75"/> | |
| 15 | <use href="#tile" fill="url(#orange)" x="3" y="111"/> | |
| 16 | <use href="#tile" fill="url(#teal)" x="3" y="147"/> | |
| 17 | <use href="#tile" fill="url(#orange)" x="3" y="183"/> | |
| 18 | <use href="#tile" fill="url(#teal)" x="3" y="219"/> | |
| 19 | <use href="#tile" fill="url(#orange)" x="3" y="255"/> | |
| 20 | <use href="#tile" fill="url(#teal)" x="3" y="291"/> | |
| 21 | <use href="#tile" fill="url(#orange)" x="3" y="327"/> | |
| 22 | <use href="#tile" fill="url(#teal)" x="3" y="363"/> | |
| 23 | <use href="#tile" fill="url(#orange)" x="3" y="399"/> | |
| 24 | <use href="#tile" fill="url(#teal)" x="3" y="435"/> | |
| 25 | <use href="#tile" fill="url(#orange)" x="3" y="471"/> | |
| 26 | <use href="#tile" fill="url(#teal)" x="3" y="507"/> | |
| 27 | <use href="#tile" fill="url(#orange)" x="3" y="543"/> | |
| 28 | <use href="#tile" fill="url(#teal)" x="3" y="579"/> | |
| 29 | <use href="#tile" fill="url(#orange)" x="3" y="615"/> | |
| 30 | <use href="#tile" fill="url(#teal)" x="3" y="651"/> | |
| 31 | <use href="#tile" fill="url(#orange)" x="3" y="687"/> | |
| 32 | <use href="#tile" fill="url(#teal)" x="3" y="723"/> | |
| 33 | <use href="#tile" fill="url(#orange)" x="3" y="759"/> | |
| 34 | <g mask="url(#pale)"> | |
| 35 | <use href="#tile" fill="url(#orange)" x="39" y="3"/> | |
| 36 | <use href="#tile" fill="url(#teal)" x="75" y="3"/> | |
| 37 | <use href="#tile" fill="url(#orange)" x="111" y="3"/> | |
| 38 | <use href="#tile" fill="url(#teal)" x="147" y="3"/> | |
| 39 | <use href="#tile" fill="url(#orange)" x="183" y="3"/> | |
| 40 | <use href="#tile" fill="url(#teal)" x="219" y="3"/> | |
| 41 | <use href="#tile" fill="url(#orange)" x="255" y="3"/> | |
| 42 | <use href="#tile" fill="url(#teal)" x="291" y="3"/> | |
| 43 | <use href="#tile" fill="url(#orange)" x="327" y="3"/> | |
| 44 | <use href="#tile" fill="url(#teal)" x="363" y="3"/> | |
| 45 | <use href="#tile" fill="url(#orange)" x="399" y="3"/> | |
| 46 | <use href="#tile" fill="url(#teal)" x="435" y="3"/> | |
| 47 | <use href="#tile" fill="url(#orange)" x="471" y="3"/> | |
| 48 | <use href="#tile" fill="url(#teal)" x="507" y="3"/> | |
| 49 | <use href="#tile" fill="url(#orange)" x="543" y="3"/> | |
| 50 | <use href="#tile" fill="url(#teal)" x="579" y="3"/> | |
| 51 | <use href="#tile" fill="url(#teal)" x="39" y="39"/> | |
| 52 | <use href="#tile" fill="url(#orange)" x="75" y="39"/> | |
| 53 | <use href="#tile" fill="url(#teal)" x="111" y="39"/> | |
| 54 | <use href="#tile" fill="url(#orange)" x="147" y="39"/> | |
| 55 | <use href="#tile" fill="url(#teal)" x="183" y="39"/> | |
| 56 | <use href="#tile" fill="url(#orange)" x="219" y="39"/> | |
| 57 | <use href="#tile" fill="url(#teal)" x="255" y="39"/> | |
| 58 | <use href="#tile" fill="url(#orange)" x="291" y="39"/> | |
| 59 | <use href="#tile" fill="url(#teal)" x="327" y="39"/> | |
| 60 | <use href="#tile" fill="url(#orange)" x="363" y="39"/> | |
| 61 | <use href="#tile" fill="url(#teal)" x="399" y="39"/> | |
| 62 | <use href="#tile" fill="url(#orange)" x="435" y="39"/> | |
| 63 | <use href="#tile" fill="url(#teal)" x="471" y="39"/> | |
| 64 | <use href="#tile" fill="url(#orange)" x="507" y="39"/> | |
| 65 | <use href="#tile" fill="url(#teal)" x="543" y="39"/> | |
| 66 | <use href="#tile" fill="url(#orange)" x="579" y="39"/> | |
| 67 | <use href="#tile" fill="url(#orange)" x="39" y="75"/> | |
| 68 | <use href="#tile" fill="url(#teal)" x="75" y="75"/> | |
| 69 | <use href="#tile" fill="url(#orange)" x="111" y="75"/> | |
| 70 | <use href="#tile" fill="url(#teal)" x="147" y="75"/> | |
| 71 | <use href="#tile" fill="url(#orange)" x="183" y="75"/> | |
| 72 | <use href="#tile" fill="url(#teal)" x="219" y="75"/> | |
| 73 | <use href="#tile" fill="url(#orange)" x="255" y="75"/> | |
| 74 | <use href="#tile" fill="url(#teal)" x="291" y="75"/> | |
| 75 | <use href="#tile" fill="url(#orange)" x="327" y="75"/> | |
| 76 | <use href="#tile" fill="url(#teal)" x="363" y="75"/> | |
| 77 | <use href="#tile" fill="url(#orange)" x="399" y="75"/> | |
| 78 | <use href="#tile" fill="url(#teal)" x="435" y="75"/> | |
| 79 | <use href="#tile" fill="url(#orange)" x="471" y="75"/> | |
| 80 | <use href="#tile" fill="url(#teal)" x="507" y="75"/> | |
| 81 | <use href="#tile" fill="url(#orange)" x="543" y="75"/> | |
| 82 | <use href="#tile" fill="url(#teal)" x="579" y="75"/> | |
| 83 | <use href="#tile" fill="url(#teal)" x="39" y="111"/> | |
| 84 | <use href="#tile" fill="url(#orange)" x="75" y="111"/> | |
| 85 | <use href="#tile" fill="url(#teal)" x="111" y="111"/> | |
| 86 | <use href="#tile" fill="url(#orange)" x="147" y="111"/> | |
| 87 | <use href="#tile" fill="url(#teal)" x="183" y="111"/> | |
| 88 | <use href="#tile" fill="url(#orange)" x="219" y="111"/> | |
| 89 | <use href="#tile" fill="url(#teal)" x="255" y="111"/> | |
| 90 | <use href="#tile" fill="url(#orange)" x="291" y="111"/> | |
| 91 | <use href="#tile" fill="url(#teal)" x="327" y="111"/> | |
| 92 | <use href="#tile" fill="url(#orange)" x="363" y="111"/> | |
| 93 | <use href="#tile" fill="url(#teal)" x="399" y="111"/> | |
| 94 | <use href="#tile" fill="url(#orange)" x="435" y="111"/> | |
| 95 | <use href="#tile" fill="url(#teal)" x="471" y="111"/> | |
| 96 | <use href="#tile" fill="url(#orange)" x="507" y="111"/> | |
| 97 | <use href="#tile" fill="url(#teal)" x="543" y="111"/> | |
| 98 | <use href="#tile" fill="url(#orange)" x="579" y="111"/> | |
| 99 | <use href="#tile" fill="url(#orange)" x="39" y="147"/> | |
| 100 | <use href="#tile" fill="url(#teal)" x="75" y="147"/> | |
| 101 | <use href="#tile" fill="url(#orange)" x="111" y="147"/> | |
| 102 | <use href="#tile" fill="url(#teal)" x="147" y="147"/> | |
| 103 | <use href="#tile" fill="url(#orange)" x="183" y="147"/> | |
| 104 | <use href="#tile" fill="url(#teal)" x="219" y="147"/> | |
| 105 | <use href="#tile" fill="url(#orange)" x="255" y="147"/> | |
| 106 | <use href="#tile" fill="url(#teal)" x="291" y="147"/> | |
| 107 | <use href="#tile" fill="url(#orange)" x="327" y="147"/> | |
| 108 | <use href="#tile" fill="url(#teal)" x="363" y="147"/> | |
| 109 | <use href="#tile" fill="url(#orange)" x="399" y="147"/> | |
| 110 | <use href="#tile" fill="url(#teal)" x="435" y="147"/> | |
| 111 | <use href="#tile" fill="url(#orange)" x="471" y="147"/> | |
| 112 | <use href="#tile" fill="url(#teal)" x="507" y="147"/> | |
| 113 | <use href="#tile" fill="url(#orange)" x="543" y="147"/> | |
| 114 | <use href="#tile" fill="url(#teal)" x="579" y="147"/> | |
| 115 | <use href="#tile" fill="url(#teal)" x="39" y="183"/> | |
| 116 | <use href="#tile" fill="url(#orange)" x="75" y="183"/> | |
| 117 | <use href="#tile" fill="url(#teal)" x="111" y="183"/> | |
| 118 | <use href="#tile" fill="url(#orange)" x="147" y="183"/> | |
| 119 | <use href="#tile" fill="url(#teal)" x="183" y="183"/> | |
| 120 | <use href="#tile" fill="url(#orange)" x="219" y="183"/> | |
| 121 | <use href="#tile" fill="url(#teal)" x="255" y="183"/> | |
| 122 | <use href="#tile" fill="url(#orange)" x="291" y="183"/> | |
| 123 | <use href="#tile" fill="url(#teal)" x="327" y="183"/> | |
| 124 | <use href="#tile" fill="url(#orange)" x="363" y="183"/> | |
| 125 | <use href="#tile" fill="url(#teal)" x="399" y="183"/> | |
| 126 | <use href="#tile" fill="url(#orange)" x="435" y="183"/> | |
| 127 | <use href="#tile" fill="url(#teal)" x="471" y="183"/> | |
| 128 | <use href="#tile" fill="url(#orange)" x="507" y="183"/> | |
| 129 | <use href="#tile" fill="url(#teal)" x="543" y="183"/> | |
| 130 | <use href="#tile" fill="url(#orange)" x="579" y="183"/> | |
| 131 | <use href="#tile" fill="url(#orange)" x="39" y="219"/> | |
| 132 | <use href="#tile" fill="url(#teal)" x="75" y="219"/> | |
| 133 | <use href="#tile" fill="url(#orange)" x="111" y="219"/> | |
| 134 | <use href="#tile" fill="url(#teal)" x="147" y="219"/> | |
| 135 | <use href="#tile" fill="url(#orange)" x="183" y="219"/> | |
| 136 | <use href="#tile" fill="url(#teal)" x="219" y="219"/> | |
| 137 | <use href="#tile" fill="url(#orange)" x="255" y="219"/> | |
| 138 | <use href="#tile" fill="url(#teal)" x="291" y="219"/> | |
| 139 | <use href="#tile" fill="url(#orange)" x="327" y="219"/> | |
| 140 | <use href="#tile" fill="url(#teal)" x="363" y="219"/> | |
| 141 | <use href="#tile" fill="url(#orange)" x="399" y="219"/> | |
| 142 | <use href="#tile" fill="url(#teal)" x="435" y="219"/> | |
| 143 | <use href="#tile" fill="url(#orange)" x="471" y="219"/> | |
| 144 | <use href="#tile" fill="url(#teal)" x="507" y="219"/> | |
| 145 | <use href="#tile" fill="url(#orange)" x="543" y="219"/> | |
| 146 | <use href="#tile" fill="url(#teal)" x="579" y="219"/> | |
| 147 | <use href="#tile" fill="url(#teal)" x="39" y="255"/> | |
| 148 | <use href="#tile" fill="url(#orange)" x="75" y="255"/> | |
| 149 | <use href="#tile" fill="url(#teal)" x="111" y="255"/> | |
| 150 | <use href="#tile" fill="url(#orange)" x="147" y="255"/> | |
| 151 | <use href="#tile" fill="url(#teal)" x="183" y="255"/> | |
| 152 | <use href="#tile" fill="url(#orange)" x="219" y="255"/> | |
| 153 | <use href="#tile" fill="url(#teal)" x="255" y="255"/> | |
| 154 | <use href="#tile" fill="url(#orange)" x="291" y="255"/> | |
| 155 | <use href="#tile" fill="url(#teal)" x="327" y="255"/> | |
| 156 | <use href="#tile" fill="url(#orange)" x="363" y="255"/> | |
| 157 | <use href="#tile" fill="url(#teal)" x="399" y="255"/> | |
| 158 | <use href="#tile" fill="url(#orange)" x="435" y="255"/> | |
| 159 | <use href="#tile" fill="url(#teal)" x="471" y="255"/> | |
| 160 | <use href="#tile" fill="url(#orange)" x="507" y="255"/> | |
| 161 | <use href="#tile" fill="url(#teal)" x="543" y="255"/> | |
| 162 | <use href="#tile" fill="url(#orange)" x="579" y="255"/> | |
| 163 | <use href="#tile" fill="url(#orange)" x="39" y="291"/> | |
| 164 | <use href="#tile" fill="url(#teal)" x="75" y="291"/> | |
| 165 | <use href="#tile" fill="url(#orange)" x="111" y="291"/> | |
| 166 | <use href="#tile" fill="url(#teal)" x="147" y="291"/> | |
| 167 | <use href="#tile" fill="url(#orange)" x="183" y="291"/> | |
| 168 | <use href="#tile" fill="url(#teal)" x="219" y="291"/> | |
| 169 | <use href="#tile" fill="url(#orange)" x="255" y="291"/> | |
| 170 | <use href="#tile" fill="url(#teal)" x="291" y="291"/> | |
| 171 | <use href="#tile" fill="url(#orange)" x="327" y="291"/> | |
| 172 | <use href="#tile" fill="url(#teal)" x="363" y="291"/> | |
| 173 | <use href="#tile" fill="url(#orange)" x="399" y="291"/> | |
| 174 | <use href="#tile" fill="url(#teal)" x="435" y="291"/> | |
| 175 | <use href="#tile" fill="url(#orange)" x="471" y="291"/> | |
| 176 | <use href="#tile" fill="url(#teal)" x="507" y="291"/> | |
| 177 | <use href="#tile" fill="url(#orange)" x="543" y="291"/> | |
| 178 | <use href="#tile" fill="url(#teal)" x="579" y="291"/> | |
| 179 | <use href="#tile" fill="url(#teal)" x="39" y="327"/> | |
| 180 | <use href="#tile" fill="url(#orange)" x="75" y="327"/> | |
| 181 | <use href="#tile" fill="url(#teal)" x="111" y="327"/> | |
| 182 | <use href="#tile" fill="url(#orange)" x="147" y="327"/> | |
| 183 | <use href="#tile" fill="url(#teal)" x="183" y="327"/> | |
| 184 | <use href="#tile" fill="url(#orange)" x="219" y="327"/> | |
| 185 | <use href="#tile" fill="url(#teal)" x="255" y="327"/> | |
| 186 | <use href="#tile" fill="url(#orange)" x="291" y="327"/> | |
| 187 | <use href="#tile" fill="url(#teal)" x="327" y="327"/> | |
| 188 | <use href="#tile" fill="url(#orange)" x="363" y="327"/> | |
| 189 | <use href="#tile" fill="url(#teal)" x="399" y="327"/> | |
| 190 | <use href="#tile" fill="url(#orange)" x="435" y="327"/> | |
| 191 | <use href="#tile" fill="url(#teal)" x="471" y="327"/> | |
| 192 | <use href="#tile" fill="url(#orange)" x="507" y="327"/> | |
| 193 | <use href="#tile" fill="url(#teal)" x="543" y="327"/> | |
| 194 | <use href="#tile" fill="url(#orange)" x="579" y="327"/> | |
| 195 | <use href="#tile" fill="url(#orange)" x="39" y="363"/> | |
| 196 | <use href="#tile" fill="url(#teal)" x="75" y="363"/> | |
| 197 | <use href="#tile" fill="url(#orange)" x="111" y="363"/> | |
| 198 | <use href="#tile" fill="url(#teal)" x="147" y="363"/> | |
| 199 | <use href="#tile" fill="url(#orange)" x="183" y="363"/> | |
| 200 | <use href="#tile" fill="url(#teal)" x="219" y="363"/> | |
| 201 | <use href="#tile" fill="url(#orange)" x="255" y="363"/> | |
| 202 | <use href="#tile" fill="url(#teal)" x="291" y="363"/> | |
| 203 | <use href="#tile" fill="url(#orange)" x="327" y="363"/> | |
| 204 | <use href="#tile" fill="url(#teal)" x="363" y="363"/> | |
| 205 | <use href="#tile" fill="url(#orange)" x="399" y="363"/> | |
| 206 | <use href="#tile" fill="url(#teal)" x="435" y="363"/> | |
| 207 | <use href="#tile" fill="url(#orange)" x="471" y="363"/> | |
| 208 | <use href="#tile" fill="url(#teal)" x="507" y="363"/> | |
| 209 | <use href="#tile" fill="url(#orange)" x="543" y="363"/> | |
| 210 | <use href="#tile" fill="url(#teal)" x="579" y="363"/> | |
| 211 | <use href="#tile" fill="url(#teal)" x="39" y="399"/> | |
| 212 | <use href="#tile" fill="url(#orange)" x="75" y="399"/> | |
| 213 | <use href="#tile" fill="url(#teal)" x="111" y="399"/> | |
| 214 | <use href="#tile" fill="url(#orange)" x="147" y="399"/> | |
| 215 | <use href="#tile" fill="url(#teal)" x="183" y="399"/> | |
| 216 | <use href="#tile" fill="url(#orange)" x="219" y="399"/> | |
| 217 | <use href="#tile" fill="url(#teal)" x="255" y="399"/> | |
| 218 | <use href="#tile" fill="url(#orange)" x="291" y="399"/> | |
| 219 | <use href="#tile" fill="url(#teal)" x="327" y="399"/> | |
| 220 | <use href="#tile" fill="url(#orange)" x="363" y="399"/> | |
| 221 | <use href="#tile" fill="url(#teal)" x="399" y="399"/> | |
| 222 | <use href="#tile" fill="url(#orange)" x="435" y="399"/> | |
| 223 | <use href="#tile" fill="url(#teal)" x="471" y="399"/> | |
| 224 | <use href="#tile" fill="url(#orange)" x="507" y="399"/> | |
| 225 | <use href="#tile" fill="url(#teal)" x="543" y="399"/> | |
| 226 | <use href="#tile" fill="url(#orange)" x="579" y="399"/> | |
| 227 | <use href="#tile" fill="url(#orange)" x="39" y="435"/> | |
| 228 | <use href="#tile" fill="url(#teal)" x="75" y="435"/> | |
| 229 | <use href="#tile" fill="url(#orange)" x="111" y="435"/> | |
| 230 | <use href="#tile" fill="url(#teal)" x="147" y="435"/> | |
| 231 | <use href="#tile" fill="url(#orange)" x="183" y="435"/> | |
| 232 | <use href="#tile" fill="url(#teal)" x="219" y="435"/> | |
| 233 | <use href="#tile" fill="url(#orange)" x="255" y="435"/> | |
| 234 | <use href="#tile" fill="url(#teal)" x="291" y="435"/> | |
| 235 | <use href="#tile" fill="url(#orange)" x="327" y="435"/> | |
| 236 | <use href="#tile" fill="url(#teal)" x="363" y="435"/> | |
| 237 | <use href="#tile" fill="url(#orange)" x="399" y="435"/> | |
| 238 | <use href="#tile" fill="url(#teal)" x="435" y="435"/> | |
| 239 | <use href="#tile" fill="url(#orange)" x="471" y="435"/> | |
| 240 | <use href="#tile" fill="url(#teal)" x="507" y="435"/> | |
| 241 | <use href="#tile" fill="url(#orange)" x="543" y="435"/> | |
| 242 | <use href="#tile" fill="url(#teal)" x="579" y="435"/> | |
| 243 | <use href="#tile" fill="url(#teal)" x="39" y="471"/> | |
| 244 | <use href="#tile" fill="url(#orange)" x="75" y="471"/> | |
| 245 | <use href="#tile" fill="url(#teal)" x="111" y="471"/> | |
| 246 | <use href="#tile" fill="url(#orange)" x="147" y="471"/> | |
| 247 | <use href="#tile" fill="url(#teal)" x="183" y="471"/> | |
| 248 | <use href="#tile" fill="url(#orange)" x="219" y="471"/> | |
| 249 | <use href="#tile" fill="url(#teal)" x="255" y="471"/> | |
| 250 | <use href="#tile" fill="url(#orange)" x="291" y="471"/> | |
| 251 | <use href="#tile" fill="url(#teal)" x="327" y="471"/> | |
| 252 | <use href="#tile" fill="url(#orange)" x="363" y="471"/> | |
| 253 | <use href="#tile" fill="url(#teal)" x="399" y="471"/> | |
| 254 | <use href="#tile" fill="url(#orange)" x="435" y="471"/> | |
| 255 | <use href="#tile" fill="url(#teal)" x="471" y="471"/> | |
| 256 | <use href="#tile" fill="url(#orange)" x="507" y="471"/> | |
| 257 | <use href="#tile" fill="url(#teal)" x="543" y="471"/> | |
| 258 | <use href="#tile" fill="url(#orange)" x="579" y="471"/> | |
| 259 | <use href="#tile" fill="url(#orange)" x="39" y="507"/> | |
| 260 | <use href="#tile" fill="url(#teal)" x="75" y="507"/> | |
| 261 | <use href="#tile" fill="url(#orange)" x="111" y="507"/> | |
| 262 | <use href="#tile" fill="url(#teal)" x="147" y="507"/> | |
| 263 | <use href="#tile" fill="url(#orange)" x="183" y="507"/> | |
| 264 | <use href="#tile" fill="url(#teal)" x="219" y="507"/> | |
| 265 | <use href="#tile" fill="url(#orange)" x="255" y="507"/> | |
| 266 | <use href="#tile" fill="url(#teal)" x="291" y="507"/> | |
| 267 | <use href="#tile" fill="url(#orange)" x="327" y="507"/> | |
| 268 | <use href="#tile" fill="url(#teal)" x="363" y="507"/> | |
| 269 | <use href="#tile" fill="url(#orange)" x="399" y="507"/> | |
| 270 | <use href="#tile" fill="url(#teal)" x="435" y="507"/> | |
| 271 | <use href="#tile" fill="url(#orange)" x="471" y="507"/> | |
| 272 | <use href="#tile" fill="url(#teal)" x="507" y="507"/> | |
| 273 | <use href="#tile" fill="url(#orange)" x="543" y="507"/> | |
| 274 | <use href="#tile" fill="url(#teal)" x="579" y="507"/> | |
| 275 | <use href="#tile" fill="url(#teal)" x="39" y="543"/> | |
| 276 | <use href="#tile" fill="url(#orange)" x="75" y="543"/> | |
| 277 | <use href="#tile" fill="url(#teal)" x="111" y="543"/> | |
| 278 | <use href="#tile" fill="url(#orange)" x="147" y="543"/> | |
| 279 | <use href="#tile" fill="url(#teal)" x="183" y="543"/> | |
| 280 | <use href="#tile" fill="url(#orange)" x="219" y="543"/> | |
| 281 | <use href="#tile" fill="url(#teal)" x="255" y="543"/> | |
| 282 | <use href="#tile" fill="url(#orange)" x="291" y="543"/> | |
| 283 | <use href="#tile" fill="url(#teal)" x="327" y="543"/> | |
| 284 | <use href="#tile" fill="url(#orange)" x="363" y="543"/> | |
| 285 | <use href="#tile" fill="url(#teal)" x="399" y="543"/> | |
| 286 | <use href="#tile" fill="url(#orange)" x="435" y="543"/> | |
| 287 | <use href="#tile" fill="url(#teal)" x="471" y="543"/> | |
| 288 | <use href="#tile" fill="url(#orange)" x="507" y="543"/> | |
| 289 | <use href="#tile" fill="url(#teal)" x="543" y="543"/> | |
| 290 | <use href="#tile" fill="url(#orange)" x="579" y="543"/> | |
| 291 | <use href="#tile" fill="url(#orange)" x="39" y="579"/> | |
| 292 | <use href="#tile" fill="url(#teal)" x="75" y="579"/> | |
| 293 | <use href="#tile" fill="url(#orange)" x="111" y="579"/> | |
| 294 | <use href="#tile" fill="url(#teal)" x="147" y="579"/> | |
| 295 | <use href="#tile" fill="url(#orange)" x="183" y="579"/> | |
| 296 | <use href="#tile" fill="url(#teal)" x="219" y="579"/> | |
| 297 | <use href="#tile" fill="url(#orange)" x="255" y="579"/> | |
| 298 | <use href="#tile" fill="url(#teal)" x="291" y="579"/> | |
| 299 | <use href="#tile" fill="url(#orange)" x="327" y="579"/> | |
| 300 | <use href="#tile" fill="url(#teal)" x="363" y="579"/> | |
| 301 | <use href="#tile" fill="url(#orange)" x="399" y="579"/> | |
| 302 | <use href="#tile" fill="url(#teal)" x="435" y="579"/> | |
| 303 | <use href="#tile" fill="url(#orange)" x="471" y="579"/> | |
| 304 | <use href="#tile" fill="url(#teal)" x="507" y="579"/> | |
| 305 | <use href="#tile" fill="url(#orange)" x="543" y="579"/> | |
| 306 | <use href="#tile" fill="url(#teal)" x="579" y="579"/> | |
| 307 | <use href="#tile" fill="url(#teal)" x="39" y="615"/> | |
| 308 | <use href="#tile" fill="url(#orange)" x="75" y="615"/> | |
| 309 | <use href="#tile" fill="url(#teal)" x="111" y="615"/> | |
| 310 | <use href="#tile" fill="url(#orange)" x="147" y="615"/> | |
| 311 | <use href="#tile" fill="url(#teal)" x="183" y="615"/> | |
| 312 | <use href="#tile" fill="url(#orange)" x="219" y="615"/> | |
| 313 | <use href="#tile" fill="url(#teal)" x="255" y="615"/> | |
| 314 | <use href="#tile" fill="url(#orange)" x="291" y="615"/> | |
| 315 | <use href="#tile" fill="url(#teal)" x="327" y="615"/> | |
| 316 | <use href="#tile" fill="url(#orange)" x="363" y="615"/> | |
| 317 | <use href="#tile" fill="url(#teal)" x="399" y="615"/> | |
| 318 | <use href="#tile" fill="url(#orange)" x="435" y="615"/> | |
| 319 | <use href="#tile" fill="url(#teal)" x="471" y="615"/> | |
| 320 | <use href="#tile" fill="url(#orange)" x="507" y="615"/> | |
| 321 | <use href="#tile" fill="url(#teal)" x="543" y="615"/> | |
| 322 | <use href="#tile" fill="url(#orange)" x="579" y="615"/> | |
| 323 | <use href="#tile" fill="url(#orange)" x="39" y="651"/> | |
| 324 | <use href="#tile" fill="url(#teal)" x="75" y="651"/> | |
| 325 | <use href="#tile" fill="url(#orange)" x="111" y="651"/> | |
| 326 | <use href="#tile" fill="url(#teal)" x="147" y="651"/> | |
| 327 | <use href="#tile" fill="url(#orange)" x="183" y="651"/> | |
| 328 | <use href="#tile" fill="url(#teal)" x="219" y="651"/> | |
| 329 | <use href="#tile" fill="url(#orange)" x="255" y="651"/> | |
| 330 | <use href="#tile" fill="url(#teal)" x="291" y="651"/> | |
| 331 | <use href="#tile" fill="url(#orange)" x="327" y="651"/> | |
| 332 | <use href="#tile" fill="url(#teal)" x="363" y="651"/> | |
| 333 | <use href="#tile" fill="url(#orange)" x="399" y="651"/> | |
| 334 | <use href="#tile" fill="url(#teal)" x="435" y="651"/> | |
| 335 | <use href="#tile" fill="url(#orange)" x="471" y="651"/> | |
| 336 | <use href="#tile" fill="url(#teal)" x="507" y="651"/> | |
| 337 | <use href="#tile" fill="url(#orange)" x="543" y="651"/> | |
| 338 | <use href="#tile" fill="url(#teal)" x="579" y="651"/> | |
| 339 | <use href="#tile" fill="url(#teal)" x="39" y="687"/> | |
| 340 | <use href="#tile" fill="url(#orange)" x="75" y="687"/> | |
| 341 | <use href="#tile" fill="url(#teal)" x="111" y="687"/> | |
| 342 | <use href="#tile" fill="url(#orange)" x="147" y="687"/> | |
| 343 | <use href="#tile" fill="url(#teal)" x="183" y="687"/> | |
| 344 | <use href="#tile" fill="url(#orange)" x="219" y="687"/> | |
| 345 | <use href="#tile" fill="url(#teal)" x="255" y="687"/> | |
| 346 | <use href="#tile" fill="url(#orange)" x="291" y="687"/> | |
| 347 | <use href="#tile" fill="url(#teal)" x="327" y="687"/> | |
| 348 | <use href="#tile" fill="url(#orange)" x="363" y="687"/> | |
| 349 | <use href="#tile" fill="url(#teal)" x="399" y="687"/> | |
| 350 | <use href="#tile" fill="url(#orange)" x="435" y="687"/> | |
| 351 | <use href="#tile" fill="url(#teal)" x="471" y="687"/> | |
| 352 | <use href="#tile" fill="url(#orange)" x="507" y="687"/> | |
| 353 | <use href="#tile" fill="url(#teal)" x="543" y="687"/> | |
| 354 | <use href="#tile" fill="url(#orange)" x="579" y="687"/> | |
| 355 | <use href="#tile" fill="url(#orange)" x="39" y="723"/> | |
| 356 | <use href="#tile" fill="url(#teal)" x="75" y="723"/> | |
| 357 | <use href="#tile" fill="url(#orange)" x="111" y="723"/> | |
| 358 | <use href="#tile" fill="url(#teal)" x="147" y="723"/> | |
| 359 | <use href="#tile" fill="url(#orange)" x="183" y="723"/> | |
| 360 | <use href="#tile" fill="url(#teal)" x="219" y="723"/> | |
| 361 | <use href="#tile" fill="url(#orange)" x="255" y="723"/> | |
| 362 | <use href="#tile" fill="url(#teal)" x="291" y="723"/> | |
| 363 | <use href="#tile" fill="url(#orange)" x="327" y="723"/> | |
| 364 | <use href="#tile" fill="url(#teal)" x="363" y="723"/> | |
| 365 | <use href="#tile" fill="url(#orange)" x="399" y="723"/> | |
| 366 | <use href="#tile" fill="url(#teal)" x="435" y="723"/> | |
| 367 | <use href="#tile" fill="url(#orange)" x="471" y="723"/> | |
| 368 | <use href="#tile" fill="url(#teal)" x="507" y="723"/> | |
| 369 | <use href="#tile" fill="url(#orange)" x="543" y="723"/> | |
| 370 | <use href="#tile" fill="url(#teal)" x="579" y="723"/> | |
| 371 | <use href="#tile" fill="url(#teal)" x="39" y="759"/> | |
| 372 | <use href="#tile" fill="url(#orange)" x="75" y="759"/> | |
| 373 | <use href="#tile" fill="url(#teal)" x="111" y="759"/> | |
| 374 | <use href="#tile" fill="url(#orange)" x="147" y="759"/> | |
| 375 | <use href="#tile" fill="url(#teal)" x="183" y="759"/> | |
| 376 | <use href="#tile" fill="url(#orange)" x="219" y="759"/> | |
| 377 | <use href="#tile" fill="url(#teal)" x="255" y="759"/> | |
| 378 | <use href="#tile" fill="url(#orange)" x="291" y="759"/> | |
| 379 | <use href="#tile" fill="url(#teal)" x="327" y="759"/> | |
| 380 | <use href="#tile" fill="url(#orange)" x="363" y="759"/> | |
| 381 | <use href="#tile" fill="url(#teal)" x="399" y="759"/> | |
| 382 | <use href="#tile" fill="url(#orange)" x="435" y="759"/> | |
| 383 | <use href="#tile" fill="url(#teal)" x="471" y="759"/> | |
| 384 | <use href="#tile" fill="url(#orange)" x="507" y="759"/> | |
| 385 | <use href="#tile" fill="url(#teal)" x="543" y="759"/> | |
| 386 | <use href="#tile" fill="url(#orange)" x="579" y="759"/> | |
| 387 | </g> | |
| 388 | </g> | |
| 389 | </svg> |
crates/canvas/assets/backgrounds/triangles-title-margin.svg created+6| ... | ... | @@ -0,0 +1,6 @@ |
| 1 | <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 14 341" preserveAspectRatio="none"> | |
| 2 | <rect x="4" y="4" width="6" height="8" fill="#1ab39e"/> | |
| 3 | <rect x="4" y="16" width="6" height="16" fill="#d9d9d9"/> | |
| 4 | <rect x="4" y="34" width="6" height="50" fill="#404040"/> | |
| 5 | <rect x="4" y="88" width="6" height="249" fill="#1ab39e"/> | |
| 6 | </svg> |
crates/canvas/assets/backgrounds/triangles-title-top.svg created+6| ... | ... | @@ -0,0 +1,6 @@ |
| 1 | <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 357 69" preserveAspectRatio="none"> | |
| 2 | <path d="M0 0H66L106 67L0 28.6Z" fill="#d9d9d9"/> | |
| 3 | <path d="M141.5 0H154L132.5 42Z" fill="#7f7f7f"/> | |
| 4 | <path d="M159 0H244L138.5 42Z" fill="#7f7f7f"/> | |
| 5 | <path d="M268.5 0H353L184 41Z" fill="#404040"/> | |
| 6 | </svg> |
crates/canvas/assets/backgrounds/tulips.svg created+131| ... | ... | @@ -0,0 +1,131 @@ |
| 1 | <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 612 792" preserveAspectRatio="none"> | |
| 2 | <defs> | |
| 3 | <linearGradient id="r1" x2="0" y2="1"><stop offset="0" stop-color="#e2462e"/><stop offset="1" stop-color="#9d381c"/></linearGradient><linearGradient id="r2" x2="0" y2="1"><stop offset="0" stop-color="#b82b22"/><stop offset="1" stop-color="#7d2716"/></linearGradient> | |
| 4 | <linearGradient id="a1" x2="0" y2="1"><stop offset="0" stop-color="#ffd23a"/><stop offset="1" stop-color="#fa8600"/></linearGradient><linearGradient id="a2" x2="0" y2="1"><stop offset="0" stop-color="#f6990d"/><stop offset="1" stop-color="#c3790c"/></linearGradient> | |
| 5 | <linearGradient id="o1" x2="0" y2="1"><stop offset="0" stop-color="#fcb35e"/><stop offset="1" stop-color="#df452b"/></linearGradient><linearGradient id="o2" x2="0" y2="1"><stop offset="0" stop-color="#e0672c"/><stop offset="1" stop-color="#9f3c1c"/></linearGradient> | |
| 6 | <radialGradient id="reach" cx="120" cy="0" r="1" gradientTransform="translate(120 0) scale(420 330) translate(-120 0)" gradientUnits="userSpaceOnUse"><stop offset="0.45" stop-color="white"/><stop offset="1" stop-color="white" stop-opacity="0"/></radialGradient><mask id="art"><rect width="612" height="792" fill="url(#reach)"/></mask> | |
| 7 | <linearGradient id="down" x2="0" y2="1"><stop offset="0.28" stop-color="white"/><stop offset="0.5" stop-color="white" stop-opacity="0"/></linearGradient><mask id="strip-fade"><rect width="612" height="792" fill="url(#down)"/></mask> | |
| 8 | <clipPath id="strip"><rect width="55" height="792"/></clipPath> | |
| 9 | <clipPath id="page"><rect x="62" width="550" height="792"/></clipPath> | |
| 10 | <g id="field"><rect width="612" height="400" fill="#5d4012"/><g transform="translate(354.7179610072187 -7.475204080338864) rotate(-0.17204412826884408) scale(1.801703323320122)"> | |
| 11 | <path d="M-16-2C-18-20-10-34-4-38L0-26L4-38C10-34 18-20 16-2C14 10 6 16 0 16S-14 10-16-2Z" fill="url(#a1)"/> | |
| 12 | <path d="M-16-2C-12 6-4 10 0 10C-6 2-8-14-4-30C-12-24-16-12-16-2Z" fill="url(#a2)"/> | |
| 13 | </g><g transform="translate(45.28162494278361 -1.5907181115666837) rotate(-14.086252336220806) scale(2.388204713072701)"> | |
| 14 | <path d="M-16-2C-18-20-10-34-4-38L0-26L4-38C10-34 18-20 16-2C14 10 6 16 0 16S-14 10-16-2Z" fill="url(#a1)"/> | |
| 15 | <path d="M-16-2C-12 6-4 10 0 10C-6 2-8-14-4-30C-12-24-16-12-16-2Z" fill="url(#a2)"/> | |
| 16 | </g><g transform="translate(154.13324230371168 25.773336678898893) rotate(3.2678262277239227) scale(2.060031773764911)"> | |
| 17 | <path d="M-16-2C-18-20-10-34-4-38L0-26L4-38C10-34 18-20 16-2C14 10 6 16 0 16S-14 10-16-2Z" fill="url(#o1)"/> | |
| 18 | <path d="M-16-2C-12 6-4 10 0 10C-6 2-8-14-4-30C-12-24-16-12-16-2Z" fill="url(#o2)"/> | |
| 19 | </g><g transform="translate(309.30105622372 27.198816419637815) rotate(-10.334620176767324) scale(2.043171130209867)"> | |
| 20 | <path d="M-16-2C-18-20-10-34-4-38L0-26L4-38C10-34 18-20 16-2C14 10 6 16 0 16S-14 10-16-2Z" fill="url(#o1)"/> | |
| 21 | <path d="M-16-2C-12 6-4 10 0 10C-6 2-8-14-4-30C-12-24-16-12-16-2Z" fill="url(#o2)"/> | |
| 22 | </g><g transform="translate(329.2590423366911 69.84779175004361) rotate(12.750192563870058) scale(1.9841425916395257)"> | |
| 23 | <path d="M-16-2C-18-20-10-34-4-38L0-26L4-38C10-34 18-20 16-2C14 10 6 16 0 16S-14 10-16-2Z" fill="url(#a1)"/> | |
| 24 | <path d="M-16-2C-12 6-4 10 0 10C-6 2-8-14-4-30C-12-24-16-12-16-2Z" fill="url(#a2)"/> | |
| 25 | </g><g transform="translate(370.87482657030677 71.85762024840614) rotate(22.46617083978174) scale(2.293700183362649)"> | |
| 26 | <path d="M-16-2C-18-20-10-34-4-38L0-26L4-38C10-34 18-20 16-2C14 10 6 16 0 16S-14 10-16-2Z" fill="url(#o1)"/> | |
| 27 | <path d="M-16-2C-12 6-4 10 0 10C-6 2-8-14-4-30C-12-24-16-12-16-2Z" fill="url(#o2)"/> | |
| 28 | </g><g transform="translate(112.66137003126684 74.1305119377721) rotate(1.1009243751681375) scale(2.526083866982673)"> | |
| 29 | <path d="M-16-2C-18-20-10-34-4-38L0-26L4-38C10-34 18-20 16-2C14 10 6 16 0 16S-14 10-16-2Z" fill="url(#r1)"/> | |
| 30 | <path d="M-16-2C-12 6-4 10 0 10C-6 2-8-14-4-30C-12-24-16-12-16-2Z" fill="url(#r2)"/> | |
| 31 | </g><g transform="translate(74.11931341636591 83.12986381717688) rotate(9.208040911109052) scale(1.551556010582952)"> | |
| 32 | <path d="M-16-2C-18-20-10-34-4-38L0-26L4-38C10-34 18-20 16-2C14 10 6 16 0 16S-14 10-16-2Z" fill="url(#o1)"/> | |
| 33 | <path d="M-16-2C-12 6-4 10 0 10C-6 2-8-14-4-30C-12-24-16-12-16-2Z" fill="url(#o2)"/> | |
| 34 | </g><g transform="translate(224.0365637202624 96.5031829584713) rotate(22.5778807464944) scale(2.0336685577180496)"> | |
| 35 | <path d="M-16-2C-18-20-10-34-4-38L0-26L4-38C10-34 18-20 16-2C14 10 6 16 0 16S-14 10-16-2Z" fill="url(#a1)"/> | |
| 36 | <path d="M-16-2C-12 6-4 10 0 10C-6 2-8-14-4-30C-12-24-16-12-16-2Z" fill="url(#a2)"/> | |
| 37 | </g><g transform="translate(268.3109141263248 101.09273215290361) rotate(2.7113179159122858) scale(2.229107181266345)"> | |
| 38 | <path d="M-16-2C-18-20-10-34-4-38L0-26L4-38C10-34 18-20 16-2C14 10 6 16 0 16S-14 10-16-2Z" fill="url(#a1)"/> | |
| 39 | <path d="M-16-2C-12 6-4 10 0 10C-6 2-8-14-4-30C-12-24-16-12-16-2Z" fill="url(#a2)"/> | |
| 40 | </g><g transform="translate(58.94126199174708 104.68696451678211) rotate(5.274938896349319) scale(1.9513508283653462)"> | |
| 41 | <path d="M-16-2C-18-20-10-34-4-38L0-26L4-38C10-34 18-20 16-2C14 10 6 16 0 16S-14 10-16-2Z" fill="url(#a1)"/> | |
| 42 | <path d="M-16-2C-12 6-4 10 0 10C-6 2-8-14-4-30C-12-24-16-12-16-2Z" fill="url(#a2)"/> | |
| 43 | </g><g transform="translate(331.4757565704397 111.65870923907937) rotate(-12.084255581587206) scale(2.286048035142127)"> | |
| 44 | <path d="M-16-2C-18-20-10-34-4-38L0-26L4-38C10-34 18-20 16-2C14 10 6 16 0 16S-14 10-16-2Z" fill="url(#a1)"/> | |
| 45 | <path d="M-16-2C-12 6-4 10 0 10C-6 2-8-14-4-30C-12-24-16-12-16-2Z" fill="url(#a2)"/> | |
| 46 | </g><g transform="translate(113.47544510233143 115.9201233970027) rotate(-20.016974076246484) scale(1.8171144914174715)"> | |
| 47 | <path d="M-16-2C-18-20-10-34-4-38L0-26L4-38C10-34 18-20 16-2C14 10 6 16 0 16S-14 10-16-2Z" fill="url(#r1)"/> | |
| 48 | <path d="M-16-2C-12 6-4 10 0 10C-6 2-8-14-4-30C-12-24-16-12-16-2Z" fill="url(#r2)"/> | |
| 49 | </g><g transform="translate(399.61305859690526 116.3910660204541) rotate(-22.83957294191987) scale(2.519573380358554)"> | |
| 50 | <path d="M-16-2C-18-20-10-34-4-38L0-26L4-38C10-34 18-20 16-2C14 10 6 16 0 16S-14 10-16-2Z" fill="url(#o1)"/> | |
| 51 | <path d="M-16-2C-12 6-4 10 0 10C-6 2-8-14-4-30C-12-24-16-12-16-2Z" fill="url(#o2)"/> | |
| 52 | </g><g transform="translate(-3.232918832816516 119.17209141876597) rotate(-22.71958455450498) scale(1.482293549997923)"> | |
| 53 | <path d="M-16-2C-18-20-10-34-4-38L0-26L4-38C10-34 18-20 16-2C14 10 6 16 0 16S-14 10-16-2Z" fill="url(#r1)"/> | |
| 54 | <path d="M-16-2C-12 6-4 10 0 10C-6 2-8-14-4-30C-12-24-16-12-16-2Z" fill="url(#r2)"/> | |
| 55 | </g><g transform="translate(228.2209833847395 130.35217616524483) rotate(6.932232273281123) scale(2.4663552650495375)"> | |
| 56 | <path d="M-16-2C-18-20-10-34-4-38L0-26L4-38C10-34 18-20 16-2C14 10 6 16 0 16S-14 10-16-2Z" fill="url(#a1)"/> | |
| 57 | <path d="M-16-2C-12 6-4 10 0 10C-6 2-8-14-4-30C-12-24-16-12-16-2Z" fill="url(#a2)"/> | |
| 58 | </g><g transform="translate(84.5700377076439 153.720637888465) rotate(4.561249181124477) scale(2.0970494631093293)"> | |
| 59 | <path d="M-16-2C-18-20-10-34-4-38L0-26L4-38C10-34 18-20 16-2C14 10 6 16 0 16S-14 10-16-2Z" fill="url(#r1)"/> | |
| 60 | <path d="M-16-2C-12 6-4 10 0 10C-6 2-8-14-4-30C-12-24-16-12-16-2Z" fill="url(#r2)"/> | |
| 61 | </g><g transform="translate(406.7064168633326 160.84028727203892) rotate(-14.638694637806687) scale(1.6676635444417776)"> | |
| 62 | <path d="M-16-2C-18-20-10-34-4-38L0-26L4-38C10-34 18-20 16-2C14 10 6 16 0 16S-14 10-16-2Z" fill="url(#o1)"/> | |
| 63 | <path d="M-16-2C-12 6-4 10 0 10C-6 2-8-14-4-30C-12-24-16-12-16-2Z" fill="url(#o2)"/> | |
| 64 | </g><g transform="translate(263.8404619751218 164.3049865293584) rotate(-18.924190324528475) scale(2.5974965272300246)"> | |
| 65 | <path d="M-16-2C-18-20-10-34-4-38L0-26L4-38C10-34 18-20 16-2C14 10 6 16 0 16S-14 10-16-2Z" fill="url(#a1)"/> | |
| 66 | <path d="M-16-2C-12 6-4 10 0 10C-6 2-8-14-4-30C-12-24-16-12-16-2Z" fill="url(#a2)"/> | |
| 67 | </g><g transform="translate(326.4566414130759 174.35248292454375) rotate(-3.542742265937182) scale(1.6007939672691576)"> | |
| 68 | <path d="M-16-2C-18-20-10-34-4-38L0-26L4-38C10-34 18-20 16-2C14 10 6 16 0 16S-14 10-16-2Z" fill="url(#a1)"/> | |
| 69 | <path d="M-16-2C-12 6-4 10 0 10C-6 2-8-14-4-30C-12-24-16-12-16-2Z" fill="url(#a2)"/> | |
| 70 | </g><g transform="translate(173.834647281628 180.64625358268566) rotate(19.87882966248177) scale(2.4075867433587557)"> | |
| 71 | <path d="M-16-2C-18-20-10-34-4-38L0-26L4-38C10-34 18-20 16-2C14 10 6 16 0 16S-14 10-16-2Z" fill="url(#r1)"/> | |
| 72 | <path d="M-16-2C-12 6-4 10 0 10C-6 2-8-14-4-30C-12-24-16-12-16-2Z" fill="url(#r2)"/> | |
| 73 | </g><g transform="translate(195.9966221036221 184.8329145219545) rotate(1.7526206729860334) scale(1.4522905887756523)"> | |
| 74 | <path d="M-16-2C-18-20-10-34-4-38L0-26L4-38C10-34 18-20 16-2C14 10 6 16 0 16S-14 10-16-2Z" fill="url(#o1)"/> | |
| 75 | <path d="M-16-2C-12 6-4 10 0 10C-6 2-8-14-4-30C-12-24-16-12-16-2Z" fill="url(#o2)"/> | |
| 76 | </g><g transform="translate(320.0810231825232 187.2316791905006) rotate(-7.45903349465863) scale(1.644616877800562)"> | |
| 77 | <path d="M-16-2C-18-20-10-34-4-38L0-26L4-38C10-34 18-20 16-2C14 10 6 16 0 16S-14 10-16-2Z" fill="url(#o1)"/> | |
| 78 | <path d="M-16-2C-12 6-4 10 0 10C-6 2-8-14-4-30C-12-24-16-12-16-2Z" fill="url(#o2)"/> | |
| 79 | </g><g transform="translate(362.4616213073707 204.33500853857592) rotate(1.5657375923509136) scale(1.556255627311708)"> | |
| 80 | <path d="M-16-2C-18-20-10-34-4-38L0-26L4-38C10-34 18-20 16-2C14 10 6 16 0 16S-14 10-16-2Z" fill="url(#a1)"/> | |
| 81 | <path d="M-16-2C-12 6-4 10 0 10C-6 2-8-14-4-30C-12-24-16-12-16-2Z" fill="url(#a2)"/> | |
| 82 | </g><g transform="translate(385.37223996074596 208.6691000375746) rotate(-0.9632792355501252) scale(1.6285145918980666)"> | |
| 83 | <path d="M-16-2C-18-20-10-34-4-38L0-26L4-38C10-34 18-20 16-2C14 10 6 16 0 16S-14 10-16-2Z" fill="url(#a1)"/> | |
| 84 | <path d="M-16-2C-12 6-4 10 0 10C-6 2-8-14-4-30C-12-24-16-12-16-2Z" fill="url(#a2)"/> | |
| 85 | </g><g transform="translate(93.96373635480838 219.84029753758793) rotate(8.309277946934067) scale(2.182411672746169)"> | |
| 86 | <path d="M-16-2C-18-20-10-34-4-38L0-26L4-38C10-34 18-20 16-2C14 10 6 16 0 16S-14 10-16-2Z" fill="url(#a1)"/> | |
| 87 | <path d="M-16-2C-12 6-4 10 0 10C-6 2-8-14-4-30C-12-24-16-12-16-2Z" fill="url(#a2)"/> | |
| 88 | </g><g transform="translate(370.11332188223554 225.54123601506626) rotate(7.561588521706291) scale(1.4879309335087016)"> | |
| 89 | <path d="M-16-2C-18-20-10-34-4-38L0-26L4-38C10-34 18-20 16-2C14 10 6 16 0 16S-14 10-16-2Z" fill="url(#a1)"/> | |
| 90 | <path d="M-16-2C-12 6-4 10 0 10C-6 2-8-14-4-30C-12-24-16-12-16-2Z" fill="url(#a2)"/> | |
| 91 | </g><g transform="translate(144.89274702187907 230.77888168320735) rotate(10.195031614415342) scale(2.4817270991620717)"> | |
| 92 | <path d="M-16-2C-18-20-10-34-4-38L0-26L4-38C10-34 18-20 16-2C14 10 6 16 0 16S-14 10-16-2Z" fill="url(#a1)"/> | |
| 93 | <path d="M-16-2C-12 6-4 10 0 10C-6 2-8-14-4-30C-12-24-16-12-16-2Z" fill="url(#a2)"/> | |
| 94 | </g><g transform="translate(74.05235603942211 231.4311245324479) rotate(21.022290275798944) scale(2.023875028963595)"> | |
| 95 | <path d="M-16-2C-18-20-10-34-4-38L0-26L4-38C10-34 18-20 16-2C14 10 6 16 0 16S-14 10-16-2Z" fill="url(#a1)"/> | |
| 96 | <path d="M-16-2C-12 6-4 10 0 10C-6 2-8-14-4-30C-12-24-16-12-16-2Z" fill="url(#a2)"/> | |
| 97 | </g><g transform="translate(138.11450165905876 234.6927519102449) rotate(-15.35627277028433) scale(1.6185812350689626)"> | |
| 98 | <path d="M-16-2C-18-20-10-34-4-38L0-26L4-38C10-34 18-20 16-2C14 10 6 16 0 16S-14 10-16-2Z" fill="url(#a1)"/> | |
| 99 | <path d="M-16-2C-12 6-4 10 0 10C-6 2-8-14-4-30C-12-24-16-12-16-2Z" fill="url(#a2)"/> | |
| 100 | </g><g transform="translate(36.91660900222494 261.2845250011288) rotate(19.360038841293324) scale(1.5999674966296338)"> | |
| 101 | <path d="M-16-2C-18-20-10-34-4-38L0-26L4-38C10-34 18-20 16-2C14 10 6 16 0 16S-14 10-16-2Z" fill="url(#a1)"/> | |
| 102 | <path d="M-16-2C-12 6-4 10 0 10C-6 2-8-14-4-30C-12-24-16-12-16-2Z" fill="url(#a2)"/> | |
| 103 | </g><g transform="translate(42.368047975987075 262.133347126437) rotate(-17.012164096105252) scale(2.166866617682868)"> | |
| 104 | <path d="M-16-2C-18-20-10-34-4-38L0-26L4-38C10-34 18-20 16-2C14 10 6 16 0 16S-14 10-16-2Z" fill="url(#o1)"/> | |
| 105 | <path d="M-16-2C-12 6-4 10 0 10C-6 2-8-14-4-30C-12-24-16-12-16-2Z" fill="url(#o2)"/> | |
| 106 | </g><g transform="translate(241.84940788676823 262.45310850561003) rotate(16.267823183185996) scale(2.565327999945795)"> | |
| 107 | <path d="M-16-2C-18-20-10-34-4-38L0-26L4-38C10-34 18-20 16-2C14 10 6 16 0 16S-14 10-16-2Z" fill="url(#r1)"/> | |
| 108 | <path d="M-16-2C-12 6-4 10 0 10C-6 2-8-14-4-30C-12-24-16-12-16-2Z" fill="url(#r2)"/> | |
| 109 | </g><g transform="translate(280.9809673029485 277.1915567316862) rotate(-13.211634995205435) scale(1.909821661603557)"> | |
| 110 | <path d="M-16-2C-18-20-10-34-4-38L0-26L4-38C10-34 18-20 16-2C14 10 6 16 0 16S-14 10-16-2Z" fill="url(#a1)"/> | |
| 111 | <path d="M-16-2C-12 6-4 10 0 10C-6 2-8-14-4-30C-12-24-16-12-16-2Z" fill="url(#a2)"/> | |
| 112 | </g><g transform="translate(161.93599793279435 283.8845322128177) rotate(17.68271089736306) scale(1.6436699513083093)"> | |
| 113 | <path d="M-16-2C-18-20-10-34-4-38L0-26L4-38C10-34 18-20 16-2C14 10 6 16 0 16S-14 10-16-2Z" fill="url(#a1)"/> | |
| 114 | <path d="M-16-2C-12 6-4 10 0 10C-6 2-8-14-4-30C-12-24-16-12-16-2Z" fill="url(#a2)"/> | |
| 115 | </g><g transform="translate(305.4495720213634 295.81811943448037) rotate(9.07934574400985) scale(2.4948213476066723)"> | |
| 116 | <path d="M-16-2C-18-20-10-34-4-38L0-26L4-38C10-34 18-20 16-2C14 10 6 16 0 16S-14 10-16-2Z" fill="url(#a1)"/> | |
| 117 | <path d="M-16-2C-12 6-4 10 0 10C-6 2-8-14-4-30C-12-24-16-12-16-2Z" fill="url(#a2)"/> | |
| 118 | </g><g transform="translate(19.460965772915515 306.15742631314976) rotate(15.089258112578975) scale(1.813967322683937)"> | |
| 119 | <path d="M-16-2C-18-20-10-34-4-38L0-26L4-38C10-34 18-20 16-2C14 10 6 16 0 16S-14 10-16-2Z" fill="url(#r1)"/> | |
| 120 | <path d="M-16-2C-12 6-4 10 0 10C-6 2-8-14-4-30C-12-24-16-12-16-2Z" fill="url(#r2)"/> | |
| 121 | </g><g transform="translate(419.12296836339124 327.54130688714156) rotate(14.705493230275664) scale(1.501940884035391)"> | |
| 122 | <path d="M-16-2C-18-20-10-34-4-38L0-26L4-38C10-34 18-20 16-2C14 10 6 16 0 16S-14 10-16-2Z" fill="url(#a1)"/> | |
| 123 | <path d="M-16-2C-12 6-4 10 0 10C-6 2-8-14-4-30C-12-24-16-12-16-2Z" fill="url(#a2)"/> | |
| 124 | </g></g> | |
| 125 | </defs> | |
| 126 | <g mask="url(#strip-fade)"> | |
| 127 | <use href="#field" clip-path="url(#strip)"/> | |
| 128 | <rect x="55" width="7" height="792" fill="#fc700e"/> | |
| 129 | </g> | |
| 130 | <g mask="url(#art)"><use href="#field" clip-path="url(#page)" opacity=".38"/></g> | |
| 131 | </svg> |
crates/canvas/assets/backgrounds/window-reflection.svg created+14| ... | ... | @@ -0,0 +1,14 @@ |
| 1 | <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 612 792" preserveAspectRatio="none"> | |
| 2 | <defs> | |
| 3 | <radialGradient id="wood" cx="80" cy="0" r="1" gradientTransform="translate(80 0) scale(620 400) translate(-80 0)" gradientUnits="userSpaceOnUse"><stop offset="0.45" stop-color="#2a1a10"/><stop offset="1" stop-color="#2a1a10" stop-opacity="0"/></radialGradient><radialGradient id="light" cx="80" cy="0" r="1" gradientTransform="translate(80 0) scale(620 400) translate(-80 0)" gradientUnits="userSpaceOnUse"><stop offset="0.45" stop-color="#e8c070"/><stop offset="1" stop-color="#e8c070" stop-opacity="0"/></radialGradient><radialGradient id="lead" cx="80" cy="0" r="1" gradientTransform="translate(80 0) scale(620 400) translate(-80 0)" gradientUnits="userSpaceOnUse"><stop offset="0.45" stop-color="#e4be97"/><stop offset="1" stop-color="#e4be97" stop-opacity="0"/></radialGradient><radialGradient id="beam" cx="80" cy="0" r="1" gradientTransform="translate(80 0) scale(620 400) translate(-80 0)" gradientUnits="userSpaceOnUse"><stop offset="0.45" stop-color="#8a5a2a"/><stop offset="1" stop-color="#8a5a2a" stop-opacity="0"/></radialGradient> | |
| 4 | <radialGradient id="pale" cx="80" cy="0" r="1" gradientTransform="translate(80 0) scale(620 400) translate(-80 0)" gradientUnits="userSpaceOnUse"><stop offset="0.45" stop-color="#f7f4f0"/><stop offset="1" stop-color="#f7f4f0" stop-opacity="0"/></radialGradient><radialGradient id="thread" cx="80" cy="0" r="1" gradientTransform="translate(80 0) scale(620 400) translate(-80 0)" gradientUnits="userSpaceOnUse"><stop offset="0.45" stop-color="#dedcd9"/><stop offset="1" stop-color="#dedcd9" stop-opacity="0"/></radialGradient><radialGradient id="mullion" cx="80" cy="0" r="1" gradientTransform="translate(80 0) scale(620 400) translate(-80 0)" gradientUnits="userSpaceOnUse"><stop offset="0.45" stop-color="#d3d0cd"/><stop offset="1" stop-color="#d3d0cd" stop-opacity="0"/></radialGradient><radialGradient id="transom" cx="80" cy="0" r="1" gradientTransform="translate(80 0) scale(620 400) translate(-80 0)" gradientUnits="userSpaceOnUse"><stop offset="0.45" stop-color="#e2dfdc"/><stop offset="1" stop-color="#e2dfdc" stop-opacity="0"/></radialGradient> | |
| 5 | </defs> | |
| 6 | <path d="M0 0H420L0 150Z" fill="url(#wood)"/> | |
| 7 | <path d="M0 105L21.4 142.4M0 35L54.6 130.5M20 0L87.8 118.6M60 0L121 106.8M100 0L154.2 94.9M140 0L187.5 83.1M180 0L220.7 71.2M220 0L253.9 59.3M260 0L287.1 47.5M300 0L320.3 35.6M340 0L353.6 23.7M380 0L386.8 11.9" stroke="url(#light)" stroke-width="5"/> | |
| 8 | <path d="M0 20L40 0M0 60L120 -0M0 100L200 0M0 140L280 0M210 75L360 0" stroke="url(#lead)" stroke-width="2"/> | |
| 9 | <path d="M420 0H612V792H0V150Z" fill="url(#pale)"/> | |
| 10 | <path d="M0 158L56 130M0 166L112 110M0 174L168 90M0 182L224 70M0 190L280 50M0 198L336 30M0 206L392 10M0 214L428 0M0 222L444 0M0 230L460 0M0 238L476 0M0 246L492 0M0 254L508 0M0 262L524 0M0 270L540 0M0 278L556 0M0 286L572 0M0 294L588 0M0 302L604 0M0 310L612 4M0 318L612 12M0 326L612 20M0 334L612 28M0 342L612 36M0 350L612 44M0 358L612 52M0 366L612 60M0 374L612 68M0 382L612 76M0 390L612 84M0 398L612 92M0 406L612 100M0 414L612 108M0 422L612 116M0 430L612 124M0 438L612 132M0 446L612 140M0 454L612 148M0 462L612 156M0 470L612 164M0 478L612 172M0 486L612 180M0 494L612 188M0 502L612 196M0 510L612 204M0 518L612 212M0 526L612 220M0 534L612 228M0 542L612 236M0 550L612 244M0 558L612 252M0 566L612 260M0 574L612 268M0 582L612 276M0 590L612 284M0 598L612 292M0 606L612 300M0 614L612 308M0 622L612 316M0 630L612 324M0 638L612 332M0 646L612 340M0 654L612 348M0 662L612 356M0 670L612 364M0 678L612 372M0 686L612 380M0 694L612 388M0 702L612 396M0 710L612 404M0 718L612 412M0 726L612 420M0 734L612 428M0 742L612 436M0 750L612 444M0 758L612 452M0 766L612 460M0 774L612 468M0 782L612 476M0 790L612 484M12 792L612 492M28 792L612 500M44 792L612 508M60 792L612 516M76 792L612 524M92 792L612 532M108 792L612 540M124 792L612 548M140 792L612 556" stroke="url(#thread)" stroke-width=".8"/> | |
| 11 | <path d="M0 525L100 700M0 455L140 700M0 385L180 700M0 315L220 700M0 245L260 700M0 175L300 700M21.4 142.4L340 700M54.6 130.5L380 700M87.8 118.6L420 700M121 106.8L460 700M154.2 94.9L500 700M187.5 83.1L540 700M220.7 71.2L580 700M253.9 59.3L612 686M287.1 47.5L612 616M320.3 35.6L612 546M353.6 23.7L612 476M386.8 11.9L612 406M420 0L612 336M460 0L612 266M500 0L612 196M540 0L612 126M580 0L612 56" stroke="url(#mullion)" stroke-width="9"/> | |
| 12 | <path d="M0 180L210 75M0 220L440 0M0 260L520 0M0 300L600 0M0 340L612 34M0 380L612 74M0 420L612 114M0 460L612 154M0 500L612 194M0 540L612 234M0 580L612 274M0 620L612 314M0 660L612 354M0 700L612 394M0 740L612 434M0 780L612 474M56 792L612 514M136 792L612 554M216 792L612 594M296 792L612 634M376 792L612 674M456 792L612 714M536 792L612 754" stroke="url(#transom)" stroke-width="4"/> | |
| 13 | <path d="M0 150L420 0" stroke="url(#beam)" stroke-width="6"/> | |
| 14 | </svg> |
crates/canvas/assets/backgrounds/writing.svg created+25| ... | ... | @@ -0,0 +1,25 @@ |
| 1 | <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 612 792" preserveAspectRatio="none"> | |
| 2 | <defs> | |
| 3 | <linearGradient id="fx0" x2="612" gradientUnits="userSpaceOnUse"><stop offset="0.490" stop-color="white"/><stop offset="0.882" stop-color="white" stop-opacity="0"/></linearGradient> | |
| 4 | <linearGradient id="fy0" x2="0" y2="792" gradientUnits="userSpaceOnUse"><stop offset="0.480" stop-color="white"/><stop offset="0.682" stop-color="white" stop-opacity="0"/></linearGradient> | |
| 5 | <mask id="across"><rect width="612" height="792" fill="url(#fx0)"/></mask> | |
| 6 | <mask id="down"><rect width="612" height="792" fill="url(#fy0)"/></mask> | |
| 7 | <radialGradient id="corner" cx="120" cy="0" r="1" gradientTransform="translate(120 0) scale(300 260) translate(-120 0)" gradientUnits="userSpaceOnUse"><stop offset=".4" stop-color="white"/><stop offset="1" stop-color="white" stop-opacity="0"/></radialGradient> | |
| 8 | <mask id="art"><rect width="612" height="792" fill="url(#corner)"/></mask> | |
| 9 | <filter id="blur" x="-20%" y="-20%" width="140%" height="140%"><feGaussianBlur stdDeviation="3.5"/></filter> | |
| 10 | </defs> | |
| 11 | <rect width="30" height="560" fill="#e6eef7" mask="url(#down)"/> | |
| 12 | <g mask="url(#art)" filter="url(#blur)"> | |
| 13 | <path d="M20 -10L260 260" stroke="#c3c8cc" stroke-width="14"/> | |
| 14 | <g fill="none" stroke="#b9bec2" stroke-width="5" stroke-linecap="round"> | |
| 15 | <path d="M60 60q12-14 24 0"/><path d="M90 94q12-14 24 0"/><path d="M120 128q12-14 24 0"/><path d="M150 162q12-14 24 0"/><path d="M180 196q12-14 24 0"/> | |
| 16 | </g> | |
| 17 | <path d="M170 -20L250 40" stroke="#464a4f" stroke-width="10" stroke-linecap="round"/> | |
| 18 | <path d="M250 40L330 105" stroke="#c7c9cc" stroke-width="8" stroke-linecap="round"/> | |
| 19 | <ellipse cx="300" cy="70" rx="80" ry="50" fill="#e7dfdb"/><path d="M-10 120L140 290" stroke="#d6dde2" stroke-width="30"/> | |
| 20 | </g> | |
| 21 | <rect x="31" y="26" width="12" height="766" fill="#2f6780" mask="url(#down)"/> | |
| 22 | <rect x="45" y="40" width="3" height="752" fill="#94b0bb" mask="url(#down)"/> | |
| 23 | <rect x="31" y="26" width="581" height="12" fill="#2f6780" mask="url(#across)"/> | |
| 24 | <rect x="45" y="40" width="567" height="3" fill="#94b0bb" mask="url(#across)"/> | |
| 25 | </svg> |
crates/canvas/examples/render_page.rs+6-3| ... | ... | @@ -1,4 +1,4 @@ |
| 1 | use canvas::gpu::{Viewport, page::PageScene}; | |
| 1 | use canvas::gpu::{Paper, Viewport, page::PageScene}; | |
| 2 | 2 | use canvas::layout::TextEngine; |
| 3 | 3 | use draw::Renderer; |
| 4 | 4 | use onestore::page::Page; |
| ... | ... | @@ -38,9 +38,12 @@ fn main() -> Result<(), Box<dyn std::error::Error>> { |
| 38 | 38 | }; |
| 39 | 39 | let margin_origin = page.margin_origin; |
| 40 | 40 | let object_count = page.objects.len(); |
| 41 | let scene = PageScene::new(page, &mut engine)?; | |
| 41 | let mut scene = PageScene::new(page, &mut engine)?; | |
| 42 | while !scene.update_backgrounds(Paper::WHITE, 96.0 / 72.0, std::task::Waker::noop()) { | |
| 43 | std::thread::sleep(Duration::from_millis(1)); | |
| 44 | } | |
| 42 | 45 | let mut primitives = Vec::new(); |
| 43 | scene.append_primitives(&mut primitives, [0.0; 2])?; | |
| 46 | scene.append_primitives(&mut primitives, [0.0; 2], Paper::WHITE)?; | |
| 44 | 47 | let instance = wgpu::Instance::new(wgpu::InstanceDescriptor::new_without_display_handle()); |
| 45 | 48 | let adapter = pollster::block_on(instance.request_adapter(&Default::default()))?; |
| 46 | 49 | let (device, queue) = pollster::block_on(adapter.request_device(&Default::default()))?; |
crates/canvas/src/document.rs+507-74| ... | ... | @@ -16,6 +16,11 @@ pub struct TextPosition { |
| 16 | 16 | #[derive(Clone, Debug, PartialEq)] |
| 17 | 17 | pub struct TextDocument { |
| 18 | 18 | nodes: Vec<PageParagraph>, |
| 19 | /// The paragraph position of each root node's first text leaf, then the paragraph count, so | |
| 20 | /// a position finds its node by search instead of walking every leaf before it. | |
| 21 | starts: Vec<usize>, | |
| 22 | /// Root indices of the tables, so a cell is found without walking every node. | |
| 23 | tables: Vec<usize>, | |
| 19 | 24 | } |
| 20 | 25 | |
| 21 | 26 | #[derive(Clone, Debug, PartialEq)] |
| ... | ... | @@ -123,85 +128,98 @@ pub(crate) fn swap_columns(nodes: &mut [PageParagraph], widths: &mut BTreeMap<Ex |
| 123 | 128 | } |
| 124 | 129 | } |
| 125 | 130 | |
| 131 | /// Identities a node holds itself; paragraphs in its cells hold their own. | |
| 132 | fn owned_ids(node: &PageParagraph) -> impl Iterator<Item = ExGuid> + '_ { | |
| 133 | let (content, rows) = match &node.content { | |
| 134 | ParagraphContent::Text(text) => (text.id, &[][..]), | |
| 135 | ParagraphContent::Image(image) => (image.id, &[][..]), | |
| 136 | ParagraphContent::Attachment(file) => (file.id, &[][..]), | |
| 137 | ParagraphContent::Ink(ink) => (ink.id, &[][..]), | |
| 138 | ParagraphContent::Unsupported(unsupported) => (unsupported.id, &[][..]), | |
| 139 | ParagraphContent::Table(table) => (table.id, table.rows.as_slice()), | |
| 140 | }; | |
| 141 | [node.id, content].into_iter().chain( | |
| 142 | rows.iter() | |
| 143 | .flat_map(|row| std::iter::once(row.id).chain(row.cells.iter().map(|cell| cell.id))), | |
| 144 | ) | |
| 145 | } | |
| 146 | ||
| 126 | 147 | pub(crate) fn validate_nodes( |
| 127 | 148 | nodes: &[PageParagraph], |
| 128 | edit: Option<&DocumentEdit>, | |
| 129 | 149 | ids: &mut BTreeSet<ExGuid>, |
| 130 | 150 | ) -> Result<(), EditError> { |
| 131 | let mut pending = vec![(None, nodes, 0)]; | |
| 132 | while let Some((container, nodes, depth)) = pending.pop() { | |
| 151 | if nodes.is_empty() { | |
| 152 | return Err(EditError::InvalidRange); | |
| 153 | } | |
| 154 | validate_run(nodes, &[], 0, ids) | |
| 155 | } | |
| 156 | ||
| 157 | /// Validates `nodes`, which follow `earlier` in a container at `depth`, and every container | |
| 158 | /// nested in them; `earlier` is searched only for parents the run lacks. | |
| 159 | fn validate_run( | |
| 160 | nodes: &[PageParagraph], | |
| 161 | earlier: &[PageParagraph], | |
| 162 | depth: usize, | |
| 163 | ids: &mut BTreeSet<ExGuid>, | |
| 164 | ) -> Result<(), EditError> { | |
| 165 | let mut pending = vec![(nodes, earlier, depth)]; | |
| 166 | while let Some((nodes, earlier, depth)) = pending.pop() { | |
| 133 | 167 | if depth > 64 { |
| 134 | 168 | return Err(EditError::InvalidStructure); |
| 135 | 169 | } |
| 136 | let mut nodes = edited_nodes(nodes, container, edit).peekable(); | |
| 137 | if nodes.peek().is_none() { | |
| 138 | return Err(EditError::InvalidRange); | |
| 139 | } | |
| 140 | 170 | let mut parents = BTreeMap::new(); |
| 141 | 171 | for node in nodes { |
| 142 | if !ids.insert(node.id) | |
| 172 | let parent = |id| { | |
| 173 | parents.get(&id).copied().or_else(|| { | |
| 174 | earlier | |
| 175 | .iter() | |
| 176 | .rev() | |
| 177 | .find(|node| node.id == id) | |
| 178 | .map(|node| node.level) | |
| 179 | }) | |
| 180 | }; | |
| 181 | if owned_ids(node).any(|id| !ids.insert(id)) | |
| 143 | 182 | || node.level == 0 |
| 144 | 183 | || node |
| 145 | 184 | .parent |
| 146 | .is_some_and(|id| parents.get(&id).is_none_or(|level| *level >= node.level)) | |
| 185 | .is_some_and(|id| parent(id).is_none_or(|level| level >= node.level)) | |
| 147 | 186 | { |
| 148 | 187 | return Err(EditError::InvalidStructure); |
| 149 | 188 | } |
| 150 | 189 | parents.insert(node.id, node.level); |
| 151 | 190 | match &node.content { |
| 152 | ParagraphContent::Text(text) => { | |
| 153 | if !ids.insert(text.id) { | |
| 154 | return Err(EditError::InvalidStructure); | |
| 155 | } | |
| 156 | } | |
| 157 | 191 | ParagraphContent::Image(image) => { |
| 158 | if !ids.insert(image.id) { | |
| 159 | return Err(EditError::InvalidStructure); | |
| 160 | } | |
| 161 | 192 | crate::outline::image_size(image).ok_or(EditError::UnsupportedContent)?; |
| 162 | 193 | } |
| 163 | ParagraphContent::Attachment(file) => { | |
| 164 | if !ids.insert(file.id) { | |
| 165 | return Err(EditError::InvalidStructure); | |
| 166 | } | |
| 167 | } | |
| 168 | ParagraphContent::Ink(ink) => { | |
| 169 | if !ids.insert(ink.id) { | |
| 170 | return Err(EditError::InvalidStructure); | |
| 171 | } | |
| 172 | } | |
| 173 | ParagraphContent::Unsupported(unsupported) => { | |
| 174 | if !ids.insert(unsupported.id) { | |
| 175 | return Err(EditError::InvalidStructure); | |
| 176 | } | |
| 177 | } | |
| 178 | 194 | ParagraphContent::Table(table) => { |
| 179 | if !ids.insert(table.id) | |
| 180 | || table.rows.is_empty() | |
| 195 | if table.rows.is_empty() | |
| 181 | 196 | || table.columns.is_empty() |
| 182 | 197 | || table.columns.len() > 255 |
| 183 | 198 | || table |
| 184 | 199 | .columns |
| 185 | 200 | .iter() |
| 186 | 201 | .any(|column| !column.width.is_finite() || column.width < 36.0) |
| 202 | || table | |
| 203 | .rows | |
| 204 | .iter() | |
| 205 | .any(|row| row.cells.len() != table.columns.len()) | |
| 187 | 206 | { |
| 188 | 207 | return Err(EditError::InvalidStructure); |
| 189 | 208 | } |
| 190 | for row in &table.rows { | |
| 191 | if !ids.insert(row.id) || row.cells.len() != table.columns.len() { | |
| 192 | return Err(EditError::InvalidStructure); | |
| 209 | for cell in table.rows.iter().flat_map(|row| &row.cells) { | |
| 210 | if !cell.unsupported.is_empty() { | |
| 211 | return Err(EditError::UnsupportedContent); | |
| 193 | 212 | } |
| 194 | for cell in &row.cells { | |
| 195 | if !ids.insert(cell.id) { | |
| 196 | return Err(EditError::InvalidStructure); | |
| 197 | } | |
| 198 | if !cell.unsupported.is_empty() { | |
| 199 | return Err(EditError::UnsupportedContent); | |
| 200 | } | |
| 201 | pending.push((Some(cell.id), cell.paragraphs.as_slice(), depth + 1)); | |
| 213 | if cell.paragraphs.is_empty() { | |
| 214 | return Err(EditError::InvalidRange); | |
| 202 | 215 | } |
| 216 | pending.push((cell.paragraphs.as_slice(), &[][..], depth + 1)); | |
| 203 | 217 | } |
| 204 | 218 | } |
| 219 | ParagraphContent::Text(_) | |
| 220 | | ParagraphContent::Attachment(_) | |
| 221 | | ParagraphContent::Ink(_) | |
| 222 | | ParagraphContent::Unsupported(_) => {} | |
| 205 | 223 | } |
| 206 | 224 | } |
| 207 | 225 | } |
| ... | ... | @@ -216,6 +234,15 @@ fn validate_text(nodes: &[PageParagraph]) -> Result<(), EditError> { |
| 216 | 234 | Ok(()) |
| 217 | 235 | } |
| 218 | 236 | |
| 237 | /// Paragraph positions of each node's first text leaf, counting from `first`. | |
| 238 | fn starts(nodes: &[PageParagraph], first: usize) -> impl Iterator<Item = usize> + '_ { | |
| 239 | nodes.iter().scan(first, |next, node| { | |
| 240 | let start = *next; | |
| 241 | *next += leaves(std::slice::from_ref(node), None).count(); | |
| 242 | Some(start) | |
| 243 | }) | |
| 244 | } | |
| 245 | ||
| 219 | 246 | pub(crate) fn validate_flat<'a>( |
| 220 | 247 | mut nodes: impl Iterator<Item = &'a PageParagraph>, |
| 221 | 248 | ) -> Result<(), EditError> { |
| ... | ... | @@ -235,16 +262,20 @@ pub(crate) fn container_mut( |
| 235 | 262 | nodes: &mut Vec<PageParagraph>, |
| 236 | 263 | id: Option<ExGuid>, |
| 237 | 264 | ) -> Option<&mut Vec<PageParagraph>> { |
| 238 | let Some(id) = id else { | |
| 239 | return Some(nodes); | |
| 240 | }; | |
| 265 | match id { | |
| 266 | None => Some(nodes), | |
| 267 | Some(id) => cell_mut(nodes, id), | |
| 268 | } | |
| 269 | } | |
| 270 | ||
| 271 | fn cell_mut(nodes: &mut [PageParagraph], id: ExGuid) -> Option<&mut Vec<PageParagraph>> { | |
| 241 | 272 | for node in nodes { |
| 242 | 273 | if let ParagraphContent::Table(table) = &mut node.content { |
| 243 | 274 | for cell in table.rows.iter_mut().flat_map(|row| &mut row.cells) { |
| 244 | 275 | if cell.id == id { |
| 245 | 276 | return Some(&mut cell.paragraphs); |
| 246 | 277 | } |
| 247 | if let Some(nodes) = container_mut(&mut cell.paragraphs, Some(id)) { | |
| 278 | if let Some(nodes) = cell_mut(&mut cell.paragraphs, id) { | |
| 248 | 279 | return Some(nodes); |
| 249 | 280 | } |
| 250 | 281 | } |
| ... | ... | @@ -253,6 +284,14 @@ pub(crate) fn container_mut( |
| 253 | 284 | None |
| 254 | 285 | } |
| 255 | 286 | |
| 287 | fn tables(nodes: &[PageParagraph], first: usize) -> impl Iterator<Item = usize> + '_ { | |
| 288 | nodes | |
| 289 | .iter() | |
| 290 | .enumerate() | |
| 291 | .filter(|(_, node)| matches!(node.content, ParagraphContent::Table(_))) | |
| 292 | .map(move |(index, _)| first + index) | |
| 293 | } | |
| 294 | ||
| 256 | 295 | impl TextDocument { |
| 257 | 296 | pub fn new(paragraphs: Vec<Paragraph>) -> Result<Self, EditError> { |
| 258 | 297 | Self::from_nodes( |
| ... | ... | @@ -264,9 +303,15 @@ impl TextDocument { |
| 264 | 303 | } |
| 265 | 304 | |
| 266 | 305 | pub fn from_nodes(nodes: Vec<PageParagraph>) -> Result<Self, EditError> { |
| 267 | validate_nodes(&nodes, None, &mut BTreeSet::new())?; | |
| 306 | validate_nodes(&nodes, &mut BTreeSet::new())?; | |
| 268 | 307 | validate_text(&nodes)?; |
| 269 | Ok(Self { nodes }) | |
| 308 | let mut starts = starts(&nodes, 0).collect::<Vec<_>>(); | |
| 309 | starts.push(leaves(&nodes, None).count()); | |
| 310 | Ok(Self { | |
| 311 | tables: tables(&nodes, 0).collect(), | |
| 312 | nodes, | |
| 313 | starts, | |
| 314 | }) | |
| 270 | 315 | } |
| 271 | 316 | |
| 272 | 317 | pub fn nodes(&self) -> &[PageParagraph] { |
| ... | ... | @@ -285,19 +330,90 @@ impl TextDocument { |
| 285 | 330 | self.text_nodes().map(|node| &node.text().unwrap().text) |
| 286 | 331 | } |
| 287 | 332 | |
| 333 | /// The text leaf at a paragraph position, with its container and index there. | |
| 334 | pub(crate) fn leaf(&self, paragraph: usize) -> Option<(Option<ExGuid>, usize, &PageParagraph)> { | |
| 335 | let root = self | |
| 336 | .starts | |
| 337 | .partition_point(|start| *start <= paragraph) | |
| 338 | .checked_sub(1)?; | |
| 339 | let node = self.nodes.get(root)?; | |
| 340 | match node.text() { | |
| 341 | Some(_) => Some((None, root, node)), | |
| 342 | None => leaves(std::slice::from_ref(node), None).nth(paragraph - self.starts[root]), | |
| 343 | } | |
| 344 | } | |
| 345 | ||
| 346 | /// The text leaf at a paragraph position once a valid `edit` applies, with the position it | |
| 347 | /// has now unless the edit supplies it. | |
| 348 | pub(crate) fn edited_leaf<'a>( | |
| 349 | &'a self, | |
| 350 | edit: &'a DocumentEdit, | |
| 351 | paragraph: usize, | |
| 352 | ) -> Option<(&'a PageParagraph, Option<usize>)> { | |
| 353 | let nodes = self.container(edit.container).ok()?; | |
| 354 | let first = match edit.container { | |
| 355 | None => self.starts[edit.range.start], | |
| 356 | Some(cell) => { | |
| 357 | let root = self.root(cell).ok()?; | |
| 358 | self.starts[root] | |
| 359 | + descendants(std::slice::from_ref(&self.nodes[root]), None) | |
| 360 | .take_while(|(container, _, _)| *container != edit.container) | |
| 361 | .filter(|(_, _, node)| node.text().is_some()) | |
| 362 | .count() | |
| 363 | + leaves(&nodes[..edit.range.start], None).count() | |
| 364 | } | |
| 365 | }; | |
| 366 | let added = leaves(&edit.replacement, None).count(); | |
| 367 | match paragraph.checked_sub(first) { | |
| 368 | Some(offset) if offset < added => leaves(&edit.replacement, None) | |
| 369 | .nth(offset) | |
| 370 | .map(|(_, _, node)| (node, None)), | |
| 371 | Some(_) => { | |
| 372 | let before = paragraph - added + leaves(&nodes[edit.range.clone()], None).count(); | |
| 373 | self.leaf(before).map(|(_, _, node)| (node, Some(before))) | |
| 374 | } | |
| 375 | None => self | |
| 376 | .leaf(paragraph) | |
| 377 | .map(|(_, _, node)| (node, Some(paragraph))), | |
| 378 | } | |
| 379 | } | |
| 380 | ||
| 381 | /// The root node holding a table cell. | |
| 382 | pub(crate) fn root(&self, cell: ExGuid) -> Result<usize, EditError> { | |
| 383 | self.tables | |
| 384 | .iter() | |
| 385 | .copied() | |
| 386 | .find(|&root| { | |
| 387 | descendants(std::slice::from_ref(&self.nodes[root]), None) | |
| 388 | .any(|(container, _, _)| container == Some(cell)) | |
| 389 | }) | |
| 390 | .ok_or(EditError::InvalidRange) | |
| 391 | } | |
| 392 | ||
| 393 | pub(crate) fn paragraph(&self, index: usize) -> Option<&Paragraph> { | |
| 394 | self.leaf(index) | |
| 395 | .map(|(_, _, node)| &node.text().unwrap().text) | |
| 396 | } | |
| 397 | ||
| 288 | 398 | pub(crate) fn container(&self, id: Option<ExGuid>) -> Result<&[PageParagraph], EditError> { |
| 399 | self.nested_container(id).map(|(nodes, _)| nodes) | |
| 400 | } | |
| 401 | ||
| 402 | /// The container's paragraphs and how many tables enclose them. | |
| 403 | fn nested_container(&self, id: Option<ExGuid>) -> Result<(&[PageParagraph], usize), EditError> { | |
| 289 | 404 | let Some(id) = id else { |
| 290 | return Ok(&self.nodes); | |
| 405 | return Ok((&self.nodes, 0)); | |
| 291 | 406 | }; |
| 292 | let mut pending = vec![self.nodes.as_slice()]; | |
| 293 | while let Some(nodes) = pending.pop() { | |
| 407 | let root = self.root(id)?; | |
| 408 | let mut pending = vec![(std::slice::from_ref(&self.nodes[root]), 0)]; | |
| 409 | while let Some((nodes, depth)) = pending.pop() { | |
| 294 | 410 | for node in nodes { |
| 295 | 411 | if let ParagraphContent::Table(table) = &node.content { |
| 296 | 412 | for cell in table.rows.iter().flat_map(|row| &row.cells) { |
| 297 | 413 | if cell.id == id { |
| 298 | return Ok(&cell.paragraphs); | |
| 414 | return Ok((&cell.paragraphs, depth + 1)); | |
| 299 | 415 | } |
| 300 | pending.push(&cell.paragraphs); | |
| 416 | pending.push((&cell.paragraphs, depth + 1)); | |
| 301 | 417 | } |
| 302 | 418 | } |
| 303 | 419 | } |
| ... | ... | @@ -344,11 +460,11 @@ impl TextDocument { |
| 344 | 460 | if range.start > range.end { |
| 345 | 461 | return Err(EditError::InvalidRange); |
| 346 | 462 | } |
| 347 | let (container, start, first) = leaves(&self.nodes, None) | |
| 348 | .nth(range.start.paragraph) | |
| 463 | let (container, start, first) = self | |
| 464 | .leaf(range.start.paragraph) | |
| 349 | 465 | .ok_or(EditError::InvalidRange)?; |
| 350 | let (end_container, end, last) = leaves(&self.nodes, None) | |
| 351 | .nth(range.end.paragraph) | |
| 466 | let (end_container, end, last) = self | |
| 467 | .leaf(range.end.paragraph) | |
| 352 | 468 | .ok_or(EditError::InvalidRange)?; |
| 353 | 469 | if container != end_container { |
| 354 | 470 | return Err(EditError::UnsupportedContent); |
| ... | ... | @@ -394,12 +510,60 @@ impl TextDocument { |
| 394 | 510 | }) |
| 395 | 511 | } |
| 396 | 512 | |
| 513 | /// Rejects exactly the edits after which [`validate_nodes`] would reject the document, or | |
| 514 | /// whose column widths or text are invalid, looking only where the edit can conflict. | |
| 397 | 515 | pub(crate) fn validate_edit(&self, edit: &DocumentEdit) -> Result<(), EditError> { |
| 398 | let nodes = self.container(edit.container)?; | |
| 399 | if edit.range.start > edit.range.end || edit.range.end > nodes.len() { | |
| 516 | let (nodes, depth) = self.nested_container(edit.container)?; | |
| 517 | if edit.range.start > edit.range.end | |
| 518 | || edit.range.end > nodes.len() | |
| 519 | || nodes.len() - edit.range.len() + edit.replacement.len() == 0 | |
| 520 | { | |
| 400 | 521 | return Err(EditError::InvalidRange); |
| 401 | 522 | } |
| 402 | validate_nodes(&self.nodes, Some(edit), &mut BTreeSet::new())?; | |
| 523 | let mut ids = BTreeSet::new(); | |
| 524 | validate_run( | |
| 525 | &edit.replacement, | |
| 526 | &nodes[..edit.range.start], | |
| 527 | depth, | |
| 528 | &mut ids, | |
| 529 | )?; | |
| 530 | let removed = &nodes[edit.range.clone()]; | |
| 531 | let levels = edit | |
| 532 | .replacement | |
| 533 | .iter() | |
| 534 | .map(|node| (node.id, node.level)) | |
| 535 | .collect::<BTreeMap<_, _>>(); | |
| 536 | // A later paragraph may name a removed or deepened paragraph as its parent. | |
| 537 | let moved = removed | |
| 538 | .iter() | |
| 539 | .filter_map(|node| { | |
| 540 | let level = levels.get(&node.id).copied(); | |
| 541 | level | |
| 542 | .is_none_or(|level| level > node.level) | |
| 543 | .then_some((node.id, level)) | |
| 544 | }) | |
| 545 | .collect::<BTreeMap<_, _>>(); | |
| 546 | if !moved.is_empty() | |
| 547 | && nodes[edit.range.end..].iter().any(|node| { | |
| 548 | node.parent | |
| 549 | .and_then(|id| moved.get(&id)) | |
| 550 | .is_some_and(|level| level.is_none_or(|level| level >= node.level)) | |
| 551 | }) | |
| 552 | { | |
| 553 | return Err(EditError::InvalidStructure); | |
| 554 | } | |
| 555 | // Only identities the removed nodes did not hold can collide elsewhere. | |
| 556 | for (_, _, node) in descendants(removed, None) { | |
| 557 | for id in owned_ids(node) { | |
| 558 | ids.remove(&id); | |
| 559 | } | |
| 560 | } | |
| 561 | if !ids.is_empty() | |
| 562 | && descendants(&self.nodes, None) | |
| 563 | .any(|(_, _, node)| owned_ids(node).any(|id| ids.contains(&id))) | |
| 564 | { | |
| 565 | return Err(EditError::InvalidStructure); | |
| 566 | } | |
| 403 | 567 | if !edit.columns.is_empty() { |
| 404 | 568 | let replaced = descendants(&edit.replacement, None) |
| 405 | 569 | .filter_map(|(_, _, node)| match &node.content { |
| ... | ... | @@ -431,23 +595,59 @@ impl TextDocument { |
| 431 | 595 | |
| 432 | 596 | pub(crate) fn apply(&mut self, edit: DocumentEdit) -> Result<DocumentEdit, EditError> { |
| 433 | 597 | self.validate_edit(&edit)?; |
| 434 | let end = edit | |
| 435 | .range | |
| 436 | .start | |
| 437 | .checked_add(edit.replacement.len()) | |
| 438 | .ok_or(EditError::TextTooLong)?; | |
| 439 | let range = edit.range.start..end; | |
| 440 | let nodes = | |
| 441 | container_mut(&mut self.nodes, edit.container).ok_or(EditError::InvalidRange)?; | |
| 442 | let replacement = nodes.splice(edit.range, edit.replacement).collect(); | |
| 598 | Ok(self.splice(edit)) | |
| 599 | } | |
| 600 | ||
| 601 | /// Applies an edit [`Self::validate_edit`] accepted, returning its inverse. | |
| 602 | pub(crate) fn splice(&mut self, edit: DocumentEdit) -> DocumentEdit { | |
| 603 | let range = edit.range.start..edit.range.start + edit.replacement.len(); | |
| 604 | let root = edit.container.map(|cell| { | |
| 605 | ( | |
| 606 | cell, | |
| 607 | self.root(cell).expect("a validated edit's cell exists"), | |
| 608 | ) | |
| 609 | }); | |
| 610 | let nodes = match root { | |
| 611 | Some((cell, root)) => cell_mut(std::slice::from_mut(&mut self.nodes[root]), cell) | |
| 612 | .expect("a validated edit's cell exists"), | |
| 613 | None => &mut self.nodes, | |
| 614 | }; | |
| 615 | let replacement: Vec<_> = nodes.splice(edit.range.clone(), edit.replacement).collect(); | |
| 616 | let delta = leaves(&nodes[range.clone()], None).count() as isize | |
| 617 | - leaves(&replacement, None).count() as isize; | |
| 618 | let after = match root { | |
| 619 | Some((_, root)) => root + 1, | |
| 620 | None => { | |
| 621 | let first = self.starts[range.start]; | |
| 622 | self.starts.splice( | |
| 623 | edit.range.clone(), | |
| 624 | starts(&self.nodes[range.clone()], first), | |
| 625 | ); | |
| 626 | let [start, end] = [edit.range.start, edit.range.end] | |
| 627 | .map(|index| self.tables.partition_point(|table| *table < index)); | |
| 628 | let added = tables(&self.nodes[range.clone()], range.start).collect::<Vec<_>>(); | |
| 629 | let later = start + added.len(); | |
| 630 | self.tables.splice(start..end, added); | |
| 631 | for table in &mut self.tables[later..] { | |
| 632 | *table = | |
| 633 | table.wrapping_add_signed(range.len() as isize - edit.range.len() as isize); | |
| 634 | } | |
| 635 | range.end | |
| 636 | } | |
| 637 | }; | |
| 638 | if delta != 0 { | |
| 639 | for start in &mut self.starts[after..] { | |
| 640 | *start = start.wrapping_add_signed(delta); | |
| 641 | } | |
| 642 | } | |
| 443 | 643 | let mut columns = edit.columns; |
| 444 | 644 | swap_columns(&mut self.nodes, &mut columns); |
| 445 | Ok(DocumentEdit { | |
| 645 | DocumentEdit { | |
| 446 | 646 | container: edit.container, |
| 447 | 647 | range, |
| 448 | 648 | replacement, |
| 449 | 649 | columns, |
| 450 | }) | |
| 650 | } | |
| 451 | 651 | } |
| 452 | 652 | } |
| 453 | 653 | |
| ... | ... | @@ -1252,6 +1452,239 @@ mod tests { |
| 1252 | 1452 | } |
| 1253 | 1453 | } |
| 1254 | 1454 | |
| 1455 | /// Validates the materialized edited document from scratch. | |
| 1456 | fn full_validation(document: &TextDocument, edit: &DocumentEdit) -> Result<(), EditError> { | |
| 1457 | let mut nodes = document.nodes.clone(); | |
| 1458 | let container = container_mut(&mut nodes, edit.container).ok_or(EditError::InvalidRange)?; | |
| 1459 | if edit.range.start > edit.range.end || edit.range.end > container.len() { | |
| 1460 | return Err(EditError::InvalidRange); | |
| 1461 | } | |
| 1462 | container.splice(edit.range.clone(), edit.replacement.iter().cloned()); | |
| 1463 | validate_nodes(&nodes, &mut BTreeSet::new())?; | |
| 1464 | let tables = |nodes| { | |
| 1465 | descendants(nodes, None) | |
| 1466 | .filter_map(|(_, _, node)| match &node.content { | |
| 1467 | ParagraphContent::Table(table) => Some((table.id, table.columns.len())), | |
| 1468 | _ => None, | |
| 1469 | }) | |
| 1470 | .collect::<BTreeMap<_, _>>() | |
| 1471 | }; | |
| 1472 | let replaced = tables(&edit.replacement); | |
| 1473 | let tables = tables(&nodes); | |
| 1474 | for (id, widths) in &edit.columns { | |
| 1475 | if replaced.contains_key(id) | |
| 1476 | || tables.get(id) != Some(&widths.len()) | |
| 1477 | || widths | |
| 1478 | .iter() | |
| 1479 | .any(|width| !width.is_finite() || *width < 36.0) | |
| 1480 | { | |
| 1481 | return Err(EditError::InvalidStructure); | |
| 1482 | } | |
| 1483 | } | |
| 1484 | validate_text(&edit.replacement) | |
| 1485 | } | |
| 1486 | ||
| 1487 | #[test] | |
| 1488 | fn incremental_validation_matches_full_validation_of_random_edits() { | |
| 1489 | let mut seed = 0x9e37_79b9_u64; | |
| 1490 | let mut next = |n: usize| { | |
| 1491 | seed = seed | |
| 1492 | .wrapping_mul(6364136223846793005) | |
| 1493 | .wrapping_add(1442695040888963407); | |
| 1494 | (seed >> 33) as usize % n.max(1) | |
| 1495 | }; | |
| 1496 | let fresh = |text: &str| { | |
| 1497 | node( | |
| 1498 | Paragraph::new(text.into(), Format::default()), | |
| 1499 | Format::default(), | |
| 1500 | ) | |
| 1501 | .unwrap() | |
| 1502 | }; | |
| 1503 | let mut document = table_document(); | |
| 1504 | let [mut accepted, mut rejected] = [0; 2]; | |
| 1505 | for step in 0..4000 { | |
| 1506 | let all = descendants(&document.nodes, None) | |
| 1507 | .map(|(_, _, node)| node.clone()) | |
| 1508 | .collect::<Vec<_>>(); | |
| 1509 | let cells = all | |
| 1510 | .iter() | |
| 1511 | .filter_map(|node| match &node.content { | |
| 1512 | ParagraphContent::Table(table) => Some(table), | |
| 1513 | _ => None, | |
| 1514 | }) | |
| 1515 | .flat_map(|table| table.rows.iter().flat_map(|row| &row.cells)) | |
| 1516 | .map(|cell| Some(cell.id)) | |
| 1517 | .collect::<Vec<_>>(); | |
| 1518 | let tables = all | |
| 1519 | .iter() | |
| 1520 | .filter_map(|node| match &node.content { | |
| 1521 | ParagraphContent::Table(table) => Some(table.id), | |
| 1522 | _ => None, | |
| 1523 | }) | |
| 1524 | .collect::<Vec<_>>(); | |
| 1525 | let container = match next(8) { | |
| 1526 | 0..4 => None, | |
| 1527 | 7 if step % 5 == 0 => Some(new_id().unwrap()), | |
| 1528 | _ => cells.get(next(cells.len())).copied().flatten(), | |
| 1529 | }; | |
| 1530 | let nodes = document.container(container).unwrap_or(&[]).to_vec(); | |
| 1531 | let start = next(nodes.len() + 2); | |
| 1532 | let range = if next(16) == 0 { | |
| 1533 | start..start.saturating_sub(1) | |
| 1534 | } else { | |
| 1535 | start..(start + next(3)).min(nodes.len() + usize::from(next(8) == 0)) | |
| 1536 | }; | |
| 1537 | let removed = nodes.get(range.clone()).unwrap_or_default(); | |
| 1538 | let prefix = &nodes[..range.start.min(nodes.len())]; | |
| 1539 | let suffix = nodes.get(range.end..).unwrap_or_default(); | |
| 1540 | let ids = | |
| 1541 | |nodes: &[PageParagraph]| nodes.iter().map(|node| node.id).collect::<Vec<_>>(); | |
| 1542 | let parents = [ids(prefix), ids(removed), ids(suffix), ids(&all)].concat(); | |
| 1543 | let mut replacement = Vec::new(); | |
| 1544 | for _ in 0..next(4) { | |
| 1545 | let mut node = match next(10) { | |
| 1546 | 0..3 if !removed.is_empty() => removed[next(removed.len())].clone(), | |
| 1547 | 3 if !all.is_empty() => all[next(all.len())].clone(), | |
| 1548 | 4 => { | |
| 1549 | let mut table = table_document().nodes.remove(1); | |
| 1550 | let ParagraphContent::Table(inner) = &mut table.content else { | |
| 1551 | unreachable!() | |
| 1552 | }; | |
| 1553 | match next(6) { | |
| 1554 | 0 => inner.rows[0].cells[0].paragraphs.clear(), | |
| 1555 | 1 => inner.columns[0].width = 35.0, | |
| 1556 | 2 => { | |
| 1557 | inner.rows[0].cells.pop(); | |
| 1558 | } | |
| 1559 | 3 => { | |
| 1560 | inner.rows[0].cells[1].paragraphs[0].parent = | |
| 1561 | Some(inner.rows[0].cells[0].paragraphs[0].id); | |
| 1562 | } | |
| 1563 | _ => {} | |
| 1564 | } | |
| 1565 | table | |
| 1566 | } | |
| 1567 | _ => fresh("new"), | |
| 1568 | }; | |
| 1569 | match next(6) { | |
| 1570 | 0 => node.level = next(4) as u32, | |
| 1571 | 1 => node.level += 1, | |
| 1572 | 2 => { | |
| 1573 | node.level = 1 + next(4) as u32; | |
| 1574 | node.parent = (!parents.is_empty()).then(|| parents[next(parents.len())]); | |
| 1575 | } | |
| 1576 | 3 => node.parent = None, | |
| 1577 | _ => {} | |
| 1578 | } | |
| 1579 | replacement.push(node); | |
| 1580 | } | |
| 1581 | let mut columns = BTreeMap::new(); | |
| 1582 | if next(12) == 0 { | |
| 1583 | let id = tables | |
| 1584 | .get(next(tables.len() + 1)) | |
| 1585 | .copied() | |
| 1586 | .unwrap_or_else(|| new_id().unwrap()); | |
| 1587 | columns.insert(id, vec![[72.0, 30.0, f32::NAN][next(3)]; 1 + next(3)]); | |
| 1588 | } | |
| 1589 | let edit = if next(4) == 0 { | |
| 1590 | let positions = document.paragraphs().count(); | |
| 1591 | let paragraph = next(positions); | |
| 1592 | let offset = document.paragraphs().nth(paragraph).unwrap().text().len() as u32; | |
| 1593 | let start = TextPosition { | |
| 1594 | paragraph, | |
| 1595 | offset: next(offset as usize + 1) as u32, | |
| 1596 | }; | |
| 1597 | let end = TextPosition { | |
| 1598 | paragraph: paragraph + usize::from(paragraph + 1 < positions && next(2) == 0), | |
| 1599 | offset: 0, | |
| 1600 | }; | |
| 1601 | let texts = vec![Paragraph::new("typed".into(), Format::default()); 1 + next(2)]; | |
| 1602 | match document.replace(start..end.max(start), texts) { | |
| 1603 | Ok(edit) => edit, | |
| 1604 | Err(_) => continue, | |
| 1605 | } | |
| 1606 | } else { | |
| 1607 | DocumentEdit { | |
| 1608 | container, | |
| 1609 | range, | |
| 1610 | replacement, | |
| 1611 | columns, | |
| 1612 | } | |
| 1613 | }; | |
| 1614 | let expected = full_validation(&document, &edit); | |
| 1615 | assert_eq!( | |
| 1616 | document.validate_edit(&edit).is_ok(), | |
| 1617 | expected.is_ok(), | |
| 1618 | "step {step}: {expected:?} for {edit:#?}" | |
| 1619 | ); | |
| 1620 | if expected.is_ok() { | |
| 1621 | accepted += 1; | |
| 1622 | let edited = leaves(&document.nodes, Some(&edit)).collect::<Vec<_>>(); | |
| 1623 | let supplied = descendants(&edit.replacement, None) | |
| 1624 | .map(|(_, _, node)| node.id) | |
| 1625 | .collect::<BTreeSet<_>>(); | |
| 1626 | for paragraph in 0..=edited.len() { | |
| 1627 | let leaf = document.edited_leaf(&edit, paragraph); | |
| 1628 | assert_eq!( | |
| 1629 | leaf.map(|(node, _)| node), | |
| 1630 | edited.get(paragraph).map(|(_, _, node)| *node) | |
| 1631 | ); | |
| 1632 | if let Some((node, before)) = leaf { | |
| 1633 | assert_eq!(before.is_none(), supplied.contains(&node.id)); | |
| 1634 | assert!( | |
| 1635 | before.is_none_or(|before| document.leaf(before).unwrap().2 == node) | |
| 1636 | ); | |
| 1637 | } | |
| 1638 | } | |
| 1639 | document.apply(edit).unwrap(); | |
| 1640 | assert_eq!( | |
| 1641 | document, | |
| 1642 | TextDocument::from_nodes(document.nodes.clone()).unwrap() | |
| 1643 | ); | |
| 1644 | let walked = leaves(&document.nodes, None).collect::<Vec<_>>(); | |
| 1645 | for paragraph in 0..=walked.len() { | |
| 1646 | assert_eq!(document.leaf(paragraph), walked.get(paragraph).copied()); | |
| 1647 | } | |
| 1648 | } else { | |
| 1649 | rejected += 1; | |
| 1650 | } | |
| 1651 | } | |
| 1652 | assert!(accepted > 500 && rejected > 500, "{accepted} {rejected}"); | |
| 1653 | let nest = |mut node, depth| { | |
| 1654 | for _ in 0..depth { | |
| 1655 | let mut table = table_document().nodes.remove(1); | |
| 1656 | let ParagraphContent::Table(inner) = &mut table.content else { | |
| 1657 | unreachable!() | |
| 1658 | }; | |
| 1659 | inner.rows[0].cells[0].paragraphs = vec![node]; | |
| 1660 | node = table; | |
| 1661 | } | |
| 1662 | node | |
| 1663 | }; | |
| 1664 | let document = TextDocument::from_nodes(vec![nest(fresh("leaf"), 64)]).unwrap(); | |
| 1665 | let (innermost, _, _) = leaves(&document.nodes, None) | |
| 1666 | .find(|(_, _, node)| node.text().unwrap().text.text() == "leaf") | |
| 1667 | .unwrap(); | |
| 1668 | for (container, depth, valid) in [ | |
| 1669 | (innermost, 0, true), | |
| 1670 | (innermost, 1, false), | |
| 1671 | (None, 64, true), | |
| 1672 | (None, 65, false), | |
| 1673 | ] { | |
| 1674 | let edit = DocumentEdit { | |
| 1675 | container, | |
| 1676 | range: 0..0, | |
| 1677 | replacement: vec![nest(fresh("deep"), depth)], | |
| 1678 | columns: BTreeMap::new(), | |
| 1679 | }; | |
| 1680 | assert_eq!( | |
| 1681 | document.validate_edit(&edit).is_ok(), | |
| 1682 | full_validation(&document, &edit).is_ok() | |
| 1683 | ); | |
| 1684 | assert_eq!(document.validate_edit(&edit).is_ok(), valid); | |
| 1685 | } | |
| 1686 | } | |
| 1687 | ||
| 1255 | 1688 | #[test] |
| 1256 | 1689 | fn invalid_ranges_and_last_paragraph_removal_are_atomic() { |
| 1257 | 1690 | let original = |
crates/canvas/src/editor.rs+268-260| ... | ... | @@ -1,12 +1,10 @@ |
| 1 | 1 | use crate::{ |
| 2 | 2 | date::PageDate, |
| 3 | document::{DocumentEdit, TextDocument, TextPosition, descendants, edited_nodes, leaves}, | |
| 3 | document::{DocumentEdit, TextDocument, TextPosition, descendants, leaves}, | |
| 4 | 4 | layout::{LayoutError, TextEngine}, |
| 5 | outline::{ | |
| 6 | Arrange, OutlineLayout, ParagraphLayout, arrange, indentation, outline_layout, | |
| 7 | visible_paragraphs, | |
| 8 | }, | |
| 5 | outline::{Arrange, OutlineLayout, ParagraphLayout, outline_layout, visible_paragraphs}, | |
| 9 | 6 | }; |
| 7 | use draw::edit::{self, Movement, SelectionUnit}; | |
| 10 | 8 | use onestore::ExGuid; |
| 11 | 9 | use onestore::page::text::{EditError, Paragraph}; |
| 12 | 10 | use onestore::page::{Definition, Outline, Page, PageObject, PageParagraph, Title}; |
| ... | ... | @@ -26,29 +24,6 @@ pub(crate) mod page; |
| 26 | 24 | mod table; |
| 27 | 25 | pub use page::ReadOnlyObject; |
| 28 | 26 | |
| 29 | #[derive(Clone, Copy, Debug)] | |
| 30 | pub enum Movement { | |
| 31 | Left, | |
| 32 | Right, | |
| 33 | Up, | |
| 34 | Down, | |
| 35 | WordLeft, | |
| 36 | WordRight, | |
| 37 | LineStart, | |
| 38 | LineEnd, | |
| 39 | ParagraphStart, | |
| 40 | ParagraphEnd, | |
| 41 | DocumentStart, | |
| 42 | DocumentEnd, | |
| 43 | } | |
| 44 | ||
| 45 | #[derive(Clone, Copy, Debug, PartialEq, Eq)] | |
| 46 | pub enum SelectionUnit { | |
| 47 | Grapheme, | |
| 48 | Word, | |
| 49 | Paragraph, | |
| 50 | } | |
| 51 | ||
| 52 | 27 | #[derive(Debug)] |
| 53 | 28 | pub enum EditorError { |
| 54 | 29 | Edit(EditError), |
| ... | ... | @@ -319,19 +294,7 @@ impl TextOutline { |
| 319 | 294 | let text_width = if self.shaped.tables.is_empty() { |
| 320 | 295 | Some(self.shaped.size[0]) |
| 321 | 296 | } else { |
| 322 | let roots = self | |
| 323 | .document | |
| 324 | .nodes() | |
| 325 | .iter() | |
| 326 | .filter(|node| node.text().is_some()) | |
| 327 | .map(|node| node.id) | |
| 328 | .collect::<BTreeSet<_>>(); | |
| 329 | self.shaped | |
| 330 | .paragraphs | |
| 331 | .iter() | |
| 332 | .filter(|paragraph| roots.contains(&paragraph.id)) | |
| 333 | .map(|paragraph| paragraph.origin[0] + paragraph.text.shaped.width()) | |
| 334 | .reduce(f32::max) | |
| 297 | Some(self.shaped.widths[1]).filter(|width| *width > f32::NEG_INFINITY) | |
| 335 | 298 | }; |
| 336 | 299 | text_width.map_or(self.shaped.size[0], |width| { |
| 337 | 300 | (width + 36.0) |
| ... | ... | @@ -370,13 +333,23 @@ impl TextOutline { |
| 370 | 333 | }) |
| 371 | 334 | } |
| 372 | 335 | |
| 336 | /// Layouts follow the text leaves minus hidden ones, so an index shared by a leaf and its | |
| 337 | /// layout needs no search. | |
| 373 | 338 | fn visible_index(&self, source: usize) -> Result<usize, EditError> { |
| 374 | 339 | let id = self |
| 375 | 340 | .document |
| 376 | .text_nodes() | |
| 377 | .nth(source) | |
| 341 | .leaf(source) | |
| 378 | 342 | .ok_or(EditError::InvalidRange)? |
| 343 | .2 | |
| 379 | 344 | .id; |
| 345 | if self | |
| 346 | .shaped | |
| 347 | .paragraphs | |
| 348 | .get(source) | |
| 349 | .is_some_and(|paragraph| paragraph.id == id) | |
| 350 | { | |
| 351 | return Ok(source); | |
| 352 | } | |
| 380 | 353 | self.shaped |
| 381 | 354 | .paragraphs |
| 382 | 355 | .iter() |
| ... | ... | @@ -386,6 +359,13 @@ impl TextOutline { |
| 386 | 359 | |
| 387 | 360 | fn source_index(&self, visible: usize) -> usize { |
| 388 | 361 | let id = self.shaped.paragraphs[visible].id; |
| 362 | if self | |
| 363 | .document | |
| 364 | .leaf(visible) | |
| 365 | .is_some_and(|(_, _, node)| node.id == id) | |
| 366 | { | |
| 367 | return visible; | |
| 368 | } | |
| 389 | 369 | self.document |
| 390 | 370 | .text_nodes() |
| 391 | 371 | .position(|paragraph| paragraph.id == id) |
| ... | ... | @@ -989,7 +969,7 @@ impl CanvasEditor { |
| 989 | 969 | if !ids.insert(outline_id) { |
| 990 | 970 | return Err(EditError::InvalidStructure.into()); |
| 991 | 971 | } |
| 992 | crate::document::validate_nodes(paragraphs, None, &mut ids)?; | |
| 972 | crate::document::validate_nodes(paragraphs, &mut ids)?; | |
| 993 | 973 | } |
| 994 | 974 | Ok(Self { |
| 995 | 975 | outlines, |
| ... | ... | @@ -1574,8 +1554,7 @@ impl CanvasEditor { |
| 1574 | 1554 | self.active_outline().visible_index(position.paragraph)?; |
| 1575 | 1555 | self.active_outline() |
| 1576 | 1556 | .document |
| 1577 | .paragraphs() | |
| 1578 | .nth(position.paragraph) | |
| 1557 | .paragraph(position.paragraph) | |
| 1579 | 1558 | .ok_or(EditError::InvalidRange)? |
| 1580 | 1559 | .byte_offset(position.offset)?; |
| 1581 | 1560 | } |
| ... | ... | @@ -1589,12 +1568,7 @@ impl CanvasEditor { |
| 1589 | 1568 | let paragraph = self |
| 1590 | 1569 | .active_outline() |
| 1591 | 1570 | .source_index(self.active_outline().shaped.paragraphs.len() - 1); |
| 1592 | let last = self | |
| 1593 | .active_outline() | |
| 1594 | .document | |
| 1595 | .paragraphs() | |
| 1596 | .nth(paragraph) | |
| 1597 | .unwrap(); | |
| 1571 | let last = self.active_outline().document.paragraph(paragraph).unwrap(); | |
| 1598 | 1572 | self.select( |
| 1599 | 1573 | [ |
| 1600 | 1574 | TextPosition { |
| ... | ... | @@ -1632,22 +1606,12 @@ impl CanvasEditor { |
| 1632 | 1606 | let paragraph = &self.active_outline().shaped.paragraphs[index]; |
| 1633 | 1607 | let layout = &paragraph.text.shaped; |
| 1634 | 1608 | let point = [x - paragraph.origin[0], y - paragraph.origin[1]]; |
| 1635 | let cursor = paragraph.text.hit_test(point[0], point[1]); | |
| 1636 | let local = match unit { | |
| 1637 | SelectionUnit::Grapheme => cursor.into(), | |
| 1638 | SelectionUnit::Word => { | |
| 1639 | let rect = cursor.geometry(layout, 1.0); | |
| 1640 | ParagraphSelection::word_from_point( | |
| 1641 | layout, | |
| 1642 | point[0], | |
| 1643 | ((rect.y0 + rect.y1) * 0.5) as f32, | |
| 1644 | ) | |
| 1645 | } | |
| 1646 | SelectionUnit::Paragraph => ParagraphSelection::new( | |
| 1647 | Cursor::from_byte_index(layout, 0, Affinity::Downstream), | |
| 1648 | Cursor::from_byte_index(layout, usize::MAX, Affinity::Upstream), | |
| 1649 | ), | |
| 1650 | }; | |
| 1609 | let local = edit::selection_at( | |
| 1610 | layout, | |
| 1611 | paragraph.text.hit_test(point[0], point[1]), | |
| 1612 | point[0], | |
| 1613 | unit, | |
| 1614 | ); | |
| 1651 | 1615 | let mut selection = self.selection(); |
| 1652 | 1616 | for (slot, cursor) in [local.anchor(), local.focus()].into_iter().enumerate() { |
| 1653 | 1617 | let visible = paragraph.projection.text().utf16_offset(cursor.index())?; |
| ... | ... | @@ -1665,8 +1629,8 @@ impl CanvasEditor { |
| 1665 | 1629 | let document = &self.active_outline().document; |
| 1666 | 1630 | let source = selection.positions[0].paragraph; |
| 1667 | 1631 | let next = self.active_outline().source_index(index + 1); |
| 1668 | let (container, local, _) = leaves(document.nodes(), None).nth(source).unwrap(); | |
| 1669 | let (next_container, next_local, _) = leaves(document.nodes(), None).nth(next).unwrap(); | |
| 1632 | let (container, local, _) = document.leaf(source).unwrap(); | |
| 1633 | let (next_container, next_local, _) = document.leaf(next).unwrap(); | |
| 1670 | 1634 | if container == next_container && next_local == local + 1 { |
| 1671 | 1635 | selection.positions[1] = TextPosition { |
| 1672 | 1636 | paragraph: next, |
| ... | ... | @@ -1767,7 +1731,7 @@ impl CanvasEditor { |
| 1767 | 1731 | let outline = self.active_outline(); |
| 1768 | 1732 | let current = outline.visible_index(focus.paragraph)?; |
| 1769 | 1733 | let end = matches!(movement, Movement::ParagraphEnd | Movement::DocumentEnd); |
| 1770 | let current_end = outline.document.paragraphs().nth(focus.paragraph).unwrap(); | |
| 1734 | let current_end = outline.document.paragraph(focus.paragraph).unwrap(); | |
| 1771 | 1735 | let current_end = current_end.utf16_offset(current_end.text().len())?; |
| 1772 | 1736 | let index = match movement { |
| 1773 | 1737 | Movement::DocumentStart => 0, |
| ... | ... | @@ -1779,7 +1743,7 @@ impl CanvasEditor { |
| 1779 | 1743 | _ => current, |
| 1780 | 1744 | }; |
| 1781 | 1745 | let source = outline.source_index(index); |
| 1782 | let paragraph = outline.document.paragraphs().nth(source).unwrap(); | |
| 1746 | let paragraph = outline.document.paragraph(source).unwrap(); | |
| 1783 | 1747 | let position = TextPosition { |
| 1784 | 1748 | paragraph: source, |
| 1785 | 1749 | offset: if end { |
| ... | ... | @@ -2030,22 +1994,7 @@ impl CanvasEditor { |
| 2030 | 1994 | } else { |
| 2031 | 1995 | cursor.into() |
| 2032 | 1996 | }; |
| 2033 | let layout = &paragraph.text.shaped; | |
| 2034 | next = match movement { | |
| 2035 | Movement::Left => local.previous_visual(layout, extend), | |
| 2036 | Movement::Right => local.next_visual(layout, extend), | |
| 2037 | Movement::WordLeft => paragraph.text.word_cursor(local.focus(), true).into(), | |
| 2038 | Movement::WordRight => paragraph.text.word_cursor(local.focus(), false).into(), | |
| 2039 | Movement::LineStart => local.line_start(layout, extend), | |
| 2040 | Movement::LineEnd => local.line_end(layout, extend), | |
| 2041 | Movement::Up | |
| 2042 | | Movement::Down | |
| 2043 | | Movement::ParagraphStart | |
| 2044 | | Movement::ParagraphEnd | |
| 2045 | | Movement::DocumentStart | |
| 2046 | | Movement::DocumentEnd => unreachable!(), | |
| 2047 | } | |
| 2048 | .focus(); | |
| 1997 | next = edit::step(&paragraph.text.shaped, local, movement, extend).focus(); | |
| 2049 | 1998 | if horizontal && next == cursor && (extend || anchor == focus) { |
| 2050 | 1999 | let neighbor = if left { |
| 2051 | 2000 | index.checked_sub(1) |
| ... | ... | @@ -2067,7 +2016,7 @@ impl CanvasEditor { |
| 2067 | 2016 | line.top + line.height * 0.5, |
| 2068 | 2017 | ); |
| 2069 | 2018 | if matches!(movement, Movement::WordLeft | Movement::WordRight) { |
| 2070 | next = paragraph.text.word_cursor(next, left); | |
| 2019 | next = edit::word_cursor(&paragraph.text.shaped, next, left); | |
| 2071 | 2020 | } |
| 2072 | 2021 | } |
| 2073 | 2022 | } |
| ... | ... | @@ -2161,12 +2110,7 @@ impl CanvasEditor { |
| 2161 | 2110 | .enumerate() |
| 2162 | 2111 | .filter(|(_, (index, _))| *index >= start.paragraph && *index <= end.paragraph) |
| 2163 | 2112 | { |
| 2164 | let source = self | |
| 2165 | .active_outline() | |
| 2166 | .document | |
| 2167 | .paragraphs() | |
| 2168 | .nth(index) | |
| 2169 | .unwrap(); | |
| 2113 | let source = self.active_outline().document.paragraph(index).unwrap(); | |
| 2170 | 2114 | let first = if index == start.paragraph { |
| 2171 | 2115 | paragraph.cursor(start.offset, affinities[0])? |
| 2172 | 2116 | } else { |
| ... | ... | @@ -2249,8 +2193,7 @@ impl CanvasEditor { |
| 2249 | 2193 | let format = self |
| 2250 | 2194 | .active_outline() |
| 2251 | 2195 | .document |
| 2252 | .paragraphs() | |
| 2253 | .nth(start.paragraph) | |
| 2196 | .paragraph(start.paragraph) | |
| 2254 | 2197 | .unwrap() |
| 2255 | 2198 | .format_at(start.offset)?; |
| 2256 | 2199 | let replacement = text |
| ... | ... | @@ -2268,11 +2211,13 @@ impl CanvasEditor { |
| 2268 | 2211 | let [anchor, focus] = outline.selection.positions; |
| 2269 | 2212 | let start = anchor.min(focus); |
| 2270 | 2213 | let end = anchor.max(focus); |
| 2271 | let (container, local_start, _) = leaves(outline.document.nodes(), None) | |
| 2272 | .nth(start.paragraph) | |
| 2214 | let (container, local_start, _) = outline | |
| 2215 | .document | |
| 2216 | .leaf(start.paragraph) | |
| 2273 | 2217 | .ok_or(EditError::InvalidRange)?; |
| 2274 | let (end_container, local_end, _) = leaves(outline.document.nodes(), None) | |
| 2275 | .nth(end.paragraph) | |
| 2218 | let (end_container, local_end, _) = outline | |
| 2219 | .document | |
| 2220 | .leaf(end.paragraph) | |
| 2276 | 2221 | .ok_or(EditError::InvalidRange)?; |
| 2277 | 2222 | if container != end_container { |
| 2278 | 2223 | return Err(EditError::UnsupportedContent.into()); |
| ... | ... | @@ -2343,12 +2288,10 @@ impl CanvasEditor { |
| 2343 | 2288 | }) else { |
| 2344 | 2289 | return Ok(false); |
| 2345 | 2290 | }; |
| 2346 | let (container, local, current) = leaves(document.nodes(), None) | |
| 2347 | .nth(focus.paragraph) | |
| 2291 | let (container, local, current) = document | |
| 2292 | .leaf(focus.paragraph) | |
| 2348 | 2293 | .ok_or(EditError::InvalidRange)?; |
| 2349 | let Some((next_container, next_local, next)) = | |
| 2350 | leaves(document.nodes(), None).nth(neighbor) | |
| 2351 | else { | |
| 2294 | let Some((next_container, next_local, next)) = document.leaf(neighbor) else { | |
| 2352 | 2295 | return Ok(false); |
| 2353 | 2296 | }; |
| 2354 | 2297 | if container != next_container || local.abs_diff(next_local) != 1 { |
| ... | ... | @@ -2406,8 +2349,7 @@ impl CanvasEditor { |
| 2406 | 2349 | let format = self |
| 2407 | 2350 | .active_outline() |
| 2408 | 2351 | .document |
| 2409 | .paragraphs() | |
| 2410 | .nth(caret.paragraph) | |
| 2352 | .paragraph(caret.paragraph) | |
| 2411 | 2353 | .unwrap() |
| 2412 | 2354 | .format_at(caret.offset)? |
| 2413 | 2355 | .clone(); |
| ... | ... | @@ -2779,8 +2721,7 @@ impl CanvasEditor { |
| 2779 | 2721 | let format = self |
| 2780 | 2722 | .active_outline() |
| 2781 | 2723 | .document |
| 2782 | .paragraphs() | |
| 2783 | .nth(range.start.paragraph) | |
| 2724 | .paragraph(range.start.paragraph) | |
| 2784 | 2725 | .unwrap() |
| 2785 | 2726 | .format_at(range.start.offset)?; |
| 2786 | 2727 | let replacement = text |
| ... | ... | @@ -2922,55 +2863,29 @@ impl CanvasEditor { |
| 2922 | 2863 | count -= 1; |
| 2923 | 2864 | } |
| 2924 | 2865 | if count < outline.document.nodes().len() { |
| 2925 | let removed = outline.document.nodes().len() - count; | |
| 2926 | outline | |
| 2927 | .document | |
| 2928 | .apply(DocumentEdit { | |
| 2929 | columns: BTreeMap::new(), | |
| 2930 | container: None, | |
| 2931 | range: count..outline.document.nodes().len(), | |
| 2932 | replacement: Vec::new(), | |
| 2933 | }) | |
| 2934 | .expect("removing a provisional suffix preserves a valid document"); | |
| 2935 | outline | |
| 2866 | let edit = DocumentEdit { | |
| 2867 | columns: BTreeMap::new(), | |
| 2868 | container: None, | |
| 2869 | range: count..outline.document.nodes().len(), | |
| 2870 | replacement: Vec::new(), | |
| 2871 | }; | |
| 2872 | let relayout = outline | |
| 2936 | 2873 | .shaped |
| 2937 | .paragraphs | |
| 2938 | .truncate(outline.shaped.paragraphs.len() - removed); | |
| 2939 | if outline.shaped.tables.is_empty() && outline.shaped.objects.is_empty() { | |
| 2940 | let (origins, size) = arrange( | |
| 2941 | outline.shaped.paragraphs.iter(), | |
| 2942 | outline.wrap_width(), | |
| 2943 | outline.layout.width_set_by_user == Some(true), | |
| 2944 | ) | |
| 2945 | .expect("a prefix of the validated layout fits"); | |
| 2946 | for (paragraph, y) in outline.shaped.paragraphs.iter_mut().zip(origins) { | |
| 2947 | paragraph.origin[1] = y; | |
| 2948 | } | |
| 2949 | outline.shaped.size = size; | |
| 2950 | } else { | |
| 2951 | let cached = outline | |
| 2952 | .shaped | |
| 2953 | .paragraphs | |
| 2954 | .iter() | |
| 2955 | .chain(outline.shaped.objects.iter().filter_map(|o| o.label())) | |
| 2956 | .map(|paragraph| (paragraph.id, paragraph)) | |
| 2957 | .collect::<BTreeMap<_, _>>(); | |
| 2958 | let shaped = OutlineLayout::flow( | |
| 2959 | outline.document.nodes().iter(), | |
| 2874 | .relayout( | |
| 2875 | outline.document.nodes(), | |
| 2876 | &edit, | |
| 2877 | edit.range.clone(), | |
| 2960 | 2878 | &outline.indents, |
| 2961 | 2879 | outline.wrap_width(), |
| 2962 | 2880 | outline.layout.width_set_by_user == Some(true), |
| 2963 | 0, | |
| 2964 | None, | |
| 2965 | &mut |node, width, indents| { | |
| 2966 | let mut paragraph = cached[&node.id].clone(); | |
| 2967 | paragraph.reset_origin(indentation(node.level, indents, width)?); | |
| 2968 | Ok(paragraph) | |
| 2969 | }, | |
| 2881 | &mut |_, _, _| Err(LayoutError::UnsupportedContent), | |
| 2970 | 2882 | ) |
| 2971 | .expect("removing a provisional suffix preserves table layout"); | |
| 2972 | outline.shaped = shaped; | |
| 2973 | } | |
| 2883 | .expect("removing trailing text lays nothing out anew"); | |
| 2884 | outline | |
| 2885 | .document | |
| 2886 | .apply(edit) | |
| 2887 | .expect("removing a provisional suffix preserves a valid document"); | |
| 2888 | outline.shaped.commit(relayout); | |
| 2974 | 2889 | } |
| 2975 | 2890 | if outline.document == self.outlines[index].document { |
| 2976 | 2891 | self.outlines[index].selection = outline.selection; |
| ... | ... | @@ -3054,117 +2969,47 @@ impl CanvasEditor { |
| 3054 | 2969 | ) -> Result<TextChange, EditorError> { |
| 3055 | 2970 | let outline = self.active_outline(); |
| 3056 | 2971 | outline.document.validate_edit(&edit)?; |
| 3057 | let width = outline.wrap_width(); | |
| 3058 | let (visible, shaped, origins, size, tables, objects) = if !crate::outline::all_text( | |
| 3059 | outline.document.nodes().iter().chain(&edit.replacement), | |
| 3060 | ) { | |
| 3061 | let sources = outline | |
| 3062 | .document | |
| 3063 | .text_nodes() | |
| 3064 | .map(|node| (node.id, node)) | |
| 3065 | .collect::<BTreeMap<_, _>>(); | |
| 3066 | let existing = outline | |
| 3067 | .shaped | |
| 3068 | .paragraphs | |
| 3069 | .iter() | |
| 3070 | .map(|layout| (layout.id, (sources[&layout.id], layout))) | |
| 3071 | .collect::<BTreeMap<_, _>>(); | |
| 3072 | let layout = OutlineLayout::flow( | |
| 3073 | edited_nodes(outline.document.nodes(), None, Some(&edit)), | |
| 3074 | &outline.indents, | |
| 3075 | width, | |
| 3076 | outline.layout.width_set_by_user == Some(true), | |
| 3077 | 0, | |
| 3078 | Some(&edit), | |
| 3079 | &mut |node, width, indents| { | |
| 3080 | let indent = indentation(node.level, indents, width)?; | |
| 3081 | if let Some((source, cached)) = existing.get(&node.id) | |
| 3082 | && *source == node | |
| 3083 | && cached.text.shaped.layout_max_advance() == width - indent | |
| 3084 | && cached | |
| 3085 | .markers | |
| 3086 | .iter() | |
| 3087 | .all(|(marker, _)| marker.shaped.layout_max_advance() == width) | |
| 3088 | { | |
| 3089 | let mut result = (*cached).clone(); | |
| 3090 | result.reset_origin(indent); | |
| 3091 | return Ok(result); | |
| 3092 | } | |
| 3093 | ParagraphLayout::shape(engine, node, width, indents, &self.definitions) | |
| 3094 | }, | |
| 3095 | )?; | |
| 3096 | let origins = layout | |
| 3097 | .paragraphs | |
| 3098 | .iter() | |
| 3099 | .map(|p| p.origin[1]) | |
| 3100 | .collect::<Vec<_>>(); | |
| 3101 | ( | |
| 3102 | 0..outline.shaped.paragraphs.len(), | |
| 3103 | layout.paragraphs, | |
| 3104 | origins, | |
| 3105 | layout.size, | |
| 3106 | layout.tables, | |
| 3107 | layout.objects, | |
| 3108 | ) | |
| 3109 | } else { | |
| 3110 | let visible_start = outline | |
| 3111 | .layouts() | |
| 3112 | .take_while(|(i, _)| *i < edit.range.start) | |
| 3113 | .count(); | |
| 3114 | let visible_end = outline | |
| 3115 | .layouts() | |
| 3116 | .take_while(|(i, _)| *i < edit.range.end) | |
| 3117 | .count(); | |
| 3118 | if visible_end - visible_start != edit.range.len() { | |
| 3119 | return Err(EditError::UnsupportedContent.into()); | |
| 2972 | let range = match edit.container { | |
| 2973 | None => edit.range.clone(), | |
| 2974 | Some(cell) => { | |
| 2975 | let root = outline.document.root(cell)?; | |
| 2976 | root..root + 1 | |
| 3120 | 2977 | } |
| 3121 | let shaped = edit | |
| 3122 | .replacement | |
| 3123 | .iter() | |
| 3124 | .map(|paragraph| { | |
| 3125 | ParagraphLayout::shape( | |
| 3126 | engine, | |
| 3127 | paragraph, | |
| 3128 | width, | |
| 3129 | &outline.indents, | |
| 3130 | &self.definitions, | |
| 3131 | ) | |
| 3132 | }) | |
| 3133 | .collect::<Result<Vec<_>, _>>()?; | |
| 3134 | let (origins, size) = arrange( | |
| 3135 | outline.shaped.paragraphs[..visible_start] | |
| 3136 | .iter() | |
| 3137 | .chain(&shaped) | |
| 3138 | .chain(&outline.shaped.paragraphs[visible_end..]), | |
| 3139 | width, | |
| 3140 | outline.layout.width_set_by_user == Some(true), | |
| 3141 | )?; | |
| 3142 | ( | |
| 3143 | visible_start..visible_end, | |
| 3144 | shaped, | |
| 3145 | origins, | |
| 3146 | size, | |
| 3147 | Vec::new(), | |
| 3148 | Vec::new(), | |
| 3149 | ) | |
| 3150 | 2978 | }; |
| 2979 | let relayout = outline.shaped.relayout( | |
| 2980 | outline.document.nodes(), | |
| 2981 | &edit, | |
| 2982 | range, | |
| 2983 | &outline.indents, | |
| 2984 | outline.wrap_width(), | |
| 2985 | outline.layout.width_set_by_user == Some(true), | |
| 2986 | &mut |node, width, indents| { | |
| 2987 | ParagraphLayout::shape(engine, node, width, indents, &self.definitions) | |
| 2988 | }, | |
| 2989 | )?; | |
| 2990 | let [replaced, ..] = outline.shaped.pieces(relayout.range.clone()); | |
| 3151 | 2991 | for position in selection.positions { |
| 3152 | let (_, _, node) = leaves(outline.document.nodes(), Some(&edit)) | |
| 3153 | .nth(position.paragraph) | |
| 2992 | let (node, before) = outline | |
| 2993 | .document | |
| 2994 | .edited_leaf(&edit, position.paragraph) | |
| 3154 | 2995 | .ok_or(EditError::InvalidRange)?; |
| 3155 | 2996 | node.text().unwrap().text.byte_offset(position.offset)?; |
| 3156 | if !outline.shaped.paragraphs[..visible.start] | |
| 2997 | // A paragraph the edit leaves keeps its layout unless that layout is replaced. | |
| 2998 | if !relayout | |
| 2999 | .segment | |
| 3000 | .paragraphs | |
| 3157 | 3001 | .iter() |
| 3158 | .chain(&shaped) | |
| 3159 | .chain(&outline.shaped.paragraphs[visible.end..]) | |
| 3160 | 3002 | .any(|paragraph| paragraph.id == node.id) |
| 3003 | && !before | |
| 3004 | .and_then(|before| outline.visible_index(before).ok()) | |
| 3005 | .is_some_and(|index| !replaced.contains(&index)) | |
| 3161 | 3006 | { |
| 3162 | 3007 | return Err(EditError::InvalidRange.into()); |
| 3163 | 3008 | } |
| 3164 | 3009 | } |
| 3165 | 3010 | let placements = match positions { |
| 3166 | 3011 | Some(positions) => positions.to_vec(), |
| 3167 | None => self.title_flow(size[1])?, | |
| 3012 | None => self.title_flow(relayout.size[1])?, | |
| 3168 | 3013 | }; |
| 3169 | 3014 | let previous_positions = placements |
| 3170 | 3015 | .iter() |
| ... | ... | @@ -3193,14 +3038,8 @@ impl CanvasEditor { |
| 3193 | 3038 | return self.apply(engine, edit, selection, false, positions); |
| 3194 | 3039 | } |
| 3195 | 3040 | let outline = self.active_outline_mut(); |
| 3196 | let inverse = outline.document.apply(edit)?; | |
| 3197 | outline.shaped.paragraphs.splice(visible, shaped); | |
| 3198 | outline.shaped.tables = tables; | |
| 3199 | outline.shaped.objects = objects; | |
| 3200 | for (paragraph, y) in outline.shaped.paragraphs.iter_mut().zip(origins) { | |
| 3201 | paragraph.origin[1] = y; | |
| 3202 | } | |
| 3203 | outline.shaped.size = size; | |
| 3041 | let inverse = outline.document.splice(edit); | |
| 3042 | outline.shaped.commit(relayout); | |
| 3204 | 3043 | self.preferred_x = None; |
| 3205 | 3044 | let previous_selection = |
| 3206 | 3045 | std::mem::replace(&mut self.active_outline_mut().selection, selection); |
| ... | ... | @@ -7414,6 +7253,175 @@ mod tests { |
| 7414 | 7253 | assert!(!editor.redo(&mut engine).unwrap()); |
| 7415 | 7254 | } |
| 7416 | 7255 | |
| 7256 | #[test] | |
| 7257 | fn typing_in_place_matches_a_fresh_layout_for_automatic_and_fixed_widths() { | |
| 7258 | use onestore::page::{Image, ParagraphContent, text::new_id}; | |
| 7259 | let mut engine = TextEngine::default(); | |
| 7260 | let mut seed = 0x51ed_u64; | |
| 7261 | let mut next = |n: usize| { | |
| 7262 | seed = seed | |
| 7263 | .wrapping_mul(6364136223846793005) | |
| 7264 | .wrapping_add(1442695040888963407); | |
| 7265 | (seed >> 33) as usize % n | |
| 7266 | }; | |
| 7267 | for (text, objects) in [ | |
| 7268 | (vec!["x"], false), | |
| 7269 | (vec!["one two", "three", "four five six seven"], false), | |
| 7270 | (vec!["x"], true), | |
| 7271 | (vec!["one two", "three", "four five six seven"], true), | |
| 7272 | ] { | |
| 7273 | let fixed = text.len() > 1; | |
| 7274 | let empty = TextDocument::new(vec![Paragraph::new(String::new(), Format::default())]); | |
| 7275 | let mut editor = CanvasEditor::new(&mut engine, empty.unwrap(), 120.0).unwrap(); | |
| 7276 | editor.insert(&mut engine, &text.join("\n")).unwrap(); | |
| 7277 | if fixed { | |
| 7278 | editor.resize(&mut engine, 120.0).unwrap(); | |
| 7279 | } | |
| 7280 | let table = table_editor(&mut engine).active_outline().document.nodes()[0].clone(); | |
| 7281 | let mut picture = table.clone(); | |
| 7282 | picture.id = new_id().unwrap(); | |
| 7283 | picture.content = ParagraphContent::Image(Image { | |
| 7284 | id: new_id().unwrap(), | |
| 7285 | layout: onestore::document::Layout { | |
| 7286 | max_width: Some(90.0), | |
| 7287 | max_height: Some(40.0), | |
| 7288 | ..Default::default() | |
| 7289 | }, | |
| 7290 | size: None, | |
| 7291 | bytes: None, | |
| 7292 | alt: None, | |
| 7293 | background: false, | |
| 7294 | }); | |
| 7295 | if objects { | |
| 7296 | let selection = [TextPosition { | |
| 7297 | paragraph: 0, | |
| 7298 | offset: 0, | |
| 7299 | }; 2] | |
| 7300 | .into(); | |
| 7301 | editor | |
| 7302 | .commit( | |
| 7303 | &mut engine, | |
| 7304 | DocumentEdit { | |
| 7305 | columns: BTreeMap::new(), | |
| 7306 | container: None, | |
| 7307 | range: 1..1, | |
| 7308 | replacement: vec![picture.clone(), table], | |
| 7309 | }, | |
| 7310 | selection, | |
| 7311 | ) | |
| 7312 | .unwrap(); | |
| 7313 | } | |
| 7314 | for step in 0..300 { | |
| 7315 | let outline = editor.active_outline(); | |
| 7316 | let paragraph = next(outline.document.paragraphs().count()); | |
| 7317 | let text = outline.document.paragraph(paragraph).unwrap(); | |
| 7318 | let offset = text.utf16_offset(text.text().len()).unwrap(); | |
| 7319 | let position = TextPosition { | |
| 7320 | paragraph, | |
| 7321 | offset: next(offset as usize + 1) as u32, | |
| 7322 | }; | |
| 7323 | // Paragraphs under a collapsed one hold no caret. | |
| 7324 | let _ = editor.select([position; 2].into()); | |
| 7325 | let nodes = editor.active_outline().document.nodes(); | |
| 7326 | let root = next(nodes.len()); | |
| 7327 | let mut node = nodes[root].clone(); | |
| 7328 | let edit = |range, replacement| DocumentEdit { | |
| 7329 | columns: BTreeMap::new(), | |
| 7330 | container: None, | |
| 7331 | range, | |
| 7332 | replacement, | |
| 7333 | }; | |
| 7334 | let edit = match next(14) { | |
| 7335 | 10 => { | |
| 7336 | node.collapsed = !node.collapsed; | |
| 7337 | Some(edit(root..root + 1, vec![node])) | |
| 7338 | } | |
| 7339 | 11 if root > 0 && node.text().is_some() => { | |
| 7340 | let parent = &nodes[root - 1]; | |
| 7341 | node.parent = Some(parent.id); | |
| 7342 | node.level = parent.level + 1; | |
| 7343 | Some(edit(root..root + 1, vec![node])) | |
| 7344 | } | |
| 7345 | 12 if node.text().is_none() || nodes.len() > 3 => { | |
| 7346 | Some(edit(root..root + 1, Vec::new())) | |
| 7347 | } | |
| 7348 | 13 => { | |
| 7349 | let mut picture = picture.clone(); | |
| 7350 | picture.id = new_id().unwrap(); | |
| 7351 | let ParagraphContent::Image(image) = &mut picture.content else { | |
| 7352 | unreachable!() | |
| 7353 | }; | |
| 7354 | image.id = new_id().unwrap(); | |
| 7355 | Some(edit(root..root, vec![picture])) | |
| 7356 | } | |
| 7357 | _ => None, | |
| 7358 | }; | |
| 7359 | let _ = match (edit, next(10)) { | |
| 7360 | (Some(edit), _) => { | |
| 7361 | let start = [TextPosition { | |
| 7362 | paragraph: 0, | |
| 7363 | offset: 0, | |
| 7364 | }; 2]; | |
| 7365 | editor.commit(&mut engine, edit, start.into()) | |
| 7366 | } | |
| 7367 | (None, 0) => editor.insert(&mut engine, "\n"), | |
| 7368 | (None, 1 | 2) => editor.delete(&mut engine, next(2) == 0).map(drop), | |
| 7369 | (None, 3) => editor.undo(&mut engine).map(drop), | |
| 7370 | (None, _) => editor.insert(&mut engine, ["a", " ", "wide", "W"][next(4)]), | |
| 7371 | }; | |
| 7372 | let outline = editor.active_outline(); | |
| 7373 | let fresh = | |
| 7374 | TextOutline::from_outline(&mut engine, &outline.snapshot(), &BTreeMap::new()) | |
| 7375 | .unwrap(); | |
| 7376 | let layout = |outline: &TextOutline| { | |
| 7377 | let shaped = &outline.shaped; | |
| 7378 | let paragraphs = shaped.paragraphs.iter().map(|paragraph| { | |
| 7379 | ( | |
| 7380 | paragraph.id, | |
| 7381 | paragraph.origin, | |
| 7382 | paragraph | |
| 7383 | .markers | |
| 7384 | .iter() | |
| 7385 | .map(|marker| marker.1) | |
| 7386 | .collect::<Vec<_>>(), | |
| 7387 | paragraph | |
| 7388 | .tags | |
| 7389 | .iter() | |
| 7390 | .map(|tag| tag.origin) | |
| 7391 | .collect::<Vec<_>>(), | |
| 7392 | ) | |
| 7393 | }); | |
| 7394 | let cells = shaped.tables.iter().flat_map(|table| { | |
| 7395 | table | |
| 7396 | .cells | |
| 7397 | .iter() | |
| 7398 | .map(|cell| (cell.id, cell.rect, cell.paragraphs.clone())) | |
| 7399 | }); | |
| 7400 | let objects = shaped.objects.iter().map(|object| { | |
| 7401 | let label = object.label().map(|label| label.origin); | |
| 7402 | (object.id, object.rect, object.bottom, label) | |
| 7403 | }); | |
| 7404 | format!( | |
| 7405 | "{:?}", | |
| 7406 | ( | |
| 7407 | shaped.size, | |
| 7408 | shaped.widths, | |
| 7409 | outline.bounds(), | |
| 7410 | paragraphs.collect::<Vec<_>>(), | |
| 7411 | cells.collect::<Vec<_>>(), | |
| 7412 | objects.collect::<Vec<_>>(), | |
| 7413 | ) | |
| 7414 | ) | |
| 7415 | }; | |
| 7416 | assert_eq!(layout(outline), layout(&fresh), "step {step}"); | |
| 7417 | } | |
| 7418 | assert_eq!( | |
| 7419 | editor.active_outline().layout.width_set_by_user, | |
| 7420 | Some(fixed) | |
| 7421 | ); | |
| 7422 | } | |
| 7423 | } | |
| 7424 | ||
| 7417 | 7425 | #[test] |
| 7418 | 7426 | fn structural_edits_reflow_only_affected_paragraphs_and_restore_selection() { |
| 7419 | 7427 | let mut engine = TextEngine::default(); |
crates/canvas/src/editor/page.rs-1| ... | ... | @@ -97,7 +97,6 @@ impl ReadOnlyObject { |
| 97 | 97 | onestore::document::Format { |
| 98 | 98 | font: Some("Arial".into()), |
| 99 | 99 | font_size: Some(11.0), |
| 100 | color: Some(0x005d554e), | |
| 101 | 100 | ..Default::default() |
| 102 | 101 | }, |
| 103 | 102 | ), |
crates/canvas/src/editor/table.rs+6-4| ... | ... | @@ -84,8 +84,9 @@ impl CanvasEditor { |
| 84 | 84 | } |
| 85 | 85 | let outline = self.active_outline(); |
| 86 | 86 | let [anchor, focus] = outline.selection.positions; |
| 87 | let (cell, local, source) = leaves(outline.document.nodes(), None) | |
| 88 | .nth(focus.paragraph) | |
| 87 | let (cell, local, source) = outline | |
| 88 | .document | |
| 89 | .leaf(focus.paragraph) | |
| 89 | 90 | .ok_or(EditError::InvalidRange)?; |
| 90 | 91 | let Some(cell) = cell else { |
| 91 | 92 | if backward || anchor != focus || focus.offset == 0 { |
| ... | ... | @@ -255,8 +256,9 @@ impl CanvasEditor { |
| 255 | 256 | } |
| 256 | 257 | let outline = self.active_outline(); |
| 257 | 258 | let [anchor, focus] = outline.selection.positions; |
| 258 | let (cell, local, source) = leaves(outline.document.nodes(), None) | |
| 259 | .nth(focus.paragraph) | |
| 259 | let (cell, local, source) = outline | |
| 260 | .document | |
| 261 | .leaf(focus.paragraph) | |
| 260 | 262 | .ok_or(EditError::InvalidRange)?; |
| 261 | 263 | let Some(cell) = cell.filter(|_| anchor == focus) else { |
| 262 | 264 | return self.insert(engine, "\n"); |
crates/canvas/src/gpu/mod.rs+71-18| ... | ... | @@ -37,6 +37,32 @@ impl Viewport { |
| 37 | 37 | } |
| 38 | 38 | } |
| 39 | 39 | |
| 40 | /// The page's paper and the ink that content in OneNote's automatic colour draws in, | |
| 41 | /// linear RGBA. | |
| 42 | #[derive(Clone, Copy, Debug)] | |
| 43 | pub struct Paper { | |
| 44 | pub color: [f32; 4], | |
| 45 | pub ink: [f32; 4], | |
| 46 | } | |
| 47 | ||
| 48 | impl Paper { | |
| 49 | pub const WHITE: Self = Self { | |
| 50 | color: [1.0; 4], | |
| 51 | ink: [0.0, 0.0, 0.0, 1.0], | |
| 52 | }; | |
| 53 | ||
| 54 | /// A near-neutral colour OneNote draws on white paper, moved onto this paper: its | |
| 55 | /// darkness becomes ink and its tint tints the paper. | |
| 56 | fn shade(&self, light: [f32; 4]) -> [f32; 4] { | |
| 57 | let ink = 1.0 - (light[0] + light[1] + light[2]) / 3.0; | |
| 58 | let mut shade = light; | |
| 59 | for channel in 0..3 { | |
| 60 | shade[channel] = self.color[channel] * light[channel] + self.ink[channel] * ink; | |
| 61 | } | |
| 62 | shade | |
| 63 | } | |
| 64 | } | |
| 65 | ||
| 40 | 66 | impl Glyphs for TextLayout { |
| 41 | 67 | fn runs( |
| 42 | 68 | &self, |
| ... | ... | @@ -52,7 +78,8 @@ impl Glyphs for TextLayout { |
| 52 | 78 | bounds.baseline, |
| 53 | 79 | run.style().brush.rise, |
| 54 | 80 | [bounds.top, bounds.height], |
| 55 | |brush: &TextBrush| colorref(brush.color), | |
| 81 | // A highlight keeps its stored colour, so automatic text on it stays black. | |
| 82 | |brush: &TextBrush| brush.color.or(brush.highlight.map(|_| 0)).map(colorref), | |
| 56 | 83 | paint, |
| 57 | 84 | )?; |
| 58 | 85 | } |
| ... | ... | @@ -147,14 +174,24 @@ mod tests { |
| 147 | 174 | ]; |
| 148 | 175 | let text = TextEngine::default() |
| 149 | 176 | .layout( |
| 150 | &Paragraph::new( | |
| 151 | "Tagged".into(), | |
| 152 | Format { | |
| 153 | color: Some(0x000000ff), | |
| 154 | underline: Some(true), | |
| 155 | ..Format::default() | |
| 156 | }, | |
| 157 | ), | |
| 177 | &Paragraph::from_runs([ | |
| 178 | ( | |
| 179 | "Tagged".into(), | |
| 180 | Format { | |
| 181 | color: Some(0x000000ff), | |
| 182 | underline: Some(true), | |
| 183 | ..Format::default() | |
| 184 | }, | |
| 185 | ), | |
| 186 | (" automatic".into(), Format::default()), | |
| 187 | ( | |
| 188 | " lit".into(), | |
| 189 | Format { | |
| 190 | highlight: Some(0x0000ffff), | |
| 191 | ..Format::default() | |
| 192 | }, | |
| 193 | ), | |
| 194 | ]), | |
| 158 | 195 | 100.0, |
| 159 | 196 | ) |
| 160 | 197 | .unwrap(); |
| ... | ... | @@ -165,13 +202,14 @@ mod tests { |
| 165 | 202 | sources: tag_sources(*icon), |
| 166 | 203 | origin: [2.0 + 16.0 * index as f32, 2.0], |
| 167 | 204 | size: crate::outline::ParagraphTag::SIZE, |
| 168 | opacity: 1.0, | |
| 205 | tint: [1.0; 4], | |
| 169 | 206 | }) |
| 170 | 207 | .collect(); |
| 171 | 208 | primitives.push(Primitive::Text { |
| 172 | 209 | text: &text, |
| 173 | 210 | origin: [2.0, 20.0], |
| 174 | 211 | clip: None, |
| 212 | ink: [0.0, 1.0, 0.0, 1.0], | |
| 175 | 213 | }); |
| 176 | 214 | let viewport = Viewport { |
| 177 | 215 | size, |
| ... | ... | @@ -224,13 +262,28 @@ mod tests { |
| 224 | 262 | .count(); |
| 225 | 263 | assert!(painted > 20, "tag {index} was not painted"); |
| 226 | 264 | } |
| 227 | let red = (40..64) | |
| 228 | .flat_map(|y| (4..120).map(move |x| (x, y))) | |
| 229 | .filter(|(x, y)| { | |
| 230 | let [r, g, b] = pixel(*x, *y); | |
| 231 | r > 200 && g < 80 && b < 80 | |
| 232 | }) | |
| 233 | .count(); | |
| 234 | assert!(red > 50, "text colour or underline was not painted"); | |
| 265 | let count = |color: [u8; 3]| { | |
| 266 | (40..64) | |
| 267 | .flat_map(|y| (4..size[0]).map(move |x| (x, y))) | |
| 268 | .filter(|(x, y)| { | |
| 269 | pixel(*x, *y) | |
| 270 | .iter() | |
| 271 | .zip(color) | |
| 272 | .all(|(v, c)| v.abs_diff(c) < 80) | |
| 273 | }) | |
| 274 | .count() | |
| 275 | }; | |
| 276 | assert!( | |
| 277 | count([255, 0, 0]) > 50, | |
| 278 | "text colour or underline was not painted" | |
| 279 | ); | |
| 280 | assert!( | |
| 281 | count([0, 255, 0]) > 50, | |
| 282 | "automatic text did not paint in the primitive's ink" | |
| 283 | ); | |
| 284 | assert!( | |
| 285 | count([0, 0, 0]) > 10, | |
| 286 | "automatic text on a highlight did not stay black" | |
| 287 | ); | |
| 235 | 288 | } |
| 236 | 289 | } |
crates/canvas/src/gpu/page.rs+153-68| ... | ... | @@ -1,4 +1,6 @@ |
| 1 | use super::colorref; | |
| 1 | mod background; | |
| 2 | ||
| 3 | use super::{Paper, colorref}; | |
| 2 | 4 | use crate::editor::ReadOnlyObject; |
| 3 | 5 | use crate::editor::page::Content; |
| 4 | 6 | use crate::{ |
| ... | ... | @@ -6,6 +8,7 @@ use crate::{ |
| 6 | 8 | editor::{CanvasEditor, EditorError}, |
| 7 | 9 | layout::{LayoutError, TextEngine}, |
| 8 | 10 | }; |
| 11 | use background::Background; | |
| 9 | 12 | use draw::{Primitive, RasterImage, RenderError}; |
| 10 | 13 | use onestore::page::Page; |
| 11 | 14 | use std::fmt; |
| ... | ... | @@ -14,6 +17,8 @@ use std::fmt; |
| 14 | 17 | pub struct PageScene { |
| 15 | 18 | reference: Option<Vec<Content>>, |
| 16 | 19 | images: std::collections::BTreeMap<onestore::ExGuid, RasterImage>, |
| 20 | /// Background pictures from OneNote's page templates, painted from their recreations. | |
| 21 | backgrounds: std::collections::BTreeMap<onestore::ExGuid, Background>, | |
| 17 | 22 | } |
| 18 | 23 | |
| 19 | 24 | #[derive(Debug, PartialEq)] |
| ... | ... | @@ -82,6 +87,17 @@ fn append_ink(ink: &onestore::page::Ink, offset: [f32; 2], primitives: &mut Vec< |
| 82 | 87 | } |
| 83 | 88 | } |
| 84 | 89 | |
| 90 | fn append_placeholder(rect: [f32; 4], paper: Paper, primitives: &mut Vec<Primitive<'_>>) { | |
| 91 | primitives.push(Primitive::Rect { | |
| 92 | rect, | |
| 93 | color: paper.shade(colorref(0x00e4ddd6)), | |
| 94 | }); | |
| 95 | primitives.push(Primitive::Rect { | |
| 96 | rect: [rect[0] + 1.0, rect[1] + 1.0, rect[2] - 1.0, rect[3] - 1.0], | |
| 97 | color: paper.shade(colorref(0x00faf7f3)), | |
| 98 | }); | |
| 99 | } | |
| 100 | ||
| 85 | 101 | #[derive(Debug)] |
| 86 | 102 | pub enum SceneError { |
| 87 | 103 | Layout(LayoutError), |
| ... | ... | @@ -127,10 +143,10 @@ impl PageScene { |
| 127 | 143 | let objects = crate::editor::page::build(&mut page, engine, false) |
| 128 | 144 | .map_err(SceneError::from)? |
| 129 | 145 | .objects; |
| 130 | let images = Self::decode_images(&objects, None)?; | |
| 146 | let pictures = Self::decode_images(&objects, None)?; | |
| 131 | 147 | Ok(Self { |
| 132 | 148 | reference: Some(objects), |
| 133 | images, | |
| 149 | ..pictures | |
| 134 | 150 | }) |
| 135 | 151 | } |
| 136 | 152 | |
| ... | ... | @@ -139,21 +155,15 @@ impl PageScene { |
| 139 | 155 | engine: &mut TextEngine, |
| 140 | 156 | ) -> Result<(Self, CanvasEditor), SceneError> { |
| 141 | 157 | let editor = CanvasEditor::from_page(page, engine).map_err(SceneError::from)?; |
| 142 | let images = Self::decode_images(&editor.objects, Some(&editor))?; | |
| 143 | Ok(( | |
| 144 | Self { | |
| 145 | reference: None, | |
| 146 | images, | |
| 147 | }, | |
| 148 | editor, | |
| 149 | )) | |
| 158 | Ok((Self::decode_images(&editor.objects, Some(&editor))?, editor)) | |
| 150 | 159 | } |
| 151 | 160 | |
| 152 | /// Decodes every picture the page draws: page-level ones and those inside outlines. | |
| 161 | /// Decodes every picture the page draws, page-level ones and those inside outlines, | |
| 162 | /// except the template backgrounds it recognises. | |
| 153 | 163 | fn decode_images( |
| 154 | 164 | objects: &[Content], |
| 155 | 165 | editor: Option<&CanvasEditor>, |
| 156 | ) -> Result<std::collections::BTreeMap<onestore::ExGuid, RasterImage>, SceneError> { | |
| 166 | ) -> Result<Self, SceneError> { | |
| 157 | 167 | fn nested<'a>( |
| 158 | 168 | nodes: &'a [onestore::page::PageParagraph], |
| 159 | 169 | payloads: &mut Vec<(onestore::ExGuid, Option<&'a [u8]>)>, |
| ... | ... | @@ -179,9 +189,18 @@ impl PageScene { |
| 179 | 189 | } |
| 180 | 190 | } |
| 181 | 191 | let mut payloads = Vec::new(); |
| 192 | let mut backgrounds = std::collections::BTreeMap::new(); | |
| 182 | 193 | for object in objects { |
| 183 | 194 | match object { |
| 184 | Content::Image(source) => payloads.push((source.id, source.bytes.as_deref())), | |
| 195 | Content::Image(source) => { | |
| 196 | if source.background | |
| 197 | && let Some(art) = Background::recognise(source) | |
| 198 | { | |
| 199 | backgrounds.insert(source.id, art); | |
| 200 | } else { | |
| 201 | payloads.push((source.id, source.bytes.as_deref())) | |
| 202 | } | |
| 203 | } | |
| 185 | 204 | Content::Outline { source, .. } => nested(&source.paragraphs, &mut payloads), |
| 186 | 205 | Content::Editable(id) => { |
| 187 | 206 | if let Some(outline) = |
| ... | ... | @@ -206,13 +225,34 @@ impl PageScene { |
| 206 | 225 | return Err(SceneError::InvalidGeometry); |
| 207 | 226 | } |
| 208 | 227 | } |
| 209 | Ok(images) | |
| 228 | Ok(Self { | |
| 229 | reference: None, | |
| 230 | images, | |
| 231 | backgrounds, | |
| 232 | }) | |
| 210 | 233 | } |
| 211 | 234 | |
| 212 | 235 | pub fn image(&self, id: onestore::ExGuid) -> Option<&RasterImage> { |
| 213 | 236 | self.images.get(&id) |
| 214 | 237 | } |
| 215 | 238 | |
| 239 | /// Brings template backgrounds to `paper` at `scale` device pixels per point, rasterizing | |
| 240 | /// off this thread; `waker` is woken when a raster lands and the page should be drawn | |
| 241 | /// again. Call before collecting each frame's primitives. True once every background | |
| 242 | /// shows its raster for this paper and scale. | |
| 243 | pub fn update_backgrounds( | |
| 244 | &mut self, | |
| 245 | paper: Paper, | |
| 246 | scale: f32, | |
| 247 | waker: &std::task::Waker, | |
| 248 | ) -> bool { | |
| 249 | let mut settled = true; | |
| 250 | for art in self.backgrounds.values_mut() { | |
| 251 | settled &= art.update(paper, scale, waker); | |
| 252 | } | |
| 253 | settled | |
| 254 | } | |
| 255 | ||
| 216 | 256 | /// Pictures, files and handwriting inside an outline whose origin is `origin`. |
| 217 | 257 | /// `moving` draws one picture at a previewed rectangle, in the same coordinates as `origin`. |
| 218 | 258 | pub fn append_outline_objects<'a>( |
| ... | ... | @@ -220,6 +260,7 @@ impl PageScene { |
| 220 | 260 | outline: &'a crate::outline::OutlineLayout, |
| 221 | 261 | origin: [f32; 2], |
| 222 | 262 | moving: Option<(onestore::ExGuid, [f32; 4])>, |
| 263 | paper: Paper, | |
| 223 | 264 | primitives: &mut Vec<Primitive<'a>>, |
| 224 | 265 | ) { |
| 225 | 266 | for object in &outline.objects { |
| ... | ... | @@ -241,14 +282,7 @@ impl PageScene { |
| 241 | 282 | append_ink(ink, [rect[0], rect[1]], primitives) |
| 242 | 283 | } |
| 243 | 284 | crate::outline::ObjectKind::Unsupported(_) => { |
| 244 | primitives.push(Primitive::Rect { | |
| 245 | rect, | |
| 246 | color: colorref(0x00e4ddd6), | |
| 247 | }); | |
| 248 | primitives.push(Primitive::Rect { | |
| 249 | rect: [rect[0] + 1.0, rect[1] + 1.0, rect[2] - 1.0, rect[3] - 1.0], | |
| 250 | color: colorref(0x00faf7f3), | |
| 251 | }); | |
| 285 | append_placeholder(rect, paper, primitives) | |
| 252 | 286 | } |
| 253 | 287 | crate::outline::ObjectKind::Picture | crate::outline::ObjectKind::File(_) => {} |
| 254 | 288 | } |
| ... | ... | @@ -257,6 +291,7 @@ impl PageScene { |
| 257 | 291 | text: &label.text, |
| 258 | 292 | origin: [origin[0] + label.origin[0], origin[1] + label.origin[1]], |
| 259 | 293 | clip: None, |
| 294 | ink: paper.ink, | |
| 260 | 295 | }); |
| 261 | 296 | } |
| 262 | 297 | } |
| ... | ... | @@ -445,8 +480,9 @@ impl PageScene { |
| 445 | 480 | &'a self, |
| 446 | 481 | primitives: &mut Vec<Primitive<'a>>, |
| 447 | 482 | offset: [f32; 2], |
| 483 | paper: Paper, | |
| 448 | 484 | ) -> Result<(), SceneError> { |
| 449 | self.append_primitives_with(primitives, offset, None, None, |_, _, _| { | |
| 485 | self.append_primitives_with(primitives, offset, None, None, paper, |_, _, _| { | |
| 450 | 486 | Err(SceneError::MissingOutline) |
| 451 | 487 | }) |
| 452 | 488 | } |
| ... | ... | @@ -458,6 +494,7 @@ impl PageScene { |
| 458 | 494 | offset: [f32; 2], |
| 459 | 495 | editor: Option<&'a CanvasEditor>, |
| 460 | 496 | moving: Option<(onestore::ExGuid, [f32; 4])>, |
| 497 | paper: Paper, | |
| 461 | 498 | mut outline: impl FnMut(onestore::ExGuid, [f32; 2], &mut Vec<Primitive<'a>>) -> Result<(), E>, |
| 462 | 499 | ) -> Result<(), E> { |
| 463 | 500 | for content in self.objects(editor)? { |
| ... | ... | @@ -499,18 +536,12 @@ impl PageScene { |
| 499 | 536 | x1 + offset[0], |
| 500 | 537 | y1 + offset[1], |
| 501 | 538 | ]; |
| 502 | primitives.push(Primitive::Rect { | |
| 503 | rect, | |
| 504 | color: colorref(0x00e4ddd6), | |
| 505 | }); | |
| 506 | primitives.push(Primitive::Rect { | |
| 507 | rect: [rect[0] + 1.0, rect[1] + 1.0, rect[2] - 1.0, rect[3] - 1.0], | |
| 508 | color: colorref(0x00faf7f3), | |
| 509 | }); | |
| 539 | append_placeholder(rect, paper, primitives); | |
| 510 | 540 | primitives.push(Primitive::Text { |
| 511 | 541 | clip: None, |
| 512 | 542 | text: &object.label, |
| 513 | 543 | origin: [rect[0] + 8.0, rect[1] + 8.0], |
| 544 | ink: paper.shade(colorref(0x005d554e)), | |
| 514 | 545 | }); |
| 515 | 546 | continue; |
| 516 | 547 | } |
| ... | ... | @@ -525,28 +556,41 @@ impl PageScene { |
| 525 | 556 | }; |
| 526 | 557 | let object_origin = [origin[0] + offset[0], origin[1] + offset[1]]; |
| 527 | 558 | match content { |
| 528 | Content::Image(source) => primitives.push(Primitive::Image { | |
| 529 | image: self | |
| 530 | .images | |
| 531 | .get(&source.id) | |
| 532 | .ok_or(SceneError::MissingImage)?, | |
| 533 | rect: match moving { | |
| 534 | Some((id, [x0, y0, x1, y1])) if id == source.id => [ | |
| 535 | x0 + offset[0], | |
| 536 | y0 + offset[1], | |
| 537 | x1 + offset[0], | |
| 538 | y1 + offset[1], | |
| 539 | ], | |
| 540 | _ => [ | |
| 541 | object_origin[0], | |
| 542 | object_origin[1], | |
| 543 | object_origin[0] | |
| 544 | + source.layout.max_width.ok_or(SceneError::MissingImage)?, | |
| 545 | object_origin[1] | |
| 546 | + source.layout.max_height.ok_or(SceneError::MissingImage)?, | |
| 547 | ], | |
| 548 | }, | |
| 549 | }), | |
| 559 | Content::Image(source) => { | |
| 560 | let (image, [width, height]) = match self.backgrounds.get(&source.id) { | |
| 561 | // The paper shows until the worker's raster lands. | |
| 562 | Some(art) => match art.image(paper) { | |
| 563 | Some(image) => (image, art.size), | |
| 564 | None => continue, | |
| 565 | }, | |
| 566 | None => ( | |
| 567 | self.images | |
| 568 | .get(&source.id) | |
| 569 | .ok_or(SceneError::MissingImage)?, | |
| 570 | [ | |
| 571 | source.layout.max_width.ok_or(SceneError::MissingImage)?, | |
| 572 | source.layout.max_height.ok_or(SceneError::MissingImage)?, | |
| 573 | ], | |
| 574 | ), | |
| 575 | }; | |
| 576 | primitives.push(Primitive::Image { | |
| 577 | image, | |
| 578 | rect: match moving { | |
| 579 | Some((id, [x0, y0, x1, y1])) if id == source.id => [ | |
| 580 | x0 + offset[0], | |
| 581 | y0 + offset[1], | |
| 582 | x1 + offset[0], | |
| 583 | y1 + offset[1], | |
| 584 | ], | |
| 585 | _ => [ | |
| 586 | object_origin[0], | |
| 587 | object_origin[1], | |
| 588 | object_origin[0] + width, | |
| 589 | object_origin[1] + height, | |
| 590 | ], | |
| 591 | }, | |
| 592 | }) | |
| 593 | } | |
| 550 | 594 | Content::Outline { .. } | Content::Date { .. } => { |
| 551 | 595 | let outline = match content { |
| 552 | 596 | Content::Outline { layout, .. } => layout, |
| ... | ... | @@ -555,14 +599,16 @@ impl PageScene { |
| 555 | 599 | .ok_or(SceneError::MissingOutline)? |
| 556 | 600 | .layout(), |
| 557 | 601 | }; |
| 558 | outline.append_table_primitives(primitives, object_origin); | |
| 559 | outline.append_background_primitives(primitives, object_origin); | |
| 560 | self.append_outline_objects(outline, object_origin, None, primitives); | |
| 561 | for (index, paragraph) in outline.paragraphs.iter().enumerate() { | |
| 602 | outline.append_table_primitives(primitives, object_origin, paper); | |
| 603 | let everything = [f32::NEG_INFINITY, f32::INFINITY]; | |
| 604 | outline.append_background_primitives(primitives, object_origin, everything); | |
| 605 | self.append_outline_objects(outline, object_origin, None, paper, primitives); | |
| 606 | for (index, paragraph) in outline.visible(everything) { | |
| 562 | 607 | outline.append_paragraph_primitives( |
| 563 | 608 | index, |
| 564 | 609 | paragraph, |
| 565 | 610 | object_origin, |
| 611 | paper.ink, | |
| 566 | 612 | primitives, |
| 567 | 613 | ); |
| 568 | 614 | } |
| ... | ... | @@ -575,12 +621,33 @@ impl PageScene { |
| 575 | 621 | } |
| 576 | 622 | |
| 577 | 623 | impl crate::outline::OutlineLayout { |
| 624 | /// Paragraphs that may paint between outline-local `rows`, with their indices. | |
| 625 | pub fn visible( | |
| 626 | &self, | |
| 627 | rows: [f32; 2], | |
| 628 | ) -> impl Iterator<Item = (usize, &crate::outline::ParagraphLayout)> { | |
| 629 | // Markers, tags and overhanging ink stay within this of a paragraph's lines. | |
| 630 | const OVERHANG: f32 = 72.0; | |
| 631 | self.paragraphs | |
| 632 | .iter() | |
| 633 | .enumerate() | |
| 634 | .filter(move |(_, paragraph)| { | |
| 635 | let [start, end] = [ | |
| 636 | paragraph.origin[1] - OVERHANG, | |
| 637 | paragraph.origin[1] + paragraph.text.height() + OVERHANG, | |
| 638 | ]; | |
| 639 | paragraph.math.is_some() || (end >= rows[0] && start <= rows[1]) | |
| 640 | }) | |
| 641 | } | |
| 642 | ||
| 643 | /// Highlights behind the paragraphs `visible` finds between outline-local `rows`. | |
| 578 | 644 | pub fn append_background_primitives( |
| 579 | 645 | &self, |
| 580 | 646 | primitives: &mut Vec<Primitive<'_>>, |
| 581 | 647 | origin: [f32; 2], |
| 648 | rows: [f32; 2], | |
| 582 | 649 | ) { |
| 583 | for (index, paragraph) in self.paragraphs.iter().enumerate() { | |
| 650 | for (index, paragraph) in self.visible(rows) { | |
| 584 | 651 | for (mut rect, color) in paragraph.text.backgrounds() { |
| 585 | 652 | rect.x0 += f64::from(paragraph.origin[0]); |
| 586 | 653 | rect.x1 += f64::from(paragraph.origin[0]); |
| ... | ... | @@ -612,6 +679,7 @@ impl crate::outline::OutlineLayout { |
| 612 | 679 | index: usize, |
| 613 | 680 | paragraph: &'a crate::outline::ParagraphLayout, |
| 614 | 681 | origin: [f32; 2], |
| 682 | ink: [f32; 4], | |
| 615 | 683 | primitives: &mut Vec<Primitive<'a>>, |
| 616 | 684 | ) { |
| 617 | 685 | let [x, y] = [ |
| ... | ... | @@ -635,6 +703,7 @@ impl crate::outline::OutlineLayout { |
| 635 | 703 | clip, |
| 636 | 704 | text: layout, |
| 637 | 705 | origin: [x + origin[0], y + origin[1]], |
| 706 | ink, | |
| 638 | 707 | }, |
| 639 | 708 | // As pen strokes, so hairline rules keep a device pixel. |
| 640 | 709 | crate::math::MathItem::Rule([x0, y0, x1, y1]) => { |
| ... | ... | @@ -651,7 +720,7 @@ impl crate::outline::OutlineLayout { |
| 651 | 720 | to: [x + to[0], y + to[1]], |
| 652 | 721 | width, |
| 653 | 722 | round: false, |
| 654 | color: colorref(math.color), | |
| 723 | color: math.color.map_or(ink, colorref), | |
| 655 | 724 | } |
| 656 | 725 | } |
| 657 | 726 | crate::math::MathItem::Stroke { from, to, width } => Primitive::Segment { |
| ... | ... | @@ -659,7 +728,7 @@ impl crate::outline::OutlineLayout { |
| 659 | 728 | to: [x + to[0], y + to[1]], |
| 660 | 729 | width: *width, |
| 661 | 730 | round: true, |
| 662 | color: colorref(math.color), | |
| 731 | color: math.color.map_or(ink, colorref), | |
| 663 | 732 | }, |
| 664 | 733 | }); |
| 665 | 734 | } |
| ... | ... | @@ -668,6 +737,7 @@ impl crate::outline::OutlineLayout { |
| 668 | 737 | clip, |
| 669 | 738 | text: &paragraph.text, |
| 670 | 739 | origin: [x, y], |
| 740 | ink, | |
| 671 | 741 | }), |
| 672 | 742 | } |
| 673 | 743 | for (layout, marker) in &paragraph.markers { |
| ... | ... | @@ -675,6 +745,7 @@ impl crate::outline::OutlineLayout { |
| 675 | 745 | clip, |
| 676 | 746 | text: layout, |
| 677 | 747 | origin: [origin[0] + marker[0], y + marker[1]], |
| 748 | ink, | |
| 678 | 749 | }); |
| 679 | 750 | } |
| 680 | 751 | for tag in &paragraph.tags { |
| ... | ... | @@ -685,12 +756,17 @@ impl crate::outline::OutlineLayout { |
| 685 | 756 | y + tag.origin[1], |
| 686 | 757 | ], |
| 687 | 758 | size: crate::outline::ParagraphTag::SIZE, |
| 688 | opacity: if tag.disabled { 0.45 } else { 1.0 }, | |
| 759 | tint: [1.0, 1.0, 1.0, if tag.disabled { 0.45 } else { 1.0 }], | |
| 689 | 760 | }); |
| 690 | 761 | } |
| 691 | 762 | } |
| 692 | 763 | |
| 693 | pub fn append_table_primitives(&self, primitives: &mut Vec<Primitive<'_>>, origin: [f32; 2]) { | |
| 764 | pub fn append_table_primitives( | |
| 765 | &self, | |
| 766 | primitives: &mut Vec<Primitive<'_>>, | |
| 767 | origin: [f32; 2], | |
| 768 | paper: Paper, | |
| 769 | ) { | |
| 694 | 770 | for table in &self.tables { |
| 695 | 771 | let (Some(first), Some(last)) = (table.cells.first(), table.cells.last()) else { |
| 696 | 772 | continue; |
| ... | ... | @@ -699,7 +775,7 @@ impl crate::outline::OutlineLayout { |
| 699 | 775 | continue; |
| 700 | 776 | } |
| 701 | 777 | let bounds = [first.rect[0], first.rect[1], last.rect[2], last.rect[3]]; |
| 702 | let color = colorref(0x00a3a3a3); | |
| 778 | let color = paper.shade(colorref(0x00a3a3a3)); | |
| 703 | 779 | primitives.push(Primitive::RoundedRect { |
| 704 | 780 | rect: [ |
| 705 | 781 | bounds[0] + origin[0] - 0.375, |
| ... | ... | @@ -1155,6 +1231,7 @@ mod tests { |
| 1155 | 1231 | [0.0; 2], |
| 1156 | 1232 | Some(editor), |
| 1157 | 1233 | None, |
| 1234 | Paper::WHITE, | |
| 1158 | 1235 | |_, _, _| Ok(()), |
| 1159 | 1236 | ) |
| 1160 | 1237 | .unwrap(); |
| ... | ... | @@ -1553,6 +1630,7 @@ mod tests { |
| 1553 | 1630 | [0.0; 2], |
| 1554 | 1631 | Some(editor), |
| 1555 | 1632 | None, |
| 1633 | Paper::WHITE, | |
| 1556 | 1634 | |id, offset, primitives| { |
| 1557 | 1635 | let outline = editor |
| 1558 | 1636 | .outlines() |
| ... | ... | @@ -1563,6 +1641,7 @@ mod tests { |
| 1563 | 1641 | primitives.push(Primitive::Text { |
| 1564 | 1642 | clip: None, |
| 1565 | 1643 | text: &paragraph.text, |
| 1644 | ink: Paper::WHITE.ink, | |
| 1566 | 1645 | origin: [ |
| 1567 | 1646 | outline.origin()[0] + paragraph.origin[0] + offset[0], |
| 1568 | 1647 | outline.origin()[1] + paragraph.origin[1] + offset[1], |
| ... | ... | @@ -1643,7 +1722,7 @@ mod tests { |
| 1643 | 1722 | let (scene, mut editor) = PageScene::from_page(page(), &mut engine).unwrap(); |
| 1644 | 1723 | let mut reference_primitives = Vec::new(); |
| 1645 | 1724 | reference |
| 1646 | .append_primitives(&mut reference_primitives, [0.0; 2]) | |
| 1725 | .append_primitives(&mut reference_primitives, [0.0; 2], Paper::WHITE) | |
| 1647 | 1726 | .unwrap(); |
| 1648 | 1727 | let image_id; |
| 1649 | 1728 | { |
| ... | ... | @@ -1656,11 +1735,13 @@ mod tests { |
| 1656 | 1735 | clip: None, |
| 1657 | 1736 | text: a, |
| 1658 | 1737 | origin: x, |
| 1738 | .. | |
| 1659 | 1739 | }, |
| 1660 | 1740 | Primitive::Text { |
| 1661 | 1741 | clip: None, |
| 1662 | 1742 | text: b, |
| 1663 | 1743 | origin: y, |
| 1744 | .. | |
| 1664 | 1745 | }, |
| 1665 | 1746 | ) => { |
| 1666 | 1747 | assert_eq!(x, y); |
| ... | ... | @@ -1753,7 +1834,7 @@ mod tests { |
| 1753 | 1834 | .advance |
| 1754 | 1835 | ); |
| 1755 | 1836 | assert!(matches!( |
| 1756 | scene.append_primitives(&mut Vec::new(), [0.0; 2]), | |
| 1837 | scene.append_primitives(&mut Vec::new(), [0.0; 2], Paper::WHITE), | |
| 1757 | 1838 | Err(SceneError::MissingOutline) |
| 1758 | 1839 | )); |
| 1759 | 1840 | } |
| ... | ... | @@ -1804,6 +1885,7 @@ mod tests { |
| 1804 | 1885 | [5.0, 0.0], |
| 1805 | 1886 | Some(&editor), |
| 1806 | 1887 | None, |
| 1888 | Paper::WHITE, | |
| 1807 | 1889 | |_, _, _| Ok(()), |
| 1808 | 1890 | ) |
| 1809 | 1891 | .unwrap(); |
| ... | ... | @@ -1870,9 +1952,11 @@ mod tests { |
| 1870 | 1952 | )); |
| 1871 | 1953 | } |
| 1872 | 1954 | let mut primitives = Vec::new(); |
| 1873 | scene.append_primitives(&mut primitives, [0.0; 2]).unwrap(); | |
| 1874 | 1955 | scene |
| 1875 | .append_primitives(&mut primitives, [10.0, -20.0]) | |
| 1956 | .append_primitives(&mut primitives, [0.0; 2], Paper::WHITE) | |
| 1957 | .unwrap(); | |
| 1958 | scene | |
| 1959 | .append_primitives(&mut primitives, [10.0, -20.0], Paper::WHITE) | |
| 1876 | 1960 | .unwrap(); |
| 1877 | 1961 | let [ |
| 1878 | 1962 | Primitive::Image { |
| ... | ... | @@ -1942,6 +2026,7 @@ mod tests { |
| 1942 | 2026 | [0.0; 2], |
| 1943 | 2027 | Some(&editor), |
| 1944 | 2028 | Some((id, [70.0, 5.0, 120.0, 55.0])), |
| 2029 | Paper::WHITE, | |
| 1945 | 2030 | |_, _, _| Ok(()), |
| 1946 | 2031 | ) |
| 1947 | 2032 | .unwrap(); |
crates/canvas/src/gpu/page/background.rs created+807| ... | ... | @@ -0,0 +1,807 @@ |
| 1 | //! OneNote 2010's page template art, recognised by the SHA-256 of the embedded picture and | |
| 2 | //! painted from vector recreations that suit the paper. | |
| 3 | ||
| 4 | use crate::gpu::Paper; | |
| 5 | use draw::RasterImage; | |
| 6 | use resvg::{tiny_skia, usvg}; | |
| 7 | use sha2::{Digest, Sha256}; | |
| 8 | use std::{ | |
| 9 | sync::{Arc, Mutex, OnceLock, Weak, mpsc}, | |
| 10 | task::Waker, | |
| 11 | time::{Duration, Instant}, | |
| 12 | }; | |
| 13 | ||
| 14 | /// Each template picture's SHA-256 and its recreation, drawn for white paper in | |
| 15 | /// `#rrggbb` colours over a view box stretched to the picture's rectangle. | |
| 16 | const TEMPLATES: &[(&str, &str)] = &[ | |
| 17 | ( | |
| 18 | "a7fa7f092eff43030a56342c39a765f8d5cc48c7db815ddfc8c1e5ec40117fae", | |
| 19 | include_str!("../../../assets/backgrounds/bamboo.svg"), | |
| 20 | ), | |
| 21 | ( | |
| 22 | "74c131777e7c437fd654427417097bc01b0813ba8e1e50e4b937bd50a1bebcdb", | |
| 23 | include_str!("../../../assets/backgrounds/binoculars-corner.svg"), | |
| 24 | ), | |
| 25 | ( | |
| 26 | "9c552717e8d5079bbb226948641ff13532df3d7be434c6ce545f1692fa57d45a", | |
| 27 | include_str!("../../../assets/backgrounds/black-and-green-title.svg"), | |
| 28 | ), | |
| 29 | ( | |
| 30 | "3c68c7ee798e62a4a99c740153f3980d7df029605c843410942c7f85e794823b", | |
| 31 | include_str!("../../../assets/backgrounds/blue-bubbles-corner-margin.svg"), | |
| 32 | ), | |
| 33 | ( | |
| 34 | "d0a3a1c3cd63c4023fe5716cbe2c211307d0e277e444d9ef76c7fc097a845fd4", | |
| 35 | include_str!("../../../assets/backgrounds/blue-bubbles-corner-top.svg"), | |
| 36 | ), | |
| 37 | ( | |
| 38 | "7966f3d8a2d61ecb49a35e163781858e052c0b122a18a1238afe27b57e2850e8", | |
| 39 | include_str!("../../../assets/backgrounds/blue-clouds.svg"), | |
| 40 | ), | |
| 41 | ( | |
| 42 | "139bb0e79f89c3ddef79b1716a5fbab4c07df5785fb3cdf6b4eeddbf6c078452", | |
| 43 | include_str!("../../../assets/backgrounds/blue-dots.svg"), | |
| 44 | ), | |
| 45 | ( | |
| 46 | "473c90248fa33f8e49b2daf38673d87a9405481eae6a2087cc5384b695b6a4c0", | |
| 47 | include_str!("../../../assets/backgrounds/blue-flow.svg"), | |
| 48 | ), | |
| 49 | ( | |
| 50 | "51e05999a1c9f17df28cb474e57dd8e64bdab824874a532c20a23766a01f8967", | |
| 51 | include_str!("../../../assets/backgrounds/blue-flow.svg"), | |
| 52 | ), | |
| 53 | ( | |
| 54 | "83245f217deae4a4143b565e13c045dbb32a9063e8c6b2e43bb15cd76c5f9219", | |
| 55 | include_str!("../../../assets/backgrounds/blue-mist-margin.svg"), | |
| 56 | ), | |
| 57 | ( | |
| 58 | "474f9a8c25d5e21192315397ea995b1e11e2c1608157c6e0277688091bfd136a", | |
| 59 | include_str!("../../../assets/backgrounds/blue-stripe-title.svg"), | |
| 60 | ), | |
| 61 | ( | |
| 62 | "06e09de80c3f32254da4fe6b2cbad7c05ef144dd54b8c65745e195bbf7317a2e", | |
| 63 | include_str!("../../../assets/backgrounds/blue-stripes.svg"), | |
| 64 | ), | |
| 65 | ( | |
| 66 | "10c833e47be1c8496f949a6b059c2d79212a4dd66bde62116ea337fa4fe0b654", | |
| 67 | include_str!("../../../assets/backgrounds/blue-swirls.svg"), | |
| 68 | ), | |
| 69 | ( | |
| 70 | "646586cb71742a2369a529876b41af6a472c35cc508d1ae5d8395d55784814f2", | |
| 71 | include_str!("../../../assets/backgrounds/blue-wave-title.svg"), | |
| 72 | ), | |
| 73 | ( | |
| 74 | "fbb4573e3bee1b337077691bebae15d6fac52432405d31396d526d7694a8283d", | |
| 75 | include_str!("../../../assets/backgrounds/blue-waves.svg"), | |
| 76 | ), | |
| 77 | ( | |
| 78 | "1591fd26e7fff5be97431d0ed3d0ade5cfc5fa74e3d7ec282fd242160ce68c1f", | |
| 79 | include_str!("../../../assets/backgrounds/books.svg"), | |
| 80 | ), | |
| 81 | ( | |
| 82 | "bdd997068701ed3a00a224eb694b003c01ac69b857fe7b4147d6c34875b1632b", | |
| 83 | include_str!("../../../assets/backgrounds/bouquet.svg"), | |
| 84 | ), | |
| 85 | ( | |
| 86 | "e64fdcd0b108737d8b8f7b677029f924031d6bbaa50585d9c3def7c7e92ecaf2", | |
| 87 | include_str!("../../../assets/backgrounds/boxes.svg"), | |
| 88 | ), | |
| 89 | ( | |
| 90 | "639eadad468b6b32b9124b1f4395a8da3027ff7258d102173ba070ae2ed541ae", | |
| 91 | include_str!("../../../assets/backgrounds/boxes-shadow.svg"), | |
| 92 | ), | |
| 93 | ( | |
| 94 | "5eebe803e434a845d19bc600df3c75e98bb69bd0de473ceec410d1b3a9154e28", | |
| 95 | include_str!("../../../assets/backgrounds/bubbles.svg"), | |
| 96 | ), | |
| 97 | ( | |
| 98 | "509a6945facfb3ddc7be6ee8b82797ad0c72db5755486ee878125a959cc09b59", | |
| 99 | include_str!("../../../assets/backgrounds/bubbles-margin-margin.svg"), | |
| 100 | ), | |
| 101 | ( | |
| 102 | "e9746b4e9ae9ce7b3b0068779db3e113e2dfc9880f25373d745d0e700e69a906", | |
| 103 | include_str!("../../../assets/backgrounds/bubbles-margin-top.svg"), | |
| 104 | ), | |
| 105 | ( | |
| 106 | "505f731cb7707efab2eb06685b392dc7e59265a40b55aae43e5dc15c0a86cba4", | |
| 107 | include_str!("../../../assets/backgrounds/buildings.svg"), | |
| 108 | ), | |
| 109 | ( | |
| 110 | "edad0f03e6ff99fef9ef8e8b834ce74f26cd23c5f8c067f5cee66f304181e64d", | |
| 111 | include_str!("../../../assets/backgrounds/chain-title.svg"), | |
| 112 | ), | |
| 113 | ( | |
| 114 | "e1cb1a0ec9be62d5445c73aa84df38234002a7e164ee830c9df24997802cb5d2", | |
| 115 | include_str!("../../../assets/backgrounds/circles.svg"), | |
| 116 | ), | |
| 117 | ( | |
| 118 | "adb1ebbe18d6cd8ff08aa9bf5c83cdb83bf9aa179698e34e93dbcdde12f04d32", | |
| 119 | include_str!("../../../assets/backgrounds/clock.svg"), | |
| 120 | ), | |
| 121 | ( | |
| 122 | "70475431cca3c91a4efa3b8f04864371d2d3a45696674a1a0562fe9cd8db287c", | |
| 123 | include_str!("../../../assets/backgrounds/columns.svg"), | |
| 124 | ), | |
| 125 | ( | |
| 126 | "9301db6d2d87282fcee450189aeace16d85f64273bf62713a3044992b6b7a9e9", | |
| 127 | include_str!("../../../assets/backgrounds/computer.svg"), | |
| 128 | ), | |
| 129 | ( | |
| 130 | "20016e5fa1a32dce5af4e92872597e36432185a7bb2e61c91f362bd68484529b", | |
| 131 | include_str!("../../../assets/backgrounds/cyan-title.svg"), | |
| 132 | ), | |
| 133 | ( | |
| 134 | "035b26df61855a3f36dbd30fdab0c157c04c9e8ae2197ea4d4aeb3e82e6a4c2b", | |
| 135 | include_str!("../../../assets/backgrounds/day-planner.svg"), | |
| 136 | ), | |
| 137 | ( | |
| 138 | "b243a24fa13bc8523450e22f408f9eff15301c938f8ca52a57018b58ce6785de", | |
| 139 | include_str!("../../../assets/backgrounds/fireworks.svg"), | |
| 140 | ), | |
| 141 | ( | |
| 142 | "1262dd23ad54e935cfa10feb1be56648e43bef1116696ca71d87e6e033b1ca7d", | |
| 143 | include_str!("../../../assets/backgrounds/flat-squares.svg"), | |
| 144 | ), | |
| 145 | ( | |
| 146 | "423b0dd1a93b391d15b1dc8d8757c3bf5725ff2e7a59e6e3140033e2876b67f6", | |
| 147 | include_str!("../../../assets/backgrounds/flowers-and-hearts.svg"), | |
| 148 | ), | |
| 149 | ( | |
| 150 | "131d637cdc5d0b094fb9fad17f4d2a1ace0d03613588155aacaa2d1cb4e16da9", | |
| 151 | include_str!("../../../assets/backgrounds/glasses-corner.svg"), | |
| 152 | ), | |
| 153 | ( | |
| 154 | "20abe389c885e42b6ebe9e902976229bb6fd63c8c34cb61aa70b8b746209f90a", | |
| 155 | include_str!("../../../assets/backgrounds/globe.svg"), | |
| 156 | ), | |
| 157 | ( | |
| 158 | "95f59a0433050180d4c0e8858b83363d51bea6752a8b7ca516a8677854d8f5b6", | |
| 159 | include_str!("../../../assets/backgrounds/graph.svg"), | |
| 160 | ), | |
| 161 | ( | |
| 162 | "d56cefe9ee2fae72e31bdba7dd2aa4426ea22e3ceb22ef68c8f63f9f24d5a8bc", | |
| 163 | include_str!("../../../assets/backgrounds/green-stripes.svg"), | |
| 164 | ), | |
| 165 | ( | |
| 166 | "2035407a0540e1c4f7934db08ba4add750fcb9a62863ddd9553e7871c81a99e3", | |
| 167 | include_str!("../../../assets/backgrounds/hearts.svg"), | |
| 168 | ), | |
| 169 | ( | |
| 170 | "7ce10d1ea660d2f9cf8b704f3fab2966a4ce2627d9858d32c75d857095012098", | |
| 171 | include_str!("../../../assets/backgrounds/ivy.svg"), | |
| 172 | ), | |
| 173 | ( | |
| 174 | "fb12d59b8be911247bbafdd416852e8b74b028005a141cb4dbbba109b4b6ed2c", | |
| 175 | include_str!("../../../assets/backgrounds/large-push-pins.svg"), | |
| 176 | ), | |
| 177 | ( | |
| 178 | "7d2b01f17354d9237a6ab99d5b9afdf0e1cc43687125848b0c2dedfb44ce3843", | |
| 179 | include_str!("../../../assets/backgrounds/light-bulb-corner.svg"), | |
| 180 | ), | |
| 181 | ( | |
| 182 | "140dd943d0f0cfe6aaa98470b7d1a7cb62ca02cb1d8f522dd2ac77433232ef41", | |
| 183 | include_str!("../../../assets/backgrounds/lilies.svg"), | |
| 184 | ), | |
| 185 | ( | |
| 186 | "1f81dde3b42f23f0666d92ebf14d62893b31b39d72c07aee070eae28c2e6980e", | |
| 187 | include_str!("../../../assets/backgrounds/math.svg"), | |
| 188 | ), | |
| 189 | ( | |
| 190 | "ec8cd7250f3d82e900e99114869777ee859ec73effabed108815f65742078c3a", | |
| 191 | include_str!("../../../assets/backgrounds/math-science-class-notes.svg"), | |
| 192 | ), | |
| 193 | ( | |
| 194 | "0401451e1d1d7dfdc29ad1b2b68a6c8ac0b706e9868bf22fab26a01cd48620ce", | |
| 195 | include_str!("../../../assets/backgrounds/networking.svg"), | |
| 196 | ), | |
| 197 | ( | |
| 198 | "7e6a7714a69688d9ffdf16aa942b66064a0c77fcd9b3e469f89730b4b9290c3e", | |
| 199 | include_str!("../../../assets/backgrounds/notebook.svg"), | |
| 200 | ), | |
| 201 | ( | |
| 202 | "be4f0e6c89fce91b9ebd2623567f7dfc259e0e3c77c9158742b8f64b724df673", | |
| 203 | include_str!("../../../assets/backgrounds/orange-margin.svg"), | |
| 204 | ), | |
| 205 | ( | |
| 206 | "7354001527ab554c44e7d6981b86dd933b7dc2e0d3dc8512ad3eecd843245c52", | |
| 207 | include_str!("../../../assets/backgrounds/paper.svg"), | |
| 208 | ), | |
| 209 | ( | |
| 210 | "e9ead0bfc09d32cb366010cdfede1c432a2d1d550cb7332badac1bee9482bc86", | |
| 211 | include_str!("../../../assets/backgrounds/paws-cyan.svg"), | |
| 212 | ), | |
| 213 | ( | |
| 214 | "28f334b77068f71f5f92a95695433b950610204a0e5580ce567db8fad4993ecb", | |
| 215 | include_str!("../../../assets/backgrounds/pencil-and-notebook.svg"), | |
| 216 | ), | |
| 217 | ( | |
| 218 | "3fbe3c1c238bd7dbc67f8cff5f3bddfd513c96a9851b9616477947d21dff4b2e", | |
| 219 | include_str!("../../../assets/backgrounds/pens.svg"), | |
| 220 | ), | |
| 221 | ( | |
| 222 | "5dc2367a80588a7518db5014122510bf0fd784711015ef83a8718336584f82d0", | |
| 223 | include_str!("../../../assets/backgrounds/pixel.svg"), | |
| 224 | ), | |
| 225 | ( | |
| 226 | "edbb86f160050fbf1f9860276802bae292dbfd0bc98e3ea90d43d981e9f0c54a", | |
| 227 | include_str!("../../../assets/backgrounds/pixel-blends.svg"), | |
| 228 | ), | |
| 229 | ( | |
| 230 | "de35af949d4f83e97ee22f817afe2531cc4b59ff9ee6026dca7ecebc5cf2737f", | |
| 231 | include_str!("../../../assets/backgrounds/plain-and-simple.svg"), | |
| 232 | ), | |
| 233 | ( | |
| 234 | "e111f96490755c7d71e87c88acaea38afe55bb865b1a14a83c5bd239648d5e2c", | |
| 235 | include_str!("../../../assets/backgrounds/purple-clouds.svg"), | |
| 236 | ), | |
| 237 | ( | |
| 238 | "62ad3c277e54f03f1adb44062407346f789e63859b7afabfd64be6af5e9f66ec", | |
| 239 | include_str!("../../../assets/backgrounds/pushpins-corner.svg"), | |
| 240 | ), | |
| 241 | ( | |
| 242 | "f927c7825851974a2149868146970706523a49165133cee6027a43e8c9abdf27", | |
| 243 | include_str!("../../../assets/backgrounds/rainbow.svg"), | |
| 244 | ), | |
| 245 | ( | |
| 246 | "0e31da4dfcff4a36c64c1ce940362d2309769f36369e4c43c317d5f2fa15658e", | |
| 247 | include_str!("../../../assets/backgrounds/rainbow-border.svg"), | |
| 248 | ), | |
| 249 | ( | |
| 250 | "2e7aaf26bec32148b96442e8fff1bd2cef2d72630969f23b9a2abedb6cfec93b", | |
| 251 | include_str!("../../../assets/backgrounds/rainbow-mini-border.svg"), | |
| 252 | ), | |
| 253 | ( | |
| 254 | "525a857d0eda855a64d3619df58b1c2d013a73e60fa0d49b155ecfcb2c134c7c", | |
| 255 | include_str!("../../../assets/backgrounds/recipe.svg"), | |
| 256 | ), | |
| 257 | ( | |
| 258 | "b4daa90d5a53fcbc85119050b5b76962443c4dd18d7f42cdc6d4e0ad8efad872", | |
| 259 | include_str!("../../../assets/backgrounds/red-and-black-margin.svg"), | |
| 260 | ), | |
| 261 | ( | |
| 262 | "14fa2d16310485aa1ce41f6d774a3d637e8cf8b03c4f72990155df274fdb6bd9", | |
| 263 | include_str!("../../../assets/backgrounds/side-stripes.svg"), | |
| 264 | ), | |
| 265 | ( | |
| 266 | "d74c3973c8d1f7c77274691afb1aa934940674341d7eee563be75e563281bdfd", | |
| 267 | include_str!("../../../assets/backgrounds/small-push-pins.svg"), | |
| 268 | ), | |
| 269 | ( | |
| 270 | "f091ded5e283af6848670a3172e7c43c6099875d39b3fc69c2bdba914f609602", | |
| 271 | include_str!("../../../assets/backgrounds/sparks.svg"), | |
| 272 | ), | |
| 273 | ( | |
| 274 | "7f5b660a1a0bf46c75aaf19b4f77a0e086de003ec03afc1f58d871d55aa5ba9e", | |
| 275 | include_str!("../../../assets/backgrounds/stars.svg"), | |
| 276 | ), | |
| 277 | ( | |
| 278 | "0908a4cfa23f93011176d47f45843e9ca2973030421996e8e27484781f54b0ec", | |
| 279 | include_str!("../../../assets/backgrounds/swirls.svg"), | |
| 280 | ), | |
| 281 | ( | |
| 282 | "8b025b80e7d398229ef19384156fe98aeefcf8f69169d69e1eeba73281b24d7e", | |
| 283 | include_str!("../../../assets/backgrounds/swirls.svg"), | |
| 284 | ), | |
| 285 | ( | |
| 286 | "2a4a3530d652e227ddd5adc096a95f6034718f7c380b07db622022d768815059", | |
| 287 | include_str!("../../../assets/backgrounds/tiles.svg"), | |
| 288 | ), | |
| 289 | ( | |
| 290 | "d72900e8da72d1a7f3729971aa558e1e9b6e9cf9a0d51e83852e567256dbbfef", | |
| 291 | include_str!("../../../assets/backgrounds/triangles-title-margin.svg"), | |
| 292 | ), | |
| 293 | ( | |
| 294 | "2df453410796aec7b9efec00059b6ce64bcf67313a95ae458ba600ea5de14811", | |
| 295 | include_str!("../../../assets/backgrounds/triangles-title-top.svg"), | |
| 296 | ), | |
| 297 | ( | |
| 298 | "5b321a4d81bd499b289b1755f6450a42047c494dfbc112dbd56da4ced2c15c1a", | |
| 299 | include_str!("../../../assets/backgrounds/tulips.svg"), | |
| 300 | ), | |
| 301 | ( | |
| 302 | "494547e566fb7a63dd429eb0699fe41aa8998f8ea2f758d813fe3d56c3075719", | |
| 303 | include_str!("../../../assets/backgrounds/window-reflection.svg"), | |
| 304 | ), | |
| 305 | ( | |
| 306 | "5a36960df32817e8426bd40a88f88b04fb55b84baef60f1e71e0872217fdb134", | |
| 307 | include_str!("../../../assets/backgrounds/writing.svg"), | |
| 308 | ), | |
| 309 | ]; | |
| 310 | ||
| 311 | /// A raster holds at most this many bytes; page-sized art stops sharpening past about 2.9 | |
| 312 | /// device pixels per point. | |
| 313 | const MAX_RASTER_BYTES: f32 = 16.0 * 1024.0 * 1024.0; | |
| 314 | /// How long the zoom must hold before art is rasterized again for it. | |
| 315 | const SETTLE: Duration = Duration::from_millis(150); | |
| 316 | ||
| 317 | /// Template art standing in for a page's background picture. | |
| 318 | pub(super) struct Background { | |
| 319 | svg: &'static str, | |
| 320 | /// Points; OneNote draws a picture without a stored size at its natural size. | |
| 321 | pub size: [f32; 2], | |
| 322 | /// For light and for dark paper. | |
| 323 | variants: [Variant; 2], | |
| 324 | } | |
| 325 | ||
| 326 | #[derive(Default)] | |
| 327 | struct Variant { | |
| 328 | /// The raster painted and its device pixels per point. | |
| 329 | shown: Option<(RasterImage, f32)>, | |
| 330 | /// The one raster being made and its device pixels per point; replacing it abandons it. | |
| 331 | pending: Option<(Arc<Mutex<Option<RasterImage>>>, f32)>, | |
| 332 | } | |
| 333 | ||
| 334 | /// Art rasterized on the worker thread, into `slot` unless its scene has let it go. | |
| 335 | struct Job { | |
| 336 | svg: &'static str, | |
| 337 | /// The dark paper to recolour for; light paper takes the art as drawn. | |
| 338 | dark: Option<Paper>, | |
| 339 | size: [u32; 2], | |
| 340 | start: Instant, | |
| 341 | slot: Weak<Mutex<Option<RasterImage>>>, | |
| 342 | waker: Waker, | |
| 343 | } | |
| 344 | ||
| 345 | impl Background { | |
| 346 | pub fn recognise(picture: &onestore::page::Image) -> Option<Self> { | |
| 347 | let digest: String = Sha256::digest(picture.bytes.as_deref()?) | |
| 348 | .iter() | |
| 349 | .map(|byte| format!("{byte:02x}")) | |
| 350 | .collect(); | |
| 351 | let (_, svg) = TEMPLATES.iter().find(|(hash, _)| *hash == digest)?; | |
| 352 | let size = match (picture.layout.max_width, picture.layout.max_height) { | |
| 353 | (Some(width), Some(height)) => [width, height], | |
| 354 | _ => { | |
| 355 | let size = parse(svg).size(); | |
| 356 | [size.width() * 0.75, size.height() * 0.75] | |
| 357 | } | |
| 358 | }; | |
| 359 | Some(Self { | |
| 360 | svg, | |
| 361 | size, | |
| 362 | variants: Default::default(), | |
| 363 | }) | |
| 364 | } | |
| 365 | ||
| 366 | /// The latest raster for this paper, if one has landed. | |
| 367 | pub fn image(&self, paper: Paper) -> Option<&RasterImage> { | |
| 368 | let (image, _) = self.variants[usize::from(dark(paper))].shown.as_ref()?; | |
| 369 | Some(image) | |
| 370 | } | |
| 371 | ||
| 372 | /// Takes a finished raster and asks the worker for one at `scale` device pixels per | |
| 373 | /// point when the shown one differs; `waker` hears when it lands. True once the raster | |
| 374 | /// shown matches. | |
| 375 | pub fn update(&mut self, paper: Paper, scale: f32, waker: &Waker) -> bool { | |
| 376 | let dark = dark(paper); | |
| 377 | let [width, height] = self.size.map(|side| side.max(1.0)); | |
| 378 | let density = 2_f32 | |
| 379 | .powf((scale.max(0.25).log2() * 2.0).ceil() / 2.0) | |
| 380 | .min((MAX_RASTER_BYTES / (4.0 * width * height)).sqrt()) | |
| 381 | .min(4096.0 / width.max(height)); | |
| 382 | let variant = &mut self.variants[usize::from(dark)]; | |
| 383 | let landed = variant | |
| 384 | .pending | |
| 385 | .as_ref() | |
| 386 | .and_then(|(slot, made)| Some((slot.lock().unwrap().take()?, *made))); | |
| 387 | if let Some((image, made)) = landed { | |
| 388 | if made == density || variant.shown.is_none() { | |
| 389 | variant.shown = Some((image, made)); | |
| 390 | } | |
| 391 | variant.pending = None; | |
| 392 | } | |
| 393 | let shown = variant.shown.as_ref().map(|(_, made)| *made); | |
| 394 | if shown != Some(density) | |
| 395 | && variant.pending.as_ref().map(|(_, made)| *made) != Some(density) | |
| 396 | { | |
| 397 | let slot = Arc::default(); | |
| 398 | worker() | |
| 399 | .send(Job { | |
| 400 | svg: self.svg, | |
| 401 | dark: dark.then_some(paper), | |
| 402 | size: [width, height].map(|side| (side * density).round().max(1.0) as u32), | |
| 403 | start: Instant::now() | |
| 404 | + if shown.is_some() { | |
| 405 | SETTLE | |
| 406 | } else { | |
| 407 | Duration::ZERO | |
| 408 | }, | |
| 409 | slot: Arc::downgrade(&slot), | |
| 410 | waker: waker.clone(), | |
| 411 | }) | |
| 412 | .expect("The background worker outlives its jobs"); | |
| 413 | variant.pending = Some((slot, density)); | |
| 414 | } | |
| 415 | shown == Some(density) | |
| 416 | } | |
| 417 | } | |
| 418 | ||
| 419 | fn dark(paper: Paper) -> bool { | |
| 420 | let [lightness, ..] = oklab(paper.color); | |
| 421 | let [ink, ..] = oklab(paper.ink); | |
| 422 | ink > lightness | |
| 423 | } | |
| 424 | ||
| 425 | /// The process's one background rasterizer, started on first use. | |
| 426 | fn worker() -> &'static mpsc::Sender<Job> { | |
| 427 | static WORKER: OnceLock<mpsc::Sender<Job>> = OnceLock::new(); | |
| 428 | WORKER.get_or_init(|| { | |
| 429 | let (sender, jobs) = mpsc::channel::<Job>(); | |
| 430 | std::thread::Builder::new() | |
| 431 | .name("template backgrounds".into()) | |
| 432 | .spawn(move || { | |
| 433 | for job in jobs { | |
| 434 | std::thread::sleep(job.start.saturating_duration_since(Instant::now())); | |
| 435 | let Some(slot) = job.slot.upgrade() else { | |
| 436 | continue; | |
| 437 | }; | |
| 438 | let image = rasterize(job.svg, job.dark, job.size); | |
| 439 | *slot.lock().unwrap() = Some(image); | |
| 440 | job.waker.wake(); | |
| 441 | } | |
| 442 | }) | |
| 443 | .expect("The background worker starts"); | |
| 444 | sender | |
| 445 | }) | |
| 446 | } | |
| 447 | ||
| 448 | fn rasterize(svg: &str, dark: Option<Paper>, [width, height]: [u32; 2]) -> RasterImage { | |
| 449 | let tree = match dark { | |
| 450 | Some(paper) => parse(&onto(svg, paper)), | |
| 451 | None => parse(svg), | |
| 452 | }; | |
| 453 | let mut pixmap = tiny_skia::Pixmap::new(width, height).expect("Art has a size"); | |
| 454 | let view = tree.size(); | |
| 455 | resvg::render( | |
| 456 | &tree, | |
| 457 | tiny_skia::Transform::from_scale( | |
| 458 | width as f32 / view.width(), | |
| 459 | height as f32 / view.height(), | |
| 460 | ), | |
| 461 | &mut pixmap.as_mut(), | |
| 462 | ); | |
| 463 | let pixels = pixmap | |
| 464 | .pixels() | |
| 465 | .iter() | |
| 466 | .flat_map(|pixel| { | |
| 467 | let color = pixel.demultiply(); | |
| 468 | [color.red(), color.green(), color.blue(), color.alpha()] | |
| 469 | }) | |
| 470 | .collect(); | |
| 471 | RasterImage::new([width, height], pixels).expect("Art is within the image budget") | |
| 472 | } | |
| 473 | ||
| 474 | fn parse(svg: &str) -> usvg::Tree { | |
| 475 | usvg::Tree::from_str(svg, &usvg::Options::default()).expect("Bundled template art is valid") | |
| 476 | } | |
| 477 | ||
| 478 | /// Recolours `#rrggbb` art drawn for white paper onto dark `paper`: lightness turns about | |
| 479 | /// the paper towards its ink, easing off so the art stays behind the text, and hue and | |
| 480 | /// chroma carry over. Keywords, such as a mask's `white`, stay as they are. | |
| 481 | fn onto(svg: &str, paper: Paper) -> String { | |
| 482 | let [lightness, a, b] = oklab(paper.color); | |
| 483 | let [ink, ..] = oklab(paper.ink); | |
| 484 | let mut pieces = svg.split('#'); | |
| 485 | let mut recoloured = pieces.next().unwrap_or_default().to_owned(); | |
| 486 | for piece in pieces { | |
| 487 | let color = piece | |
| 488 | .get(..6) | |
| 489 | .filter(|_| !piece[6..].starts_with(|c: char| c.is_ascii_alphanumeric())) | |
| 490 | .and_then(|hex| u32::from_str_radix(hex, 16).ok()); | |
| 491 | let Some(color) = color else { | |
| 492 | recoloured.push('#'); | |
| 493 | recoloured.push_str(piece); | |
| 494 | continue; | |
| 495 | }; | |
| 496 | let [_, red, green, blue] = color.to_be_bytes(); | |
| 497 | let [l, ca, cb] = oklab(draw::srgb(red, green, blue)); | |
| 498 | let turned = lightness + 0.7 * (1.0 - l).max(0.0).sqrt() * (ink - lightness); | |
| 499 | let [red, green, blue] = linear([turned, a + ca, b + cb]).map(|value| { | |
| 500 | let encoded = if value <= 0.003_130_8 { | |
| 501 | value * 12.92 | |
| 502 | } else { | |
| 503 | 1.055 * value.powf(1.0 / 2.4) - 0.055 | |
| 504 | }; | |
| 505 | (encoded * 255.0).round() as u8 | |
| 506 | }); | |
| 507 | recoloured.push_str(&format!("#{red:02x}{green:02x}{blue:02x}")); | |
| 508 | recoloured.push_str(&piece[6..]); | |
| 509 | } | |
| 510 | recoloured | |
| 511 | } | |
| 512 | ||
| 513 | /// OKLab lightness and opponent axes of linear RGB. | |
| 514 | fn oklab([red, green, blue, _]: [f32; 4]) -> [f32; 3] { | |
| 515 | let [l, m, s] = [ | |
| 516 | [0.412_221_46, 0.536_332_55, 0.051_445_995], | |
| 517 | [0.211_903_5, 0.680_699_5, 0.107_396_96], | |
| 518 | [0.088_302_46, 0.281_718_85, 0.629_978_7], | |
| 519 | ] | |
| 520 | .map(|[r, g, b]| (r * red + g * green + b * blue).cbrt()); | |
| 521 | [ | |
| 522 | 0.210_454_26 * l + 0.793_617_8 * m - 0.004_072_047 * s, | |
| 523 | 1.977_998_5 * l - 2.428_592_2 * m + 0.450_593_7 * s, | |
| 524 | 0.025_904_037 * l + 0.782_771_77 * m - 0.808_675_77 * s, | |
| 525 | ] | |
| 526 | } | |
| 527 | ||
| 528 | /// Linear RGB of OKLab, clipped to the sRGB gamut. | |
| 529 | fn linear([lightness, a, b]: [f32; 3]) -> [f32; 3] { | |
| 530 | let [l, m, s] = [ | |
| 531 | lightness + 0.396_337_78 * a + 0.215_803_76 * b, | |
| 532 | lightness - 0.105_561_346 * a - 0.063_854_17 * b, | |
| 533 | lightness - 0.089_484_18 * a - 1.291_485_5 * b, | |
| 534 | ] | |
| 535 | .map(|value| value.powi(3)); | |
| 536 | [ | |
| 537 | [4.076_741_7, -3.307_711_6, 0.230_969_94], | |
| 538 | [-1.268_438, 2.609_757_4, -0.341_319_38], | |
| 539 | [-0.004_196_086_3, -0.703_418_6, 1.707_614_7], | |
| 540 | ] | |
| 541 | .map(|[x, y, z]| (x * l + y * m + z * s).clamp(0.0, 1.0)) | |
| 542 | } | |
| 543 | ||
| 544 | #[cfg(test)] | |
| 545 | mod tests { | |
| 546 | use super::*; | |
| 547 | use crate::{gpu::page::PageScene, layout::TextEngine}; | |
| 548 | use onestore::{ | |
| 549 | ExGuid, | |
| 550 | document::Layout, | |
| 551 | page::{Image, Page, PageObject}, | |
| 552 | }; | |
| 553 | use std::collections::BTreeMap; | |
| 554 | ||
| 555 | const DARK: Paper = Paper { | |
| 556 | color: [0.0137, 0.0144, 0.0159, 1.0], | |
| 557 | ink: [0.791, 0.791, 0.791, 1.0], | |
| 558 | }; | |
| 559 | ||
| 560 | fn picture(bytes: &[u8]) -> Image { | |
| 561 | Image { | |
| 562 | id: ExGuid::default(), | |
| 563 | layout: Layout { | |
| 564 | x: Some(0.0), | |
| 565 | y: Some(0.0), | |
| 566 | max_width: Some(64.0), | |
| 567 | max_height: Some(48.0), | |
| 568 | ..Default::default() | |
| 569 | }, | |
| 570 | size: None, | |
| 571 | bytes: Some(Arc::from(bytes)), | |
| 572 | alt: None, | |
| 573 | background: true, | |
| 574 | } | |
| 575 | } | |
| 576 | ||
| 577 | fn red_png() -> Vec<u8> { | |
| 578 | let mut encoded = Vec::new(); | |
| 579 | let mut encoder = png::Encoder::new(&mut encoded, 1, 1); | |
| 580 | encoder.set_color(png::ColorType::Rgba); | |
| 581 | encoder.set_depth(png::BitDepth::Eight); | |
| 582 | encoder | |
| 583 | .write_header() | |
| 584 | .unwrap() | |
| 585 | .write_image_data(&[255, 0, 0, 255]) | |
| 586 | .unwrap(); | |
| 587 | encoded | |
| 588 | } | |
| 589 | ||
| 590 | fn ivy() -> Background { | |
| 591 | let (_, svg) = TEMPLATES | |
| 592 | .iter() | |
| 593 | .find(|(_, svg)| svg.contains("id=\"ivy\"")) | |
| 594 | .unwrap(); | |
| 595 | Background { | |
| 596 | svg, | |
| 597 | size: [64.0, 48.0], | |
| 598 | variants: Default::default(), | |
| 599 | } | |
| 600 | } | |
| 601 | ||
| 602 | fn settle(mut update: impl FnMut() -> bool) { | |
| 603 | let give_up = Instant::now() + Duration::from_secs(10); | |
| 604 | while !update() { | |
| 605 | assert!(Instant::now() < give_up, "the worker never delivered"); | |
| 606 | std::thread::sleep(Duration::from_millis(2)); | |
| 607 | } | |
| 608 | } | |
| 609 | ||
| 610 | #[test] | |
| 611 | fn rasters_land_from_the_worker_and_follow_the_zoom() { | |
| 612 | let mut art = ivy(); | |
| 613 | assert!(!art.update(DARK, 1.0, Waker::noop())); | |
| 614 | assert!(art.image(DARK).is_none()); | |
| 615 | settle(|| art.update(DARK, 1.0, Waker::noop())); | |
| 616 | let size = |art: &Background| art.image(DARK).map(|image| image.pixels().len() / 4); | |
| 617 | assert_eq!(size(&art), Some(64 * 48)); | |
| 618 | assert!( | |
| 619 | art.image(Paper::WHITE).is_none(), | |
| 620 | "each paper has its own raster" | |
| 621 | ); | |
| 622 | // Zooming keeps the last raster up while one for the new scale is made, and a | |
| 623 | // newer scale abandons the older request. | |
| 624 | assert!(!art.update(DARK, 4.0, Waker::noop())); | |
| 625 | let abandoned = Arc::downgrade(&art.variants[1].pending.as_ref().unwrap().0); | |
| 626 | assert!(!art.update(DARK, 2.0, Waker::noop())); | |
| 627 | assert!(abandoned.upgrade().is_none()); | |
| 628 | assert_eq!(size(&art), Some(64 * 48)); | |
| 629 | settle(|| art.update(DARK, 2.0, Waker::noop())); | |
| 630 | assert_eq!(size(&art), Some(128 * 96)); | |
| 631 | assert!(art.variants[1].pending.is_none()); | |
| 632 | } | |
| 633 | ||
| 634 | #[test] | |
| 635 | fn every_recreation_parses_for_light_and_dark_paper_under_a_distinct_hash() { | |
| 636 | let mut hashes = std::collections::BTreeSet::new(); | |
| 637 | for (hash, svg) in TEMPLATES { | |
| 638 | assert!( | |
| 639 | hash.len() == 64 && hash.bytes().all(|b| matches!(b, b'0'..=b'9' | b'a'..=b'f')) | |
| 640 | ); | |
| 641 | assert!(hashes.insert(hash), "{hash} is listed twice"); | |
| 642 | for svg in [svg.to_string(), onto(svg, DARK)] { | |
| 643 | usvg::Tree::from_str(&svg, &usvg::Options::default()).unwrap(); | |
| 644 | } | |
| 645 | } | |
| 646 | } | |
| 647 | ||
| 648 | #[test] | |
| 649 | fn other_pictures_keep_their_own_bytes() { | |
| 650 | assert!(Background::recognise(&picture(&red_png())).is_none()); | |
| 651 | assert!( | |
| 652 | Background::recognise(&Image { | |
| 653 | bytes: None, | |
| 654 | ..picture(&[]) | |
| 655 | }) | |
| 656 | .is_none() | |
| 657 | ); | |
| 658 | } | |
| 659 | ||
| 660 | #[test] | |
| 661 | fn dark_paper_takes_white_to_the_paper_and_keeps_hue() { | |
| 662 | assert_eq!(onto(r##"fill="#ffffff""##, DARK), r##"fill="#1f2022""##); | |
| 663 | assert_eq!( | |
| 664 | onto("href=\"#beads\" stop-color=\"white\"", DARK), | |
| 665 | "href=\"#beads\" stop-color=\"white\"" | |
| 666 | ); | |
| 667 | let leaf = onto("#8fce73", DARK); | |
| 668 | let [red, green, blue] = | |
| 669 | [1, 3, 5].map(|at| u8::from_str_radix(&leaf[at..at + 2], 16).unwrap()); | |
| 670 | assert!(green > red && green > blue && green < 0xce, "{leaf}"); | |
| 671 | } | |
| 672 | ||
| 673 | #[test] | |
| 674 | #[ignore = "requires CANVAS_TEST_SECTION pointing to a section whose pages use OneNote's page templates"] | |
| 675 | fn recognises_template_backgrounds_in_a_section() { | |
| 676 | let bytes = std::fs::read(std::env::var("CANVAS_TEST_SECTION").unwrap()).unwrap(); | |
| 677 | let store = onestore::Store::parse(&bytes).unwrap(); | |
| 678 | let index = onestore::RevisionIndex::parse(&store).unwrap(); | |
| 679 | let document = onestore::document::Document::parse(&index).unwrap(); | |
| 680 | let mut recognised = 0; | |
| 681 | for (space, id) in document.pages().unwrap() { | |
| 682 | let page = Page::from_revision(document.active(space).unwrap(), id).unwrap(); | |
| 683 | for object in &page.objects { | |
| 684 | if let PageObject::Image(image) = object | |
| 685 | && image.background | |
| 686 | { | |
| 687 | assert!(Background::recognise(image).is_some(), "{:?}", image.id); | |
| 688 | recognised += 1; | |
| 689 | } | |
| 690 | } | |
| 691 | } | |
| 692 | assert!(recognised > 0); | |
| 693 | } | |
| 694 | ||
| 695 | #[test] | |
| 696 | #[ignore = "requires a native GPU adapter"] | |
| 697 | fn dark_paper_paints_the_dark_recreation_instead_of_the_embedded_picture() { | |
| 698 | let background = picture(&red_png()); | |
| 699 | let id = background.id; | |
| 700 | let page = Page { | |
| 701 | identity: None, | |
| 702 | created: None, | |
| 703 | title: String::new(), | |
| 704 | margin_origin: [0.0; 2], | |
| 705 | definitions: BTreeMap::new(), | |
| 706 | objects: vec![PageObject::Image(background)], | |
| 707 | }; | |
| 708 | let mut scene = PageScene::new(page, &mut TextEngine::default()).unwrap(); | |
| 709 | scene.backgrounds.insert(id, ivy()); | |
| 710 | let mut before = Vec::new(); | |
| 711 | scene | |
| 712 | .append_primitives(&mut before, [0.0; 2], DARK) | |
| 713 | .unwrap(); | |
| 714 | assert!(before.is_empty(), "the paper shows until the raster lands"); | |
| 715 | drop(before); | |
| 716 | settle(|| scene.update_backgrounds(DARK, 1.0, Waker::noop())); | |
| 717 | let mut primitives = Vec::new(); | |
| 718 | scene | |
| 719 | .append_primitives(&mut primitives, [0.0; 2], DARK) | |
| 720 | .unwrap(); | |
| 721 | ||
| 722 | let instance = wgpu::Instance::new(wgpu::InstanceDescriptor::new_without_display_handle()); | |
| 723 | let adapter = pollster::block_on(instance.request_adapter(&Default::default())).unwrap(); | |
| 724 | let (device, queue) = | |
| 725 | pollster::block_on(adapter.request_device(&Default::default())).unwrap(); | |
| 726 | let size = [64_u32, 48]; | |
| 727 | let target = device.create_texture(&wgpu::TextureDescriptor { | |
| 728 | label: Some("Background readback test"), | |
| 729 | size: wgpu::Extent3d { | |
| 730 | width: size[0], | |
| 731 | height: size[1], | |
| 732 | depth_or_array_layers: 1, | |
| 733 | }, | |
| 734 | mip_level_count: 1, | |
| 735 | sample_count: 1, | |
| 736 | dimension: wgpu::TextureDimension::D2, | |
| 737 | format: wgpu::TextureFormat::Rgba8UnormSrgb, | |
| 738 | usage: wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::COPY_SRC, | |
| 739 | view_formats: &[], | |
| 740 | }); | |
| 741 | let readback = device.create_buffer(&wgpu::BufferDescriptor { | |
| 742 | label: Some("Background readback"), | |
| 743 | size: u64::from(size[0] * size[1] * 4), | |
| 744 | usage: wgpu::BufferUsages::MAP_READ | wgpu::BufferUsages::COPY_DST, | |
| 745 | mapped_at_creation: false, | |
| 746 | }); | |
| 747 | let mut renderer = draw::Renderer::new(device, queue, wgpu::TextureFormat::Rgba8UnormSrgb); | |
| 748 | let viewport = crate::gpu::Viewport { | |
| 749 | size, | |
| 750 | scale: 1.0, | |
| 751 | origin: [0.0; 2], | |
| 752 | }; | |
| 753 | renderer | |
| 754 | .draw( | |
| 755 | &target.create_view(&Default::default()), | |
| 756 | size, | |
| 757 | DARK.color, | |
| 758 | &[viewport.layer(&primitives)], | |
| 759 | ) | |
| 760 | .unwrap(); | |
| 761 | let mut encoder = renderer.device.create_command_encoder(&Default::default()); | |
| 762 | encoder.copy_texture_to_buffer( | |
| 763 | target.as_image_copy(), | |
| 764 | wgpu::TexelCopyBufferInfo { | |
| 765 | buffer: &readback, | |
| 766 | layout: wgpu::TexelCopyBufferLayout { | |
| 767 | offset: 0, | |
| 768 | bytes_per_row: Some(size[0] * 4), | |
| 769 | rows_per_image: Some(size[1]), | |
| 770 | }, | |
| 771 | }, | |
| 772 | wgpu::Extent3d { | |
| 773 | width: size[0], | |
| 774 | height: size[1], | |
| 775 | depth_or_array_layers: 1, | |
| 776 | }, | |
| 777 | ); | |
| 778 | renderer.queue.submit([encoder.finish()]); | |
| 779 | readback.map_async(wgpu::MapMode::Read, .., |result| result.unwrap()); | |
| 780 | renderer | |
| 781 | .device | |
| 782 | .poll(wgpu::PollType::Wait { | |
| 783 | submission_index: None, | |
| 784 | timeout: Some(Duration::from_secs(5)), | |
| 785 | }) | |
| 786 | .unwrap(); | |
| 787 | let pixels = readback.get_mapped_range(..).unwrap().to_vec(); | |
| 788 | let pixels: Vec<[u8; 3]> = pixels.chunks_exact(4).map(|p| [p[0], p[1], p[2]]).collect(); | |
| 789 | assert!( | |
| 790 | pixels | |
| 791 | .iter() | |
| 792 | .all(|[r, g, b]| !(*r > 200 && *g < 80 && *b < 80)), | |
| 793 | "the embedded picture was painted" | |
| 794 | ); | |
| 795 | // Leaves on dark paper stay green, darker than they are on white. | |
| 796 | assert!( | |
| 797 | pixels | |
| 798 | .iter() | |
| 799 | .any(|[r, g, b]| g > r && g > b && *g > 0x40 && *g < 0xce), | |
| 800 | "the dark recreation was not painted" | |
| 801 | ); | |
| 802 | assert!( | |
| 803 | pixels.iter().all(|[_, g, _]| *g < 0xe0), | |
| 804 | "the art kept white paper" | |
| 805 | ); | |
| 806 | } | |
| 807 | } |
crates/canvas/src/gpu/profile.rs+203-1| ... | ... | @@ -101,12 +101,15 @@ fn renderer_cost() { |
| 101 | 101 | } |
| 102 | 102 | let mut primitives = Vec::new(); |
| 103 | 103 | if let Some(scene) = &scene { |
| 104 | scene.append_primitives(&mut primitives, [0.0; 2]).unwrap(); | |
| 104 | scene | |
| 105 | .append_primitives(&mut primitives, [0.0; 2], Paper::WHITE) | |
| 106 | .unwrap(); | |
| 105 | 107 | } else { |
| 106 | 108 | primitives.push(draw::Primitive::Text { |
| 107 | 109 | clip: None, |
| 108 | 110 | text: layout.as_ref().unwrap(), |
| 109 | 111 | origin: [36.0, 90.0], |
| 112 | ink: Paper::WHITE.ink, | |
| 110 | 113 | }); |
| 111 | 114 | } |
| 112 | 115 | let start = Instant::now(); |
| ... | ... | @@ -150,3 +153,202 @@ fn load_scene(bytes: &[u8], title: &str, engine: &mut TextEngine) -> page::PageS |
| 150 | 153 | ) |
| 151 | 154 | .unwrap() |
| 152 | 155 | } |
| 156 | ||
| 157 | /// Frame cost of a large page as the app draws it: the view's primitives and the renderer's | |
| 158 | /// submission, resting, scrolling, typing and pressing Enter. `CANVAS_TEST_SECTION` and | |
| 159 | /// `CANVAS_TEST_PAGE` choose a stored page; otherwise one outline of 4000 paragraphs, which then | |
| 160 | /// gains a picture and a table in its middle to type beside and in. | |
| 161 | #[cfg(feature = "interaction")] | |
| 162 | #[test] | |
| 163 | #[ignore = "manual release frame timing probe"] | |
| 164 | fn frame_cost() { | |
| 165 | use crate::document::{TextDocument, TextPosition}; | |
| 166 | use crate::editor::CanvasEditor; | |
| 167 | use crate::interaction::{PageView, TextColors}; | |
| 168 | use draw::edit::{Key, NamedKey}; | |
| 169 | use onestore::page::{ | |
| 170 | Image, ParagraphContent, Table, TableCell, TableColumn, TableRow, text::new_id, | |
| 171 | }; | |
| 172 | const LINES: usize = 4000; | |
| 173 | let lines = || { | |
| 174 | (0..LINES).map(|line| { | |
| 175 | Paragraph::new( | |
| 176 | format!("Line {line}: a paragraph of notes with café and a few more words to wrap across the outline's width."), | |
| 177 | Default::default(), | |
| 178 | ) | |
| 179 | }) | |
| 180 | }; | |
| 181 | let synthetic = std::env::var_os("CANVAS_TEST_SECTION").is_none(); | |
| 182 | let mut engine = TextEngine::default(); | |
| 183 | let (editor, scene) = match std::env::var_os("CANVAS_TEST_SECTION") { | |
| 184 | Some(path) => { | |
| 185 | let bytes = std::fs::read(path).unwrap(); | |
| 186 | let store = onestore::Store::parse(&bytes).unwrap(); | |
| 187 | let index = onestore::RevisionIndex::parse(&store).unwrap(); | |
| 188 | let document = onestore::document::Document::parse(&index).unwrap(); | |
| 189 | let title = std::env::var("CANVAS_TEST_PAGE").unwrap_or_default(); | |
| 190 | let page = Page::from_document(&document, &title).unwrap(); | |
| 191 | let (scene, editor) = page::PageScene::from_page(page, &mut engine).unwrap(); | |
| 192 | (editor, Some((scene, [0.0; 2]))) | |
| 193 | } | |
| 194 | None => { | |
| 195 | let document = TextDocument::new(lines().collect()).unwrap(); | |
| 196 | let editor = CanvasEditor::new(&mut engine, document, 480.0).unwrap(); | |
| 197 | (editor, None) | |
| 198 | } | |
| 199 | }; | |
| 200 | let size = [2360, 1300]; | |
| 201 | let mut view = PageView::new(editor, engine, scene, size, 2.0, Duration::from_millis(500)); | |
| 202 | let instance = wgpu::Instance::new(wgpu::InstanceDescriptor::new_without_display_handle()); | |
| 203 | let adapter = pollster::block_on(instance.request_adapter(&Default::default())).unwrap(); | |
| 204 | let (device, queue) = pollster::block_on(adapter.request_device(&Default::default())).unwrap(); | |
| 205 | let format = wgpu::TextureFormat::Rgba8UnormSrgb; | |
| 206 | let target = device.create_texture(&wgpu::TextureDescriptor { | |
| 207 | label: Some("Frame profile target"), | |
| 208 | size: wgpu::Extent3d { | |
| 209 | width: size[0], | |
| 210 | height: size[1], | |
| 211 | depth_or_array_layers: 1, | |
| 212 | }, | |
| 213 | mip_level_count: 1, | |
| 214 | sample_count: 1, | |
| 215 | dimension: wgpu::TextureDimension::D2, | |
| 216 | format, | |
| 217 | usage: wgpu::TextureUsages::RENDER_ATTACHMENT, | |
| 218 | view_formats: &[], | |
| 219 | }); | |
| 220 | let target = target.create_view(&Default::default()); | |
| 221 | let mut renderer = Renderer::new(device, queue, format); | |
| 222 | let colors = TextColors { | |
| 223 | caret: [0.0, 0.0, 0.0, 1.0], | |
| 224 | selection: [0.7, 0.8, 1.0, 1.0], | |
| 225 | paper: Paper::WHITE, | |
| 226 | }; | |
| 227 | let _ = view.pointer_moved([400.0, 400.0]).unwrap(); | |
| 228 | for phase in ["rest", "scroll", "type", "enter", "picture", "cell"] { | |
| 229 | match phase { | |
| 230 | "picture" if synthetic => { | |
| 231 | let mut nodes = TextDocument::new(lines().collect()) | |
| 232 | .unwrap() | |
| 233 | .nodes() | |
| 234 | .to_vec(); | |
| 235 | let mut picture = nodes[0].clone(); | |
| 236 | picture.id = new_id().unwrap(); | |
| 237 | picture.content = ParagraphContent::Image(Image { | |
| 238 | id: new_id().unwrap(), | |
| 239 | layout: onestore::document::Layout { | |
| 240 | max_width: Some(120.0), | |
| 241 | max_height: Some(80.0), | |
| 242 | ..Default::default() | |
| 243 | }, | |
| 244 | size: None, | |
| 245 | bytes: None, | |
| 246 | alt: None, | |
| 247 | background: false, | |
| 248 | }); | |
| 249 | let cell = || TableCell { | |
| 250 | id: new_id().unwrap(), | |
| 251 | layout: Default::default(), | |
| 252 | indents: vec![18.0, 0.0, 27.0], | |
| 253 | shading: None, | |
| 254 | paragraphs: vec![ | |
| 255 | crate::document::node( | |
| 256 | Paragraph::new("Cell".into(), Default::default()), | |
| 257 | Default::default(), | |
| 258 | ) | |
| 259 | .unwrap(), | |
| 260 | ], | |
| 261 | unsupported: Vec::new(), | |
| 262 | }; | |
| 263 | let mut table = nodes[0].clone(); | |
| 264 | table.id = new_id().unwrap(); | |
| 265 | table.content = ParagraphContent::Table(Table { | |
| 266 | id: new_id().unwrap(), | |
| 267 | columns: vec![ | |
| 268 | TableColumn { | |
| 269 | width: 72.0, | |
| 270 | locked: true, | |
| 271 | }; | |
| 272 | 2 | |
| 273 | ], | |
| 274 | rows: (0..2) | |
| 275 | .map(|_| TableRow { | |
| 276 | id: new_id().unwrap(), | |
| 277 | cells: vec![cell(), cell()], | |
| 278 | }) | |
| 279 | .collect(), | |
| 280 | borders: Some(true), | |
| 281 | layout: Default::default(), | |
| 282 | tags: Vec::new(), | |
| 283 | }); | |
| 284 | // The picture precedes paragraph LINES / 2, which the table follows. | |
| 285 | nodes.insert(LINES / 2, picture); | |
| 286 | nodes.insert(LINES / 2 + 2, table); | |
| 287 | let document = TextDocument::from_nodes(nodes).unwrap(); | |
| 288 | view.editor = CanvasEditor::new(&mut view.engine, document, 480.0).unwrap(); | |
| 289 | } | |
| 290 | "picture" | "cell" if !synthetic => continue, | |
| 291 | _ => {} | |
| 292 | } | |
| 293 | let caret = match phase { | |
| 294 | "picture" => Some(LINES / 2), | |
| 295 | "cell" => Some(LINES / 2 + 1), | |
| 296 | _ => None, | |
| 297 | }; | |
| 298 | if let Some(paragraph) = caret { | |
| 299 | view.editor | |
| 300 | .select( | |
| 301 | [TextPosition { | |
| 302 | paragraph, | |
| 303 | offset: 0, | |
| 304 | }; 2] | |
| 305 | .into(), | |
| 306 | ) | |
| 307 | .unwrap(); | |
| 308 | } | |
| 309 | let mut times = [Vec::new(), Vec::new(), Vec::new()]; | |
| 310 | for _ in 0..64 { | |
| 311 | let start = Instant::now(); | |
| 312 | match phase { | |
| 313 | "scroll" => { | |
| 314 | let _ = view.wheel([0.0, -120.0]).unwrap(); | |
| 315 | } | |
| 316 | "enter" => { | |
| 317 | let _ = view.key(&Key::Named(NamedKey::Enter), None).unwrap(); | |
| 318 | } | |
| 319 | "type" | "picture" | "cell" => { | |
| 320 | let _ = view.key(&Key::Named(NamedKey::Other), Some("a")).unwrap(); | |
| 321 | } | |
| 322 | _ => {} | |
| 323 | } | |
| 324 | let input = start.elapsed(); | |
| 325 | let start = Instant::now(); | |
| 326 | let primitives = view.primitives(colors).unwrap(); | |
| 327 | let built = start.elapsed(); | |
| 328 | let start = Instant::now(); | |
| 329 | renderer | |
| 330 | .draw(&target, size, [1.0; 4], &[view.viewport.layer(&primitives)]) | |
| 331 | .unwrap(); | |
| 332 | let drawn = start.elapsed(); | |
| 333 | drop(primitives); | |
| 334 | renderer | |
| 335 | .device | |
| 336 | .poll(wgpu::PollType::Wait { | |
| 337 | submission_index: None, | |
| 338 | timeout: Some(Duration::from_secs(10)), | |
| 339 | }) | |
| 340 | .unwrap(); | |
| 341 | for (list, time) in times.iter_mut().zip([input, built, drawn]) { | |
| 342 | list.push(time); | |
| 343 | } | |
| 344 | } | |
| 345 | for (name, mut list) in ["input", "primitives", "draw"].into_iter().zip(times) { | |
| 346 | list.sort(); | |
| 347 | eprintln!( | |
| 348 | "frame_cost\t{phase}\t{name}\tp50 {:.2} ms\tp99 {:.2} ms", | |
| 349 | list[list.len() / 2].as_secs_f64() * 1e3, | |
| 350 | list[list.len() * 99 / 100].as_secs_f64() * 1e3 | |
| 351 | ); | |
| 352 | } | |
| 353 | } | |
| 354 | } |
crates/canvas/src/interaction/accessibility.rs+813-582| ... | ... | @@ -1,141 +1,89 @@ |
| 1 | use crate::gpu::Viewport; | |
| 1 | use crate::gpu::{Viewport, page::PageScene}; | |
| 2 | 2 | use crate::{ |
| 3 | 3 | date::DateField, |
| 4 | 4 | document::TextPosition, |
| 5 | editor::{CanvasEditor, Selection, TextOutline}, | |
| 5 | editor::{CanvasEditor, Selection}, | |
| 6 | outline::ParagraphLayout, | |
| 6 | 7 | }; |
| 7 | 8 | use accesskit::{ |
| 8 | 9 | Action, Affine, Node, NodeId, Rect, Role, TextDirection, TextSelection, TreeId, TreeInfo, |
| 9 | 10 | TreeUpdate, |
| 10 | 11 | }; |
| 11 | use onestore::page::text::EditError; | |
| 12 | use onestore::{ExGuid, page::text::EditError}; | |
| 12 | 13 | use parley::Affinity; |
| 13 | use std::{collections::HashMap, ops::Range}; | |
| 14 | use std::collections::HashMap; | |
| 14 | 15 | |
| 15 | 16 | pub const ROOT: NodeId = NodeId(0); |
| 17 | /// The page in document coordinates; its transform is the viewport, so scrolling and zooming | |
| 18 | /// change only this node. | |
| 19 | const PAGE: NodeId = NodeId(1); | |
| 16 | 20 | |
| 17 | 21 | struct Run { |
| 18 | outline: onestore::ExGuid, | |
| 19 | 22 | id: NodeId, |
| 20 | 23 | node: Node, |
| 21 | positions: Vec<TextPosition>, | |
| 24 | /// Paragraph-relative source offset of each character boundary, except the end of a | |
| 25 | /// paragraph break, which is the next paragraph's start. | |
| 26 | offsets: Vec<u32>, | |
| 22 | 27 | } |
| 23 | 28 | |
| 24 | #[derive(Hash, PartialEq, Eq)] | |
| 25 | struct ParagraphKey { | |
| 29 | /// A paragraph's runs, in paragraph coordinates under a node at its origin, so reflow above | |
| 30 | /// it moves one node. | |
| 31 | struct Paragraph { | |
| 32 | id: NodeId, | |
| 26 | 33 | layout: u64, |
| 27 | 34 | source: usize, |
| 28 | next: Option<usize>, | |
| 29 | origin: [u32; 2], | |
| 35 | origin: [f32; 2], | |
| 36 | /// Its text's byte length in the field's value. | |
| 37 | length: usize, | |
| 38 | runs: Vec<Run>, | |
| 39 | } | |
| 40 | ||
| 41 | impl Paragraph { | |
| 42 | fn node(&self) -> Node { | |
| 43 | let mut node = Node::new(Role::GenericContainer); | |
| 44 | node.set_transform(Affine::translate(( | |
| 45 | f64::from(self.origin[0]), | |
| 46 | f64::from(self.origin[1]), | |
| 47 | ))); | |
| 48 | node.set_children(self.runs.iter().map(|run| run.id).collect::<Vec<_>>()); | |
| 49 | node | |
| 50 | } | |
| 30 | 51 | } |
| 31 | 52 | |
| 53 | struct Field { | |
| 54 | outline: ExGuid, | |
| 55 | id: NodeId, | |
| 56 | node: Node, | |
| 57 | paragraphs: Vec<Paragraph>, | |
| 58 | } | |
| 59 | ||
| 60 | /// The nodes last sent to assistive technology, so each update sends only what changed. | |
| 32 | 61 | pub struct Accessibility { |
| 33 | outlines: Vec<(onestore::ExGuid, NodeId)>, | |
| 34 | runs: Vec<Run>, | |
| 35 | paragraphs: HashMap<ParagraphKey, Range<usize>>, | |
| 36 | read_only: Vec<NodeId>, | |
| 37 | dates: Vec<(DateField, NodeId)>, | |
| 62 | fields: Vec<Field>, | |
| 63 | read_only: Vec<(NodeId, Node)>, | |
| 64 | dates: Vec<(DateField, NodeId, Node)>, | |
| 38 | 65 | next_id: u64, |
| 39 | 66 | } |
| 40 | 67 | |
| 41 | 68 | impl Default for Accessibility { |
| 42 | 69 | fn default() -> Self { |
| 43 | 70 | Self { |
| 44 | outlines: Vec::new(), | |
| 45 | runs: Vec::new(), | |
| 46 | paragraphs: HashMap::new(), | |
| 71 | fields: Vec::new(), | |
| 47 | 72 | read_only: Vec::new(), |
| 48 | 73 | dates: Vec::new(), |
| 49 | next_id: 1, | |
| 74 | next_id: 2, | |
| 50 | 75 | } |
| 51 | 76 | } |
| 52 | 77 | } |
| 53 | 78 | |
| 54 | 79 | impl Accessibility { |
| 55 | /// Retire published runs without recycling their IDs. | |
| 80 | /// Retire published nodes without recycling their IDs; the next update sends the whole tree. | |
| 56 | 81 | pub fn deactivate(&mut self) { |
| 57 | self.runs.clear(); | |
| 58 | self.paragraphs.clear(); | |
| 59 | self.outlines.clear(); | |
| 82 | self.fields.clear(); | |
| 60 | 83 | self.read_only.clear(); |
| 61 | 84 | self.dates.clear(); |
| 62 | 85 | } |
| 63 | 86 | |
| 64 | pub fn append_page_fields( | |
| 65 | &mut self, | |
| 66 | update: &mut TreeUpdate, | |
| 67 | scene: Option<&(crate::gpu::page::PageScene, [f32; 2])>, | |
| 68 | editor: &CanvasEditor, | |
| 69 | viewport: Viewport, | |
| 70 | focus: Option<usize>, | |
| 71 | ) { | |
| 72 | let mut count = 0; | |
| 73 | if let Some((scene, offset)) = scene { | |
| 74 | for (index, object) in scene.read_only(Some(editor)).enumerate() { | |
| 75 | if index == self.read_only.len() { | |
| 76 | let id = self.allocate(); | |
| 77 | self.read_only.push(id); | |
| 78 | } | |
| 79 | let id = self.read_only[index]; | |
| 80 | let mut node = Node::new(Role::Label); | |
| 81 | node.set_value(object.message); | |
| 82 | node.set_read_only(); | |
| 83 | node.add_action(Action::Focus); | |
| 84 | if focus == Some(index) { | |
| 85 | update.focus = id; | |
| 86 | } | |
| 87 | let [x0, y0, x1, y1] = object.rect(); | |
| 88 | let x = |v| f64::from((v + offset[0]) * viewport.scale + viewport.origin[0]); | |
| 89 | let y = |v| f64::from((v + offset[1]) * viewport.scale + viewport.origin[1]); | |
| 90 | node.set_bounds(Rect::new(x(x0), y(y0), x(x1), y(y1))); | |
| 91 | update | |
| 92 | .nodes | |
| 93 | .iter_mut() | |
| 94 | .find(|(id, _)| *id == ROOT) | |
| 95 | .unwrap() | |
| 96 | .1 | |
| 97 | .push_child(id); | |
| 98 | update.nodes.push((id, node)); | |
| 99 | count += 1; | |
| 100 | } | |
| 101 | } | |
| 102 | self.read_only.truncate(count); | |
| 103 | let mut date_count = 0; | |
| 104 | if let Some((scene, offset)) = scene { | |
| 105 | for (index, (field, rect)) in scene.date_fields(editor).enumerate() { | |
| 106 | if index == self.dates.len() { | |
| 107 | let id = self.allocate(); | |
| 108 | self.dates.push((field, id)); | |
| 109 | } | |
| 110 | self.dates[index].0 = field; | |
| 111 | let id = self.dates[index].1; | |
| 112 | let mut node = Node::new(Role::Button); | |
| 113 | node.set_label(super::DATE_LABELS[field as usize]); | |
| 114 | node.set_value( | |
| 115 | editor.date().unwrap().source().paragraphs[index] | |
| 116 | .text() | |
| 117 | .unwrap() | |
| 118 | .text | |
| 119 | .text(), | |
| 120 | ); | |
| 121 | node.add_action(Action::Click); | |
| 122 | let x = |v| f64::from((v + offset[0]) * viewport.scale + viewport.origin[0]); | |
| 123 | let y = |v| f64::from((v + offset[1]) * viewport.scale + viewport.origin[1]); | |
| 124 | node.set_bounds(Rect::new(x(rect[0]), y(rect[1]), x(rect[2]), y(rect[3]))); | |
| 125 | update | |
| 126 | .nodes | |
| 127 | .iter_mut() | |
| 128 | .find(|(id, _)| *id == ROOT) | |
| 129 | .unwrap() | |
| 130 | .1 | |
| 131 | .push_child(id); | |
| 132 | update.nodes.push((id, node)); | |
| 133 | date_count += 1; | |
| 134 | } | |
| 135 | } | |
| 136 | self.dates.truncate(date_count); | |
| 137 | } | |
| 138 | ||
| 139 | 87 | fn allocate(&mut self) -> NodeId { |
| 140 | 88 | let id = NodeId(self.next_id); |
| 141 | 89 | self.next_id = self |
| ... | ... | @@ -146,309 +94,132 @@ impl Accessibility { |
| 146 | 94 | } |
| 147 | 95 | |
| 148 | 96 | pub fn read_only_for_node(&self, node: NodeId) -> Option<usize> { |
| 149 | self.read_only.iter().position(|id| *id == node) | |
| 97 | self.read_only.iter().position(|(id, _)| *id == node) | |
| 150 | 98 | } |
| 151 | 99 | |
| 152 | 100 | pub fn date_for_node(&self, node: NodeId) -> Option<DateField> { |
| 153 | 101 | self.dates |
| 154 | 102 | .iter() |
| 155 | .find(|(_, id)| *id == node) | |
| 156 | .map(|(field, _)| *field) | |
| 103 | .find(|(_, id, _)| *id == node) | |
| 104 | .map(|(field, ..)| *field) | |
| 157 | 105 | } |
| 158 | 106 | |
| 159 | pub fn outline_for_node(&self, node: NodeId) -> Option<onestore::ExGuid> { | |
| 160 | self.outlines | |
| 107 | pub fn outline_for_node(&self, node: NodeId) -> Option<ExGuid> { | |
| 108 | self.fields | |
| 161 | 109 | .iter() |
| 162 | .find(|(_, id)| *id == node) | |
| 163 | .map(|(outline, _)| *outline) | |
| 110 | .find(|field| field.id == node) | |
| 111 | .map(|field| field.outline) | |
| 164 | 112 | } |
| 165 | 113 | |
| 114 | /// The nodes that changed since the last update. After an error, `deactivate` before the | |
| 115 | /// next update. | |
| 166 | 116 | pub fn update( |
| 167 | 117 | &mut self, |
| 168 | 118 | editor: &CanvasEditor, |
| 119 | scene: Option<&(PageScene, [f32; 2])>, | |
| 169 | 120 | viewport: Viewport, |
| 170 | 121 | title: &str, |
| 171 | preview: Option<(onestore::ExGuid, [f32; 2])>, | |
| 122 | preview: Option<(ExGuid, [f32; 2])>, | |
| 123 | read_only_focus: Option<usize>, | |
| 172 | 124 | ) -> Result<TreeUpdate, EditError> { |
| 173 | let mut runs = Vec::new(); | |
| 174 | let mut outlines = Vec::new(); | |
| 175 | let mut paragraph_ranges = HashMap::new(); | |
| 176 | for outline in editor.visible_outlines().chain(editor.caret_outline()) { | |
| 177 | let field = self | |
| 178 | .outlines | |
| 125 | let mut nodes = Vec::new(); | |
| 126 | let mut focus = ROOT; | |
| 127 | let mut previous: HashMap<_, _> = self | |
| 128 | .fields | |
| 129 | .drain(..) | |
| 130 | .map(|field| (field.outline, field)) | |
| 131 | .collect(); | |
| 132 | for (ordinal, outline) in editor | |
| 133 | .visible_outlines() | |
| 134 | .chain(editor.caret_outline()) | |
| 135 | .enumerate() | |
| 136 | { | |
| 137 | let (id, sent, old) = match previous.remove(&outline.id) { | |
| 138 | Some(field) => (field.id, Some(field.node), field.paragraphs), | |
| 139 | None => (self.allocate(), None, Vec::new()), | |
| 140 | }; | |
| 141 | let layouts: Vec<_> = outline.layouts().collect(); | |
| 142 | // Only a paragraph with a successor ends its last run with the paragraph break. | |
| 143 | let same = |old_index: usize, index: usize| { | |
| 144 | old[old_index].layout == layouts[index].1.text.id() | |
| 145 | && (old_index + 1 == old.len()) == (index + 1 == layouts.len()) | |
| 146 | }; | |
| 147 | let shorter = old.len().min(layouts.len()); | |
| 148 | let prefix = (0..shorter).take_while(|&i| same(i, i)).count(); | |
| 149 | let suffix = (1..=shorter - prefix) | |
| 150 | .take_while(|&i| same(old.len() - i, layouts.len() - i)) | |
| 151 | .count(); | |
| 152 | let text = sent.as_ref().and_then(Node::value).unwrap_or_default(); | |
| 153 | let length = |paragraphs: &[Paragraph]| paragraphs.iter().map(|p| p.length).sum(); | |
| 154 | let mut value = text[..length(&old[..prefix])].to_owned(); | |
| 155 | let tail = &text[text.len() - length(&old[old.len() - suffix..])..]; | |
| 156 | let mut paragraphs = old; | |
| 157 | let kept = paragraphs.split_off(paragraphs.len() - suffix); | |
| 158 | let mut replaced = paragraphs.split_off(prefix).into_iter(); | |
| 159 | for (at, (source, shaped)) in layouts | |
| 179 | 160 | .iter() |
| 180 | .find(|(id, _)| *id == outline.id) | |
| 181 | .map(|(_, node)| *node) | |
| 182 | .unwrap_or_else(|| self.allocate()); | |
| 183 | outlines.push((outline.id, field)); | |
| 184 | let mut paragraphs = outline.layouts().peekable(); | |
| 185 | while let Some((paragraph, shaped)) = paragraphs.next() { | |
| 186 | let layout = &shaped.text; | |
| 187 | let origin = shaped.origin; | |
| 188 | let key = ParagraphKey { | |
| 189 | layout: layout.id(), | |
| 190 | source: paragraph, | |
| 191 | next: paragraphs.peek().map(|(next, _)| *next), | |
| 192 | origin: origin.map(f32::to_bits), | |
| 193 | }; | |
| 194 | let start = runs.len(); | |
| 195 | if let Some(range) = self.paragraphs.get(&key) { | |
| 196 | runs.extend( | |
| 197 | self.runs[range.clone()] | |
| 198 | .iter() | |
| 199 | .map(|run| (run.outline, run.node.clone(), run.positions.clone())), | |
| 200 | ); | |
| 201 | paragraph_ranges.insert(key, start..runs.len()); | |
| 202 | continue; | |
| 203 | } | |
| 204 | let projection = &shaped.projection; | |
| 205 | let text = projection.text().text(); | |
| 206 | let boundaries: Vec<_> = projection.source_boundaries().collect(); | |
| 207 | let source_position = |byte| -> Result<TextPosition, EditError> { | |
| 208 | let index = boundaries | |
| 209 | .binary_search_by_key(&byte, |(byte, _)| *byte) | |
| 210 | .map_err(|_| EditError::InvalidRange)?; | |
| 211 | Ok(TextPosition { | |
| 212 | paragraph, | |
| 213 | offset: boundaries[index].1, | |
| 214 | }) | |
| 161 | .enumerate() | |
| 162 | .take(layouts.len() - suffix) | |
| 163 | .skip(prefix) | |
| 164 | { | |
| 165 | let (id, old) = match replaced.next() { | |
| 166 | Some(paragraph) => (paragraph.id, paragraph.runs), | |
| 167 | None => (self.allocate(), Vec::new()), | |
| 215 | 168 | }; |
| 216 | let line_count = layout.lines().count(); | |
| 217 | for (line_index, (line, bounds)) in layout.lines().enumerate() { | |
| 218 | let line_start = runs.len(); | |
| 219 | let mut x = line.metrics().offset; | |
| 220 | let mut logical_runs = Vec::new(); | |
| 221 | for run in line.runs() { | |
| 222 | let advance = run.advance(); | |
| 223 | logical_runs.push((run, x)); | |
| 224 | x += advance; | |
| 225 | } | |
| 226 | logical_runs.sort_by_key(|(run, _)| run.text_range().start); | |
| 227 | for (run, x) in logical_runs { | |
| 228 | // AccessKit stores each character's byte length in a u8. | |
| 229 | let mut characters = Vec::new(); | |
| 230 | for cluster in run.clusters() { | |
| 231 | let range = cluster.text_range(); | |
| 232 | if range.is_empty() || range.start == text.len() { | |
| 233 | continue; | |
| 234 | } | |
| 235 | let cluster_text = | |
| 236 | text.get(range.clone()).ok_or(EditError::InvalidRange)?; | |
| 237 | if range.len() <= u8::MAX as usize { | |
| 238 | characters.push(( | |
| 239 | range, | |
| 240 | cluster.advance(), | |
| 241 | cluster.is_word_boundary(), | |
| 242 | )); | |
| 243 | } else { | |
| 244 | for (index, (offset, ch)) in cluster_text.char_indices().enumerate() | |
| 245 | { | |
| 246 | characters.push(( | |
| 247 | range.start + offset..range.start + offset + ch.len_utf8(), | |
| 248 | if index == 0 { cluster.advance() } else { 0.0 }, | |
| 249 | index == 0 && cluster.is_word_boundary(), | |
| 250 | )); | |
| 251 | } | |
| 252 | } | |
| 253 | } | |
| 254 | let mut advance = 0.0; | |
| 255 | for chunk in characters.chunks(256) { | |
| 256 | let width: f32 = chunk.iter().map(|(_, width, _)| width).sum(); | |
| 257 | let left = x + if run.is_rtl() { | |
| 258 | run.advance() - advance - width | |
| 259 | } else { | |
| 260 | advance | |
| 261 | }; | |
| 262 | let mut node = Node::new(Role::TextRun); | |
| 263 | node.set_bounds(Rect::new( | |
| 264 | f64::from(left + origin[0]), | |
| 265 | f64::from(bounds.top + origin[1]), | |
| 266 | f64::from(left + width + origin[0]), | |
| 267 | f64::from(bounds.top + bounds.height + origin[1]), | |
| 268 | )); | |
| 269 | node.set_text_direction(if run.is_rtl() { | |
| 270 | TextDirection::RightToLeft | |
| 271 | } else { | |
| 272 | TextDirection::LeftToRight | |
| 273 | }); | |
| 274 | node.set_font_size(run.font_size()); | |
| 275 | let start = chunk[0].0.start; | |
| 276 | let end = chunk.last().unwrap().0.end; | |
| 277 | node.set_value(text.get(start..end).ok_or(EditError::InvalidRange)?); | |
| 278 | node.set_character_lengths( | |
| 279 | chunk | |
| 280 | .iter() | |
| 281 | .map(|(range, _, _)| u8::try_from(range.len()).unwrap()) | |
| 282 | .collect::<Vec<_>>(), | |
| 283 | ); | |
| 284 | let mut offset = 0.0; | |
| 285 | node.set_character_positions( | |
| 286 | chunk | |
| 287 | .iter() | |
| 288 | .map(|(_, width, _)| { | |
| 289 | let position = offset; | |
| 290 | offset += width; | |
| 291 | position | |
| 292 | }) | |
| 293 | .collect::<Vec<_>>(), | |
| 294 | ); | |
| 295 | node.set_character_widths( | |
| 296 | chunk.iter().map(|(_, width, _)| *width).collect::<Vec<_>>(), | |
| 297 | ); | |
| 298 | node.set_word_starts( | |
| 299 | chunk | |
| 300 | .iter() | |
| 301 | .enumerate() | |
| 302 | .filter_map(|(index, (_, _, word))| word.then_some(index as u8)) | |
| 303 | .collect::<Vec<_>>(), | |
| 304 | ); | |
| 305 | let positions = chunk | |
| 306 | .iter() | |
| 307 | .map(|(range, _, _)| source_position(range.start)) | |
| 308 | .chain(std::iter::once(source_position(end))) | |
| 309 | .collect::<Result<Vec<_>, _>>()?; | |
| 310 | runs.push((outline.id, node, positions)); | |
| 311 | advance += width; | |
| 169 | let mut runs = Vec::new(); | |
| 170 | for (index, (node, offsets)) in runs_of(shaped, at + 1 < layouts.len())? | |
| 171 | .into_iter() | |
| 172 | .enumerate() | |
| 173 | { | |
| 174 | let id = match old.get(index) { | |
| 175 | Some(old) if old.offsets == offsets && old.node.value() == node.value() => { | |
| 176 | old.id | |
| 312 | 177 | } |
| 313 | } | |
| 314 | // Empty lines still need a text position for selection and insertion. | |
| 315 | if runs.len() == line_start { | |
| 316 | let mut node = Node::new(Role::TextRun); | |
| 317 | node.set_value(""); | |
| 318 | node.set_character_lengths(Vec::<u8>::new()); | |
| 319 | node.set_character_positions(Vec::<f32>::new()); | |
| 320 | node.set_character_widths(Vec::<f32>::new()); | |
| 321 | node.set_text_direction(TextDirection::LeftToRight); | |
| 322 | node.set_bounds(Rect::new( | |
| 323 | f64::from(origin[0]), | |
| 324 | f64::from(bounds.top + origin[1]), | |
| 325 | f64::from(origin[0]), | |
| 326 | f64::from(bounds.top + bounds.height + origin[1]), | |
| 327 | )); | |
| 328 | runs.push(( | |
| 329 | outline.id, | |
| 330 | node, | |
| 331 | vec![source_position(bounds.source.start)?], | |
| 332 | )); | |
| 333 | } | |
| 334 | if line_index + 1 == line_count | |
| 335 | && let Some((next, _)) = paragraphs.peek() | |
| 178 | _ => self.allocate(), | |
| 179 | }; | |
| 180 | runs.push(Run { id, node, offsets }); | |
| 181 | } | |
| 182 | for index in 1..runs.len() { | |
| 183 | if runs[index - 1].node.bounds().map(|r| r.y0) | |
| 184 | == runs[index].node.bounds().map(|r| r.y0) | |
| 336 | 185 | { |
| 337 | let (_, node, source_positions) = runs.last_mut().unwrap(); | |
| 338 | let mut value = node.value().unwrap().to_owned(); | |
| 339 | value.push('\n'); | |
| 340 | node.set_value(value); | |
| 341 | let mut lengths = node.character_lengths().to_vec(); | |
| 342 | lengths.push(1); | |
| 343 | node.set_character_lengths(lengths); | |
| 344 | if let Some(rect) = node.bounds() { | |
| 345 | let mut positions = | |
| 346 | node.character_positions().unwrap_or_default().to_vec(); | |
| 347 | positions.push(rect.width() as f32); | |
| 348 | node.set_character_positions(positions); | |
| 349 | let mut widths = node.character_widths().unwrap_or_default().to_vec(); | |
| 350 | widths.push(4.0); | |
| 351 | node.set_character_widths(widths); | |
| 352 | } | |
| 353 | source_positions.push(TextPosition { | |
| 354 | paragraph: *next, | |
| 355 | offset: 0, | |
| 356 | }); | |
| 186 | let (before, after) = runs.split_at_mut(index); | |
| 187 | before[index - 1].node.set_next_on_line(after[0].id); | |
| 188 | after[0].node.set_previous_on_line(before[index - 1].id); | |
| 357 | 189 | } |
| 358 | 190 | } |
| 359 | if !shaped.tags.is_empty() { | |
| 360 | let descriptions = shaped | |
| 361 | .tags | |
| 362 | .iter() | |
| 363 | .map(|tag| { | |
| 364 | use crate::outline::TagIcon; | |
| 365 | let fallback = match tag.icon { | |
| 366 | TagIcon::CheckBox { .. } => "To do", | |
| 367 | TagIcon::Question => "Question", | |
| 368 | TagIcon::Music => "Music", | |
| 369 | TagIcon::Exclamation => "Critical", | |
| 370 | TagIcon::RedSquare => "Project A", | |
| 371 | TagIcon::YellowSquare => "Project B", | |
| 372 | TagIcon::BlueSquare => "Project C", | |
| 373 | }; | |
| 374 | let label = if tag.label.is_empty() { | |
| 375 | fallback | |
| 376 | } else { | |
| 377 | &tag.label | |
| 378 | }; | |
| 379 | let state = match tag.icon { | |
| 380 | TagIcon::CheckBox { checked: true } => ", completed", | |
| 381 | TagIcon::CheckBox { checked: false } => ", incomplete", | |
| 382 | _ => "", | |
| 383 | }; | |
| 384 | format!( | |
| 385 | "{label}{state}{}", | |
| 386 | if tag.disabled { ", disabled" } else { "" } | |
| 387 | ) | |
| 388 | }) | |
| 389 | .collect::<Vec<_>>() | |
| 390 | .join("; "); | |
| 391 | runs[start].1.set_description(descriptions); | |
| 392 | } | |
| 393 | paragraph_ranges.insert(key, start..runs.len()); | |
| 394 | } | |
| 395 | } | |
| 396 | let mut runs: Vec<_> = runs | |
| 397 | .into_iter() | |
| 398 | .enumerate() | |
| 399 | .map(|(index, (outline, mut node, positions))| { | |
| 400 | node.clear_next_on_line(); | |
| 401 | node.clear_previous_on_line(); | |
| 402 | let id = match self.runs.get(index) { | |
| 403 | Some(old) | |
| 404 | if old.outline == outline | |
| 405 | && old.positions == positions | |
| 406 | && old.node.value() == node.value() => | |
| 191 | for (index, run) in runs.iter().enumerate() { | |
| 192 | if old | |
| 193 | .get(index) | |
| 194 | .is_none_or(|old| old.id != run.id || old.node != run.node) | |
| 407 | 195 | { |
| 408 | old.id | |
| 196 | nodes.push((run.id, run.node.clone())); | |
| 409 | 197 | } |
| 410 | _ => self.allocate(), | |
| 411 | }; | |
| 412 | Run { | |
| 413 | outline, | |
| 198 | } | |
| 199 | let start = value.len(); | |
| 200 | value.extend(runs.iter().filter_map(|run| run.node.value())); | |
| 201 | let paragraph = Paragraph { | |
| 414 | 202 | id, |
| 415 | node, | |
| 416 | positions, | |
| 203 | layout: shaped.text.id(), | |
| 204 | source: *source, | |
| 205 | origin: shaped.origin, | |
| 206 | length: value.len() - start, | |
| 207 | runs, | |
| 208 | }; | |
| 209 | nodes.push((id, paragraph.node())); | |
| 210 | paragraphs.push(paragraph); | |
| 211 | } | |
| 212 | paragraphs.extend(kept); | |
| 213 | value.push_str(tail); | |
| 214 | for (paragraph, (source, shaped)) in paragraphs.iter_mut().zip(&layouts) { | |
| 215 | paragraph.source = *source; | |
| 216 | if paragraph.origin != shaped.origin { | |
| 217 | paragraph.origin = shaped.origin; | |
| 218 | nodes.push((paragraph.id, paragraph.node())); | |
| 417 | 219 | } |
| 418 | }) | |
| 419 | .collect(); | |
| 420 | for index in 1..runs.len() { | |
| 421 | if runs[index - 1].outline == runs[index].outline | |
| 422 | && runs[index - 1].node.bounds().map(|r| r.y0) | |
| 423 | == runs[index].node.bounds().map(|r| r.y0) | |
| 424 | { | |
| 425 | let (before, after) = runs.split_at_mut(index); | |
| 426 | before[index - 1].node.set_next_on_line(after[0].id); | |
| 427 | after[0].node.set_previous_on_line(before[index - 1].id); | |
| 428 | 220 | } |
| 429 | } | |
| 430 | self.runs = runs; | |
| 431 | self.paragraphs = paragraph_ranges; | |
| 432 | self.outlines = outlines; | |
| 433 | let mut root = Node::new(Role::Window); | |
| 434 | root.set_label(title); | |
| 435 | root.set_children(self.outlines.iter().map(|(_, id)| *id).collect::<Vec<_>>()); | |
| 436 | root.set_bounds(Rect::new( | |
| 437 | 0.0, | |
| 438 | 0.0, | |
| 439 | viewport.size[0] as f64, | |
| 440 | viewport.size[1] as f64, | |
| 441 | )); | |
| 442 | let mut nodes = vec![(ROOT, root)]; | |
| 443 | let mut focus = ROOT; | |
| 444 | for (index, (outline, (_, id))) in editor | |
| 445 | .visible_outlines() | |
| 446 | .chain(editor.caret_outline()) | |
| 447 | .zip(&self.outlines) | |
| 448 | .enumerate() | |
| 449 | { | |
| 450 | let mut field = Node::new(Role::MultilineTextInput); | |
| 451 | field.set_label(if outline.title { | |
| 221 | let mut node = Node::new(Role::MultilineTextInput); | |
| 222 | node.set_label(if outline.title { | |
| 452 | 223 | "Page title".into() |
| 453 | 224 | } else if editor |
| 454 | 225 | .caret_outline() |
| ... | ... | @@ -456,47 +227,121 @@ impl Accessibility { |
| 456 | 227 | { |
| 457 | 228 | "Text input".into() |
| 458 | 229 | } else { |
| 459 | format!("Text outline {}", index + 1) | |
| 230 | format!("Text outline {}", ordinal + 1) | |
| 460 | 231 | }); |
| 461 | field.add_action(Action::Focus); | |
| 462 | field.add_action(Action::SetTextSelection); | |
| 463 | field.add_action(Action::ReplaceSelectedText); | |
| 464 | field.add_action(Action::SetValue); | |
| 465 | let runs = self.runs.iter().filter(|run| run.outline == outline.id); | |
| 466 | field.set_value( | |
| 467 | runs.clone() | |
| 468 | .filter_map(|run| run.node.value()) | |
| 469 | .collect::<String>(), | |
| 232 | node.add_action(Action::Focus); | |
| 233 | node.add_action(Action::SetTextSelection); | |
| 234 | node.add_action(Action::ReplaceSelectedText); | |
| 235 | node.add_action(Action::SetValue); | |
| 236 | node.set_value(value); | |
| 237 | node.set_children( | |
| 238 | paragraphs | |
| 239 | .iter() | |
| 240 | .map(|paragraph| paragraph.id) | |
| 241 | .collect::<Vec<_>>(), | |
| 470 | 242 | ); |
| 471 | field.set_children(runs.map(|run| run.id).collect::<Vec<_>>()); | |
| 472 | 243 | let origin = preview |
| 473 | 244 | .filter(|(id, _)| *id == outline.id) |
| 474 | 245 | .map(|(_, origin)| origin) |
| 475 | 246 | .unwrap_or_else(|| outline.origin()); |
| 476 | field.set_transform(Affine::new([ | |
| 477 | f64::from(viewport.scale), | |
| 478 | 0.0, | |
| 479 | 0.0, | |
| 480 | f64::from(viewport.scale), | |
| 481 | f64::from(viewport.origin[0]) + f64::from(origin[0]) * f64::from(viewport.scale), | |
| 482 | f64::from(viewport.origin[1]) + f64::from(origin[1]) * f64::from(viewport.scale), | |
| 483 | ])); | |
| 247 | node.set_transform(Affine::translate(( | |
| 248 | f64::from(origin[0]), | |
| 249 | f64::from(origin[1]), | |
| 250 | ))); | |
| 484 | 251 | let bounds = outline.bounds(); |
| 485 | field.set_bounds(Rect::new(0.0, 0.0, bounds.width(), bounds.height())); | |
| 252 | node.set_bounds(Rect::new(0.0, 0.0, bounds.width(), bounds.height())); | |
| 486 | 253 | if outline.id == editor.active_outline().id { |
| 487 | 254 | let Selection { |
| 488 | 255 | positions: [anchor, caret], |
| 489 | 256 | affinities, |
| 490 | 257 | } = editor.selection(); |
| 491 | field.set_text_selection(TextSelection { | |
| 492 | anchor: self.position(outline, anchor, affinities[0])?, | |
| 493 | focus: self.position(outline, caret, affinities[1])?, | |
| 258 | node.set_text_selection(TextSelection { | |
| 259 | anchor: position(&layouts, &paragraphs, anchor, affinities[0])?, | |
| 260 | focus: position(&layouts, &paragraphs, caret, affinities[1])?, | |
| 494 | 261 | }); |
| 495 | focus = *id; | |
| 262 | focus = id; | |
| 263 | } | |
| 264 | if sent.as_ref() != Some(&node) { | |
| 265 | nodes.push((id, node.clone())); | |
| 266 | } | |
| 267 | self.fields.push(Field { | |
| 268 | outline: outline.id, | |
| 269 | id, | |
| 270 | node, | |
| 271 | paragraphs, | |
| 272 | }); | |
| 273 | } | |
| 274 | let mut children: Vec<_> = self.fields.iter().map(|field| field.id).collect(); | |
| 275 | let sent_read_only = std::mem::take(&mut self.read_only); | |
| 276 | let sent_dates = std::mem::take(&mut self.dates); | |
| 277 | if let Some((scene, offset)) = scene { | |
| 278 | let rect = |[x0, y0, x1, y1]: [f32; 4]| { | |
| 279 | Rect::new( | |
| 280 | f64::from(x0 + offset[0]), | |
| 281 | f64::from(y0 + offset[1]), | |
| 282 | f64::from(x1 + offset[0]), | |
| 283 | f64::from(y1 + offset[1]), | |
| 284 | ) | |
| 285 | }; | |
| 286 | for (index, object) in scene.read_only(Some(editor)).enumerate() { | |
| 287 | let mut node = Node::new(Role::Label); | |
| 288 | node.set_value(object.message); | |
| 289 | node.set_read_only(); | |
| 290 | node.add_action(Action::Focus); | |
| 291 | node.set_bounds(rect(object.rect())); | |
| 292 | let sent = sent_read_only.get(index); | |
| 293 | let id = sent.map_or_else(|| self.allocate(), |(id, _)| *id); | |
| 294 | if sent.is_none_or(|(_, sent)| *sent != node) { | |
| 295 | nodes.push((id, node.clone())); | |
| 296 | } | |
| 297 | if read_only_focus == Some(index) { | |
| 298 | focus = id; | |
| 299 | } | |
| 300 | children.push(id); | |
| 301 | self.read_only.push((id, node)); | |
| 302 | } | |
| 303 | for (index, (field, bounds)) in scene.date_fields(editor).enumerate() { | |
| 304 | let mut node = Node::new(Role::Button); | |
| 305 | node.set_label(super::DATE_LABELS[field as usize]); | |
| 306 | node.set_value( | |
| 307 | editor.date().unwrap().source().paragraphs[index] | |
| 308 | .text() | |
| 309 | .unwrap() | |
| 310 | .text | |
| 311 | .text(), | |
| 312 | ); | |
| 313 | node.add_action(Action::Click); | |
| 314 | node.set_bounds(rect(bounds)); | |
| 315 | let sent = sent_dates.get(index); | |
| 316 | let id = sent.map_or_else(|| self.allocate(), |(_, id, _)| *id); | |
| 317 | if sent.is_none_or(|(_, _, sent)| *sent != node) { | |
| 318 | nodes.push((id, node.clone())); | |
| 319 | } | |
| 320 | children.push(id); | |
| 321 | self.dates.push((field, id, node)); | |
| 496 | 322 | } |
| 497 | nodes.push((*id, field)); | |
| 498 | 323 | } |
| 499 | nodes.extend(self.runs.iter().map(|run| (run.id, run.node.clone()))); | |
| 324 | let mut page = Node::new(Role::GenericContainer); | |
| 325 | page.set_transform(Affine::new([ | |
| 326 | f64::from(viewport.scale), | |
| 327 | 0.0, | |
| 328 | 0.0, | |
| 329 | f64::from(viewport.scale), | |
| 330 | f64::from(viewport.origin[0]), | |
| 331 | f64::from(viewport.origin[1]), | |
| 332 | ])); | |
| 333 | page.set_children(children); | |
| 334 | let mut root = Node::new(Role::Window); | |
| 335 | root.set_label(title); | |
| 336 | root.set_children(vec![PAGE]); | |
| 337 | root.set_bounds(Rect::new( | |
| 338 | 0.0, | |
| 339 | 0.0, | |
| 340 | viewport.size[0] as f64, | |
| 341 | viewport.size[1] as f64, | |
| 342 | )); | |
| 343 | nodes.push((ROOT, root)); | |
| 344 | nodes.push((PAGE, page)); | |
| 500 | 345 | Ok(TreeUpdate { |
| 501 | 346 | nodes, |
| 502 | 347 | tree: Some(TreeInfo::new(ROOT)), |
| ... | ... | @@ -505,71 +350,47 @@ impl Accessibility { |
| 505 | 350 | }) |
| 506 | 351 | } |
| 507 | 352 | |
| 508 | fn position( | |
| 509 | &self, | |
| 510 | outline: &TextOutline, | |
| 511 | mut position: TextPosition, | |
| 512 | affinity: Affinity, | |
| 513 | ) -> Result<accesskit::TextPosition, EditError> { | |
| 514 | let shaped = outline.paragraph_layout(position.paragraph)?; | |
| 515 | let projection = &shaped.projection; | |
| 516 | let layout = &shaped.text; | |
| 517 | let origin = shaped.origin; | |
| 518 | let cursor = layout.cursor( | |
| 519 | projection | |
| 520 | .text() | |
| 521 | .byte_offset(projection.visible_offset(position.offset)?)?, | |
| 522 | affinity, | |
| 523 | ); | |
| 524 | position.offset = projection.source_offset( | |
| 525 | projection.text().utf16_offset(cursor.index())?, | |
| 526 | onestore::page::text::Affinity::Downstream, | |
| 527 | )?; | |
| 528 | let caret = layout.caret(cursor, 0.0); | |
| 529 | let mut positions = self | |
| 530 | .runs | |
| 531 | .iter() | |
| 532 | .filter(|run| { | |
| 533 | run.outline == outline.id | |
| 534 | && run | |
| 535 | .node | |
| 536 | .bounds() | |
| 537 | .is_some_and(|rect| rect.y0 == f64::from(caret.y0 as f32 + origin[1])) | |
| 538 | }) | |
| 539 | .filter_map(|run| { | |
| 540 | run.positions | |
| 541 | .iter() | |
| 542 | .position(|p| *p == position) | |
| 543 | .map(|character_index| accesskit::TextPosition { | |
| 544 | node: run.id, | |
| 545 | character_index, | |
| 546 | }) | |
| 547 | }); | |
| 548 | match affinity { | |
| 549 | Affinity::Upstream => positions.next(), | |
| 550 | Affinity::Downstream => positions.next_back(), | |
| 551 | } | |
| 552 | .ok_or(EditError::InvalidRange) | |
| 553 | } | |
| 554 | ||
| 555 | 353 | pub fn selection( |
| 556 | 354 | &self, |
| 557 | outline: onestore::ExGuid, | |
| 355 | outline: ExGuid, | |
| 558 | 356 | selection: TextSelection, |
| 559 | 357 | ) -> Result<Selection, EditError> { |
| 358 | let field = self | |
| 359 | .fields | |
| 360 | .iter() | |
| 361 | .find(|field| field.outline == outline) | |
| 362 | .ok_or(EditError::InvalidRange)?; | |
| 560 | 363 | let position = |position: accesskit::TextPosition| { |
| 561 | let run = self | |
| 562 | .runs | |
| 364 | let (index, paragraph, run) = field | |
| 365 | .paragraphs | |
| 563 | 366 | .iter() |
| 564 | .find(|run| run.outline == outline && run.id == position.node) | |
| 565 | .ok_or(EditError::InvalidRange)?; | |
| 566 | let source = *run | |
| 567 | .positions | |
| 568 | .get(position.character_index) | |
| 367 | .enumerate() | |
| 368 | .find_map(|(index, paragraph)| { | |
| 369 | let run = paragraph | |
| 370 | .runs | |
| 371 | .iter() | |
| 372 | .position(|run| run.id == position.node)?; | |
| 373 | Some((index, paragraph, run)) | |
| 374 | }) | |
| 569 | 375 | .ok_or(EditError::InvalidRange)?; |
| 570 | let affinity = if position.character_index > 0 | |
| 571 | && position.character_index + 1 == run.positions.len() | |
| 572 | { | |
| 376 | let offsets = &paragraph.runs[run].offsets; | |
| 377 | let breaks = run + 1 == paragraph.runs.len() && index + 1 < field.paragraphs.len(); | |
| 378 | let count = offsets.len() + usize::from(breaks); | |
| 379 | let character = position.character_index; | |
| 380 | if character >= count { | |
| 381 | return Err(EditError::InvalidRange); | |
| 382 | } | |
| 383 | let source = match offsets.get(character) { | |
| 384 | Some(offset) => TextPosition { | |
| 385 | paragraph: paragraph.source, | |
| 386 | offset: *offset, | |
| 387 | }, | |
| 388 | None => TextPosition { | |
| 389 | paragraph: field.paragraphs[index + 1].source, | |
| 390 | offset: 0, | |
| 391 | }, | |
| 392 | }; | |
| 393 | let affinity = if character > 0 && character + 1 == count { | |
| 573 | 394 | Affinity::Upstream |
| 574 | 395 | } else { |
| 575 | 396 | Affinity::Downstream |
| ... | ... | @@ -585,13 +406,304 @@ impl Accessibility { |
| 585 | 406 | } |
| 586 | 407 | } |
| 587 | 408 | |
| 409 | /// The accessible position of a source position in `layouts`, which `paragraphs` mirrors. | |
| 410 | fn position( | |
| 411 | layouts: &[(usize, &ParagraphLayout)], | |
| 412 | paragraphs: &[Paragraph], | |
| 413 | mut position: TextPosition, | |
| 414 | affinity: Affinity, | |
| 415 | ) -> Result<accesskit::TextPosition, EditError> { | |
| 416 | let index = layouts | |
| 417 | .binary_search_by_key(&position.paragraph, |(source, _)| *source) | |
| 418 | .map_err(|_| EditError::InvalidRange)?; | |
| 419 | let shaped = layouts[index].1; | |
| 420 | let projection = &shaped.projection; | |
| 421 | let layout = &shaped.text; | |
| 422 | let cursor = layout.cursor( | |
| 423 | projection | |
| 424 | .text() | |
| 425 | .byte_offset(projection.visible_offset(position.offset)?)?, | |
| 426 | affinity, | |
| 427 | ); | |
| 428 | position.offset = projection.source_offset( | |
| 429 | projection.text().utf16_offset(cursor.index())?, | |
| 430 | onestore::page::text::Affinity::Downstream, | |
| 431 | )?; | |
| 432 | let caret = layout.caret(cursor, 0.0); | |
| 433 | let mut positions = paragraphs[index] | |
| 434 | .runs | |
| 435 | .iter() | |
| 436 | .filter(|run| run.node.bounds().is_some_and(|rect| rect.y0 == caret.y0)) | |
| 437 | .filter_map(|run| { | |
| 438 | run.offsets | |
| 439 | .iter() | |
| 440 | .position(|offset| *offset == position.offset) | |
| 441 | .map(|character_index| accesskit::TextPosition { | |
| 442 | node: run.id, | |
| 443 | character_index, | |
| 444 | }) | |
| 445 | }); | |
| 446 | match affinity { | |
| 447 | Affinity::Upstream => positions.next(), | |
| 448 | Affinity::Downstream => positions.next_back(), | |
| 449 | } | |
| 450 | .ok_or(EditError::InvalidRange) | |
| 451 | } | |
| 452 | ||
| 453 | /// A paragraph's text runs in paragraph coordinates, each with its character boundaries' | |
| 454 | /// source offsets; `breaks` ends the last run with the paragraph break. | |
| 455 | fn runs_of(shaped: &ParagraphLayout, breaks: bool) -> Result<Vec<(Node, Vec<u32>)>, EditError> { | |
| 456 | let mut runs = Vec::new(); | |
| 457 | let layout = &shaped.text; | |
| 458 | let projection = &shaped.projection; | |
| 459 | let text = projection.text().text(); | |
| 460 | let boundaries: Vec<_> = projection.source_boundaries().collect(); | |
| 461 | let source_offset = |byte| -> Result<u32, EditError> { | |
| 462 | let index = boundaries | |
| 463 | .binary_search_by_key(&byte, |(byte, _)| *byte) | |
| 464 | .map_err(|_| EditError::InvalidRange)?; | |
| 465 | Ok(boundaries[index].1) | |
| 466 | }; | |
| 467 | for (line, bounds) in layout.lines() { | |
| 468 | let line_start = runs.len(); | |
| 469 | let mut x = line.metrics().offset; | |
| 470 | let mut logical_runs = Vec::new(); | |
| 471 | for run in line.runs() { | |
| 472 | let advance = run.advance(); | |
| 473 | logical_runs.push((run, x)); | |
| 474 | x += advance; | |
| 475 | } | |
| 476 | logical_runs.sort_by_key(|(run, _)| run.text_range().start); | |
| 477 | for (run, x) in logical_runs { | |
| 478 | // AccessKit stores each character's byte length in a u8. | |
| 479 | let mut characters = Vec::new(); | |
| 480 | for cluster in run.clusters() { | |
| 481 | let range = cluster.text_range(); | |
| 482 | if range.is_empty() || range.start == text.len() { | |
| 483 | continue; | |
| 484 | } | |
| 485 | let cluster_text = text.get(range.clone()).ok_or(EditError::InvalidRange)?; | |
| 486 | if range.len() <= u8::MAX as usize { | |
| 487 | characters.push((range, cluster.advance(), cluster.is_word_boundary())); | |
| 488 | } else { | |
| 489 | for (index, (offset, ch)) in cluster_text.char_indices().enumerate() { | |
| 490 | characters.push(( | |
| 491 | range.start + offset..range.start + offset + ch.len_utf8(), | |
| 492 | if index == 0 { cluster.advance() } else { 0.0 }, | |
| 493 | index == 0 && cluster.is_word_boundary(), | |
| 494 | )); | |
| 495 | } | |
| 496 | } | |
| 497 | } | |
| 498 | let mut advance = 0.0; | |
| 499 | for chunk in characters.chunks(256) { | |
| 500 | let width: f32 = chunk.iter().map(|(_, width, _)| width).sum(); | |
| 501 | let left = x + if run.is_rtl() { | |
| 502 | run.advance() - advance - width | |
| 503 | } else { | |
| 504 | advance | |
| 505 | }; | |
| 506 | let mut node = Node::new(Role::TextRun); | |
| 507 | node.set_bounds(Rect::new( | |
| 508 | f64::from(left), | |
| 509 | f64::from(bounds.top), | |
| 510 | f64::from(left + width), | |
| 511 | f64::from(bounds.top + bounds.height), | |
| 512 | )); | |
| 513 | node.set_text_direction(if run.is_rtl() { | |
| 514 | TextDirection::RightToLeft | |
| 515 | } else { | |
| 516 | TextDirection::LeftToRight | |
| 517 | }); | |
| 518 | node.set_font_size(run.font_size()); | |
| 519 | let start = chunk[0].0.start; | |
| 520 | let end = chunk.last().unwrap().0.end; | |
| 521 | node.set_value(text.get(start..end).ok_or(EditError::InvalidRange)?); | |
| 522 | node.set_character_lengths( | |
| 523 | chunk | |
| 524 | .iter() | |
| 525 | .map(|(range, _, _)| u8::try_from(range.len()).unwrap()) | |
| 526 | .collect::<Vec<_>>(), | |
| 527 | ); | |
| 528 | let mut offset = 0.0; | |
| 529 | node.set_character_positions( | |
| 530 | chunk | |
| 531 | .iter() | |
| 532 | .map(|(_, width, _)| { | |
| 533 | let position = offset; | |
| 534 | offset += width; | |
| 535 | position | |
| 536 | }) | |
| 537 | .collect::<Vec<_>>(), | |
| 538 | ); | |
| 539 | node.set_character_widths( | |
| 540 | chunk.iter().map(|(_, width, _)| *width).collect::<Vec<_>>(), | |
| 541 | ); | |
| 542 | node.set_word_starts( | |
| 543 | chunk | |
| 544 | .iter() | |
| 545 | .enumerate() | |
| 546 | .filter_map(|(index, (_, _, word))| word.then_some(index as u8)) | |
| 547 | .collect::<Vec<_>>(), | |
| 548 | ); | |
| 549 | let offsets = chunk | |
| 550 | .iter() | |
| 551 | .map(|(range, _, _)| source_offset(range.start)) | |
| 552 | .chain(std::iter::once(source_offset(end))) | |
| 553 | .collect::<Result<Vec<_>, _>>()?; | |
| 554 | runs.push((node, offsets)); | |
| 555 | advance += width; | |
| 556 | } | |
| 557 | } | |
| 558 | // Empty lines still need a text position for selection and insertion. | |
| 559 | if runs.len() == line_start { | |
| 560 | let mut node = Node::new(Role::TextRun); | |
| 561 | node.set_value(""); | |
| 562 | node.set_character_lengths(Vec::<u8>::new()); | |
| 563 | node.set_character_positions(Vec::<f32>::new()); | |
| 564 | node.set_character_widths(Vec::<f32>::new()); | |
| 565 | node.set_text_direction(TextDirection::LeftToRight); | |
| 566 | node.set_bounds(Rect::new( | |
| 567 | 0.0, | |
| 568 | f64::from(bounds.top), | |
| 569 | 0.0, | |
| 570 | f64::from(bounds.top + bounds.height), | |
| 571 | )); | |
| 572 | runs.push((node, vec![source_offset(bounds.source.start)?])); | |
| 573 | } | |
| 574 | } | |
| 575 | if breaks { | |
| 576 | let (node, _) = runs.last_mut().unwrap(); | |
| 577 | let mut value = node.value().unwrap().to_owned(); | |
| 578 | value.push('\n'); | |
| 579 | node.set_value(value); | |
| 580 | let mut lengths = node.character_lengths().to_vec(); | |
| 581 | lengths.push(1); | |
| 582 | node.set_character_lengths(lengths); | |
| 583 | if let Some(rect) = node.bounds() { | |
| 584 | let mut positions = node.character_positions().unwrap_or_default().to_vec(); | |
| 585 | positions.push(rect.width() as f32); | |
| 586 | node.set_character_positions(positions); | |
| 587 | let mut widths = node.character_widths().unwrap_or_default().to_vec(); | |
| 588 | widths.push(4.0); | |
| 589 | node.set_character_widths(widths); | |
| 590 | } | |
| 591 | } | |
| 592 | if !shaped.tags.is_empty() { | |
| 593 | let descriptions = shaped | |
| 594 | .tags | |
| 595 | .iter() | |
| 596 | .map(|tag| { | |
| 597 | use crate::outline::TagIcon; | |
| 598 | let fallback = match tag.icon { | |
| 599 | TagIcon::CheckBox { .. } => "To do", | |
| 600 | TagIcon::Question => "Question", | |
| 601 | TagIcon::Music => "Music", | |
| 602 | TagIcon::Exclamation => "Critical", | |
| 603 | TagIcon::RedSquare => "Project A", | |
| 604 | TagIcon::YellowSquare => "Project B", | |
| 605 | TagIcon::BlueSquare => "Project C", | |
| 606 | }; | |
| 607 | let label = if tag.label.is_empty() { | |
| 608 | fallback | |
| 609 | } else { | |
| 610 | &tag.label | |
| 611 | }; | |
| 612 | let state = match tag.icon { | |
| 613 | TagIcon::CheckBox { checked: true } => ", completed", | |
| 614 | TagIcon::CheckBox { checked: false } => ", incomplete", | |
| 615 | _ => "", | |
| 616 | }; | |
| 617 | format!( | |
| 618 | "{label}{state}{}", | |
| 619 | if tag.disabled { ", disabled" } else { "" } | |
| 620 | ) | |
| 621 | }) | |
| 622 | .collect::<Vec<_>>() | |
| 623 | .join("; "); | |
| 624 | runs[0].0.set_description(descriptions); | |
| 625 | } | |
| 626 | Ok(runs) | |
| 627 | } | |
| 628 | ||
| 588 | 629 | #[cfg(test)] |
| 589 | mod tests { | |
| 630 | pub(super) mod tests { | |
| 590 | 631 | use super::*; |
| 591 | use crate::{document::TextDocument, layout::TextEngine}; | |
| 632 | use crate::{document::TextDocument, editor::TextOutline, layout::TextEngine}; | |
| 633 | use accesskit_consumer::{NodeRef, Tree, TreeState}; | |
| 592 | 634 | use onestore::document::Format; |
| 593 | 635 | use onestore::page::text::Paragraph; |
| 594 | 636 | |
| 637 | /// Applies `update` to the platform's copy of the tree, as an adapter does. | |
| 638 | pub(in crate::interaction) fn apply(tree: &mut Option<Tree>, update: TreeUpdate) -> &TreeState { | |
| 639 | struct Ignore; | |
| 640 | impl accesskit_consumer::TreeChangeHandler for Ignore { | |
| 641 | fn node_added(&mut self, _: &NodeRef) {} | |
| 642 | fn node_updated(&mut self, _: &NodeRef, _: &NodeRef) {} | |
| 643 | fn focus_moved(&mut self, _: Option<&NodeRef>, _: Option<&NodeRef>) {} | |
| 644 | fn node_removed(&mut self, _: &NodeRef) {} | |
| 645 | } | |
| 646 | match tree { | |
| 647 | Some(tree) => tree.update_and_process_changes(update, &mut Ignore), | |
| 648 | None => *tree = Some(Tree::new(update, true)), | |
| 649 | } | |
| 650 | tree.as_ref().unwrap().state() | |
| 651 | } | |
| 652 | ||
| 653 | /// Every node of `state`, depth first. | |
| 654 | pub(in crate::interaction) fn nodes(state: &TreeState) -> Vec<NodeRef<'_>> { | |
| 655 | let mut nodes = Vec::new(); | |
| 656 | let mut pending = vec![state.root()]; | |
| 657 | while let Some(node) = pending.pop() { | |
| 658 | pending.extend(node.children().rev()); | |
| 659 | nodes.push(node); | |
| 660 | } | |
| 661 | nodes | |
| 662 | } | |
| 663 | ||
| 664 | /// `state`'s nodes and focus with IDs renumbered depth first, to compare trees whose IDs differ. | |
| 665 | fn canonical(state: &TreeState) -> (Vec<Node>, NodeId) { | |
| 666 | let nodes = nodes(state); | |
| 667 | let order: HashMap<_, _> = nodes | |
| 668 | .iter() | |
| 669 | .enumerate() | |
| 670 | .map(|(index, node)| (node.locate().0, NodeId(index as u64))) | |
| 671 | .collect(); | |
| 672 | let id = |id: NodeId| order[&id]; | |
| 673 | let canonical = nodes | |
| 674 | .iter() | |
| 675 | .map(|node| { | |
| 676 | let mut data = node.data().clone(); | |
| 677 | data.set_children( | |
| 678 | data.children() | |
| 679 | .iter() | |
| 680 | .map(|child| id(*child)) | |
| 681 | .collect::<Vec<_>>(), | |
| 682 | ); | |
| 683 | if let Some(next) = data.next_on_line() { | |
| 684 | data.set_next_on_line(id(next)); | |
| 685 | } | |
| 686 | if let Some(previous) = data.previous_on_line() { | |
| 687 | data.set_previous_on_line(id(previous)); | |
| 688 | } | |
| 689 | if let Some(&TextSelection { anchor, focus }) = data.text_selection() { | |
| 690 | data.set_text_selection(TextSelection { | |
| 691 | anchor: accesskit::TextPosition { | |
| 692 | node: id(anchor.node), | |
| 693 | ..anchor | |
| 694 | }, | |
| 695 | focus: accesskit::TextPosition { | |
| 696 | node: id(focus.node), | |
| 697 | ..focus | |
| 698 | }, | |
| 699 | }); | |
| 700 | } | |
| 701 | data | |
| 702 | }) | |
| 703 | .collect(); | |
| 704 | (canonical, id(state.focus_in_tree().locate().0)) | |
| 705 | } | |
| 706 | ||
| 595 | 707 | #[test] |
| 596 | 708 | fn title_flow_updates_read_only_hit_and_accessibility_bounds() { |
| 597 | 709 | use onestore::page::{Page, PageObject, Title, Unsupported}; |
| ... | ... | @@ -649,6 +761,7 @@ mod tests { |
| 649 | 761 | scale: 2.0, |
| 650 | 762 | }; |
| 651 | 763 | let mut access = Accessibility::default(); |
| 764 | let mut tree = None; | |
| 652 | 765 | let mut identity = None; |
| 653 | 766 | for phase in 0..4 { |
| 654 | 767 | match phase { |
| ... | ... | @@ -664,17 +777,18 @@ mod tests { |
| 664 | 777 | } |
| 665 | 778 | _ => {} |
| 666 | 779 | } |
| 667 | let mut update = access.update(&editor, viewport, "Test", None).unwrap(); | |
| 668 | access.append_page_fields(&mut update, Some(&scene), &editor, viewport, Some(0)); | |
| 669 | let (id, node) = update | |
| 670 | .nodes | |
| 671 | .iter() | |
| 672 | .find(|(_, node)| node.role() == Role::Label) | |
| 780 | let update = access | |
| 781 | .update(&editor, Some(&scene), viewport, "Test", None, Some(0)) | |
| 673 | 782 | .unwrap(); |
| 783 | let node = nodes(apply(&mut tree, update)) | |
| 784 | .into_iter() | |
| 785 | .find(|node| node.role() == Role::Label) | |
| 786 | .unwrap(); | |
| 787 | let id = node.locate().0; | |
| 674 | 788 | if let Some(identity) = identity { |
| 675 | assert_eq!(*id, identity); | |
| 789 | assert_eq!(id, identity); | |
| 676 | 790 | } |
| 677 | identity = Some(*id); | |
| 791 | identity = Some(id); | |
| 678 | 792 | let y = if phase % 2 == 0 { 80.0 } else { 145.92 }; |
| 679 | 793 | let expected = Rect::new( |
| 680 | 794 | 310.0, |
| ... | ... | @@ -682,7 +796,7 @@ mod tests { |
| 682 | 796 | 710.0, |
| 683 | 797 | f64::from((y + 100.0) * 2.0 - 20.0), |
| 684 | 798 | ); |
| 685 | let bounds = node.bounds().unwrap(); | |
| 799 | let bounds = node.bounding_box().unwrap(); | |
| 686 | 800 | assert_eq!(bounds.x0, expected.x0); |
| 687 | 801 | assert_eq!(bounds.x1, expected.x1); |
| 688 | 802 | assert!((bounds.y0 - expected.y0).abs() < 0.001); |
| ... | ... | @@ -726,6 +840,7 @@ mod tests { |
| 726 | 840 | let (scene, mut editor) = PageScene::from_page(page, &mut engine).unwrap(); |
| 727 | 841 | let scene = (scene, [30.0, 40.0]); |
| 728 | 842 | let mut access = Accessibility::default(); |
| 843 | let mut tree = None; | |
| 729 | 844 | let mut identity = None; |
| 730 | 845 | for scale in [1.0, 2.0, 0.5] { |
| 731 | 846 | editor.insert(&mut engine, "annotation ").unwrap(); |
| ... | ... | @@ -734,29 +849,31 @@ mod tests { |
| 734 | 849 | origin: [-200.0, 10.0], |
| 735 | 850 | scale, |
| 736 | 851 | }; |
| 737 | let mut update = access.update(&editor, viewport, "Test", None).unwrap(); | |
| 738 | access.append_page_fields(&mut update, Some(&scene), &editor, viewport, None); | |
| 739 | let (id, node) = update | |
| 740 | .nodes | |
| 741 | .iter() | |
| 742 | .find(|(_, node)| node.role() == Role::Label) | |
| 852 | let update = access | |
| 853 | .update(&editor, Some(&scene), viewport, "Test", None, None) | |
| 743 | 854 | .unwrap(); |
| 855 | let state = apply(&mut tree, update); | |
| 856 | let node = nodes(state) | |
| 857 | .into_iter() | |
| 858 | .find(|node| node.role() == Role::Label) | |
| 859 | .unwrap(); | |
| 860 | let id = node.locate().0; | |
| 744 | 861 | if let Some(identity) = identity { |
| 745 | assert_eq!(*id, identity); | |
| 862 | assert_eq!(id, identity); | |
| 746 | 863 | } |
| 747 | identity = Some(*id); | |
| 748 | assert_eq!(node.value(), Some("Unsupported content\nRead-only")); | |
| 864 | identity = Some(id); | |
| 865 | assert_eq!(node.data().value(), Some("Unsupported content\nRead-only")); | |
| 749 | 866 | assert!(node.is_read_only()); |
| 750 | assert!(node.supports_action(Action::Focus)); | |
| 867 | assert!(node.data().supports_action(Action::Focus)); | |
| 751 | 868 | for action in [ |
| 752 | 869 | Action::SetValue, |
| 753 | 870 | Action::ReplaceSelectedText, |
| 754 | 871 | Action::SetTextSelection, |
| 755 | 872 | ] { |
| 756 | assert!(!node.supports_action(action)); | |
| 873 | assert!(!node.data().supports_action(action)); | |
| 757 | 874 | } |
| 758 | 875 | assert_eq!( |
| 759 | node.bounds(), | |
| 876 | node.bounding_box(), | |
| 760 | 877 | Some(Rect::new( |
| 761 | 878 | f64::from(40.0 * scale - 200.0), |
| 762 | 879 | f64::from(60.0 * scale + 10.0), |
| ... | ... | @@ -764,19 +881,22 @@ mod tests { |
| 764 | 881 | f64::from(120.0 * scale + 10.0) |
| 765 | 882 | )) |
| 766 | 883 | ); |
| 767 | assert_eq!(access.outline_for_node(*id), None); | |
| 768 | let tree = accesskit_consumer::Tree::new(update, true); | |
| 884 | assert_eq!(access.outline_for_node(id), None); | |
| 769 | 885 | assert_eq!( |
| 770 | tree.state().focus().unwrap().document_range().text(), | |
| 886 | state.focus().unwrap().document_range().text(), | |
| 771 | 887 | "annotation " |
| 772 | 888 | ); |
| 773 | let mut focused = access.update(&editor, viewport, "Test", None).unwrap(); | |
| 774 | access.append_page_fields(&mut focused, Some(&scene), &editor, viewport, Some(0)); | |
| 889 | let focused = access | |
| 890 | .update(&editor, Some(&scene), viewport, "Test", None, Some(0)) | |
| 891 | .unwrap(); | |
| 775 | 892 | assert_eq!(focused.focus, identity.unwrap()); |
| 776 | 893 | assert_eq!(access.read_only_for_node(focused.focus), Some(0)); |
| 777 | let tree = accesskit_consumer::Tree::new(focused, true); | |
| 778 | 894 | assert_eq!( |
| 779 | tree.state().focus().unwrap().value().as_deref(), | |
| 895 | apply(&mut tree, focused) | |
| 896 | .focus() | |
| 897 | .unwrap() | |
| 898 | .value() | |
| 899 | .as_deref(), | |
| 780 | 900 | Some("Unsupported content\nRead-only") |
| 781 | 901 | ); |
| 782 | 902 | editor.undo(&mut engine).unwrap(); |
| ... | ... | @@ -800,7 +920,7 @@ mod tests { |
| 800 | 920 | .collect(), |
| 801 | 921 | ) |
| 802 | 922 | .unwrap(); |
| 803 | let mut nodes = text.nodes().to_vec(); | |
| 923 | let mut paragraphs = text.nodes().to_vec(); | |
| 804 | 924 | let mut definitions = BTreeMap::new(); |
| 805 | 925 | for (index, (shape, label, status)) in [ |
| 806 | 926 | (3, Some("Rehearsal"), 0), |
| ... | ... | @@ -828,7 +948,7 @@ mod tests { |
| 828 | 948 | format: Format::default(), |
| 829 | 949 | }, |
| 830 | 950 | ); |
| 831 | nodes[index].text_mut().unwrap().tags.push(Tag { | |
| 951 | paragraphs[index].text_mut().unwrap().tags.push(Tag { | |
| 832 | 952 | definition: Some(id), |
| 833 | 953 | action_type: None, |
| 834 | 954 | status, |
| ... | ... | @@ -855,13 +975,14 @@ mod tests { |
| 855 | 975 | ..Layout::default() |
| 856 | 976 | }, |
| 857 | 977 | indents: vec![18.0, 0.0], |
| 858 | paragraphs: nodes, | |
| 978 | paragraphs, | |
| 859 | 979 | unsupported: Vec::new(), |
| 860 | 980 | }], |
| 861 | 981 | definitions, |
| 862 | 982 | ) |
| 863 | 983 | .unwrap(); |
| 864 | 984 | let mut access = Accessibility::default(); |
| 985 | let mut tree = None; | |
| 865 | 986 | let viewport = Viewport { |
| 866 | 987 | size: [800, 600], |
| 867 | 988 | origin: [48.0; 2], |
| ... | ... | @@ -874,12 +995,14 @@ mod tests { |
| 874 | 995 | if step == 3 { |
| 875 | 996 | editor.undo(&mut engine).unwrap(); |
| 876 | 997 | } |
| 877 | let update = access.update(&editor, viewport, "Test", None).unwrap(); | |
| 998 | let update = access | |
| 999 | .update(&editor, None, viewport, "Test", None, None) | |
| 1000 | .unwrap(); | |
| 1001 | let state = apply(&mut tree, update); | |
| 878 | 1002 | assert_eq!( |
| 879 | update | |
| 880 | .nodes | |
| 1003 | nodes(state) | |
| 881 | 1004 | .iter() |
| 882 | .filter_map(|(_, n)| n.description()) | |
| 1005 | .filter_map(|node| node.data().description()) | |
| 883 | 1006 | .collect::<Vec<_>>(), |
| 884 | 1007 | [ |
| 885 | 1008 | "Rehearsal, incomplete", |
| ... | ... | @@ -888,8 +1011,7 @@ mod tests { |
| 888 | 1011 | "Music, disabled" |
| 889 | 1012 | ] |
| 890 | 1013 | ); |
| 891 | let tree = accesskit_consumer::Tree::new(update, true); | |
| 892 | let text = tree.state().focus().unwrap().document_range().text(); | |
| 1014 | let text = state.focus().unwrap().document_range().text(); | |
| 893 | 1015 | assert_eq!( |
| 894 | 1016 | text, |
| 895 | 1017 | if step == 2 { |
| ... | ... | @@ -929,24 +1051,27 @@ mod tests { |
| 929 | 1051 | CanvasEditor::new(&mut engine, TextDocument::from_nodes(nodes).unwrap(), 180.0) |
| 930 | 1052 | .unwrap(); |
| 931 | 1053 | let mut access = Accessibility::default(); |
| 1054 | let mut tree = None; | |
| 932 | 1055 | let viewport = Viewport { |
| 933 | 1056 | size: [900, 700], |
| 934 | 1057 | scale: 2.0, |
| 935 | 1058 | origin: [13.0, 19.0], |
| 936 | 1059 | }; |
| 937 | let update = access.update(&editor, viewport, "Test", None).unwrap(); | |
| 938 | let tree = accesskit_consumer::Tree::new(update, true); | |
| 939 | let field = tree.state().focus().unwrap(); | |
| 1060 | let update = access | |
| 1061 | .update(&editor, None, viewport, "Test", None, None) | |
| 1062 | .unwrap(); | |
| 1063 | let field = apply(&mut tree, update).focus().unwrap(); | |
| 940 | 1064 | assert_eq!( |
| 941 | 1065 | field.document_range().text(), |
| 942 | 1066 | "root\nfolded\nlast\nfolded tail" |
| 943 | 1067 | ); |
| 944 | assert!( | |
| 945 | access | |
| 946 | .runs | |
| 1068 | assert_eq!( | |
| 1069 | access.fields[0] | |
| 1070 | .paragraphs | |
| 947 | 1071 | .iter() |
| 948 | .flat_map(|run| &run.positions) | |
| 949 | .all(|position| ![2, 5].contains(&position.paragraph)) | |
| 1072 | .map(|paragraph| paragraph.source) | |
| 1073 | .collect::<Vec<_>>(), | |
| 1074 | [0, 1, 3, 4] | |
| 950 | 1075 | ); |
| 951 | 1076 | let source = access |
| 952 | 1077 | .selection( |
| ... | ... | @@ -969,10 +1094,11 @@ mod tests { |
| 969 | 1094 | } |
| 970 | 1095 | ); |
| 971 | 1096 | editor.select(source).unwrap(); |
| 972 | let update = access.update(&editor, viewport, "Test", None).unwrap(); | |
| 973 | let tree = accesskit_consumer::Tree::new(update, true); | |
| 1097 | let update = access | |
| 1098 | .update(&editor, None, viewport, "Test", None, None) | |
| 1099 | .unwrap(); | |
| 974 | 1100 | assert_eq!( |
| 975 | tree.state() | |
| 1101 | apply(&mut tree, update) | |
| 976 | 1102 | .focus() |
| 977 | 1103 | .unwrap() |
| 978 | 1104 | .text_selection() |
| ... | ... | @@ -989,23 +1115,20 @@ mod tests { |
| 989 | 1115 | .into(), |
| 990 | 1116 | ) |
| 991 | 1117 | .unwrap(); |
| 992 | access.update(&editor, viewport, "Test", None).unwrap(); | |
| 1118 | let update = access | |
| 1119 | .update(&editor, None, viewport, "Test", None, None) | |
| 1120 | .unwrap(); | |
| 993 | 1121 | let caret = editor.caret(1.0).unwrap(); |
| 994 | 1122 | assert_eq!(caret.x0, 27.0); |
| 995 | let position = access | |
| 996 | .position( | |
| 997 | editor.active_outline(), | |
| 998 | editor.selection().positions[1], | |
| 999 | Affinity::Downstream, | |
| 1000 | ) | |
| 1001 | .unwrap(); | |
| 1002 | let run = access | |
| 1003 | .runs | |
| 1004 | .iter() | |
| 1005 | .find(|run| run.id == position.node) | |
| 1006 | .unwrap(); | |
| 1007 | assert_eq!(run.node.bounds().unwrap().x0, 27.0); | |
| 1008 | assert_eq!(run.node.bounds().unwrap().y0, caret.y0); | |
| 1123 | let rects = apply(&mut tree, update) | |
| 1124 | .focus() | |
| 1125 | .unwrap() | |
| 1126 | .text_selection() | |
| 1127 | .unwrap() | |
| 1128 | .bounding_boxes(); | |
| 1129 | assert_eq!(rects.len(), 1); | |
| 1130 | assert_eq!(rects[0].x0, 27.0 * 2.0 + 13.0); | |
| 1131 | assert_eq!(rects[0].y0, caret.y0 * 2.0 + 19.0); | |
| 1009 | 1132 | } |
| 1010 | 1133 | |
| 1011 | 1134 | #[test] |
| ... | ... | @@ -1043,12 +1166,15 @@ mod tests { |
| 1043 | 1166 | .unwrap(); |
| 1044 | 1167 | let mut editor = CanvasEditor::new(&mut engine, document, width).unwrap(); |
| 1045 | 1168 | let mut access = Accessibility::default(); |
| 1169 | let mut tree = None; | |
| 1046 | 1170 | let viewport = Viewport { |
| 1047 | 1171 | size: [800, 600], |
| 1048 | 1172 | origin: [48.0; 2], |
| 1049 | 1173 | scale: 2.0, |
| 1050 | 1174 | }; |
| 1051 | let update = access.update(&editor, viewport, "Test", None).unwrap(); | |
| 1175 | let update = access | |
| 1176 | .update(&editor, None, viewport, "Test", None, None) | |
| 1177 | .unwrap(); | |
| 1052 | 1178 | for (_, node) in &update.nodes { |
| 1053 | 1179 | if node.role() == Role::TextRun { |
| 1054 | 1180 | assert_eq!( |
| ... | ... | @@ -1066,18 +1192,18 @@ mod tests { |
| 1066 | 1192 | } |
| 1067 | 1193 | } |
| 1068 | 1194 | } |
| 1069 | let tree = accesskit_consumer::Tree::new(update, true); | |
| 1070 | let field = tree.state().focus().unwrap(); | |
| 1195 | let field = apply(&mut tree, update).focus().unwrap(); | |
| 1071 | 1196 | assert_eq!(field.document_range().text(), text, "width {width}"); |
| 1072 | 1197 | let selection = field.document_range().to_text_selection(); |
| 1073 | 1198 | let source_selection = access |
| 1074 | 1199 | .selection(editor.active_outline().id, selection) |
| 1075 | 1200 | .unwrap(); |
| 1076 | 1201 | editor.select(source_selection).unwrap(); |
| 1077 | let selected = access.update(&editor, viewport, "Test", None).unwrap(); | |
| 1078 | let tree = accesskit_consumer::Tree::new(selected, true); | |
| 1202 | let selected = access | |
| 1203 | .update(&editor, None, viewport, "Test", None, None) | |
| 1204 | .unwrap(); | |
| 1079 | 1205 | assert_eq!( |
| 1080 | tree.state() | |
| 1206 | apply(&mut tree, selected) | |
| 1081 | 1207 | .focus() |
| 1082 | 1208 | .unwrap() |
| 1083 | 1209 | .text_selection() |
| ... | ... | @@ -1085,48 +1211,57 @@ mod tests { |
| 1085 | 1211 | .text(), |
| 1086 | 1212 | text |
| 1087 | 1213 | ); |
| 1088 | let stable_ids: Vec<_> = access.runs.iter().map(|run| run.id).collect(); | |
| 1089 | access | |
| 1214 | let zoomed = access | |
| 1090 | 1215 | .update( |
| 1091 | 1216 | &editor, |
| 1217 | None, | |
| 1092 | 1218 | Viewport { |
| 1093 | 1219 | scale: 3.0, |
| 1094 | 1220 | ..viewport |
| 1095 | 1221 | }, |
| 1096 | 1222 | "Test", |
| 1097 | 1223 | None, |
| 1224 | None, | |
| 1098 | 1225 | ) |
| 1099 | 1226 | .unwrap(); |
| 1100 | 1227 | assert_eq!( |
| 1101 | stable_ids, | |
| 1102 | access.runs.iter().map(|run| run.id).collect::<Vec<_>>() | |
| 1228 | zoomed.nodes.iter().map(|(id, _)| *id).collect::<Vec<_>>(), | |
| 1229 | [ROOT, PAGE] | |
| 1103 | 1230 | ); |
| 1231 | apply(&mut tree, zoomed); | |
| 1104 | 1232 | access.deactivate(); |
| 1105 | access.update(&editor, viewport, "Test", None).unwrap(); | |
| 1233 | let update = access | |
| 1234 | .update(&editor, None, viewport, "Test", None, None) | |
| 1235 | .unwrap(); | |
| 1236 | let state = apply(&mut tree, update); | |
| 1106 | 1237 | assert!( |
| 1107 | 1238 | access |
| 1108 | 1239 | .selection(editor.active_outline().id, selection) |
| 1109 | 1240 | .is_err() |
| 1110 | 1241 | ); |
| 1111 | let selection = access | |
| 1112 | .update(&editor, viewport, "Test", None) | |
| 1113 | .unwrap() | |
| 1114 | .nodes[1] | |
| 1115 | .1 | |
| 1116 | .text_selection() | |
| 1117 | .copied() | |
| 1118 | .unwrap(); | |
| 1242 | let selection = *state.focus().unwrap().data().text_selection().unwrap(); | |
| 1119 | 1243 | editor.insert(&mut engine, "replacement").unwrap(); |
| 1120 | access.update(&editor, viewport, "Test", None).unwrap(); | |
| 1244 | let update = access | |
| 1245 | .update(&editor, None, viewport, "Test", None, None) | |
| 1246 | .unwrap(); | |
| 1247 | apply(&mut tree, update); | |
| 1121 | 1248 | assert!( |
| 1122 | 1249 | access |
| 1123 | 1250 | .selection(editor.active_outline().id, selection) |
| 1124 | 1251 | .is_err() |
| 1125 | 1252 | ); |
| 1126 | 1253 | editor.undo(&mut engine).unwrap(); |
| 1127 | let update = access.update(&editor, viewport, "Test", None).unwrap(); | |
| 1128 | let tree = accesskit_consumer::Tree::new(update, true); | |
| 1129 | assert_eq!(tree.state().focus().unwrap().document_range().text(), text); | |
| 1254 | let update = access | |
| 1255 | .update(&editor, None, viewport, "Test", None, None) | |
| 1256 | .unwrap(); | |
| 1257 | assert_eq!( | |
| 1258 | apply(&mut tree, update) | |
| 1259 | .focus() | |
| 1260 | .unwrap() | |
| 1261 | .document_range() | |
| 1262 | .text(), | |
| 1263 | text | |
| 1264 | ); | |
| 1130 | 1265 | } |
| 1131 | 1266 | } |
| 1132 | 1267 | } |
| ... | ... | @@ -1166,6 +1301,7 @@ mod tests { |
| 1166 | 1301 | .collect(); |
| 1167 | 1302 | let mut editor = CanvasEditor::new(&mut engine, document, 48.0).unwrap(); |
| 1168 | 1303 | let mut access = Accessibility::default(); |
| 1304 | let mut tree = None; | |
| 1169 | 1305 | let viewport = Viewport { |
| 1170 | 1306 | size: [800, 600], |
| 1171 | 1307 | origin: [31.0, -57.0], |
| ... | ... | @@ -1181,9 +1317,10 @@ mod tests { |
| 1181 | 1317 | }) |
| 1182 | 1318 | .unwrap(); |
| 1183 | 1319 | let caret = editor.caret(0.0).unwrap(); |
| 1184 | let update = access.update(&editor, viewport, "Test", None).unwrap(); | |
| 1185 | let tree = accesskit_consumer::Tree::new(update, true); | |
| 1186 | let field = tree.state().focus().unwrap(); | |
| 1320 | let update = access | |
| 1321 | .update(&editor, None, viewport, "Test", None, None) | |
| 1322 | .unwrap(); | |
| 1323 | let field = apply(&mut tree, update).focus().unwrap(); | |
| 1187 | 1324 | assert_eq!( |
| 1188 | 1325 | field.document_range().text(), |
| 1189 | 1326 | "abc e\u{301} 🌳 words wrap here\n\nabc שלום def عالم end\nlast\nline\n" |
| ... | ... | @@ -1217,7 +1354,7 @@ mod tests { |
| 1217 | 1354 | } |
| 1218 | 1355 | #[test] |
| 1219 | 1356 | fn keyboard_and_accessibility_preserve_visual_caret_at_wraps() { |
| 1220 | use crate::editor::Movement; | |
| 1357 | use draw::edit::Movement; | |
| 1221 | 1358 | let mut engine = TextEngine::default(); |
| 1222 | 1359 | let document = TextDocument::new(vec![ |
| 1223 | 1360 | Paragraph::new( |
| ... | ... | @@ -1230,6 +1367,7 @@ mod tests { |
| 1230 | 1367 | .unwrap(); |
| 1231 | 1368 | let mut editor = CanvasEditor::new(&mut engine, document, 72.0).unwrap(); |
| 1232 | 1369 | let mut access = Accessibility::default(); |
| 1370 | let mut tree = None; | |
| 1233 | 1371 | let viewport = Viewport { |
| 1234 | 1372 | size: [800, 600], |
| 1235 | 1373 | origin: [0.0; 2], |
| ... | ... | @@ -1256,11 +1394,10 @@ mod tests { |
| 1256 | 1394 | ] { |
| 1257 | 1395 | editor.move_selection(&mut engine, movement, false).unwrap(); |
| 1258 | 1396 | let caret = editor.caret(0.0).unwrap(); |
| 1259 | let tree = accesskit_consumer::Tree::new( | |
| 1260 | access.update(&editor, viewport, "Test", None).unwrap(), | |
| 1261 | true, | |
| 1262 | ); | |
| 1263 | let field = tree.state().focus().unwrap(); | |
| 1397 | let update = access | |
| 1398 | .update(&editor, None, viewport, "Test", None, None) | |
| 1399 | .unwrap(); | |
| 1400 | let field = apply(&mut tree, update).focus().unwrap(); | |
| 1264 | 1401 | let selection = field.text_selection().unwrap(); |
| 1265 | 1402 | let rects = selection.bounding_boxes(); |
| 1266 | 1403 | assert_eq!(rects.len(), 1); |
| ... | ... | @@ -1303,7 +1440,7 @@ mod tests { |
| 1303 | 1440 | .unwrap(); |
| 1304 | 1441 | let mut editor = CanvasEditor::new(&mut engine, document, 72.0).unwrap(); |
| 1305 | 1442 | let mut cached = Accessibility::default(); |
| 1306 | let mut rebuilt = Accessibility::default(); | |
| 1443 | let mut tree = None; | |
| 1307 | 1444 | let mut compare = |editor: &CanvasEditor, step: usize| { |
| 1308 | 1445 | let viewport = Viewport { |
| 1309 | 1446 | size: [800, 600], |
| ... | ... | @@ -1314,28 +1451,44 @@ mod tests { |
| 1314 | 1451 | .is_multiple_of(2) |
| 1315 | 1452 | .then_some((editor.active_outline().id, [13.5, -7.25])); |
| 1316 | 1453 | for repeat in 0..2 { |
| 1317 | rebuilt.paragraphs.clear(); | |
| 1318 | let actual = cached.update(editor, viewport, "Test", preview).unwrap(); | |
| 1319 | let expected = rebuilt.update(editor, viewport, "Test", preview).unwrap(); | |
| 1320 | assert_eq!(actual.nodes, expected.nodes, "step {step}, repeat {repeat}"); | |
| 1321 | assert_eq!(actual.focus, expected.focus); | |
| 1322 | let tree = accesskit_consumer::Tree::new(actual, true); | |
| 1323 | let field = tree.state().focus().unwrap(); | |
| 1324 | let selection = field.document_range().to_text_selection(); | |
| 1325 | assert_eq!( | |
| 1326 | cached | |
| 1327 | .selection(editor.active_outline().id, selection) | |
| 1328 | .unwrap(), | |
| 1454 | let update = cached | |
| 1455 | .update(editor, None, viewport, "Test", preview, None) | |
| 1456 | .unwrap(); | |
| 1457 | let actual = apply(&mut tree, update); | |
| 1458 | let mut rebuilt = Accessibility::default(); | |
| 1459 | let expected = Tree::new( | |
| 1329 | 1460 | rebuilt |
| 1330 | .selection(editor.active_outline().id, selection) | |
| 1331 | .unwrap() | |
| 1461 | .update(editor, None, viewport, "Test", preview, None) | |
| 1462 | .unwrap(), | |
| 1463 | true, | |
| 1332 | 1464 | ); |
| 1465 | assert_eq!( | |
| 1466 | canonical(actual), | |
| 1467 | canonical(expected.state()), | |
| 1468 | "step {step}, repeat {repeat}" | |
| 1469 | ); | |
| 1470 | let [actual, expected] = | |
| 1471 | [(&cached, actual), (&rebuilt, expected.state())].map(|(access, state)| { | |
| 1472 | let field = state.focus().unwrap(); | |
| 1473 | let selection = field.document_range().to_text_selection(); | |
| 1474 | access | |
| 1475 | .selection(editor.active_outline().id, selection) | |
| 1476 | .unwrap() | |
| 1477 | }); | |
| 1478 | assert_eq!(actual, expected); | |
| 1333 | 1479 | let count = editor |
| 1334 | 1480 | .outlines() |
| 1335 | 1481 | .iter() |
| 1336 | 1482 | .map(|o| o.layouts().count()) |
| 1337 | 1483 | .sum::<usize>(); |
| 1338 | assert_eq!(cached.paragraphs.len(), count); | |
| 1484 | assert_eq!( | |
| 1485 | cached | |
| 1486 | .fields | |
| 1487 | .iter() | |
| 1488 | .map(|field| field.paragraphs.len()) | |
| 1489 | .sum::<usize>(), | |
| 1490 | count | |
| 1491 | ); | |
| 1339 | 1492 | } |
| 1340 | 1493 | }; |
| 1341 | 1494 | compare(&editor, 0); |
| ... | ... | @@ -1405,6 +1558,72 @@ mod tests { |
| 1405 | 1558 | compare(&editor, 162); |
| 1406 | 1559 | } |
| 1407 | 1560 | } |
| 1561 | #[test] | |
| 1562 | fn scrolling_sends_the_page_and_typing_sends_the_edited_paragraph() { | |
| 1563 | let mut engine = TextEngine::default(); | |
| 1564 | let document = TextDocument::new( | |
| 1565 | (0..200) | |
| 1566 | .map(|index| Paragraph::new(format!("paragraph {index}"), Format::default())) | |
| 1567 | .collect(), | |
| 1568 | ) | |
| 1569 | .unwrap(); | |
| 1570 | let mut editor = CanvasEditor::new(&mut engine, document, 480.0).unwrap(); | |
| 1571 | let mut access = Accessibility::default(); | |
| 1572 | let mut tree = None; | |
| 1573 | let mut viewport = Viewport { | |
| 1574 | size: [800, 600], | |
| 1575 | origin: [48.0; 2], | |
| 1576 | scale: 2.0, | |
| 1577 | }; | |
| 1578 | let update = access | |
| 1579 | .update(&editor, None, viewport, "Test", None, None) | |
| 1580 | .unwrap(); | |
| 1581 | apply(&mut tree, update); | |
| 1582 | let ids = |update: &TreeUpdate| update.nodes.iter().map(|(id, _)| *id).collect::<Vec<_>>(); | |
| 1583 | for (scroll, zoom) in [(-300.0, 1.0), (150.0, 1.25)] { | |
| 1584 | viewport.origin[1] += scroll; | |
| 1585 | viewport.scale *= zoom; | |
| 1586 | let update = access | |
| 1587 | .update(&editor, None, viewport, "Test", None, None) | |
| 1588 | .unwrap(); | |
| 1589 | assert_eq!(ids(&update), [ROOT, PAGE]); | |
| 1590 | apply(&mut tree, update); | |
| 1591 | } | |
| 1592 | let field = access.fields[0].id; | |
| 1593 | editor | |
| 1594 | .select( | |
| 1595 | [TextPosition { | |
| 1596 | paragraph: 150, | |
| 1597 | offset: 3, | |
| 1598 | }; 2] | |
| 1599 | .into(), | |
| 1600 | ) | |
| 1601 | .unwrap(); | |
| 1602 | let update = access | |
| 1603 | .update(&editor, None, viewport, "Test", None, None) | |
| 1604 | .unwrap(); | |
| 1605 | assert_eq!(ids(&update), [field, ROOT, PAGE]); | |
| 1606 | apply(&mut tree, update); | |
| 1607 | editor.insert(&mut engine, "x").unwrap(); | |
| 1608 | let update = access | |
| 1609 | .update(&editor, None, viewport, "Test", None, None) | |
| 1610 | .unwrap(); | |
| 1611 | let paragraph = &access.fields[0].paragraphs[150]; | |
| 1612 | assert_eq!( | |
| 1613 | ids(&update), | |
| 1614 | [paragraph.runs[0].id, paragraph.id, field, ROOT, PAGE] | |
| 1615 | ); | |
| 1616 | assert_eq!(update.nodes[0].1.value(), Some("parxagraph 150\n")); | |
| 1617 | let state = apply(&mut tree, update); | |
| 1618 | let expected = Tree::new( | |
| 1619 | Accessibility::default() | |
| 1620 | .update(&editor, None, viewport, "Test", None, None) | |
| 1621 | .unwrap(), | |
| 1622 | true, | |
| 1623 | ); | |
| 1624 | assert_eq!(canonical(state), canonical(expected.state())); | |
| 1625 | } | |
| 1626 | ||
| 1408 | 1627 | #[test] |
| 1409 | 1628 | fn outlines_have_independent_accessibility_identity_and_selection() { |
| 1410 | 1629 | let mut engine = TextEngine::default(); |
| ... | ... | @@ -1422,7 +1641,9 @@ mod tests { |
| 1422 | 1641 | scale: 2.0, |
| 1423 | 1642 | origin: [48.0; 2], |
| 1424 | 1643 | }; |
| 1425 | let update = access.update(&editor, viewport, "Test", None).unwrap(); | |
| 1644 | let update = access | |
| 1645 | .update(&editor, None, viewport, "Test", None, None) | |
| 1646 | .unwrap(); | |
| 1426 | 1647 | let fields: Vec<_> = update |
| 1427 | 1648 | .nodes |
| 1428 | 1649 | .iter() |
| ... | ... | @@ -1439,35 +1660,45 @@ mod tests { |
| 1439 | 1660 | assert_ne!(fields[0].0, fields[1].0); |
| 1440 | 1661 | assert_eq!(access.outline_for_node(fields[0].0), Some(first)); |
| 1441 | 1662 | assert_eq!(access.outline_for_node(fields[1].0), Some(second)); |
| 1442 | let tree = accesskit_consumer::Tree::new(update, true); | |
| 1443 | let field = tree.state().focus().unwrap(); | |
| 1663 | let mut tree = None; | |
| 1664 | let field = apply(&mut tree, update).focus().unwrap(); | |
| 1444 | 1665 | assert_eq!(field.document_range().text(), "annotation 🌳"); |
| 1445 | 1666 | let selection = field.document_range().to_text_selection(); |
| 1446 | 1667 | assert!(access.selection(first, selection).is_err()); |
| 1447 | 1668 | let source_selection = access.selection(second, selection).unwrap(); |
| 1448 | 1669 | editor.select(source_selection).unwrap(); |
| 1449 | 1670 | let preview = access |
| 1450 | .update(&editor, viewport, "Test", Some((second, [400.0, 50.0]))) | |
| 1671 | .update( | |
| 1672 | &editor, | |
| 1673 | None, | |
| 1674 | viewport, | |
| 1675 | "Test", | |
| 1676 | Some((second, [400.0, 50.0])), | |
| 1677 | None, | |
| 1678 | ) | |
| 1451 | 1679 | .unwrap(); |
| 1452 | let preview_tree = accesskit_consumer::Tree::new(preview, true); | |
| 1453 | let preview_field = preview_tree.state().focus().unwrap(); | |
| 1680 | let preview_field = apply(&mut tree, preview).focus().unwrap(); | |
| 1454 | 1681 | let rect = preview_field.document_range().bounding_boxes()[0]; |
| 1455 | 1682 | assert_eq!(rect.x0, 848.0); |
| 1456 | 1683 | assert_eq!(rect.y0, 148.0); |
| 1457 | 1684 | assert_eq!(editor.active_outline().origin(), [300.0, 40.0]); |
| 1458 | 1685 | editor.move_outline(second, [400.0, 50.0]).unwrap(); |
| 1459 | access.update(&editor, viewport, "Test", None).unwrap(); | |
| 1686 | let update = access | |
| 1687 | .update(&editor, None, viewport, "Test", None, None) | |
| 1688 | .unwrap(); | |
| 1689 | apply(&mut tree, update); | |
| 1460 | 1690 | assert_eq!(access.outline_for_node(fields[1].0), Some(second)); |
| 1461 | 1691 | assert!(access.selection(second, selection).is_ok()); |
| 1462 | 1692 | editor.undo(&mut engine).unwrap(); |
| 1463 | 1693 | editor.undo(&mut engine).unwrap(); |
| 1464 | 1694 | editor.undo(&mut engine).unwrap(); |
| 1465 | let update = access.update(&editor, viewport, "Test", None).unwrap(); | |
| 1695 | let update = access | |
| 1696 | .update(&editor, None, viewport, "Test", None, None) | |
| 1697 | .unwrap(); | |
| 1466 | 1698 | assert_eq!( |
| 1467 | update | |
| 1468 | .nodes | |
| 1699 | nodes(apply(&mut tree, update)) | |
| 1469 | 1700 | .iter() |
| 1470 | .filter(|(_, node)| node.role() == Role::MultilineTextInput) | |
| 1701 | .filter(|node| node.role() == Role::MultilineTextInput) | |
| 1471 | 1702 | .count(), |
| 1472 | 1703 | 1 |
| 1473 | 1704 | ); |
crates/canvas/src/interaction/mod.rs+109-183| ... | ... | @@ -11,15 +11,16 @@ pub use scroll::Scroll; |
| 11 | 11 | #[cfg(test)] |
| 12 | 12 | mod tests; |
| 13 | 13 | |
| 14 | use crate::gpu::{Viewport, page::PageScene}; | |
| 14 | use crate::gpu::{Paper, Viewport, page::PageScene}; | |
| 15 | 15 | use crate::{ |
| 16 | 16 | date::DateField, |
| 17 | editor::{ | |
| 18 | CanvasEditor, DEFAULT_OUTLINE_WIDTH, Movement, Selection, SelectionUnit, TextOutline, | |
| 19 | }, | |
| 17 | editor::{CanvasEditor, DEFAULT_OUTLINE_WIDTH, Selection, TextOutline}, | |
| 20 | 18 | layout::TextEngine, |
| 21 | 19 | }; |
| 22 | use draw::{Primitive, Stroke}; | |
| 20 | use draw::{ | |
| 21 | Primitive, Stroke, | |
| 22 | edit::{self, Clicks, Command, Key, Modifiers, Movement, NamedKey, SelectionUnit}, | |
| 23 | }; | |
| 23 | 24 | use std::{ |
| 24 | 25 | error::Error, |
| 25 | 26 | time::{Duration, Instant}, |
| ... | ... | @@ -31,39 +32,6 @@ pub const DATE_LABELS: [&str; 2] = ["Page date", "Page time"]; |
| 31 | 32 | |
| 32 | 33 | pub type Result<T> = std::result::Result<T, Box<dyn Error>>; |
| 33 | 34 | |
| 34 | #[derive(Clone, Debug, PartialEq)] | |
| 35 | pub enum Key { | |
| 36 | Named(NamedKey), | |
| 37 | Character(String), | |
| 38 | } | |
| 39 | ||
| 40 | #[derive(Clone, Copy, Debug, PartialEq)] | |
| 41 | pub enum NamedKey { | |
| 42 | Escape, | |
| 43 | Tab, | |
| 44 | Space, | |
| 45 | Enter, | |
| 46 | Backspace, | |
| 47 | Delete, | |
| 48 | ArrowLeft, | |
| 49 | ArrowRight, | |
| 50 | ArrowUp, | |
| 51 | ArrowDown, | |
| 52 | Home, | |
| 53 | End, | |
| 54 | /// Shift, Control, Option or Command pressed alone. | |
| 55 | Modifier, | |
| 56 | Other, | |
| 57 | } | |
| 58 | ||
| 59 | #[derive(Clone, Copy, Debug, Default, PartialEq)] | |
| 60 | pub struct Modifiers { | |
| 61 | pub shift: bool, | |
| 62 | pub control: bool, | |
| 63 | pub option: bool, | |
| 64 | pub command: bool, | |
| 65 | } | |
| 66 | ||
| 67 | 35 | #[derive(Clone, Copy, Debug, PartialEq)] |
| 68 | 36 | pub enum Cursor { |
| 69 | 37 | Default, |
| ... | ... | @@ -89,12 +57,14 @@ pub enum Request { |
| 89 | 57 | CharacterPalette, |
| 90 | 58 | } |
| 91 | 59 | |
| 92 | /// What an event did: `changed` means the page, selection or view changed (the host saves, | |
| 93 | /// updates accessibility and redraws); `redraw` alone repaints hover feedback. | |
| 60 | /// What an event did: `changed` means the page or selection changed (the host saves, | |
| 61 | /// updates accessibility and redraws); `moved` that only the view scrolled or zoomed (the | |
| 62 | /// host updates accessibility and redraws); `redraw` alone repaints hover feedback. | |
| 94 | 63 | #[must_use] |
| 95 | 64 | #[derive(Debug, Default, PartialEq)] |
| 96 | 65 | pub struct Response { |
| 97 | 66 | pub changed: bool, |
| 67 | pub moved: bool, | |
| 98 | 68 | pub redraw: bool, |
| 99 | 69 | pub request: Option<Request>, |
| 100 | 70 | } |
| ... | ... | @@ -104,7 +74,7 @@ impl Response { |
| 104 | 74 | Self { |
| 105 | 75 | changed: true, |
| 106 | 76 | redraw: true, |
| 107 | request: None, | |
| 77 | ..Self::default() | |
| 108 | 78 | } |
| 109 | 79 | } |
| 110 | 80 | |
| ... | ... | @@ -123,11 +93,12 @@ impl Response { |
| 123 | 93 | } |
| 124 | 94 | } |
| 125 | 95 | |
| 126 | /// The platform's caret and text selection colours, linear RGBA. | |
| 96 | /// The platform's caret and text selection colours, linear RGBA, and the page's paper. | |
| 127 | 97 | #[derive(Clone, Copy, Debug)] |
| 128 | 98 | pub struct TextColors { |
| 129 | 99 | pub caret: [f32; 4], |
| 130 | 100 | pub selection: [f32; 4], |
| 101 | pub paper: Paper, | |
| 131 | 102 | } |
| 132 | 103 | |
| 133 | 104 | /// How to paint an outline: whether the caret is in its blink-on phase, the view scale, |
| ... | ... | @@ -138,6 +109,9 @@ struct Paint { |
| 138 | 109 | scale: f32, |
| 139 | 110 | pixel: f32, |
| 140 | 111 | colors: TextColors, |
| 112 | /// The document's top and bottom the view shows; paragraphs wholly outside it are | |
| 113 | /// not painted. | |
| 114 | visible: [f32; 2], | |
| 141 | 115 | } |
| 142 | 116 | |
| 143 | 117 | enum Drag { |
| ... | ... | @@ -205,10 +179,9 @@ pub struct PageView { |
| 205 | 179 | pub viewport: Viewport, |
| 206 | 180 | /// Device pixels per logical pixel. |
| 207 | 181 | display_scale: f32, |
| 208 | double_click: Duration, | |
| 182 | clicks: Clicks, | |
| 209 | 183 | pointer: [f32; 2], |
| 210 | 184 | pointer_inside: bool, |
| 211 | last_click: Option<(Instant, [f32; 2], u8)>, | |
| 212 | 185 | drag: Option<Drag>, |
| 213 | 186 | object_focus: Option<ObjectFocus>, |
| 214 | 187 | modifiers: Modifiers, |
| ... | ... | @@ -242,10 +215,9 @@ impl PageView { |
| 242 | 215 | origin: home(display_scale), |
| 243 | 216 | }, |
| 244 | 217 | display_scale, |
| 245 | double_click, | |
| 218 | clicks: Clicks::new(double_click), | |
| 246 | 219 | pointer: [0.0; 2], |
| 247 | 220 | pointer_inside: false, |
| 248 | last_click: None, | |
| 249 | 221 | drag: None, |
| 250 | 222 | object_focus: None, |
| 251 | 223 | modifiers: Modifiers::default(), |
| ... | ... | @@ -397,6 +369,15 @@ impl PageView { |
| 397 | 369 | Ok(Response::changed()) |
| 398 | 370 | } |
| 399 | 371 | |
| 372 | fn moved(&mut self) -> Result<Response> { | |
| 373 | self.scroll().clamp(&mut self.viewport); | |
| 374 | Ok(Response { | |
| 375 | moved: true, | |
| 376 | redraw: true, | |
| 377 | ..Response::default() | |
| 378 | }) | |
| 379 | } | |
| 380 | ||
| 400 | 381 | /// The caret's rectangle in device pixels, for placing the platform's input method. |
| 401 | 382 | pub fn caret_area(&self) -> Result<[f32; 4]> { |
| 402 | 383 | let mut rect = self.editor.caret(1.0)?; |
| ... | ... | @@ -474,13 +455,26 @@ impl PageView { |
| 474 | 455 | self.changed() |
| 475 | 456 | } |
| 476 | 457 | |
| 477 | fn zoom(&mut self, factor: f32) { | |
| 478 | let point = self.viewport.document_point(self.pointer); | |
| 458 | /// The page's zoom, where 1 is OneNote's 100%: 96 pixels per inch. | |
| 459 | pub fn zoom(&self) -> f32 { | |
| 460 | self.viewport.scale / (self.display_scale * 96.0 / 72.0) | |
| 461 | } | |
| 462 | ||
| 463 | /// Zooms to `zoom` about the middle of the view, within 25% to 400%. | |
| 464 | pub fn set_zoom(&mut self, zoom: f32) -> Result<Response> { | |
| 465 | let middle = self.viewport.size.map(|size| size as f32 / 2.0); | |
| 466 | self.zoom_about(zoom / self.zoom(), middle); | |
| 467 | self.moved() | |
| 468 | } | |
| 469 | ||
| 470 | /// Scales the view by `factor`, keeping the document point under `anchor` in place. | |
| 471 | fn zoom_about(&mut self, factor: f32, anchor: [f32; 2]) { | |
| 472 | let point = self.viewport.document_point(anchor); | |
| 479 | 473 | let dpr = self.display_scale; |
| 480 | 474 | self.viewport.scale = (self.viewport.scale * factor).clamp(dpr / 3.0, dpr * 16.0 / 3.0); |
| 481 | 475 | self.viewport.origin = [ |
| 482 | self.pointer[0] - point[0] * self.viewport.scale, | |
| 483 | self.pointer[1] - point[1] * self.viewport.scale, | |
| 476 | anchor[0] - point[0] * self.viewport.scale, | |
| 477 | anchor[1] - point[1] * self.viewport.scale, | |
| 484 | 478 | ]; |
| 485 | 479 | } |
| 486 | 480 | |
| ... | ... | @@ -522,7 +516,16 @@ impl PageView { |
| 522 | 516 | /// within the page's bounds. |
| 523 | 517 | pub fn scroll_to(&mut self, axis: usize, offset: f32) -> Result<Response> { |
| 524 | 518 | self.viewport.origin[axis] = -offset; |
| 525 | self.changed() | |
| 519 | self.moved() | |
| 520 | } | |
| 521 | ||
| 522 | /// Brings the page's template backgrounds to `paper` and the current zoom before a | |
| 523 | /// frame's primitives; `waker` is woken off the main thread when the view should be | |
| 524 | /// drawn again because a background raster landed. | |
| 525 | pub fn update_backgrounds(&mut self, paper: Paper, waker: &std::task::Waker) { | |
| 526 | if let Some((scene, _)) = &mut self.scene { | |
| 527 | scene.update_backgrounds(paper, self.viewport.scale, waker); | |
| 528 | } | |
| 526 | 529 | } |
| 527 | 530 | |
| 528 | 531 | /// Everything to draw this frame. |
| ... | ... | @@ -571,6 +574,8 @@ impl PageView { |
| 571 | 574 | scale: self.viewport.scale, |
| 572 | 575 | pixel: self.pixel(), |
| 573 | 576 | colors, |
| 577 | visible: [0.0, self.viewport.size[1] as f32] | |
| 578 | .map(|y| (y - self.viewport.origin[1]) / self.viewport.scale), | |
| 574 | 579 | }, |
| 575 | 580 | ) |
| 576 | 581 | } |
| ... | ... | @@ -609,7 +614,7 @@ impl PageView { |
| 609 | 614 | if size.contains(&0) { |
| 610 | 615 | return Ok(Response::default()); |
| 611 | 616 | } |
| 612 | self.changed() | |
| 617 | self.moved() | |
| 613 | 618 | } |
| 614 | 619 | |
| 615 | 620 | pub fn scale_factor_changed(&mut self, scale: f32) -> Result<Response> { |
| ... | ... | @@ -618,7 +623,7 @@ impl PageView { |
| 618 | 623 | self.viewport.origin[0] *= ratio; |
| 619 | 624 | self.viewport.origin[1] *= ratio; |
| 620 | 625 | self.display_scale = scale; |
| 621 | self.changed() | |
| 626 | self.moved() | |
| 622 | 627 | } |
| 623 | 628 | |
| 624 | 629 | pub fn focus_changed(&mut self, focused: bool) -> Result<Response> { |
| ... | ... | @@ -654,7 +659,14 @@ impl PageView { |
| 654 | 659 | let target = |
| 655 | 660 | self.editor |
| 656 | 661 | .selection_at(point[0] - origin[0], point[1] - origin[1], unit)?; |
| 657 | self.editor.select(drag_selection(anchor, target, unit))?; | |
| 662 | let pair = |selection: Selection| { | |
| 663 | [0, 1].map(|end| (selection.positions[end], selection.affinities[end])) | |
| 664 | }; | |
| 665 | let ends = edit::drag(pair(anchor), pair(target), unit, |(position, _)| position); | |
| 666 | self.editor.select(Selection { | |
| 667 | positions: ends.map(|(position, _)| position), | |
| 668 | affinities: ends.map(|(_, affinity)| affinity), | |
| 669 | })?; | |
| 658 | 670 | self.changed() |
| 659 | 671 | } |
| 660 | 672 | Some(Drag::Outline { pending_press, .. } | Drag::Image { pending_press, .. }) => { |
| ... | ... | @@ -688,16 +700,9 @@ impl PageView { |
| 688 | 700 | /// A primary-button press at the pointer's position, at `now` for click counting. |
| 689 | 701 | pub fn pointer_pressed(&mut self, now: Instant) -> Result<Response> { |
| 690 | 702 | let point = self.viewport.document_point(self.pointer); |
| 691 | let count = self | |
| 692 | .last_click | |
| 693 | .filter(|(time, point, _)| { | |
| 694 | now.duration_since(*time) <= self.double_click | |
| 695 | && (0..2).all(|axis| { | |
| 696 | (point[axis] - self.pointer[axis]).abs() <= 4.0 * self.display_scale | |
| 697 | }) | |
| 698 | }) | |
| 699 | .map_or(1, |(_, _, count)| (count % 3) + 1); | |
| 700 | self.last_click = Some((now, self.pointer, count)); | |
| 703 | let unit = self | |
| 704 | .clicks | |
| 705 | .press(now, self.pointer, 4.0 * self.display_scale); | |
| 701 | 706 | match self.hit_test(point) { |
| 702 | 707 | Some(Hit::Date(field)) => { |
| 703 | 708 | self.editor.finish_composition(); |
| ... | ... | @@ -739,11 +744,6 @@ impl PageView { |
| 739 | 744 | self.editor.focus_outline(id)?; |
| 740 | 745 | let previous = self.editor.selection(); |
| 741 | 746 | self.editor.select_below(&mut self.engine, id, point)?; |
| 742 | let unit = match count { | |
| 743 | 2 => SelectionUnit::Word, | |
| 744 | 3 => SelectionUnit::Paragraph, | |
| 745 | _ => SelectionUnit::Grapheme, | |
| 746 | }; | |
| 747 | 747 | let selection = self.editor.selection_at(point[0], point[1], unit)?; |
| 748 | 748 | let selection = if extend { |
| 749 | 749 | Selection { |
| ... | ... | @@ -802,12 +802,12 @@ impl PageView { |
| 802 | 802 | /// `delta` in device pixels; Command zooms about the pointer instead of scrolling. |
| 803 | 803 | pub fn wheel(&mut self, delta: [f32; 2]) -> Result<Response> { |
| 804 | 804 | if self.modifiers.command { |
| 805 | self.zoom((delta[1] * 0.005).exp()); | |
| 805 | self.zoom_about((delta[1] * 0.005).exp(), self.pointer); | |
| 806 | 806 | } else { |
| 807 | 807 | self.viewport.origin[0] += delta[0]; |
| 808 | 808 | self.viewport.origin[1] += delta[1]; |
| 809 | 809 | } |
| 810 | self.changed() | |
| 810 | self.moved() | |
| 811 | 811 | } |
| 812 | 812 | |
| 813 | 813 | /// Text the platform inserts outside key events, such as the character picker's; |
| ... | ... | @@ -1046,118 +1046,50 @@ impl PageView { |
| 1046 | 1046 | } |
| 1047 | 1047 | "v" => request = Some(Request::Paste), |
| 1048 | 1048 | "+" | "=" => { |
| 1049 | self.zoom(1.1); | |
| 1050 | return self.changed(); | |
| 1049 | self.zoom_about(1.1, self.pointer); | |
| 1050 | return self.moved(); | |
| 1051 | 1051 | } |
| 1052 | 1052 | "-" => { |
| 1053 | self.zoom(1.0 / 1.1); | |
| 1054 | return self.changed(); | |
| 1053 | self.zoom_about(1.0 / 1.1, self.pointer); | |
| 1054 | return self.moved(); | |
| 1055 | 1055 | } |
| 1056 | 1056 | "0" => { |
| 1057 | self.zoom((self.display_scale * 96.0 / 72.0) / self.viewport.scale); | |
| 1058 | return self.changed(); | |
| 1057 | self.zoom_about(1.0 / self.zoom(), self.pointer); | |
| 1058 | return self.moved(); | |
| 1059 | 1059 | } |
| 1060 | 1060 | _ => return Ok(Response::default()), |
| 1061 | 1061 | } |
| 1062 | 1062 | } else { |
| 1063 | let movement = match key { | |
| 1064 | Key::Named(NamedKey::ArrowLeft) => Some(if command { | |
| 1065 | Movement::LineStart | |
| 1066 | } else if option { | |
| 1067 | Movement::WordLeft | |
| 1068 | } else { | |
| 1069 | Movement::Left | |
| 1070 | }), | |
| 1071 | Key::Named(NamedKey::ArrowRight) => Some(if command { | |
| 1072 | Movement::LineEnd | |
| 1073 | } else if option { | |
| 1074 | Movement::WordRight | |
| 1075 | } else { | |
| 1076 | Movement::Right | |
| 1077 | }), | |
| 1078 | Key::Named(NamedKey::ArrowUp) => Some(if command { | |
| 1079 | Movement::DocumentStart | |
| 1080 | } else if option { | |
| 1081 | Movement::ParagraphStart | |
| 1082 | } else { | |
| 1083 | Movement::Up | |
| 1084 | }), | |
| 1085 | Key::Named(NamedKey::ArrowDown) => Some(if command { | |
| 1086 | Movement::DocumentEnd | |
| 1087 | } else if option { | |
| 1088 | Movement::ParagraphEnd | |
| 1089 | } else { | |
| 1090 | Movement::Down | |
| 1091 | }), | |
| 1092 | Key::Named(NamedKey::Home) if shift => Some(Movement::DocumentStart), | |
| 1093 | Key::Named(NamedKey::End) if shift => Some(Movement::DocumentEnd), | |
| 1094 | Key::Character(key) if control && !option => match key.as_str() { | |
| 1095 | "a" => Some(Movement::LineStart), | |
| 1096 | "e" => Some(Movement::LineEnd), | |
| 1097 | "b" => Some(Movement::Left), | |
| 1098 | "f" => Some(Movement::Right), | |
| 1099 | "p" => Some(Movement::Up), | |
| 1100 | "n" => Some(Movement::Down), | |
| 1101 | _ => None, | |
| 1102 | }, | |
| 1103 | _ => None, | |
| 1104 | }; | |
| 1105 | if let Some(movement) = movement { | |
| 1063 | let chord = Command::from_key(key, self.modifiers); | |
| 1064 | if let Some(Command::Move(movement)) = chord { | |
| 1106 | 1065 | self.editor |
| 1107 | 1066 | .move_selection(&mut self.engine, movement, shift)?; |
| 1108 | 1067 | } else if self.editor.marked_range().is_none() { |
| 1109 | match key { | |
| 1110 | Key::Named(NamedKey::Backspace) if command || option => { | |
| 1111 | self.editor.delete_to( | |
| 1112 | &mut self.engine, | |
| 1113 | if command { | |
| 1114 | Movement::LineStart | |
| 1115 | } else { | |
| 1116 | Movement::WordLeft | |
| 1117 | }, | |
| 1118 | )?; | |
| 1119 | } | |
| 1120 | Key::Named(NamedKey::Delete) if command || option => { | |
| 1121 | self.editor.delete_to( | |
| 1122 | &mut self.engine, | |
| 1123 | if command { | |
| 1124 | Movement::LineEnd | |
| 1125 | } else { | |
| 1126 | Movement::WordRight | |
| 1127 | }, | |
| 1128 | )?; | |
| 1068 | match (chord, key) { | |
| 1069 | (Some(Command::DeleteTo(movement)), _) => { | |
| 1070 | self.editor.delete_to(&mut self.engine, movement)?; | |
| 1129 | 1071 | } |
| 1130 | Key::Named(NamedKey::Backspace) => { | |
| 1131 | self.editor.delete(&mut self.engine, true)?; | |
| 1072 | (Some(Command::Delete { backward }), _) => { | |
| 1073 | self.editor.delete(&mut self.engine, backward)?; | |
| 1132 | 1074 | } |
| 1133 | Key::Named(NamedKey::Delete) => { | |
| 1134 | self.editor.delete(&mut self.engine, false)?; | |
| 1075 | (Some(Command::Kill), _) => { | |
| 1076 | if !self.editor.delete_to(&mut self.engine, Movement::LineEnd)? { | |
| 1077 | self.editor.delete(&mut self.engine, false)?; | |
| 1078 | } | |
| 1135 | 1079 | } |
| 1136 | Key::Named(end @ (NamedKey::Home | NamedKey::End)) => { | |
| 1080 | (_, Key::Named(end @ (NamedKey::Home | NamedKey::End))) => { | |
| 1137 | 1081 | let limits = self.scroll(); |
| 1138 | 1082 | self.viewport.origin[1] = -if *end == NamedKey::Home { |
| 1139 | 1083 | limits.min[1] |
| 1140 | 1084 | } else { |
| 1141 | 1085 | limits.max[1] |
| 1142 | 1086 | }; |
| 1143 | return self.changed(); | |
| 1087 | return self.moved(); | |
| 1144 | 1088 | } |
| 1145 | Key::Character(key) if control && !option => match key.as_str() { | |
| 1146 | "h" => { | |
| 1147 | self.editor.delete(&mut self.engine, true)?; | |
| 1148 | } | |
| 1149 | "d" => { | |
| 1150 | self.editor.delete(&mut self.engine, false)?; | |
| 1151 | } | |
| 1152 | "k" if !self.editor.delete_to(&mut self.engine, Movement::LineEnd)? => { | |
| 1153 | self.editor.delete(&mut self.engine, false)?; | |
| 1154 | } | |
| 1155 | _ => {} | |
| 1156 | }, | |
| 1157 | Key::Named(NamedKey::Enter) => { | |
| 1089 | (_, Key::Named(NamedKey::Enter)) => { | |
| 1158 | 1090 | self.editor.enter(&mut self.engine, shift)?; |
| 1159 | 1091 | } |
| 1160 | Key::Named(NamedKey::Tab) => { | |
| 1092 | (_, Key::Named(NamedKey::Tab)) => { | |
| 1161 | 1093 | self.editor.tab(&mut self.engine, shift)?; |
| 1162 | 1094 | } |
| 1163 | 1095 | _ if !command && !control => { |
| ... | ... | @@ -1392,6 +1324,7 @@ fn page_primitives<'a>( |
| 1392 | 1324 | outline.shaped(), |
| 1393 | 1325 | [origin[0] + offset[0], origin[1] + offset[1]], |
| 1394 | 1326 | moving, |
| 1327 | paint.colors.paper, | |
| 1395 | 1328 | primitives, |
| 1396 | 1329 | ); |
| 1397 | 1330 | } |
| ... | ... | @@ -1407,6 +1340,7 @@ fn page_primitives<'a>( |
| 1407 | 1340 | *origin, |
| 1408 | 1341 | Some(editor), |
| 1409 | 1342 | moving, |
| 1343 | paint.colors.paper, | |
| 1410 | 1344 | &draw_outline, |
| 1411 | 1345 | )?; |
| 1412 | 1346 | } |
| ... | ... | @@ -1578,22 +1512,6 @@ fn append_image_chrome(rect: [f32; 4], pixel: f32, primitives: &mut Vec<Primitiv |
| 1578 | 1512 | } |
| 1579 | 1513 | } |
| 1580 | 1514 | |
| 1581 | fn drag_selection(anchor: Selection, target: Selection, unit: SelectionUnit) -> Selection { | |
| 1582 | if unit == SelectionUnit::Grapheme { | |
| 1583 | return Selection { | |
| 1584 | positions: [anchor.positions[0], target.positions[1]], | |
| 1585 | affinities: [anchor.affinities[0], target.affinities[1]], | |
| 1586 | }; | |
| 1587 | } | |
| 1588 | let backwards = target.positions[0] < anchor.positions[0].min(anchor.positions[1]); | |
| 1589 | let start = usize::from((anchor.positions[0] > anchor.positions[1]) != backwards); | |
| 1590 | let end = usize::from((target.positions[0] > target.positions[1]) == backwards); | |
| 1591 | Selection { | |
| 1592 | positions: [anchor.positions[start], target.positions[end]], | |
| 1593 | affinities: [anchor.affinities[start], target.affinities[end]], | |
| 1594 | } | |
| 1595 | } | |
| 1596 | ||
| 1597 | 1515 | fn snap_to_grid(point: [f32; 2], margin: [f32; 2]) -> [f32; 2] { |
| 1598 | 1516 | std::array::from_fn(|axis| { |
| 1599 | 1517 | let offset = margin[axis]; |
| ... | ... | @@ -1722,11 +1640,15 @@ fn append_outline<'a>( |
| 1722 | 1640 | scale, |
| 1723 | 1641 | pixel, |
| 1724 | 1642 | colors, |
| 1643 | visible, | |
| 1725 | 1644 | } = paint; |
| 1726 | outline.shaped().append_table_primitives(primitives, origin); | |
| 1645 | let rows = [visible[0] - y, visible[1] - y]; | |
| 1727 | 1646 | outline |
| 1728 | 1647 | .shaped() |
| 1729 | .append_background_primitives(primitives, origin); | |
| 1648 | .append_table_primitives(primitives, origin, colors.paper); | |
| 1649 | outline | |
| 1650 | .shaped() | |
| 1651 | .append_background_primitives(primitives, origin, rows); | |
| 1730 | 1652 | if let Some(editor) = editor { |
| 1731 | 1653 | for rect in editor.selection_rects()? { |
| 1732 | 1654 | primitives.push(Primitive::Rect { |
| ... | ... | @@ -1740,10 +1662,14 @@ fn append_outline<'a>( |
| 1740 | 1662 | }); |
| 1741 | 1663 | } |
| 1742 | 1664 | } |
| 1743 | for (index, (_, paragraph)) in outline.layouts().enumerate() { | |
| 1744 | outline | |
| 1745 | .shaped() | |
| 1746 | .append_paragraph_primitives(index, paragraph, origin, primitives); | |
| 1665 | for (index, paragraph) in outline.shaped().visible(rows) { | |
| 1666 | outline.shaped().append_paragraph_primitives( | |
| 1667 | index, | |
| 1668 | paragraph, | |
| 1669 | origin, | |
| 1670 | colors.paper.ink, | |
| 1671 | primitives, | |
| 1672 | ); | |
| 1747 | 1673 | } |
| 1748 | 1674 | if let Some(editor) = editor { |
| 1749 | 1675 | for rect in editor.marked_rects()? { |
| ... | ... | @@ -1754,7 +1680,7 @@ fn append_outline<'a>( |
| 1754 | 1680 | rect.x1 as f32 + x, |
| 1755 | 1681 | rect.y1 as f32 + y, |
| 1756 | 1682 | ], |
| 1757 | color: [0.0, 0.0, 0.0, 1.0], | |
| 1683 | color: colors.paper.ink, | |
| 1758 | 1684 | }); |
| 1759 | 1685 | } |
| 1760 | 1686 | let [anchor, focus] = editor.selection().positions; |
crates/canvas/src/interaction/profile.rs+22-4| ... | ... | @@ -45,7 +45,7 @@ fn editing_cost( |
| 45 | 45 | editor: &mut CanvasEditor, |
| 46 | 46 | scene: Option<&(PageScene, [f32; 2])>, |
| 47 | 47 | ) { |
| 48 | let viewport = Viewport { | |
| 48 | let mut viewport = Viewport { | |
| 49 | 49 | size: [1000, 720], |
| 50 | 50 | scale: 96.0 / 72.0, |
| 51 | 51 | origin: [48.0; 2], |
| ... | ... | @@ -59,7 +59,9 @@ fn editing_cost( |
| 59 | 59 | }); |
| 60 | 60 | let mut access = accessibility::Accessibility::default(); |
| 61 | 61 | let update = timed(case, "ax_first", 0, || { |
| 62 | access.update(editor, viewport, "Profile", None).unwrap() | |
| 62 | access | |
| 63 | .update(editor, scene, viewport, "Profile", None, None) | |
| 64 | .unwrap() | |
| 63 | 65 | }); |
| 64 | 66 | eprintln!("canvas_count\t{case}\tax_nodes\t0\t{}", update.nodes.len()); |
| 65 | 67 | drop(update); |
| ... | ... | @@ -113,14 +115,30 @@ fn editing_cost( |
| 113 | 115 | }); |
| 114 | 116 | eprintln!("canvas_count\t{case}\tprimitives\t{sample}\t{primitives}"); |
| 115 | 117 | timed(case, "ax_insert", sample, || { |
| 116 | black_box(access.update(editor, viewport, "Profile", None).unwrap()); | |
| 118 | black_box( | |
| 119 | access | |
| 120 | .update(editor, scene, viewport, "Profile", None, None) | |
| 121 | .unwrap(), | |
| 122 | ); | |
| 123 | }); | |
| 124 | viewport.origin[1] -= 40.0; | |
| 125 | timed(case, "ax_scroll", sample, || { | |
| 126 | black_box( | |
| 127 | access | |
| 128 | .update(editor, scene, viewport, "Profile", None, None) | |
| 129 | .unwrap(), | |
| 130 | ); | |
| 117 | 131 | }); |
| 118 | 132 | assert_eq!(edited_layouts, layout_ids(editor)); |
| 119 | 133 | timed(case, "undo", sample, || { |
| 120 | 134 | assert!(editor.undo(engine).unwrap()) |
| 121 | 135 | }); |
| 122 | 136 | timed(case, "ax_undo", sample, || { |
| 123 | black_box(access.update(editor, viewport, "Profile", None).unwrap()); | |
| 137 | black_box( | |
| 138 | access | |
| 139 | .update(editor, scene, viewport, "Profile", None, None) | |
| 140 | .unwrap(), | |
| 141 | ); | |
| 124 | 142 | }); |
| 125 | 143 | assert_eq!(editor.active_outline().document(), &initial[index]); |
| 126 | 144 | } |
crates/canvas/src/interaction/tests.rs+70-60| ... | ... | @@ -9,6 +9,7 @@ use std::sync::Arc; |
| 9 | 9 | const COLORS: TextColors = TextColors { |
| 10 | 10 | caret: [0.1, 0.3, 0.9, 1.0], |
| 11 | 11 | selection: [0.7, 0.8, 1.0, 1.0], |
| 12 | paper: crate::gpu::Paper::WHITE, | |
| 12 | 13 | }; |
| 13 | 14 | |
| 14 | 15 | /// Paint for a view at `scale` device pixels per point on a display of `display_scale`. |
| ... | ... | @@ -18,6 +19,7 @@ pub(super) fn paint(show_caret: bool, scale: f32, display_scale: f32) -> Paint { |
| 18 | 19 | scale, |
| 19 | 20 | pixel: display_scale / scale, |
| 20 | 21 | colors: COLORS, |
| 22 | visible: [f32::NEG_INFINITY, f32::INFINITY], | |
| 21 | 23 | } |
| 22 | 24 | } |
| 23 | 25 | |
| ... | ... | @@ -61,6 +63,7 @@ fn date_buttons_keep_accessibility_identity_and_match_mouse_hits_after_reflow() |
| 61 | 63 | origin: [-30.0, -50.0], |
| 62 | 64 | }; |
| 63 | 65 | let mut access = accessibility::Accessibility::default(); |
| 66 | let mut tree = None; | |
| 64 | 67 | let mut identities = Vec::new(); |
| 65 | 68 | for phase in 0..3 { |
| 66 | 69 | if phase == 1 { |
| ... | ... | @@ -73,19 +76,21 @@ fn date_buttons_keep_accessibility_identity_and_match_mouse_hits_after_reflow() |
| 73 | 76 | .change_date(&mut engine, 2, ["Wednesday".into(), "8:40 AM".into()]) |
| 74 | 77 | .unwrap(); |
| 75 | 78 | } |
| 76 | let mut update = access.update(&editor, viewport, "Header", None).unwrap(); | |
| 77 | access.append_page_fields(&mut update, Some(&scene), &editor, viewport, None); | |
| 78 | let buttons: Vec<_> = update | |
| 79 | .nodes | |
| 80 | .iter() | |
| 81 | .filter(|(_, node)| node.role() == accesskit::Role::Button) | |
| 82 | .collect(); | |
| 79 | let update = access | |
| 80 | .update(&editor, Some(&scene), viewport, "Header", None, None) | |
| 81 | .unwrap(); | |
| 82 | let buttons: Vec<_> = | |
| 83 | accessibility::tests::nodes(accessibility::tests::apply(&mut tree, update)) | |
| 84 | .into_iter() | |
| 85 | .filter(|node| node.role() == accesskit::Role::Button) | |
| 86 | .collect(); | |
| 83 | 87 | assert_eq!(buttons.len(), 2); |
| 84 | for (index, (id, node)) in buttons.iter().enumerate() { | |
| 88 | for (index, node) in buttons.iter().enumerate() { | |
| 89 | let id = node.locate().0; | |
| 85 | 90 | let field = [DateField::Date, DateField::Time][index]; |
| 86 | assert_eq!(access.date_for_node(*id), Some(field)); | |
| 87 | assert!(node.supports_action(accesskit::Action::Click)); | |
| 88 | let rect = node.bounds().unwrap(); | |
| 91 | assert_eq!(access.date_for_node(id), Some(field)); | |
| 92 | assert!(node.data().supports_action(accesskit::Action::Click)); | |
| 93 | let rect = node.bounding_box().unwrap(); | |
| 89 | 94 | let point = viewport.document_point([ |
| 90 | 95 | ((rect.x0 + rect.x1) * 0.5) as f32, |
| 91 | 96 | ((rect.y0 + rect.y1) * 0.5) as f32, |
| ... | ... | @@ -95,15 +100,14 @@ fn date_buttons_keep_accessibility_identity_and_match_mouse_hits_after_reflow() |
| 95 | 100 | Some(Hit::Date(field)) |
| 96 | 101 | ); |
| 97 | 102 | if phase == 0 { |
| 98 | identities.push(*id); | |
| 103 | identities.push(id); | |
| 99 | 104 | } else { |
| 100 | assert_eq!(*id, identities[index]); | |
| 105 | assert_eq!(id, identities[index]); | |
| 101 | 106 | } |
| 102 | 107 | } |
| 103 | 108 | if phase == 2 { |
| 104 | assert_eq!(buttons[1].1.value(), Some("8:40 AM")); | |
| 109 | assert_eq!(buttons[1].data().value(), Some("8:40 AM")); | |
| 105 | 110 | } |
| 106 | accesskit_consumer::Tree::new(update, true); | |
| 107 | 111 | } |
| 108 | 112 | } |
| 109 | 113 | |
| ... | ... | @@ -169,6 +173,7 @@ fn title_chrome_selects_text_and_exposes_a_named_editable_field() { |
| 169 | 173 | let update = access |
| 170 | 174 | .update( |
| 171 | 175 | &editor, |
| 176 | None, | |
| 172 | 177 | Viewport { |
| 173 | 178 | size: [800, 600], |
| 174 | 179 | scale: 1.0, |
| ... | ... | @@ -176,6 +181,7 @@ fn title_chrome_selects_text_and_exposes_a_named_editable_field() { |
| 176 | 181 | }, |
| 177 | 182 | "Header", |
| 178 | 183 | None, |
| 184 | None, | |
| 179 | 185 | ) |
| 180 | 186 | .unwrap(); |
| 181 | 187 | assert!(update.nodes.iter().any(|(_, node)| node.role() |
| ... | ... | @@ -214,6 +220,7 @@ fn provisional_lines_share_one_drawn_hit_tested_and_accessible_outline() { |
| 214 | 220 | let update = access |
| 215 | 221 | .update( |
| 216 | 222 | &editor, |
| 223 | None, | |
| 217 | 224 | Viewport { |
| 218 | 225 | size: [800, 600], |
| 219 | 226 | scale: 1.0, |
| ... | ... | @@ -221,6 +228,7 @@ fn provisional_lines_share_one_drawn_hit_tested_and_accessible_outline() { |
| 221 | 228 | }, |
| 222 | 229 | "Test", |
| 223 | 230 | None, |
| 231 | None, | |
| 224 | 232 | ) |
| 225 | 233 | .unwrap(); |
| 226 | 234 | let fields = update |
| ... | ... | @@ -264,7 +272,9 @@ fn blank_caret_and_composition_have_accessible_text_without_outline_chrome() { |
| 264 | 272 | scale: 96.0 / 72.0, |
| 265 | 273 | origin: [48.0; 2], |
| 266 | 274 | }; |
| 267 | let initial = access.update(&editor, viewport, "Page", None).unwrap(); | |
| 275 | let initial = access | |
| 276 | .update(&editor, None, viewport, "Page", None, None) | |
| 277 | .unwrap(); | |
| 268 | 278 | let id = editor.active_outline().id; |
| 269 | 279 | assert_eq!(access.outline_for_node(initial.focus), Some(id)); |
| 270 | 280 | let rectangles = |editor: &CanvasEditor| { |
| ... | ... | @@ -291,16 +301,22 @@ fn blank_caret_and_composition_have_accessible_text_without_outline_chrome() { |
| 291 | 301 | .iter() |
| 292 | 302 | .all(|(_, color)| *color == [0.0, 0.0, 0.0, 1.0] || *color == COLORS.caret) |
| 293 | 303 | ); |
| 294 | let preedit = access.update(&editor, viewport, "Page", None).unwrap(); | |
| 304 | let preedit = access | |
| 305 | .update(&editor, None, viewport, "Page", None, None) | |
| 306 | .unwrap(); | |
| 295 | 307 | assert_eq!(preedit.focus, initial.focus); |
| 296 | 308 | editor.commit_text(&mut engine, "日本".into()).unwrap(); |
| 297 | let committed = access.update(&editor, viewport, "Page", None).unwrap(); | |
| 309 | let committed = access | |
| 310 | .update(&editor, None, viewport, "Page", None, None) | |
| 311 | .unwrap(); | |
| 298 | 312 | assert_eq!(committed.focus, initial.focus); |
| 299 | 313 | assert!(rectangles(&editor).len() > 1); |
| 300 | 314 | editor.undo(&mut engine).unwrap(); |
| 301 | 315 | assert!(editor.outlines().is_empty()); |
| 302 | 316 | assert_eq!(rectangles(&editor).len(), 1); |
| 303 | let empty = access.update(&editor, viewport, "Page", None).unwrap(); | |
| 317 | let empty = access | |
| 318 | .update(&editor, None, viewport, "Page", None, None) | |
| 319 | .unwrap(); | |
| 304 | 320 | assert_eq!(empty.focus, committed.focus); |
| 305 | 321 | assert_eq!(access.outline_for_node(committed.focus), Some(id)); |
| 306 | 322 | editor.redo(&mut engine).unwrap(); |
| ... | ... | @@ -308,12 +324,16 @@ fn blank_caret_and_composition_have_accessible_text_without_outline_chrome() { |
| 308 | 324 | editor.delete(&mut engine, true).unwrap(); |
| 309 | 325 | assert!(editor.outlines().is_empty()); |
| 310 | 326 | assert_eq!(rectangles(&editor).len(), 1); |
| 311 | let retired = access.update(&editor, viewport, "Page", None).unwrap(); | |
| 327 | let retired = access | |
| 328 | .update(&editor, None, viewport, "Page", None, None) | |
| 329 | .unwrap(); | |
| 312 | 330 | assert_eq!(retired.focus, committed.focus); |
| 313 | 331 | assert_eq!(access.outline_for_node(retired.focus), Some(id)); |
| 314 | 332 | editor.undo(&mut engine).unwrap(); |
| 315 | 333 | assert!(rectangles(&editor).len() > 1); |
| 316 | let restored = access.update(&editor, viewport, "Page", None).unwrap(); | |
| 334 | let restored = access | |
| 335 | .update(&editor, None, viewport, "Page", None, None) | |
| 336 | .unwrap(); | |
| 317 | 337 | assert_eq!(restored.focus, committed.focus); |
| 318 | 338 | editor.redo(&mut engine).unwrap(); |
| 319 | 339 | assert_eq!(rectangles(&editor).len(), 1); |
| ... | ... | @@ -375,6 +395,7 @@ fn imported_page_widgets_and_accessibility_follow_edits() { |
| 375 | 395 | } |
| 376 | 396 | eprintln!("Verified {tag_hits} imported tag-gutter targets"); |
| 377 | 397 | let mut access = accessibility::Accessibility::default(); |
| 398 | let mut tree = None; | |
| 378 | 399 | let tags = |editor: &CanvasEditor| { |
| 379 | 400 | page_primitives( |
| 380 | 401 | editor, |
| ... | ... | @@ -389,9 +410,9 @@ fn imported_page_widgets_and_accessibility_follow_edits() { |
| 389 | 410 | Primitive::Icon { |
| 390 | 411 | sources, |
| 391 | 412 | origin, |
| 392 | opacity, | |
| 413 | tint, | |
| 393 | 414 | .. |
| 394 | } => Some(((sources, opacity), origin)), | |
| 415 | } => Some(((sources, tint[3]), origin)), | |
| 395 | 416 | _ => None, |
| 396 | 417 | }) |
| 397 | 418 | .collect::<Vec<_>>() |
| ... | ... | @@ -410,15 +431,25 @@ fn imported_page_widgets_and_accessibility_follow_edits() { |
| 410 | 431 | .into(), |
| 411 | 432 | ) |
| 412 | 433 | .unwrap(); |
| 413 | let before = access.update(&editor, viewport, "Test", None).unwrap(); | |
| 414 | let before = accesskit_consumer::Tree::new(before, true); | |
| 415 | let before = before.state().focus().unwrap().document_range().text(); | |
| 434 | let before = access | |
| 435 | .update(&editor, None, viewport, "Test", None, None) | |
| 436 | .unwrap(); | |
| 437 | let before = accessibility::tests::apply(&mut tree, before) | |
| 438 | .focus() | |
| 439 | .unwrap() | |
| 440 | .document_range() | |
| 441 | .text(); | |
| 416 | 442 | editor.insert(&mut engine, "CANVAS CHECK ").unwrap(); |
| 417 | 443 | { |
| 418 | let update = access.update(&editor, viewport, "Test", None).unwrap(); | |
| 419 | let tree = accesskit_consumer::Tree::new(update, true); | |
| 444 | let update = access | |
| 445 | .update(&editor, None, viewport, "Test", None, None) | |
| 446 | .unwrap(); | |
| 420 | 447 | assert_eq!( |
| 421 | tree.state().focus().unwrap().document_range().text(), | |
| 448 | accessibility::tests::apply(&mut tree, update) | |
| 449 | .focus() | |
| 450 | .unwrap() | |
| 451 | .document_range() | |
| 452 | .text(), | |
| 422 | 453 | format!("CANVAS CHECK {before}") |
| 423 | 454 | ); |
| 424 | 455 | let primitives = page_primitives( |
| ... | ... | @@ -439,10 +470,15 @@ fn imported_page_widgets_and_accessibility_follow_edits() { |
| 439 | 470 | editor.undo(&mut engine).unwrap(); |
| 440 | 471 | assert_eq!(editor.active_outline().document(), &original); |
| 441 | 472 | assert_eq!(tags(&editor), original_tags); |
| 442 | let update = access.update(&editor, viewport, "Test", None).unwrap(); | |
| 443 | let tree = accesskit_consumer::Tree::new(update, true); | |
| 473 | let update = access | |
| 474 | .update(&editor, None, viewport, "Test", None, None) | |
| 475 | .unwrap(); | |
| 444 | 476 | assert_eq!( |
| 445 | tree.state().focus().unwrap().document_range().text(), | |
| 477 | accessibility::tests::apply(&mut tree, update) | |
| 478 | .focus() | |
| 479 | .unwrap() | |
| 480 | .document_range() | |
| 481 | .text(), | |
| 446 | 482 | before |
| 447 | 483 | ); |
| 448 | 484 | } |
| ... | ... | @@ -661,34 +697,6 @@ fn picture_handles_resize_as_onenote_does() { |
| 661 | 697 | assert_eq!(image_handle_at(rect, pixel, [600.0, 150.0]), None); |
| 662 | 698 | } |
| 663 | 699 | |
| 664 | #[test] | |
| 665 | fn grouped_drag_keeps_the_initial_word_when_reversing_direction() { | |
| 666 | let selection = |start, end| { | |
| 667 | Selection::from([start, end].map(|offset| crate::document::TextPosition { | |
| 668 | paragraph: 0, | |
| 669 | offset, | |
| 670 | })) | |
| 671 | }; | |
| 672 | let anchor = selection(6, 10); | |
| 673 | assert_eq!( | |
| 674 | drag_selection(anchor, selection(11, 16), SelectionUnit::Word).positions, | |
| 675 | selection(6, 16).positions | |
| 676 | ); | |
| 677 | assert_eq!( | |
| 678 | drag_selection(anchor, selection(0, 5), SelectionUnit::Word).positions, | |
| 679 | selection(10, 0).positions | |
| 680 | ); | |
| 681 | assert_eq!( | |
| 682 | drag_selection(anchor, anchor, SelectionUnit::Word).positions, | |
| 683 | anchor.positions | |
| 684 | ); | |
| 685 | let anchor = selection(10, 3); | |
| 686 | assert_eq!( | |
| 687 | drag_selection(anchor, selection(7, 7), SelectionUnit::Grapheme).positions, | |
| 688 | selection(10, 7).positions | |
| 689 | ); | |
| 690 | } | |
| 691 | ||
| 692 | 700 | const DEFAULT_MARGIN: [f32; 2] = [36.0, 14.4]; |
| 693 | 701 | |
| 694 | 702 | #[test] |
| ... | ... | @@ -819,6 +827,7 @@ fn table_glyphs_highlights_and_selection_share_cell_paint_bounds() { |
| 819 | 827 | scale: 1.0, |
| 820 | 828 | pixel: 1.0, |
| 821 | 829 | colors: COLORS, |
| 830 | visible: [f32::NEG_INFINITY, f32::INFINITY], | |
| 822 | 831 | }, |
| 823 | 832 | &mut primitives, |
| 824 | 833 | ) |
| ... | ... | @@ -889,6 +898,7 @@ fn editable_tables_paint_borders_before_selection_and_cell_text() { |
| 889 | 898 | scale: 1.0, |
| 890 | 899 | pixel: 1.0, |
| 891 | 900 | colors: COLORS, |
| 901 | visible: [f32::NEG_INFINITY, f32::INFINITY], | |
| 892 | 902 | }, |
| 893 | 903 | &mut primitives, |
| 894 | 904 | ) |
crates/canvas/src/layout.rs+13-58| ... | ... | @@ -30,7 +30,8 @@ const SUBSCRIPT_DROP: f32 = 0.08; |
| 30 | 30 | |
| 31 | 31 | #[derive(Clone, Copy, Debug, Default, PartialEq)] |
| 32 | 32 | pub struct TextBrush { |
| 33 | pub color: u32, | |
| 33 | /// COLORREF, or `None` for OneNote's automatic colour, which follows the paper. | |
| 34 | pub color: Option<u32>, | |
| 34 | 35 | pub highlight: Option<u32>, |
| 35 | 36 | /// Points the run draws above the line's baseline: positive for a superscript, |
| 36 | 37 | /// negative for a subscript. Line metrics and hit-testing keep the baseline. |
| ... | ... | @@ -191,9 +192,9 @@ impl TextEngine { |
| 191 | 192 | StyleProperty::Brush(TextBrush { |
| 192 | 193 | // Links take OneNote's blue unless given a colour of their own. |
| 193 | 194 | color: match format.color { |
| 194 | Some(color) if color != 0xff000000 => color, | |
| 195 | _ if link => 0x00ff0000, | |
| 196 | _ => 0, | |
| 195 | Some(color) if color != 0xff000000 => Some(color), | |
| 196 | _ if link => Some(0x00ff0000), | |
| 197 | _ => None, | |
| 197 | 198 | }, |
| 198 | 199 | highlight: format.highlight.filter(|color| *color != 0xff000000), |
| 199 | 200 | rise, |
| ... | ... | @@ -398,59 +399,6 @@ impl TextLayout { |
| 398 | 399 | Cursor::from_byte_index(&self.shaped, byte, affinity) |
| 399 | 400 | } |
| 400 | 401 | |
| 401 | pub(crate) fn word_cursor(&self, cursor: Cursor, backward: bool) -> Cursor { | |
| 402 | let mut current = cursor; | |
| 403 | let mut visited = BTreeSet::new(); | |
| 404 | loop { | |
| 405 | if !visited.insert((current.index(), current.affinity() == Affinity::Upstream)) { | |
| 406 | // Parley visual cursors can cycle at soft-wrapped bidi boundaries. | |
| 407 | let [left, right] = cursor.visual_clusters(&self.shaped); | |
| 408 | let rtl = if backward { | |
| 409 | left.or(right) | |
| 410 | } else { | |
| 411 | right.or(left) | |
| 412 | } | |
| 413 | .is_some_and(|cluster| cluster.is_rtl()); | |
| 414 | return if backward != rtl { | |
| 415 | cursor.previous_logical_word(&self.shaped) | |
| 416 | } else { | |
| 417 | cursor.next_logical_word(&self.shaped) | |
| 418 | }; | |
| 419 | } | |
| 420 | let next = if backward { | |
| 421 | current.previous_visual(&self.shaped) | |
| 422 | } else { | |
| 423 | current.next_visual(&self.shaped) | |
| 424 | }; | |
| 425 | if next == current { | |
| 426 | return current; | |
| 427 | } | |
| 428 | current = next; | |
| 429 | let [Some(left), Some(right)] = current.visual_clusters(&self.shaped) else { | |
| 430 | return current; | |
| 431 | }; | |
| 432 | let boundary = if left.is_rtl() { | |
| 433 | left.is_word_boundary() | |
| 434 | && if backward { | |
| 435 | left.is_space_or_nbsp() | |
| 436 | || (right.is_word_boundary() && !right.is_space_or_nbsp()) | |
| 437 | } else { | |
| 438 | !left.is_space_or_nbsp() | |
| 439 | } | |
| 440 | } else { | |
| 441 | right.is_word_boundary() | |
| 442 | && if backward { | |
| 443 | !right.is_space_or_nbsp() | |
| 444 | } else { | |
| 445 | !left.is_space_or_nbsp() | |
| 446 | } | |
| 447 | }; | |
| 448 | if boundary { | |
| 449 | return current; | |
| 450 | } | |
| 451 | } | |
| 452 | } | |
| 453 | ||
| 454 | 402 | pub fn hit_test(&self, x: f32, y: f32) -> Cursor { |
| 455 | 403 | let index = self |
| 456 | 404 | .lines |
| ... | ... | @@ -664,7 +612,14 @@ mod tests { |
| 664 | 612 | _ => None, |
| 665 | 613 | }) |
| 666 | 614 | .collect(); |
| 667 | assert_eq!(runs, [(0, false), (0x00ff_0000, true), (0x0000_00ff, true)]); | |
| 615 | assert_eq!( | |
| 616 | runs, | |
| 617 | [ | |
| 618 | (None, false), | |
| 619 | (Some(0x00ff_0000), true), | |
| 620 | (Some(0x0000_00ff), true) | |
| 621 | ] | |
| 622 | ); | |
| 668 | 623 | } |
| 669 | 624 | |
| 670 | 625 | #[test] |
crates/canvas/src/math.rs+3-3| ... | ... | @@ -10,8 +10,8 @@ use onestore::page::text::Paragraph; |
| 10 | 10 | #[derive(Clone)] |
| 11 | 11 | pub struct MathLayout { |
| 12 | 12 | pub size: [f32; 2], |
| 13 | /// COLORREF for rules and strokes. | |
| 14 | pub color: u32, | |
| 13 | /// COLORREF for rules and strokes, or `None` for the paper's ink. | |
| 14 | pub color: Option<u32>, | |
| 15 | 15 | /// Distance from the top to the baseline of the equation's main row. |
| 16 | 16 | pub baseline: f32, |
| 17 | 17 | pub items: Vec<MathItem>, |
| ... | ... | @@ -616,7 +616,7 @@ pub fn layout(engine: &mut TextEngine, paragraph: &Paragraph) -> Result<MathLayo |
| 616 | 616 | body.descent = body.descent.max(line.height - line.baseline); |
| 617 | 617 | Ok(MathLayout { |
| 618 | 618 | size: [body.width, body.height()], |
| 619 | color: color.unwrap_or(0), | |
| 619 | color, | |
| 620 | 620 | baseline: body.ascent, |
| 621 | 621 | items: body |
| 622 | 622 | .items |
crates/canvas/src/outline.rs+555-223| ... | ... | @@ -1,5 +1,5 @@ |
| 1 | 1 | use crate::{ |
| 2 | document::{DocumentEdit, edited_nodes}, | |
| 2 | document::{DocumentEdit, edited_nodes, leaves}, | |
| 3 | 3 | layout::{LayoutError, TextEngine, TextLayout}, |
| 4 | 4 | }; |
| 5 | 5 | use onestore::page::text::{Paragraph, TextProjection}; |
| ... | ... | @@ -8,17 +8,50 @@ use onestore::{ |
| 8 | 8 | ExGuid, |
| 9 | 9 | document::{Format, Kind}, |
| 10 | 10 | }; |
| 11 | use std::collections::{BTreeMap, BTreeSet}; | |
| 11 | use std::{ | |
| 12 | collections::{BTreeMap, BTreeSet}, | |
| 13 | ops::Range, | |
| 14 | }; | |
| 12 | 15 | |
| 13 | 16 | pub(crate) const TITLE_WIDTH: f32 = 468.0; |
| 14 | 17 | |
| 15 | #[derive(Clone)] | |
| 18 | #[derive(Clone, Default)] | |
| 16 | 19 | pub struct OutlineLayout { |
| 17 | 20 | pub paragraphs: Vec<ParagraphLayout>, |
| 18 | 21 | pub tables: Vec<TableLayout>, |
| 19 | 22 | /// Pictures, files and handwriting that occupy a paragraph of their own. |
| 20 | 23 | pub objects: Vec<ObjectLayout>, |
| 21 | 24 | pub size: [f32; 2], |
| 25 | /// One per root node, so an edit places anew only what it changes and moves what follows. | |
| 26 | blocks: Vec<Block>, | |
| 27 | /// The widest root node, at least 36 points, and the widest root text's line boxes; kept so | |
| 28 | /// an edit sizes an automatic width without measuring every node. | |
| 29 | pub(crate) widths: [f32; 2], | |
| 30 | } | |
| 31 | ||
| 32 | /// A root node's place in its outline's flow. | |
| 33 | #[derive(Clone)] | |
| 34 | struct Block { | |
| 35 | /// Where the node's paragraphs, tables and objects start. | |
| 36 | first: [usize; 3], | |
| 37 | /// Spacing before and after the node and its own height, or `None` while hidden. | |
| 38 | metrics: Option<[f32; 3]>, | |
| 39 | /// The node's right edge and, for text, its line boxes' right edge. | |
| 40 | extent: [f32; 2], | |
| 41 | /// The flow's bottom and pending spacing after the node. | |
| 42 | state: (f64, Option<f32>), | |
| 43 | /// A table's vertical coordinates before its offset. | |
| 44 | rel: Vec<f32>, | |
| 45 | } | |
| 46 | ||
| 47 | /// A layout of root nodes `range` once an edit applies, and how the nodes after them move. | |
| 48 | pub(crate) struct Relayout { | |
| 49 | pub range: Range<usize>, | |
| 50 | pub segment: OutlineLayout, | |
| 51 | /// Each following node's new top and flow state, up to the first that stays. | |
| 52 | moved: Vec<(f32, (f64, Option<f32>))>, | |
| 53 | pub size: [f32; 2], | |
| 54 | widths: [f32; 2], | |
| 22 | 55 | } |
| 23 | 56 | |
| 24 | 57 | #[derive(Clone)] |
| ... | ... | @@ -50,6 +83,61 @@ impl ObjectLayout { |
| 50 | 83 | ObjectKind::Picture | ObjectKind::Ink(_) => None, |
| 51 | 84 | } |
| 52 | 85 | } |
| 86 | ||
| 87 | /// The rect's top and bottom, the object's bottom and its label's top once the object | |
| 88 | /// starts at `y`, given its own height, and the height it takes in the flow. | |
| 89 | fn at(&self, y: f32, height: f32) -> ([f32; 4], f32) { | |
| 90 | match &self.kind { | |
| 91 | ObjectKind::File(label) => { | |
| 92 | let top = y + 6.0; | |
| 93 | let label_top = top + height + 10.5; | |
| 94 | let bottom = label_top + label.text.height() + 9.0; | |
| 95 | ([top, top + height, bottom, label_top], bottom - y) | |
| 96 | } | |
| 97 | ObjectKind::Unsupported(_) => ([y, y + height, y + height, y + 8.0], height), | |
| 98 | ObjectKind::Picture | ObjectKind::Ink(_) => ([y, y + height, y + height, 0.0], height), | |
| 99 | } | |
| 100 | } | |
| 101 | ||
| 102 | fn place(&mut self, y: f32, height: f32) -> f32 { | |
| 103 | let ([top, end, bottom, label_top], flow) = self.at(y, height); | |
| 104 | [self.rect[1], self.rect[3], self.bottom] = [top, end, bottom]; | |
| 105 | if let ObjectKind::File(label) | ObjectKind::Unsupported(label) = &mut self.kind { | |
| 106 | label.origin[1] = label_top; | |
| 107 | } | |
| 108 | flow | |
| 109 | } | |
| 110 | } | |
| 111 | ||
| 112 | /// The vertical coordinates of these pieces, in a fixed order. | |
| 113 | fn verticals<'a>( | |
| 114 | paragraphs: &'a mut [ParagraphLayout], | |
| 115 | tables: &'a mut [TableLayout], | |
| 116 | objects: &'a mut [ObjectLayout], | |
| 117 | ) -> impl Iterator<Item = &'a mut f32> { | |
| 118 | let cells = tables.iter_mut().flat_map(|table| &mut table.cells); | |
| 119 | paragraphs | |
| 120 | .iter_mut() | |
| 121 | .map(|paragraph| &mut paragraph.origin[1]) | |
| 122 | .chain(cells.flat_map(|cell| { | |
| 123 | let [_, top, _, bottom] = &mut cell.rect; | |
| 124 | [top, bottom] | |
| 125 | })) | |
| 126 | .chain(objects.iter_mut().flat_map(|object| { | |
| 127 | let ObjectLayout { | |
| 128 | rect: [_, top, _, end], | |
| 129 | bottom, | |
| 130 | kind, | |
| 131 | .. | |
| 132 | } = object; | |
| 133 | let label = match kind { | |
| 134 | ObjectKind::File(label) | ObjectKind::Unsupported(label) => { | |
| 135 | Some(&mut label.origin[1]) | |
| 136 | } | |
| 137 | ObjectKind::Picture | ObjectKind::Ink(_) => None, | |
| 138 | }; | |
| 139 | [top, end, bottom].into_iter().chain(label) | |
| 140 | })) | |
| 53 | 141 | } |
| 54 | 142 | |
| 55 | 143 | /// OneNote centers a file's icon and name in a column this wide. |
| ... | ... | @@ -181,21 +269,21 @@ pub(crate) fn indentation(level: u32, indents: &[f32], width: f32) -> Result<f32 |
| 181 | 269 | Ok(indent) |
| 182 | 270 | } |
| 183 | 271 | |
| 184 | fn spacing( | |
| 185 | bottom: &mut f64, | |
| 186 | previous: &mut Option<f32>, | |
| 187 | format: &Format, | |
| 188 | ) -> Result<f32, LayoutError> { | |
| 189 | let before = format.space_before.unwrap_or(0.0); | |
| 190 | let after = format.space_after.unwrap_or(0.0); | |
| 191 | if [before, after].iter().any(|v| !v.is_finite() || *v < 0.0) { | |
| 272 | fn spacing(format: &Format) -> Result<[f32; 2], LayoutError> { | |
| 273 | let spacing = [format.space_before, format.space_after].map(|space| space.unwrap_or(0.0)); | |
| 274 | if spacing.iter().any(|v| !v.is_finite() || *v < 0.0) { | |
| 192 | 275 | return Err(LayoutError::InvalidSpacing); |
| 193 | 276 | } |
| 194 | if let Some(previous) = previous { | |
| 195 | *bottom += f64::from(previous.max(before)); | |
| 277 | Ok(spacing) | |
| 278 | } | |
| 279 | ||
| 280 | /// Where a node with `spacing` starts after the flow `state` of a bottom and pending spacing. | |
| 281 | fn top(state: &mut (f64, Option<f32>), [before, after]: [f32; 2]) -> f32 { | |
| 282 | if let Some(previous) = state.1 { | |
| 283 | state.0 += f64::from(previous.max(before)); | |
| 196 | 284 | } |
| 197 | *previous = Some(after); | |
| 198 | Ok(*bottom as f32) | |
| 285 | state.1 = Some(after); | |
| 286 | state.0 as f32 | |
| 199 | 287 | } |
| 200 | 288 | |
| 201 | 289 | impl ParagraphLayout { |
| ... | ... | @@ -386,42 +474,6 @@ pub(crate) fn visible_paragraphs<'a>( |
| 386 | 474 | }) |
| 387 | 475 | } |
| 388 | 476 | |
| 389 | pub(crate) fn arrange<'a>( | |
| 390 | paragraphs: impl Iterator<Item = &'a ParagraphLayout>, | |
| 391 | width: f32, | |
| 392 | width_set_by_user: bool, | |
| 393 | ) -> Result<(Vec<f32>, [f32; 2]), LayoutError> { | |
| 394 | let mut origins = Vec::new(); | |
| 395 | let mut bottom = 0.0_f64; | |
| 396 | let mut previous_after = None; | |
| 397 | let mut content_width = 0.0_f32; | |
| 398 | for paragraph in paragraphs { | |
| 399 | let format = &paragraph.projection.text().spans()[0].format; | |
| 400 | origins.push(spacing(&mut bottom, &mut previous_after, format)?); | |
| 401 | let size = paragraph.size(); | |
| 402 | bottom += f64::from(size[1]); | |
| 403 | if !(bottom as f32).is_finite() { | |
| 404 | return Err(LayoutError::InvalidSpacing); | |
| 405 | } | |
| 406 | content_width = content_width.max(size[0]); | |
| 407 | } | |
| 408 | let height = bottom as f32; | |
| 409 | if !height.is_finite() || !content_width.is_finite() { | |
| 410 | return Err(LayoutError::InvalidSpacing); | |
| 411 | } | |
| 412 | Ok(( | |
| 413 | origins, | |
| 414 | [ | |
| 415 | if width_set_by_user { | |
| 416 | width | |
| 417 | } else { | |
| 418 | content_width.max(36.0) | |
| 419 | }, | |
| 420 | height, | |
| 421 | ], | |
| 422 | )) | |
| 423 | } | |
| 424 | ||
| 425 | 477 | impl OutlineLayout { |
| 426 | 478 | /// OneNote gives an outline one tag column, as wide as its most-tagged paragraph needs; each |
| 427 | 479 | /// paragraph's tags start at the column's left edge. Tag origins assume a one-tag column. |
| ... | ... | @@ -491,6 +543,33 @@ impl OutlineLayout { |
| 491 | 543 | depth: usize, |
| 492 | 544 | edit: Option<&DocumentEdit>, |
| 493 | 545 | shape: &mut impl FnMut(&PageParagraph, f32, &[f32]) -> Result<ParagraphLayout, LayoutError>, |
| 546 | ) -> Result<Self, LayoutError> { | |
| 547 | let mut result = Self::stack( | |
| 548 | nodes, | |
| 549 | indents, | |
| 550 | width, | |
| 551 | depth, | |
| 552 | edit, | |
| 553 | BTreeSet::new(), | |
| 554 | (0.0, None), | |
| 555 | shape, | |
| 556 | )?; | |
| 557 | result.size[0] = if fixed_width { width } else { result.size[0] }.max(result.table_width()); | |
| 558 | Ok(result) | |
| 559 | } | |
| 560 | ||
| 561 | /// Lays out `nodes` after the flow `state`, hiding the children of `hiding`; at the root, | |
| 562 | /// keeps a block for each node. | |
| 563 | #[allow(clippy::too_many_arguments)] | |
| 564 | fn stack<'a>( | |
| 565 | nodes: impl Iterator<Item = &'a PageParagraph>, | |
| 566 | indents: &[f32], | |
| 567 | width: f32, | |
| 568 | depth: usize, | |
| 569 | edit: Option<&DocumentEdit>, | |
| 570 | mut hiding: BTreeSet<ExGuid>, | |
| 571 | mut state: (f64, Option<f32>), | |
| 572 | shape: &mut impl FnMut(&PageParagraph, f32, &[f32]) -> Result<ParagraphLayout, LayoutError>, | |
| 494 | 573 | ) -> Result<Self, LayoutError> { |
| 495 | 574 | if !width.is_finite() || width <= 0.0 { |
| 496 | 575 | return Err(LayoutError::InvalidWidth); |
| ... | ... | @@ -499,145 +578,424 @@ impl OutlineLayout { |
| 499 | 578 | return Err(LayoutError::UnsupportedContent); |
| 500 | 579 | } |
| 501 | 580 | let mut result = Self { |
| 502 | paragraphs: Vec::new(), | |
| 503 | tables: Vec::new(), | |
| 504 | objects: Vec::new(), | |
| 505 | 581 | size: [36.0, 0.0], |
| 582 | widths: [36.0, f32::NEG_INFINITY], | |
| 583 | ..Self::default() | |
| 506 | 584 | }; |
| 507 | let mut bottom = 0.0; | |
| 508 | let mut previous = None; | |
| 509 | for node in visible_paragraphs(nodes) { | |
| 510 | match &node.content { | |
| 511 | ParagraphContent::Text(_) => { | |
| 512 | let mut paragraph = shape(node, width, indents)?; | |
| 513 | let y = spacing( | |
| 514 | &mut bottom, | |
| 515 | &mut previous, | |
| 516 | &paragraph.projection.text().spans()[0].format, | |
| 517 | )?; | |
| 518 | let size = paragraph.size(); | |
| 519 | paragraph.origin[1] = y; | |
| 520 | result.paragraphs.push(paragraph); | |
| 521 | result.size[0] = result.size[0].max(size[0]); | |
| 522 | bottom += f64::from(size[1]); | |
| 585 | for node in nodes { | |
| 586 | let hidden = node.parent.is_some_and(|parent| hiding.contains(&parent)); | |
| 587 | if hidden || node.collapsed { | |
| 588 | hiding.insert(node.id); | |
| 589 | } | |
| 590 | let mut block = Block { | |
| 591 | first: [ | |
| 592 | result.paragraphs.len(), | |
| 593 | result.tables.len(), | |
| 594 | result.objects.len(), | |
| 595 | ], | |
| 596 | metrics: None, | |
| 597 | extent: [f32::NEG_INFINITY; 2], | |
| 598 | state, | |
| 599 | rel: Vec::new(), | |
| 600 | }; | |
| 601 | if !hidden { | |
| 602 | let (space, height, flow, extent) = match &node.content { | |
| 603 | ParagraphContent::Text(_) => { | |
| 604 | let mut paragraph = shape(node, width, indents)?; | |
| 605 | let space = spacing(&paragraph.projection.text().spans()[0].format)?; | |
| 606 | paragraph.origin[1] = top(&mut state, space); | |
| 607 | let size = paragraph.size(); | |
| 608 | block.extent[1] = paragraph.origin[0] + paragraph.text.shaped.width(); | |
| 609 | result.paragraphs.push(paragraph); | |
| 610 | (space, size[1], size[1], size[0]) | |
| 611 | } | |
| 612 | ParagraphContent::Table(table) => { | |
| 613 | let x = indentation(node.level, indents, width)?; | |
| 614 | let space = spacing(&node.format)?; | |
| 615 | let y = top(&mut state, space); | |
| 616 | let mut child = Self::table(table, depth + 1, edit, shape)?; | |
| 617 | if depth == 0 { | |
| 618 | block.rel = verticals( | |
| 619 | &mut child.paragraphs, | |
| 620 | &mut child.tables, | |
| 621 | &mut child.objects, | |
| 622 | ) | |
| 623 | .map(|value| *value) | |
| 624 | .collect(); | |
| 625 | } | |
| 626 | let size = child.size; | |
| 627 | result.append(child, [x, y]); | |
| 628 | (space, size[1], size[1], x + size[0]) | |
| 629 | } | |
| 630 | content => { | |
| 631 | let x = indentation(node.level, indents, width)?; | |
| 632 | let space = spacing(&node.format)?; | |
| 633 | let y = top(&mut state, space); | |
| 634 | let (mut object, height, extent) = match content { | |
| 635 | ParagraphContent::Image(image) => { | |
| 636 | let [w, h] = | |
| 637 | image_size(image).ok_or(LayoutError::UnsupportedContent)?; | |
| 638 | let object = ObjectLayout { | |
| 639 | id: image.id, | |
| 640 | rect: [x, 0.0, x + w, 0.0], | |
| 641 | kind: ObjectKind::Picture, | |
| 642 | bottom: 0.0, | |
| 643 | }; | |
| 644 | (object, h, x + w) | |
| 645 | } | |
| 646 | ParagraphContent::Attachment(file) => { | |
| 647 | let [w, h] = file.size.unwrap_or([24.0, 24.0]); | |
| 648 | let left = x + (ATTACHMENT_WIDTH - w) / 2.0; | |
| 649 | // OneNote shows the name without its extension. | |
| 650 | let name = std::path::Path::new(&file.filename) | |
| 651 | .file_stem() | |
| 652 | .and_then(|stem| stem.to_str()) | |
| 653 | .unwrap_or(&file.filename); | |
| 654 | let format = Format { | |
| 655 | alignment: Some(1), | |
| 656 | ..node.format.clone() | |
| 657 | }; | |
| 658 | let mut label = shape( | |
| 659 | &caption(node.id, file.id, name, format), | |
| 660 | ATTACHMENT_WIDTH, | |
| 661 | &[0.0], | |
| 662 | )?; | |
| 663 | label.reset_origin(x); | |
| 664 | let object = ObjectLayout { | |
| 665 | id: file.id, | |
| 666 | rect: [left, 0.0, left + w, 0.0], | |
| 667 | kind: ObjectKind::File(label), | |
| 668 | bottom: 0.0, | |
| 669 | }; | |
| 670 | (object, h, x + ATTACHMENT_WIDTH) | |
| 671 | } | |
| 672 | ParagraphContent::Ink(ink) => { | |
| 673 | // The paragraph reaches from its origin to the farthest stroke point. | |
| 674 | let [w, h] = ink | |
| 675 | .bounds() | |
| 676 | .map(|[x, y, w, h]| [x + w, y + h]) | |
| 677 | .filter(|size| size.iter().all(|v| v.is_finite() && *v >= 0.0)) | |
| 678 | .ok_or(LayoutError::UnsupportedContent)?; | |
| 679 | let object = ObjectLayout { | |
| 680 | id: ink.id, | |
| 681 | rect: [x, 0.0, x + w, 0.0], | |
| 682 | kind: ObjectKind::Ink(ink.clone()), | |
| 683 | bottom: 0.0, | |
| 684 | }; | |
| 685 | (object, h, x + w) | |
| 686 | } | |
| 687 | ParagraphContent::Unsupported(unsupported) => { | |
| 688 | let w = unsupported.layout.max_width.unwrap_or(160.0).max(160.0); | |
| 689 | let mut label = shape( | |
| 690 | &caption( | |
| 691 | node.id, | |
| 692 | unsupported.id, | |
| 693 | "Unsupported content", | |
| 694 | Format::default(), | |
| 695 | ), | |
| 696 | w - 16.0, | |
| 697 | &[0.0], | |
| 698 | )?; | |
| 699 | label.reset_origin(x + 8.0); | |
| 700 | let h = unsupported | |
| 701 | .layout | |
| 702 | .max_height | |
| 703 | .unwrap_or(0.0) | |
| 704 | .max(label.text.height() + 16.0); | |
| 705 | let object = ObjectLayout { | |
| 706 | id: unsupported.id, | |
| 707 | rect: [x, 0.0, x + w, 0.0], | |
| 708 | kind: ObjectKind::Unsupported(label), | |
| 709 | bottom: 0.0, | |
| 710 | }; | |
| 711 | (object, h, x + w) | |
| 712 | } | |
| 713 | ParagraphContent::Text(_) | ParagraphContent::Table(_) => { | |
| 714 | unreachable!("text and tables are not objects") | |
| 715 | } | |
| 716 | }; | |
| 717 | let flow = object.place(y, height); | |
| 718 | result.objects.push(object); | |
| 719 | (space, height, flow, extent) | |
| 720 | } | |
| 721 | }; | |
| 722 | state.0 += f64::from(flow); | |
| 723 | result.size[0] = result.size[0].max(extent); | |
| 724 | result.widths[1] = result.widths[1].max(block.extent[1]); | |
| 725 | block.extent[0] = extent; | |
| 726 | block.metrics = Some([space[0], space[1], height]); | |
| 727 | if !(state.0 as f32).is_finite() || !result.size[0].is_finite() { | |
| 728 | return Err(LayoutError::InvalidSpacing); | |
| 523 | 729 | } |
| 524 | ParagraphContent::Table(table) => { | |
| 525 | let x = indentation(node.level, indents, width)?; | |
| 526 | let y = spacing(&mut bottom, &mut previous, &node.format)?; | |
| 527 | let child = Self::table(table, depth + 1, edit, shape)?; | |
| 528 | result.size[0] = result.size[0].max(x + child.size[0]); | |
| 529 | bottom += f64::from(child.size[1]); | |
| 530 | result.append(child, [x, y]); | |
| 730 | } | |
| 731 | block.state = state; | |
| 732 | if depth == 0 { | |
| 733 | result.blocks.push(block); | |
| 734 | } | |
| 735 | } | |
| 736 | result.size[1] = state.0 as f32; | |
| 737 | result.widths[0] = result.size[0]; | |
| 738 | Ok(result) | |
| 739 | } | |
| 740 | ||
| 741 | /// The paragraphs, tables and objects of root nodes `range`. | |
| 742 | pub(crate) fn pieces(&self, range: Range<usize>) -> [Range<usize>; 3] { | |
| 743 | let first = |root: usize| { | |
| 744 | self.blocks.get(root).map_or( | |
| 745 | [self.paragraphs.len(), self.tables.len(), self.objects.len()], | |
| 746 | |block| block.first, | |
| 747 | ) | |
| 748 | }; | |
| 749 | let [start, end] = [first(range.start), first(range.end)]; | |
| 750 | [0, 1, 2].map(|kind| start[kind]..end[kind]) | |
| 751 | } | |
| 752 | ||
| 753 | /// Lays out root nodes `range` of `nodes` once `edit` applies, the edit's own or the table | |
| 754 | /// holding its cell, and works out where the nodes after them move. Resizing columns or | |
| 755 | /// showing or hiding other nodes lays out every node. | |
| 756 | #[allow(clippy::too_many_arguments)] | |
| 757 | pub(crate) fn relayout( | |
| 758 | &self, | |
| 759 | nodes: &[PageParagraph], | |
| 760 | edit: &DocumentEdit, | |
| 761 | range: Range<usize>, | |
| 762 | indents: &[f32], | |
| 763 | width: f32, | |
| 764 | fixed_width: bool, | |
| 765 | shape: &mut impl FnMut(&PageParagraph, f32, &[f32]) -> Result<ParagraphLayout, LayoutError>, | |
| 766 | ) -> Result<Relayout, LayoutError> { | |
| 767 | let mut range = if edit.columns.is_empty() { | |
| 768 | range | |
| 769 | } else { | |
| 770 | 0..nodes.len() | |
| 771 | }; | |
| 772 | loop { | |
| 773 | let old = self.pieces(range.clone()); | |
| 774 | let sources = leaves(&nodes[range.clone()], None) | |
| 775 | .map(|(_, _, node)| (node.id, node)) | |
| 776 | .collect::<BTreeMap<_, _>>(); | |
| 777 | let cached = self.paragraphs[old[0].clone()] | |
| 778 | .iter() | |
| 779 | .filter_map(|layout| Some((layout.id, (*sources.get(&layout.id)?, layout)))) | |
| 780 | .collect::<BTreeMap<_, _>>(); | |
| 781 | let count = match edit.container { | |
| 782 | None => range.len() - edit.range.len() + edit.replacement.len(), | |
| 783 | Some(_) => range.len(), | |
| 784 | }; | |
| 785 | let replacement = edited_nodes(nodes, None, Some(edit)) | |
| 786 | .skip(range.start) | |
| 787 | .take(count); | |
| 788 | let supplied = replacement | |
| 789 | .clone() | |
| 790 | .map(|node| node.id) | |
| 791 | .collect::<BTreeSet<_>>(); | |
| 792 | // Children of a hidden or collapsed earlier node stay hidden. | |
| 793 | let hiding = replacement | |
| 794 | .clone() | |
| 795 | .filter_map(|node| { | |
| 796 | let parent = node.parent.filter(|parent| !supplied.contains(parent))?; | |
| 797 | let root = nodes[..range.start] | |
| 798 | .iter() | |
| 799 | .rposition(|node| node.id == parent)?; | |
| 800 | (self.blocks[root].metrics.is_none() || nodes[root].collapsed).then_some(parent) | |
| 801 | }) | |
| 802 | .collect(); | |
| 803 | let start = range | |
| 804 | .start | |
| 805 | .checked_sub(1) | |
| 806 | .map_or((0.0, None), |root| self.blocks[root].state); | |
| 807 | let segment = Self::stack( | |
| 808 | replacement.clone(), | |
| 809 | indents, | |
| 810 | width, | |
| 811 | 0, | |
| 812 | Some(edit), | |
| 813 | hiding, | |
| 814 | start, | |
| 815 | &mut |node, width, indents| { | |
| 816 | let indent = indentation(node.level, indents, width)?; | |
| 817 | if let Some((source, cached)) = cached.get(&node.id) | |
| 818 | && *source == node | |
| 819 | && cached.text.shaped.layout_max_advance() == width - indent | |
| 820 | && cached | |
| 821 | .markers | |
| 822 | .iter() | |
| 823 | .all(|(marker, _)| marker.shaped.layout_max_advance() == width) | |
| 824 | { | |
| 825 | let mut result = (*cached).clone(); | |
| 826 | result.reset_origin(indent); | |
| 827 | return Ok(result); | |
| 828 | } | |
| 829 | shape(node, width, indents) | |
| 830 | }, | |
| 831 | )?; | |
| 832 | let hides = | |
| 833 | |block: &Block, node: &PageParagraph| block.metrics.is_none() || node.collapsed; | |
| 834 | if range.end < nodes.len() { | |
| 835 | let now = replacement | |
| 836 | .zip(&segment.blocks) | |
| 837 | .map(|(node, block)| (node.id, hides(block, node))) | |
| 838 | .collect::<BTreeMap<_, _>>(); | |
| 839 | if nodes[range.clone()] | |
| 840 | .iter() | |
| 841 | .zip(&self.blocks[range.clone()]) | |
| 842 | .any(|(node, block)| { | |
| 843 | now.get(&node.id) | |
| 844 | .is_some_and(|now| *now != hides(block, node)) | |
| 845 | }) | |
| 846 | { | |
| 847 | range = 0..nodes.len(); | |
| 848 | continue; | |
| 531 | 849 | } |
| 532 | ParagraphContent::Image(image) => { | |
| 533 | let x = indentation(node.level, indents, width)?; | |
| 534 | let y = spacing(&mut bottom, &mut previous, &node.format)?; | |
| 535 | let [w, h] = image_size(image).ok_or(LayoutError::UnsupportedContent)?; | |
| 536 | result.objects.push(ObjectLayout { | |
| 537 | id: image.id, | |
| 538 | rect: [x, y, x + w, y + h], | |
| 539 | kind: ObjectKind::Picture, | |
| 540 | bottom: y + h, | |
| 541 | }); | |
| 542 | result.size[0] = result.size[0].max(x + w); | |
| 543 | bottom += f64::from(h); | |
| 850 | } | |
| 851 | let mut state = segment.blocks.last().map_or(start, |block| block.state); | |
| 852 | let mut incoming = range | |
| 853 | .end | |
| 854 | .checked_sub(1) | |
| 855 | .map_or((0.0, None), |root| self.blocks[root].state); | |
| 856 | let mut moved = Vec::new(); | |
| 857 | for root in range.end..self.blocks.len() { | |
| 858 | if state == incoming { | |
| 859 | break; | |
| 544 | 860 | } |
| 545 | ParagraphContent::Attachment(file) => { | |
| 546 | let x = indentation(node.level, indents, width)?; | |
| 547 | let y = spacing(&mut bottom, &mut previous, &node.format)?; | |
| 548 | let [w, h] = file.size.unwrap_or([24.0, 24.0]); | |
| 549 | let icon = [x + (ATTACHMENT_WIDTH - w) / 2.0, y + 6.0]; | |
| 550 | // OneNote shows the name without its extension. | |
| 551 | let name = std::path::Path::new(&file.filename) | |
| 552 | .file_stem() | |
| 553 | .and_then(|stem| stem.to_str()) | |
| 554 | .unwrap_or(&file.filename); | |
| 555 | let format = Format { | |
| 556 | alignment: Some(1), | |
| 557 | ..node.format.clone() | |
| 861 | let block = &self.blocks[root]; | |
| 862 | incoming = block.state; | |
| 863 | let mut y = 0.0; | |
| 864 | if let Some([before, after, height]) = block.metrics { | |
| 865 | y = top(&mut state, [before, after]); | |
| 866 | let [_, tables, objects] = self.pieces(root..root + 1); | |
| 867 | let flow = match self.objects[objects].first() { | |
| 868 | Some(object) if tables.is_empty() => object.at(y, height).1, | |
| 869 | _ => height, | |
| 558 | 870 | }; |
| 559 | let mut label = shape( | |
| 560 | &caption(node.id, file.id, name, format), | |
| 561 | ATTACHMENT_WIDTH, | |
| 562 | &[0.0], | |
| 563 | )?; | |
| 564 | label.reset_origin(x); | |
| 565 | label.origin[1] = icon[1] + h + 10.5; | |
| 566 | let end = label.origin[1] + label.text.height() + 9.0; | |
| 567 | result.objects.push(ObjectLayout { | |
| 568 | id: file.id, | |
| 569 | rect: [icon[0], icon[1], icon[0] + w, icon[1] + h], | |
| 570 | kind: ObjectKind::File(label), | |
| 571 | bottom: end, | |
| 572 | }); | |
| 573 | result.size[0] = result.size[0].max(x + ATTACHMENT_WIDTH); | |
| 574 | bottom += f64::from(end - y); | |
| 871 | state.0 += f64::from(flow); | |
| 872 | if !(state.0 as f32).is_finite() { | |
| 873 | return Err(LayoutError::InvalidSpacing); | |
| 874 | } | |
| 575 | 875 | } |
| 576 | ParagraphContent::Ink(ink) => { | |
| 577 | let x = indentation(node.level, indents, width)?; | |
| 578 | let y = spacing(&mut bottom, &mut previous, &node.format)?; | |
| 579 | // The paragraph reaches from its origin to the farthest stroke point. | |
| 580 | let [w, h] = ink | |
| 581 | .bounds() | |
| 582 | .map(|[x, y, w, h]| [x + w, y + h]) | |
| 583 | .filter(|size| size.iter().all(|v| v.is_finite() && *v >= 0.0)) | |
| 584 | .ok_or(LayoutError::UnsupportedContent)?; | |
| 585 | result.objects.push(ObjectLayout { | |
| 586 | id: ink.id, | |
| 587 | rect: [x, y, x + w, y + h], | |
| 588 | kind: ObjectKind::Ink(ink.clone()), | |
| 589 | bottom: y + h, | |
| 590 | }); | |
| 591 | result.size[0] = result.size[0].max(x + w); | |
| 592 | bottom += f64::from(h); | |
| 876 | moved.push((y, state)); | |
| 877 | } | |
| 878 | let bottom = if moved.len() == self.blocks.len() - range.end { | |
| 879 | state.0 as f32 | |
| 880 | } else { | |
| 881 | self.size[1] | |
| 882 | }; | |
| 883 | let widest = |blocks: &[Block], side: usize| { | |
| 884 | blocks | |
| 885 | .iter() | |
| 886 | .map(|block| block.extent[side]) | |
| 887 | .fold(f32::NEG_INFINITY, f32::max) | |
| 888 | }; | |
| 889 | let widths = [0, 1].map(|side| { | |
| 890 | let [before, after] = [ | |
| 891 | widest(&self.blocks[range.clone()], side), | |
| 892 | widest(&segment.blocks, side), | |
| 893 | ]; | |
| 894 | if after >= before || before < self.widths[side] { | |
| 895 | self.widths[side].max(after) | |
| 896 | } else { | |
| 897 | [ | |
| 898 | &self.blocks[..range.start], | |
| 899 | &segment.blocks, | |
| 900 | &self.blocks[range.end..], | |
| 901 | ] | |
| 902 | .into_iter() | |
| 903 | .map(|blocks| widest(blocks, side)) | |
| 904 | .fold([36.0, f32::NEG_INFINITY][side], f32::max) | |
| 593 | 905 | } |
| 594 | ParagraphContent::Unsupported(unsupported) => { | |
| 595 | let x = indentation(node.level, indents, width)?; | |
| 596 | let y = spacing(&mut bottom, &mut previous, &node.format)?; | |
| 597 | let w = unsupported.layout.max_width.unwrap_or(160.0).max(160.0); | |
| 598 | let mut label = shape( | |
| 599 | &caption( | |
| 600 | node.id, | |
| 601 | unsupported.id, | |
| 602 | "Unsupported content", | |
| 603 | Format::default(), | |
| 604 | ), | |
| 605 | w - 16.0, | |
| 606 | &[0.0], | |
| 607 | )?; | |
| 608 | label.reset_origin(x + 8.0); | |
| 609 | label.origin[1] = y + 8.0; | |
| 610 | let h = unsupported | |
| 611 | .layout | |
| 612 | .max_height | |
| 613 | .unwrap_or(0.0) | |
| 614 | .max(label.text.height() + 16.0); | |
| 615 | result.objects.push(ObjectLayout { | |
| 616 | id: unsupported.id, | |
| 617 | rect: [x, y, x + w, y + h], | |
| 618 | kind: ObjectKind::Unsupported(label), | |
| 619 | bottom: y + h, | |
| 620 | }); | |
| 621 | result.size[0] = result.size[0].max(x + w); | |
| 622 | bottom += f64::from(h); | |
| 906 | }); | |
| 907 | let tables = &old[1]; | |
| 908 | let tables = self.tables[..tables.start] | |
| 909 | .iter() | |
| 910 | .chain(&segment.tables) | |
| 911 | .chain(&self.tables[tables.end..]); | |
| 912 | let size = [ | |
| 913 | if fixed_width { width } else { widths[0] }.max(table_width(tables)), | |
| 914 | bottom, | |
| 915 | ]; | |
| 916 | return Ok(Relayout { | |
| 917 | range, | |
| 918 | segment, | |
| 919 | moved, | |
| 920 | size, | |
| 921 | widths, | |
| 922 | }); | |
| 923 | } | |
| 924 | } | |
| 925 | ||
| 926 | /// Puts a [`Relayout`] of this layout in place. | |
| 927 | pub(crate) fn commit(&mut self, relayout: Relayout) { | |
| 928 | let Relayout { | |
| 929 | range, | |
| 930 | mut segment, | |
| 931 | moved, | |
| 932 | size, | |
| 933 | widths, | |
| 934 | } = relayout; | |
| 935 | let [paragraphs, tables, objects] = self.pieces(range.clone()); | |
| 936 | let first = [paragraphs.start, tables.start, objects.start]; | |
| 937 | let delta = [ | |
| 938 | segment.paragraphs.len() as isize - paragraphs.len() as isize, | |
| 939 | segment.tables.len() as isize - tables.len() as isize, | |
| 940 | segment.objects.len() as isize - objects.len() as isize, | |
| 941 | ]; | |
| 942 | for cell in segment.tables.iter_mut().flat_map(|table| &mut table.cells) { | |
| 943 | cell.paragraphs = cell.paragraphs.start + first[0]..cell.paragraphs.end + first[0]; | |
| 944 | } | |
| 945 | for block in &mut segment.blocks { | |
| 946 | for (value, first) in block.first.iter_mut().zip(first) { | |
| 947 | *value += first; | |
| 948 | } | |
| 949 | } | |
| 950 | let after = range.start + segment.blocks.len(); | |
| 951 | let later_tables = tables.start + segment.tables.len(); | |
| 952 | self.paragraphs.splice(paragraphs, segment.paragraphs); | |
| 953 | self.tables.splice(tables, segment.tables); | |
| 954 | self.objects.splice(objects, segment.objects); | |
| 955 | self.blocks.splice(range, segment.blocks); | |
| 956 | if delta != [0; 3] { | |
| 957 | for block in &mut self.blocks[after..] { | |
| 958 | for (value, delta) in block.first.iter_mut().zip(delta) { | |
| 959 | *value = value.wrapping_add_signed(delta); | |
| 623 | 960 | } |
| 624 | 961 | } |
| 625 | if !(bottom as f32).is_finite() || !result.size[0].is_finite() { | |
| 626 | return Err(LayoutError::InvalidSpacing); | |
| 962 | for cell in self.tables[later_tables..] | |
| 963 | .iter_mut() | |
| 964 | .flat_map(|table| &mut table.cells) | |
| 965 | { | |
| 966 | cell.paragraphs = cell.paragraphs.start.wrapping_add_signed(delta[0]) | |
| 967 | ..cell.paragraphs.end.wrapping_add_signed(delta[0]); | |
| 627 | 968 | } |
| 628 | 969 | } |
| 629 | result.size[1] = bottom as f32; | |
| 630 | result.size[0] = if fixed_width { width } else { result.size[0] }.max(result.table_width()); | |
| 631 | Ok(result) | |
| 970 | for (root, (y, state)) in (after..).zip(moved) { | |
| 971 | let [paragraphs, tables, objects] = self.pieces(root..root + 1); | |
| 972 | let block = &mut self.blocks[root]; | |
| 973 | block.state = state; | |
| 974 | let Some([.., height]) = block.metrics else { | |
| 975 | continue; | |
| 976 | }; | |
| 977 | if !tables.is_empty() { | |
| 978 | for (value, rel) in verticals( | |
| 979 | &mut self.paragraphs[paragraphs], | |
| 980 | &mut self.tables[tables], | |
| 981 | &mut self.objects[objects], | |
| 982 | ) | |
| 983 | .zip(&block.rel) | |
| 984 | { | |
| 985 | *value = rel + y; | |
| 986 | } | |
| 987 | } else if let Some(object) = self.objects[objects].first_mut() { | |
| 988 | object.place(y, height); | |
| 989 | } else { | |
| 990 | self.paragraphs[paragraphs.start].origin[1] = y; | |
| 991 | } | |
| 992 | } | |
| 993 | self.size = size; | |
| 994 | self.widths = widths; | |
| 632 | 995 | } |
| 633 | 996 | |
| 634 | 997 | pub(crate) fn table_width(&self) -> f32 { |
| 635 | // Stored column widths extend 1.77pt past the final cell border. | |
| 636 | self.tables | |
| 637 | .iter() | |
| 638 | .filter_map(|table| table.cells.last()) | |
| 639 | .map(|cell| cell.rect[2] + 1.77) | |
| 640 | .fold(0.0, f32::max) | |
| 998 | table_width(&self.tables) | |
| 641 | 999 | } |
| 642 | 1000 | |
| 643 | 1001 | fn table( |
| ... | ... | @@ -666,13 +1024,11 @@ impl OutlineLayout { |
| 666 | 1024 | return Err(LayoutError::UnsupportedContent); |
| 667 | 1025 | } |
| 668 | 1026 | let mut result = Self { |
| 669 | paragraphs: Vec::new(), | |
| 670 | 1027 | tables: vec![TableLayout { |
| 671 | 1028 | id: table.id, |
| 672 | 1029 | cells: Vec::new(), |
| 673 | 1030 | borders: table.borders.unwrap_or(true), |
| 674 | 1031 | }], |
| 675 | objects: Vec::new(), | |
| 676 | 1032 | size: [ |
| 677 | 1033 | (0..table.columns.len()) |
| 678 | 1034 | .map(|index| width(index) + 4.98) |
| ... | ... | @@ -680,6 +1036,7 @@ impl OutlineLayout { |
| 680 | 1036 | - 1.83, |
| 681 | 1037 | 3.54, |
| 682 | 1038 | ], |
| 1039 | ..Self::default() | |
| 683 | 1040 | }; |
| 684 | 1041 | let mut y = 0.0; |
| 685 | 1042 | for row in &table.rows { |
| ... | ... | @@ -724,26 +1081,15 @@ impl OutlineLayout { |
| 724 | 1081 | } |
| 725 | 1082 | Ok(result) |
| 726 | 1083 | } |
| 1084 | } | |
| 727 | 1085 | |
| 728 | pub(crate) fn new( | |
| 729 | mut paragraphs: Vec<ParagraphLayout>, | |
| 730 | width: f32, | |
| 731 | width_set_by_user: bool, | |
| 732 | ) -> Result<Self, LayoutError> { | |
| 733 | if !width.is_finite() || width <= 0.0 { | |
| 734 | return Err(LayoutError::InvalidWidth); | |
| 735 | } | |
| 736 | let (origins, size) = arrange(paragraphs.iter(), width, width_set_by_user)?; | |
| 737 | for (paragraph, y) in paragraphs.iter_mut().zip(origins) { | |
| 738 | paragraph.origin[1] = y; | |
| 739 | } | |
| 740 | Ok(Self { | |
| 741 | paragraphs, | |
| 742 | tables: Vec::new(), | |
| 743 | objects: Vec::new(), | |
| 744 | size, | |
| 745 | }) | |
| 746 | } | |
| 1086 | /// Stored column widths extend 1.77pt past the final cell border. | |
| 1087 | fn table_width<'a>(tables: impl IntoIterator<Item = &'a TableLayout>) -> f32 { | |
| 1088 | tables | |
| 1089 | .into_iter() | |
| 1090 | .filter_map(|table| table.cells.last()) | |
| 1091 | .map(|cell| cell.rect[2] + 1.77) | |
| 1092 | .fold(0.0, f32::max) | |
| 747 | 1093 | } |
| 748 | 1094 | |
| 749 | 1095 | /// Shapes a page object into positioned paragraph layouts. |
| ... | ... | @@ -773,13 +1119,6 @@ impl Arrange for Outline { |
| 773 | 1119 | } |
| 774 | 1120 | } |
| 775 | 1121 | |
| 776 | /// Outlines of plain paragraphs stack them; tables and pictures, files or ink need the full flow. | |
| 777 | pub(crate) fn all_text<'a>(nodes: impl IntoIterator<Item = &'a PageParagraph>) -> bool { | |
| 778 | nodes | |
| 779 | .into_iter() | |
| 780 | .all(|node| matches!(node.content, ParagraphContent::Text(_))) | |
| 781 | } | |
| 782 | ||
| 783 | 1122 | pub(crate) fn outline_layout( |
| 784 | 1123 | outline: &Outline, |
| 785 | 1124 | engine: &mut TextEngine, |
| ... | ... | @@ -789,26 +1128,16 @@ pub(crate) fn outline_layout( |
| 789 | 1128 | if !outline.unsupported.is_empty() { |
| 790 | 1129 | return Err(LayoutError::UnsupportedContent); |
| 791 | 1130 | } |
| 792 | if !all_text(&outline.paragraphs) { | |
| 793 | return OutlineLayout::flow( | |
| 794 | outline.paragraphs.iter(), | |
| 795 | &outline.indents, | |
| 796 | width, | |
| 797 | outline.layout.width_set_by_user == Some(true), | |
| 798 | 0, | |
| 799 | None, | |
| 800 | &mut |node, width, indents| { | |
| 801 | ParagraphLayout::shape(engine, node, width, indents, definitions) | |
| 802 | }, | |
| 803 | ); | |
| 804 | } | |
| 805 | let paragraphs = visible_paragraphs(outline.paragraphs.iter()) | |
| 806 | .map(|p| ParagraphLayout::shape(engine, p, width, &outline.indents, definitions)) | |
| 807 | .collect::<Result<_, _>>()?; | |
| 808 | OutlineLayout::new( | |
| 809 | paragraphs, | |
| 1131 | OutlineLayout::flow( | |
| 1132 | outline.paragraphs.iter(), | |
| 1133 | &outline.indents, | |
| 810 | 1134 | width, |
| 811 | 1135 | outline.layout.width_set_by_user == Some(true), |
| 1136 | 0, | |
| 1137 | None, | |
| 1138 | &mut |node, width, indents| { | |
| 1139 | ParagraphLayout::shape(engine, node, width, indents, definitions) | |
| 1140 | }, | |
| 812 | 1141 | ) |
| 813 | 1142 | } |
| 814 | 1143 | |
| ... | ... | @@ -1377,9 +1706,12 @@ mod tests { |
| 1377 | 1706 | _ => None, |
| 1378 | 1707 | }) |
| 1379 | 1708 | .collect(); |
| 1380 | assert_eq!(colors, [0x0080_8000]); | |
| 1709 | assert_eq!(colors, [Some(0x0080_8000)]); | |
| 1381 | 1710 | assert_eq!(shaped.projection.text().spans()[0].format.color, None); |
| 1382 | let outline = OutlineLayout::new(vec![shaped], 200.0, false).unwrap(); | |
| 1711 | let outline = OutlineLayout { | |
| 1712 | paragraphs: vec![shaped], | |
| 1713 | ..OutlineLayout::default() | |
| 1714 | }; | |
| 1383 | 1715 | assert_eq!(outline.tag_column_offset(), -12.0); |
| 1384 | 1716 | } |
| 1385 | 1717 |
crates/draw/Cargo.toml+11-5| ... | ... | @@ -4,13 +4,19 @@ version = "0.1.0" |
| 4 | 4 | edition = "2024" |
| 5 | 5 | publish = false |
| 6 | 6 | |
| 7 | [features] | |
| 8 | default = ["render"] | |
| 9 | # The wgpu renderer; `edit` needs only parley. | |
| 10 | render = ["dep:bytemuck", "dep:image", "dep:roxmltree", "dep:swash", "dep:wgpu"] | |
| 11 | ||
| 7 | 12 | [dependencies] |
| 8 | bytemuck = { version = "1.25", features = ["derive"] } | |
| 9 | image = { version = "0.25", default-features = false, features = ["gif", "jpeg", "png"] } | |
| 10 | 13 | parley.workspace = true |
| 11 | roxmltree = "0.21" | |
| 12 | swash = "0.2.10" | |
| 13 | wgpu.workspace = true | |
| 14 | ||
| 15 | bytemuck = { version = "1.25", features = ["derive"], optional = true } | |
| 16 | image = { version = "0.25", default-features = false, features = ["gif", "jpeg", "png"], optional = true } | |
| 17 | roxmltree = { version = "0.21", optional = true } | |
| 18 | swash = { version = "0.2.10", optional = true } | |
| 19 | wgpu = { workspace = true, optional = true } | |
| 14 | 20 | |
| 15 | 21 | [dev-dependencies] |
| 16 | 22 | pollster = "0.4" |
crates/draw/src/edit.rs created+457| ... | ... | @@ -0,0 +1,457 @@ |
| 1 | //! Text editing shared by the page and interface text: keys, macOS editing chords, click | |
| 2 | //! counting and caret movement within one parley layout. | |
| 3 | ||
| 4 | use parley::{ | |
| 5 | Affinity, Brush, Layout, | |
| 6 | editing::{Cursor, Selection}, | |
| 7 | }; | |
| 8 | use std::{ | |
| 9 | collections::BTreeSet, | |
| 10 | time::{Duration, Instant}, | |
| 11 | }; | |
| 12 | ||
| 13 | #[derive(Clone, Debug, PartialEq)] | |
| 14 | pub enum Key { | |
| 15 | Named(NamedKey), | |
| 16 | Character(String), | |
| 17 | } | |
| 18 | ||
| 19 | #[derive(Clone, Copy, Debug, PartialEq)] | |
| 20 | pub enum NamedKey { | |
| 21 | Escape, | |
| 22 | Tab, | |
| 23 | Space, | |
| 24 | Enter, | |
| 25 | Backspace, | |
| 26 | Delete, | |
| 27 | ArrowLeft, | |
| 28 | ArrowRight, | |
| 29 | ArrowUp, | |
| 30 | ArrowDown, | |
| 31 | Home, | |
| 32 | End, | |
| 33 | /// Shift, Control, Option or Command pressed alone. | |
| 34 | Modifier, | |
| 35 | Other, | |
| 36 | } | |
| 37 | ||
| 38 | #[derive(Clone, Copy, Debug, Default, PartialEq)] | |
| 39 | pub struct Modifiers { | |
| 40 | pub shift: bool, | |
| 41 | pub control: bool, | |
| 42 | pub option: bool, | |
| 43 | pub command: bool, | |
| 44 | } | |
| 45 | ||
| 46 | #[derive(Clone, Copy, Debug, PartialEq)] | |
| 47 | pub enum Movement { | |
| 48 | Left, | |
| 49 | Right, | |
| 50 | Up, | |
| 51 | Down, | |
| 52 | WordLeft, | |
| 53 | WordRight, | |
| 54 | LineStart, | |
| 55 | LineEnd, | |
| 56 | ParagraphStart, | |
| 57 | ParagraphEnd, | |
| 58 | DocumentStart, | |
| 59 | DocumentEnd, | |
| 60 | } | |
| 61 | ||
| 62 | #[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] | |
| 63 | pub enum SelectionUnit { | |
| 64 | #[default] | |
| 65 | Grapheme, | |
| 66 | Word, | |
| 67 | Paragraph, | |
| 68 | } | |
| 69 | ||
| 70 | /// What a key does to text under macOS conventions. | |
| 71 | #[derive(Clone, Copy, Debug, PartialEq)] | |
| 72 | pub enum Command { | |
| 73 | /// Moves the focus, extending the selection with Shift. | |
| 74 | Move(Movement), | |
| 75 | /// Deletes the selection, or the grapheme before or after the caret. | |
| 76 | Delete { backward: bool }, | |
| 77 | /// Deletes the selection, or from the caret to where the movement would take it. | |
| 78 | DeleteTo(Movement), | |
| 79 | /// Control-K: deletes to the line's end, or the break after it when already there. | |
| 80 | Kill, | |
| 81 | } | |
| 82 | ||
| 83 | impl Command { | |
| 84 | pub fn from_key(key: &Key, modifiers: Modifiers) -> Option<Self> { | |
| 85 | let Modifiers { | |
| 86 | shift, | |
| 87 | control, | |
| 88 | option, | |
| 89 | command, | |
| 90 | } = modifiers; | |
| 91 | let pick = |plain, with_option, with_command| { | |
| 92 | Some(Self::Move(if command { | |
| 93 | with_command | |
| 94 | } else if option { | |
| 95 | with_option | |
| 96 | } else { | |
| 97 | plain | |
| 98 | })) | |
| 99 | }; | |
| 100 | match key { | |
| 101 | Key::Named(NamedKey::ArrowLeft) => { | |
| 102 | pick(Movement::Left, Movement::WordLeft, Movement::LineStart) | |
| 103 | } | |
| 104 | Key::Named(NamedKey::ArrowRight) => { | |
| 105 | pick(Movement::Right, Movement::WordRight, Movement::LineEnd) | |
| 106 | } | |
| 107 | Key::Named(NamedKey::ArrowUp) => pick( | |
| 108 | Movement::Up, | |
| 109 | Movement::ParagraphStart, | |
| 110 | Movement::DocumentStart, | |
| 111 | ), | |
| 112 | Key::Named(NamedKey::ArrowDown) => pick( | |
| 113 | Movement::Down, | |
| 114 | Movement::ParagraphEnd, | |
| 115 | Movement::DocumentEnd, | |
| 116 | ), | |
| 117 | Key::Named(NamedKey::Home) if shift => Some(Self::Move(Movement::DocumentStart)), | |
| 118 | Key::Named(NamedKey::End) if shift => Some(Self::Move(Movement::DocumentEnd)), | |
| 119 | Key::Named(NamedKey::Backspace) if command || option => { | |
| 120 | Some(Self::DeleteTo(if command { | |
| 121 | Movement::LineStart | |
| 122 | } else { | |
| 123 | Movement::WordLeft | |
| 124 | })) | |
| 125 | } | |
| 126 | Key::Named(NamedKey::Delete) if command || option => Some(Self::DeleteTo(if command { | |
| 127 | Movement::LineEnd | |
| 128 | } else { | |
| 129 | Movement::WordRight | |
| 130 | })), | |
| 131 | Key::Named(NamedKey::Backspace) => Some(Self::Delete { backward: true }), | |
| 132 | Key::Named(NamedKey::Delete) => Some(Self::Delete { backward: false }), | |
| 133 | Key::Character(key) if control && !option => match key.as_str() { | |
| 134 | "a" => Some(Self::Move(Movement::LineStart)), | |
| 135 | "e" => Some(Self::Move(Movement::LineEnd)), | |
| 136 | "b" => Some(Self::Move(Movement::Left)), | |
| 137 | "f" => Some(Self::Move(Movement::Right)), | |
| 138 | "p" => Some(Self::Move(Movement::Up)), | |
| 139 | "n" => Some(Self::Move(Movement::Down)), | |
| 140 | "h" => Some(Self::Delete { backward: true }), | |
| 141 | "d" => Some(Self::Delete { backward: false }), | |
| 142 | "k" => Some(Self::Kill), | |
| 143 | _ => None, | |
| 144 | }, | |
| 145 | _ => None, | |
| 146 | } | |
| 147 | } | |
| 148 | } | |
| 149 | ||
| 150 | /// Counts presses into single, double and triple clicks. | |
| 151 | #[derive(Debug)] | |
| 152 | pub struct Clicks { | |
| 153 | interval: Duration, | |
| 154 | last: Option<(Instant, [f32; 2], SelectionUnit)>, | |
| 155 | } | |
| 156 | ||
| 157 | impl Clicks { | |
| 158 | /// `interval` is the platform's double-click interval. | |
| 159 | pub fn new(interval: Duration) -> Self { | |
| 160 | Self { | |
| 161 | interval, | |
| 162 | last: None, | |
| 163 | } | |
| 164 | } | |
| 165 | ||
| 166 | /// Records a press at `point` and returns the unit it selects by. A press within the | |
| 167 | /// interval and `slop` of the previous one on each axis continues its count, cycling | |
| 168 | /// after three. | |
| 169 | pub fn press(&mut self, at: Instant, point: [f32; 2], slop: f32) -> SelectionUnit { | |
| 170 | let unit = self | |
| 171 | .last | |
| 172 | .filter(|(time, last, _)| { | |
| 173 | at.saturating_duration_since(*time) <= self.interval | |
| 174 | && (0..2).all(|axis| (last[axis] - point[axis]).abs() <= slop) | |
| 175 | }) | |
| 176 | .map_or(SelectionUnit::Grapheme, |(_, _, unit)| match unit { | |
| 177 | SelectionUnit::Grapheme => SelectionUnit::Word, | |
| 178 | SelectionUnit::Word => SelectionUnit::Paragraph, | |
| 179 | SelectionUnit::Paragraph => SelectionUnit::Grapheme, | |
| 180 | }); | |
| 181 | self.last = Some((at, point, unit)); | |
| 182 | unit | |
| 183 | } | |
| 184 | } | |
| 185 | ||
| 186 | /// The selection a drag makes from the selection its press made to the one under the | |
| 187 | /// pointer, both by `unit`, ordered by `position`. Words and paragraphs stay whole, so | |
| 188 | /// the press's selection stays covered when the drag reverses past it. | |
| 189 | pub fn drag<T: Copy, P: PartialOrd>( | |
| 190 | anchor: [T; 2], | |
| 191 | target: [T; 2], | |
| 192 | unit: SelectionUnit, | |
| 193 | position: impl Fn(T) -> P, | |
| 194 | ) -> [T; 2] { | |
| 195 | if unit == SelectionUnit::Grapheme { | |
| 196 | return [anchor[0], target[1]]; | |
| 197 | } | |
| 198 | let [anchor_start, anchor_end] = anchor.map(&position); | |
| 199 | let [target_start, target_end] = target.map(&position); | |
| 200 | let backwards = target_start < anchor_start && target_start < anchor_end; | |
| 201 | let start = usize::from((anchor_start > anchor_end) != backwards); | |
| 202 | let end = usize::from((target_start > target_end) == backwards); | |
| 203 | [anchor[start], target[end]] | |
| 204 | } | |
| 205 | ||
| 206 | /// The selection a press makes by `unit`, where `hit` is the cursor at the press's `x`. | |
| 207 | pub fn selection_at<B: Brush>( | |
| 208 | layout: &Layout<B>, | |
| 209 | hit: Cursor, | |
| 210 | x: f32, | |
| 211 | unit: SelectionUnit, | |
| 212 | ) -> Selection { | |
| 213 | match unit { | |
| 214 | SelectionUnit::Grapheme => hit.into(), | |
| 215 | SelectionUnit::Word => { | |
| 216 | let rect = hit.geometry(layout, 1.0); | |
| 217 | Selection::word_from_point(layout, x, ((rect.y0 + rect.y1) * 0.5) as f32) | |
| 218 | } | |
| 219 | SelectionUnit::Paragraph => Selection::new( | |
| 220 | Cursor::from_byte_index(layout, 0, Affinity::Downstream), | |
| 221 | Cursor::from_byte_index(layout, usize::MAX, Affinity::Upstream), | |
| 222 | ), | |
| 223 | } | |
| 224 | } | |
| 225 | ||
| 226 | /// Where `movement` takes `selection` within `layout`, keeping its anchor when `extend`. | |
| 227 | /// Up, Down and the paragraph and document movements stop at the layout's ends. | |
| 228 | pub fn step<B: Brush>( | |
| 229 | layout: &Layout<B>, | |
| 230 | selection: Selection, | |
| 231 | movement: Movement, | |
| 232 | extend: bool, | |
| 233 | ) -> Selection { | |
| 234 | let to = |focus: Cursor| { | |
| 235 | if extend { | |
| 236 | selection.extend(focus) | |
| 237 | } else { | |
| 238 | focus.into() | |
| 239 | } | |
| 240 | }; | |
| 241 | match movement { | |
| 242 | Movement::Left => selection.previous_visual(layout, extend), | |
| 243 | Movement::Right => selection.next_visual(layout, extend), | |
| 244 | Movement::WordLeft => to(word_cursor(layout, selection.focus(), true)), | |
| 245 | Movement::WordRight => to(word_cursor(layout, selection.focus(), false)), | |
| 246 | Movement::LineStart => selection.line_start(layout, extend), | |
| 247 | Movement::LineEnd => selection.line_end(layout, extend), | |
| 248 | Movement::Up | Movement::ParagraphStart | Movement::DocumentStart => { | |
| 249 | to(Cursor::from_byte_index(layout, 0, Affinity::Downstream)) | |
| 250 | } | |
| 251 | Movement::Down | Movement::ParagraphEnd | Movement::DocumentEnd => to( | |
| 252 | Cursor::from_byte_index(layout, usize::MAX, Affinity::Upstream), | |
| 253 | ), | |
| 254 | } | |
| 255 | } | |
| 256 | ||
| 257 | /// The next word boundary in visual order, stopping before a word going left and after | |
| 258 | /// one going right, as macOS does. | |
| 259 | pub fn word_cursor<B: Brush>(layout: &Layout<B>, cursor: Cursor, backward: bool) -> Cursor { | |
| 260 | let mut current = cursor; | |
| 261 | let mut visited = BTreeSet::new(); | |
| 262 | loop { | |
| 263 | if !visited.insert((current.index(), current.affinity() == Affinity::Upstream)) { | |
| 264 | // Parley visual cursors can cycle at soft-wrapped bidi boundaries. | |
| 265 | let [left, right] = cursor.visual_clusters(layout); | |
| 266 | let rtl = if backward { | |
| 267 | left.or(right) | |
| 268 | } else { | |
| 269 | right.or(left) | |
| 270 | } | |
| 271 | .is_some_and(|cluster| cluster.is_rtl()); | |
| 272 | return if backward != rtl { | |
| 273 | cursor.previous_logical_word(layout) | |
| 274 | } else { | |
| 275 | cursor.next_logical_word(layout) | |
| 276 | }; | |
| 277 | } | |
| 278 | let next = if backward { | |
| 279 | current.previous_visual(layout) | |
| 280 | } else { | |
| 281 | current.next_visual(layout) | |
| 282 | }; | |
| 283 | if next == current { | |
| 284 | return current; | |
| 285 | } | |
| 286 | current = next; | |
| 287 | let [Some(left), Some(right)] = current.visual_clusters(layout) else { | |
| 288 | return current; | |
| 289 | }; | |
| 290 | let boundary = if left.is_rtl() { | |
| 291 | left.is_word_boundary() | |
| 292 | && if backward { | |
| 293 | left.is_space_or_nbsp() | |
| 294 | || (right.is_word_boundary() && !right.is_space_or_nbsp()) | |
| 295 | } else { | |
| 296 | !left.is_space_or_nbsp() | |
| 297 | } | |
| 298 | } else { | |
| 299 | right.is_word_boundary() | |
| 300 | && if backward { | |
| 301 | !right.is_space_or_nbsp() | |
| 302 | } else { | |
| 303 | !left.is_space_or_nbsp() | |
| 304 | } | |
| 305 | }; | |
| 306 | if boundary { | |
| 307 | return current; | |
| 308 | } | |
| 309 | } | |
| 310 | } | |
| 311 | ||
| 312 | #[cfg(test)] | |
| 313 | mod tests { | |
| 314 | use super::*; | |
| 315 | use parley::{FontContext, LayoutContext}; | |
| 316 | ||
| 317 | fn layout(text: &str) -> Layout<()> { | |
| 318 | let mut fonts = FontContext::new(); | |
| 319 | let mut context = LayoutContext::new(); | |
| 320 | let mut layout = context | |
| 321 | .ranged_builder(&mut fonts, text, 1.0, false) | |
| 322 | .build(text); | |
| 323 | layout.break_all_lines(None); | |
| 324 | layout | |
| 325 | } | |
| 326 | ||
| 327 | #[test] | |
| 328 | fn keys_follow_macos_editing_chords() { | |
| 329 | let with = |modifiers: fn(&mut Modifiers)| { | |
| 330 | let mut value = Modifiers::default(); | |
| 331 | modifiers(&mut value); | |
| 332 | value | |
| 333 | }; | |
| 334 | let named = |named| Key::Named(named); | |
| 335 | let cases = [ | |
| 336 | ( | |
| 337 | named(NamedKey::ArrowLeft), | |
| 338 | with(|m| m.option = true), | |
| 339 | Some(Command::Move(Movement::WordLeft)), | |
| 340 | ), | |
| 341 | ( | |
| 342 | named(NamedKey::ArrowRight), | |
| 343 | with(|m| (m.command, m.option) = (true, true)), | |
| 344 | Some(Command::Move(Movement::LineEnd)), | |
| 345 | ), | |
| 346 | ( | |
| 347 | named(NamedKey::ArrowUp), | |
| 348 | with(|m| m.option = true), | |
| 349 | Some(Command::Move(Movement::ParagraphStart)), | |
| 350 | ), | |
| 351 | (named(NamedKey::Home), Modifiers::default(), None), | |
| 352 | ( | |
| 353 | named(NamedKey::End), | |
| 354 | with(|m| m.shift = true), | |
| 355 | Some(Command::Move(Movement::DocumentEnd)), | |
| 356 | ), | |
| 357 | ( | |
| 358 | named(NamedKey::Backspace), | |
| 359 | with(|m| m.option = true), | |
| 360 | Some(Command::DeleteTo(Movement::WordLeft)), | |
| 361 | ), | |
| 362 | ( | |
| 363 | named(NamedKey::Delete), | |
| 364 | with(|m| m.command = true), | |
| 365 | Some(Command::DeleteTo(Movement::LineEnd)), | |
| 366 | ), | |
| 367 | ( | |
| 368 | named(NamedKey::Backspace), | |
| 369 | with(|m| m.control = true), | |
| 370 | Some(Command::Delete { backward: true }), | |
| 371 | ), | |
| 372 | ( | |
| 373 | Key::Character("e".into()), | |
| 374 | with(|m| m.control = true), | |
| 375 | Some(Command::Move(Movement::LineEnd)), | |
| 376 | ), | |
| 377 | ( | |
| 378 | Key::Character("k".into()), | |
| 379 | with(|m| m.control = true), | |
| 380 | Some(Command::Kill), | |
| 381 | ), | |
| 382 | ( | |
| 383 | Key::Character("k".into()), | |
| 384 | with(|m| (m.control, m.option) = (true, true)), | |
| 385 | None, | |
| 386 | ), | |
| 387 | (Key::Character("a".into()), Modifiers::default(), None), | |
| 388 | ]; | |
| 389 | for (key, modifiers, command) in cases { | |
| 390 | assert_eq!( | |
| 391 | Command::from_key(&key, modifiers), | |
| 392 | command, | |
| 393 | "{key:?} {modifiers:?}" | |
| 394 | ); | |
| 395 | } | |
| 396 | } | |
| 397 | ||
| 398 | #[test] | |
| 399 | fn presses_count_within_the_interval_and_slop_and_cycle() { | |
| 400 | let mut clicks = Clicks::new(Duration::from_millis(500)); | |
| 401 | let start = Instant::now(); | |
| 402 | let at = |millis| start + Duration::from_millis(millis); | |
| 403 | let units: Vec<_> = [(0, 0.0), (100, 3.0), (200, 3.0), (300, 0.0), (1000, 0.0)] | |
| 404 | .map(|(millis, x)| clicks.press(at(millis), [x, 0.0], 4.0)) | |
| 405 | .into(); | |
| 406 | use SelectionUnit::*; | |
| 407 | assert_eq!(units, [Grapheme, Word, Paragraph, Grapheme, Grapheme]); | |
| 408 | assert_eq!(clicks.press(at(1100), [10.0, 0.0], 4.0), Grapheme); | |
| 409 | } | |
| 410 | ||
| 411 | #[test] | |
| 412 | fn grouped_drag_keeps_the_initial_word_when_reversing_direction() { | |
| 413 | let drag = |anchor, target, unit| drag(anchor, target, unit, |offset: usize| offset); | |
| 414 | let word = [6, 10]; | |
| 415 | assert_eq!(drag(word, [11, 16], SelectionUnit::Word), [6, 16]); | |
| 416 | assert_eq!(drag(word, [0, 5], SelectionUnit::Word), [10, 0]); | |
| 417 | assert_eq!(drag(word, word, SelectionUnit::Word), word); | |
| 418 | assert_eq!(drag([10, 3], [7, 7], SelectionUnit::Grapheme), [10, 7]); | |
| 419 | } | |
| 420 | ||
| 421 | #[test] | |
| 422 | fn words_stop_at_their_edges_in_both_directions() { | |
| 423 | let layout = layout("one two three"); | |
| 424 | let at = |index| Cursor::from_byte_index(&layout, index, Affinity::Downstream); | |
| 425 | let words = |start, backward| { | |
| 426 | std::iter::successors(Some(at(start)), |cursor| { | |
| 427 | let next = word_cursor(&layout, *cursor, backward); | |
| 428 | (next != *cursor).then_some(next) | |
| 429 | }) | |
| 430 | .skip(1) | |
| 431 | .map(|cursor| cursor.index()) | |
| 432 | .collect::<Vec<_>>() | |
| 433 | }; | |
| 434 | assert_eq!(words(0, false), [3, 7, 14]); | |
| 435 | assert_eq!(words(14, true), [9, 4, 0]); | |
| 436 | } | |
| 437 | ||
| 438 | #[test] | |
| 439 | fn steps_collapse_extend_and_reach_the_ends() { | |
| 440 | let layout = layout("one two"); | |
| 441 | let all = selection_at(&layout, Cursor::default(), 0.0, SelectionUnit::Paragraph); | |
| 442 | assert_eq!(all.text_range(), 0..7); | |
| 443 | let left = step(&layout, all, Movement::Left, false); | |
| 444 | assert_eq!(left.text_range(), 0..0); | |
| 445 | let word = step(&layout, left, Movement::WordRight, true); | |
| 446 | assert_eq!(word.text_range(), 0..3); | |
| 447 | assert_eq!( | |
| 448 | step(&layout, word, Movement::Down, false).focus().index(), | |
| 449 | 7 | |
| 450 | ); | |
| 451 | assert_eq!( | |
| 452 | step(&layout, word, Movement::Up, true).text_range(), | |
| 453 | 0..0, | |
| 454 | "extending keeps the anchor" | |
| 455 | ); | |
| 456 | } | |
| 457 | } |
crates/draw/src/icon.rs deleted-122| ... | ... | @@ -1,122 +0,0 @@ |
| 1 | use swash::{ | |
| 2 | scale::image::{Content, Image}, | |
| 3 | zeno::{Mask, Placement, Transform, Vector}, | |
| 4 | }; | |
| 5 | ||
| 6 | /// Rasterizes 16×16 SVG sources over each other at `size` device pixels, offset by | |
| 7 | /// `phase` quarter pixels. Paths are filled with two-stop vertical gradients. | |
| 8 | pub(crate) fn rasterize(sources: &[&str], size: f32, phase: [u8; 2]) -> Image { | |
| 9 | let width = (size + 1.0).ceil() as u32; | |
| 10 | let mut pixels = vec![[0.0_f32; 4]; (width * width) as usize]; | |
| 11 | for source in sources { | |
| 12 | let svg = roxmltree::Document::parse(source).expect("Bundled icon SVG must be valid"); | |
| 13 | assert_eq!(svg.root_element().attribute("viewBox"), Some("0 0 16 16")); | |
| 14 | for path in svg.descendants().filter(|node| node.has_tag_name("path")) { | |
| 15 | let gradient = path | |
| 16 | .attribute("fill") | |
| 17 | .unwrap() | |
| 18 | .strip_prefix("url(#") | |
| 19 | .unwrap() | |
| 20 | .strip_suffix(')') | |
| 21 | .unwrap(); | |
| 22 | let gradient = svg | |
| 23 | .descendants() | |
| 24 | .find(|node| node.attribute("id") == Some(gradient)) | |
| 25 | .unwrap(); | |
| 26 | let mut stops = gradient | |
| 27 | .children() | |
| 28 | .filter(|node| node.has_tag_name("stop")) | |
| 29 | .map(|node| { | |
| 30 | let color = u32::from_str_radix( | |
| 31 | node.attribute("stop-color") | |
| 32 | .unwrap() | |
| 33 | .strip_prefix('#') | |
| 34 | .unwrap(), | |
| 35 | 16, | |
| 36 | ) | |
| 37 | .unwrap(); | |
| 38 | let [_, r, g, b] = color.to_be_bytes(); | |
| 39 | super::srgb(r, g, b) | |
| 40 | }); | |
| 41 | let top = stops.next().unwrap(); | |
| 42 | let bottom = stops.next().unwrap(); | |
| 43 | assert!(stops.next().is_none()); | |
| 44 | let (mask, _) = Mask::new(path.attribute("d").unwrap()) | |
| 45 | .transform(Some(Transform::scale(size / 16.0, size / 16.0))) | |
| 46 | .render_offset(Vector::new(phase[0] as f32 * 0.25, phase[1] as f32 * 0.25)) | |
| 47 | .size(width, width) | |
| 48 | .render(); | |
| 49 | for (index, (pixel, coverage)) in pixels.iter_mut().zip(mask).enumerate() { | |
| 50 | let alpha = f32::from(coverage) / 255.0; | |
| 51 | let y = ((index / width as usize) as f32 / size).min(1.0); | |
| 52 | for channel in 0..3 { | |
| 53 | let color = top[channel] + (bottom[channel] - top[channel]) * y; | |
| 54 | pixel[channel] = color * alpha + pixel[channel] * (1.0 - alpha); | |
| 55 | } | |
| 56 | pixel[3] = alpha + pixel[3] * (1.0 - alpha); | |
| 57 | } | |
| 58 | } | |
| 59 | } | |
| 60 | let data = pixels | |
| 61 | .into_iter() | |
| 62 | .flat_map(|pixel| { | |
| 63 | let alpha = pixel[3]; | |
| 64 | let channel = |value: f32| { | |
| 65 | let value = if alpha > 0.0 { value / alpha } else { 0.0 }; | |
| 66 | super::srgb_byte(value) | |
| 67 | }; | |
| 68 | [ | |
| 69 | channel(pixel[0]), | |
| 70 | channel(pixel[1]), | |
| 71 | channel(pixel[2]), | |
| 72 | (alpha * 255.0).round() as u8, | |
| 73 | ] | |
| 74 | }) | |
| 75 | .collect(); | |
| 76 | Image { | |
| 77 | content: Content::Color, | |
| 78 | placement: Placement { | |
| 79 | left: 0, | |
| 80 | top: 0, | |
| 81 | width, | |
| 82 | height: width, | |
| 83 | }, | |
| 84 | data, | |
| 85 | ..Image::default() | |
| 86 | } | |
| 87 | } | |
| 88 | ||
| 89 | #[cfg(test)] | |
| 90 | mod tests { | |
| 91 | use super::*; | |
| 92 | ||
| 93 | const BOX: &str = r##"<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><defs><linearGradient id="a" x1="0" y1="0" x2="0" y2="1"><stop offset="0" stop-color="#ffffff"/><stop offset="1" stop-color="#8090a0"/></linearGradient></defs><path fill="url(#a)" d="M2 2h12v12H2z"/></svg>"##; | |
| 94 | const MARK: &str = r##"<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><defs><linearGradient id="b" x1="0" y1="0" x2="0" y2="1"><stop offset="0" stop-color="#ff2000"/><stop offset="1" stop-color="#a00000"/></linearGradient></defs><path fill="url(#b)" d="M4 8l3 3 5-7-1-1-4 5-2-2z"/></svg>"##; | |
| 95 | ||
| 96 | #[test] | |
| 97 | fn icons_keep_transparency_and_overlays_across_scales() { | |
| 98 | for size in [8.0, 16.0, 32.0, 57.5] { | |
| 99 | for phase in [[0, 0], [1, 2], [3, 3]] { | |
| 100 | let plain = rasterize(&[BOX], size, phase); | |
| 101 | let marked = rasterize(&[BOX, MARK], size, phase); | |
| 102 | for image in [&plain, &marked] { | |
| 103 | assert_eq!( | |
| 104 | image.data.len(), | |
| 105 | (image.placement.width * image.placement.height * 4) as usize | |
| 106 | ); | |
| 107 | assert!(image.data.chunks_exact(4).any(|p| p[3] > 0)); | |
| 108 | assert!(image.data.chunks_exact(4).any(|p| p[3] < 255)); | |
| 109 | assert!( | |
| 110 | image | |
| 111 | .data | |
| 112 | .chunks_exact(4) | |
| 113 | .filter(|p| p[3] == 0) | |
| 114 | .all(|p| p[..3] == [0; 3]) | |
| 115 | ); | |
| 116 | } | |
| 117 | assert_ne!(plain.data, marked.data); | |
| 118 | assert!(marked.data.chunks_exact(4).any(|p| p[0] > p[2] && p[3] > 0)); | |
| 119 | } | |
| 120 | } | |
| 121 | } | |
| 122 | } |
crates/draw/src/lib.rs+8-1826| ... | ... | @@ -1,1828 +1,10 @@ |
| 1 | //! Paints layers of primitives with wgpu: glyphs and icons through one atlas, images, | |
| 2 | //! rounded rectangles and pen strokes. Each layer has its own transform and clip. | |
| 1 | //! What the page and the interface share: painting layers of glyphs, icons, paths, | |
| 2 | //! images, rounded rectangles and pen strokes with wgpu, each layer with its own transform | |
| 3 | //! and clip, and editing text within a laid-out line. | |
| 3 | 4 | |
| 4 | mod icon; | |
| 5 | mod text; | |
| 5 | pub mod edit; | |
| 6 | #[cfg(feature = "render")] | |
| 7 | mod render; | |
| 6 | 8 | |
| 7 | pub use text::{Decoration, Glyph, GlyphRun, Glyphs, paint_parley_run}; | |
| 8 | ||
| 9 | use bytemuck::{Pod, Zeroable}; | |
| 10 | use image::ImageDecoder; | |
| 11 | use parley::fontique::Blob; | |
| 12 | use std::{ | |
| 13 | collections::{HashMap, HashSet}, | |
| 14 | ops::Range, | |
| 15 | }; | |
| 16 | use swash::{ | |
| 17 | FontRef, | |
| 18 | scale::{Render, ScaleContext, Source, StrikeWith, image::Content}, | |
| 19 | zeno::{Angle, Format, Transform, Vector}, | |
| 20 | }; | |
| 21 | ||
| 22 | const ATLAS_SIZE: u32 = 2048; | |
| 23 | const MAX_GLYPHS: usize = 8192; | |
| 24 | const MAX_VERTICES: usize = 65_536; | |
| 25 | /// Decoded bytes of all images one frame may paint. | |
| 26 | pub const MAX_IMAGE_BYTES: u64 = 64 * 1024 * 1024; | |
| 27 | const MAX_IMAGES: usize = 256; | |
| 28 | ||
| 29 | #[derive(Clone)] | |
| 30 | pub struct RasterImage { | |
| 31 | size: [u32; 2], | |
| 32 | pixels: Blob<u8>, | |
| 33 | } | |
| 34 | ||
| 35 | impl RasterImage { | |
| 36 | pub fn decode(encoded: &[u8]) -> Result<Self, RenderError> { | |
| 37 | if encoded.len() as u64 > MAX_IMAGE_BYTES { | |
| 38 | return Err(RenderError::ImageBudget); | |
| 39 | } | |
| 40 | let format = image::guess_format(encoded).map_err(RenderError::ImageDecode)?; | |
| 41 | let mut reader = image::ImageReader::with_format(std::io::Cursor::new(encoded), format); | |
| 42 | let mut limits = image::Limits::default(); | |
| 43 | limits.max_image_width = Some(16_384); | |
| 44 | limits.max_image_height = Some(16_384); | |
| 45 | limits.max_alloc = Some(MAX_IMAGE_BYTES); | |
| 46 | reader.limits(limits.clone()); | |
| 47 | let mut decoder = reader.into_decoder().map_err(RenderError::ImageDecode)?; | |
| 48 | let (width, height) = decoder.dimensions(); | |
| 49 | if u64::from(width) * u64::from(height) * 4 > MAX_IMAGE_BYTES | |
| 50 | || decoder.total_bytes() > MAX_IMAGE_BYTES | |
| 51 | { | |
| 52 | return Err(RenderError::ImageBudget); | |
| 53 | } | |
| 54 | limits | |
| 55 | .reserve(decoder.total_bytes()) | |
| 56 | .map_err(RenderError::ImageDecode)?; | |
| 57 | decoder | |
| 58 | .set_limits(limits) | |
| 59 | .map_err(RenderError::ImageDecode)?; | |
| 60 | let pixels = image::DynamicImage::from_decoder(decoder) | |
| 61 | .map_err(RenderError::ImageDecode)? | |
| 62 | .into_rgba8(); | |
| 63 | Self::new([width, height], pixels.into_raw()) | |
| 64 | } | |
| 65 | ||
| 66 | /// Immutable, straight-alpha sRGB RGBA pixels retain one cache identity across clones. | |
| 67 | pub fn new(size: [u32; 2], mut pixels: Vec<u8>) -> Result<Self, RenderError> { | |
| 68 | let bytes = u64::from(size[0]) | |
| 69 | .checked_mul(u64::from(size[1])) | |
| 70 | .and_then(|v| v.checked_mul(4)) | |
| 71 | .ok_or(RenderError::InvalidImage)?; | |
| 72 | if size.contains(&0) || bytes != pixels.len() as u64 { | |
| 73 | return Err(RenderError::InvalidImage); | |
| 74 | } | |
| 75 | if bytes > MAX_IMAGE_BYTES { | |
| 76 | return Err(RenderError::ImageBudget); | |
| 77 | } | |
| 78 | // Premultiply in linear light before filtering; transparent RGB must not bleed at edges. | |
| 79 | for pixel in pixels.chunks_exact_mut(4) { | |
| 80 | let alpha = pixel[3]; | |
| 81 | if alpha == 255 { | |
| 82 | continue; | |
| 83 | } | |
| 84 | let linear = srgb(pixel[0], pixel[1], pixel[2]); | |
| 85 | for (byte, value) in pixel[..3].iter_mut().zip(linear) { | |
| 86 | let value = value * f32::from(alpha) / 255.0; | |
| 87 | *byte = srgb_byte(value); | |
| 88 | } | |
| 89 | } | |
| 90 | Ok(Self { | |
| 91 | size, | |
| 92 | pixels: pixels.into(), | |
| 93 | }) | |
| 94 | } | |
| 95 | ||
| 96 | /// The identity caches and uploads share across clones. | |
| 97 | pub fn id(&self) -> u64 { | |
| 98 | self.pixels.id() | |
| 99 | } | |
| 100 | ||
| 101 | /// Premultiplied sRGB RGBA rows. | |
| 102 | pub fn pixels(&self) -> &[u8] { | |
| 103 | self.pixels.as_ref() | |
| 104 | } | |
| 105 | } | |
| 106 | ||
| 107 | struct CachedImage { | |
| 108 | binding: wgpu::BindGroup, | |
| 109 | bytes: u64, | |
| 110 | } | |
| 111 | ||
| 112 | struct Batch { | |
| 113 | vertices: Range<u32>, | |
| 114 | image: Option<u64>, | |
| 115 | scissor: [u32; 4], | |
| 116 | } | |
| 117 | ||
| 118 | #[repr(C)] | |
| 119 | #[derive(Clone, Copy, Pod, Zeroable)] | |
| 120 | struct Vertex { | |
| 121 | position: [f32; 2], | |
| 122 | uv: [f32; 2], | |
| 123 | color: [f32; 4], | |
| 124 | local: [f32; 2], | |
| 125 | shape: [f32; 4], | |
| 126 | stroke: f32, | |
| 127 | } | |
| 128 | ||
| 129 | #[derive(Hash, PartialEq, Eq)] | |
| 130 | enum AtlasKey { | |
| 131 | Text { | |
| 132 | font: u64, | |
| 133 | index: u32, | |
| 134 | glyph: u16, | |
| 135 | size: u32, | |
| 136 | coords: Vec<i16>, | |
| 137 | phase: [u8; 2], | |
| 138 | embolden: bool, | |
| 139 | skew: u32, | |
| 140 | }, | |
| 141 | Icon { | |
| 142 | sources: &'static [&'static str], | |
| 143 | size: u32, | |
| 144 | phase: [u8; 2], | |
| 145 | }, | |
| 146 | } | |
| 147 | ||
| 148 | #[derive(Clone, Copy)] | |
| 149 | struct AtlasGlyph { | |
| 150 | x: u32, | |
| 151 | y: u32, | |
| 152 | width: u32, | |
| 153 | height: u32, | |
| 154 | left: i32, | |
| 155 | top: i32, | |
| 156 | color: bool, | |
| 157 | } | |
| 158 | ||
| 159 | /// Primitives sharing one transform and clip. | |
| 160 | pub struct Layer<'a> { | |
| 161 | /// Device pixels per layer unit. | |
| 162 | pub scale: f32, | |
| 163 | /// Device position of the layer origin. | |
| 164 | pub origin: [f32; 2], | |
| 165 | /// Device bounds `[left, top, right, bottom]` the layer paints within. | |
| 166 | pub clip: Option<[f32; 4]>, | |
| 167 | pub primitives: &'a [Primitive<'a>], | |
| 168 | } | |
| 169 | ||
| 170 | /// A layer's transform onto the target. | |
| 171 | #[derive(Clone, Copy)] | |
| 172 | struct Space { | |
| 173 | size: [u32; 2], | |
| 174 | scale: f32, | |
| 175 | origin: [f32; 2], | |
| 176 | } | |
| 177 | ||
| 178 | impl Space { | |
| 179 | fn visible_rect(&self, rect: [f32; 4]) -> Result<Option<[f32; 4]>, RenderError> { | |
| 180 | let rect = [ | |
| 181 | rect[0] * self.scale + self.origin[0], | |
| 182 | rect[1] * self.scale + self.origin[1], | |
| 183 | rect[2] * self.scale + self.origin[0], | |
| 184 | rect[3] * self.scale + self.origin[1], | |
| 185 | ]; | |
| 186 | if rect.iter().any(|v| !v.is_finite()) || rect[2] < rect[0] || rect[3] < rect[1] { | |
| 187 | return Err(RenderError::InvalidPrimitive); | |
| 188 | } | |
| 189 | Ok((rect[0] < self.size[0] as f32 | |
| 190 | && rect[1] < self.size[1] as f32 | |
| 191 | && rect[2] > 0.0 | |
| 192 | && rect[3] > 0.0 | |
| 193 | && rect[0] < rect[2] | |
| 194 | && rect[1] < rect[3]) | |
| 195 | .then_some(rect)) | |
| 196 | } | |
| 197 | ||
| 198 | /// Whole device pixels covering `rect` within the target, as a scissor rectangle. | |
| 199 | fn scissor(&self, rect: [f32; 4]) -> [u32; 4] { | |
| 200 | let left = rect[0].max(0.0).floor() as u32; | |
| 201 | let top = rect[1].max(0.0).floor() as u32; | |
| 202 | let right = (rect[2].min(self.size[0] as f32).ceil() as u32).max(left); | |
| 203 | let bottom = (rect[3].min(self.size[1] as f32).ceil() as u32).max(top); | |
| 204 | [left, top, right - left, bottom - top] | |
| 205 | } | |
| 206 | } | |
| 207 | ||
| 208 | fn intersect(a: [u32; 4], b: [u32; 4]) -> [u32; 4] { | |
| 209 | let left = a[0].max(b[0]); | |
| 210 | let top = a[1].max(b[1]); | |
| 211 | let right = (a[0] + a[2]).min(b[0] + b[2]).max(left); | |
| 212 | let bottom = (a[1] + a[3]).min(b[1] + b[3]).max(top); | |
| 213 | [left, top, right - left, bottom - top] | |
| 214 | } | |
| 215 | ||
| 216 | #[derive(Clone, Copy, Debug, PartialEq)] | |
| 217 | pub enum Stroke { | |
| 218 | Solid(f32), | |
| 219 | /// Dash and gap lengths are twice the stroke width. | |
| 220 | Dashed(f32), | |
| 221 | } | |
| 222 | ||
| 223 | /// Coordinates are layer units; colors are linear RGBA. | |
| 224 | pub enum Primitive<'a> { | |
| 225 | Text { | |
| 226 | text: &'a dyn Glyphs, | |
| 227 | origin: [f32; 2], | |
| 228 | /// Layer bounds of the painted glyphs, which keep their full shaping and advances. | |
| 229 | clip: Option<[f32; 4]>, | |
| 230 | }, | |
| 231 | /// 16×16 SVG artwork painted `size` units square, later sources over earlier ones. | |
| 232 | Icon { | |
| 233 | sources: &'static [&'static str], | |
| 234 | origin: [f32; 2], | |
| 235 | size: f32, | |
| 236 | opacity: f32, | |
| 237 | }, | |
| 238 | Rect { | |
| 239 | rect: [f32; 4], | |
| 240 | color: [f32; 4], | |
| 241 | }, | |
| 242 | RoundedRect { | |
| 243 | rect: [f32; 4], | |
| 244 | radius: [f32; 2], | |
| 245 | /// None fills the shape; stroke widths are positive layer units. | |
| 246 | stroke: Option<Stroke>, | |
| 247 | color: [f32; 4], | |
| 248 | }, | |
| 249 | Image { | |
| 250 | image: &'a RasterImage, | |
| 251 | rect: [f32; 4], | |
| 252 | }, | |
| 253 | /// A pen stroke between two points, `width` units across. | |
| 254 | Segment { | |
| 255 | from: [f32; 2], | |
| 256 | to: [f32; 2], | |
| 257 | width: f32, | |
| 258 | /// A round pen tip caps the ends; otherwise they are square. | |
| 259 | round: bool, | |
| 260 | color: [f32; 4], | |
| 261 | }, | |
| 262 | } | |
| 263 | ||
| 264 | #[derive(Debug)] | |
| 265 | pub enum RenderError { | |
| 266 | AtlasFull, | |
| 267 | FrameTooLarge, | |
| 268 | UnreadableFont, | |
| 269 | UnsupportedGlyph, | |
| 270 | InvalidLayer, | |
| 271 | InvalidPrimitive, | |
| 272 | InvalidImage, | |
| 273 | ImageBudget, | |
| 274 | ImageTooLarge, | |
| 275 | ImageDecode(image::ImageError), | |
| 276 | } | |
| 277 | ||
| 278 | /// Cache and buffer use after the last frame, for resource probes. Texture and buffer | |
| 279 | /// sizes are as requested, without driver overhead. | |
| 280 | #[derive(Debug)] | |
| 281 | pub struct Occupancy { | |
| 282 | /// Glyph and icon atlas entries, including glyphs with no pixels. | |
| 283 | pub glyphs: usize, | |
| 284 | pub atlas_texels: u64, | |
| 285 | pub images: usize, | |
| 286 | pub image_bytes: u64, | |
| 287 | pub vertices: usize, | |
| 288 | pub batches: usize, | |
| 289 | pub vertex_capacity_bytes: usize, | |
| 290 | pub batch_capacity_bytes: usize, | |
| 291 | pub glyph_capacity: usize, | |
| 292 | pub image_capacity: usize, | |
| 293 | pub atlas_bytes: u64, | |
| 294 | pub vertex_buffer_bytes: u64, | |
| 295 | pub within_budget: bool, | |
| 296 | } | |
| 297 | ||
| 298 | pub struct Renderer { | |
| 299 | pub device: wgpu::Device, | |
| 300 | pub queue: wgpu::Queue, | |
| 301 | pipeline: wgpu::RenderPipeline, | |
| 302 | image_pipeline: wgpu::RenderPipeline, | |
| 303 | atlas: wgpu::Texture, | |
| 304 | bind_group: wgpu::BindGroup, | |
| 305 | image_sampler: wgpu::Sampler, | |
| 306 | vertex_buffer: wgpu::Buffer, | |
| 307 | vertices: Vec<Vertex>, | |
| 308 | glyphs: HashMap<AtlasKey, Option<AtlasGlyph>>, | |
| 309 | images: HashMap<u64, CachedImage>, | |
| 310 | batches: Vec<Batch>, | |
| 311 | scaler: ScaleContext, | |
| 312 | pen: [u32; 2], | |
| 313 | row_height: u32, | |
| 314 | } | |
| 315 | ||
| 316 | impl Renderer { | |
| 317 | pub fn new(device: wgpu::Device, queue: wgpu::Queue, format: wgpu::TextureFormat) -> Self { | |
| 318 | let shader = device.create_shader_module(wgpu::include_wgsl!("draw.wgsl")); | |
| 319 | let binding_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { | |
| 320 | label: Some("Draw texture"), | |
| 321 | entries: &[ | |
| 322 | wgpu::BindGroupLayoutEntry { | |
| 323 | binding: 0, | |
| 324 | visibility: wgpu::ShaderStages::FRAGMENT, | |
| 325 | ty: wgpu::BindingType::Texture { | |
| 326 | sample_type: wgpu::TextureSampleType::Float { filterable: true }, | |
| 327 | view_dimension: wgpu::TextureViewDimension::D2, | |
| 328 | multisampled: false, | |
| 329 | }, | |
| 330 | count: None, | |
| 331 | }, | |
| 332 | wgpu::BindGroupLayoutEntry { | |
| 333 | binding: 1, | |
| 334 | visibility: wgpu::ShaderStages::FRAGMENT, | |
| 335 | ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering), | |
| 336 | count: None, | |
| 337 | }, | |
| 338 | ], | |
| 339 | }); | |
| 340 | let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor { | |
| 341 | label: Some("Draw"), | |
| 342 | bind_group_layouts: &[Some(&binding_layout)], | |
| 343 | ..Default::default() | |
| 344 | }); | |
| 345 | let make_pipeline = |blend| { | |
| 346 | device.create_render_pipeline(&wgpu::RenderPipelineDescriptor { | |
| 347 | label: Some("Draw"), | |
| 348 | layout: Some(&pipeline_layout), | |
| 349 | vertex: wgpu::VertexState { | |
| 350 | module: &shader, | |
| 351 | entry_point: Some("vertex"), | |
| 352 | compilation_options: Default::default(), | |
| 353 | buffers: &[Some(wgpu::VertexBufferLayout { | |
| 354 | array_stride: size_of::<Vertex>() as u64, | |
| 355 | step_mode: wgpu::VertexStepMode::Vertex, | |
| 356 | attributes: &wgpu::vertex_attr_array![0 => Float32x2, 1 => Float32x2, 2 => Float32x4, 3 => Float32x2, 4 => Float32x4, 5 => Float32], | |
| 357 | })], | |
| 358 | }, | |
| 359 | fragment: Some(wgpu::FragmentState { | |
| 360 | module: &shader, | |
| 361 | entry_point: Some("fragment"), | |
| 362 | compilation_options: Default::default(), | |
| 363 | targets: &[Some(wgpu::ColorTargetState { | |
| 364 | format, | |
| 365 | blend: Some(blend), | |
| 366 | write_mask: wgpu::ColorWrites::ALL, | |
| 367 | })], | |
| 368 | }), | |
| 369 | primitive: Default::default(), | |
| 370 | depth_stencil: None, | |
| 371 | multisample: Default::default(), | |
| 372 | multiview_mask: None, | |
| 373 | cache: None, | |
| 374 | }) | |
| 375 | }; | |
| 376 | let pipeline = make_pipeline(wgpu::BlendState::ALPHA_BLENDING); | |
| 377 | let image_pipeline = make_pipeline(wgpu::BlendState::PREMULTIPLIED_ALPHA_BLENDING); | |
| 378 | let atlas = device.create_texture(&wgpu::TextureDescriptor { | |
| 379 | label: Some("Glyph atlas"), | |
| 380 | size: wgpu::Extent3d { | |
| 381 | width: ATLAS_SIZE, | |
| 382 | height: ATLAS_SIZE, | |
| 383 | depth_or_array_layers: 1, | |
| 384 | }, | |
| 385 | mip_level_count: 1, | |
| 386 | sample_count: 1, | |
| 387 | dimension: wgpu::TextureDimension::D2, | |
| 388 | format: wgpu::TextureFormat::Rgba8UnormSrgb, | |
| 389 | usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST, | |
| 390 | view_formats: &[], | |
| 391 | }); | |
| 392 | let view = atlas.create_view(&Default::default()); | |
| 393 | let sampler = device.create_sampler(&wgpu::SamplerDescriptor { | |
| 394 | mag_filter: wgpu::FilterMode::Nearest, | |
| 395 | min_filter: wgpu::FilterMode::Nearest, | |
| 396 | ..Default::default() | |
| 397 | }); | |
| 398 | let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor { | |
| 399 | label: Some("Glyph atlas"), | |
| 400 | layout: &pipeline.get_bind_group_layout(0), | |
| 401 | entries: &[ | |
| 402 | wgpu::BindGroupEntry { | |
| 403 | binding: 0, | |
| 404 | resource: wgpu::BindingResource::TextureView(&view), | |
| 405 | }, | |
| 406 | wgpu::BindGroupEntry { | |
| 407 | binding: 1, | |
| 408 | resource: wgpu::BindingResource::Sampler(&sampler), | |
| 409 | }, | |
| 410 | ], | |
| 411 | }); | |
| 412 | let vertex_buffer = device.create_buffer(&wgpu::BufferDescriptor { | |
| 413 | label: Some("Draw vertices"), | |
| 414 | size: (MAX_VERTICES * size_of::<Vertex>()) as u64, | |
| 415 | usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST, | |
| 416 | mapped_at_creation: false, | |
| 417 | }); | |
| 418 | let image_sampler = device.create_sampler(&wgpu::SamplerDescriptor { | |
| 419 | label: Some("Draw image filtering"), | |
| 420 | mag_filter: wgpu::FilterMode::Linear, | |
| 421 | min_filter: wgpu::FilterMode::Linear, | |
| 422 | ..Default::default() | |
| 423 | }); | |
| 424 | queue.write_texture( | |
| 425 | atlas.as_image_copy(), | |
| 426 | &[255; 4], | |
| 427 | wgpu::TexelCopyBufferLayout { | |
| 428 | offset: 0, | |
| 429 | bytes_per_row: Some(4), | |
| 430 | rows_per_image: Some(1), | |
| 431 | }, | |
| 432 | wgpu::Extent3d { | |
| 433 | width: 1, | |
| 434 | height: 1, | |
| 435 | depth_or_array_layers: 1, | |
| 436 | }, | |
| 437 | ); | |
| 438 | Self { | |
| 439 | device, | |
| 440 | queue, | |
| 441 | pipeline, | |
| 442 | image_pipeline, | |
| 443 | atlas, | |
| 444 | bind_group, | |
| 445 | image_sampler, | |
| 446 | vertex_buffer, | |
| 447 | vertices: Vec::new(), | |
| 448 | glyphs: HashMap::new(), | |
| 449 | images: HashMap::new(), | |
| 450 | batches: Vec::new(), | |
| 451 | scaler: ScaleContext::with_max_entries(32), | |
| 452 | pen: [1, 0], | |
| 453 | row_height: 1, | |
| 454 | } | |
| 455 | } | |
| 456 | ||
| 457 | pub fn clear_glyph_cache(&mut self) { | |
| 458 | self.glyphs.clear(); | |
| 459 | self.pen = [1, 0]; | |
| 460 | self.row_height = 1; | |
| 461 | } | |
| 462 | ||
| 463 | /// Forgets uploaded images; they upload again when next painted. | |
| 464 | pub fn clear_images(&mut self) { | |
| 465 | self.images.clear(); | |
| 466 | } | |
| 467 | ||
| 468 | pub fn occupancy(&self) -> Occupancy { | |
| 469 | let image_bytes = self.images.values().map(|image| image.bytes).sum(); | |
| 470 | Occupancy { | |
| 471 | glyphs: self.glyphs.len(), | |
| 472 | atlas_texels: self | |
| 473 | .glyphs | |
| 474 | .values() | |
| 475 | .flatten() | |
| 476 | .map(|glyph| u64::from(glyph.width) * u64::from(glyph.height)) | |
| 477 | .sum(), | |
| 478 | images: self.images.len(), | |
| 479 | image_bytes, | |
| 480 | vertices: self.vertices.len(), | |
| 481 | batches: self.batches.len(), | |
| 482 | vertex_capacity_bytes: self.vertices.capacity() * size_of::<Vertex>(), | |
| 483 | batch_capacity_bytes: self.batches.capacity() * size_of::<Batch>(), | |
| 484 | glyph_capacity: self.glyphs.capacity(), | |
| 485 | image_capacity: self.images.capacity(), | |
| 486 | atlas_bytes: u64::from(self.atlas.width()) * u64::from(self.atlas.height()) * 4, | |
| 487 | vertex_buffer_bytes: self.vertex_buffer.size(), | |
| 488 | within_budget: self.glyphs.len() <= MAX_GLYPHS | |
| 489 | && self.images.len() <= MAX_IMAGES | |
| 490 | && image_bytes <= MAX_IMAGE_BYTES | |
| 491 | && self.vertices.len() <= MAX_VERTICES, | |
| 492 | } | |
| 493 | } | |
| 494 | ||
| 495 | /// Clears `target`, `size` device pixels, to linear `clear` and paints the layers in order. | |
| 496 | pub fn draw( | |
| 497 | &mut self, | |
| 498 | target: &wgpu::TextureView, | |
| 499 | size: [u32; 2], | |
| 500 | clear: [f32; 4], | |
| 501 | layers: &[Layer<'_>], | |
| 502 | ) -> Result<(), RenderError> { | |
| 503 | if size.contains(&0) | |
| 504 | || clear.iter().any(|v| !v.is_finite()) | |
| 505 | || layers.iter().any(|layer| { | |
| 506 | !layer.scale.is_finite() | |
| 507 | || layer.scale <= 0.0 | |
| 508 | || layer.origin.iter().any(|v| !v.is_finite()) | |
| 509 | || layer | |
| 510 | .clip | |
| 511 | .is_some_and(|clip| clip.iter().any(|v| !v.is_finite())) | |
| 512 | }) | |
| 513 | { | |
| 514 | return Err(RenderError::InvalidLayer); | |
| 515 | } | |
| 516 | let mut active_images = HashSet::new(); | |
| 517 | let mut image_bytes = 0; | |
| 518 | for layer in layers { | |
| 519 | let space = Space { | |
| 520 | size, | |
| 521 | scale: layer.scale, | |
| 522 | origin: layer.origin, | |
| 523 | }; | |
| 524 | for primitive in layer.primitives { | |
| 525 | if let Primitive::Image { image, rect } = primitive | |
| 526 | && space.visible_rect(*rect)?.is_some() | |
| 527 | && active_images.insert(image.id()) | |
| 528 | { | |
| 529 | image_bytes += image.pixels().len() as u64; | |
| 530 | if image_bytes > MAX_IMAGE_BYTES || active_images.len() > MAX_IMAGES { | |
| 531 | return Err(RenderError::ImageBudget); | |
| 532 | } | |
| 533 | } | |
| 534 | } | |
| 535 | } | |
| 536 | for attempt in 0..2 { | |
| 537 | self.vertices.clear(); | |
| 538 | self.batches.clear(); | |
| 539 | match self.prepare(size, layers, &active_images) { | |
| 540 | Err(RenderError::AtlasFull) if attempt == 0 => self.clear_glyph_cache(), | |
| 541 | result => { | |
| 542 | result?; | |
| 543 | break; | |
| 544 | } | |
| 545 | } | |
| 546 | } | |
| 547 | self.queue | |
| 548 | .write_buffer(&self.vertex_buffer, 0, bytemuck::cast_slice(&self.vertices)); | |
| 549 | let mut encoder = self.device.create_command_encoder(&Default::default()); | |
| 550 | { | |
| 551 | let [r, g, b, a] = clear.map(f64::from); | |
| 552 | let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor { | |
| 553 | label: Some("Draw"), | |
| 554 | color_attachments: &[Some(wgpu::RenderPassColorAttachment { | |
| 555 | view: target, | |
| 556 | depth_slice: None, | |
| 557 | resolve_target: None, | |
| 558 | ops: wgpu::Operations { | |
| 559 | load: wgpu::LoadOp::Clear(wgpu::Color { r, g, b, a }), | |
| 560 | store: wgpu::StoreOp::Store, | |
| 561 | }, | |
| 562 | })], | |
| 563 | ..Default::default() | |
| 564 | }); | |
| 565 | pass.set_vertex_buffer(0, self.vertex_buffer.slice(..)); | |
| 566 | for batch in &self.batches { | |
| 567 | let [x, y, width, height] = batch.scissor; | |
| 568 | pass.set_scissor_rect(x, y, width, height); | |
| 569 | pass.set_pipeline(if batch.image.is_some() { | |
| 570 | &self.image_pipeline | |
| 571 | } else { | |
| 572 | &self.pipeline | |
| 573 | }); | |
| 574 | let binding = batch | |
| 575 | .image | |
| 576 | .map(|id| &self.images[&id].binding) | |
| 577 | .unwrap_or(&self.bind_group); | |
| 578 | pass.set_bind_group(0, binding, &[]); | |
| 579 | pass.draw(batch.vertices.clone(), 0..1); | |
| 580 | } | |
| 581 | } | |
| 582 | self.queue.submit([encoder.finish()]); | |
| 583 | Ok(()) | |
| 584 | } | |
| 585 | ||
| 586 | fn prepare( | |
| 587 | &mut self, | |
| 588 | size: [u32; 2], | |
| 589 | layers: &[Layer<'_>], | |
| 590 | active_images: &HashSet<u64>, | |
| 591 | ) -> Result<(), RenderError> { | |
| 592 | for layer in layers { | |
| 593 | let space = Space { | |
| 594 | size, | |
| 595 | scale: layer.scale, | |
| 596 | origin: layer.origin, | |
| 597 | }; | |
| 598 | let bounds = match layer.clip { | |
| 599 | Some(clip) => space.scissor(clip), | |
| 600 | None => [0, 0, size[0], size[1]], | |
| 601 | }; | |
| 602 | if bounds[2] == 0 || bounds[3] == 0 { | |
| 603 | continue; | |
| 604 | } | |
| 605 | for primitive in layer.primitives { | |
| 606 | self.primitive(space, bounds, primitive, active_images)?; | |
| 607 | } | |
| 608 | } | |
| 609 | Ok(()) | |
| 610 | } | |
| 611 | ||
| 612 | fn primitive( | |
| 613 | &mut self, | |
| 614 | space: Space, | |
| 615 | bounds: [u32; 4], | |
| 616 | primitive: &Primitive<'_>, | |
| 617 | active_images: &HashSet<u64>, | |
| 618 | ) -> Result<(), RenderError> { | |
| 619 | let start = self.vertices.len() as u32; | |
| 620 | let mut image_id = None; | |
| 621 | let mut scissor = bounds; | |
| 622 | match primitive { | |
| 623 | Primitive::Icon { | |
| 624 | sources, | |
| 625 | origin, | |
| 626 | size, | |
| 627 | opacity, | |
| 628 | } => { | |
| 629 | let origin = [ | |
| 630 | space.origin[0] + origin[0] * space.scale, | |
| 631 | space.origin[1] + origin[1] * space.scale, | |
| 632 | ]; | |
| 633 | self.icon(Space { origin, ..space }, sources, *size, *opacity)?; | |
| 634 | } | |
| 635 | Primitive::Text { text, origin, clip } => { | |
| 636 | let origin = [ | |
| 637 | space.origin[0] + origin[0] * space.scale, | |
| 638 | space.origin[1] + origin[1] * space.scale, | |
| 639 | ]; | |
| 640 | if origin.iter().any(|v| !v.is_finite()) { | |
| 641 | return Err(RenderError::InvalidPrimitive); | |
| 642 | } | |
| 643 | if let Some(clip) = clip { | |
| 644 | let Some(rect) = space.visible_rect(*clip)? else { | |
| 645 | return Ok(()); | |
| 646 | }; | |
| 647 | scissor = intersect(bounds, space.scissor(rect)); | |
| 648 | if scissor[2] == 0 || scissor[3] == 0 { | |
| 649 | return Ok(()); | |
| 650 | } | |
| 651 | } | |
| 652 | let space = Space { origin, ..space }; | |
| 653 | text.runs(&mut |run| self.glyph_run(space, run))?; | |
| 654 | } | |
| 655 | Primitive::Rect { rect, color } => { | |
| 656 | self.quad( | |
| 657 | space, | |
| 658 | [ | |
| 659 | rect[0] * space.scale + space.origin[0], | |
| 660 | rect[1] * space.scale + space.origin[1], | |
| 661 | rect[2] * space.scale + space.origin[0], | |
| 662 | rect[3] * space.scale + space.origin[1], | |
| 663 | ], | |
| 664 | [0.5 / ATLAS_SIZE as f32; 4], | |
| 665 | *color, | |
| 666 | )?; | |
| 667 | } | |
| 668 | Primitive::RoundedRect { | |
| 669 | rect, | |
| 670 | radius, | |
| 671 | stroke, | |
| 672 | color, | |
| 673 | } => { | |
| 674 | if radius | |
| 675 | .iter() | |
| 676 | .any(|value| !value.is_finite() || *value < 0.0) | |
| 677 | { | |
| 678 | return Err(RenderError::InvalidPrimitive); | |
| 679 | } | |
| 680 | let width = match stroke { | |
| 681 | None => 0.0, | |
| 682 | Some(Stroke::Solid(width) | Stroke::Dashed(width)) => { | |
| 683 | if !width.is_finite() || *width <= 0.0 || width * space.scale == 0.0 { | |
| 684 | return Err(RenderError::InvalidPrimitive); | |
| 685 | } | |
| 686 | *width | |
| 687 | } | |
| 688 | }; | |
| 689 | let rect = [ | |
| 690 | rect[0] * space.scale + space.origin[0], | |
| 691 | rect[1] * space.scale + space.origin[1], | |
| 692 | rect[2] * space.scale + space.origin[0], | |
| 693 | rect[3] * space.scale + space.origin[1], | |
| 694 | ]; | |
| 695 | self.quad(space, rect, [0.5 / ATLAS_SIZE as f32; 4], *color)?; | |
| 696 | let half = [(rect[2] - rect[0]) * 0.5, (rect[3] - rect[1]) * 0.5]; | |
| 697 | if !(4.0 * (half[0] + half[1])).is_finite() { | |
| 698 | return Err(RenderError::InvalidPrimitive); | |
| 699 | } | |
| 700 | let radius = if radius.contains(&0.0) { | |
| 701 | [0.0; 2] | |
| 702 | } else { | |
| 703 | std::array::from_fn(|axis| (radius[axis] * space.scale).min(half[axis])) | |
| 704 | }; | |
| 705 | let width = (width * space.scale).min(half[0].min(half[1])); | |
| 706 | // The stroke sign selects its dash pattern at the shader boundary. | |
| 707 | let width = if matches!(stroke, Some(Stroke::Dashed(_))) { | |
| 708 | -width | |
| 709 | } else { | |
| 710 | width | |
| 711 | }; | |
| 712 | for vertex in &mut self.vertices[start as usize..] { | |
| 713 | vertex.shape = [half[0], half[1], radius[0], radius[1]]; | |
| 714 | vertex.stroke = width; | |
| 715 | } | |
| 716 | } | |
| 717 | Primitive::Segment { | |
| 718 | from, | |
| 719 | to, | |
| 720 | width, | |
| 721 | round, | |
| 722 | color, | |
| 723 | } => self.segment(space, *from, *to, *width, *round, *color)?, | |
| 724 | Primitive::Image { image, rect } => { | |
| 725 | if let Some(rect) = space.visible_rect(*rect)? { | |
| 726 | self.image(image, active_images)?; | |
| 727 | image_id = Some(image.id()); | |
| 728 | self.quad(space, rect, [0.0, 0.0, 1.0, 1.0], [1.0; 4])?; | |
| 729 | } | |
| 730 | } | |
| 731 | } | |
| 732 | let end = self.vertices.len() as u32; | |
| 733 | if start != end { | |
| 734 | if let Some(last) = self.batches.last_mut() | |
| 735 | && last.image == image_id | |
| 736 | && last.scissor == scissor | |
| 737 | { | |
| 738 | last.vertices.end = end; | |
| 739 | } else { | |
| 740 | self.batches.push(Batch { | |
| 741 | vertices: start..end, | |
| 742 | image: image_id, | |
| 743 | scissor, | |
| 744 | }); | |
| 745 | } | |
| 746 | } | |
| 747 | Ok(()) | |
| 748 | } | |
| 749 | ||
| 750 | fn image(&mut self, image: &RasterImage, active: &HashSet<u64>) -> Result<(), RenderError> { | |
| 751 | if self.images.contains_key(&image.id()) { | |
| 752 | return Ok(()); | |
| 753 | } | |
| 754 | if image | |
| 755 | .size | |
| 756 | .iter() | |
| 757 | .any(|v| *v > self.device.limits().max_texture_dimension_2d) | |
| 758 | { | |
| 759 | return Err(RenderError::ImageTooLarge); | |
| 760 | } | |
| 761 | let bytes = image.pixels().len() as u64; | |
| 762 | while self.images.values().map(|i| i.bytes).sum::<u64>() + bytes > MAX_IMAGE_BYTES | |
| 763 | || self.images.len() >= MAX_IMAGES | |
| 764 | { | |
| 765 | let id = self | |
| 766 | .images | |
| 767 | .keys() | |
| 768 | .find(|id| !active.contains(id)) | |
| 769 | .copied() | |
| 770 | .ok_or(RenderError::ImageBudget)?; | |
| 771 | self.images.remove(&id); | |
| 772 | } | |
| 773 | let size = wgpu::Extent3d { | |
| 774 | width: image.size[0], | |
| 775 | height: image.size[1], | |
| 776 | depth_or_array_layers: 1, | |
| 777 | }; | |
| 778 | let texture = self.device.create_texture(&wgpu::TextureDescriptor { | |
| 779 | label: Some("Draw image"), | |
| 780 | size, | |
| 781 | mip_level_count: 1, | |
| 782 | sample_count: 1, | |
| 783 | dimension: wgpu::TextureDimension::D2, | |
| 784 | format: wgpu::TextureFormat::Rgba8UnormSrgb, | |
| 785 | usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST, | |
| 786 | view_formats: &[], | |
| 787 | }); | |
| 788 | self.queue.write_texture( | |
| 789 | texture.as_image_copy(), | |
| 790 | image.pixels(), | |
| 791 | wgpu::TexelCopyBufferLayout { | |
| 792 | offset: 0, | |
| 793 | bytes_per_row: Some(image.size[0] * 4), | |
| 794 | rows_per_image: Some(image.size[1]), | |
| 795 | }, | |
| 796 | size, | |
| 797 | ); | |
| 798 | let binding = self.device.create_bind_group(&wgpu::BindGroupDescriptor { | |
| 799 | label: Some("Draw image"), | |
| 800 | layout: &self.pipeline.get_bind_group_layout(0), | |
| 801 | entries: &[ | |
| 802 | wgpu::BindGroupEntry { | |
| 803 | binding: 0, | |
| 804 | resource: wgpu::BindingResource::TextureView( | |
| 805 | &texture.create_view(&Default::default()), | |
| 806 | ), | |
| 807 | }, | |
| 808 | wgpu::BindGroupEntry { | |
| 809 | binding: 1, | |
| 810 | resource: wgpu::BindingResource::Sampler(&self.image_sampler), | |
| 811 | }, | |
| 812 | ], | |
| 813 | }); | |
| 814 | self.images | |
| 815 | .insert(image.id(), CachedImage { binding, bytes }); | |
| 816 | Ok(()) | |
| 817 | } | |
| 818 | ||
| 819 | fn glyph_run(&mut self, space: Space, run: GlyphRun<'_>) -> Result<(), RenderError> { | |
| 820 | let scale = space.scale; | |
| 821 | let origin = space.origin; | |
| 822 | let line_top = run.line[0] * scale + origin[1]; | |
| 823 | let line_height = run.line[1] * scale; | |
| 824 | if line_top + line_height < 0.0 || line_top > space.size[1] as f32 { | |
| 825 | return Ok(()); | |
| 826 | } | |
| 827 | let size = run.size * scale; | |
| 828 | if !size.is_finite() || size > ATLAS_SIZE as f32 { | |
| 829 | return Err(RenderError::AtlasFull); | |
| 830 | } | |
| 831 | for glyph in run.glyphs { | |
| 832 | let x = glyph.x * scale + origin[0]; | |
| 833 | let y = glyph.y * scale + origin[1]; | |
| 834 | // Quantization affects raster coverage only, never advances or line breaks. | |
| 835 | let x = (x * 4.0).round() * 0.25; | |
| 836 | let y = (y * 4.0).round() * 0.25; | |
| 837 | let glyph_id = glyph | |
| 838 | .id | |
| 839 | .try_into() | |
| 840 | .map_err(|_| RenderError::UnsupportedGlyph)?; | |
| 841 | let phase = [((x - x.floor()) * 4.0) as u8, ((y - y.floor()) * 4.0) as u8]; | |
| 842 | let key = AtlasKey::Text { | |
| 843 | font: run.font.id(), | |
| 844 | index: run.index, | |
| 845 | glyph: glyph_id, | |
| 846 | size: size.to_bits(), | |
| 847 | coords: run.coords.to_vec(), | |
| 848 | phase, | |
| 849 | embolden: run.embolden, | |
| 850 | skew: run.skew.unwrap_or(0.0).to_bits(), | |
| 851 | }; | |
| 852 | let cached = if let Some(cached) = self.glyphs.get(&key) { | |
| 853 | *cached | |
| 854 | } else { | |
| 855 | if self.glyphs.len() >= MAX_GLYPHS { | |
| 856 | return Err(RenderError::AtlasFull); | |
| 857 | } | |
| 858 | let font = FontRef::from_index(run.font.as_ref(), run.index as usize) | |
| 859 | .ok_or(RenderError::UnreadableFont)?; | |
| 860 | let mut scaler = self | |
| 861 | .scaler | |
| 862 | .builder_with_id(font, [run.font.id(), u64::from(run.index)]) | |
| 863 | .size(size) | |
| 864 | .hint(false) | |
| 865 | .normalized_coords(run.coords.iter().copied()) | |
| 866 | .build(); | |
| 867 | let mut render = Render::new(&[ | |
| 868 | Source::ColorOutline(0), | |
| 869 | Source::ColorBitmap(StrikeWith::BestFit), | |
| 870 | Source::Outline, | |
| 871 | ]); | |
| 872 | // Swash rasterizes outlines in Y-up coordinates. | |
| 873 | render.format(Format::Alpha).offset(Vector::new( | |
| 874 | f32::from(phase[0]) * 0.25, | |
| 875 | -f32::from(phase[1]) * 0.25, | |
| 876 | )); | |
| 877 | if run.embolden { | |
| 878 | render.embolden(size / 24.0); | |
| 879 | } | |
| 880 | if let Some(skew) = run.skew { | |
| 881 | render.transform(Some(Transform::skew( | |
| 882 | Angle::from_degrees(skew), | |
| 883 | Angle::from_degrees(0.0), | |
| 884 | ))); | |
| 885 | } | |
| 886 | let image = render.render(&mut scaler, glyph_id); | |
| 887 | let cached = if let Some(image) = | |
| 888 | image.filter(|i| i.placement.width > 0 && i.placement.height > 0) | |
| 889 | { | |
| 890 | Some(self.upload(image)?) | |
| 891 | } else { | |
| 892 | None | |
| 893 | }; | |
| 894 | self.glyphs.insert(key, cached); | |
| 895 | cached | |
| 896 | }; | |
| 897 | if let Some(glyph) = cached { | |
| 898 | let mut left = x.floor() + glyph.left as f32; | |
| 899 | let mut top = y.floor() - glyph.top as f32; | |
| 900 | let mut width = glyph.width as f32; | |
| 901 | let mut height = glyph.height as f32; | |
| 902 | if glyph.color { | |
| 903 | let ratio = (line_height / height).min(1.0); | |
| 904 | left += width * (1.0 - ratio) * 0.5; | |
| 905 | width *= ratio; | |
| 906 | height *= ratio; | |
| 907 | top = top.clamp(line_top, (line_top + line_height - height).max(line_top)); | |
| 908 | } | |
| 909 | let atlas_size = ATLAS_SIZE as f32; | |
| 910 | self.quad( | |
| 911 | space, | |
| 912 | [left, top, left + width, top + height], | |
| 913 | [ | |
| 914 | glyph.x as f32 / atlas_size, | |
| 915 | glyph.y as f32 / atlas_size, | |
| 916 | (glyph.x + glyph.width) as f32 / atlas_size, | |
| 917 | (glyph.y + glyph.height) as f32 / atlas_size, | |
| 918 | ], | |
| 919 | if glyph.color { [1.0; 4] } else { run.color }, | |
| 920 | )?; | |
| 921 | } | |
| 922 | } | |
| 923 | for decoration in run.decorations { | |
| 924 | let x = decoration.x * scale + origin[0]; | |
| 925 | let y = decoration.y * scale + origin[1]; | |
| 926 | self.quad( | |
| 927 | space, | |
| 928 | [ | |
| 929 | x, | |
| 930 | y, | |
| 931 | x + decoration.width * scale, | |
| 932 | y + (decoration.thickness * scale).max(1.0), | |
| 933 | ], | |
| 934 | [0.5 / ATLAS_SIZE as f32; 4], | |
| 935 | decoration.color, | |
| 936 | )?; | |
| 937 | } | |
| 938 | Ok(()) | |
| 939 | } | |
| 940 | ||
| 941 | fn upload(&mut self, image: swash::scale::image::Image) -> Result<AtlasGlyph, RenderError> { | |
| 942 | let p = image.placement; | |
| 943 | if p.width + 2 > ATLAS_SIZE || p.height + 2 > ATLAS_SIZE { | |
| 944 | return Err(RenderError::AtlasFull); | |
| 945 | } | |
| 946 | if self.pen[0] + p.width + 1 > ATLAS_SIZE { | |
| 947 | self.pen[0] = 0; | |
| 948 | self.pen[1] += self.row_height + 1; | |
| 949 | self.row_height = 0; | |
| 950 | } | |
| 951 | if self.pen[1] + p.height + 1 > ATLAS_SIZE { | |
| 952 | return Err(RenderError::AtlasFull); | |
| 953 | } | |
| 954 | let cached = AtlasGlyph { | |
| 955 | x: self.pen[0], | |
| 956 | y: self.pen[1], | |
| 957 | width: p.width, | |
| 958 | height: p.height, | |
| 959 | left: p.left, | |
| 960 | top: p.top, | |
| 961 | color: image.content == Content::Color, | |
| 962 | }; | |
| 963 | let rgba = match image.content { | |
| 964 | Content::Mask => image | |
| 965 | .data | |
| 966 | .iter() | |
| 967 | .flat_map(|a| [255, 255, 255, *a]) | |
| 968 | .collect(), | |
| 969 | _ => image.data, | |
| 970 | }; | |
| 971 | self.queue.write_texture( | |
| 972 | wgpu::TexelCopyTextureInfo { | |
| 973 | origin: wgpu::Origin3d { | |
| 974 | x: cached.x, | |
| 975 | y: cached.y, | |
| 976 | z: 0, | |
| 977 | }, | |
| 978 | ..self.atlas.as_image_copy() | |
| 979 | }, | |
| 980 | &rgba, | |
| 981 | wgpu::TexelCopyBufferLayout { | |
| 982 | offset: 0, | |
| 983 | bytes_per_row: Some(p.width * 4), | |
| 984 | rows_per_image: Some(p.height), | |
| 985 | }, | |
| 986 | wgpu::Extent3d { | |
| 987 | width: p.width, | |
| 988 | height: p.height, | |
| 989 | depth_or_array_layers: 1, | |
| 990 | }, | |
| 991 | ); | |
| 992 | self.pen[0] += p.width + 1; | |
| 993 | self.row_height = self.row_height.max(p.height); | |
| 994 | Ok(cached) | |
| 995 | } | |
| 996 | ||
| 997 | fn icon( | |
| 998 | &mut self, | |
| 999 | space: Space, | |
| 1000 | sources: &'static [&'static str], | |
| 1001 | size: f32, | |
| 1002 | opacity: f32, | |
| 1003 | ) -> Result<(), RenderError> { | |
| 1004 | let size = size * space.scale; | |
| 1005 | let [x, y] = space.origin.map(|v| (v * 4.0).round() * 0.25); | |
| 1006 | if !size.is_finite() || [x, y].iter().any(|v| !v.is_finite()) { | |
| 1007 | return Err(RenderError::InvalidPrimitive); | |
| 1008 | } | |
| 1009 | if x + size + 1.0 < 0.0 | |
| 1010 | || y + size + 1.0 < 0.0 | |
| 1011 | || x - 1.0 > space.size[0] as f32 | |
| 1012 | || y - 1.0 > space.size[1] as f32 | |
| 1013 | { | |
| 1014 | return Ok(()); | |
| 1015 | } | |
| 1016 | if size + 3.0 > ATLAS_SIZE as f32 { | |
| 1017 | return Err(RenderError::AtlasFull); | |
| 1018 | } | |
| 1019 | let phase = [((x - x.floor()) * 4.0) as u8, ((y - y.floor()) * 4.0) as u8]; | |
| 1020 | let key = AtlasKey::Icon { | |
| 1021 | sources, | |
| 1022 | size: size.to_bits(), | |
| 1023 | phase, | |
| 1024 | }; | |
| 1025 | let glyph = if let Some(Some(glyph)) = self.glyphs.get(&key) { | |
| 1026 | *glyph | |
| 1027 | } else { | |
| 1028 | if self.glyphs.len() >= MAX_GLYPHS { | |
| 1029 | return Err(RenderError::AtlasFull); | |
| 1030 | } | |
| 1031 | let glyph = self.upload(icon::rasterize(sources, size, phase))?; | |
| 1032 | self.glyphs.insert(key, Some(glyph)); | |
| 1033 | glyph | |
| 1034 | }; | |
| 1035 | let left = x.floor(); | |
| 1036 | let top = y.floor(); | |
| 1037 | let atlas = ATLAS_SIZE as f32; | |
| 1038 | self.quad( | |
| 1039 | space, | |
| 1040 | [ | |
| 1041 | left, | |
| 1042 | top, | |
| 1043 | left + glyph.width as f32, | |
| 1044 | top + glyph.height as f32, | |
| 1045 | ], | |
| 1046 | [ | |
| 1047 | glyph.x as f32 / atlas, | |
| 1048 | glyph.y as f32 / atlas, | |
| 1049 | (glyph.x + glyph.width) as f32 / atlas, | |
| 1050 | (glyph.y + glyph.height) as f32 / atlas, | |
| 1051 | ], | |
| 1052 | [1.0, 1.0, 1.0, opacity], | |
| 1053 | ) | |
| 1054 | } | |
| 1055 | ||
| 1056 | /// Draws the segment as a capsule in its own frame, reusing the rounded-rectangle distance. | |
| 1057 | fn segment( | |
| 1058 | &mut self, | |
| 1059 | space: Space, | |
| 1060 | from: [f32; 2], | |
| 1061 | to: [f32; 2], | |
| 1062 | width: f32, | |
| 1063 | round: bool, | |
| 1064 | color: [f32; 4], | |
| 1065 | ) -> Result<(), RenderError> { | |
| 1066 | let pixel = |p: [f32; 2]| { | |
| 1067 | [ | |
| 1068 | p[0] * space.scale + space.origin[0], | |
| 1069 | p[1] * space.scale + space.origin[1], | |
| 1070 | ] | |
| 1071 | }; | |
| 1072 | let [from, to] = [pixel(from), pixel(to)]; | |
| 1073 | // Hairlines stay one device pixel wide. | |
| 1074 | let radius = (width * space.scale).max(1.0) * 0.5; | |
| 1075 | if from.iter().chain(&to).chain(&color).any(|v| !v.is_finite()) || !radius.is_finite() { | |
| 1076 | return Err(RenderError::InvalidPrimitive); | |
| 1077 | } | |
| 1078 | let pad = radius + 1.0; | |
| 1079 | if from[0].max(to[0]) + pad < 0.0 | |
| 1080 | || from[1].max(to[1]) + pad < 0.0 | |
| 1081 | || from[0].min(to[0]) - pad > space.size[0] as f32 | |
| 1082 | || from[1].min(to[1]) - pad > space.size[1] as f32 | |
| 1083 | { | |
| 1084 | return Ok(()); | |
| 1085 | } | |
| 1086 | if self.vertices.len() + 6 > MAX_VERTICES { | |
| 1087 | return Err(RenderError::FrameTooLarge); | |
| 1088 | } | |
| 1089 | let delta = [to[0] - from[0], to[1] - from[1]]; | |
| 1090 | let length = delta[0].hypot(delta[1]); | |
| 1091 | let along = if length > 0.0 { | |
| 1092 | [delta[0] / length, delta[1] / length] | |
| 1093 | } else { | |
| 1094 | [1.0, 0.0] | |
| 1095 | }; | |
| 1096 | let across = [-along[1], along[0]]; | |
| 1097 | let center = [(from[0] + to[0]) * 0.5, (from[1] + to[1]) * 0.5]; | |
| 1098 | let half = [length * 0.5 + radius, radius]; | |
| 1099 | let corner = if round { radius } else { 0.0 }; | |
| 1100 | let [hx, hy] = [half[0] + 1.0, half[1] + 1.0]; | |
| 1101 | for local in [ | |
| 1102 | [-hx, -hy], | |
| 1103 | [-hx, hy], | |
| 1104 | [hx, hy], | |
| 1105 | [-hx, -hy], | |
| 1106 | [hx, hy], | |
| 1107 | [hx, -hy], | |
| 1108 | ] { | |
| 1109 | let x = center[0] + along[0] * local[0] + across[0] * local[1]; | |
| 1110 | let y = center[1] + along[1] * local[0] + across[1] * local[1]; | |
| 1111 | self.vertices.push(Vertex { | |
| 1112 | position: [ | |
| 1113 | x * 2.0 / space.size[0] as f32 - 1.0, | |
| 1114 | 1.0 - y * 2.0 / space.size[1] as f32, | |
| 1115 | ], | |
| 1116 | uv: [0.5 / ATLAS_SIZE as f32; 2], | |
| 1117 | color, | |
| 1118 | local, | |
| 1119 | shape: [half[0], half[1], corner, corner], | |
| 1120 | stroke: 0.0, | |
| 1121 | }); | |
| 1122 | } | |
| 1123 | Ok(()) | |
| 1124 | } | |
| 1125 | ||
| 1126 | fn quad( | |
| 1127 | &mut self, | |
| 1128 | space: Space, | |
| 1129 | rect: [f32; 4], | |
| 1130 | uv: [f32; 4], | |
| 1131 | color: [f32; 4], | |
| 1132 | ) -> Result<(), RenderError> { | |
| 1133 | if rect.iter().chain(&color).any(|v| !v.is_finite()) | |
| 1134 | || rect[2] < rect[0] | |
| 1135 | || rect[3] < rect[1] | |
| 1136 | { | |
| 1137 | return Err(RenderError::InvalidPrimitive); | |
| 1138 | } | |
| 1139 | if rect[2] < 0.0 | |
| 1140 | || rect[3] < 0.0 | |
| 1141 | || rect[0] > space.size[0] as f32 | |
| 1142 | || rect[1] > space.size[1] as f32 | |
| 1143 | { | |
| 1144 | return Ok(()); | |
| 1145 | } | |
| 1146 | if self.vertices.len() + 6 > MAX_VERTICES { | |
| 1147 | return Err(RenderError::FrameTooLarge); | |
| 1148 | } | |
| 1149 | let x0 = rect[0] * 2.0 / space.size[0] as f32 - 1.0; | |
| 1150 | let x1 = rect[2] * 2.0 / space.size[0] as f32 - 1.0; | |
| 1151 | let y0 = 1.0 - rect[1] * 2.0 / space.size[1] as f32; | |
| 1152 | let y1 = 1.0 - rect[3] * 2.0 / space.size[1] as f32; | |
| 1153 | if [x0, x1, y0, y1].iter().any(|v| !v.is_finite()) { | |
| 1154 | return Err(RenderError::InvalidPrimitive); | |
| 1155 | } | |
| 1156 | let [hx, hy] = [(rect[2] - rect[0]) * 0.5, (rect[3] - rect[1]) * 0.5]; | |
| 1157 | for (position, uv, local) in [ | |
| 1158 | ([x0, y0], [uv[0], uv[1]], [-hx, -hy]), | |
| 1159 | ([x0, y1], [uv[0], uv[3]], [-hx, hy]), | |
| 1160 | ([x1, y1], [uv[2], uv[3]], [hx, hy]), | |
| 1161 | ([x0, y0], [uv[0], uv[1]], [-hx, -hy]), | |
| 1162 | ([x1, y1], [uv[2], uv[3]], [hx, hy]), | |
| 1163 | ([x1, y0], [uv[2], uv[1]], [hx, -hy]), | |
| 1164 | ] { | |
| 1165 | self.vertices.push(Vertex { | |
| 1166 | position, | |
| 1167 | uv, | |
| 1168 | color, | |
| 1169 | local, | |
| 1170 | shape: [0.0; 4], | |
| 1171 | stroke: 0.0, | |
| 1172 | }); | |
| 1173 | } | |
| 1174 | Ok(()) | |
| 1175 | } | |
| 1176 | } | |
| 1177 | ||
| 1178 | fn srgb_byte(value: f32) -> u8 { | |
| 1179 | let srgb = if value <= 0.0031308 { | |
| 1180 | value * 12.92 | |
| 1181 | } else { | |
| 1182 | 1.055 * value.powf(1.0 / 2.4) - 0.055 | |
| 1183 | }; | |
| 1184 | (srgb * 255.0).round() as u8 | |
| 1185 | } | |
| 1186 | ||
| 1187 | /// Linear RGBA of an opaque sRGB colour. | |
| 1188 | pub fn srgb(red: u8, green: u8, blue: u8) -> [f32; 4] { | |
| 1189 | let linear = |byte: u8| { | |
| 1190 | let value = f32::from(byte) / 255.0; | |
| 1191 | if value <= 0.04045 { | |
| 1192 | value / 12.92 | |
| 1193 | } else { | |
| 1194 | ((value + 0.055) / 1.055).powf(2.4) | |
| 1195 | } | |
| 1196 | }; | |
| 1197 | [linear(red), linear(green), linear(blue), 1.0] | |
| 1198 | } | |
| 1199 | ||
| 1200 | #[cfg(test)] | |
| 1201 | mod tests { | |
| 1202 | use super::*; | |
| 1203 | use parley::{ | |
| 1204 | FontContext, FontFamily, FontFamilyName, GenericFamily, Layout, LayoutContext, | |
| 1205 | PositionedLayoutItem, StyleProperty, | |
| 1206 | }; | |
| 1207 | use std::time::Duration; | |
| 1208 | ||
| 1209 | const CHECKBOX: &str = r##"<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><defs><linearGradient id="a" x1="0" y1="0" x2="0" y2="1"><stop offset="0" stop-color="#ffffff"/><stop offset="1" stop-color="#7090b0"/></linearGradient></defs><path fill="url(#a)" d="M2 2h12v12H2zM4 4v8h8V4z"/></svg>"##; | |
| 1210 | const MARK: &str = r##"<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><defs><linearGradient id="b" x1="0" y1="0" x2="0" y2="1"><stop offset="0" stop-color="#20c040"/><stop offset="1" stop-color="#107020"/></linearGradient></defs><path fill="url(#b)" d="M4 8l3 3 5-7-1-1-4 5-2-2z"/></svg>"##; | |
| 1211 | const DOT: &str = r##"<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><defs><linearGradient id="c" x1="0" y1="0" x2="0" y2="1"><stop offset="0" stop-color="#ffd000"/><stop offset="1" stop-color="#c08000"/></linearGradient></defs><path fill="url(#c)" d="M8 2a6 6 0 1 0 0.01 0z"/></svg>"##; | |
| 1212 | ||
| 1213 | struct Text(Layout<[u8; 4]>); | |
| 1214 | ||
| 1215 | impl Glyphs for Text { | |
| 1216 | fn runs( | |
| 1217 | &self, | |
| 1218 | paint: &mut dyn FnMut(GlyphRun<'_>) -> Result<(), RenderError>, | |
| 1219 | ) -> Result<(), RenderError> { | |
| 1220 | for line in self.0.lines() { | |
| 1221 | let metrics = line.metrics(); | |
| 1222 | for item in line.items() { | |
| 1223 | if let PositionedLayoutItem::GlyphRun(run) = item { | |
| 1224 | paint_parley_run( | |
| 1225 | &run, | |
| 1226 | metrics.baseline, | |
| 1227 | 0.0, | |
| 1228 | [metrics.block_min_coord, metrics.line_height], | |
| 1229 | |_| [0.0, 0.0, 0.0, 1.0], | |
| 1230 | paint, | |
| 1231 | )?; | |
| 1232 | } | |
| 1233 | } | |
| 1234 | } | |
| 1235 | Ok(()) | |
| 1236 | } | |
| 1237 | } | |
| 1238 | ||
| 1239 | fn shape(text: &str, family: &str, size: f32, underline: bool, width: f32) -> Text { | |
| 1240 | let mut fonts = FontContext::default(); | |
| 1241 | let mut context = LayoutContext::default(); | |
| 1242 | let mut builder = context.ranged_builder(&mut fonts, text, 1.0, false); | |
| 1243 | builder.push_default(StyleProperty::FontFamily(FontFamily::List( | |
| 1244 | vec![ | |
| 1245 | FontFamilyName::named(family), | |
| 1246 | GenericFamily::SansSerif.into(), | |
| 1247 | ] | |
| 1248 | .into(), | |
| 1249 | ))); | |
| 1250 | builder.push_default(StyleProperty::FontSize(size)); | |
| 1251 | builder.push_default(StyleProperty::Underline(underline)); | |
| 1252 | let mut layout = builder.build(text); | |
| 1253 | layout.break_all_lines(Some(width)); | |
| 1254 | Text(layout) | |
| 1255 | } | |
| 1256 | ||
| 1257 | #[test] | |
| 1258 | fn decoding_preserves_pixels_and_rejects_large_or_truncated_images() { | |
| 1259 | let mut encoded = Vec::new(); | |
| 1260 | let mut encoder = png::Encoder::new(&mut encoded, 2, 1); | |
| 1261 | encoder.set_color(png::ColorType::Rgba); | |
| 1262 | encoder.set_depth(png::BitDepth::Eight); | |
| 1263 | let rgba = [255, 64, 0, 255, 0, 128, 255, 128]; | |
| 1264 | encoder | |
| 1265 | .write_header() | |
| 1266 | .unwrap() | |
| 1267 | .write_image_data(&rgba) | |
| 1268 | .unwrap(); | |
| 1269 | let decoded = RasterImage::decode(&encoded).unwrap(); | |
| 1270 | assert_eq!(decoded.size, [2, 1]); | |
| 1271 | assert_eq!(decoded.pixels(), [255, 64, 0, 255, 0, 93, 188, 128]); | |
| 1272 | for end in 0..encoded.len() - 12 { | |
| 1273 | assert!(RasterImage::decode(&encoded[..end]).is_err(), "{end}"); | |
| 1274 | } | |
| 1275 | ||
| 1276 | let mut jpeg = Vec::new(); | |
| 1277 | image::codecs::jpeg::JpegEncoder::new(&mut jpeg) | |
| 1278 | .encode(&[255; 4 * 4 * 3], 4, 4, image::ExtendedColorType::Rgb8) | |
| 1279 | .unwrap(); | |
| 1280 | let decoded = RasterImage::decode(&jpeg).unwrap(); | |
| 1281 | assert_eq!(decoded.size, [4, 4]); | |
| 1282 | assert_eq!(decoded.pixels(), [255; 4 * 4 * 4]); | |
| 1283 | ||
| 1284 | let mut gif = Vec::new(); | |
| 1285 | image::codecs::gif::GifEncoder::new(&mut gif) | |
| 1286 | .encode(&[255; 2 * 2 * 4], 2, 2, image::ExtendedColorType::Rgba8) | |
| 1287 | .unwrap(); | |
| 1288 | let decoded = RasterImage::decode(&gif).unwrap(); | |
| 1289 | assert_eq!(decoded.size, [2, 2]); | |
| 1290 | assert_eq!(decoded.pixels(), [255; 2 * 2 * 4]); | |
| 1291 | ||
| 1292 | for size in [[8192, 8192], [16_385, 1]] { | |
| 1293 | let mut header = Vec::new(); | |
| 1294 | let mut encoder = png::Encoder::new(&mut header, size[0], size[1]); | |
| 1295 | encoder.set_color(png::ColorType::Rgba); | |
| 1296 | encoder.set_depth(png::BitDepth::Eight); | |
| 1297 | let mut writer = encoder.write_header().unwrap(); | |
| 1298 | writer.write_chunk(png::chunk::IDAT, &[]).unwrap(); | |
| 1299 | drop(writer); | |
| 1300 | let error = RasterImage::decode(&header).err().unwrap(); | |
| 1301 | if size[0] == 8192 { | |
| 1302 | assert!(matches!(error, RenderError::ImageBudget), "{error:?}"); | |
| 1303 | } else { | |
| 1304 | assert!( | |
| 1305 | matches!( | |
| 1306 | error, | |
| 1307 | RenderError::ImageDecode(image::ImageError::Limits(_)) | |
| 1308 | ), | |
| 1309 | "{error:?}" | |
| 1310 | ); | |
| 1311 | } | |
| 1312 | } | |
| 1313 | } | |
| 1314 | ||
| 1315 | #[test] | |
| 1316 | fn image_pixels_have_valid_dimensions_and_immutable_clone_identity() { | |
| 1317 | assert!(matches!( | |
| 1318 | RasterImage::new([0, 1], Vec::new()), | |
| 1319 | Err(RenderError::InvalidImage) | |
| 1320 | )); | |
| 1321 | assert!(matches!( | |
| 1322 | RasterImage::new([2, 2], vec![255; 15]), | |
| 1323 | Err(RenderError::InvalidImage) | |
| 1324 | )); | |
| 1325 | assert!(matches!( | |
| 1326 | RasterImage::new([u32::MAX; 2], Vec::new()), | |
| 1327 | Err(RenderError::InvalidImage) | |
| 1328 | )); | |
| 1329 | let image = RasterImage::new([1, 1], vec![1, 2, 3, 255]).unwrap(); | |
| 1330 | assert_eq!(image.id(), image.clone().id()); | |
| 1331 | } | |
| 1332 | ||
| 1333 | struct Target { | |
| 1334 | texture: wgpu::Texture, | |
| 1335 | readback: wgpu::Buffer, | |
| 1336 | } | |
| 1337 | ||
| 1338 | impl Target { | |
| 1339 | fn new(device: &wgpu::Device) -> Self { | |
| 1340 | Self { | |
| 1341 | texture: device.create_texture(&wgpu::TextureDescriptor { | |
| 1342 | label: Some("Draw readback test"), | |
| 1343 | size: wgpu::Extent3d { | |
| 1344 | width: 512, | |
| 1345 | height: 256, | |
| 1346 | depth_or_array_layers: 1, | |
| 1347 | }, | |
| 1348 | mip_level_count: 1, | |
| 1349 | sample_count: 1, | |
| 1350 | dimension: wgpu::TextureDimension::D2, | |
| 1351 | format: wgpu::TextureFormat::Rgba8UnormSrgb, | |
| 1352 | usage: wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::COPY_SRC, | |
| 1353 | view_formats: &[], | |
| 1354 | }), | |
| 1355 | readback: device.create_buffer(&wgpu::BufferDescriptor { | |
| 1356 | label: Some("Draw readback"), | |
| 1357 | size: 512 * 256 * 4, | |
| 1358 | usage: wgpu::BufferUsages::MAP_READ | wgpu::BufferUsages::COPY_DST, | |
| 1359 | mapped_at_creation: false, | |
| 1360 | }), | |
| 1361 | } | |
| 1362 | } | |
| 1363 | ||
| 1364 | fn draw(&self, renderer: &mut Renderer, layers: &[Layer<'_>]) -> Result<(), RenderError> { | |
| 1365 | renderer.draw( | |
| 1366 | &self.texture.create_view(&Default::default()), | |
| 1367 | [512, 256], | |
| 1368 | [1.0; 4], | |
| 1369 | layers, | |
| 1370 | ) | |
| 1371 | } | |
| 1372 | ||
| 1373 | fn capture(&self, renderer: &Renderer) -> Vec<u8> { | |
| 1374 | let mut encoder = renderer.device.create_command_encoder(&Default::default()); | |
| 1375 | encoder.copy_texture_to_buffer( | |
| 1376 | self.texture.as_image_copy(), | |
| 1377 | wgpu::TexelCopyBufferInfo { | |
| 1378 | buffer: &self.readback, | |
| 1379 | layout: wgpu::TexelCopyBufferLayout { | |
| 1380 | offset: 0, | |
| 1381 | bytes_per_row: Some(512 * 4), | |
| 1382 | rows_per_image: Some(256), | |
| 1383 | }, | |
| 1384 | }, | |
| 1385 | wgpu::Extent3d { | |
| 1386 | width: 512, | |
| 1387 | height: 256, | |
| 1388 | depth_or_array_layers: 1, | |
| 1389 | }, | |
| 1390 | ); | |
| 1391 | renderer.queue.submit([encoder.finish()]); | |
| 1392 | let (sender, receiver) = std::sync::mpsc::channel(); | |
| 1393 | self.readback | |
| 1394 | .map_async(wgpu::MapMode::Read, .., move |result| { | |
| 1395 | sender.send(result).unwrap(); | |
| 1396 | }); | |
| 1397 | renderer | |
| 1398 | .device | |
| 1399 | .poll(wgpu::PollType::Wait { | |
| 1400 | submission_index: None, | |
| 1401 | timeout: Some(Duration::from_secs(5)), | |
| 1402 | }) | |
| 1403 | .unwrap(); | |
| 1404 | receiver | |
| 1405 | .recv_timeout(Duration::from_secs(5)) | |
| 1406 | .unwrap() | |
| 1407 | .unwrap(); | |
| 1408 | let capture = self.readback.get_mapped_range(..).unwrap().to_vec(); | |
| 1409 | self.readback.unmap(); | |
| 1410 | capture | |
| 1411 | } | |
| 1412 | } | |
| 1413 | ||
| 1414 | fn page<'a>(primitives: &'a [Primitive<'a>]) -> [Layer<'a>; 1] { | |
| 1415 | [Layer { | |
| 1416 | scale: 2.0, | |
| 1417 | origin: [24.0; 2], | |
| 1418 | clip: None, | |
| 1419 | primitives, | |
| 1420 | }] | |
| 1421 | } | |
| 1422 | ||
| 1423 | #[test] | |
| 1424 | #[ignore = "requires a native GPU adapter"] | |
| 1425 | fn eviction_and_repaint_reproduce_pixels() { | |
| 1426 | let instance = wgpu::Instance::new(wgpu::InstanceDescriptor::new_without_display_handle()); | |
| 1427 | let adapter = pollster::block_on(instance.request_adapter(&Default::default())).unwrap(); | |
| 1428 | let (device, queue) = | |
| 1429 | pollster::block_on(adapter.request_device(&Default::default())).unwrap(); | |
| 1430 | let format = wgpu::TextureFormat::Rgba8UnormSrgb; | |
| 1431 | let target = Target::new(&device); | |
| 1432 | let mut renderer = Renderer::new(device, queue, format); | |
| 1433 | let layout = shape("Hello 🌳\nCafé e\u{301} שלום", "Arial", 11.0, true, 220.0); | |
| 1434 | let mut captures = Vec::new(); | |
| 1435 | let image = RasterImage::new( | |
| 1436 | [2, 2], | |
| 1437 | vec![255, 0, 0, 255, 0, 255, 0, 255, 0, 0, 255, 255, 0, 0, 0, 0], | |
| 1438 | ) | |
| 1439 | .unwrap(); | |
| 1440 | let transparent_edge = | |
| 1441 | RasterImage::new([2, 1], vec![255, 0, 0, 255, 0, 0, 255, 0]).unwrap(); | |
| 1442 | let icons: [&'static [&'static str]; 4] = [&[CHECKBOX], &[CHECKBOX, MARK], &[DOT], &[MARK]]; | |
| 1443 | let phase_layout = shape("H", "Arial", 11.0, false, 20.0); | |
| 1444 | let mut primitives = vec![ | |
| 1445 | Primitive::Rect { | |
| 1446 | rect: [0.0, 0.0, 70.0, 14.0], | |
| 1447 | color: [0.55, 0.73, 1.0, 0.5], | |
| 1448 | }, | |
| 1449 | Primitive::Text { | |
| 1450 | clip: None, | |
| 1451 | text: &layout, | |
| 1452 | origin: [0.0; 2], | |
| 1453 | }, | |
| 1454 | Primitive::Text { | |
| 1455 | clip: None, | |
| 1456 | text: &layout, | |
| 1457 | origin: [8.0, 64.0], | |
| 1458 | }, | |
| 1459 | Primitive::Image { | |
| 1460 | image: &image, | |
| 1461 | rect: [128.0, 8.0, 160.0, 40.0], | |
| 1462 | }, | |
| 1463 | Primitive::Rect { | |
| 1464 | rect: [128.0, 8.0, 132.0, 12.0], | |
| 1465 | color: [0.0, 0.0, 1.0, 1.0], | |
| 1466 | }, | |
| 1467 | Primitive::Image { | |
| 1468 | image: &transparent_edge, | |
| 1469 | rect: [176.0, 8.0, 208.0, 40.0], | |
| 1470 | }, | |
| 1471 | Primitive::RoundedRect { | |
| 1472 | rect: [128.0, 48.0, 148.0, 68.0], | |
| 1473 | radius: [5.0, 10.0], | |
| 1474 | stroke: None, | |
| 1475 | color: [1.0, 0.0, 0.0, 1.0], | |
| 1476 | }, | |
| 1477 | Primitive::RoundedRect { | |
| 1478 | rect: [164.0, 48.0, 184.0, 68.0], | |
| 1479 | radius: [5.0; 2], | |
| 1480 | stroke: Some(Stroke::Solid(1.0)), | |
| 1481 | color: [0.0, 0.0, 1.0, 1.0], | |
| 1482 | }, | |
| 1483 | Primitive::RoundedRect { | |
| 1484 | rect: [200.0, 48.0, 240.0, 68.0], | |
| 1485 | radius: [3.0, 10.0], | |
| 1486 | stroke: Some(Stroke::Dashed(1.0)), | |
| 1487 | color: [0.0, 0.0, 1.0, 1.0], | |
| 1488 | }, | |
| 1489 | Primitive::Segment { | |
| 1490 | from: [226.0, 104.0], | |
| 1491 | to: [240.0, 104.0], | |
| 1492 | width: 4.0, | |
| 1493 | round: true, | |
| 1494 | color: [1.0, 0.0, 0.0, 1.0], | |
| 1495 | }, | |
| 1496 | ]; | |
| 1497 | for (index, sources) in icons.iter().enumerate() { | |
| 1498 | primitives.push(Primitive::Icon { | |
| 1499 | sources, | |
| 1500 | origin: [128.125 + 20.0 * index as f32, 80.25], | |
| 1501 | size: 12.0, | |
| 1502 | opacity: 1.0, | |
| 1503 | }); | |
| 1504 | } | |
| 1505 | primitives.extend((0..9).map(|step| Primitive::Text { | |
| 1506 | clip: None, | |
| 1507 | text: &phase_layout, | |
| 1508 | origin: [8.0 + 24.0 * step as f32, 96.0 + 0.125 * step as f32], | |
| 1509 | })); | |
| 1510 | for pass in 0..6 { | |
| 1511 | if pass == 2 { | |
| 1512 | renderer.clear_glyph_cache(); | |
| 1513 | renderer.images.clear(); | |
| 1514 | } else if pass == 3 { | |
| 1515 | renderer = Renderer::new(renderer.device.clone(), renderer.queue.clone(), format); | |
| 1516 | } | |
| 1517 | let clipping = [ | |
| 1518 | Primitive::Text { | |
| 1519 | text: &layout, | |
| 1520 | origin: [0.0; 2], | |
| 1521 | clip: (pass == 5).then_some([2.5, 1.25, 30.75, 20.5]), | |
| 1522 | }, | |
| 1523 | Primitive::Rect { | |
| 1524 | rect: [40.0, 40.0, 70.0, 50.0], | |
| 1525 | color: [0.0, 0.0, 1.0, 1.0], | |
| 1526 | }, | |
| 1527 | ]; | |
| 1528 | target | |
| 1529 | .draw( | |
| 1530 | &mut renderer, | |
| 1531 | &page(if pass < 4 { &primitives } else { &clipping }), | |
| 1532 | ) | |
| 1533 | .unwrap(); | |
| 1534 | captures.push(target.capture(&renderer)); | |
| 1535 | } | |
| 1536 | let centroids: Vec<_> = (0..9) | |
| 1537 | .map(|step| { | |
| 1538 | let mut weight = 0.0_f64; | |
| 1539 | let mut moment = 0.0_f64; | |
| 1540 | for y in 212..252 { | |
| 1541 | for x in (40 + 48 * step)..(72 + 48 * step) { | |
| 1542 | let value = captures[0][(y * 512 + x) * 4]; | |
| 1543 | let coverage = 1.0 - f64::from(srgb(value, 0, 0)[0]); | |
| 1544 | weight += coverage; | |
| 1545 | moment += coverage * y as f64; | |
| 1546 | } | |
| 1547 | } | |
| 1548 | assert!(weight > 10.0, "phase sample {step} was not painted"); | |
| 1549 | moment / weight | |
| 1550 | }) | |
| 1551 | .collect(); | |
| 1552 | let pixel = |x: usize, y: usize| &captures[0][(y * 512 + x) * 4..(y * 512 + x) * 4 + 4]; | |
| 1553 | assert_eq!(pixel(490, 232), [255, 0, 0, 255]); | |
| 1554 | assert_eq!(pixel(473, 232), [255, 0, 0, 255]); | |
| 1555 | assert_eq!(pixel(490, 238), [255; 4]); | |
| 1556 | assert_eq!(pixel(472, 228), [255; 4]); | |
| 1557 | assert_eq!(pixel(300, 140), [255, 0, 0, 255]); | |
| 1558 | assert_eq!(pixel(280, 120), [255; 4]); | |
| 1559 | assert_eq!(pixel(282, 124), [255; 4]); | |
| 1560 | assert_eq!(pixel(281, 140), [255, 0, 0, 255]); | |
| 1561 | assert_eq!(pixel(372, 140), [255; 4]); | |
| 1562 | assert_eq!(pixel(372, 120), [0, 0, 255, 255]); | |
| 1563 | assert_eq!(pixel(352, 120), [255; 4]); | |
| 1564 | assert_eq!(pixel(424, 120), [255; 4]); | |
| 1565 | assert_eq!(pixel(464, 140), [255; 4]); | |
| 1566 | assert!((440..480).any(|x| pixel(x, 120)[0] < 40)); | |
| 1567 | assert!((440..480).any(|x| pixel(x, 120)[0] > 250)); | |
| 1568 | for x in 440..472 { | |
| 1569 | assert!(pixel(x, 120)[0].abs_diff(pixel(x + 8, 120)[0]) <= 1); | |
| 1570 | } | |
| 1571 | for (step, centroid) in centroids.iter().enumerate() { | |
| 1572 | let movement = centroid - centroids[0]; | |
| 1573 | assert!( | |
| 1574 | (movement - step as f64 * 0.25).abs() < 0.06, | |
| 1575 | "quarter-pixel step {step} moved the glyph by {movement} px" | |
| 1576 | ); | |
| 1577 | } | |
| 1578 | assert!(!renderer.glyphs.is_empty()); | |
| 1579 | assert!(renderer.glyphs.len() <= MAX_GLYPHS); | |
| 1580 | assert_eq!( | |
| 1581 | renderer | |
| 1582 | .glyphs | |
| 1583 | .keys() | |
| 1584 | .filter(|key| matches!(key, AtlasKey::Icon { .. })) | |
| 1585 | .count(), | |
| 1586 | 4 | |
| 1587 | ); | |
| 1588 | for x in [280, 320, 360, 400] { | |
| 1589 | let colored = (184..210) | |
| 1590 | .flat_map(|y| (x..x + 26).map(move |x| (y * 512 + x) * 4)) | |
| 1591 | .filter(|offset| captures[0][*offset..*offset + 3].iter().any(|v| *v < 180)) | |
| 1592 | .count(); | |
| 1593 | assert!(colored > 10, "icon at {x} was not painted"); | |
| 1594 | } | |
| 1595 | let count = renderer.glyphs.len(); | |
| 1596 | target | |
| 1597 | .draw( | |
| 1598 | &mut renderer, | |
| 1599 | &page(&[Primitive::Icon { | |
| 1600 | sources: icons[0], | |
| 1601 | origin: [10000.0; 2], | |
| 1602 | size: 12.0, | |
| 1603 | opacity: 1.0, | |
| 1604 | }]), | |
| 1605 | ) | |
| 1606 | .unwrap(); | |
| 1607 | assert_eq!(renderer.glyphs.len(), count); | |
| 1608 | assert!( | |
| 1609 | captures[0] | |
| 1610 | .chunks_exact(4) | |
| 1611 | .filter(|pixel| pixel[0] < 200) | |
| 1612 | .count() | |
| 1613 | > 100 | |
| 1614 | ); | |
| 1615 | assert_eq!(captures[0], captures[1]); | |
| 1616 | assert_eq!(captures[0], captures[2]); | |
| 1617 | assert_eq!(captures[0], captures[3]); | |
| 1618 | assert_ne!(captures[4], captures[5]); | |
| 1619 | for y in 0..256 { | |
| 1620 | for x in 0..512 { | |
| 1621 | let offset = (y * 512 + x) * 4; | |
| 1622 | let expected = if ((29..86).contains(&x) && (26..65).contains(&y)) | |
| 1623 | || ((104..164).contains(&x) && (104..124).contains(&y)) | |
| 1624 | { | |
| 1625 | &captures[4][offset..offset + 4] | |
| 1626 | } else { | |
| 1627 | &[255; 4] | |
| 1628 | }; | |
| 1629 | assert_eq!( | |
| 1630 | &captures[5][offset..offset + 4], | |
| 1631 | expected, | |
| 1632 | "clipping at {x},{y}" | |
| 1633 | ); | |
| 1634 | } | |
| 1635 | } | |
| 1636 | for (x, y, color) in [ | |
| 1637 | (288, 48, [255, 0, 0, 255]), | |
| 1638 | (336, 96, [255; 4]), | |
| 1639 | (284, 44, [0, 0, 255, 255]), | |
| 1640 | ] { | |
| 1641 | let offset = (y * 512 + x) * 4; | |
| 1642 | assert_eq!(captures[0][offset..offset + 4], color); | |
| 1643 | } | |
| 1644 | let edge = &captures[0][(48 * 512 + 408) * 4..(48 * 512 + 408) * 4 + 4]; | |
| 1645 | assert_eq!(edge[0], 255, "{edge:?}"); | |
| 1646 | assert_eq!(edge[1], edge[2], "{edge:?}"); | |
| 1647 | assert!((180..=200).contains(&edge[1]), "{edge:?}"); | |
| 1648 | assert_eq!(renderer.images.len(), 2); | |
| 1649 | for primitive in [ | |
| 1650 | Primitive::RoundedRect { | |
| 1651 | rect: [-1e38, 0.0, 1e38, 1e38], | |
| 1652 | radius: [3.0; 2], | |
| 1653 | stroke: Some(Stroke::Dashed(1.0)), | |
| 1654 | color: [1.0; 4], | |
| 1655 | }, | |
| 1656 | Primitive::RoundedRect { | |
| 1657 | rect: [0.0, 0.0, 10.0, 10.0], | |
| 1658 | radius: [f32::NAN, 2.0], | |
| 1659 | stroke: None, | |
| 1660 | color: [1.0; 4], | |
| 1661 | }, | |
| 1662 | Primitive::RoundedRect { | |
| 1663 | rect: [0.0, 0.0, 10.0, 10.0], | |
| 1664 | radius: [2.0; 2], | |
| 1665 | stroke: Some(Stroke::Solid(0.0)), | |
| 1666 | color: [1.0; 4], | |
| 1667 | }, | |
| 1668 | Primitive::RoundedRect { | |
| 1669 | rect: [0.0, 0.0, 10.0, 10.0], | |
| 1670 | radius: [2.0; 2], | |
| 1671 | stroke: Some(Stroke::Dashed(f32::NAN)), | |
| 1672 | color: [1.0; 4], | |
| 1673 | }, | |
| 1674 | Primitive::Rect { | |
| 1675 | rect: [0.0, 0.0, f32::NAN, 1.0], | |
| 1676 | color: [1.0; 4], | |
| 1677 | }, | |
| 1678 | Primitive::Rect { | |
| 1679 | rect: [2.0, 0.0, 1.0, 1.0], | |
| 1680 | color: [1.0; 4], | |
| 1681 | }, | |
| 1682 | Primitive::Text { | |
| 1683 | clip: None, | |
| 1684 | text: &layout, | |
| 1685 | origin: [f32::MAX; 2], | |
| 1686 | }, | |
| 1687 | Primitive::Text { | |
| 1688 | clip: Some([0.0, 0.0, f32::NAN, 1.0]), | |
| 1689 | text: &layout, | |
| 1690 | origin: [0.0; 2], | |
| 1691 | }, | |
| 1692 | Primitive::Text { | |
| 1693 | clip: Some([2.0, 0.0, 1.0, 1.0]), | |
| 1694 | text: &layout, | |
| 1695 | origin: [0.0; 2], | |
| 1696 | }, | |
| 1697 | ] { | |
| 1698 | assert!(matches!( | |
| 1699 | target.draw(&mut renderer, &page(&[primitive])), | |
| 1700 | Err(RenderError::InvalidPrimitive) | |
| 1701 | )); | |
| 1702 | } | |
| 1703 | let too_many: Vec<_> = (0..=MAX_IMAGES) | |
| 1704 | .map(|_| RasterImage::new([1, 1], vec![255; 4]).unwrap()) | |
| 1705 | .collect(); | |
| 1706 | let primitives: Vec<_> = too_many | |
| 1707 | .iter() | |
| 1708 | .map(|image| Primitive::Image { | |
| 1709 | image, | |
| 1710 | rect: [0.0, 0.0, 1.0, 1.0], | |
| 1711 | }) | |
| 1712 | .collect(); | |
| 1713 | assert!(matches!( | |
| 1714 | target.draw(&mut renderer, &page(&primitives)), | |
| 1715 | Err(RenderError::ImageBudget) | |
| 1716 | )); | |
| 1717 | let primitives: Vec<_> = too_many | |
| 1718 | .iter() | |
| 1719 | .map(|image| Primitive::Image { | |
| 1720 | image, | |
| 1721 | rect: [-20.0, -20.0, -19.0, -19.0], | |
| 1722 | }) | |
| 1723 | .collect(); | |
| 1724 | target.draw(&mut renderer, &page(&primitives)).unwrap(); | |
| 1725 | assert_eq!(renderer.images.len(), 2); | |
| 1726 | let mut seen = HashSet::from([image.id(), transparent_edge.id()]); | |
| 1727 | let mut previous = None; | |
| 1728 | for _ in 0..5 { | |
| 1729 | let large = RasterImage::new([2048; 2], vec![255; 2048 * 2048 * 4]).unwrap(); | |
| 1730 | seen.insert(large.id()); | |
| 1731 | let mut primitives = vec![Primitive::Image { | |
| 1732 | image: &large, | |
| 1733 | rect: [0.0, 0.0, 16.0, 16.0], | |
| 1734 | }]; | |
| 1735 | if let Some(image) = &previous { | |
| 1736 | primitives.push(Primitive::Image { | |
| 1737 | image, | |
| 1738 | rect: [16.0, 0.0, 32.0, 16.0], | |
| 1739 | }); | |
| 1740 | } | |
| 1741 | target.draw(&mut renderer, &page(&primitives)).unwrap(); | |
| 1742 | assert!(renderer.images.contains_key(&large.id())); | |
| 1743 | if let Some(image) = &previous { | |
| 1744 | assert!(renderer.images.contains_key(&image.id())); | |
| 1745 | } | |
| 1746 | assert!(renderer.occupancy().within_budget); | |
| 1747 | previous = Some(large); | |
| 1748 | } | |
| 1749 | assert!(renderer.images.len() <= MAX_IMAGES); | |
| 1750 | assert!(seen.iter().any(|id| !renderer.images.contains_key(id))); | |
| 1751 | renderer.images.clear(); | |
| 1752 | target | |
| 1753 | .draw( | |
| 1754 | &mut renderer, | |
| 1755 | &page(&[Primitive::Image { | |
| 1756 | image: &image, | |
| 1757 | rect: [128.0, 8.0, 160.0, 40.0], | |
| 1758 | }]), | |
| 1759 | ) | |
| 1760 | .unwrap(); | |
| 1761 | assert_eq!(renderer.images.len(), 1); | |
| 1762 | assert!(renderer.images.contains_key(&image.id())); | |
| 1763 | } | |
| 1764 | ||
| 1765 | #[test] | |
| 1766 | #[ignore = "requires a native GPU adapter"] | |
| 1767 | fn layers_transform_and_clip_independently() { | |
| 1768 | let instance = wgpu::Instance::new(wgpu::InstanceDescriptor::new_without_display_handle()); | |
| 1769 | let adapter = pollster::block_on(instance.request_adapter(&Default::default())).unwrap(); | |
| 1770 | let (device, queue) = | |
| 1771 | pollster::block_on(adapter.request_device(&Default::default())).unwrap(); | |
| 1772 | let target = Target::new(&device); | |
| 1773 | let mut renderer = Renderer::new(device, queue, wgpu::TextureFormat::Rgba8UnormSrgb); | |
| 1774 | let fill = [Primitive::Rect { | |
| 1775 | rect: [0.0, 0.0, 300.0, 100.0], | |
| 1776 | color: [1.0, 0.0, 0.0, 1.0], | |
| 1777 | }]; | |
| 1778 | let chrome = [Primitive::Rect { | |
| 1779 | rect: [0.0, 0.0, 10.0, 10.0], | |
| 1780 | color: [0.0, 0.0, 1.0, 1.0], | |
| 1781 | }]; | |
| 1782 | target | |
| 1783 | .draw( | |
| 1784 | &mut renderer, | |
| 1785 | &[ | |
| 1786 | Layer { | |
| 1787 | scale: 1.0, | |
| 1788 | origin: [0.0; 2], | |
| 1789 | clip: Some([100.0, 50.0, 200.5, 80.0]), | |
| 1790 | primitives: &fill, | |
| 1791 | }, | |
| 1792 | Layer { | |
| 1793 | scale: 2.0, | |
| 1794 | origin: [300.0, 10.0], | |
| 1795 | clip: None, | |
| 1796 | primitives: &chrome, | |
| 1797 | }, | |
| 1798 | ], | |
| 1799 | ) | |
| 1800 | .unwrap(); | |
| 1801 | let capture = target.capture(&renderer); | |
| 1802 | let pixel = |x: usize, y: usize| &capture[(y * 512 + x) * 4..(y * 512 + x) * 4 + 4]; | |
| 1803 | assert_eq!(pixel(10, 10), [255; 4], "clipped away"); | |
| 1804 | assert_eq!(pixel(100, 50), [255, 0, 0, 255]); | |
| 1805 | assert_eq!( | |
| 1806 | pixel(200, 79), | |
| 1807 | [255, 0, 0, 255], | |
| 1808 | "partial pixels round outward" | |
| 1809 | ); | |
| 1810 | assert_eq!(pixel(99, 60), [255; 4]); | |
| 1811 | assert_eq!(pixel(201, 60), [255; 4]); | |
| 1812 | assert_eq!(pixel(150, 80), [255; 4]); | |
| 1813 | assert_eq!(pixel(319, 29), [0, 0, 255, 255]); | |
| 1814 | assert_eq!(pixel(320, 30), [255; 4]); | |
| 1815 | assert!(matches!( | |
| 1816 | target.draw( | |
| 1817 | &mut renderer, | |
| 1818 | &[Layer { | |
| 1819 | scale: 0.0, | |
| 1820 | origin: [0.0; 2], | |
| 1821 | clip: None, | |
| 1822 | primitives: &fill, | |
| 1823 | }] | |
| 1824 | ), | |
| 1825 | Err(RenderError::InvalidLayer) | |
| 1826 | )); | |
| 1827 | } | |
| 1828 | } | |
| 9 | #[cfg(feature = "render")] | |
| 10 | pub use render::*; |
crates/draw/src/render.rs created+2135| ... | ... | @@ -0,0 +1,2135 @@ |
| 1 | mod icon; | |
| 2 | mod text; | |
| 3 | ||
| 4 | pub use text::{Decoration, Glyph, GlyphRun, Glyphs, paint_parley_run}; | |
| 5 | ||
| 6 | use bytemuck::{Pod, Zeroable}; | |
| 7 | use image::ImageDecoder; | |
| 8 | use parley::fontique::Blob; | |
| 9 | use std::{ | |
| 10 | collections::{HashMap, HashSet}, | |
| 11 | ops::Range, | |
| 12 | }; | |
| 13 | use swash::{ | |
| 14 | FontRef, | |
| 15 | scale::{Render, ScaleContext, Source, StrikeWith, image::Content}, | |
| 16 | zeno::{Angle, Cap, Format, Join, Mask, Transform, Vector}, | |
| 17 | }; | |
| 18 | ||
| 19 | const ATLAS_SIZE: u32 = 2048; | |
| 20 | const MAX_GLYPHS: usize = 8192; | |
| 21 | const MAX_VERTICES: usize = 65_536; | |
| 22 | /// Decoded bytes of all images one frame may paint. | |
| 23 | pub const MAX_IMAGE_BYTES: u64 = 64 * 1024 * 1024; | |
| 24 | const MAX_IMAGES: usize = 256; | |
| 25 | ||
| 26 | #[derive(Clone)] | |
| 27 | pub struct RasterImage { | |
| 28 | size: [u32; 2], | |
| 29 | pixels: Blob<u8>, | |
| 30 | } | |
| 31 | ||
| 32 | impl RasterImage { | |
| 33 | pub fn decode(encoded: &[u8]) -> Result<Self, RenderError> { | |
| 34 | if encoded.len() as u64 > MAX_IMAGE_BYTES { | |
| 35 | return Err(RenderError::ImageBudget); | |
| 36 | } | |
| 37 | let format = image::guess_format(encoded).map_err(RenderError::ImageDecode)?; | |
| 38 | let mut reader = image::ImageReader::with_format(std::io::Cursor::new(encoded), format); | |
| 39 | let mut limits = image::Limits::default(); | |
| 40 | limits.max_image_width = Some(16_384); | |
| 41 | limits.max_image_height = Some(16_384); | |
| 42 | limits.max_alloc = Some(MAX_IMAGE_BYTES); | |
| 43 | reader.limits(limits.clone()); | |
| 44 | let mut decoder = reader.into_decoder().map_err(RenderError::ImageDecode)?; | |
| 45 | let (width, height) = decoder.dimensions(); | |
| 46 | if u64::from(width) * u64::from(height) * 4 > MAX_IMAGE_BYTES | |
| 47 | || decoder.total_bytes() > MAX_IMAGE_BYTES | |
| 48 | { | |
| 49 | return Err(RenderError::ImageBudget); | |
| 50 | } | |
| 51 | limits | |
| 52 | .reserve(decoder.total_bytes()) | |
| 53 | .map_err(RenderError::ImageDecode)?; | |
| 54 | decoder | |
| 55 | .set_limits(limits) | |
| 56 | .map_err(RenderError::ImageDecode)?; | |
| 57 | let pixels = image::DynamicImage::from_decoder(decoder) | |
| 58 | .map_err(RenderError::ImageDecode)? | |
| 59 | .into_rgba8(); | |
| 60 | Self::new([width, height], pixels.into_raw()) | |
| 61 | } | |
| 62 | ||
| 63 | /// Immutable, straight-alpha sRGB RGBA pixels retain one cache identity across clones. | |
| 64 | pub fn new(size: [u32; 2], mut pixels: Vec<u8>) -> Result<Self, RenderError> { | |
| 65 | let bytes = u64::from(size[0]) | |
| 66 | .checked_mul(u64::from(size[1])) | |
| 67 | .and_then(|v| v.checked_mul(4)) | |
| 68 | .ok_or(RenderError::InvalidImage)?; | |
| 69 | if size.contains(&0) || bytes != pixels.len() as u64 { | |
| 70 | return Err(RenderError::InvalidImage); | |
| 71 | } | |
| 72 | if bytes > MAX_IMAGE_BYTES { | |
| 73 | return Err(RenderError::ImageBudget); | |
| 74 | } | |
| 75 | // Premultiply in linear light before filtering; transparent RGB must not bleed at edges. | |
| 76 | for pixel in pixels.chunks_exact_mut(4) { | |
| 77 | let alpha = pixel[3]; | |
| 78 | if alpha == 255 { | |
| 79 | continue; | |
| 80 | } | |
| 81 | let linear = srgb(pixel[0], pixel[1], pixel[2]); | |
| 82 | for (byte, value) in pixel[..3].iter_mut().zip(linear) { | |
| 83 | let value = value * f32::from(alpha) / 255.0; | |
| 84 | *byte = srgb_byte(value); | |
| 85 | } | |
| 86 | } | |
| 87 | Ok(Self { | |
| 88 | size, | |
| 89 | pixels: pixels.into(), | |
| 90 | }) | |
| 91 | } | |
| 92 | ||
| 93 | /// The identity caches and uploads share across clones. | |
| 94 | pub fn id(&self) -> u64 { | |
| 95 | self.pixels.id() | |
| 96 | } | |
| 97 | ||
| 98 | /// Premultiplied sRGB RGBA rows. | |
| 99 | pub fn pixels(&self) -> &[u8] { | |
| 100 | self.pixels.as_ref() | |
| 101 | } | |
| 102 | } | |
| 103 | ||
| 104 | struct CachedImage { | |
| 105 | binding: wgpu::BindGroup, | |
| 106 | bytes: u64, | |
| 107 | } | |
| 108 | ||
| 109 | struct Batch { | |
| 110 | vertices: Range<u32>, | |
| 111 | image: Option<u64>, | |
| 112 | scissor: [u32; 4], | |
| 113 | } | |
| 114 | ||
| 115 | #[repr(C)] | |
| 116 | #[derive(Clone, Copy, Pod, Zeroable)] | |
| 117 | struct Vertex { | |
| 118 | position: [f32; 2], | |
| 119 | uv: [f32; 2], | |
| 120 | color: [f32; 4], | |
| 121 | local: [f32; 2], | |
| 122 | shape: [f32; 4], | |
| 123 | stroke: f32, | |
| 124 | } | |
| 125 | ||
| 126 | #[derive(Hash, PartialEq, Eq)] | |
| 127 | enum AtlasKey { | |
| 128 | Text { | |
| 129 | font: u64, | |
| 130 | index: u32, | |
| 131 | glyph: u16, | |
| 132 | size: u32, | |
| 133 | coords: Vec<i16>, | |
| 134 | phase: [u8; 2], | |
| 135 | embolden: bool, | |
| 136 | skew: u32, | |
| 137 | }, | |
| 138 | Icon { | |
| 139 | sources: &'static [&'static str], | |
| 140 | size: u32, | |
| 141 | phase: [u8; 2], | |
| 142 | }, | |
| 143 | Path { | |
| 144 | data: String, | |
| 145 | scale: u32, | |
| 146 | /// The style's kind and width bits. | |
| 147 | style: (u8, u32), | |
| 148 | phase: [u8; 2], | |
| 149 | }, | |
| 150 | } | |
| 151 | ||
| 152 | #[derive(Clone, Copy)] | |
| 153 | struct AtlasGlyph { | |
| 154 | x: u32, | |
| 155 | y: u32, | |
| 156 | width: u32, | |
| 157 | height: u32, | |
| 158 | left: i32, | |
| 159 | top: i32, | |
| 160 | color: bool, | |
| 161 | } | |
| 162 | ||
| 163 | /// Primitives sharing one transform and clip. | |
| 164 | pub struct Layer<'a> { | |
| 165 | /// Device pixels per layer unit. | |
| 166 | pub scale: f32, | |
| 167 | /// Device position of the layer origin. | |
| 168 | pub origin: [f32; 2], | |
| 169 | /// Device bounds `[left, top, right, bottom]` the layer paints within. | |
| 170 | pub clip: Option<[f32; 4]>, | |
| 171 | pub primitives: &'a [Primitive<'a>], | |
| 172 | } | |
| 173 | ||
| 174 | /// A layer's transform onto the target. | |
| 175 | #[derive(Clone, Copy)] | |
| 176 | struct Space { | |
| 177 | size: [u32; 2], | |
| 178 | scale: f32, | |
| 179 | origin: [f32; 2], | |
| 180 | } | |
| 181 | ||
| 182 | impl Space { | |
| 183 | fn visible_rect(&self, rect: [f32; 4]) -> Result<Option<[f32; 4]>, RenderError> { | |
| 184 | let rect = [ | |
| 185 | rect[0] * self.scale + self.origin[0], | |
| 186 | rect[1] * self.scale + self.origin[1], | |
| 187 | rect[2] * self.scale + self.origin[0], | |
| 188 | rect[3] * self.scale + self.origin[1], | |
| 189 | ]; | |
| 190 | if rect.iter().any(|v| !v.is_finite()) || rect[2] < rect[0] || rect[3] < rect[1] { | |
| 191 | return Err(RenderError::InvalidPrimitive); | |
| 192 | } | |
| 193 | Ok((rect[0] < self.size[0] as f32 | |
| 194 | && rect[1] < self.size[1] as f32 | |
| 195 | && rect[2] > 0.0 | |
| 196 | && rect[3] > 0.0 | |
| 197 | && rect[0] < rect[2] | |
| 198 | && rect[1] < rect[3]) | |
| 199 | .then_some(rect)) | |
| 200 | } | |
| 201 | ||
| 202 | /// Whole device pixels covering `rect` within the target, as a scissor rectangle. | |
| 203 | fn scissor(&self, rect: [f32; 4]) -> [u32; 4] { | |
| 204 | let left = rect[0].max(0.0).floor() as u32; | |
| 205 | let top = rect[1].max(0.0).floor() as u32; | |
| 206 | let right = (rect[2].min(self.size[0] as f32).ceil() as u32).max(left); | |
| 207 | let bottom = (rect[3].min(self.size[1] as f32).ceil() as u32).max(top); | |
| 208 | [left, top, right - left, bottom - top] | |
| 209 | } | |
| 210 | } | |
| 211 | ||
| 212 | fn intersect(a: [u32; 4], b: [u32; 4]) -> [u32; 4] { | |
| 213 | let left = a[0].max(b[0]); | |
| 214 | let top = a[1].max(b[1]); | |
| 215 | let right = (a[0] + a[2]).min(b[0] + b[2]).max(left); | |
| 216 | let bottom = (a[1] + a[3]).min(b[1] + b[3]).max(top); | |
| 217 | [left, top, right - left, bottom - top] | |
| 218 | } | |
| 219 | ||
| 220 | /// How a path paints; widths are layer units. | |
| 221 | #[derive(Clone, Copy, Debug, PartialEq)] | |
| 222 | pub enum PathStyle { | |
| 223 | Fill, | |
| 224 | /// Round caps and joins. | |
| 225 | Stroke(f32), | |
| 226 | /// Filled and blurred this far, as a soft shadow. | |
| 227 | Shadow(f32), | |
| 228 | } | |
| 229 | ||
| 230 | #[derive(Clone, Copy, Debug, PartialEq)] | |
| 231 | pub enum Stroke { | |
| 232 | Solid(f32), | |
| 233 | /// Dash and gap lengths are twice the stroke width. | |
| 234 | Dashed(f32), | |
| 235 | } | |
| 236 | ||
| 237 | /// Coordinates are layer units; colors are linear RGBA. | |
| 238 | pub enum Primitive<'a> { | |
| 239 | Text { | |
| 240 | text: &'a dyn Glyphs, | |
| 241 | origin: [f32; 2], | |
| 242 | /// Layer bounds of the painted glyphs, which keep their full shaping and advances. | |
| 243 | clip: Option<[f32; 4]>, | |
| 244 | /// The colour of runs and decorations without their own. | |
| 245 | ink: [f32; 4], | |
| 246 | }, | |
| 247 | /// 16×16 SVG artwork painted `size` units square, later sources over earlier ones. | |
| 248 | /// `tint` multiplies it; `currentColor` paints white, so it takes the tint's colour. | |
| 249 | Icon { | |
| 250 | sources: &'static [&'static str], | |
| 251 | origin: [f32; 2], | |
| 252 | size: f32, | |
| 253 | tint: [f32; 4], | |
| 254 | }, | |
| 255 | /// SVG path data in layer units from `origin`, shaded from `colors[0]` at the top of | |
| 256 | /// what it paints to `colors[1]` at the bottom. Each distinct path is rasterized once | |
| 257 | /// per scale and style. | |
| 258 | Path { | |
| 259 | data: &'a str, | |
| 260 | origin: [f32; 2], | |
| 261 | style: PathStyle, | |
| 262 | colors: [[f32; 4]; 2], | |
| 263 | }, | |
| 264 | Rect { | |
| 265 | rect: [f32; 4], | |
| 266 | color: [f32; 4], | |
| 267 | }, | |
| 268 | RoundedRect { | |
| 269 | rect: [f32; 4], | |
| 270 | radius: [f32; 2], | |
| 271 | /// None fills the shape; stroke widths are positive layer units. | |
| 272 | stroke: Option<Stroke>, | |
| 273 | color: [f32; 4], | |
| 274 | }, | |
| 275 | /// A rounded rectangle filled from `colors[0]` at its top to `colors[1]` at its bottom. | |
| 276 | Gradient { | |
| 277 | rect: [f32; 4], | |
| 278 | radius: [f32; 2], | |
| 279 | colors: [[f32; 4]; 2], | |
| 280 | }, | |
| 281 | Image { | |
| 282 | image: &'a RasterImage, | |
| 283 | rect: [f32; 4], | |
| 284 | }, | |
| 285 | /// A pen stroke between two points, `width` units across. | |
| 286 | Segment { | |
| 287 | from: [f32; 2], | |
| 288 | to: [f32; 2], | |
| 289 | width: f32, | |
| 290 | /// A round pen tip caps the ends; otherwise they are square. | |
| 291 | round: bool, | |
| 292 | color: [f32; 4], | |
| 293 | }, | |
| 294 | } | |
| 295 | ||
| 296 | #[derive(Debug)] | |
| 297 | pub enum RenderError { | |
| 298 | AtlasFull, | |
| 299 | FrameTooLarge, | |
| 300 | UnreadableFont, | |
| 301 | UnsupportedGlyph, | |
| 302 | InvalidLayer, | |
| 303 | InvalidPrimitive, | |
| 304 | InvalidImage, | |
| 305 | ImageBudget, | |
| 306 | ImageTooLarge, | |
| 307 | ImageDecode(image::ImageError), | |
| 308 | } | |
| 309 | ||
| 310 | /// Cache and buffer use after the last frame, for resource probes. Texture and buffer | |
| 311 | /// sizes are as requested, without driver overhead. | |
| 312 | #[derive(Debug)] | |
| 313 | pub struct Occupancy { | |
| 314 | /// Glyph and icon atlas entries, including glyphs with no pixels. | |
| 315 | pub glyphs: usize, | |
| 316 | pub atlas_texels: u64, | |
| 317 | pub images: usize, | |
| 318 | pub image_bytes: u64, | |
| 319 | pub vertices: usize, | |
| 320 | pub batches: usize, | |
| 321 | pub vertex_capacity_bytes: usize, | |
| 322 | pub batch_capacity_bytes: usize, | |
| 323 | pub glyph_capacity: usize, | |
| 324 | pub image_capacity: usize, | |
| 325 | pub atlas_bytes: u64, | |
| 326 | pub vertex_buffer_bytes: u64, | |
| 327 | pub within_budget: bool, | |
| 328 | } | |
| 329 | ||
| 330 | pub struct Renderer { | |
| 331 | pub device: wgpu::Device, | |
| 332 | pub queue: wgpu::Queue, | |
| 333 | pipeline: wgpu::RenderPipeline, | |
| 334 | image_pipeline: wgpu::RenderPipeline, | |
| 335 | atlas: wgpu::Texture, | |
| 336 | bind_group: wgpu::BindGroup, | |
| 337 | image_sampler: wgpu::Sampler, | |
| 338 | vertex_buffer: wgpu::Buffer, | |
| 339 | vertices: Vec<Vertex>, | |
| 340 | glyphs: HashMap<AtlasKey, Option<AtlasGlyph>>, | |
| 341 | images: HashMap<u64, CachedImage>, | |
| 342 | batches: Vec<Batch>, | |
| 343 | scaler: ScaleContext, | |
| 344 | pen: [u32; 2], | |
| 345 | row_height: u32, | |
| 346 | } | |
| 347 | ||
| 348 | impl Renderer { | |
| 349 | pub fn new(device: wgpu::Device, queue: wgpu::Queue, format: wgpu::TextureFormat) -> Self { | |
| 350 | let shader = device.create_shader_module(wgpu::include_wgsl!("draw.wgsl")); | |
| 351 | let binding_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { | |
| 352 | label: Some("Draw texture"), | |
| 353 | entries: &[ | |
| 354 | wgpu::BindGroupLayoutEntry { | |
| 355 | binding: 0, | |
| 356 | visibility: wgpu::ShaderStages::FRAGMENT, | |
| 357 | ty: wgpu::BindingType::Texture { | |
| 358 | sample_type: wgpu::TextureSampleType::Float { filterable: true }, | |
| 359 | view_dimension: wgpu::TextureViewDimension::D2, | |
| 360 | multisampled: false, | |
| 361 | }, | |
| 362 | count: None, | |
| 363 | }, | |
| 364 | wgpu::BindGroupLayoutEntry { | |
| 365 | binding: 1, | |
| 366 | visibility: wgpu::ShaderStages::FRAGMENT, | |
| 367 | ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering), | |
| 368 | count: None, | |
| 369 | }, | |
| 370 | ], | |
| 371 | }); | |
| 372 | let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor { | |
| 373 | label: Some("Draw"), | |
| 374 | bind_group_layouts: &[Some(&binding_layout)], | |
| 375 | ..Default::default() | |
| 376 | }); | |
| 377 | let make_pipeline = |blend| { | |
| 378 | device.create_render_pipeline(&wgpu::RenderPipelineDescriptor { | |
| 379 | label: Some("Draw"), | |
| 380 | layout: Some(&pipeline_layout), | |
| 381 | vertex: wgpu::VertexState { | |
| 382 | module: &shader, | |
| 383 | entry_point: Some("vertex"), | |
| 384 | compilation_options: Default::default(), | |
| 385 | buffers: &[Some(wgpu::VertexBufferLayout { | |
| 386 | array_stride: size_of::<Vertex>() as u64, | |
| 387 | step_mode: wgpu::VertexStepMode::Vertex, | |
| 388 | attributes: &wgpu::vertex_attr_array![0 => Float32x2, 1 => Float32x2, 2 => Float32x4, 3 => Float32x2, 4 => Float32x4, 5 => Float32], | |
| 389 | })], | |
| 390 | }, | |
| 391 | fragment: Some(wgpu::FragmentState { | |
| 392 | module: &shader, | |
| 393 | entry_point: Some("fragment"), | |
| 394 | compilation_options: Default::default(), | |
| 395 | targets: &[Some(wgpu::ColorTargetState { | |
| 396 | format, | |
| 397 | blend: Some(blend), | |
| 398 | write_mask: wgpu::ColorWrites::ALL, | |
| 399 | })], | |
| 400 | }), | |
| 401 | primitive: Default::default(), | |
| 402 | depth_stencil: None, | |
| 403 | multisample: Default::default(), | |
| 404 | multiview_mask: None, | |
| 405 | cache: None, | |
| 406 | }) | |
| 407 | }; | |
| 408 | let pipeline = make_pipeline(wgpu::BlendState::ALPHA_BLENDING); | |
| 409 | let image_pipeline = make_pipeline(wgpu::BlendState::PREMULTIPLIED_ALPHA_BLENDING); | |
| 410 | let atlas = device.create_texture(&wgpu::TextureDescriptor { | |
| 411 | label: Some("Glyph atlas"), | |
| 412 | size: wgpu::Extent3d { | |
| 413 | width: ATLAS_SIZE, | |
| 414 | height: ATLAS_SIZE, | |
| 415 | depth_or_array_layers: 1, | |
| 416 | }, | |
| 417 | mip_level_count: 1, | |
| 418 | sample_count: 1, | |
| 419 | dimension: wgpu::TextureDimension::D2, | |
| 420 | format: wgpu::TextureFormat::Rgba8UnormSrgb, | |
| 421 | usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST, | |
| 422 | view_formats: &[], | |
| 423 | }); | |
| 424 | let view = atlas.create_view(&Default::default()); | |
| 425 | let sampler = device.create_sampler(&wgpu::SamplerDescriptor { | |
| 426 | mag_filter: wgpu::FilterMode::Nearest, | |
| 427 | min_filter: wgpu::FilterMode::Nearest, | |
| 428 | ..Default::default() | |
| 429 | }); | |
| 430 | let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor { | |
| 431 | label: Some("Glyph atlas"), | |
| 432 | layout: &pipeline.get_bind_group_layout(0), | |
| 433 | entries: &[ | |
| 434 | wgpu::BindGroupEntry { | |
| 435 | binding: 0, | |
| 436 | resource: wgpu::BindingResource::TextureView(&view), | |
| 437 | }, | |
| 438 | wgpu::BindGroupEntry { | |
| 439 | binding: 1, | |
| 440 | resource: wgpu::BindingResource::Sampler(&sampler), | |
| 441 | }, | |
| 442 | ], | |
| 443 | }); | |
| 444 | let vertex_buffer = device.create_buffer(&wgpu::BufferDescriptor { | |
| 445 | label: Some("Draw vertices"), | |
| 446 | size: (MAX_VERTICES * size_of::<Vertex>()) as u64, | |
| 447 | usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST, | |
| 448 | mapped_at_creation: false, | |
| 449 | }); | |
| 450 | let image_sampler = device.create_sampler(&wgpu::SamplerDescriptor { | |
| 451 | label: Some("Draw image filtering"), | |
| 452 | mag_filter: wgpu::FilterMode::Linear, | |
| 453 | min_filter: wgpu::FilterMode::Linear, | |
| 454 | ..Default::default() | |
| 455 | }); | |
| 456 | queue.write_texture( | |
| 457 | atlas.as_image_copy(), | |
| 458 | &[255; 4], | |
| 459 | wgpu::TexelCopyBufferLayout { | |
| 460 | offset: 0, | |
| 461 | bytes_per_row: Some(4), | |
| 462 | rows_per_image: Some(1), | |
| 463 | }, | |
| 464 | wgpu::Extent3d { | |
| 465 | width: 1, | |
| 466 | height: 1, | |
| 467 | depth_or_array_layers: 1, | |
| 468 | }, | |
| 469 | ); | |
| 470 | Self { | |
| 471 | device, | |
| 472 | queue, | |
| 473 | pipeline, | |
| 474 | image_pipeline, | |
| 475 | atlas, | |
| 476 | bind_group, | |
| 477 | image_sampler, | |
| 478 | vertex_buffer, | |
| 479 | vertices: Vec::new(), | |
| 480 | glyphs: HashMap::new(), | |
| 481 | images: HashMap::new(), | |
| 482 | batches: Vec::new(), | |
| 483 | scaler: ScaleContext::with_max_entries(32), | |
| 484 | pen: [1, 0], | |
| 485 | row_height: 1, | |
| 486 | } | |
| 487 | } | |
| 488 | ||
| 489 | pub fn clear_glyph_cache(&mut self) { | |
| 490 | self.glyphs.clear(); | |
| 491 | self.pen = [1, 0]; | |
| 492 | self.row_height = 1; | |
| 493 | } | |
| 494 | ||
| 495 | /// Forgets uploaded images; they upload again when next painted. | |
| 496 | pub fn clear_images(&mut self) { | |
| 497 | self.images.clear(); | |
| 498 | } | |
| 499 | ||
| 500 | pub fn occupancy(&self) -> Occupancy { | |
| 501 | let image_bytes = self.images.values().map(|image| image.bytes).sum(); | |
| 502 | Occupancy { | |
| 503 | glyphs: self.glyphs.len(), | |
| 504 | atlas_texels: self | |
| 505 | .glyphs | |
| 506 | .values() | |
| 507 | .flatten() | |
| 508 | .map(|glyph| u64::from(glyph.width) * u64::from(glyph.height)) | |
| 509 | .sum(), | |
| 510 | images: self.images.len(), | |
| 511 | image_bytes, | |
| 512 | vertices: self.vertices.len(), | |
| 513 | batches: self.batches.len(), | |
| 514 | vertex_capacity_bytes: self.vertices.capacity() * size_of::<Vertex>(), | |
| 515 | batch_capacity_bytes: self.batches.capacity() * size_of::<Batch>(), | |
| 516 | glyph_capacity: self.glyphs.capacity(), | |
| 517 | image_capacity: self.images.capacity(), | |
| 518 | atlas_bytes: u64::from(self.atlas.width()) * u64::from(self.atlas.height()) * 4, | |
| 519 | vertex_buffer_bytes: self.vertex_buffer.size(), | |
| 520 | within_budget: self.glyphs.len() <= MAX_GLYPHS | |
| 521 | && self.images.len() <= MAX_IMAGES | |
| 522 | && image_bytes <= MAX_IMAGE_BYTES | |
| 523 | && self.vertices.len() <= MAX_VERTICES, | |
| 524 | } | |
| 525 | } | |
| 526 | ||
| 527 | /// Clears `target`, `size` device pixels, to linear `clear` and paints the layers in order. | |
| 528 | pub fn draw( | |
| 529 | &mut self, | |
| 530 | target: &wgpu::TextureView, | |
| 531 | size: [u32; 2], | |
| 532 | clear: [f32; 4], | |
| 533 | layers: &[Layer<'_>], | |
| 534 | ) -> Result<(), RenderError> { | |
| 535 | if size.contains(&0) | |
| 536 | || clear.iter().any(|v| !v.is_finite()) | |
| 537 | || layers.iter().any(|layer| { | |
| 538 | !layer.scale.is_finite() | |
| 539 | || layer.scale <= 0.0 | |
| 540 | || layer.origin.iter().any(|v| !v.is_finite()) | |
| 541 | || layer | |
| 542 | .clip | |
| 543 | .is_some_and(|clip| clip.iter().any(|v| !v.is_finite())) | |
| 544 | }) | |
| 545 | { | |
| 546 | return Err(RenderError::InvalidLayer); | |
| 547 | } | |
| 548 | let mut active_images = HashSet::new(); | |
| 549 | let mut image_bytes = 0; | |
| 550 | for layer in layers { | |
| 551 | let space = Space { | |
| 552 | size, | |
| 553 | scale: layer.scale, | |
| 554 | origin: layer.origin, | |
| 555 | }; | |
| 556 | for primitive in layer.primitives { | |
| 557 | if let Primitive::Image { image, rect } = primitive | |
| 558 | && space.visible_rect(*rect)?.is_some() | |
| 559 | && active_images.insert(image.id()) | |
| 560 | { | |
| 561 | image_bytes += image.pixels().len() as u64; | |
| 562 | if image_bytes > MAX_IMAGE_BYTES || active_images.len() > MAX_IMAGES { | |
| 563 | return Err(RenderError::ImageBudget); | |
| 564 | } | |
| 565 | } | |
| 566 | } | |
| 567 | } | |
| 568 | for attempt in 0..2 { | |
| 569 | self.vertices.clear(); | |
| 570 | self.batches.clear(); | |
| 571 | match self.prepare(size, layers, &active_images) { | |
| 572 | Err(RenderError::AtlasFull) if attempt == 0 => self.clear_glyph_cache(), | |
| 573 | result => { | |
| 574 | result?; | |
| 575 | break; | |
| 576 | } | |
| 577 | } | |
| 578 | } | |
| 579 | self.queue | |
| 580 | .write_buffer(&self.vertex_buffer, 0, bytemuck::cast_slice(&self.vertices)); | |
| 581 | let mut encoder = self.device.create_command_encoder(&Default::default()); | |
| 582 | { | |
| 583 | let [r, g, b, a] = clear.map(f64::from); | |
| 584 | let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor { | |
| 585 | label: Some("Draw"), | |
| 586 | color_attachments: &[Some(wgpu::RenderPassColorAttachment { | |
| 587 | view: target, | |
| 588 | depth_slice: None, | |
| 589 | resolve_target: None, | |
| 590 | ops: wgpu::Operations { | |
| 591 | load: wgpu::LoadOp::Clear(wgpu::Color { r, g, b, a }), | |
| 592 | store: wgpu::StoreOp::Store, | |
| 593 | }, | |
| 594 | })], | |
| 595 | ..Default::default() | |
| 596 | }); | |
| 597 | pass.set_vertex_buffer(0, self.vertex_buffer.slice(..)); | |
| 598 | for batch in &self.batches { | |
| 599 | let [x, y, width, height] = batch.scissor; | |
| 600 | pass.set_scissor_rect(x, y, width, height); | |
| 601 | pass.set_pipeline(if batch.image.is_some() { | |
| 602 | &self.image_pipeline | |
| 603 | } else { | |
| 604 | &self.pipeline | |
| 605 | }); | |
| 606 | let binding = batch | |
| 607 | .image | |
| 608 | .map(|id| &self.images[&id].binding) | |
| 609 | .unwrap_or(&self.bind_group); | |
| 610 | pass.set_bind_group(0, binding, &[]); | |
| 611 | pass.draw(batch.vertices.clone(), 0..1); | |
| 612 | } | |
| 613 | } | |
| 614 | self.queue.submit([encoder.finish()]); | |
| 615 | Ok(()) | |
| 616 | } | |
| 617 | ||
| 618 | fn prepare( | |
| 619 | &mut self, | |
| 620 | size: [u32; 2], | |
| 621 | layers: &[Layer<'_>], | |
| 622 | active_images: &HashSet<u64>, | |
| 623 | ) -> Result<(), RenderError> { | |
| 624 | for layer in layers { | |
| 625 | let space = Space { | |
| 626 | size, | |
| 627 | scale: layer.scale, | |
| 628 | origin: layer.origin, | |
| 629 | }; | |
| 630 | let bounds = match layer.clip { | |
| 631 | Some(clip) => space.scissor(clip), | |
| 632 | None => [0, 0, size[0], size[1]], | |
| 633 | }; | |
| 634 | if bounds[2] == 0 || bounds[3] == 0 { | |
| 635 | continue; | |
| 636 | } | |
| 637 | for primitive in layer.primitives { | |
| 638 | self.primitive(space, bounds, primitive, active_images)?; | |
| 639 | } | |
| 640 | } | |
| 641 | Ok(()) | |
| 642 | } | |
| 643 | ||
| 644 | fn primitive( | |
| 645 | &mut self, | |
| 646 | space: Space, | |
| 647 | bounds: [u32; 4], | |
| 648 | primitive: &Primitive<'_>, | |
| 649 | active_images: &HashSet<u64>, | |
| 650 | ) -> Result<(), RenderError> { | |
| 651 | let start = self.vertices.len() as u32; | |
| 652 | let mut image_id = None; | |
| 653 | let mut scissor = bounds; | |
| 654 | match primitive { | |
| 655 | Primitive::Icon { | |
| 656 | sources, | |
| 657 | origin, | |
| 658 | size, | |
| 659 | tint, | |
| 660 | } => { | |
| 661 | let origin = [ | |
| 662 | space.origin[0] + origin[0] * space.scale, | |
| 663 | space.origin[1] + origin[1] * space.scale, | |
| 664 | ]; | |
| 665 | self.icon(Space { origin, ..space }, sources, *size, *tint)?; | |
| 666 | } | |
| 667 | Primitive::Path { | |
| 668 | data, | |
| 669 | origin, | |
| 670 | style, | |
| 671 | colors, | |
| 672 | } => self.path(space, data, *origin, *style, *colors)?, | |
| 673 | Primitive::Text { | |
| 674 | text, | |
| 675 | origin, | |
| 676 | clip, | |
| 677 | ink, | |
| 678 | } => { | |
| 679 | let origin = [ | |
| 680 | space.origin[0] + origin[0] * space.scale, | |
| 681 | space.origin[1] + origin[1] * space.scale, | |
| 682 | ]; | |
| 683 | if origin.iter().any(|v| !v.is_finite()) { | |
| 684 | return Err(RenderError::InvalidPrimitive); | |
| 685 | } | |
| 686 | if let Some(clip) = clip { | |
| 687 | let Some(rect) = space.visible_rect(*clip)? else { | |
| 688 | return Ok(()); | |
| 689 | }; | |
| 690 | scissor = intersect(bounds, space.scissor(rect)); | |
| 691 | if scissor[2] == 0 || scissor[3] == 0 { | |
| 692 | return Ok(()); | |
| 693 | } | |
| 694 | } | |
| 695 | let space = Space { origin, ..space }; | |
| 696 | text.runs(&mut |run| self.glyph_run(space, run, *ink))?; | |
| 697 | } | |
| 698 | Primitive::Rect { rect, color } => { | |
| 699 | self.quad( | |
| 700 | space, | |
| 701 | [ | |
| 702 | rect[0] * space.scale + space.origin[0], | |
| 703 | rect[1] * space.scale + space.origin[1], | |
| 704 | rect[2] * space.scale + space.origin[0], | |
| 705 | rect[3] * space.scale + space.origin[1], | |
| 706 | ], | |
| 707 | [0.5 / ATLAS_SIZE as f32; 4], | |
| 708 | *color, | |
| 709 | )?; | |
| 710 | } | |
| 711 | Primitive::RoundedRect { | |
| 712 | rect, | |
| 713 | radius, | |
| 714 | stroke, | |
| 715 | color, | |
| 716 | } => self.rounded_rect(space, *rect, *radius, *stroke, [*color; 2])?, | |
| 717 | Primitive::Gradient { | |
| 718 | rect, | |
| 719 | radius, | |
| 720 | colors, | |
| 721 | } => self.rounded_rect(space, *rect, *radius, None, *colors)?, | |
| 722 | Primitive::Segment { | |
| 723 | from, | |
| 724 | to, | |
| 725 | width, | |
| 726 | round, | |
| 727 | color, | |
| 728 | } => self.segment(space, *from, *to, *width, *round, *color)?, | |
| 729 | Primitive::Image { image, rect } => { | |
| 730 | if let Some(rect) = space.visible_rect(*rect)? { | |
| 731 | self.image(image, active_images)?; | |
| 732 | image_id = Some(image.id()); | |
| 733 | self.quad(space, rect, [0.0, 0.0, 1.0, 1.0], [1.0; 4])?; | |
| 734 | } | |
| 735 | } | |
| 736 | } | |
| 737 | let end = self.vertices.len() as u32; | |
| 738 | if start != end { | |
| 739 | if let Some(last) = self.batches.last_mut() | |
| 740 | && last.image == image_id | |
| 741 | && last.scissor == scissor | |
| 742 | { | |
| 743 | last.vertices.end = end; | |
| 744 | } else { | |
| 745 | self.batches.push(Batch { | |
| 746 | vertices: start..end, | |
| 747 | image: image_id, | |
| 748 | scissor, | |
| 749 | }); | |
| 750 | } | |
| 751 | } | |
| 752 | Ok(()) | |
| 753 | } | |
| 754 | ||
| 755 | fn image(&mut self, image: &RasterImage, active: &HashSet<u64>) -> Result<(), RenderError> { | |
| 756 | if self.images.contains_key(&image.id()) { | |
| 757 | return Ok(()); | |
| 758 | } | |
| 759 | if image | |
| 760 | .size | |
| 761 | .iter() | |
| 762 | .any(|v| *v > self.device.limits().max_texture_dimension_2d) | |
| 763 | { | |
| 764 | return Err(RenderError::ImageTooLarge); | |
| 765 | } | |
| 766 | let bytes = image.pixels().len() as u64; | |
| 767 | while self.images.values().map(|i| i.bytes).sum::<u64>() + bytes > MAX_IMAGE_BYTES | |
| 768 | || self.images.len() >= MAX_IMAGES | |
| 769 | { | |
| 770 | let id = self | |
| 771 | .images | |
| 772 | .keys() | |
| 773 | .find(|id| !active.contains(id)) | |
| 774 | .copied() | |
| 775 | .ok_or(RenderError::ImageBudget)?; | |
| 776 | self.images.remove(&id); | |
| 777 | } | |
| 778 | let size = wgpu::Extent3d { | |
| 779 | width: image.size[0], | |
| 780 | height: image.size[1], | |
| 781 | depth_or_array_layers: 1, | |
| 782 | }; | |
| 783 | let texture = self.device.create_texture(&wgpu::TextureDescriptor { | |
| 784 | label: Some("Draw image"), | |
| 785 | size, | |
| 786 | mip_level_count: 1, | |
| 787 | sample_count: 1, | |
| 788 | dimension: wgpu::TextureDimension::D2, | |
| 789 | format: wgpu::TextureFormat::Rgba8UnormSrgb, | |
| 790 | usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST, | |
| 791 | view_formats: &[], | |
| 792 | }); | |
| 793 | self.queue.write_texture( | |
| 794 | texture.as_image_copy(), | |
| 795 | image.pixels(), | |
| 796 | wgpu::TexelCopyBufferLayout { | |
| 797 | offset: 0, | |
| 798 | bytes_per_row: Some(image.size[0] * 4), | |
| 799 | rows_per_image: Some(image.size[1]), | |
| 800 | }, | |
| 801 | size, | |
| 802 | ); | |
| 803 | let binding = self.device.create_bind_group(&wgpu::BindGroupDescriptor { | |
| 804 | label: Some("Draw image"), | |
| 805 | layout: &self.pipeline.get_bind_group_layout(0), | |
| 806 | entries: &[ | |
| 807 | wgpu::BindGroupEntry { | |
| 808 | binding: 0, | |
| 809 | resource: wgpu::BindingResource::TextureView( | |
| 810 | &texture.create_view(&Default::default()), | |
| 811 | ), | |
| 812 | }, | |
| 813 | wgpu::BindGroupEntry { | |
| 814 | binding: 1, | |
| 815 | resource: wgpu::BindingResource::Sampler(&self.image_sampler), | |
| 816 | }, | |
| 817 | ], | |
| 818 | }); | |
| 819 | self.images | |
| 820 | .insert(image.id(), CachedImage { binding, bytes }); | |
| 821 | Ok(()) | |
| 822 | } | |
| 823 | ||
| 824 | fn glyph_run( | |
| 825 | &mut self, | |
| 826 | space: Space, | |
| 827 | run: GlyphRun<'_>, | |
| 828 | ink: [f32; 4], | |
| 829 | ) -> Result<(), RenderError> { | |
| 830 | let scale = space.scale; | |
| 831 | let origin = space.origin; | |
| 832 | let line_top = run.line[0] * scale + origin[1]; | |
| 833 | let line_height = run.line[1] * scale; | |
| 834 | if line_top + line_height < 0.0 || line_top > space.size[1] as f32 { | |
| 835 | return Ok(()); | |
| 836 | } | |
| 837 | let size = run.size * scale; | |
| 838 | if !size.is_finite() || size > ATLAS_SIZE as f32 { | |
| 839 | return Err(RenderError::AtlasFull); | |
| 840 | } | |
| 841 | for glyph in run.glyphs { | |
| 842 | let x = glyph.x * scale + origin[0]; | |
| 843 | let y = glyph.y * scale + origin[1]; | |
| 844 | // Quantization affects raster coverage only, never advances or line breaks. | |
| 845 | let x = (x * 4.0).round() * 0.25; | |
| 846 | let y = (y * 4.0).round() * 0.25; | |
| 847 | let glyph_id = glyph | |
| 848 | .id | |
| 849 | .try_into() | |
| 850 | .map_err(|_| RenderError::UnsupportedGlyph)?; | |
| 851 | let phase = [((x - x.floor()) * 4.0) as u8, ((y - y.floor()) * 4.0) as u8]; | |
| 852 | let key = AtlasKey::Text { | |
| 853 | font: run.font.id(), | |
| 854 | index: run.index, | |
| 855 | glyph: glyph_id, | |
| 856 | size: size.to_bits(), | |
| 857 | coords: run.coords.to_vec(), | |
| 858 | phase, | |
| 859 | embolden: run.embolden, | |
| 860 | skew: run.skew.unwrap_or(0.0).to_bits(), | |
| 861 | }; | |
| 862 | let cached = if let Some(cached) = self.glyphs.get(&key) { | |
| 863 | *cached | |
| 864 | } else { | |
| 865 | if self.glyphs.len() >= MAX_GLYPHS { | |
| 866 | return Err(RenderError::AtlasFull); | |
| 867 | } | |
| 868 | let font = FontRef::from_index(run.font.as_ref(), run.index as usize) | |
| 869 | .ok_or(RenderError::UnreadableFont)?; | |
| 870 | let mut scaler = self | |
| 871 | .scaler | |
| 872 | .builder_with_id(font, [run.font.id(), u64::from(run.index)]) | |
| 873 | .size(size) | |
| 874 | .hint(false) | |
| 875 | .normalized_coords(run.coords.iter().copied()) | |
| 876 | .build(); | |
| 877 | let mut render = Render::new(&[ | |
| 878 | Source::ColorOutline(0), | |
| 879 | Source::ColorBitmap(StrikeWith::BestFit), | |
| 880 | Source::Outline, | |
| 881 | ]); | |
| 882 | // Swash rasterizes outlines in Y-up coordinates. | |
| 883 | render.format(Format::Alpha).offset(Vector::new( | |
| 884 | f32::from(phase[0]) * 0.25, | |
| 885 | -f32::from(phase[1]) * 0.25, | |
| 886 | )); | |
| 887 | if run.embolden { | |
| 888 | render.embolden(size / 24.0); | |
| 889 | } | |
| 890 | if let Some(skew) = run.skew { | |
| 891 | render.transform(Some(Transform::skew( | |
| 892 | Angle::from_degrees(skew), | |
| 893 | Angle::from_degrees(0.0), | |
| 894 | ))); | |
| 895 | } | |
| 896 | let image = render.render(&mut scaler, glyph_id); | |
| 897 | let cached = if let Some(image) = | |
| 898 | image.filter(|i| i.placement.width > 0 && i.placement.height > 0) | |
| 899 | { | |
| 900 | Some(self.upload(image)?) | |
| 901 | } else { | |
| 902 | None | |
| 903 | }; | |
| 904 | self.glyphs.insert(key, cached); | |
| 905 | cached | |
| 906 | }; | |
| 907 | if let Some(glyph) = cached { | |
| 908 | let mut left = x.floor() + glyph.left as f32; | |
| 909 | let mut top = y.floor() - glyph.top as f32; | |
| 910 | let mut width = glyph.width as f32; | |
| 911 | let mut height = glyph.height as f32; | |
| 912 | if glyph.color { | |
| 913 | let ratio = (line_height / height).min(1.0); | |
| 914 | left += width * (1.0 - ratio) * 0.5; | |
| 915 | width *= ratio; | |
| 916 | height *= ratio; | |
| 917 | top = top.clamp(line_top, (line_top + line_height - height).max(line_top)); | |
| 918 | } | |
| 919 | let atlas_size = ATLAS_SIZE as f32; | |
| 920 | self.quad( | |
| 921 | space, | |
| 922 | [left, top, left + width, top + height], | |
| 923 | [ | |
| 924 | glyph.x as f32 / atlas_size, | |
| 925 | glyph.y as f32 / atlas_size, | |
| 926 | (glyph.x + glyph.width) as f32 / atlas_size, | |
| 927 | (glyph.y + glyph.height) as f32 / atlas_size, | |
| 928 | ], | |
| 929 | if glyph.color { | |
| 930 | [1.0; 4] | |
| 931 | } else { | |
| 932 | run.color.unwrap_or(ink) | |
| 933 | }, | |
| 934 | )?; | |
| 935 | } | |
| 936 | } | |
| 937 | for decoration in run.decorations { | |
| 938 | let x = decoration.x * scale + origin[0]; | |
| 939 | let y = decoration.y * scale + origin[1]; | |
| 940 | self.quad( | |
| 941 | space, | |
| 942 | [ | |
| 943 | x, | |
| 944 | y, | |
| 945 | x + decoration.width * scale, | |
| 946 | y + (decoration.thickness * scale).max(1.0), | |
| 947 | ], | |
| 948 | [0.5 / ATLAS_SIZE as f32; 4], | |
| 949 | decoration.color.unwrap_or(ink), | |
| 950 | )?; | |
| 951 | } | |
| 952 | Ok(()) | |
| 953 | } | |
| 954 | ||
| 955 | fn upload(&mut self, image: swash::scale::image::Image) -> Result<AtlasGlyph, RenderError> { | |
| 956 | let p = image.placement; | |
| 957 | if p.width + 2 > ATLAS_SIZE || p.height + 2 > ATLAS_SIZE { | |
| 958 | return Err(RenderError::AtlasFull); | |
| 959 | } | |
| 960 | if self.pen[0] + p.width + 1 > ATLAS_SIZE { | |
| 961 | self.pen[0] = 0; | |
| 962 | self.pen[1] += self.row_height + 1; | |
| 963 | self.row_height = 0; | |
| 964 | } | |
| 965 | if self.pen[1] + p.height + 1 > ATLAS_SIZE { | |
| 966 | return Err(RenderError::AtlasFull); | |
| 967 | } | |
| 968 | let cached = AtlasGlyph { | |
| 969 | x: self.pen[0], | |
| 970 | y: self.pen[1], | |
| 971 | width: p.width, | |
| 972 | height: p.height, | |
| 973 | left: p.left, | |
| 974 | top: p.top, | |
| 975 | color: image.content == Content::Color, | |
| 976 | }; | |
| 977 | let rgba = match image.content { | |
| 978 | Content::Mask => image | |
| 979 | .data | |
| 980 | .iter() | |
| 981 | .flat_map(|a| [255, 255, 255, *a]) | |
| 982 | .collect(), | |
| 983 | _ => image.data, | |
| 984 | }; | |
| 985 | self.queue.write_texture( | |
| 986 | wgpu::TexelCopyTextureInfo { | |
| 987 | origin: wgpu::Origin3d { | |
| 988 | x: cached.x, | |
| 989 | y: cached.y, | |
| 990 | z: 0, | |
| 991 | }, | |
| 992 | ..self.atlas.as_image_copy() | |
| 993 | }, | |
| 994 | &rgba, | |
| 995 | wgpu::TexelCopyBufferLayout { | |
| 996 | offset: 0, | |
| 997 | bytes_per_row: Some(p.width * 4), | |
| 998 | rows_per_image: Some(p.height), | |
| 999 | }, | |
| 1000 | wgpu::Extent3d { | |
| 1001 | width: p.width, | |
| 1002 | height: p.height, | |
| 1003 | depth_or_array_layers: 1, | |
| 1004 | }, | |
| 1005 | ); | |
| 1006 | self.pen[0] += p.width + 1; | |
| 1007 | self.row_height = self.row_height.max(p.height); | |
| 1008 | Ok(cached) | |
| 1009 | } | |
| 1010 | ||
| 1011 | fn icon( | |
| 1012 | &mut self, | |
| 1013 | space: Space, | |
| 1014 | sources: &'static [&'static str], | |
| 1015 | size: f32, | |
| 1016 | tint: [f32; 4], | |
| 1017 | ) -> Result<(), RenderError> { | |
| 1018 | let size = size * space.scale; | |
| 1019 | let [x, y] = space.origin.map(|v| (v * 4.0).round() * 0.25); | |
| 1020 | if !size.is_finite() || [x, y].iter().any(|v| !v.is_finite()) { | |
| 1021 | return Err(RenderError::InvalidPrimitive); | |
| 1022 | } | |
| 1023 | if x + size + 1.0 < 0.0 | |
| 1024 | || y + size + 1.0 < 0.0 | |
| 1025 | || x - 1.0 > space.size[0] as f32 | |
| 1026 | || y - 1.0 > space.size[1] as f32 | |
| 1027 | { | |
| 1028 | return Ok(()); | |
| 1029 | } | |
| 1030 | if size + 3.0 > ATLAS_SIZE as f32 { | |
| 1031 | return Err(RenderError::AtlasFull); | |
| 1032 | } | |
| 1033 | let phase = [((x - x.floor()) * 4.0) as u8, ((y - y.floor()) * 4.0) as u8]; | |
| 1034 | let key = AtlasKey::Icon { | |
| 1035 | sources, | |
| 1036 | size: size.to_bits(), | |
| 1037 | phase, | |
| 1038 | }; | |
| 1039 | let glyph = if let Some(Some(glyph)) = self.glyphs.get(&key) { | |
| 1040 | *glyph | |
| 1041 | } else { | |
| 1042 | if self.glyphs.len() >= MAX_GLYPHS { | |
| 1043 | return Err(RenderError::AtlasFull); | |
| 1044 | } | |
| 1045 | let glyph = self.upload(icon::rasterize(sources, size, phase))?; | |
| 1046 | self.glyphs.insert(key, Some(glyph)); | |
| 1047 | glyph | |
| 1048 | }; | |
| 1049 | let left = x.floor(); | |
| 1050 | let top = y.floor(); | |
| 1051 | let atlas = ATLAS_SIZE as f32; | |
| 1052 | self.quad( | |
| 1053 | space, | |
| 1054 | [ | |
| 1055 | left, | |
| 1056 | top, | |
| 1057 | left + glyph.width as f32, | |
| 1058 | top + glyph.height as f32, | |
| 1059 | ], | |
| 1060 | [ | |
| 1061 | glyph.x as f32 / atlas, | |
| 1062 | glyph.y as f32 / atlas, | |
| 1063 | (glyph.x + glyph.width) as f32 / atlas, | |
| 1064 | (glyph.y + glyph.height) as f32 / atlas, | |
| 1065 | ], | |
| 1066 | tint, | |
| 1067 | ) | |
| 1068 | } | |
| 1069 | ||
| 1070 | /// `colors` are the top and bottom edges' colours. | |
| 1071 | fn rounded_rect( | |
| 1072 | &mut self, | |
| 1073 | space: Space, | |
| 1074 | rect: [f32; 4], | |
| 1075 | radius: [f32; 2], | |
| 1076 | stroke: Option<Stroke>, | |
| 1077 | colors: [[f32; 4]; 2], | |
| 1078 | ) -> Result<(), RenderError> { | |
| 1079 | if radius | |
| 1080 | .iter() | |
| 1081 | .any(|value| !value.is_finite() || *value < 0.0) | |
| 1082 | { | |
| 1083 | return Err(RenderError::InvalidPrimitive); | |
| 1084 | } | |
| 1085 | let width = match stroke { | |
| 1086 | None => 0.0, | |
| 1087 | Some(Stroke::Solid(width) | Stroke::Dashed(width)) => { | |
| 1088 | if !width.is_finite() || width <= 0.0 || width * space.scale == 0.0 { | |
| 1089 | return Err(RenderError::InvalidPrimitive); | |
| 1090 | } | |
| 1091 | width | |
| 1092 | } | |
| 1093 | }; | |
| 1094 | let rect = [ | |
| 1095 | rect[0] * space.scale + space.origin[0], | |
| 1096 | rect[1] * space.scale + space.origin[1], | |
| 1097 | rect[2] * space.scale + space.origin[0], | |
| 1098 | rect[3] * space.scale + space.origin[1], | |
| 1099 | ]; | |
| 1100 | let start = self.vertices.len(); | |
| 1101 | self.quad(space, rect, [0.5 / ATLAS_SIZE as f32; 4], colors[0])?; | |
| 1102 | let half = [(rect[2] - rect[0]) * 0.5, (rect[3] - rect[1]) * 0.5]; | |
| 1103 | if !(4.0 * (half[0] + half[1])).is_finite() || !colors[1].iter().all(|v| v.is_finite()) { | |
| 1104 | return Err(RenderError::InvalidPrimitive); | |
| 1105 | } | |
| 1106 | let radius = if radius.contains(&0.0) { | |
| 1107 | [0.0; 2] | |
| 1108 | } else { | |
| 1109 | std::array::from_fn(|axis| (radius[axis] * space.scale).min(half[axis])) | |
| 1110 | }; | |
| 1111 | let width = (width * space.scale).min(half[0].min(half[1])); | |
| 1112 | // The stroke sign selects its dash pattern at the shader boundary. | |
| 1113 | let width = if matches!(stroke, Some(Stroke::Dashed(_))) { | |
| 1114 | -width | |
| 1115 | } else { | |
| 1116 | width | |
| 1117 | }; | |
| 1118 | for vertex in &mut self.vertices[start..] { | |
| 1119 | vertex.shape = [half[0], half[1], radius[0], radius[1]]; | |
| 1120 | vertex.stroke = width; | |
| 1121 | if vertex.local[1] > 0.0 { | |
| 1122 | vertex.color = colors[1]; | |
| 1123 | } | |
| 1124 | } | |
| 1125 | Ok(()) | |
| 1126 | } | |
| 1127 | ||
| 1128 | fn path( | |
| 1129 | &mut self, | |
| 1130 | space: Space, | |
| 1131 | data: &str, | |
| 1132 | origin: [f32; 2], | |
| 1133 | style: PathStyle, | |
| 1134 | colors: [[f32; 4]; 2], | |
| 1135 | ) -> Result<(), RenderError> { | |
| 1136 | let [x, y] = [0, 1] | |
| 1137 | .map(|axis| ((space.origin[axis] + origin[axis] * space.scale) * 4.0).round() * 0.25); | |
| 1138 | let (kind, width) = match style { | |
| 1139 | PathStyle::Fill => (0, 0.0), | |
| 1140 | PathStyle::Stroke(width) => (1, width), | |
| 1141 | PathStyle::Shadow(width) => (2, width), | |
| 1142 | }; | |
| 1143 | if [x, y] | |
| 1144 | .iter() | |
| 1145 | .chain(colors.as_flattened()) | |
| 1146 | .any(|v| !v.is_finite()) | |
| 1147 | || !width.is_finite() | |
| 1148 | || width < 0.0 | |
| 1149 | || (width == 0.0 && kind != 0) | |
| 1150 | { | |
| 1151 | return Err(RenderError::InvalidPrimitive); | |
| 1152 | } | |
| 1153 | let phase = [((x - x.floor()) * 4.0) as u8, ((y - y.floor()) * 4.0) as u8]; | |
| 1154 | let key = AtlasKey::Path { | |
| 1155 | data: data.to_owned(), | |
| 1156 | scale: space.scale.to_bits(), | |
| 1157 | style: (kind, width.to_bits()), | |
| 1158 | phase, | |
| 1159 | }; | |
| 1160 | let glyph = match self.glyphs.get(&key) { | |
| 1161 | Some(glyph) => *glyph, | |
| 1162 | None => { | |
| 1163 | if self.glyphs.len() >= MAX_GLYPHS { | |
| 1164 | return Err(RenderError::AtlasFull); | |
| 1165 | } | |
| 1166 | let mut mask = Mask::new(data); | |
| 1167 | mask.transform(Some(Transform::scale(space.scale, space.scale))) | |
| 1168 | .offset(phase.map(|quarter| f32::from(quarter) * 0.25)); | |
| 1169 | if let PathStyle::Stroke(width) = style { | |
| 1170 | mask.style(swash::zeno::Stroke { | |
| 1171 | start_cap: Cap::Round, | |
| 1172 | end_cap: Cap::Round, | |
| 1173 | join: Join::Round, | |
| 1174 | ..swash::zeno::Stroke::new(width) | |
| 1175 | }); | |
| 1176 | } | |
| 1177 | let (mut pixels, mut placement) = mask.render(); | |
| 1178 | if let PathStyle::Shadow(blur) = style { | |
| 1179 | (pixels, placement) = soften(&pixels, placement, blur * space.scale); | |
| 1180 | } | |
| 1181 | let glyph = (placement.width > 0 && placement.height > 0) | |
| 1182 | .then(|| { | |
| 1183 | self.upload(swash::scale::image::Image { | |
| 1184 | content: Content::Mask, | |
| 1185 | placement, | |
| 1186 | data: pixels, | |
| 1187 | ..Default::default() | |
| 1188 | }) | |
| 1189 | }) | |
| 1190 | .transpose()?; | |
| 1191 | self.glyphs.insert(key, glyph); | |
| 1192 | glyph | |
| 1193 | } | |
| 1194 | }; | |
| 1195 | let Some(glyph) = glyph else { | |
| 1196 | return Ok(()); | |
| 1197 | }; | |
| 1198 | let left = x.floor() + glyph.left as f32; | |
| 1199 | let top = y.floor() + glyph.top as f32; | |
| 1200 | let atlas = ATLAS_SIZE as f32; | |
| 1201 | let start = self.vertices.len(); | |
| 1202 | self.quad( | |
| 1203 | space, | |
| 1204 | [ | |
| 1205 | left, | |
| 1206 | top, | |
| 1207 | left + glyph.width as f32, | |
| 1208 | top + glyph.height as f32, | |
| 1209 | ], | |
| 1210 | [ | |
| 1211 | glyph.x as f32 / atlas, | |
| 1212 | glyph.y as f32 / atlas, | |
| 1213 | (glyph.x + glyph.width) as f32 / atlas, | |
| 1214 | (glyph.y + glyph.height) as f32 / atlas, | |
| 1215 | ], | |
| 1216 | colors[0], | |
| 1217 | )?; | |
| 1218 | for vertex in &mut self.vertices[start..] { | |
| 1219 | if vertex.local[1] > 0.0 { | |
| 1220 | vertex.color = colors[1]; | |
| 1221 | } | |
| 1222 | } | |
| 1223 | Ok(()) | |
| 1224 | } | |
| 1225 | ||
| 1226 | /// Draws the segment as a capsule in its own frame, reusing the rounded-rectangle distance. | |
| 1227 | fn segment( | |
| 1228 | &mut self, | |
| 1229 | space: Space, | |
| 1230 | from: [f32; 2], | |
| 1231 | to: [f32; 2], | |
| 1232 | width: f32, | |
| 1233 | round: bool, | |
| 1234 | color: [f32; 4], | |
| 1235 | ) -> Result<(), RenderError> { | |
| 1236 | let pixel = |p: [f32; 2]| { | |
| 1237 | [ | |
| 1238 | p[0] * space.scale + space.origin[0], | |
| 1239 | p[1] * space.scale + space.origin[1], | |
| 1240 | ] | |
| 1241 | }; | |
| 1242 | let [from, to] = [pixel(from), pixel(to)]; | |
| 1243 | // Hairlines stay one device pixel wide. | |
| 1244 | let radius = (width * space.scale).max(1.0) * 0.5; | |
| 1245 | if from.iter().chain(&to).chain(&color).any(|v| !v.is_finite()) || !radius.is_finite() { | |
| 1246 | return Err(RenderError::InvalidPrimitive); | |
| 1247 | } | |
| 1248 | let pad = radius + 1.0; | |
| 1249 | if from[0].max(to[0]) + pad < 0.0 | |
| 1250 | || from[1].max(to[1]) + pad < 0.0 | |
| 1251 | || from[0].min(to[0]) - pad > space.size[0] as f32 | |
| 1252 | || from[1].min(to[1]) - pad > space.size[1] as f32 | |
| 1253 | { | |
| 1254 | return Ok(()); | |
| 1255 | } | |
| 1256 | if self.vertices.len() + 6 > MAX_VERTICES { | |
| 1257 | return Err(RenderError::FrameTooLarge); | |
| 1258 | } | |
| 1259 | let delta = [to[0] - from[0], to[1] - from[1]]; | |
| 1260 | let length = delta[0].hypot(delta[1]); | |
| 1261 | let along = if length > 0.0 { | |
| 1262 | [delta[0] / length, delta[1] / length] | |
| 1263 | } else { | |
| 1264 | [1.0, 0.0] | |
| 1265 | }; | |
| 1266 | let across = [-along[1], along[0]]; | |
| 1267 | let center = [(from[0] + to[0]) * 0.5, (from[1] + to[1]) * 0.5]; | |
| 1268 | let half = [length * 0.5 + radius, radius]; | |
| 1269 | let corner = if round { radius } else { 0.0 }; | |
| 1270 | let [hx, hy] = [half[0] + 1.0, half[1] + 1.0]; | |
| 1271 | for local in [ | |
| 1272 | [-hx, -hy], | |
| 1273 | [-hx, hy], | |
| 1274 | [hx, hy], | |
| 1275 | [-hx, -hy], | |
| 1276 | [hx, hy], | |
| 1277 | [hx, -hy], | |
| 1278 | ] { | |
| 1279 | let x = center[0] + along[0] * local[0] + across[0] * local[1]; | |
| 1280 | let y = center[1] + along[1] * local[0] + across[1] * local[1]; | |
| 1281 | self.vertices.push(Vertex { | |
| 1282 | position: [ | |
| 1283 | x * 2.0 / space.size[0] as f32 - 1.0, | |
| 1284 | 1.0 - y * 2.0 / space.size[1] as f32, | |
| 1285 | ], | |
| 1286 | uv: [0.5 / ATLAS_SIZE as f32; 2], | |
| 1287 | color, | |
| 1288 | local, | |
| 1289 | shape: [half[0], half[1], corner, corner], | |
| 1290 | stroke: 0.0, | |
| 1291 | }); | |
| 1292 | } | |
| 1293 | Ok(()) | |
| 1294 | } | |
| 1295 | ||
| 1296 | fn quad( | |
| 1297 | &mut self, | |
| 1298 | space: Space, | |
| 1299 | rect: [f32; 4], | |
| 1300 | uv: [f32; 4], | |
| 1301 | color: [f32; 4], | |
| 1302 | ) -> Result<(), RenderError> { | |
| 1303 | if rect.iter().chain(&color).any(|v| !v.is_finite()) | |
| 1304 | || rect[2] < rect[0] | |
| 1305 | || rect[3] < rect[1] | |
| 1306 | { | |
| 1307 | return Err(RenderError::InvalidPrimitive); | |
| 1308 | } | |
| 1309 | if rect[2] < 0.0 | |
| 1310 | || rect[3] < 0.0 | |
| 1311 | || rect[0] > space.size[0] as f32 | |
| 1312 | || rect[1] > space.size[1] as f32 | |
| 1313 | { | |
| 1314 | return Ok(()); | |
| 1315 | } | |
| 1316 | if self.vertices.len() + 6 > MAX_VERTICES { | |
| 1317 | return Err(RenderError::FrameTooLarge); | |
| 1318 | } | |
| 1319 | let x0 = rect[0] * 2.0 / space.size[0] as f32 - 1.0; | |
| 1320 | let x1 = rect[2] * 2.0 / space.size[0] as f32 - 1.0; | |
| 1321 | let y0 = 1.0 - rect[1] * 2.0 / space.size[1] as f32; | |
| 1322 | let y1 = 1.0 - rect[3] * 2.0 / space.size[1] as f32; | |
| 1323 | if [x0, x1, y0, y1].iter().any(|v| !v.is_finite()) { | |
| 1324 | return Err(RenderError::InvalidPrimitive); | |
| 1325 | } | |
| 1326 | let [hx, hy] = [(rect[2] - rect[0]) * 0.5, (rect[3] - rect[1]) * 0.5]; | |
| 1327 | for (position, uv, local) in [ | |
| 1328 | ([x0, y0], [uv[0], uv[1]], [-hx, -hy]), | |
| 1329 | ([x0, y1], [uv[0], uv[3]], [-hx, hy]), | |
| 1330 | ([x1, y1], [uv[2], uv[3]], [hx, hy]), | |
| 1331 | ([x0, y0], [uv[0], uv[1]], [-hx, -hy]), | |
| 1332 | ([x1, y1], [uv[2], uv[3]], [hx, hy]), | |
| 1333 | ([x1, y0], [uv[2], uv[1]], [hx, -hy]), | |
| 1334 | ] { | |
| 1335 | self.vertices.push(Vertex { | |
| 1336 | position, | |
| 1337 | uv, | |
| 1338 | color, | |
| 1339 | local, | |
| 1340 | shape: [0.0; 4], | |
| 1341 | stroke: 0.0, | |
| 1342 | }); | |
| 1343 | } | |
| 1344 | Ok(()) | |
| 1345 | } | |
| 1346 | } | |
| 1347 | ||
| 1348 | /// Blurs a coverage mask about `radius` device pixels with three box blurs on each axis, | |
| 1349 | /// growing it to hold the spread. | |
| 1350 | fn soften( | |
| 1351 | pixels: &[u8], | |
| 1352 | placement: swash::zeno::Placement, | |
| 1353 | radius: f32, | |
| 1354 | ) -> (Vec<u8>, swash::zeno::Placement) { | |
| 1355 | let reach = (radius / 2.0).ceil().max(1.0) as usize; | |
| 1356 | let pad = 3 * reach; | |
| 1357 | let [width, height] = [placement.width as usize, placement.height as usize]; | |
| 1358 | let [wide, tall] = [width + 2 * pad, height + 2 * pad]; | |
| 1359 | let mut values = vec![0.0_f32; wide * tall]; | |
| 1360 | for row in 0..height { | |
| 1361 | for column in 0..width { | |
| 1362 | values[(row + pad) * wide + column + pad] = f32::from(pixels[row * width + column]); | |
| 1363 | } | |
| 1364 | } | |
| 1365 | let window = (2 * reach + 1) as f32; | |
| 1366 | let mut line = Vec::new(); | |
| 1367 | for _ in 0..3 { | |
| 1368 | // Rows, then columns. | |
| 1369 | for (count, step, lines, stride) in [(wide, 1, tall, wide), (tall, wide, wide, 1)] { | |
| 1370 | for index in 0..lines { | |
| 1371 | let at = |position: usize| index * stride + position * step; | |
| 1372 | line.clear(); | |
| 1373 | line.extend((0..count).map(|position| values[at(position)])); | |
| 1374 | let mut sum: f32 = line[..reach.min(count)].iter().sum(); | |
| 1375 | for position in 0..count { | |
| 1376 | if position + reach < count { | |
| 1377 | sum += line[position + reach]; | |
| 1378 | } | |
| 1379 | values[at(position)] = sum / window; | |
| 1380 | if position >= reach { | |
| 1381 | sum -= line[position - reach]; | |
| 1382 | } | |
| 1383 | } | |
| 1384 | } | |
| 1385 | } | |
| 1386 | } | |
| 1387 | ( | |
| 1388 | values | |
| 1389 | .into_iter() | |
| 1390 | .map(|value| value.round().clamp(0.0, 255.0) as u8) | |
| 1391 | .collect(), | |
| 1392 | swash::zeno::Placement { | |
| 1393 | left: placement.left - pad as i32, | |
| 1394 | top: placement.top - pad as i32, | |
| 1395 | width: wide as u32, | |
| 1396 | height: tall as u32, | |
| 1397 | }, | |
| 1398 | ) | |
| 1399 | } | |
| 1400 | ||
| 1401 | fn srgb_byte(value: f32) -> u8 { | |
| 1402 | let srgb = if value <= 0.0031308 { | |
| 1403 | value * 12.92 | |
| 1404 | } else { | |
| 1405 | 1.055 * value.powf(1.0 / 2.4) - 0.055 | |
| 1406 | }; | |
| 1407 | (srgb * 255.0).round() as u8 | |
| 1408 | } | |
| 1409 | ||
| 1410 | /// Linear RGBA of an opaque sRGB colour. | |
| 1411 | pub fn srgb(red: u8, green: u8, blue: u8) -> [f32; 4] { | |
| 1412 | let linear = |byte: u8| { | |
| 1413 | let value = f32::from(byte) / 255.0; | |
| 1414 | if value <= 0.04045 { | |
| 1415 | value / 12.92 | |
| 1416 | } else { | |
| 1417 | ((value + 0.055) / 1.055).powf(2.4) | |
| 1418 | } | |
| 1419 | }; | |
| 1420 | [linear(red), linear(green), linear(blue), 1.0] | |
| 1421 | } | |
| 1422 | ||
| 1423 | #[cfg(test)] | |
| 1424 | mod tests { | |
| 1425 | use super::*; | |
| 1426 | use parley::{ | |
| 1427 | FontContext, FontFamily, FontFamilyName, GenericFamily, Layout, LayoutContext, | |
| 1428 | PositionedLayoutItem, StyleProperty, | |
| 1429 | }; | |
| 1430 | use std::time::Duration; | |
| 1431 | ||
| 1432 | const CHECKBOX: &str = r##"<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><defs><linearGradient id="a" x1="0" y1="0" x2="0" y2="1"><stop offset="0" stop-color="#ffffff"/><stop offset="1" stop-color="#7090b0"/></linearGradient></defs><path fill="url(#a)" d="M2 2h12v12H2zM4 4v8h8V4z"/></svg>"##; | |
| 1433 | const MARK: &str = r##"<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><defs><linearGradient id="b" x1="0" y1="0" x2="0" y2="1"><stop offset="0" stop-color="#20c040"/><stop offset="1" stop-color="#107020"/></linearGradient></defs><path fill="url(#b)" d="M4 8l3 3 5-7-1-1-4 5-2-2z"/></svg>"##; | |
| 1434 | const DOT: &str = r##"<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><defs><linearGradient id="c" x1="0" y1="0" x2="0" y2="1"><stop offset="0" stop-color="#ffd000"/><stop offset="1" stop-color="#c08000"/></linearGradient></defs><path fill="url(#c)" d="M8 2a6 6 0 1 0 0.01 0z"/></svg>"##; | |
| 1435 | ||
| 1436 | struct Text(Layout<[u8; 4]>); | |
| 1437 | ||
| 1438 | impl Glyphs for Text { | |
| 1439 | fn runs( | |
| 1440 | &self, | |
| 1441 | paint: &mut dyn FnMut(GlyphRun<'_>) -> Result<(), RenderError>, | |
| 1442 | ) -> Result<(), RenderError> { | |
| 1443 | for line in self.0.lines() { | |
| 1444 | let metrics = line.metrics(); | |
| 1445 | for item in line.items() { | |
| 1446 | if let PositionedLayoutItem::GlyphRun(run) = item { | |
| 1447 | paint_parley_run( | |
| 1448 | &run, | |
| 1449 | metrics.baseline, | |
| 1450 | 0.0, | |
| 1451 | [metrics.block_min_coord, metrics.line_height], | |
| 1452 | |_| None, | |
| 1453 | paint, | |
| 1454 | )?; | |
| 1455 | } | |
| 1456 | } | |
| 1457 | } | |
| 1458 | Ok(()) | |
| 1459 | } | |
| 1460 | } | |
| 1461 | ||
| 1462 | fn shape(text: &str, family: &str, size: f32, underline: bool, width: f32) -> Text { | |
| 1463 | let mut fonts = FontContext::default(); | |
| 1464 | let mut context = LayoutContext::default(); | |
| 1465 | let mut builder = context.ranged_builder(&mut fonts, text, 1.0, false); | |
| 1466 | builder.push_default(StyleProperty::FontFamily(FontFamily::List( | |
| 1467 | vec![ | |
| 1468 | FontFamilyName::named(family), | |
| 1469 | GenericFamily::SansSerif.into(), | |
| 1470 | ] | |
| 1471 | .into(), | |
| 1472 | ))); | |
| 1473 | builder.push_default(StyleProperty::FontSize(size)); | |
| 1474 | builder.push_default(StyleProperty::Underline(underline)); | |
| 1475 | let mut layout = builder.build(text); | |
| 1476 | layout.break_all_lines(Some(width)); | |
| 1477 | Text(layout) | |
| 1478 | } | |
| 1479 | ||
| 1480 | #[test] | |
| 1481 | fn decoding_preserves_pixels_and_rejects_large_or_truncated_images() { | |
| 1482 | let mut encoded = Vec::new(); | |
| 1483 | let mut encoder = png::Encoder::new(&mut encoded, 2, 1); | |
| 1484 | encoder.set_color(png::ColorType::Rgba); | |
| 1485 | encoder.set_depth(png::BitDepth::Eight); | |
| 1486 | let rgba = [255, 64, 0, 255, 0, 128, 255, 128]; | |
| 1487 | encoder | |
| 1488 | .write_header() | |
| 1489 | .unwrap() | |
| 1490 | .write_image_data(&rgba) | |
| 1491 | .unwrap(); | |
| 1492 | let decoded = RasterImage::decode(&encoded).unwrap(); | |
| 1493 | assert_eq!(decoded.size, [2, 1]); | |
| 1494 | assert_eq!(decoded.pixels(), [255, 64, 0, 255, 0, 93, 188, 128]); | |
| 1495 | for end in 0..encoded.len() - 12 { | |
| 1496 | assert!(RasterImage::decode(&encoded[..end]).is_err(), "{end}"); | |
| 1497 | } | |
| 1498 | ||
| 1499 | let mut jpeg = Vec::new(); | |
| 1500 | image::codecs::jpeg::JpegEncoder::new(&mut jpeg) | |
| 1501 | .encode(&[255; 4 * 4 * 3], 4, 4, image::ExtendedColorType::Rgb8) | |
| 1502 | .unwrap(); | |
| 1503 | let decoded = RasterImage::decode(&jpeg).unwrap(); | |
| 1504 | assert_eq!(decoded.size, [4, 4]); | |
| 1505 | assert_eq!(decoded.pixels(), [255; 4 * 4 * 4]); | |
| 1506 | ||
| 1507 | let mut gif = Vec::new(); | |
| 1508 | image::codecs::gif::GifEncoder::new(&mut gif) | |
| 1509 | .encode(&[255; 2 * 2 * 4], 2, 2, image::ExtendedColorType::Rgba8) | |
| 1510 | .unwrap(); | |
| 1511 | let decoded = RasterImage::decode(&gif).unwrap(); | |
| 1512 | assert_eq!(decoded.size, [2, 2]); | |
| 1513 | assert_eq!(decoded.pixels(), [255; 2 * 2 * 4]); | |
| 1514 | ||
| 1515 | for size in [[8192, 8192], [16_385, 1]] { | |
| 1516 | let mut header = Vec::new(); | |
| 1517 | let mut encoder = png::Encoder::new(&mut header, size[0], size[1]); | |
| 1518 | encoder.set_color(png::ColorType::Rgba); | |
| 1519 | encoder.set_depth(png::BitDepth::Eight); | |
| 1520 | let mut writer = encoder.write_header().unwrap(); | |
| 1521 | writer.write_chunk(png::chunk::IDAT, &[]).unwrap(); | |
| 1522 | drop(writer); | |
| 1523 | let error = RasterImage::decode(&header).err().unwrap(); | |
| 1524 | if size[0] == 8192 { | |
| 1525 | assert!(matches!(error, RenderError::ImageBudget), "{error:?}"); | |
| 1526 | } else { | |
| 1527 | assert!( | |
| 1528 | matches!( | |
| 1529 | error, | |
| 1530 | RenderError::ImageDecode(image::ImageError::Limits(_)) | |
| 1531 | ), | |
| 1532 | "{error:?}" | |
| 1533 | ); | |
| 1534 | } | |
| 1535 | } | |
| 1536 | } | |
| 1537 | ||
| 1538 | #[test] | |
| 1539 | fn image_pixels_have_valid_dimensions_and_immutable_clone_identity() { | |
| 1540 | assert!(matches!( | |
| 1541 | RasterImage::new([0, 1], Vec::new()), | |
| 1542 | Err(RenderError::InvalidImage) | |
| 1543 | )); | |
| 1544 | assert!(matches!( | |
| 1545 | RasterImage::new([2, 2], vec![255; 15]), | |
| 1546 | Err(RenderError::InvalidImage) | |
| 1547 | )); | |
| 1548 | assert!(matches!( | |
| 1549 | RasterImage::new([u32::MAX; 2], Vec::new()), | |
| 1550 | Err(RenderError::InvalidImage) | |
| 1551 | )); | |
| 1552 | let image = RasterImage::new([1, 1], vec![1, 2, 3, 255]).unwrap(); | |
| 1553 | assert_eq!(image.id(), image.clone().id()); | |
| 1554 | } | |
| 1555 | ||
| 1556 | struct Target { | |
| 1557 | texture: wgpu::Texture, | |
| 1558 | readback: wgpu::Buffer, | |
| 1559 | } | |
| 1560 | ||
| 1561 | impl Target { | |
| 1562 | fn new(device: &wgpu::Device) -> Self { | |
| 1563 | Self { | |
| 1564 | texture: device.create_texture(&wgpu::TextureDescriptor { | |
| 1565 | label: Some("Draw readback test"), | |
| 1566 | size: wgpu::Extent3d { | |
| 1567 | width: 512, | |
| 1568 | height: 256, | |
| 1569 | depth_or_array_layers: 1, | |
| 1570 | }, | |
| 1571 | mip_level_count: 1, | |
| 1572 | sample_count: 1, | |
| 1573 | dimension: wgpu::TextureDimension::D2, | |
| 1574 | format: wgpu::TextureFormat::Rgba8UnormSrgb, | |
| 1575 | usage: wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::COPY_SRC, | |
| 1576 | view_formats: &[], | |
| 1577 | }), | |
| 1578 | readback: device.create_buffer(&wgpu::BufferDescriptor { | |
| 1579 | label: Some("Draw readback"), | |
| 1580 | size: 512 * 256 * 4, | |
| 1581 | usage: wgpu::BufferUsages::MAP_READ | wgpu::BufferUsages::COPY_DST, | |
| 1582 | mapped_at_creation: false, | |
| 1583 | }), | |
| 1584 | } | |
| 1585 | } | |
| 1586 | ||
| 1587 | fn draw(&self, renderer: &mut Renderer, layers: &[Layer<'_>]) -> Result<(), RenderError> { | |
| 1588 | renderer.draw( | |
| 1589 | &self.texture.create_view(&Default::default()), | |
| 1590 | [512, 256], | |
| 1591 | [1.0; 4], | |
| 1592 | layers, | |
| 1593 | ) | |
| 1594 | } | |
| 1595 | ||
| 1596 | fn capture(&self, renderer: &Renderer) -> Vec<u8> { | |
| 1597 | let mut encoder = renderer.device.create_command_encoder(&Default::default()); | |
| 1598 | encoder.copy_texture_to_buffer( | |
| 1599 | self.texture.as_image_copy(), | |
| 1600 | wgpu::TexelCopyBufferInfo { | |
| 1601 | buffer: &self.readback, | |
| 1602 | layout: wgpu::TexelCopyBufferLayout { | |
| 1603 | offset: 0, | |
| 1604 | bytes_per_row: Some(512 * 4), | |
| 1605 | rows_per_image: Some(256), | |
| 1606 | }, | |
| 1607 | }, | |
| 1608 | wgpu::Extent3d { | |
| 1609 | width: 512, | |
| 1610 | height: 256, | |
| 1611 | depth_or_array_layers: 1, | |
| 1612 | }, | |
| 1613 | ); | |
| 1614 | renderer.queue.submit([encoder.finish()]); | |
| 1615 | let (sender, receiver) = std::sync::mpsc::channel(); | |
| 1616 | self.readback | |
| 1617 | .map_async(wgpu::MapMode::Read, .., move |result| { | |
| 1618 | sender.send(result).unwrap(); | |
| 1619 | }); | |
| 1620 | renderer | |
| 1621 | .device | |
| 1622 | .poll(wgpu::PollType::Wait { | |
| 1623 | submission_index: None, | |
| 1624 | timeout: Some(Duration::from_secs(5)), | |
| 1625 | }) | |
| 1626 | .unwrap(); | |
| 1627 | receiver | |
| 1628 | .recv_timeout(Duration::from_secs(5)) | |
| 1629 | .unwrap() | |
| 1630 | .unwrap(); | |
| 1631 | let capture = self.readback.get_mapped_range(..).unwrap().to_vec(); | |
| 1632 | self.readback.unmap(); | |
| 1633 | capture | |
| 1634 | } | |
| 1635 | } | |
| 1636 | ||
| 1637 | fn page<'a>(primitives: &'a [Primitive<'a>]) -> [Layer<'a>; 1] { | |
| 1638 | [Layer { | |
| 1639 | scale: 2.0, | |
| 1640 | origin: [24.0; 2], | |
| 1641 | clip: None, | |
| 1642 | primitives, | |
| 1643 | }] | |
| 1644 | } | |
| 1645 | ||
| 1646 | #[test] | |
| 1647 | #[ignore = "requires a native GPU adapter"] | |
| 1648 | fn eviction_and_repaint_reproduce_pixels() { | |
| 1649 | let instance = wgpu::Instance::new(wgpu::InstanceDescriptor::new_without_display_handle()); | |
| 1650 | let adapter = pollster::block_on(instance.request_adapter(&Default::default())).unwrap(); | |
| 1651 | let (device, queue) = | |
| 1652 | pollster::block_on(adapter.request_device(&Default::default())).unwrap(); | |
| 1653 | let format = wgpu::TextureFormat::Rgba8UnormSrgb; | |
| 1654 | let target = Target::new(&device); | |
| 1655 | let mut renderer = Renderer::new(device, queue, format); | |
| 1656 | let layout = shape("Hello 🌳\nCafé e\u{301} שלום", "Arial", 11.0, true, 220.0); | |
| 1657 | let mut captures = Vec::new(); | |
| 1658 | let image = RasterImage::new( | |
| 1659 | [2, 2], | |
| 1660 | vec![255, 0, 0, 255, 0, 255, 0, 255, 0, 0, 255, 255, 0, 0, 0, 0], | |
| 1661 | ) | |
| 1662 | .unwrap(); | |
| 1663 | let transparent_edge = | |
| 1664 | RasterImage::new([2, 1], vec![255, 0, 0, 255, 0, 0, 255, 0]).unwrap(); | |
| 1665 | let icons: [&'static [&'static str]; 4] = [&[CHECKBOX], &[CHECKBOX, MARK], &[DOT], &[MARK]]; | |
| 1666 | let phase_layout = shape("H", "Arial", 11.0, false, 20.0); | |
| 1667 | let mut primitives = vec![ | |
| 1668 | Primitive::Rect { | |
| 1669 | rect: [0.0, 0.0, 70.0, 14.0], | |
| 1670 | color: [0.55, 0.73, 1.0, 0.5], | |
| 1671 | }, | |
| 1672 | Primitive::Text { | |
| 1673 | clip: None, | |
| 1674 | text: &layout, | |
| 1675 | ink: [0.0, 0.0, 0.0, 1.0], | |
| 1676 | origin: [0.0; 2], | |
| 1677 | }, | |
| 1678 | Primitive::Text { | |
| 1679 | clip: None, | |
| 1680 | text: &layout, | |
| 1681 | ink: [0.0, 0.0, 0.0, 1.0], | |
| 1682 | origin: [8.0, 64.0], | |
| 1683 | }, | |
| 1684 | Primitive::Image { | |
| 1685 | image: &image, | |
| 1686 | rect: [128.0, 8.0, 160.0, 40.0], | |
| 1687 | }, | |
| 1688 | Primitive::Rect { | |
| 1689 | rect: [128.0, 8.0, 132.0, 12.0], | |
| 1690 | color: [0.0, 0.0, 1.0, 1.0], | |
| 1691 | }, | |
| 1692 | Primitive::Image { | |
| 1693 | image: &transparent_edge, | |
| 1694 | rect: [176.0, 8.0, 208.0, 40.0], | |
| 1695 | }, | |
| 1696 | Primitive::RoundedRect { | |
| 1697 | rect: [128.0, 48.0, 148.0, 68.0], | |
| 1698 | radius: [5.0, 10.0], | |
| 1699 | stroke: None, | |
| 1700 | color: [1.0, 0.0, 0.0, 1.0], | |
| 1701 | }, | |
| 1702 | Primitive::RoundedRect { | |
| 1703 | rect: [164.0, 48.0, 184.0, 68.0], | |
| 1704 | radius: [5.0; 2], | |
| 1705 | stroke: Some(Stroke::Solid(1.0)), | |
| 1706 | color: [0.0, 0.0, 1.0, 1.0], | |
| 1707 | }, | |
| 1708 | Primitive::RoundedRect { | |
| 1709 | rect: [200.0, 48.0, 240.0, 68.0], | |
| 1710 | radius: [3.0, 10.0], | |
| 1711 | stroke: Some(Stroke::Dashed(1.0)), | |
| 1712 | color: [0.0, 0.0, 1.0, 1.0], | |
| 1713 | }, | |
| 1714 | Primitive::Segment { | |
| 1715 | from: [226.0, 104.0], | |
| 1716 | to: [240.0, 104.0], | |
| 1717 | width: 4.0, | |
| 1718 | round: true, | |
| 1719 | color: [1.0, 0.0, 0.0, 1.0], | |
| 1720 | }, | |
| 1721 | ]; | |
| 1722 | for (index, sources) in icons.iter().enumerate() { | |
| 1723 | primitives.push(Primitive::Icon { | |
| 1724 | sources, | |
| 1725 | origin: [128.125 + 20.0 * index as f32, 80.25], | |
| 1726 | size: 12.0, | |
| 1727 | tint: [1.0; 4], | |
| 1728 | }); | |
| 1729 | } | |
| 1730 | primitives.extend((0..9).map(|step| Primitive::Text { | |
| 1731 | clip: None, | |
| 1732 | text: &phase_layout, | |
| 1733 | ink: [0.0, 0.0, 0.0, 1.0], | |
| 1734 | origin: [8.0 + 24.0 * step as f32, 96.0 + 0.125 * step as f32], | |
| 1735 | })); | |
| 1736 | for pass in 0..6 { | |
| 1737 | if pass == 2 { | |
| 1738 | renderer.clear_glyph_cache(); | |
| 1739 | renderer.images.clear(); | |
| 1740 | } else if pass == 3 { | |
| 1741 | renderer = Renderer::new(renderer.device.clone(), renderer.queue.clone(), format); | |
| 1742 | } | |
| 1743 | let clipping = [ | |
| 1744 | Primitive::Text { | |
| 1745 | text: &layout, | |
| 1746 | ink: [0.0, 0.0, 0.0, 1.0], | |
| 1747 | origin: [0.0; 2], | |
| 1748 | clip: (pass == 5).then_some([2.5, 1.25, 30.75, 20.5]), | |
| 1749 | }, | |
| 1750 | Primitive::Rect { | |
| 1751 | rect: [40.0, 40.0, 70.0, 50.0], | |
| 1752 | color: [0.0, 0.0, 1.0, 1.0], | |
| 1753 | }, | |
| 1754 | ]; | |
| 1755 | target | |
| 1756 | .draw( | |
| 1757 | &mut renderer, | |
| 1758 | &page(if pass < 4 { &primitives } else { &clipping }), | |
| 1759 | ) | |
| 1760 | .unwrap(); | |
| 1761 | captures.push(target.capture(&renderer)); | |
| 1762 | } | |
| 1763 | let centroids: Vec<_> = (0..9) | |
| 1764 | .map(|step| { | |
| 1765 | let mut weight = 0.0_f64; | |
| 1766 | let mut moment = 0.0_f64; | |
| 1767 | for y in 212..252 { | |
| 1768 | for x in (40 + 48 * step)..(72 + 48 * step) { | |
| 1769 | let value = captures[0][(y * 512 + x) * 4]; | |
| 1770 | let coverage = 1.0 - f64::from(srgb(value, 0, 0)[0]); | |
| 1771 | weight += coverage; | |
| 1772 | moment += coverage * y as f64; | |
| 1773 | } | |
| 1774 | } | |
| 1775 | assert!(weight > 10.0, "phase sample {step} was not painted"); | |
| 1776 | moment / weight | |
| 1777 | }) | |
| 1778 | .collect(); | |
| 1779 | let pixel = |x: usize, y: usize| &captures[0][(y * 512 + x) * 4..(y * 512 + x) * 4 + 4]; | |
| 1780 | assert_eq!(pixel(490, 232), [255, 0, 0, 255]); | |
| 1781 | assert_eq!(pixel(473, 232), [255, 0, 0, 255]); | |
| 1782 | assert_eq!(pixel(490, 238), [255; 4]); | |
| 1783 | assert_eq!(pixel(472, 228), [255; 4]); | |
| 1784 | assert_eq!(pixel(300, 140), [255, 0, 0, 255]); | |
| 1785 | assert_eq!(pixel(280, 120), [255; 4]); | |
| 1786 | assert_eq!(pixel(282, 124), [255; 4]); | |
| 1787 | assert_eq!(pixel(281, 140), [255, 0, 0, 255]); | |
| 1788 | assert_eq!(pixel(372, 140), [255; 4]); | |
| 1789 | assert_eq!(pixel(372, 120), [0, 0, 255, 255]); | |
| 1790 | assert_eq!(pixel(352, 120), [255; 4]); | |
| 1791 | assert_eq!(pixel(424, 120), [255; 4]); | |
| 1792 | assert_eq!(pixel(464, 140), [255; 4]); | |
| 1793 | assert!((440..480).any(|x| pixel(x, 120)[0] < 40)); | |
| 1794 | assert!((440..480).any(|x| pixel(x, 120)[0] > 250)); | |
| 1795 | for x in 440..472 { | |
| 1796 | assert!(pixel(x, 120)[0].abs_diff(pixel(x + 8, 120)[0]) <= 1); | |
| 1797 | } | |
| 1798 | for (step, centroid) in centroids.iter().enumerate() { | |
| 1799 | let movement = centroid - centroids[0]; | |
| 1800 | assert!( | |
| 1801 | (movement - step as f64 * 0.25).abs() < 0.06, | |
| 1802 | "quarter-pixel step {step} moved the glyph by {movement} px" | |
| 1803 | ); | |
| 1804 | } | |
| 1805 | assert!(!renderer.glyphs.is_empty()); | |
| 1806 | assert!(renderer.glyphs.len() <= MAX_GLYPHS); | |
| 1807 | assert_eq!( | |
| 1808 | renderer | |
| 1809 | .glyphs | |
| 1810 | .keys() | |
| 1811 | .filter(|key| matches!(key, AtlasKey::Icon { .. })) | |
| 1812 | .count(), | |
| 1813 | 4 | |
| 1814 | ); | |
| 1815 | for x in [280, 320, 360, 400] { | |
| 1816 | let colored = (184..210) | |
| 1817 | .flat_map(|y| (x..x + 26).map(move |x| (y * 512 + x) * 4)) | |
| 1818 | .filter(|offset| captures[0][*offset..*offset + 3].iter().any(|v| *v < 180)) | |
| 1819 | .count(); | |
| 1820 | assert!(colored > 10, "icon at {x} was not painted"); | |
| 1821 | } | |
| 1822 | let count = renderer.glyphs.len(); | |
| 1823 | target | |
| 1824 | .draw( | |
| 1825 | &mut renderer, | |
| 1826 | &page(&[Primitive::Icon { | |
| 1827 | sources: icons[0], | |
| 1828 | origin: [10000.0; 2], | |
| 1829 | size: 12.0, | |
| 1830 | tint: [1.0; 4], | |
| 1831 | }]), | |
| 1832 | ) | |
| 1833 | .unwrap(); | |
| 1834 | assert_eq!(renderer.glyphs.len(), count); | |
| 1835 | assert!( | |
| 1836 | captures[0] | |
| 1837 | .chunks_exact(4) | |
| 1838 | .filter(|pixel| pixel[0] < 200) | |
| 1839 | .count() | |
| 1840 | > 100 | |
| 1841 | ); | |
| 1842 | assert_eq!(captures[0], captures[1]); | |
| 1843 | assert_eq!(captures[0], captures[2]); | |
| 1844 | assert_eq!(captures[0], captures[3]); | |
| 1845 | assert_ne!(captures[4], captures[5]); | |
| 1846 | for y in 0..256 { | |
| 1847 | for x in 0..512 { | |
| 1848 | let offset = (y * 512 + x) * 4; | |
| 1849 | let expected = if ((29..86).contains(&x) && (26..65).contains(&y)) | |
| 1850 | || ((104..164).contains(&x) && (104..124).contains(&y)) | |
| 1851 | { | |
| 1852 | &captures[4][offset..offset + 4] | |
| 1853 | } else { | |
| 1854 | &[255; 4] | |
| 1855 | }; | |
| 1856 | assert_eq!( | |
| 1857 | &captures[5][offset..offset + 4], | |
| 1858 | expected, | |
| 1859 | "clipping at {x},{y}" | |
| 1860 | ); | |
| 1861 | } | |
| 1862 | } | |
| 1863 | for (x, y, color) in [ | |
| 1864 | (288, 48, [255, 0, 0, 255]), | |
| 1865 | (336, 96, [255; 4]), | |
| 1866 | (284, 44, [0, 0, 255, 255]), | |
| 1867 | ] { | |
| 1868 | let offset = (y * 512 + x) * 4; | |
| 1869 | assert_eq!(captures[0][offset..offset + 4], color); | |
| 1870 | } | |
| 1871 | let edge = &captures[0][(48 * 512 + 408) * 4..(48 * 512 + 408) * 4 + 4]; | |
| 1872 | assert_eq!(edge[0], 255, "{edge:?}"); | |
| 1873 | assert_eq!(edge[1], edge[2], "{edge:?}"); | |
| 1874 | assert!((180..=200).contains(&edge[1]), "{edge:?}"); | |
| 1875 | assert_eq!(renderer.images.len(), 2); | |
| 1876 | for primitive in [ | |
| 1877 | Primitive::RoundedRect { | |
| 1878 | rect: [-1e38, 0.0, 1e38, 1e38], | |
| 1879 | radius: [3.0; 2], | |
| 1880 | stroke: Some(Stroke::Dashed(1.0)), | |
| 1881 | color: [1.0; 4], | |
| 1882 | }, | |
| 1883 | Primitive::RoundedRect { | |
| 1884 | rect: [0.0, 0.0, 10.0, 10.0], | |
| 1885 | radius: [f32::NAN, 2.0], | |
| 1886 | stroke: None, | |
| 1887 | color: [1.0; 4], | |
| 1888 | }, | |
| 1889 | Primitive::RoundedRect { | |
| 1890 | rect: [0.0, 0.0, 10.0, 10.0], | |
| 1891 | radius: [2.0; 2], | |
| 1892 | stroke: Some(Stroke::Solid(0.0)), | |
| 1893 | color: [1.0; 4], | |
| 1894 | }, | |
| 1895 | Primitive::RoundedRect { | |
| 1896 | rect: [0.0, 0.0, 10.0, 10.0], | |
| 1897 | radius: [2.0; 2], | |
| 1898 | stroke: Some(Stroke::Dashed(f32::NAN)), | |
| 1899 | color: [1.0; 4], | |
| 1900 | }, | |
| 1901 | Primitive::Rect { | |
| 1902 | rect: [0.0, 0.0, f32::NAN, 1.0], | |
| 1903 | color: [1.0; 4], | |
| 1904 | }, | |
| 1905 | Primitive::Rect { | |
| 1906 | rect: [2.0, 0.0, 1.0, 1.0], | |
| 1907 | color: [1.0; 4], | |
| 1908 | }, | |
| 1909 | Primitive::Text { | |
| 1910 | clip: None, | |
| 1911 | text: &layout, | |
| 1912 | ink: [0.0, 0.0, 0.0, 1.0], | |
| 1913 | origin: [f32::MAX; 2], | |
| 1914 | }, | |
| 1915 | Primitive::Text { | |
| 1916 | clip: Some([0.0, 0.0, f32::NAN, 1.0]), | |
| 1917 | text: &layout, | |
| 1918 | ink: [0.0, 0.0, 0.0, 1.0], | |
| 1919 | origin: [0.0; 2], | |
| 1920 | }, | |
| 1921 | Primitive::Text { | |
| 1922 | clip: Some([2.0, 0.0, 1.0, 1.0]), | |
| 1923 | text: &layout, | |
| 1924 | ink: [0.0, 0.0, 0.0, 1.0], | |
| 1925 | origin: [0.0; 2], | |
| 1926 | }, | |
| 1927 | ] { | |
| 1928 | assert!(matches!( | |
| 1929 | target.draw(&mut renderer, &page(&[primitive])), | |
| 1930 | Err(RenderError::InvalidPrimitive) | |
| 1931 | )); | |
| 1932 | } | |
| 1933 | let too_many: Vec<_> = (0..=MAX_IMAGES) | |
| 1934 | .map(|_| RasterImage::new([1, 1], vec![255; 4]).unwrap()) | |
| 1935 | .collect(); | |
| 1936 | let primitives: Vec<_> = too_many | |
| 1937 | .iter() | |
| 1938 | .map(|image| Primitive::Image { | |
| 1939 | image, | |
| 1940 | rect: [0.0, 0.0, 1.0, 1.0], | |
| 1941 | }) | |
| 1942 | .collect(); | |
| 1943 | assert!(matches!( | |
| 1944 | target.draw(&mut renderer, &page(&primitives)), | |
| 1945 | Err(RenderError::ImageBudget) | |
| 1946 | )); | |
| 1947 | let primitives: Vec<_> = too_many | |
| 1948 | .iter() | |
| 1949 | .map(|image| Primitive::Image { | |
| 1950 | image, | |
| 1951 | rect: [-20.0, -20.0, -19.0, -19.0], | |
| 1952 | }) | |
| 1953 | .collect(); | |
| 1954 | target.draw(&mut renderer, &page(&primitives)).unwrap(); | |
| 1955 | assert_eq!(renderer.images.len(), 2); | |
| 1956 | let mut seen = HashSet::from([image.id(), transparent_edge.id()]); | |
| 1957 | let mut previous = None; | |
| 1958 | for _ in 0..5 { | |
| 1959 | let large = RasterImage::new([2048; 2], vec![255; 2048 * 2048 * 4]).unwrap(); | |
| 1960 | seen.insert(large.id()); | |
| 1961 | let mut primitives = vec![Primitive::Image { | |
| 1962 | image: &large, | |
| 1963 | rect: [0.0, 0.0, 16.0, 16.0], | |
| 1964 | }]; | |
| 1965 | if let Some(image) = &previous { | |
| 1966 | primitives.push(Primitive::Image { | |
| 1967 | image, | |
| 1968 | rect: [16.0, 0.0, 32.0, 16.0], | |
| 1969 | }); | |
| 1970 | } | |
| 1971 | target.draw(&mut renderer, &page(&primitives)).unwrap(); | |
| 1972 | assert!(renderer.images.contains_key(&large.id())); | |
| 1973 | if let Some(image) = &previous { | |
| 1974 | assert!(renderer.images.contains_key(&image.id())); | |
| 1975 | } | |
| 1976 | assert!(renderer.occupancy().within_budget); | |
| 1977 | previous = Some(large); | |
| 1978 | } | |
| 1979 | assert!(renderer.images.len() <= MAX_IMAGES); | |
| 1980 | assert!(seen.iter().any(|id| !renderer.images.contains_key(id))); | |
| 1981 | renderer.images.clear(); | |
| 1982 | target | |
| 1983 | .draw( | |
| 1984 | &mut renderer, | |
| 1985 | &page(&[Primitive::Image { | |
| 1986 | image: &image, | |
| 1987 | rect: [128.0, 8.0, 160.0, 40.0], | |
| 1988 | }]), | |
| 1989 | ) | |
| 1990 | .unwrap(); | |
| 1991 | assert_eq!(renderer.images.len(), 1); | |
| 1992 | assert!(renderer.images.contains_key(&image.id())); | |
| 1993 | } | |
| 1994 | ||
| 1995 | #[test] | |
| 1996 | #[ignore = "requires a native GPU adapter"] | |
| 1997 | fn paths_fill_and_stroke_in_layer_units() { | |
| 1998 | let instance = wgpu::Instance::new(wgpu::InstanceDescriptor::new_without_display_handle()); | |
| 1999 | let adapter = pollster::block_on(instance.request_adapter(&Default::default())).unwrap(); | |
| 2000 | let (device, queue) = | |
| 2001 | pollster::block_on(adapter.request_device(&Default::default())).unwrap(); | |
| 2002 | let target = Target::new(&device); | |
| 2003 | let mut renderer = Renderer::new(device, queue, wgpu::TextureFormat::Rgba8UnormSrgb); | |
| 2004 | let primitives = [ | |
| 2005 | Primitive::Path { | |
| 2006 | data: "M0 0H20L30 20H0Z", | |
| 2007 | origin: [10.0, 10.0], | |
| 2008 | style: PathStyle::Fill, | |
| 2009 | colors: [[0.0, 0.0, 1.0, 1.0]; 2], | |
| 2010 | }, | |
| 2011 | Primitive::Path { | |
| 2012 | data: "M0 0H40", | |
| 2013 | origin: [10.0, 60.0], | |
| 2014 | style: PathStyle::Stroke(4.0), | |
| 2015 | colors: [[1.0, 0.0, 0.0, 1.0]; 2], | |
| 2016 | }, | |
| 2017 | Primitive::Path { | |
| 2018 | data: "M0 0H20V20H0Z", | |
| 2019 | origin: [150.0, 20.0], | |
| 2020 | style: PathStyle::Shadow(6.0), | |
| 2021 | colors: [[0.0, 0.0, 0.0, 1.0]; 2], | |
| 2022 | }, | |
| 2023 | Primitive::Gradient { | |
| 2024 | rect: [100.0, 10.0, 120.0, 110.0], | |
| 2025 | radius: [0.0; 2], | |
| 2026 | colors: [[0.0, 0.0, 1.0, 1.0], [0.0, 1.0, 0.0, 1.0]], | |
| 2027 | }, | |
| 2028 | ]; | |
| 2029 | target | |
| 2030 | .draw( | |
| 2031 | &mut renderer, | |
| 2032 | &[Layer { | |
| 2033 | scale: 2.0, | |
| 2034 | origin: [0.0; 2], | |
| 2035 | clip: None, | |
| 2036 | primitives: &primitives, | |
| 2037 | }], | |
| 2038 | ) | |
| 2039 | .unwrap(); | |
| 2040 | let capture = target.capture(&renderer); | |
| 2041 | let pixel = |x: usize, y: usize| &capture[(y * 512 + x) * 4..(y * 512 + x) * 4 + 4]; | |
| 2042 | assert_eq!(pixel(40, 40), [0, 0, 255, 255]); | |
| 2043 | assert_eq!(pixel(70, 55), [0, 0, 255, 255], "inside the slanted edge"); | |
| 2044 | assert_eq!(pixel(75, 30), [255; 4], "outside the slanted edge"); | |
| 2045 | let edge = pixel(65, 30); | |
| 2046 | assert!( | |
| 2047 | edge[0] > 0 && edge[0] < 255, | |
| 2048 | "the slant is antialiased: {edge:?}" | |
| 2049 | ); | |
| 2050 | assert_eq!(pixel(60, 120), [255, 0, 0, 255]); | |
| 2051 | assert_eq!(pixel(60, 112), [255; 4], "strokes are their width across"); | |
| 2052 | assert_eq!(pixel(17, 120)[1], 0, "round caps reach past the ends"); | |
| 2053 | assert_eq!(pixel(12, 120), [255; 4]); | |
| 2054 | let [top, bottom] = [pixel(220, 21), pixel(220, 218)]; | |
| 2055 | assert!( | |
| 2056 | top[2] > 250 && top[1] < 60, | |
| 2057 | "gradients start at the top colour: {top:?}" | |
| 2058 | ); | |
| 2059 | assert!( | |
| 2060 | bottom[1] > 250 && bottom[2] < 60, | |
| 2061 | "and end at the bottom's: {bottom:?}" | |
| 2062 | ); | |
| 2063 | let [inside, fading, beyond] = [pixel(320, 60), pixel(298, 60), pixel(270, 60)]; | |
| 2064 | assert!(inside[0] < 40, "a shadow is dark inside: {inside:?}"); | |
| 2065 | assert!( | |
| 2066 | fading[0] > 40 && fading[0] < 230, | |
| 2067 | "and fades at its edge: {fading:?}" | |
| 2068 | ); | |
| 2069 | assert_eq!(beyond, [255; 4]); | |
| 2070 | } | |
| 2071 | ||
| 2072 | #[test] | |
| 2073 | #[ignore = "requires a native GPU adapter"] | |
| 2074 | fn layers_transform_and_clip_independently() { | |
| 2075 | let instance = wgpu::Instance::new(wgpu::InstanceDescriptor::new_without_display_handle()); | |
| 2076 | let adapter = pollster::block_on(instance.request_adapter(&Default::default())).unwrap(); | |
| 2077 | let (device, queue) = | |
| 2078 | pollster::block_on(adapter.request_device(&Default::default())).unwrap(); | |
| 2079 | let target = Target::new(&device); | |
| 2080 | let mut renderer = Renderer::new(device, queue, wgpu::TextureFormat::Rgba8UnormSrgb); | |
| 2081 | let fill = [Primitive::Rect { | |
| 2082 | rect: [0.0, 0.0, 300.0, 100.0], | |
| 2083 | color: [1.0, 0.0, 0.0, 1.0], | |
| 2084 | }]; | |
| 2085 | let chrome = [Primitive::Rect { | |
| 2086 | rect: [0.0, 0.0, 10.0, 10.0], | |
| 2087 | color: [0.0, 0.0, 1.0, 1.0], | |
| 2088 | }]; | |
| 2089 | target | |
| 2090 | .draw( | |
| 2091 | &mut renderer, | |
| 2092 | &[ | |
| 2093 | Layer { | |
| 2094 | scale: 1.0, | |
| 2095 | origin: [0.0; 2], | |
| 2096 | clip: Some([100.0, 50.0, 200.5, 80.0]), | |
| 2097 | primitives: &fill, | |
| 2098 | }, | |
| 2099 | Layer { | |
| 2100 | scale: 2.0, | |
| 2101 | origin: [300.0, 10.0], | |
| 2102 | clip: None, | |
| 2103 | primitives: &chrome, | |
| 2104 | }, | |
| 2105 | ], | |
| 2106 | ) | |
| 2107 | .unwrap(); | |
| 2108 | let capture = target.capture(&renderer); | |
| 2109 | let pixel = |x: usize, y: usize| &capture[(y * 512 + x) * 4..(y * 512 + x) * 4 + 4]; | |
| 2110 | assert_eq!(pixel(10, 10), [255; 4], "clipped away"); | |
| 2111 | assert_eq!(pixel(100, 50), [255, 0, 0, 255]); | |
| 2112 | assert_eq!( | |
| 2113 | pixel(200, 79), | |
| 2114 | [255, 0, 0, 255], | |
| 2115 | "partial pixels round outward" | |
| 2116 | ); | |
| 2117 | assert_eq!(pixel(99, 60), [255; 4]); | |
| 2118 | assert_eq!(pixel(201, 60), [255; 4]); | |
| 2119 | assert_eq!(pixel(150, 80), [255; 4]); | |
| 2120 | assert_eq!(pixel(319, 29), [0, 0, 255, 255]); | |
| 2121 | assert_eq!(pixel(320, 30), [255; 4]); | |
| 2122 | assert!(matches!( | |
| 2123 | target.draw( | |
| 2124 | &mut renderer, | |
| 2125 | &[Layer { | |
| 2126 | scale: 0.0, | |
| 2127 | origin: [0.0; 2], | |
| 2128 | clip: None, | |
| 2129 | primitives: &fill, | |
| 2130 | }] | |
| 2131 | ), | |
| 2132 | Err(RenderError::InvalidLayer) | |
| 2133 | )); | |
| 2134 | } | |
| 2135 | } |
crates/draw/src/render/icon.rs created+161| ... | ... | @@ -0,0 +1,161 @@ |
| 1 | use swash::{ | |
| 2 | scale::image::{Content, Image}, | |
| 3 | zeno::{Cap, Fill, Join, Mask, Placement, Stroke, Style, Transform, Vector}, | |
| 4 | }; | |
| 5 | ||
| 6 | /// Rasterizes 16×16 SVG sources over each other at `size` device pixels, offset by | |
| 7 | /// `phase` quarter pixels. Paths are filled and stroked with `#rrggbb`, two-stop vertical | |
| 8 | /// gradients or `currentColor`, which paints white; strokes have round caps and joins. | |
| 9 | pub(crate) fn rasterize(sources: &[&str], size: f32, phase: [u8; 2]) -> Image { | |
| 10 | let side = (size + 1.0).ceil() as u32; | |
| 11 | let mut pixels = vec![[0.0_f32; 4]; (side * side) as usize]; | |
| 12 | for source in sources { | |
| 13 | let svg = roxmltree::Document::parse(source).expect("Bundled icon SVG must be valid"); | |
| 14 | assert_eq!(svg.root_element().attribute("viewBox"), Some("0 0 16 16")); | |
| 15 | let paint = |value: &str| -> Option<[[f32; 4]; 2]> { | |
| 16 | let hex = |value: &str| { | |
| 17 | let [_, r, g, b] = u32::from_str_radix(value.strip_prefix('#').unwrap(), 16) | |
| 18 | .unwrap() | |
| 19 | .to_be_bytes(); | |
| 20 | super::srgb(r, g, b) | |
| 21 | }; | |
| 22 | match value { | |
| 23 | "none" => None, | |
| 24 | "currentColor" => Some([[1.0; 4]; 2]), | |
| 25 | _ => Some(match value.strip_prefix("url(#") { | |
| 26 | Some(id) => { | |
| 27 | let id = id.strip_suffix(')').unwrap(); | |
| 28 | let gradient = svg | |
| 29 | .descendants() | |
| 30 | .find(|node| node.attribute("id") == Some(id)) | |
| 31 | .unwrap(); | |
| 32 | let stops: Vec<_> = gradient | |
| 33 | .children() | |
| 34 | .filter(|node| node.has_tag_name("stop")) | |
| 35 | .map(|node| hex(node.attribute("stop-color").unwrap())) | |
| 36 | .collect(); | |
| 37 | stops.try_into().expect("Icon gradients have two stops") | |
| 38 | } | |
| 39 | None => [hex(value); 2], | |
| 40 | }), | |
| 41 | } | |
| 42 | }; | |
| 43 | for path in svg.descendants().filter(|node| node.has_tag_name("path")) { | |
| 44 | let fill = match path.attribute("fill-rule") { | |
| 45 | Some("evenodd") => Fill::EvenOdd, | |
| 46 | _ => Fill::NonZero, | |
| 47 | }; | |
| 48 | let pen = path | |
| 49 | .attribute("stroke-width") | |
| 50 | .map_or(1.0, |width| width.parse().unwrap()); | |
| 51 | let layers = [ | |
| 52 | ( | |
| 53 | path.attribute("fill").unwrap_or("#000000"), | |
| 54 | Style::from(fill), | |
| 55 | ), | |
| 56 | ( | |
| 57 | path.attribute("stroke").unwrap_or("none"), | |
| 58 | Style::from(Stroke { | |
| 59 | start_cap: Cap::Round, | |
| 60 | end_cap: Cap::Round, | |
| 61 | join: Join::Round, | |
| 62 | ..Stroke::new(pen) | |
| 63 | }), | |
| 64 | ), | |
| 65 | ]; | |
| 66 | for (value, style) in layers { | |
| 67 | let Some([top, bottom]) = paint(value) else { | |
| 68 | continue; | |
| 69 | }; | |
| 70 | let (mask, _) = Mask::new(path.attribute("d").unwrap()) | |
| 71 | .style(style) | |
| 72 | .transform(Some(Transform::scale(size / 16.0, size / 16.0))) | |
| 73 | .render_offset(Vector::new(phase[0] as f32 * 0.25, phase[1] as f32 * 0.25)) | |
| 74 | .size(side, side) | |
| 75 | .render(); | |
| 76 | for (index, (pixel, coverage)) in pixels.iter_mut().zip(mask).enumerate() { | |
| 77 | let alpha = f32::from(coverage) / 255.0; | |
| 78 | let y = ((index / side as usize) as f32 / size).min(1.0); | |
| 79 | for channel in 0..3 { | |
| 80 | let color = top[channel] + (bottom[channel] - top[channel]) * y; | |
| 81 | pixel[channel] = color * alpha + pixel[channel] * (1.0 - alpha); | |
| 82 | } | |
| 83 | pixel[3] = alpha + pixel[3] * (1.0 - alpha); | |
| 84 | } | |
| 85 | } | |
| 86 | } | |
| 87 | } | |
| 88 | let data = pixels | |
| 89 | .into_iter() | |
| 90 | .flat_map(|pixel| { | |
| 91 | let alpha = pixel[3]; | |
| 92 | let channel = |value: f32| { | |
| 93 | let value = if alpha > 0.0 { value / alpha } else { 0.0 }; | |
| 94 | super::srgb_byte(value) | |
| 95 | }; | |
| 96 | [ | |
| 97 | channel(pixel[0]), | |
| 98 | channel(pixel[1]), | |
| 99 | channel(pixel[2]), | |
| 100 | (alpha * 255.0).round() as u8, | |
| 101 | ] | |
| 102 | }) | |
| 103 | .collect(); | |
| 104 | Image { | |
| 105 | content: Content::Color, | |
| 106 | placement: Placement { | |
| 107 | left: 0, | |
| 108 | top: 0, | |
| 109 | width: side, | |
| 110 | height: side, | |
| 111 | }, | |
| 112 | data, | |
| 113 | ..Image::default() | |
| 114 | } | |
| 115 | } | |
| 116 | ||
| 117 | #[cfg(test)] | |
| 118 | mod tests { | |
| 119 | use super::*; | |
| 120 | ||
| 121 | const BOX: &str = r##"<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><defs><linearGradient id="a" x1="0" y1="0" x2="0" y2="1"><stop offset="0" stop-color="#ffffff"/><stop offset="1" stop-color="#8090a0"/></linearGradient></defs><path fill="url(#a)" d="M2 2h12v12H2z"/></svg>"##; | |
| 122 | const MARK: &str = r##"<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><defs><linearGradient id="b" x1="0" y1="0" x2="0" y2="1"><stop offset="0" stop-color="#ff2000"/><stop offset="1" stop-color="#a00000"/></linearGradient></defs><path fill="url(#b)" d="M4 8l3 3 5-7-1-1-4 5-2-2z"/></svg>"##; | |
| 123 | ||
| 124 | #[test] | |
| 125 | fn current_color_strokes_paint_white_coverage() { | |
| 126 | const LINE: &str = r##"<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><path d="M2 8H14" fill="none" stroke="currentColor" stroke-width="2"/></svg>"##; | |
| 127 | let image = rasterize(&[LINE], 16.0, [0, 0]); | |
| 128 | let pixel = |x: usize, y: usize| &image.data[(y * 17 + x) * 4..][..4]; | |
| 129 | assert_eq!(pixel(8, 7), [255, 255, 255, 255]); | |
| 130 | assert_eq!(pixel(8, 2)[3], 0); | |
| 131 | // Round caps reach past the path's ends. | |
| 132 | assert!(pixel(1, 7)[3] > 0); | |
| 133 | } | |
| 134 | ||
| 135 | #[test] | |
| 136 | fn icons_keep_transparency_and_overlays_across_scales() { | |
| 137 | for size in [8.0, 16.0, 32.0, 57.5] { | |
| 138 | for phase in [[0, 0], [1, 2], [3, 3]] { | |
| 139 | let plain = rasterize(&[BOX], size, phase); | |
| 140 | let marked = rasterize(&[BOX, MARK], size, phase); | |
| 141 | for image in [&plain, &marked] { | |
| 142 | assert_eq!( | |
| 143 | image.data.len(), | |
| 144 | (image.placement.width * image.placement.height * 4) as usize | |
| 145 | ); | |
| 146 | assert!(image.data.chunks_exact(4).any(|p| p[3] > 0)); | |
| 147 | assert!(image.data.chunks_exact(4).any(|p| p[3] < 255)); | |
| 148 | assert!( | |
| 149 | image | |
| 150 | .data | |
| 151 | .chunks_exact(4) | |
| 152 | .filter(|p| p[3] == 0) | |
| 153 | .all(|p| p[..3] == [0; 3]) | |
| 154 | ); | |
| 155 | } | |
| 156 | assert_ne!(plain.data, marked.data); | |
| 157 | assert!(marked.data.chunks_exact(4).any(|p| p[0] > p[2] && p[3] > 0)); | |
| 158 | } | |
| 159 | } | |
| 160 | } | |
| 161 | } |
crates/draw/src/render/text.rs created+115| ... | ... | @@ -0,0 +1,115 @@ |
| 1 | use crate::RenderError; | |
| 2 | use parley::fontique::Blob; | |
| 3 | ||
| 4 | /// Text the renderer can paint, as glyph runs in layer units. Owners recover their | |
| 5 | /// concrete text from a primitive through `Any`. | |
| 6 | pub trait Glyphs: std::any::Any { | |
| 7 | fn runs( | |
| 8 | &self, | |
| 9 | paint: &mut dyn FnMut(GlyphRun<'_>) -> Result<(), RenderError>, | |
| 10 | ) -> Result<(), RenderError>; | |
| 11 | } | |
| 12 | ||
| 13 | /// Glyphs sharing one font, size and colour. | |
| 14 | pub struct GlyphRun<'a> { | |
| 15 | pub font: &'a Blob<u8>, | |
| 16 | /// The face within a font collection. | |
| 17 | pub index: u32, | |
| 18 | pub size: f32, | |
| 19 | /// Variation coordinates in F2Dot14 bits. | |
| 20 | pub coords: &'a [i16], | |
| 21 | pub embolden: bool, | |
| 22 | /// Synthesized oblique angle in degrees. | |
| 23 | pub skew: Option<f32>, | |
| 24 | /// `None` paints in the primitive's ink. | |
| 25 | pub color: Option<[f32; 4]>, | |
| 26 | /// Top and height of the run's line: lines outside the target are skipped, and | |
| 27 | /// colour glyphs shrink to fit. | |
| 28 | pub line: [f32; 2], | |
| 29 | pub glyphs: &'a mut dyn Iterator<Item = Glyph>, | |
| 30 | /// Underline and strikethrough, painted after the glyphs. | |
| 31 | pub decorations: &'a [Decoration], | |
| 32 | } | |
| 33 | ||
| 34 | /// A glyph's baseline position. | |
| 35 | #[derive(Clone, Copy)] | |
| 36 | pub struct Glyph { | |
| 37 | pub id: u32, | |
| 38 | pub x: f32, | |
| 39 | pub y: f32, | |
| 40 | } | |
| 41 | ||
| 42 | /// A decoration line whose top edge is `y`; it stays at least one device pixel thick. | |
| 43 | #[derive(Clone, Copy)] | |
| 44 | pub struct Decoration { | |
| 45 | pub x: f32, | |
| 46 | pub y: f32, | |
| 47 | pub width: f32, | |
| 48 | pub thickness: f32, | |
| 49 | /// `None` paints in the primitive's ink. | |
| 50 | pub color: Option<[f32; 4]>, | |
| 51 | } | |
| 52 | ||
| 53 | /// Paints one parley glyph run with its line's baseline moved to `baseline` and its | |
| 54 | /// glyphs raised by `rise`. | |
| 55 | pub fn paint_parley_run<B: parley::Brush>( | |
| 56 | run: &parley::GlyphRun<'_, B>, | |
| 57 | baseline: f32, | |
| 58 | rise: f32, | |
| 59 | line: [f32; 2], | |
| 60 | color: impl Fn(&B) -> Option<[f32; 4]>, | |
| 61 | paint: &mut dyn FnMut(GlyphRun<'_>) -> Result<(), RenderError>, | |
| 62 | ) -> Result<(), RenderError> { | |
| 63 | let shaped = run.run(); | |
| 64 | let font = &shaped.font().font; | |
| 65 | let coords: Vec<i16> = shaped | |
| 66 | .normalized_coords() | |
| 67 | .iter() | |
| 68 | .map(|c| c.to_bits()) | |
| 69 | .collect(); | |
| 70 | let synthesis = shaped.synthesis(); | |
| 71 | let style = run.style(); | |
| 72 | let metrics = shaped.font_metrics(); | |
| 73 | let decorations: Vec<_> = [ | |
| 74 | ( | |
| 75 | &style.underline, | |
| 76 | metrics.underline_offset, | |
| 77 | metrics.underline_size, | |
| 78 | ), | |
| 79 | ( | |
| 80 | &style.strikethrough, | |
| 81 | metrics.strikethrough_offset, | |
| 82 | metrics.strikethrough_size, | |
| 83 | ), | |
| 84 | ] | |
| 85 | .into_iter() | |
| 86 | .filter_map(|(decoration, offset, thickness)| { | |
| 87 | let decoration = decoration.as_ref()?; | |
| 88 | Some(Decoration { | |
| 89 | x: run.offset(), | |
| 90 | y: baseline - rise - decoration.offset.unwrap_or(offset), | |
| 91 | width: run.advance(), | |
| 92 | thickness: decoration.size.unwrap_or(thickness), | |
| 93 | color: color(&decoration.brush), | |
| 94 | }) | |
| 95 | }) | |
| 96 | .collect(); | |
| 97 | let line_baseline = run.baseline(); | |
| 98 | let mut glyphs = run.positioned_glyphs().map(|glyph| Glyph { | |
| 99 | id: glyph.id, | |
| 100 | x: glyph.x, | |
| 101 | y: glyph.y + baseline - line_baseline - rise, | |
| 102 | }); | |
| 103 | paint(GlyphRun { | |
| 104 | font: &font.data, | |
| 105 | index: font.index, | |
| 106 | size: shaped.font_size(), | |
| 107 | coords: &coords, | |
| 108 | embolden: synthesis.embolden(), | |
| 109 | skew: synthesis.skew(), | |
| 110 | color: color(&style.brush), | |
| 111 | line, | |
| 112 | glyphs: &mut glyphs, | |
| 113 | decorations: &decorations, | |
| 114 | }) | |
| 115 | } |
crates/draw/src/text.rs deleted-113| ... | ... | @@ -1,113 +0,0 @@ |
| 1 | use crate::RenderError; | |
| 2 | use parley::fontique::Blob; | |
| 3 | ||
| 4 | /// Text the renderer can paint, as glyph runs in layer units. Owners recover their | |
| 5 | /// concrete text from a primitive through `Any`. | |
| 6 | pub trait Glyphs: std::any::Any { | |
| 7 | fn runs( | |
| 8 | &self, | |
| 9 | paint: &mut dyn FnMut(GlyphRun<'_>) -> Result<(), RenderError>, | |
| 10 | ) -> Result<(), RenderError>; | |
| 11 | } | |
| 12 | ||
| 13 | /// Glyphs sharing one font, size and colour. | |
| 14 | pub struct GlyphRun<'a> { | |
| 15 | pub font: &'a Blob<u8>, | |
| 16 | /// The face within a font collection. | |
| 17 | pub index: u32, | |
| 18 | pub size: f32, | |
| 19 | /// Variation coordinates in F2Dot14 bits. | |
| 20 | pub coords: &'a [i16], | |
| 21 | pub embolden: bool, | |
| 22 | /// Synthesized oblique angle in degrees. | |
| 23 | pub skew: Option<f32>, | |
| 24 | pub color: [f32; 4], | |
| 25 | /// Top and height of the run's line: lines outside the target are skipped, and | |
| 26 | /// colour glyphs shrink to fit. | |
| 27 | pub line: [f32; 2], | |
| 28 | pub glyphs: &'a mut dyn Iterator<Item = Glyph>, | |
| 29 | /// Underline and strikethrough, painted after the glyphs. | |
| 30 | pub decorations: &'a [Decoration], | |
| 31 | } | |
| 32 | ||
| 33 | /// A glyph's baseline position. | |
| 34 | #[derive(Clone, Copy)] | |
| 35 | pub struct Glyph { | |
| 36 | pub id: u32, | |
| 37 | pub x: f32, | |
| 38 | pub y: f32, | |
| 39 | } | |
| 40 | ||
| 41 | /// A decoration line whose top edge is `y`; it stays at least one device pixel thick. | |
| 42 | #[derive(Clone, Copy)] | |
| 43 | pub struct Decoration { | |
| 44 | pub x: f32, | |
| 45 | pub y: f32, | |
| 46 | pub width: f32, | |
| 47 | pub thickness: f32, | |
| 48 | pub color: [f32; 4], | |
| 49 | } | |
| 50 | ||
| 51 | /// Paints one parley glyph run with its line's baseline moved to `baseline` and its | |
| 52 | /// glyphs raised by `rise`. | |
| 53 | pub fn paint_parley_run<B: parley::Brush>( | |
| 54 | run: &parley::GlyphRun<'_, B>, | |
| 55 | baseline: f32, | |
| 56 | rise: f32, | |
| 57 | line: [f32; 2], | |
| 58 | color: impl Fn(&B) -> [f32; 4], | |
| 59 | paint: &mut dyn FnMut(GlyphRun<'_>) -> Result<(), RenderError>, | |
| 60 | ) -> Result<(), RenderError> { | |
| 61 | let shaped = run.run(); | |
| 62 | let font = &shaped.font().font; | |
| 63 | let coords: Vec<i16> = shaped | |
| 64 | .normalized_coords() | |
| 65 | .iter() | |
| 66 | .map(|c| c.to_bits()) | |
| 67 | .collect(); | |
| 68 | let synthesis = shaped.synthesis(); | |
| 69 | let style = run.style(); | |
| 70 | let metrics = shaped.font_metrics(); | |
| 71 | let decorations: Vec<_> = [ | |
| 72 | ( | |
| 73 | &style.underline, | |
| 74 | metrics.underline_offset, | |
| 75 | metrics.underline_size, | |
| 76 | ), | |
| 77 | ( | |
| 78 | &style.strikethrough, | |
| 79 | metrics.strikethrough_offset, | |
| 80 | metrics.strikethrough_size, | |
| 81 | ), | |
| 82 | ] | |
| 83 | .into_iter() | |
| 84 | .filter_map(|(decoration, offset, thickness)| { | |
| 85 | let decoration = decoration.as_ref()?; | |
| 86 | Some(Decoration { | |
| 87 | x: run.offset(), | |
| 88 | y: baseline - rise - decoration.offset.unwrap_or(offset), | |
| 89 | width: run.advance(), | |
| 90 | thickness: decoration.size.unwrap_or(thickness), | |
| 91 | color: color(&decoration.brush), | |
| 92 | }) | |
| 93 | }) | |
| 94 | .collect(); | |
| 95 | let line_baseline = run.baseline(); | |
| 96 | let mut glyphs = run.positioned_glyphs().map(|glyph| Glyph { | |
| 97 | id: glyph.id, | |
| 98 | x: glyph.x, | |
| 99 | y: glyph.y + baseline - line_baseline - rise, | |
| 100 | }); | |
| 101 | paint(GlyphRun { | |
| 102 | font: &font.data, | |
| 103 | index: font.index, | |
| 104 | size: shaped.font_size(), | |
| 105 | coords: &coords, | |
| 106 | embolden: synthesis.embolden(), | |
| 107 | skew: synthesis.skew(), | |
| 108 | color: color(&style.brush), | |
| 109 | line, | |
| 110 | glyphs: &mut glyphs, | |
| 111 | decorations: &decorations, | |
| 112 | }) | |
| 113 | } |
crates/snowbound/Cargo.toml+1-1| ... | ... | @@ -20,5 +20,5 @@ accesskit = "0.25.0" |
| 20 | 20 | accesskit_winit = { version = "0.34.0", default-features = false, features = ["rwh_06"] } |
| 21 | 21 | objc2 = "0.5.2" |
| 22 | 22 | objc2-app-kit = { version = "0.2.2", default-features = false, features = ["NSAlert", "NSButton", "NSControl", "NSView", "NSResponder", "NSWindow", "NSApplication", "NSEvent", "NSColor", "NSColorSpace", "NSDatePicker", "NSDatePickerCell"] } |
| 23 | objc2-foundation = { version = "0.2.2", default-features = false, features = ["NSString", "NSThread", "NSAttributedString", "NSDate", "NSCalendar", "NSDateFormatter", "NSLocale", "NSTimeZone"] } | |
| 23 | objc2-foundation = { version = "0.2.2", default-features = false, features = ["NSString", "NSThread", "NSAttributedString", "NSDate", "NSCalendar", "NSDateFormatter", "NSGeometry", "NSLocale", "NSTimeZone"] } | |
| 24 | 24 |
crates/snowbound/assets/icons/align-left.svg created+3| ... | ... | @@ -0,0 +1,3 @@ |
| 1 | <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"> | |
| 2 | <path d="M2.5 3.5H13.5M2.5 6.5H9.5M2.5 9.5H13.5M2.5 12.5H9.5" fill="none" stroke="currentColor" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/> | |
| 3 | </svg> |
crates/snowbound/assets/icons/attachment.svg created+3| ... | ... | @@ -0,0 +1,3 @@ |
| 1 | <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"> | |
| 2 | <path d="M11.89 8.15L8 12.04A2.5 2.5 0 0 1 4.46 8.5L9.59 3.37A1.5 1.5 0 0 1 11.71 5.49L7.29 9.91" fill="none" stroke="currentColor" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/> | |
| 3 | </svg> |
crates/snowbound/assets/icons/bold.svg created+3| ... | ... | @@ -0,0 +1,3 @@ |
| 1 | <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"> | |
| 2 | <path d="M4.5 8H8.5A2.5 2.5 0 0 0 8.5 3H4.5V13H9.25A2.5 2.5 0 0 0 9.25 8H8.5" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/> | |
| 3 | </svg> |
crates/snowbound/assets/icons/bullets.svg created+6| ... | ... | @@ -0,0 +1,6 @@ |
| 1 | <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"> | |
| 2 | <path d="M7 4.5H13.5M7 8.5H13.5M7 12.5H13.5" fill="none" stroke="currentColor" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/> | |
| 3 | <path d="M2 4.5A1.25 1.25 0 1 0 4.5 4.5A1.25 1.25 0 1 0 2 4.5Z" fill="currentColor"/> | |
| 4 | <path d="M2 8.5A1.25 1.25 0 1 0 4.5 8.5A1.25 1.25 0 1 0 2 8.5Z" fill="currentColor"/> | |
| 5 | <path d="M2 12.5A1.25 1.25 0 1 0 4.5 12.5A1.25 1.25 0 1 0 2 12.5Z" fill="currentColor"/> | |
| 6 | </svg> |
crates/snowbound/assets/icons/calendar.svg created+4| ... | ... | @@ -0,0 +1,4 @@ |
| 1 | <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"> | |
| 2 | <path d="M4 3.5H12A1.5 1.5 0 0 1 13.5 5V12A1.5 1.5 0 0 1 12 13.5H4A1.5 1.5 0 0 1 2.5 12V5A1.5 1.5 0 0 1 4 3.5ZM2.5 6.5H13.5M5.5 1.5V4.5M10.5 1.5V4.5" fill="none" stroke="currentColor" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/> | |
| 3 | <path d="M9.5 9H11A0.5 0.5 0 0 1 11.5 9.5V11A0.5 0.5 0 0 1 11 11.5H9.5A0.5 0.5 0 0 1 9 11V9.5A0.5 0.5 0 0 1 9.5 9Z" fill="currentColor"/> | |
| 4 | </svg> |
crates/snowbound/assets/icons/clear-formatting.svg created+3| ... | ... | @@ -0,0 +1,3 @@ |
| 1 | <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"> | |
| 2 | <path d="M2.5 3.5H10.5M6.5 3.5V13.5M10 10L13.5 13.5M13.5 10L10 13.5" fill="none" stroke="currentColor" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/> | |
| 3 | </svg> |
crates/snowbound/assets/icons/clock.svg created+3| ... | ... | @@ -0,0 +1,3 @@ |
| 1 | <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"> | |
| 2 | <path d="M1.75 8A6.25 6.25 0 1 0 14.25 8A6.25 6.25 0 1 0 1.75 8ZM8 4.5V8L10.5 9.5" fill="none" stroke="currentColor" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/> | |
| 3 | </svg> |
crates/snowbound/assets/icons/close.svg created+3| ... | ... | @@ -0,0 +1,3 @@ |
| 1 | <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"> | |
| 2 | <path d="M4 4L12 12M12 4L4 12" fill="none" stroke="currentColor" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/> | |
| 3 | </svg> |
crates/snowbound/assets/icons/equation.svg created+3| ... | ... | @@ -0,0 +1,3 @@ |
| 1 | <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"> | |
| 2 | <path d="M2.5 4H13.5M6 4V12.5M10.5 4V11C10.5 12.25 11.25 12.75 12.75 12.25" fill="none" stroke="currentColor" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/> | |
| 3 | </svg> |
crates/snowbound/assets/icons/font-color.svg created+3| ... | ... | @@ -0,0 +1,3 @@ |
| 1 | <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"> | |
| 2 | <path d="M3.5 12L8 1.75L12.5 12M5.3 8.25H10.7" fill="none" stroke="currentColor" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/> | |
| 3 | </svg> |
crates/snowbound/assets/icons/highlighter.svg created+3| ... | ... | @@ -0,0 +1,3 @@ |
| 1 | <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"> | |
| 2 | <path d="M11 1.75L13.75 4.5L8.5 9.75L5.75 7ZM5.75 7L4.75 8L3.5 11.25L4.25 12L7.5 10.75L8.5 9.75" fill="none" stroke="currentColor" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/> | |
| 3 | </svg> |
crates/snowbound/assets/icons/indent.svg created+4| ... | ... | @@ -0,0 +1,4 @@ |
| 1 | <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"> | |
| 2 | <path d="M2.5 3.5H13.5M7.5 6.5H13.5M7.5 9.5H13.5M2.5 12.5H13.5" fill="none" stroke="currentColor" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/> | |
| 3 | <path d="M2.5 5.5L5 8L2.5 10.5" fill="none" stroke="currentColor" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/> | |
| 4 | </svg> |
crates/snowbound/assets/icons/italic.svg created+3| ... | ... | @@ -0,0 +1,3 @@ |
| 1 | <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"> | |
| 2 | <path d="M7 2.5H12.5M3.5 13.5H9M9.75 2.5L6.25 13.5" fill="none" stroke="currentColor" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/> | |
| 3 | </svg> |
crates/snowbound/assets/icons/link.svg created+3| ... | ... | @@ -0,0 +1,3 @@ |
| 1 | <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"> | |
| 2 | <path d="M6.5 4.75H5A3.25 3.25 0 0 0 5 11.25H6.5M9.5 4.75H11A3.25 3.25 0 0 1 11 11.25H9.5M5.5 8H10.5" fill="none" stroke="currentColor" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/> | |
| 3 | </svg> |
crates/snowbound/assets/icons/new-page.svg created+3| ... | ... | @@ -0,0 +1,3 @@ |
| 1 | <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"> | |
| 2 | <path d="M9 1.5H4.5A1 1 0 0 0 3.5 2.5V13.5A1 1 0 0 0 4.5 14.5H11.5A1 1 0 0 0 12.5 13.5V5L9 1.5V5H12.5M8 7.25V11.75M5.75 9.5H10.25" fill="none" stroke="currentColor" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/> | |
| 3 | </svg> |
crates/snowbound/assets/icons/numbering.svg created+5| ... | ... | @@ -0,0 +1,5 @@ |
| 1 | <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"> | |
| 2 | <path d="M7.5 4.5H13.5M7.5 8.5H13.5M7.5 12.5H13.5" fill="none" stroke="currentColor" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/> | |
| 3 | <path d="M2.25 4L3.75 3V7.25" fill="none" stroke="currentColor" stroke-width="1.1" stroke-linecap="round" stroke-linejoin="round"/> | |
| 4 | <path d="M2 11.25C2.2 10.15 4.6 10.05 4.6 11.35C4.6 12.55 2 13.05 2 14.25H4.8" fill="none" stroke="currentColor" stroke-width="1.1" stroke-linecap="round" stroke-linejoin="round"/> | |
| 5 | </svg> |
crates/snowbound/assets/icons/outdent.svg created+4| ... | ... | @@ -0,0 +1,4 @@ |
| 1 | <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"> | |
| 2 | <path d="M2.5 3.5H13.5M7.5 6.5H13.5M7.5 9.5H13.5M2.5 12.5H13.5" fill="none" stroke="currentColor" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/> | |
| 3 | <path d="M5 5.5L2.5 8L5 10.5" fill="none" stroke="currentColor" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/> | |
| 4 | </svg> |
crates/snowbound/assets/icons/picture.svg created+4| ... | ... | @@ -0,0 +1,4 @@ |
| 1 | <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"> | |
| 2 | <path d="M3 2.5H13A1.5 1.5 0 0 1 14.5 4V12A1.5 1.5 0 0 1 13 13.5H3A1.5 1.5 0 0 1 1.5 12V4A1.5 1.5 0 0 1 3 2.5ZM1.75 12L6 7.5L10.5 12M9 10.5L10.75 8.75L14.25 12.25" fill="none" stroke="currentColor" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/> | |
| 3 | <path d="M9.5 5.75A1.25 1.25 0 1 0 12 5.75A1.25 1.25 0 1 0 9.5 5.75Z" fill="currentColor"/> | |
| 4 | </svg> |
crates/snowbound/assets/icons/redo.svg created+3| ... | ... | @@ -0,0 +1,3 @@ |
| 1 | <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"> | |
| 2 | <path d="M10.5 3.5L13.5 6.5L10.5 9.5M13.5 6.5H6.25A3.5 3.5 0 0 0 6.25 13.5H9.5" fill="none" stroke="currentColor" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/> | |
| 3 | </svg> |
crates/snowbound/assets/icons/search.svg created+3| ... | ... | @@ -0,0 +1,3 @@ |
| 1 | <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"> | |
| 2 | <path d="M2.25 7A4.75 4.75 0 1 0 11.75 7A4.75 4.75 0 1 0 2.25 7ZM10.5 10.5L13.5 13.5" fill="none" stroke="currentColor" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/> | |
| 3 | </svg> |
crates/snowbound/assets/icons/sidebar-collapse.svg created+3| ... | ... | @@ -0,0 +1,3 @@ |
| 1 | <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"> | |
| 2 | <path d="M3 2.5H13A1.5 1.5 0 0 1 14.5 4V12A1.5 1.5 0 0 1 13 13.5H3A1.5 1.5 0 0 1 1.5 12V4A1.5 1.5 0 0 1 3 2.5ZM10.5 2.5V13.5M4.75 5.5L7.25 8L4.75 10.5" fill="none" stroke="currentColor" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/> | |
| 3 | </svg> |
crates/snowbound/assets/icons/sidebar-expand.svg created+3| ... | ... | @@ -0,0 +1,3 @@ |
| 1 | <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"> | |
| 2 | <path d="M3 2.5H13A1.5 1.5 0 0 1 14.5 4V12A1.5 1.5 0 0 1 13 13.5H3A1.5 1.5 0 0 1 1.5 12V4A1.5 1.5 0 0 1 3 2.5ZM10.5 2.5V13.5M7.25 5.5L4.75 8L7.25 10.5" fill="none" stroke="currentColor" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/> | |
| 3 | </svg> |
crates/snowbound/assets/icons/strikethrough.svg created+3| ... | ... | @@ -0,0 +1,3 @@ |
| 1 | <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"> | |
| 2 | <path d="M11.5 3.5C10.9 2.8 10 2.5 8 2.5C5.5 2.5 4.5 3.6 4.5 5C4.5 5.6 4.7 6.1 5.1 6.5M10.5 9.5C11.2 10 11.5 10.6 11.5 11.25C11.5 12.6 10.2 13.5 8 13.5C6.2 13.5 4.9 13.1 4.5 12.3M2.5 8.5H13.5" fill="none" stroke="currentColor" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/> | |
| 3 | </svg> |
crates/snowbound/assets/icons/subscript.svg created+4| ... | ... | @@ -0,0 +1,4 @@ |
| 1 | <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"> | |
| 2 | <path d="M2.5 3.5L9.5 11.5M9.5 3.5L2.5 11.5" fill="none" stroke="currentColor" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/> | |
| 3 | <path d="M11 11C11.2 9.9 13.6 9.8 13.6 11.1C13.6 12.3 11 12.8 11 14H13.8" fill="none" stroke="currentColor" stroke-width="1.1" stroke-linecap="round" stroke-linejoin="round"/> | |
| 4 | </svg> |
crates/snowbound/assets/icons/table.svg created+3| ... | ... | @@ -0,0 +1,3 @@ |
| 1 | <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"> | |
| 2 | <path d="M4 2.5H12A1.5 1.5 0 0 1 13.5 4V12A1.5 1.5 0 0 1 12 13.5H4A1.5 1.5 0 0 1 2.5 12V4A1.5 1.5 0 0 1 4 2.5ZM2.5 6.5H13.5M2.5 9.5H13.5M6.5 2.5V13.5M9.5 2.5V13.5" fill="none" stroke="currentColor" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/> | |
| 3 | </svg> |
crates/snowbound/assets/icons/underline.svg created+3| ... | ... | @@ -0,0 +1,3 @@ |
| 1 | <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"> | |
| 2 | <path d="M4.5 2.5V7A3.5 3.5 0 0 0 11.5 7V2.5M3.5 13.5H12.5" fill="none" stroke="currentColor" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/> | |
| 3 | </svg> |
crates/snowbound/assets/icons/undo.svg created+3| ... | ... | @@ -0,0 +1,3 @@ |
| 1 | <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"> | |
| 2 | <path d="M5.5 3.5L2.5 6.5L5.5 9.5M2.5 6.5H9.75A3.5 3.5 0 0 1 9.75 13.5H6.5" fill="none" stroke="currentColor" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/> | |
| 3 | </svg> |
crates/snowbound/assets/icons/zoom-in.svg created+3| ... | ... | @@ -0,0 +1,3 @@ |
| 1 | <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"> | |
| 2 | <path d="M2.25 7A4.75 4.75 0 1 0 11.75 7A4.75 4.75 0 1 0 2.25 7ZM10.5 10.5L13.5 13.5M4.75 7H9.25M7 4.75V9.25" fill="none" stroke="currentColor" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/> | |
| 3 | </svg> |
crates/snowbound/assets/icons/zoom-out.svg created+3| ... | ... | @@ -0,0 +1,3 @@ |
| 1 | <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"> | |
| 2 | <path d="M2.25 7A4.75 4.75 0 1 0 11.75 7A4.75 4.75 0 1 0 2.25 7ZM10.5 10.5L13.5 13.5M4.75 7H9.25" fill="none" stroke="currentColor" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/> | |
| 3 | </svg> |
crates/snowbound/assets/tags/address.svg created+34| ... | ... | @@ -0,0 +1,34 @@ |
| 1 | <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"> | |
| 2 | <defs> | |
| 3 | <linearGradient id="layer0" x1="0" y1="0" x2="0" y2="16" gradientUnits="userSpaceOnUse"> | |
| 4 | <stop offset="0" stop-color="#9a4a2a"/> | |
| 5 | <stop offset="1" stop-color="#7a3218"/> | |
| 6 | </linearGradient> | |
| 7 | <linearGradient id="layer1" x1="0" y1="0" x2="0" y2="16" gradientUnits="userSpaceOnUse"> | |
| 8 | <stop offset="0" stop-color="#b58a3a"/> | |
| 9 | <stop offset="1" stop-color="#8a6420"/> | |
| 10 | </linearGradient> | |
| 11 | <linearGradient id="layer2" x1="0" y1="0" x2="0" y2="16" gradientUnits="userSpaceOnUse"> | |
| 12 | <stop offset="0" stop-color="#fff1b0"/> | |
| 13 | <stop offset="1" stop-color="#f2c25c"/> | |
| 14 | </linearGradient> | |
| 15 | <linearGradient id="layer3" x1="0" y1="0" x2="0" y2="16" gradientUnits="userSpaceOnUse"> | |
| 16 | <stop offset="0" stop-color="#e0672e"/> | |
| 17 | <stop offset="1" stop-color="#a8401a"/> | |
| 18 | </linearGradient> | |
| 19 | <linearGradient id="layer4" x1="0" y1="0" x2="0" y2="16" gradientUnits="userSpaceOnUse"> | |
| 20 | <stop offset="0" stop-color="#9a5a1e"/> | |
| 21 | <stop offset="1" stop-color="#6e3a10"/> | |
| 22 | </linearGradient> | |
| 23 | <linearGradient id="layer5" x1="0" y1="0" x2="0" y2="16" gradientUnits="userSpaceOnUse"> | |
| 24 | <stop offset="0" stop-color="#e3a54c"/> | |
| 25 | <stop offset="1" stop-color="#c07a26"/> | |
| 26 | </linearGradient> | |
| 27 | </defs> | |
| 28 | <path d="M10.5 1.5H13V5.5L10.5 3.5Z" fill="url(#layer0)"/> | |
| 29 | <path d="M3 7H13V15H3Z" fill="url(#layer1)"/> | |
| 30 | <path d="M4 7.5H12V14H4Z" fill="url(#layer2)"/> | |
| 31 | <path d="M8 0.75L15.25 8L14 9.25L8 3.25L2 9.25L0.75 8Z" fill="url(#layer3)"/> | |
| 32 | <path d="M6.75 9.5H9.25V14H6.75Z" fill="url(#layer4)"/> | |
| 33 | <path d="M7.25 10H8.75V14H7.25Z" fill="url(#layer5)"/> | |
| 34 | </svg> |
crates/snowbound/assets/tags/contact.svg created+25| ... | ... | @@ -0,0 +1,25 @@ |
| 1 | <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"> | |
| 2 | <defs> | |
| 3 | <linearGradient id="layer0" x1="0" y1="0" x2="0" y2="16" gradientUnits="userSpaceOnUse"> | |
| 4 | <stop offset="0" stop-color="#8aa3c8"/> | |
| 5 | <stop offset="1" stop-color="#5d7aa6"/> | |
| 6 | </linearGradient> | |
| 7 | <linearGradient id="layer1" x1="0" y1="0" x2="0" y2="16" gradientUnits="userSpaceOnUse"> | |
| 8 | <stop offset="0" stop-color="#ffffff"/> | |
| 9 | <stop offset="1" stop-color="#d3e0f2"/> | |
| 10 | </linearGradient> | |
| 11 | <linearGradient id="layer2" x1="0" y1="0" x2="0" y2="16" gradientUnits="userSpaceOnUse"> | |
| 12 | <stop offset="0" stop-color="#e39a5c"/> | |
| 13 | <stop offset="1" stop-color="#b8612a"/> | |
| 14 | </linearGradient> | |
| 15 | <linearGradient id="layer3" x1="0" y1="0" x2="0" y2="16" gradientUnits="userSpaceOnUse"> | |
| 16 | <stop offset="0" stop-color="#e05a30"/> | |
| 17 | <stop offset="1" stop-color="#b03a1a"/> | |
| 18 | </linearGradient> | |
| 19 | </defs> | |
| 20 | <path d="M1.5 2.5H14.5A1 1 0 0 1 15.5 3.5V12.5A1 1 0 0 1 14.5 13.5H1.5A1 1 0 0 1 0.5 12.5V3.5A1 1 0 0 1 1.5 2.5Z" fill="url(#layer0)"/> | |
| 21 | <path d="M1.75 3.5H14.25A0.25 0.25 0 0 1 14.5 3.75V12.25A0.25 0.25 0 0 1 14.25 12.5H1.75A0.25 0.25 0 0 1 1.5 12.25V3.75A0.25 0.25 0 0 1 1.75 3.5Z" fill="url(#layer1)"/> | |
| 22 | <path d="M3.5 6.5A1.75 1.75 0 1 0 7 6.5A1.75 1.75 0 1 0 3.5 6.5Z" fill="url(#layer2)"/> | |
| 23 | <path d="M2.25 11.75C2.25 9.5 3.6 8.75 5.25 8.75C6.9 8.75 8.25 9.5 8.25 11.75Z" fill="url(#layer3)"/> | |
| 24 | <path d="M9.5 5.5H13.5V6.75H9.5ZM9.5 8H13.5V9.25H9.5ZM9.5 10.5H12.5V11.75H9.5Z" fill="#4d6d9c"/> | |
| 25 | </svg> |
crates/snowbound/assets/tags/definition.svg created+15| ... | ... | @@ -0,0 +1,15 @@ |
| 1 | <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"> | |
| 2 | <defs> | |
| 3 | <linearGradient id="layer0" x1="0" y1="0" x2="0" y2="16" gradientUnits="userSpaceOnUse"> | |
| 4 | <stop offset="0" stop-color="#3eaa3e"/> | |
| 5 | <stop offset="1" stop-color="#238a23"/> | |
| 6 | </linearGradient> | |
| 7 | <linearGradient id="layer1" x1="0" y1="0" x2="0" y2="16" gradientUnits="userSpaceOnUse"> | |
| 8 | <stop offset="0" stop-color="#9dff9d"/> | |
| 9 | <stop offset="1" stop-color="#2ef02e"/> | |
| 10 | </linearGradient> | |
| 11 | </defs> | |
| 12 | <path d="M2 3H14A1 1 0 0 1 15 4V12A1 1 0 0 1 14 13H2A1 1 0 0 1 1 12V4A1 1 0 0 1 2 3Z" fill="url(#layer0)"/> | |
| 13 | <path d="M2.25 4H13.75A0.25 0.25 0 0 1 14 4.25V11.75A0.25 0.25 0 0 1 13.75 12H2.25A0.25 0.25 0 0 1 2 11.75V4.25A0.25 0.25 0 0 1 2.25 4Z" fill="url(#layer1)"/> | |
| 14 | <path d="M3.5 6H12.5V7.25H3.5ZM3.5 8.75H10V10H3.5Z" fill="#1d4f1d"/> | |
| 15 | </svg> |
crates/snowbound/assets/tags/highlight.svg created+29| ... | ... | @@ -0,0 +1,29 @@ |
| 1 | <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"> | |
| 2 | <defs> | |
| 3 | <linearGradient id="layer0" x1="0" y1="0" x2="0" y2="16" gradientUnits="userSpaceOnUse"> | |
| 4 | <stop offset="0" stop-color="#b23a64"/> | |
| 5 | <stop offset="1" stop-color="#8c2248"/> | |
| 6 | </linearGradient> | |
| 7 | <linearGradient id="layer1" x1="0" y1="0" x2="0" y2="16" gradientUnits="userSpaceOnUse"> | |
| 8 | <stop offset="0" stop-color="#7a2a40"/> | |
| 9 | <stop offset="1" stop-color="#5a1a2c"/> | |
| 10 | </linearGradient> | |
| 11 | <linearGradient id="layer2" x1="0" y1="0" x2="0" y2="16" gradientUnits="userSpaceOnUse"> | |
| 12 | <stop offset="0" stop-color="#ff9ec0"/> | |
| 13 | <stop offset="1" stop-color="#e0487c"/> | |
| 14 | </linearGradient> | |
| 15 | <linearGradient id="layer3" x1="0" y1="0" x2="0" y2="16" gradientUnits="userSpaceOnUse"> | |
| 16 | <stop offset="0" stop-color="#e76b98"/> | |
| 17 | <stop offset="1" stop-color="#c0305e"/> | |
| 18 | </linearGradient> | |
| 19 | <linearGradient id="layer4" x1="0" y1="0" x2="0" y2="16" gradientUnits="userSpaceOnUse"> | |
| 20 | <stop offset="0" stop-color="#ff5f8f"/> | |
| 21 | <stop offset="1" stop-color="#e0205a"/> | |
| 22 | </linearGradient> | |
| 23 | </defs> | |
| 24 | <path d="M12.31 1.14L14.86 3.69L7.01 11.54L4.46 8.99Z" fill="url(#layer0)"/> | |
| 25 | <path d="M5.35 7.89L8.11 10.65L4.55 13.04L2.96 11.45Z" fill="url(#layer1)"/> | |
| 26 | <path d="M12.31 2.13L13.87 3.69L7.01 10.55L5.45 8.99Z" fill="url(#layer2)"/> | |
| 27 | <path d="M12.31 2.13L13.87 3.69L12.17 5.38L10.62 3.83Z" fill="url(#layer3)"/> | |
| 28 | <path d="M5.45 8.99L7.01 10.55L4.64 12.14L3.86 11.36Z" fill="url(#layer4)"/> | |
| 29 | </svg> |
crates/snowbound/assets/tags/phone.svg created+14| ... | ... | @@ -0,0 +1,14 @@ |
| 1 | <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"> | |
| 2 | <defs> | |
| 3 | <linearGradient id="layer0" x1="0" y1="0" x2="0" y2="16" gradientUnits="userSpaceOnUse"> | |
| 4 | <stop offset="0" stop-color="#7f8b99"/> | |
| 5 | <stop offset="1" stop-color="#56606d"/> | |
| 6 | </linearGradient> | |
| 7 | <linearGradient id="layer1" x1="0" y1="0" x2="0" y2="16" gradientUnits="userSpaceOnUse"> | |
| 8 | <stop offset="0" stop-color="#f4f7fb"/> | |
| 9 | <stop offset="1" stop-color="#b3c1d3"/> | |
| 10 | </linearGradient> | |
| 11 | </defs> | |
| 12 | <path d="M12.86 14.11L12.59 14.07L12.31 14.03L12.04 13.98L11.77 13.92L11.49 13.86L11.22 13.79L10.96 13.72L10.69 13.64L10.42 13.56L10.16 13.46L9.9 13.37L9.64 13.27L9.38 13.16L9.13 13.04L8.88 12.92L8.63 12.8L8.38 12.67L8.14 12.53L7.9 12.39L7.66 12.25L7.43 12.09L7.2 11.94L6.97 11.78L6.75 11.61L6.53 11.44L6.31 11.26L6.1 11.08L5.89 10.89L5.69 10.7L5.49 10.51L5.3 10.31L5.11 10.11L4.92 9.9L4.74 9.69L4.56 9.47L4.39 9.25L4.22 9.03L4.06 8.8L3.91 8.57L3.75 8.34L3.61 8.1L3.47 7.86L3.33 7.62L3.2 7.37L3.08 7.12L2.96 6.87L2.84 6.62L2.73 6.36L2.63 6.1L2.54 5.84L2.44 5.58L2.36 5.31L2.28 5.04L2.21 4.78L2.14 4.51L2.08 4.23L2.02 3.96L1.97 3.69L1.93 3.41L1.89 3.14L7.45 2.45L7.47 2.61L7.5 2.76L7.52 2.91L7.55 3.06L7.59 3.21L7.63 3.36L7.67 3.51L7.71 3.66L7.76 3.81L7.81 3.96L7.86 4.1L7.77 4.3L7.44 4.62L6.98 5.02L6.51 5.45L6.15 5.85L5.99 6.17L6.07 6.37L6.18 6.55L6.29 6.73L6.4 6.91L6.52 7.09L6.65 7.26L6.77 7.43L6.9 7.6L7.04 7.76L7.18 7.92L7.32 8.08L7.46 8.24L7.61 8.39L7.76 8.54L7.92 8.68L8.08 8.82L8.24 8.96L8.4 9.1L8.57 9.23L8.74 9.35L8.91 9.48L9.09 9.6L9.27 9.71L9.45 9.82L9.63 9.93L9.83 10.01L10.15 9.85L10.55 9.49L10.98 9.02L11.38 8.56L11.7 8.23L11.9 8.14L12.04 8.19L12.19 8.24L12.34 8.29L12.49 8.33L12.64 8.37L12.79 8.41L12.94 8.45L13.09 8.48L13.24 8.5L13.39 8.53L13.55 8.55Z" fill="url(#layer0)"/> | |
| 13 | <path d="M12.13 13.15L12.71 13.23L12.45 13.19L12.2 13.14L11.94 13.09L11.69 13.03L11.44 12.97L11.19 12.9L10.94 12.83L10.69 12.75L10.45 12.66L10.2 12.57L9.96 12.48L9.72 12.38L9.48 12.27L9.25 12.16L9.02 12.04L8.79 11.92L8.56 11.8L8.34 11.66L8.12 11.53L7.9 11.39L7.68 11.24L7.47 11.09L7.26 10.93L7.06 10.77L6.86 10.61L6.66 10.44L6.47 10.27L6.28 10.09L6.09 9.91L5.91 9.72L5.73 9.53L5.56 9.34L5.39 9.14L5.23 8.94L5.07 8.74L4.91 8.53L4.76 8.32L4.61 8.1L4.47 7.88L4.34 7.66L4.2 7.44L4.08 7.21L3.96 6.98L3.84 6.75L3.73 6.52L3.62 6.28L3.52 6.04L3.43 5.8L3.34 5.55L3.25 5.31L3.17 5.06L3.1 4.81L3.03 4.56L2.97 4.31L2.91 4.06L2.86 3.8L2.81 3.55L2.77 3.29L2.85 3.87L6.72 3.4L6.63 2.73L6.66 2.9L6.69 3.07L6.72 3.24L6.76 3.41L6.8 3.58L6.85 3.75L6.9 3.91L6.95 4.08L7.01 4.24L6.95 4.07L7.06 3.81L6.87 3.99L6.41 4.38L5.91 4.85L5.43 5.37L5.06 6.14L5.31 6.75L5.45 6.99L5.57 7.19L5.7 7.38L5.83 7.57L5.96 7.76L6.1 7.95L6.24 8.13L6.39 8.31L6.54 8.48L6.69 8.66L6.85 8.82L7.01 8.99L7.18 9.15L7.34 9.31L7.52 9.46L7.69 9.61L7.87 9.76L8.05 9.9L8.24 10.04L8.43 10.17L8.62 10.3L8.81 10.43L9.01 10.55L9.25 10.69L9.86 10.94L10.63 10.57L11.15 10.09L11.62 9.59L12.01 9.13L12.19 8.94L11.93 9.05L11.76 8.99L11.92 9.05L12.09 9.1L12.25 9.15L12.42 9.2L12.59 9.24L12.76 9.28L12.93 9.31L13.1 9.34L13.27 9.37L12.6 9.28Z" fill="url(#layer1)"/> | |
| 14 | </svg> |
crates/snowbound/assets/tags/remember.svg created+15| ... | ... | @@ -0,0 +1,15 @@ |
| 1 | <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"> | |
| 2 | <defs> | |
| 3 | <linearGradient id="layer0" x1="0" y1="0" x2="0" y2="16" gradientUnits="userSpaceOnUse"> | |
| 4 | <stop offset="0" stop-color="#c9b52a"/> | |
| 5 | <stop offset="1" stop-color="#a8931a"/> | |
| 6 | </linearGradient> | |
| 7 | <linearGradient id="layer1" x1="0" y1="0" x2="0" y2="16" gradientUnits="userSpaceOnUse"> | |
| 8 | <stop offset="0" stop-color="#fffb7a"/> | |
| 9 | <stop offset="1" stop-color="#fff200"/> | |
| 10 | </linearGradient> | |
| 11 | </defs> | |
| 12 | <path d="M2 3H14A1 1 0 0 1 15 4V12A1 1 0 0 1 14 13H2A1 1 0 0 1 1 12V4A1 1 0 0 1 2 3Z" fill="url(#layer0)"/> | |
| 13 | <path d="M2.25 4H13.75A0.25 0.25 0 0 1 14 4.25V11.75A0.25 0.25 0 0 1 13.75 12H2.25A0.25 0.25 0 0 1 2 11.75V4.25A0.25 0.25 0 0 1 2.25 4Z" fill="url(#layer1)"/> | |
| 14 | <path d="M3.5 6H12.5V7.25H3.5ZM3.5 8.75H10V10H3.5Z" fill="#5a5418"/> | |
| 15 | </svg> |
crates/snowbound/assets/tags/star.svg created+14| ... | ... | @@ -0,0 +1,14 @@ |
| 1 | <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"> | |
| 2 | <defs> | |
| 3 | <linearGradient id="layer0" x1="0" y1="0" x2="0" y2="16" gradientUnits="userSpaceOnUse"> | |
| 4 | <stop offset="0" stop-color="#c8a049"/> | |
| 5 | <stop offset="1" stop-color="#a57a2c"/> | |
| 6 | </linearGradient> | |
| 7 | <linearGradient id="layer1" x1="0" y1="0" x2="0" y2="16" gradientUnits="userSpaceOnUse"> | |
| 8 | <stop offset="0" stop-color="#fff4c4"/> | |
| 9 | <stop offset="1" stop-color="#f1c35a"/> | |
| 10 | </linearGradient> | |
| 11 | </defs> | |
| 12 | <path d="M8 1.35L9.94 6.08L15.04 6.46L11.14 9.77L12.35 14.74L8 12.05L3.65 14.74L4.86 9.77L0.96 6.46L6.06 6.08Z" fill="url(#layer0)"/> | |
| 13 | <path d="M8 3.72L9.32 6.94L12.78 7.2L10.13 9.44L10.96 12.82L8 10.99L5.04 12.82L5.87 9.44L3.22 7.2L6.68 6.94Z" fill="url(#layer1)"/> | |
| 14 | </svg> |
crates/snowbound/src/art.rs created+45| ... | ... | @@ -0,0 +1,45 @@ |
| 1 | //! Artwork for the window's chrome: tinted interface icons and the toolbar's tag buttons. | |
| 2 | ||
| 3 | macro_rules! art { | |
| 4 | ($path:literal) => { | |
| 5 | &[include_str!(concat!("../assets/", $path, ".svg"))] | |
| 6 | }; | |
| 7 | } | |
| 8 | ||
| 9 | pub const ALIGN_LEFT: &[&str] = art!("icons/align-left"); | |
| 10 | pub const ATTACHMENT: &[&str] = art!("icons/attachment"); | |
| 11 | pub const BOLD: &[&str] = art!("icons/bold"); | |
| 12 | pub const BULLETS: &[&str] = art!("icons/bullets"); | |
| 13 | pub const CALENDAR: &[&str] = art!("icons/calendar"); | |
| 14 | pub const CLEAR_FORMATTING: &[&str] = art!("icons/clear-formatting"); | |
| 15 | pub const CLOCK: &[&str] = art!("icons/clock"); | |
| 16 | pub const CLOSE: &[&str] = art!("icons/close"); | |
| 17 | pub const EQUATION: &[&str] = art!("icons/equation"); | |
| 18 | pub const FONT_COLOR: &[&str] = art!("icons/font-color"); | |
| 19 | pub const HIGHLIGHTER: &[&str] = art!("icons/highlighter"); | |
| 20 | pub const INDENT: &[&str] = art!("icons/indent"); | |
| 21 | pub const ITALIC: &[&str] = art!("icons/italic"); | |
| 22 | pub const LINK: &[&str] = art!("icons/link"); | |
| 23 | pub const NEW_PAGE: &[&str] = art!("icons/new-page"); | |
| 24 | pub const NUMBERING: &[&str] = art!("icons/numbering"); | |
| 25 | pub const OUTDENT: &[&str] = art!("icons/outdent"); | |
| 26 | pub const PICTURE: &[&str] = art!("icons/picture"); | |
| 27 | pub const REDO: &[&str] = art!("icons/redo"); | |
| 28 | pub const SEARCH: &[&str] = art!("icons/search"); | |
| 29 | pub const SIDEBAR_COLLAPSE: &[&str] = art!("icons/sidebar-collapse"); | |
| 30 | pub const SIDEBAR_EXPAND: &[&str] = art!("icons/sidebar-expand"); | |
| 31 | pub const STRIKETHROUGH: &[&str] = art!("icons/strikethrough"); | |
| 32 | pub const SUBSCRIPT: &[&str] = art!("icons/subscript"); | |
| 33 | pub const TABLE: &[&str] = art!("icons/table"); | |
| 34 | pub const UNDERLINE: &[&str] = art!("icons/underline"); | |
| 35 | pub const UNDO: &[&str] = art!("icons/undo"); | |
| 36 | pub const ZOOM_IN: &[&str] = art!("icons/zoom-in"); | |
| 37 | pub const ZOOM_OUT: &[&str] = art!("icons/zoom-out"); | |
| 38 | ||
| 39 | pub const TAG_STAR: &[&str] = art!("tags/star"); | |
| 40 | pub const TAG_REMEMBER: &[&str] = art!("tags/remember"); | |
| 41 | pub const TAG_DEFINITION: &[&str] = art!("tags/definition"); | |
| 42 | pub const TAG_HIGHLIGHT: &[&str] = art!("tags/highlight"); | |
| 43 | pub const TAG_CONTACT: &[&str] = art!("tags/contact"); | |
| 44 | pub const TAG_ADDRESS: &[&str] = art!("tags/address"); | |
| 45 | pub const TAG_PHONE: &[&str] = art!("tags/phone"); |
crates/snowbound/src/macos.rs+51-1| ... | ... | @@ -13,7 +13,7 @@ use objc2_app_kit::{ |
| 13 | 13 | }; |
| 14 | 14 | use objc2_foundation::{ |
| 15 | 15 | MainThreadMarker, NSAttributedString, NSCalendar, NSCalendarUnit, NSDate, NSDateFormatter, |
| 16 | NSDateFormatterStyle, NSRange, NSString, | |
| 16 | NSDateFormatterStyle, NSPoint, NSRange, NSRect, NSSize, NSString, | |
| 17 | 17 | }; |
| 18 | 18 | use std::{cell::Cell, sync::OnceLock}; |
| 19 | 19 | use winit::{ |
| ... | ... | @@ -261,6 +261,56 @@ pub fn configure_presentation(surface: &wgpu::Surface<'_>) { |
| 261 | 261 | } |
| 262 | 262 | } |
| 263 | 263 | |
| 264 | /// The application's icon as the Dock shows it, `pixels` square. | |
| 265 | pub fn app_icon(pixels: u32) -> Option<draw::RasterImage> { | |
| 266 | let side = pixels as usize; | |
| 267 | let mut rgba = unsafe { | |
| 268 | let mtm = MainThreadMarker::new()?; | |
| 269 | let icon: Retained<AnyObject> = | |
| 270 | msg_send_id![&NSApplication::sharedApplication(mtm), applicationIconImage]; | |
| 271 | let bitmap: Allocated<AnyObject> = msg_send_id![AnyClass::get("NSBitmapImageRep")?, alloc]; | |
| 272 | let planes: *mut *mut u8 = std::ptr::null_mut(); | |
| 273 | let bitmap: Option<Retained<AnyObject>> = msg_send_id![ | |
| 274 | bitmap, | |
| 275 | initWithBitmapDataPlanes: planes, | |
| 276 | pixelsWide: side as isize, | |
| 277 | pixelsHigh: side as isize, | |
| 278 | bitsPerSample: 8isize, | |
| 279 | samplesPerPixel: 4isize, | |
| 280 | hasAlpha: true, | |
| 281 | isPlanar: false, | |
| 282 | colorSpaceName: &*NSString::from_str("NSCalibratedRGBColorSpace"), | |
| 283 | bytesPerRow: 4 * side as isize, | |
| 284 | bitsPerPixel: 32isize | |
| 285 | ]; | |
| 286 | let bitmap = bitmap?; | |
| 287 | let contexts = AnyClass::get("NSGraphicsContext")?; | |
| 288 | let context: Option<Retained<AnyObject>> = | |
| 289 | msg_send_id![contexts, graphicsContextWithBitmapImageRep: &*bitmap]; | |
| 290 | let _: () = msg_send![contexts, saveGraphicsState]; | |
| 291 | let _: () = msg_send![contexts, setCurrentContext: &*context?]; | |
| 292 | let bounds = NSRect::new( | |
| 293 | NSPoint::new(0.0, 0.0), | |
| 294 | NSSize::new(side as f64, side as f64), | |
| 295 | ); | |
| 296 | // NSCompositingOperationSourceOver | |
| 297 | let _: () = msg_send![&icon, drawInRect: bounds, fromRect: NSRect::ZERO, operation: 2usize, fraction: 1.0f64]; | |
| 298 | let _: () = msg_send![contexts, restoreGraphicsState]; | |
| 299 | let data: *const u8 = msg_send![&bitmap, bitmapData]; | |
| 300 | std::slice::from_raw_parts(data, 4 * side * side).to_vec() | |
| 301 | }; | |
| 302 | // AppKit's bitmap is premultiplied. | |
| 303 | for pixel in rgba.chunks_exact_mut(4) { | |
| 304 | let alpha = u32::from(pixel[3]); | |
| 305 | for channel in &mut pixel[..3] { | |
| 306 | *channel = (u32::from(*channel) * 255) | |
| 307 | .checked_div(alpha) | |
| 308 | .map_or(0, |value| value.min(255) as u8); | |
| 309 | } | |
| 310 | } | |
| 311 | draw::RasterImage::new([pixels; 2], rgba).ok() | |
| 312 | } | |
| 313 | ||
| 264 | 314 | pub fn text_colors() -> [[f32; 4]; 2] { |
| 265 | 315 | unsafe { |
| 266 | 316 | let space = NSColorSpace::sRGBColorSpace(); |
crates/snowbound/src/main.rs+705-230| ... | ... | @@ -1,16 +1,15 @@ |
| 1 | mod art; | |
| 1 | 2 | mod macos; |
| 2 | 3 | |
| 3 | 4 | use canvas::gpu::page::PageScene; |
| 4 | use canvas::interaction::{ | |
| 5 | Cursor, Key as PageKey, Modifiers, NamedKey as PageNamedKey, PageView, Request, Response, | |
| 6 | TextColors, accessibility, | |
| 7 | }; | |
| 5 | use canvas::interaction::{Cursor, PageView, Request, Response, TextColors, accessibility}; | |
| 8 | 6 | use canvas::{ |
| 9 | 7 | date::DateField, |
| 10 | 8 | document::TextDocument, |
| 11 | 9 | editor::{CanvasEditor, DEFAULT_OUTLINE_WIDTH, TextOutline}, |
| 12 | 10 | layout::TextEngine, |
| 13 | 11 | }; |
| 12 | use canvas::{gpu::tag_sources, outline::TagIcon}; | |
| 14 | 13 | use draw::Renderer; |
| 15 | 14 | use onestore::ExGuid; |
| 16 | 15 | use onestore::document::Format; |
| ... | ... | @@ -22,7 +21,7 @@ use std::{ |
| 22 | 21 | sync::Arc, |
| 23 | 22 | time::Instant, |
| 24 | 23 | }; |
| 25 | use ui::{Axis, Flags, Id, Spec, Theme, Ui, fill, fit, px}; | |
| 24 | use ui::{Axis, Flags, Id, Shape, Spec, Theme, Ui, children, fill, fit, px}; | |
| 26 | 25 | use winit::{ |
| 27 | 26 | application::ApplicationHandler, |
| 28 | 27 | dpi::{LogicalSize, PhysicalPosition, PhysicalSize}, |
| ... | ... | @@ -33,10 +32,15 @@ use winit::{ |
| 33 | 32 | window::{CursorIcon, Window, WindowId}, |
| 34 | 33 | }; |
| 35 | 34 | |
| 36 | /// Height of the window's top strip, which holds the traffic lights and section tabs. | |
| 37 | const STRIP: f32 = 38.0; | |
| 38 | /// Room the traffic lights take at the strip's leading edge. | |
| 35 | /// Height of the macOS title bar, which holds the traffic lights. | |
| 36 | const TITLE: f32 = 28.0; | |
| 37 | /// Room the traffic lights take at the title bar's leading edge. | |
| 39 | 38 | const LIGHTS: f32 = 78.0; |
| 39 | const TAB_ROW: f32 = 28.0; | |
| 40 | /// Width of the section colour around the page. | |
| 41 | const FRAME: f32 = 6.0; | |
| 42 | /// macOS rounds windows' corners by 10 pt; the page's corners share their centres. | |
| 43 | const ROUNDING: f32 = 10.0 - FRAME; | |
| 40 | 44 | const PAGE_LIST: f32 = 240.0; |
| 41 | 45 | |
| 42 | 46 | #[derive(Debug)] |
| ... | ... | @@ -48,6 +52,17 @@ enum UserEvent { |
| 48 | 52 | Sync, |
| 49 | 53 | /// Scripted input from `SNOWBOUND_REPLAY`. |
| 50 | 54 | Replay(Replay), |
| 55 | /// A page background finished rasterizing on its worker thread. | |
| 56 | Redraw, | |
| 57 | } | |
| 58 | ||
| 59 | /// Asks the event loop for a frame from any thread. | |
| 60 | struct Redraw(EventLoopProxy<UserEvent>); | |
| 61 | ||
| 62 | impl std::task::Wake for Redraw { | |
| 63 | fn wake(self: Arc<Self>) { | |
| 64 | let _ = self.0.send_event(UserEvent::Redraw); | |
| 65 | } | |
| 51 | 66 | } |
| 52 | 67 | |
| 53 | 68 | #[derive(Debug)] |
| ... | ... | @@ -55,6 +70,9 @@ enum Replay { |
| 55 | 70 | Input(ui::Event), |
| 56 | 71 | /// Paints the next frame into a PNG as well as the window. |
| 57 | 72 | Snapshot(PathBuf), |
| 73 | /// A frame during a wait, as a visible window's display would ask for. | |
| 74 | Tick, | |
| 75 | Appearance(winit::window::Theme), | |
| 58 | 76 | } |
| 59 | 77 | |
| 60 | 78 | impl From<accesskit_winit::Event> for UserEvent { |
| ... | ... | @@ -63,46 +81,17 @@ impl From<accesskit_winit::Event> for UserEvent { |
| 63 | 81 | } |
| 64 | 82 | } |
| 65 | 83 | |
| 66 | fn trace_input(event: &impl std::fmt::Debug) { | |
| 67 | if std::env::var_os("SNOWBOUND_TRACE_INPUT").is_some() { | |
| 68 | eprintln!("Input {:?}: {event:?}", std::time::SystemTime::now()); | |
| 84 | /// With `SNOWBOUND_PROFILE` set, prints how long a phase of the frame took since `start`. | |
| 85 | fn lap(phase: &str, start: Instant) { | |
| 86 | static PROFILE: std::sync::OnceLock<bool> = std::sync::OnceLock::new(); | |
| 87 | if *PROFILE.get_or_init(|| std::env::var_os("SNOWBOUND_PROFILE").is_some()) { | |
| 88 | eprintln!("profile\t{phase}\t{}", start.elapsed().as_micros()); | |
| 69 | 89 | } |
| 70 | 90 | } |
| 71 | 91 | |
| 72 | fn page_key(key: &Key) -> PageKey { | |
| 73 | match key { | |
| 74 | Key::Character(text) => PageKey::Character(text.to_string()), | |
| 75 | Key::Named(named) => PageKey::Named(match named { | |
| 76 | NamedKey::Escape => PageNamedKey::Escape, | |
| 77 | NamedKey::Tab => PageNamedKey::Tab, | |
| 78 | NamedKey::Space => PageNamedKey::Space, | |
| 79 | NamedKey::Enter => PageNamedKey::Enter, | |
| 80 | NamedKey::Backspace => PageNamedKey::Backspace, | |
| 81 | NamedKey::Delete => PageNamedKey::Delete, | |
| 82 | NamedKey::ArrowLeft => PageNamedKey::ArrowLeft, | |
| 83 | NamedKey::ArrowRight => PageNamedKey::ArrowRight, | |
| 84 | NamedKey::ArrowUp => PageNamedKey::ArrowUp, | |
| 85 | NamedKey::ArrowDown => PageNamedKey::ArrowDown, | |
| 86 | NamedKey::Home => PageNamedKey::Home, | |
| 87 | NamedKey::End => PageNamedKey::End, | |
| 88 | NamedKey::Alt | |
| 89 | | NamedKey::AltGraph | |
| 90 | | NamedKey::Control | |
| 91 | | NamedKey::Shift | |
| 92 | | NamedKey::Super | |
| 93 | | NamedKey::Meta => PageNamedKey::Modifier, | |
| 94 | _ => PageNamedKey::Other, | |
| 95 | }), | |
| 96 | _ => PageKey::Named(PageNamedKey::Other), | |
| 97 | } | |
| 98 | } | |
| 99 | ||
| 100 | fn page_modifiers(modifiers: ModifiersState) -> Modifiers { | |
| 101 | Modifiers { | |
| 102 | shift: modifiers.shift_key(), | |
| 103 | control: modifiers.control_key(), | |
| 104 | option: modifiers.alt_key(), | |
| 105 | command: modifiers.super_key(), | |
| 92 | fn trace_input(event: &impl std::fmt::Debug) { | |
| 93 | if std::env::var_os("SNOWBOUND_TRACE_INPUT").is_some() { | |
| 94 | eprintln!("Input {:?}: {event:?}", std::time::SystemTime::now()); | |
| 106 | 95 | } |
| 107 | 96 | } |
| 108 | 97 | |
| ... | ... | @@ -123,6 +112,8 @@ struct App { |
| 123 | 112 | proxy: EventLoopProxy<UserEvent>, |
| 124 | 113 | input: Option<Input>, |
| 125 | 114 | substitutes: Vec<PathBuf>, |
| 115 | /// Font files for the interface in place of the system's. | |
| 116 | interface_font: Vec<Vec<u8>>, | |
| 126 | 117 | state: Option<State>, |
| 127 | 118 | startup_error: Option<Box<dyn Error>>, |
| 128 | 119 | } |
| ... | ... | @@ -156,6 +147,8 @@ struct Tab { |
| 156 | 147 | |
| 157 | 148 | /// The sections the tabs offer. |
| 158 | 149 | struct Library { |
| 150 | /// The notebook's folder name, or the section file's. | |
| 151 | name: String, | |
| 159 | 152 | notebook: Option<notebook::session::Notebook>, |
| 160 | 153 | tabs: Vec<Tab>, |
| 161 | 154 | cache: PathBuf, |
| ... | ... | @@ -220,6 +213,8 @@ enum Command { |
| 220 | 213 | struct State { |
| 221 | 214 | window: Arc<Window>, |
| 222 | 215 | proxy: EventLoopProxy<UserEvent>, |
| 216 | /// Asks for a frame when a worker thread finishes something the page shows. | |
| 217 | redraw: std::task::Waker, | |
| 223 | 218 | instance: wgpu::Instance, |
| 224 | 219 | surface: wgpu::Surface<'static>, |
| 225 | 220 | config: wgpu::SurfaceConfiguration, |
| ... | ... | @@ -230,9 +225,15 @@ struct State { |
| 230 | 225 | session: Option<Session>, |
| 231 | 226 | /// Text filtering the page list. |
| 232 | 227 | filter: String, |
| 228 | /// Whether the page list is shown beside the page. | |
| 229 | pages_open: bool, | |
| 230 | /// The application's icon at the display's scale, for the title bar. | |
| 231 | app_icon: Option<draw::RasterImage>, | |
| 233 | 232 | commands: Vec<Command>, |
| 234 | 233 | /// The page changed during this frame, to be saved and announced after it. |
| 235 | 234 | changed: bool, |
| 235 | /// Only the view scrolled or zoomed during this frame, to be announced after it. | |
| 236 | moved: bool, | |
| 236 | 237 | /// Whether the page last heard it had keyboard focus. |
| 237 | 238 | page_focused: bool, |
| 238 | 239 | window_focused: bool, |
| ... | ... | @@ -260,6 +261,11 @@ fn page() -> Id { |
| 260 | 261 | Id::ROOT.child("page") |
| 261 | 262 | } |
| 262 | 263 | |
| 264 | /// The section's colour around the page and its tabs. | |
| 265 | fn frame() -> Id { | |
| 266 | Id::ROOT.child("frame") | |
| 267 | } | |
| 268 | ||
| 263 | 269 | /// The field filtering the page list. |
| 264 | 270 | fn filter() -> Id { |
| 265 | 271 | Id::ROOT.child("filter") |
| ... | ... | @@ -271,6 +277,7 @@ impl State { |
| 271 | 277 | proxy: EventLoopProxy<UserEvent>, |
| 272 | 278 | input: Input, |
| 273 | 279 | substitutes: &[PathBuf], |
| 280 | interface_font: Vec<Vec<u8>>, | |
| 274 | 281 | ) -> Result<Self, Box<dyn Error>> { |
| 275 | 282 | let window = Arc::new( |
| 276 | 283 | event_loop.create_window( |
| ... | ... | @@ -344,6 +351,7 @@ impl State { |
| 344 | 351 | } |
| 345 | 352 | Input::Section { file, title, cache } => { |
| 346 | 353 | let opened = Library { |
| 354 | name: file_name(file.parent().unwrap_or(&file)), | |
| 347 | 355 | notebook: None, |
| 348 | 356 | tabs: vec![Tab { |
| 349 | 357 | name: file |
| ... | ... | @@ -374,6 +382,7 @@ impl State { |
| 374 | 382 | return Err(format!("No readable sections in {}", root.display()).into()); |
| 375 | 383 | } |
| 376 | 384 | let opened = Library { |
| 385 | name: file_name(&root), | |
| 377 | 386 | notebook: Some(notebook), |
| 378 | 387 | tabs, |
| 379 | 388 | cache, |
| ... | ... | @@ -398,10 +407,20 @@ impl State { |
| 398 | 407 | window.set_ime_allowed(true); |
| 399 | 408 | window.request_redraw(); |
| 400 | 409 | eprintln!("Canvas GPU: {:?}; scale factor {dpr}", adapter.get_info()); |
| 401 | let mut ui = Ui::new(Theme::dark()); | |
| 410 | let mut ui = Ui::new( | |
| 411 | theme(window.theme().unwrap_or(winit::window::Theme::Dark)), | |
| 412 | macos::double_click_interval(), | |
| 413 | ); | |
| 414 | if !interface_font.is_empty() { | |
| 415 | let family = ui | |
| 416 | .use_fonts(interface_font) | |
| 417 | .ok_or("The interface font files hold no font")?; | |
| 418 | eprintln!("Using {family} for the interface"); | |
| 419 | } | |
| 402 | 420 | ui.set_focus(Some(page())); |
| 403 | 421 | let state = Self { |
| 404 | 422 | window, |
| 423 | redraw: Arc::new(Redraw(proxy.clone())).into(), | |
| 405 | 424 | proxy, |
| 406 | 425 | instance, |
| 407 | 426 | surface, |
| ... | ... | @@ -419,8 +438,11 @@ impl State { |
| 419 | 438 | library, |
| 420 | 439 | session, |
| 421 | 440 | filter: String::new(), |
| 441 | pages_open: true, | |
| 442 | app_icon: macos::app_icon((16.0 * dpr).round() as u32), | |
| 422 | 443 | commands: Vec::new(), |
| 423 | 444 | changed: false, |
| 445 | moved: false, | |
| 424 | 446 | page_focused: true, |
| 425 | 447 | window_focused: true, |
| 426 | 448 | pointer: [0.0; 2], |
| ... | ... | @@ -442,6 +464,7 @@ impl State { |
| 442 | 464 | /// Builds, lays out and paints one frame, then does what it asked for and asks for the |
| 443 | 465 | /// frame that shows the result. |
| 444 | 466 | fn frame(&mut self) -> Result<(), Box<dyn Error>> { |
| 467 | let start = Instant::now(); | |
| 445 | 468 | let size = self.window.inner_size(); |
| 446 | 469 | let scale = self.window.scale_factor() as f32; |
| 447 | 470 | self.ui.begin( |
| ... | ... | @@ -449,8 +472,10 @@ impl State { |
| 449 | 472 | scale, |
| 450 | 473 | Instant::now(), |
| 451 | 474 | ); |
| 452 | self.build()?; | |
| 475 | let (section, open_tab, open_page) = self.build()?; | |
| 453 | 476 | self.ui.end(); |
| 477 | self.edges(section, open_tab, open_page); | |
| 478 | lap("build", start); | |
| 454 | 479 | if let Some(rect) = self.ui.rect(page()) { |
| 455 | 480 | let size = [ |
| 456 | 481 | ((rect[2] - rect[0]) * scale).round() as u32, |
| ... | ... | @@ -476,6 +501,7 @@ impl State { |
| 476 | 501 | self.window.set_ime_allowed(ime); |
| 477 | 502 | } |
| 478 | 503 | self.draw()?; |
| 504 | lap("drawn", start); | |
| 479 | 505 | let commands = std::mem::take(&mut self.commands); |
| 480 | 506 | let follow = !commands.is_empty() || self.ui.wants_frame(); |
| 481 | 507 | for command in commands { |
| ... | ... | @@ -483,101 +509,100 @@ impl State { |
| 483 | 509 | } |
| 484 | 510 | if std::mem::take(&mut self.changed) { |
| 485 | 511 | self.after_edit()?; |
| 512 | } else if self.moved { | |
| 513 | self.after_move()?; | |
| 486 | 514 | } |
| 487 | if follow { | |
| 515 | self.moved = false; | |
| 516 | // A covered window shows nothing, so animations wait for it to be uncovered. | |
| 517 | if follow && !self.occluded { | |
| 488 | 518 | self.window.request_redraw(); |
| 489 | 519 | } |
| 520 | lap("frame", start); | |
| 490 | 521 | Ok(()) |
| 491 | 522 | } |
| 492 | 523 | |
| 493 | fn build(&mut self) -> Result<(), Box<dyn Error>> { | |
| 524 | /// Builds the frame's boxes and returns the section's colours as they ease, the open | |
| 525 | /// section tab and the open page's tab, for the edges drawn once they are laid out. | |
| 526 | fn build(&mut self) -> Result<(ui::Section, Id, Option<Id>), Box<dyn Error>> { | |
| 527 | let target = self.ui.theme.section(match (&self.library, &self.session) { | |
| 528 | (Some(library), Some(session)) => section_color(library.tabs[session.tab].color), | |
| 529 | _ => section_color(None), | |
| 530 | }); | |
| 531 | let mut ease = |part: &str, color: [f32; 4]| -> [f32; 4] { | |
| 532 | std::array::from_fn(|channel| { | |
| 533 | self.ui | |
| 534 | .animate(Id::ROOT.child((part, channel)), color[channel]) | |
| 535 | }) | |
| 536 | }; | |
| 537 | let section = ui::Section { | |
| 538 | frame: [ | |
| 539 | ease("top", target.frame[0]), | |
| 540 | ease("bottom", target.frame[1]), | |
| 541 | ], | |
| 542 | tab: ease("tab", target.tab), | |
| 543 | edge: ease("edge", target.edge), | |
| 544 | accent: ease("accent", target.accent), | |
| 545 | }; | |
| 546 | self.ui.theme.accent = section.accent; | |
| 494 | 547 | let theme = self.ui.theme.clone(); |
| 495 | self.ui.open_as( | |
| 496 | strip(), | |
| 548 | self.title_bar(&theme); | |
| 549 | self.toolbar(&theme)?; | |
| 550 | self.ui.open( | |
| 551 | "tabs", | |
| 497 | 552 | Spec { |
| 498 | flags: Flags::CLICKABLE, | |
| 499 | size: [fill(), px(STRIP)], | |
| 553 | size: [fill(), px(TAB_ROW)], | |
| 500 | 554 | fill: Some(theme.strip), |
| 501 | pad: [0.0, 8.0], | |
| 502 | gap: 2.0, | |
| 555 | pad: [FRAME, 0.0], | |
| 503 | 556 | ..Spec::default() |
| 504 | 557 | }, |
| 505 | 558 | ); |
| 506 | 559 | self.ui.leaf( |
| 507 | "lights", | |
| 560 | "corner", | |
| 508 | 561 | Spec { |
| 509 | size: [px(LIGHTS), px(1.0)], | |
| 562 | size: [px(ROUNDING), px(1.0)], | |
| 510 | 563 | ..Spec::default() |
| 511 | 564 | }, |
| 512 | 565 | ); |
| 513 | match (&self.library, &self.session) { | |
| 566 | let (clicked, open_tab) = match (&self.library, &self.session) { | |
| 514 | 567 | (Some(library), Some(session)) => { |
| 515 | for (index, tab) in library.tabs.iter().enumerate() { | |
| 516 | let active = index == session.tab; | |
| 517 | let id = self.ui.open( | |
| 518 | ("tab", index), | |
| 519 | Spec { | |
| 520 | flags: Flags::CLICKABLE, | |
| 521 | size: [fit(), px(STRIP - 8.0)], | |
| 522 | text: Some(&tab.name), | |
| 523 | color: Some(if active { theme.text } else { theme.text_dim }), | |
| 524 | fill: Some(if active { theme.base } else { theme.strip }), | |
| 525 | hover_fill: (!active).then_some(theme.chip), | |
| 526 | radius: 6.0, | |
| 527 | pad: [14.0 + 14.0, 0.0], | |
| 528 | center: true, | |
| 529 | ..Spec::default() | |
| 530 | }, | |
| 531 | ); | |
| 532 | let swatch = tab.color.map_or(theme.text_dim, canvas::gpu::colorref); | |
| 533 | let middle = (STRIP - 8.0) / 2.0; | |
| 534 | self.ui | |
| 535 | .mark([12.0, middle - 4.0, 20.0, middle + 4.0], swatch); | |
| 536 | self.ui.close(); | |
| 537 | if self.ui.signal(id).clicked && !active { | |
| 538 | self.commands.push(Command::OpenSection(index)); | |
| 539 | } | |
| 540 | } | |
| 541 | } | |
| 542 | _ => { | |
| 543 | self.ui.leaf( | |
| 544 | "tab", | |
| 545 | Spec { | |
| 546 | size: [fit(), px(STRIP - 8.0)], | |
| 547 | text: Some("Temporary page"), | |
| 548 | fill: Some(theme.base), | |
| 549 | radius: 6.0, | |
| 550 | pad: [16.0, 0.0], | |
| 551 | ..Spec::default() | |
| 552 | }, | |
| 553 | ); | |
| 568 | let tabs: Vec<_> = library | |
| 569 | .tabs | |
| 570 | .iter() | |
| 571 | .map(|tab| (tab.name.as_str(), section_color(tab.color))) | |
| 572 | .collect(); | |
| 573 | ui::shell::section_tabs( | |
| 574 | &mut self.ui, | |
| 575 | "sections", | |
| 576 | &tabs, | |
| 577 | session.tab, | |
| 578 | &section, | |
| 579 | TAB_ROW, | |
| 580 | ) | |
| 554 | 581 | } |
| 582 | _ => ui::shell::section_tabs( | |
| 583 | &mut self.ui, | |
| 584 | "sections", | |
| 585 | &[("Temporary page", section_color(None))], | |
| 586 | 0, | |
| 587 | &section, | |
| 588 | TAB_ROW, | |
| 589 | ), | |
| 590 | }; | |
| 591 | if let Some(index) = clicked { | |
| 592 | self.commands.push(Command::OpenSection(index)); | |
| 555 | 593 | } |
| 556 | self.ui.leaf( | |
| 557 | "space", | |
| 558 | Spec { | |
| 559 | size: [fill(), px(1.0)], | |
| 560 | ..Spec::default() | |
| 561 | }, | |
| 562 | ); | |
| 563 | if let Some(session) = &self.session { | |
| 564 | self.ui.leaf( | |
| 565 | "status", | |
| 566 | Spec { | |
| 567 | size: [fit(), px(STRIP - 8.0)], | |
| 568 | text: Some(session.status), | |
| 569 | color: Some(theme.text_dim), | |
| 570 | pad: [14.0, 0.0], | |
| 571 | ..Spec::default() | |
| 572 | }, | |
| 573 | ); | |
| 594 | if self.session.is_some() { | |
| 595 | self.page_tools(&theme); | |
| 574 | 596 | } |
| 575 | 597 | self.ui.close(); |
| 576 | 598 | |
| 577 | self.ui.open( | |
| 578 | "body", | |
| 599 | self.ui.open_as( | |
| 600 | frame(), | |
| 579 | 601 | Spec { |
| 580 | 602 | size: [fill(), fill()], |
| 603 | fill: Some(section.frame[0]), | |
| 604 | gradient: Some(section.frame[1]), | |
| 605 | pad: [FRAME, FRAME], | |
| 581 | 606 | ..Spec::default() |
| 582 | 607 | }, |
| 583 | 608 | ); |
| ... | ... | @@ -601,7 +626,7 @@ impl State { |
| 601 | 626 | Spec { |
| 602 | 627 | flags: Flags::CUSTOM | Flags::FOCUSABLE, |
| 603 | 628 | size: [fill(), fill()], |
| 604 | fill: Some([1.0; 4]), | |
| 629 | fill: Some(theme.paper), | |
| 605 | 630 | ..Spec::default() |
| 606 | 631 | }, |
| 607 | 632 | ); |
| ... | ... | @@ -630,37 +655,124 @@ impl State { |
| 630 | 655 | } |
| 631 | 656 | self.page_events(signal.events)?; |
| 632 | 657 | self.ui.close(); |
| 633 | if self.session.is_some() { | |
| 634 | self.ui.leaf( | |
| 635 | "separator", | |
| 636 | Spec { | |
| 637 | size: [px(1.0), fill()], | |
| 638 | fill: Some(theme.separator), | |
| 639 | ..Spec::default() | |
| 640 | }, | |
| 641 | ); | |
| 642 | self.page_list(&theme); | |
| 643 | } | |
| 658 | let open_page = self.page_list(&theme, &section); | |
| 644 | 659 | self.ui.close(); |
| 645 | Ok(()) | |
| 660 | Ok((section, open_tab, open_page)) | |
| 646 | 661 | } |
| 647 | 662 | |
| 648 | fn conflict_bar(&mut self, theme: &Theme) { | |
| 649 | self.ui.open( | |
| 650 | "conflict", | |
| 663 | /// Borders the open section tab and the frame's top, and rounds and borders the page | |
| 664 | /// together with the open page's tab, joined where they meet. | |
| 665 | fn edges(&mut self, section: ui::Section, open_tab: Id, open_page: Option<Id>) { | |
| 666 | let panel = self.ui.rect(frame().child("panel")); | |
| 667 | let (Some(frame), Some(page)) = (self.ui.rect(frame()), self.ui.rect(page())) else { | |
| 668 | return; | |
| 669 | }; | |
| 670 | let [left, top, right, bottom] = page; | |
| 671 | let tab = open_page | |
| 672 | .and_then(|id| self.ui.rect(id)) | |
| 673 | .zip(panel) | |
| 674 | .and_then(|(row, panel)| { | |
| 675 | let [start, end] = [ | |
| 676 | row[1].max(panel[1]).max(top), | |
| 677 | row[3].min(panel[3]).min(bottom), | |
| 678 | ]; | |
| 679 | let reach = row[2].min(panel[2]); | |
| 680 | (reach - right > 2.0 * ROUNDING && end - start > 2.0 * ROUNDING).then(|| { | |
| 681 | // A tab too near the page's corner joins it along the edge. | |
| 682 | let start = if start < top + 2.0 * ROUNDING { | |
| 683 | top | |
| 684 | } else { | |
| 685 | start | |
| 686 | }; | |
| 687 | let end = if end > bottom - 2.0 * ROUNDING { | |
| 688 | bottom | |
| 689 | } else { | |
| 690 | end | |
| 691 | }; | |
| 692 | [start, end, reach] | |
| 693 | }) | |
| 694 | }); | |
| 695 | let mut outline = vec![([left, top], ROUNDING)]; | |
| 696 | match tab { | |
| 697 | Some([start, end, reach]) => { | |
| 698 | if start > top { | |
| 699 | outline.extend([([right, top], ROUNDING), ([right, start], ROUNDING)]); | |
| 700 | } | |
| 701 | outline.extend([([reach, start], ROUNDING), ([reach, end], ROUNDING)]); | |
| 702 | if end < bottom { | |
| 703 | outline.extend([([right, end], ROUNDING), ([right, bottom], ROUNDING)]); | |
| 704 | } | |
| 705 | } | |
| 706 | None => outline.extend([([right, top], ROUNDING), ([right, bottom], ROUNDING)]), | |
| 707 | } | |
| 708 | outline.push(([left, bottom], ROUNDING)); | |
| 709 | let height = (frame[3] - frame[1]).max(1.0); | |
| 710 | let paper = self.ui.theme.paper; | |
| 711 | self.ui.round_corners(&outline, paper, |y| { | |
| 712 | ui::mix(section.frame[0], section.frame[1], (y - frame[1]) / height) | |
| 713 | }); | |
| 714 | self.ui.border(&outline, true, section.edge); | |
| 715 | // The frame's top corners share the page's centres; its top edge breaks where the | |
| 716 | // open tab stands on it. | |
| 717 | let [start, end] = [frame[0], frame[2]]; | |
| 718 | let outer = FRAME + ROUNDING; | |
| 719 | let strip = self.ui.theme.strip; | |
| 720 | self.ui.round_corners( | |
| 721 | &[ | |
| 722 | ([start, frame[1]], outer), | |
| 723 | ([end, frame[1]], outer), | |
| 724 | ([end, frame[3]], 0.0), | |
| 725 | ([start, frame[3]], 0.0), | |
| 726 | ], | |
| 727 | paper, | |
| 728 | |_| strip, | |
| 729 | ); | |
| 730 | let [foot, toe] = self.ui.rect(open_tab).map_or([end; 2], ui::shell::tab_base); | |
| 731 | for edge in [ | |
| 732 | [ | |
| 733 | ([start, frame[1] + outer], 0.0), | |
| 734 | ([start, frame[1]], outer), | |
| 735 | ([foot, frame[1]], 0.0), | |
| 736 | ], | |
| 737 | [ | |
| 738 | ([toe, frame[1]], 0.0), | |
| 739 | ([end, frame[1]], outer), | |
| 740 | ([end, frame[1] + outer], 0.0), | |
| 741 | ], | |
| 742 | ] { | |
| 743 | self.ui.border(&edge, false, section.edge); | |
| 744 | } | |
| 745 | } | |
| 746 | ||
| 747 | /// The window's title bar beside the traffic lights: the application's icon, the | |
| 748 | /// notebook's name and the saving status. | |
| 749 | fn title_bar(&mut self, theme: &Theme) { | |
| 750 | self.ui.open_as( | |
| 751 | strip(), | |
| 651 | 752 | Spec { |
| 652 | size: [fill(), px(40.0)], | |
| 653 | fill: Some(theme.panel), | |
| 654 | pad: [12.0, 7.0], | |
| 655 | gap: 8.0, | |
| 753 | flags: Flags::CLICKABLE, | |
| 754 | size: [fill(), px(TITLE)], | |
| 755 | fill: Some(theme.strip), | |
| 656 | 756 | ..Spec::default() |
| 657 | 757 | }, |
| 658 | 758 | ); |
| 659 | 759 | self.ui.leaf( |
| 660 | "message", | |
| 760 | "lights", | |
| 661 | 761 | Spec { |
| 662 | size: [fit(), px(26.0)], | |
| 663 | text: Some("This page also changed on another computer."), | |
| 762 | size: [px(LIGHTS), px(1.0)], | |
| 763 | ..Spec::default() | |
| 764 | }, | |
| 765 | ); | |
| 766 | self.ui.leaf( | |
| 767 | "notebook", | |
| 768 | Spec { | |
| 769 | size: [fit(), px(TITLE)], | |
| 770 | image: self.app_icon.as_ref(), | |
| 771 | text: Some( | |
| 772 | self.library | |
| 773 | .as_ref() | |
| 774 | .map_or("Temporary page", |library| &library.name), | |
| 775 | ), | |
| 664 | 776 | ..Spec::default() |
| 665 | 777 | }, |
| 666 | 778 | ); |
| ... | ... | @@ -671,122 +783,341 @@ impl State { |
| 671 | 783 | ..Spec::default() |
| 672 | 784 | }, |
| 673 | 785 | ); |
| 674 | if ui::button(&mut self.ui, "mine", "Keep mine").clicked { | |
| 675 | self.commands.push(Command::Resolve { keep_mine: true }); | |
| 676 | } | |
| 677 | if ui::button(&mut self.ui, "theirs", "Keep theirs").clicked { | |
| 678 | self.commands.push(Command::Resolve { keep_mine: false }); | |
| 786 | if let Some(session) = &self.session { | |
| 787 | self.ui.leaf( | |
| 788 | "status", | |
| 789 | Spec { | |
| 790 | size: [fit(), px(TITLE)], | |
| 791 | text: Some(session.status), | |
| 792 | color: Some(theme.text_dim), | |
| 793 | pad: [12.0, 0.0], | |
| 794 | ..Spec::default() | |
| 795 | }, | |
| 796 | ); | |
| 679 | 797 | } |
| 680 | 798 | self.ui.close(); |
| 681 | 799 | } |
| 682 | 800 | |
| 683 | fn page_list(&mut self, theme: &Theme) { | |
| 801 | /// Two rows of formatting, tag, insert and zoom buttons, grouped as OneNote's Home | |
| 802 | /// ribbon groups them. | |
| 803 | fn toolbar(&mut self, theme: &Theme) -> Result<(), Box<dyn Error>> { | |
| 804 | let ui = &mut self.ui; | |
| 805 | let text = theme.text; | |
| 806 | ui.open( | |
| 807 | "toolbar", | |
| 808 | Spec { | |
| 809 | size: [fill(), children()], | |
| 810 | fill: Some(theme.strip), | |
| 811 | pad: [8.0, 3.0], | |
| 812 | gap: 6.0, | |
| 813 | ..Spec::default() | |
| 814 | }, | |
| 815 | ); | |
| 816 | group(ui, "history", |ui| { | |
| 817 | ui::shell::tool_button(ui, "undo", art::UNDO, text); | |
| 818 | ui::shell::tool_button(ui, "redo", art::REDO, text); | |
| 819 | }); | |
| 820 | divider(ui, "history", theme); | |
| 821 | group(ui, "text", |ui| { | |
| 822 | row(ui, 0, |ui| { | |
| 823 | ui::shell::combo(ui, "font", "Calibri", 120.0); | |
| 824 | ui::shell::combo(ui, "size", "11", 44.0); | |
| 825 | ui::shell::split_button(ui, "bullets", art::BULLETS, None); | |
| 826 | ui::shell::split_button(ui, "numbering", art::NUMBERING, None); | |
| 827 | ui::shell::tool_button(ui, "clear", art::CLEAR_FORMATTING, text); | |
| 828 | }); | |
| 829 | row(ui, 1, |ui| { | |
| 830 | for (part, icon) in [ | |
| 831 | ("bold", art::BOLD), | |
| 832 | ("italic", art::ITALIC), | |
| 833 | ("underline", art::UNDERLINE), | |
| 834 | ("strikethrough", art::STRIKETHROUGH), | |
| 835 | ] { | |
| 836 | ui::shell::tool_button(ui, part, icon, text); | |
| 837 | } | |
| 838 | ui::shell::split_button(ui, "script", art::SUBSCRIPT, None); | |
| 839 | ui::shell::split_button( | |
| 840 | ui, | |
| 841 | "highlight", | |
| 842 | art::HIGHLIGHTER, | |
| 843 | Some(draw::srgb(0xff, 0xff, 0x00)), | |
| 844 | ); | |
| 845 | ui::shell::split_button( | |
| 846 | ui, | |
| 847 | "color", | |
| 848 | art::FONT_COLOR, | |
| 849 | Some(draw::srgb(0xe8, 0x3a, 0x30)), | |
| 850 | ); | |
| 851 | ui::shell::tool_button(ui, "outdent", art::OUTDENT, text); | |
| 852 | ui::shell::tool_button(ui, "indent", art::INDENT, text); | |
| 853 | ui::shell::split_button(ui, "align", art::ALIGN_LEFT, None); | |
| 854 | }); | |
| 855 | }); | |
| 856 | divider(ui, "text", theme); | |
| 857 | let tags = [ | |
| 858 | tag_sources(TagIcon::CheckBox { checked: false }), | |
| 859 | art::TAG_STAR, | |
| 860 | tag_sources(TagIcon::Question), | |
| 861 | art::TAG_REMEMBER, | |
| 862 | art::TAG_DEFINITION, | |
| 863 | art::TAG_HIGHLIGHT, | |
| 864 | art::TAG_CONTACT, | |
| 865 | art::TAG_ADDRESS, | |
| 866 | art::TAG_PHONE, | |
| 867 | ]; | |
| 868 | group(ui, "tags", |ui| { | |
| 869 | for (index, tags) in tags.chunks(5).enumerate() { | |
| 870 | row(ui, index, |ui| { | |
| 871 | for (column, icon) in tags.iter().enumerate() { | |
| 872 | ui::shell::tool_button(ui, column, icon, [1.0; 4]); | |
| 873 | } | |
| 874 | if index == 1 { | |
| 875 | ui::shell::tool_button(ui, "more", ui::shell::CHEVRON, text); | |
| 876 | } | |
| 877 | }); | |
| 878 | } | |
| 879 | }); | |
| 880 | divider(ui, "tags", theme); | |
| 881 | group(ui, "insert", |ui| { | |
| 882 | row(ui, 0, |ui| { | |
| 883 | for (part, icon) in [ | |
| 884 | ("table", art::TABLE), | |
| 885 | ("picture", art::PICTURE), | |
| 886 | ("file", art::ATTACHMENT), | |
| 887 | ("link", art::LINK), | |
| 888 | ] { | |
| 889 | ui::shell::tool_button(ui, part, icon, text); | |
| 890 | } | |
| 891 | }); | |
| 892 | row(ui, 1, |ui| { | |
| 893 | for (part, icon) in [ | |
| 894 | ("date", art::CALENDAR), | |
| 895 | ("time", art::CLOCK), | |
| 896 | ("equation", art::EQUATION), | |
| 897 | ] { | |
| 898 | ui::shell::tool_button(ui, part, icon, text); | |
| 899 | } | |
| 900 | }); | |
| 901 | }); | |
| 902 | ui.leaf( | |
| 903 | "space", | |
| 904 | Spec { | |
| 905 | size: [fill(), px(1.0)], | |
| 906 | ..Spec::default() | |
| 907 | }, | |
| 908 | ); | |
| 909 | let zoom = self.view.zoom(); | |
| 910 | let label = format!("{:.0}%", zoom * 100.0); | |
| 911 | let mut chosen = None; | |
| 912 | group(ui, "zoom", |ui| { | |
| 913 | row(ui, 0, |ui| { | |
| 914 | if ui::shell::tool_button(ui, "out", art::ZOOM_OUT, text).clicked { | |
| 915 | chosen = Some(zoom / 1.1); | |
| 916 | } | |
| 917 | let level = ui.leaf( | |
| 918 | "level", | |
| 919 | Spec { | |
| 920 | flags: Flags::CLICKABLE, | |
| 921 | size: [px(44.0), px(ui::shell::TOOL)], | |
| 922 | text: Some(&label), | |
| 923 | hover_fill: Some(theme.hover()), | |
| 924 | radius: 4.0, | |
| 925 | center: true, | |
| 926 | ..Spec::default() | |
| 927 | }, | |
| 928 | ); | |
| 929 | if level.clicked { | |
| 930 | chosen = Some(1.0); | |
| 931 | } | |
| 932 | if ui::shell::tool_button(ui, "in", art::ZOOM_IN, text).clicked { | |
| 933 | chosen = Some(zoom * 1.1); | |
| 934 | } | |
| 935 | }); | |
| 936 | }); | |
| 937 | ui.close(); | |
| 938 | if let Some(zoom) = chosen { | |
| 939 | let response = self.view.set_zoom(zoom)?; | |
| 940 | self.respond(response); | |
| 941 | } | |
| 942 | Ok(()) | |
| 943 | } | |
| 944 | ||
| 945 | /// The search box and the page list's buttons, above the list. | |
| 946 | fn page_tools(&mut self, theme: &Theme) { | |
| 684 | 947 | let Some(session) = &self.session else { |
| 685 | 948 | return; |
| 686 | 949 | }; |
| 687 | 950 | self.ui.open( |
| 688 | "panel", | |
| 951 | "tools", | |
| 689 | 952 | Spec { |
| 690 | axis: Axis::Y, | |
| 691 | size: [px(PAGE_LIST), fill()], | |
| 692 | fill: Some(theme.panel), | |
| 953 | size: [px(PAGE_LIST), px(TAB_ROW)], | |
| 954 | pad: [0.0, (TAB_ROW - ui::shell::TOOL) / 2.0], | |
| 955 | gap: 3.0, | |
| 693 | 956 | ..Spec::default() |
| 694 | 957 | }, |
| 695 | 958 | ); |
| 959 | let focused = self.ui.focused() == Some(filter()); | |
| 696 | 960 | self.ui.open( |
| 697 | "pages", | |
| 961 | "search", | |
| 698 | 962 | Spec { |
| 699 | flags: Flags::SCROLL | Flags::CLIP, | |
| 700 | axis: Axis::Y, | |
| 701 | size: [fill(), fill()], | |
| 702 | pad: [6.0, 6.0], | |
| 703 | gap: 1.0, | |
| 963 | size: [fill(), px(ui::shell::TOOL)], | |
| 964 | fill: Some(theme.base), | |
| 965 | border: Some(if focused { theme.accent } else { theme.chip }), | |
| 966 | radius: 4.0, | |
| 967 | pad: [6.0, 0.0], | |
| 704 | 968 | ..Spec::default() |
| 705 | 969 | }, |
| 706 | 970 | ); |
| 707 | let query = self.filter.to_lowercase(); | |
| 708 | let mut first = None; | |
| 709 | for (space, title, level) in &session.pages { | |
| 710 | if !title.to_lowercase().contains(&query) { | |
| 711 | continue; | |
| 712 | } | |
| 713 | first.get_or_insert(*space); | |
| 714 | let selected = *space == session.space; | |
| 715 | let signal = self.ui.leaf( | |
| 716 | space, | |
| 717 | Spec { | |
| 718 | flags: Flags::CLICKABLE, | |
| 719 | size: [fill(), px(26.0)], | |
| 720 | text: Some(if title.is_empty() { | |
| 721 | "Untitled page" | |
| 722 | } else { | |
| 723 | title | |
| 724 | }), | |
| 725 | color: Some(if selected || !title.is_empty() { | |
| 726 | theme.text | |
| 727 | } else { | |
| 728 | theme.text_dim | |
| 729 | }), | |
| 730 | fill: selected.then_some(theme.accent), | |
| 731 | hover_fill: (!selected).then_some(theme.hover), | |
| 732 | hover_border: (!selected).then_some(theme.accent), | |
| 733 | radius: 4.0, | |
| 734 | pad: [10.0 + 16.0 * level.saturating_sub(1) as f32, 0.0], | |
| 735 | ..Spec::default() | |
| 736 | }, | |
| 737 | ); | |
| 738 | if signal.clicked && !selected { | |
| 739 | self.commands.push(Command::OpenPage(*space)); | |
| 740 | } | |
| 741 | } | |
| 742 | self.ui.close(); | |
| 743 | let placeholder = format!("Filter {} pages…", session.pages.len()); | |
| 744 | self.ui.open( | |
| 745 | "footer", | |
| 971 | self.ui.leaf( | |
| 972 | "icon", | |
| 746 | 973 | Spec { |
| 747 | size: [fill(), px(38.0)], | |
| 748 | pad: [6.0, 6.0], | |
| 974 | size: [fit(), px(ui::shell::TOOL)], | |
| 975 | icon: Some(art::SEARCH), | |
| 976 | color: Some(theme.text_dim), | |
| 749 | 977 | ..Spec::default() |
| 750 | 978 | }, |
| 751 | 979 | ); |
| 752 | let before = self.filter.clone(); | |
| 753 | 980 | let signal = ui::text_field( |
| 754 | 981 | &mut self.ui, |
| 755 | 982 | filter(), |
| 756 | 983 | &mut self.filter, |
| 757 | &placeholder, | |
| 984 | "Search pages", | |
| 758 | 985 | Spec { |
| 759 | size: [fill(), px(26.0)], | |
| 760 | fill: Some(theme.base), | |
| 761 | border: Some(theme.separator), | |
| 762 | radius: 4.0, | |
| 763 | pad: [8.0, 0.0], | |
| 986 | size: [fill(), px(ui::shell::TOOL)], | |
| 987 | pad: [6.0, 0.0], | |
| 764 | 988 | ..Spec::default() |
| 765 | 989 | }, |
| 766 | 990 | ); |
| 767 | // The list above was built with the filter as it stood. | |
| 768 | if self.filter != before { | |
| 769 | self.window.request_redraw(); | |
| 991 | if !self.filter.is_empty() | |
| 992 | && ui::shell::tool_button(&mut self.ui, "clear", art::CLOSE, theme.text_dim).clicked | |
| 993 | { | |
| 994 | self.filter.clear(); | |
| 770 | 995 | } |
| 996 | self.ui.close(); | |
| 771 | 997 | for event in &signal.events { |
| 772 | 998 | if let ui::Event::Key { |
| 773 | 999 | key: Key::Named(key @ (NamedKey::Escape | NamedKey::Enter)), |
| 774 | 1000 | .. |
| 775 | 1001 | } = event |
| 776 | 1002 | { |
| 1003 | let first = matching(&session.pages, &self.filter).next(); | |
| 777 | 1004 | if *key == NamedKey::Enter |
| 778 | && let Some(space) = first.filter(|space| *space != session.space) | |
| 1005 | && let Some((space, ..)) = first.filter(|(space, ..)| *space != session.space) | |
| 779 | 1006 | { |
| 780 | self.commands.push(Command::OpenPage(space)); | |
| 1007 | self.commands.push(Command::OpenPage(*space)); | |
| 781 | 1008 | } |
| 782 | 1009 | self.filter.clear(); |
| 783 | 1010 | self.ui.set_focus(Some(page())); |
| 784 | 1011 | } |
| 785 | 1012 | } |
| 1013 | ui::shell::tool_button(&mut self.ui, "new", art::NEW_PAGE, theme.text); | |
| 1014 | let toggle = if self.pages_open { | |
| 1015 | art::SIDEBAR_COLLAPSE | |
| 1016 | } else { | |
| 1017 | art::SIDEBAR_EXPAND | |
| 1018 | }; | |
| 1019 | if ui::shell::tool_button(&mut self.ui, "toggle", toggle, theme.text).clicked { | |
| 1020 | self.pages_open = !self.pages_open; | |
| 1021 | } | |
| 786 | 1022 | self.ui.close(); |
| 1023 | } | |
| 1024 | ||
| 1025 | fn conflict_bar(&mut self, theme: &Theme) { | |
| 1026 | self.ui.open( | |
| 1027 | "conflict", | |
| 1028 | Spec { | |
| 1029 | size: [fill(), px(40.0)], | |
| 1030 | fill: Some(theme.panel), | |
| 1031 | pad: [12.0, 7.0], | |
| 1032 | gap: 8.0, | |
| 1033 | ..Spec::default() | |
| 1034 | }, | |
| 1035 | ); | |
| 1036 | self.ui.leaf( | |
| 1037 | "message", | |
| 1038 | Spec { | |
| 1039 | size: [fit(), px(26.0)], | |
| 1040 | text: Some("This page also changed on another computer."), | |
| 1041 | ..Spec::default() | |
| 1042 | }, | |
| 1043 | ); | |
| 1044 | self.ui.leaf( | |
| 1045 | "space", | |
| 1046 | Spec { | |
| 1047 | size: [fill(), px(1.0)], | |
| 1048 | ..Spec::default() | |
| 1049 | }, | |
| 1050 | ); | |
| 1051 | if ui::button(&mut self.ui, "mine", "Keep mine").clicked { | |
| 1052 | self.commands.push(Command::Resolve { keep_mine: true }); | |
| 1053 | } | |
| 1054 | if ui::button(&mut self.ui, "theirs", "Keep theirs").clicked { | |
| 1055 | self.commands.push(Command::Resolve { keep_mine: false }); | |
| 1056 | } | |
| 787 | 1057 | self.ui.close(); |
| 788 | 1058 | } |
| 789 | 1059 | |
| 1060 | /// The section's pages as tabs down the frame's right side, returning the open page's | |
| 1061 | /// tab, which is the page's colour and joins it. | |
| 1062 | fn page_list(&mut self, theme: &Theme, section: &ui::Section) -> Option<Id> { | |
| 1063 | let session = self.session.as_ref()?; | |
| 1064 | let panel = self.ui.id("panel"); | |
| 1065 | let width = self | |
| 1066 | .ui | |
| 1067 | .animate(panel, if self.pages_open { PAGE_LIST } else { 0.0 }); | |
| 1068 | self.ui.open( | |
| 1069 | "panel", | |
| 1070 | Spec { | |
| 1071 | flags: Flags::SCROLL | Flags::CLIP, | |
| 1072 | axis: Axis::Y, | |
| 1073 | size: [px(width), fill()], | |
| 1074 | gap: 1.0, | |
| 1075 | ..Spec::default() | |
| 1076 | }, | |
| 1077 | ); | |
| 1078 | let mut open = None; | |
| 1079 | for (space, title, level) in matching(&session.pages, &self.filter) { | |
| 1080 | let selected = *space == session.space; | |
| 1081 | let spec = Spec { | |
| 1082 | flags: Flags::CLICKABLE, | |
| 1083 | size: [px(PAGE_LIST), px(26.0)], | |
| 1084 | text: Some(if title.is_empty() { | |
| 1085 | "Untitled page" | |
| 1086 | } else { | |
| 1087 | title | |
| 1088 | }), | |
| 1089 | pad: [10.0 + 16.0 * level.saturating_sub(1) as f32, 0.0], | |
| 1090 | ..Spec::default() | |
| 1091 | }; | |
| 1092 | let spec = if selected { | |
| 1093 | open = Some(self.ui.id(space)); | |
| 1094 | Spec { | |
| 1095 | color: Some(theme.paper_ink), | |
| 1096 | fill: Some(theme.paper), | |
| 1097 | ..spec | |
| 1098 | } | |
| 1099 | } else { | |
| 1100 | Spec { | |
| 1101 | color: Some(if title.is_empty() { | |
| 1102 | ui::mix(theme.ink, section.tab, 0.5) | |
| 1103 | } else { | |
| 1104 | theme.ink | |
| 1105 | }), | |
| 1106 | fill: Some(section.tab), | |
| 1107 | hover_fill: Some(section.frame[0]), | |
| 1108 | radius: ROUNDING, | |
| 1109 | shape: Shape::Trailing, | |
| 1110 | ..spec | |
| 1111 | } | |
| 1112 | }; | |
| 1113 | if self.ui.leaf(space, spec).clicked && !selected { | |
| 1114 | self.commands.push(Command::OpenPage(*space)); | |
| 1115 | } | |
| 1116 | } | |
| 1117 | self.ui.close(); | |
| 1118 | open | |
| 1119 | } | |
| 1120 | ||
| 790 | 1121 | /// Hands the page the events routed to its box, in its device pixels. |
| 791 | 1122 | fn page_events(&mut self, events: Vec<ui::Event>) -> Result<(), Box<dyn Error>> { |
| 792 | 1123 | let scale = self.ui.scale(); |
| ... | ... | @@ -822,13 +1153,13 @@ impl State { |
| 822 | 1153 | self.ui.set_focus(Some(filter())); |
| 823 | 1154 | continue; |
| 824 | 1155 | } |
| 825 | self.view.key(&page_key(&key), text.as_deref())? | |
| 1156 | self.view.key(&ui::edit_key(&key), text.as_deref())? | |
| 826 | 1157 | } |
| 827 | 1158 | ui::Event::Ime(Ime::Preedit(text, cursor)) => self.view.compose(text, cursor)?, |
| 828 | 1159 | ui::Event::Ime(Ime::Commit(text)) => self.view.commit_text(text)?, |
| 829 | 1160 | ui::Event::Ime(Ime::Disabled) => self.view.cancel_composition()?, |
| 830 | 1161 | ui::Event::Modifiers(modifiers) => { |
| 831 | self.view.modifiers_changed(page_modifiers(modifiers))? | |
| 1162 | self.view.modifiers_changed(ui::edit_modifiers(modifiers))? | |
| 832 | 1163 | } |
| 833 | 1164 | }; |
| 834 | 1165 | self.respond(response); |
| ... | ... | @@ -842,6 +1173,7 @@ impl State { |
| 842 | 1173 | /// Notes a page change for after the frame and queues what the page asked of the host. |
| 843 | 1174 | fn respond(&mut self, response: Response) { |
| 844 | 1175 | self.changed |= response.changed; |
| 1176 | self.moved |= response.moved; | |
| 845 | 1177 | if let Some(request) = response.request { |
| 846 | 1178 | self.commands.push(Command::Page(request)); |
| 847 | 1179 | } |
| ... | ... | @@ -892,6 +1224,16 @@ impl State { |
| 892 | 1224 | |
| 893 | 1225 | /// Follows an edit: the input method's position, accessibility, saving and the title. |
| 894 | 1226 | fn after_edit(&mut self) -> Result<(), Box<dyn Error>> { |
| 1227 | self.after_move()?; | |
| 1228 | let start = Instant::now(); | |
| 1229 | self.persist()?; | |
| 1230 | lap("save", start); | |
| 1231 | self.title(); | |
| 1232 | Ok(()) | |
| 1233 | } | |
| 1234 | ||
| 1235 | /// Follows the view moving: the input method's position and accessibility. | |
| 1236 | fn after_move(&mut self) -> Result<(), Box<dyn Error>> { | |
| 895 | 1237 | let scale = self.ui.scale(); |
| 896 | 1238 | let corner = self.ui.rect(page()).unwrap_or_default(); |
| 897 | 1239 | let [x0, y0, x1, y1] = self.view.caret_area()?; |
| ... | ... | @@ -899,9 +1241,9 @@ impl State { |
| 899 | 1241 | PhysicalPosition::new(x0 + corner[0] * scale, y0 + corner[1] * scale), |
| 900 | 1242 | PhysicalSize::new(x1 - x0, y1 - y0), |
| 901 | 1243 | ); |
| 1244 | let start = Instant::now(); | |
| 902 | 1245 | self.update_accessibility()?; |
| 903 | self.persist()?; | |
| 904 | self.title(); | |
| 1246 | lap("accessibility", start); | |
| 905 | 1247 | Ok(()) |
| 906 | 1248 | } |
| 907 | 1249 | |
| ... | ... | @@ -1095,20 +1437,13 @@ impl State { |
| 1095 | 1437 | self.access_adapter.update_if_active(|| { |
| 1096 | 1438 | match self.accessibility.update( |
| 1097 | 1439 | &view.editor, |
| 1440 | view.scene.as_ref(), | |
| 1098 | 1441 | viewport, |
| 1099 | 1442 | &self.window.title(), |
| 1100 | 1443 | view.outline_preview(), |
| 1444 | view.object_focus().and_then(|focus| focus.read_only()), | |
| 1101 | 1445 | ) { |
| 1102 | Ok(mut update) => { | |
| 1103 | self.accessibility.append_page_fields( | |
| 1104 | &mut update, | |
| 1105 | view.scene.as_ref(), | |
| 1106 | &view.editor, | |
| 1107 | viewport, | |
| 1108 | view.object_focus().and_then(|focus| focus.read_only()), | |
| 1109 | ); | |
| 1110 | update | |
| 1111 | } | |
| 1446 | Ok(update) => update, | |
| 1112 | 1447 | Err(failure) => { |
| 1113 | 1448 | error = Some(failure); |
| 1114 | 1449 | self.accessibility.deactivate(); |
| ... | ... | @@ -1185,6 +1520,7 @@ impl State { |
| 1185 | 1520 | } |
| 1186 | 1521 | |
| 1187 | 1522 | fn draw(&mut self) -> Result<(), Box<dyn Error>> { |
| 1523 | let start = Instant::now(); | |
| 1188 | 1524 | if let Some(path) = self.snapshot.take() { |
| 1189 | 1525 | self.snapshot(&path)?; |
| 1190 | 1526 | } |
| ... | ... | @@ -1222,9 +1558,12 @@ impl State { |
| 1222 | 1558 | return Err("Canvas surface validation failed".into()); |
| 1223 | 1559 | } |
| 1224 | 1560 | }; |
| 1561 | lap("acquire", start); | |
| 1225 | 1562 | self.paint(&frame.texture.create_view(&Default::default()))?; |
| 1226 | 1563 | self.window.pre_present_notify(); |
| 1564 | let start = Instant::now(); | |
| 1227 | 1565 | self.renderer.queue.present(frame); |
| 1566 | lap("present", start); | |
| 1228 | 1567 | trace_input(&"Present submitted"); |
| 1229 | 1568 | if reconfigure { |
| 1230 | 1569 | self.surface.configure(&self.renderer.device, &self.config); |
| ... | ... | @@ -1234,8 +1573,19 @@ impl State { |
| 1234 | 1573 | |
| 1235 | 1574 | /// Paints the interface with the page in its box. |
| 1236 | 1575 | fn paint(&mut self, target: &wgpu::TextureView) -> Result<(), Box<dyn Error>> { |
| 1576 | let start = Instant::now(); | |
| 1237 | 1577 | let [caret, selection] = macos::text_colors(); |
| 1238 | let page_primitives = self.view.primitives(TextColors { caret, selection })?; | |
| 1578 | let paper = canvas::gpu::Paper { | |
| 1579 | color: self.ui.theme.paper, | |
| 1580 | ink: self.ui.theme.paper_ink, | |
| 1581 | }; | |
| 1582 | self.view.update_backgrounds(paper, &self.redraw); | |
| 1583 | let page_primitives = self.view.primitives(TextColors { | |
| 1584 | caret, | |
| 1585 | selection, | |
| 1586 | paper, | |
| 1587 | })?; | |
| 1588 | lap("page primitives", start); | |
| 1239 | 1589 | let scale = self.ui.scale(); |
| 1240 | 1590 | let corner = self.ui.rect(page()).unwrap_or_default(); |
| 1241 | 1591 | let viewport = self.view.viewport; |
| ... | ... | @@ -1261,6 +1611,7 @@ impl State { |
| 1261 | 1611 | }) |
| 1262 | 1612 | .collect(); |
| 1263 | 1613 | trace_input(&("Draw", viewport.origin, viewport.scale)); |
| 1614 | let start = Instant::now(); | |
| 1264 | 1615 | self.renderer |
| 1265 | 1616 | .draw( |
| 1266 | 1617 | target, |
| ... | ... | @@ -1268,7 +1619,9 @@ impl State { |
| 1268 | 1619 | self.ui.theme.base, |
| 1269 | 1620 | &layers, |
| 1270 | 1621 | ) |
| 1271 | .map_err(|error| format!("Canvas drawing failed: {error:?}").into()) | |
| 1622 | .map_err(|error| format!("Canvas drawing failed: {error:?}"))?; | |
| 1623 | lap("render", start); | |
| 1624 | Ok(()) | |
| 1272 | 1625 | } |
| 1273 | 1626 | |
| 1274 | 1627 | /// Writes the frame to a PNG, so a covered window can be reviewed. |
| ... | ... | @@ -1373,6 +1726,76 @@ impl State { |
| 1373 | 1726 | } |
| 1374 | 1727 | } |
| 1375 | 1728 | |
| 1729 | fn theme(appearance: winit::window::Theme) -> Theme { | |
| 1730 | match appearance { | |
| 1731 | winit::window::Theme::Dark => Theme::dark(), | |
| 1732 | winit::window::Theme::Light => Theme::light(), | |
| 1733 | } | |
| 1734 | } | |
| 1735 | ||
| 1736 | fn file_name(path: &Path) -> String { | |
| 1737 | path.canonicalize() | |
| 1738 | .ok() | |
| 1739 | .as_deref() | |
| 1740 | .and_then(Path::file_name) | |
| 1741 | .map(|name| name.to_string_lossy().into_owned()) | |
| 1742 | .unwrap_or_default() | |
| 1743 | } | |
| 1744 | ||
| 1745 | /// A section's colour as linear RGBA; sections without one take OneNote's default blue. | |
| 1746 | fn section_color(color: Option<u32>) -> [f32; 4] { | |
| 1747 | color.map_or(draw::srgb(0x8a, 0xa8, 0xe4), canvas::gpu::colorref) | |
| 1748 | } | |
| 1749 | ||
| 1750 | /// The pages whose titles contain `filter`, ignoring case. | |
| 1751 | fn matching<'a>( | |
| 1752 | pages: &'a [(ExGuid, String, u32)], | |
| 1753 | filter: &str, | |
| 1754 | ) -> impl Iterator<Item = &'a (ExGuid, String, u32)> { | |
| 1755 | let filter = filter.to_lowercase(); | |
| 1756 | pages | |
| 1757 | .iter() | |
| 1758 | .filter(move |(_, title, _)| title.to_lowercase().contains(&filter)) | |
| 1759 | } | |
| 1760 | ||
| 1761 | /// A toolbar group's rows, stacked. | |
| 1762 | fn group(ui: &mut Ui, part: &str, rows: impl FnOnce(&mut Ui)) { | |
| 1763 | ui.open( | |
| 1764 | part, | |
| 1765 | Spec { | |
| 1766 | axis: Axis::Y, | |
| 1767 | gap: 2.0, | |
| 1768 | ..Spec::default() | |
| 1769 | }, | |
| 1770 | ); | |
| 1771 | rows(ui); | |
| 1772 | ui.close(); | |
| 1773 | } | |
| 1774 | ||
| 1775 | fn row(ui: &mut Ui, part: usize, buttons: impl FnOnce(&mut Ui)) { | |
| 1776 | ui.open( | |
| 1777 | part, | |
| 1778 | Spec { | |
| 1779 | gap: 1.0, | |
| 1780 | ..Spec::default() | |
| 1781 | }, | |
| 1782 | ); | |
| 1783 | buttons(ui); | |
| 1784 | ui.close(); | |
| 1785 | } | |
| 1786 | ||
| 1787 | /// The line between two toolbar groups. | |
| 1788 | fn divider(ui: &mut Ui, part: &str, theme: &Theme) { | |
| 1789 | ui.leaf( | |
| 1790 | ("divider", part), | |
| 1791 | Spec { | |
| 1792 | size: [px(1.0), px(2.0 * ui::shell::TOOL + 2.0)], | |
| 1793 | fill: Some(theme.chip), | |
| 1794 | ..Spec::default() | |
| 1795 | }, | |
| 1796 | ); | |
| 1797 | } | |
| 1798 | ||
| 1376 | 1799 | /// Section tabs for a notebook's readable top-level sections, in its order. |
| 1377 | 1800 | fn tabs(notebook: &notebook::session::Notebook) -> Vec<Tab> { |
| 1378 | 1801 | notebook |
| ... | ... | @@ -1481,11 +1904,19 @@ impl ApplicationHandler<UserEvent> for App { |
| 1481 | 1904 | } |
| 1482 | 1905 | return; |
| 1483 | 1906 | } |
| 1907 | UserEvent::Redraw => { | |
| 1908 | if let Some(state) = &self.state { | |
| 1909 | state.window.request_redraw(); | |
| 1910 | } | |
| 1911 | return; | |
| 1912 | } | |
| 1484 | 1913 | UserEvent::Replay(replay) => { |
| 1485 | 1914 | if let Some(state) = &mut self.state { |
| 1486 | 1915 | match replay { |
| 1487 | 1916 | Replay::Input(event) => state.input(event), |
| 1488 | 1917 | Replay::Snapshot(path) => state.snapshot = Some(path), |
| 1918 | Replay::Tick => {} | |
| 1919 | Replay::Appearance(appearance) => state.ui.theme = theme(appearance), | |
| 1489 | 1920 | } |
| 1490 | 1921 | // A covered window gets no redraws, so each step draws its own frame. |
| 1491 | 1922 | if let Err(error) = state.frame() { |
| ... | ... | @@ -1504,7 +1935,10 @@ impl ApplicationHandler<UserEvent> for App { |
| 1504 | 1935 | } |
| 1505 | 1936 | let was_marked = state.view.editor.marked_range().is_some(); |
| 1506 | 1937 | let result = match event.window_event { |
| 1507 | accesskit_winit::WindowEvent::InitialTreeRequested => state.update_accessibility(), | |
| 1938 | accesskit_winit::WindowEvent::InitialTreeRequested => { | |
| 1939 | state.accessibility.deactivate(); | |
| 1940 | state.update_accessibility() | |
| 1941 | } | |
| 1508 | 1942 | accesskit_winit::WindowEvent::ActionRequested(request) => state.access_action(request), |
| 1509 | 1943 | accesskit_winit::WindowEvent::AccessibilityDeactivated => { |
| 1510 | 1944 | state.accessibility.deactivate(); |
| ... | ... | @@ -1528,6 +1962,7 @@ impl ApplicationHandler<UserEvent> for App { |
| 1528 | 1962 | self.proxy.clone(), |
| 1529 | 1963 | self.input.take().unwrap(), |
| 1530 | 1964 | &self.substitutes, |
| 1965 | std::mem::take(&mut self.interface_font), | |
| 1531 | 1966 | )) { |
| 1532 | 1967 | Ok(state) => self.state = Some(state), |
| 1533 | 1968 | Err(error) => { |
| ... | ... | @@ -1575,8 +2010,13 @@ impl ApplicationHandler<UserEvent> for App { |
| 1575 | 2010 | // lets the window show the previous frame at the new size. |
| 1576 | 2011 | return state.frame(); |
| 1577 | 2012 | } |
| 2013 | WindowEvent::ThemeChanged(appearance) => { | |
| 2014 | state.ui.theme = theme(appearance); | |
| 2015 | state.window.request_redraw(); | |
| 2016 | } | |
| 1578 | 2017 | WindowEvent::ScaleFactorChanged { scale_factor, .. } => { |
| 1579 | 2018 | state.renderer.clear_glyph_cache(); |
| 2019 | state.app_icon = macos::app_icon((16.0 * scale_factor).round() as u32); | |
| 1580 | 2020 | let response = state.view.scale_factor_changed(scale_factor as f32)?; |
| 1581 | 2021 | state.respond(response); |
| 1582 | 2022 | state.window.request_redraw(); |
| ... | ... | @@ -1589,7 +2029,7 @@ impl ApplicationHandler<UserEvent> for App { |
| 1589 | 2029 | trace_input(&("Window occluded", occluded)); |
| 1590 | 2030 | state.occluded = occluded; |
| 1591 | 2031 | if !occluded { |
| 1592 | state.changed = true; | |
| 2032 | state.moved = true; | |
| 1593 | 2033 | state.window.request_redraw(); |
| 1594 | 2034 | } |
| 1595 | 2035 | } |
| ... | ... | @@ -1654,7 +2094,8 @@ impl ApplicationHandler<UserEvent> for App { |
| 1654 | 2094 | |
| 1655 | 2095 | /// Feeds a development script to the window from another thread, one command per line |
| 1656 | 2096 | /// in logical pixels: `move X Y`, `press`, `release`, `wheel DX DY`, `key NAME`, `type |
| 1657 | /// TEXT`, `modifiers [shift] [command]`, `wait MILLISECONDS` and `snapshot PNG_PATH`. | |
| 2097 | /// TEXT`, `modifiers [shift] [command]`, `wait MILLISECONDS`, `snapshot PNG_PATH` and | |
| 2098 | /// `appearance light|dark`. | |
| 1658 | 2099 | fn replay(script: String, proxy: EventLoopProxy<UserEvent>) -> Result<(), Box<dyn Error>> { |
| 1659 | 2100 | let mut steps = Vec::new(); |
| 1660 | 2101 | for line in script.lines().filter(|line| !line.trim().is_empty()) { |
| ... | ... | @@ -1702,6 +2143,11 @@ fn replay(script: String, proxy: EventLoopProxy<UserEvent>) -> Result<(), Box<dy |
| 1702 | 2143 | ))), |
| 1703 | 2144 | "wait" => Err(std::time::Duration::from_millis(rest.parse()?)), |
| 1704 | 2145 | "snapshot" => Ok(Replay::Snapshot(rest.into())), |
| 2146 | "appearance" => Ok(Replay::Appearance(match rest { | |
| 2147 | "light" => winit::window::Theme::Light, | |
| 2148 | "dark" => winit::window::Theme::Dark, | |
| 2149 | _ => return Err(format!("Unknown appearance {rest}").into()), | |
| 2150 | })), | |
| 1705 | 2151 | _ => return Err(format!("Unknown replay command: {line}").into()), |
| 1706 | 2152 | }); |
| 1707 | 2153 | } |
| ... | ... | @@ -1714,7 +2160,13 @@ fn replay(script: String, proxy: EventLoopProxy<UserEvent>) -> Result<(), Box<dy |
| 1714 | 2160 | } |
| 1715 | 2161 | let _ = proxy.send_event(UserEvent::Replay(replay)); |
| 1716 | 2162 | } |
| 1717 | Err(duration) => std::thread::sleep(duration), | |
| 2163 | Err(duration) => { | |
| 2164 | let start = Instant::now(); | |
| 2165 | while start.elapsed() < duration { | |
| 2166 | std::thread::sleep(std::time::Duration::from_millis(16)); | |
| 2167 | let _ = proxy.send_event(UserEvent::Replay(Replay::Tick)); | |
| 2168 | } | |
| 2169 | } | |
| 1718 | 2170 | } |
| 1719 | 2171 | } |
| 1720 | 2172 | }); |
| ... | ... | @@ -1725,6 +2177,7 @@ fn main() -> Result<(), Box<dyn Error>> { |
| 1725 | 2177 | let mut args = std::env::args_os().skip(1); |
| 1726 | 2178 | let mut positional = Vec::new(); |
| 1727 | 2179 | let mut substitutes = Vec::new(); |
| 2180 | let mut interface_font = Vec::new(); | |
| 1728 | 2181 | let mut reference = None; |
| 1729 | 2182 | let mut editable = false; |
| 1730 | 2183 | let mut section = None; |
| ... | ... | @@ -1736,6 +2189,27 @@ fn main() -> Result<(), Box<dyn Error>> { |
| 1736 | 2189 | args.next() |
| 1737 | 2190 | .ok_or("Provide a font file after --substitute-font.")?, |
| 1738 | 2191 | )); |
| 2192 | } else if arg == "--ui-font" { | |
| 2193 | let path = PathBuf::from( | |
| 2194 | args.next() | |
| 2195 | .ok_or("Provide a font file or folder after --ui-font.")?, | |
| 2196 | ); | |
| 2197 | let mut files = if path.is_dir() { | |
| 2198 | std::fs::read_dir(&path)? | |
| 2199 | .map(|entry| entry.map(|entry| entry.path())) | |
| 2200 | .collect::<Result<Vec<_>, _>>()? | |
| 2201 | } else { | |
| 2202 | vec![path] | |
| 2203 | }; | |
| 2204 | files.retain(|file| { | |
| 2205 | file.extension().is_some_and(|extension| { | |
| 2206 | matches!(extension.to_str(), Some("otf" | "ttf" | "ttc")) | |
| 2207 | }) | |
| 2208 | }); | |
| 2209 | files.sort(); | |
| 2210 | for file in files { | |
| 2211 | interface_font.push(std::fs::read(file)?); | |
| 2212 | } | |
| 1739 | 2213 | } else if arg == "--notebook" { |
| 1740 | 2214 | notebook = Some(PathBuf::from( |
| 1741 | 2215 | args.next() |
| ... | ... | @@ -1794,7 +2268,7 @@ fn main() -> Result<(), Box<dyn Error>> { |
| 1794 | 2268 | } |
| 1795 | 2269 | if positional.len() > 2 { |
| 1796 | 2270 | return Err( |
| 1797 | "Usage: snowbound [TEXT_FILE] [WIDTH_POINTS] [--reference SECTION PAGE_TITLE | --page SECTION PAGE_TITLE | --section SECTION PAGE_TITLE | --notebook FOLDER] [--cache DIR] [--substitute-font FONT_FILE]..." | |
| 2271 | "Usage: snowbound [TEXT_FILE] [WIDTH_POINTS] [--reference SECTION PAGE_TITLE | --page SECTION PAGE_TITLE | --section SECTION PAGE_TITLE | --notebook FOLDER] [--cache DIR] [--ui-font FONT_FILE_OR_FOLDER] [--substitute-font FONT_FILE]..." | |
| 1798 | 2272 | .into(), |
| 1799 | 2273 | ); |
| 1800 | 2274 | } |
| ... | ... | @@ -1842,6 +2316,7 @@ fn main() -> Result<(), Box<dyn Error>> { |
| 1842 | 2316 | proxy: event_loop.create_proxy(), |
| 1843 | 2317 | input: Some(input), |
| 1844 | 2318 | substitutes, |
| 2319 | interface_font, | |
| 1845 | 2320 | state: None, |
| 1846 | 2321 | startup_error: None, |
| 1847 | 2322 | }; |
crates/ui/assets/chevron-down.svg created+3| ... | ... | @@ -0,0 +1,3 @@ |
| 1 | <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"> | |
| 2 | <path d="M5 6.75L8 9.75L11 6.75" fill="none" stroke="currentColor" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/> | |
| 3 | </svg> |
crates/ui/src/layout.rs+11-1| ... | ... | @@ -11,7 +11,17 @@ pub(crate) fn solve(nodes: &mut [Built], states: &HashMap<Id, State>, scale: f32 |
| 11 | 11 | node.computed[axis] = match node.size[axis].size { |
| 12 | 12 | Size::Pixels(pixels) => pixels, |
| 13 | 13 | Size::Text => { |
| 14 | node.label.as_ref().map_or(0.0, |label| label.size[axis]) + 2.0 * node.pad[axis] | |
| 14 | let content = if axis == 0 { | |
| 15 | node.content_width() | |
| 16 | } else { | |
| 17 | let label = node.label.as_ref().map_or(0.0, |label| label.size[1]); | |
| 18 | if node.icon.is_some() || node.image.is_some() { | |
| 19 | label.max(crate::ICON) | |
| 20 | } else { | |
| 21 | label | |
| 22 | } | |
| 23 | }; | |
| 24 | content + 2.0 * node.pad[axis] | |
| 15 | 25 | } |
| 16 | 26 | Size::Fraction(_) | Size::Children => 0.0, |
| 17 | 27 | }; |
crates/ui/src/lib.rs+348-52| ... | ... | @@ -4,22 +4,27 @@ |
| 4 | 4 | //! events are routed before building and its layout is solved after. |
| 5 | 5 | |
| 6 | 6 | mod layout; |
| 7 | pub mod shell; | |
| 7 | 8 | mod text; |
| 8 | 9 | mod theme; |
| 9 | 10 | mod widgets; |
| 10 | 11 | |
| 11 | pub use theme::Theme; | |
| 12 | pub use widgets::{button, scrollbar, text_field}; | |
| 12 | pub use theme::{Section, Shades, Theme}; | |
| 13 | pub use widgets::{button, edit_key, edit_modifiers, scrollbar, text_field}; | |
| 13 | 14 | |
| 14 | use draw::{Primitive, Stroke}; | |
| 15 | use draw::{ | |
| 16 | PathStyle, Primitive, RasterImage, Stroke, | |
| 17 | edit::{Clicks, SelectionUnit}, | |
| 18 | }; | |
| 19 | use parley::editing::Selection; | |
| 15 | 20 | use std::{ |
| 16 | 21 | collections::HashMap, |
| 17 | 22 | hash::{DefaultHasher, Hash, Hasher}, |
| 18 | 23 | ops::BitOr, |
| 19 | 24 | rc::Rc, |
| 20 | time::Instant, | |
| 25 | time::{Duration, Instant}, | |
| 21 | 26 | }; |
| 22 | use text::{Label, Painted, Texts}; | |
| 27 | use text::{Label, Texts}; | |
| 23 | 28 | use winit::{ |
| 24 | 29 | event::{Ime, MouseButton}, |
| 25 | 30 | keyboard::{Key, ModifiersState}, |
| ... | ... | @@ -28,6 +33,11 @@ use winit::{ |
| 28 | 33 | |
| 29 | 34 | /// Seconds for an animated value to close half of its remaining distance. |
| 30 | 35 | const HALF_LIFE: f32 = 0.03; |
| 36 | /// How far a box's shadow spreads, in logical pixels. | |
| 37 | const SHADOW: f32 = 3.0; | |
| 38 | /// Logical size of a box's icon, and its distance from the label. | |
| 39 | const ICON: f32 = 16.0; | |
| 40 | const ICON_GAP: f32 = 6.0; | |
| 31 | 41 | |
| 32 | 42 | /// A box's identity across frames: its parent's id combined with a builder-chosen part. |
| 33 | 43 | #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] |
| ... | ... | @@ -139,6 +149,18 @@ impl BitOr for Flags { |
| 139 | 149 | } |
| 140 | 150 | } |
| 141 | 151 | |
| 152 | /// How a box's fill and border are outlined; `radius` rounds the named corners. | |
| 153 | #[derive(Clone, Copy, Debug, Default, PartialEq)] | |
| 154 | pub enum Shape { | |
| 155 | #[default] | |
| 156 | Rounded, | |
| 157 | /// A section tab: the top leading corner rounded and the trailing edge leaning out | |
| 158 | /// `slant` pixels from top to bottom, centred on the box's edge so neighbours overlap. | |
| 159 | Tab { slant: f32 }, | |
| 160 | /// Only the trailing corners rounded, so the leading edge joins what it sits against. | |
| 161 | Trailing, | |
| 162 | } | |
| 163 | ||
| 142 | 164 | /// What a box is this frame. Colours are linear RGBA. |
| 143 | 165 | #[derive(Clone, Default)] |
| 144 | 166 | pub struct Spec<'a> { |
| ... | ... | @@ -150,11 +172,20 @@ pub struct Spec<'a> { |
| 150 | 172 | /// The label's colour; the theme's text colour otherwise. |
| 151 | 173 | pub color: Option<[f32; 4]>, |
| 152 | 174 | pub fill: Option<[f32; 4]>, |
| 175 | /// The fill's colour at the bottom, fading from `fill` at the top. | |
| 176 | pub gradient: Option<[f32; 4]>, | |
| 177 | /// A soft shadow of the box's outline, painted beneath it. | |
| 178 | pub shadow: Option<[f32; 4]>, | |
| 153 | 179 | /// The fill under the pointer, blended in as hover animates. |
| 154 | 180 | pub hover_fill: Option<[f32; 4]>, |
| 155 | 181 | pub border: Option<[f32; 4]>, |
| 156 | 182 | pub hover_border: Option<[f32; 4]>, |
| 157 | 183 | pub radius: f32, |
| 184 | pub shape: Shape, | |
| 185 | /// 16 px artwork before the label, tinted with the label's colour. | |
| 186 | pub icon: Option<&'static [&'static str]>, | |
| 187 | /// A 16 px picture in the icon's place. | |
| 188 | pub image: Option<&'a RasterImage>, | |
| 158 | 189 | /// Inset of the label and children on each axis. |
| 159 | 190 | pub pad: [f32; 2], |
| 160 | 191 | /// Space between children along the flow. |
| ... | ... | @@ -190,6 +221,8 @@ pub struct Signal { |
| 190 | 221 | pub hovered: bool, |
| 191 | 222 | /// The primary button went down on the box. |
| 192 | 223 | pub pressed: bool, |
| 224 | /// What the press selects text by: a single, double or triple click. | |
| 225 | pub unit: SelectionUnit, | |
| 193 | 226 | /// The primary button came up over the box it went down on. |
| 194 | 227 | pub clicked: bool, |
| 195 | 228 | /// The primary button is held after pressing the box. |
| ... | ... | @@ -224,10 +257,15 @@ struct Built { |
| 224 | 257 | label: Option<Rc<Label>>, |
| 225 | 258 | color: [f32; 4], |
| 226 | 259 | fill: Option<[f32; 4]>, |
| 260 | gradient: Option<[f32; 4]>, | |
| 261 | shadow: Option<[f32; 4]>, | |
| 227 | 262 | hover_fill: Option<[f32; 4]>, |
| 228 | 263 | border: Option<[f32; 4]>, |
| 229 | 264 | hover_border: Option<[f32; 4]>, |
| 230 | 265 | radius: f32, |
| 266 | shape: Shape, | |
| 267 | icon: Option<&'static [&'static str]>, | |
| 268 | image: Option<RasterImage>, | |
| 231 | 269 | pad: [f32; 2], |
| 232 | 270 | gap: f32, |
| 233 | 271 | center: bool, |
| ... | ... | @@ -245,17 +283,20 @@ struct Built { |
| 245 | 283 | struct State { |
| 246 | 284 | touched: u64, |
| 247 | 285 | hot: f32, |
| 248 | active: f32, | |
| 286 | /// The box paints differently under the pointer, so its hover is worth easing. | |
| 287 | hovers: bool, | |
| 249 | 288 | scroll: f32, |
| 250 | 289 | scroll_target: f32, |
| 251 | 290 | /// Height of a scrolling box's children last frame. |
| 252 | 291 | content: f32, |
| 253 | 292 | rect: [f32; 4], |
| 254 | /// A text field's caret and selection anchor, in bytes. | |
| 255 | caret: usize, | |
| 256 | mark: usize, | |
| 293 | /// A text field's selection, and the selection and unit of the press a drag extends. | |
| 294 | selection: Selection, | |
| 295 | press: (Selection, SelectionUnit), | |
| 257 | 296 | /// Where a dragged scrollbar thumb was taken, from its start. |
| 258 | 297 | grab: f32, |
| 298 | /// An animated value and its target, for `Ui::animate`. | |
| 299 | tween: Option<[f32; 2]>, | |
| 259 | 300 | } |
| 260 | 301 | |
| 261 | 302 | struct Hit { |
| ... | ... | @@ -270,13 +311,39 @@ enum Display { |
| 270 | 311 | Rect { |
| 271 | 312 | rect: [f32; 4], |
| 272 | 313 | fill: [f32; 4], |
| 314 | /// The fill's colour at the bottom. | |
| 315 | shade: Option<[f32; 4]>, | |
| 273 | 316 | border: Option<[f32; 4]>, |
| 274 | 317 | radius: f32, |
| 275 | 318 | }, |
| 319 | /// A hairline 1 px wide. | |
| 320 | Segment { | |
| 321 | from: [f32; 2], | |
| 322 | to: [f32; 2], | |
| 323 | color: [f32; 4], | |
| 324 | }, | |
| 325 | Image { | |
| 326 | image: RasterImage, | |
| 327 | rect: [f32; 4], | |
| 328 | }, | |
| 329 | /// A shaped outline, filled or stroked. | |
| 330 | Path { | |
| 331 | data: String, | |
| 332 | origin: [f32; 2], | |
| 333 | style: PathStyle, | |
| 334 | /// Top and bottom. | |
| 335 | colors: [[f32; 4]; 2], | |
| 336 | }, | |
| 337 | Icon { | |
| 338 | sources: &'static [&'static str], | |
| 339 | origin: [f32; 2], | |
| 340 | tint: [f32; 4], | |
| 341 | }, | |
| 276 | 342 | Text { |
| 277 | painted: usize, | |
| 343 | label: Rc<Label>, | |
| 278 | 344 | origin: [f32; 2], |
| 279 | 345 | clip: [f32; 4], |
| 346 | color: [f32; 4], | |
| 280 | 347 | }, |
| 281 | 348 | Custom { |
| 282 | 349 | id: Id, |
| ... | ... | @@ -302,13 +369,14 @@ pub struct Ui { |
| 302 | 369 | active: Option<Id>, |
| 303 | 370 | focus: Option<Id>, |
| 304 | 371 | modifiers: ModifiersState, |
| 372 | clicks: Clicks, | |
| 305 | 373 | display: Vec<Display>, |
| 306 | painted: Vec<Painted>, | |
| 307 | 374 | animating: bool, |
| 308 | 375 | } |
| 309 | 376 | |
| 310 | 377 | impl Ui { |
| 311 | pub fn new(theme: Theme) -> Self { | |
| 378 | /// `double_click` is the platform's double-click interval. | |
| 379 | pub fn new(theme: Theme, double_click: Duration) -> Self { | |
| 312 | 380 | Self { |
| 313 | 381 | theme, |
| 314 | 382 | frame: 0, |
| ... | ... | @@ -326,8 +394,8 @@ impl Ui { |
| 326 | 394 | active: None, |
| 327 | 395 | focus: None, |
| 328 | 396 | modifiers: ModifiersState::empty(), |
| 397 | clicks: Clicks::new(double_click), | |
| 329 | 398 | display: Vec::new(), |
| 330 | painted: Vec::new(), | |
| 331 | 399 | animating: false, |
| 332 | 400 | } |
| 333 | 401 | } |
| ... | ... | @@ -412,7 +480,7 @@ impl Ui { |
| 412 | 480 | for event in std::mem::take(&mut self.queue) { |
| 413 | 481 | self.route(event); |
| 414 | 482 | } |
| 415 | self.animate(dt); | |
| 483 | self.ease(dt); | |
| 416 | 484 | } |
| 417 | 485 | |
| 418 | 486 | fn route(&mut self, event: Event) { |
| ... | ... | @@ -438,7 +506,7 @@ impl Ui { |
| 438 | 506 | Event::Button { |
| 439 | 507 | button, |
| 440 | 508 | pressed: true, |
| 441 | .. | |
| 509 | at, | |
| 442 | 510 | } => { |
| 443 | 511 | let Some(point) = self.pointer else { |
| 444 | 512 | return; |
| ... | ... | @@ -448,7 +516,9 @@ impl Ui { |
| 448 | 516 | && button == MouseButton::Left |
| 449 | 517 | { |
| 450 | 518 | self.active = Some(id); |
| 451 | self.signals.entry(id).or_default().pressed = true; | |
| 519 | let signal = self.signals.entry(id).or_default(); | |
| 520 | signal.pressed = true; | |
| 521 | signal.unit = self.clicks.press(at, point, 4.0); | |
| 452 | 522 | if self.hit_flags(id).contains(Flags::FOCUSABLE) { |
| 453 | 523 | self.focus = Some(id); |
| 454 | 524 | } |
| ... | ... | @@ -547,21 +617,23 @@ impl Ui { |
| 547 | 617 | .map(|hit| hit.id) |
| 548 | 618 | } |
| 549 | 619 | |
| 550 | fn animate(&mut self, dt: f32) { | |
| 620 | fn ease(&mut self, dt: f32) { | |
| 551 | 621 | let rate = 1.0 - 0.5_f32.powf(dt / HALF_LIFE); |
| 552 | 622 | let mut animating = false; |
| 553 | 623 | for (id, state) in &mut self.states { |
| 554 | 624 | let hot = self.hover == Some(*id) && self.active.is_none_or(|active| active == *id); |
| 625 | if !state.hovers { | |
| 626 | state.hot = f32::from(u8::from(hot)); | |
| 627 | } | |
| 555 | 628 | let targets = [ |
| 556 | 629 | (&mut state.hot, f32::from(u8::from(hot)), 0.002), |
| 557 | ( | |
| 558 | &mut state.active, | |
| 559 | f32::from(u8::from(self.active == Some(*id))), | |
| 560 | 0.002, | |
| 561 | ), | |
| 562 | 630 | (&mut state.scroll, state.scroll_target, 0.25), |
| 563 | 631 | ]; |
| 564 | for (value, target, close) in targets { | |
| 632 | let tween = state | |
| 633 | .tween | |
| 634 | .as_mut() | |
| 635 | .map(|[value, target]| (value, *target, 0.005)); | |
| 636 | for (value, target, close) in targets.into_iter().chain(tween) { | |
| 565 | 637 | let delta = target - *value; |
| 566 | 638 | if delta.abs() < close { |
| 567 | 639 | *value = target; |
| ... | ... | @@ -617,6 +689,29 @@ impl Ui { |
| 617 | 689 | self.signal(id) |
| 618 | 690 | } |
| 619 | 691 | |
| 692 | /// Eases towards `target` over the next frames, starting there the first time `id` asks. | |
| 693 | pub fn animate(&mut self, id: Id, target: f32) -> f32 { | |
| 694 | let state = self.states.entry(id).or_default(); | |
| 695 | state.touched = self.frame; | |
| 696 | let [value, goal] = state.tween.get_or_insert([target; 2]); | |
| 697 | *goal = target; | |
| 698 | self.animating |= value != goal; | |
| 699 | *value | |
| 700 | } | |
| 701 | ||
| 702 | /// Shapes labels in the family the font `files` define, in place of the system's | |
| 703 | /// interface font. Returns the family's name, or None when the files hold no font. | |
| 704 | pub fn use_fonts(&mut self, files: impl IntoIterator<Item = Vec<u8>>) -> Option<String> { | |
| 705 | self.texts.use_fonts(files) | |
| 706 | } | |
| 707 | ||
| 708 | /// The size of `text` as a label, in logical pixels. | |
| 709 | pub fn measure(&mut self, text: &str) -> [f32; 2] { | |
| 710 | self.texts | |
| 711 | .label(text, self.theme.font_size, self.frame) | |
| 712 | .size | |
| 713 | } | |
| 714 | ||
| 620 | 715 | /// Paints `color` over the current box at `rect`, relative to its corner. |
| 621 | 716 | pub fn mark(&mut self, rect: [f32; 4], color: [f32; 4]) { |
| 622 | 717 | let index = *self.stack.last().unwrap(); |
| ... | ... | @@ -639,6 +734,7 @@ impl Ui { |
| 639 | 734 | for node in &self.nodes { |
| 640 | 735 | let state = self.states.entry(node.id).or_default(); |
| 641 | 736 | state.rect = node.rect; |
| 737 | state.hovers = node.hover_fill.is_some() || node.hover_border.is_some(); | |
| 642 | 738 | if node.flags.contains(Flags::SCROLL) { |
| 643 | 739 | state.content = node.content; |
| 644 | 740 | let most = (node.content - (node.rect[3] - node.rect[1])).max(0.0); |
| ... | ... | @@ -650,7 +746,6 @@ impl Ui { |
| 650 | 746 | .retain(|id, state| state.touched == self.frame || *id == Id::ROOT); |
| 651 | 747 | self.texts.prune(self.frame); |
| 652 | 748 | self.display.clear(); |
| 653 | self.painted.clear(); | |
| 654 | 749 | self.hits.clear(); |
| 655 | 750 | self.paint(0, None); |
| 656 | 751 | for id in [&mut self.hover, &mut self.active, &mut self.focus] { |
| ... | ... | @@ -665,20 +760,59 @@ impl Ui { |
| 665 | 760 | let state = &self.states[&node.id]; |
| 666 | 761 | let rect = node.rect; |
| 667 | 762 | let visible = clip.map_or(Some(rect), |clip| intersect(rect, clip)); |
| 763 | // Without a colour of its own, a box fades the hover colour in rather than | |
| 764 | // blending from transparent black. | |
| 668 | 765 | let blend = |base: Option<[f32; 4]>, hover: Option<[f32; 4]>| match (base, hover) { |
| 669 | (base, Some(hover)) => Some(mix(base.unwrap_or([0.0; 4]), hover, state.hot)), | |
| 766 | (base, Some(hover)) => Some(mix( | |
| 767 | base.unwrap_or([hover[0], hover[1], hover[2], 0.0]), | |
| 768 | hover, | |
| 769 | state.hot, | |
| 770 | )), | |
| 670 | 771 | (base, None) => base, |
| 671 | 772 | }; |
| 672 | 773 | let fill = blend(node.fill, node.hover_fill); |
| 774 | // The gradient keeps its difference from the fill as hover moves it. | |
| 775 | let shade = node.gradient.map(|shade| { | |
| 776 | let base = node.fill.unwrap_or(shade); | |
| 777 | std::array::from_fn(|channel| { | |
| 778 | shade[channel] + fill.unwrap_or(base)[channel] - base[channel] | |
| 779 | }) | |
| 780 | }); | |
| 673 | 781 | let border = blend(node.border, node.hover_border); |
| 674 | if fill.is_some() || border.is_some() { | |
| 675 | self.display.push(Display::Rect { | |
| 676 | rect, | |
| 677 | fill: fill.unwrap_or([0.0; 4]), | |
| 678 | border, | |
| 679 | radius: node.radius, | |
| 782 | let size = [rect[2] - rect[0], rect[3] - rect[1]]; | |
| 783 | if let Some(shadow) = node.shadow { | |
| 784 | self.display.push(Display::Path { | |
| 785 | data: outline(node.shape, size, node.radius), | |
| 786 | origin: [rect[0], rect[1]], | |
| 787 | style: PathStyle::Shadow(SHADOW), | |
| 788 | colors: [shadow; 2], | |
| 680 | 789 | }); |
| 681 | 790 | } |
| 791 | if node.shape == Shape::Rounded { | |
| 792 | if fill.is_some() || border.is_some() { | |
| 793 | self.display.push(Display::Rect { | |
| 794 | rect, | |
| 795 | fill: fill.unwrap_or([0.0; 4]), | |
| 796 | shade, | |
| 797 | border, | |
| 798 | radius: node.radius, | |
| 799 | }); | |
| 800 | } | |
| 801 | } else { | |
| 802 | let data = outline(node.shape, size, node.radius); | |
| 803 | let paints = [ | |
| 804 | fill.map(|fill| (PathStyle::Fill, [fill, shade.unwrap_or(fill)])), | |
| 805 | border.map(|border| (PathStyle::Stroke(1.0), [border; 2])), | |
| 806 | ]; | |
| 807 | for (style, colors) in paints.into_iter().flatten() { | |
| 808 | self.display.push(Display::Path { | |
| 809 | data: data.clone(), | |
| 810 | origin: [rect[0], rect[1]], | |
| 811 | style, | |
| 812 | colors, | |
| 813 | }); | |
| 814 | } | |
| 815 | } | |
| 682 | 816 | for (mark, color) in &node.marks { |
| 683 | 817 | self.display.push(Display::Rect { |
| 684 | 818 | rect: [ |
| ... | ... | @@ -688,30 +822,43 @@ impl Ui { |
| 688 | 822 | rect[1] + mark[3], |
| 689 | 823 | ], |
| 690 | 824 | fill: *color, |
| 825 | shade: None, | |
| 691 | 826 | border: None, |
| 692 | 827 | radius: 0.0, |
| 693 | 828 | }); |
| 694 | 829 | } |
| 830 | let inner = [ | |
| 831 | rect[0] + node.pad[0], | |
| 832 | rect[1], | |
| 833 | rect[2] - node.pad[0], | |
| 834 | rect[3], | |
| 835 | ]; | |
| 836 | let mut x = if node.center { | |
| 837 | (inner[0] + inner[2] - node.content_width()) / 2.0 | |
| 838 | } else { | |
| 839 | inner[0] | |
| 840 | }; | |
| 841 | let top = (rect[1] + rect[3] - ICON) / 2.0; | |
| 842 | if let Some(sources) = node.icon { | |
| 843 | self.display.push(Display::Icon { | |
| 844 | sources, | |
| 845 | origin: [x, top], | |
| 846 | tint: node.color, | |
| 847 | }); | |
| 848 | x += ICON + ICON_GAP; | |
| 849 | } else if let Some(image) = &node.image { | |
| 850 | self.display.push(Display::Image { | |
| 851 | image: image.clone(), | |
| 852 | rect: [x, top, x + ICON, top + ICON], | |
| 853 | }); | |
| 854 | x += ICON + ICON_GAP; | |
| 855 | } | |
| 695 | 856 | if let Some(label) = &node.label { |
| 696 | let inner = [ | |
| 697 | rect[0] + node.pad[0], | |
| 698 | rect[1], | |
| 699 | rect[2] - node.pad[0], | |
| 700 | rect[3], | |
| 701 | ]; | |
| 702 | let x = if node.center { | |
| 703 | (inner[0] + inner[2] - label.size[0]) / 2.0 | |
| 704 | } else { | |
| 705 | inner[0] | |
| 706 | }; | |
| 707 | 857 | let y = (rect[1] + rect[3] - label.size[1]) / 2.0; |
| 708 | 858 | self.display.push(Display::Text { |
| 709 | painted: self.painted.len(), | |
| 859 | label: label.clone(), | |
| 710 | 860 | origin: [x, y], |
| 711 | 861 | clip: inner, |
| 712 | }); | |
| 713 | self.painted.push(Painted { | |
| 714 | label: label.clone(), | |
| 715 | 862 | color: node.color, |
| 716 | 863 | }); |
| 717 | 864 | } |
| ... | ... | @@ -768,10 +915,17 @@ impl Ui { |
| 768 | 915 | Display::Rect { |
| 769 | 916 | rect, |
| 770 | 917 | fill, |
| 918 | shade, | |
| 771 | 919 | border, |
| 772 | 920 | radius, |
| 773 | 921 | } => { |
| 774 | if fill[3] > 0.0 { | |
| 922 | if let Some(shade) = shade { | |
| 923 | primitives.push(Primitive::Gradient { | |
| 924 | rect: *rect, | |
| 925 | radius: [*radius; 2], | |
| 926 | colors: [*fill, *shade], | |
| 927 | }); | |
| 928 | } else if fill[3] > 0.0 { | |
| 775 | 929 | primitives.push(if *radius > 0.0 { |
| 776 | 930 | Primitive::RoundedRect { |
| 777 | 931 | rect: *rect, |
| ... | ... | @@ -795,14 +949,47 @@ impl Ui { |
| 795 | 949 | }); |
| 796 | 950 | } |
| 797 | 951 | } |
| 952 | Display::Path { | |
| 953 | data, | |
| 954 | origin, | |
| 955 | style, | |
| 956 | colors, | |
| 957 | } => primitives.push(Primitive::Path { | |
| 958 | data, | |
| 959 | origin: *origin, | |
| 960 | style: *style, | |
| 961 | colors: *colors, | |
| 962 | }), | |
| 963 | Display::Segment { from, to, color } => primitives.push(Primitive::Segment { | |
| 964 | from: *from, | |
| 965 | to: *to, | |
| 966 | width: 1.0, | |
| 967 | round: false, | |
| 968 | color: *color, | |
| 969 | }), | |
| 970 | Display::Image { image, rect } => { | |
| 971 | primitives.push(Primitive::Image { image, rect: *rect }) | |
| 972 | } | |
| 973 | Display::Icon { | |
| 974 | sources, | |
| 975 | origin, | |
| 976 | tint, | |
| 977 | } => primitives.push(Primitive::Icon { | |
| 978 | sources, | |
| 979 | origin: *origin, | |
| 980 | size: ICON, | |
| 981 | tint: *tint, | |
| 982 | }), | |
| 798 | 983 | Display::Text { |
| 799 | painted, | |
| 984 | label, | |
| 800 | 985 | origin, |
| 801 | 986 | clip, |
| 987 | color, | |
| 802 | 988 | } => primitives.push(Primitive::Text { |
| 803 | text: &self.painted[*painted], | |
| 989 | text: &**label, | |
| 804 | 990 | origin: *origin, |
| 805 | 991 | clip: Some(*clip), |
| 992 | ink: *color, | |
| 806 | 993 | }), |
| 807 | 994 | Display::Custom { id, rect } => { |
| 808 | 995 | if !primitives.is_empty() { |
| ... | ... | @@ -828,9 +1015,9 @@ impl Ui { |
| 828 | 1015 | (&mut self.texts, self.frame) |
| 829 | 1016 | } |
| 830 | 1017 | |
| 831 | pub(crate) fn caret(&mut self, id: Id) -> (&mut usize, &mut usize) { | |
| 1018 | pub(crate) fn field(&mut self, id: Id) -> (&mut Selection, &mut (Selection, SelectionUnit)) { | |
| 832 | 1019 | let state = self.states.entry(id).or_default(); |
| 833 | (&mut state.caret, &mut state.mark) | |
| 1020 | (&mut state.selection, &mut state.press) | |
| 834 | 1021 | } |
| 835 | 1022 | |
| 836 | 1023 | pub(crate) fn grab(&mut self, id: Id) -> &mut f32 { |
| ... | ... | @@ -839,6 +1026,16 @@ impl Ui { |
| 839 | 1026 | } |
| 840 | 1027 | |
| 841 | 1028 | impl Built { |
| 1029 | /// The width of the icon and label together. | |
| 1030 | fn content_width(&self) -> f32 { | |
| 1031 | let label = self.label.as_ref().map_or(0.0, |label| label.size[0]); | |
| 1032 | match (self.icon.is_some() || self.image.is_some(), label > 0.0) { | |
| 1033 | (true, true) => ICON + ICON_GAP + label, | |
| 1034 | (true, false) => ICON, | |
| 1035 | (false, _) => label, | |
| 1036 | } | |
| 1037 | } | |
| 1038 | ||
| 842 | 1039 | fn new( |
| 843 | 1040 | id: Id, |
| 844 | 1041 | parent: usize, |
| ... | ... | @@ -856,10 +1053,15 @@ impl Built { |
| 856 | 1053 | label, |
| 857 | 1054 | color: spec.color.unwrap_or(text), |
| 858 | 1055 | fill: spec.fill, |
| 1056 | gradient: spec.gradient, | |
| 1057 | shadow: spec.shadow, | |
| 859 | 1058 | hover_fill: spec.hover_fill, |
| 860 | 1059 | border: spec.border, |
| 861 | 1060 | hover_border: spec.hover_border, |
| 862 | 1061 | radius: spec.radius, |
| 1062 | shape: spec.shape, | |
| 1063 | icon: spec.icon, | |
| 1064 | image: spec.image.cloned(), | |
| 863 | 1065 | pad: spec.pad, |
| 864 | 1066 | gap: spec.gap, |
| 865 | 1067 | center: spec.center, |
| ... | ... | @@ -874,7 +1076,101 @@ impl Built { |
| 874 | 1076 | } |
| 875 | 1077 | } |
| 876 | 1078 | |
| 877 | fn mix(a: [f32; 4], b: [f32; 4], t: f32) -> [f32; 4] { | |
| 1079 | /// SVG path data outlining a box of `size` from its corner, rounding corners by `radius`. | |
| 1080 | /// A tab's outline stays open along its bottom, so a border leaves the edge it stands on. | |
| 1081 | fn outline(shape: Shape, [width, height]: [f32; 2], radius: f32) -> String { | |
| 1082 | let corners = match shape { | |
| 1083 | Shape::Tab { slant } => [ | |
| 1084 | ([0.0, height], 0.0), | |
| 1085 | ([0.0, 0.0], radius), | |
| 1086 | ([width - slant / 2.0, 0.0], radius / 2.0), | |
| 1087 | ([width + slant / 2.0, height], 0.0), | |
| 1088 | ], | |
| 1089 | Shape::Trailing => [ | |
| 1090 | ([0.0, height], 0.0), | |
| 1091 | ([0.0, 0.0], 0.0), | |
| 1092 | ([width, 0.0], radius), | |
| 1093 | ([width, height], radius), | |
| 1094 | ], | |
| 1095 | Shape::Rounded => [ | |
| 1096 | ([0.0, height], radius), | |
| 1097 | ([0.0, 0.0], radius), | |
| 1098 | ([width, 0.0], radius), | |
| 1099 | ([width, height], radius), | |
| 1100 | ], | |
| 1101 | }; | |
| 1102 | let mut data = String::new(); | |
| 1103 | for index in 0..corners.len() { | |
| 1104 | let corner = Corner::new(&corners, index); | |
| 1105 | let verb = if index == 0 { 'M' } else { 'L' }; | |
| 1106 | data += &format!( | |
| 1107 | "{verb}{} {}{}", | |
| 1108 | corner.start[0], | |
| 1109 | corner.start[1], | |
| 1110 | corner.curve([0.0; 2]) | |
| 1111 | ); | |
| 1112 | } | |
| 1113 | if !matches!(shape, Shape::Tab { .. }) { | |
| 1114 | data.push('Z'); | |
| 1115 | } | |
| 1116 | data | |
| 1117 | } | |
| 1118 | ||
| 1119 | /// A polygon's corner rounded by a quarter-circle's cubic, from where it leaves the edge | |
| 1120 | /// before to where it joins the edge after. | |
| 1121 | struct Corner { | |
| 1122 | point: [f32; 2], | |
| 1123 | start: [f32; 2], | |
| 1124 | end: [f32; 2], | |
| 1125 | controls: [[f32; 2]; 2], | |
| 1126 | /// It turns clockwise, so it bulges out of a shape listed clockwise. | |
| 1127 | outward: bool, | |
| 1128 | } | |
| 1129 | ||
| 1130 | impl Corner { | |
| 1131 | /// The corner at `points[index]`, each point with its rounding radius. | |
| 1132 | fn new(points: &[([f32; 2], f32)], index: usize) -> Self { | |
| 1133 | let count = points.len(); | |
| 1134 | let (point, radius) = points[index]; | |
| 1135 | let [before, after] = | |
| 1136 | [(index + count - 1) % count, (index + 1) % count].map(|index| points[index].0); | |
| 1137 | let direction = |from: [f32; 2], to: [f32; 2]| { | |
| 1138 | let length = (to[0] - from[0]).hypot(to[1] - from[1]).max(f32::EPSILON); | |
| 1139 | [(to[0] - from[0]) / length, (to[1] - from[1]) / length] | |
| 1140 | }; | |
| 1141 | let [incoming, outgoing] = [direction(before, point), direction(point, after)]; | |
| 1142 | let along = |from: [f32; 2], direction: [f32; 2], distance: f32| { | |
| 1143 | [ | |
| 1144 | from[0] + direction[0] * distance, | |
| 1145 | from[1] + direction[1] * distance, | |
| 1146 | ] | |
| 1147 | }; | |
| 1148 | let start = along(point, incoming, -radius); | |
| 1149 | let end = along(point, outgoing, radius); | |
| 1150 | // A quarter circle's cubic control distance. | |
| 1151 | let handle = radius * 0.552_284_8; | |
| 1152 | Self { | |
| 1153 | point, | |
| 1154 | start, | |
| 1155 | end, | |
| 1156 | controls: [ | |
| 1157 | along(start, incoming, handle), | |
| 1158 | along(end, outgoing, -handle), | |
| 1159 | ], | |
| 1160 | outward: incoming[0] * outgoing[1] - incoming[1] * outgoing[0] > 0.0, | |
| 1161 | } | |
| 1162 | } | |
| 1163 | ||
| 1164 | /// The cubic from `start` to `end`, as path data relative to `origin`. | |
| 1165 | fn curve(&self, origin: [f32; 2]) -> String { | |
| 1166 | let [a, b, c] = [self.controls[0], self.controls[1], self.end] | |
| 1167 | .map(|point| [point[0] - origin[0], point[1] - origin[1]]); | |
| 1168 | format!("C{} {} {} {} {} {}", a[0], a[1], b[0], b[1], c[0], c[1]) | |
| 1169 | } | |
| 1170 | } | |
| 1171 | ||
| 1172 | /// `a` moved `t` of the way to `b`. | |
| 1173 | pub fn mix(a: [f32; 4], b: [f32; 4], t: f32) -> [f32; 4] { | |
| 878 | 1174 | std::array::from_fn(|i| a[i] + (b[i] - a[i]) * t) |
| 879 | 1175 | } |
| 880 | 1176 |
crates/ui/src/shell.rs created+282| ... | ... | @@ -0,0 +1,282 @@ |
| 1 | //! Controls a notebook window's chrome is built from: OneNote's section tabs and the | |
| 2 | //! toolbar's compact buttons. | |
| 3 | ||
| 4 | use draw::PathStyle; | |
| 5 | ||
| 6 | use crate::{ | |
| 7 | Corner, Display, Flags, Id, Section, Shape, Signal, Spec, Ui, children, fill, mix, px, | |
| 8 | }; | |
| 9 | use std::hash::Hash; | |
| 10 | ||
| 11 | /// A menu's downward arrow. | |
| 12 | pub const CHEVRON: &[&str] = &[include_str!("../assets/chevron-down.svg")]; | |
| 13 | /// Rounding of a section tab's top leading corner; the trailing one takes half. | |
| 14 | const TAB_ROUNDING: f32 = 4.0; | |
| 15 | /// Side of a toolbar button. | |
| 16 | pub const TOOL: f32 = 22.0; | |
| 17 | ||
| 18 | /// Section tabs in a row `height` tall, as OneNote draws them: each leans over the next | |
| 19 | /// at 45°, and the open one lies on top in `section`'s colours, rising to meet the frame | |
| 20 | /// below as its outline and shadow fade in. `tabs` are names and section colours. Returns | |
| 21 | /// the tab clicked and the open tab's id, for `tab_base`. | |
| 22 | pub fn section_tabs( | |
| 23 | ui: &mut Ui, | |
| 24 | part: impl Hash, | |
| 25 | tabs: &[(&str, [f32; 4])], | |
| 26 | active: usize, | |
| 27 | section: &Section, | |
| 28 | height: f32, | |
| 29 | ) -> (Option<usize>, Id) { | |
| 30 | let theme = ui.theme.clone(); | |
| 31 | ui.open( | |
| 32 | part, | |
| 33 | Spec { | |
| 34 | flags: Flags::CLIP, | |
| 35 | size: [fill(), px(height)], | |
| 36 | ..Spec::default() | |
| 37 | }, | |
| 38 | ); | |
| 39 | let [low, tallest] = [height - 6.0, height - 2.0]; | |
| 40 | let mut left = 0.0; | |
| 41 | let placed: Vec<_> = tabs | |
| 42 | .iter() | |
| 43 | .map(|(name, _)| { | |
| 44 | let width = 10.0 + ui.measure(name)[0] + 4.0 + tallest / 2.0; | |
| 45 | left += width; | |
| 46 | (left - width, width) | |
| 47 | }) | |
| 48 | .collect(); | |
| 49 | let mut clicked = None; | |
| 50 | // Earlier tabs lie over later ones; the open tab over all. | |
| 51 | let order = (0..tabs.len()) | |
| 52 | .rev() | |
| 53 | .filter(|index| *index != active) | |
| 54 | .chain((active < tabs.len()).then_some(active)); | |
| 55 | for index in order { | |
| 56 | let (name, color) = tabs[index]; | |
| 57 | let (x, width) = placed[index]; | |
| 58 | let open = ui.animate(ui.id(("open", index)), f32::from(u8::from(index == active))); | |
| 59 | // Whole device pixels, so a rising tab reuses its rasterized outlines. | |
| 60 | let tall = ((low + (tallest - low) * open) * ui.scale()).round() / ui.scale(); | |
| 61 | let colors = theme.section(color); | |
| 62 | let fill = mix(colors.tab, section.frame[0], open); | |
| 63 | let fade = |color: [f32; 4], alpha: f32| [color[0], color[1], color[2], color[3] * alpha]; | |
| 64 | let signal = ui.leaf( | |
| 65 | ("tab", index), | |
| 66 | Spec { | |
| 67 | flags: Flags::CLICKABLE | Flags::FLOAT, | |
| 68 | size: [px(width), px(tall)], | |
| 69 | position: [x, height - tall], | |
| 70 | text: Some(name), | |
| 71 | color: Some(mix(mix(theme.ink, fill, 0.2), theme.ink, open)), | |
| 72 | fill: Some(mix(fill, [1.0; 4], 0.08)), | |
| 73 | gradient: Some(fill), | |
| 74 | hover_fill: (index != active).then(|| mix(colors.frame[0], [1.0; 4], 0.08)), | |
| 75 | border: Some(fade(section.edge, open)), | |
| 76 | shadow: (open > 0.0).then(|| fade([0.0, 0.0, 0.0, 0.35], open)), | |
| 77 | radius: TAB_ROUNDING, | |
| 78 | shape: Shape::Tab { slant: tall }, | |
| 79 | pad: [10.0, 0.0], | |
| 80 | ..Spec::default() | |
| 81 | }, | |
| 82 | ); | |
| 83 | if signal.clicked && index != active { | |
| 84 | clicked = Some(index); | |
| 85 | } | |
| 86 | } | |
| 87 | let open_tab = ui.id(("tab", active)); | |
| 88 | ui.close(); | |
| 89 | (clicked, open_tab) | |
| 90 | } | |
| 91 | ||
| 92 | /// Where the tab laid out at `tab` meets the edge below it: from its leading edge to the | |
| 93 | /// foot of its slant. | |
| 94 | pub fn tab_base(tab: [f32; 4]) -> [f32; 2] { | |
| 95 | [tab[0], tab[2] + (tab[3] - tab[1]) / 2.0] | |
| 96 | } | |
| 97 | ||
| 98 | /// A square button showing `icon` tinted by `tint`; artwork in its own colours takes white. | |
| 99 | pub fn tool_button( | |
| 100 | ui: &mut Ui, | |
| 101 | part: impl Hash, | |
| 102 | icon: &'static [&'static str], | |
| 103 | tint: [f32; 4], | |
| 104 | ) -> Signal { | |
| 105 | let hover = ui.theme.hover(); | |
| 106 | ui.leaf( | |
| 107 | part, | |
| 108 | Spec { | |
| 109 | flags: Flags::CLICKABLE, | |
| 110 | size: [px(TOOL), px(TOOL)], | |
| 111 | icon: Some(icon), | |
| 112 | color: Some(tint), | |
| 113 | hover_fill: Some(hover), | |
| 114 | radius: 4.0, | |
| 115 | center: true, | |
| 116 | ..Spec::default() | |
| 117 | }, | |
| 118 | ) | |
| 119 | } | |
| 120 | ||
| 121 | /// A button showing `icon` with a menu arrow beside it, and optionally the colour it | |
| 122 | /// applies as a bar under the icon. Returns the button's and the arrow's signals. | |
| 123 | pub fn split_button( | |
| 124 | ui: &mut Ui, | |
| 125 | part: impl Hash, | |
| 126 | icon: &'static [&'static str], | |
| 127 | bar: Option<[f32; 4]>, | |
| 128 | ) -> [Signal; 2] { | |
| 129 | let theme = ui.theme.clone(); | |
| 130 | ui.open( | |
| 131 | part, | |
| 132 | Spec { | |
| 133 | size: [children(), px(TOOL)], | |
| 134 | ..Spec::default() | |
| 135 | }, | |
| 136 | ); | |
| 137 | let button = ui.open( | |
| 138 | "button", | |
| 139 | Spec { | |
| 140 | flags: Flags::CLICKABLE, | |
| 141 | size: [px(TOOL), px(TOOL)], | |
| 142 | icon: Some(icon), | |
| 143 | color: Some(theme.text), | |
| 144 | hover_fill: Some(theme.hover()), | |
| 145 | radius: 4.0, | |
| 146 | center: true, | |
| 147 | ..Spec::default() | |
| 148 | }, | |
| 149 | ); | |
| 150 | if let Some(color) = bar { | |
| 151 | // The icon leaves its lowest three units for the bar. | |
| 152 | let top = (TOOL - 16.0) / 2.0; | |
| 153 | ui.mark([top, top + 13.0, top + 16.0, top + 16.0], color); | |
| 154 | } | |
| 155 | ui.close(); | |
| 156 | let menu = ui.leaf( | |
| 157 | "menu", | |
| 158 | Spec { | |
| 159 | flags: Flags::CLICKABLE, | |
| 160 | size: [px(10.0), px(TOOL)], | |
| 161 | icon: Some(CHEVRON), | |
| 162 | color: Some(theme.text_dim), | |
| 163 | hover_fill: Some(theme.hover()), | |
| 164 | radius: 4.0, | |
| 165 | center: true, | |
| 166 | ..Spec::default() | |
| 167 | }, | |
| 168 | ); | |
| 169 | ui.close(); | |
| 170 | [ui.signal(button), menu] | |
| 171 | } | |
| 172 | ||
| 173 | /// A drop-down box `width` wide showing `text`. | |
| 174 | pub fn combo(ui: &mut Ui, part: impl Hash, text: &str, width: f32) -> Signal { | |
| 175 | let theme = ui.theme.clone(); | |
| 176 | let id = ui.open( | |
| 177 | part, | |
| 178 | Spec { | |
| 179 | flags: Flags::CLICKABLE, | |
| 180 | size: [px(width), px(TOOL)], | |
| 181 | fill: Some(theme.base), | |
| 182 | hover_border: Some(theme.accent), | |
| 183 | border: Some(theme.chip), | |
| 184 | radius: 4.0, | |
| 185 | pad: [8.0, 0.0], | |
| 186 | ..Spec::default() | |
| 187 | }, | |
| 188 | ); | |
| 189 | ui.leaf( | |
| 190 | "text", | |
| 191 | Spec { | |
| 192 | size: [fill(), px(TOOL)], | |
| 193 | text: Some(text), | |
| 194 | ..Spec::default() | |
| 195 | }, | |
| 196 | ); | |
| 197 | ui.leaf( | |
| 198 | "arrow", | |
| 199 | Spec { | |
| 200 | size: [px(12.0), px(TOOL)], | |
| 201 | icon: Some(CHEVRON), | |
| 202 | color: Some(theme.text_dim), | |
| 203 | center: true, | |
| 204 | ..Spec::default() | |
| 205 | }, | |
| 206 | ); | |
| 207 | ui.close(); | |
| 208 | ui.signal(id) | |
| 209 | } | |
| 210 | ||
| 211 | impl Ui { | |
| 212 | /// Draws a 1 px border along `points`, each with the radius its corner rounds by, | |
| 213 | /// joining the last back to the first when `closed`. Called between `end` and | |
| 214 | /// `layers`, it paints over the frame's boxes where they were just laid out. | |
| 215 | pub fn border(&mut self, points: &[([f32; 2], f32)], closed: bool, color: [f32; 4]) { | |
| 216 | let count = points.len(); | |
| 217 | let corners: Vec<_> = (0..count) | |
| 218 | .map(|index| { | |
| 219 | let open_end = !closed && (index == 0 || index == count - 1); | |
| 220 | (!open_end && points[index].1 > 0.0).then(|| Corner::new(points, index)) | |
| 221 | }) | |
| 222 | .collect(); | |
| 223 | for index in 0..count - usize::from(!closed) { | |
| 224 | let next = (index + 1) % count; | |
| 225 | let from = corners[index] | |
| 226 | .as_ref() | |
| 227 | .map_or(points[index].0, |corner| corner.end); | |
| 228 | let to = corners[next] | |
| 229 | .as_ref() | |
| 230 | .map_or(points[next].0, |corner| corner.start); | |
| 231 | self.display.push(Display::Segment { from, to, color }); | |
| 232 | } | |
| 233 | for corner in corners.into_iter().flatten() { | |
| 234 | let start = [ | |
| 235 | corner.start[0] - corner.point[0], | |
| 236 | corner.start[1] - corner.point[1], | |
| 237 | ]; | |
| 238 | self.display.push(Display::Path { | |
| 239 | data: format!("M{} {}{}", start[0], start[1], corner.curve(corner.point)), | |
| 240 | origin: corner.point, | |
| 241 | style: PathStyle::Stroke(1.0), | |
| 242 | colors: [color; 2], | |
| 243 | }); | |
| 244 | } | |
| 245 | } | |
| 246 | ||
| 247 | /// Rounds the corners of the clockwise outline `points`, as `border` does: paints | |
| 248 | /// `outside(y)` beyond each outward corner at height `y`, and `inside` into each | |
| 249 | /// inward one. Called between `end` and `layers`. | |
| 250 | pub fn round_corners( | |
| 251 | &mut self, | |
| 252 | points: &[([f32; 2], f32)], | |
| 253 | inside: [f32; 4], | |
| 254 | outside: impl Fn(f32) -> [f32; 4], | |
| 255 | ) { | |
| 256 | for index in 0..points.len() { | |
| 257 | if points[index].1 <= 0.0 { | |
| 258 | continue; | |
| 259 | } | |
| 260 | let corner = Corner::new(points, index); | |
| 261 | let start = [ | |
| 262 | corner.start[0] - corner.point[0], | |
| 263 | corner.start[1] - corner.point[1], | |
| 264 | ]; | |
| 265 | self.display.push(Display::Path { | |
| 266 | data: format!( | |
| 267 | "M{} {}{}L0 0Z", | |
| 268 | start[0], | |
| 269 | start[1], | |
| 270 | corner.curve(corner.point) | |
| 271 | ), | |
| 272 | origin: corner.point, | |
| 273 | style: PathStyle::Fill, | |
| 274 | colors: [if corner.outward { | |
| 275 | outside(corner.point[1]) | |
| 276 | } else { | |
| 277 | inside | |
| 278 | }; 2], | |
| 279 | }); | |
| 280 | } | |
| 281 | } | |
| 282 | } |
crates/ui/src/tests.rs+192-48| ... | ... | @@ -2,6 +2,8 @@ use super::*; |
| 2 | 2 | use std::time::Duration; |
| 3 | 3 | use winit::keyboard::NamedKey; |
| 4 | 4 | |
| 5 | const DOUBLE_CLICK: Duration = Duration::from_millis(500); | |
| 6 | ||
| 5 | 7 | thread_local! { |
| 6 | 8 | static START: Instant = Instant::now(); |
| 7 | 9 | static FRAMES: std::cell::Cell<u32> = const { std::cell::Cell::new(0) }; |
| ... | ... | @@ -21,7 +23,7 @@ fn frame(ui: &mut Ui, build: impl FnOnce(&mut Ui)) { |
| 21 | 23 | |
| 22 | 24 | #[test] |
| 23 | 25 | fn fill_yields_to_strict_siblings_along_the_flow() { |
| 24 | let mut ui = Ui::new(Theme::dark()); | |
| 26 | let mut ui = Ui::new(Theme::dark(), DOUBLE_CLICK); | |
| 25 | 27 | let mut ids = Vec::new(); |
| 26 | 28 | frame(&mut ui, |ui| { |
| 27 | 29 | ui.open( |
| ... | ... | @@ -55,7 +57,7 @@ fn fill_yields_to_strict_siblings_along_the_flow() { |
| 55 | 57 | |
| 56 | 58 | #[test] |
| 57 | 59 | fn children_sum_with_gaps_and_padding() { |
| 58 | let mut ui = Ui::new(Theme::dark()); | |
| 60 | let mut ui = Ui::new(Theme::dark(), DOUBLE_CLICK); | |
| 59 | 61 | let mut column = None; |
| 60 | 62 | let mut second = None; |
| 61 | 63 | frame(&mut ui, |ui| { |
| ... | ... | @@ -107,7 +109,7 @@ fn button_frame(ui: &mut Ui) -> Signal { |
| 107 | 109 | |
| 108 | 110 | #[test] |
| 109 | 111 | fn clicks_use_the_previous_layout_and_hover_animates() { |
| 110 | let mut ui = Ui::new(Theme::dark()); | |
| 112 | let mut ui = Ui::new(Theme::dark(), DOUBLE_CLICK); | |
| 111 | 113 | button_frame(&mut ui); |
| 112 | 114 | let at = Instant::now(); |
| 113 | 115 | ui.event(Event::PointerMoved([10.0, 10.0])); |
| ... | ... | @@ -147,9 +149,28 @@ fn clicks_use_the_previous_layout_and_hover_animates() { |
| 147 | 149 | assert!(!ui.wants_frame()); |
| 148 | 150 | } |
| 149 | 151 | |
| 152 | #[test] | |
| 153 | fn hover_fades_its_own_colour_in_over_a_box_without_a_fill() { | |
| 154 | let mut ui = Ui::new(Theme::dark(), DOUBLE_CLICK); | |
| 155 | button_frame(&mut ui); | |
| 156 | ui.event(Event::PointerMoved([10.0, 10.0])); | |
| 157 | button_frame(&mut ui); | |
| 158 | let fill = ui.display.iter().find_map(|item| match item { | |
| 159 | Display::Rect { fill, .. } => Some(*fill), | |
| 160 | _ => None, | |
| 161 | }); | |
| 162 | let [red, green, blue, alpha] = fill.expect("the hovered button paints"); | |
| 163 | assert_eq!( | |
| 164 | [red, green, blue], | |
| 165 | [1.0; 3], | |
| 166 | "no darker colour on the way in" | |
| 167 | ); | |
| 168 | assert!(alpha > 0.0 && alpha < 1.0); | |
| 169 | } | |
| 170 | ||
| 150 | 171 | #[test] |
| 151 | 172 | fn custom_boxes_receive_their_events_and_a_leave_when_the_pointer_moves_off() { |
| 152 | let mut ui = Ui::new(Theme::dark()); | |
| 173 | let mut ui = Ui::new(Theme::dark(), DOUBLE_CLICK); | |
| 153 | 174 | let build = |ui: &mut Ui| { |
| 154 | 175 | let mut signals = Vec::new(); |
| 155 | 176 | frame(ui, |ui| { |
| ... | ... | @@ -225,7 +246,7 @@ fn custom_boxes_receive_their_events_and_a_leave_when_the_pointer_moves_off() { |
| 225 | 246 | |
| 226 | 247 | #[test] |
| 227 | 248 | fn wheel_scrolls_within_the_content_and_clips_children() { |
| 228 | let mut ui = Ui::new(Theme::dark()); | |
| 249 | let mut ui = Ui::new(Theme::dark(), DOUBLE_CLICK); | |
| 229 | 250 | let mut list = None; |
| 230 | 251 | let mut last = None; |
| 231 | 252 | let mut build = |ui: &mut Ui| { |
| ... | ... | @@ -271,69 +292,192 @@ fn wheel_scrolls_within_the_content_and_clips_children() { |
| 271 | 292 | )); |
| 272 | 293 | } |
| 273 | 294 | |
| 274 | #[test] | |
| 275 | fn text_fields_edit_with_keys_and_selection() { | |
| 276 | let mut ui = Ui::new(Theme::dark()); | |
| 277 | let mut text = String::from("café"); | |
| 278 | let build = |ui: &mut Ui, text: &mut String| { | |
| 279 | let mut signal = Signal::default(); | |
| 280 | frame(ui, |ui| { | |
| 281 | signal = text_field( | |
| 282 | ui, | |
| 283 | Id::ROOT.child("filter"), | |
| 284 | text, | |
| 285 | "Filter", | |
| 286 | Spec { | |
| 287 | size: [px(200.0), px(26.0)], | |
| 288 | pad: [6.0, 0.0], | |
| 289 | ..Spec::default() | |
| 290 | }, | |
| 291 | ); | |
| 292 | }); | |
| 293 | signal | |
| 294 | }; | |
| 295 | build(&mut ui, &mut text); | |
| 296 | let id = Id::ROOT.child("filter"); | |
| 297 | ui.set_focus(Some(id)); | |
| 298 | let key = |named| Event::Key { | |
| 295 | fn field_frame(ui: &mut Ui, text: &mut String) -> Signal { | |
| 296 | let mut signal = Signal::default(); | |
| 297 | frame(ui, |ui| { | |
| 298 | signal = text_field( | |
| 299 | ui, | |
| 300 | field(), | |
| 301 | text, | |
| 302 | "Filter", | |
| 303 | Spec { | |
| 304 | size: [px(200.0), px(26.0)], | |
| 305 | pad: [6.0, 0.0], | |
| 306 | ..Spec::default() | |
| 307 | }, | |
| 308 | ); | |
| 309 | }); | |
| 310 | signal | |
| 311 | } | |
| 312 | ||
| 313 | fn field() -> Id { | |
| 314 | Id::ROOT.child("filter") | |
| 315 | } | |
| 316 | ||
| 317 | fn key(named: NamedKey) -> Event { | |
| 318 | Event::Key { | |
| 299 | 319 | key: Key::Named(named), |
| 300 | 320 | text: None, |
| 301 | }; | |
| 302 | ui.event(Event::Key { | |
| 303 | key: Key::Named(NamedKey::End), | |
| 304 | text: None, | |
| 321 | } | |
| 322 | } | |
| 323 | ||
| 324 | fn typed(text: &str) -> Event { | |
| 325 | Event::Key { | |
| 326 | key: Key::Character(text.into()), | |
| 327 | text: Some(text.into()), | |
| 328 | } | |
| 329 | } | |
| 330 | ||
| 331 | /// A focused field holding `text`. | |
| 332 | fn focused_field(text: &str) -> (Ui, String) { | |
| 333 | let mut ui = Ui::new(Theme::dark(), DOUBLE_CLICK); | |
| 334 | let mut text = text.to_owned(); | |
| 335 | field_frame(&mut ui, &mut text); | |
| 336 | ui.set_focus(Some(field())); | |
| 337 | (ui, text) | |
| 338 | } | |
| 339 | ||
| 340 | /// Moves the pointer over the caret before byte `index` of the field's `text`. | |
| 341 | fn point_to(ui: &mut Ui, text: &str, index: usize) { | |
| 342 | let size = ui.theme.font_size; | |
| 343 | let (texts, frame) = ui.texts(); | |
| 344 | let layout = &texts.label(text, size, frame).layout; | |
| 345 | let x = parley::editing::Cursor::from_byte_index(layout, index, parley::Affinity::Downstream) | |
| 346 | .geometry(layout, 1.0) | |
| 347 | .x0 as f32; | |
| 348 | ui.event(Event::PointerMoved([6.0 + x, 13.0])); | |
| 349 | } | |
| 350 | ||
| 351 | fn press(ui: &mut Ui, at: Instant, pressed: bool) { | |
| 352 | ui.event(Event::Button { | |
| 353 | button: MouseButton::Left, | |
| 354 | pressed, | |
| 355 | at, | |
| 305 | 356 | }); |
| 357 | } | |
| 358 | ||
| 359 | #[test] | |
| 360 | fn text_fields_edit_with_keys_and_selection() { | |
| 361 | let (mut ui, mut text) = focused_field("café"); | |
| 362 | ui.event(key(NamedKey::End)); | |
| 306 | 363 | ui.event(key(NamedKey::Backspace)); |
| 307 | ui.event(Event::Key { | |
| 308 | key: Key::Character("e".into()), | |
| 309 | text: Some("e".into()), | |
| 310 | }); | |
| 311 | build(&mut ui, &mut text); | |
| 364 | ui.event(typed("e")); | |
| 365 | field_frame(&mut ui, &mut text); | |
| 312 | 366 | assert_eq!(text, "cafe"); |
| 313 | 367 | ui.event(Event::Modifiers(ModifiersState::SHIFT)); |
| 314 | 368 | ui.event(key(NamedKey::ArrowLeft)); |
| 315 | 369 | ui.event(key(NamedKey::ArrowLeft)); |
| 316 | build(&mut ui, &mut text); | |
| 370 | field_frame(&mut ui, &mut text); | |
| 317 | 371 | ui.event(Event::Modifiers(ModifiersState::empty())); |
| 318 | 372 | ui.event(Event::Ime(Ime::Commit("é".into()))); |
| 319 | build(&mut ui, &mut text); | |
| 373 | field_frame(&mut ui, &mut text); | |
| 320 | 374 | assert_eq!(text, "caé"); |
| 321 | 375 | ui.event(Event::Modifiers(ModifiersState::SUPER)); |
| 322 | ui.event(Event::Key { | |
| 323 | key: Key::Character("a".into()), | |
| 324 | text: Some("a".into()), | |
| 325 | }); | |
| 326 | build(&mut ui, &mut text); | |
| 376 | ui.event(typed("a")); | |
| 377 | field_frame(&mut ui, &mut text); | |
| 327 | 378 | ui.event(Event::Modifiers(ModifiersState::empty())); |
| 328 | 379 | ui.event(key(NamedKey::Delete)); |
| 329 | let signal = build(&mut ui, &mut text); | |
| 380 | let signal = field_frame(&mut ui, &mut text); | |
| 330 | 381 | assert!(signal.focused); |
| 331 | 382 | assert_eq!(text, ""); |
| 332 | 383 | } |
| 333 | 384 | |
| 385 | #[test] | |
| 386 | fn text_fields_move_and_select_by_word() { | |
| 387 | let (mut ui, mut text) = focused_field("one two three"); | |
| 388 | ui.event(key(NamedKey::End)); | |
| 389 | ui.event(Event::Modifiers(ModifiersState::ALT)); | |
| 390 | ui.event(key(NamedKey::ArrowLeft)); | |
| 391 | ui.event(key(NamedKey::ArrowLeft)); | |
| 392 | field_frame(&mut ui, &mut text); | |
| 393 | ui.event(Event::Modifiers( | |
| 394 | ModifiersState::ALT | ModifiersState::SHIFT, | |
| 395 | )); | |
| 396 | ui.event(key(NamedKey::ArrowRight)); | |
| 397 | field_frame(&mut ui, &mut text); | |
| 398 | ui.event(Event::Modifiers(ModifiersState::empty())); | |
| 399 | ui.event(typed("2")); | |
| 400 | field_frame(&mut ui, &mut text); | |
| 401 | assert_eq!(text, "one 2 three"); | |
| 402 | } | |
| 403 | ||
| 404 | #[test] | |
| 405 | fn text_fields_delete_by_word_and_line_chords() { | |
| 406 | let (mut ui, mut text) = focused_field("one two three"); | |
| 407 | ui.event(key(NamedKey::End)); | |
| 408 | ui.event(Event::Modifiers(ModifiersState::ALT)); | |
| 409 | ui.event(key(NamedKey::Backspace)); | |
| 410 | field_frame(&mut ui, &mut text); | |
| 411 | assert_eq!(text, "one two "); | |
| 412 | ui.event(Event::Modifiers(ModifiersState::CONTROL)); | |
| 413 | ui.event(typed("b")); | |
| 414 | ui.event(typed("k")); | |
| 415 | field_frame(&mut ui, &mut text); | |
| 416 | assert_eq!(text, "one two"); | |
| 417 | ui.event(Event::Modifiers(ModifiersState::SUPER)); | |
| 418 | ui.event(key(NamedKey::Backspace)); | |
| 419 | field_frame(&mut ui, &mut text); | |
| 420 | assert_eq!(text, ""); | |
| 421 | } | |
| 422 | ||
| 423 | #[test] | |
| 424 | fn text_fields_double_press_selects_a_word_and_drags_by_words() { | |
| 425 | let (mut ui, mut text) = focused_field("one two three"); | |
| 426 | let at = Instant::now(); | |
| 427 | point_to(&mut ui, &text, 5); | |
| 428 | press(&mut ui, at, true); | |
| 429 | press(&mut ui, at, false); | |
| 430 | field_frame(&mut ui, &mut text); | |
| 431 | press(&mut ui, at + Duration::from_millis(100), true); | |
| 432 | field_frame(&mut ui, &mut text); | |
| 433 | assert_eq!(ui.states[&field()].selection.text_range(), 4..7); | |
| 434 | point_to(&mut ui, &text, 10); | |
| 435 | field_frame(&mut ui, &mut text); | |
| 436 | assert_eq!(ui.states[&field()].selection.text_range(), 4..13); | |
| 437 | point_to(&mut ui, &text, 1); | |
| 438 | field_frame(&mut ui, &mut text); | |
| 439 | press(&mut ui, at + Duration::from_millis(200), false); | |
| 440 | field_frame(&mut ui, &mut text); | |
| 441 | let selection = ui.states[&field()].selection; | |
| 442 | assert_eq!( | |
| 443 | (selection.anchor().index(), selection.focus().index()), | |
| 444 | (7, 0), | |
| 445 | "reversing keeps the pressed word" | |
| 446 | ); | |
| 447 | } | |
| 448 | ||
| 449 | #[test] | |
| 450 | fn text_fields_drag_select_and_shift_press_extends() { | |
| 451 | let (mut ui, mut text) = focused_field("one two three"); | |
| 452 | let at = Instant::now(); | |
| 453 | point_to(&mut ui, &text, 4); | |
| 454 | press(&mut ui, at, true); | |
| 455 | field_frame(&mut ui, &mut text); | |
| 456 | point_to(&mut ui, &text, 7); | |
| 457 | field_frame(&mut ui, &mut text); | |
| 458 | press(&mut ui, at, false); | |
| 459 | ui.event(Event::Modifiers(ModifiersState::SHIFT)); | |
| 460 | field_frame(&mut ui, &mut text); | |
| 461 | assert_eq!(ui.states[&field()].selection.text_range(), 4..7); | |
| 462 | point_to(&mut ui, &text, 13); | |
| 463 | press(&mut ui, at + Duration::from_secs(1), true); | |
| 464 | press(&mut ui, at + Duration::from_secs(1), false); | |
| 465 | field_frame(&mut ui, &mut text); | |
| 466 | assert_eq!(ui.states[&field()].selection.text_range(), 4..13); | |
| 467 | ui.event(Event::Modifiers(ModifiersState::empty())); | |
| 468 | field_frame(&mut ui, &mut text); | |
| 469 | ui.event(key(NamedKey::ArrowLeft)); | |
| 470 | ui.event(typed("_")); | |
| 471 | field_frame(&mut ui, &mut text); | |
| 472 | assert_eq!( | |
| 473 | text, "one _two three", | |
| 474 | "Left collapses to the selection's start" | |
| 475 | ); | |
| 476 | } | |
| 477 | ||
| 334 | 478 | #[test] |
| 335 | 479 | fn scrollbar_thumbs_track_the_offset_and_drags_reach_both_ends() { |
| 336 | let mut ui = Ui::new(Theme::dark()); | |
| 480 | let mut ui = Ui::new(Theme::dark(), DOUBLE_CLICK); | |
| 337 | 481 | let mut offset = 0.0; |
| 338 | 482 | let build = |ui: &mut Ui, offset: &mut f32| { |
| 339 | 483 | frame(ui, |ui| { |
crates/ui/src/text.rs+30-30| ... | ... | @@ -1,35 +1,23 @@ |
| 1 | 1 | use draw::{GlyphRun, Glyphs, RenderError}; |
| 2 | 2 | use parley::{ |
| 3 | Affinity, FontContext, FontFamily, GenericFamily, Layout, LayoutContext, PositionedLayoutItem, | |
| 4 | StyleProperty, editing::Cursor, | |
| 3 | FontContext, FontFamily, FontFamilyName, GenericFamily, Layout, LayoutContext, | |
| 4 | PositionedLayoutItem, StyleProperty, fontique::Blob, | |
| 5 | 5 | }; |
| 6 | use std::{collections::HashMap, rc::Rc}; | |
| 6 | use std::{borrow::Cow, collections::HashMap, rc::Rc, sync::Arc}; | |
| 7 | 7 | |
| 8 | 8 | /// A shaped single line of interface text, in logical pixels. |
| 9 | 9 | pub(crate) struct Label { |
| 10 | layout: Layout<()>, | |
| 10 | pub layout: Layout<()>, | |
| 11 | 11 | pub size: [f32; 2], |
| 12 | 12 | } |
| 13 | 13 | |
| 14 | impl Label { | |
| 15 | /// The caret's left edge before byte `index`. | |
| 16 | pub fn caret_x(&self, index: usize) -> f32 { | |
| 17 | Cursor::from_byte_index(&self.layout, index, Affinity::Downstream) | |
| 18 | .geometry(&self.layout, 1.0) | |
| 19 | .x0 as f32 | |
| 20 | } | |
| 21 | ||
| 22 | /// The byte index whose caret position is nearest `x`. | |
| 23 | pub fn index_at(&self, x: f32) -> usize { | |
| 24 | Cursor::from_point(&self.layout, x, self.size[1] / 2.0).index() | |
| 25 | } | |
| 26 | } | |
| 27 | ||
| 28 | 14 | /// Labels shaped this frame or the previous one, keyed by their text and size. |
| 29 | 15 | #[derive(Default)] |
| 30 | 16 | pub(crate) struct Texts { |
| 31 | 17 | fonts: FontContext, |
| 32 | 18 | context: LayoutContext<()>, |
| 19 | /// A family registered in place of the system's interface font. | |
| 20 | family: Option<String>, | |
| 33 | 21 | cache: HashMap<(String, u32), (Rc<Label>, u64)>, |
| 34 | 22 | } |
| 35 | 23 | |
| ... | ... | @@ -44,9 +32,13 @@ impl Texts { |
| 44 | 32 | let mut builder = self |
| 45 | 33 | .context |
| 46 | 34 | .ranged_builder(&mut self.fonts, text, 1.0, false); |
| 47 | builder.push_default(StyleProperty::FontFamily(FontFamily::from( | |
| 48 | GenericFamily::SystemUi, | |
| 49 | ))); | |
| 35 | let system = FontFamilyName::Generic(GenericFamily::SystemUi); | |
| 36 | builder.push_default(StyleProperty::FontFamily(match &self.family { | |
| 37 | Some(family) => { | |
| 38 | FontFamily::List(Cow::Owned(vec![FontFamilyName::named(family), system])) | |
| 39 | } | |
| 40 | None => FontFamily::Single(system), | |
| 41 | })); | |
| 50 | 42 | builder.push_default(StyleProperty::FontSize(size)); |
| 51 | 43 | let mut layout = builder.build(text); |
| 52 | 44 | layout.break_all_lines(None); |
| ... | ... | @@ -56,23 +48,31 @@ impl Texts { |
| 56 | 48 | label |
| 57 | 49 | } |
| 58 | 50 | |
| 51 | /// Shapes labels in the family `files` define, falling back to the system's | |
| 52 | /// interface font; returns the family's name, or None when the files hold no font. | |
| 53 | pub fn use_fonts(&mut self, files: impl IntoIterator<Item = Vec<u8>>) -> Option<String> { | |
| 54 | let collection = &mut self.fonts.collection; | |
| 55 | let families: Vec<_> = files | |
| 56 | .into_iter() | |
| 57 | .flat_map(|file| collection.register_fonts(Blob::new(Arc::new(file)), None)) | |
| 58 | .collect(); | |
| 59 | let family = collection.family_name(families.first()?.0)?.to_owned(); | |
| 60 | self.family = Some(family.clone()); | |
| 61 | self.cache.clear(); | |
| 62 | Some(family) | |
| 63 | } | |
| 64 | ||
| 59 | 65 | pub fn prune(&mut self, frame: u64) { |
| 60 | 66 | self.cache.retain(|_, (_, touched)| *touched + 1 >= frame); |
| 61 | 67 | } |
| 62 | 68 | } |
| 63 | 69 | |
| 64 | /// A label painted in one colour this frame. | |
| 65 | pub(crate) struct Painted { | |
| 66 | pub label: Rc<Label>, | |
| 67 | pub color: [f32; 4], | |
| 68 | } | |
| 69 | ||
| 70 | impl Glyphs for Painted { | |
| 70 | impl Glyphs for Label { | |
| 71 | 71 | fn runs( |
| 72 | 72 | &self, |
| 73 | 73 | paint: &mut dyn FnMut(GlyphRun<'_>) -> Result<(), RenderError>, |
| 74 | 74 | ) -> Result<(), RenderError> { |
| 75 | for line in self.label.layout.lines() { | |
| 75 | for line in self.layout.lines() { | |
| 76 | 76 | let metrics = line.metrics(); |
| 77 | 77 | for item in line.items() { |
| 78 | 78 | if let PositionedLayoutItem::GlyphRun(run) = item { |
| ... | ... | @@ -81,7 +81,7 @@ impl Glyphs for Painted { |
| 81 | 81 | metrics.baseline, |
| 82 | 82 | 0.0, |
| 83 | 83 | [metrics.block_min_coord, metrics.line_height], |
| 84 | |_| self.color, | |
| 84 | |_| None, | |
| 85 | 85 | paint, |
| 86 | 86 | )?; |
| 87 | 87 | } |
crates/ui/src/theme.rs+137-9| ... | ... | @@ -4,21 +4,47 @@ use draw::srgb; |
| 4 | 4 | #[derive(Clone, Debug)] |
| 5 | 5 | pub struct Theme { |
| 6 | 6 | pub font_size: f32, |
| 7 | /// Content surfaces, such as the active tab and lists. | |
| 7 | /// Content surfaces, such as fields and lists. | |
| 8 | 8 | pub base: [f32; 4], |
| 9 | 9 | /// Side panels. |
| 10 | 10 | pub panel: [f32; 4], |
| 11 | /// The window's top strip and inactive tabs. | |
| 11 | /// The window's title bar, toolbar and tab row. | |
| 12 | 12 | pub strip: [f32; 4], |
| 13 | /// Hairlines between rows and panels. | |
| 14 | pub separator: [f32; 4], | |
| 15 | 13 | pub accent: [f32; 4], |
| 16 | /// A hovered row under the pointer, outlined in the accent. | |
| 17 | pub hover: [f32; 4], | |
| 18 | 14 | pub text: [f32; 4], |
| 19 | 15 | pub text_dim: [f32; 4], |
| 20 | /// Key caps and small controls. | |
| 16 | /// Text on section colours. | |
| 17 | pub ink: [f32; 4], | |
| 18 | /// The page's colour, which the open page's tab shares. | |
| 19 | pub paper: [f32; 4], | |
| 20 | /// Text on the paper, and on the page wherever its colour is left automatic. | |
| 21 | pub paper_ink: [f32; 4], | |
| 22 | /// Key caps, small controls and field borders. | |
| 21 | 23 | pub chip: [f32; 4], |
| 24 | /// Saturation and lightness each of a section's colours takes from its hue. | |
| 25 | pub shades: Shades, | |
| 26 | } | |
| 27 | ||
| 28 | /// Saturation and lightness pairs, each from 0 to 1. | |
| 29 | #[derive(Clone, Copy, Debug)] | |
| 30 | pub struct Shades { | |
| 31 | /// The frame's top and bottom; the bottom turns 10° further round the hue. | |
| 32 | pub frame: [[f32; 2]; 2], | |
| 33 | pub tab: [f32; 2], | |
| 34 | pub edge: [f32; 2], | |
| 35 | pub accent: [f32; 2], | |
| 36 | } | |
| 37 | ||
| 38 | /// A section's colours in a theme. | |
| 39 | #[derive(Clone, Copy, Debug)] | |
| 40 | pub struct Section { | |
| 41 | /// Top and bottom of the frame around the page, which the open tab shares. | |
| 42 | pub frame: [[f32; 4]; 2], | |
| 43 | /// Tabs not open. | |
| 44 | pub tab: [f32; 4], | |
| 45 | /// Borders around the open tab and the page. | |
| 46 | pub edge: [f32; 4], | |
| 47 | pub accent: [f32; 4], | |
| 22 | 48 | } |
| 23 | 49 | |
| 24 | 50 | impl Theme { |
| ... | ... | @@ -29,12 +55,114 @@ impl Theme { |
| 29 | 55 | base: srgb(0x19, 0x1b, 0x1c), |
| 30 | 56 | panel: srgb(0x1f, 0x22, 0x23), |
| 31 | 57 | strip: srgb(0x27, 0x2a, 0x2b), |
| 32 | separator: srgb(0x20, 0x23, 0x24), | |
| 33 | 58 | accent: srgb(0x00, 0x79, 0xa6), |
| 34 | hover: srgb(0x10, 0x3c, 0x4c), | |
| 35 | 59 | text: srgb(0xdd, 0xde, 0xe0), |
| 36 | 60 | text_dim: srgb(0x6b, 0x70, 0x78), |
| 61 | ink: srgb(0xe8, 0xe9, 0xeb), | |
| 62 | paper: srgb(0x1f, 0x20, 0x22), | |
| 63 | paper_ink: srgb(0xe6, 0xe6, 0xe6), | |
| 37 | 64 | chip: srgb(0x38, 0x3c, 0x3d), |
| 65 | shades: Shades { | |
| 66 | frame: [[0.30, 0.36], [0.30, 0.30]], | |
| 67 | tab: [0.22, 0.25], | |
| 68 | edge: [0.30, 0.16], | |
| 69 | accent: [0.55, 0.55], | |
| 70 | }, | |
| 71 | } | |
| 72 | } | |
| 73 | ||
| 74 | pub fn light() -> Self { | |
| 75 | Self { | |
| 76 | font_size: 13.0, | |
| 77 | base: srgb(0xfc, 0xfc, 0xfd), | |
| 78 | panel: srgb(0xf4, 0xf5, 0xf7), | |
| 79 | strip: srgb(0xeb, 0xed, 0xf0), | |
| 80 | accent: srgb(0x00, 0x79, 0xa6), | |
| 81 | text: srgb(0x1f, 0x23, 0x28), | |
| 82 | text_dim: srgb(0x7b, 0x82, 0x8c), | |
| 83 | ink: srgb(0x1d, 0x1e, 0x20), | |
| 84 | paper: [1.0; 4], | |
| 85 | paper_ink: [0.0, 0.0, 0.0, 1.0], | |
| 86 | chip: srgb(0xcf, 0xd3, 0xd9), | |
| 87 | shades: Shades { | |
| 88 | frame: [[0.55, 0.82], [0.55, 0.76]], | |
| 89 | tab: [0.45, 0.70], | |
| 90 | edge: [0.30, 0.52], | |
| 91 | accent: [0.60, 0.45], | |
| 92 | }, | |
| 93 | } | |
| 94 | } | |
| 95 | ||
| 96 | /// Hovered controls and selected text: the accent over the base. | |
| 97 | pub fn hover(&self) -> [f32; 4] { | |
| 98 | crate::mix(self.base, self.accent, 0.35) | |
| 99 | } | |
| 100 | ||
| 101 | /// The colours a section takes from the hue of `color`, linear RGBA. | |
| 102 | pub fn section(&self, color: [f32; 4]) -> Section { | |
| 103 | let hue = hue(color); | |
| 104 | let shade = |hue: f32, [saturation, lightness]: [f32; 2]| hsl(hue, saturation, lightness); | |
| 105 | let Shades { | |
| 106 | frame, | |
| 107 | tab, | |
| 108 | edge, | |
| 109 | accent, | |
| 110 | } = self.shades; | |
| 111 | Section { | |
| 112 | frame: [shade(hue, frame[0]), shade(hue + 10.0, frame[1])], | |
| 113 | tab: shade(hue, tab), | |
| 114 | edge: shade(hue, edge), | |
| 115 | accent: shade(hue, accent), | |
| 116 | } | |
| 117 | } | |
| 118 | } | |
| 119 | ||
| 120 | /// The hue in degrees of a linear colour, as it looks in sRGB. | |
| 121 | fn hue(color: [f32; 4]) -> f32 { | |
| 122 | let [r, g, b] = [color[0], color[1], color[2]].map(|value| { | |
| 123 | if value <= 0.003_130_8 { | |
| 124 | value * 12.92 | |
| 125 | } else { | |
| 126 | 1.055 * value.powf(1.0 / 2.4) - 0.055 | |
| 38 | 127 | } |
| 128 | }); | |
| 129 | let max = r.max(g).max(b); | |
| 130 | let range = max - r.min(g).min(b); | |
| 131 | if range == 0.0 { | |
| 132 | return 0.0; | |
| 133 | } | |
| 134 | let sector = if max == r { | |
| 135 | (g - b) / range | |
| 136 | } else if max == g { | |
| 137 | (b - r) / range + 2.0 | |
| 138 | } else { | |
| 139 | (r - g) / range + 4.0 | |
| 140 | }; | |
| 141 | (sector * 60.0).rem_euclid(360.0) | |
| 142 | } | |
| 143 | ||
| 144 | /// An sRGB hue, saturation and lightness as linear RGBA. | |
| 145 | fn hsl(hue: f32, saturation: f32, lightness: f32) -> [f32; 4] { | |
| 146 | let chroma = (1.0 - (2.0 * lightness - 1.0).abs()) * saturation; | |
| 147 | let channel = |offset: f32| { | |
| 148 | let k = (offset + hue / 30.0).rem_euclid(12.0); | |
| 149 | let value = lightness - chroma / 2.0 * (k - 3.0).min(9.0 - k).clamp(-1.0, 1.0); | |
| 150 | (value * 255.0).round().clamp(0.0, 255.0) as u8 | |
| 151 | }; | |
| 152 | srgb(channel(0.0), channel(8.0), channel(4.0)) | |
| 153 | } | |
| 154 | ||
| 155 | #[cfg(test)] | |
| 156 | mod tests { | |
| 157 | use super::*; | |
| 158 | ||
| 159 | #[test] | |
| 160 | fn sections_keep_their_hue_at_the_theme_s_shades() { | |
| 161 | let orange = srgb(0xf6, 0xb0, 0x78); | |
| 162 | let section = Theme::dark().section(orange); | |
| 163 | assert!((hue(section.frame[0]) - hue(orange)).abs() < 3.0); | |
| 164 | assert!((hue(section.frame[1]) - hue(orange) - 10.0).abs() < 3.0); | |
| 165 | assert_eq!(hsl(0.0, 1.0, 0.5), srgb(0xff, 0, 0)); | |
| 166 | assert_eq!(hsl(240.0, 0.0, 1.0), srgb(0xff, 0xff, 0xff)); | |
| 39 | 167 | } |
| 40 | 168 | } |
crates/ui/src/widgets.rs+147-106| ... | ... | @@ -1,8 +1,13 @@ |
| 1 | 1 | use crate::{Axis, Event, Flags, Id, Signal, Spec, Ui, fit, px}; |
| 2 | use draw::edit::{self, Command, Movement}; | |
| 3 | use parley::{ | |
| 4 | Affinity, | |
| 5 | editing::{Cursor, Selection}, | |
| 6 | }; | |
| 2 | 7 | use std::hash::Hash; |
| 3 | 8 | use winit::{ |
| 4 | 9 | event::Ime, |
| 5 | keyboard::{Key, NamedKey}, | |
| 10 | keyboard::{Key, ModifiersState, NamedKey}, | |
| 6 | 11 | window::CursorIcon, |
| 7 | 12 | }; |
| 8 | 13 | |
| ... | ... | @@ -14,7 +19,7 @@ pub fn button(ui: &mut Ui, part: impl Hash, text: &str) -> Signal { |
| 14 | 19 | size: [fit(), px(theme.font_size * 2.0)], |
| 15 | 20 | text: Some(text), |
| 16 | 21 | fill: Some(theme.chip), |
| 17 | hover_fill: Some(theme.hover), | |
| 22 | hover_fill: Some(theme.hover()), | |
| 18 | 23 | hover_border: Some(theme.accent), |
| 19 | 24 | radius: 4.0, |
| 20 | 25 | pad: [theme.font_size * 0.75, 0.0], |
| ... | ... | @@ -82,9 +87,9 @@ pub fn scrollbar( |
| 82 | 87 | chosen |
| 83 | 88 | } |
| 84 | 89 | |
| 85 | /// A single-line field editing `text`, showing `placeholder` while empty. Typing, | |
| 86 | /// Backspace/Delete, Left/Right with Shift to select, Home/End, Command-A and input | |
| 87 | /// method commits edit it; a press places the caret. | |
| 90 | /// A single-line field editing `text`, showing `placeholder` while empty. Presses, drags | |
| 91 | /// and keys select and edit as on the page: a double press selects a word, a triple press | |
| 92 | /// everything, and dragging extends by that unit. | |
| 88 | 93 | pub fn text_field( |
| 89 | 94 | ui: &mut Ui, |
| 90 | 95 | id: Id, |
| ... | ... | @@ -95,94 +100,114 @@ pub fn text_field( |
| 95 | 100 | let signal = ui.signal(id); |
| 96 | 101 | let pad = spec.pad[0]; |
| 97 | 102 | let size = ui.theme.font_size; |
| 98 | let clicked_at = signal | |
| 99 | .pressed | |
| 100 | .then(|| ui.pointer().zip(ui.rect(id))) | |
| 101 | .flatten() | |
| 102 | .map(|(pointer, rect)| pointer[0] - rect[0] - pad); | |
| 103 | let command = ui.modifiers().super_key() || ui.modifiers().control_key(); | |
| 104 | let shift = ui.modifiers().shift_key(); | |
| 105 | let (mut caret, mut mark) = { | |
| 106 | let (caret, mark) = ui.caret(id); | |
| 107 | (*caret, *mark) | |
| 103 | let modifiers = edit_modifiers(ui.modifiers()); | |
| 104 | let pointer = ui | |
| 105 | .pointer() | |
| 106 | .zip(ui.rect(id)) | |
| 107 | .map(|(pointer, rect)| pointer[0] - rect[0] - pad) | |
| 108 | .filter(|_| signal.pressed || signal.dragging); | |
| 109 | let (mut selection, mut press) = { | |
| 110 | let (selection, press) = ui.field(id); | |
| 111 | (*selection, *press) | |
| 108 | 112 | }; |
| 109 | caret = floor_boundary(text, caret); | |
| 110 | mark = floor_boundary(text, mark); | |
| 111 | if let Some(x) = clicked_at { | |
| 112 | let (texts, frame) = ui.texts(); | |
| 113 | caret = texts.label(text, size, frame).index_at(x); | |
| 114 | mark = caret; | |
| 113 | let (texts, frame) = ui.texts(); | |
| 114 | let mut label = texts.label(text, size, frame); | |
| 115 | if let Some(x) = pointer { | |
| 116 | let layout = &label.layout; | |
| 117 | let unit = if signal.pressed { signal.unit } else { press.1 }; | |
| 118 | let hit = Cursor::from_point(layout, x, label.size[1] / 2.0); | |
| 119 | let target = edit::selection_at(layout, hit, x, unit); | |
| 120 | selection = if signal.pressed { | |
| 121 | let pressed = if modifiers.shift { | |
| 122 | Selection::new(selection.anchor(), target.focus()) | |
| 123 | } else { | |
| 124 | target | |
| 125 | }; | |
| 126 | press = (pressed, unit); | |
| 127 | pressed | |
| 128 | } else { | |
| 129 | let [anchor, focus] = edit::drag( | |
| 130 | [press.0.anchor(), press.0.focus()], | |
| 131 | [target.anchor(), target.focus()], | |
| 132 | unit, | |
| 133 | |cursor| cursor.index(), | |
| 134 | ); | |
| 135 | Selection::new(anchor, focus) | |
| 136 | }; | |
| 115 | 137 | } |
| 138 | selection = selection.refresh(&label.layout); | |
| 116 | 139 | for event in &signal.events { |
| 117 | let selection = caret.min(mark)..caret.max(mark); | |
| 118 | let insert = |text: &mut String, inserted: &str, caret: &mut usize| { | |
| 119 | text.replace_range(selection.clone(), inserted); | |
| 120 | *caret = selection.start + inserted.len(); | |
| 121 | }; | |
| 122 | match event { | |
| 123 | Event::Ime(Ime::Commit(committed)) => { | |
| 124 | insert(text, committed, &mut caret); | |
| 125 | mark = caret; | |
| 126 | } | |
| 127 | Event::Key { key, text: typed } => match key { | |
| 128 | Key::Named(NamedKey::Backspace | NamedKey::Delete) => { | |
| 129 | if selection.is_empty() { | |
| 130 | let edge = if matches!(key, Key::Named(NamedKey::Backspace)) { | |
| 131 | previous(text, caret)..caret | |
| 132 | } else { | |
| 133 | caret..next(text, caret) | |
| 134 | }; | |
| 135 | text.replace_range(edge.clone(), ""); | |
| 136 | caret = edge.start; | |
| 137 | } else { | |
| 138 | insert(text, "", &mut caret); | |
| 140 | let layout = &label.layout; | |
| 141 | let (range, inserted) = match event { | |
| 142 | Event::Ime(Ime::Commit(committed)) => (selection.text_range(), committed.as_str()), | |
| 143 | Event::Key { key, text: typed } => { | |
| 144 | let key = edit_key(key); | |
| 145 | // One line has no break for Control-K to join, nor room for Home and End to scroll. | |
| 146 | let command = match (Command::from_key(&key, modifiers), &key) { | |
| 147 | (Some(Command::Kill), _) => Some(Command::DeleteTo(Movement::LineEnd)), | |
| 148 | (None, edit::Key::Named(edit::NamedKey::Home)) => { | |
| 149 | Some(Command::Move(Movement::DocumentStart)) | |
| 139 | 150 | } |
| 140 | mark = caret; | |
| 141 | } | |
| 142 | Key::Named(NamedKey::ArrowLeft | NamedKey::ArrowRight) => { | |
| 143 | let left = matches!(key, Key::Named(NamedKey::ArrowLeft)); | |
| 144 | caret = match (shift, selection.is_empty(), left) { | |
| 145 | (false, false, true) => selection.start, | |
| 146 | (false, false, false) => selection.end, | |
| 147 | (_, _, true) => previous(text, caret), | |
| 148 | (_, _, false) => next(text, caret), | |
| 149 | }; | |
| 150 | if !shift { | |
| 151 | mark = caret; | |
| 151 | (None, edit::Key::Named(edit::NamedKey::End)) => { | |
| 152 | Some(Command::Move(Movement::DocumentEnd)) | |
| 152 | 153 | } |
| 153 | } | |
| 154 | Key::Named(NamedKey::Home | NamedKey::End) => { | |
| 155 | caret = if matches!(key, Key::Named(NamedKey::Home)) { | |
| 156 | 0 | |
| 157 | } else { | |
| 158 | text.len() | |
| 159 | }; | |
| 160 | if !shift { | |
| 161 | mark = caret; | |
| 154 | (command, _) => command, | |
| 155 | }; | |
| 156 | let collapsed = selection.is_collapsed(); | |
| 157 | match (command, key) { | |
| 158 | (Some(Command::Move(movement)), _) => { | |
| 159 | selection = edit::step(layout, selection, movement, modifiers.shift); | |
| 160 | continue; | |
| 162 | 161 | } |
| 163 | } | |
| 164 | Key::Character(character) if command => { | |
| 165 | if character.eq_ignore_ascii_case("a") { | |
| 166 | mark = 0; | |
| 167 | caret = text.len(); | |
| 162 | (Some(Command::DeleteTo(movement)), _) if collapsed => ( | |
| 163 | edit::step(layout, selection, movement, true).text_range(), | |
| 164 | "", | |
| 165 | ), | |
| 166 | (Some(Command::Delete { backward }), _) if collapsed => { | |
| 167 | let caret = selection.focus(); | |
| 168 | let cluster = caret.logical_clusters(layout)[usize::from(!backward)]; | |
| 169 | ( | |
| 170 | cluster.map_or(caret.index()..caret.index(), |cluster| { | |
| 171 | cluster.text_range() | |
| 172 | }), | |
| 173 | "", | |
| 174 | ) | |
| 168 | 175 | } |
| 169 | } | |
| 170 | _ => { | |
| 171 | if let Some(typed) = typed.as_deref().filter(|typed| { | |
| 172 | !command && !typed.is_empty() && !typed.chars().any(char::is_control) | |
| 173 | }) { | |
| 174 | insert(text, typed, &mut caret); | |
| 175 | mark = caret; | |
| 176 | (Some(_), _) => (selection.text_range(), ""), | |
| 177 | (None, edit::Key::Character(character)) | |
| 178 | if modifiers.command && character.eq_ignore_ascii_case("a") => | |
| 179 | { | |
| 180 | selection = Selection::new( | |
| 181 | Cursor::from_byte_index(layout, 0, Affinity::Downstream), | |
| 182 | Cursor::from_byte_index(layout, usize::MAX, Affinity::Upstream), | |
| 183 | ); | |
| 184 | continue; | |
| 176 | 185 | } |
| 186 | (None, _) => match typed.as_deref().filter(|typed| { | |
| 187 | !modifiers.command | |
| 188 | && !modifiers.control | |
| 189 | && !typed.is_empty() | |
| 190 | && !typed.chars().any(char::is_control) | |
| 191 | }) { | |
| 192 | Some(typed) => (selection.text_range(), typed), | |
| 193 | None => continue, | |
| 194 | }, | |
| 177 | 195 | } |
| 178 | }, | |
| 179 | _ => {} | |
| 180 | } | |
| 196 | } | |
| 197 | _ => continue, | |
| 198 | }; | |
| 199 | text.replace_range(range.clone(), inserted); | |
| 200 | label = texts.label(text, size, frame); | |
| 201 | selection = Selection::from_byte_index( | |
| 202 | &label.layout, | |
| 203 | range.start + inserted.len(), | |
| 204 | Affinity::Downstream, | |
| 205 | ); | |
| 181 | 206 | } |
| 182 | 207 | { |
| 183 | let (stored_caret, stored_mark) = ui.caret(id); | |
| 184 | *stored_caret = caret; | |
| 185 | *stored_mark = mark; | |
| 208 | let (stored, stored_press) = ui.field(id); | |
| 209 | *stored = selection; | |
| 210 | *stored_press = press; | |
| 186 | 211 | } |
| 187 | 212 | let theme = ui.theme.clone(); |
| 188 | 213 | let shown = if text.is_empty() { |
| ... | ... | @@ -206,43 +231,59 @@ pub fn text_field( |
| 206 | 231 | }, |
| 207 | 232 | ); |
| 208 | 233 | if signal.focused { |
| 209 | let (texts, frame) = ui.texts(); | |
| 210 | let label = texts.label(text, size, frame); | |
| 211 | 234 | let line = label.size[1]; |
| 212 | let [start, end] = | |
| 213 | [caret.min(mark), caret.max(mark)].map(|index| pad + label.caret_x(index)); | |
| 214 | 235 | let top = match height.size { |
| 215 | 236 | crate::Size::Pixels(height) => (height - line) / 2.0, |
| 216 | 237 | _ => 0.0, |
| 217 | 238 | }; |
| 218 | if end > start { | |
| 219 | ui.mark([start, top, end, top + line], theme.hover); | |
| 239 | for (rect, _) in selection.geometry(&label.layout) { | |
| 240 | ui.mark( | |
| 241 | [pad + rect.x0 as f32, top, pad + rect.x1 as f32, top + line], | |
| 242 | theme.hover(), | |
| 243 | ); | |
| 220 | 244 | } |
| 221 | let x = pad + label.caret_x(caret); | |
| 245 | let x = pad + selection.focus().geometry(&label.layout, 1.0).x0 as f32; | |
| 222 | 246 | ui.mark([x, top, x + 1.0, top + line], theme.text); |
| 223 | 247 | } |
| 224 | 248 | ui.close(); |
| 225 | 249 | signal |
| 226 | 250 | } |
| 227 | 251 | |
| 228 | fn floor_boundary(text: &str, index: usize) -> usize { | |
| 229 | let mut index = index.min(text.len()); | |
| 230 | while !text.is_char_boundary(index) { | |
| 231 | index -= 1; | |
| 252 | /// A winit key in the editing vocabulary text fields and the page share. | |
| 253 | pub fn edit_key(key: &Key) -> edit::Key { | |
| 254 | use edit::NamedKey as Edit; | |
| 255 | match key { | |
| 256 | Key::Character(text) => edit::Key::Character(text.to_string()), | |
| 257 | Key::Named(named) => edit::Key::Named(match named { | |
| 258 | NamedKey::Escape => Edit::Escape, | |
| 259 | NamedKey::Tab => Edit::Tab, | |
| 260 | NamedKey::Space => Edit::Space, | |
| 261 | NamedKey::Enter => Edit::Enter, | |
| 262 | NamedKey::Backspace => Edit::Backspace, | |
| 263 | NamedKey::Delete => Edit::Delete, | |
| 264 | NamedKey::ArrowLeft => Edit::ArrowLeft, | |
| 265 | NamedKey::ArrowRight => Edit::ArrowRight, | |
| 266 | NamedKey::ArrowUp => Edit::ArrowUp, | |
| 267 | NamedKey::ArrowDown => Edit::ArrowDown, | |
| 268 | NamedKey::Home => Edit::Home, | |
| 269 | NamedKey::End => Edit::End, | |
| 270 | NamedKey::Alt | |
| 271 | | NamedKey::AltGraph | |
| 272 | | NamedKey::Control | |
| 273 | | NamedKey::Shift | |
| 274 | | NamedKey::Super | |
| 275 | | NamedKey::Meta => Edit::Modifier, | |
| 276 | _ => Edit::Other, | |
| 277 | }), | |
| 278 | _ => edit::Key::Named(Edit::Other), | |
| 232 | 279 | } |
| 233 | index | |
| 234 | } | |
| 235 | ||
| 236 | fn previous(text: &str, index: usize) -> usize { | |
| 237 | text[..index] | |
| 238 | .char_indices() | |
| 239 | .next_back() | |
| 240 | .map_or(0, |(index, _)| index) | |
| 241 | 280 | } |
| 242 | 281 | |
| 243 | fn next(text: &str, index: usize) -> usize { | |
| 244 | text[index..] | |
| 245 | .chars() | |
| 246 | .next() | |
| 247 | .map_or(index, |character| index + character.len_utf8()) | |
| 282 | pub fn edit_modifiers(modifiers: ModifiersState) -> edit::Modifiers { | |
| 283 | edit::Modifiers { | |
| 284 | shift: modifiers.shift_key(), | |
| 285 | control: modifiers.control_key(), | |
| 286 | option: modifiers.alt_key(), | |
| 287 | command: modifiers.super_key(), | |
| 288 | } | |
| 248 | 289 | } |
fuzz/Cargo.lock+10| ... | ... | @@ -81,6 +81,7 @@ dependencies = [ |
| 81 | 81 | name = "canvas" |
| 82 | 82 | version = "0.1.0" |
| 83 | 83 | dependencies = [ |
| 84 | "draw", | |
| 84 | 85 | "onestore", |
| 85 | 86 | "parley", |
| 86 | 87 | "serde", |
| ... | ... | @@ -195,6 +196,13 @@ dependencies = [ |
| 195 | 196 | "libloading", |
| 196 | 197 | ] |
| 197 | 198 | |
| 199 | [[package]] | |
| 200 | name = "draw" | |
| 201 | version = "0.1.0" | |
| 202 | dependencies = [ | |
| 203 | "parley", | |
| 204 | ] | |
| 205 | ||
| 198 | 206 | [[package]] |
| 199 | 207 | name = "errno" |
| 200 | 208 | version = "0.3.14" |
| ... | ... | @@ -646,6 +654,7 @@ version = "0.1.0" |
| 646 | 654 | dependencies = [ |
| 647 | 655 | "aes", |
| 648 | 656 | "base64", |
| 657 | "bumpalo", | |
| 649 | 658 | "cbc", |
| 650 | 659 | "getrandom", |
| 651 | 660 | "md5", |
| ... | ... | @@ -663,6 +672,7 @@ name = "onestore-fuzz" |
| 663 | 672 | version = "0.0.0" |
| 664 | 673 | dependencies = [ |
| 665 | 674 | "canvas", |
| 675 | "draw", | |
| 666 | 676 | "libfuzzer-sys", |
| 667 | 677 | "md5", |
| 668 | 678 | "notebook", |
fuzz/Cargo.toml+1| ... | ... | @@ -12,6 +12,7 @@ libfuzzer-sys = "0.4" |
| 12 | 12 | onestore = { path = "../crates/onestore", features = ["protected"] } |
| 13 | 13 | notebook = { path = "../crates/notebook" } |
| 14 | 14 | canvas = { path = "../crates/canvas" } |
| 15 | draw = { path = "../crates/draw", default-features = false } | |
| 15 | 16 | tempfile = "3" |
| 16 | 17 | md5 = "0.8.1" |
| 17 | 18 | serde_json = "1" |
fuzz/fuzz_targets/canvas_editor.rs+2-1| ... | ... | @@ -1,5 +1,6 @@ |
| 1 | 1 | #![no_main] |
| 2 | use canvas::{date::PageDate, document::{TextDocument, TextPosition}, editor::{CanvasEditor, Movement, SelectionUnit, TextOutline}, layout::TextEngine}; | |
| 2 | use canvas::{date::PageDate, document::{TextDocument, TextPosition}, editor::{CanvasEditor, TextOutline}, layout::TextEngine}; | |
| 3 | use draw::edit::{Movement, SelectionUnit}; | |
| 3 | 4 | use onestore::page::text::{Paragraph}; |
| 4 | 5 | use libfuzzer_sys::fuzz_target; |
| 5 | 6 | use onestore::document::Format; |
tools/canvas/README.md+8-6| ... | ... | @@ -34,23 +34,25 @@ canvas text edits · inverse history · composition · paragraph layout · |
| 34 | 34 | canvas::gpu page scene · text runs, tag icons, pictures and ink as draw primitives |
| 35 | 35 | ↓ |
| 36 | 36 | draw Swash glyph rasterization · custom wgpu quads · bounded glyph/image caches · layers |
| 37 | · draw::edit: key chords, click counts and caret steps within one layout | |
| 37 | 38 | ↓ |
| 38 | 39 | canvas::interaction hit layers · drags · placement grid · picture handles · chrome · key routing |
| 39 | 40 | · scroll bounds and zoom · caret blink · AccessKit tree |
| 40 | 41 | ↓ |
| 41 | 42 | ui immediate-mode boxes · keyed state · autolayout · event routing · labels · widgets |
| 43 | · shell: section tabs, toolbar buttons and drop-downs | |
| 42 | 44 | ↓ |
| 43 | snowbound section tabs · page list · the page as a custom box · Winit window and events | |
| 45 | snowbound title bar · toolbar · section frame · page tabs · the page as a custom box · Winit window and events | |
| 44 | 46 | · AppKit pickers and quit guard · clipboard · saving |
| 45 | 47 | ``` |
| 46 | 48 | |
| 47 | 49 | ## Desktop shell |
| 48 | 50 | |
| 49 | `--notebook FOLDER` opens a notebook with its readable top-level sections as tabs in the window's top strip, in the notebook's order and colours, and the open section's pages listed on the right, subpages indented. Command-F filters the list; Enter opens the first match and Escape clears it. A page that also changed on another computer shows Keep mine / Keep theirs above it. `--section FILE TITLE` opens one section the same way at that page. Section files must be regular files; discovery rejects symbolic links. | |
| 51 | `--notebook FOLDER` opens a notebook laid out as OneNote lays one out. The title bar shows the application's icon and the notebook's name; below it two rows of toolbar buttons follow the Home ribbon's Basic Text group, the first nine tags and zoom (only zoom acts yet). Readable top-level sections are tabs in the notebook's order, each leaning over the next at 45° with a faint highlight along its top, and the open one on top, casting a soft shadow and framing the page and the page tabs on the right in its colour, which shades slightly down the window; a tab that opens rises to meet the frame as its outline fades in. The frame's top corners and the page's corners are rounded concentric with the window's. The open page's tab is the page's colour and joins it with rounded inside corners; subpages are indented. The interface follows the system's light or dark appearance, and in the dark one the page's paper darkens and text left at OneNote's automatic colour turns light (except on a highlight), resolved when painting so nothing re-lays out; explicit colours, highlights, pictures and ink keep theirs, except that the background art of OneNote 2010's page templates, recognised by the picture's hash, paints from bundled vector recreations drawn for the paper. Sections take their stored colour's hue at a saturation and lightness chosen for each appearance. `--ui-font FONT_FILE_OR_FOLDER` sets the interface's typeface in place of the system's; fonts are not bundled. The section's colour is also the interface accent, easing to the next section's when another opens. The search box, New Page and the page-tab toggle sit above the page tabs. Command-F searches the page titles; Enter opens the first match and Escape clears it. A page that also changed on another computer shows Keep mine / Keep theirs above it. `--section FILE TITLE` opens one section the same way at that page. Section files must be regular files; discovery rejects symbolic links. | |
| 50 | 52 | |
| 51 | The `ui` crate builds every frame from the application's state. A cache keyed by stable box ids keeps hover, press, focus, scroll and animation values, and the previous frame's layout routes the frame's input before building, so a frame answers input one layout late and paints with its own. Sizes are solved per axis after building: fixed, label-sized, a fraction of an ancestor, or the sum of children, with overflow shared out by each box's strictness. The page is a custom box: `ui` routes it the pointer, wheel, key and input-method events that land on it, in order, and the host hands them to `PageView` and paints the page in a clipped layer of the same frame. Page scrollbars are `ui` widgets over that box; the canvas reports only its scroll bounds. Work the frame asks for (opening pages or sections, page requests, saving) runs after the frame is painted and requests the frame that shows it. | |
| 53 | The `ui` crate builds every frame from the application's state. A cache keyed by stable box ids keeps hover, press, focus, scroll and animation values, and the previous frame's layout routes the frame's input before building, so a frame answers input one layout late and paints with its own. Sizes are solved per axis after building: fixed, label-sized, a fraction of an ancestor, or the sum of children, with overflow shared out by each box's strictness. The page is a custom box: `ui` routes it the pointer, wheel, key and input-method events that land on it, in order, and the host hands them to `PageView` and paints the page in a clipped layer of the same frame. Page scrollbars are `ui` widgets over that box; the canvas reports only its scroll bounds. Text fields edit through `draw::edit`, as the page does, so keys, clicks and drags select and move the same way in both. Work the frame asks for (opening pages or sections, page requests, saving) runs after the frame is painted and requests the frame that shows it. | |
| 52 | 54 | |
| 53 | A covered window receives no redraws, so interaction can be scripted: `SNOWBOUND_REPLAY` names a file of `move X Y`, `press`, `release`, `wheel DX DY`, `key NAME`, `type TEXT`, `modifiers [shift] [command]`, `wait MS` and `snapshot PNG_PATH` lines in logical pixels, and each step draws its own frame. Replays edit and save like a user, so point them at a copy of a notebook. | |
| 55 | A covered window receives no redraws, so interaction can be scripted: `SNOWBOUND_REPLAY` names a file of `move X Y`, `press`, `release`, `wheel DX DY`, `key NAME`, `type TEXT`, `modifiers [shift] [command]`, `wait MS`, `snapshot PNG_PATH` and `appearance light|dark` lines in logical pixels, and each step and every 16 ms of a wait draws a frame. Snapshots omit the window's traffic lights, which AppKit draws. Replays edit and save like a user, so point them at a copy of a notebook. | |
| 54 | 56 | |
| 55 | 57 | ```sh |
| 56 | 58 | cp -RL SOURCE_NOTEBOOK /tmp/notebook-copy && chmod u+w /tmp/notebook-copy/*.one |
| ... | ... | @@ -60,7 +62,7 @@ cargo test -p ui |
| 60 | 62 | |
| 61 | 63 | The renderer allocates a 2048² RGBA glyph atlas and a 3.5 MiB vertex buffer, limits combined text-glyph and tag-icon entries to 8192, and rebuilds a frame after atlas eviction. Font scaling uses 32 cache entries. These limits bound those caches; they are not a process-memory or worst-case rasterization budget. Drawing and hit-testing share Windows-metric line boxes. First-baseline fidelity and substitute-font metric selection remain experimental; the `windows_*` probe values expose that policy independently from Parley's original metrics. |
| 62 | 64 | |
| 63 | The `draw` crate owns the renderer so the page and application chrome share one pass, atlas and image cache. It paints layers, each a transform and device clip over a flat sequence of glyph runs, SVG icons, rectangles, pen strokes and immutable raster images. Text reaches it through the `Glyphs` trait, which visits positioned glyph runs, so the page's Windows-metric line boxes stay in `canvas` and the renderer never measures text. Consecutive primitives with the same texture and clip share a batch without changing paint order. The host emits selection, composition and caret rectangles through that same interface; repainting multiple placements reuses the retained layout and glyph cache. | |
| 65 | The `draw` crate owns the renderer so the page and application chrome share one pass, atlas and image cache. It paints layers, each a transform and device clip over a flat sequence of glyph runs, SVG icons (in their own colours or tinted), SVG paths filled or stroked, rectangles (rounded, or shaded top to bottom), pen strokes and immutable raster images. Icons and paths rasterize once per size and quarter-pixel phase into the glyph atlas. The renderer is the default `render` feature; `canvas` without `gpu` uses only `draw::edit`. Text reaches it through the `Glyphs` trait, which visits positioned glyph runs, so the page's Windows-metric line boxes stay in `canvas` and the renderer never measures text. Consecutive primitives with the same texture and clip share a batch without changing paint order. The host emits selection, composition and caret rectangles through that same interface; repainting multiple placements reuses the retained layout and glyph cache. | |
| 64 | 66 | |
| 65 | 67 | Glyph coverage uses quarter-pixel phases with the vertical sign converted to Swash's Y-up coordinates. The `draw` GPU readback test checks nine successive quarter-pixel translations by their linear-light ink centroids, then requires identical pixels on repaint, cache eviction and renderer reconstruction on the same device. The native caret experiment exposed the previous inverted vertical phase; correcting raster placement does not change line metrics or wrapping. |
| 66 | 68 | |
| ... | ... | @@ -242,7 +244,7 @@ Full native captures, candidate font binaries/licenses and probe outputs remain |
| 242 | 244 | |
| 243 | 245 | ## CPU pipeline profiling |
| 244 | 246 | |
| 245 | The opt-in release probe measures engine construction, document baseline cloning, editing, undo, the host’s production page primitive collection and accessibility construction. Synthetic cases contain 100, 1,000 and 5,000 paragraphs. Each runs 32 insertion/undo samples alternating between the first and last paragraph; assertions require one changed layout per insertion, retained layouts through drawing/accessibility, and exact document restoration after undo. The accessibility regression compares cached output with full reconstruction through 160 mixed editing operations and complete undo/redo, including composition, source index changes, resize and viewport transforms. | |
| 247 | The opt-in release probe measures engine construction, document baseline cloning, editing, undo, the host’s production page primitive collection and accessibility construction. Synthetic cases contain 100, 1,000 and 5,000 paragraphs. Each runs 32 insertion/undo samples alternating between the first and last paragraph, timing the accessibility update after each insertion, undo and a 40-pixel scroll; assertions require one changed layout per insertion, retained layouts through drawing/accessibility, and exact document restoration after undo. The accessibility regression applies incremental updates to an AccessKit consumer tree and compares it with a full rebuild through 160 mixed editing operations and complete undo/redo, including composition, source index changes, resize and viewport transforms. | |
| 246 | 248 | |
| 247 | 249 | ```sh |
| 248 | 250 | cargo test --release -p canvas --features interaction canvas_pipeline_cost -- --ignored --nocapture --test-threads=1 |