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 = [
400400 "parley",
401401 "png",
402402 "pollster",
403 "resvg",
403404 "serde",
404405 "serde_json",
406 "sha2",
405407 "skrifa",
406408 "wgpu",
407409]
......@@ -642,6 +644,12 @@ version = "1.2.0"
642644source = "registry+https://github.com/rust-lang/crates.io-index"
643645checksum = "f27ae1dd37df86211c42e150270f82743308803d90a6f6e6651cd730d5e1732f"
644646
647[[package]]
648name = "data-url"
649version = "0.3.2"
650source = "registry+https://github.com/rust-lang/crates.io-index"
651checksum = "be1e0bca6c3637f992fc1cc7cbc52a78c1ef6db076dbf1059c4323d6a2048376"
652
645653[[package]]
646654name = "dbl"
647655version = "0.5.0"
......@@ -757,6 +765,15 @@ version = "3.4.0"
757765source = "registry+https://github.com/rust-lang/crates.io-index"
758766checksum = "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
760777[[package]]
761778name = "fallible-iterator"
762779version = "0.3.0"
......@@ -801,6 +818,12 @@ dependencies = [
801818 "zlib-rs",
802819]
803820
821[[package]]
822name = "float-cmp"
823version = "0.9.0"
824source = "registry+https://github.com/rust-lang/crates.io-index"
825checksum = "98de4bbd547a563b716d8dfa9aad1cb19bfab00f4fa09a6a4ed21dbcf44ce9c4"
826
804827[[package]]
805828name = "foldhash"
806829version = "0.2.0"
......@@ -1210,6 +1233,12 @@ dependencies = [
12101233 "zune-jpeg",
12111234]
12121235
1236[[package]]
1237name = "imagesize"
1238version = "0.15.0"
1239source = "registry+https://github.com/rust-lang/crates.io-index"
1240checksum = "65b27460c2c92b037f3f94c538ed9a3342f3fdf923606781629ccb35f82d042a"
1241
12131242[[package]]
12141243name = "indexmap"
12151244version = "2.14.2"
......@@ -1332,6 +1361,18 @@ version = "3.1.0"
13321361source = "registry+https://github.com/rust-lang/crates.io-index"
13331362checksum = "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
13351376[[package]]
13361377name = "libc"
13371378version = "0.2.189"
......@@ -1958,6 +1999,7 @@ version = "0.1.0"
19581999dependencies = [
19592000 "aes",
19602001 "base64",
2002 "bumpalo",
19612003 "cbc",
19622004 "getrandom 0.4.3",
19632005 "md5",
......@@ -2081,6 +2123,12 @@ version = "2.3.2"
20812123source = "registry+https://github.com/rust-lang/crates.io-index"
20822124checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220"
20832125
2126[[package]]
2127name = "pico-args"
2128version = "0.5.0"
2129source = "registry+https://github.com/rust-lang/crates.io-index"
2130checksum = "5be167a7af36ee22fe3115051bc51f6e6c7054c9348e28deb4f49bd6f705a315"
2131
20842132[[package]]
20852133name = "pin-project"
20862134version = "1.1.13"
......@@ -2152,6 +2200,15 @@ version = "0.4.0"
21522200source = "registry+https://github.com/rust-lang/crates.io-index"
21532201checksum = "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
21552212[[package]]
21562213name = "polyval"
21572214version = "0.7.3"
......@@ -2329,6 +2386,30 @@ version = "1.1.0"
23292386source = "registry+https://github.com/rust-lang/crates.io-index"
23302387checksum = "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
23322413[[package]]
23332414name = "roxmltree"
23342415version = "0.21.1"
......@@ -2441,7 +2522,7 @@ dependencies = [
24412522 "log",
24422523 "memmap2",
24432524 "smithay-client-toolkit",
2444 "tiny-skia",
2525 "tiny-skia 0.11.4",
24452526]
24462527
24472528[[package]]
......@@ -2543,6 +2624,21 @@ version = "0.1.5"
25432624source = "registry+https://github.com/rust-lang/crates.io-index"
25442625checksum = "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
25462642[[package]]
25472643name = "skrifa"
25482644version = "0.44.0"
......@@ -2705,6 +2801,9 @@ name = "strict-num"
27052801version = "0.1.1"
27062802source = "registry+https://github.com/rust-lang/crates.io-index"
27072803checksum = "6637bab7722d379c8b41ba849228d680cc12d0a45ba1fa2b48f2a30577a06731"
2804dependencies = [
2805 "float-cmp",
2806]
27082807
27092808[[package]]
27102809name = "subtle"
......@@ -2712,6 +2811,16 @@ version = "2.6.1"
27122811source = "registry+https://github.com/rust-lang/crates.io-index"
27132812checksum = "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
27152824[[package]]
27162825name = "swash"
27172826version = "0.2.10"
......@@ -2829,7 +2938,22 @@ dependencies = [
28292938 "bytemuck",
28302939 "cfg-if",
28312940 "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",
28332957]
28342958
28352959[[package]]
......@@ -2843,6 +2967,17 @@ dependencies = [
28432967 "strict-num",
28442968]
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
28462981[[package]]
28472982name = "tinystr"
28482983version = "0.8.4"
......@@ -2969,6 +3104,25 @@ dependencies = [
29693104 "ctutils",
29703105]
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
29723126[[package]]
29733127name = "utf8_iter"
29743128version = "1.0.4"
Cargo.toml+5
......@@ -6,3 +6,8 @@ resolver = "3"
66[workspace.dependencies]
77parley = { git = "https://github.com/linebender/parley", rev = "4f67747d3690bb21d25cc70b674ab0d269595759", features = ["complex-scripts"] }
88wgpu = "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"
55publish = false
66
77[features]
8gpu = ["dep:draw"]
8gpu = ["draw/render", "dep:resvg", "dep:sha2"]
99# How a page responds to input and presents itself to assistive technology.
1010interaction = ["gpu", "dep:accesskit"]
1111
1212[dependencies]
13draw = { path = "../draw", default-features = false }
1314onestore = { path = "../onestore" }
1415parley.workspace = true
1516serde = { version = "1.0.229", features = ["derive"] }
1617serde_json = "1.0.151"
1718skrifa = "0.44.0"
1819
19draw = { path = "../draw", optional = true }
2020accesskit = { version = "0.25.0", optional = true }
21resvg = { version = "0.48.1", default-features = false, optional = true }
22sha2 = { version = "0.11", optional = true }
2123
2224[[bin]]
2325name = "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 @@
1use canvas::gpu::{Viewport, page::PageScene};
1use canvas::gpu::{Paper, Viewport, page::PageScene};
22use canvas::layout::TextEngine;
33use draw::Renderer;
44use onestore::page::Page;
......@@ -38,9 +38,12 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
3838 };
3939 let margin_origin = page.margin_origin;
4040 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 }
4245 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)?;
4447 let instance = wgpu::Instance::new(wgpu::InstanceDescriptor::new_without_display_handle());
4548 let adapter = pollster::block_on(instance.request_adapter(&Default::default()))?;
4649 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 {
1616#[derive(Clone, Debug, PartialEq)]
1717pub struct TextDocument {
1818 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>,
1924}
2025
2126#[derive(Clone, Debug, PartialEq)]
......@@ -123,85 +128,98 @@ pub(crate) fn swap_columns(nodes: &mut [PageParagraph], widths: &mut BTreeMap<Ex
123128 }
124129}
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
126147pub(crate) fn validate_nodes(
127148 nodes: &[PageParagraph],
128 edit: Option<&DocumentEdit>,
129149 ids: &mut BTreeSet<ExGuid>,
130150) -> Result<(), EditError> {
131 let mut pending = vec![(None, nodes, 0)];
132 while let Some((container, nodes, depth)) = pending.pop() {
151 if nodes.is_empty() {
152 return Err(EditError::InvalidRange);
153 }
154 validate_run(nodes, &[], 0, ids)
155}
156
157/// Validates `nodes`, which follow `earlier` in a container at `depth`, and every container
158/// nested in them; `earlier` is searched only for parents the run lacks.
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() {
133167 if depth > 64 {
134168 return Err(EditError::InvalidStructure);
135169 }
136 let mut nodes = edited_nodes(nodes, container, edit).peekable();
137 if nodes.peek().is_none() {
138 return Err(EditError::InvalidRange);
139 }
140170 let mut parents = BTreeMap::new();
141171 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))
143182 || node.level == 0
144183 || node
145184 .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))
147186 {
148187 return Err(EditError::InvalidStructure);
149188 }
150189 parents.insert(node.id, node.level);
151190 match &node.content {
152 ParagraphContent::Text(text) => {
153 if !ids.insert(text.id) {
154 return Err(EditError::InvalidStructure);
155 }
156 }
157191 ParagraphContent::Image(image) => {
158 if !ids.insert(image.id) {
159 return Err(EditError::InvalidStructure);
160 }
161192 crate::outline::image_size(image).ok_or(EditError::UnsupportedContent)?;
162193 }
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 }
178194 ParagraphContent::Table(table) => {
179 if !ids.insert(table.id)
180 || table.rows.is_empty()
195 if table.rows.is_empty()
181196 || table.columns.is_empty()
182197 || table.columns.len() > 255
183198 || table
184199 .columns
185200 .iter()
186201 .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())
187206 {
188207 return Err(EditError::InvalidStructure);
189208 }
190 for row in &table.rows {
191 if !ids.insert(row.id) || row.cells.len() != table.columns.len() {
192 return Err(EditError::InvalidStructure);
209 for cell in table.rows.iter().flat_map(|row| &row.cells) {
210 if !cell.unsupported.is_empty() {
211 return Err(EditError::UnsupportedContent);
193212 }
194 for cell in &row.cells {
195 if !ids.insert(cell.id) {
196 return Err(EditError::InvalidStructure);
197 }
198 if !cell.unsupported.is_empty() {
199 return Err(EditError::UnsupportedContent);
200 }
201 pending.push((Some(cell.id), cell.paragraphs.as_slice(), depth + 1));
213 if cell.paragraphs.is_empty() {
214 return Err(EditError::InvalidRange);
202215 }
216 pending.push((cell.paragraphs.as_slice(), &[][..], depth + 1));
203217 }
204218 }
219 ParagraphContent::Text(_)
220 | ParagraphContent::Attachment(_)
221 | ParagraphContent::Ink(_)
222 | ParagraphContent::Unsupported(_) => {}
205223 }
206224 }
207225 }
......@@ -216,6 +234,15 @@ fn validate_text(nodes: &[PageParagraph]) -> Result<(), EditError> {
216234 Ok(())
217235}
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
219246pub(crate) fn validate_flat<'a>(
220247 mut nodes: impl Iterator<Item = &'a PageParagraph>,
221248) -> Result<(), EditError> {
......@@ -235,16 +262,20 @@ pub(crate) fn container_mut(
235262 nodes: &mut Vec<PageParagraph>,
236263 id: Option<ExGuid>,
237264) -> Option<&mut Vec<PageParagraph>> {
238 let Some(id) = id else {
239 return Some(nodes);
240 };
265 match id {
266 None => Some(nodes),
267 Some(id) => cell_mut(nodes, id),
268 }
269}
270
271fn cell_mut(nodes: &mut [PageParagraph], id: ExGuid) -> Option<&mut Vec<PageParagraph>> {
241272 for node in nodes {
242273 if let ParagraphContent::Table(table) = &mut node.content {
243274 for cell in table.rows.iter_mut().flat_map(|row| &mut row.cells) {
244275 if cell.id == id {
245276 return Some(&mut cell.paragraphs);
246277 }
247 if let Some(nodes) = container_mut(&mut cell.paragraphs, Some(id)) {
278 if let Some(nodes) = cell_mut(&mut cell.paragraphs, id) {
248279 return Some(nodes);
249280 }
250281 }
......@@ -253,6 +284,14 @@ pub(crate) fn container_mut(
253284 None
254285}
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
256295impl TextDocument {
257296 pub fn new(paragraphs: Vec<Paragraph>) -> Result<Self, EditError> {
258297 Self::from_nodes(
......@@ -264,9 +303,15 @@ impl TextDocument {
264303 }
265304
266305 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())?;
268307 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 })
270315 }
271316
272317 pub fn nodes(&self) -> &[PageParagraph] {
......@@ -285,19 +330,90 @@ impl TextDocument {
285330 self.text_nodes().map(|node| &node.text().unwrap().text)
286331 }
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
288398 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> {
289404 let Some(id) = id else {
290 return Ok(&self.nodes);
405 return Ok((&self.nodes, 0));
291406 };
292 let mut pending = vec![self.nodes.as_slice()];
293 while let Some(nodes) = pending.pop() {
407 let root = self.root(id)?;
408 let mut pending = vec![(std::slice::from_ref(&self.nodes[root]), 0)];
409 while let Some((nodes, depth)) = pending.pop() {
294410 for node in nodes {
295411 if let ParagraphContent::Table(table) = &node.content {
296412 for cell in table.rows.iter().flat_map(|row| &row.cells) {
297413 if cell.id == id {
298 return Ok(&cell.paragraphs);
414 return Ok((&cell.paragraphs, depth + 1));
299415 }
300 pending.push(&cell.paragraphs);
416 pending.push((&cell.paragraphs, depth + 1));
301417 }
302418 }
303419 }
......@@ -344,11 +460,11 @@ impl TextDocument {
344460 if range.start > range.end {
345461 return Err(EditError::InvalidRange);
346462 }
347 let (container, start, first) = leaves(&self.nodes, None)
348 .nth(range.start.paragraph)
463 let (container, start, first) = self
464 .leaf(range.start.paragraph)
349465 .ok_or(EditError::InvalidRange)?;
350 let (end_container, end, last) = leaves(&self.nodes, None)
351 .nth(range.end.paragraph)
466 let (end_container, end, last) = self
467 .leaf(range.end.paragraph)
352468 .ok_or(EditError::InvalidRange)?;
353469 if container != end_container {
354470 return Err(EditError::UnsupportedContent);
......@@ -394,12 +510,60 @@ impl TextDocument {
394510 })
395511 }
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.
397515 pub(crate) fn validate_edit(&self, edit: &DocumentEdit) -> Result<(), EditError> {
398 let nodes = self.container(edit.container)?;
399 if edit.range.start > edit.range.end || edit.range.end > nodes.len() {
516 let (nodes, depth) = self.nested_container(edit.container)?;
517 if edit.range.start > edit.range.end
518 || edit.range.end > nodes.len()
519 || nodes.len() - edit.range.len() + edit.replacement.len() == 0
520 {
400521 return Err(EditError::InvalidRange);
401522 }
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 }
403567 if !edit.columns.is_empty() {
404568 let replaced = descendants(&edit.replacement, None)
405569 .filter_map(|(_, _, node)| match &node.content {
......@@ -431,23 +595,59 @@ impl TextDocument {
431595
432596 pub(crate) fn apply(&mut self, edit: DocumentEdit) -> Result<DocumentEdit, EditError> {
433597 self.validate_edit(&edit)?;
434 let end = edit
435 .range
436 .start
437 .checked_add(edit.replacement.len())
438 .ok_or(EditError::TextTooLong)?;
439 let range = edit.range.start..end;
440 let nodes =
441 container_mut(&mut self.nodes, edit.container).ok_or(EditError::InvalidRange)?;
442 let replacement = nodes.splice(edit.range, edit.replacement).collect();
598 Ok(self.splice(edit))
599 }
600
601 /// Applies an edit [`Self::validate_edit`] accepted, returning its inverse.
602 pub(crate) fn splice(&mut self, edit: DocumentEdit) -> DocumentEdit {
603 let range = edit.range.start..edit.range.start + edit.replacement.len();
604 let root = edit.container.map(|cell| {
605 (
606 cell,
607 self.root(cell).expect("a validated edit's cell exists"),
608 )
609 });
610 let nodes = match root {
611 Some((cell, root)) => cell_mut(std::slice::from_mut(&mut self.nodes[root]), cell)
612 .expect("a validated edit's cell exists"),
613 None => &mut self.nodes,
614 };
615 let replacement: Vec<_> = nodes.splice(edit.range.clone(), edit.replacement).collect();
616 let delta = leaves(&nodes[range.clone()], None).count() as isize
617 - leaves(&replacement, None).count() as isize;
618 let after = match root {
619 Some((_, root)) => root + 1,
620 None => {
621 let first = self.starts[range.start];
622 self.starts.splice(
623 edit.range.clone(),
624 starts(&self.nodes[range.clone()], first),
625 );
626 let [start, end] = [edit.range.start, edit.range.end]
627 .map(|index| self.tables.partition_point(|table| *table < index));
628 let added = tables(&self.nodes[range.clone()], range.start).collect::<Vec<_>>();
629 let later = start + added.len();
630 self.tables.splice(start..end, added);
631 for table in &mut self.tables[later..] {
632 *table =
633 table.wrapping_add_signed(range.len() as isize - edit.range.len() as isize);
634 }
635 range.end
636 }
637 };
638 if delta != 0 {
639 for start in &mut self.starts[after..] {
640 *start = start.wrapping_add_signed(delta);
641 }
642 }
443643 let mut columns = edit.columns;
444644 swap_columns(&mut self.nodes, &mut columns);
445 Ok(DocumentEdit {
645 DocumentEdit {
446646 container: edit.container,
447647 range,
448648 replacement,
449649 columns,
450 })
650 }
451651 }
452652}
453653
......@@ -1252,6 +1452,239 @@ mod tests {
12521452 }
12531453 }
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
12551688 #[test]
12561689 fn invalid_ranges_and_last_paragraph_removal_are_atomic() {
12571690 let original =
crates/canvas/src/editor.rs+268-260
......@@ -1,12 +1,10 @@
11use crate::{
22 date::PageDate,
3 document::{DocumentEdit, TextDocument, TextPosition, descendants, edited_nodes, leaves},
3 document::{DocumentEdit, TextDocument, TextPosition, descendants, leaves},
44 layout::{LayoutError, TextEngine},
5 outline::{
6 Arrange, OutlineLayout, ParagraphLayout, arrange, indentation, outline_layout,
7 visible_paragraphs,
8 },
5 outline::{Arrange, OutlineLayout, ParagraphLayout, outline_layout, visible_paragraphs},
96};
7use draw::edit::{self, Movement, SelectionUnit};
108use onestore::ExGuid;
119use onestore::page::text::{EditError, Paragraph};
1210use onestore::page::{Definition, Outline, Page, PageObject, PageParagraph, Title};
......@@ -26,29 +24,6 @@ pub(crate) mod page;
2624mod table;
2725pub 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
5227#[derive(Debug)]
5328pub enum EditorError {
5429 Edit(EditError),
......@@ -319,19 +294,7 @@ impl TextOutline {
319294 let text_width = if self.shaped.tables.is_empty() {
320295 Some(self.shaped.size[0])
321296 } else {
322 let roots = self
323 .document
324 .nodes()
325 .iter()
326 .filter(|node| node.text().is_some())
327 .map(|node| node.id)
328 .collect::<BTreeSet<_>>();
329 self.shaped
330 .paragraphs
331 .iter()
332 .filter(|paragraph| roots.contains(&paragraph.id))
333 .map(|paragraph| paragraph.origin[0] + paragraph.text.shaped.width())
334 .reduce(f32::max)
297 Some(self.shaped.widths[1]).filter(|width| *width > f32::NEG_INFINITY)
335298 };
336299 text_width.map_or(self.shaped.size[0], |width| {
337300 (width + 36.0)
......@@ -370,13 +333,23 @@ impl TextOutline {
370333 })
371334 }
372335
336 /// Layouts follow the text leaves minus hidden ones, so an index shared by a leaf and its
337 /// layout needs no search.
373338 fn visible_index(&self, source: usize) -> Result<usize, EditError> {
374339 let id = self
375340 .document
376 .text_nodes()
377 .nth(source)
341 .leaf(source)
378342 .ok_or(EditError::InvalidRange)?
343 .2
379344 .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 }
380353 self.shaped
381354 .paragraphs
382355 .iter()
......@@ -386,6 +359,13 @@ impl TextOutline {
386359
387360 fn source_index(&self, visible: usize) -> usize {
388361 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 }
389369 self.document
390370 .text_nodes()
391371 .position(|paragraph| paragraph.id == id)
......@@ -989,7 +969,7 @@ impl CanvasEditor {
989969 if !ids.insert(outline_id) {
990970 return Err(EditError::InvalidStructure.into());
991971 }
992 crate::document::validate_nodes(paragraphs, None, &mut ids)?;
972 crate::document::validate_nodes(paragraphs, &mut ids)?;
993973 }
994974 Ok(Self {
995975 outlines,
......@@ -1574,8 +1554,7 @@ impl CanvasEditor {
15741554 self.active_outline().visible_index(position.paragraph)?;
15751555 self.active_outline()
15761556 .document
1577 .paragraphs()
1578 .nth(position.paragraph)
1557 .paragraph(position.paragraph)
15791558 .ok_or(EditError::InvalidRange)?
15801559 .byte_offset(position.offset)?;
15811560 }
......@@ -1589,12 +1568,7 @@ impl CanvasEditor {
15891568 let paragraph = self
15901569 .active_outline()
15911570 .source_index(self.active_outline().shaped.paragraphs.len() - 1);
1592 let last = self
1593 .active_outline()
1594 .document
1595 .paragraphs()
1596 .nth(paragraph)
1597 .unwrap();
1571 let last = self.active_outline().document.paragraph(paragraph).unwrap();
15981572 self.select(
15991573 [
16001574 TextPosition {
......@@ -1632,22 +1606,12 @@ impl CanvasEditor {
16321606 let paragraph = &self.active_outline().shaped.paragraphs[index];
16331607 let layout = &paragraph.text.shaped;
16341608 let point = [x - paragraph.origin[0], y - paragraph.origin[1]];
1635 let cursor = paragraph.text.hit_test(point[0], point[1]);
1636 let local = match unit {
1637 SelectionUnit::Grapheme => cursor.into(),
1638 SelectionUnit::Word => {
1639 let rect = cursor.geometry(layout, 1.0);
1640 ParagraphSelection::word_from_point(
1641 layout,
1642 point[0],
1643 ((rect.y0 + rect.y1) * 0.5) as f32,
1644 )
1645 }
1646 SelectionUnit::Paragraph => ParagraphSelection::new(
1647 Cursor::from_byte_index(layout, 0, Affinity::Downstream),
1648 Cursor::from_byte_index(layout, usize::MAX, Affinity::Upstream),
1649 ),
1650 };
1609 let local = edit::selection_at(
1610 layout,
1611 paragraph.text.hit_test(point[0], point[1]),
1612 point[0],
1613 unit,
1614 );
16511615 let mut selection = self.selection();
16521616 for (slot, cursor) in [local.anchor(), local.focus()].into_iter().enumerate() {
16531617 let visible = paragraph.projection.text().utf16_offset(cursor.index())?;
......@@ -1665,8 +1629,8 @@ impl CanvasEditor {
16651629 let document = &self.active_outline().document;
16661630 let source = selection.positions[0].paragraph;
16671631 let next = self.active_outline().source_index(index + 1);
1668 let (container, local, _) = leaves(document.nodes(), None).nth(source).unwrap();
1669 let (next_container, next_local, _) = leaves(document.nodes(), None).nth(next).unwrap();
1632 let (container, local, _) = document.leaf(source).unwrap();
1633 let (next_container, next_local, _) = document.leaf(next).unwrap();
16701634 if container == next_container && next_local == local + 1 {
16711635 selection.positions[1] = TextPosition {
16721636 paragraph: next,
......@@ -1767,7 +1731,7 @@ impl CanvasEditor {
17671731 let outline = self.active_outline();
17681732 let current = outline.visible_index(focus.paragraph)?;
17691733 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();
17711735 let current_end = current_end.utf16_offset(current_end.text().len())?;
17721736 let index = match movement {
17731737 Movement::DocumentStart => 0,
......@@ -1779,7 +1743,7 @@ impl CanvasEditor {
17791743 _ => current,
17801744 };
17811745 let source = outline.source_index(index);
1782 let paragraph = outline.document.paragraphs().nth(source).unwrap();
1746 let paragraph = outline.document.paragraph(source).unwrap();
17831747 let position = TextPosition {
17841748 paragraph: source,
17851749 offset: if end {
......@@ -2030,22 +1994,7 @@ impl CanvasEditor {
20301994 } else {
20311995 cursor.into()
20321996 };
2033 let layout = &paragraph.text.shaped;
2034 next = match movement {
2035 Movement::Left => local.previous_visual(layout, extend),
2036 Movement::Right => local.next_visual(layout, extend),
2037 Movement::WordLeft => paragraph.text.word_cursor(local.focus(), true).into(),
2038 Movement::WordRight => paragraph.text.word_cursor(local.focus(), false).into(),
2039 Movement::LineStart => local.line_start(layout, extend),
2040 Movement::LineEnd => local.line_end(layout, extend),
2041 Movement::Up
2042 | Movement::Down
2043 | Movement::ParagraphStart
2044 | Movement::ParagraphEnd
2045 | Movement::DocumentStart
2046 | Movement::DocumentEnd => unreachable!(),
2047 }
2048 .focus();
1997 next = edit::step(&paragraph.text.shaped, local, movement, extend).focus();
20491998 if horizontal && next == cursor && (extend || anchor == focus) {
20501999 let neighbor = if left {
20512000 index.checked_sub(1)
......@@ -2067,7 +2016,7 @@ impl CanvasEditor {
20672016 line.top + line.height * 0.5,
20682017 );
20692018 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);
20712020 }
20722021 }
20732022 }
......@@ -2161,12 +2110,7 @@ impl CanvasEditor {
21612110 .enumerate()
21622111 .filter(|(_, (index, _))| *index >= start.paragraph && *index <= end.paragraph)
21632112 {
2164 let source = self
2165 .active_outline()
2166 .document
2167 .paragraphs()
2168 .nth(index)
2169 .unwrap();
2113 let source = self.active_outline().document.paragraph(index).unwrap();
21702114 let first = if index == start.paragraph {
21712115 paragraph.cursor(start.offset, affinities[0])?
21722116 } else {
......@@ -2249,8 +2193,7 @@ impl CanvasEditor {
22492193 let format = self
22502194 .active_outline()
22512195 .document
2252 .paragraphs()
2253 .nth(start.paragraph)
2196 .paragraph(start.paragraph)
22542197 .unwrap()
22552198 .format_at(start.offset)?;
22562199 let replacement = text
......@@ -2268,11 +2211,13 @@ impl CanvasEditor {
22682211 let [anchor, focus] = outline.selection.positions;
22692212 let start = anchor.min(focus);
22702213 let end = anchor.max(focus);
2271 let (container, local_start, _) = leaves(outline.document.nodes(), None)
2272 .nth(start.paragraph)
2214 let (container, local_start, _) = outline
2215 .document
2216 .leaf(start.paragraph)
22732217 .ok_or(EditError::InvalidRange)?;
2274 let (end_container, local_end, _) = leaves(outline.document.nodes(), None)
2275 .nth(end.paragraph)
2218 let (end_container, local_end, _) = outline
2219 .document
2220 .leaf(end.paragraph)
22762221 .ok_or(EditError::InvalidRange)?;
22772222 if container != end_container {
22782223 return Err(EditError::UnsupportedContent.into());
......@@ -2343,12 +2288,10 @@ impl CanvasEditor {
23432288 }) else {
23442289 return Ok(false);
23452290 };
2346 let (container, local, current) = leaves(document.nodes(), None)
2347 .nth(focus.paragraph)
2291 let (container, local, current) = document
2292 .leaf(focus.paragraph)
23482293 .ok_or(EditError::InvalidRange)?;
2349 let Some((next_container, next_local, next)) =
2350 leaves(document.nodes(), None).nth(neighbor)
2351 else {
2294 let Some((next_container, next_local, next)) = document.leaf(neighbor) else {
23522295 return Ok(false);
23532296 };
23542297 if container != next_container || local.abs_diff(next_local) != 1 {
......@@ -2406,8 +2349,7 @@ impl CanvasEditor {
24062349 let format = self
24072350 .active_outline()
24082351 .document
2409 .paragraphs()
2410 .nth(caret.paragraph)
2352 .paragraph(caret.paragraph)
24112353 .unwrap()
24122354 .format_at(caret.offset)?
24132355 .clone();
......@@ -2779,8 +2721,7 @@ impl CanvasEditor {
27792721 let format = self
27802722 .active_outline()
27812723 .document
2782 .paragraphs()
2783 .nth(range.start.paragraph)
2724 .paragraph(range.start.paragraph)
27842725 .unwrap()
27852726 .format_at(range.start.offset)?;
27862727 let replacement = text
......@@ -2922,55 +2863,29 @@ impl CanvasEditor {
29222863 count -= 1;
29232864 }
29242865 if count < outline.document.nodes().len() {
2925 let removed = outline.document.nodes().len() - count;
2926 outline
2927 .document
2928 .apply(DocumentEdit {
2929 columns: BTreeMap::new(),
2930 container: None,
2931 range: count..outline.document.nodes().len(),
2932 replacement: Vec::new(),
2933 })
2934 .expect("removing a provisional suffix preserves a valid document");
2935 outline
2866 let edit = DocumentEdit {
2867 columns: BTreeMap::new(),
2868 container: None,
2869 range: count..outline.document.nodes().len(),
2870 replacement: Vec::new(),
2871 };
2872 let relayout = outline
29362873 .shaped
2937 .paragraphs
2938 .truncate(outline.shaped.paragraphs.len() - removed);
2939 if outline.shaped.tables.is_empty() && outline.shaped.objects.is_empty() {
2940 let (origins, size) = arrange(
2941 outline.shaped.paragraphs.iter(),
2942 outline.wrap_width(),
2943 outline.layout.width_set_by_user == Some(true),
2944 )
2945 .expect("a prefix of the validated layout fits");
2946 for (paragraph, y) in outline.shaped.paragraphs.iter_mut().zip(origins) {
2947 paragraph.origin[1] = y;
2948 }
2949 outline.shaped.size = size;
2950 } else {
2951 let cached = outline
2952 .shaped
2953 .paragraphs
2954 .iter()
2955 .chain(outline.shaped.objects.iter().filter_map(|o| o.label()))
2956 .map(|paragraph| (paragraph.id, paragraph))
2957 .collect::<BTreeMap<_, _>>();
2958 let shaped = OutlineLayout::flow(
2959 outline.document.nodes().iter(),
2874 .relayout(
2875 outline.document.nodes(),
2876 &edit,
2877 edit.range.clone(),
29602878 &outline.indents,
29612879 outline.wrap_width(),
29622880 outline.layout.width_set_by_user == Some(true),
2963 0,
2964 None,
2965 &mut |node, width, indents| {
2966 let mut paragraph = cached[&node.id].clone();
2967 paragraph.reset_origin(indentation(node.level, indents, width)?);
2968 Ok(paragraph)
2969 },
2881 &mut |_, _, _| Err(LayoutError::UnsupportedContent),
29702882 )
2971 .expect("removing a provisional suffix preserves table layout");
2972 outline.shaped = shaped;
2973 }
2883 .expect("removing trailing text lays nothing out anew");
2884 outline
2885 .document
2886 .apply(edit)
2887 .expect("removing a provisional suffix preserves a valid document");
2888 outline.shaped.commit(relayout);
29742889 }
29752890 if outline.document == self.outlines[index].document {
29762891 self.outlines[index].selection = outline.selection;
......@@ -3054,117 +2969,47 @@ impl CanvasEditor {
30542969 ) -> Result<TextChange, EditorError> {
30552970 let outline = self.active_outline();
30562971 outline.document.validate_edit(&edit)?;
3057 let width = outline.wrap_width();
3058 let (visible, shaped, origins, size, tables, objects) = if !crate::outline::all_text(
3059 outline.document.nodes().iter().chain(&edit.replacement),
3060 ) {
3061 let sources = outline
3062 .document
3063 .text_nodes()
3064 .map(|node| (node.id, node))
3065 .collect::<BTreeMap<_, _>>();
3066 let existing = outline
3067 .shaped
3068 .paragraphs
3069 .iter()
3070 .map(|layout| (layout.id, (sources[&layout.id], layout)))
3071 .collect::<BTreeMap<_, _>>();
3072 let layout = OutlineLayout::flow(
3073 edited_nodes(outline.document.nodes(), None, Some(&edit)),
3074 &outline.indents,
3075 width,
3076 outline.layout.width_set_by_user == Some(true),
3077 0,
3078 Some(&edit),
3079 &mut |node, width, indents| {
3080 let indent = indentation(node.level, indents, width)?;
3081 if let Some((source, cached)) = existing.get(&node.id)
3082 && *source == node
3083 && cached.text.shaped.layout_max_advance() == width - indent
3084 && cached
3085 .markers
3086 .iter()
3087 .all(|(marker, _)| marker.shaped.layout_max_advance() == width)
3088 {
3089 let mut result = (*cached).clone();
3090 result.reset_origin(indent);
3091 return Ok(result);
3092 }
3093 ParagraphLayout::shape(engine, node, width, indents, &self.definitions)
3094 },
3095 )?;
3096 let origins = layout
3097 .paragraphs
3098 .iter()
3099 .map(|p| p.origin[1])
3100 .collect::<Vec<_>>();
3101 (
3102 0..outline.shaped.paragraphs.len(),
3103 layout.paragraphs,
3104 origins,
3105 layout.size,
3106 layout.tables,
3107 layout.objects,
3108 )
3109 } else {
3110 let visible_start = outline
3111 .layouts()
3112 .take_while(|(i, _)| *i < edit.range.start)
3113 .count();
3114 let visible_end = outline
3115 .layouts()
3116 .take_while(|(i, _)| *i < edit.range.end)
3117 .count();
3118 if visible_end - visible_start != edit.range.len() {
3119 return Err(EditError::UnsupportedContent.into());
2972 let range = match edit.container {
2973 None => edit.range.clone(),
2974 Some(cell) => {
2975 let root = outline.document.root(cell)?;
2976 root..root + 1
31202977 }
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 )
31502978 };
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());
31512991 for position in selection.positions {
3152 let (_, _, node) = leaves(outline.document.nodes(), Some(&edit))
3153 .nth(position.paragraph)
2992 let (node, before) = outline
2993 .document
2994 .edited_leaf(&edit, position.paragraph)
31542995 .ok_or(EditError::InvalidRange)?;
31552996 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
31573001 .iter()
3158 .chain(&shaped)
3159 .chain(&outline.shaped.paragraphs[visible.end..])
31603002 .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))
31613006 {
31623007 return Err(EditError::InvalidRange.into());
31633008 }
31643009 }
31653010 let placements = match positions {
31663011 Some(positions) => positions.to_vec(),
3167 None => self.title_flow(size[1])?,
3012 None => self.title_flow(relayout.size[1])?,
31683013 };
31693014 let previous_positions = placements
31703015 .iter()
......@@ -3193,14 +3038,8 @@ impl CanvasEditor {
31933038 return self.apply(engine, edit, selection, false, positions);
31943039 }
31953040 let outline = self.active_outline_mut();
3196 let inverse = outline.document.apply(edit)?;
3197 outline.shaped.paragraphs.splice(visible, shaped);
3198 outline.shaped.tables = tables;
3199 outline.shaped.objects = objects;
3200 for (paragraph, y) in outline.shaped.paragraphs.iter_mut().zip(origins) {
3201 paragraph.origin[1] = y;
3202 }
3203 outline.shaped.size = size;
3041 let inverse = outline.document.splice(edit);
3042 outline.shaped.commit(relayout);
32043043 self.preferred_x = None;
32053044 let previous_selection =
32063045 std::mem::replace(&mut self.active_outline_mut().selection, selection);
......@@ -7414,6 +7253,175 @@ mod tests {
74147253 assert!(!editor.redo(&mut engine).unwrap());
74157254 }
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
74177425 #[test]
74187426 fn structural_edits_reflow_only_affected_paragraphs_and_restore_selection() {
74197427 let mut engine = TextEngine::default();
crates/canvas/src/editor/page.rs-1
......@@ -97,7 +97,6 @@ impl ReadOnlyObject {
9797 onestore::document::Format {
9898 font: Some("Arial".into()),
9999 font_size: Some(11.0),
100 color: Some(0x005d554e),
101100 ..Default::default()
102101 },
103102 ),
crates/canvas/src/editor/table.rs+6-4
......@@ -84,8 +84,9 @@ impl CanvasEditor {
8484 }
8585 let outline = self.active_outline();
8686 let [anchor, focus] = outline.selection.positions;
87 let (cell, local, source) = leaves(outline.document.nodes(), None)
88 .nth(focus.paragraph)
87 let (cell, local, source) = outline
88 .document
89 .leaf(focus.paragraph)
8990 .ok_or(EditError::InvalidRange)?;
9091 let Some(cell) = cell else {
9192 if backward || anchor != focus || focus.offset == 0 {
......@@ -255,8 +256,9 @@ impl CanvasEditor {
255256 }
256257 let outline = self.active_outline();
257258 let [anchor, focus] = outline.selection.positions;
258 let (cell, local, source) = leaves(outline.document.nodes(), None)
259 .nth(focus.paragraph)
259 let (cell, local, source) = outline
260 .document
261 .leaf(focus.paragraph)
260262 .ok_or(EditError::InvalidRange)?;
261263 let Some(cell) = cell.filter(|_| anchor == focus) else {
262264 return self.insert(engine, "\n");
crates/canvas/src/gpu/mod.rs+71-18
......@@ -37,6 +37,32 @@ impl Viewport {
3737 }
3838}
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
4066impl Glyphs for TextLayout {
4167 fn runs(
4268 &self,
......@@ -52,7 +78,8 @@ impl Glyphs for TextLayout {
5278 bounds.baseline,
5379 run.style().brush.rise,
5480 [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),
5683 paint,
5784 )?;
5885 }
......@@ -147,14 +174,24 @@ mod tests {
147174 ];
148175 let text = TextEngine::default()
149176 .layout(
150 &Paragraph::new(
151 "Tagged".into(),
152 Format {
153 color: Some(0x000000ff),
154 underline: Some(true),
155 ..Format::default()
156 },
157 ),
177 &Paragraph::from_runs([
178 (
179 "Tagged".into(),
180 Format {
181 color: Some(0x000000ff),
182 underline: Some(true),
183 ..Format::default()
184 },
185 ),
186 (" automatic".into(), Format::default()),
187 (
188 " lit".into(),
189 Format {
190 highlight: Some(0x0000ffff),
191 ..Format::default()
192 },
193 ),
194 ]),
158195 100.0,
159196 )
160197 .unwrap();
......@@ -165,13 +202,14 @@ mod tests {
165202 sources: tag_sources(*icon),
166203 origin: [2.0 + 16.0 * index as f32, 2.0],
167204 size: crate::outline::ParagraphTag::SIZE,
168 opacity: 1.0,
205 tint: [1.0; 4],
169206 })
170207 .collect();
171208 primitives.push(Primitive::Text {
172209 text: &text,
173210 origin: [2.0, 20.0],
174211 clip: None,
212 ink: [0.0, 1.0, 0.0, 1.0],
175213 });
176214 let viewport = Viewport {
177215 size,
......@@ -224,13 +262,28 @@ mod tests {
224262 .count();
225263 assert!(painted > 20, "tag {index} was not painted");
226264 }
227 let red = (40..64)
228 .flat_map(|y| (4..120).map(move |x| (x, y)))
229 .filter(|(x, y)| {
230 let [r, g, b] = pixel(*x, *y);
231 r > 200 && g < 80 && b < 80
232 })
233 .count();
234 assert!(red > 50, "text colour or underline was not painted");
265 let count = |color: [u8; 3]| {
266 (40..64)
267 .flat_map(|y| (4..size[0]).map(move |x| (x, y)))
268 .filter(|(x, y)| {
269 pixel(*x, *y)
270 .iter()
271 .zip(color)
272 .all(|(v, c)| v.abs_diff(c) < 80)
273 })
274 .count()
275 };
276 assert!(
277 count([255, 0, 0]) > 50,
278 "text colour or underline was not painted"
279 );
280 assert!(
281 count([0, 255, 0]) > 50,
282 "automatic text did not paint in the primitive's ink"
283 );
284 assert!(
285 count([0, 0, 0]) > 10,
286 "automatic text on a highlight did not stay black"
287 );
235288 }
236289}
crates/canvas/src/gpu/page.rs+153-68
......@@ -1,4 +1,6 @@
1use super::colorref;
1mod background;
2
3use super::{Paper, colorref};
24use crate::editor::ReadOnlyObject;
35use crate::editor::page::Content;
46use crate::{
......@@ -6,6 +8,7 @@ use crate::{
68 editor::{CanvasEditor, EditorError},
79 layout::{LayoutError, TextEngine},
810};
11use background::Background;
912use draw::{Primitive, RasterImage, RenderError};
1013use onestore::page::Page;
1114use std::fmt;
......@@ -14,6 +17,8 @@ use std::fmt;
1417pub struct PageScene {
1518 reference: Option<Vec<Content>>,
1619 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>,
1722}
1823
1924#[derive(Debug, PartialEq)]
......@@ -82,6 +87,17 @@ fn append_ink(ink: &onestore::page::Ink, offset: [f32; 2], primitives: &mut Vec<
8287 }
8388}
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
85101#[derive(Debug)]
86102pub enum SceneError {
87103 Layout(LayoutError),
......@@ -127,10 +143,10 @@ impl PageScene {
127143 let objects = crate::editor::page::build(&mut page, engine, false)
128144 .map_err(SceneError::from)?
129145 .objects;
130 let images = Self::decode_images(&objects, None)?;
146 let pictures = Self::decode_images(&objects, None)?;
131147 Ok(Self {
132148 reference: Some(objects),
133 images,
149 ..pictures
134150 })
135151 }
136152
......@@ -139,21 +155,15 @@ impl PageScene {
139155 engine: &mut TextEngine,
140156 ) -> Result<(Self, CanvasEditor), SceneError> {
141157 let editor = CanvasEditor::from_page(page, engine).map_err(SceneError::from)?;
142 let images = Self::decode_images(&editor.objects, Some(&editor))?;
143 Ok((
144 Self {
145 reference: None,
146 images,
147 },
148 editor,
149 ))
158 Ok((Self::decode_images(&editor.objects, Some(&editor))?, editor))
150159 }
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.
153163 fn decode_images(
154164 objects: &[Content],
155165 editor: Option<&CanvasEditor>,
156 ) -> Result<std::collections::BTreeMap<onestore::ExGuid, RasterImage>, SceneError> {
166 ) -> Result<Self, SceneError> {
157167 fn nested<'a>(
158168 nodes: &'a [onestore::page::PageParagraph],
159169 payloads: &mut Vec<(onestore::ExGuid, Option<&'a [u8]>)>,
......@@ -179,9 +189,18 @@ impl PageScene {
179189 }
180190 }
181191 let mut payloads = Vec::new();
192 let mut backgrounds = std::collections::BTreeMap::new();
182193 for object in objects {
183194 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 }
185204 Content::Outline { source, .. } => nested(&source.paragraphs, &mut payloads),
186205 Content::Editable(id) => {
187206 if let Some(outline) =
......@@ -206,13 +225,34 @@ impl PageScene {
206225 return Err(SceneError::InvalidGeometry);
207226 }
208227 }
209 Ok(images)
228 Ok(Self {
229 reference: None,
230 images,
231 backgrounds,
232 })
210233 }
211234
212235 pub fn image(&self, id: onestore::ExGuid) -> Option<&RasterImage> {
213236 self.images.get(&id)
214237 }
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
216256 /// Pictures, files and handwriting inside an outline whose origin is `origin`.
217257 /// `moving` draws one picture at a previewed rectangle, in the same coordinates as `origin`.
218258 pub fn append_outline_objects<'a>(
......@@ -220,6 +260,7 @@ impl PageScene {
220260 outline: &'a crate::outline::OutlineLayout,
221261 origin: [f32; 2],
222262 moving: Option<(onestore::ExGuid, [f32; 4])>,
263 paper: Paper,
223264 primitives: &mut Vec<Primitive<'a>>,
224265 ) {
225266 for object in &outline.objects {
......@@ -241,14 +282,7 @@ impl PageScene {
241282 append_ink(ink, [rect[0], rect[1]], primitives)
242283 }
243284 crate::outline::ObjectKind::Unsupported(_) => {
244 primitives.push(Primitive::Rect {
245 rect,
246 color: colorref(0x00e4ddd6),
247 });
248 primitives.push(Primitive::Rect {
249 rect: [rect[0] + 1.0, rect[1] + 1.0, rect[2] - 1.0, rect[3] - 1.0],
250 color: colorref(0x00faf7f3),
251 });
285 append_placeholder(rect, paper, primitives)
252286 }
253287 crate::outline::ObjectKind::Picture | crate::outline::ObjectKind::File(_) => {}
254288 }
......@@ -257,6 +291,7 @@ impl PageScene {
257291 text: &label.text,
258292 origin: [origin[0] + label.origin[0], origin[1] + label.origin[1]],
259293 clip: None,
294 ink: paper.ink,
260295 });
261296 }
262297 }
......@@ -445,8 +480,9 @@ impl PageScene {
445480 &'a self,
446481 primitives: &mut Vec<Primitive<'a>>,
447482 offset: [f32; 2],
483 paper: Paper,
448484 ) -> Result<(), SceneError> {
449 self.append_primitives_with(primitives, offset, None, None, |_, _, _| {
485 self.append_primitives_with(primitives, offset, None, None, paper, |_, _, _| {
450486 Err(SceneError::MissingOutline)
451487 })
452488 }
......@@ -458,6 +494,7 @@ impl PageScene {
458494 offset: [f32; 2],
459495 editor: Option<&'a CanvasEditor>,
460496 moving: Option<(onestore::ExGuid, [f32; 4])>,
497 paper: Paper,
461498 mut outline: impl FnMut(onestore::ExGuid, [f32; 2], &mut Vec<Primitive<'a>>) -> Result<(), E>,
462499 ) -> Result<(), E> {
463500 for content in self.objects(editor)? {
......@@ -499,18 +536,12 @@ impl PageScene {
499536 x1 + offset[0],
500537 y1 + offset[1],
501538 ];
502 primitives.push(Primitive::Rect {
503 rect,
504 color: colorref(0x00e4ddd6),
505 });
506 primitives.push(Primitive::Rect {
507 rect: [rect[0] + 1.0, rect[1] + 1.0, rect[2] - 1.0, rect[3] - 1.0],
508 color: colorref(0x00faf7f3),
509 });
539 append_placeholder(rect, paper, primitives);
510540 primitives.push(Primitive::Text {
511541 clip: None,
512542 text: &object.label,
513543 origin: [rect[0] + 8.0, rect[1] + 8.0],
544 ink: paper.shade(colorref(0x005d554e)),
514545 });
515546 continue;
516547 }
......@@ -525,28 +556,41 @@ impl PageScene {
525556 };
526557 let object_origin = [origin[0] + offset[0], origin[1] + offset[1]];
527558 match content {
528 Content::Image(source) => primitives.push(Primitive::Image {
529 image: self
530 .images
531 .get(&source.id)
532 .ok_or(SceneError::MissingImage)?,
533 rect: match moving {
534 Some((id, [x0, y0, x1, y1])) if id == source.id => [
535 x0 + offset[0],
536 y0 + offset[1],
537 x1 + offset[0],
538 y1 + offset[1],
539 ],
540 _ => [
541 object_origin[0],
542 object_origin[1],
543 object_origin[0]
544 + source.layout.max_width.ok_or(SceneError::MissingImage)?,
545 object_origin[1]
546 + source.layout.max_height.ok_or(SceneError::MissingImage)?,
547 ],
548 },
549 }),
559 Content::Image(source) => {
560 let (image, [width, height]) = match self.backgrounds.get(&source.id) {
561 // The paper shows until the worker's raster lands.
562 Some(art) => match art.image(paper) {
563 Some(image) => (image, art.size),
564 None => continue,
565 },
566 None => (
567 self.images
568 .get(&source.id)
569 .ok_or(SceneError::MissingImage)?,
570 [
571 source.layout.max_width.ok_or(SceneError::MissingImage)?,
572 source.layout.max_height.ok_or(SceneError::MissingImage)?,
573 ],
574 ),
575 };
576 primitives.push(Primitive::Image {
577 image,
578 rect: match moving {
579 Some((id, [x0, y0, x1, y1])) if id == source.id => [
580 x0 + offset[0],
581 y0 + offset[1],
582 x1 + offset[0],
583 y1 + offset[1],
584 ],
585 _ => [
586 object_origin[0],
587 object_origin[1],
588 object_origin[0] + width,
589 object_origin[1] + height,
590 ],
591 },
592 })
593 }
550594 Content::Outline { .. } | Content::Date { .. } => {
551595 let outline = match content {
552596 Content::Outline { layout, .. } => layout,
......@@ -555,14 +599,16 @@ impl PageScene {
555599 .ok_or(SceneError::MissingOutline)?
556600 .layout(),
557601 };
558 outline.append_table_primitives(primitives, object_origin);
559 outline.append_background_primitives(primitives, object_origin);
560 self.append_outline_objects(outline, object_origin, None, primitives);
561 for (index, paragraph) in outline.paragraphs.iter().enumerate() {
602 outline.append_table_primitives(primitives, object_origin, paper);
603 let everything = [f32::NEG_INFINITY, f32::INFINITY];
604 outline.append_background_primitives(primitives, object_origin, everything);
605 self.append_outline_objects(outline, object_origin, None, paper, primitives);
606 for (index, paragraph) in outline.visible(everything) {
562607 outline.append_paragraph_primitives(
563608 index,
564609 paragraph,
565610 object_origin,
611 paper.ink,
566612 primitives,
567613 );
568614 }
......@@ -575,12 +621,33 @@ impl PageScene {
575621}
576622
577623impl 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`.
578644 pub fn append_background_primitives(
579645 &self,
580646 primitives: &mut Vec<Primitive<'_>>,
581647 origin: [f32; 2],
648 rows: [f32; 2],
582649 ) {
583 for (index, paragraph) in self.paragraphs.iter().enumerate() {
650 for (index, paragraph) in self.visible(rows) {
584651 for (mut rect, color) in paragraph.text.backgrounds() {
585652 rect.x0 += f64::from(paragraph.origin[0]);
586653 rect.x1 += f64::from(paragraph.origin[0]);
......@@ -612,6 +679,7 @@ impl crate::outline::OutlineLayout {
612679 index: usize,
613680 paragraph: &'a crate::outline::ParagraphLayout,
614681 origin: [f32; 2],
682 ink: [f32; 4],
615683 primitives: &mut Vec<Primitive<'a>>,
616684 ) {
617685 let [x, y] = [
......@@ -635,6 +703,7 @@ impl crate::outline::OutlineLayout {
635703 clip,
636704 text: layout,
637705 origin: [x + origin[0], y + origin[1]],
706 ink,
638707 },
639708 // As pen strokes, so hairline rules keep a device pixel.
640709 crate::math::MathItem::Rule([x0, y0, x1, y1]) => {
......@@ -651,7 +720,7 @@ impl crate::outline::OutlineLayout {
651720 to: [x + to[0], y + to[1]],
652721 width,
653722 round: false,
654 color: colorref(math.color),
723 color: math.color.map_or(ink, colorref),
655724 }
656725 }
657726 crate::math::MathItem::Stroke { from, to, width } => Primitive::Segment {
......@@ -659,7 +728,7 @@ impl crate::outline::OutlineLayout {
659728 to: [x + to[0], y + to[1]],
660729 width: *width,
661730 round: true,
662 color: colorref(math.color),
731 color: math.color.map_or(ink, colorref),
663732 },
664733 });
665734 }
......@@ -668,6 +737,7 @@ impl crate::outline::OutlineLayout {
668737 clip,
669738 text: &paragraph.text,
670739 origin: [x, y],
740 ink,
671741 }),
672742 }
673743 for (layout, marker) in &paragraph.markers {
......@@ -675,6 +745,7 @@ impl crate::outline::OutlineLayout {
675745 clip,
676746 text: layout,
677747 origin: [origin[0] + marker[0], y + marker[1]],
748 ink,
678749 });
679750 }
680751 for tag in &paragraph.tags {
......@@ -685,12 +756,17 @@ impl crate::outline::OutlineLayout {
685756 y + tag.origin[1],
686757 ],
687758 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 }],
689760 });
690761 }
691762 }
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 ) {
694770 for table in &self.tables {
695771 let (Some(first), Some(last)) = (table.cells.first(), table.cells.last()) else {
696772 continue;
......@@ -699,7 +775,7 @@ impl crate::outline::OutlineLayout {
699775 continue;
700776 }
701777 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));
703779 primitives.push(Primitive::RoundedRect {
704780 rect: [
705781 bounds[0] + origin[0] - 0.375,
......@@ -1155,6 +1231,7 @@ mod tests {
11551231 [0.0; 2],
11561232 Some(editor),
11571233 None,
1234 Paper::WHITE,
11581235 |_, _, _| Ok(()),
11591236 )
11601237 .unwrap();
......@@ -1553,6 +1630,7 @@ mod tests {
15531630 [0.0; 2],
15541631 Some(editor),
15551632 None,
1633 Paper::WHITE,
15561634 |id, offset, primitives| {
15571635 let outline = editor
15581636 .outlines()
......@@ -1563,6 +1641,7 @@ mod tests {
15631641 primitives.push(Primitive::Text {
15641642 clip: None,
15651643 text: &paragraph.text,
1644 ink: Paper::WHITE.ink,
15661645 origin: [
15671646 outline.origin()[0] + paragraph.origin[0] + offset[0],
15681647 outline.origin()[1] + paragraph.origin[1] + offset[1],
......@@ -1643,7 +1722,7 @@ mod tests {
16431722 let (scene, mut editor) = PageScene::from_page(page(), &mut engine).unwrap();
16441723 let mut reference_primitives = Vec::new();
16451724 reference
1646 .append_primitives(&mut reference_primitives, [0.0; 2])
1725 .append_primitives(&mut reference_primitives, [0.0; 2], Paper::WHITE)
16471726 .unwrap();
16481727 let image_id;
16491728 {
......@@ -1656,11 +1735,13 @@ mod tests {
16561735 clip: None,
16571736 text: a,
16581737 origin: x,
1738 ..
16591739 },
16601740 Primitive::Text {
16611741 clip: None,
16621742 text: b,
16631743 origin: y,
1744 ..
16641745 },
16651746 ) => {
16661747 assert_eq!(x, y);
......@@ -1753,7 +1834,7 @@ mod tests {
17531834 .advance
17541835 );
17551836 assert!(matches!(
1756 scene.append_primitives(&mut Vec::new(), [0.0; 2]),
1837 scene.append_primitives(&mut Vec::new(), [0.0; 2], Paper::WHITE),
17571838 Err(SceneError::MissingOutline)
17581839 ));
17591840 }
......@@ -1804,6 +1885,7 @@ mod tests {
18041885 [5.0, 0.0],
18051886 Some(&editor),
18061887 None,
1888 Paper::WHITE,
18071889 |_, _, _| Ok(()),
18081890 )
18091891 .unwrap();
......@@ -1870,9 +1952,11 @@ mod tests {
18701952 ));
18711953 }
18721954 let mut primitives = Vec::new();
1873 scene.append_primitives(&mut primitives, [0.0; 2]).unwrap();
18741955 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)
18761960 .unwrap();
18771961 let [
18781962 Primitive::Image {
......@@ -1942,6 +2026,7 @@ mod tests {
19422026 [0.0; 2],
19432027 Some(&editor),
19442028 Some((id, [70.0, 5.0, 120.0, 55.0])),
2029 Paper::WHITE,
19452030 |_, _, _| Ok(()),
19462031 )
19472032 .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() {
101101 }
102102 let mut primitives = Vec::new();
103103 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();
105107 } else {
106108 primitives.push(draw::Primitive::Text {
107109 clip: None,
108110 text: layout.as_ref().unwrap(),
109111 origin: [36.0, 90.0],
112 ink: Paper::WHITE.ink,
110113 });
111114 }
112115 let start = Instant::now();
......@@ -150,3 +153,202 @@ fn load_scene(bytes: &[u8], title: &str, engine: &mut TextEngine) -> page::PageS
150153 )
151154 .unwrap()
152155}
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 @@
1use crate::gpu::Viewport;
1use crate::gpu::{Viewport, page::PageScene};
22use crate::{
33 date::DateField,
44 document::TextPosition,
5 editor::{CanvasEditor, Selection, TextOutline},
5 editor::{CanvasEditor, Selection},
6 outline::ParagraphLayout,
67};
78use accesskit::{
89 Action, Affine, Node, NodeId, Rect, Role, TextDirection, TextSelection, TreeId, TreeInfo,
910 TreeUpdate,
1011};
11use onestore::page::text::EditError;
12use onestore::{ExGuid, page::text::EditError};
1213use parley::Affinity;
13use std::{collections::HashMap, ops::Range};
14use std::collections::HashMap;
1415
1516pub 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
1721struct Run {
18 outline: onestore::ExGuid,
1922 id: NodeId,
2023 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>,
2227}
2328
24#[derive(Hash, PartialEq, Eq)]
25struct ParagraphKey {
29/// A paragraph's runs, in paragraph coordinates under a node at its origin, so reflow above
30/// it moves one node.
31struct Paragraph {
32 id: NodeId,
2633 layout: u64,
2734 source: usize,
28 next: Option<usize>,
29 origin: [u32; 2],
35 origin: [f32; 2],
36 /// Its text's byte length in the field's value.
37 length: usize,
38 runs: Vec<Run>,
39}
40
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 }
3051}
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.
3261pub struct Accessibility {
33 outlines: Vec<(onestore::ExGuid, NodeId)>,
34 runs: Vec<Run>,
35 paragraphs: HashMap<ParagraphKey, Range<usize>>,
36 read_only: Vec<NodeId>,
37 dates: Vec<(DateField, NodeId)>,
62 fields: Vec<Field>,
63 read_only: Vec<(NodeId, Node)>,
64 dates: Vec<(DateField, NodeId, Node)>,
3865 next_id: u64,
3966}
4067
4168impl Default for Accessibility {
4269 fn default() -> Self {
4370 Self {
44 outlines: Vec::new(),
45 runs: Vec::new(),
46 paragraphs: HashMap::new(),
71 fields: Vec::new(),
4772 read_only: Vec::new(),
4873 dates: Vec::new(),
49 next_id: 1,
74 next_id: 2,
5075 }
5176 }
5277}
5378
5479impl Accessibility {
55 /// Retire published runs without recycling their IDs.
80 /// Retire published nodes without recycling their IDs; the next update sends the whole tree.
5681 pub fn deactivate(&mut self) {
57 self.runs.clear();
58 self.paragraphs.clear();
59 self.outlines.clear();
82 self.fields.clear();
6083 self.read_only.clear();
6184 self.dates.clear();
6285 }
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
13987 fn allocate(&mut self) -> NodeId {
14088 let id = NodeId(self.next_id);
14189 self.next_id = self
......@@ -146,309 +94,132 @@ impl Accessibility {
14694 }
14795
14896 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)
15098 }
15199
152100 pub fn date_for_node(&self, node: NodeId) -> Option<DateField> {
153101 self.dates
154102 .iter()
155 .find(|(_, id)| *id == node)
156 .map(|(field, _)| *field)
103 .find(|(_, id, _)| *id == node)
104 .map(|(field, ..)| *field)
157105 }
158106
159 pub fn outline_for_node(&self, node: NodeId) -> Option<onestore::ExGuid> {
160 self.outlines
107 pub fn outline_for_node(&self, node: NodeId) -> Option<ExGuid> {
108 self.fields
161109 .iter()
162 .find(|(_, id)| *id == node)
163 .map(|(outline, _)| *outline)
110 .find(|field| field.id == node)
111 .map(|field| field.outline)
164112 }
165113
114 /// The nodes that changed since the last update. After an error, `deactivate` before the
115 /// next update.
166116 pub fn update(
167117 &mut self,
168118 editor: &CanvasEditor,
119 scene: Option<&(PageScene, [f32; 2])>,
169120 viewport: Viewport,
170121 title: &str,
171 preview: Option<(onestore::ExGuid, [f32; 2])>,
122 preview: Option<(ExGuid, [f32; 2])>,
123 read_only_focus: Option<usize>,
172124 ) -> Result<TreeUpdate, EditError> {
173 let mut runs = Vec::new();
174 let mut outlines = Vec::new();
175 let mut paragraph_ranges = HashMap::new();
176 for outline in editor.visible_outlines().chain(editor.caret_outline()) {
177 let field = self
178 .outlines
125 let mut nodes = Vec::new();
126 let mut focus = ROOT;
127 let mut previous: HashMap<_, _> = self
128 .fields
129 .drain(..)
130 .map(|field| (field.outline, field))
131 .collect();
132 for (ordinal, outline) in editor
133 .visible_outlines()
134 .chain(editor.caret_outline())
135 .enumerate()
136 {
137 let (id, sent, old) = match previous.remove(&outline.id) {
138 Some(field) => (field.id, Some(field.node), field.paragraphs),
139 None => (self.allocate(), None, Vec::new()),
140 };
141 let layouts: Vec<_> = outline.layouts().collect();
142 // Only a paragraph with a successor ends its last run with the paragraph break.
143 let same = |old_index: usize, index: usize| {
144 old[old_index].layout == layouts[index].1.text.id()
145 && (old_index + 1 == old.len()) == (index + 1 == layouts.len())
146 };
147 let shorter = old.len().min(layouts.len());
148 let prefix = (0..shorter).take_while(|&i| same(i, i)).count();
149 let suffix = (1..=shorter - prefix)
150 .take_while(|&i| same(old.len() - i, layouts.len() - i))
151 .count();
152 let text = sent.as_ref().and_then(Node::value).unwrap_or_default();
153 let length = |paragraphs: &[Paragraph]| paragraphs.iter().map(|p| p.length).sum();
154 let mut value = text[..length(&old[..prefix])].to_owned();
155 let tail = &text[text.len() - length(&old[old.len() - suffix..])..];
156 let mut paragraphs = old;
157 let kept = paragraphs.split_off(paragraphs.len() - suffix);
158 let mut replaced = paragraphs.split_off(prefix).into_iter();
159 for (at, (source, shaped)) in layouts
179160 .iter()
180 .find(|(id, _)| *id == outline.id)
181 .map(|(_, node)| *node)
182 .unwrap_or_else(|| self.allocate());
183 outlines.push((outline.id, field));
184 let mut paragraphs = outline.layouts().peekable();
185 while let Some((paragraph, shaped)) = paragraphs.next() {
186 let layout = &shaped.text;
187 let origin = shaped.origin;
188 let key = ParagraphKey {
189 layout: layout.id(),
190 source: paragraph,
191 next: paragraphs.peek().map(|(next, _)| *next),
192 origin: origin.map(f32::to_bits),
193 };
194 let start = runs.len();
195 if let Some(range) = self.paragraphs.get(&key) {
196 runs.extend(
197 self.runs[range.clone()]
198 .iter()
199 .map(|run| (run.outline, run.node.clone(), run.positions.clone())),
200 );
201 paragraph_ranges.insert(key, start..runs.len());
202 continue;
203 }
204 let projection = &shaped.projection;
205 let text = projection.text().text();
206 let boundaries: Vec<_> = projection.source_boundaries().collect();
207 let source_position = |byte| -> Result<TextPosition, EditError> {
208 let index = boundaries
209 .binary_search_by_key(&byte, |(byte, _)| *byte)
210 .map_err(|_| EditError::InvalidRange)?;
211 Ok(TextPosition {
212 paragraph,
213 offset: boundaries[index].1,
214 })
161 .enumerate()
162 .take(layouts.len() - suffix)
163 .skip(prefix)
164 {
165 let (id, old) = match replaced.next() {
166 Some(paragraph) => (paragraph.id, paragraph.runs),
167 None => (self.allocate(), Vec::new()),
215168 };
216 let line_count = layout.lines().count();
217 for (line_index, (line, bounds)) in layout.lines().enumerate() {
218 let line_start = runs.len();
219 let mut x = line.metrics().offset;
220 let mut logical_runs = Vec::new();
221 for run in line.runs() {
222 let advance = run.advance();
223 logical_runs.push((run, x));
224 x += advance;
225 }
226 logical_runs.sort_by_key(|(run, _)| run.text_range().start);
227 for (run, x) in logical_runs {
228 // AccessKit stores each character's byte length in a u8.
229 let mut characters = Vec::new();
230 for cluster in run.clusters() {
231 let range = cluster.text_range();
232 if range.is_empty() || range.start == text.len() {
233 continue;
234 }
235 let cluster_text =
236 text.get(range.clone()).ok_or(EditError::InvalidRange)?;
237 if range.len() <= u8::MAX as usize {
238 characters.push((
239 range,
240 cluster.advance(),
241 cluster.is_word_boundary(),
242 ));
243 } else {
244 for (index, (offset, ch)) in cluster_text.char_indices().enumerate()
245 {
246 characters.push((
247 range.start + offset..range.start + offset + ch.len_utf8(),
248 if index == 0 { cluster.advance() } else { 0.0 },
249 index == 0 && cluster.is_word_boundary(),
250 ));
251 }
252 }
253 }
254 let mut advance = 0.0;
255 for chunk in characters.chunks(256) {
256 let width: f32 = chunk.iter().map(|(_, width, _)| width).sum();
257 let left = x + if run.is_rtl() {
258 run.advance() - advance - width
259 } else {
260 advance
261 };
262 let mut node = Node::new(Role::TextRun);
263 node.set_bounds(Rect::new(
264 f64::from(left + origin[0]),
265 f64::from(bounds.top + origin[1]),
266 f64::from(left + width + origin[0]),
267 f64::from(bounds.top + bounds.height + origin[1]),
268 ));
269 node.set_text_direction(if run.is_rtl() {
270 TextDirection::RightToLeft
271 } else {
272 TextDirection::LeftToRight
273 });
274 node.set_font_size(run.font_size());
275 let start = chunk[0].0.start;
276 let end = chunk.last().unwrap().0.end;
277 node.set_value(text.get(start..end).ok_or(EditError::InvalidRange)?);
278 node.set_character_lengths(
279 chunk
280 .iter()
281 .map(|(range, _, _)| u8::try_from(range.len()).unwrap())
282 .collect::<Vec<_>>(),
283 );
284 let mut offset = 0.0;
285 node.set_character_positions(
286 chunk
287 .iter()
288 .map(|(_, width, _)| {
289 let position = offset;
290 offset += width;
291 position
292 })
293 .collect::<Vec<_>>(),
294 );
295 node.set_character_widths(
296 chunk.iter().map(|(_, width, _)| *width).collect::<Vec<_>>(),
297 );
298 node.set_word_starts(
299 chunk
300 .iter()
301 .enumerate()
302 .filter_map(|(index, (_, _, word))| word.then_some(index as u8))
303 .collect::<Vec<_>>(),
304 );
305 let positions = chunk
306 .iter()
307 .map(|(range, _, _)| source_position(range.start))
308 .chain(std::iter::once(source_position(end)))
309 .collect::<Result<Vec<_>, _>>()?;
310 runs.push((outline.id, node, positions));
311 advance += width;
169 let mut runs = Vec::new();
170 for (index, (node, offsets)) in runs_of(shaped, at + 1 < layouts.len())?
171 .into_iter()
172 .enumerate()
173 {
174 let id = match old.get(index) {
175 Some(old) if old.offsets == offsets && old.node.value() == node.value() => {
176 old.id
312177 }
313 }
314 // Empty lines still need a text position for selection and insertion.
315 if runs.len() == line_start {
316 let mut node = Node::new(Role::TextRun);
317 node.set_value("");
318 node.set_character_lengths(Vec::<u8>::new());
319 node.set_character_positions(Vec::<f32>::new());
320 node.set_character_widths(Vec::<f32>::new());
321 node.set_text_direction(TextDirection::LeftToRight);
322 node.set_bounds(Rect::new(
323 f64::from(origin[0]),
324 f64::from(bounds.top + origin[1]),
325 f64::from(origin[0]),
326 f64::from(bounds.top + bounds.height + origin[1]),
327 ));
328 runs.push((
329 outline.id,
330 node,
331 vec![source_position(bounds.source.start)?],
332 ));
333 }
334 if line_index + 1 == line_count
335 && let Some((next, _)) = paragraphs.peek()
178 _ => self.allocate(),
179 };
180 runs.push(Run { id, node, offsets });
181 }
182 for index in 1..runs.len() {
183 if runs[index - 1].node.bounds().map(|r| r.y0)
184 == runs[index].node.bounds().map(|r| r.y0)
336185 {
337 let (_, node, source_positions) = runs.last_mut().unwrap();
338 let mut value = node.value().unwrap().to_owned();
339 value.push('\n');
340 node.set_value(value);
341 let mut lengths = node.character_lengths().to_vec();
342 lengths.push(1);
343 node.set_character_lengths(lengths);
344 if let Some(rect) = node.bounds() {
345 let mut positions =
346 node.character_positions().unwrap_or_default().to_vec();
347 positions.push(rect.width() as f32);
348 node.set_character_positions(positions);
349 let mut widths = node.character_widths().unwrap_or_default().to_vec();
350 widths.push(4.0);
351 node.set_character_widths(widths);
352 }
353 source_positions.push(TextPosition {
354 paragraph: *next,
355 offset: 0,
356 });
186 let (before, after) = runs.split_at_mut(index);
187 before[index - 1].node.set_next_on_line(after[0].id);
188 after[0].node.set_previous_on_line(before[index - 1].id);
357189 }
358190 }
359 if !shaped.tags.is_empty() {
360 let descriptions = shaped
361 .tags
362 .iter()
363 .map(|tag| {
364 use crate::outline::TagIcon;
365 let fallback = match tag.icon {
366 TagIcon::CheckBox { .. } => "To do",
367 TagIcon::Question => "Question",
368 TagIcon::Music => "Music",
369 TagIcon::Exclamation => "Critical",
370 TagIcon::RedSquare => "Project A",
371 TagIcon::YellowSquare => "Project B",
372 TagIcon::BlueSquare => "Project C",
373 };
374 let label = if tag.label.is_empty() {
375 fallback
376 } else {
377 &tag.label
378 };
379 let state = match tag.icon {
380 TagIcon::CheckBox { checked: true } => ", completed",
381 TagIcon::CheckBox { checked: false } => ", incomplete",
382 _ => "",
383 };
384 format!(
385 "{label}{state}{}",
386 if tag.disabled { ", disabled" } else { "" }
387 )
388 })
389 .collect::<Vec<_>>()
390 .join("; ");
391 runs[start].1.set_description(descriptions);
392 }
393 paragraph_ranges.insert(key, start..runs.len());
394 }
395 }
396 let mut runs: Vec<_> = runs
397 .into_iter()
398 .enumerate()
399 .map(|(index, (outline, mut node, positions))| {
400 node.clear_next_on_line();
401 node.clear_previous_on_line();
402 let id = match self.runs.get(index) {
403 Some(old)
404 if old.outline == outline
405 && old.positions == positions
406 && old.node.value() == node.value() =>
191 for (index, run) in runs.iter().enumerate() {
192 if old
193 .get(index)
194 .is_none_or(|old| old.id != run.id || old.node != run.node)
407195 {
408 old.id
196 nodes.push((run.id, run.node.clone()));
409197 }
410 _ => self.allocate(),
411 };
412 Run {
413 outline,
198 }
199 let start = value.len();
200 value.extend(runs.iter().filter_map(|run| run.node.value()));
201 let paragraph = Paragraph {
414202 id,
415 node,
416 positions,
203 layout: shaped.text.id(),
204 source: *source,
205 origin: shaped.origin,
206 length: value.len() - start,
207 runs,
208 };
209 nodes.push((id, paragraph.node()));
210 paragraphs.push(paragraph);
211 }
212 paragraphs.extend(kept);
213 value.push_str(tail);
214 for (paragraph, (source, shaped)) in paragraphs.iter_mut().zip(&layouts) {
215 paragraph.source = *source;
216 if paragraph.origin != shaped.origin {
217 paragraph.origin = shaped.origin;
218 nodes.push((paragraph.id, paragraph.node()));
417219 }
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);
428220 }
429 }
430 self.runs = runs;
431 self.paragraphs = paragraph_ranges;
432 self.outlines = outlines;
433 let mut root = Node::new(Role::Window);
434 root.set_label(title);
435 root.set_children(self.outlines.iter().map(|(_, id)| *id).collect::<Vec<_>>());
436 root.set_bounds(Rect::new(
437 0.0,
438 0.0,
439 viewport.size[0] as f64,
440 viewport.size[1] as f64,
441 ));
442 let mut nodes = vec![(ROOT, root)];
443 let mut focus = ROOT;
444 for (index, (outline, (_, id))) in editor
445 .visible_outlines()
446 .chain(editor.caret_outline())
447 .zip(&self.outlines)
448 .enumerate()
449 {
450 let mut field = Node::new(Role::MultilineTextInput);
451 field.set_label(if outline.title {
221 let mut node = Node::new(Role::MultilineTextInput);
222 node.set_label(if outline.title {
452223 "Page title".into()
453224 } else if editor
454225 .caret_outline()
......@@ -456,47 +227,121 @@ impl Accessibility {
456227 {
457228 "Text input".into()
458229 } else {
459 format!("Text outline {}", index + 1)
230 format!("Text outline {}", ordinal + 1)
460231 });
461 field.add_action(Action::Focus);
462 field.add_action(Action::SetTextSelection);
463 field.add_action(Action::ReplaceSelectedText);
464 field.add_action(Action::SetValue);
465 let runs = self.runs.iter().filter(|run| run.outline == outline.id);
466 field.set_value(
467 runs.clone()
468 .filter_map(|run| run.node.value())
469 .collect::<String>(),
232 node.add_action(Action::Focus);
233 node.add_action(Action::SetTextSelection);
234 node.add_action(Action::ReplaceSelectedText);
235 node.add_action(Action::SetValue);
236 node.set_value(value);
237 node.set_children(
238 paragraphs
239 .iter()
240 .map(|paragraph| paragraph.id)
241 .collect::<Vec<_>>(),
470242 );
471 field.set_children(runs.map(|run| run.id).collect::<Vec<_>>());
472243 let origin = preview
473244 .filter(|(id, _)| *id == outline.id)
474245 .map(|(_, origin)| origin)
475246 .unwrap_or_else(|| outline.origin());
476 field.set_transform(Affine::new([
477 f64::from(viewport.scale),
478 0.0,
479 0.0,
480 f64::from(viewport.scale),
481 f64::from(viewport.origin[0]) + f64::from(origin[0]) * f64::from(viewport.scale),
482 f64::from(viewport.origin[1]) + f64::from(origin[1]) * f64::from(viewport.scale),
483 ]));
247 node.set_transform(Affine::translate((
248 f64::from(origin[0]),
249 f64::from(origin[1]),
250 )));
484251 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()));
486253 if outline.id == editor.active_outline().id {
487254 let Selection {
488255 positions: [anchor, caret],
489256 affinities,
490257 } = editor.selection();
491 field.set_text_selection(TextSelection {
492 anchor: self.position(outline, anchor, affinities[0])?,
493 focus: self.position(outline, caret, affinities[1])?,
258 node.set_text_selection(TextSelection {
259 anchor: position(&layouts, &paragraphs, anchor, affinities[0])?,
260 focus: position(&layouts, &paragraphs, caret, affinities[1])?,
494261 });
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));
496322 }
497 nodes.push((*id, field));
498323 }
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));
500345 Ok(TreeUpdate {
501346 nodes,
502347 tree: Some(TreeInfo::new(ROOT)),
......@@ -505,71 +350,47 @@ impl Accessibility {
505350 })
506351 }
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
555353 pub fn selection(
556354 &self,
557 outline: onestore::ExGuid,
355 outline: ExGuid,
558356 selection: TextSelection,
559357 ) -> Result<Selection, EditError> {
358 let field = self
359 .fields
360 .iter()
361 .find(|field| field.outline == outline)
362 .ok_or(EditError::InvalidRange)?;
560363 let position = |position: accesskit::TextPosition| {
561 let run = self
562 .runs
364 let (index, paragraph, run) = field
365 .paragraphs
563366 .iter()
564 .find(|run| run.outline == outline && run.id == position.node)
565 .ok_or(EditError::InvalidRange)?;
566 let source = *run
567 .positions
568 .get(position.character_index)
367 .enumerate()
368 .find_map(|(index, paragraph)| {
369 let run = paragraph
370 .runs
371 .iter()
372 .position(|run| run.id == position.node)?;
373 Some((index, paragraph, run))
374 })
569375 .ok_or(EditError::InvalidRange)?;
570 let affinity = if position.character_index > 0
571 && position.character_index + 1 == run.positions.len()
572 {
376 let offsets = &paragraph.runs[run].offsets;
377 let breaks = run + 1 == paragraph.runs.len() && index + 1 < field.paragraphs.len();
378 let count = offsets.len() + usize::from(breaks);
379 let character = position.character_index;
380 if character >= count {
381 return Err(EditError::InvalidRange);
382 }
383 let source = match offsets.get(character) {
384 Some(offset) => TextPosition {
385 paragraph: paragraph.source,
386 offset: *offset,
387 },
388 None => TextPosition {
389 paragraph: field.paragraphs[index + 1].source,
390 offset: 0,
391 },
392 };
393 let affinity = if character > 0 && character + 1 == count {
573394 Affinity::Upstream
574395 } else {
575396 Affinity::Downstream
......@@ -585,13 +406,304 @@ impl Accessibility {
585406 }
586407}
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
588629#[cfg(test)]
589mod tests {
630pub(super) mod tests {
590631 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};
592634 use onestore::document::Format;
593635 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
595707 #[test]
596708 fn title_flow_updates_read_only_hit_and_accessibility_bounds() {
597709 use onestore::page::{Page, PageObject, Title, Unsupported};
......@@ -649,6 +761,7 @@ mod tests {
649761 scale: 2.0,
650762 };
651763 let mut access = Accessibility::default();
764 let mut tree = None;
652765 let mut identity = None;
653766 for phase in 0..4 {
654767 match phase {
......@@ -664,17 +777,18 @@ mod tests {
664777 }
665778 _ => {}
666779 }
667 let mut update = access.update(&editor, viewport, "Test", None).unwrap();
668 access.append_page_fields(&mut update, Some(&scene), &editor, viewport, Some(0));
669 let (id, node) = update
670 .nodes
671 .iter()
672 .find(|(_, node)| node.role() == Role::Label)
780 let update = access
781 .update(&editor, Some(&scene), viewport, "Test", None, Some(0))
673782 .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;
674788 if let Some(identity) = identity {
675 assert_eq!(*id, identity);
789 assert_eq!(id, identity);
676790 }
677 identity = Some(*id);
791 identity = Some(id);
678792 let y = if phase % 2 == 0 { 80.0 } else { 145.92 };
679793 let expected = Rect::new(
680794 310.0,
......@@ -682,7 +796,7 @@ mod tests {
682796 710.0,
683797 f64::from((y + 100.0) * 2.0 - 20.0),
684798 );
685 let bounds = node.bounds().unwrap();
799 let bounds = node.bounding_box().unwrap();
686800 assert_eq!(bounds.x0, expected.x0);
687801 assert_eq!(bounds.x1, expected.x1);
688802 assert!((bounds.y0 - expected.y0).abs() < 0.001);
......@@ -726,6 +840,7 @@ mod tests {
726840 let (scene, mut editor) = PageScene::from_page(page, &mut engine).unwrap();
727841 let scene = (scene, [30.0, 40.0]);
728842 let mut access = Accessibility::default();
843 let mut tree = None;
729844 let mut identity = None;
730845 for scale in [1.0, 2.0, 0.5] {
731846 editor.insert(&mut engine, "annotation ").unwrap();
......@@ -734,29 +849,31 @@ mod tests {
734849 origin: [-200.0, 10.0],
735850 scale,
736851 };
737 let mut update = access.update(&editor, viewport, "Test", None).unwrap();
738 access.append_page_fields(&mut update, Some(&scene), &editor, viewport, None);
739 let (id, node) = update
740 .nodes
741 .iter()
742 .find(|(_, node)| node.role() == Role::Label)
852 let update = access
853 .update(&editor, Some(&scene), viewport, "Test", None, None)
743854 .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;
744861 if let Some(identity) = identity {
745 assert_eq!(*id, identity);
862 assert_eq!(id, identity);
746863 }
747 identity = Some(*id);
748 assert_eq!(node.value(), Some("Unsupported content\nRead-only"));
864 identity = Some(id);
865 assert_eq!(node.data().value(), Some("Unsupported content\nRead-only"));
749866 assert!(node.is_read_only());
750 assert!(node.supports_action(Action::Focus));
867 assert!(node.data().supports_action(Action::Focus));
751868 for action in [
752869 Action::SetValue,
753870 Action::ReplaceSelectedText,
754871 Action::SetTextSelection,
755872 ] {
756 assert!(!node.supports_action(action));
873 assert!(!node.data().supports_action(action));
757874 }
758875 assert_eq!(
759 node.bounds(),
876 node.bounding_box(),
760877 Some(Rect::new(
761878 f64::from(40.0 * scale - 200.0),
762879 f64::from(60.0 * scale + 10.0),
......@@ -764,19 +881,22 @@ mod tests {
764881 f64::from(120.0 * scale + 10.0)
765882 ))
766883 );
767 assert_eq!(access.outline_for_node(*id), None);
768 let tree = accesskit_consumer::Tree::new(update, true);
884 assert_eq!(access.outline_for_node(id), None);
769885 assert_eq!(
770 tree.state().focus().unwrap().document_range().text(),
886 state.focus().unwrap().document_range().text(),
771887 "annotation "
772888 );
773 let mut focused = access.update(&editor, viewport, "Test", None).unwrap();
774 access.append_page_fields(&mut focused, Some(&scene), &editor, viewport, Some(0));
889 let focused = access
890 .update(&editor, Some(&scene), viewport, "Test", None, Some(0))
891 .unwrap();
775892 assert_eq!(focused.focus, identity.unwrap());
776893 assert_eq!(access.read_only_for_node(focused.focus), Some(0));
777 let tree = accesskit_consumer::Tree::new(focused, true);
778894 assert_eq!(
779 tree.state().focus().unwrap().value().as_deref(),
895 apply(&mut tree, focused)
896 .focus()
897 .unwrap()
898 .value()
899 .as_deref(),
780900 Some("Unsupported content\nRead-only")
781901 );
782902 editor.undo(&mut engine).unwrap();
......@@ -800,7 +920,7 @@ mod tests {
800920 .collect(),
801921 )
802922 .unwrap();
803 let mut nodes = text.nodes().to_vec();
923 let mut paragraphs = text.nodes().to_vec();
804924 let mut definitions = BTreeMap::new();
805925 for (index, (shape, label, status)) in [
806926 (3, Some("Rehearsal"), 0),
......@@ -828,7 +948,7 @@ mod tests {
828948 format: Format::default(),
829949 },
830950 );
831 nodes[index].text_mut().unwrap().tags.push(Tag {
951 paragraphs[index].text_mut().unwrap().tags.push(Tag {
832952 definition: Some(id),
833953 action_type: None,
834954 status,
......@@ -855,13 +975,14 @@ mod tests {
855975 ..Layout::default()
856976 },
857977 indents: vec![18.0, 0.0],
858 paragraphs: nodes,
978 paragraphs,
859979 unsupported: Vec::new(),
860980 }],
861981 definitions,
862982 )
863983 .unwrap();
864984 let mut access = Accessibility::default();
985 let mut tree = None;
865986 let viewport = Viewport {
866987 size: [800, 600],
867988 origin: [48.0; 2],
......@@ -874,12 +995,14 @@ mod tests {
874995 if step == 3 {
875996 editor.undo(&mut engine).unwrap();
876997 }
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);
8781002 assert_eq!(
879 update
880 .nodes
1003 nodes(state)
8811004 .iter()
882 .filter_map(|(_, n)| n.description())
1005 .filter_map(|node| node.data().description())
8831006 .collect::<Vec<_>>(),
8841007 [
8851008 "Rehearsal, incomplete",
......@@ -888,8 +1011,7 @@ mod tests {
8881011 "Music, disabled"
8891012 ]
8901013 );
891 let tree = accesskit_consumer::Tree::new(update, true);
892 let text = tree.state().focus().unwrap().document_range().text();
1014 let text = state.focus().unwrap().document_range().text();
8931015 assert_eq!(
8941016 text,
8951017 if step == 2 {
......@@ -929,24 +1051,27 @@ mod tests {
9291051 CanvasEditor::new(&mut engine, TextDocument::from_nodes(nodes).unwrap(), 180.0)
9301052 .unwrap();
9311053 let mut access = Accessibility::default();
1054 let mut tree = None;
9321055 let viewport = Viewport {
9331056 size: [900, 700],
9341057 scale: 2.0,
9351058 origin: [13.0, 19.0],
9361059 };
937 let update = access.update(&editor, viewport, "Test", None).unwrap();
938 let tree = accesskit_consumer::Tree::new(update, true);
939 let field = tree.state().focus().unwrap();
1060 let update = access
1061 .update(&editor, None, viewport, "Test", None, None)
1062 .unwrap();
1063 let field = apply(&mut tree, update).focus().unwrap();
9401064 assert_eq!(
9411065 field.document_range().text(),
9421066 "root\nfolded\nlast\nfolded tail"
9431067 );
944 assert!(
945 access
946 .runs
1068 assert_eq!(
1069 access.fields[0]
1070 .paragraphs
9471071 .iter()
948 .flat_map(|run| &run.positions)
949 .all(|position| ![2, 5].contains(&position.paragraph))
1072 .map(|paragraph| paragraph.source)
1073 .collect::<Vec<_>>(),
1074 [0, 1, 3, 4]
9501075 );
9511076 let source = access
9521077 .selection(
......@@ -969,10 +1094,11 @@ mod tests {
9691094 }
9701095 );
9711096 editor.select(source).unwrap();
972 let update = access.update(&editor, viewport, "Test", None).unwrap();
973 let tree = accesskit_consumer::Tree::new(update, true);
1097 let update = access
1098 .update(&editor, None, viewport, "Test", None, None)
1099 .unwrap();
9741100 assert_eq!(
975 tree.state()
1101 apply(&mut tree, update)
9761102 .focus()
9771103 .unwrap()
9781104 .text_selection()
......@@ -989,23 +1115,20 @@ mod tests {
9891115 .into(),
9901116 )
9911117 .unwrap();
992 access.update(&editor, viewport, "Test", None).unwrap();
1118 let update = access
1119 .update(&editor, None, viewport, "Test", None, None)
1120 .unwrap();
9931121 let caret = editor.caret(1.0).unwrap();
9941122 assert_eq!(caret.x0, 27.0);
995 let position = access
996 .position(
997 editor.active_outline(),
998 editor.selection().positions[1],
999 Affinity::Downstream,
1000 )
1001 .unwrap();
1002 let run = access
1003 .runs
1004 .iter()
1005 .find(|run| run.id == position.node)
1006 .unwrap();
1007 assert_eq!(run.node.bounds().unwrap().x0, 27.0);
1008 assert_eq!(run.node.bounds().unwrap().y0, caret.y0);
1123 let rects = apply(&mut tree, update)
1124 .focus()
1125 .unwrap()
1126 .text_selection()
1127 .unwrap()
1128 .bounding_boxes();
1129 assert_eq!(rects.len(), 1);
1130 assert_eq!(rects[0].x0, 27.0 * 2.0 + 13.0);
1131 assert_eq!(rects[0].y0, caret.y0 * 2.0 + 19.0);
10091132 }
10101133
10111134 #[test]
......@@ -1043,12 +1166,15 @@ mod tests {
10431166 .unwrap();
10441167 let mut editor = CanvasEditor::new(&mut engine, document, width).unwrap();
10451168 let mut access = Accessibility::default();
1169 let mut tree = None;
10461170 let viewport = Viewport {
10471171 size: [800, 600],
10481172 origin: [48.0; 2],
10491173 scale: 2.0,
10501174 };
1051 let update = access.update(&editor, viewport, "Test", None).unwrap();
1175 let update = access
1176 .update(&editor, None, viewport, "Test", None, None)
1177 .unwrap();
10521178 for (_, node) in &update.nodes {
10531179 if node.role() == Role::TextRun {
10541180 assert_eq!(
......@@ -1066,18 +1192,18 @@ mod tests {
10661192 }
10671193 }
10681194 }
1069 let tree = accesskit_consumer::Tree::new(update, true);
1070 let field = tree.state().focus().unwrap();
1195 let field = apply(&mut tree, update).focus().unwrap();
10711196 assert_eq!(field.document_range().text(), text, "width {width}");
10721197 let selection = field.document_range().to_text_selection();
10731198 let source_selection = access
10741199 .selection(editor.active_outline().id, selection)
10751200 .unwrap();
10761201 editor.select(source_selection).unwrap();
1077 let selected = access.update(&editor, viewport, "Test", None).unwrap();
1078 let tree = accesskit_consumer::Tree::new(selected, true);
1202 let selected = access
1203 .update(&editor, None, viewport, "Test", None, None)
1204 .unwrap();
10791205 assert_eq!(
1080 tree.state()
1206 apply(&mut tree, selected)
10811207 .focus()
10821208 .unwrap()
10831209 .text_selection()
......@@ -1085,48 +1211,57 @@ mod tests {
10851211 .text(),
10861212 text
10871213 );
1088 let stable_ids: Vec<_> = access.runs.iter().map(|run| run.id).collect();
1089 access
1214 let zoomed = access
10901215 .update(
10911216 &editor,
1217 None,
10921218 Viewport {
10931219 scale: 3.0,
10941220 ..viewport
10951221 },
10961222 "Test",
10971223 None,
1224 None,
10981225 )
10991226 .unwrap();
11001227 assert_eq!(
1101 stable_ids,
1102 access.runs.iter().map(|run| run.id).collect::<Vec<_>>()
1228 zoomed.nodes.iter().map(|(id, _)| *id).collect::<Vec<_>>(),
1229 [ROOT, PAGE]
11031230 );
1231 apply(&mut tree, zoomed);
11041232 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);
11061237 assert!(
11071238 access
11081239 .selection(editor.active_outline().id, selection)
11091240 .is_err()
11101241 );
1111 let selection = access
1112 .update(&editor, viewport, "Test", None)
1113 .unwrap()
1114 .nodes[1]
1115 .1
1116 .text_selection()
1117 .copied()
1118 .unwrap();
1242 let selection = *state.focus().unwrap().data().text_selection().unwrap();
11191243 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);
11211248 assert!(
11221249 access
11231250 .selection(editor.active_outline().id, selection)
11241251 .is_err()
11251252 );
11261253 editor.undo(&mut engine).unwrap();
1127 let update = access.update(&editor, viewport, "Test", None).unwrap();
1128 let tree = accesskit_consumer::Tree::new(update, true);
1129 assert_eq!(tree.state().focus().unwrap().document_range().text(), text);
1254 let update = access
1255 .update(&editor, None, viewport, "Test", None, None)
1256 .unwrap();
1257 assert_eq!(
1258 apply(&mut tree, update)
1259 .focus()
1260 .unwrap()
1261 .document_range()
1262 .text(),
1263 text
1264 );
11301265 }
11311266 }
11321267 }
......@@ -1166,6 +1301,7 @@ mod tests {
11661301 .collect();
11671302 let mut editor = CanvasEditor::new(&mut engine, document, 48.0).unwrap();
11681303 let mut access = Accessibility::default();
1304 let mut tree = None;
11691305 let viewport = Viewport {
11701306 size: [800, 600],
11711307 origin: [31.0, -57.0],
......@@ -1181,9 +1317,10 @@ mod tests {
11811317 })
11821318 .unwrap();
11831319 let caret = editor.caret(0.0).unwrap();
1184 let update = access.update(&editor, viewport, "Test", None).unwrap();
1185 let tree = accesskit_consumer::Tree::new(update, true);
1186 let field = tree.state().focus().unwrap();
1320 let update = access
1321 .update(&editor, None, viewport, "Test", None, None)
1322 .unwrap();
1323 let field = apply(&mut tree, update).focus().unwrap();
11871324 assert_eq!(
11881325 field.document_range().text(),
11891326 "abc e\u{301} 🌳 words wrap here\n\nabc שלום def عالم end\nlast\nline\n"
......@@ -1217,7 +1354,7 @@ mod tests {
12171354 }
12181355 #[test]
12191356 fn keyboard_and_accessibility_preserve_visual_caret_at_wraps() {
1220 use crate::editor::Movement;
1357 use draw::edit::Movement;
12211358 let mut engine = TextEngine::default();
12221359 let document = TextDocument::new(vec![
12231360 Paragraph::new(
......@@ -1230,6 +1367,7 @@ mod tests {
12301367 .unwrap();
12311368 let mut editor = CanvasEditor::new(&mut engine, document, 72.0).unwrap();
12321369 let mut access = Accessibility::default();
1370 let mut tree = None;
12331371 let viewport = Viewport {
12341372 size: [800, 600],
12351373 origin: [0.0; 2],
......@@ -1256,11 +1394,10 @@ mod tests {
12561394 ] {
12571395 editor.move_selection(&mut engine, movement, false).unwrap();
12581396 let caret = editor.caret(0.0).unwrap();
1259 let tree = accesskit_consumer::Tree::new(
1260 access.update(&editor, viewport, "Test", None).unwrap(),
1261 true,
1262 );
1263 let field = tree.state().focus().unwrap();
1397 let update = access
1398 .update(&editor, None, viewport, "Test", None, None)
1399 .unwrap();
1400 let field = apply(&mut tree, update).focus().unwrap();
12641401 let selection = field.text_selection().unwrap();
12651402 let rects = selection.bounding_boxes();
12661403 assert_eq!(rects.len(), 1);
......@@ -1303,7 +1440,7 @@ mod tests {
13031440 .unwrap();
13041441 let mut editor = CanvasEditor::new(&mut engine, document, 72.0).unwrap();
13051442 let mut cached = Accessibility::default();
1306 let mut rebuilt = Accessibility::default();
1443 let mut tree = None;
13071444 let mut compare = |editor: &CanvasEditor, step: usize| {
13081445 let viewport = Viewport {
13091446 size: [800, 600],
......@@ -1314,28 +1451,44 @@ mod tests {
13141451 .is_multiple_of(2)
13151452 .then_some((editor.active_outline().id, [13.5, -7.25]));
13161453 for repeat in 0..2 {
1317 rebuilt.paragraphs.clear();
1318 let actual = cached.update(editor, viewport, "Test", preview).unwrap();
1319 let expected = rebuilt.update(editor, viewport, "Test", preview).unwrap();
1320 assert_eq!(actual.nodes, expected.nodes, "step {step}, repeat {repeat}");
1321 assert_eq!(actual.focus, expected.focus);
1322 let tree = accesskit_consumer::Tree::new(actual, true);
1323 let field = tree.state().focus().unwrap();
1324 let selection = field.document_range().to_text_selection();
1325 assert_eq!(
1326 cached
1327 .selection(editor.active_outline().id, selection)
1328 .unwrap(),
1454 let update = cached
1455 .update(editor, None, viewport, "Test", preview, None)
1456 .unwrap();
1457 let actual = apply(&mut tree, update);
1458 let mut rebuilt = Accessibility::default();
1459 let expected = Tree::new(
13291460 rebuilt
1330 .selection(editor.active_outline().id, selection)
1331 .unwrap()
1461 .update(editor, None, viewport, "Test", preview, None)
1462 .unwrap(),
1463 true,
13321464 );
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);
13331479 let count = editor
13341480 .outlines()
13351481 .iter()
13361482 .map(|o| o.layouts().count())
13371483 .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 );
13391492 }
13401493 };
13411494 compare(&editor, 0);
......@@ -1405,6 +1558,72 @@ mod tests {
14051558 compare(&editor, 162);
14061559 }
14071560 }
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
14081627 #[test]
14091628 fn outlines_have_independent_accessibility_identity_and_selection() {
14101629 let mut engine = TextEngine::default();
......@@ -1422,7 +1641,9 @@ mod tests {
14221641 scale: 2.0,
14231642 origin: [48.0; 2],
14241643 };
1425 let update = access.update(&editor, viewport, "Test", None).unwrap();
1644 let update = access
1645 .update(&editor, None, viewport, "Test", None, None)
1646 .unwrap();
14261647 let fields: Vec<_> = update
14271648 .nodes
14281649 .iter()
......@@ -1439,35 +1660,45 @@ mod tests {
14391660 assert_ne!(fields[0].0, fields[1].0);
14401661 assert_eq!(access.outline_for_node(fields[0].0), Some(first));
14411662 assert_eq!(access.outline_for_node(fields[1].0), Some(second));
1442 let tree = accesskit_consumer::Tree::new(update, true);
1443 let field = tree.state().focus().unwrap();
1663 let mut tree = None;
1664 let field = apply(&mut tree, update).focus().unwrap();
14441665 assert_eq!(field.document_range().text(), "annotation 🌳");
14451666 let selection = field.document_range().to_text_selection();
14461667 assert!(access.selection(first, selection).is_err());
14471668 let source_selection = access.selection(second, selection).unwrap();
14481669 editor.select(source_selection).unwrap();
14491670 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 )
14511679 .unwrap();
1452 let preview_tree = accesskit_consumer::Tree::new(preview, true);
1453 let preview_field = preview_tree.state().focus().unwrap();
1680 let preview_field = apply(&mut tree, preview).focus().unwrap();
14541681 let rect = preview_field.document_range().bounding_boxes()[0];
14551682 assert_eq!(rect.x0, 848.0);
14561683 assert_eq!(rect.y0, 148.0);
14571684 assert_eq!(editor.active_outline().origin(), [300.0, 40.0]);
14581685 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);
14601690 assert_eq!(access.outline_for_node(fields[1].0), Some(second));
14611691 assert!(access.selection(second, selection).is_ok());
14621692 editor.undo(&mut engine).unwrap();
14631693 editor.undo(&mut engine).unwrap();
14641694 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();
14661698 assert_eq!(
1467 update
1468 .nodes
1699 nodes(apply(&mut tree, update))
14691700 .iter()
1470 .filter(|(_, node)| node.role() == Role::MultilineTextInput)
1701 .filter(|node| node.role() == Role::MultilineTextInput)
14711702 .count(),
14721703 1
14731704 );
crates/canvas/src/interaction/mod.rs+109-183
......@@ -11,15 +11,16 @@ pub use scroll::Scroll;
1111#[cfg(test)]
1212mod tests;
1313
14use crate::gpu::{Viewport, page::PageScene};
14use crate::gpu::{Paper, Viewport, page::PageScene};
1515use crate::{
1616 date::DateField,
17 editor::{
18 CanvasEditor, DEFAULT_OUTLINE_WIDTH, Movement, Selection, SelectionUnit, TextOutline,
19 },
17 editor::{CanvasEditor, DEFAULT_OUTLINE_WIDTH, Selection, TextOutline},
2018 layout::TextEngine,
2119};
22use draw::{Primitive, Stroke};
20use draw::{
21 Primitive, Stroke,
22 edit::{self, Clicks, Command, Key, Modifiers, Movement, NamedKey, SelectionUnit},
23};
2324use std::{
2425 error::Error,
2526 time::{Duration, Instant},
......@@ -31,39 +32,6 @@ pub const DATE_LABELS: [&str; 2] = ["Page date", "Page time"];
3132
3233pub 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
6735#[derive(Clone, Copy, Debug, PartialEq)]
6836pub enum Cursor {
6937 Default,
......@@ -89,12 +57,14 @@ pub enum Request {
8957 CharacterPalette,
9058}
9159
92/// What an event did: `changed` means the page, selection or view changed (the host saves,
93/// updates accessibility and redraws); `redraw` alone repaints hover feedback.
60/// What an event did: `changed` means the page or selection changed (the host saves,
61/// updates accessibility and redraws); `moved` that only the view scrolled or zoomed (the
62/// host updates accessibility and redraws); `redraw` alone repaints hover feedback.
9463#[must_use]
9564#[derive(Debug, Default, PartialEq)]
9665pub struct Response {
9766 pub changed: bool,
67 pub moved: bool,
9868 pub redraw: bool,
9969 pub request: Option<Request>,
10070}
......@@ -104,7 +74,7 @@ impl Response {
10474 Self {
10575 changed: true,
10676 redraw: true,
107 request: None,
77 ..Self::default()
10878 }
10979 }
11080
......@@ -123,11 +93,12 @@ impl Response {
12393 }
12494}
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.
12797#[derive(Clone, Copy, Debug)]
12898pub struct TextColors {
12999 pub caret: [f32; 4],
130100 pub selection: [f32; 4],
101 pub paper: Paper,
131102}
132103
133104/// How to paint an outline: whether the caret is in its blink-on phase, the view scale,
......@@ -138,6 +109,9 @@ struct Paint {
138109 scale: f32,
139110 pixel: f32,
140111 colors: TextColors,
112 /// The document's top and bottom the view shows; paragraphs wholly outside it are
113 /// not painted.
114 visible: [f32; 2],
141115}
142116
143117enum Drag {
......@@ -205,10 +179,9 @@ pub struct PageView {
205179 pub viewport: Viewport,
206180 /// Device pixels per logical pixel.
207181 display_scale: f32,
208 double_click: Duration,
182 clicks: Clicks,
209183 pointer: [f32; 2],
210184 pointer_inside: bool,
211 last_click: Option<(Instant, [f32; 2], u8)>,
212185 drag: Option<Drag>,
213186 object_focus: Option<ObjectFocus>,
214187 modifiers: Modifiers,
......@@ -242,10 +215,9 @@ impl PageView {
242215 origin: home(display_scale),
243216 },
244217 display_scale,
245 double_click,
218 clicks: Clicks::new(double_click),
246219 pointer: [0.0; 2],
247220 pointer_inside: false,
248 last_click: None,
249221 drag: None,
250222 object_focus: None,
251223 modifiers: Modifiers::default(),
......@@ -397,6 +369,15 @@ impl PageView {
397369 Ok(Response::changed())
398370 }
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
400381 /// The caret's rectangle in device pixels, for placing the platform's input method.
401382 pub fn caret_area(&self) -> Result<[f32; 4]> {
402383 let mut rect = self.editor.caret(1.0)?;
......@@ -474,13 +455,26 @@ impl PageView {
474455 self.changed()
475456 }
476457
477 fn zoom(&mut self, factor: f32) {
478 let point = self.viewport.document_point(self.pointer);
458 /// The page's zoom, where 1 is OneNote's 100%: 96 pixels per inch.
459 pub fn zoom(&self) -> f32 {
460 self.viewport.scale / (self.display_scale * 96.0 / 72.0)
461 }
462
463 /// Zooms to `zoom` about the middle of the view, within 25% to 400%.
464 pub fn set_zoom(&mut self, zoom: f32) -> Result<Response> {
465 let middle = self.viewport.size.map(|size| size as f32 / 2.0);
466 self.zoom_about(zoom / self.zoom(), middle);
467 self.moved()
468 }
469
470 /// Scales the view by `factor`, keeping the document point under `anchor` in place.
471 fn zoom_about(&mut self, factor: f32, anchor: [f32; 2]) {
472 let point = self.viewport.document_point(anchor);
479473 let dpr = self.display_scale;
480474 self.viewport.scale = (self.viewport.scale * factor).clamp(dpr / 3.0, dpr * 16.0 / 3.0);
481475 self.viewport.origin = [
482 self.pointer[0] - point[0] * self.viewport.scale,
483 self.pointer[1] - point[1] * self.viewport.scale,
476 anchor[0] - point[0] * self.viewport.scale,
477 anchor[1] - point[1] * self.viewport.scale,
484478 ];
485479 }
486480
......@@ -522,7 +516,16 @@ impl PageView {
522516 /// within the page's bounds.
523517 pub fn scroll_to(&mut self, axis: usize, offset: f32) -> Result<Response> {
524518 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 }
526529 }
527530
528531 /// Everything to draw this frame.
......@@ -571,6 +574,8 @@ impl PageView {
571574 scale: self.viewport.scale,
572575 pixel: self.pixel(),
573576 colors,
577 visible: [0.0, self.viewport.size[1] as f32]
578 .map(|y| (y - self.viewport.origin[1]) / self.viewport.scale),
574579 },
575580 )
576581 }
......@@ -609,7 +614,7 @@ impl PageView {
609614 if size.contains(&0) {
610615 return Ok(Response::default());
611616 }
612 self.changed()
617 self.moved()
613618 }
614619
615620 pub fn scale_factor_changed(&mut self, scale: f32) -> Result<Response> {
......@@ -618,7 +623,7 @@ impl PageView {
618623 self.viewport.origin[0] *= ratio;
619624 self.viewport.origin[1] *= ratio;
620625 self.display_scale = scale;
621 self.changed()
626 self.moved()
622627 }
623628
624629 pub fn focus_changed(&mut self, focused: bool) -> Result<Response> {
......@@ -654,7 +659,14 @@ impl PageView {
654659 let target =
655660 self.editor
656661 .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 })?;
658670 self.changed()
659671 }
660672 Some(Drag::Outline { pending_press, .. } | Drag::Image { pending_press, .. }) => {
......@@ -688,16 +700,9 @@ impl PageView {
688700 /// A primary-button press at the pointer's position, at `now` for click counting.
689701 pub fn pointer_pressed(&mut self, now: Instant) -> Result<Response> {
690702 let point = self.viewport.document_point(self.pointer);
691 let count = self
692 .last_click
693 .filter(|(time, point, _)| {
694 now.duration_since(*time) <= self.double_click
695 && (0..2).all(|axis| {
696 (point[axis] - self.pointer[axis]).abs() <= 4.0 * self.display_scale
697 })
698 })
699 .map_or(1, |(_, _, count)| (count % 3) + 1);
700 self.last_click = Some((now, self.pointer, count));
703 let unit = self
704 .clicks
705 .press(now, self.pointer, 4.0 * self.display_scale);
701706 match self.hit_test(point) {
702707 Some(Hit::Date(field)) => {
703708 self.editor.finish_composition();
......@@ -739,11 +744,6 @@ impl PageView {
739744 self.editor.focus_outline(id)?;
740745 let previous = self.editor.selection();
741746 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 };
747747 let selection = self.editor.selection_at(point[0], point[1], unit)?;
748748 let selection = if extend {
749749 Selection {
......@@ -802,12 +802,12 @@ impl PageView {
802802 /// `delta` in device pixels; Command zooms about the pointer instead of scrolling.
803803 pub fn wheel(&mut self, delta: [f32; 2]) -> Result<Response> {
804804 if self.modifiers.command {
805 self.zoom((delta[1] * 0.005).exp());
805 self.zoom_about((delta[1] * 0.005).exp(), self.pointer);
806806 } else {
807807 self.viewport.origin[0] += delta[0];
808808 self.viewport.origin[1] += delta[1];
809809 }
810 self.changed()
810 self.moved()
811811 }
812812
813813 /// Text the platform inserts outside key events, such as the character picker's;
......@@ -1046,118 +1046,50 @@ impl PageView {
10461046 }
10471047 "v" => request = Some(Request::Paste),
10481048 "+" | "=" => {
1049 self.zoom(1.1);
1050 return self.changed();
1049 self.zoom_about(1.1, self.pointer);
1050 return self.moved();
10511051 }
10521052 "-" => {
1053 self.zoom(1.0 / 1.1);
1054 return self.changed();
1053 self.zoom_about(1.0 / 1.1, self.pointer);
1054 return self.moved();
10551055 }
10561056 "0" => {
1057 self.zoom((self.display_scale * 96.0 / 72.0) / self.viewport.scale);
1058 return self.changed();
1057 self.zoom_about(1.0 / self.zoom(), self.pointer);
1058 return self.moved();
10591059 }
10601060 _ => return Ok(Response::default()),
10611061 }
10621062 } else {
1063 let movement = match key {
1064 Key::Named(NamedKey::ArrowLeft) => Some(if command {
1065 Movement::LineStart
1066 } else if option {
1067 Movement::WordLeft
1068 } else {
1069 Movement::Left
1070 }),
1071 Key::Named(NamedKey::ArrowRight) => Some(if command {
1072 Movement::LineEnd
1073 } else if option {
1074 Movement::WordRight
1075 } else {
1076 Movement::Right
1077 }),
1078 Key::Named(NamedKey::ArrowUp) => Some(if command {
1079 Movement::DocumentStart
1080 } else if option {
1081 Movement::ParagraphStart
1082 } else {
1083 Movement::Up
1084 }),
1085 Key::Named(NamedKey::ArrowDown) => Some(if command {
1086 Movement::DocumentEnd
1087 } else if option {
1088 Movement::ParagraphEnd
1089 } else {
1090 Movement::Down
1091 }),
1092 Key::Named(NamedKey::Home) if shift => Some(Movement::DocumentStart),
1093 Key::Named(NamedKey::End) if shift => Some(Movement::DocumentEnd),
1094 Key::Character(key) if control && !option => match key.as_str() {
1095 "a" => Some(Movement::LineStart),
1096 "e" => Some(Movement::LineEnd),
1097 "b" => Some(Movement::Left),
1098 "f" => Some(Movement::Right),
1099 "p" => Some(Movement::Up),
1100 "n" => Some(Movement::Down),
1101 _ => None,
1102 },
1103 _ => None,
1104 };
1105 if let Some(movement) = movement {
1063 let chord = Command::from_key(key, self.modifiers);
1064 if let Some(Command::Move(movement)) = chord {
11061065 self.editor
11071066 .move_selection(&mut self.engine, movement, shift)?;
11081067 } else if self.editor.marked_range().is_none() {
1109 match key {
1110 Key::Named(NamedKey::Backspace) if command || option => {
1111 self.editor.delete_to(
1112 &mut self.engine,
1113 if command {
1114 Movement::LineStart
1115 } else {
1116 Movement::WordLeft
1117 },
1118 )?;
1119 }
1120 Key::Named(NamedKey::Delete) if command || option => {
1121 self.editor.delete_to(
1122 &mut self.engine,
1123 if command {
1124 Movement::LineEnd
1125 } else {
1126 Movement::WordRight
1127 },
1128 )?;
1068 match (chord, key) {
1069 (Some(Command::DeleteTo(movement)), _) => {
1070 self.editor.delete_to(&mut self.engine, movement)?;
11291071 }
1130 Key::Named(NamedKey::Backspace) => {
1131 self.editor.delete(&mut self.engine, true)?;
1072 (Some(Command::Delete { backward }), _) => {
1073 self.editor.delete(&mut self.engine, backward)?;
11321074 }
1133 Key::Named(NamedKey::Delete) => {
1134 self.editor.delete(&mut self.engine, false)?;
1075 (Some(Command::Kill), _) => {
1076 if !self.editor.delete_to(&mut self.engine, Movement::LineEnd)? {
1077 self.editor.delete(&mut self.engine, false)?;
1078 }
11351079 }
1136 Key::Named(end @ (NamedKey::Home | NamedKey::End)) => {
1080 (_, Key::Named(end @ (NamedKey::Home | NamedKey::End))) => {
11371081 let limits = self.scroll();
11381082 self.viewport.origin[1] = -if *end == NamedKey::Home {
11391083 limits.min[1]
11401084 } else {
11411085 limits.max[1]
11421086 };
1143 return self.changed();
1087 return self.moved();
11441088 }
1145 Key::Character(key) if control && !option => match key.as_str() {
1146 "h" => {
1147 self.editor.delete(&mut self.engine, true)?;
1148 }
1149 "d" => {
1150 self.editor.delete(&mut self.engine, false)?;
1151 }
1152 "k" if !self.editor.delete_to(&mut self.engine, Movement::LineEnd)? => {
1153 self.editor.delete(&mut self.engine, false)?;
1154 }
1155 _ => {}
1156 },
1157 Key::Named(NamedKey::Enter) => {
1089 (_, Key::Named(NamedKey::Enter)) => {
11581090 self.editor.enter(&mut self.engine, shift)?;
11591091 }
1160 Key::Named(NamedKey::Tab) => {
1092 (_, Key::Named(NamedKey::Tab)) => {
11611093 self.editor.tab(&mut self.engine, shift)?;
11621094 }
11631095 _ if !command && !control => {
......@@ -1392,6 +1324,7 @@ fn page_primitives<'a>(
13921324 outline.shaped(),
13931325 [origin[0] + offset[0], origin[1] + offset[1]],
13941326 moving,
1327 paint.colors.paper,
13951328 primitives,
13961329 );
13971330 }
......@@ -1407,6 +1340,7 @@ fn page_primitives<'a>(
14071340 *origin,
14081341 Some(editor),
14091342 moving,
1343 paint.colors.paper,
14101344 &draw_outline,
14111345 )?;
14121346 }
......@@ -1578,22 +1512,6 @@ fn append_image_chrome(rect: [f32; 4], pixel: f32, primitives: &mut Vec<Primitiv
15781512 }
15791513}
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
15971515fn snap_to_grid(point: [f32; 2], margin: [f32; 2]) -> [f32; 2] {
15981516 std::array::from_fn(|axis| {
15991517 let offset = margin[axis];
......@@ -1722,11 +1640,15 @@ fn append_outline<'a>(
17221640 scale,
17231641 pixel,
17241642 colors,
1643 visible,
17251644 } = paint;
1726 outline.shaped().append_table_primitives(primitives, origin);
1645 let rows = [visible[0] - y, visible[1] - y];
17271646 outline
17281647 .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);
17301652 if let Some(editor) = editor {
17311653 for rect in editor.selection_rects()? {
17321654 primitives.push(Primitive::Rect {
......@@ -1740,10 +1662,14 @@ fn append_outline<'a>(
17401662 });
17411663 }
17421664 }
1743 for (index, (_, paragraph)) in outline.layouts().enumerate() {
1744 outline
1745 .shaped()
1746 .append_paragraph_primitives(index, paragraph, origin, primitives);
1665 for (index, paragraph) in outline.shaped().visible(rows) {
1666 outline.shaped().append_paragraph_primitives(
1667 index,
1668 paragraph,
1669 origin,
1670 colors.paper.ink,
1671 primitives,
1672 );
17471673 }
17481674 if let Some(editor) = editor {
17491675 for rect in editor.marked_rects()? {
......@@ -1754,7 +1680,7 @@ fn append_outline<'a>(
17541680 rect.x1 as f32 + x,
17551681 rect.y1 as f32 + y,
17561682 ],
1757 color: [0.0, 0.0, 0.0, 1.0],
1683 color: colors.paper.ink,
17581684 });
17591685 }
17601686 let [anchor, focus] = editor.selection().positions;
crates/canvas/src/interaction/profile.rs+22-4
......@@ -45,7 +45,7 @@ fn editing_cost(
4545 editor: &mut CanvasEditor,
4646 scene: Option<&(PageScene, [f32; 2])>,
4747) {
48 let viewport = Viewport {
48 let mut viewport = Viewport {
4949 size: [1000, 720],
5050 scale: 96.0 / 72.0,
5151 origin: [48.0; 2],
......@@ -59,7 +59,9 @@ fn editing_cost(
5959 });
6060 let mut access = accessibility::Accessibility::default();
6161 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()
6365 });
6466 eprintln!("canvas_count\t{case}\tax_nodes\t0\t{}", update.nodes.len());
6567 drop(update);
......@@ -113,14 +115,30 @@ fn editing_cost(
113115 });
114116 eprintln!("canvas_count\t{case}\tprimitives\t{sample}\t{primitives}");
115117 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 );
117131 });
118132 assert_eq!(edited_layouts, layout_ids(editor));
119133 timed(case, "undo", sample, || {
120134 assert!(editor.undo(engine).unwrap())
121135 });
122136 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 );
124142 });
125143 assert_eq!(editor.active_outline().document(), &initial[index]);
126144 }
crates/canvas/src/interaction/tests.rs+70-60
......@@ -9,6 +9,7 @@ use std::sync::Arc;
99const COLORS: TextColors = TextColors {
1010 caret: [0.1, 0.3, 0.9, 1.0],
1111 selection: [0.7, 0.8, 1.0, 1.0],
12 paper: crate::gpu::Paper::WHITE,
1213};
1314
1415/// 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 {
1819 scale,
1920 pixel: display_scale / scale,
2021 colors: COLORS,
22 visible: [f32::NEG_INFINITY, f32::INFINITY],
2123 }
2224}
2325
......@@ -61,6 +63,7 @@ fn date_buttons_keep_accessibility_identity_and_match_mouse_hits_after_reflow()
6163 origin: [-30.0, -50.0],
6264 };
6365 let mut access = accessibility::Accessibility::default();
66 let mut tree = None;
6467 let mut identities = Vec::new();
6568 for phase in 0..3 {
6669 if phase == 1 {
......@@ -73,19 +76,21 @@ fn date_buttons_keep_accessibility_identity_and_match_mouse_hits_after_reflow()
7376 .change_date(&mut engine, 2, ["Wednesday".into(), "8:40 AM".into()])
7477 .unwrap();
7578 }
76 let mut update = access.update(&editor, viewport, "Header", None).unwrap();
77 access.append_page_fields(&mut update, Some(&scene), &editor, viewport, None);
78 let buttons: Vec<_> = update
79 .nodes
80 .iter()
81 .filter(|(_, node)| node.role() == accesskit::Role::Button)
82 .collect();
79 let update = access
80 .update(&editor, Some(&scene), viewport, "Header", None, None)
81 .unwrap();
82 let buttons: Vec<_> =
83 accessibility::tests::nodes(accessibility::tests::apply(&mut tree, update))
84 .into_iter()
85 .filter(|node| node.role() == accesskit::Role::Button)
86 .collect();
8387 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;
8590 let field = [DateField::Date, DateField::Time][index];
86 assert_eq!(access.date_for_node(*id), Some(field));
87 assert!(node.supports_action(accesskit::Action::Click));
88 let rect = node.bounds().unwrap();
91 assert_eq!(access.date_for_node(id), Some(field));
92 assert!(node.data().supports_action(accesskit::Action::Click));
93 let rect = node.bounding_box().unwrap();
8994 let point = viewport.document_point([
9095 ((rect.x0 + rect.x1) * 0.5) as f32,
9196 ((rect.y0 + rect.y1) * 0.5) as f32,
......@@ -95,15 +100,14 @@ fn date_buttons_keep_accessibility_identity_and_match_mouse_hits_after_reflow()
95100 Some(Hit::Date(field))
96101 );
97102 if phase == 0 {
98 identities.push(*id);
103 identities.push(id);
99104 } else {
100 assert_eq!(*id, identities[index]);
105 assert_eq!(id, identities[index]);
101106 }
102107 }
103108 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"));
105110 }
106 accesskit_consumer::Tree::new(update, true);
107111 }
108112}
109113
......@@ -169,6 +173,7 @@ fn title_chrome_selects_text_and_exposes_a_named_editable_field() {
169173 let update = access
170174 .update(
171175 &editor,
176 None,
172177 Viewport {
173178 size: [800, 600],
174179 scale: 1.0,
......@@ -176,6 +181,7 @@ fn title_chrome_selects_text_and_exposes_a_named_editable_field() {
176181 },
177182 "Header",
178183 None,
184 None,
179185 )
180186 .unwrap();
181187 assert!(update.nodes.iter().any(|(_, node)| node.role()
......@@ -214,6 +220,7 @@ fn provisional_lines_share_one_drawn_hit_tested_and_accessible_outline() {
214220 let update = access
215221 .update(
216222 &editor,
223 None,
217224 Viewport {
218225 size: [800, 600],
219226 scale: 1.0,
......@@ -221,6 +228,7 @@ fn provisional_lines_share_one_drawn_hit_tested_and_accessible_outline() {
221228 },
222229 "Test",
223230 None,
231 None,
224232 )
225233 .unwrap();
226234 let fields = update
......@@ -264,7 +272,9 @@ fn blank_caret_and_composition_have_accessible_text_without_outline_chrome() {
264272 scale: 96.0 / 72.0,
265273 origin: [48.0; 2],
266274 };
267 let initial = access.update(&editor, viewport, "Page", None).unwrap();
275 let initial = access
276 .update(&editor, None, viewport, "Page", None, None)
277 .unwrap();
268278 let id = editor.active_outline().id;
269279 assert_eq!(access.outline_for_node(initial.focus), Some(id));
270280 let rectangles = |editor: &CanvasEditor| {
......@@ -291,16 +301,22 @@ fn blank_caret_and_composition_have_accessible_text_without_outline_chrome() {
291301 .iter()
292302 .all(|(_, color)| *color == [0.0, 0.0, 0.0, 1.0] || *color == COLORS.caret)
293303 );
294 let preedit = access.update(&editor, viewport, "Page", None).unwrap();
304 let preedit = access
305 .update(&editor, None, viewport, "Page", None, None)
306 .unwrap();
295307 assert_eq!(preedit.focus, initial.focus);
296308 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();
298312 assert_eq!(committed.focus, initial.focus);
299313 assert!(rectangles(&editor).len() > 1);
300314 editor.undo(&mut engine).unwrap();
301315 assert!(editor.outlines().is_empty());
302316 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();
304320 assert_eq!(empty.focus, committed.focus);
305321 assert_eq!(access.outline_for_node(committed.focus), Some(id));
306322 editor.redo(&mut engine).unwrap();
......@@ -308,12 +324,16 @@ fn blank_caret_and_composition_have_accessible_text_without_outline_chrome() {
308324 editor.delete(&mut engine, true).unwrap();
309325 assert!(editor.outlines().is_empty());
310326 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();
312330 assert_eq!(retired.focus, committed.focus);
313331 assert_eq!(access.outline_for_node(retired.focus), Some(id));
314332 editor.undo(&mut engine).unwrap();
315333 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();
317337 assert_eq!(restored.focus, committed.focus);
318338 editor.redo(&mut engine).unwrap();
319339 assert_eq!(rectangles(&editor).len(), 1);
......@@ -375,6 +395,7 @@ fn imported_page_widgets_and_accessibility_follow_edits() {
375395 }
376396 eprintln!("Verified {tag_hits} imported tag-gutter targets");
377397 let mut access = accessibility::Accessibility::default();
398 let mut tree = None;
378399 let tags = |editor: &CanvasEditor| {
379400 page_primitives(
380401 editor,
......@@ -389,9 +410,9 @@ fn imported_page_widgets_and_accessibility_follow_edits() {
389410 Primitive::Icon {
390411 sources,
391412 origin,
392 opacity,
413 tint,
393414 ..
394 } => Some(((sources, opacity), origin)),
415 } => Some(((sources, tint[3]), origin)),
395416 _ => None,
396417 })
397418 .collect::<Vec<_>>()
......@@ -410,15 +431,25 @@ fn imported_page_widgets_and_accessibility_follow_edits() {
410431 .into(),
411432 )
412433 .unwrap();
413 let before = access.update(&editor, viewport, "Test", None).unwrap();
414 let before = accesskit_consumer::Tree::new(before, true);
415 let before = before.state().focus().unwrap().document_range().text();
434 let before = access
435 .update(&editor, None, viewport, "Test", None, None)
436 .unwrap();
437 let before = accessibility::tests::apply(&mut tree, before)
438 .focus()
439 .unwrap()
440 .document_range()
441 .text();
416442 editor.insert(&mut engine, "CANVAS CHECK ").unwrap();
417443 {
418 let update = access.update(&editor, viewport, "Test", None).unwrap();
419 let tree = accesskit_consumer::Tree::new(update, true);
444 let update = access
445 .update(&editor, None, viewport, "Test", None, None)
446 .unwrap();
420447 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(),
422453 format!("CANVAS CHECK {before}")
423454 );
424455 let primitives = page_primitives(
......@@ -439,10 +470,15 @@ fn imported_page_widgets_and_accessibility_follow_edits() {
439470 editor.undo(&mut engine).unwrap();
440471 assert_eq!(editor.active_outline().document(), &original);
441472 assert_eq!(tags(&editor), original_tags);
442 let update = access.update(&editor, viewport, "Test", None).unwrap();
443 let tree = accesskit_consumer::Tree::new(update, true);
473 let update = access
474 .update(&editor, None, viewport, "Test", None, None)
475 .unwrap();
444476 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(),
446482 before
447483 );
448484 }
......@@ -661,34 +697,6 @@ fn picture_handles_resize_as_onenote_does() {
661697 assert_eq!(image_handle_at(rect, pixel, [600.0, 150.0]), None);
662698}
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
692700const DEFAULT_MARGIN: [f32; 2] = [36.0, 14.4];
693701
694702#[test]
......@@ -819,6 +827,7 @@ fn table_glyphs_highlights_and_selection_share_cell_paint_bounds() {
819827 scale: 1.0,
820828 pixel: 1.0,
821829 colors: COLORS,
830 visible: [f32::NEG_INFINITY, f32::INFINITY],
822831 },
823832 &mut primitives,
824833 )
......@@ -889,6 +898,7 @@ fn editable_tables_paint_borders_before_selection_and_cell_text() {
889898 scale: 1.0,
890899 pixel: 1.0,
891900 colors: COLORS,
901 visible: [f32::NEG_INFINITY, f32::INFINITY],
892902 },
893903 &mut primitives,
894904 )
crates/canvas/src/layout.rs+13-58
......@@ -30,7 +30,8 @@ const SUBSCRIPT_DROP: f32 = 0.08;
3030
3131#[derive(Clone, Copy, Debug, Default, PartialEq)]
3232pub struct TextBrush {
33 pub color: u32,
33 /// COLORREF, or `None` for OneNote's automatic colour, which follows the paper.
34 pub color: Option<u32>,
3435 pub highlight: Option<u32>,
3536 /// Points the run draws above the line's baseline: positive for a superscript,
3637 /// negative for a subscript. Line metrics and hit-testing keep the baseline.
......@@ -191,9 +192,9 @@ impl TextEngine {
191192 StyleProperty::Brush(TextBrush {
192193 // Links take OneNote's blue unless given a colour of their own.
193194 color: match format.color {
194 Some(color) if color != 0xff000000 => color,
195 _ if link => 0x00ff0000,
196 _ => 0,
195 Some(color) if color != 0xff000000 => Some(color),
196 _ if link => Some(0x00ff0000),
197 _ => None,
197198 },
198199 highlight: format.highlight.filter(|color| *color != 0xff000000),
199200 rise,
......@@ -398,59 +399,6 @@ impl TextLayout {
398399 Cursor::from_byte_index(&self.shaped, byte, affinity)
399400 }
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
454402 pub fn hit_test(&self, x: f32, y: f32) -> Cursor {
455403 let index = self
456404 .lines
......@@ -664,7 +612,14 @@ mod tests {
664612 _ => None,
665613 })
666614 .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 );
668623 }
669624
670625 #[test]
crates/canvas/src/math.rs+3-3
......@@ -10,8 +10,8 @@ use onestore::page::text::Paragraph;
1010#[derive(Clone)]
1111pub struct MathLayout {
1212 pub size: [f32; 2],
13 /// COLORREF for rules and strokes.
14 pub color: u32,
13 /// COLORREF for rules and strokes, or `None` for the paper's ink.
14 pub color: Option<u32>,
1515 /// Distance from the top to the baseline of the equation's main row.
1616 pub baseline: f32,
1717 pub items: Vec<MathItem>,
......@@ -616,7 +616,7 @@ pub fn layout(engine: &mut TextEngine, paragraph: &Paragraph) -> Result<MathLayo
616616 body.descent = body.descent.max(line.height - line.baseline);
617617 Ok(MathLayout {
618618 size: [body.width, body.height()],
619 color: color.unwrap_or(0),
619 color,
620620 baseline: body.ascent,
621621 items: body
622622 .items
crates/canvas/src/outline.rs+555-223
......@@ -1,5 +1,5 @@
11use crate::{
2 document::{DocumentEdit, edited_nodes},
2 document::{DocumentEdit, edited_nodes, leaves},
33 layout::{LayoutError, TextEngine, TextLayout},
44};
55use onestore::page::text::{Paragraph, TextProjection};
......@@ -8,17 +8,50 @@ use onestore::{
88 ExGuid,
99 document::{Format, Kind},
1010};
11use std::collections::{BTreeMap, BTreeSet};
11use std::{
12 collections::{BTreeMap, BTreeSet},
13 ops::Range,
14};
1215
1316pub(crate) const TITLE_WIDTH: f32 = 468.0;
1417
15#[derive(Clone)]
18#[derive(Clone, Default)]
1619pub struct OutlineLayout {
1720 pub paragraphs: Vec<ParagraphLayout>,
1821 pub tables: Vec<TableLayout>,
1922 /// Pictures, files and handwriting that occupy a paragraph of their own.
2023 pub objects: Vec<ObjectLayout>,
2124 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],
2255}
2356
2457#[derive(Clone)]
......@@ -50,6 +83,61 @@ impl ObjectLayout {
5083 ObjectKind::Picture | ObjectKind::Ink(_) => None,
5184 }
5285 }
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 }))
53141}
54142
55143/// 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
181269 Ok(indent)
182270}
183271
184fn spacing(
185 bottom: &mut f64,
186 previous: &mut Option<f32>,
187 format: &Format,
188) -> Result<f32, LayoutError> {
189 let before = format.space_before.unwrap_or(0.0);
190 let after = format.space_after.unwrap_or(0.0);
191 if [before, after].iter().any(|v| !v.is_finite() || *v < 0.0) {
272fn spacing(format: &Format) -> Result<[f32; 2], LayoutError> {
273 let spacing = [format.space_before, format.space_after].map(|space| space.unwrap_or(0.0));
274 if spacing.iter().any(|v| !v.is_finite() || *v < 0.0) {
192275 return Err(LayoutError::InvalidSpacing);
193276 }
194 if let Some(previous) = previous {
195 *bottom += f64::from(previous.max(before));
277 Ok(spacing)
278}
279
280/// Where a node with `spacing` starts after the flow `state` of a bottom and pending spacing.
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));
196284 }
197 *previous = Some(after);
198 Ok(*bottom as f32)
285 state.1 = Some(after);
286 state.0 as f32
199287}
200288
201289impl ParagraphLayout {
......@@ -386,42 +474,6 @@ pub(crate) fn visible_paragraphs<'a>(
386474 })
387475}
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
425477impl OutlineLayout {
426478 /// OneNote gives an outline one tag column, as wide as its most-tagged paragraph needs; each
427479 /// paragraph's tags start at the column's left edge. Tag origins assume a one-tag column.
......@@ -491,6 +543,33 @@ impl OutlineLayout {
491543 depth: usize,
492544 edit: Option<&DocumentEdit>,
493545 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>,
494573 ) -> Result<Self, LayoutError> {
495574 if !width.is_finite() || width <= 0.0 {
496575 return Err(LayoutError::InvalidWidth);
......@@ -499,145 +578,424 @@ impl OutlineLayout {
499578 return Err(LayoutError::UnsupportedContent);
500579 }
501580 let mut result = Self {
502 paragraphs: Vec::new(),
503 tables: Vec::new(),
504 objects: Vec::new(),
505581 size: [36.0, 0.0],
582 widths: [36.0, f32::NEG_INFINITY],
583 ..Self::default()
506584 };
507 let mut bottom = 0.0;
508 let mut previous = None;
509 for node in visible_paragraphs(nodes) {
510 match &node.content {
511 ParagraphContent::Text(_) => {
512 let mut paragraph = shape(node, width, indents)?;
513 let y = spacing(
514 &mut bottom,
515 &mut previous,
516 &paragraph.projection.text().spans()[0].format,
517 )?;
518 let size = paragraph.size();
519 paragraph.origin[1] = y;
520 result.paragraphs.push(paragraph);
521 result.size[0] = result.size[0].max(size[0]);
522 bottom += f64::from(size[1]);
585 for node in nodes {
586 let hidden = node.parent.is_some_and(|parent| hiding.contains(&parent));
587 if hidden || node.collapsed {
588 hiding.insert(node.id);
589 }
590 let mut block = Block {
591 first: [
592 result.paragraphs.len(),
593 result.tables.len(),
594 result.objects.len(),
595 ],
596 metrics: None,
597 extent: [f32::NEG_INFINITY; 2],
598 state,
599 rel: Vec::new(),
600 };
601 if !hidden {
602 let (space, height, flow, extent) = match &node.content {
603 ParagraphContent::Text(_) => {
604 let mut paragraph = shape(node, width, indents)?;
605 let space = spacing(&paragraph.projection.text().spans()[0].format)?;
606 paragraph.origin[1] = top(&mut state, space);
607 let size = paragraph.size();
608 block.extent[1] = paragraph.origin[0] + paragraph.text.shaped.width();
609 result.paragraphs.push(paragraph);
610 (space, size[1], size[1], size[0])
611 }
612 ParagraphContent::Table(table) => {
613 let x = indentation(node.level, indents, width)?;
614 let space = spacing(&node.format)?;
615 let y = top(&mut state, space);
616 let mut child = Self::table(table, depth + 1, edit, shape)?;
617 if depth == 0 {
618 block.rel = verticals(
619 &mut child.paragraphs,
620 &mut child.tables,
621 &mut child.objects,
622 )
623 .map(|value| *value)
624 .collect();
625 }
626 let size = child.size;
627 result.append(child, [x, y]);
628 (space, size[1], size[1], x + size[0])
629 }
630 content => {
631 let x = indentation(node.level, indents, width)?;
632 let space = spacing(&node.format)?;
633 let y = top(&mut state, space);
634 let (mut object, height, extent) = match content {
635 ParagraphContent::Image(image) => {
636 let [w, h] =
637 image_size(image).ok_or(LayoutError::UnsupportedContent)?;
638 let object = ObjectLayout {
639 id: image.id,
640 rect: [x, 0.0, x + w, 0.0],
641 kind: ObjectKind::Picture,
642 bottom: 0.0,
643 };
644 (object, h, x + w)
645 }
646 ParagraphContent::Attachment(file) => {
647 let [w, h] = file.size.unwrap_or([24.0, 24.0]);
648 let left = x + (ATTACHMENT_WIDTH - w) / 2.0;
649 // OneNote shows the name without its extension.
650 let name = std::path::Path::new(&file.filename)
651 .file_stem()
652 .and_then(|stem| stem.to_str())
653 .unwrap_or(&file.filename);
654 let format = Format {
655 alignment: Some(1),
656 ..node.format.clone()
657 };
658 let mut label = shape(
659 &caption(node.id, file.id, name, format),
660 ATTACHMENT_WIDTH,
661 &[0.0],
662 )?;
663 label.reset_origin(x);
664 let object = ObjectLayout {
665 id: file.id,
666 rect: [left, 0.0, left + w, 0.0],
667 kind: ObjectKind::File(label),
668 bottom: 0.0,
669 };
670 (object, h, x + ATTACHMENT_WIDTH)
671 }
672 ParagraphContent::Ink(ink) => {
673 // The paragraph reaches from its origin to the farthest stroke point.
674 let [w, h] = ink
675 .bounds()
676 .map(|[x, y, w, h]| [x + w, y + h])
677 .filter(|size| size.iter().all(|v| v.is_finite() && *v >= 0.0))
678 .ok_or(LayoutError::UnsupportedContent)?;
679 let object = ObjectLayout {
680 id: ink.id,
681 rect: [x, 0.0, x + w, 0.0],
682 kind: ObjectKind::Ink(ink.clone()),
683 bottom: 0.0,
684 };
685 (object, h, x + w)
686 }
687 ParagraphContent::Unsupported(unsupported) => {
688 let w = unsupported.layout.max_width.unwrap_or(160.0).max(160.0);
689 let mut label = shape(
690 &caption(
691 node.id,
692 unsupported.id,
693 "Unsupported content",
694 Format::default(),
695 ),
696 w - 16.0,
697 &[0.0],
698 )?;
699 label.reset_origin(x + 8.0);
700 let h = unsupported
701 .layout
702 .max_height
703 .unwrap_or(0.0)
704 .max(label.text.height() + 16.0);
705 let object = ObjectLayout {
706 id: unsupported.id,
707 rect: [x, 0.0, x + w, 0.0],
708 kind: ObjectKind::Unsupported(label),
709 bottom: 0.0,
710 };
711 (object, h, x + w)
712 }
713 ParagraphContent::Text(_) | ParagraphContent::Table(_) => {
714 unreachable!("text and tables are not objects")
715 }
716 };
717 let flow = object.place(y, height);
718 result.objects.push(object);
719 (space, height, flow, extent)
720 }
721 };
722 state.0 += f64::from(flow);
723 result.size[0] = result.size[0].max(extent);
724 result.widths[1] = result.widths[1].max(block.extent[1]);
725 block.extent[0] = extent;
726 block.metrics = Some([space[0], space[1], height]);
727 if !(state.0 as f32).is_finite() || !result.size[0].is_finite() {
728 return Err(LayoutError::InvalidSpacing);
523729 }
524 ParagraphContent::Table(table) => {
525 let x = indentation(node.level, indents, width)?;
526 let y = spacing(&mut bottom, &mut previous, &node.format)?;
527 let child = Self::table(table, depth + 1, edit, shape)?;
528 result.size[0] = result.size[0].max(x + child.size[0]);
529 bottom += f64::from(child.size[1]);
530 result.append(child, [x, y]);
730 }
731 block.state = state;
732 if depth == 0 {
733 result.blocks.push(block);
734 }
735 }
736 result.size[1] = state.0 as f32;
737 result.widths[0] = result.size[0];
738 Ok(result)
739 }
740
741 /// The paragraphs, tables and objects of root nodes `range`.
742 pub(crate) fn pieces(&self, range: Range<usize>) -> [Range<usize>; 3] {
743 let first = |root: usize| {
744 self.blocks.get(root).map_or(
745 [self.paragraphs.len(), self.tables.len(), self.objects.len()],
746 |block| block.first,
747 )
748 };
749 let [start, end] = [first(range.start), first(range.end)];
750 [0, 1, 2].map(|kind| start[kind]..end[kind])
751 }
752
753 /// Lays out root nodes `range` of `nodes` once `edit` applies, the edit's own or the table
754 /// holding its cell, and works out where the nodes after them move. Resizing columns or
755 /// showing or hiding other nodes lays out every node.
756 #[allow(clippy::too_many_arguments)]
757 pub(crate) fn relayout(
758 &self,
759 nodes: &[PageParagraph],
760 edit: &DocumentEdit,
761 range: Range<usize>,
762 indents: &[f32],
763 width: f32,
764 fixed_width: bool,
765 shape: &mut impl FnMut(&PageParagraph, f32, &[f32]) -> Result<ParagraphLayout, LayoutError>,
766 ) -> Result<Relayout, LayoutError> {
767 let mut range = if edit.columns.is_empty() {
768 range
769 } else {
770 0..nodes.len()
771 };
772 loop {
773 let old = self.pieces(range.clone());
774 let sources = leaves(&nodes[range.clone()], None)
775 .map(|(_, _, node)| (node.id, node))
776 .collect::<BTreeMap<_, _>>();
777 let cached = self.paragraphs[old[0].clone()]
778 .iter()
779 .filter_map(|layout| Some((layout.id, (*sources.get(&layout.id)?, layout))))
780 .collect::<BTreeMap<_, _>>();
781 let count = match edit.container {
782 None => range.len() - edit.range.len() + edit.replacement.len(),
783 Some(_) => range.len(),
784 };
785 let replacement = edited_nodes(nodes, None, Some(edit))
786 .skip(range.start)
787 .take(count);
788 let supplied = replacement
789 .clone()
790 .map(|node| node.id)
791 .collect::<BTreeSet<_>>();
792 // Children of a hidden or collapsed earlier node stay hidden.
793 let hiding = replacement
794 .clone()
795 .filter_map(|node| {
796 let parent = node.parent.filter(|parent| !supplied.contains(parent))?;
797 let root = nodes[..range.start]
798 .iter()
799 .rposition(|node| node.id == parent)?;
800 (self.blocks[root].metrics.is_none() || nodes[root].collapsed).then_some(parent)
801 })
802 .collect();
803 let start = range
804 .start
805 .checked_sub(1)
806 .map_or((0.0, None), |root| self.blocks[root].state);
807 let segment = Self::stack(
808 replacement.clone(),
809 indents,
810 width,
811 0,
812 Some(edit),
813 hiding,
814 start,
815 &mut |node, width, indents| {
816 let indent = indentation(node.level, indents, width)?;
817 if let Some((source, cached)) = cached.get(&node.id)
818 && *source == node
819 && cached.text.shaped.layout_max_advance() == width - indent
820 && cached
821 .markers
822 .iter()
823 .all(|(marker, _)| marker.shaped.layout_max_advance() == width)
824 {
825 let mut result = (*cached).clone();
826 result.reset_origin(indent);
827 return Ok(result);
828 }
829 shape(node, width, indents)
830 },
831 )?;
832 let hides =
833 |block: &Block, node: &PageParagraph| block.metrics.is_none() || node.collapsed;
834 if range.end < nodes.len() {
835 let now = replacement
836 .zip(&segment.blocks)
837 .map(|(node, block)| (node.id, hides(block, node)))
838 .collect::<BTreeMap<_, _>>();
839 if nodes[range.clone()]
840 .iter()
841 .zip(&self.blocks[range.clone()])
842 .any(|(node, block)| {
843 now.get(&node.id)
844 .is_some_and(|now| *now != hides(block, node))
845 })
846 {
847 range = 0..nodes.len();
848 continue;
531849 }
532 ParagraphContent::Image(image) => {
533 let x = indentation(node.level, indents, width)?;
534 let y = spacing(&mut bottom, &mut previous, &node.format)?;
535 let [w, h] = image_size(image).ok_or(LayoutError::UnsupportedContent)?;
536 result.objects.push(ObjectLayout {
537 id: image.id,
538 rect: [x, y, x + w, y + h],
539 kind: ObjectKind::Picture,
540 bottom: y + h,
541 });
542 result.size[0] = result.size[0].max(x + w);
543 bottom += f64::from(h);
850 }
851 let mut state = segment.blocks.last().map_or(start, |block| block.state);
852 let mut incoming = range
853 .end
854 .checked_sub(1)
855 .map_or((0.0, None), |root| self.blocks[root].state);
856 let mut moved = Vec::new();
857 for root in range.end..self.blocks.len() {
858 if state == incoming {
859 break;
544860 }
545 ParagraphContent::Attachment(file) => {
546 let x = indentation(node.level, indents, width)?;
547 let y = spacing(&mut bottom, &mut previous, &node.format)?;
548 let [w, h] = file.size.unwrap_or([24.0, 24.0]);
549 let icon = [x + (ATTACHMENT_WIDTH - w) / 2.0, y + 6.0];
550 // OneNote shows the name without its extension.
551 let name = std::path::Path::new(&file.filename)
552 .file_stem()
553 .and_then(|stem| stem.to_str())
554 .unwrap_or(&file.filename);
555 let format = Format {
556 alignment: Some(1),
557 ..node.format.clone()
861 let block = &self.blocks[root];
862 incoming = block.state;
863 let mut y = 0.0;
864 if let Some([before, after, height]) = block.metrics {
865 y = top(&mut state, [before, after]);
866 let [_, tables, objects] = self.pieces(root..root + 1);
867 let flow = match self.objects[objects].first() {
868 Some(object) if tables.is_empty() => object.at(y, height).1,
869 _ => height,
558870 };
559 let mut label = shape(
560 &caption(node.id, file.id, name, format),
561 ATTACHMENT_WIDTH,
562 &[0.0],
563 )?;
564 label.reset_origin(x);
565 label.origin[1] = icon[1] + h + 10.5;
566 let end = label.origin[1] + label.text.height() + 9.0;
567 result.objects.push(ObjectLayout {
568 id: file.id,
569 rect: [icon[0], icon[1], icon[0] + w, icon[1] + h],
570 kind: ObjectKind::File(label),
571 bottom: end,
572 });
573 result.size[0] = result.size[0].max(x + ATTACHMENT_WIDTH);
574 bottom += f64::from(end - y);
871 state.0 += f64::from(flow);
872 if !(state.0 as f32).is_finite() {
873 return Err(LayoutError::InvalidSpacing);
874 }
575875 }
576 ParagraphContent::Ink(ink) => {
577 let x = indentation(node.level, indents, width)?;
578 let y = spacing(&mut bottom, &mut previous, &node.format)?;
579 // The paragraph reaches from its origin to the farthest stroke point.
580 let [w, h] = ink
581 .bounds()
582 .map(|[x, y, w, h]| [x + w, y + h])
583 .filter(|size| size.iter().all(|v| v.is_finite() && *v >= 0.0))
584 .ok_or(LayoutError::UnsupportedContent)?;
585 result.objects.push(ObjectLayout {
586 id: ink.id,
587 rect: [x, y, x + w, y + h],
588 kind: ObjectKind::Ink(ink.clone()),
589 bottom: y + h,
590 });
591 result.size[0] = result.size[0].max(x + w);
592 bottom += f64::from(h);
876 moved.push((y, state));
877 }
878 let bottom = if moved.len() == self.blocks.len() - range.end {
879 state.0 as f32
880 } else {
881 self.size[1]
882 };
883 let widest = |blocks: &[Block], side: usize| {
884 blocks
885 .iter()
886 .map(|block| block.extent[side])
887 .fold(f32::NEG_INFINITY, f32::max)
888 };
889 let widths = [0, 1].map(|side| {
890 let [before, after] = [
891 widest(&self.blocks[range.clone()], side),
892 widest(&segment.blocks, side),
893 ];
894 if after >= before || before < self.widths[side] {
895 self.widths[side].max(after)
896 } else {
897 [
898 &self.blocks[..range.start],
899 &segment.blocks,
900 &self.blocks[range.end..],
901 ]
902 .into_iter()
903 .map(|blocks| widest(blocks, side))
904 .fold([36.0, f32::NEG_INFINITY][side], f32::max)
593905 }
594 ParagraphContent::Unsupported(unsupported) => {
595 let x = indentation(node.level, indents, width)?;
596 let y = spacing(&mut bottom, &mut previous, &node.format)?;
597 let w = unsupported.layout.max_width.unwrap_or(160.0).max(160.0);
598 let mut label = shape(
599 &caption(
600 node.id,
601 unsupported.id,
602 "Unsupported content",
603 Format::default(),
604 ),
605 w - 16.0,
606 &[0.0],
607 )?;
608 label.reset_origin(x + 8.0);
609 label.origin[1] = y + 8.0;
610 let h = unsupported
611 .layout
612 .max_height
613 .unwrap_or(0.0)
614 .max(label.text.height() + 16.0);
615 result.objects.push(ObjectLayout {
616 id: unsupported.id,
617 rect: [x, y, x + w, y + h],
618 kind: ObjectKind::Unsupported(label),
619 bottom: y + h,
620 });
621 result.size[0] = result.size[0].max(x + w);
622 bottom += f64::from(h);
906 });
907 let tables = &old[1];
908 let tables = self.tables[..tables.start]
909 .iter()
910 .chain(&segment.tables)
911 .chain(&self.tables[tables.end..]);
912 let size = [
913 if fixed_width { width } else { widths[0] }.max(table_width(tables)),
914 bottom,
915 ];
916 return Ok(Relayout {
917 range,
918 segment,
919 moved,
920 size,
921 widths,
922 });
923 }
924 }
925
926 /// Puts a [`Relayout`] of this layout in place.
927 pub(crate) fn commit(&mut self, relayout: Relayout) {
928 let Relayout {
929 range,
930 mut segment,
931 moved,
932 size,
933 widths,
934 } = relayout;
935 let [paragraphs, tables, objects] = self.pieces(range.clone());
936 let first = [paragraphs.start, tables.start, objects.start];
937 let delta = [
938 segment.paragraphs.len() as isize - paragraphs.len() as isize,
939 segment.tables.len() as isize - tables.len() as isize,
940 segment.objects.len() as isize - objects.len() as isize,
941 ];
942 for cell in segment.tables.iter_mut().flat_map(|table| &mut table.cells) {
943 cell.paragraphs = cell.paragraphs.start + first[0]..cell.paragraphs.end + first[0];
944 }
945 for block in &mut segment.blocks {
946 for (value, first) in block.first.iter_mut().zip(first) {
947 *value += first;
948 }
949 }
950 let after = range.start + segment.blocks.len();
951 let later_tables = tables.start + segment.tables.len();
952 self.paragraphs.splice(paragraphs, segment.paragraphs);
953 self.tables.splice(tables, segment.tables);
954 self.objects.splice(objects, segment.objects);
955 self.blocks.splice(range, segment.blocks);
956 if delta != [0; 3] {
957 for block in &mut self.blocks[after..] {
958 for (value, delta) in block.first.iter_mut().zip(delta) {
959 *value = value.wrapping_add_signed(delta);
623960 }
624961 }
625 if !(bottom as f32).is_finite() || !result.size[0].is_finite() {
626 return Err(LayoutError::InvalidSpacing);
962 for cell in self.tables[later_tables..]
963 .iter_mut()
964 .flat_map(|table| &mut table.cells)
965 {
966 cell.paragraphs = cell.paragraphs.start.wrapping_add_signed(delta[0])
967 ..cell.paragraphs.end.wrapping_add_signed(delta[0]);
627968 }
628969 }
629 result.size[1] = bottom as f32;
630 result.size[0] = if fixed_width { width } else { result.size[0] }.max(result.table_width());
631 Ok(result)
970 for (root, (y, state)) in (after..).zip(moved) {
971 let [paragraphs, tables, objects] = self.pieces(root..root + 1);
972 let block = &mut self.blocks[root];
973 block.state = state;
974 let Some([.., height]) = block.metrics else {
975 continue;
976 };
977 if !tables.is_empty() {
978 for (value, rel) in verticals(
979 &mut self.paragraphs[paragraphs],
980 &mut self.tables[tables],
981 &mut self.objects[objects],
982 )
983 .zip(&block.rel)
984 {
985 *value = rel + y;
986 }
987 } else if let Some(object) = self.objects[objects].first_mut() {
988 object.place(y, height);
989 } else {
990 self.paragraphs[paragraphs.start].origin[1] = y;
991 }
992 }
993 self.size = size;
994 self.widths = widths;
632995 }
633996
634997 pub(crate) fn table_width(&self) -> f32 {
635 // Stored column widths extend 1.77pt past the final cell border.
636 self.tables
637 .iter()
638 .filter_map(|table| table.cells.last())
639 .map(|cell| cell.rect[2] + 1.77)
640 .fold(0.0, f32::max)
998 table_width(&self.tables)
641999 }
6421000
6431001 fn table(
......@@ -666,13 +1024,11 @@ impl OutlineLayout {
6661024 return Err(LayoutError::UnsupportedContent);
6671025 }
6681026 let mut result = Self {
669 paragraphs: Vec::new(),
6701027 tables: vec![TableLayout {
6711028 id: table.id,
6721029 cells: Vec::new(),
6731030 borders: table.borders.unwrap_or(true),
6741031 }],
675 objects: Vec::new(),
6761032 size: [
6771033 (0..table.columns.len())
6781034 .map(|index| width(index) + 4.98)
......@@ -680,6 +1036,7 @@ impl OutlineLayout {
6801036 - 1.83,
6811037 3.54,
6821038 ],
1039 ..Self::default()
6831040 };
6841041 let mut y = 0.0;
6851042 for row in &table.rows {
......@@ -724,26 +1081,15 @@ impl OutlineLayout {
7241081 }
7251082 Ok(result)
7261083 }
1084}
7271085
728 pub(crate) fn new(
729 mut paragraphs: Vec<ParagraphLayout>,
730 width: f32,
731 width_set_by_user: bool,
732 ) -> Result<Self, LayoutError> {
733 if !width.is_finite() || width <= 0.0 {
734 return Err(LayoutError::InvalidWidth);
735 }
736 let (origins, size) = arrange(paragraphs.iter(), width, width_set_by_user)?;
737 for (paragraph, y) in paragraphs.iter_mut().zip(origins) {
738 paragraph.origin[1] = y;
739 }
740 Ok(Self {
741 paragraphs,
742 tables: Vec::new(),
743 objects: Vec::new(),
744 size,
745 })
746 }
1086/// Stored column widths extend 1.77pt past the final cell border.
1087fn table_width<'a>(tables: impl IntoIterator<Item = &'a TableLayout>) -> f32 {
1088 tables
1089 .into_iter()
1090 .filter_map(|table| table.cells.last())
1091 .map(|cell| cell.rect[2] + 1.77)
1092 .fold(0.0, f32::max)
7471093}
7481094
7491095/// Shapes a page object into positioned paragraph layouts.
......@@ -773,13 +1119,6 @@ impl Arrange for Outline {
7731119 }
7741120}
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
7831122pub(crate) fn outline_layout(
7841123 outline: &Outline,
7851124 engine: &mut TextEngine,
......@@ -789,26 +1128,16 @@ pub(crate) fn outline_layout(
7891128 if !outline.unsupported.is_empty() {
7901129 return Err(LayoutError::UnsupportedContent);
7911130 }
792 if !all_text(&outline.paragraphs) {
793 return OutlineLayout::flow(
794 outline.paragraphs.iter(),
795 &outline.indents,
796 width,
797 outline.layout.width_set_by_user == Some(true),
798 0,
799 None,
800 &mut |node, width, indents| {
801 ParagraphLayout::shape(engine, node, width, indents, definitions)
802 },
803 );
804 }
805 let paragraphs = visible_paragraphs(outline.paragraphs.iter())
806 .map(|p| ParagraphLayout::shape(engine, p, width, &outline.indents, definitions))
807 .collect::<Result<_, _>>()?;
808 OutlineLayout::new(
809 paragraphs,
1131 OutlineLayout::flow(
1132 outline.paragraphs.iter(),
1133 &outline.indents,
8101134 width,
8111135 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 },
8121141 )
8131142}
8141143
......@@ -1377,9 +1706,12 @@ mod tests {
13771706 _ => None,
13781707 })
13791708 .collect();
1380 assert_eq!(colors, [0x0080_8000]);
1709 assert_eq!(colors, [Some(0x0080_8000)]);
13811710 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 };
13831715 assert_eq!(outline.tag_column_offset(), -12.0);
13841716 }
13851717
crates/draw/Cargo.toml+11-5
......@@ -4,13 +4,19 @@ version = "0.1.0"
44edition = "2024"
55publish = 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
712[dependencies]
8bytemuck = { version = "1.25", features = ["derive"] }
9image = { version = "0.25", default-features = false, features = ["gif", "jpeg", "png"] }
1013parley.workspace = true
11roxmltree = "0.21"
12swash = "0.2.10"
13wgpu.workspace = true
14
15bytemuck = { version = "1.25", features = ["derive"], optional = true }
16image = { 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
1521[dev-dependencies]
1622pollster = "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//! Paints layers of primitives with wgpu: glyphs and icons through one atlas, images,
2//! rounded rectangles and pen strokes. Each layer has its own transform and clip.
1//! What the page and the interface share: painting layers of glyphs, icons, paths,
2//! images, rounded rectangles and pen strokes with wgpu, each layer with its own transform
3//! and clip, and editing text within a laid-out line.
34
4mod icon;
5mod text;
5pub mod edit;
6#[cfg(feature = "render")]
7mod render;
68
7pub use text::{Decoration, Glyph, GlyphRun, Glyphs, paint_parley_run};
8
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}
9#[cfg(feature = "render")]
10pub use render::*;
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"
2020accesskit_winit = { version = "0.34.0", default-features = false, features = ["rwh_06"] }
2121objc2 = "0.5.2"
2222objc2-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::{
1313};
1414use objc2_foundation::{
1515 MainThreadMarker, NSAttributedString, NSCalendar, NSCalendarUnit, NSDate, NSDateFormatter,
16 NSDateFormatterStyle, NSRange, NSString,
16 NSDateFormatterStyle, NSPoint, NSRange, NSRect, NSSize, NSString,
1717};
1818use std::{cell::Cell, sync::OnceLock};
1919use winit::{
......@@ -261,6 +261,56 @@ pub fn configure_presentation(surface: &wgpu::Surface<'_>) {
261261 }
262262}
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
264314pub fn text_colors() -> [[f32; 4]; 2] {
265315 unsafe {
266316 let space = NSColorSpace::sRGBColorSpace();
crates/snowbound/src/main.rs+705-230
......@@ -1,16 +1,15 @@
1mod art;
12mod macos;
23
34use canvas::gpu::page::PageScene;
4use canvas::interaction::{
5 Cursor, Key as PageKey, Modifiers, NamedKey as PageNamedKey, PageView, Request, Response,
6 TextColors, accessibility,
7};
5use canvas::interaction::{Cursor, PageView, Request, Response, TextColors, accessibility};
86use canvas::{
97 date::DateField,
108 document::TextDocument,
119 editor::{CanvasEditor, DEFAULT_OUTLINE_WIDTH, TextOutline},
1210 layout::TextEngine,
1311};
12use canvas::{gpu::tag_sources, outline::TagIcon};
1413use draw::Renderer;
1514use onestore::ExGuid;
1615use onestore::document::Format;
......@@ -22,7 +21,7 @@ use std::{
2221 sync::Arc,
2322 time::Instant,
2423};
25use ui::{Axis, Flags, Id, Spec, Theme, Ui, fill, fit, px};
24use ui::{Axis, Flags, Id, Shape, Spec, Theme, Ui, children, fill, fit, px};
2625use winit::{
2726 application::ApplicationHandler,
2827 dpi::{LogicalSize, PhysicalPosition, PhysicalSize},
......@@ -33,10 +32,15 @@ use winit::{
3332 window::{CursorIcon, Window, WindowId},
3433};
3534
36/// Height of the window's top strip, which holds the traffic lights and section tabs.
37const STRIP: f32 = 38.0;
38/// Room the traffic lights take at the strip's leading edge.
35/// Height of the macOS title bar, which holds the traffic lights.
36const TITLE: f32 = 28.0;
37/// Room the traffic lights take at the title bar's leading edge.
3938const 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;
4044const PAGE_LIST: f32 = 240.0;
4145
4246#[derive(Debug)]
......@@ -48,6 +52,17 @@ enum UserEvent {
4852 Sync,
4953 /// Scripted input from `SNOWBOUND_REPLAY`.
5054 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 }
5166}
5267
5368#[derive(Debug)]
......@@ -55,6 +70,9 @@ enum Replay {
5570 Input(ui::Event),
5671 /// Paints the next frame into a PNG as well as the window.
5772 Snapshot(PathBuf),
73 /// A frame during a wait, as a visible window's display would ask for.
74 Tick,
75 Appearance(winit::window::Theme),
5876}
5977
6078impl From<accesskit_winit::Event> for UserEvent {
......@@ -63,46 +81,17 @@ impl From<accesskit_winit::Event> for UserEvent {
6381 }
6482}
6583
66fn trace_input(event: &impl std::fmt::Debug) {
67 if std::env::var_os("SNOWBOUND_TRACE_INPUT").is_some() {
68 eprintln!("Input {:?}: {event:?}", std::time::SystemTime::now());
84/// With `SNOWBOUND_PROFILE` set, prints how long a phase of the frame took since `start`.
85fn lap(phase: &str, start: Instant) {
86 static PROFILE: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
87 if *PROFILE.get_or_init(|| std::env::var_os("SNOWBOUND_PROFILE").is_some()) {
88 eprintln!("profile\t{phase}\t{}", start.elapsed().as_micros());
6989 }
7090}
7191
72fn page_key(key: &Key) -> PageKey {
73 match key {
74 Key::Character(text) => PageKey::Character(text.to_string()),
75 Key::Named(named) => PageKey::Named(match named {
76 NamedKey::Escape => PageNamedKey::Escape,
77 NamedKey::Tab => PageNamedKey::Tab,
78 NamedKey::Space => PageNamedKey::Space,
79 NamedKey::Enter => PageNamedKey::Enter,
80 NamedKey::Backspace => PageNamedKey::Backspace,
81 NamedKey::Delete => PageNamedKey::Delete,
82 NamedKey::ArrowLeft => PageNamedKey::ArrowLeft,
83 NamedKey::ArrowRight => PageNamedKey::ArrowRight,
84 NamedKey::ArrowUp => PageNamedKey::ArrowUp,
85 NamedKey::ArrowDown => PageNamedKey::ArrowDown,
86 NamedKey::Home => PageNamedKey::Home,
87 NamedKey::End => PageNamedKey::End,
88 NamedKey::Alt
89 | NamedKey::AltGraph
90 | NamedKey::Control
91 | NamedKey::Shift
92 | NamedKey::Super
93 | NamedKey::Meta => PageNamedKey::Modifier,
94 _ => PageNamedKey::Other,
95 }),
96 _ => PageKey::Named(PageNamedKey::Other),
97 }
98}
99
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(),
92fn trace_input(event: &impl std::fmt::Debug) {
93 if std::env::var_os("SNOWBOUND_TRACE_INPUT").is_some() {
94 eprintln!("Input {:?}: {event:?}", std::time::SystemTime::now());
10695 }
10796}
10897
......@@ -123,6 +112,8 @@ struct App {
123112 proxy: EventLoopProxy<UserEvent>,
124113 input: Option<Input>,
125114 substitutes: Vec<PathBuf>,
115 /// Font files for the interface in place of the system's.
116 interface_font: Vec<Vec<u8>>,
126117 state: Option<State>,
127118 startup_error: Option<Box<dyn Error>>,
128119}
......@@ -156,6 +147,8 @@ struct Tab {
156147
157148/// The sections the tabs offer.
158149struct Library {
150 /// The notebook's folder name, or the section file's.
151 name: String,
159152 notebook: Option<notebook::session::Notebook>,
160153 tabs: Vec<Tab>,
161154 cache: PathBuf,
......@@ -220,6 +213,8 @@ enum Command {
220213struct State {
221214 window: Arc<Window>,
222215 proxy: EventLoopProxy<UserEvent>,
216 /// Asks for a frame when a worker thread finishes something the page shows.
217 redraw: std::task::Waker,
223218 instance: wgpu::Instance,
224219 surface: wgpu::Surface<'static>,
225220 config: wgpu::SurfaceConfiguration,
......@@ -230,9 +225,15 @@ struct State {
230225 session: Option<Session>,
231226 /// Text filtering the page list.
232227 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>,
233232 commands: Vec<Command>,
234233 /// The page changed during this frame, to be saved and announced after it.
235234 changed: bool,
235 /// Only the view scrolled or zoomed during this frame, to be announced after it.
236 moved: bool,
236237 /// Whether the page last heard it had keyboard focus.
237238 page_focused: bool,
238239 window_focused: bool,
......@@ -260,6 +261,11 @@ fn page() -> Id {
260261 Id::ROOT.child("page")
261262}
262263
264/// The section's colour around the page and its tabs.
265fn frame() -> Id {
266 Id::ROOT.child("frame")
267}
268
263269/// The field filtering the page list.
264270fn filter() -> Id {
265271 Id::ROOT.child("filter")
......@@ -271,6 +277,7 @@ impl State {
271277 proxy: EventLoopProxy<UserEvent>,
272278 input: Input,
273279 substitutes: &[PathBuf],
280 interface_font: Vec<Vec<u8>>,
274281 ) -> Result<Self, Box<dyn Error>> {
275282 let window = Arc::new(
276283 event_loop.create_window(
......@@ -344,6 +351,7 @@ impl State {
344351 }
345352 Input::Section { file, title, cache } => {
346353 let opened = Library {
354 name: file_name(file.parent().unwrap_or(&file)),
347355 notebook: None,
348356 tabs: vec![Tab {
349357 name: file
......@@ -374,6 +382,7 @@ impl State {
374382 return Err(format!("No readable sections in {}", root.display()).into());
375383 }
376384 let opened = Library {
385 name: file_name(&root),
377386 notebook: Some(notebook),
378387 tabs,
379388 cache,
......@@ -398,10 +407,20 @@ impl State {
398407 window.set_ime_allowed(true);
399408 window.request_redraw();
400409 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 }
402420 ui.set_focus(Some(page()));
403421 let state = Self {
404422 window,
423 redraw: Arc::new(Redraw(proxy.clone())).into(),
405424 proxy,
406425 instance,
407426 surface,
......@@ -419,8 +438,11 @@ impl State {
419438 library,
420439 session,
421440 filter: String::new(),
441 pages_open: true,
442 app_icon: macos::app_icon((16.0 * dpr).round() as u32),
422443 commands: Vec::new(),
423444 changed: false,
445 moved: false,
424446 page_focused: true,
425447 window_focused: true,
426448 pointer: [0.0; 2],
......@@ -442,6 +464,7 @@ impl State {
442464 /// Builds, lays out and paints one frame, then does what it asked for and asks for the
443465 /// frame that shows the result.
444466 fn frame(&mut self) -> Result<(), Box<dyn Error>> {
467 let start = Instant::now();
445468 let size = self.window.inner_size();
446469 let scale = self.window.scale_factor() as f32;
447470 self.ui.begin(
......@@ -449,8 +472,10 @@ impl State {
449472 scale,
450473 Instant::now(),
451474 );
452 self.build()?;
475 let (section, open_tab, open_page) = self.build()?;
453476 self.ui.end();
477 self.edges(section, open_tab, open_page);
478 lap("build", start);
454479 if let Some(rect) = self.ui.rect(page()) {
455480 let size = [
456481 ((rect[2] - rect[0]) * scale).round() as u32,
......@@ -476,6 +501,7 @@ impl State {
476501 self.window.set_ime_allowed(ime);
477502 }
478503 self.draw()?;
504 lap("drawn", start);
479505 let commands = std::mem::take(&mut self.commands);
480506 let follow = !commands.is_empty() || self.ui.wants_frame();
481507 for command in commands {
......@@ -483,101 +509,100 @@ impl State {
483509 }
484510 if std::mem::take(&mut self.changed) {
485511 self.after_edit()?;
512 } else if self.moved {
513 self.after_move()?;
486514 }
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 {
488518 self.window.request_redraw();
489519 }
520 lap("frame", start);
490521 Ok(())
491522 }
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;
494547 let theme = self.ui.theme.clone();
495 self.ui.open_as(
496 strip(),
548 self.title_bar(&theme);
549 self.toolbar(&theme)?;
550 self.ui.open(
551 "tabs",
497552 Spec {
498 flags: Flags::CLICKABLE,
499 size: [fill(), px(STRIP)],
553 size: [fill(), px(TAB_ROW)],
500554 fill: Some(theme.strip),
501 pad: [0.0, 8.0],
502 gap: 2.0,
555 pad: [FRAME, 0.0],
503556 ..Spec::default()
504557 },
505558 );
506559 self.ui.leaf(
507 "lights",
560 "corner",
508561 Spec {
509 size: [px(LIGHTS), px(1.0)],
562 size: [px(ROUNDING), px(1.0)],
510563 ..Spec::default()
511564 },
512565 );
513 match (&self.library, &self.session) {
566 let (clicked, open_tab) = match (&self.library, &self.session) {
514567 (Some(library), Some(session)) => {
515 for (index, tab) in library.tabs.iter().enumerate() {
516 let active = index == session.tab;
517 let id = self.ui.open(
518 ("tab", index),
519 Spec {
520 flags: Flags::CLICKABLE,
521 size: [fit(), px(STRIP - 8.0)],
522 text: Some(&tab.name),
523 color: Some(if active { theme.text } else { theme.text_dim }),
524 fill: Some(if active { theme.base } else { theme.strip }),
525 hover_fill: (!active).then_some(theme.chip),
526 radius: 6.0,
527 pad: [14.0 + 14.0, 0.0],
528 center: true,
529 ..Spec::default()
530 },
531 );
532 let swatch = tab.color.map_or(theme.text_dim, canvas::gpu::colorref);
533 let middle = (STRIP - 8.0) / 2.0;
534 self.ui
535 .mark([12.0, middle - 4.0, 20.0, middle + 4.0], swatch);
536 self.ui.close();
537 if self.ui.signal(id).clicked && !active {
538 self.commands.push(Command::OpenSection(index));
539 }
540 }
541 }
542 _ => {
543 self.ui.leaf(
544 "tab",
545 Spec {
546 size: [fit(), px(STRIP - 8.0)],
547 text: Some("Temporary page"),
548 fill: Some(theme.base),
549 radius: 6.0,
550 pad: [16.0, 0.0],
551 ..Spec::default()
552 },
553 );
568 let tabs: Vec<_> = library
569 .tabs
570 .iter()
571 .map(|tab| (tab.name.as_str(), section_color(tab.color)))
572 .collect();
573 ui::shell::section_tabs(
574 &mut self.ui,
575 "sections",
576 &tabs,
577 session.tab,
578 &section,
579 TAB_ROW,
580 )
554581 }
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));
555593 }
556 self.ui.leaf(
557 "space",
558 Spec {
559 size: [fill(), px(1.0)],
560 ..Spec::default()
561 },
562 );
563 if let Some(session) = &self.session {
564 self.ui.leaf(
565 "status",
566 Spec {
567 size: [fit(), px(STRIP - 8.0)],
568 text: Some(session.status),
569 color: Some(theme.text_dim),
570 pad: [14.0, 0.0],
571 ..Spec::default()
572 },
573 );
594 if self.session.is_some() {
595 self.page_tools(&theme);
574596 }
575597 self.ui.close();
576598
577 self.ui.open(
578 "body",
599 self.ui.open_as(
600 frame(),
579601 Spec {
580602 size: [fill(), fill()],
603 fill: Some(section.frame[0]),
604 gradient: Some(section.frame[1]),
605 pad: [FRAME, FRAME],
581606 ..Spec::default()
582607 },
583608 );
......@@ -601,7 +626,7 @@ impl State {
601626 Spec {
602627 flags: Flags::CUSTOM | Flags::FOCUSABLE,
603628 size: [fill(), fill()],
604 fill: Some([1.0; 4]),
629 fill: Some(theme.paper),
605630 ..Spec::default()
606631 },
607632 );
......@@ -630,37 +655,124 @@ impl State {
630655 }
631656 self.page_events(signal.events)?;
632657 self.ui.close();
633 if self.session.is_some() {
634 self.ui.leaf(
635 "separator",
636 Spec {
637 size: [px(1.0), fill()],
638 fill: Some(theme.separator),
639 ..Spec::default()
640 },
641 );
642 self.page_list(&theme);
643 }
658 let open_page = self.page_list(&theme, &section);
644659 self.ui.close();
645 Ok(())
660 Ok((section, open_tab, open_page))
646661 }
647662
648 fn conflict_bar(&mut self, theme: &Theme) {
649 self.ui.open(
650 "conflict",
663 /// Borders the open section tab and the frame's top, and rounds and borders the page
664 /// together with the open page's tab, joined where they meet.
665 fn edges(&mut self, section: ui::Section, open_tab: Id, open_page: Option<Id>) {
666 let panel = self.ui.rect(frame().child("panel"));
667 let (Some(frame), Some(page)) = (self.ui.rect(frame()), self.ui.rect(page())) else {
668 return;
669 };
670 let [left, top, right, bottom] = page;
671 let tab = open_page
672 .and_then(|id| self.ui.rect(id))
673 .zip(panel)
674 .and_then(|(row, panel)| {
675 let [start, end] = [
676 row[1].max(panel[1]).max(top),
677 row[3].min(panel[3]).min(bottom),
678 ];
679 let reach = row[2].min(panel[2]);
680 (reach - right > 2.0 * ROUNDING && end - start > 2.0 * ROUNDING).then(|| {
681 // A tab too near the page's corner joins it along the edge.
682 let start = if start < top + 2.0 * ROUNDING {
683 top
684 } else {
685 start
686 };
687 let end = if end > bottom - 2.0 * ROUNDING {
688 bottom
689 } else {
690 end
691 };
692 [start, end, reach]
693 })
694 });
695 let mut outline = vec![([left, top], ROUNDING)];
696 match tab {
697 Some([start, end, reach]) => {
698 if start > top {
699 outline.extend([([right, top], ROUNDING), ([right, start], ROUNDING)]);
700 }
701 outline.extend([([reach, start], ROUNDING), ([reach, end], ROUNDING)]);
702 if end < bottom {
703 outline.extend([([right, end], ROUNDING), ([right, bottom], ROUNDING)]);
704 }
705 }
706 None => outline.extend([([right, top], ROUNDING), ([right, bottom], ROUNDING)]),
707 }
708 outline.push(([left, bottom], ROUNDING));
709 let height = (frame[3] - frame[1]).max(1.0);
710 let paper = self.ui.theme.paper;
711 self.ui.round_corners(&outline, paper, |y| {
712 ui::mix(section.frame[0], section.frame[1], (y - frame[1]) / height)
713 });
714 self.ui.border(&outline, true, section.edge);
715 // The frame's top corners share the page's centres; its top edge breaks where the
716 // open tab stands on it.
717 let [start, end] = [frame[0], frame[2]];
718 let outer = FRAME + ROUNDING;
719 let strip = self.ui.theme.strip;
720 self.ui.round_corners(
721 &[
722 ([start, frame[1]], outer),
723 ([end, frame[1]], outer),
724 ([end, frame[3]], 0.0),
725 ([start, frame[3]], 0.0),
726 ],
727 paper,
728 |_| strip,
729 );
730 let [foot, toe] = self.ui.rect(open_tab).map_or([end; 2], ui::shell::tab_base);
731 for edge in [
732 [
733 ([start, frame[1] + outer], 0.0),
734 ([start, frame[1]], outer),
735 ([foot, frame[1]], 0.0),
736 ],
737 [
738 ([toe, frame[1]], 0.0),
739 ([end, frame[1]], outer),
740 ([end, frame[1] + outer], 0.0),
741 ],
742 ] {
743 self.ui.border(&edge, false, section.edge);
744 }
745 }
746
747 /// The window's title bar beside the traffic lights: the application's icon, the
748 /// notebook's name and the saving status.
749 fn title_bar(&mut self, theme: &Theme) {
750 self.ui.open_as(
751 strip(),
651752 Spec {
652 size: [fill(), px(40.0)],
653 fill: Some(theme.panel),
654 pad: [12.0, 7.0],
655 gap: 8.0,
753 flags: Flags::CLICKABLE,
754 size: [fill(), px(TITLE)],
755 fill: Some(theme.strip),
656756 ..Spec::default()
657757 },
658758 );
659759 self.ui.leaf(
660 "message",
760 "lights",
661761 Spec {
662 size: [fit(), px(26.0)],
663 text: Some("This page also changed on another computer."),
762 size: [px(LIGHTS), px(1.0)],
763 ..Spec::default()
764 },
765 );
766 self.ui.leaf(
767 "notebook",
768 Spec {
769 size: [fit(), px(TITLE)],
770 image: self.app_icon.as_ref(),
771 text: Some(
772 self.library
773 .as_ref()
774 .map_or("Temporary page", |library| &library.name),
775 ),
664776 ..Spec::default()
665777 },
666778 );
......@@ -671,122 +783,341 @@ impl State {
671783 ..Spec::default()
672784 },
673785 );
674 if ui::button(&mut self.ui, "mine", "Keep mine").clicked {
675 self.commands.push(Command::Resolve { keep_mine: true });
676 }
677 if ui::button(&mut self.ui, "theirs", "Keep theirs").clicked {
678 self.commands.push(Command::Resolve { keep_mine: false });
786 if let Some(session) = &self.session {
787 self.ui.leaf(
788 "status",
789 Spec {
790 size: [fit(), px(TITLE)],
791 text: Some(session.status),
792 color: Some(theme.text_dim),
793 pad: [12.0, 0.0],
794 ..Spec::default()
795 },
796 );
679797 }
680798 self.ui.close();
681799 }
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) {
684947 let Some(session) = &self.session else {
685948 return;
686949 };
687950 self.ui.open(
688 "panel",
951 "tools",
689952 Spec {
690 axis: Axis::Y,
691 size: [px(PAGE_LIST), fill()],
692 fill: Some(theme.panel),
953 size: [px(PAGE_LIST), px(TAB_ROW)],
954 pad: [0.0, (TAB_ROW - ui::shell::TOOL) / 2.0],
955 gap: 3.0,
693956 ..Spec::default()
694957 },
695958 );
959 let focused = self.ui.focused() == Some(filter());
696960 self.ui.open(
697 "pages",
961 "search",
698962 Spec {
699 flags: Flags::SCROLL | Flags::CLIP,
700 axis: Axis::Y,
701 size: [fill(), fill()],
702 pad: [6.0, 6.0],
703 gap: 1.0,
963 size: [fill(), px(ui::shell::TOOL)],
964 fill: Some(theme.base),
965 border: Some(if focused { theme.accent } else { theme.chip }),
966 radius: 4.0,
967 pad: [6.0, 0.0],
704968 ..Spec::default()
705969 },
706970 );
707 let query = self.filter.to_lowercase();
708 let mut first = None;
709 for (space, title, level) in &session.pages {
710 if !title.to_lowercase().contains(&query) {
711 continue;
712 }
713 first.get_or_insert(*space);
714 let selected = *space == session.space;
715 let signal = self.ui.leaf(
716 space,
717 Spec {
718 flags: Flags::CLICKABLE,
719 size: [fill(), px(26.0)],
720 text: Some(if title.is_empty() {
721 "Untitled page"
722 } else {
723 title
724 }),
725 color: Some(if selected || !title.is_empty() {
726 theme.text
727 } else {
728 theme.text_dim
729 }),
730 fill: selected.then_some(theme.accent),
731 hover_fill: (!selected).then_some(theme.hover),
732 hover_border: (!selected).then_some(theme.accent),
733 radius: 4.0,
734 pad: [10.0 + 16.0 * level.saturating_sub(1) as f32, 0.0],
735 ..Spec::default()
736 },
737 );
738 if signal.clicked && !selected {
739 self.commands.push(Command::OpenPage(*space));
740 }
741 }
742 self.ui.close();
743 let placeholder = format!("Filter {} pages…", session.pages.len());
744 self.ui.open(
745 "footer",
971 self.ui.leaf(
972 "icon",
746973 Spec {
747 size: [fill(), px(38.0)],
748 pad: [6.0, 6.0],
974 size: [fit(), px(ui::shell::TOOL)],
975 icon: Some(art::SEARCH),
976 color: Some(theme.text_dim),
749977 ..Spec::default()
750978 },
751979 );
752 let before = self.filter.clone();
753980 let signal = ui::text_field(
754981 &mut self.ui,
755982 filter(),
756983 &mut self.filter,
757 &placeholder,
984 "Search pages",
758985 Spec {
759 size: [fill(), px(26.0)],
760 fill: Some(theme.base),
761 border: Some(theme.separator),
762 radius: 4.0,
763 pad: [8.0, 0.0],
986 size: [fill(), px(ui::shell::TOOL)],
987 pad: [6.0, 0.0],
764988 ..Spec::default()
765989 },
766990 );
767 // The list above was built with the filter as it stood.
768 if self.filter != before {
769 self.window.request_redraw();
991 if !self.filter.is_empty()
992 && ui::shell::tool_button(&mut self.ui, "clear", art::CLOSE, theme.text_dim).clicked
993 {
994 self.filter.clear();
770995 }
996 self.ui.close();
771997 for event in &signal.events {
772998 if let ui::Event::Key {
773999 key: Key::Named(key @ (NamedKey::Escape | NamedKey::Enter)),
7741000 ..
7751001 } = event
7761002 {
1003 let first = matching(&session.pages, &self.filter).next();
7771004 if *key == NamedKey::Enter
778 && let Some(space) = first.filter(|space| *space != session.space)
1005 && let Some((space, ..)) = first.filter(|(space, ..)| *space != session.space)
7791006 {
780 self.commands.push(Command::OpenPage(space));
1007 self.commands.push(Command::OpenPage(*space));
7811008 }
7821009 self.filter.clear();
7831010 self.ui.set_focus(Some(page()));
7841011 }
7851012 }
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 }
7861022 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 }
7871057 self.ui.close();
7881058 }
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
7901121 /// Hands the page the events routed to its box, in its device pixels.
7911122 fn page_events(&mut self, events: Vec<ui::Event>) -> Result<(), Box<dyn Error>> {
7921123 let scale = self.ui.scale();
......@@ -822,13 +1153,13 @@ impl State {
8221153 self.ui.set_focus(Some(filter()));
8231154 continue;
8241155 }
825 self.view.key(&page_key(&key), text.as_deref())?
1156 self.view.key(&ui::edit_key(&key), text.as_deref())?
8261157 }
8271158 ui::Event::Ime(Ime::Preedit(text, cursor)) => self.view.compose(text, cursor)?,
8281159 ui::Event::Ime(Ime::Commit(text)) => self.view.commit_text(text)?,
8291160 ui::Event::Ime(Ime::Disabled) => self.view.cancel_composition()?,
8301161 ui::Event::Modifiers(modifiers) => {
831 self.view.modifiers_changed(page_modifiers(modifiers))?
1162 self.view.modifiers_changed(ui::edit_modifiers(modifiers))?
8321163 }
8331164 };
8341165 self.respond(response);
......@@ -842,6 +1173,7 @@ impl State {
8421173 /// Notes a page change for after the frame and queues what the page asked of the host.
8431174 fn respond(&mut self, response: Response) {
8441175 self.changed |= response.changed;
1176 self.moved |= response.moved;
8451177 if let Some(request) = response.request {
8461178 self.commands.push(Command::Page(request));
8471179 }
......@@ -892,6 +1224,16 @@ impl State {
8921224
8931225 /// Follows an edit: the input method's position, accessibility, saving and the title.
8941226 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>> {
8951237 let scale = self.ui.scale();
8961238 let corner = self.ui.rect(page()).unwrap_or_default();
8971239 let [x0, y0, x1, y1] = self.view.caret_area()?;
......@@ -899,9 +1241,9 @@ impl State {
8991241 PhysicalPosition::new(x0 + corner[0] * scale, y0 + corner[1] * scale),
9001242 PhysicalSize::new(x1 - x0, y1 - y0),
9011243 );
1244 let start = Instant::now();
9021245 self.update_accessibility()?;
903 self.persist()?;
904 self.title();
1246 lap("accessibility", start);
9051247 Ok(())
9061248 }
9071249
......@@ -1095,20 +1437,13 @@ impl State {
10951437 self.access_adapter.update_if_active(|| {
10961438 match self.accessibility.update(
10971439 &view.editor,
1440 view.scene.as_ref(),
10981441 viewport,
10991442 &self.window.title(),
11001443 view.outline_preview(),
1444 view.object_focus().and_then(|focus| focus.read_only()),
11011445 ) {
1102 Ok(mut update) => {
1103 self.accessibility.append_page_fields(
1104 &mut update,
1105 view.scene.as_ref(),
1106 &view.editor,
1107 viewport,
1108 view.object_focus().and_then(|focus| focus.read_only()),
1109 );
1110 update
1111 }
1446 Ok(update) => update,
11121447 Err(failure) => {
11131448 error = Some(failure);
11141449 self.accessibility.deactivate();
......@@ -1185,6 +1520,7 @@ impl State {
11851520 }
11861521
11871522 fn draw(&mut self) -> Result<(), Box<dyn Error>> {
1523 let start = Instant::now();
11881524 if let Some(path) = self.snapshot.take() {
11891525 self.snapshot(&path)?;
11901526 }
......@@ -1222,9 +1558,12 @@ impl State {
12221558 return Err("Canvas surface validation failed".into());
12231559 }
12241560 };
1561 lap("acquire", start);
12251562 self.paint(&frame.texture.create_view(&Default::default()))?;
12261563 self.window.pre_present_notify();
1564 let start = Instant::now();
12271565 self.renderer.queue.present(frame);
1566 lap("present", start);
12281567 trace_input(&"Present submitted");
12291568 if reconfigure {
12301569 self.surface.configure(&self.renderer.device, &self.config);
......@@ -1234,8 +1573,19 @@ impl State {
12341573
12351574 /// Paints the interface with the page in its box.
12361575 fn paint(&mut self, target: &wgpu::TextureView) -> Result<(), Box<dyn Error>> {
1576 let start = Instant::now();
12371577 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);
12391589 let scale = self.ui.scale();
12401590 let corner = self.ui.rect(page()).unwrap_or_default();
12411591 let viewport = self.view.viewport;
......@@ -1261,6 +1611,7 @@ impl State {
12611611 })
12621612 .collect();
12631613 trace_input(&("Draw", viewport.origin, viewport.scale));
1614 let start = Instant::now();
12641615 self.renderer
12651616 .draw(
12661617 target,
......@@ -1268,7 +1619,9 @@ impl State {
12681619 self.ui.theme.base,
12691620 &layers,
12701621 )
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(())
12721625 }
12731626
12741627 /// Writes the frame to a PNG, so a covered window can be reviewed.
......@@ -1373,6 +1726,76 @@ impl State {
13731726 }
13741727}
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
13761799/// Section tabs for a notebook's readable top-level sections, in its order.
13771800fn tabs(notebook: &notebook::session::Notebook) -> Vec<Tab> {
13781801 notebook
......@@ -1481,11 +1904,19 @@ impl ApplicationHandler<UserEvent> for App {
14811904 }
14821905 return;
14831906 }
1907 UserEvent::Redraw => {
1908 if let Some(state) = &self.state {
1909 state.window.request_redraw();
1910 }
1911 return;
1912 }
14841913 UserEvent::Replay(replay) => {
14851914 if let Some(state) = &mut self.state {
14861915 match replay {
14871916 Replay::Input(event) => state.input(event),
14881917 Replay::Snapshot(path) => state.snapshot = Some(path),
1918 Replay::Tick => {}
1919 Replay::Appearance(appearance) => state.ui.theme = theme(appearance),
14891920 }
14901921 // A covered window gets no redraws, so each step draws its own frame.
14911922 if let Err(error) = state.frame() {
......@@ -1504,7 +1935,10 @@ impl ApplicationHandler<UserEvent> for App {
15041935 }
15051936 let was_marked = state.view.editor.marked_range().is_some();
15061937 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 }
15081942 accesskit_winit::WindowEvent::ActionRequested(request) => state.access_action(request),
15091943 accesskit_winit::WindowEvent::AccessibilityDeactivated => {
15101944 state.accessibility.deactivate();
......@@ -1528,6 +1962,7 @@ impl ApplicationHandler<UserEvent> for App {
15281962 self.proxy.clone(),
15291963 self.input.take().unwrap(),
15301964 &self.substitutes,
1965 std::mem::take(&mut self.interface_font),
15311966 )) {
15321967 Ok(state) => self.state = Some(state),
15331968 Err(error) => {
......@@ -1575,8 +2010,13 @@ impl ApplicationHandler<UserEvent> for App {
15752010 // lets the window show the previous frame at the new size.
15762011 return state.frame();
15772012 }
2013 WindowEvent::ThemeChanged(appearance) => {
2014 state.ui.theme = theme(appearance);
2015 state.window.request_redraw();
2016 }
15782017 WindowEvent::ScaleFactorChanged { scale_factor, .. } => {
15792018 state.renderer.clear_glyph_cache();
2019 state.app_icon = macos::app_icon((16.0 * scale_factor).round() as u32);
15802020 let response = state.view.scale_factor_changed(scale_factor as f32)?;
15812021 state.respond(response);
15822022 state.window.request_redraw();
......@@ -1589,7 +2029,7 @@ impl ApplicationHandler<UserEvent> for App {
15892029 trace_input(&("Window occluded", occluded));
15902030 state.occluded = occluded;
15912031 if !occluded {
1592 state.changed = true;
2032 state.moved = true;
15932033 state.window.request_redraw();
15942034 }
15952035 }
......@@ -1654,7 +2094,8 @@ impl ApplicationHandler<UserEvent> for App {
16542094
16552095/// Feeds a development script to the window from another thread, one command per line
16562096/// 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`.
16582099fn replay(script: String, proxy: EventLoopProxy<UserEvent>) -> Result<(), Box<dyn Error>> {
16592100 let mut steps = Vec::new();
16602101 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
17022143 ))),
17032144 "wait" => Err(std::time::Duration::from_millis(rest.parse()?)),
17042145 "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 })),
17052151 _ => return Err(format!("Unknown replay command: {line}").into()),
17062152 });
17072153 }
......@@ -1714,7 +2160,13 @@ fn replay(script: String, proxy: EventLoopProxy<UserEvent>) -> Result<(), Box<dy
17142160 }
17152161 let _ = proxy.send_event(UserEvent::Replay(replay));
17162162 }
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 }
17182170 }
17192171 }
17202172 });
......@@ -1725,6 +2177,7 @@ fn main() -> Result<(), Box<dyn Error>> {
17252177 let mut args = std::env::args_os().skip(1);
17262178 let mut positional = Vec::new();
17272179 let mut substitutes = Vec::new();
2180 let mut interface_font = Vec::new();
17282181 let mut reference = None;
17292182 let mut editable = false;
17302183 let mut section = None;
......@@ -1736,6 +2189,27 @@ fn main() -> Result<(), Box<dyn Error>> {
17362189 args.next()
17372190 .ok_or("Provide a font file after --substitute-font.")?,
17382191 ));
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 }
17392213 } else if arg == "--notebook" {
17402214 notebook = Some(PathBuf::from(
17412215 args.next()
......@@ -1794,7 +2268,7 @@ fn main() -> Result<(), Box<dyn Error>> {
17942268 }
17952269 if positional.len() > 2 {
17962270 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]..."
17982272 .into(),
17992273 );
18002274 }
......@@ -1842,6 +2316,7 @@ fn main() -> Result<(), Box<dyn Error>> {
18422316 proxy: event_loop.create_proxy(),
18432317 input: Some(input),
18442318 substitutes,
2319 interface_font,
18452320 state: None,
18462321 startup_error: None,
18472322 };
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
1111 node.computed[axis] = match node.size[axis].size {
1212 Size::Pixels(pixels) => pixels,
1313 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]
1525 }
1626 Size::Fraction(_) | Size::Children => 0.0,
1727 };
crates/ui/src/lib.rs+348-52
......@@ -4,22 +4,27 @@
44//! events are routed before building and its layout is solved after.
55
66mod layout;
7pub mod shell;
78mod text;
89mod theme;
910mod widgets;
1011
11pub use theme::Theme;
12pub use widgets::{button, scrollbar, text_field};
12pub use theme::{Section, Shades, Theme};
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;
1520use std::{
1621 collections::HashMap,
1722 hash::{DefaultHasher, Hash, Hasher},
1823 ops::BitOr,
1924 rc::Rc,
20 time::Instant,
25 time::{Duration, Instant},
2126};
22use text::{Label, Painted, Texts};
27use text::{Label, Texts};
2328use winit::{
2429 event::{Ime, MouseButton},
2530 keyboard::{Key, ModifiersState},
......@@ -28,6 +33,11 @@ use winit::{
2833
2934/// Seconds for an animated value to close half of its remaining distance.
3035const 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
3242/// A box's identity across frames: its parent's id combined with a builder-chosen part.
3343#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
......@@ -139,6 +149,18 @@ impl BitOr for Flags {
139149 }
140150}
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
142164/// What a box is this frame. Colours are linear RGBA.
143165#[derive(Clone, Default)]
144166pub struct Spec<'a> {
......@@ -150,11 +172,20 @@ pub struct Spec<'a> {
150172 /// The label's colour; the theme's text colour otherwise.
151173 pub color: Option<[f32; 4]>,
152174 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]>,
153179 /// The fill under the pointer, blended in as hover animates.
154180 pub hover_fill: Option<[f32; 4]>,
155181 pub border: Option<[f32; 4]>,
156182 pub hover_border: Option<[f32; 4]>,
157183 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>,
158189 /// Inset of the label and children on each axis.
159190 pub pad: [f32; 2],
160191 /// Space between children along the flow.
......@@ -190,6 +221,8 @@ pub struct Signal {
190221 pub hovered: bool,
191222 /// The primary button went down on the box.
192223 pub pressed: bool,
224 /// What the press selects text by: a single, double or triple click.
225 pub unit: SelectionUnit,
193226 /// The primary button came up over the box it went down on.
194227 pub clicked: bool,
195228 /// The primary button is held after pressing the box.
......@@ -224,10 +257,15 @@ struct Built {
224257 label: Option<Rc<Label>>,
225258 color: [f32; 4],
226259 fill: Option<[f32; 4]>,
260 gradient: Option<[f32; 4]>,
261 shadow: Option<[f32; 4]>,
227262 hover_fill: Option<[f32; 4]>,
228263 border: Option<[f32; 4]>,
229264 hover_border: Option<[f32; 4]>,
230265 radius: f32,
266 shape: Shape,
267 icon: Option<&'static [&'static str]>,
268 image: Option<RasterImage>,
231269 pad: [f32; 2],
232270 gap: f32,
233271 center: bool,
......@@ -245,17 +283,20 @@ struct Built {
245283struct State {
246284 touched: u64,
247285 hot: f32,
248 active: f32,
286 /// The box paints differently under the pointer, so its hover is worth easing.
287 hovers: bool,
249288 scroll: f32,
250289 scroll_target: f32,
251290 /// Height of a scrolling box's children last frame.
252291 content: f32,
253292 rect: [f32; 4],
254 /// A text field's caret and selection anchor, in bytes.
255 caret: usize,
256 mark: usize,
293 /// A text field's selection, and the selection and unit of the press a drag extends.
294 selection: Selection,
295 press: (Selection, SelectionUnit),
257296 /// Where a dragged scrollbar thumb was taken, from its start.
258297 grab: f32,
298 /// An animated value and its target, for `Ui::animate`.
299 tween: Option<[f32; 2]>,
259300}
260301
261302struct Hit {
......@@ -270,13 +311,39 @@ enum Display {
270311 Rect {
271312 rect: [f32; 4],
272313 fill: [f32; 4],
314 /// The fill's colour at the bottom.
315 shade: Option<[f32; 4]>,
273316 border: Option<[f32; 4]>,
274317 radius: f32,
275318 },
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 },
276342 Text {
277 painted: usize,
343 label: Rc<Label>,
278344 origin: [f32; 2],
279345 clip: [f32; 4],
346 color: [f32; 4],
280347 },
281348 Custom {
282349 id: Id,
......@@ -302,13 +369,14 @@ pub struct Ui {
302369 active: Option<Id>,
303370 focus: Option<Id>,
304371 modifiers: ModifiersState,
372 clicks: Clicks,
305373 display: Vec<Display>,
306 painted: Vec<Painted>,
307374 animating: bool,
308375}
309376
310377impl 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 {
312380 Self {
313381 theme,
314382 frame: 0,
......@@ -326,8 +394,8 @@ impl Ui {
326394 active: None,
327395 focus: None,
328396 modifiers: ModifiersState::empty(),
397 clicks: Clicks::new(double_click),
329398 display: Vec::new(),
330 painted: Vec::new(),
331399 animating: false,
332400 }
333401 }
......@@ -412,7 +480,7 @@ impl Ui {
412480 for event in std::mem::take(&mut self.queue) {
413481 self.route(event);
414482 }
415 self.animate(dt);
483 self.ease(dt);
416484 }
417485
418486 fn route(&mut self, event: Event) {
......@@ -438,7 +506,7 @@ impl Ui {
438506 Event::Button {
439507 button,
440508 pressed: true,
441 ..
509 at,
442510 } => {
443511 let Some(point) = self.pointer else {
444512 return;
......@@ -448,7 +516,9 @@ impl Ui {
448516 && button == MouseButton::Left
449517 {
450518 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);
452522 if self.hit_flags(id).contains(Flags::FOCUSABLE) {
453523 self.focus = Some(id);
454524 }
......@@ -547,21 +617,23 @@ impl Ui {
547617 .map(|hit| hit.id)
548618 }
549619
550 fn animate(&mut self, dt: f32) {
620 fn ease(&mut self, dt: f32) {
551621 let rate = 1.0 - 0.5_f32.powf(dt / HALF_LIFE);
552622 let mut animating = false;
553623 for (id, state) in &mut self.states {
554624 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 }
555628 let targets = [
556629 (&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 ),
562630 (&mut state.scroll, state.scroll_target, 0.25),
563631 ];
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) {
565637 let delta = target - *value;
566638 if delta.abs() < close {
567639 *value = target;
......@@ -617,6 +689,29 @@ impl Ui {
617689 self.signal(id)
618690 }
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
620715 /// Paints `color` over the current box at `rect`, relative to its corner.
621716 pub fn mark(&mut self, rect: [f32; 4], color: [f32; 4]) {
622717 let index = *self.stack.last().unwrap();
......@@ -639,6 +734,7 @@ impl Ui {
639734 for node in &self.nodes {
640735 let state = self.states.entry(node.id).or_default();
641736 state.rect = node.rect;
737 state.hovers = node.hover_fill.is_some() || node.hover_border.is_some();
642738 if node.flags.contains(Flags::SCROLL) {
643739 state.content = node.content;
644740 let most = (node.content - (node.rect[3] - node.rect[1])).max(0.0);
......@@ -650,7 +746,6 @@ impl Ui {
650746 .retain(|id, state| state.touched == self.frame || *id == Id::ROOT);
651747 self.texts.prune(self.frame);
652748 self.display.clear();
653 self.painted.clear();
654749 self.hits.clear();
655750 self.paint(0, None);
656751 for id in [&mut self.hover, &mut self.active, &mut self.focus] {
......@@ -665,20 +760,59 @@ impl Ui {
665760 let state = &self.states[&node.id];
666761 let rect = node.rect;
667762 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.
668765 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 )),
670771 (base, None) => base,
671772 };
672773 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 });
673781 let border = blend(node.border, node.hover_border);
674 if fill.is_some() || border.is_some() {
675 self.display.push(Display::Rect {
676 rect,
677 fill: fill.unwrap_or([0.0; 4]),
678 border,
679 radius: node.radius,
782 let size = [rect[2] - rect[0], rect[3] - rect[1]];
783 if let Some(shadow) = node.shadow {
784 self.display.push(Display::Path {
785 data: outline(node.shape, size, node.radius),
786 origin: [rect[0], rect[1]],
787 style: PathStyle::Shadow(SHADOW),
788 colors: [shadow; 2],
680789 });
681790 }
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 }
682816 for (mark, color) in &node.marks {
683817 self.display.push(Display::Rect {
684818 rect: [
......@@ -688,30 +822,43 @@ impl Ui {
688822 rect[1] + mark[3],
689823 ],
690824 fill: *color,
825 shade: None,
691826 border: None,
692827 radius: 0.0,
693828 });
694829 }
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 }
695856 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 };
707857 let y = (rect[1] + rect[3] - label.size[1]) / 2.0;
708858 self.display.push(Display::Text {
709 painted: self.painted.len(),
859 label: label.clone(),
710860 origin: [x, y],
711861 clip: inner,
712 });
713 self.painted.push(Painted {
714 label: label.clone(),
715862 color: node.color,
716863 });
717864 }
......@@ -768,10 +915,17 @@ impl Ui {
768915 Display::Rect {
769916 rect,
770917 fill,
918 shade,
771919 border,
772920 radius,
773921 } => {
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 {
775929 primitives.push(if *radius > 0.0 {
776930 Primitive::RoundedRect {
777931 rect: *rect,
......@@ -795,14 +949,47 @@ impl Ui {
795949 });
796950 }
797951 }
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 }),
798983 Display::Text {
799 painted,
984 label,
800985 origin,
801986 clip,
987 color,
802988 } => primitives.push(Primitive::Text {
803 text: &self.painted[*painted],
989 text: &**label,
804990 origin: *origin,
805991 clip: Some(*clip),
992 ink: *color,
806993 }),
807994 Display::Custom { id, rect } => {
808995 if !primitives.is_empty() {
......@@ -828,9 +1015,9 @@ impl Ui {
8281015 (&mut self.texts, self.frame)
8291016 }
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)) {
8321019 let state = self.states.entry(id).or_default();
833 (&mut state.caret, &mut state.mark)
1020 (&mut state.selection, &mut state.press)
8341021 }
8351022
8361023 pub(crate) fn grab(&mut self, id: Id) -> &mut f32 {
......@@ -839,6 +1026,16 @@ impl Ui {
8391026}
8401027
8411028impl 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
8421039 fn new(
8431040 id: Id,
8441041 parent: usize,
......@@ -856,10 +1053,15 @@ impl Built {
8561053 label,
8571054 color: spec.color.unwrap_or(text),
8581055 fill: spec.fill,
1056 gradient: spec.gradient,
1057 shadow: spec.shadow,
8591058 hover_fill: spec.hover_fill,
8601059 border: spec.border,
8611060 hover_border: spec.hover_border,
8621061 radius: spec.radius,
1062 shape: spec.shape,
1063 icon: spec.icon,
1064 image: spec.image.cloned(),
8631065 pad: spec.pad,
8641066 gap: spec.gap,
8651067 center: spec.center,
......@@ -874,7 +1076,101 @@ impl Built {
8741076 }
8751077}
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] {
8781174 std::array::from_fn(|i| a[i] + (b[i] - a[i]) * t)
8791175}
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::*;
22use std::time::Duration;
33use winit::keyboard::NamedKey;
44
5const DOUBLE_CLICK: Duration = Duration::from_millis(500);
6
57thread_local! {
68 static START: Instant = Instant::now();
79 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)) {
2123
2224#[test]
2325fn 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);
2527 let mut ids = Vec::new();
2628 frame(&mut ui, |ui| {
2729 ui.open(
......@@ -55,7 +57,7 @@ fn fill_yields_to_strict_siblings_along_the_flow() {
5557
5658#[test]
5759fn children_sum_with_gaps_and_padding() {
58 let mut ui = Ui::new(Theme::dark());
60 let mut ui = Ui::new(Theme::dark(), DOUBLE_CLICK);
5961 let mut column = None;
6062 let mut second = None;
6163 frame(&mut ui, |ui| {
......@@ -107,7 +109,7 @@ fn button_frame(ui: &mut Ui) -> Signal {
107109
108110#[test]
109111fn 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);
111113 button_frame(&mut ui);
112114 let at = Instant::now();
113115 ui.event(Event::PointerMoved([10.0, 10.0]));
......@@ -147,9 +149,28 @@ fn clicks_use_the_previous_layout_and_hover_animates() {
147149 assert!(!ui.wants_frame());
148150}
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
150171#[test]
151172fn 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);
153174 let build = |ui: &mut Ui| {
154175 let mut signals = Vec::new();
155176 frame(ui, |ui| {
......@@ -225,7 +246,7 @@ fn custom_boxes_receive_their_events_and_a_leave_when_the_pointer_moves_off() {
225246
226247#[test]
227248fn 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);
229250 let mut list = None;
230251 let mut last = None;
231252 let mut build = |ui: &mut Ui| {
......@@ -271,69 +292,192 @@ fn wheel_scrolls_within_the_content_and_clips_children() {
271292 ));
272293}
273294
274#[test]
275fn text_fields_edit_with_keys_and_selection() {
276 let mut ui = Ui::new(Theme::dark());
277 let mut text = String::from("café");
278 let build = |ui: &mut Ui, text: &mut String| {
279 let mut signal = Signal::default();
280 frame(ui, |ui| {
281 signal = text_field(
282 ui,
283 Id::ROOT.child("filter"),
284 text,
285 "Filter",
286 Spec {
287 size: [px(200.0), px(26.0)],
288 pad: [6.0, 0.0],
289 ..Spec::default()
290 },
291 );
292 });
293 signal
294 };
295 build(&mut ui, &mut text);
296 let id = Id::ROOT.child("filter");
297 ui.set_focus(Some(id));
298 let key = |named| Event::Key {
295fn field_frame(ui: &mut Ui, text: &mut String) -> Signal {
296 let mut signal = Signal::default();
297 frame(ui, |ui| {
298 signal = text_field(
299 ui,
300 field(),
301 text,
302 "Filter",
303 Spec {
304 size: [px(200.0), px(26.0)],
305 pad: [6.0, 0.0],
306 ..Spec::default()
307 },
308 );
309 });
310 signal
311}
312
313fn field() -> Id {
314 Id::ROOT.child("filter")
315}
316
317fn key(named: NamedKey) -> Event {
318 Event::Key {
299319 key: Key::Named(named),
300320 text: None,
301 };
302 ui.event(Event::Key {
303 key: Key::Named(NamedKey::End),
304 text: None,
321 }
322}
323
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,
305356 });
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));
306363 ui.event(key(NamedKey::Backspace));
307 ui.event(Event::Key {
308 key: Key::Character("e".into()),
309 text: Some("e".into()),
310 });
311 build(&mut ui, &mut text);
364 ui.event(typed("e"));
365 field_frame(&mut ui, &mut text);
312366 assert_eq!(text, "cafe");
313367 ui.event(Event::Modifiers(ModifiersState::SHIFT));
314368 ui.event(key(NamedKey::ArrowLeft));
315369 ui.event(key(NamedKey::ArrowLeft));
316 build(&mut ui, &mut text);
370 field_frame(&mut ui, &mut text);
317371 ui.event(Event::Modifiers(ModifiersState::empty()));
318372 ui.event(Event::Ime(Ime::Commit("é".into())));
319 build(&mut ui, &mut text);
373 field_frame(&mut ui, &mut text);
320374 assert_eq!(text, "caé");
321375 ui.event(Event::Modifiers(ModifiersState::SUPER));
322 ui.event(Event::Key {
323 key: Key::Character("a".into()),
324 text: Some("a".into()),
325 });
326 build(&mut ui, &mut text);
376 ui.event(typed("a"));
377 field_frame(&mut ui, &mut text);
327378 ui.event(Event::Modifiers(ModifiersState::empty()));
328379 ui.event(key(NamedKey::Delete));
329 let signal = build(&mut ui, &mut text);
380 let signal = field_frame(&mut ui, &mut text);
330381 assert!(signal.focused);
331382 assert_eq!(text, "");
332383}
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
334478#[test]
335479fn 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);
337481 let mut offset = 0.0;
338482 let build = |ui: &mut Ui, offset: &mut f32| {
339483 frame(ui, |ui| {
crates/ui/src/text.rs+30-30
......@@ -1,35 +1,23 @@
11use draw::{GlyphRun, Glyphs, RenderError};
22use parley::{
3 Affinity, FontContext, FontFamily, GenericFamily, Layout, LayoutContext, PositionedLayoutItem,
4 StyleProperty, editing::Cursor,
3 FontContext, FontFamily, FontFamilyName, GenericFamily, Layout, LayoutContext,
4 PositionedLayoutItem, StyleProperty, fontique::Blob,
55};
6use std::{collections::HashMap, rc::Rc};
6use std::{borrow::Cow, collections::HashMap, rc::Rc, sync::Arc};
77
88/// A shaped single line of interface text, in logical pixels.
99pub(crate) struct Label {
10 layout: Layout<()>,
10 pub layout: Layout<()>,
1111 pub size: [f32; 2],
1212}
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
2814/// Labels shaped this frame or the previous one, keyed by their text and size.
2915#[derive(Default)]
3016pub(crate) struct Texts {
3117 fonts: FontContext,
3218 context: LayoutContext<()>,
19 /// A family registered in place of the system's interface font.
20 family: Option<String>,
3321 cache: HashMap<(String, u32), (Rc<Label>, u64)>,
3422}
3523
......@@ -44,9 +32,13 @@ impl Texts {
4432 let mut builder = self
4533 .context
4634 .ranged_builder(&mut self.fonts, text, 1.0, false);
47 builder.push_default(StyleProperty::FontFamily(FontFamily::from(
48 GenericFamily::SystemUi,
49 )));
35 let system = FontFamilyName::Generic(GenericFamily::SystemUi);
36 builder.push_default(StyleProperty::FontFamily(match &self.family {
37 Some(family) => {
38 FontFamily::List(Cow::Owned(vec![FontFamilyName::named(family), system]))
39 }
40 None => FontFamily::Single(system),
41 }));
5042 builder.push_default(StyleProperty::FontSize(size));
5143 let mut layout = builder.build(text);
5244 layout.break_all_lines(None);
......@@ -56,23 +48,31 @@ impl Texts {
5648 label
5749 }
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
5965 pub fn prune(&mut self, frame: u64) {
6066 self.cache.retain(|_, (_, touched)| *touched + 1 >= frame);
6167 }
6268}
6369
64/// A label painted in one colour this frame.
65pub(crate) struct Painted {
66 pub label: Rc<Label>,
67 pub color: [f32; 4],
68}
69
70impl Glyphs for Painted {
70impl Glyphs for Label {
7171 fn runs(
7272 &self,
7373 paint: &mut dyn FnMut(GlyphRun<'_>) -> Result<(), RenderError>,
7474 ) -> Result<(), RenderError> {
75 for line in self.label.layout.lines() {
75 for line in self.layout.lines() {
7676 let metrics = line.metrics();
7777 for item in line.items() {
7878 if let PositionedLayoutItem::GlyphRun(run) = item {
......@@ -81,7 +81,7 @@ impl Glyphs for Painted {
8181 metrics.baseline,
8282 0.0,
8383 [metrics.block_min_coord, metrics.line_height],
84 |_| self.color,
84 |_| None,
8585 paint,
8686 )?;
8787 }
crates/ui/src/theme.rs+137-9
......@@ -4,21 +4,47 @@ use draw::srgb;
44#[derive(Clone, Debug)]
55pub struct Theme {
66 pub font_size: f32,
7 /// Content surfaces, such as the active tab and lists.
7 /// Content surfaces, such as fields and lists.
88 pub base: [f32; 4],
99 /// Side panels.
1010 pub panel: [f32; 4],
11 /// The window's top strip and inactive tabs.
11 /// The window's title bar, toolbar and tab row.
1212 pub strip: [f32; 4],
13 /// Hairlines between rows and panels.
14 pub separator: [f32; 4],
1513 pub accent: [f32; 4],
16 /// A hovered row under the pointer, outlined in the accent.
17 pub hover: [f32; 4],
1814 pub text: [f32; 4],
1915 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.
2123 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],
2248}
2349
2450impl Theme {
......@@ -29,12 +55,114 @@ impl Theme {
2955 base: srgb(0x19, 0x1b, 0x1c),
3056 panel: srgb(0x1f, 0x22, 0x23),
3157 strip: srgb(0x27, 0x2a, 0x2b),
32 separator: srgb(0x20, 0x23, 0x24),
3358 accent: srgb(0x00, 0x79, 0xa6),
34 hover: srgb(0x10, 0x3c, 0x4c),
3559 text: srgb(0xdd, 0xde, 0xe0),
3660 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),
3764 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
38127 }
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));
39167 }
40168}
crates/ui/src/widgets.rs+147-106
......@@ -1,8 +1,13 @@
11use 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};
27use std::hash::Hash;
38use winit::{
49 event::Ime,
5 keyboard::{Key, NamedKey},
10 keyboard::{Key, ModifiersState, NamedKey},
611 window::CursorIcon,
712};
813
......@@ -14,7 +19,7 @@ pub fn button(ui: &mut Ui, part: impl Hash, text: &str) -> Signal {
1419 size: [fit(), px(theme.font_size * 2.0)],
1520 text: Some(text),
1621 fill: Some(theme.chip),
17 hover_fill: Some(theme.hover),
22 hover_fill: Some(theme.hover()),
1823 hover_border: Some(theme.accent),
1924 radius: 4.0,
2025 pad: [theme.font_size * 0.75, 0.0],
......@@ -82,9 +87,9 @@ pub fn scrollbar(
8287 chosen
8388}
8489
85/// A single-line field editing `text`, showing `placeholder` while empty. Typing,
86/// Backspace/Delete, Left/Right with Shift to select, Home/End, Command-A and input
87/// method commits edit it; a press places the caret.
90/// A single-line field editing `text`, showing `placeholder` while empty. Presses, drags
91/// and keys select and edit as on the page: a double press selects a word, a triple press
92/// everything, and dragging extends by that unit.
8893pub fn text_field(
8994 ui: &mut Ui,
9095 id: Id,
......@@ -95,94 +100,114 @@ pub fn text_field(
95100 let signal = ui.signal(id);
96101 let pad = spec.pad[0];
97102 let size = ui.theme.font_size;
98 let clicked_at = signal
99 .pressed
100 .then(|| ui.pointer().zip(ui.rect(id)))
101 .flatten()
102 .map(|(pointer, rect)| pointer[0] - rect[0] - pad);
103 let command = ui.modifiers().super_key() || ui.modifiers().control_key();
104 let shift = ui.modifiers().shift_key();
105 let (mut caret, mut mark) = {
106 let (caret, mark) = ui.caret(id);
107 (*caret, *mark)
103 let modifiers = edit_modifiers(ui.modifiers());
104 let pointer = ui
105 .pointer()
106 .zip(ui.rect(id))
107 .map(|(pointer, rect)| pointer[0] - rect[0] - pad)
108 .filter(|_| signal.pressed || signal.dragging);
109 let (mut selection, mut press) = {
110 let (selection, press) = ui.field(id);
111 (*selection, *press)
108112 };
109 caret = floor_boundary(text, caret);
110 mark = floor_boundary(text, mark);
111 if let Some(x) = clicked_at {
112 let (texts, frame) = ui.texts();
113 caret = texts.label(text, size, frame).index_at(x);
114 mark = caret;
113 let (texts, frame) = ui.texts();
114 let mut label = texts.label(text, size, frame);
115 if let Some(x) = pointer {
116 let layout = &label.layout;
117 let unit = if signal.pressed { signal.unit } else { press.1 };
118 let hit = Cursor::from_point(layout, x, label.size[1] / 2.0);
119 let target = edit::selection_at(layout, hit, x, unit);
120 selection = if signal.pressed {
121 let pressed = if modifiers.shift {
122 Selection::new(selection.anchor(), target.focus())
123 } else {
124 target
125 };
126 press = (pressed, unit);
127 pressed
128 } else {
129 let [anchor, focus] = edit::drag(
130 [press.0.anchor(), press.0.focus()],
131 [target.anchor(), target.focus()],
132 unit,
133 |cursor| cursor.index(),
134 );
135 Selection::new(anchor, focus)
136 };
115137 }
138 selection = selection.refresh(&label.layout);
116139 for event in &signal.events {
117 let selection = caret.min(mark)..caret.max(mark);
118 let insert = |text: &mut String, inserted: &str, caret: &mut usize| {
119 text.replace_range(selection.clone(), inserted);
120 *caret = selection.start + inserted.len();
121 };
122 match event {
123 Event::Ime(Ime::Commit(committed)) => {
124 insert(text, committed, &mut caret);
125 mark = caret;
126 }
127 Event::Key { key, text: typed } => match key {
128 Key::Named(NamedKey::Backspace | NamedKey::Delete) => {
129 if selection.is_empty() {
130 let edge = if matches!(key, Key::Named(NamedKey::Backspace)) {
131 previous(text, caret)..caret
132 } else {
133 caret..next(text, caret)
134 };
135 text.replace_range(edge.clone(), "");
136 caret = edge.start;
137 } else {
138 insert(text, "", &mut caret);
140 let layout = &label.layout;
141 let (range, inserted) = match event {
142 Event::Ime(Ime::Commit(committed)) => (selection.text_range(), committed.as_str()),
143 Event::Key { key, text: typed } => {
144 let key = edit_key(key);
145 // One line has no break for Control-K to join, nor room for Home and End to scroll.
146 let command = match (Command::from_key(&key, modifiers), &key) {
147 (Some(Command::Kill), _) => Some(Command::DeleteTo(Movement::LineEnd)),
148 (None, edit::Key::Named(edit::NamedKey::Home)) => {
149 Some(Command::Move(Movement::DocumentStart))
139150 }
140 mark = caret;
141 }
142 Key::Named(NamedKey::ArrowLeft | NamedKey::ArrowRight) => {
143 let left = matches!(key, Key::Named(NamedKey::ArrowLeft));
144 caret = match (shift, selection.is_empty(), left) {
145 (false, false, true) => selection.start,
146 (false, false, false) => selection.end,
147 (_, _, true) => previous(text, caret),
148 (_, _, false) => next(text, caret),
149 };
150 if !shift {
151 mark = caret;
151 (None, edit::Key::Named(edit::NamedKey::End)) => {
152 Some(Command::Move(Movement::DocumentEnd))
152153 }
153 }
154 Key::Named(NamedKey::Home | NamedKey::End) => {
155 caret = if matches!(key, Key::Named(NamedKey::Home)) {
156 0
157 } else {
158 text.len()
159 };
160 if !shift {
161 mark = caret;
154 (command, _) => command,
155 };
156 let collapsed = selection.is_collapsed();
157 match (command, key) {
158 (Some(Command::Move(movement)), _) => {
159 selection = edit::step(layout, selection, movement, modifiers.shift);
160 continue;
162161 }
163 }
164 Key::Character(character) if command => {
165 if character.eq_ignore_ascii_case("a") {
166 mark = 0;
167 caret = text.len();
162 (Some(Command::DeleteTo(movement)), _) if collapsed => (
163 edit::step(layout, selection, movement, true).text_range(),
164 "",
165 ),
166 (Some(Command::Delete { backward }), _) if collapsed => {
167 let caret = selection.focus();
168 let cluster = caret.logical_clusters(layout)[usize::from(!backward)];
169 (
170 cluster.map_or(caret.index()..caret.index(), |cluster| {
171 cluster.text_range()
172 }),
173 "",
174 )
168175 }
169 }
170 _ => {
171 if let Some(typed) = typed.as_deref().filter(|typed| {
172 !command && !typed.is_empty() && !typed.chars().any(char::is_control)
173 }) {
174 insert(text, typed, &mut caret);
175 mark = caret;
176 (Some(_), _) => (selection.text_range(), ""),
177 (None, edit::Key::Character(character))
178 if modifiers.command && character.eq_ignore_ascii_case("a") =>
179 {
180 selection = Selection::new(
181 Cursor::from_byte_index(layout, 0, Affinity::Downstream),
182 Cursor::from_byte_index(layout, usize::MAX, Affinity::Upstream),
183 );
184 continue;
176185 }
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 },
177195 }
178 },
179 _ => {}
180 }
196 }
197 _ => continue,
198 };
199 text.replace_range(range.clone(), inserted);
200 label = texts.label(text, size, frame);
201 selection = Selection::from_byte_index(
202 &label.layout,
203 range.start + inserted.len(),
204 Affinity::Downstream,
205 );
181206 }
182207 {
183 let (stored_caret, stored_mark) = ui.caret(id);
184 *stored_caret = caret;
185 *stored_mark = mark;
208 let (stored, stored_press) = ui.field(id);
209 *stored = selection;
210 *stored_press = press;
186211 }
187212 let theme = ui.theme.clone();
188213 let shown = if text.is_empty() {
......@@ -206,43 +231,59 @@ pub fn text_field(
206231 },
207232 );
208233 if signal.focused {
209 let (texts, frame) = ui.texts();
210 let label = texts.label(text, size, frame);
211234 let line = label.size[1];
212 let [start, end] =
213 [caret.min(mark), caret.max(mark)].map(|index| pad + label.caret_x(index));
214235 let top = match height.size {
215236 crate::Size::Pixels(height) => (height - line) / 2.0,
216237 _ => 0.0,
217238 };
218 if end > start {
219 ui.mark([start, top, end, top + line], theme.hover);
239 for (rect, _) in selection.geometry(&label.layout) {
240 ui.mark(
241 [pad + rect.x0 as f32, top, pad + rect.x1 as f32, top + line],
242 theme.hover(),
243 );
220244 }
221 let x = pad + label.caret_x(caret);
245 let x = pad + selection.focus().geometry(&label.layout, 1.0).x0 as f32;
222246 ui.mark([x, top, x + 1.0, top + line], theme.text);
223247 }
224248 ui.close();
225249 signal
226250}
227251
228fn floor_boundary(text: &str, index: usize) -> usize {
229 let mut index = index.min(text.len());
230 while !text.is_char_boundary(index) {
231 index -= 1;
252/// A winit key in the editing vocabulary text fields and the page share.
253pub fn edit_key(key: &Key) -> edit::Key {
254 use edit::NamedKey as Edit;
255 match key {
256 Key::Character(text) => edit::Key::Character(text.to_string()),
257 Key::Named(named) => edit::Key::Named(match named {
258 NamedKey::Escape => Edit::Escape,
259 NamedKey::Tab => Edit::Tab,
260 NamedKey::Space => Edit::Space,
261 NamedKey::Enter => Edit::Enter,
262 NamedKey::Backspace => Edit::Backspace,
263 NamedKey::Delete => Edit::Delete,
264 NamedKey::ArrowLeft => Edit::ArrowLeft,
265 NamedKey::ArrowRight => Edit::ArrowRight,
266 NamedKey::ArrowUp => Edit::ArrowUp,
267 NamedKey::ArrowDown => Edit::ArrowDown,
268 NamedKey::Home => Edit::Home,
269 NamedKey::End => Edit::End,
270 NamedKey::Alt
271 | NamedKey::AltGraph
272 | NamedKey::Control
273 | NamedKey::Shift
274 | NamedKey::Super
275 | NamedKey::Meta => Edit::Modifier,
276 _ => Edit::Other,
277 }),
278 _ => edit::Key::Named(Edit::Other),
232279 }
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)
241280}
242281
243fn next(text: &str, index: usize) -> usize {
244 text[index..]
245 .chars()
246 .next()
247 .map_or(index, |character| index + character.len_utf8())
282pub fn edit_modifiers(modifiers: ModifiersState) -> edit::Modifiers {
283 edit::Modifiers {
284 shift: modifiers.shift_key(),
285 control: modifiers.control_key(),
286 option: modifiers.alt_key(),
287 command: modifiers.super_key(),
288 }
248289}
fuzz/Cargo.lock+10
......@@ -81,6 +81,7 @@ dependencies = [
8181name = "canvas"
8282version = "0.1.0"
8383dependencies = [
84 "draw",
8485 "onestore",
8586 "parley",
8687 "serde",
......@@ -195,6 +196,13 @@ dependencies = [
195196 "libloading",
196197]
197198
199[[package]]
200name = "draw"
201version = "0.1.0"
202dependencies = [
203 "parley",
204]
205
198206[[package]]
199207name = "errno"
200208version = "0.3.14"
......@@ -646,6 +654,7 @@ version = "0.1.0"
646654dependencies = [
647655 "aes",
648656 "base64",
657 "bumpalo",
649658 "cbc",
650659 "getrandom",
651660 "md5",
......@@ -663,6 +672,7 @@ name = "onestore-fuzz"
663672version = "0.0.0"
664673dependencies = [
665674 "canvas",
675 "draw",
666676 "libfuzzer-sys",
667677 "md5",
668678 "notebook",
fuzz/Cargo.toml+1
......@@ -12,6 +12,7 @@ libfuzzer-sys = "0.4"
1212onestore = { path = "../crates/onestore", features = ["protected"] }
1313notebook = { path = "../crates/notebook" }
1414canvas = { path = "../crates/canvas" }
15draw = { path = "../crates/draw", default-features = false }
1516tempfile = "3"
1617md5 = "0.8.1"
1718serde_json = "1"
fuzz/fuzz_targets/canvas_editor.rs+2-1
......@@ -1,5 +1,6 @@
11#![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};
34use onestore::page::text::{Paragraph};
45use libfuzzer_sys::fuzz_target;
56use onestore::document::Format;
tools/canvas/README.md+8-6
......@@ -34,23 +34,25 @@ canvas text edits · inverse history · composition · paragraph layout ·
3434canvas::gpu page scene · text runs, tag icons, pictures and ink as draw primitives
3535 ↓
3636draw Swash glyph rasterization · custom wgpu quads · bounded glyph/image caches · layers
37 · draw::edit: key chords, click counts and caret steps within one layout
3738 ↓
3839canvas::interaction hit layers · drags · placement grid · picture handles · chrome · key routing
3940 · scroll bounds and zoom · caret blink · AccessKit tree
4041 ↓
4142ui immediate-mode boxes · keyed state · autolayout · event routing · labels · widgets
43 · shell: section tabs, toolbar buttons and drop-downs
4244 ↓
43snowbound section tabs · page list · the page as a custom box · Winit window and events
45snowbound title bar · toolbar · section frame · page tabs · the page as a custom box · Winit window and events
4446 · AppKit pickers and quit guard · clipboard · saving
4547```
4648
4749## 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
5557```sh
5658cp -RL SOURCE_NOTEBOOK /tmp/notebook-copy && chmod u+w /tmp/notebook-copy/*.one
......@@ -60,7 +62,7 @@ cargo test -p ui
6062
6163The 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
6567Glyph 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
242244
243245## 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
247249```sh
248250cargo test --release -p canvas --features interaction canvas_pipeline_cost -- --ignored --nocapture --test-threads=1