authorgravatar for git@paperclover.netclover caruso <git@paperclover.net> 2026-09-25 17:00:54-07:00
committergravatar for git@paperclover.netclover caruso <git@paperclover.net> 2026-09-25 20:26:21-07:00
log0d005581890507e46939e22ee0d991887bb3c1b7
tree3c307a53c1aa057a8ada6386c7cf63c0e5a9c8d9
parent14ffea224c2042a06a245dcbcc1710ce2cfd9df7
signature Signed by SSH key SHA256:52mNGHRsVFBDED9IAX5pe+LRWUefqTbxEReunq21QvU

feat: OneNote-shaped shell, themes, dark pages, template backgrounds and large-page speed

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-5

150 files changed, 11001 insertions(+), 4097 deletions(-)

Cargo.lock+156-2
...@@ -400,8 +400,10 @@ dependencies = [...@@ -400,8 +400,10 @@ dependencies = [
400 "parley",400 "parley",
401 "png",401 "png",
402 "pollster",402 "pollster",
403 "resvg",
403 "serde",404 "serde",
404 "serde_json",405 "serde_json",
406 "sha2",
405 "skrifa",407 "skrifa",
406 "wgpu",408 "wgpu",
407]409]
...@@ -642,6 +644,12 @@ version = "1.2.0"...@@ -642,6 +644,12 @@ version = "1.2.0"
642source = "registry+https://github.com/rust-lang/crates.io-index"644source = "registry+https://github.com/rust-lang/crates.io-index"
643checksum = "f27ae1dd37df86211c42e150270f82743308803d90a6f6e6651cd730d5e1732f"645checksum = "f27ae1dd37df86211c42e150270f82743308803d90a6f6e6651cd730d5e1732f"
644646
647[[package]]
648name = "data-url"
649version = "0.3.2"
650source = "registry+https://github.com/rust-lang/crates.io-index"
651checksum = "be1e0bca6c3637f992fc1cc7cbc52a78c1ef6db076dbf1059c4323d6a2048376"
652
645[[package]]653[[package]]
646name = "dbl"654name = "dbl"
647version = "0.5.0"655version = "0.5.0"
...@@ -757,6 +765,15 @@ version = "3.4.0"...@@ -757,6 +765,15 @@ version = "3.4.0"
757source = "registry+https://github.com/rust-lang/crates.io-index"765source = "registry+https://github.com/rust-lang/crates.io-index"
758checksum = "0b5343afd4a8365a643ac588dab4cf234a190c7f6c88c9f6dd6ffe00837661b7"766checksum = "0b5343afd4a8365a643ac588dab4cf234a190c7f6c88c9f6dd6ffe00837661b7"
759767
768[[package]]
769name = "euclid"
770version = "0.22.14"
771source = "registry+https://github.com/rust-lang/crates.io-index"
772checksum = "f1a05365e3b1c6d1650318537c7460c6923f1abdd272ad6842baa2b509957a06"
773dependencies = [
774 "num-traits",
775]
776
760[[package]]777[[package]]
761name = "fallible-iterator"778name = "fallible-iterator"
762version = "0.3.0"779version = "0.3.0"
...@@ -801,6 +818,12 @@ dependencies = [...@@ -801,6 +818,12 @@ dependencies = [
801 "zlib-rs",818 "zlib-rs",
802]819]
803820
821[[package]]
822name = "float-cmp"
823version = "0.9.0"
824source = "registry+https://github.com/rust-lang/crates.io-index"
825checksum = "98de4bbd547a563b716d8dfa9aad1cb19bfab00f4fa09a6a4ed21dbcf44ce9c4"
826
804[[package]]827[[package]]
805name = "foldhash"828name = "foldhash"
806version = "0.2.0"829version = "0.2.0"
...@@ -1210,6 +1233,12 @@ dependencies = [...@@ -1210,6 +1233,12 @@ dependencies = [
1210 "zune-jpeg",1233 "zune-jpeg",
1211]1234]
12121235
1236[[package]]
1237name = "imagesize"
1238version = "0.15.0"
1239source = "registry+https://github.com/rust-lang/crates.io-index"
1240checksum = "65b27460c2c92b037f3f94c538ed9a3342f3fdf923606781629ccb35f82d042a"
1241
1213[[package]]1242[[package]]
1214name = "indexmap"1243name = "indexmap"
1215version = "2.14.2"1244version = "2.14.2"
...@@ -1332,6 +1361,18 @@ version = "3.1.0"...@@ -1332,6 +1361,18 @@ version = "3.1.0"
1332source = "registry+https://github.com/rust-lang/crates.io-index"1361source = "registry+https://github.com/rust-lang/crates.io-index"
1333checksum = "e2db585e1d738fc771bf08a151420d3ed193d9d895a36df7f6f8a9456b911ddc"1362checksum = "e2db585e1d738fc771bf08a151420d3ed193d9d895a36df7f6f8a9456b911ddc"
13341363
1364[[package]]
1365name = "kurbo"
1366version = "0.13.1"
1367source = "registry+https://github.com/rust-lang/crates.io-index"
1368checksum = "4b60dfc32f652b926df6192e55525b16d186c69d47876c3ead4da5cc9f8450e2"
1369dependencies = [
1370 "arrayvec",
1371 "euclid",
1372 "polycool",
1373 "smallvec",
1374]
1375
1335[[package]]1376[[package]]
1336name = "libc"1377name = "libc"
1337version = "0.2.189"1378version = "0.2.189"
...@@ -1958,6 +1999,7 @@ version = "0.1.0"...@@ -1958,6 +1999,7 @@ version = "0.1.0"
1958dependencies = [1999dependencies = [
1959 "aes",2000 "aes",
1960 "base64",2001 "base64",
2002 "bumpalo",
1961 "cbc",2003 "cbc",
1962 "getrandom 0.4.3",2004 "getrandom 0.4.3",
1963 "md5",2005 "md5",
...@@ -2081,6 +2123,12 @@ version = "2.3.2"...@@ -2081,6 +2123,12 @@ version = "2.3.2"
2081source = "registry+https://github.com/rust-lang/crates.io-index"2123source = "registry+https://github.com/rust-lang/crates.io-index"
2082checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220"2124checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220"
20832125
2126[[package]]
2127name = "pico-args"
2128version = "0.5.0"
2129source = "registry+https://github.com/rust-lang/crates.io-index"
2130checksum = "5be167a7af36ee22fe3115051bc51f6e6c7054c9348e28deb4f49bd6f705a315"
2131
2084[[package]]2132[[package]]
2085name = "pin-project"2133name = "pin-project"
2086version = "1.1.13"2134version = "1.1.13"
...@@ -2152,6 +2200,15 @@ version = "0.4.0"...@@ -2152,6 +2200,15 @@ version = "0.4.0"
2152source = "registry+https://github.com/rust-lang/crates.io-index"2200source = "registry+https://github.com/rust-lang/crates.io-index"
2153checksum = "2f3a9f18d041e6d0e102a0a46750538147e5e8992d3b4873aaafee2520b00ce3"2201checksum = "2f3a9f18d041e6d0e102a0a46750538147e5e8992d3b4873aaafee2520b00ce3"
21542202
2203[[package]]
2204name = "polycool"
2205version = "0.4.0"
2206source = "registry+https://github.com/rust-lang/crates.io-index"
2207checksum = "50596ddc09eb5ad5f75cacd40209568e66df71baf86e1499a0e99c4cff12a5a6"
2208dependencies = [
2209 "arrayvec",
2210]
2211
2155[[package]]2212[[package]]
2156name = "polyval"2213name = "polyval"
2157version = "0.7.3"2214version = "0.7.3"
...@@ -2329,6 +2386,30 @@ version = "1.1.0"...@@ -2329,6 +2386,30 @@ version = "1.1.0"
2329source = "registry+https://github.com/rust-lang/crates.io-index"2386source = "registry+https://github.com/rust-lang/crates.io-index"
2330checksum = "19b30a45b0cd0bcca8037f3d0dc3421eaf95327a17cad11964fb8179b4fc4832"2387checksum = "19b30a45b0cd0bcca8037f3d0dc3421eaf95327a17cad11964fb8179b4fc4832"
23312388
2389[[package]]
2390name = "resvg"
2391version = "0.48.1"
2392source = "registry+https://github.com/rust-lang/crates.io-index"
2393checksum = "67e3803f97b999e80cbf7c6ecdd07a8102204d92e1633cf48783720c521196bd"
2394dependencies = [
2395 "bytemuck",
2396 "log",
2397 "pico-args",
2398 "rgb",
2399 "svgtypes",
2400 "tiny-skia 0.12.0",
2401 "usvg",
2402]
2403
2404[[package]]
2405name = "rgb"
2406version = "0.8.53"
2407source = "registry+https://github.com/rust-lang/crates.io-index"
2408checksum = "47b34b781b31e5d73e9fbc8689c70551fd1ade9a19e3e28cfec8580a79290cc4"
2409dependencies = [
2410 "bytemuck",
2411]
2412
2332[[package]]2413[[package]]
2333name = "roxmltree"2414name = "roxmltree"
2334version = "0.21.1"2415version = "0.21.1"
...@@ -2441,7 +2522,7 @@ dependencies = [...@@ -2441,7 +2522,7 @@ dependencies = [
2441 "log",2522 "log",
2442 "memmap2",2523 "memmap2",
2443 "smithay-client-toolkit",2524 "smithay-client-toolkit",
2444 "tiny-skia",2525 "tiny-skia 0.11.4",
2445]2526]
24462527
2447[[package]]2528[[package]]
...@@ -2543,6 +2624,21 @@ version = "0.1.5"...@@ -2543,6 +2624,21 @@ version = "0.1.5"
2543source = "registry+https://github.com/rust-lang/crates.io-index"2624source = "registry+https://github.com/rust-lang/crates.io-index"
2544checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e"2625checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e"
25452626
2627[[package]]
2628name = "simplecss"
2629version = "0.2.2"
2630source = "registry+https://github.com/rust-lang/crates.io-index"
2631checksum = "7a9c6883ca9c3c7c90e888de77b7a5c849c779d25d74a1269b0218b14e8b136c"
2632dependencies = [
2633 "log",
2634]
2635
2636[[package]]
2637name = "siphasher"
2638version = "1.0.4"
2639source = "registry+https://github.com/rust-lang/crates.io-index"
2640checksum = "33f4fe9184a62d842c9ef383018f3306d8ba224fd9d836f56d7288308847c256"
2641
2546[[package]]2642[[package]]
2547name = "skrifa"2643name = "skrifa"
2548version = "0.44.0"2644version = "0.44.0"
...@@ -2705,6 +2801,9 @@ name = "strict-num"...@@ -2705,6 +2801,9 @@ name = "strict-num"
2705version = "0.1.1"2801version = "0.1.1"
2706source = "registry+https://github.com/rust-lang/crates.io-index"2802source = "registry+https://github.com/rust-lang/crates.io-index"
2707checksum = "6637bab7722d379c8b41ba849228d680cc12d0a45ba1fa2b48f2a30577a06731"2803checksum = "6637bab7722d379c8b41ba849228d680cc12d0a45ba1fa2b48f2a30577a06731"
2804dependencies = [
2805 "float-cmp",
2806]
27082807
2709[[package]]2808[[package]]
2710name = "subtle"2809name = "subtle"
...@@ -2712,6 +2811,16 @@ version = "2.6.1"...@@ -2712,6 +2811,16 @@ version = "2.6.1"
2712source = "registry+https://github.com/rust-lang/crates.io-index"2811source = "registry+https://github.com/rust-lang/crates.io-index"
2713checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292"2812checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292"
27142813
2814[[package]]
2815name = "svgtypes"
2816version = "0.16.1"
2817source = "registry+https://github.com/rust-lang/crates.io-index"
2818checksum = "695b5790b3131dafa99b3bbfd25a216edb3d216dad9ca208d4657bfb8f2abc3d"
2819dependencies = [
2820 "kurbo",
2821 "siphasher",
2822]
2823
2715[[package]]2824[[package]]
2716name = "swash"2825name = "swash"
2717version = "0.2.10"2826version = "0.2.10"
...@@ -2829,7 +2938,22 @@ dependencies = [...@@ -2829,7 +2938,22 @@ dependencies = [
2829 "bytemuck",2938 "bytemuck",
2830 "cfg-if",2939 "cfg-if",
2831 "log",2940 "log",
2832 "tiny-skia-path",2941 "tiny-skia-path 0.11.4",
2942]
2943
2944[[package]]
2945name = "tiny-skia"
2946version = "0.12.0"
2947source = "registry+https://github.com/rust-lang/crates.io-index"
2948checksum = "47ffee5eaaf5527f630fb0e356b90ebdec84d5d18d937c5e440350f88c5a91ea"
2949dependencies = [
2950 "arrayref",
2951 "arrayvec",
2952 "bytemuck",
2953 "cfg-if",
2954 "log",
2955 "png",
2956 "tiny-skia-path 0.12.0",
2833]2957]
28342958
2835[[package]]2959[[package]]
...@@ -2843,6 +2967,17 @@ dependencies = [...@@ -2843,6 +2967,17 @@ dependencies = [
2843 "strict-num",2967 "strict-num",
2844]2968]
28452969
2970[[package]]
2971name = "tiny-skia-path"
2972version = "0.12.0"
2973source = "registry+https://github.com/rust-lang/crates.io-index"
2974checksum = "edca365c3faccca67d06593c5980fa6c57687de727a03131735bb85f01fdeeb9"
2975dependencies = [
2976 "arrayref",
2977 "bytemuck",
2978 "strict-num",
2979]
2980
2846[[package]]2981[[package]]
2847name = "tinystr"2982name = "tinystr"
2848version = "0.8.4"2983version = "0.8.4"
...@@ -2969,6 +3104,25 @@ dependencies = [...@@ -2969,6 +3104,25 @@ dependencies = [
2969 "ctutils",3104 "ctutils",
2970]3105]
29713106
3107[[package]]
3108name = "usvg"
3109version = "0.48.1"
3110source = "registry+https://github.com/rust-lang/crates.io-index"
3111checksum = "977d0a4abdef933f424a99fe09f95576e089b90aebc6f016a3bc813762493e91"
3112dependencies = [
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[[package]]3126[[package]]
2973name = "utf8_iter"3127name = "utf8_iter"
2974version = "1.0.4"3128version = "1.0.4"
Cargo.toml+5
...@@ -6,3 +6,8 @@ resolver = "3"...@@ -6,3 +6,8 @@ resolver = "3"
6[workspace.dependencies]6[workspace.dependencies]
7parley = { git = "https://github.com/linebender/parley", rev = "4f67747d3690bb21d25cc70b674ab0d269595759", features = ["complex-scripts"] }7parley = { git = "https://github.com/linebender/parley", rev = "4f67747d3690bb21d25cc70b674ab0d269595759", features = ["complex-scripts"] }
8wgpu = "30.0.1"8wgpu = "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."*"]
13opt-level = 3
crates/canvas/Cargo.toml+4-2
...@@ -5,19 +5,21 @@ edition = "2024"...@@ -5,19 +5,21 @@ edition = "2024"
5publish = false5publish = false
66
7[features]7[features]
8gpu = ["dep:draw"]8gpu = ["draw/render", "dep:resvg", "dep:sha2"]
9# How a page responds to input and presents itself to assistive technology.9# How a page responds to input and presents itself to assistive technology.
10interaction = ["gpu", "dep:accesskit"]10interaction = ["gpu", "dep:accesskit"]
1111
12[dependencies]12[dependencies]
13draw = { path = "../draw", default-features = false }
13onestore = { path = "../onestore" }14onestore = { path = "../onestore" }
14parley.workspace = true15parley.workspace = true
15serde = { version = "1.0.229", features = ["derive"] }16serde = { version = "1.0.229", features = ["derive"] }
16serde_json = "1.0.151"17serde_json = "1.0.151"
17skrifa = "0.44.0"18skrifa = "0.44.0"
1819
19draw = { path = "../draw", optional = true }
20accesskit = { version = "0.25.0", optional = true }20accesskit = { version = "0.25.0", optional = true }
21resvg = { version = "0.48.1", default-features = false, optional = true }
22sha2 = { version = "0.11", optional = true }
2123
22[[bin]]24[[bin]]
23name = "layout-probe"25name = "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,4 +1,4 @@
1use canvas::gpu::{Viewport, page::PageScene};1use canvas::gpu::{Paper, Viewport, page::PageScene};
2use canvas::layout::TextEngine;2use canvas::layout::TextEngine;
3use draw::Renderer;3use draw::Renderer;
4use onestore::page::Page;4use onestore::page::Page;
...@@ -38,9 +38,12 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {...@@ -38,9 +38,12 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
38 };38 };
39 let margin_origin = page.margin_origin;39 let margin_origin = page.margin_origin;
40 let object_count = page.objects.len();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 let mut primitives = Vec::new();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 let instance = wgpu::Instance::new(wgpu::InstanceDescriptor::new_without_display_handle());47 let instance = wgpu::Instance::new(wgpu::InstanceDescriptor::new_without_display_handle());
45 let adapter = pollster::block_on(instance.request_adapter(&Default::default()))?;48 let adapter = pollster::block_on(instance.request_adapter(&Default::default()))?;
46 let (device, queue) = pollster::block_on(adapter.request_device(&Default::default()))?;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,6 +16,11 @@ pub struct TextPosition {
16#[derive(Clone, Debug, PartialEq)]16#[derive(Clone, Debug, PartialEq)]
17pub struct TextDocument {17pub struct TextDocument {
18 nodes: Vec<PageParagraph>,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}
2025
21#[derive(Clone, Debug, PartialEq)]26#[derive(Clone, Debug, PartialEq)]
...@@ -123,85 +128,98 @@ pub(crate) fn swap_columns(nodes: &mut [PageParagraph], widths: &mut BTreeMap<Ex...@@ -123,85 +128,98 @@ pub(crate) fn swap_columns(nodes: &mut [PageParagraph], widths: &mut BTreeMap<Ex
123 }128 }
124}129}
125130
131/// Identities a node holds itself; paragraphs in its cells hold their own.
132fn 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
126pub(crate) fn validate_nodes(147pub(crate) fn validate_nodes(
127 nodes: &[PageParagraph],148 nodes: &[PageParagraph],
128 edit: Option<&DocumentEdit>,
129 ids: &mut BTreeSet<ExGuid>,149 ids: &mut BTreeSet<ExGuid>,
130) -> Result<(), EditError> {150) -> Result<(), EditError> {
131 let mut pending = vec![(None, nodes, 0)];151 if nodes.is_empty() {
132 while let Some((container, nodes, depth)) = pending.pop() {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.
159fn 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 if depth > 64 {167 if depth > 64 {
134 return Err(EditError::InvalidStructure);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 let mut parents = BTreeMap::new();170 let mut parents = BTreeMap::new();
141 for node in nodes {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 || node.level == 0182 || node.level == 0
144 || node183 || node
145 .parent184 .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 return Err(EditError::InvalidStructure);187 return Err(EditError::InvalidStructure);
149 }188 }
150 parents.insert(node.id, node.level);189 parents.insert(node.id, node.level);
151 match &node.content {190 match &node.content {
152 ParagraphContent::Text(text) => {
153 if !ids.insert(text.id) {
154 return Err(EditError::InvalidStructure);
155 }
156 }
157 ParagraphContent::Image(image) => {191 ParagraphContent::Image(image) => {
158 if !ids.insert(image.id) {
159 return Err(EditError::InvalidStructure);
160 }
161 crate::outline::image_size(image).ok_or(EditError::UnsupportedContent)?;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 ParagraphContent::Table(table) => {194 ParagraphContent::Table(table) => {
179 if !ids.insert(table.id)195 if table.rows.is_empty()
180 || table.rows.is_empty()
181 || table.columns.is_empty()196 || table.columns.is_empty()
182 || table.columns.len() > 255197 || table.columns.len() > 255
183 || table198 || table
184 .columns199 .columns
185 .iter()200 .iter()
186 .any(|column| !column.width.is_finite() || column.width < 36.0)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 return Err(EditError::InvalidStructure);207 return Err(EditError::InvalidStructure);
189 }208 }
190 for row in &table.rows {209 for cell in table.rows.iter().flat_map(|row| &row.cells) {
191 if !ids.insert(row.id) || row.cells.len() != table.columns.len() {210 if !cell.unsupported.is_empty() {
192 return Err(EditError::InvalidStructure);211 return Err(EditError::UnsupportedContent);
193 }212 }
194 for cell in &row.cells {213 if cell.paragraphs.is_empty() {
195 if !ids.insert(cell.id) {214 return Err(EditError::InvalidRange);
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));
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,6 +234,15 @@ fn validate_text(nodes: &[PageParagraph]) -> Result<(), EditError> {
216 Ok(())234 Ok(())
217}235}
218236
237/// Paragraph positions of each node's first text leaf, counting from `first`.
238fn 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
219pub(crate) fn validate_flat<'a>(246pub(crate) fn validate_flat<'a>(
220 mut nodes: impl Iterator<Item = &'a PageParagraph>,247 mut nodes: impl Iterator<Item = &'a PageParagraph>,
221) -> Result<(), EditError> {248) -> Result<(), EditError> {
...@@ -235,16 +262,20 @@ pub(crate) fn container_mut(...@@ -235,16 +262,20 @@ pub(crate) fn container_mut(
235 nodes: &mut Vec<PageParagraph>,262 nodes: &mut Vec<PageParagraph>,
236 id: Option<ExGuid>,263 id: Option<ExGuid>,
237) -> Option<&mut Vec<PageParagraph>> {264) -> Option<&mut Vec<PageParagraph>> {
238 let Some(id) = id else {265 match id {
239 return Some(nodes);266 None => Some(nodes),
240 };267 Some(id) => cell_mut(nodes, id),
268 }
269}
270
271fn cell_mut(nodes: &mut [PageParagraph], id: ExGuid) -> Option<&mut Vec<PageParagraph>> {
241 for node in nodes {272 for node in nodes {
242 if let ParagraphContent::Table(table) = &mut node.content {273 if let ParagraphContent::Table(table) = &mut node.content {
243 for cell in table.rows.iter_mut().flat_map(|row| &mut row.cells) {274 for cell in table.rows.iter_mut().flat_map(|row| &mut row.cells) {
244 if cell.id == id {275 if cell.id == id {
245 return Some(&mut cell.paragraphs);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 return Some(nodes);279 return Some(nodes);
249 }280 }
250 }281 }
...@@ -253,6 +284,14 @@ pub(crate) fn container_mut(...@@ -253,6 +284,14 @@ pub(crate) fn container_mut(
253 None284 None
254}285}
255286
287fn 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
256impl TextDocument {295impl TextDocument {
257 pub fn new(paragraphs: Vec<Paragraph>) -> Result<Self, EditError> {296 pub fn new(paragraphs: Vec<Paragraph>) -> Result<Self, EditError> {
258 Self::from_nodes(297 Self::from_nodes(
...@@ -264,9 +303,15 @@ impl TextDocument {...@@ -264,9 +303,15 @@ impl TextDocument {
264 }303 }
265304
266 pub fn from_nodes(nodes: Vec<PageParagraph>) -> Result<Self, EditError> {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 validate_text(&nodes)?;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 }
271316
272 pub fn nodes(&self) -> &[PageParagraph] {317 pub fn nodes(&self) -> &[PageParagraph] {
...@@ -285,19 +330,90 @@ impl TextDocument {...@@ -285,19 +330,90 @@ impl TextDocument {
285 self.text_nodes().map(|node| &node.text().unwrap().text)330 self.text_nodes().map(|node| &node.text().unwrap().text)
286 }331 }
287332
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 pub(crate) fn container(&self, id: Option<ExGuid>) -> Result<&[PageParagraph], EditError> {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 let Some(id) = id else {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()];407 let root = self.root(id)?;
293 while let Some(nodes) = pending.pop() {408 let mut pending = vec![(std::slice::from_ref(&self.nodes[root]), 0)];
409 while let Some((nodes, depth)) = pending.pop() {
294 for node in nodes {410 for node in nodes {
295 if let ParagraphContent::Table(table) = &node.content {411 if let ParagraphContent::Table(table) = &node.content {
296 for cell in table.rows.iter().flat_map(|row| &row.cells) {412 for cell in table.rows.iter().flat_map(|row| &row.cells) {
297 if cell.id == id {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,11 +460,11 @@ impl TextDocument {
344 if range.start > range.end {460 if range.start > range.end {
345 return Err(EditError::InvalidRange);461 return Err(EditError::InvalidRange);
346 }462 }
347 let (container, start, first) = leaves(&self.nodes, None)463 let (container, start, first) = self
348 .nth(range.start.paragraph)464 .leaf(range.start.paragraph)
349 .ok_or(EditError::InvalidRange)?;465 .ok_or(EditError::InvalidRange)?;
350 let (end_container, end, last) = leaves(&self.nodes, None)466 let (end_container, end, last) = self
351 .nth(range.end.paragraph)467 .leaf(range.end.paragraph)
352 .ok_or(EditError::InvalidRange)?;468 .ok_or(EditError::InvalidRange)?;
353 if container != end_container {469 if container != end_container {
354 return Err(EditError::UnsupportedContent);470 return Err(EditError::UnsupportedContent);
...@@ -394,12 +510,60 @@ impl TextDocument {...@@ -394,12 +510,60 @@ impl TextDocument {
394 })510 })
395 }511 }
396512
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 pub(crate) fn validate_edit(&self, edit: &DocumentEdit) -> Result<(), EditError> {515 pub(crate) fn validate_edit(&self, edit: &DocumentEdit) -> Result<(), EditError> {
398 let nodes = self.container(edit.container)?;516 let (nodes, depth) = self.nested_container(edit.container)?;
399 if edit.range.start > edit.range.end || edit.range.end > nodes.len() {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 return Err(EditError::InvalidRange);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 if !edit.columns.is_empty() {567 if !edit.columns.is_empty() {
404 let replaced = descendants(&edit.replacement, None)568 let replaced = descendants(&edit.replacement, None)
405 .filter_map(|(_, _, node)| match &node.content {569 .filter_map(|(_, _, node)| match &node.content {
...@@ -431,23 +595,59 @@ impl TextDocument {...@@ -431,23 +595,59 @@ impl TextDocument {
431595
432 pub(crate) fn apply(&mut self, edit: DocumentEdit) -> Result<DocumentEdit, EditError> {596 pub(crate) fn apply(&mut self, edit: DocumentEdit) -> Result<DocumentEdit, EditError> {
433 self.validate_edit(&edit)?;597 self.validate_edit(&edit)?;
434 let end = edit598 Ok(self.splice(edit))
435 .range599 }
436 .start600
437 .checked_add(edit.replacement.len())601 /// Applies an edit [`Self::validate_edit`] accepted, returning its inverse.
438 .ok_or(EditError::TextTooLong)?;602 pub(crate) fn splice(&mut self, edit: DocumentEdit) -> DocumentEdit {
439 let range = edit.range.start..end;603 let range = edit.range.start..edit.range.start + edit.replacement.len();
440 let nodes =604 let root = edit.container.map(|cell| {
441 container_mut(&mut self.nodes, edit.container).ok_or(EditError::InvalidRange)?;605 (
442 let replacement = nodes.splice(edit.range, edit.replacement).collect();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 let mut columns = edit.columns;643 let mut columns = edit.columns;
444 swap_columns(&mut self.nodes, &mut columns);644 swap_columns(&mut self.nodes, &mut columns);
445 Ok(DocumentEdit {645 DocumentEdit {
446 container: edit.container,646 container: edit.container,
447 range,647 range,
448 replacement,648 replacement,
449 columns,649 columns,
450 })650 }
451 }651 }
452}652}
453653
...@@ -1252,6 +1452,239 @@ mod tests {...@@ -1252,6 +1452,239 @@ mod tests {
1252 }1452 }
1253 }1453 }
12541454
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 #[test]1688 #[test]
1256 fn invalid_ranges_and_last_paragraph_removal_are_atomic() {1689 fn invalid_ranges_and_last_paragraph_removal_are_atomic() {
1257 let original =1690 let original =
crates/canvas/src/editor.rs+268-260
...@@ -1,12 +1,10 @@...@@ -1,12 +1,10 @@
1use crate::{1use crate::{
2 date::PageDate,2 date::PageDate,
3 document::{DocumentEdit, TextDocument, TextPosition, descendants, edited_nodes, leaves},3 document::{DocumentEdit, TextDocument, TextPosition, descendants, leaves},
4 layout::{LayoutError, TextEngine},4 layout::{LayoutError, TextEngine},
5 outline::{5 outline::{Arrange, OutlineLayout, ParagraphLayout, outline_layout, visible_paragraphs},
6 Arrange, OutlineLayout, ParagraphLayout, arrange, indentation, outline_layout,
7 visible_paragraphs,
8 },
9};6};
7use draw::edit::{self, Movement, SelectionUnit};
10use onestore::ExGuid;8use onestore::ExGuid;
11use onestore::page::text::{EditError, Paragraph};9use onestore::page::text::{EditError, Paragraph};
12use onestore::page::{Definition, Outline, Page, PageObject, PageParagraph, Title};10use onestore::page::{Definition, Outline, Page, PageObject, PageParagraph, Title};
...@@ -26,29 +24,6 @@ pub(crate) mod page;...@@ -26,29 +24,6 @@ pub(crate) mod page;
26mod table;24mod table;
27pub use page::ReadOnlyObject;25pub use page::ReadOnlyObject;
2826
29#[derive(Clone, Copy, Debug)]
30pub 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)]
46pub enum SelectionUnit {
47 Grapheme,
48 Word,
49 Paragraph,
50}
51
52#[derive(Debug)]27#[derive(Debug)]
53pub enum EditorError {28pub enum EditorError {
54 Edit(EditError),29 Edit(EditError),
...@@ -319,19 +294,7 @@ impl TextOutline {...@@ -319,19 +294,7 @@ impl TextOutline {
319 let text_width = if self.shaped.tables.is_empty() {294 let text_width = if self.shaped.tables.is_empty() {
320 Some(self.shaped.size[0])295 Some(self.shaped.size[0])
321 } else {296 } else {
322 let roots = self297 Some(self.shaped.widths[1]).filter(|width| *width > f32::NEG_INFINITY)
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)
335 };298 };
336 text_width.map_or(self.shaped.size[0], |width| {299 text_width.map_or(self.shaped.size[0], |width| {
337 (width + 36.0)300 (width + 36.0)
...@@ -370,13 +333,23 @@ impl TextOutline {...@@ -370,13 +333,23 @@ impl TextOutline {
370 })333 })
371 }334 }
372335
336 /// Layouts follow the text leaves minus hidden ones, so an index shared by a leaf and its
337 /// layout needs no search.
373 fn visible_index(&self, source: usize) -> Result<usize, EditError> {338 fn visible_index(&self, source: usize) -> Result<usize, EditError> {
374 let id = self339 let id = self
375 .document340 .document
376 .text_nodes()341 .leaf(source)
377 .nth(source)
378 .ok_or(EditError::InvalidRange)?342 .ok_or(EditError::InvalidRange)?
343 .2
379 .id;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 self.shaped353 self.shaped
381 .paragraphs354 .paragraphs
382 .iter()355 .iter()
...@@ -386,6 +359,13 @@ impl TextOutline {...@@ -386,6 +359,13 @@ impl TextOutline {
386359
387 fn source_index(&self, visible: usize) -> usize {360 fn source_index(&self, visible: usize) -> usize {
388 let id = self.shaped.paragraphs[visible].id;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 self.document369 self.document
390 .text_nodes()370 .text_nodes()
391 .position(|paragraph| paragraph.id == id)371 .position(|paragraph| paragraph.id == id)
...@@ -989,7 +969,7 @@ impl CanvasEditor {...@@ -989,7 +969,7 @@ impl CanvasEditor {
989 if !ids.insert(outline_id) {969 if !ids.insert(outline_id) {
990 return Err(EditError::InvalidStructure.into());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 Ok(Self {974 Ok(Self {
995 outlines,975 outlines,
...@@ -1574,8 +1554,7 @@ impl CanvasEditor {...@@ -1574,8 +1554,7 @@ impl CanvasEditor {
1574 self.active_outline().visible_index(position.paragraph)?;1554 self.active_outline().visible_index(position.paragraph)?;
1575 self.active_outline()1555 self.active_outline()
1576 .document1556 .document
1577 .paragraphs()1557 .paragraph(position.paragraph)
1578 .nth(position.paragraph)
1579 .ok_or(EditError::InvalidRange)?1558 .ok_or(EditError::InvalidRange)?
1580 .byte_offset(position.offset)?;1559 .byte_offset(position.offset)?;
1581 }1560 }
...@@ -1589,12 +1568,7 @@ impl CanvasEditor {...@@ -1589,12 +1568,7 @@ impl CanvasEditor {
1589 let paragraph = self1568 let paragraph = self
1590 .active_outline()1569 .active_outline()
1591 .source_index(self.active_outline().shaped.paragraphs.len() - 1);1570 .source_index(self.active_outline().shaped.paragraphs.len() - 1);
1592 let last = self1571 let last = self.active_outline().document.paragraph(paragraph).unwrap();
1593 .active_outline()
1594 .document
1595 .paragraphs()
1596 .nth(paragraph)
1597 .unwrap();
1598 self.select(1572 self.select(
1599 [1573 [
1600 TextPosition {1574 TextPosition {
...@@ -1632,22 +1606,12 @@ impl CanvasEditor {...@@ -1632,22 +1606,12 @@ impl CanvasEditor {
1632 let paragraph = &self.active_outline().shaped.paragraphs[index];1606 let paragraph = &self.active_outline().shaped.paragraphs[index];
1633 let layout = &paragraph.text.shaped;1607 let layout = &paragraph.text.shaped;
1634 let point = [x - paragraph.origin[0], y - paragraph.origin[1]];1608 let point = [x - paragraph.origin[0], y - paragraph.origin[1]];
1635 let cursor = paragraph.text.hit_test(point[0], point[1]);1609 let local = edit::selection_at(
1636 let local = match unit {1610 layout,
1637 SelectionUnit::Grapheme => cursor.into(),1611 paragraph.text.hit_test(point[0], point[1]),
1638 SelectionUnit::Word => {1612 point[0],
1639 let rect = cursor.geometry(layout, 1.0);1613 unit,
1640 ParagraphSelection::word_from_point(1614 );
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 };
1651 let mut selection = self.selection();1615 let mut selection = self.selection();
1652 for (slot, cursor) in [local.anchor(), local.focus()].into_iter().enumerate() {1616 for (slot, cursor) in [local.anchor(), local.focus()].into_iter().enumerate() {
1653 let visible = paragraph.projection.text().utf16_offset(cursor.index())?;1617 let visible = paragraph.projection.text().utf16_offset(cursor.index())?;
...@@ -1665,8 +1629,8 @@ impl CanvasEditor {...@@ -1665,8 +1629,8 @@ impl CanvasEditor {
1665 let document = &self.active_outline().document;1629 let document = &self.active_outline().document;
1666 let source = selection.positions[0].paragraph;1630 let source = selection.positions[0].paragraph;
1667 let next = self.active_outline().source_index(index + 1);1631 let next = self.active_outline().source_index(index + 1);
1668 let (container, local, _) = leaves(document.nodes(), None).nth(source).unwrap();1632 let (container, local, _) = document.leaf(source).unwrap();
1669 let (next_container, next_local, _) = leaves(document.nodes(), None).nth(next).unwrap();1633 let (next_container, next_local, _) = document.leaf(next).unwrap();
1670 if container == next_container && next_local == local + 1 {1634 if container == next_container && next_local == local + 1 {
1671 selection.positions[1] = TextPosition {1635 selection.positions[1] = TextPosition {
1672 paragraph: next,1636 paragraph: next,
...@@ -1767,7 +1731,7 @@ impl CanvasEditor {...@@ -1767,7 +1731,7 @@ impl CanvasEditor {
1767 let outline = self.active_outline();1731 let outline = self.active_outline();
1768 let current = outline.visible_index(focus.paragraph)?;1732 let current = outline.visible_index(focus.paragraph)?;
1769 let end = matches!(movement, Movement::ParagraphEnd | Movement::DocumentEnd);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 let current_end = current_end.utf16_offset(current_end.text().len())?;1735 let current_end = current_end.utf16_offset(current_end.text().len())?;
1772 let index = match movement {1736 let index = match movement {
1773 Movement::DocumentStart => 0,1737 Movement::DocumentStart => 0,
...@@ -1779,7 +1743,7 @@ impl CanvasEditor {...@@ -1779,7 +1743,7 @@ impl CanvasEditor {
1779 _ => current,1743 _ => current,
1780 };1744 };
1781 let source = outline.source_index(index);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 let position = TextPosition {1747 let position = TextPosition {
1784 paragraph: source,1748 paragraph: source,
1785 offset: if end {1749 offset: if end {
...@@ -2030,22 +1994,7 @@ impl CanvasEditor {...@@ -2030,22 +1994,7 @@ impl CanvasEditor {
2030 } else {1994 } else {
2031 cursor.into()1995 cursor.into()
2032 };1996 };
2033 let layout = &paragraph.text.shaped;1997 next = edit::step(&paragraph.text.shaped, local, movement, extend).focus();
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();
2049 if horizontal && next == cursor && (extend || anchor == focus) {1998 if horizontal && next == cursor && (extend || anchor == focus) {
2050 let neighbor = if left {1999 let neighbor = if left {
2051 index.checked_sub(1)2000 index.checked_sub(1)
...@@ -2067,7 +2016,7 @@ impl CanvasEditor {...@@ -2067,7 +2016,7 @@ impl CanvasEditor {
2067 line.top + line.height * 0.5,2016 line.top + line.height * 0.5,
2068 );2017 );
2069 if matches!(movement, Movement::WordLeft | Movement::WordRight) {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,12 +2110,7 @@ impl CanvasEditor {
2161 .enumerate()2110 .enumerate()
2162 .filter(|(_, (index, _))| *index >= start.paragraph && *index <= end.paragraph)2111 .filter(|(_, (index, _))| *index >= start.paragraph && *index <= end.paragraph)
2163 {2112 {
2164 let source = self2113 let source = self.active_outline().document.paragraph(index).unwrap();
2165 .active_outline()
2166 .document
2167 .paragraphs()
2168 .nth(index)
2169 .unwrap();
2170 let first = if index == start.paragraph {2114 let first = if index == start.paragraph {
2171 paragraph.cursor(start.offset, affinities[0])?2115 paragraph.cursor(start.offset, affinities[0])?
2172 } else {2116 } else {
...@@ -2249,8 +2193,7 @@ impl CanvasEditor {...@@ -2249,8 +2193,7 @@ impl CanvasEditor {
2249 let format = self2193 let format = self
2250 .active_outline()2194 .active_outline()
2251 .document2195 .document
2252 .paragraphs()2196 .paragraph(start.paragraph)
2253 .nth(start.paragraph)
2254 .unwrap()2197 .unwrap()
2255 .format_at(start.offset)?;2198 .format_at(start.offset)?;
2256 let replacement = text2199 let replacement = text
...@@ -2268,11 +2211,13 @@ impl CanvasEditor {...@@ -2268,11 +2211,13 @@ impl CanvasEditor {
2268 let [anchor, focus] = outline.selection.positions;2211 let [anchor, focus] = outline.selection.positions;
2269 let start = anchor.min(focus);2212 let start = anchor.min(focus);
2270 let end = anchor.max(focus);2213 let end = anchor.max(focus);
2271 let (container, local_start, _) = leaves(outline.document.nodes(), None)2214 let (container, local_start, _) = outline
2272 .nth(start.paragraph)2215 .document
2216 .leaf(start.paragraph)
2273 .ok_or(EditError::InvalidRange)?;2217 .ok_or(EditError::InvalidRange)?;
2274 let (end_container, local_end, _) = leaves(outline.document.nodes(), None)2218 let (end_container, local_end, _) = outline
2275 .nth(end.paragraph)2219 .document
2220 .leaf(end.paragraph)
2276 .ok_or(EditError::InvalidRange)?;2221 .ok_or(EditError::InvalidRange)?;
2277 if container != end_container {2222 if container != end_container {
2278 return Err(EditError::UnsupportedContent.into());2223 return Err(EditError::UnsupportedContent.into());
...@@ -2343,12 +2288,10 @@ impl CanvasEditor {...@@ -2343,12 +2288,10 @@ impl CanvasEditor {
2343 }) else {2288 }) else {
2344 return Ok(false);2289 return Ok(false);
2345 };2290 };
2346 let (container, local, current) = leaves(document.nodes(), None)2291 let (container, local, current) = document
2347 .nth(focus.paragraph)2292 .leaf(focus.paragraph)
2348 .ok_or(EditError::InvalidRange)?;2293 .ok_or(EditError::InvalidRange)?;
2349 let Some((next_container, next_local, next)) =2294 let Some((next_container, next_local, next)) = document.leaf(neighbor) else {
2350 leaves(document.nodes(), None).nth(neighbor)
2351 else {
2352 return Ok(false);2295 return Ok(false);
2353 };2296 };
2354 if container != next_container || local.abs_diff(next_local) != 1 {2297 if container != next_container || local.abs_diff(next_local) != 1 {
...@@ -2406,8 +2349,7 @@ impl CanvasEditor {...@@ -2406,8 +2349,7 @@ impl CanvasEditor {
2406 let format = self2349 let format = self
2407 .active_outline()2350 .active_outline()
2408 .document2351 .document
2409 .paragraphs()2352 .paragraph(caret.paragraph)
2410 .nth(caret.paragraph)
2411 .unwrap()2353 .unwrap()
2412 .format_at(caret.offset)?2354 .format_at(caret.offset)?
2413 .clone();2355 .clone();
...@@ -2779,8 +2721,7 @@ impl CanvasEditor {...@@ -2779,8 +2721,7 @@ impl CanvasEditor {
2779 let format = self2721 let format = self
2780 .active_outline()2722 .active_outline()
2781 .document2723 .document
2782 .paragraphs()2724 .paragraph(range.start.paragraph)
2783 .nth(range.start.paragraph)
2784 .unwrap()2725 .unwrap()
2785 .format_at(range.start.offset)?;2726 .format_at(range.start.offset)?;
2786 let replacement = text2727 let replacement = text
...@@ -2922,55 +2863,29 @@ impl CanvasEditor {...@@ -2922,55 +2863,29 @@ impl CanvasEditor {
2922 count -= 1;2863 count -= 1;
2923 }2864 }
2924 if count < outline.document.nodes().len() {2865 if count < outline.document.nodes().len() {
2925 let removed = outline.document.nodes().len() - count;2866 let edit = DocumentEdit {
2926 outline2867 columns: BTreeMap::new(),
2927 .document2868 container: None,
2928 .apply(DocumentEdit {2869 range: count..outline.document.nodes().len(),
2929 columns: BTreeMap::new(),2870 replacement: Vec::new(),
2930 container: None,2871 };
2931 range: count..outline.document.nodes().len(),2872 let relayout = outline
2932 replacement: Vec::new(),
2933 })
2934 .expect("removing a provisional suffix preserves a valid document");
2935 outline
2936 .shaped2873 .shaped
2937 .paragraphs2874 .relayout(
2938 .truncate(outline.shaped.paragraphs.len() - removed);2875 outline.document.nodes(),
2939 if outline.shaped.tables.is_empty() && outline.shaped.objects.is_empty() {2876 &edit,
2940 let (origins, size) = arrange(2877 edit.range.clone(),
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(),
2960 &outline.indents,2878 &outline.indents,
2961 outline.wrap_width(),2879 outline.wrap_width(),
2962 outline.layout.width_set_by_user == Some(true),2880 outline.layout.width_set_by_user == Some(true),
2963 0,2881 &mut |_, _, _| Err(LayoutError::UnsupportedContent),
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 },
2970 )2882 )
2971 .expect("removing a provisional suffix preserves table layout");2883 .expect("removing trailing text lays nothing out anew");
2972 outline.shaped = shaped;2884 outline
2973 }2885 .document
2886 .apply(edit)
2887 .expect("removing a provisional suffix preserves a valid document");
2888 outline.shaped.commit(relayout);
2974 }2889 }
2975 if outline.document == self.outlines[index].document {2890 if outline.document == self.outlines[index].document {
2976 self.outlines[index].selection = outline.selection;2891 self.outlines[index].selection = outline.selection;
...@@ -3054,117 +2969,47 @@ impl CanvasEditor {...@@ -3054,117 +2969,47 @@ impl CanvasEditor {
3054 ) -> Result<TextChange, EditorError> {2969 ) -> Result<TextChange, EditorError> {
3055 let outline = self.active_outline();2970 let outline = self.active_outline();
3056 outline.document.validate_edit(&edit)?;2971 outline.document.validate_edit(&edit)?;
3057 let width = outline.wrap_width();2972 let range = match edit.container {
3058 let (visible, shaped, origins, size, tables, objects) = if !crate::outline::all_text(2973 None => edit.range.clone(),
3059 outline.document.nodes().iter().chain(&edit.replacement),2974 Some(cell) => {
3060 ) {2975 let root = outline.document.root(cell)?;
3061 let sources = outline2976 root..root + 1
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());
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 for position in selection.positions {2991 for position in selection.positions {
3152 let (_, _, node) = leaves(outline.document.nodes(), Some(&edit))2992 let (node, before) = outline
3153 .nth(position.paragraph)2993 .document
2994 .edited_leaf(&edit, position.paragraph)
3154 .ok_or(EditError::InvalidRange)?;2995 .ok_or(EditError::InvalidRange)?;
3155 node.text().unwrap().text.byte_offset(position.offset)?;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 .iter()3001 .iter()
3158 .chain(&shaped)
3159 .chain(&outline.shaped.paragraphs[visible.end..])
3160 .any(|paragraph| paragraph.id == node.id)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 return Err(EditError::InvalidRange.into());3007 return Err(EditError::InvalidRange.into());
3163 }3008 }
3164 }3009 }
3165 let placements = match positions {3010 let placements = match positions {
3166 Some(positions) => positions.to_vec(),3011 Some(positions) => positions.to_vec(),
3167 None => self.title_flow(size[1])?,3012 None => self.title_flow(relayout.size[1])?,
3168 };3013 };
3169 let previous_positions = placements3014 let previous_positions = placements
3170 .iter()3015 .iter()
...@@ -3193,14 +3038,8 @@ impl CanvasEditor {...@@ -3193,14 +3038,8 @@ impl CanvasEditor {
3193 return self.apply(engine, edit, selection, false, positions);3038 return self.apply(engine, edit, selection, false, positions);
3194 }3039 }
3195 let outline = self.active_outline_mut();3040 let outline = self.active_outline_mut();
3196 let inverse = outline.document.apply(edit)?;3041 let inverse = outline.document.splice(edit);
3197 outline.shaped.paragraphs.splice(visible, shaped);3042 outline.shaped.commit(relayout);
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;
3204 self.preferred_x = None;3043 self.preferred_x = None;
3205 let previous_selection =3044 let previous_selection =
3206 std::mem::replace(&mut self.active_outline_mut().selection, selection);3045 std::mem::replace(&mut self.active_outline_mut().selection, selection);
...@@ -7414,6 +7253,175 @@ mod tests {...@@ -7414,6 +7253,175 @@ mod tests {
7414 assert!(!editor.redo(&mut engine).unwrap());7253 assert!(!editor.redo(&mut engine).unwrap());
7415 }7254 }
74167255
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 #[test]7425 #[test]
7418 fn structural_edits_reflow_only_affected_paragraphs_and_restore_selection() {7426 fn structural_edits_reflow_only_affected_paragraphs_and_restore_selection() {
7419 let mut engine = TextEngine::default();7427 let mut engine = TextEngine::default();
crates/canvas/src/editor/page.rs-1
...@@ -97,7 +97,6 @@ impl ReadOnlyObject {...@@ -97,7 +97,6 @@ impl ReadOnlyObject {
97 onestore::document::Format {97 onestore::document::Format {
98 font: Some("Arial".into()),98 font: Some("Arial".into()),
99 font_size: Some(11.0),99 font_size: Some(11.0),
100 color: Some(0x005d554e),
101 ..Default::default()100 ..Default::default()
102 },101 },
103 ),102 ),
crates/canvas/src/editor/table.rs+6-4
...@@ -84,8 +84,9 @@ impl CanvasEditor {...@@ -84,8 +84,9 @@ impl CanvasEditor {
84 }84 }
85 let outline = self.active_outline();85 let outline = self.active_outline();
86 let [anchor, focus] = outline.selection.positions;86 let [anchor, focus] = outline.selection.positions;
87 let (cell, local, source) = leaves(outline.document.nodes(), None)87 let (cell, local, source) = outline
88 .nth(focus.paragraph)88 .document
89 .leaf(focus.paragraph)
89 .ok_or(EditError::InvalidRange)?;90 .ok_or(EditError::InvalidRange)?;
90 let Some(cell) = cell else {91 let Some(cell) = cell else {
91 if backward || anchor != focus || focus.offset == 0 {92 if backward || anchor != focus || focus.offset == 0 {
...@@ -255,8 +256,9 @@ impl CanvasEditor {...@@ -255,8 +256,9 @@ impl CanvasEditor {
255 }256 }
256 let outline = self.active_outline();257 let outline = self.active_outline();
257 let [anchor, focus] = outline.selection.positions;258 let [anchor, focus] = outline.selection.positions;
258 let (cell, local, source) = leaves(outline.document.nodes(), None)259 let (cell, local, source) = outline
259 .nth(focus.paragraph)260 .document
261 .leaf(focus.paragraph)
260 .ok_or(EditError::InvalidRange)?;262 .ok_or(EditError::InvalidRange)?;
261 let Some(cell) = cell.filter(|_| anchor == focus) else {263 let Some(cell) = cell.filter(|_| anchor == focus) else {
262 return self.insert(engine, "\n");264 return self.insert(engine, "\n");
crates/canvas/src/gpu/mod.rs+71-18
...@@ -37,6 +37,32 @@ impl Viewport {...@@ -37,6 +37,32 @@ impl Viewport {
37 }37 }
38}38}
3939
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)]
43pub struct Paper {
44 pub color: [f32; 4],
45 pub ink: [f32; 4],
46}
47
48impl 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
40impl Glyphs for TextLayout {66impl Glyphs for TextLayout {
41 fn runs(67 fn runs(
42 &self,68 &self,
...@@ -52,7 +78,8 @@ impl Glyphs for TextLayout {...@@ -52,7 +78,8 @@ impl Glyphs for TextLayout {
52 bounds.baseline,78 bounds.baseline,
53 run.style().brush.rise,79 run.style().brush.rise,
54 [bounds.top, bounds.height],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 paint,83 paint,
57 )?;84 )?;
58 }85 }
...@@ -147,14 +174,24 @@ mod tests {...@@ -147,14 +174,24 @@ mod tests {
147 ];174 ];
148 let text = TextEngine::default()175 let text = TextEngine::default()
149 .layout(176 .layout(
150 &Paragraph::new(177 &Paragraph::from_runs([
151 "Tagged".into(),178 (
152 Format {179 "Tagged".into(),
153 color: Some(0x000000ff),180 Format {
154 underline: Some(true),181 color: Some(0x000000ff),
155 ..Format::default()182 underline: Some(true),
156 },183 ..Format::default()
157 ),184 },
185 ),
186 (" automatic".into(), Format::default()),
187 (
188 " lit".into(),
189 Format {
190 highlight: Some(0x0000ffff),
191 ..Format::default()
192 },
193 ),
194 ]),
158 100.0,195 100.0,
159 )196 )
160 .unwrap();197 .unwrap();
...@@ -165,13 +202,14 @@ mod tests {...@@ -165,13 +202,14 @@ mod tests {
165 sources: tag_sources(*icon),202 sources: tag_sources(*icon),
166 origin: [2.0 + 16.0 * index as f32, 2.0],203 origin: [2.0 + 16.0 * index as f32, 2.0],
167 size: crate::outline::ParagraphTag::SIZE,204 size: crate::outline::ParagraphTag::SIZE,
168 opacity: 1.0,205 tint: [1.0; 4],
169 })206 })
170 .collect();207 .collect();
171 primitives.push(Primitive::Text {208 primitives.push(Primitive::Text {
172 text: &text,209 text: &text,
173 origin: [2.0, 20.0],210 origin: [2.0, 20.0],
174 clip: None,211 clip: None,
212 ink: [0.0, 1.0, 0.0, 1.0],
175 });213 });
176 let viewport = Viewport {214 let viewport = Viewport {
177 size,215 size,
...@@ -224,13 +262,28 @@ mod tests {...@@ -224,13 +262,28 @@ mod tests {
224 .count();262 .count();
225 assert!(painted > 20, "tag {index} was not painted");263 assert!(painted > 20, "tag {index} was not painted");
226 }264 }
227 let red = (40..64)265 let count = |color: [u8; 3]| {
228 .flat_map(|y| (4..120).map(move |x| (x, y)))266 (40..64)
229 .filter(|(x, y)| {267 .flat_map(|y| (4..size[0]).map(move |x| (x, y)))
230 let [r, g, b] = pixel(*x, *y);268 .filter(|(x, y)| {
231 r > 200 && g < 80 && b < 80269 pixel(*x, *y)
232 })270 .iter()
233 .count();271 .zip(color)
234 assert!(red > 50, "text colour or underline was not painted");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,4 +1,6 @@
1use super::colorref;1mod background;
2
3use super::{Paper, colorref};
2use crate::editor::ReadOnlyObject;4use crate::editor::ReadOnlyObject;
3use crate::editor::page::Content;5use crate::editor::page::Content;
4use crate::{6use crate::{
...@@ -6,6 +8,7 @@ use crate::{...@@ -6,6 +8,7 @@ use crate::{
6 editor::{CanvasEditor, EditorError},8 editor::{CanvasEditor, EditorError},
7 layout::{LayoutError, TextEngine},9 layout::{LayoutError, TextEngine},
8};10};
11use background::Background;
9use draw::{Primitive, RasterImage, RenderError};12use draw::{Primitive, RasterImage, RenderError};
10use onestore::page::Page;13use onestore::page::Page;
11use std::fmt;14use std::fmt;
...@@ -14,6 +17,8 @@ use std::fmt;...@@ -14,6 +17,8 @@ use std::fmt;
14pub struct PageScene {17pub struct PageScene {
15 reference: Option<Vec<Content>>,18 reference: Option<Vec<Content>>,
16 images: std::collections::BTreeMap<onestore::ExGuid, RasterImage>,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}
1823
19#[derive(Debug, PartialEq)]24#[derive(Debug, PartialEq)]
...@@ -82,6 +87,17 @@ fn append_ink(ink: &onestore::page::Ink, offset: [f32; 2], primitives: &mut Vec<...@@ -82,6 +87,17 @@ fn append_ink(ink: &onestore::page::Ink, offset: [f32; 2], primitives: &mut Vec<
82 }87 }
83}88}
8489
90fn 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#[derive(Debug)]101#[derive(Debug)]
86pub enum SceneError {102pub enum SceneError {
87 Layout(LayoutError),103 Layout(LayoutError),
...@@ -127,10 +143,10 @@ impl PageScene {...@@ -127,10 +143,10 @@ impl PageScene {
127 let objects = crate::editor::page::build(&mut page, engine, false)143 let objects = crate::editor::page::build(&mut page, engine, false)
128 .map_err(SceneError::from)?144 .map_err(SceneError::from)?
129 .objects;145 .objects;
130 let images = Self::decode_images(&objects, None)?;146 let pictures = Self::decode_images(&objects, None)?;
131 Ok(Self {147 Ok(Self {
132 reference: Some(objects),148 reference: Some(objects),
133 images,149 ..pictures
134 })150 })
135 }151 }
136152
...@@ -139,21 +155,15 @@ impl PageScene {...@@ -139,21 +155,15 @@ impl PageScene {
139 engine: &mut TextEngine,155 engine: &mut TextEngine,
140 ) -> Result<(Self, CanvasEditor), SceneError> {156 ) -> Result<(Self, CanvasEditor), SceneError> {
141 let editor = CanvasEditor::from_page(page, engine).map_err(SceneError::from)?;157 let editor = CanvasEditor::from_page(page, engine).map_err(SceneError::from)?;
142 let images = Self::decode_images(&editor.objects, Some(&editor))?;158 Ok((Self::decode_images(&editor.objects, Some(&editor))?, editor))
143 Ok((
144 Self {
145 reference: None,
146 images,
147 },
148 editor,
149 ))
150 }159 }
151160
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 fn decode_images(163 fn decode_images(
154 objects: &[Content],164 objects: &[Content],
155 editor: Option<&CanvasEditor>,165 editor: Option<&CanvasEditor>,
156 ) -> Result<std::collections::BTreeMap<onestore::ExGuid, RasterImage>, SceneError> {166 ) -> Result<Self, SceneError> {
157 fn nested<'a>(167 fn nested<'a>(
158 nodes: &'a [onestore::page::PageParagraph],168 nodes: &'a [onestore::page::PageParagraph],
159 payloads: &mut Vec<(onestore::ExGuid, Option<&'a [u8]>)>,169 payloads: &mut Vec<(onestore::ExGuid, Option<&'a [u8]>)>,
...@@ -179,9 +189,18 @@ impl PageScene {...@@ -179,9 +189,18 @@ impl PageScene {
179 }189 }
180 }190 }
181 let mut payloads = Vec::new();191 let mut payloads = Vec::new();
192 let mut backgrounds = std::collections::BTreeMap::new();
182 for object in objects {193 for object in objects {
183 match object {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 Content::Outline { source, .. } => nested(&source.paragraphs, &mut payloads),204 Content::Outline { source, .. } => nested(&source.paragraphs, &mut payloads),
186 Content::Editable(id) => {205 Content::Editable(id) => {
187 if let Some(outline) =206 if let Some(outline) =
...@@ -206,13 +225,34 @@ impl PageScene {...@@ -206,13 +225,34 @@ impl PageScene {
206 return Err(SceneError::InvalidGeometry);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 }
211234
212 pub fn image(&self, id: onestore::ExGuid) -> Option<&RasterImage> {235 pub fn image(&self, id: onestore::ExGuid) -> Option<&RasterImage> {
213 self.images.get(&id)236 self.images.get(&id)
214 }237 }
215238
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 /// Pictures, files and handwriting inside an outline whose origin is `origin`.256 /// Pictures, files and handwriting inside an outline whose origin is `origin`.
217 /// `moving` draws one picture at a previewed rectangle, in the same coordinates as `origin`.257 /// `moving` draws one picture at a previewed rectangle, in the same coordinates as `origin`.
218 pub fn append_outline_objects<'a>(258 pub fn append_outline_objects<'a>(
...@@ -220,6 +260,7 @@ impl PageScene {...@@ -220,6 +260,7 @@ impl PageScene {
220 outline: &'a crate::outline::OutlineLayout,260 outline: &'a crate::outline::OutlineLayout,
221 origin: [f32; 2],261 origin: [f32; 2],
222 moving: Option<(onestore::ExGuid, [f32; 4])>,262 moving: Option<(onestore::ExGuid, [f32; 4])>,
263 paper: Paper,
223 primitives: &mut Vec<Primitive<'a>>,264 primitives: &mut Vec<Primitive<'a>>,
224 ) {265 ) {
225 for object in &outline.objects {266 for object in &outline.objects {
...@@ -241,14 +282,7 @@ impl PageScene {...@@ -241,14 +282,7 @@ impl PageScene {
241 append_ink(ink, [rect[0], rect[1]], primitives)282 append_ink(ink, [rect[0], rect[1]], primitives)
242 }283 }
243 crate::outline::ObjectKind::Unsupported(_) => {284 crate::outline::ObjectKind::Unsupported(_) => {
244 primitives.push(Primitive::Rect {285 append_placeholder(rect, paper, primitives)
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 });
252 }286 }
253 crate::outline::ObjectKind::Picture | crate::outline::ObjectKind::File(_) => {}287 crate::outline::ObjectKind::Picture | crate::outline::ObjectKind::File(_) => {}
254 }288 }
...@@ -257,6 +291,7 @@ impl PageScene {...@@ -257,6 +291,7 @@ impl PageScene {
257 text: &label.text,291 text: &label.text,
258 origin: [origin[0] + label.origin[0], origin[1] + label.origin[1]],292 origin: [origin[0] + label.origin[0], origin[1] + label.origin[1]],
259 clip: None,293 clip: None,
294 ink: paper.ink,
260 });295 });
261 }296 }
262 }297 }
...@@ -445,8 +480,9 @@ impl PageScene {...@@ -445,8 +480,9 @@ impl PageScene {
445 &'a self,480 &'a self,
446 primitives: &mut Vec<Primitive<'a>>,481 primitives: &mut Vec<Primitive<'a>>,
447 offset: [f32; 2],482 offset: [f32; 2],
483 paper: Paper,
448 ) -> Result<(), SceneError> {484 ) -> Result<(), SceneError> {
449 self.append_primitives_with(primitives, offset, None, None, |_, _, _| {485 self.append_primitives_with(primitives, offset, None, None, paper, |_, _, _| {
450 Err(SceneError::MissingOutline)486 Err(SceneError::MissingOutline)
451 })487 })
452 }488 }
...@@ -458,6 +494,7 @@ impl PageScene {...@@ -458,6 +494,7 @@ impl PageScene {
458 offset: [f32; 2],494 offset: [f32; 2],
459 editor: Option<&'a CanvasEditor>,495 editor: Option<&'a CanvasEditor>,
460 moving: Option<(onestore::ExGuid, [f32; 4])>,496 moving: Option<(onestore::ExGuid, [f32; 4])>,
497 paper: Paper,
461 mut outline: impl FnMut(onestore::ExGuid, [f32; 2], &mut Vec<Primitive<'a>>) -> Result<(), E>,498 mut outline: impl FnMut(onestore::ExGuid, [f32; 2], &mut Vec<Primitive<'a>>) -> Result<(), E>,
462 ) -> Result<(), E> {499 ) -> Result<(), E> {
463 for content in self.objects(editor)? {500 for content in self.objects(editor)? {
...@@ -499,18 +536,12 @@ impl PageScene {...@@ -499,18 +536,12 @@ impl PageScene {
499 x1 + offset[0],536 x1 + offset[0],
500 y1 + offset[1],537 y1 + offset[1],
501 ];538 ];
502 primitives.push(Primitive::Rect {539 append_placeholder(rect, paper, primitives);
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 });
510 primitives.push(Primitive::Text {540 primitives.push(Primitive::Text {
511 clip: None,541 clip: None,
512 text: &object.label,542 text: &object.label,
513 origin: [rect[0] + 8.0, rect[1] + 8.0],543 origin: [rect[0] + 8.0, rect[1] + 8.0],
544 ink: paper.shade(colorref(0x005d554e)),
514 });545 });
515 continue;546 continue;
516 }547 }
...@@ -525,28 +556,41 @@ impl PageScene {...@@ -525,28 +556,41 @@ impl PageScene {
525 };556 };
526 let object_origin = [origin[0] + offset[0], origin[1] + offset[1]];557 let object_origin = [origin[0] + offset[0], origin[1] + offset[1]];
527 match content {558 match content {
528 Content::Image(source) => primitives.push(Primitive::Image {559 Content::Image(source) => {
529 image: self560 let (image, [width, height]) = match self.backgrounds.get(&source.id) {
530 .images561 // The paper shows until the worker's raster lands.
531 .get(&source.id)562 Some(art) => match art.image(paper) {
532 .ok_or(SceneError::MissingImage)?,563 Some(image) => (image, art.size),
533 rect: match moving {564 None => continue,
534 Some((id, [x0, y0, x1, y1])) if id == source.id => [565 },
535 x0 + offset[0],566 None => (
536 y0 + offset[1],567 self.images
537 x1 + offset[0],568 .get(&source.id)
538 y1 + offset[1],569 .ok_or(SceneError::MissingImage)?,
539 ],570 [
540 _ => [571 source.layout.max_width.ok_or(SceneError::MissingImage)?,
541 object_origin[0],572 source.layout.max_height.ok_or(SceneError::MissingImage)?,
542 object_origin[1],573 ],
543 object_origin[0]574 ),
544 + source.layout.max_width.ok_or(SceneError::MissingImage)?,575 };
545 object_origin[1]576 primitives.push(Primitive::Image {
546 + source.layout.max_height.ok_or(SceneError::MissingImage)?,577 image,
547 ],578 rect: match moving {
548 },579 Some((id, [x0, y0, x1, y1])) if id == source.id => [
549 }),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 Content::Outline { .. } | Content::Date { .. } => {594 Content::Outline { .. } | Content::Date { .. } => {
551 let outline = match content {595 let outline = match content {
552 Content::Outline { layout, .. } => layout,596 Content::Outline { layout, .. } => layout,
...@@ -555,14 +599,16 @@ impl PageScene {...@@ -555,14 +599,16 @@ impl PageScene {
555 .ok_or(SceneError::MissingOutline)?599 .ok_or(SceneError::MissingOutline)?
556 .layout(),600 .layout(),
557 };601 };
558 outline.append_table_primitives(primitives, object_origin);602 outline.append_table_primitives(primitives, object_origin, paper);
559 outline.append_background_primitives(primitives, object_origin);603 let everything = [f32::NEG_INFINITY, f32::INFINITY];
560 self.append_outline_objects(outline, object_origin, None, primitives);604 outline.append_background_primitives(primitives, object_origin, everything);
561 for (index, paragraph) in outline.paragraphs.iter().enumerate() {605 self.append_outline_objects(outline, object_origin, None, paper, primitives);
606 for (index, paragraph) in outline.visible(everything) {
562 outline.append_paragraph_primitives(607 outline.append_paragraph_primitives(
563 index,608 index,
564 paragraph,609 paragraph,
565 object_origin,610 object_origin,
611 paper.ink,
566 primitives,612 primitives,
567 );613 );
568 }614 }
...@@ -575,12 +621,33 @@ impl PageScene {...@@ -575,12 +621,33 @@ impl PageScene {
575}621}
576622
577impl crate::outline::OutlineLayout {623impl 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 pub fn append_background_primitives(644 pub fn append_background_primitives(
579 &self,645 &self,
580 primitives: &mut Vec<Primitive<'_>>,646 primitives: &mut Vec<Primitive<'_>>,
581 origin: [f32; 2],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 for (mut rect, color) in paragraph.text.backgrounds() {651 for (mut rect, color) in paragraph.text.backgrounds() {
585 rect.x0 += f64::from(paragraph.origin[0]);652 rect.x0 += f64::from(paragraph.origin[0]);
586 rect.x1 += f64::from(paragraph.origin[0]);653 rect.x1 += f64::from(paragraph.origin[0]);
...@@ -612,6 +679,7 @@ impl crate::outline::OutlineLayout {...@@ -612,6 +679,7 @@ impl crate::outline::OutlineLayout {
612 index: usize,679 index: usize,
613 paragraph: &'a crate::outline::ParagraphLayout,680 paragraph: &'a crate::outline::ParagraphLayout,
614 origin: [f32; 2],681 origin: [f32; 2],
682 ink: [f32; 4],
615 primitives: &mut Vec<Primitive<'a>>,683 primitives: &mut Vec<Primitive<'a>>,
616 ) {684 ) {
617 let [x, y] = [685 let [x, y] = [
...@@ -635,6 +703,7 @@ impl crate::outline::OutlineLayout {...@@ -635,6 +703,7 @@ impl crate::outline::OutlineLayout {
635 clip,703 clip,
636 text: layout,704 text: layout,
637 origin: [x + origin[0], y + origin[1]],705 origin: [x + origin[0], y + origin[1]],
706 ink,
638 },707 },
639 // As pen strokes, so hairline rules keep a device pixel.708 // As pen strokes, so hairline rules keep a device pixel.
640 crate::math::MathItem::Rule([x0, y0, x1, y1]) => {709 crate::math::MathItem::Rule([x0, y0, x1, y1]) => {
...@@ -651,7 +720,7 @@ impl crate::outline::OutlineLayout {...@@ -651,7 +720,7 @@ impl crate::outline::OutlineLayout {
651 to: [x + to[0], y + to[1]],720 to: [x + to[0], y + to[1]],
652 width,721 width,
653 round: false,722 round: false,
654 color: colorref(math.color),723 color: math.color.map_or(ink, colorref),
655 }724 }
656 }725 }
657 crate::math::MathItem::Stroke { from, to, width } => Primitive::Segment {726 crate::math::MathItem::Stroke { from, to, width } => Primitive::Segment {
...@@ -659,7 +728,7 @@ impl crate::outline::OutlineLayout {...@@ -659,7 +728,7 @@ impl crate::outline::OutlineLayout {
659 to: [x + to[0], y + to[1]],728 to: [x + to[0], y + to[1]],
660 width: *width,729 width: *width,
661 round: true,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,6 +737,7 @@ impl crate::outline::OutlineLayout {
668 clip,737 clip,
669 text: &paragraph.text,738 text: &paragraph.text,
670 origin: [x, y],739 origin: [x, y],
740 ink,
671 }),741 }),
672 }742 }
673 for (layout, marker) in &paragraph.markers {743 for (layout, marker) in &paragraph.markers {
...@@ -675,6 +745,7 @@ impl crate::outline::OutlineLayout {...@@ -675,6 +745,7 @@ impl crate::outline::OutlineLayout {
675 clip,745 clip,
676 text: layout,746 text: layout,
677 origin: [origin[0] + marker[0], y + marker[1]],747 origin: [origin[0] + marker[0], y + marker[1]],
748 ink,
678 });749 });
679 }750 }
680 for tag in &paragraph.tags {751 for tag in &paragraph.tags {
...@@ -685,12 +756,17 @@ impl crate::outline::OutlineLayout {...@@ -685,12 +756,17 @@ impl crate::outline::OutlineLayout {
685 y + tag.origin[1],756 y + tag.origin[1],
686 ],757 ],
687 size: crate::outline::ParagraphTag::SIZE,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 }
692763
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 for table in &self.tables {770 for table in &self.tables {
695 let (Some(first), Some(last)) = (table.cells.first(), table.cells.last()) else {771 let (Some(first), Some(last)) = (table.cells.first(), table.cells.last()) else {
696 continue;772 continue;
...@@ -699,7 +775,7 @@ impl crate::outline::OutlineLayout {...@@ -699,7 +775,7 @@ impl crate::outline::OutlineLayout {
699 continue;775 continue;
700 }776 }
701 let bounds = [first.rect[0], first.rect[1], last.rect[2], last.rect[3]];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 primitives.push(Primitive::RoundedRect {779 primitives.push(Primitive::RoundedRect {
704 rect: [780 rect: [
705 bounds[0] + origin[0] - 0.375,781 bounds[0] + origin[0] - 0.375,
...@@ -1155,6 +1231,7 @@ mod tests {...@@ -1155,6 +1231,7 @@ mod tests {
1155 [0.0; 2],1231 [0.0; 2],
1156 Some(editor),1232 Some(editor),
1157 None,1233 None,
1234 Paper::WHITE,
1158 |_, _, _| Ok(()),1235 |_, _, _| Ok(()),
1159 )1236 )
1160 .unwrap();1237 .unwrap();
...@@ -1553,6 +1630,7 @@ mod tests {...@@ -1553,6 +1630,7 @@ mod tests {
1553 [0.0; 2],1630 [0.0; 2],
1554 Some(editor),1631 Some(editor),
1555 None,1632 None,
1633 Paper::WHITE,
1556 |id, offset, primitives| {1634 |id, offset, primitives| {
1557 let outline = editor1635 let outline = editor
1558 .outlines()1636 .outlines()
...@@ -1563,6 +1641,7 @@ mod tests {...@@ -1563,6 +1641,7 @@ mod tests {
1563 primitives.push(Primitive::Text {1641 primitives.push(Primitive::Text {
1564 clip: None,1642 clip: None,
1565 text: &paragraph.text,1643 text: &paragraph.text,
1644 ink: Paper::WHITE.ink,
1566 origin: [1645 origin: [
1567 outline.origin()[0] + paragraph.origin[0] + offset[0],1646 outline.origin()[0] + paragraph.origin[0] + offset[0],
1568 outline.origin()[1] + paragraph.origin[1] + offset[1],1647 outline.origin()[1] + paragraph.origin[1] + offset[1],
...@@ -1643,7 +1722,7 @@ mod tests {...@@ -1643,7 +1722,7 @@ mod tests {
1643 let (scene, mut editor) = PageScene::from_page(page(), &mut engine).unwrap();1722 let (scene, mut editor) = PageScene::from_page(page(), &mut engine).unwrap();
1644 let mut reference_primitives = Vec::new();1723 let mut reference_primitives = Vec::new();
1645 reference1724 reference
1646 .append_primitives(&mut reference_primitives, [0.0; 2])1725 .append_primitives(&mut reference_primitives, [0.0; 2], Paper::WHITE)
1647 .unwrap();1726 .unwrap();
1648 let image_id;1727 let image_id;
1649 {1728 {
...@@ -1656,11 +1735,13 @@ mod tests {...@@ -1656,11 +1735,13 @@ mod tests {
1656 clip: None,1735 clip: None,
1657 text: a,1736 text: a,
1658 origin: x,1737 origin: x,
1738 ..
1659 },1739 },
1660 Primitive::Text {1740 Primitive::Text {
1661 clip: None,1741 clip: None,
1662 text: b,1742 text: b,
1663 origin: y,1743 origin: y,
1744 ..
1664 },1745 },
1665 ) => {1746 ) => {
1666 assert_eq!(x, y);1747 assert_eq!(x, y);
...@@ -1753,7 +1834,7 @@ mod tests {...@@ -1753,7 +1834,7 @@ mod tests {
1753 .advance1834 .advance
1754 );1835 );
1755 assert!(matches!(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 Err(SceneError::MissingOutline)1838 Err(SceneError::MissingOutline)
1758 ));1839 ));
1759 }1840 }
...@@ -1804,6 +1885,7 @@ mod tests {...@@ -1804,6 +1885,7 @@ mod tests {
1804 [5.0, 0.0],1885 [5.0, 0.0],
1805 Some(&editor),1886 Some(&editor),
1806 None,1887 None,
1888 Paper::WHITE,
1807 |_, _, _| Ok(()),1889 |_, _, _| Ok(()),
1808 )1890 )
1809 .unwrap();1891 .unwrap();
...@@ -1870,9 +1952,11 @@ mod tests {...@@ -1870,9 +1952,11 @@ mod tests {
1870 ));1952 ));
1871 }1953 }
1872 let mut primitives = Vec::new();1954 let mut primitives = Vec::new();
1873 scene.append_primitives(&mut primitives, [0.0; 2]).unwrap();
1874 scene1955 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 .unwrap();1960 .unwrap();
1877 let [1961 let [
1878 Primitive::Image {1962 Primitive::Image {
...@@ -1942,6 +2026,7 @@ mod tests {...@@ -1942,6 +2026,7 @@ mod tests {
1942 [0.0; 2],2026 [0.0; 2],
1943 Some(&editor),2027 Some(&editor),
1944 Some((id, [70.0, 5.0, 120.0, 55.0])),2028 Some((id, [70.0, 5.0, 120.0, 55.0])),
2029 Paper::WHITE,
1945 |_, _, _| Ok(()),2030 |_, _, _| Ok(()),
1946 )2031 )
1947 .unwrap();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
4use crate::gpu::Paper;
5use draw::RasterImage;
6use resvg::{tiny_skia, usvg};
7use sha2::{Digest, Sha256};
8use 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.
16const 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.
313const MAX_RASTER_BYTES: f32 = 16.0 * 1024.0 * 1024.0;
314/// How long the zoom must hold before art is rasterized again for it.
315const SETTLE: Duration = Duration::from_millis(150);
316
317/// Template art standing in for a page's background picture.
318pub(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)]
327struct 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.
335struct 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
345impl 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
419fn 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.
426fn 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
448fn 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
474fn 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.
481fn 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.
514fn 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.
529fn 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)]
545mod 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,12 +101,15 @@ fn renderer_cost() {
101 }101 }
102 let mut primitives = Vec::new();102 let mut primitives = Vec::new();
103 if let Some(scene) = &scene {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 } else {107 } else {
106 primitives.push(draw::Primitive::Text {108 primitives.push(draw::Primitive::Text {
107 clip: None,109 clip: None,
108 text: layout.as_ref().unwrap(),110 text: layout.as_ref().unwrap(),
109 origin: [36.0, 90.0],111 origin: [36.0, 90.0],
112 ink: Paper::WHITE.ink,
110 });113 });
111 }114 }
112 let start = Instant::now();115 let start = Instant::now();
...@@ -150,3 +153,202 @@ fn load_scene(bytes: &[u8], title: &str, engine: &mut TextEngine) -> page::PageS...@@ -150,3 +153,202 @@ fn load_scene(bytes: &[u8], title: &str, engine: &mut TextEngine) -> page::PageS
150 )153 )
151 .unwrap()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"]
164fn 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,141 +1,89 @@
1use crate::gpu::Viewport;1use crate::gpu::{Viewport, page::PageScene};
2use crate::{2use crate::{
3 date::DateField,3 date::DateField,
4 document::TextPosition,4 document::TextPosition,
5 editor::{CanvasEditor, Selection, TextOutline},5 editor::{CanvasEditor, Selection},
6 outline::ParagraphLayout,
6};7};
7use accesskit::{8use accesskit::{
8 Action, Affine, Node, NodeId, Rect, Role, TextDirection, TextSelection, TreeId, TreeInfo,9 Action, Affine, Node, NodeId, Rect, Role, TextDirection, TextSelection, TreeId, TreeInfo,
9 TreeUpdate,10 TreeUpdate,
10};11};
11use onestore::page::text::EditError;12use onestore::{ExGuid, page::text::EditError};
12use parley::Affinity;13use parley::Affinity;
13use std::{collections::HashMap, ops::Range};14use std::collections::HashMap;
1415
15pub const ROOT: NodeId = NodeId(0);16pub 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.
19const PAGE: NodeId = NodeId(1);
1620
17struct Run {21struct Run {
18 outline: onestore::ExGuid,
19 id: NodeId,22 id: NodeId,
20 node: Node,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}
2328
24#[derive(Hash, PartialEq, Eq)]29/// A paragraph's runs, in paragraph coordinates under a node at its origin, so reflow above
25struct ParagraphKey {30/// it moves one node.
31struct Paragraph {
32 id: NodeId,
26 layout: u64,33 layout: u64,
27 source: usize,34 source: usize,
28 next: Option<usize>,35 origin: [f32; 2],
29 origin: [u32; 2],36 /// Its text's byte length in the field's value.
37 length: usize,
38 runs: Vec<Run>,
39}
40
41impl 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}
3152
53struct 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.
32pub struct Accessibility {61pub struct Accessibility {
33 outlines: Vec<(onestore::ExGuid, NodeId)>,62 fields: Vec<Field>,
34 runs: Vec<Run>,63 read_only: Vec<(NodeId, Node)>,
35 paragraphs: HashMap<ParagraphKey, Range<usize>>,64 dates: Vec<(DateField, NodeId, Node)>,
36 read_only: Vec<NodeId>,
37 dates: Vec<(DateField, NodeId)>,
38 next_id: u64,65 next_id: u64,
39}66}
4067
41impl Default for Accessibility {68impl Default for Accessibility {
42 fn default() -> Self {69 fn default() -> Self {
43 Self {70 Self {
44 outlines: Vec::new(),71 fields: Vec::new(),
45 runs: Vec::new(),
46 paragraphs: HashMap::new(),
47 read_only: Vec::new(),72 read_only: Vec::new(),
48 dates: Vec::new(),73 dates: Vec::new(),
49 next_id: 1,74 next_id: 2,
50 }75 }
51 }76 }
52}77}
5378
54impl Accessibility {79impl 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 pub fn deactivate(&mut self) {81 pub fn deactivate(&mut self) {
57 self.runs.clear();82 self.fields.clear();
58 self.paragraphs.clear();
59 self.outlines.clear();
60 self.read_only.clear();83 self.read_only.clear();
61 self.dates.clear();84 self.dates.clear();
62 }85 }
6386
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 fn allocate(&mut self) -> NodeId {87 fn allocate(&mut self) -> NodeId {
140 let id = NodeId(self.next_id);88 let id = NodeId(self.next_id);
141 self.next_id = self89 self.next_id = self
...@@ -146,309 +94,132 @@ impl Accessibility {...@@ -146,309 +94,132 @@ impl Accessibility {
146 }94 }
14795
148 pub fn read_only_for_node(&self, node: NodeId) -> Option<usize> {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 }
15199
152 pub fn date_for_node(&self, node: NodeId) -> Option<DateField> {100 pub fn date_for_node(&self, node: NodeId) -> Option<DateField> {
153 self.dates101 self.dates
154 .iter()102 .iter()
155 .find(|(_, id)| *id == node)103 .find(|(_, id, _)| *id == node)
156 .map(|(field, _)| *field)104 .map(|(field, ..)| *field)
157 }105 }
158106
159 pub fn outline_for_node(&self, node: NodeId) -> Option<onestore::ExGuid> {107 pub fn outline_for_node(&self, node: NodeId) -> Option<ExGuid> {
160 self.outlines108 self.fields
161 .iter()109 .iter()
162 .find(|(_, id)| *id == node)110 .find(|field| field.id == node)
163 .map(|(outline, _)| *outline)111 .map(|field| field.outline)
164 }112 }
165113
114 /// The nodes that changed since the last update. After an error, `deactivate` before the
115 /// next update.
166 pub fn update(116 pub fn update(
167 &mut self,117 &mut self,
168 editor: &CanvasEditor,118 editor: &CanvasEditor,
119 scene: Option<&(PageScene, [f32; 2])>,
169 viewport: Viewport,120 viewport: Viewport,
170 title: &str,121 title: &str,
171 preview: Option<(onestore::ExGuid, [f32; 2])>,122 preview: Option<(ExGuid, [f32; 2])>,
123 read_only_focus: Option<usize>,
172 ) -> Result<TreeUpdate, EditError> {124 ) -> Result<TreeUpdate, EditError> {
173 let mut runs = Vec::new();125 let mut nodes = Vec::new();
174 let mut outlines = Vec::new();126 let mut focus = ROOT;
175 let mut paragraph_ranges = HashMap::new();127 let mut previous: HashMap<_, _> = self
176 for outline in editor.visible_outlines().chain(editor.caret_outline()) {128 .fields
177 let field = self129 .drain(..)
178 .outlines130 .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 .iter()160 .iter()
180 .find(|(id, _)| *id == outline.id)161 .enumerate()
181 .map(|(_, node)| *node)162 .take(layouts.len() - suffix)
182 .unwrap_or_else(|| self.allocate());163 .skip(prefix)
183 outlines.push((outline.id, field));164 {
184 let mut paragraphs = outline.layouts().peekable();165 let (id, old) = match replaced.next() {
185 while let Some((paragraph, shaped)) = paragraphs.next() {166 Some(paragraph) => (paragraph.id, paragraph.runs),
186 let layout = &shaped.text;167 None => (self.allocate(), Vec::new()),
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 })
215 };168 };
216 let line_count = layout.lines().count();169 let mut runs = Vec::new();
217 for (line_index, (line, bounds)) in layout.lines().enumerate() {170 for (index, (node, offsets)) in runs_of(shaped, at + 1 < layouts.len())?
218 let line_start = runs.len();171 .into_iter()
219 let mut x = line.metrics().offset;172 .enumerate()
220 let mut logical_runs = Vec::new();173 {
221 for run in line.runs() {174 let id = match old.get(index) {
222 let advance = run.advance();175 Some(old) if old.offsets == offsets && old.node.value() == node.value() => {
223 logical_runs.push((run, x));176 old.id
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;
312 }177 }
313 }178 _ => self.allocate(),
314 // Empty lines still need a text position for selection and insertion.179 };
315 if runs.len() == line_start {180 runs.push(Run { id, node, offsets });
316 let mut node = Node::new(Role::TextRun);181 }
317 node.set_value("");182 for index in 1..runs.len() {
318 node.set_character_lengths(Vec::<u8>::new());183 if runs[index - 1].node.bounds().map(|r| r.y0)
319 node.set_character_positions(Vec::<f32>::new());184 == runs[index].node.bounds().map(|r| r.y0)
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()
336 {185 {
337 let (_, node, source_positions) = runs.last_mut().unwrap();186 let (before, after) = runs.split_at_mut(index);
338 let mut value = node.value().unwrap().to_owned();187 before[index - 1].node.set_next_on_line(after[0].id);
339 value.push('\n');188 after[0].node.set_previous_on_line(before[index - 1].id);
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 });
357 }189 }
358 }190 }
359 if !shaped.tags.is_empty() {191 for (index, run) in runs.iter().enumerate() {
360 let descriptions = shaped192 if old
361 .tags193 .get(index)
362 .iter()194 .is_none_or(|old| old.id != run.id || old.node != run.node)
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() =>
407 {195 {
408 old.id196 nodes.push((run.id, run.node.clone()));
409 }197 }
410 _ => self.allocate(),198 }
411 };199 let start = value.len();
412 Run {200 value.extend(runs.iter().filter_map(|run| run.node.value()));
413 outline,201 let paragraph = Paragraph {
414 id,202 id,
415 node,203 layout: shaped.text.id(),
416 positions,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 }221 let mut node = Node::new(Role::MultilineTextInput);
430 self.runs = runs;222 node.set_label(if outline.title {
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 {
452 "Page title".into()223 "Page title".into()
453 } else if editor224 } else if editor
454 .caret_outline()225 .caret_outline()
...@@ -456,47 +227,121 @@ impl Accessibility {...@@ -456,47 +227,121 @@ impl Accessibility {
456 {227 {
457 "Text input".into()228 "Text input".into()
458 } else {229 } else {
459 format!("Text outline {}", index + 1)230 format!("Text outline {}", ordinal + 1)
460 });231 });
461 field.add_action(Action::Focus);232 node.add_action(Action::Focus);
462 field.add_action(Action::SetTextSelection);233 node.add_action(Action::SetTextSelection);
463 field.add_action(Action::ReplaceSelectedText);234 node.add_action(Action::ReplaceSelectedText);
464 field.add_action(Action::SetValue);235 node.add_action(Action::SetValue);
465 let runs = self.runs.iter().filter(|run| run.outline == outline.id);236 node.set_value(value);
466 field.set_value(237 node.set_children(
467 runs.clone()238 paragraphs
468 .filter_map(|run| run.node.value())239 .iter()
469 .collect::<String>(),240 .map(|paragraph| paragraph.id)
241 .collect::<Vec<_>>(),
470 );242 );
471 field.set_children(runs.map(|run| run.id).collect::<Vec<_>>());
472 let origin = preview243 let origin = preview
473 .filter(|(id, _)| *id == outline.id)244 .filter(|(id, _)| *id == outline.id)
474 .map(|(_, origin)| origin)245 .map(|(_, origin)| origin)
475 .unwrap_or_else(|| outline.origin());246 .unwrap_or_else(|| outline.origin());
476 field.set_transform(Affine::new([247 node.set_transform(Affine::translate((
477 f64::from(viewport.scale),248 f64::from(origin[0]),
478 0.0,249 f64::from(origin[1]),
479 0.0,250 )));
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 ]));
484 let bounds = outline.bounds();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 if outline.id == editor.active_outline().id {253 if outline.id == editor.active_outline().id {
487 let Selection {254 let Selection {
488 positions: [anchor, caret],255 positions: [anchor, caret],
489 affinities,256 affinities,
490 } = editor.selection();257 } = editor.selection();
491 field.set_text_selection(TextSelection {258 node.set_text_selection(TextSelection {
492 anchor: self.position(outline, anchor, affinities[0])?,259 anchor: position(&layouts, &paragraphs, anchor, affinities[0])?,
493 focus: self.position(outline, caret, affinities[1])?,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 Ok(TreeUpdate {345 Ok(TreeUpdate {
501 nodes,346 nodes,
502 tree: Some(TreeInfo::new(ROOT)),347 tree: Some(TreeInfo::new(ROOT)),
...@@ -505,71 +350,47 @@ impl Accessibility {...@@ -505,71 +350,47 @@ impl Accessibility {
505 })350 })
506 }351 }
507352
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 pub fn selection(353 pub fn selection(
556 &self,354 &self,
557 outline: onestore::ExGuid,355 outline: ExGuid,
558 selection: TextSelection,356 selection: TextSelection,
559 ) -> Result<Selection, EditError> {357 ) -> Result<Selection, EditError> {
358 let field = self
359 .fields
360 .iter()
361 .find(|field| field.outline == outline)
362 .ok_or(EditError::InvalidRange)?;
560 let position = |position: accesskit::TextPosition| {363 let position = |position: accesskit::TextPosition| {
561 let run = self364 let (index, paragraph, run) = field
562 .runs365 .paragraphs
563 .iter()366 .iter()
564 .find(|run| run.outline == outline && run.id == position.node)367 .enumerate()
565 .ok_or(EditError::InvalidRange)?;368 .find_map(|(index, paragraph)| {
566 let source = *run369 let run = paragraph
567 .positions370 .runs
568 .get(position.character_index)371 .iter()
372 .position(|run| run.id == position.node)?;
373 Some((index, paragraph, run))
374 })
569 .ok_or(EditError::InvalidRange)?;375 .ok_or(EditError::InvalidRange)?;
570 let affinity = if position.character_index > 0376 let offsets = &paragraph.runs[run].offsets;
571 && position.character_index + 1 == run.positions.len()377 let breaks = run + 1 == paragraph.runs.len() && index + 1 < field.paragraphs.len();
572 {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 Affinity::Upstream394 Affinity::Upstream
574 } else {395 } else {
575 Affinity::Downstream396 Affinity::Downstream
...@@ -585,13 +406,304 @@ impl Accessibility {...@@ -585,13 +406,304 @@ impl Accessibility {
585 }406 }
586}407}
587408
409/// The accessible position of a source position in `layouts`, which `paragraphs` mirrors.
410fn 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.
455fn 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#[cfg(test)]629#[cfg(test)]
589mod tests {630pub(super) mod tests {
590 use super::*;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 use onestore::document::Format;634 use onestore::document::Format;
593 use onestore::page::text::Paragraph;635 use onestore::page::text::Paragraph;
594636
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 #[test]707 #[test]
596 fn title_flow_updates_read_only_hit_and_accessibility_bounds() {708 fn title_flow_updates_read_only_hit_and_accessibility_bounds() {
597 use onestore::page::{Page, PageObject, Title, Unsupported};709 use onestore::page::{Page, PageObject, Title, Unsupported};
...@@ -649,6 +761,7 @@ mod tests {...@@ -649,6 +761,7 @@ mod tests {
649 scale: 2.0,761 scale: 2.0,
650 };762 };
651 let mut access = Accessibility::default();763 let mut access = Accessibility::default();
764 let mut tree = None;
652 let mut identity = None;765 let mut identity = None;
653 for phase in 0..4 {766 for phase in 0..4 {
654 match phase {767 match phase {
...@@ -664,17 +777,18 @@ mod tests {...@@ -664,17 +777,18 @@ mod tests {
664 }777 }
665 _ => {}778 _ => {}
666 }779 }
667 let mut update = access.update(&editor, viewport, "Test", None).unwrap();780 let update = access
668 access.append_page_fields(&mut update, Some(&scene), &editor, viewport, Some(0));781 .update(&editor, Some(&scene), viewport, "Test", None, Some(0))
669 let (id, node) = update
670 .nodes
671 .iter()
672 .find(|(_, node)| node.role() == Role::Label)
673 .unwrap();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 if let Some(identity) = identity {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 let y = if phase % 2 == 0 { 80.0 } else { 145.92 };792 let y = if phase % 2 == 0 { 80.0 } else { 145.92 };
679 let expected = Rect::new(793 let expected = Rect::new(
680 310.0,794 310.0,
...@@ -682,7 +796,7 @@ mod tests {...@@ -682,7 +796,7 @@ mod tests {
682 710.0,796 710.0,
683 f64::from((y + 100.0) * 2.0 - 20.0),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 assert_eq!(bounds.x0, expected.x0);800 assert_eq!(bounds.x0, expected.x0);
687 assert_eq!(bounds.x1, expected.x1);801 assert_eq!(bounds.x1, expected.x1);
688 assert!((bounds.y0 - expected.y0).abs() < 0.001);802 assert!((bounds.y0 - expected.y0).abs() < 0.001);
...@@ -726,6 +840,7 @@ mod tests {...@@ -726,6 +840,7 @@ mod tests {
726 let (scene, mut editor) = PageScene::from_page(page, &mut engine).unwrap();840 let (scene, mut editor) = PageScene::from_page(page, &mut engine).unwrap();
727 let scene = (scene, [30.0, 40.0]);841 let scene = (scene, [30.0, 40.0]);
728 let mut access = Accessibility::default();842 let mut access = Accessibility::default();
843 let mut tree = None;
729 let mut identity = None;844 let mut identity = None;
730 for scale in [1.0, 2.0, 0.5] {845 for scale in [1.0, 2.0, 0.5] {
731 editor.insert(&mut engine, "annotation ").unwrap();846 editor.insert(&mut engine, "annotation ").unwrap();
...@@ -734,29 +849,31 @@ mod tests {...@@ -734,29 +849,31 @@ mod tests {
734 origin: [-200.0, 10.0],849 origin: [-200.0, 10.0],
735 scale,850 scale,
736 };851 };
737 let mut update = access.update(&editor, viewport, "Test", None).unwrap();852 let update = access
738 access.append_page_fields(&mut update, Some(&scene), &editor, viewport, None);853 .update(&editor, Some(&scene), viewport, "Test", None, None)
739 let (id, node) = update
740 .nodes
741 .iter()
742 .find(|(_, node)| node.role() == Role::Label)
743 .unwrap();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 if let Some(identity) = identity {861 if let Some(identity) = identity {
745 assert_eq!(*id, identity);862 assert_eq!(id, identity);
746 }863 }
747 identity = Some(*id);864 identity = Some(id);
748 assert_eq!(node.value(), Some("Unsupported content\nRead-only"));865 assert_eq!(node.data().value(), Some("Unsupported content\nRead-only"));
749 assert!(node.is_read_only());866 assert!(node.is_read_only());
750 assert!(node.supports_action(Action::Focus));867 assert!(node.data().supports_action(Action::Focus));
751 for action in [868 for action in [
752 Action::SetValue,869 Action::SetValue,
753 Action::ReplaceSelectedText,870 Action::ReplaceSelectedText,
754 Action::SetTextSelection,871 Action::SetTextSelection,
755 ] {872 ] {
756 assert!(!node.supports_action(action));873 assert!(!node.data().supports_action(action));
757 }874 }
758 assert_eq!(875 assert_eq!(
759 node.bounds(),876 node.bounding_box(),
760 Some(Rect::new(877 Some(Rect::new(
761 f64::from(40.0 * scale - 200.0),878 f64::from(40.0 * scale - 200.0),
762 f64::from(60.0 * scale + 10.0),879 f64::from(60.0 * scale + 10.0),
...@@ -764,19 +881,22 @@ mod tests {...@@ -764,19 +881,22 @@ mod tests {
764 f64::from(120.0 * scale + 10.0)881 f64::from(120.0 * scale + 10.0)
765 ))882 ))
766 );883 );
767 assert_eq!(access.outline_for_node(*id), None);884 assert_eq!(access.outline_for_node(id), None);
768 let tree = accesskit_consumer::Tree::new(update, true);
769 assert_eq!(885 assert_eq!(
770 tree.state().focus().unwrap().document_range().text(),886 state.focus().unwrap().document_range().text(),
771 "annotation "887 "annotation "
772 );888 );
773 let mut focused = access.update(&editor, viewport, "Test", None).unwrap();889 let focused = access
774 access.append_page_fields(&mut focused, Some(&scene), &editor, viewport, Some(0));890 .update(&editor, Some(&scene), viewport, "Test", None, Some(0))
891 .unwrap();
775 assert_eq!(focused.focus, identity.unwrap());892 assert_eq!(focused.focus, identity.unwrap());
776 assert_eq!(access.read_only_for_node(focused.focus), Some(0));893 assert_eq!(access.read_only_for_node(focused.focus), Some(0));
777 let tree = accesskit_consumer::Tree::new(focused, true);
778 assert_eq!(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 Some("Unsupported content\nRead-only")900 Some("Unsupported content\nRead-only")
781 );901 );
782 editor.undo(&mut engine).unwrap();902 editor.undo(&mut engine).unwrap();
...@@ -800,7 +920,7 @@ mod tests {...@@ -800,7 +920,7 @@ mod tests {
800 .collect(),920 .collect(),
801 )921 )
802 .unwrap();922 .unwrap();
803 let mut nodes = text.nodes().to_vec();923 let mut paragraphs = text.nodes().to_vec();
804 let mut definitions = BTreeMap::new();924 let mut definitions = BTreeMap::new();
805 for (index, (shape, label, status)) in [925 for (index, (shape, label, status)) in [
806 (3, Some("Rehearsal"), 0),926 (3, Some("Rehearsal"), 0),
...@@ -828,7 +948,7 @@ mod tests {...@@ -828,7 +948,7 @@ mod tests {
828 format: Format::default(),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 definition: Some(id),952 definition: Some(id),
833 action_type: None,953 action_type: None,
834 status,954 status,
...@@ -855,13 +975,14 @@ mod tests {...@@ -855,13 +975,14 @@ mod tests {
855 ..Layout::default()975 ..Layout::default()
856 },976 },
857 indents: vec![18.0, 0.0],977 indents: vec![18.0, 0.0],
858 paragraphs: nodes,978 paragraphs,
859 unsupported: Vec::new(),979 unsupported: Vec::new(),
860 }],980 }],
861 definitions,981 definitions,
862 )982 )
863 .unwrap();983 .unwrap();
864 let mut access = Accessibility::default();984 let mut access = Accessibility::default();
985 let mut tree = None;
865 let viewport = Viewport {986 let viewport = Viewport {
866 size: [800, 600],987 size: [800, 600],
867 origin: [48.0; 2],988 origin: [48.0; 2],
...@@ -874,12 +995,14 @@ mod tests {...@@ -874,12 +995,14 @@ mod tests {
874 if step == 3 {995 if step == 3 {
875 editor.undo(&mut engine).unwrap();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 assert_eq!(1002 assert_eq!(
879 update1003 nodes(state)
880 .nodes
881 .iter()1004 .iter()
882 .filter_map(|(_, n)| n.description())1005 .filter_map(|node| node.data().description())
883 .collect::<Vec<_>>(),1006 .collect::<Vec<_>>(),
884 [1007 [
885 "Rehearsal, incomplete",1008 "Rehearsal, incomplete",
...@@ -888,8 +1011,7 @@ mod tests {...@@ -888,8 +1011,7 @@ mod tests {
888 "Music, disabled"1011 "Music, disabled"
889 ]1012 ]
890 );1013 );
891 let tree = accesskit_consumer::Tree::new(update, true);1014 let text = state.focus().unwrap().document_range().text();
892 let text = tree.state().focus().unwrap().document_range().text();
893 assert_eq!(1015 assert_eq!(
894 text,1016 text,
895 if step == 2 {1017 if step == 2 {
...@@ -929,24 +1051,27 @@ mod tests {...@@ -929,24 +1051,27 @@ mod tests {
929 CanvasEditor::new(&mut engine, TextDocument::from_nodes(nodes).unwrap(), 180.0)1051 CanvasEditor::new(&mut engine, TextDocument::from_nodes(nodes).unwrap(), 180.0)
930 .unwrap();1052 .unwrap();
931 let mut access = Accessibility::default();1053 let mut access = Accessibility::default();
1054 let mut tree = None;
932 let viewport = Viewport {1055 let viewport = Viewport {
933 size: [900, 700],1056 size: [900, 700],
934 scale: 2.0,1057 scale: 2.0,
935 origin: [13.0, 19.0],1058 origin: [13.0, 19.0],
936 };1059 };
937 let update = access.update(&editor, viewport, "Test", None).unwrap();1060 let update = access
938 let tree = accesskit_consumer::Tree::new(update, true);1061 .update(&editor, None, viewport, "Test", None, None)
939 let field = tree.state().focus().unwrap();1062 .unwrap();
1063 let field = apply(&mut tree, update).focus().unwrap();
940 assert_eq!(1064 assert_eq!(
941 field.document_range().text(),1065 field.document_range().text(),
942 "root\nfolded\nlast\nfolded tail"1066 "root\nfolded\nlast\nfolded tail"
943 );1067 );
944 assert!(1068 assert_eq!(
945 access1069 access.fields[0]
946 .runs1070 .paragraphs
947 .iter()1071 .iter()
948 .flat_map(|run| &run.positions)1072 .map(|paragraph| paragraph.source)
949 .all(|position| ![2, 5].contains(&position.paragraph))1073 .collect::<Vec<_>>(),
1074 [0, 1, 3, 4]
950 );1075 );
951 let source = access1076 let source = access
952 .selection(1077 .selection(
...@@ -969,10 +1094,11 @@ mod tests {...@@ -969,10 +1094,11 @@ mod tests {
969 }1094 }
970 );1095 );
971 editor.select(source).unwrap();1096 editor.select(source).unwrap();
972 let update = access.update(&editor, viewport, "Test", None).unwrap();1097 let update = access
973 let tree = accesskit_consumer::Tree::new(update, true);1098 .update(&editor, None, viewport, "Test", None, None)
1099 .unwrap();
974 assert_eq!(1100 assert_eq!(
975 tree.state()1101 apply(&mut tree, update)
976 .focus()1102 .focus()
977 .unwrap()1103 .unwrap()
978 .text_selection()1104 .text_selection()
...@@ -989,23 +1115,20 @@ mod tests {...@@ -989,23 +1115,20 @@ mod tests {
989 .into(),1115 .into(),
990 )1116 )
991 .unwrap();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 let caret = editor.caret(1.0).unwrap();1121 let caret = editor.caret(1.0).unwrap();
994 assert_eq!(caret.x0, 27.0);1122 assert_eq!(caret.x0, 27.0);
995 let position = access1123 let rects = apply(&mut tree, update)
996 .position(1124 .focus()
997 editor.active_outline(),1125 .unwrap()
998 editor.selection().positions[1],1126 .text_selection()
999 Affinity::Downstream,1127 .unwrap()
1000 )1128 .bounding_boxes();
1001 .unwrap();1129 assert_eq!(rects.len(), 1);
1002 let run = access1130 assert_eq!(rects[0].x0, 27.0 * 2.0 + 13.0);
1003 .runs1131 assert_eq!(rects[0].y0, caret.y0 * 2.0 + 19.0);
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);
1009 }1132 }
10101133
1011 #[test]1134 #[test]
...@@ -1043,12 +1166,15 @@ mod tests {...@@ -1043,12 +1166,15 @@ mod tests {
1043 .unwrap();1166 .unwrap();
1044 let mut editor = CanvasEditor::new(&mut engine, document, width).unwrap();1167 let mut editor = CanvasEditor::new(&mut engine, document, width).unwrap();
1045 let mut access = Accessibility::default();1168 let mut access = Accessibility::default();
1169 let mut tree = None;
1046 let viewport = Viewport {1170 let viewport = Viewport {
1047 size: [800, 600],1171 size: [800, 600],
1048 origin: [48.0; 2],1172 origin: [48.0; 2],
1049 scale: 2.0,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 for (_, node) in &update.nodes {1178 for (_, node) in &update.nodes {
1053 if node.role() == Role::TextRun {1179 if node.role() == Role::TextRun {
1054 assert_eq!(1180 assert_eq!(
...@@ -1066,18 +1192,18 @@ mod tests {...@@ -1066,18 +1192,18 @@ mod tests {
1066 }1192 }
1067 }1193 }
1068 }1194 }
1069 let tree = accesskit_consumer::Tree::new(update, true);1195 let field = apply(&mut tree, update).focus().unwrap();
1070 let field = tree.state().focus().unwrap();
1071 assert_eq!(field.document_range().text(), text, "width {width}");1196 assert_eq!(field.document_range().text(), text, "width {width}");
1072 let selection = field.document_range().to_text_selection();1197 let selection = field.document_range().to_text_selection();
1073 let source_selection = access1198 let source_selection = access
1074 .selection(editor.active_outline().id, selection)1199 .selection(editor.active_outline().id, selection)
1075 .unwrap();1200 .unwrap();
1076 editor.select(source_selection).unwrap();1201 editor.select(source_selection).unwrap();
1077 let selected = access.update(&editor, viewport, "Test", None).unwrap();1202 let selected = access
1078 let tree = accesskit_consumer::Tree::new(selected, true);1203 .update(&editor, None, viewport, "Test", None, None)
1204 .unwrap();
1079 assert_eq!(1205 assert_eq!(
1080 tree.state()1206 apply(&mut tree, selected)
1081 .focus()1207 .focus()
1082 .unwrap()1208 .unwrap()
1083 .text_selection()1209 .text_selection()
...@@ -1085,48 +1211,57 @@ mod tests {...@@ -1085,48 +1211,57 @@ mod tests {
1085 .text(),1211 .text(),
1086 text1212 text
1087 );1213 );
1088 let stable_ids: Vec<_> = access.runs.iter().map(|run| run.id).collect();1214 let zoomed = access
1089 access
1090 .update(1215 .update(
1091 &editor,1216 &editor,
1217 None,
1092 Viewport {1218 Viewport {
1093 scale: 3.0,1219 scale: 3.0,
1094 ..viewport1220 ..viewport
1095 },1221 },
1096 "Test",1222 "Test",
1097 None,1223 None,
1224 None,
1098 )1225 )
1099 .unwrap();1226 .unwrap();
1100 assert_eq!(1227 assert_eq!(
1101 stable_ids,1228 zoomed.nodes.iter().map(|(id, _)| *id).collect::<Vec<_>>(),
1102 access.runs.iter().map(|run| run.id).collect::<Vec<_>>()1229 [ROOT, PAGE]
1103 );1230 );
1231 apply(&mut tree, zoomed);
1104 access.deactivate();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 assert!(1237 assert!(
1107 access1238 access
1108 .selection(editor.active_outline().id, selection)1239 .selection(editor.active_outline().id, selection)
1109 .is_err()1240 .is_err()
1110 );1241 );
1111 let selection = access1242 let selection = *state.focus().unwrap().data().text_selection().unwrap();
1112 .update(&editor, viewport, "Test", None)
1113 .unwrap()
1114 .nodes[1]
1115 .1
1116 .text_selection()
1117 .copied()
1118 .unwrap();
1119 editor.insert(&mut engine, "replacement").unwrap();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 assert!(1248 assert!(
1122 access1249 access
1123 .selection(editor.active_outline().id, selection)1250 .selection(editor.active_outline().id, selection)
1124 .is_err()1251 .is_err()
1125 );1252 );
1126 editor.undo(&mut engine).unwrap();1253 editor.undo(&mut engine).unwrap();
1127 let update = access.update(&editor, viewport, "Test", None).unwrap();1254 let update = access
1128 let tree = accesskit_consumer::Tree::new(update, true);1255 .update(&editor, None, viewport, "Test", None, None)
1129 assert_eq!(tree.state().focus().unwrap().document_range().text(), text);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,6 +1301,7 @@ mod tests {
1166 .collect();1301 .collect();
1167 let mut editor = CanvasEditor::new(&mut engine, document, 48.0).unwrap();1302 let mut editor = CanvasEditor::new(&mut engine, document, 48.0).unwrap();
1168 let mut access = Accessibility::default();1303 let mut access = Accessibility::default();
1304 let mut tree = None;
1169 let viewport = Viewport {1305 let viewport = Viewport {
1170 size: [800, 600],1306 size: [800, 600],
1171 origin: [31.0, -57.0],1307 origin: [31.0, -57.0],
...@@ -1181,9 +1317,10 @@ mod tests {...@@ -1181,9 +1317,10 @@ mod tests {
1181 })1317 })
1182 .unwrap();1318 .unwrap();
1183 let caret = editor.caret(0.0).unwrap();1319 let caret = editor.caret(0.0).unwrap();
1184 let update = access.update(&editor, viewport, "Test", None).unwrap();1320 let update = access
1185 let tree = accesskit_consumer::Tree::new(update, true);1321 .update(&editor, None, viewport, "Test", None, None)
1186 let field = tree.state().focus().unwrap();1322 .unwrap();
1323 let field = apply(&mut tree, update).focus().unwrap();
1187 assert_eq!(1324 assert_eq!(
1188 field.document_range().text(),1325 field.document_range().text(),
1189 "abc e\u{301} 🌳 words wrap here\n\nabc שלום def عالم end\nlast\nline\n"1326 "abc e\u{301} 🌳 words wrap here\n\nabc שלום def عالم end\nlast\nline\n"
...@@ -1217,7 +1354,7 @@ mod tests {...@@ -1217,7 +1354,7 @@ mod tests {
1217 }1354 }
1218 #[test]1355 #[test]
1219 fn keyboard_and_accessibility_preserve_visual_caret_at_wraps() {1356 fn keyboard_and_accessibility_preserve_visual_caret_at_wraps() {
1220 use crate::editor::Movement;1357 use draw::edit::Movement;
1221 let mut engine = TextEngine::default();1358 let mut engine = TextEngine::default();
1222 let document = TextDocument::new(vec![1359 let document = TextDocument::new(vec![
1223 Paragraph::new(1360 Paragraph::new(
...@@ -1230,6 +1367,7 @@ mod tests {...@@ -1230,6 +1367,7 @@ mod tests {
1230 .unwrap();1367 .unwrap();
1231 let mut editor = CanvasEditor::new(&mut engine, document, 72.0).unwrap();1368 let mut editor = CanvasEditor::new(&mut engine, document, 72.0).unwrap();
1232 let mut access = Accessibility::default();1369 let mut access = Accessibility::default();
1370 let mut tree = None;
1233 let viewport = Viewport {1371 let viewport = Viewport {
1234 size: [800, 600],1372 size: [800, 600],
1235 origin: [0.0; 2],1373 origin: [0.0; 2],
...@@ -1256,11 +1394,10 @@ mod tests {...@@ -1256,11 +1394,10 @@ mod tests {
1256 ] {1394 ] {
1257 editor.move_selection(&mut engine, movement, false).unwrap();1395 editor.move_selection(&mut engine, movement, false).unwrap();
1258 let caret = editor.caret(0.0).unwrap();1396 let caret = editor.caret(0.0).unwrap();
1259 let tree = accesskit_consumer::Tree::new(1397 let update = access
1260 access.update(&editor, viewport, "Test", None).unwrap(),1398 .update(&editor, None, viewport, "Test", None, None)
1261 true,1399 .unwrap();
1262 );1400 let field = apply(&mut tree, update).focus().unwrap();
1263 let field = tree.state().focus().unwrap();
1264 let selection = field.text_selection().unwrap();1401 let selection = field.text_selection().unwrap();
1265 let rects = selection.bounding_boxes();1402 let rects = selection.bounding_boxes();
1266 assert_eq!(rects.len(), 1);1403 assert_eq!(rects.len(), 1);
...@@ -1303,7 +1440,7 @@ mod tests {...@@ -1303,7 +1440,7 @@ mod tests {
1303 .unwrap();1440 .unwrap();
1304 let mut editor = CanvasEditor::new(&mut engine, document, 72.0).unwrap();1441 let mut editor = CanvasEditor::new(&mut engine, document, 72.0).unwrap();
1305 let mut cached = Accessibility::default();1442 let mut cached = Accessibility::default();
1306 let mut rebuilt = Accessibility::default();1443 let mut tree = None;
1307 let mut compare = |editor: &CanvasEditor, step: usize| {1444 let mut compare = |editor: &CanvasEditor, step: usize| {
1308 let viewport = Viewport {1445 let viewport = Viewport {
1309 size: [800, 600],1446 size: [800, 600],
...@@ -1314,28 +1451,44 @@ mod tests {...@@ -1314,28 +1451,44 @@ mod tests {
1314 .is_multiple_of(2)1451 .is_multiple_of(2)
1315 .then_some((editor.active_outline().id, [13.5, -7.25]));1452 .then_some((editor.active_outline().id, [13.5, -7.25]));
1316 for repeat in 0..2 {1453 for repeat in 0..2 {
1317 rebuilt.paragraphs.clear();1454 let update = cached
1318 let actual = cached.update(editor, viewport, "Test", preview).unwrap();1455 .update(editor, None, viewport, "Test", preview, None)
1319 let expected = rebuilt.update(editor, viewport, "Test", preview).unwrap();1456 .unwrap();
1320 assert_eq!(actual.nodes, expected.nodes, "step {step}, repeat {repeat}");1457 let actual = apply(&mut tree, update);
1321 assert_eq!(actual.focus, expected.focus);1458 let mut rebuilt = Accessibility::default();
1322 let tree = accesskit_consumer::Tree::new(actual, true);1459 let expected = Tree::new(
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(),
1329 rebuilt1460 rebuilt
1330 .selection(editor.active_outline().id, selection)1461 .update(editor, None, viewport, "Test", preview, None)
1331 .unwrap()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 let count = editor1479 let count = editor
1334 .outlines()1480 .outlines()
1335 .iter()1481 .iter()
1336 .map(|o| o.layouts().count())1482 .map(|o| o.layouts().count())
1337 .sum::<usize>();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 compare(&editor, 0);1494 compare(&editor, 0);
...@@ -1405,6 +1558,72 @@ mod tests {...@@ -1405,6 +1558,72 @@ mod tests {
1405 compare(&editor, 162);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 #[test]1627 #[test]
1409 fn outlines_have_independent_accessibility_identity_and_selection() {1628 fn outlines_have_independent_accessibility_identity_and_selection() {
1410 let mut engine = TextEngine::default();1629 let mut engine = TextEngine::default();
...@@ -1422,7 +1641,9 @@ mod tests {...@@ -1422,7 +1641,9 @@ mod tests {
1422 scale: 2.0,1641 scale: 2.0,
1423 origin: [48.0; 2],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 let fields: Vec<_> = update1647 let fields: Vec<_> = update
1427 .nodes1648 .nodes
1428 .iter()1649 .iter()
...@@ -1439,35 +1660,45 @@ mod tests {...@@ -1439,35 +1660,45 @@ mod tests {
1439 assert_ne!(fields[0].0, fields[1].0);1660 assert_ne!(fields[0].0, fields[1].0);
1440 assert_eq!(access.outline_for_node(fields[0].0), Some(first));1661 assert_eq!(access.outline_for_node(fields[0].0), Some(first));
1441 assert_eq!(access.outline_for_node(fields[1].0), Some(second));1662 assert_eq!(access.outline_for_node(fields[1].0), Some(second));
1442 let tree = accesskit_consumer::Tree::new(update, true);1663 let mut tree = None;
1443 let field = tree.state().focus().unwrap();1664 let field = apply(&mut tree, update).focus().unwrap();
1444 assert_eq!(field.document_range().text(), "annotation 🌳");1665 assert_eq!(field.document_range().text(), "annotation 🌳");
1445 let selection = field.document_range().to_text_selection();1666 let selection = field.document_range().to_text_selection();
1446 assert!(access.selection(first, selection).is_err());1667 assert!(access.selection(first, selection).is_err());
1447 let source_selection = access.selection(second, selection).unwrap();1668 let source_selection = access.selection(second, selection).unwrap();
1448 editor.select(source_selection).unwrap();1669 editor.select(source_selection).unwrap();
1449 let preview = access1670 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 .unwrap();1679 .unwrap();
1452 let preview_tree = accesskit_consumer::Tree::new(preview, true);1680 let preview_field = apply(&mut tree, preview).focus().unwrap();
1453 let preview_field = preview_tree.state().focus().unwrap();
1454 let rect = preview_field.document_range().bounding_boxes()[0];1681 let rect = preview_field.document_range().bounding_boxes()[0];
1455 assert_eq!(rect.x0, 848.0);1682 assert_eq!(rect.x0, 848.0);
1456 assert_eq!(rect.y0, 148.0);1683 assert_eq!(rect.y0, 148.0);
1457 assert_eq!(editor.active_outline().origin(), [300.0, 40.0]);1684 assert_eq!(editor.active_outline().origin(), [300.0, 40.0]);
1458 editor.move_outline(second, [400.0, 50.0]).unwrap();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 assert_eq!(access.outline_for_node(fields[1].0), Some(second));1690 assert_eq!(access.outline_for_node(fields[1].0), Some(second));
1461 assert!(access.selection(second, selection).is_ok());1691 assert!(access.selection(second, selection).is_ok());
1462 editor.undo(&mut engine).unwrap();1692 editor.undo(&mut engine).unwrap();
1463 editor.undo(&mut engine).unwrap();1693 editor.undo(&mut engine).unwrap();
1464 editor.undo(&mut engine).unwrap();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 assert_eq!(1698 assert_eq!(
1467 update1699 nodes(apply(&mut tree, update))
1468 .nodes
1469 .iter()1700 .iter()
1470 .filter(|(_, node)| node.role() == Role::MultilineTextInput)1701 .filter(|node| node.role() == Role::MultilineTextInput)
1471 .count(),1702 .count(),
1472 11703 1
1473 );1704 );
crates/canvas/src/interaction/mod.rs+109-183
...@@ -11,15 +11,16 @@ pub use scroll::Scroll;...@@ -11,15 +11,16 @@ pub use scroll::Scroll;
11#[cfg(test)]11#[cfg(test)]
12mod tests;12mod tests;
1313
14use crate::gpu::{Viewport, page::PageScene};14use crate::gpu::{Paper, Viewport, page::PageScene};
15use crate::{15use crate::{
16 date::DateField,16 date::DateField,
17 editor::{17 editor::{CanvasEditor, DEFAULT_OUTLINE_WIDTH, Selection, TextOutline},
18 CanvasEditor, DEFAULT_OUTLINE_WIDTH, Movement, Selection, SelectionUnit, TextOutline,
19 },
20 layout::TextEngine,18 layout::TextEngine,
21};19};
22use draw::{Primitive, Stroke};20use draw::{
21 Primitive, Stroke,
22 edit::{self, Clicks, Command, Key, Modifiers, Movement, NamedKey, SelectionUnit},
23};
23use std::{24use std::{
24 error::Error,25 error::Error,
25 time::{Duration, Instant},26 time::{Duration, Instant},
...@@ -31,39 +32,6 @@ pub const DATE_LABELS: [&str; 2] = ["Page date", "Page time"];...@@ -31,39 +32,6 @@ pub const DATE_LABELS: [&str; 2] = ["Page date", "Page time"];
3132
32pub type Result<T> = std::result::Result<T, Box<dyn Error>>;33pub type Result<T> = std::result::Result<T, Box<dyn Error>>;
3334
34#[derive(Clone, Debug, PartialEq)]
35pub enum Key {
36 Named(NamedKey),
37 Character(String),
38}
39
40#[derive(Clone, Copy, Debug, PartialEq)]
41pub 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)]
60pub struct Modifiers {
61 pub shift: bool,
62 pub control: bool,
63 pub option: bool,
64 pub command: bool,
65}
66
67#[derive(Clone, Copy, Debug, PartialEq)]35#[derive(Clone, Copy, Debug, PartialEq)]
68pub enum Cursor {36pub enum Cursor {
69 Default,37 Default,
...@@ -89,12 +57,14 @@ pub enum Request {...@@ -89,12 +57,14 @@ pub enum Request {
89 CharacterPalette,57 CharacterPalette,
90}58}
9159
92/// What an event did: `changed` means the page, selection or view changed (the host saves,60/// What an event did: `changed` means the page or selection changed (the host saves,
93/// updates accessibility and redraws); `redraw` alone repaints hover feedback.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#[must_use]63#[must_use]
95#[derive(Debug, Default, PartialEq)]64#[derive(Debug, Default, PartialEq)]
96pub struct Response {65pub struct Response {
97 pub changed: bool,66 pub changed: bool,
67 pub moved: bool,
98 pub redraw: bool,68 pub redraw: bool,
99 pub request: Option<Request>,69 pub request: Option<Request>,
100}70}
...@@ -104,7 +74,7 @@ impl Response {...@@ -104,7 +74,7 @@ impl Response {
104 Self {74 Self {
105 changed: true,75 changed: true,
106 redraw: true,76 redraw: true,
107 request: None,77 ..Self::default()
108 }78 }
109 }79 }
11080
...@@ -123,11 +93,12 @@ impl Response {...@@ -123,11 +93,12 @@ impl Response {
123 }93 }
124}94}
12595
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#[derive(Clone, Copy, Debug)]97#[derive(Clone, Copy, Debug)]
128pub struct TextColors {98pub struct TextColors {
129 pub caret: [f32; 4],99 pub caret: [f32; 4],
130 pub selection: [f32; 4],100 pub selection: [f32; 4],
101 pub paper: Paper,
131}102}
132103
133/// How to paint an outline: whether the caret is in its blink-on phase, the view scale,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,6 +109,9 @@ struct Paint {
138 scale: f32,109 scale: f32,
139 pixel: f32,110 pixel: f32,
140 colors: TextColors,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}
142116
143enum Drag {117enum Drag {
...@@ -205,10 +179,9 @@ pub struct PageView {...@@ -205,10 +179,9 @@ pub struct PageView {
205 pub viewport: Viewport,179 pub viewport: Viewport,
206 /// Device pixels per logical pixel.180 /// Device pixels per logical pixel.
207 display_scale: f32,181 display_scale: f32,
208 double_click: Duration,182 clicks: Clicks,
209 pointer: [f32; 2],183 pointer: [f32; 2],
210 pointer_inside: bool,184 pointer_inside: bool,
211 last_click: Option<(Instant, [f32; 2], u8)>,
212 drag: Option<Drag>,185 drag: Option<Drag>,
213 object_focus: Option<ObjectFocus>,186 object_focus: Option<ObjectFocus>,
214 modifiers: Modifiers,187 modifiers: Modifiers,
...@@ -242,10 +215,9 @@ impl PageView {...@@ -242,10 +215,9 @@ impl PageView {
242 origin: home(display_scale),215 origin: home(display_scale),
243 },216 },
244 display_scale,217 display_scale,
245 double_click,218 clicks: Clicks::new(double_click),
246 pointer: [0.0; 2],219 pointer: [0.0; 2],
247 pointer_inside: false,220 pointer_inside: false,
248 last_click: None,
249 drag: None,221 drag: None,
250 object_focus: None,222 object_focus: None,
251 modifiers: Modifiers::default(),223 modifiers: Modifiers::default(),
...@@ -397,6 +369,15 @@ impl PageView {...@@ -397,6 +369,15 @@ impl PageView {
397 Ok(Response::changed())369 Ok(Response::changed())
398 }370 }
399371
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 /// The caret's rectangle in device pixels, for placing the platform's input method.381 /// The caret's rectangle in device pixels, for placing the platform's input method.
401 pub fn caret_area(&self) -> Result<[f32; 4]> {382 pub fn caret_area(&self) -> Result<[f32; 4]> {
402 let mut rect = self.editor.caret(1.0)?;383 let mut rect = self.editor.caret(1.0)?;
...@@ -474,13 +455,26 @@ impl PageView {...@@ -474,13 +455,26 @@ impl PageView {
474 self.changed()455 self.changed()
475 }456 }
476457
477 fn zoom(&mut self, factor: f32) {458 /// The page's zoom, where 1 is OneNote's 100%: 96 pixels per inch.
478 let point = self.viewport.document_point(self.pointer);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 let dpr = self.display_scale;473 let dpr = self.display_scale;
480 self.viewport.scale = (self.viewport.scale * factor).clamp(dpr / 3.0, dpr * 16.0 / 3.0);474 self.viewport.scale = (self.viewport.scale * factor).clamp(dpr / 3.0, dpr * 16.0 / 3.0);
481 self.viewport.origin = [475 self.viewport.origin = [
482 self.pointer[0] - point[0] * self.viewport.scale,476 anchor[0] - point[0] * self.viewport.scale,
483 self.pointer[1] - point[1] * self.viewport.scale,477 anchor[1] - point[1] * self.viewport.scale,
484 ];478 ];
485 }479 }
486480
...@@ -522,7 +516,16 @@ impl PageView {...@@ -522,7 +516,16 @@ impl PageView {
522 /// within the page's bounds.516 /// within the page's bounds.
523 pub fn scroll_to(&mut self, axis: usize, offset: f32) -> Result<Response> {517 pub fn scroll_to(&mut self, axis: usize, offset: f32) -> Result<Response> {
524 self.viewport.origin[axis] = -offset;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 }
527530
528 /// Everything to draw this frame.531 /// Everything to draw this frame.
...@@ -571,6 +574,8 @@ impl PageView {...@@ -571,6 +574,8 @@ impl PageView {
571 scale: self.viewport.scale,574 scale: self.viewport.scale,
572 pixel: self.pixel(),575 pixel: self.pixel(),
573 colors,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,7 +614,7 @@ impl PageView {
609 if size.contains(&0) {614 if size.contains(&0) {
610 return Ok(Response::default());615 return Ok(Response::default());
611 }616 }
612 self.changed()617 self.moved()
613 }618 }
614619
615 pub fn scale_factor_changed(&mut self, scale: f32) -> Result<Response> {620 pub fn scale_factor_changed(&mut self, scale: f32) -> Result<Response> {
...@@ -618,7 +623,7 @@ impl PageView {...@@ -618,7 +623,7 @@ impl PageView {
618 self.viewport.origin[0] *= ratio;623 self.viewport.origin[0] *= ratio;
619 self.viewport.origin[1] *= ratio;624 self.viewport.origin[1] *= ratio;
620 self.display_scale = scale;625 self.display_scale = scale;
621 self.changed()626 self.moved()
622 }627 }
623628
624 pub fn focus_changed(&mut self, focused: bool) -> Result<Response> {629 pub fn focus_changed(&mut self, focused: bool) -> Result<Response> {
...@@ -654,7 +659,14 @@ impl PageView {...@@ -654,7 +659,14 @@ impl PageView {
654 let target =659 let target =
655 self.editor660 self.editor
656 .selection_at(point[0] - origin[0], point[1] - origin[1], unit)?;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 self.changed()670 self.changed()
659 }671 }
660 Some(Drag::Outline { pending_press, .. } | Drag::Image { pending_press, .. }) => {672 Some(Drag::Outline { pending_press, .. } | Drag::Image { pending_press, .. }) => {
...@@ -688,16 +700,9 @@ impl PageView {...@@ -688,16 +700,9 @@ impl PageView {
688 /// A primary-button press at the pointer's position, at `now` for click counting.700 /// A primary-button press at the pointer's position, at `now` for click counting.
689 pub fn pointer_pressed(&mut self, now: Instant) -> Result<Response> {701 pub fn pointer_pressed(&mut self, now: Instant) -> Result<Response> {
690 let point = self.viewport.document_point(self.pointer);702 let point = self.viewport.document_point(self.pointer);
691 let count = self703 let unit = self
692 .last_click704 .clicks
693 .filter(|(time, point, _)| {705 .press(now, self.pointer, 4.0 * self.display_scale);
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));
701 match self.hit_test(point) {706 match self.hit_test(point) {
702 Some(Hit::Date(field)) => {707 Some(Hit::Date(field)) => {
703 self.editor.finish_composition();708 self.editor.finish_composition();
...@@ -739,11 +744,6 @@ impl PageView {...@@ -739,11 +744,6 @@ impl PageView {
739 self.editor.focus_outline(id)?;744 self.editor.focus_outline(id)?;
740 let previous = self.editor.selection();745 let previous = self.editor.selection();
741 self.editor.select_below(&mut self.engine, id, point)?;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 let selection = self.editor.selection_at(point[0], point[1], unit)?;747 let selection = self.editor.selection_at(point[0], point[1], unit)?;
748 let selection = if extend {748 let selection = if extend {
749 Selection {749 Selection {
...@@ -802,12 +802,12 @@ impl PageView {...@@ -802,12 +802,12 @@ impl PageView {
802 /// `delta` in device pixels; Command zooms about the pointer instead of scrolling.802 /// `delta` in device pixels; Command zooms about the pointer instead of scrolling.
803 pub fn wheel(&mut self, delta: [f32; 2]) -> Result<Response> {803 pub fn wheel(&mut self, delta: [f32; 2]) -> Result<Response> {
804 if self.modifiers.command {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 } else {806 } else {
807 self.viewport.origin[0] += delta[0];807 self.viewport.origin[0] += delta[0];
808 self.viewport.origin[1] += delta[1];808 self.viewport.origin[1] += delta[1];
809 }809 }
810 self.changed()810 self.moved()
811 }811 }
812812
813 /// Text the platform inserts outside key events, such as the character picker's;813 /// Text the platform inserts outside key events, such as the character picker's;
...@@ -1046,118 +1046,50 @@ impl PageView {...@@ -1046,118 +1046,50 @@ impl PageView {
1046 }1046 }
1047 "v" => request = Some(Request::Paste),1047 "v" => request = Some(Request::Paste),
1048 "+" | "=" => {1048 "+" | "=" => {
1049 self.zoom(1.1);1049 self.zoom_about(1.1, self.pointer);
1050 return self.changed();1050 return self.moved();
1051 }1051 }
1052 "-" => {1052 "-" => {
1053 self.zoom(1.0 / 1.1);1053 self.zoom_about(1.0 / 1.1, self.pointer);
1054 return self.changed();1054 return self.moved();
1055 }1055 }
1056 "0" => {1056 "0" => {
1057 self.zoom((self.display_scale * 96.0 / 72.0) / self.viewport.scale);1057 self.zoom_about(1.0 / self.zoom(), self.pointer);
1058 return self.changed();1058 return self.moved();
1059 }1059 }
1060 _ => return Ok(Response::default()),1060 _ => return Ok(Response::default()),
1061 }1061 }
1062 } else {1062 } else {
1063 let movement = match key {1063 let chord = Command::from_key(key, self.modifiers);
1064 Key::Named(NamedKey::ArrowLeft) => Some(if command {1064 if let Some(Command::Move(movement)) = chord {
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 {
1106 self.editor1065 self.editor
1107 .move_selection(&mut self.engine, movement, shift)?;1066 .move_selection(&mut self.engine, movement, shift)?;
1108 } else if self.editor.marked_range().is_none() {1067 } else if self.editor.marked_range().is_none() {
1109 match key {1068 match (chord, key) {
1110 Key::Named(NamedKey::Backspace) if command || option => {1069 (Some(Command::DeleteTo(movement)), _) => {
1111 self.editor.delete_to(1070 self.editor.delete_to(&mut self.engine, movement)?;
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 )?;
1129 }1071 }
1130 Key::Named(NamedKey::Backspace) => {1072 (Some(Command::Delete { backward }), _) => {
1131 self.editor.delete(&mut self.engine, true)?;1073 self.editor.delete(&mut self.engine, backward)?;
1132 }1074 }
1133 Key::Named(NamedKey::Delete) => {1075 (Some(Command::Kill), _) => {
1134 self.editor.delete(&mut self.engine, false)?;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 let limits = self.scroll();1081 let limits = self.scroll();
1138 self.viewport.origin[1] = -if *end == NamedKey::Home {1082 self.viewport.origin[1] = -if *end == NamedKey::Home {
1139 limits.min[1]1083 limits.min[1]
1140 } else {1084 } else {
1141 limits.max[1]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() {1089 (_, Key::Named(NamedKey::Enter)) => {
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) => {
1158 self.editor.enter(&mut self.engine, shift)?;1090 self.editor.enter(&mut self.engine, shift)?;
1159 }1091 }
1160 Key::Named(NamedKey::Tab) => {1092 (_, Key::Named(NamedKey::Tab)) => {
1161 self.editor.tab(&mut self.engine, shift)?;1093 self.editor.tab(&mut self.engine, shift)?;
1162 }1094 }
1163 _ if !command && !control => {1095 _ if !command && !control => {
...@@ -1392,6 +1324,7 @@ fn page_primitives<'a>(...@@ -1392,6 +1324,7 @@ fn page_primitives<'a>(
1392 outline.shaped(),1324 outline.shaped(),
1393 [origin[0] + offset[0], origin[1] + offset[1]],1325 [origin[0] + offset[0], origin[1] + offset[1]],
1394 moving,1326 moving,
1327 paint.colors.paper,
1395 primitives,1328 primitives,
1396 );1329 );
1397 }1330 }
...@@ -1407,6 +1340,7 @@ fn page_primitives<'a>(...@@ -1407,6 +1340,7 @@ fn page_primitives<'a>(
1407 *origin,1340 *origin,
1408 Some(editor),1341 Some(editor),
1409 moving,1342 moving,
1343 paint.colors.paper,
1410 &draw_outline,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,22 +1512,6 @@ fn append_image_chrome(rect: [f32; 4], pixel: f32, primitives: &mut Vec<Primitiv
1578 }1512 }
1579}1513}
15801514
1581fn 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
1597fn snap_to_grid(point: [f32; 2], margin: [f32; 2]) -> [f32; 2] {1515fn snap_to_grid(point: [f32; 2], margin: [f32; 2]) -> [f32; 2] {
1598 std::array::from_fn(|axis| {1516 std::array::from_fn(|axis| {
1599 let offset = margin[axis];1517 let offset = margin[axis];
...@@ -1722,11 +1640,15 @@ fn append_outline<'a>(...@@ -1722,11 +1640,15 @@ fn append_outline<'a>(
1722 scale,1640 scale,
1723 pixel,1641 pixel,
1724 colors,1642 colors,
1643 visible,
1725 } = paint;1644 } = paint;
1726 outline.shaped().append_table_primitives(primitives, origin);1645 let rows = [visible[0] - y, visible[1] - y];
1727 outline1646 outline
1728 .shaped()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 if let Some(editor) = editor {1652 if let Some(editor) = editor {
1731 for rect in editor.selection_rects()? {1653 for rect in editor.selection_rects()? {
1732 primitives.push(Primitive::Rect {1654 primitives.push(Primitive::Rect {
...@@ -1740,10 +1662,14 @@ fn append_outline<'a>(...@@ -1740,10 +1662,14 @@ fn append_outline<'a>(
1740 });1662 });
1741 }1663 }
1742 }1664 }
1743 for (index, (_, paragraph)) in outline.layouts().enumerate() {1665 for (index, paragraph) in outline.shaped().visible(rows) {
1744 outline1666 outline.shaped().append_paragraph_primitives(
1745 .shaped()1667 index,
1746 .append_paragraph_primitives(index, paragraph, origin, primitives);1668 paragraph,
1669 origin,
1670 colors.paper.ink,
1671 primitives,
1672 );
1747 }1673 }
1748 if let Some(editor) = editor {1674 if let Some(editor) = editor {
1749 for rect in editor.marked_rects()? {1675 for rect in editor.marked_rects()? {
...@@ -1754,7 +1680,7 @@ fn append_outline<'a>(...@@ -1754,7 +1680,7 @@ fn append_outline<'a>(
1754 rect.x1 as f32 + x,1680 rect.x1 as f32 + x,
1755 rect.y1 as f32 + y,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 let [anchor, focus] = editor.selection().positions;1686 let [anchor, focus] = editor.selection().positions;
crates/canvas/src/interaction/profile.rs+22-4
...@@ -45,7 +45,7 @@ fn editing_cost(...@@ -45,7 +45,7 @@ fn editing_cost(
45 editor: &mut CanvasEditor,45 editor: &mut CanvasEditor,
46 scene: Option<&(PageScene, [f32; 2])>,46 scene: Option<&(PageScene, [f32; 2])>,
47) {47) {
48 let viewport = Viewport {48 let mut viewport = Viewport {
49 size: [1000, 720],49 size: [1000, 720],
50 scale: 96.0 / 72.0,50 scale: 96.0 / 72.0,
51 origin: [48.0; 2],51 origin: [48.0; 2],
...@@ -59,7 +59,9 @@ fn editing_cost(...@@ -59,7 +59,9 @@ fn editing_cost(
59 });59 });
60 let mut access = accessibility::Accessibility::default();60 let mut access = accessibility::Accessibility::default();
61 let update = timed(case, "ax_first", 0, || {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 eprintln!("canvas_count\t{case}\tax_nodes\t0\t{}", update.nodes.len());66 eprintln!("canvas_count\t{case}\tax_nodes\t0\t{}", update.nodes.len());
65 drop(update);67 drop(update);
...@@ -113,14 +115,30 @@ fn editing_cost(...@@ -113,14 +115,30 @@ fn editing_cost(
113 });115 });
114 eprintln!("canvas_count\t{case}\tprimitives\t{sample}\t{primitives}");116 eprintln!("canvas_count\t{case}\tprimitives\t{sample}\t{primitives}");
115 timed(case, "ax_insert", sample, || {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 assert_eq!(edited_layouts, layout_ids(editor));132 assert_eq!(edited_layouts, layout_ids(editor));
119 timed(case, "undo", sample, || {133 timed(case, "undo", sample, || {
120 assert!(editor.undo(engine).unwrap())134 assert!(editor.undo(engine).unwrap())
121 });135 });
122 timed(case, "ax_undo", sample, || {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 assert_eq!(editor.active_outline().document(), &initial[index]);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,6 +9,7 @@ use std::sync::Arc;
9const COLORS: TextColors = TextColors {9const COLORS: TextColors = TextColors {
10 caret: [0.1, 0.3, 0.9, 1.0],10 caret: [0.1, 0.3, 0.9, 1.0],
11 selection: [0.7, 0.8, 1.0, 1.0],11 selection: [0.7, 0.8, 1.0, 1.0],
12 paper: crate::gpu::Paper::WHITE,
12};13};
1314
14/// Paint for a view at `scale` device pixels per point on a display of `display_scale`.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,6 +19,7 @@ pub(super) fn paint(show_caret: bool, scale: f32, display_scale: f32) -> Paint {
18 scale,19 scale,
19 pixel: display_scale / scale,20 pixel: display_scale / scale,
20 colors: COLORS,21 colors: COLORS,
22 visible: [f32::NEG_INFINITY, f32::INFINITY],
21 }23 }
22}24}
2325
...@@ -61,6 +63,7 @@ fn date_buttons_keep_accessibility_identity_and_match_mouse_hits_after_reflow()...@@ -61,6 +63,7 @@ fn date_buttons_keep_accessibility_identity_and_match_mouse_hits_after_reflow()
61 origin: [-30.0, -50.0],63 origin: [-30.0, -50.0],
62 };64 };
63 let mut access = accessibility::Accessibility::default();65 let mut access = accessibility::Accessibility::default();
66 let mut tree = None;
64 let mut identities = Vec::new();67 let mut identities = Vec::new();
65 for phase in 0..3 {68 for phase in 0..3 {
66 if phase == 1 {69 if phase == 1 {
...@@ -73,19 +76,21 @@ fn date_buttons_keep_accessibility_identity_and_match_mouse_hits_after_reflow()...@@ -73,19 +76,21 @@ fn date_buttons_keep_accessibility_identity_and_match_mouse_hits_after_reflow()
73 .change_date(&mut engine, 2, ["Wednesday".into(), "8:40 AM".into()])76 .change_date(&mut engine, 2, ["Wednesday".into(), "8:40 AM".into()])
74 .unwrap();77 .unwrap();
75 }78 }
76 let mut update = access.update(&editor, viewport, "Header", None).unwrap();79 let update = access
77 access.append_page_fields(&mut update, Some(&scene), &editor, viewport, None);80 .update(&editor, Some(&scene), viewport, "Header", None, None)
78 let buttons: Vec<_> = update81 .unwrap();
79 .nodes82 let buttons: Vec<_> =
80 .iter()83 accessibility::tests::nodes(accessibility::tests::apply(&mut tree, update))
81 .filter(|(_, node)| node.role() == accesskit::Role::Button)84 .into_iter()
82 .collect();85 .filter(|node| node.role() == accesskit::Role::Button)
86 .collect();
83 assert_eq!(buttons.len(), 2);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 let field = [DateField::Date, DateField::Time][index];90 let field = [DateField::Date, DateField::Time][index];
86 assert_eq!(access.date_for_node(*id), Some(field));91 assert_eq!(access.date_for_node(id), Some(field));
87 assert!(node.supports_action(accesskit::Action::Click));92 assert!(node.data().supports_action(accesskit::Action::Click));
88 let rect = node.bounds().unwrap();93 let rect = node.bounding_box().unwrap();
89 let point = viewport.document_point([94 let point = viewport.document_point([
90 ((rect.x0 + rect.x1) * 0.5) as f32,95 ((rect.x0 + rect.x1) * 0.5) as f32,
91 ((rect.y0 + rect.y1) * 0.5) as f32,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,15 +100,14 @@ fn date_buttons_keep_accessibility_identity_and_match_mouse_hits_after_reflow()
95 Some(Hit::Date(field))100 Some(Hit::Date(field))
96 );101 );
97 if phase == 0 {102 if phase == 0 {
98 identities.push(*id);103 identities.push(id);
99 } else {104 } else {
100 assert_eq!(*id, identities[index]);105 assert_eq!(id, identities[index]);
101 }106 }
102 }107 }
103 if phase == 2 {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}
109113
...@@ -169,6 +173,7 @@ fn title_chrome_selects_text_and_exposes_a_named_editable_field() {...@@ -169,6 +173,7 @@ fn title_chrome_selects_text_and_exposes_a_named_editable_field() {
169 let update = access173 let update = access
170 .update(174 .update(
171 &editor,175 &editor,
176 None,
172 Viewport {177 Viewport {
173 size: [800, 600],178 size: [800, 600],
174 scale: 1.0,179 scale: 1.0,
...@@ -176,6 +181,7 @@ fn title_chrome_selects_text_and_exposes_a_named_editable_field() {...@@ -176,6 +181,7 @@ fn title_chrome_selects_text_and_exposes_a_named_editable_field() {
176 },181 },
177 "Header",182 "Header",
178 None,183 None,
184 None,
179 )185 )
180 .unwrap();186 .unwrap();
181 assert!(update.nodes.iter().any(|(_, node)| node.role()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,6 +220,7 @@ fn provisional_lines_share_one_drawn_hit_tested_and_accessible_outline() {
214 let update = access220 let update = access
215 .update(221 .update(
216 &editor,222 &editor,
223 None,
217 Viewport {224 Viewport {
218 size: [800, 600],225 size: [800, 600],
219 scale: 1.0,226 scale: 1.0,
...@@ -221,6 +228,7 @@ fn provisional_lines_share_one_drawn_hit_tested_and_accessible_outline() {...@@ -221,6 +228,7 @@ fn provisional_lines_share_one_drawn_hit_tested_and_accessible_outline() {
221 },228 },
222 "Test",229 "Test",
223 None,230 None,
231 None,
224 )232 )
225 .unwrap();233 .unwrap();
226 let fields = update234 let fields = update
...@@ -264,7 +272,9 @@ fn blank_caret_and_composition_have_accessible_text_without_outline_chrome() {...@@ -264,7 +272,9 @@ fn blank_caret_and_composition_have_accessible_text_without_outline_chrome() {
264 scale: 96.0 / 72.0,272 scale: 96.0 / 72.0,
265 origin: [48.0; 2],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 let id = editor.active_outline().id;278 let id = editor.active_outline().id;
269 assert_eq!(access.outline_for_node(initial.focus), Some(id));279 assert_eq!(access.outline_for_node(initial.focus), Some(id));
270 let rectangles = |editor: &CanvasEditor| {280 let rectangles = |editor: &CanvasEditor| {
...@@ -291,16 +301,22 @@ fn blank_caret_and_composition_have_accessible_text_without_outline_chrome() {...@@ -291,16 +301,22 @@ fn blank_caret_and_composition_have_accessible_text_without_outline_chrome() {
291 .iter()301 .iter()
292 .all(|(_, color)| *color == [0.0, 0.0, 0.0, 1.0] || *color == COLORS.caret)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 assert_eq!(preedit.focus, initial.focus);307 assert_eq!(preedit.focus, initial.focus);
296 editor.commit_text(&mut engine, "日本".into()).unwrap();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 assert_eq!(committed.focus, initial.focus);312 assert_eq!(committed.focus, initial.focus);
299 assert!(rectangles(&editor).len() > 1);313 assert!(rectangles(&editor).len() > 1);
300 editor.undo(&mut engine).unwrap();314 editor.undo(&mut engine).unwrap();
301 assert!(editor.outlines().is_empty());315 assert!(editor.outlines().is_empty());
302 assert_eq!(rectangles(&editor).len(), 1);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 assert_eq!(empty.focus, committed.focus);320 assert_eq!(empty.focus, committed.focus);
305 assert_eq!(access.outline_for_node(committed.focus), Some(id));321 assert_eq!(access.outline_for_node(committed.focus), Some(id));
306 editor.redo(&mut engine).unwrap();322 editor.redo(&mut engine).unwrap();
...@@ -308,12 +324,16 @@ fn blank_caret_and_composition_have_accessible_text_without_outline_chrome() {...@@ -308,12 +324,16 @@ fn blank_caret_and_composition_have_accessible_text_without_outline_chrome() {
308 editor.delete(&mut engine, true).unwrap();324 editor.delete(&mut engine, true).unwrap();
309 assert!(editor.outlines().is_empty());325 assert!(editor.outlines().is_empty());
310 assert_eq!(rectangles(&editor).len(), 1);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 assert_eq!(retired.focus, committed.focus);330 assert_eq!(retired.focus, committed.focus);
313 assert_eq!(access.outline_for_node(retired.focus), Some(id));331 assert_eq!(access.outline_for_node(retired.focus), Some(id));
314 editor.undo(&mut engine).unwrap();332 editor.undo(&mut engine).unwrap();
315 assert!(rectangles(&editor).len() > 1);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 assert_eq!(restored.focus, committed.focus);337 assert_eq!(restored.focus, committed.focus);
318 editor.redo(&mut engine).unwrap();338 editor.redo(&mut engine).unwrap();
319 assert_eq!(rectangles(&editor).len(), 1);339 assert_eq!(rectangles(&editor).len(), 1);
...@@ -375,6 +395,7 @@ fn imported_page_widgets_and_accessibility_follow_edits() {...@@ -375,6 +395,7 @@ fn imported_page_widgets_and_accessibility_follow_edits() {
375 }395 }
376 eprintln!("Verified {tag_hits} imported tag-gutter targets");396 eprintln!("Verified {tag_hits} imported tag-gutter targets");
377 let mut access = accessibility::Accessibility::default();397 let mut access = accessibility::Accessibility::default();
398 let mut tree = None;
378 let tags = |editor: &CanvasEditor| {399 let tags = |editor: &CanvasEditor| {
379 page_primitives(400 page_primitives(
380 editor,401 editor,
...@@ -389,9 +410,9 @@ fn imported_page_widgets_and_accessibility_follow_edits() {...@@ -389,9 +410,9 @@ fn imported_page_widgets_and_accessibility_follow_edits() {
389 Primitive::Icon {410 Primitive::Icon {
390 sources,411 sources,
391 origin,412 origin,
392 opacity,413 tint,
393 ..414 ..
394 } => Some(((sources, opacity), origin)),415 } => Some(((sources, tint[3]), origin)),
395 _ => None,416 _ => None,
396 })417 })
397 .collect::<Vec<_>>()418 .collect::<Vec<_>>()
...@@ -410,15 +431,25 @@ fn imported_page_widgets_and_accessibility_follow_edits() {...@@ -410,15 +431,25 @@ fn imported_page_widgets_and_accessibility_follow_edits() {
410 .into(),431 .into(),
411 )432 )
412 .unwrap();433 .unwrap();
413 let before = access.update(&editor, viewport, "Test", None).unwrap();434 let before = access
414 let before = accesskit_consumer::Tree::new(before, true);435 .update(&editor, None, viewport, "Test", None, None)
415 let before = before.state().focus().unwrap().document_range().text();436 .unwrap();
437 let before = accessibility::tests::apply(&mut tree, before)
438 .focus()
439 .unwrap()
440 .document_range()
441 .text();
416 editor.insert(&mut engine, "CANVAS CHECK ").unwrap();442 editor.insert(&mut engine, "CANVAS CHECK ").unwrap();
417 {443 {
418 let update = access.update(&editor, viewport, "Test", None).unwrap();444 let update = access
419 let tree = accesskit_consumer::Tree::new(update, true);445 .update(&editor, None, viewport, "Test", None, None)
446 .unwrap();
420 assert_eq!(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 format!("CANVAS CHECK {before}")453 format!("CANVAS CHECK {before}")
423 );454 );
424 let primitives = page_primitives(455 let primitives = page_primitives(
...@@ -439,10 +470,15 @@ fn imported_page_widgets_and_accessibility_follow_edits() {...@@ -439,10 +470,15 @@ fn imported_page_widgets_and_accessibility_follow_edits() {
439 editor.undo(&mut engine).unwrap();470 editor.undo(&mut engine).unwrap();
440 assert_eq!(editor.active_outline().document(), &original);471 assert_eq!(editor.active_outline().document(), &original);
441 assert_eq!(tags(&editor), original_tags);472 assert_eq!(tags(&editor), original_tags);
442 let update = access.update(&editor, viewport, "Test", None).unwrap();473 let update = access
443 let tree = accesskit_consumer::Tree::new(update, true);474 .update(&editor, None, viewport, "Test", None, None)
475 .unwrap();
444 assert_eq!(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 before482 before
447 );483 );
448 }484 }
...@@ -661,34 +697,6 @@ fn picture_handles_resize_as_onenote_does() {...@@ -661,34 +697,6 @@ fn picture_handles_resize_as_onenote_does() {
661 assert_eq!(image_handle_at(rect, pixel, [600.0, 150.0]), None);697 assert_eq!(image_handle_at(rect, pixel, [600.0, 150.0]), None);
662}698}
663699
664#[test]
665fn 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
692const DEFAULT_MARGIN: [f32; 2] = [36.0, 14.4];700const DEFAULT_MARGIN: [f32; 2] = [36.0, 14.4];
693701
694#[test]702#[test]
...@@ -819,6 +827,7 @@ fn table_glyphs_highlights_and_selection_share_cell_paint_bounds() {...@@ -819,6 +827,7 @@ fn table_glyphs_highlights_and_selection_share_cell_paint_bounds() {
819 scale: 1.0,827 scale: 1.0,
820 pixel: 1.0,828 pixel: 1.0,
821 colors: COLORS,829 colors: COLORS,
830 visible: [f32::NEG_INFINITY, f32::INFINITY],
822 },831 },
823 &mut primitives,832 &mut primitives,
824 )833 )
...@@ -889,6 +898,7 @@ fn editable_tables_paint_borders_before_selection_and_cell_text() {...@@ -889,6 +898,7 @@ fn editable_tables_paint_borders_before_selection_and_cell_text() {
889 scale: 1.0,898 scale: 1.0,
890 pixel: 1.0,899 pixel: 1.0,
891 colors: COLORS,900 colors: COLORS,
901 visible: [f32::NEG_INFINITY, f32::INFINITY],
892 },902 },
893 &mut primitives,903 &mut primitives,
894 )904 )
crates/canvas/src/layout.rs+13-58
...@@ -30,7 +30,8 @@ const SUBSCRIPT_DROP: f32 = 0.08;...@@ -30,7 +30,8 @@ const SUBSCRIPT_DROP: f32 = 0.08;
3030
31#[derive(Clone, Copy, Debug, Default, PartialEq)]31#[derive(Clone, Copy, Debug, Default, PartialEq)]
32pub struct TextBrush {32pub 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 pub highlight: Option<u32>,35 pub highlight: Option<u32>,
35 /// Points the run draws above the line's baseline: positive for a superscript,36 /// Points the run draws above the line's baseline: positive for a superscript,
36 /// negative for a subscript. Line metrics and hit-testing keep the baseline.37 /// negative for a subscript. Line metrics and hit-testing keep the baseline.
...@@ -191,9 +192,9 @@ impl TextEngine {...@@ -191,9 +192,9 @@ impl TextEngine {
191 StyleProperty::Brush(TextBrush {192 StyleProperty::Brush(TextBrush {
192 // Links take OneNote's blue unless given a colour of their own.193 // Links take OneNote's blue unless given a colour of their own.
193 color: match format.color {194 color: match format.color {
194 Some(color) if color != 0xff000000 => color,195 Some(color) if color != 0xff000000 => Some(color),
195 _ if link => 0x00ff0000,196 _ if link => Some(0x00ff0000),
196 _ => 0,197 _ => None,
197 },198 },
198 highlight: format.highlight.filter(|color| *color != 0xff000000),199 highlight: format.highlight.filter(|color| *color != 0xff000000),
199 rise,200 rise,
...@@ -398,59 +399,6 @@ impl TextLayout {...@@ -398,59 +399,6 @@ impl TextLayout {
398 Cursor::from_byte_index(&self.shaped, byte, affinity)399 Cursor::from_byte_index(&self.shaped, byte, affinity)
399 }400 }
400401
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 pub fn hit_test(&self, x: f32, y: f32) -> Cursor {402 pub fn hit_test(&self, x: f32, y: f32) -> Cursor {
455 let index = self403 let index = self
456 .lines404 .lines
...@@ -664,7 +612,14 @@ mod tests {...@@ -664,7 +612,14 @@ mod tests {
664 _ => None,612 _ => None,
665 })613 })
666 .collect();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 }
669624
670 #[test]625 #[test]
crates/canvas/src/math.rs+3-3
...@@ -10,8 +10,8 @@ use onestore::page::text::Paragraph;...@@ -10,8 +10,8 @@ use onestore::page::text::Paragraph;
10#[derive(Clone)]10#[derive(Clone)]
11pub struct MathLayout {11pub struct MathLayout {
12 pub size: [f32; 2],12 pub size: [f32; 2],
13 /// COLORREF for rules and strokes.13 /// COLORREF for rules and strokes, or `None` for the paper's ink.
14 pub color: u32,14 pub color: Option<u32>,
15 /// Distance from the top to the baseline of the equation's main row.15 /// Distance from the top to the baseline of the equation's main row.
16 pub baseline: f32,16 pub baseline: f32,
17 pub items: Vec<MathItem>,17 pub items: Vec<MathItem>,
...@@ -616,7 +616,7 @@ pub fn layout(engine: &mut TextEngine, paragraph: &Paragraph) -> Result<MathLayo...@@ -616,7 +616,7 @@ pub fn layout(engine: &mut TextEngine, paragraph: &Paragraph) -> Result<MathLayo
616 body.descent = body.descent.max(line.height - line.baseline);616 body.descent = body.descent.max(line.height - line.baseline);
617 Ok(MathLayout {617 Ok(MathLayout {
618 size: [body.width, body.height()],618 size: [body.width, body.height()],
619 color: color.unwrap_or(0),619 color,
620 baseline: body.ascent,620 baseline: body.ascent,
621 items: body621 items: body
622 .items622 .items
crates/canvas/src/outline.rs+555-223
...@@ -1,5 +1,5 @@...@@ -1,5 +1,5 @@
1use crate::{1use crate::{
2 document::{DocumentEdit, edited_nodes},2 document::{DocumentEdit, edited_nodes, leaves},
3 layout::{LayoutError, TextEngine, TextLayout},3 layout::{LayoutError, TextEngine, TextLayout},
4};4};
5use onestore::page::text::{Paragraph, TextProjection};5use onestore::page::text::{Paragraph, TextProjection};
...@@ -8,17 +8,50 @@ use onestore::{...@@ -8,17 +8,50 @@ use onestore::{
8 ExGuid,8 ExGuid,
9 document::{Format, Kind},9 document::{Format, Kind},
10};10};
11use std::collections::{BTreeMap, BTreeSet};11use std::{
12 collections::{BTreeMap, BTreeSet},
13 ops::Range,
14};
1215
13pub(crate) const TITLE_WIDTH: f32 = 468.0;16pub(crate) const TITLE_WIDTH: f32 = 468.0;
1417
15#[derive(Clone)]18#[derive(Clone, Default)]
16pub struct OutlineLayout {19pub struct OutlineLayout {
17 pub paragraphs: Vec<ParagraphLayout>,20 pub paragraphs: Vec<ParagraphLayout>,
18 pub tables: Vec<TableLayout>,21 pub tables: Vec<TableLayout>,
19 /// Pictures, files and handwriting that occupy a paragraph of their own.22 /// Pictures, files and handwriting that occupy a paragraph of their own.
20 pub objects: Vec<ObjectLayout>,23 pub objects: Vec<ObjectLayout>,
21 pub size: [f32; 2],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)]
34struct 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.
48pub(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}
2356
24#[derive(Clone)]57#[derive(Clone)]
...@@ -50,6 +83,61 @@ impl ObjectLayout {...@@ -50,6 +83,61 @@ impl ObjectLayout {
50 ObjectKind::Picture | ObjectKind::Ink(_) => None,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.
113fn 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}
54142
55/// OneNote centers a file's icon and name in a column this wide.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,21 +269,21 @@ pub(crate) fn indentation(level: u32, indents: &[f32], width: f32) -> Result<f32
181 Ok(indent)269 Ok(indent)
182}270}
183271
184fn spacing(272fn spacing(format: &Format) -> Result<[f32; 2], LayoutError> {
185 bottom: &mut f64,273 let spacing = [format.space_before, format.space_after].map(|space| space.unwrap_or(0.0));
186 previous: &mut Option<f32>,274 if spacing.iter().any(|v| !v.is_finite() || *v < 0.0) {
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) {
192 return Err(LayoutError::InvalidSpacing);275 return Err(LayoutError::InvalidSpacing);
193 }276 }
194 if let Some(previous) = previous {277 Ok(spacing)
195 *bottom += f64::from(previous.max(before));278}
279
280/// Where a node with `spacing` starts after the flow `state` of a bottom and pending spacing.
281fn 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);285 state.1 = Some(after);
198 Ok(*bottom as f32)286 state.0 as f32
199}287}
200288
201impl ParagraphLayout {289impl ParagraphLayout {
...@@ -386,42 +474,6 @@ pub(crate) fn visible_paragraphs<'a>(...@@ -386,42 +474,6 @@ pub(crate) fn visible_paragraphs<'a>(
386 })474 })
387}475}
388476
389pub(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
425impl OutlineLayout {477impl OutlineLayout {
426 /// OneNote gives an outline one tag column, as wide as its most-tagged paragraph needs; each478 /// OneNote gives an outline one tag column, as wide as its most-tagged paragraph needs; each
427 /// paragraph's tags start at the column's left edge. Tag origins assume a one-tag column.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,6 +543,33 @@ impl OutlineLayout {
491 depth: usize,543 depth: usize,
492 edit: Option<&DocumentEdit>,544 edit: Option<&DocumentEdit>,
493 shape: &mut impl FnMut(&PageParagraph, f32, &[f32]) -> Result<ParagraphLayout, LayoutError>,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 ) -> Result<Self, LayoutError> {573 ) -> Result<Self, LayoutError> {
495 if !width.is_finite() || width <= 0.0 {574 if !width.is_finite() || width <= 0.0 {
496 return Err(LayoutError::InvalidWidth);575 return Err(LayoutError::InvalidWidth);
...@@ -499,145 +578,424 @@ impl OutlineLayout {...@@ -499,145 +578,424 @@ impl OutlineLayout {
499 return Err(LayoutError::UnsupportedContent);578 return Err(LayoutError::UnsupportedContent);
500 }579 }
501 let mut result = Self {580 let mut result = Self {
502 paragraphs: Vec::new(),
503 tables: Vec::new(),
504 objects: Vec::new(),
505 size: [36.0, 0.0],581 size: [36.0, 0.0],
582 widths: [36.0, f32::NEG_INFINITY],
583 ..Self::default()
506 };584 };
507 let mut bottom = 0.0;585 for node in nodes {
508 let mut previous = None;586 let hidden = node.parent.is_some_and(|parent| hiding.contains(&parent));
509 for node in visible_paragraphs(nodes) {587 if hidden || node.collapsed {
510 match &node.content {588 hiding.insert(node.id);
511 ParagraphContent::Text(_) => {589 }
512 let mut paragraph = shape(node, width, indents)?;590 let mut block = Block {
513 let y = spacing(591 first: [
514 &mut bottom,592 result.paragraphs.len(),
515 &mut previous,593 result.tables.len(),
516 &paragraph.projection.text().spans()[0].format,594 result.objects.len(),
517 )?;595 ],
518 let size = paragraph.size();596 metrics: None,
519 paragraph.origin[1] = y;597 extent: [f32::NEG_INFINITY; 2],
520 result.paragraphs.push(paragraph);598 state,
521 result.size[0] = result.size[0].max(size[0]);599 rel: Vec::new(),
522 bottom += f64::from(size[1]);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) => {730 }
525 let x = indentation(node.level, indents, width)?;731 block.state = state;
526 let y = spacing(&mut bottom, &mut previous, &node.format)?;732 if depth == 0 {
527 let child = Self::table(table, depth + 1, edit, shape)?;733 result.blocks.push(block);
528 result.size[0] = result.size[0].max(x + child.size[0]);734 }
529 bottom += f64::from(child.size[1]);735 }
530 result.append(child, [x, y]);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) => {850 }
533 let x = indentation(node.level, indents, width)?;851 let mut state = segment.blocks.last().map_or(start, |block| block.state);
534 let y = spacing(&mut bottom, &mut previous, &node.format)?;852 let mut incoming = range
535 let [w, h] = image_size(image).ok_or(LayoutError::UnsupportedContent)?;853 .end
536 result.objects.push(ObjectLayout {854 .checked_sub(1)
537 id: image.id,855 .map_or((0.0, None), |root| self.blocks[root].state);
538 rect: [x, y, x + w, y + h],856 let mut moved = Vec::new();
539 kind: ObjectKind::Picture,857 for root in range.end..self.blocks.len() {
540 bottom: y + h,858 if state == incoming {
541 });859 break;
542 result.size[0] = result.size[0].max(x + w);
543 bottom += f64::from(h);
544 }860 }
545 ParagraphContent::Attachment(file) => {861 let block = &self.blocks[root];
546 let x = indentation(node.level, indents, width)?;862 incoming = block.state;
547 let y = spacing(&mut bottom, &mut previous, &node.format)?;863 let mut y = 0.0;
548 let [w, h] = file.size.unwrap_or([24.0, 24.0]);864 if let Some([before, after, height]) = block.metrics {
549 let icon = [x + (ATTACHMENT_WIDTH - w) / 2.0, y + 6.0];865 y = top(&mut state, [before, after]);
550 // OneNote shows the name without its extension.866 let [_, tables, objects] = self.pieces(root..root + 1);
551 let name = std::path::Path::new(&file.filename)867 let flow = match self.objects[objects].first() {
552 .file_stem()868 Some(object) if tables.is_empty() => object.at(y, height).1,
553 .and_then(|stem| stem.to_str())869 _ => height,
554 .unwrap_or(&file.filename);
555 let format = Format {
556 alignment: Some(1),
557 ..node.format.clone()
558 };870 };
559 let mut label = shape(871 state.0 += f64::from(flow);
560 &caption(node.id, file.id, name, format),872 if !(state.0 as f32).is_finite() {
561 ATTACHMENT_WIDTH,873 return Err(LayoutError::InvalidSpacing);
562 &[0.0],874 }
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);
575 }875 }
576 ParagraphContent::Ink(ink) => {876 moved.push((y, state));
577 let x = indentation(node.level, indents, width)?;877 }
578 let y = spacing(&mut bottom, &mut previous, &node.format)?;878 let bottom = if moved.len() == self.blocks.len() - range.end {
579 // The paragraph reaches from its origin to the farthest stroke point.879 state.0 as f32
580 let [w, h] = ink880 } else {
581 .bounds()881 self.size[1]
582 .map(|[x, y, w, h]| [x + w, y + h])882 };
583 .filter(|size| size.iter().all(|v| v.is_finite() && *v >= 0.0))883 let widest = |blocks: &[Block], side: usize| {
584 .ok_or(LayoutError::UnsupportedContent)?;884 blocks
585 result.objects.push(ObjectLayout {885 .iter()
586 id: ink.id,886 .map(|block| block.extent[side])
587 rect: [x, y, x + w, y + h],887 .fold(f32::NEG_INFINITY, f32::max)
588 kind: ObjectKind::Ink(ink.clone()),888 };
589 bottom: y + h,889 let widths = [0, 1].map(|side| {
590 });890 let [before, after] = [
591 result.size[0] = result.size[0].max(x + w);891 widest(&self.blocks[range.clone()], side),
592 bottom += f64::from(h);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) => {906 });
595 let x = indentation(node.level, indents, width)?;907 let tables = &old[1];
596 let y = spacing(&mut bottom, &mut previous, &node.format)?;908 let tables = self.tables[..tables.start]
597 let w = unsupported.layout.max_width.unwrap_or(160.0).max(160.0);909 .iter()
598 let mut label = shape(910 .chain(&segment.tables)
599 &caption(911 .chain(&self.tables[tables.end..]);
600 node.id,912 let size = [
601 unsupported.id,913 if fixed_width { width } else { widths[0] }.max(table_width(tables)),
602 "Unsupported content",914 bottom,
603 Format::default(),915 ];
604 ),916 return Ok(Relayout {
605 w - 16.0,917 range,
606 &[0.0],918 segment,
607 )?;919 moved,
608 label.reset_origin(x + 8.0);920 size,
609 label.origin[1] = y + 8.0;921 widths,
610 let h = unsupported922 });
611 .layout923 }
612 .max_height924 }
613 .unwrap_or(0.0)925
614 .max(label.text.height() + 16.0);926 /// Puts a [`Relayout`] of this layout in place.
615 result.objects.push(ObjectLayout {927 pub(crate) fn commit(&mut self, relayout: Relayout) {
616 id: unsupported.id,928 let Relayout {
617 rect: [x, y, x + w, y + h],929 range,
618 kind: ObjectKind::Unsupported(label),930 mut segment,
619 bottom: y + h,931 moved,
620 });932 size,
621 result.size[0] = result.size[0].max(x + w);933 widths,
622 bottom += f64::from(h);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() {962 for cell in self.tables[later_tables..]
626 return Err(LayoutError::InvalidSpacing);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;970 for (root, (y, state)) in (after..).zip(moved) {
630 result.size[0] = if fixed_width { width } else { result.size[0] }.max(result.table_width());971 let [paragraphs, tables, objects] = self.pieces(root..root + 1);
631 Ok(result)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 }
633996
634 pub(crate) fn table_width(&self) -> f32 {997 pub(crate) fn table_width(&self) -> f32 {
635 // Stored column widths extend 1.77pt past the final cell border.998 table_width(&self.tables)
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)
641 }999 }
6421000
643 fn table(1001 fn table(
...@@ -666,13 +1024,11 @@ impl OutlineLayout {...@@ -666,13 +1024,11 @@ impl OutlineLayout {
666 return Err(LayoutError::UnsupportedContent);1024 return Err(LayoutError::UnsupportedContent);
667 }1025 }
668 let mut result = Self {1026 let mut result = Self {
669 paragraphs: Vec::new(),
670 tables: vec![TableLayout {1027 tables: vec![TableLayout {
671 id: table.id,1028 id: table.id,
672 cells: Vec::new(),1029 cells: Vec::new(),
673 borders: table.borders.unwrap_or(true),1030 borders: table.borders.unwrap_or(true),
674 }],1031 }],
675 objects: Vec::new(),
676 size: [1032 size: [
677 (0..table.columns.len())1033 (0..table.columns.len())
678 .map(|index| width(index) + 4.98)1034 .map(|index| width(index) + 4.98)
...@@ -680,6 +1036,7 @@ impl OutlineLayout {...@@ -680,6 +1036,7 @@ impl OutlineLayout {
680 - 1.83,1036 - 1.83,
681 3.54,1037 3.54,
682 ],1038 ],
1039 ..Self::default()
683 };1040 };
684 let mut y = 0.0;1041 let mut y = 0.0;
685 for row in &table.rows {1042 for row in &table.rows {
...@@ -724,26 +1081,15 @@ impl OutlineLayout {...@@ -724,26 +1081,15 @@ impl OutlineLayout {
724 }1081 }
725 Ok(result)1082 Ok(result)
726 }1083 }
1084}
7271085
728 pub(crate) fn new(1086/// Stored column widths extend 1.77pt past the final cell border.
729 mut paragraphs: Vec<ParagraphLayout>,1087fn table_width<'a>(tables: impl IntoIterator<Item = &'a TableLayout>) -> f32 {
730 width: f32,1088 tables
731 width_set_by_user: bool,1089 .into_iter()
732 ) -> Result<Self, LayoutError> {1090 .filter_map(|table| table.cells.last())
733 if !width.is_finite() || width <= 0.0 {1091 .map(|cell| cell.rect[2] + 1.77)
734 return Err(LayoutError::InvalidWidth);1092 .fold(0.0, f32::max)
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 }
747}1093}
7481094
749/// Shapes a page object into positioned paragraph layouts.1095/// Shapes a page object into positioned paragraph layouts.
...@@ -773,13 +1119,6 @@ impl Arrange for Outline {...@@ -773,13 +1119,6 @@ impl Arrange for Outline {
773 }1119 }
774}1120}
7751121
776/// Outlines of plain paragraphs stack them; tables and pictures, files or ink need the full flow.
777pub(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
783pub(crate) fn outline_layout(1122pub(crate) fn outline_layout(
784 outline: &Outline,1123 outline: &Outline,
785 engine: &mut TextEngine,1124 engine: &mut TextEngine,
...@@ -789,26 +1128,16 @@ pub(crate) fn outline_layout(...@@ -789,26 +1128,16 @@ pub(crate) fn outline_layout(
789 if !outline.unsupported.is_empty() {1128 if !outline.unsupported.is_empty() {
790 return Err(LayoutError::UnsupportedContent);1129 return Err(LayoutError::UnsupportedContent);
791 }1130 }
792 if !all_text(&outline.paragraphs) {1131 OutlineLayout::flow(
793 return OutlineLayout::flow(1132 outline.paragraphs.iter(),
794 outline.paragraphs.iter(),1133 &outline.indents,
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,
810 width,1134 width,
811 outline.layout.width_set_by_user == Some(true),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}
8141143
...@@ -1377,9 +1706,12 @@ mod tests {...@@ -1377,9 +1706,12 @@ mod tests {
1377 _ => None,1706 _ => None,
1378 })1707 })
1379 .collect();1708 .collect();
1380 assert_eq!(colors, [0x0080_8000]);1709 assert_eq!(colors, [Some(0x0080_8000)]);
1381 assert_eq!(shaped.projection.text().spans()[0].format.color, None);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 assert_eq!(outline.tag_column_offset(), -12.0);1715 assert_eq!(outline.tag_column_offset(), -12.0);
1384 }1716 }
13851717
crates/draw/Cargo.toml+11-5
...@@ -4,13 +4,19 @@ version = "0.1.0"...@@ -4,13 +4,19 @@ version = "0.1.0"
4edition = "2024"4edition = "2024"
5publish = false5publish = false
66
7[features]
8default = ["render"]
9# The wgpu renderer; `edit` needs only parley.
10render = ["dep:bytemuck", "dep:image", "dep:roxmltree", "dep:swash", "dep:wgpu"]
11
7[dependencies]12[dependencies]
8bytemuck = { version = "1.25", features = ["derive"] }
9image = { version = "0.25", default-features = false, features = ["gif", "jpeg", "png"] }
10parley.workspace = true13parley.workspace = true
11roxmltree = "0.21"14
12swash = "0.2.10"15bytemuck = { version = "1.25", features = ["derive"], optional = true }
13wgpu.workspace = true16image = { version = "0.25", default-features = false, features = ["gif", "jpeg", "png"], optional = true }
17roxmltree = { version = "0.21", optional = true }
18swash = { version = "0.2.10", optional = true }
19wgpu = { workspace = true, optional = true }
1420
15[dev-dependencies]21[dev-dependencies]
16pollster = "0.4"22pollster = "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
4use parley::{
5 Affinity, Brush, Layout,
6 editing::{Cursor, Selection},
7};
8use std::{
9 collections::BTreeSet,
10 time::{Duration, Instant},
11};
12
13#[derive(Clone, Debug, PartialEq)]
14pub enum Key {
15 Named(NamedKey),
16 Character(String),
17}
18
19#[derive(Clone, Copy, Debug, PartialEq)]
20pub 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)]
39pub 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)]
47pub 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)]
63pub 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)]
72pub 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
83impl 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)]
152pub struct Clicks {
153 interval: Duration,
154 last: Option<(Instant, [f32; 2], SelectionUnit)>,
155}
156
157impl 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.
189pub 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`.
207pub 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.
228pub 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.
259pub 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)]
313mod 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 @@
1use 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.
8pub(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)]
90mod 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,1828 +1,10 @@
1//! Paints layers of primitives with wgpu: glyphs and icons through one atlas, images,1//! What the page and the interface share: painting layers of glyphs, icons, paths,
2//! rounded rectangles and pen strokes. Each layer has its own transform and clip.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.
34
4mod icon;5pub mod edit;
5mod text;6#[cfg(feature = "render")]
7mod render;
68
7pub use text::{Decoration, Glyph, GlyphRun, Glyphs, paint_parley_run};9#[cfg(feature = "render")]
810pub use render::*;
9use bytemuck::{Pod, Zeroable};
10use image::ImageDecoder;
11use parley::fontique::Blob;
12use std::{
13 collections::{HashMap, HashSet},
14 ops::Range,
15};
16use swash::{
17 FontRef,
18 scale::{Render, ScaleContext, Source, StrikeWith, image::Content},
19 zeno::{Angle, Format, Transform, Vector},
20};
21
22const ATLAS_SIZE: u32 = 2048;
23const MAX_GLYPHS: usize = 8192;
24const MAX_VERTICES: usize = 65_536;
25/// Decoded bytes of all images one frame may paint.
26pub const MAX_IMAGE_BYTES: u64 = 64 * 1024 * 1024;
27const MAX_IMAGES: usize = 256;
28
29#[derive(Clone)]
30pub struct RasterImage {
31 size: [u32; 2],
32 pixels: Blob<u8>,
33}
34
35impl 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
107struct CachedImage {
108 binding: wgpu::BindGroup,
109 bytes: u64,
110}
111
112struct Batch {
113 vertices: Range<u32>,
114 image: Option<u64>,
115 scissor: [u32; 4],
116}
117
118#[repr(C)]
119#[derive(Clone, Copy, Pod, Zeroable)]
120struct 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)]
130enum 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)]
149struct 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.
160pub 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)]
172struct Space {
173 size: [u32; 2],
174 scale: f32,
175 origin: [f32; 2],
176}
177
178impl 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
208fn 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)]
217pub 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.
224pub 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)]
265pub 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)]
281pub 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
298pub 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
316impl 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
1178fn 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.
1188pub 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)]
1201mod 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}
crates/draw/src/render.rs created+2135
...@@ -0,0 +1,2135 @@
1mod icon;
2mod text;
3
4pub use text::{Decoration, Glyph, GlyphRun, Glyphs, paint_parley_run};
5
6use bytemuck::{Pod, Zeroable};
7use image::ImageDecoder;
8use parley::fontique::Blob;
9use std::{
10 collections::{HashMap, HashSet},
11 ops::Range,
12};
13use swash::{
14 FontRef,
15 scale::{Render, ScaleContext, Source, StrikeWith, image::Content},
16 zeno::{Angle, Cap, Format, Join, Mask, Transform, Vector},
17};
18
19const ATLAS_SIZE: u32 = 2048;
20const MAX_GLYPHS: usize = 8192;
21const MAX_VERTICES: usize = 65_536;
22/// Decoded bytes of all images one frame may paint.
23pub const MAX_IMAGE_BYTES: u64 = 64 * 1024 * 1024;
24const MAX_IMAGES: usize = 256;
25
26#[derive(Clone)]
27pub struct RasterImage {
28 size: [u32; 2],
29 pixels: Blob<u8>,
30}
31
32impl 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
104struct CachedImage {
105 binding: wgpu::BindGroup,
106 bytes: u64,
107}
108
109struct Batch {
110 vertices: Range<u32>,
111 image: Option<u64>,
112 scissor: [u32; 4],
113}
114
115#[repr(C)]
116#[derive(Clone, Copy, Pod, Zeroable)]
117struct 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)]
127enum 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)]
153struct 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.
164pub 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)]
176struct Space {
177 size: [u32; 2],
178 scale: f32,
179 origin: [f32; 2],
180}
181
182impl 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
212fn 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)]
222pub 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)]
231pub 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.
238pub 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)]
297pub 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)]
313pub 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
330pub 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
348impl 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.
1350fn 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
1401fn 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.
1411pub 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)]
1424mod 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 @@
1use 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.
9pub(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)]
118mod 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 @@
1use crate::RenderError;
2use 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`.
6pub 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.
14pub 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)]
36pub 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)]
44pub 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`.
55pub 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 @@
1use crate::RenderError;
2use 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`.
6pub 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.
14pub 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)]
35pub 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)]
43pub 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`.
53pub 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,5 +20,5 @@ accesskit = "0.25.0"
20accesskit_winit = { version = "0.34.0", default-features = false, features = ["rwh_06"] }20accesskit_winit = { version = "0.34.0", default-features = false, features = ["rwh_06"] }
21objc2 = "0.5.2"21objc2 = "0.5.2"
22objc2-app-kit = { version = "0.2.2", default-features = false, features = ["NSAlert", "NSButton", "NSControl", "NSView", "NSResponder", "NSWindow", "NSApplication", "NSEvent", "NSColor", "NSColorSpace", "NSDatePicker", "NSDatePickerCell"] }22objc2-app-kit = { version = "0.2.2", default-features = false, features = ["NSAlert", "NSButton", "NSControl", "NSView", "NSResponder", "NSWindow", "NSApplication", "NSEvent", "NSColor", "NSColorSpace", "NSDatePicker", "NSDatePickerCell"] }
23objc2-foundation = { version = "0.2.2", default-features = false, features = ["NSString", "NSThread", "NSAttributedString", "NSDate", "NSCalendar", "NSDateFormatter", "NSLocale", "NSTimeZone"] }23objc2-foundation = { version = "0.2.2", default-features = false, features = ["NSString", "NSThread", "NSAttributedString", "NSDate", "NSCalendar", "NSDateFormatter", "NSGeometry", "NSLocale", "NSTimeZone"] }
2424
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
3macro_rules! art {
4 ($path:literal) => {
5 &[include_str!(concat!("../assets/", $path, ".svg"))]
6 };
7}
8
9pub const ALIGN_LEFT: &[&str] = art!("icons/align-left");
10pub const ATTACHMENT: &[&str] = art!("icons/attachment");
11pub const BOLD: &[&str] = art!("icons/bold");
12pub const BULLETS: &[&str] = art!("icons/bullets");
13pub const CALENDAR: &[&str] = art!("icons/calendar");
14pub const CLEAR_FORMATTING: &[&str] = art!("icons/clear-formatting");
15pub const CLOCK: &[&str] = art!("icons/clock");
16pub const CLOSE: &[&str] = art!("icons/close");
17pub const EQUATION: &[&str] = art!("icons/equation");
18pub const FONT_COLOR: &[&str] = art!("icons/font-color");
19pub const HIGHLIGHTER: &[&str] = art!("icons/highlighter");
20pub const INDENT: &[&str] = art!("icons/indent");
21pub const ITALIC: &[&str] = art!("icons/italic");
22pub const LINK: &[&str] = art!("icons/link");
23pub const NEW_PAGE: &[&str] = art!("icons/new-page");
24pub const NUMBERING: &[&str] = art!("icons/numbering");
25pub const OUTDENT: &[&str] = art!("icons/outdent");
26pub const PICTURE: &[&str] = art!("icons/picture");
27pub const REDO: &[&str] = art!("icons/redo");
28pub const SEARCH: &[&str] = art!("icons/search");
29pub const SIDEBAR_COLLAPSE: &[&str] = art!("icons/sidebar-collapse");
30pub const SIDEBAR_EXPAND: &[&str] = art!("icons/sidebar-expand");
31pub const STRIKETHROUGH: &[&str] = art!("icons/strikethrough");
32pub const SUBSCRIPT: &[&str] = art!("icons/subscript");
33pub const TABLE: &[&str] = art!("icons/table");
34pub const UNDERLINE: &[&str] = art!("icons/underline");
35pub const UNDO: &[&str] = art!("icons/undo");
36pub const ZOOM_IN: &[&str] = art!("icons/zoom-in");
37pub const ZOOM_OUT: &[&str] = art!("icons/zoom-out");
38
39pub const TAG_STAR: &[&str] = art!("tags/star");
40pub const TAG_REMEMBER: &[&str] = art!("tags/remember");
41pub const TAG_DEFINITION: &[&str] = art!("tags/definition");
42pub const TAG_HIGHLIGHT: &[&str] = art!("tags/highlight");
43pub const TAG_CONTACT: &[&str] = art!("tags/contact");
44pub const TAG_ADDRESS: &[&str] = art!("tags/address");
45pub const TAG_PHONE: &[&str] = art!("tags/phone");
crates/snowbound/src/macos.rs+51-1
...@@ -13,7 +13,7 @@ use objc2_app_kit::{...@@ -13,7 +13,7 @@ use objc2_app_kit::{
13};13};
14use objc2_foundation::{14use objc2_foundation::{
15 MainThreadMarker, NSAttributedString, NSCalendar, NSCalendarUnit, NSDate, NSDateFormatter,15 MainThreadMarker, NSAttributedString, NSCalendar, NSCalendarUnit, NSDate, NSDateFormatter,
16 NSDateFormatterStyle, NSRange, NSString,16 NSDateFormatterStyle, NSPoint, NSRange, NSRect, NSSize, NSString,
17};17};
18use std::{cell::Cell, sync::OnceLock};18use std::{cell::Cell, sync::OnceLock};
19use winit::{19use winit::{
...@@ -261,6 +261,56 @@ pub fn configure_presentation(surface: &wgpu::Surface<'_>) {...@@ -261,6 +261,56 @@ pub fn configure_presentation(surface: &wgpu::Surface<'_>) {
261 }261 }
262}262}
263263
264/// The application's icon as the Dock shows it, `pixels` square.
265pub 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
264pub fn text_colors() -> [[f32; 4]; 2] {314pub fn text_colors() -> [[f32; 4]; 2] {
265 unsafe {315 unsafe {
266 let space = NSColorSpace::sRGBColorSpace();316 let space = NSColorSpace::sRGBColorSpace();
crates/snowbound/src/main.rs+705-230
...@@ -1,16 +1,15 @@...@@ -1,16 +1,15 @@
1mod art;
1mod macos;2mod macos;
23
3use canvas::gpu::page::PageScene;4use canvas::gpu::page::PageScene;
4use canvas::interaction::{5use canvas::interaction::{Cursor, PageView, Request, Response, TextColors, accessibility};
5 Cursor, Key as PageKey, Modifiers, NamedKey as PageNamedKey, PageView, Request, Response,
6 TextColors, accessibility,
7};
8use canvas::{6use canvas::{
9 date::DateField,7 date::DateField,
10 document::TextDocument,8 document::TextDocument,
11 editor::{CanvasEditor, DEFAULT_OUTLINE_WIDTH, TextOutline},9 editor::{CanvasEditor, DEFAULT_OUTLINE_WIDTH, TextOutline},
12 layout::TextEngine,10 layout::TextEngine,
13};11};
12use canvas::{gpu::tag_sources, outline::TagIcon};
14use draw::Renderer;13use draw::Renderer;
15use onestore::ExGuid;14use onestore::ExGuid;
16use onestore::document::Format;15use onestore::document::Format;
...@@ -22,7 +21,7 @@ use std::{...@@ -22,7 +21,7 @@ use std::{
22 sync::Arc,21 sync::Arc,
23 time::Instant,22 time::Instant,
24};23};
25use ui::{Axis, Flags, Id, Spec, Theme, Ui, fill, fit, px};24use ui::{Axis, Flags, Id, Shape, Spec, Theme, Ui, children, fill, fit, px};
26use winit::{25use winit::{
27 application::ApplicationHandler,26 application::ApplicationHandler,
28 dpi::{LogicalSize, PhysicalPosition, PhysicalSize},27 dpi::{LogicalSize, PhysicalPosition, PhysicalSize},
...@@ -33,10 +32,15 @@ use winit::{...@@ -33,10 +32,15 @@ use winit::{
33 window::{CursorIcon, Window, WindowId},32 window::{CursorIcon, Window, WindowId},
34};33};
3534
36/// Height of the window's top strip, which holds the traffic lights and section tabs.35/// Height of the macOS title bar, which holds the traffic lights.
37const STRIP: f32 = 38.0;36const TITLE: f32 = 28.0;
38/// Room the traffic lights take at the strip's leading edge.37/// Room the traffic lights take at the title bar's leading edge.
39const LIGHTS: f32 = 78.0;38const LIGHTS: f32 = 78.0;
39const TAB_ROW: f32 = 28.0;
40/// Width of the section colour around the page.
41const FRAME: f32 = 6.0;
42/// macOS rounds windows' corners by 10 pt; the page's corners share their centres.
43const ROUNDING: f32 = 10.0 - FRAME;
40const PAGE_LIST: f32 = 240.0;44const PAGE_LIST: f32 = 240.0;
4145
42#[derive(Debug)]46#[derive(Debug)]
...@@ -48,6 +52,17 @@ enum UserEvent {...@@ -48,6 +52,17 @@ enum UserEvent {
48 Sync,52 Sync,
49 /// Scripted input from `SNOWBOUND_REPLAY`.53 /// Scripted input from `SNOWBOUND_REPLAY`.
50 Replay(Replay),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.
60struct Redraw(EventLoopProxy<UserEvent>);
61
62impl std::task::Wake for Redraw {
63 fn wake(self: Arc<Self>) {
64 let _ = self.0.send_event(UserEvent::Redraw);
65 }
51}66}
5267
53#[derive(Debug)]68#[derive(Debug)]
...@@ -55,6 +70,9 @@ enum Replay {...@@ -55,6 +70,9 @@ enum Replay {
55 Input(ui::Event),70 Input(ui::Event),
56 /// Paints the next frame into a PNG as well as the window.71 /// Paints the next frame into a PNG as well as the window.
57 Snapshot(PathBuf),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}
5977
60impl From<accesskit_winit::Event> for UserEvent {78impl From<accesskit_winit::Event> for UserEvent {
...@@ -63,46 +81,17 @@ impl From<accesskit_winit::Event> for UserEvent {...@@ -63,46 +81,17 @@ impl From<accesskit_winit::Event> for UserEvent {
63 }81 }
64}82}
6583
66fn trace_input(event: &impl std::fmt::Debug) {84/// With `SNOWBOUND_PROFILE` set, prints how long a phase of the frame took since `start`.
67 if std::env::var_os("SNOWBOUND_TRACE_INPUT").is_some() {85fn lap(phase: &str, start: Instant) {
68 eprintln!("Input {:?}: {event:?}", std::time::SystemTime::now());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}
7191
72fn page_key(key: &Key) -> PageKey {92fn trace_input(event: &impl std::fmt::Debug) {
73 match key {93 if std::env::var_os("SNOWBOUND_TRACE_INPUT").is_some() {
74 Key::Character(text) => PageKey::Character(text.to_string()),94 eprintln!("Input {:?}: {event:?}", std::time::SystemTime::now());
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
100fn 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(),
106 }95 }
107}96}
10897
...@@ -123,6 +112,8 @@ struct App {...@@ -123,6 +112,8 @@ struct App {
123 proxy: EventLoopProxy<UserEvent>,112 proxy: EventLoopProxy<UserEvent>,
124 input: Option<Input>,113 input: Option<Input>,
125 substitutes: Vec<PathBuf>,114 substitutes: Vec<PathBuf>,
115 /// Font files for the interface in place of the system's.
116 interface_font: Vec<Vec<u8>>,
126 state: Option<State>,117 state: Option<State>,
127 startup_error: Option<Box<dyn Error>>,118 startup_error: Option<Box<dyn Error>>,
128}119}
...@@ -156,6 +147,8 @@ struct Tab {...@@ -156,6 +147,8 @@ struct Tab {
156147
157/// The sections the tabs offer.148/// The sections the tabs offer.
158struct Library {149struct Library {
150 /// The notebook's folder name, or the section file's.
151 name: String,
159 notebook: Option<notebook::session::Notebook>,152 notebook: Option<notebook::session::Notebook>,
160 tabs: Vec<Tab>,153 tabs: Vec<Tab>,
161 cache: PathBuf,154 cache: PathBuf,
...@@ -220,6 +213,8 @@ enum Command {...@@ -220,6 +213,8 @@ enum Command {
220struct State {213struct State {
221 window: Arc<Window>,214 window: Arc<Window>,
222 proxy: EventLoopProxy<UserEvent>,215 proxy: EventLoopProxy<UserEvent>,
216 /// Asks for a frame when a worker thread finishes something the page shows.
217 redraw: std::task::Waker,
223 instance: wgpu::Instance,218 instance: wgpu::Instance,
224 surface: wgpu::Surface<'static>,219 surface: wgpu::Surface<'static>,
225 config: wgpu::SurfaceConfiguration,220 config: wgpu::SurfaceConfiguration,
...@@ -230,9 +225,15 @@ struct State {...@@ -230,9 +225,15 @@ struct State {
230 session: Option<Session>,225 session: Option<Session>,
231 /// Text filtering the page list.226 /// Text filtering the page list.
232 filter: String,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 commands: Vec<Command>,232 commands: Vec<Command>,
234 /// The page changed during this frame, to be saved and announced after it.233 /// The page changed during this frame, to be saved and announced after it.
235 changed: bool,234 changed: bool,
235 /// Only the view scrolled or zoomed during this frame, to be announced after it.
236 moved: bool,
236 /// Whether the page last heard it had keyboard focus.237 /// Whether the page last heard it had keyboard focus.
237 page_focused: bool,238 page_focused: bool,
238 window_focused: bool,239 window_focused: bool,
...@@ -260,6 +261,11 @@ fn page() -> Id {...@@ -260,6 +261,11 @@ fn page() -> Id {
260 Id::ROOT.child("page")261 Id::ROOT.child("page")
261}262}
262263
264/// The section's colour around the page and its tabs.
265fn frame() -> Id {
266 Id::ROOT.child("frame")
267}
268
263/// The field filtering the page list.269/// The field filtering the page list.
264fn filter() -> Id {270fn filter() -> Id {
265 Id::ROOT.child("filter")271 Id::ROOT.child("filter")
...@@ -271,6 +277,7 @@ impl State {...@@ -271,6 +277,7 @@ impl State {
271 proxy: EventLoopProxy<UserEvent>,277 proxy: EventLoopProxy<UserEvent>,
272 input: Input,278 input: Input,
273 substitutes: &[PathBuf],279 substitutes: &[PathBuf],
280 interface_font: Vec<Vec<u8>>,
274 ) -> Result<Self, Box<dyn Error>> {281 ) -> Result<Self, Box<dyn Error>> {
275 let window = Arc::new(282 let window = Arc::new(
276 event_loop.create_window(283 event_loop.create_window(
...@@ -344,6 +351,7 @@ impl State {...@@ -344,6 +351,7 @@ impl State {
344 }351 }
345 Input::Section { file, title, cache } => {352 Input::Section { file, title, cache } => {
346 let opened = Library {353 let opened = Library {
354 name: file_name(file.parent().unwrap_or(&file)),
347 notebook: None,355 notebook: None,
348 tabs: vec![Tab {356 tabs: vec![Tab {
349 name: file357 name: file
...@@ -374,6 +382,7 @@ impl State {...@@ -374,6 +382,7 @@ impl State {
374 return Err(format!("No readable sections in {}", root.display()).into());382 return Err(format!("No readable sections in {}", root.display()).into());
375 }383 }
376 let opened = Library {384 let opened = Library {
385 name: file_name(&root),
377 notebook: Some(notebook),386 notebook: Some(notebook),
378 tabs,387 tabs,
379 cache,388 cache,
...@@ -398,10 +407,20 @@ impl State {...@@ -398,10 +407,20 @@ impl State {
398 window.set_ime_allowed(true);407 window.set_ime_allowed(true);
399 window.request_redraw();408 window.request_redraw();
400 eprintln!("Canvas GPU: {:?}; scale factor {dpr}", adapter.get_info());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 ui.set_focus(Some(page()));420 ui.set_focus(Some(page()));
403 let state = Self {421 let state = Self {
404 window,422 window,
423 redraw: Arc::new(Redraw(proxy.clone())).into(),
405 proxy,424 proxy,
406 instance,425 instance,
407 surface,426 surface,
...@@ -419,8 +438,11 @@ impl State {...@@ -419,8 +438,11 @@ impl State {
419 library,438 library,
420 session,439 session,
421 filter: String::new(),440 filter: String::new(),
441 pages_open: true,
442 app_icon: macos::app_icon((16.0 * dpr).round() as u32),
422 commands: Vec::new(),443 commands: Vec::new(),
423 changed: false,444 changed: false,
445 moved: false,
424 page_focused: true,446 page_focused: true,
425 window_focused: true,447 window_focused: true,
426 pointer: [0.0; 2],448 pointer: [0.0; 2],
...@@ -442,6 +464,7 @@ impl State {...@@ -442,6 +464,7 @@ impl State {
442 /// Builds, lays out and paints one frame, then does what it asked for and asks for the464 /// Builds, lays out and paints one frame, then does what it asked for and asks for the
443 /// frame that shows the result.465 /// frame that shows the result.
444 fn frame(&mut self) -> Result<(), Box<dyn Error>> {466 fn frame(&mut self) -> Result<(), Box<dyn Error>> {
467 let start = Instant::now();
445 let size = self.window.inner_size();468 let size = self.window.inner_size();
446 let scale = self.window.scale_factor() as f32;469 let scale = self.window.scale_factor() as f32;
447 self.ui.begin(470 self.ui.begin(
...@@ -449,8 +472,10 @@ impl State {...@@ -449,8 +472,10 @@ impl State {
449 scale,472 scale,
450 Instant::now(),473 Instant::now(),
451 );474 );
452 self.build()?;475 let (section, open_tab, open_page) = self.build()?;
453 self.ui.end();476 self.ui.end();
477 self.edges(section, open_tab, open_page);
478 lap("build", start);
454 if let Some(rect) = self.ui.rect(page()) {479 if let Some(rect) = self.ui.rect(page()) {
455 let size = [480 let size = [
456 ((rect[2] - rect[0]) * scale).round() as u32,481 ((rect[2] - rect[0]) * scale).round() as u32,
...@@ -476,6 +501,7 @@ impl State {...@@ -476,6 +501,7 @@ impl State {
476 self.window.set_ime_allowed(ime);501 self.window.set_ime_allowed(ime);
477 }502 }
478 self.draw()?;503 self.draw()?;
504 lap("drawn", start);
479 let commands = std::mem::take(&mut self.commands);505 let commands = std::mem::take(&mut self.commands);
480 let follow = !commands.is_empty() || self.ui.wants_frame();506 let follow = !commands.is_empty() || self.ui.wants_frame();
481 for command in commands {507 for command in commands {
...@@ -483,101 +509,100 @@ impl State {...@@ -483,101 +509,100 @@ impl State {
483 }509 }
484 if std::mem::take(&mut self.changed) {510 if std::mem::take(&mut self.changed) {
485 self.after_edit()?;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 self.window.request_redraw();518 self.window.request_redraw();
489 }519 }
520 lap("frame", start);
490 Ok(())521 Ok(())
491 }522 }
492523
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 let theme = self.ui.theme.clone();547 let theme = self.ui.theme.clone();
495 self.ui.open_as(548 self.title_bar(&theme);
496 strip(),549 self.toolbar(&theme)?;
550 self.ui.open(
551 "tabs",
497 Spec {552 Spec {
498 flags: Flags::CLICKABLE,553 size: [fill(), px(TAB_ROW)],
499 size: [fill(), px(STRIP)],
500 fill: Some(theme.strip),554 fill: Some(theme.strip),
501 pad: [0.0, 8.0],555 pad: [FRAME, 0.0],
502 gap: 2.0,
503 ..Spec::default()556 ..Spec::default()
504 },557 },
505 );558 );
506 self.ui.leaf(559 self.ui.leaf(
507 "lights",560 "corner",
508 Spec {561 Spec {
509 size: [px(LIGHTS), px(1.0)],562 size: [px(ROUNDING), px(1.0)],
510 ..Spec::default()563 ..Spec::default()
511 },564 },
512 );565 );
513 match (&self.library, &self.session) {566 let (clicked, open_tab) = match (&self.library, &self.session) {
514 (Some(library), Some(session)) => {567 (Some(library), Some(session)) => {
515 for (index, tab) in library.tabs.iter().enumerate() {568 let tabs: Vec<_> = library
516 let active = index == session.tab;569 .tabs
517 let id = self.ui.open(570 .iter()
518 ("tab", index),571 .map(|tab| (tab.name.as_str(), section_color(tab.color)))
519 Spec {572 .collect();
520 flags: Flags::CLICKABLE,573 ui::shell::section_tabs(
521 size: [fit(), px(STRIP - 8.0)],574 &mut self.ui,
522 text: Some(&tab.name),575 "sections",
523 color: Some(if active { theme.text } else { theme.text_dim }),576 &tabs,
524 fill: Some(if active { theme.base } else { theme.strip }),577 session.tab,
525 hover_fill: (!active).then_some(theme.chip),578 &section,
526 radius: 6.0,579 TAB_ROW,
527 pad: [14.0 + 14.0, 0.0],580 )
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 );
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(594 if self.session.is_some() {
557 "space",595 self.page_tools(&theme);
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 );
574 }596 }
575 self.ui.close();597 self.ui.close();
576598
577 self.ui.open(599 self.ui.open_as(
578 "body",600 frame(),
579 Spec {601 Spec {
580 size: [fill(), fill()],602 size: [fill(), fill()],
603 fill: Some(section.frame[0]),
604 gradient: Some(section.frame[1]),
605 pad: [FRAME, FRAME],
581 ..Spec::default()606 ..Spec::default()
582 },607 },
583 );608 );
...@@ -601,7 +626,7 @@ impl State {...@@ -601,7 +626,7 @@ impl State {
601 Spec {626 Spec {
602 flags: Flags::CUSTOM | Flags::FOCUSABLE,627 flags: Flags::CUSTOM | Flags::FOCUSABLE,
603 size: [fill(), fill()],628 size: [fill(), fill()],
604 fill: Some([1.0; 4]),629 fill: Some(theme.paper),
605 ..Spec::default()630 ..Spec::default()
606 },631 },
607 );632 );
...@@ -630,37 +655,124 @@ impl State {...@@ -630,37 +655,124 @@ impl State {
630 }655 }
631 self.page_events(signal.events)?;656 self.page_events(signal.events)?;
632 self.ui.close();657 self.ui.close();
633 if self.session.is_some() {658 let open_page = self.page_list(&theme, &section);
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 }
644 self.ui.close();659 self.ui.close();
645 Ok(())660 Ok((section, open_tab, open_page))
646 }661 }
647662
648 fn conflict_bar(&mut self, theme: &Theme) {663 /// Borders the open section tab and the frame's top, and rounds and borders the page
649 self.ui.open(664 /// together with the open page's tab, joined where they meet.
650 "conflict",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 Spec {752 Spec {
652 size: [fill(), px(40.0)],753 flags: Flags::CLICKABLE,
653 fill: Some(theme.panel),754 size: [fill(), px(TITLE)],
654 pad: [12.0, 7.0],755 fill: Some(theme.strip),
655 gap: 8.0,
656 ..Spec::default()756 ..Spec::default()
657 },757 },
658 );758 );
659 self.ui.leaf(759 self.ui.leaf(
660 "message",760 "lights",
661 Spec {761 Spec {
662 size: [fit(), px(26.0)],762 size: [px(LIGHTS), px(1.0)],
663 text: Some("This page also changed on another computer."),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 ..Spec::default()776 ..Spec::default()
665 },777 },
666 );778 );
...@@ -671,122 +783,341 @@ impl State {...@@ -671,122 +783,341 @@ impl State {
671 ..Spec::default()783 ..Spec::default()
672 },784 },
673 );785 );
674 if ui::button(&mut self.ui, "mine", "Keep mine").clicked {786 if let Some(session) = &self.session {
675 self.commands.push(Command::Resolve { keep_mine: true });787 self.ui.leaf(
676 }788 "status",
677 if ui::button(&mut self.ui, "theirs", "Keep theirs").clicked {789 Spec {
678 self.commands.push(Command::Resolve { keep_mine: false });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 self.ui.close();798 self.ui.close();
681 }799 }
682800
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 let Some(session) = &self.session else {947 let Some(session) = &self.session else {
685 return;948 return;
686 };949 };
687 self.ui.open(950 self.ui.open(
688 "panel",951 "tools",
689 Spec {952 Spec {
690 axis: Axis::Y,953 size: [px(PAGE_LIST), px(TAB_ROW)],
691 size: [px(PAGE_LIST), fill()],954 pad: [0.0, (TAB_ROW - ui::shell::TOOL) / 2.0],
692 fill: Some(theme.panel),955 gap: 3.0,
693 ..Spec::default()956 ..Spec::default()
694 },957 },
695 );958 );
959 let focused = self.ui.focused() == Some(filter());
696 self.ui.open(960 self.ui.open(
697 "pages",961 "search",
698 Spec {962 Spec {
699 flags: Flags::SCROLL | Flags::CLIP,963 size: [fill(), px(ui::shell::TOOL)],
700 axis: Axis::Y,964 fill: Some(theme.base),
701 size: [fill(), fill()],965 border: Some(if focused { theme.accent } else { theme.chip }),
702 pad: [6.0, 6.0],966 radius: 4.0,
703 gap: 1.0,967 pad: [6.0, 0.0],
704 ..Spec::default()968 ..Spec::default()
705 },969 },
706 );970 );
707 let query = self.filter.to_lowercase();971 self.ui.leaf(
708 let mut first = None;972 "icon",
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",
746 Spec {973 Spec {
747 size: [fill(), px(38.0)],974 size: [fit(), px(ui::shell::TOOL)],
748 pad: [6.0, 6.0],975 icon: Some(art::SEARCH),
976 color: Some(theme.text_dim),
749 ..Spec::default()977 ..Spec::default()
750 },978 },
751 );979 );
752 let before = self.filter.clone();
753 let signal = ui::text_field(980 let signal = ui::text_field(
754 &mut self.ui,981 &mut self.ui,
755 filter(),982 filter(),
756 &mut self.filter,983 &mut self.filter,
757 &placeholder,984 "Search pages",
758 Spec {985 Spec {
759 size: [fill(), px(26.0)],986 size: [fill(), px(ui::shell::TOOL)],
760 fill: Some(theme.base),987 pad: [6.0, 0.0],
761 border: Some(theme.separator),
762 radius: 4.0,
763 pad: [8.0, 0.0],
764 ..Spec::default()988 ..Spec::default()
765 },989 },
766 );990 );
767 // The list above was built with the filter as it stood.991 if !self.filter.is_empty()
768 if self.filter != before {992 && ui::shell::tool_button(&mut self.ui, "clear", art::CLOSE, theme.text_dim).clicked
769 self.window.request_redraw();993 {
994 self.filter.clear();
770 }995 }
996 self.ui.close();
771 for event in &signal.events {997 for event in &signal.events {
772 if let ui::Event::Key {998 if let ui::Event::Key {
773 key: Key::Named(key @ (NamedKey::Escape | NamedKey::Enter)),999 key: Key::Named(key @ (NamedKey::Escape | NamedKey::Enter)),
774 ..1000 ..
775 } = event1001 } = event
776 {1002 {
1003 let first = matching(&session.pages, &self.filter).next();
777 if *key == NamedKey::Enter1004 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 self.filter.clear();1009 self.filter.clear();
783 self.ui.set_focus(Some(page()));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 self.ui.close();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 self.ui.close();1057 self.ui.close();
788 }1058 }
7891059
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 /// Hands the page the events routed to its box, in its device pixels.1121 /// Hands the page the events routed to its box, in its device pixels.
791 fn page_events(&mut self, events: Vec<ui::Event>) -> Result<(), Box<dyn Error>> {1122 fn page_events(&mut self, events: Vec<ui::Event>) -> Result<(), Box<dyn Error>> {
792 let scale = self.ui.scale();1123 let scale = self.ui.scale();
...@@ -822,13 +1153,13 @@ impl State {...@@ -822,13 +1153,13 @@ impl State {
822 self.ui.set_focus(Some(filter()));1153 self.ui.set_focus(Some(filter()));
823 continue;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 ui::Event::Ime(Ime::Preedit(text, cursor)) => self.view.compose(text, cursor)?,1158 ui::Event::Ime(Ime::Preedit(text, cursor)) => self.view.compose(text, cursor)?,
828 ui::Event::Ime(Ime::Commit(text)) => self.view.commit_text(text)?,1159 ui::Event::Ime(Ime::Commit(text)) => self.view.commit_text(text)?,
829 ui::Event::Ime(Ime::Disabled) => self.view.cancel_composition()?,1160 ui::Event::Ime(Ime::Disabled) => self.view.cancel_composition()?,
830 ui::Event::Modifiers(modifiers) => {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 self.respond(response);1165 self.respond(response);
...@@ -842,6 +1173,7 @@ impl State {...@@ -842,6 +1173,7 @@ impl State {
842 /// Notes a page change for after the frame and queues what the page asked of the host.1173 /// Notes a page change for after the frame and queues what the page asked of the host.
843 fn respond(&mut self, response: Response) {1174 fn respond(&mut self, response: Response) {
844 self.changed |= response.changed;1175 self.changed |= response.changed;
1176 self.moved |= response.moved;
845 if let Some(request) = response.request {1177 if let Some(request) = response.request {
846 self.commands.push(Command::Page(request));1178 self.commands.push(Command::Page(request));
847 }1179 }
...@@ -892,6 +1224,16 @@ impl State {...@@ -892,6 +1224,16 @@ impl State {
8921224
893 /// Follows an edit: the input method's position, accessibility, saving and the title.1225 /// Follows an edit: the input method's position, accessibility, saving and the title.
894 fn after_edit(&mut self) -> Result<(), Box<dyn Error>> {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 let scale = self.ui.scale();1237 let scale = self.ui.scale();
896 let corner = self.ui.rect(page()).unwrap_or_default();1238 let corner = self.ui.rect(page()).unwrap_or_default();
897 let [x0, y0, x1, y1] = self.view.caret_area()?;1239 let [x0, y0, x1, y1] = self.view.caret_area()?;
...@@ -899,9 +1241,9 @@ impl State {...@@ -899,9 +1241,9 @@ impl State {
899 PhysicalPosition::new(x0 + corner[0] * scale, y0 + corner[1] * scale),1241 PhysicalPosition::new(x0 + corner[0] * scale, y0 + corner[1] * scale),
900 PhysicalSize::new(x1 - x0, y1 - y0),1242 PhysicalSize::new(x1 - x0, y1 - y0),
901 );1243 );
1244 let start = Instant::now();
902 self.update_accessibility()?;1245 self.update_accessibility()?;
903 self.persist()?;1246 lap("accessibility", start);
904 self.title();
905 Ok(())1247 Ok(())
906 }1248 }
9071249
...@@ -1095,20 +1437,13 @@ impl State {...@@ -1095,20 +1437,13 @@ impl State {
1095 self.access_adapter.update_if_active(|| {1437 self.access_adapter.update_if_active(|| {
1096 match self.accessibility.update(1438 match self.accessibility.update(
1097 &view.editor,1439 &view.editor,
1440 view.scene.as_ref(),
1098 viewport,1441 viewport,
1099 &self.window.title(),1442 &self.window.title(),
1100 view.outline_preview(),1443 view.outline_preview(),
1444 view.object_focus().and_then(|focus| focus.read_only()),
1101 ) {1445 ) {
1102 Ok(mut update) => {1446 Ok(update) => 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 }
1112 Err(failure) => {1447 Err(failure) => {
1113 error = Some(failure);1448 error = Some(failure);
1114 self.accessibility.deactivate();1449 self.accessibility.deactivate();
...@@ -1185,6 +1520,7 @@ impl State {...@@ -1185,6 +1520,7 @@ impl State {
1185 }1520 }
11861521
1187 fn draw(&mut self) -> Result<(), Box<dyn Error>> {1522 fn draw(&mut self) -> Result<(), Box<dyn Error>> {
1523 let start = Instant::now();
1188 if let Some(path) = self.snapshot.take() {1524 if let Some(path) = self.snapshot.take() {
1189 self.snapshot(&path)?;1525 self.snapshot(&path)?;
1190 }1526 }
...@@ -1222,9 +1558,12 @@ impl State {...@@ -1222,9 +1558,12 @@ impl State {
1222 return Err("Canvas surface validation failed".into());1558 return Err("Canvas surface validation failed".into());
1223 }1559 }
1224 };1560 };
1561 lap("acquire", start);
1225 self.paint(&frame.texture.create_view(&Default::default()))?;1562 self.paint(&frame.texture.create_view(&Default::default()))?;
1226 self.window.pre_present_notify();1563 self.window.pre_present_notify();
1564 let start = Instant::now();
1227 self.renderer.queue.present(frame);1565 self.renderer.queue.present(frame);
1566 lap("present", start);
1228 trace_input(&"Present submitted");1567 trace_input(&"Present submitted");
1229 if reconfigure {1568 if reconfigure {
1230 self.surface.configure(&self.renderer.device, &self.config);1569 self.surface.configure(&self.renderer.device, &self.config);
...@@ -1234,8 +1573,19 @@ impl State {...@@ -1234,8 +1573,19 @@ impl State {
12341573
1235 /// Paints the interface with the page in its box.1574 /// Paints the interface with the page in its box.
1236 fn paint(&mut self, target: &wgpu::TextureView) -> Result<(), Box<dyn Error>> {1575 fn paint(&mut self, target: &wgpu::TextureView) -> Result<(), Box<dyn Error>> {
1576 let start = Instant::now();
1237 let [caret, selection] = macos::text_colors();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 let scale = self.ui.scale();1589 let scale = self.ui.scale();
1240 let corner = self.ui.rect(page()).unwrap_or_default();1590 let corner = self.ui.rect(page()).unwrap_or_default();
1241 let viewport = self.view.viewport;1591 let viewport = self.view.viewport;
...@@ -1261,6 +1611,7 @@ impl State {...@@ -1261,6 +1611,7 @@ impl State {
1261 })1611 })
1262 .collect();1612 .collect();
1263 trace_input(&("Draw", viewport.origin, viewport.scale));1613 trace_input(&("Draw", viewport.origin, viewport.scale));
1614 let start = Instant::now();
1264 self.renderer1615 self.renderer
1265 .draw(1616 .draw(
1266 target,1617 target,
...@@ -1268,7 +1619,9 @@ impl State {...@@ -1268,7 +1619,9 @@ impl State {
1268 self.ui.theme.base,1619 self.ui.theme.base,
1269 &layers,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 }
12731626
1274 /// Writes the frame to a PNG, so a covered window can be reviewed.1627 /// Writes the frame to a PNG, so a covered window can be reviewed.
...@@ -1373,6 +1726,76 @@ impl State {...@@ -1373,6 +1726,76 @@ impl State {
1373 }1726 }
1374}1727}
13751728
1729fn 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
1736fn 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.
1746fn 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.
1751fn 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.
1762fn 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
1775fn 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.
1788fn 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/// Section tabs for a notebook's readable top-level sections, in its order.1799/// Section tabs for a notebook's readable top-level sections, in its order.
1377fn tabs(notebook: &notebook::session::Notebook) -> Vec<Tab> {1800fn tabs(notebook: &notebook::session::Notebook) -> Vec<Tab> {
1378 notebook1801 notebook
...@@ -1481,11 +1904,19 @@ impl ApplicationHandler<UserEvent> for App {...@@ -1481,11 +1904,19 @@ impl ApplicationHandler<UserEvent> for App {
1481 }1904 }
1482 return;1905 return;
1483 }1906 }
1907 UserEvent::Redraw => {
1908 if let Some(state) = &self.state {
1909 state.window.request_redraw();
1910 }
1911 return;
1912 }
1484 UserEvent::Replay(replay) => {1913 UserEvent::Replay(replay) => {
1485 if let Some(state) = &mut self.state {1914 if let Some(state) = &mut self.state {
1486 match replay {1915 match replay {
1487 Replay::Input(event) => state.input(event),1916 Replay::Input(event) => state.input(event),
1488 Replay::Snapshot(path) => state.snapshot = Some(path),1917 Replay::Snapshot(path) => state.snapshot = Some(path),
1918 Replay::Tick => {}
1919 Replay::Appearance(appearance) => state.ui.theme = theme(appearance),
1489 }1920 }
1490 // A covered window gets no redraws, so each step draws its own frame.1921 // A covered window gets no redraws, so each step draws its own frame.
1491 if let Err(error) = state.frame() {1922 if let Err(error) = state.frame() {
...@@ -1504,7 +1935,10 @@ impl ApplicationHandler<UserEvent> for App {...@@ -1504,7 +1935,10 @@ impl ApplicationHandler<UserEvent> for App {
1504 }1935 }
1505 let was_marked = state.view.editor.marked_range().is_some();1936 let was_marked = state.view.editor.marked_range().is_some();
1506 let result = match event.window_event {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 accesskit_winit::WindowEvent::ActionRequested(request) => state.access_action(request),1942 accesskit_winit::WindowEvent::ActionRequested(request) => state.access_action(request),
1509 accesskit_winit::WindowEvent::AccessibilityDeactivated => {1943 accesskit_winit::WindowEvent::AccessibilityDeactivated => {
1510 state.accessibility.deactivate();1944 state.accessibility.deactivate();
...@@ -1528,6 +1962,7 @@ impl ApplicationHandler<UserEvent> for App {...@@ -1528,6 +1962,7 @@ impl ApplicationHandler<UserEvent> for App {
1528 self.proxy.clone(),1962 self.proxy.clone(),
1529 self.input.take().unwrap(),1963 self.input.take().unwrap(),
1530 &self.substitutes,1964 &self.substitutes,
1965 std::mem::take(&mut self.interface_font),
1531 )) {1966 )) {
1532 Ok(state) => self.state = Some(state),1967 Ok(state) => self.state = Some(state),
1533 Err(error) => {1968 Err(error) => {
...@@ -1575,8 +2010,13 @@ impl ApplicationHandler<UserEvent> for App {...@@ -1575,8 +2010,13 @@ impl ApplicationHandler<UserEvent> for App {
1575 // lets the window show the previous frame at the new size.2010 // lets the window show the previous frame at the new size.
1576 return state.frame();2011 return state.frame();
1577 }2012 }
2013 WindowEvent::ThemeChanged(appearance) => {
2014 state.ui.theme = theme(appearance);
2015 state.window.request_redraw();
2016 }
1578 WindowEvent::ScaleFactorChanged { scale_factor, .. } => {2017 WindowEvent::ScaleFactorChanged { scale_factor, .. } => {
1579 state.renderer.clear_glyph_cache();2018 state.renderer.clear_glyph_cache();
2019 state.app_icon = macos::app_icon((16.0 * scale_factor).round() as u32);
1580 let response = state.view.scale_factor_changed(scale_factor as f32)?;2020 let response = state.view.scale_factor_changed(scale_factor as f32)?;
1581 state.respond(response);2021 state.respond(response);
1582 state.window.request_redraw();2022 state.window.request_redraw();
...@@ -1589,7 +2029,7 @@ impl ApplicationHandler<UserEvent> for App {...@@ -1589,7 +2029,7 @@ impl ApplicationHandler<UserEvent> for App {
1589 trace_input(&("Window occluded", occluded));2029 trace_input(&("Window occluded", occluded));
1590 state.occluded = occluded;2030 state.occluded = occluded;
1591 if !occluded {2031 if !occluded {
1592 state.changed = true;2032 state.moved = true;
1593 state.window.request_redraw();2033 state.window.request_redraw();
1594 }2034 }
1595 }2035 }
...@@ -1654,7 +2094,8 @@ impl ApplicationHandler<UserEvent> for App {...@@ -1654,7 +2094,8 @@ impl ApplicationHandler<UserEvent> for App {
16542094
1655/// Feeds a development script to the window from another thread, one command per line2095/// Feeds a development script to the window from another thread, one command per line
1656/// in logical pixels: `move X Y`, `press`, `release`, `wheel DX DY`, `key NAME`, `type2096/// 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`.
1658fn replay(script: String, proxy: EventLoopProxy<UserEvent>) -> Result<(), Box<dyn Error>> {2099fn replay(script: String, proxy: EventLoopProxy<UserEvent>) -> Result<(), Box<dyn Error>> {
1659 let mut steps = Vec::new();2100 let mut steps = Vec::new();
1660 for line in script.lines().filter(|line| !line.trim().is_empty()) {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,6 +2143,11 @@ fn replay(script: String, proxy: EventLoopProxy<UserEvent>) -> Result<(), Box<dy
1702 ))),2143 ))),
1703 "wait" => Err(std::time::Duration::from_millis(rest.parse()?)),2144 "wait" => Err(std::time::Duration::from_millis(rest.parse()?)),
1704 "snapshot" => Ok(Replay::Snapshot(rest.into())),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 _ => return Err(format!("Unknown replay command: {line}").into()),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,7 +2160,13 @@ fn replay(script: String, proxy: EventLoopProxy<UserEvent>) -> Result<(), Box<dy
1714 }2160 }
1715 let _ = proxy.send_event(UserEvent::Replay(replay));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,6 +2177,7 @@ fn main() -> Result<(), Box<dyn Error>> {
1725 let mut args = std::env::args_os().skip(1);2177 let mut args = std::env::args_os().skip(1);
1726 let mut positional = Vec::new();2178 let mut positional = Vec::new();
1727 let mut substitutes = Vec::new();2179 let mut substitutes = Vec::new();
2180 let mut interface_font = Vec::new();
1728 let mut reference = None;2181 let mut reference = None;
1729 let mut editable = false;2182 let mut editable = false;
1730 let mut section = None;2183 let mut section = None;
...@@ -1736,6 +2189,27 @@ fn main() -> Result<(), Box<dyn Error>> {...@@ -1736,6 +2189,27 @@ fn main() -> Result<(), Box<dyn Error>> {
1736 args.next()2189 args.next()
1737 .ok_or("Provide a font file after --substitute-font.")?,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 } else if arg == "--notebook" {2213 } else if arg == "--notebook" {
1740 notebook = Some(PathBuf::from(2214 notebook = Some(PathBuf::from(
1741 args.next()2215 args.next()
...@@ -1794,7 +2268,7 @@ fn main() -> Result<(), Box<dyn Error>> {...@@ -1794,7 +2268,7 @@ fn main() -> Result<(), Box<dyn Error>> {
1794 }2268 }
1795 if positional.len() > 2 {2269 if positional.len() > 2 {
1796 return Err(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 .into(),2272 .into(),
1799 );2273 );
1800 }2274 }
...@@ -1842,6 +2316,7 @@ fn main() -> Result<(), Box<dyn Error>> {...@@ -1842,6 +2316,7 @@ fn main() -> Result<(), Box<dyn Error>> {
1842 proxy: event_loop.create_proxy(),2316 proxy: event_loop.create_proxy(),
1843 input: Some(input),2317 input: Some(input),
1844 substitutes,2318 substitutes,
2319 interface_font,
1845 state: None,2320 state: None,
1846 startup_error: None,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,7 +11,17 @@ pub(crate) fn solve(nodes: &mut [Built], states: &HashMap<Id, State>, scale: f32
11 node.computed[axis] = match node.size[axis].size {11 node.computed[axis] = match node.size[axis].size {
12 Size::Pixels(pixels) => pixels,12 Size::Pixels(pixels) => pixels,
13 Size::Text => {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 Size::Fraction(_) | Size::Children => 0.0,26 Size::Fraction(_) | Size::Children => 0.0,
17 };27 };
crates/ui/src/lib.rs+348-52
...@@ -4,22 +4,27 @@...@@ -4,22 +4,27 @@
4//! events are routed before building and its layout is solved after.4//! events are routed before building and its layout is solved after.
55
6mod layout;6mod layout;
7pub mod shell;
7mod text;8mod text;
8mod theme;9mod theme;
9mod widgets;10mod widgets;
1011
11pub use theme::Theme;12pub use theme::{Section, Shades, Theme};
12pub use widgets::{button, scrollbar, text_field};13pub use widgets::{button, edit_key, edit_modifiers, scrollbar, text_field};
1314
14use draw::{Primitive, Stroke};15use draw::{
16 PathStyle, Primitive, RasterImage, Stroke,
17 edit::{Clicks, SelectionUnit},
18};
19use parley::editing::Selection;
15use std::{20use std::{
16 collections::HashMap,21 collections::HashMap,
17 hash::{DefaultHasher, Hash, Hasher},22 hash::{DefaultHasher, Hash, Hasher},
18 ops::BitOr,23 ops::BitOr,
19 rc::Rc,24 rc::Rc,
20 time::Instant,25 time::{Duration, Instant},
21};26};
22use text::{Label, Painted, Texts};27use text::{Label, Texts};
23use winit::{28use winit::{
24 event::{Ime, MouseButton},29 event::{Ime, MouseButton},
25 keyboard::{Key, ModifiersState},30 keyboard::{Key, ModifiersState},
...@@ -28,6 +33,11 @@ use winit::{...@@ -28,6 +33,11 @@ use winit::{
2833
29/// Seconds for an animated value to close half of its remaining distance.34/// Seconds for an animated value to close half of its remaining distance.
30const HALF_LIFE: f32 = 0.03;35const HALF_LIFE: f32 = 0.03;
36/// How far a box's shadow spreads, in logical pixels.
37const SHADOW: f32 = 3.0;
38/// Logical size of a box's icon, and its distance from the label.
39const ICON: f32 = 16.0;
40const ICON_GAP: f32 = 6.0;
3141
32/// A box's identity across frames: its parent's id combined with a builder-chosen part.42/// A box's identity across frames: its parent's id combined with a builder-chosen part.
33#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]43#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
...@@ -139,6 +149,18 @@ impl BitOr for Flags {...@@ -139,6 +149,18 @@ impl BitOr for Flags {
139 }149 }
140}150}
141151
152/// How a box's fill and border are outlined; `radius` rounds the named corners.
153#[derive(Clone, Copy, Debug, Default, PartialEq)]
154pub 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/// What a box is this frame. Colours are linear RGBA.164/// What a box is this frame. Colours are linear RGBA.
143#[derive(Clone, Default)]165#[derive(Clone, Default)]
144pub struct Spec<'a> {166pub struct Spec<'a> {
...@@ -150,11 +172,20 @@ pub struct Spec<'a> {...@@ -150,11 +172,20 @@ pub struct Spec<'a> {
150 /// The label's colour; the theme's text colour otherwise.172 /// The label's colour; the theme's text colour otherwise.
151 pub color: Option<[f32; 4]>,173 pub color: Option<[f32; 4]>,
152 pub fill: Option<[f32; 4]>,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 /// The fill under the pointer, blended in as hover animates.179 /// The fill under the pointer, blended in as hover animates.
154 pub hover_fill: Option<[f32; 4]>,180 pub hover_fill: Option<[f32; 4]>,
155 pub border: Option<[f32; 4]>,181 pub border: Option<[f32; 4]>,
156 pub hover_border: Option<[f32; 4]>,182 pub hover_border: Option<[f32; 4]>,
157 pub radius: f32,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 /// Inset of the label and children on each axis.189 /// Inset of the label and children on each axis.
159 pub pad: [f32; 2],190 pub pad: [f32; 2],
160 /// Space between children along the flow.191 /// Space between children along the flow.
...@@ -190,6 +221,8 @@ pub struct Signal {...@@ -190,6 +221,8 @@ pub struct Signal {
190 pub hovered: bool,221 pub hovered: bool,
191 /// The primary button went down on the box.222 /// The primary button went down on the box.
192 pub pressed: bool,223 pub pressed: bool,
224 /// What the press selects text by: a single, double or triple click.
225 pub unit: SelectionUnit,
193 /// The primary button came up over the box it went down on.226 /// The primary button came up over the box it went down on.
194 pub clicked: bool,227 pub clicked: bool,
195 /// The primary button is held after pressing the box.228 /// The primary button is held after pressing the box.
...@@ -224,10 +257,15 @@ struct Built {...@@ -224,10 +257,15 @@ struct Built {
224 label: Option<Rc<Label>>,257 label: Option<Rc<Label>>,
225 color: [f32; 4],258 color: [f32; 4],
226 fill: Option<[f32; 4]>,259 fill: Option<[f32; 4]>,
260 gradient: Option<[f32; 4]>,
261 shadow: Option<[f32; 4]>,
227 hover_fill: Option<[f32; 4]>,262 hover_fill: Option<[f32; 4]>,
228 border: Option<[f32; 4]>,263 border: Option<[f32; 4]>,
229 hover_border: Option<[f32; 4]>,264 hover_border: Option<[f32; 4]>,
230 radius: f32,265 radius: f32,
266 shape: Shape,
267 icon: Option<&'static [&'static str]>,
268 image: Option<RasterImage>,
231 pad: [f32; 2],269 pad: [f32; 2],
232 gap: f32,270 gap: f32,
233 center: bool,271 center: bool,
...@@ -245,17 +283,20 @@ struct Built {...@@ -245,17 +283,20 @@ struct Built {
245struct State {283struct State {
246 touched: u64,284 touched: u64,
247 hot: f32,285 hot: f32,
248 active: f32,286 /// The box paints differently under the pointer, so its hover is worth easing.
287 hovers: bool,
249 scroll: f32,288 scroll: f32,
250 scroll_target: f32,289 scroll_target: f32,
251 /// Height of a scrolling box's children last frame.290 /// Height of a scrolling box's children last frame.
252 content: f32,291 content: f32,
253 rect: [f32; 4],292 rect: [f32; 4],
254 /// A text field's caret and selection anchor, in bytes.293 /// A text field's selection, and the selection and unit of the press a drag extends.
255 caret: usize,294 selection: Selection,
256 mark: usize,295 press: (Selection, SelectionUnit),
257 /// Where a dragged scrollbar thumb was taken, from its start.296 /// Where a dragged scrollbar thumb was taken, from its start.
258 grab: f32,297 grab: f32,
298 /// An animated value and its target, for `Ui::animate`.
299 tween: Option<[f32; 2]>,
259}300}
260301
261struct Hit {302struct Hit {
...@@ -270,13 +311,39 @@ enum Display {...@@ -270,13 +311,39 @@ enum Display {
270 Rect {311 Rect {
271 rect: [f32; 4],312 rect: [f32; 4],
272 fill: [f32; 4],313 fill: [f32; 4],
314 /// The fill's colour at the bottom.
315 shade: Option<[f32; 4]>,
273 border: Option<[f32; 4]>,316 border: Option<[f32; 4]>,
274 radius: f32,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 Text {342 Text {
277 painted: usize,343 label: Rc<Label>,
278 origin: [f32; 2],344 origin: [f32; 2],
279 clip: [f32; 4],345 clip: [f32; 4],
346 color: [f32; 4],
280 },347 },
281 Custom {348 Custom {
282 id: Id,349 id: Id,
...@@ -302,13 +369,14 @@ pub struct Ui {...@@ -302,13 +369,14 @@ pub struct Ui {
302 active: Option<Id>,369 active: Option<Id>,
303 focus: Option<Id>,370 focus: Option<Id>,
304 modifiers: ModifiersState,371 modifiers: ModifiersState,
372 clicks: Clicks,
305 display: Vec<Display>,373 display: Vec<Display>,
306 painted: Vec<Painted>,
307 animating: bool,374 animating: bool,
308}375}
309376
310impl Ui {377impl 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 Self {380 Self {
313 theme,381 theme,
314 frame: 0,382 frame: 0,
...@@ -326,8 +394,8 @@ impl Ui {...@@ -326,8 +394,8 @@ impl Ui {
326 active: None,394 active: None,
327 focus: None,395 focus: None,
328 modifiers: ModifiersState::empty(),396 modifiers: ModifiersState::empty(),
397 clicks: Clicks::new(double_click),
329 display: Vec::new(),398 display: Vec::new(),
330 painted: Vec::new(),
331 animating: false,399 animating: false,
332 }400 }
333 }401 }
...@@ -412,7 +480,7 @@ impl Ui {...@@ -412,7 +480,7 @@ impl Ui {
412 for event in std::mem::take(&mut self.queue) {480 for event in std::mem::take(&mut self.queue) {
413 self.route(event);481 self.route(event);
414 }482 }
415 self.animate(dt);483 self.ease(dt);
416 }484 }
417485
418 fn route(&mut self, event: Event) {486 fn route(&mut self, event: Event) {
...@@ -438,7 +506,7 @@ impl Ui {...@@ -438,7 +506,7 @@ impl Ui {
438 Event::Button {506 Event::Button {
439 button,507 button,
440 pressed: true,508 pressed: true,
441 ..509 at,
442 } => {510 } => {
443 let Some(point) = self.pointer else {511 let Some(point) = self.pointer else {
444 return;512 return;
...@@ -448,7 +516,9 @@ impl Ui {...@@ -448,7 +516,9 @@ impl Ui {
448 && button == MouseButton::Left516 && button == MouseButton::Left
449 {517 {
450 self.active = Some(id);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 if self.hit_flags(id).contains(Flags::FOCUSABLE) {522 if self.hit_flags(id).contains(Flags::FOCUSABLE) {
453 self.focus = Some(id);523 self.focus = Some(id);
454 }524 }
...@@ -547,21 +617,23 @@ impl Ui {...@@ -547,21 +617,23 @@ impl Ui {
547 .map(|hit| hit.id)617 .map(|hit| hit.id)
548 }618 }
549619
550 fn animate(&mut self, dt: f32) {620 fn ease(&mut self, dt: f32) {
551 let rate = 1.0 - 0.5_f32.powf(dt / HALF_LIFE);621 let rate = 1.0 - 0.5_f32.powf(dt / HALF_LIFE);
552 let mut animating = false;622 let mut animating = false;
553 for (id, state) in &mut self.states {623 for (id, state) in &mut self.states {
554 let hot = self.hover == Some(*id) && self.active.is_none_or(|active| active == *id);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 let targets = [628 let targets = [
556 (&mut state.hot, f32::from(u8::from(hot)), 0.002),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 (&mut state.scroll, state.scroll_target, 0.25),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 let delta = target - *value;637 let delta = target - *value;
566 if delta.abs() < close {638 if delta.abs() < close {
567 *value = target;639 *value = target;
...@@ -617,6 +689,29 @@ impl Ui {...@@ -617,6 +689,29 @@ impl Ui {
617 self.signal(id)689 self.signal(id)
618 }690 }
619691
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 /// Paints `color` over the current box at `rect`, relative to its corner.715 /// Paints `color` over the current box at `rect`, relative to its corner.
621 pub fn mark(&mut self, rect: [f32; 4], color: [f32; 4]) {716 pub fn mark(&mut self, rect: [f32; 4], color: [f32; 4]) {
622 let index = *self.stack.last().unwrap();717 let index = *self.stack.last().unwrap();
...@@ -639,6 +734,7 @@ impl Ui {...@@ -639,6 +734,7 @@ impl Ui {
639 for node in &self.nodes {734 for node in &self.nodes {
640 let state = self.states.entry(node.id).or_default();735 let state = self.states.entry(node.id).or_default();
641 state.rect = node.rect;736 state.rect = node.rect;
737 state.hovers = node.hover_fill.is_some() || node.hover_border.is_some();
642 if node.flags.contains(Flags::SCROLL) {738 if node.flags.contains(Flags::SCROLL) {
643 state.content = node.content;739 state.content = node.content;
644 let most = (node.content - (node.rect[3] - node.rect[1])).max(0.0);740 let most = (node.content - (node.rect[3] - node.rect[1])).max(0.0);
...@@ -650,7 +746,6 @@ impl Ui {...@@ -650,7 +746,6 @@ impl Ui {
650 .retain(|id, state| state.touched == self.frame || *id == Id::ROOT);746 .retain(|id, state| state.touched == self.frame || *id == Id::ROOT);
651 self.texts.prune(self.frame);747 self.texts.prune(self.frame);
652 self.display.clear();748 self.display.clear();
653 self.painted.clear();
654 self.hits.clear();749 self.hits.clear();
655 self.paint(0, None);750 self.paint(0, None);
656 for id in [&mut self.hover, &mut self.active, &mut self.focus] {751 for id in [&mut self.hover, &mut self.active, &mut self.focus] {
...@@ -665,20 +760,59 @@ impl Ui {...@@ -665,20 +760,59 @@ impl Ui {
665 let state = &self.states[&node.id];760 let state = &self.states[&node.id];
666 let rect = node.rect;761 let rect = node.rect;
667 let visible = clip.map_or(Some(rect), |clip| intersect(rect, clip));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 let blend = |base: Option<[f32; 4]>, hover: Option<[f32; 4]>| match (base, hover) {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 (base, None) => base,771 (base, None) => base,
671 };772 };
672 let fill = blend(node.fill, node.hover_fill);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 let border = blend(node.border, node.hover_border);781 let border = blend(node.border, node.hover_border);
674 if fill.is_some() || border.is_some() {782 let size = [rect[2] - rect[0], rect[3] - rect[1]];
675 self.display.push(Display::Rect {783 if let Some(shadow) = node.shadow {
676 rect,784 self.display.push(Display::Path {
677 fill: fill.unwrap_or([0.0; 4]),785 data: outline(node.shape, size, node.radius),
678 border,786 origin: [rect[0], rect[1]],
679 radius: node.radius,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 for (mark, color) in &node.marks {816 for (mark, color) in &node.marks {
683 self.display.push(Display::Rect {817 self.display.push(Display::Rect {
684 rect: [818 rect: [
...@@ -688,30 +822,43 @@ impl Ui {...@@ -688,30 +822,43 @@ impl Ui {
688 rect[1] + mark[3],822 rect[1] + mark[3],
689 ],823 ],
690 fill: *color,824 fill: *color,
825 shade: None,
691 border: None,826 border: None,
692 radius: 0.0,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 if let Some(label) = &node.label {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 let y = (rect[1] + rect[3] - label.size[1]) / 2.0;857 let y = (rect[1] + rect[3] - label.size[1]) / 2.0;
708 self.display.push(Display::Text {858 self.display.push(Display::Text {
709 painted: self.painted.len(),859 label: label.clone(),
710 origin: [x, y],860 origin: [x, y],
711 clip: inner,861 clip: inner,
712 });
713 self.painted.push(Painted {
714 label: label.clone(),
715 color: node.color,862 color: node.color,
716 });863 });
717 }864 }
...@@ -768,10 +915,17 @@ impl Ui {...@@ -768,10 +915,17 @@ impl Ui {
768 Display::Rect {915 Display::Rect {
769 rect,916 rect,
770 fill,917 fill,
918 shade,
771 border,919 border,
772 radius,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 primitives.push(if *radius > 0.0 {929 primitives.push(if *radius > 0.0 {
776 Primitive::RoundedRect {930 Primitive::RoundedRect {
777 rect: *rect,931 rect: *rect,
...@@ -795,14 +949,47 @@ impl Ui {...@@ -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 Display::Text {983 Display::Text {
799 painted,984 label,
800 origin,985 origin,
801 clip,986 clip,
987 color,
802 } => primitives.push(Primitive::Text {988 } => primitives.push(Primitive::Text {
803 text: &self.painted[*painted],989 text: &**label,
804 origin: *origin,990 origin: *origin,
805 clip: Some(*clip),991 clip: Some(*clip),
992 ink: *color,
806 }),993 }),
807 Display::Custom { id, rect } => {994 Display::Custom { id, rect } => {
808 if !primitives.is_empty() {995 if !primitives.is_empty() {
...@@ -828,9 +1015,9 @@ impl Ui {...@@ -828,9 +1015,9 @@ impl Ui {
828 (&mut self.texts, self.frame)1015 (&mut self.texts, self.frame)
829 }1016 }
8301017
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 let state = self.states.entry(id).or_default();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 }
8351022
836 pub(crate) fn grab(&mut self, id: Id) -> &mut f32 {1023 pub(crate) fn grab(&mut self, id: Id) -> &mut f32 {
...@@ -839,6 +1026,16 @@ impl Ui {...@@ -839,6 +1026,16 @@ impl Ui {
839}1026}
8401027
841impl Built {1028impl 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 fn new(1039 fn new(
843 id: Id,1040 id: Id,
844 parent: usize,1041 parent: usize,
...@@ -856,10 +1053,15 @@ impl Built {...@@ -856,10 +1053,15 @@ impl Built {
856 label,1053 label,
857 color: spec.color.unwrap_or(text),1054 color: spec.color.unwrap_or(text),
858 fill: spec.fill,1055 fill: spec.fill,
1056 gradient: spec.gradient,
1057 shadow: spec.shadow,
859 hover_fill: spec.hover_fill,1058 hover_fill: spec.hover_fill,
860 border: spec.border,1059 border: spec.border,
861 hover_border: spec.hover_border,1060 hover_border: spec.hover_border,
862 radius: spec.radius,1061 radius: spec.radius,
1062 shape: spec.shape,
1063 icon: spec.icon,
1064 image: spec.image.cloned(),
863 pad: spec.pad,1065 pad: spec.pad,
864 gap: spec.gap,1066 gap: spec.gap,
865 center: spec.center,1067 center: spec.center,
...@@ -874,7 +1076,101 @@ impl Built {...@@ -874,7 +1076,101 @@ impl Built {
874 }1076 }
875}1077}
8761078
877fn 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.
1081fn 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.
1121struct 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
1130impl 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`.
1173pub fn mix(a: [f32; 4], b: [f32; 4], t: f32) -> [f32; 4] {
878 std::array::from_fn(|i| a[i] + (b[i] - a[i]) * t)1174 std::array::from_fn(|i| a[i] + (b[i] - a[i]) * t)
879}1175}
8801176
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
4use draw::PathStyle;
5
6use crate::{
7 Corner, Display, Flags, Id, Section, Shape, Signal, Spec, Ui, children, fill, mix, px,
8};
9use std::hash::Hash;
10
11/// A menu's downward arrow.
12pub const CHEVRON: &[&str] = &[include_str!("../assets/chevron-down.svg")];
13/// Rounding of a section tab's top leading corner; the trailing one takes half.
14const TAB_ROUNDING: f32 = 4.0;
15/// Side of a toolbar button.
16pub 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`.
22pub 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.
94pub 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.
99pub 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.
123pub 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`.
174pub 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
211impl 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,6 +2,8 @@ use super::*;
2use std::time::Duration;2use std::time::Duration;
3use winit::keyboard::NamedKey;3use winit::keyboard::NamedKey;
44
5const DOUBLE_CLICK: Duration = Duration::from_millis(500);
6
5thread_local! {7thread_local! {
6 static START: Instant = Instant::now();8 static START: Instant = Instant::now();
7 static FRAMES: std::cell::Cell<u32> = const { std::cell::Cell::new(0) };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,7 +23,7 @@ fn frame(ui: &mut Ui, build: impl FnOnce(&mut Ui)) {
2123
22#[test]24#[test]
23fn fill_yields_to_strict_siblings_along_the_flow() {25fn 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 let mut ids = Vec::new();27 let mut ids = Vec::new();
26 frame(&mut ui, |ui| {28 frame(&mut ui, |ui| {
27 ui.open(29 ui.open(
...@@ -55,7 +57,7 @@ fn fill_yields_to_strict_siblings_along_the_flow() {...@@ -55,7 +57,7 @@ fn fill_yields_to_strict_siblings_along_the_flow() {
5557
56#[test]58#[test]
57fn children_sum_with_gaps_and_padding() {59fn 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 let mut column = None;61 let mut column = None;
60 let mut second = None;62 let mut second = None;
61 frame(&mut ui, |ui| {63 frame(&mut ui, |ui| {
...@@ -107,7 +109,7 @@ fn button_frame(ui: &mut Ui) -> Signal {...@@ -107,7 +109,7 @@ fn button_frame(ui: &mut Ui) -> Signal {
107109
108#[test]110#[test]
109fn clicks_use_the_previous_layout_and_hover_animates() {111fn 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 button_frame(&mut ui);113 button_frame(&mut ui);
112 let at = Instant::now();114 let at = Instant::now();
113 ui.event(Event::PointerMoved([10.0, 10.0]));115 ui.event(Event::PointerMoved([10.0, 10.0]));
...@@ -147,9 +149,28 @@ fn clicks_use_the_previous_layout_and_hover_animates() {...@@ -147,9 +149,28 @@ fn clicks_use_the_previous_layout_and_hover_animates() {
147 assert!(!ui.wants_frame());149 assert!(!ui.wants_frame());
148}150}
149151
152#[test]
153fn 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#[test]171#[test]
151fn custom_boxes_receive_their_events_and_a_leave_when_the_pointer_moves_off() {172fn 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 let build = |ui: &mut Ui| {174 let build = |ui: &mut Ui| {
154 let mut signals = Vec::new();175 let mut signals = Vec::new();
155 frame(ui, |ui| {176 frame(ui, |ui| {
...@@ -225,7 +246,7 @@ fn custom_boxes_receive_their_events_and_a_leave_when_the_pointer_moves_off() {...@@ -225,7 +246,7 @@ fn custom_boxes_receive_their_events_and_a_leave_when_the_pointer_moves_off() {
225246
226#[test]247#[test]
227fn wheel_scrolls_within_the_content_and_clips_children() {248fn 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 let mut list = None;250 let mut list = None;
230 let mut last = None;251 let mut last = None;
231 let mut build = |ui: &mut Ui| {252 let mut build = |ui: &mut Ui| {
...@@ -271,69 +292,192 @@ fn wheel_scrolls_within_the_content_and_clips_children() {...@@ -271,69 +292,192 @@ fn wheel_scrolls_within_the_content_and_clips_children() {
271 ));292 ));
272}293}
273294
274#[test]295fn field_frame(ui: &mut Ui, text: &mut String) -> Signal {
275fn text_fields_edit_with_keys_and_selection() {296 let mut signal = Signal::default();
276 let mut ui = Ui::new(Theme::dark());297 frame(ui, |ui| {
277 let mut text = String::from("café");298 signal = text_field(
278 let build = |ui: &mut Ui, text: &mut String| {299 ui,
279 let mut signal = Signal::default();300 field(),
280 frame(ui, |ui| {301 text,
281 signal = text_field(302 "Filter",
282 ui,303 Spec {
283 Id::ROOT.child("filter"),304 size: [px(200.0), px(26.0)],
284 text,305 pad: [6.0, 0.0],
285 "Filter",306 ..Spec::default()
286 Spec {307 },
287 size: [px(200.0), px(26.0)],308 );
288 pad: [6.0, 0.0],309 });
289 ..Spec::default()310 signal
290 },311}
291 );312
292 });313fn field() -> Id {
293 signal314 Id::ROOT.child("filter")
294 };315}
295 build(&mut ui, &mut text);316
296 let id = Id::ROOT.child("filter");317fn key(named: NamedKey) -> Event {
297 ui.set_focus(Some(id));318 Event::Key {
298 let key = |named| Event::Key {
299 key: Key::Named(named),319 key: Key::Named(named),
300 text: None,320 text: None,
301 };321 }
302 ui.event(Event::Key {322}
303 key: Key::Named(NamedKey::End),323
304 text: None,324fn 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`.
332fn 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`.
341fn 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
351fn 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]
360fn text_fields_edit_with_keys_and_selection() {
361 let (mut ui, mut text) = focused_field("café");
362 ui.event(key(NamedKey::End));
306 ui.event(key(NamedKey::Backspace));363 ui.event(key(NamedKey::Backspace));
307 ui.event(Event::Key {364 ui.event(typed("e"));
308 key: Key::Character("e".into()),365 field_frame(&mut ui, &mut text);
309 text: Some("e".into()),
310 });
311 build(&mut ui, &mut text);
312 assert_eq!(text, "cafe");366 assert_eq!(text, "cafe");
313 ui.event(Event::Modifiers(ModifiersState::SHIFT));367 ui.event(Event::Modifiers(ModifiersState::SHIFT));
314 ui.event(key(NamedKey::ArrowLeft));368 ui.event(key(NamedKey::ArrowLeft));
315 ui.event(key(NamedKey::ArrowLeft));369 ui.event(key(NamedKey::ArrowLeft));
316 build(&mut ui, &mut text);370 field_frame(&mut ui, &mut text);
317 ui.event(Event::Modifiers(ModifiersState::empty()));371 ui.event(Event::Modifiers(ModifiersState::empty()));
318 ui.event(Event::Ime(Ime::Commit("é".into())));372 ui.event(Event::Ime(Ime::Commit("é".into())));
319 build(&mut ui, &mut text);373 field_frame(&mut ui, &mut text);
320 assert_eq!(text, "caé");374 assert_eq!(text, "caé");
321 ui.event(Event::Modifiers(ModifiersState::SUPER));375 ui.event(Event::Modifiers(ModifiersState::SUPER));
322 ui.event(Event::Key {376 ui.event(typed("a"));
323 key: Key::Character("a".into()),377 field_frame(&mut ui, &mut text);
324 text: Some("a".into()),
325 });
326 build(&mut ui, &mut text);
327 ui.event(Event::Modifiers(ModifiersState::empty()));378 ui.event(Event::Modifiers(ModifiersState::empty()));
328 ui.event(key(NamedKey::Delete));379 ui.event(key(NamedKey::Delete));
329 let signal = build(&mut ui, &mut text);380 let signal = field_frame(&mut ui, &mut text);
330 assert!(signal.focused);381 assert!(signal.focused);
331 assert_eq!(text, "");382 assert_eq!(text, "");
332}383}
333384
385#[test]
386fn 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]
405fn 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]
424fn 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]
450fn 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#[test]478#[test]
335fn scrollbar_thumbs_track_the_offset_and_drags_reach_both_ends() {479fn 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 let mut offset = 0.0;481 let mut offset = 0.0;
338 let build = |ui: &mut Ui, offset: &mut f32| {482 let build = |ui: &mut Ui, offset: &mut f32| {
339 frame(ui, |ui| {483 frame(ui, |ui| {
crates/ui/src/text.rs+30-30
...@@ -1,35 +1,23 @@...@@ -1,35 +1,23 @@
1use draw::{GlyphRun, Glyphs, RenderError};1use draw::{GlyphRun, Glyphs, RenderError};
2use parley::{2use parley::{
3 Affinity, FontContext, FontFamily, GenericFamily, Layout, LayoutContext, PositionedLayoutItem,3 FontContext, FontFamily, FontFamilyName, GenericFamily, Layout, LayoutContext,
4 StyleProperty, editing::Cursor,4 PositionedLayoutItem, StyleProperty, fontique::Blob,
5};5};
6use std::{collections::HashMap, rc::Rc};6use std::{borrow::Cow, collections::HashMap, rc::Rc, sync::Arc};
77
8/// A shaped single line of interface text, in logical pixels.8/// A shaped single line of interface text, in logical pixels.
9pub(crate) struct Label {9pub(crate) struct Label {
10 layout: Layout<()>,10 pub layout: Layout<()>,
11 pub size: [f32; 2],11 pub size: [f32; 2],
12}12}
1313
14impl 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/// Labels shaped this frame or the previous one, keyed by their text and size.14/// Labels shaped this frame or the previous one, keyed by their text and size.
29#[derive(Default)]15#[derive(Default)]
30pub(crate) struct Texts {16pub(crate) struct Texts {
31 fonts: FontContext,17 fonts: FontContext,
32 context: LayoutContext<()>,18 context: LayoutContext<()>,
19 /// A family registered in place of the system's interface font.
20 family: Option<String>,
33 cache: HashMap<(String, u32), (Rc<Label>, u64)>,21 cache: HashMap<(String, u32), (Rc<Label>, u64)>,
34}22}
3523
...@@ -44,9 +32,13 @@ impl Texts {...@@ -44,9 +32,13 @@ impl Texts {
44 let mut builder = self32 let mut builder = self
45 .context33 .context
46 .ranged_builder(&mut self.fonts, text, 1.0, false);34 .ranged_builder(&mut self.fonts, text, 1.0, false);
47 builder.push_default(StyleProperty::FontFamily(FontFamily::from(35 let system = FontFamilyName::Generic(GenericFamily::SystemUi);
48 GenericFamily::SystemUi,36 builder.push_default(StyleProperty::FontFamily(match &self.family {
49 )));37 Some(family) => {
38 FontFamily::List(Cow::Owned(vec![FontFamilyName::named(family), system]))
39 }
40 None => FontFamily::Single(system),
41 }));
50 builder.push_default(StyleProperty::FontSize(size));42 builder.push_default(StyleProperty::FontSize(size));
51 let mut layout = builder.build(text);43 let mut layout = builder.build(text);
52 layout.break_all_lines(None);44 layout.break_all_lines(None);
...@@ -56,23 +48,31 @@ impl Texts {...@@ -56,23 +48,31 @@ impl Texts {
56 label48 label
57 }49 }
5850
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 pub fn prune(&mut self, frame: u64) {65 pub fn prune(&mut self, frame: u64) {
60 self.cache.retain(|_, (_, touched)| *touched + 1 >= frame);66 self.cache.retain(|_, (_, touched)| *touched + 1 >= frame);
61 }67 }
62}68}
6369
64/// A label painted in one colour this frame.70impl Glyphs for Label {
65pub(crate) struct Painted {
66 pub label: Rc<Label>,
67 pub color: [f32; 4],
68}
69
70impl Glyphs for Painted {
71 fn runs(71 fn runs(
72 &self,72 &self,
73 paint: &mut dyn FnMut(GlyphRun<'_>) -> Result<(), RenderError>,73 paint: &mut dyn FnMut(GlyphRun<'_>) -> Result<(), RenderError>,
74 ) -> Result<(), RenderError> {74 ) -> Result<(), RenderError> {
75 for line in self.label.layout.lines() {75 for line in self.layout.lines() {
76 let metrics = line.metrics();76 let metrics = line.metrics();
77 for item in line.items() {77 for item in line.items() {
78 if let PositionedLayoutItem::GlyphRun(run) = item {78 if let PositionedLayoutItem::GlyphRun(run) = item {
...@@ -81,7 +81,7 @@ impl Glyphs for Painted {...@@ -81,7 +81,7 @@ impl Glyphs for Painted {
81 metrics.baseline,81 metrics.baseline,
82 0.0,82 0.0,
83 [metrics.block_min_coord, metrics.line_height],83 [metrics.block_min_coord, metrics.line_height],
84 |_| self.color,84 |_| None,
85 paint,85 paint,
86 )?;86 )?;
87 }87 }
crates/ui/src/theme.rs+137-9
...@@ -4,21 +4,47 @@ use draw::srgb;...@@ -4,21 +4,47 @@ use draw::srgb;
4#[derive(Clone, Debug)]4#[derive(Clone, Debug)]
5pub struct Theme {5pub struct Theme {
6 pub font_size: f32,6 pub font_size: f32,
7 /// Content surfaces, such as the active tab and lists.7 /// Content surfaces, such as fields and lists.
8 pub base: [f32; 4],8 pub base: [f32; 4],
9 /// Side panels.9 /// Side panels.
10 pub panel: [f32; 4],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 pub strip: [f32; 4],12 pub strip: [f32; 4],
13 /// Hairlines between rows and panels.
14 pub separator: [f32; 4],
15 pub accent: [f32; 4],13 pub accent: [f32; 4],
16 /// A hovered row under the pointer, outlined in the accent.
17 pub hover: [f32; 4],
18 pub text: [f32; 4],14 pub text: [f32; 4],
19 pub text_dim: [f32; 4],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 pub chip: [f32; 4],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)]
30pub 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)]
40pub 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}
2349
24impl Theme {50impl Theme {
...@@ -29,12 +55,114 @@ impl Theme {...@@ -29,12 +55,114 @@ impl Theme {
29 base: srgb(0x19, 0x1b, 0x1c),55 base: srgb(0x19, 0x1b, 0x1c),
30 panel: srgb(0x1f, 0x22, 0x23),56 panel: srgb(0x1f, 0x22, 0x23),
31 strip: srgb(0x27, 0x2a, 0x2b),57 strip: srgb(0x27, 0x2a, 0x2b),
32 separator: srgb(0x20, 0x23, 0x24),
33 accent: srgb(0x00, 0x79, 0xa6),58 accent: srgb(0x00, 0x79, 0xa6),
34 hover: srgb(0x10, 0x3c, 0x4c),
35 text: srgb(0xdd, 0xde, 0xe0),59 text: srgb(0xdd, 0xde, 0xe0),
36 text_dim: srgb(0x6b, 0x70, 0x78),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 chip: srgb(0x38, 0x3c, 0x3d),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.
121fn 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.
145fn 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)]
156mod 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,8 +1,13 @@
1use crate::{Axis, Event, Flags, Id, Signal, Spec, Ui, fit, px};1use crate::{Axis, Event, Flags, Id, Signal, Spec, Ui, fit, px};
2use draw::edit::{self, Command, Movement};
3use parley::{
4 Affinity,
5 editing::{Cursor, Selection},
6};
2use std::hash::Hash;7use std::hash::Hash;
3use winit::{8use winit::{
4 event::Ime,9 event::Ime,
5 keyboard::{Key, NamedKey},10 keyboard::{Key, ModifiersState, NamedKey},
6 window::CursorIcon,11 window::CursorIcon,
7};12};
813
...@@ -14,7 +19,7 @@ pub fn button(ui: &mut Ui, part: impl Hash, text: &str) -> Signal {...@@ -14,7 +19,7 @@ pub fn button(ui: &mut Ui, part: impl Hash, text: &str) -> Signal {
14 size: [fit(), px(theme.font_size * 2.0)],19 size: [fit(), px(theme.font_size * 2.0)],
15 text: Some(text),20 text: Some(text),
16 fill: Some(theme.chip),21 fill: Some(theme.chip),
17 hover_fill: Some(theme.hover),22 hover_fill: Some(theme.hover()),
18 hover_border: Some(theme.accent),23 hover_border: Some(theme.accent),
19 radius: 4.0,24 radius: 4.0,
20 pad: [theme.font_size * 0.75, 0.0],25 pad: [theme.font_size * 0.75, 0.0],
...@@ -82,9 +87,9 @@ pub fn scrollbar(...@@ -82,9 +87,9 @@ pub fn scrollbar(
82 chosen87 chosen
83}88}
8489
85/// A single-line field editing `text`, showing `placeholder` while empty. Typing,90/// A single-line field editing `text`, showing `placeholder` while empty. Presses, drags
86/// Backspace/Delete, Left/Right with Shift to select, Home/End, Command-A and input91/// and keys select and edit as on the page: a double press selects a word, a triple press
87/// method commits edit it; a press places the caret.92/// everything, and dragging extends by that unit.
88pub fn text_field(93pub fn text_field(
89 ui: &mut Ui,94 ui: &mut Ui,
90 id: Id,95 id: Id,
...@@ -95,94 +100,114 @@ pub fn text_field(...@@ -95,94 +100,114 @@ pub fn text_field(
95 let signal = ui.signal(id);100 let signal = ui.signal(id);
96 let pad = spec.pad[0];101 let pad = spec.pad[0];
97 let size = ui.theme.font_size;102 let size = ui.theme.font_size;
98 let clicked_at = signal103 let modifiers = edit_modifiers(ui.modifiers());
99 .pressed104 let pointer = ui
100 .then(|| ui.pointer().zip(ui.rect(id)))105 .pointer()
101 .flatten()106 .zip(ui.rect(id))
102 .map(|(pointer, rect)| pointer[0] - rect[0] - pad);107 .map(|(pointer, rect)| pointer[0] - rect[0] - pad)
103 let command = ui.modifiers().super_key() || ui.modifiers().control_key();108 .filter(|_| signal.pressed || signal.dragging);
104 let shift = ui.modifiers().shift_key();109 let (mut selection, mut press) = {
105 let (mut caret, mut mark) = {110 let (selection, press) = ui.field(id);
106 let (caret, mark) = ui.caret(id);111 (*selection, *press)
107 (*caret, *mark)
108 };112 };
109 caret = floor_boundary(text, caret);113 let (texts, frame) = ui.texts();
110 mark = floor_boundary(text, mark);114 let mut label = texts.label(text, size, frame);
111 if let Some(x) = clicked_at {115 if let Some(x) = pointer {
112 let (texts, frame) = ui.texts();116 let layout = &label.layout;
113 caret = texts.label(text, size, frame).index_at(x);117 let unit = if signal.pressed { signal.unit } else { press.1 };
114 mark = caret;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 for event in &signal.events {139 for event in &signal.events {
117 let selection = caret.min(mark)..caret.max(mark);140 let layout = &label.layout;
118 let insert = |text: &mut String, inserted: &str, caret: &mut usize| {141 let (range, inserted) = match event {
119 text.replace_range(selection.clone(), inserted);142 Event::Ime(Ime::Commit(committed)) => (selection.text_range(), committed.as_str()),
120 *caret = selection.start + inserted.len();143 Event::Key { key, text: typed } => {
121 };144 let key = edit_key(key);
122 match event {145 // One line has no break for Control-K to join, nor room for Home and End to scroll.
123 Event::Ime(Ime::Commit(committed)) => {146 let command = match (Command::from_key(&key, modifiers), &key) {
124 insert(text, committed, &mut caret);147 (Some(Command::Kill), _) => Some(Command::DeleteTo(Movement::LineEnd)),
125 mark = caret;148 (None, edit::Key::Named(edit::NamedKey::Home)) => {
126 }149 Some(Command::Move(Movement::DocumentStart))
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);
139 }150 }
140 mark = caret;151 (None, edit::Key::Named(edit::NamedKey::End)) => {
141 }152 Some(Command::Move(Movement::DocumentEnd))
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;
152 }153 }
153 }154 (command, _) => command,
154 Key::Named(NamedKey::Home | NamedKey::End) => {155 };
155 caret = if matches!(key, Key::Named(NamedKey::Home)) {156 let collapsed = selection.is_collapsed();
156 0157 match (command, key) {
157 } else {158 (Some(Command::Move(movement)), _) => {
158 text.len()159 selection = edit::step(layout, selection, movement, modifiers.shift);
159 };160 continue;
160 if !shift {
161 mark = caret;
162 }161 }
163 }162 (Some(Command::DeleteTo(movement)), _) if collapsed => (
164 Key::Character(character) if command => {163 edit::step(layout, selection, movement, true).text_range(),
165 if character.eq_ignore_ascii_case("a") {164 "",
166 mark = 0;165 ),
167 caret = text.len();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 }176 (Some(_), _) => (selection.text_range(), ""),
170 _ => {177 (None, edit::Key::Character(character))
171 if let Some(typed) = typed.as_deref().filter(|typed| {178 if modifiers.command && character.eq_ignore_ascii_case("a") =>
172 !command && !typed.is_empty() && !typed.chars().any(char::is_control)179 {
173 }) {180 selection = Selection::new(
174 insert(text, typed, &mut caret);181 Cursor::from_byte_index(layout, 0, Affinity::Downstream),
175 mark = caret;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 },196 }
179 _ => {}197 _ => continue,
180 }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);208 let (stored, stored_press) = ui.field(id);
184 *stored_caret = caret;209 *stored = selection;
185 *stored_mark = mark;210 *stored_press = press;
186 }211 }
187 let theme = ui.theme.clone();212 let theme = ui.theme.clone();
188 let shown = if text.is_empty() {213 let shown = if text.is_empty() {
...@@ -206,43 +231,59 @@ pub fn text_field(...@@ -206,43 +231,59 @@ pub fn text_field(
206 },231 },
207 );232 );
208 if signal.focused {233 if signal.focused {
209 let (texts, frame) = ui.texts();
210 let label = texts.label(text, size, frame);
211 let line = label.size[1];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 let top = match height.size {235 let top = match height.size {
215 crate::Size::Pixels(height) => (height - line) / 2.0,236 crate::Size::Pixels(height) => (height - line) / 2.0,
216 _ => 0.0,237 _ => 0.0,
217 };238 };
218 if end > start {239 for (rect, _) in selection.geometry(&label.layout) {
219 ui.mark([start, top, end, top + line], theme.hover);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 ui.mark([x, top, x + 1.0, top + line], theme.text);246 ui.mark([x, top, x + 1.0, top + line], theme.text);
223 }247 }
224 ui.close();248 ui.close();
225 signal249 signal
226}250}
227251
228fn floor_boundary(text: &str, index: usize) -> usize {252/// A winit key in the editing vocabulary text fields and the page share.
229 let mut index = index.min(text.len());253pub fn edit_key(key: &Key) -> edit::Key {
230 while !text.is_char_boundary(index) {254 use edit::NamedKey as Edit;
231 index -= 1;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
236fn previous(text: &str, index: usize) -> usize {
237 text[..index]
238 .char_indices()
239 .next_back()
240 .map_or(0, |(index, _)| index)
241}280}
242281
243fn next(text: &str, index: usize) -> usize {282pub fn edit_modifiers(modifiers: ModifiersState) -> edit::Modifiers {
244 text[index..]283 edit::Modifiers {
245 .chars()284 shift: modifiers.shift_key(),
246 .next()285 control: modifiers.control_key(),
247 .map_or(index, |character| index + character.len_utf8())286 option: modifiers.alt_key(),
287 command: modifiers.super_key(),
288 }
248}289}
fuzz/Cargo.lock+10
...@@ -81,6 +81,7 @@ dependencies = [...@@ -81,6 +81,7 @@ dependencies = [
81name = "canvas"81name = "canvas"
82version = "0.1.0"82version = "0.1.0"
83dependencies = [83dependencies = [
84 "draw",
84 "onestore",85 "onestore",
85 "parley",86 "parley",
86 "serde",87 "serde",
...@@ -195,6 +196,13 @@ dependencies = [...@@ -195,6 +196,13 @@ dependencies = [
195 "libloading",196 "libloading",
196]197]
197198
199[[package]]
200name = "draw"
201version = "0.1.0"
202dependencies = [
203 "parley",
204]
205
198[[package]]206[[package]]
199name = "errno"207name = "errno"
200version = "0.3.14"208version = "0.3.14"
...@@ -646,6 +654,7 @@ version = "0.1.0"...@@ -646,6 +654,7 @@ version = "0.1.0"
646dependencies = [654dependencies = [
647 "aes",655 "aes",
648 "base64",656 "base64",
657 "bumpalo",
649 "cbc",658 "cbc",
650 "getrandom",659 "getrandom",
651 "md5",660 "md5",
...@@ -663,6 +672,7 @@ name = "onestore-fuzz"...@@ -663,6 +672,7 @@ name = "onestore-fuzz"
663version = "0.0.0"672version = "0.0.0"
664dependencies = [673dependencies = [
665 "canvas",674 "canvas",
675 "draw",
666 "libfuzzer-sys",676 "libfuzzer-sys",
667 "md5",677 "md5",
668 "notebook",678 "notebook",
fuzz/Cargo.toml+1
...@@ -12,6 +12,7 @@ libfuzzer-sys = "0.4"...@@ -12,6 +12,7 @@ libfuzzer-sys = "0.4"
12onestore = { path = "../crates/onestore", features = ["protected"] }12onestore = { path = "../crates/onestore", features = ["protected"] }
13notebook = { path = "../crates/notebook" }13notebook = { path = "../crates/notebook" }
14canvas = { path = "../crates/canvas" }14canvas = { path = "../crates/canvas" }
15draw = { path = "../crates/draw", default-features = false }
15tempfile = "3"16tempfile = "3"
16md5 = "0.8.1"17md5 = "0.8.1"
17serde_json = "1"18serde_json = "1"
fuzz/fuzz_targets/canvas_editor.rs+2-1
...@@ -1,5 +1,6 @@...@@ -1,5 +1,6 @@
1#![no_main]1#![no_main]
2use canvas::{date::PageDate, document::{TextDocument, TextPosition}, editor::{CanvasEditor, Movement, SelectionUnit, TextOutline}, layout::TextEngine};2use canvas::{date::PageDate, document::{TextDocument, TextPosition}, editor::{CanvasEditor, TextOutline}, layout::TextEngine};
3use draw::edit::{Movement, SelectionUnit};
3use onestore::page::text::{Paragraph};4use onestore::page::text::{Paragraph};
4use libfuzzer_sys::fuzz_target;5use libfuzzer_sys::fuzz_target;
5use onestore::document::Format;6use onestore::document::Format;
tools/canvas/README.md+8-6
...@@ -34,23 +34,25 @@ canvas text edits · inverse history · composition · paragraph layout ·...@@ -34,23 +34,25 @@ canvas text edits · inverse history · composition · paragraph layout ·
34canvas::gpu page scene · text runs, tag icons, pictures and ink as draw primitives34canvas::gpu page scene · text runs, tag icons, pictures and ink as draw primitives
35 ↓35 ↓
36draw Swash glyph rasterization · custom wgpu quads · bounded glyph/image caches · layers36draw 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 ↓
38canvas::interaction hit layers · drags · placement grid · picture handles · chrome · key routing39canvas::interaction hit layers · drags · placement grid · picture handles · chrome · key routing
39 · scroll bounds and zoom · caret blink · AccessKit tree40 · scroll bounds and zoom · caret blink · AccessKit tree
40 ↓41 ↓
41ui immediate-mode boxes · keyed state · autolayout · event routing · labels · widgets42ui immediate-mode boxes · keyed state · autolayout · event routing · labels · widgets
43 · shell: section tabs, toolbar buttons and drop-downs
42 ↓44 ↓
43snowbound section tabs · page list · the page as a custom box · Winit window and events45snowbound title bar · toolbar · section frame · page tabs · the page as a custom box · Winit window and events
44 · AppKit pickers and quit guard · clipboard · saving46 · AppKit pickers and quit guard · clipboard · saving
45```47```
4648
47## Desktop shell49## Desktop shell
4850
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.
5052
51The `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.53The `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.
5254
53A 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.55A 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.
5456
55```sh57```sh
56cp -RL SOURCE_NOTEBOOK /tmp/notebook-copy && chmod u+w /tmp/notebook-copy/*.one58cp -RL SOURCE_NOTEBOOK /tmp/notebook-copy && chmod u+w /tmp/notebook-copy/*.one
...@@ -60,7 +62,7 @@ cargo test -p ui...@@ -60,7 +62,7 @@ cargo test -p ui
6062
61The 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.63The 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.
6264
63The `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.65The `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.
6466
65Glyph 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.67Glyph 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.
6668
...@@ -242,7 +244,7 @@ Full native captures, candidate font binaries/licenses and probe outputs remain...@@ -242,7 +244,7 @@ Full native captures, candidate font binaries/licenses and probe outputs remain
242244
243## CPU pipeline profiling245## CPU pipeline profiling
244246
245The 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.247The 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.
246248
247```sh249```sh
248cargo test --release -p canvas --features interaction canvas_pipeline_cost -- --ignored --nocapture --test-threads=1250cargo test --release -p canvas --features interaction canvas_pipeline_cost -- --ignored --nocapture --test-threads=1