diff --git a/.npmignore b/.npmignore index b678632..a3d91eb 100644 --- a/.npmignore +++ b/.npmignore @@ -2,6 +2,7 @@ CNAME assets demos dev +fonts home lang learn diff --git a/README.md b/README.md index e7be442..93f29d9 100644 --- a/README.md +++ b/README.md @@ -339,13 +339,13 @@ Unminified: - p5.js **5112kb** ⚠️ - p5.sound.js 488kb -- q5.js 93kb +- q5.js 110kb Minified: - p5.min.js 1034kb ⚠️ - p5.sound.min.js 200kb -- q5.min.js **61kb** 🎉 +- q5.min.js **70kb** 🎉 ## Benchmarks @@ -404,6 +404,9 @@ p5.js is licensed under the LGPLv2, the two small sections of p5' code directly ## Credits +q5-webgpu msdf text rendering: +https://webgpu.github.io/webgpu-samples/?sample=textRenderingMsdf + q5-webgpu blendMode: https://webgpufundamentals.org/webgpu/lessons/webgpu-transparency.html diff --git a/fonts/YaHei-256-msdf.json b/fonts/YaHei-256-msdf.json new file mode 100644 index 0000000..52de609 --- /dev/null +++ b/fonts/YaHei-256-msdf.json @@ -0,0 +1 @@ +{"pages":["YaHei-256.png"],"chars":"id index char width height xoffset yoffset xadvance chnl x y page\n106 80 \"j\" 16 48 -6 3 11 15 0 0 0\n87 61 \"W\" 46 36 -1 4 43 15 0 49 0\n81 55 \"Q\" 35 45 0 4 34 15 17 0 0\n64 38 \"@\" 40 40 2 4 43 15 0 86 0\n98 72 \"b\" 25 38 2 2 27 15 0 127 0\n100 74 \"d\" 25 38 0 2 27 15 0 166 0\n102 76 \"f\" 18 38 -1 2 15 15 0 205 0\n103 77 \"g\" 25 38 0 13 27 15 19 205 0\n104 78 \"h\" 23 38 2 2 26 15 26 127 0\n107 81 \"k\" 23 38 2 2 23 15 26 166 0\n108 82 \"l\" 8 38 2 2 11 15 41 86 0\n112 86 \"p\" 25 38 2 13 27 15 47 46 0\n113 87 \"q\" 25 38 0 13 27 15 53 0 0\n48 22 \"0\" 25 37 0 4 25 15 45 205 0\n51 25 \"3\" 23 37 1 4 25 15 50 85 0\n54 28 \"6\" 24 37 0 4 25 15 73 39 0\n56 30 \"8\" 24 37 0 4 25 15 79 0 0\n57 31 \"9\" 24 37 0 4 25 15 50 123 0\n67 41 \"C\" 28 37 0 4 28 15 50 161 0\n71 45 \"G\" 30 37 0 4 31 15 74 77 0\n77 51 \"M\" 37 36 2 4 41 15 98 38 0\n79 53 \"O\" 34 37 0 4 34 15 104 0 0\n83 57 \"S\" 24 37 1 4 24 15 75 115 0\n105 79 \"i\" 9 37 1 3 11 15 71 199 0\n109 83 \"m\" 37 27 2 13 39 15 79 153 0\n121 95 \"y\" 26 37 -2 13 22 15 100 115 0\n33 7 \"!\" 9 36 2 4 13 15 105 75 0\n49 23 \"1\" 22 36 2 4 25 15 115 75 0\n50 24 \"2\" 24 36 0 4 25 15 136 38 0\n52 26 \"4\" 27 36 -2 4 25 15 139 0 0\n53 27 \"5\" 22 36 2 4 25 15 81 181 0\n55 29 \"7\" 25 36 0 4 25 15 81 218 0\n65 39 \"A\" 33 36 -2 4 30 15 104 181 0\n66 40 \"B\" 24 36 2 4 26 15 107 218 0\n68 42 \"D\" 30 36 2 4 32 15 132 218 0\n69 43 \"E\" 21 36 2 4 23 15 127 112 0\n70 44 \"F\" 20 36 2 4 22 15 138 75 0\n72 46 \"H\" 28 36 2 4 32 15 138 149 0\n73 47 \"I\" 14 36 -1 4 12 15 149 112 0\n74 48 \"J\" 16 36 -2 4 17 15 159 75 0\n75 49 \"K\" 27 36 2 4 27 15 164 112 0\n76 50 \"L\" 21 36 2 4 22 15 161 37 0\n78 52 \"N\" 30 36 2 4 34 15 167 0 0\n80 54 \"P\" 24 36 2 4 26 15 176 74 0\n82 56 \"R\" 27 36 2 4 27 15 183 37 0\n84 58 \"T\" 26 36 -1 4 24 15 198 0 0\n85 59 \"U\" 28 36 2 4 31 15 225 0 0\n86 60 \"V\" 32 36 -2 4 28 15 211 37 0\n88 62 \"X\" 30 36 -1 4 27 15 201 74 0\n89 63 \"Y\" 29 36 -2 4 25 15 163 186 0\n90 64 \"Z\" 28 36 -1 4 26 15 167 149 0\n119 93 \"w\" 36 27 -1 13 33 15 163 223 0\n116 90 \"t\" 18 34 -1 7 16 15 193 186 0\n58 32 \":\" 9 28 0 13 10 15 127 149 0\n97 71 \"a\" 22 28 0 13 23 15 232 74 0\n99 73 \"c\" 21 28 0 13 21 15 138 186 0\n101 75 \"e\" 24 28 0 13 24 15 232 103 0\n111 85 \"o\" 27 28 0 13 27 15 200 221 0\n115 89 \"s\" 19 28 0 13 19 15 212 111 0\n110 84 \"n\" 23 27 2 13 26 15 232 132 0\n114 88 \"r\" 16 27 2 13 16 15 192 111 0\n117 91 \"u\" 23 27 1 13 26 15 228 160 0\n118 92 \"v\" 26 27 -2 13 22 15 228 188 0\n120 94 \"x\" 24 27 -1 13 21 15 228 216 0\n122 96 \"z\" 23 27 -1 13 21 15 196 140 0\n45 19 \"-\" 16 7 1 22 18 15 0 244 0\n44 18 \",\" 10 15 -1 31 10 15 220 140 0\n39 13 \"'\" 8 14 1 4 11 15 71 237 0\n34 8 \"\\\"\" 14 14 2 4 18 15 212 168 0\n46 20 \".\" 9 9 0 31 10 15 117 153 0","info":{"face":"YaHei-256","size":42},"common":{"lineHeight":45,"base":36,"scaleW":256,"scaleH":256},"kernings":"first second amount\n39 114 -1\n39 115 -1\n34 114 -1\n34 115 -1\n65 44 1\n65 67 -1\n65 71 -1\n65 74 2\n65 79 -1\n65 84 -3\n65 85 -1\n65 86 -3\n65 87 -2\n65 89 -3\n65 90 1\n65 116 -1\n65 118 -1\n65 119 -1\n65 121 -1\n66 84 -2\n66 89 -1\n67 67 -1\n67 71 -1\n67 79 -1\n67 81 -1\n68 44 -3\n68 46 -3\n68 65 -1\n68 84 -2\n68 88 -1\n68 90 -1\n69 65 0\n69 74 1\n69 84 0\n69 87 1\n69 88 0\n70 44 -3\n70 46 -3\n70 65 -3\n70 74 -1\n70 83 -1\n70 84 0\n70 97 -2\n70 102 0\n71 84 -1\n71 86 -1\n71 121 -1\n74 44 -2\n74 46 -2\n74 65 -1\n74 74 -1\n74 97 -1\n75 44 1\n75 67 -2\n75 71 -2\n75 74 2\n75 79 -2\n75 81 -2\n75 88 1\n75 90 1\n75 99 -1\n75 100 -1\n75 101 -1\n75 103 -1\n75 111 -1\n75 113 -1\n75 116 -1\n75 118 -2\n75 119 -1\n75 121 -2\n76 65 1\n76 67 -1\n76 71 -1\n76 74 2\n76 79 -2\n76 81 -2\n76 84 -3\n76 85 -1\n76 86 -3\n76 87 -1\n76 89 -3\n76 90 1\n76 116 -1\n76 118 -2\n76 119 -1\n76 121 -2\n79 44 -2\n79 46 -2\n79 65 -1\n79 74 0\n79 84 -2\n79 88 -1\n79 89 -1\n79 90 -1\n80 44 -7\n80 46 -7\n80 65 -4\n80 71 0\n80 74 -3\n80 87 1\n80 88 -1\n80 97 -1\n80 99 -2\n80 100 -2\n80 101 -2\n80 103 -2\n80 111 -2\n80 113 -2\n81 44 -2\n81 46 -3\n81 65 -1\n81 84 -2\n81 88 -1\n81 89 0\n81 90 -1\n82 67 -1\n82 71 -1\n82 74 1\n82 79 0\n82 81 0\n82 84 -1\n82 89 -1\n82 99 -1\n82 100 -1\n82 101 -1\n82 103 -1\n82 111 -1\n82 113 -1\n83 116 -1\n83 118 -1\n83 119 -1\n83 121 -1\n84 44 -3\n84 46 -4\n84 58 -1\n84 65 -3\n84 67 -2\n84 71 -2\n84 74 -3\n84 79 -2\n84 81 -2\n84 84 1\n84 86 1\n84 87 1\n84 88 0\n84 89 1\n84 97 -5\n84 99 -5\n84 100 -5\n84 101 -5\n84 102 -2\n84 103 -5\n84 109 -4\n84 110 -4\n84 111 -5\n84 112 -4\n84 113 -5\n84 114 -4\n84 115 -3\n84 117 -4\n84 118 -2\n84 119 -3\n84 120 -4\n84 121 -3\n84 122 -3\n85 65 -1\n86 44 -5\n86 46 -5\n86 65 -3\n86 67 -1\n86 71 -1\n86 74 -2\n86 79 0\n86 81 -1\n86 83 -1\n86 84 1\n86 97 -3\n86 99 -3\n86 100 -3\n86 101 -3\n86 103 -3\n86 109 -2\n86 110 -2\n86 111 -3\n86 112 -2\n86 113 -3\n86 114 -2\n86 115 -1\n86 117 -2\n87 44 -3\n87 46 -3\n87 65 -2\n87 84 1\n87 97 -2\n87 99 -1\n87 100 -1\n87 101 -1\n87 103 -1\n87 111 -1\n87 113 -1\n88 44 1\n88 46 1\n88 67 -1\n88 71 -1\n88 74 2\n88 79 -1\n88 81 -1\n88 84 1\n89 44 -4\n89 46 -4\n89 65 -4\n89 67 -1\n89 71 -1\n89 74 -1\n89 79 -1\n89 81 -1\n89 83 -1\n89 84 1\n89 97 -4\n89 99 -4\n89 100 -4\n89 101 -4\n89 102 -1\n89 103 -4\n89 109 -3\n89 110 -3\n89 111 -4\n89 112 -3\n89 113 -4\n89 114 -3\n89 115 -3\n89 117 -3\n90 74 2\n90 84 1\n90 121 -1\n98 97 -1\n98 102 0\n98 120 -1\n99 74 2\n99 84 -2\n99 89 -2\n101 39 -2\n101 34 -2\n102 44 -3\n102 45 -2\n102 46 -3\n102 58 2\n102 98 0\n102 104 0\n102 116 1\n102 118 1\n102 119 1\n102 120 0\n102 121 1\n103 106 1\n106 106 1\n107 44 2\n107 45 -3\n107 46 2\n107 58 2\n107 99 -1\n107 100 -1\n107 101 -1\n107 103 -1\n107 111 -1\n107 113 -1\n107 116 0\n110 39 -2\n110 34 -2\n111 39 -3\n111 34 -3\n111 97 -1\n111 102 -1\n111 120 -1\n112 97 -1\n112 102 -1\n112 120 -1\n113 106 2\n114 44 -4\n114 45 -3\n114 46 -4\n114 58 2\n114 99 -1\n114 100 -1\n114 101 -1\n114 102 1\n114 103 -1\n114 109 0\n114 110 0\n114 111 -1\n114 113 -1\n114 115 0\n114 116 1\n114 118 2\n114 119 2\n114 120 1\n114 121 2\n114 122 1\n116 45 -3\n116 99 -1\n116 100 -1\n116 101 0\n116 103 0\n116 111 0\n116 113 0\n116 120 1\n117 39 -1\n117 34 -1\n118 44 -3\n118 46 -3\n118 97 -1\n118 99 0\n118 100 0\n118 101 0\n118 103 0\n118 111 0\n118 113 0\n119 44 -2\n119 46 -2\n119 99 0\n119 100 0\n119 101 0\n119 103 0\n119 111 0\n119 113 0\n120 99 0\n120 100 0\n120 101 0\n120 103 0\n120 111 0\n120 113 0\n121 39 1\n121 34 1\n121 44 -2\n121 46 -3\n121 99 0\n121 100 0\n121 101 0\n121 102 0\n121 103 0\n121 111 0\n121 113 0\n121 116 0"} \ No newline at end of file diff --git a/fonts/YaHei-256.png b/fonts/YaHei-256.png new file mode 100644 index 0000000..e721d32 Binary files /dev/null and b/fonts/YaHei-256.png differ diff --git a/fonts/YaHei-msdf.json b/fonts/YaHei-msdf.json new file mode 100644 index 0000000..1ad4da9 --- /dev/null +++ b/fonts/YaHei-msdf.json @@ -0,0 +1 @@ +{"pages":["YaHei.png"],"chars":"id index char width height xoffset yoffset xadvance chnl x y page\n124 98 \"|\" 8 49 2 1 11 15 0 0 0\n253 190 \"ý\" 26 49 -2 2 22 15 0 50 0\n106 80 \"j\" 16 48 -6 3 11 15 9 0 0\n254 191 \"þ\" 25 48 2 2 27 15 26 0 0\n87 61 \"W\" 46 36 -1 4 43 15 0 100 0\n192 129 \"À\" 33 46 -2 -6 30 15 27 49 0\n193 130 \"Á\" 33 46 -2 -6 30 15 52 0 0\n194 131 \"Â\" 33 46 -2 -6 30 15 0 137 0\n199 136 \"Ç\" 28 46 0 4 28 15 0 184 0\n200 137 \"È\" 21 46 2 -6 23 15 0 231 0\n201 138 \"É\" 21 46 2 -6 23 15 0 278 0\n202 139 \"Ê\" 21 46 2 -6 23 15 0 325 0\n204 141 \"Ì\" 15 46 -1 -6 12 15 0 372 0\n205 142 \"Í\" 15 46 -1 -6 12 15 0 419 0\n206 143 \"Î\" 17 46 -2 -6 12 15 0 466 0\n210 147 \"Ò\" 34 46 0 -6 34 15 16 372 0\n211 148 \"Ó\" 34 46 0 -6 34 15 16 419 0\n212 149 \"Ô\" 34 46 0 -6 34 15 18 466 0\n217 154 \"Ù\" 28 46 2 -6 31 15 22 231 0\n218 155 \"Ú\" 28 46 2 -6 31 15 29 184 0\n219 156 \"Û\" 28 46 2 -6 31 15 34 137 0\n221 158 \"Ý\" 29 46 -2 -6 25 15 61 47 0\n255 192 \"ÿ\" 26 46 -2 5 22 15 86 0 0\n81 55 \"Q\" 35 45 0 4 34 15 22 278 0\n213 150 \"Õ\" 34 45 0 -4 34 15 51 231 0\n36 10 \"$\" 22 44 1 0 25 15 58 184 0\n195 132 \"Ã\" 33 44 -2 -4 30 15 22 324 0\n209 146 \"Ñ\" 30 44 2 -4 34 15 63 94 0\n214 151 \"Ö\" 34 44 0 -3 34 15 63 139 0\n220 157 \"Ü\" 28 44 2 -3 31 15 91 47 0\n40 14 \"(\" 14 43 1 4 14 15 81 184 0\n41 15 \")\" 15 43 -2 4 14 15 113 0 0\n91 65 \"[\" 12 43 2 4 14 15 94 92 0\n93 67 \"]\" 12 43 -1 4 14 15 51 369 0\n123 97 \"{\" 15 43 0 4 14 15 56 324 0\n125 99 \"}\" 15 43 -1 4 14 15 58 277 0\n196 133 \"Ä\" 33 43 -2 -3 30 15 51 413 0\n197 134 \"Å\" 33 43 -2 -3 30 15 64 368 0\n203 140 \"Ë\" 21 43 2 -3 23 15 72 321 0\n207 144 \"Ï\" 17 43 -2 -3 12 15 74 277 0\n47 21 \"/\" 23 41 -3 4 18 15 86 228 0\n92 66 \"\\\\\" 23 41 -3 4 17 15 96 184 0\n198 135 \"Æ\" 41 36 -2 4 39 15 98 136 0\n64 38 \"@\" 40 40 2 4 43 15 107 92 0\n223 160 \"ß\" 24 39 2 2 25 15 120 44 0\n224 161 \"à\" 22 39 0 2 23 15 129 0 0\n225 162 \"á\" 22 39 0 2 23 15 92 270 0\n226 163 \"â\" 22 39 0 2 23 15 110 226 0\n229 166 \"å\" 22 39 0 1 23 15 145 40 0\n232 169 \"è\" 24 39 0 2 24 15 152 0 0\n233 170 \"é\" 24 39 0 2 24 15 120 173 0\n234 171 \"ê\" 24 39 0 2 24 15 140 133 0\n242 179 \"ò\" 27 39 0 2 27 15 148 80 0\n243 180 \"ó\" 27 39 0 2 27 15 168 40 0\n244 181 \"ô\" 27 39 0 2 27 15 177 0 0\n249 186 \"ù\" 23 39 1 2 26 15 53 457 0\n250 187 \"ú\" 23 39 1 2 26 15 77 457 0\n251 188 \"û\" 23 39 1 2 26 15 85 412 0\n37 11 \"%\" 38 37 0 4 37 15 94 310 0\n98 72 \"b\" 25 38 2 2 27 15 115 266 0\n100 74 \"d\" 25 38 0 2 27 15 133 213 0\n102 76 \"f\" 18 38 -1 2 15 15 145 173 0\n103 77 \"g\" 25 38 0 13 27 15 101 452 0\n104 78 \"h\" 23 38 2 2 26 15 98 348 0\n107 81 \"k\" 23 38 2 2 23 15 109 387 0\n108 82 \"l\" 8 38 2 2 11 15 122 348 0\n112 86 \"p\" 25 38 2 13 27 15 131 348 0\n113 87 \"q\" 25 38 0 13 27 15 133 305 0\n216 153 \"Ø\" 34 38 0 3 34 15 141 252 0\n230 167 \"æ\" 38 28 0 13 38 15 159 212 0\n236 173 \"ì\" 13 38 -2 2 11 15 164 173 0\n237 174 \"í\" 13 38 0 2 11 15 47 96 0\n238 175 \"î\" 17 38 -3 2 11 15 165 120 0\n240 177 \"ð\" 25 38 0 2 26 15 176 80 0\n38 12 \"&\" 37 37 1 4 37 15 196 40 0\n48 22 \"0\" 25 37 0 4 25 15 205 0 0\n51 25 \"3\" 23 37 1 4 25 15 231 0 0\n54 28 \"6\" 24 37 0 4 25 15 178 159 0\n56 30 \"8\" 24 37 0 4 25 15 183 119 0\n57 31 \"9\" 24 37 0 4 25 15 202 78 0\n63 37 \"?\" 19 37 1 4 20 15 127 426 0\n67 41 \"C\" 28 37 0 4 28 15 133 387 0\n71 45 \"G\" 30 37 0 4 31 15 157 344 0\n77 51 \"M\" 37 36 2 4 41 15 127 464 0\n79 53 \"O\" 34 37 0 4 34 15 147 425 0\n83 57 \"S\" 24 37 1 4 24 15 162 382 0\n105 79 \"i\" 9 37 1 3 11 15 165 463 0\n109 83 \"m\" 37 27 2 13 39 15 175 463 0\n121 95 \"y\" 26 37 -2 13 22 15 182 420 0\n169 106 \"©\" 37 37 2 4 40 15 187 382 0\n174 111 \"®\" 37 37 2 4 40 15 209 420 0\n191 128 \"¿\" 19 37 1 13 20 15 159 291 0\n227 164 \"ã\" 22 37 0 4 23 15 176 241 0\n231 168 \"ç\" 21 37 0 13 21 15 198 197 0\n245 182 \"õ\" 27 37 0 4 27 15 203 157 0\n33 7 \"!\" 9 36 2 4 13 15 208 116 0\n49 23 \"1\" 22 36 2 4 25 15 218 116 0\n50 24 \"2\" 24 36 0 4 25 15 227 78 0\n52 26 \"4\" 27 36 -2 4 25 15 234 38 0\n53 27 \"5\" 22 36 2 4 25 15 255 0 0\n55 29 \"7\" 25 36 0 4 25 15 213 458 0\n65 39 \"A\" 33 36 -2 4 30 15 239 458 0\n66 40 \"B\" 24 36 2 4 26 15 179 279 0\n68 42 \"D\" 30 36 2 4 32 15 199 235 0\n69 43 \"E\" 21 36 2 4 23 15 220 195 0\n70 44 \"F\" 20 36 2 4 22 15 231 153 0\n72 46 \"H\" 28 36 2 4 32 15 241 115 0\n73 47 \"I\" 14 36 -1 4 12 15 252 75 0\n74 48 \"J\" 16 36 -2 4 17 15 262 37 0\n75 49 \"K\" 27 36 2 4 27 15 278 0 0\n76 50 \"L\" 21 36 2 4 22 15 267 74 0\n78 52 \"N\" 30 36 2 4 34 15 279 37 0\n80 54 \"P\" 24 36 2 4 26 15 306 0 0\n82 56 \"R\" 27 36 2 4 27 15 188 316 0\n84 58 \"T\" 26 36 -1 4 24 15 204 272 0\n85 59 \"U\" 28 36 2 4 31 15 230 232 0\n86 60 \"V\" 32 36 -2 4 28 15 242 190 0\n88 62 \"X\" 30 36 -1 4 27 15 252 152 0\n89 63 \"Y\" 29 36 -2 4 25 15 270 111 0\n90 64 \"Z\" 28 36 -1 4 26 15 289 74 0\n119 93 \"w\" 36 27 -1 13 33 15 188 353 0\n161 4 \"¡\" 9 36 2 13 13 15 310 37 0\n162 5 \"¢\" 21 36 2 5 25 15 216 309 0\n163 6 \"£\" 24 36 0 4 25 15 231 269 0\n165 102 \"¥\" 26 36 -1 4 25 15 225 346 0\n208 145 \"Ð\" 33 36 -1 4 32 15 225 383 0\n222 159 \"Þ\" 24 36 2 4 26 15 247 420 0\n228 165 \"ä\" 22 36 0 5 23 15 238 306 0\n235 172 \"ë\" 24 36 0 5 24 15 256 269 0\n241 178 \"ñ\" 23 36 2 4 26 15 252 343 0\n246 183 \"ö\" 27 36 0 5 27 15 261 306 0\n252 189 \"ü\" 23 36 1 5 26 15 259 380 0\n128 0 \"€\" 26 35 2 5 29 15 276 343 0\n239 176 \"ï\" 17 35 -3 5 11 15 272 417 0\n116 90 \"t\" 18 34 -1 7 16 15 283 379 0\n35 9 \"#\" 29 33 -1 4 27 15 259 227 0\n59 33 \";\" 11 33 -1 13 10 15 275 189 0\n248 185 \"ø\" 29 31 -1 11 27 15 283 148 0\n960 362 \"π\" 31 27 -2 13 29 15 300 111 0\n58 32 \":\" 9 28 0 13 10 15 318 74 0\n97 71 \"a\" 22 28 0 13 23 15 320 37 0\n99 73 \"c\" 21 28 0 13 21 15 331 0 0\n101 75 \"e\" 24 28 0 13 24 15 281 261 0\n111 85 \"o\" 27 28 0 13 27 15 328 66 0\n115 89 \"s\" 19 28 0 13 19 15 343 29 0\n110 84 \"n\" 23 27 2 13 26 15 353 0 0\n114 88 \"r\" 16 27 2 13 16 15 287 180 0\n117 91 \"u\" 23 27 1 13 26 15 289 290 0\n118 92 \"v\" 26 27 -2 13 22 15 289 208 0\n120 94 \"x\" 24 27 -1 13 21 15 304 180 0\n122 96 \"z\" 23 27 -1 13 21 15 313 139 0\n60 34 \"<\" 23 26 4 12 31 15 332 95 0\n62 36 \">\" 23 26 4 12 31 15 306 236 0\n126 100 \"~\" 26 11 3 19 31 15 53 501 0\n247 184 \"÷\" 25 26 3 12 31 15 306 263 0\n43 17 \"+\" 25 25 3 12 31 15 316 208 0\n61 35 \"=\" 25 17 3 17 31 15 101 491 0\n94 68 \"^\" 25 23 3 4 31 15 289 318 0\n179 116 \"³\" 16 24 1 4 17 15 289 236 0\n95 69 \"_\" 23 7 -2 40 19 15 120 84 0\n178 115 \"²\" 16 23 0 4 17 15 109 426 0\n185 122 \"¹\" 16 23 1 4 16 15 313 290 0\n215 152 \"×\" 23 23 4 13 31 15 330 234 0\n186 123 \"º\" 20 21 0 4 20 15 175 491 0\n42 16 \"*\" 20 20 0 4 19 15 329 167 0\n8220 532 \"“\" 17 14 25 4 42 15 178 197 0\n8221 533 \"”\" 17 14 0 4 42 15 159 329 0\n45 19 \"-\" 16 7 1 22 18 15 148 120 0\n176 113 \"°\" 16 16 0 4 17 15 332 122 0\n44 18 \",\" 10 15 -1 31 10 15 98 387 0\n34 8 \"\\\"\" 14 14 2 4 18 15 80 497 0\n39 13 \"'\" 8 14 1 4 11 15 179 316 0\n8216 528 \"‘\" 10 14 32 4 42 15 196 495 0\n8217 529 \"’\" 10 14 0 4 42 15 207 495 0\n96 70 \"`\" 13 11 0 2 12 15 127 501 0\n180 117 \"´\" 13 11 1 2 13 15 141 501 0\n46 20 \".\" 9 9 0 31 10 15 98 173 0\n183 120 \"·\" 9 9 0 20 10 15 159 241 0","info":{"face":"YaHei","size":42},"common":{"lineHeight":45,"base":36,"scaleW":512,"scaleH":512},"kernings":"first second amount\n34 114 -1\n34 115 -1\n39 114 -1\n39 115 -1\n40 106 5\n42 65 -4\n42 74 -3\n42 99 -2\n42 100 -2\n42 101 -2\n42 103 -2\n42 111 -2\n42 113 -2\n44 8216 -5\n44 8217 -5\n44 8220 -5\n44 8221 -5\n46 8216 -5\n46 8217 -4\n46 8220 -5\n46 8221 -4\n65 42 -3\n65 44 1\n65 59 1\n65 67 -1\n65 71 -1\n65 74 2\n65 79 -1\n65 84 -3\n65 85 -1\n65 86 -3\n65 87 -2\n65 89 -3\n65 90 1\n65 116 -1\n65 118 -1\n65 119 -1\n65 121 -1\n65 8216 -3\n65 8217 -4\n65 8220 -3\n65 8221 -4\n66 84 -2\n66 89 -1\n67 63 0\n67 67 -1\n67 71 -1\n67 79 -1\n67 81 -1\n68 44 -3\n68 46 -3\n68 65 -1\n68 84 -2\n68 88 -1\n68 90 -1\n69 65 0\n69 74 1\n69 84 0\n69 87 1\n69 88 0\n70 44 -3\n70 46 -3\n70 65 -3\n70 74 -1\n70 83 -1\n70 84 0\n70 97 -2\n70 102 0\n71 84 -1\n71 86 -1\n71 121 -1\n74 44 -2\n74 46 -2\n74 65 -1\n74 74 -1\n74 97 -1\n74 224 -1\n74 225 -1\n75 44 1\n75 59 1\n75 67 -2\n75 71 -2\n75 74 2\n75 79 -2\n75 81 -2\n75 88 1\n75 90 1\n75 99 -1\n75 100 -1\n75 101 -1\n75 103 -1\n75 111 -1\n75 113 -1\n75 116 -1\n75 118 -2\n75 119 -1\n75 121 -2\n75 232 -1\n75 233 -1\n75 234 -1\n75 237 -1\n75 242 -1\n75 243 -1\n76 42 -5\n76 63 -2\n76 65 1\n76 67 -1\n76 71 -1\n76 74 2\n76 79 -2\n76 81 -2\n76 84 -3\n76 85 -1\n76 86 -3\n76 87 -1\n76 89 -3\n76 90 1\n76 116 -1\n76 118 -2\n76 119 -1\n76 121 -2\n76 8216 -3\n76 8217 -3\n76 8220 -3\n76 8221 -3\n79 44 -2\n79 46 -2\n79 65 -1\n79 74 0\n79 84 -2\n79 88 -1\n79 89 -1\n79 90 -1\n80 44 -7\n80 46 -7\n80 65 -4\n80 71 0\n80 74 -3\n80 87 1\n80 88 -1\n80 97 -1\n80 99 -2\n80 100 -2\n80 101 -2\n80 103 -2\n80 111 -2\n80 113 -2\n80 224 -1\n80 225 -1\n80 232 -2\n80 233 -2\n80 234 -2\n80 242 -2\n80 243 -2\n81 44 -2\n81 46 -3\n81 65 -1\n81 84 -2\n81 88 -1\n81 89 0\n81 90 -1\n82 59 2\n82 67 -1\n82 71 -1\n82 74 1\n82 79 0\n82 81 0\n82 84 -1\n82 89 -1\n82 99 -1\n82 100 -1\n82 101 -1\n82 103 -1\n82 111 -1\n82 113 -1\n82 232 -1\n82 233 -1\n82 234 -1\n82 242 -1\n82 243 -1\n83 116 -1\n83 118 -1\n83 119 -1\n83 121 -1\n84 44 -3\n84 46 -4\n84 58 -1\n84 59 -1\n84 65 -3\n84 67 -2\n84 71 -2\n84 74 -3\n84 79 -2\n84 81 -2\n84 84 1\n84 86 1\n84 87 1\n84 88 0\n84 89 1\n84 97 -5\n84 99 -5\n84 100 -5\n84 101 -5\n84 102 -2\n84 103 -5\n84 109 -4\n84 110 -4\n84 111 -5\n84 112 -4\n84 113 -5\n84 114 -4\n84 115 -3\n84 117 -4\n84 118 -2\n84 119 -3\n84 120 -4\n84 121 -3\n84 122 -3\n84 224 -5\n84 225 -5\n84 232 -5\n84 233 -5\n84 234 -5\n84 237 -1\n84 242 -5\n84 243 -5\n84 249 -4\n84 250 -4\n84 252 -4\n84 8217 1\n84 8221 1\n85 65 -1\n86 44 -5\n86 46 -5\n86 65 -3\n86 67 -1\n86 71 -1\n86 74 -2\n86 79 0\n86 81 -1\n86 83 -1\n86 84 1\n86 97 -3\n86 99 -3\n86 100 -3\n86 101 -3\n86 103 -3\n86 109 -2\n86 110 -2\n86 111 -3\n86 112 -2\n86 113 -3\n86 114 -2\n86 115 -1\n86 117 -2\n86 224 -3\n86 225 -3\n86 232 -3\n86 233 -3\n86 234 -3\n86 242 -3\n86 243 -3\n86 249 -2\n86 250 -2\n86 252 -2\n87 44 -3\n87 46 -3\n87 65 -2\n87 84 1\n87 97 -2\n87 99 -1\n87 100 -1\n87 101 -1\n87 103 -1\n87 111 -1\n87 113 -1\n87 224 -2\n87 225 -2\n87 232 -1\n87 233 -1\n87 234 -1\n87 242 -1\n87 243 -1\n88 44 1\n88 46 1\n88 59 2\n88 67 -1\n88 71 -1\n88 74 2\n88 79 -1\n88 81 -1\n88 84 1\n89 44 -4\n89 46 -4\n89 65 -4\n89 67 -1\n89 71 -1\n89 74 -1\n89 79 -1\n89 81 -1\n89 83 -1\n89 84 1\n89 97 -4\n89 99 -4\n89 100 -4\n89 101 -4\n89 102 -1\n89 103 -4\n89 109 -3\n89 110 -3\n89 111 -4\n89 112 -3\n89 113 -4\n89 114 -3\n89 115 -3\n89 117 -3\n89 224 -4\n89 225 -4\n89 232 -4\n89 233 -4\n89 234 -4\n89 242 -4\n89 243 -4\n89 249 -3\n89 250 -3\n89 252 -3\n90 74 2\n90 84 1\n90 121 -1\n91 106 5\n98 97 -1\n98 102 0\n98 120 -1\n98 224 -1\n98 225 -1\n99 74 2\n99 84 -2\n99 89 -2\n101 34 -2\n101 39 -2\n102 41 3\n102 44 -3\n102 45 -2\n102 46 -3\n102 58 2\n102 59 2\n102 63 1\n102 93 3\n102 98 0\n102 104 0\n102 116 1\n102 118 1\n102 119 1\n102 120 0\n102 121 1\n102 125 2\n102 236 1\n102 8216 2\n102 8217 2\n102 8220 2\n102 8221 2\n103 106 1\n106 106 1\n107 44 2\n107 45 -3\n107 46 2\n107 58 2\n107 59 2\n107 99 -1\n107 100 -1\n107 101 -1\n107 103 -1\n107 111 -1\n107 113 -1\n107 116 0\n107 232 -1\n107 233 -1\n107 234 -1\n107 242 -1\n107 243 -1\n110 34 -2\n110 39 -2\n111 34 -3\n111 39 -3\n111 97 -1\n111 102 -1\n111 120 -1\n111 224 -1\n111 225 -1\n111 8216 -1\n111 8217 -3\n111 8220 -2\n111 8221 -3\n112 97 -1\n112 102 -1\n112 120 -1\n112 224 -1\n112 225 -1\n112 8216 -3\n112 8217 -3\n112 8220 -1\n112 8221 -3\n113 106 2\n114 44 -4\n114 45 -3\n114 46 -4\n114 58 2\n114 59 2\n114 99 -1\n114 100 -1\n114 101 -1\n114 102 1\n114 103 -1\n114 109 0\n114 110 0\n114 111 -1\n114 113 -1\n114 115 0\n114 116 1\n114 118 2\n114 119 2\n114 120 1\n114 121 2\n114 122 1\n114 232 -1\n114 233 -1\n114 234 -1\n114 242 -1\n114 243 -1\n114 8216 4\n114 8217 3\n114 8220 4\n114 8221 3\n116 45 -3\n116 63 -1\n116 99 -1\n116 100 -1\n116 101 0\n116 103 0\n116 111 0\n116 113 0\n116 120 1\n116 232 0\n116 233 0\n116 234 0\n116 242 0\n116 243 0\n117 34 -1\n117 39 -1\n118 44 -3\n118 46 -3\n118 97 -1\n118 99 0\n118 100 0\n118 101 0\n118 103 0\n118 111 0\n118 113 0\n118 224 -1\n118 225 -1\n118 232 0\n118 233 0\n118 234 0\n118 242 0\n118 243 0\n119 44 -2\n119 46 -2\n119 99 0\n119 100 0\n119 101 0\n119 103 0\n119 111 0\n119 113 0\n119 232 0\n119 233 0\n119 234 0\n119 242 0\n119 243 0\n120 99 0\n120 100 0\n120 101 0\n120 103 0\n120 111 0\n120 113 0\n120 232 0\n120 233 0\n120 234 0\n120 242 0\n120 243 0\n121 34 1\n121 39 1\n121 44 -2\n121 46 -3\n121 63 -2\n121 99 0\n121 100 0\n121 101 0\n121 102 0\n121 103 0\n121 111 0\n121 113 0\n121 116 0\n121 232 0\n121 233 0\n121 234 0\n121 242 0\n121 243 0\n123 106 4\n242 97 -1\n242 102 -1\n242 120 -1\n243 97 -1\n243 102 -1\n243 120 -1\n8216 65 -5\n8216 67 -1\n8216 74 -3\n8216 84 2\n8216 99 -3\n8216 100 -4\n8216 101 -3\n8216 103 -3\n8216 111 -3\n8216 115 -2\n8216 8216 -4\n8217 44 -2\n8217 46 -2\n8217 65 -4\n8217 74 -4\n8217 84 2\n8217 97 -2\n8217 99 -4\n8217 100 -4\n8217 101 -4\n8217 103 -4\n8217 111 -4\n8217 113 -3\n8217 115 -3\n8217 8217 -4\n8220 44 -2\n8220 46 -2\n8220 65 -5\n8220 74 -4\n8220 84 2\n8220 99 -3\n8220 100 -3\n8220 101 -3\n8220 103 -3\n8220 115 -2\n8221 44 -2\n8221 46 -2\n8221 65 -3\n8221 84 2\n8221 99 -1\n8221 100 -4\n8221 101 -4\n8221 103 -4\n8221 111 -4\n8221 115 -3"} \ No newline at end of file diff --git a/fonts/YaHei.png b/fonts/YaHei.png new file mode 100644 index 0000000..cb4b5f1 Binary files /dev/null and b/fonts/YaHei.png differ diff --git a/package.json b/package.json index 1c8a88a..a5340c5 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "q5", - "version": "2.4.10", + "version": "2.5.0", "description": "A sequel to p5.js that's smaller and faster", "author": "quinton-ashley", "contributors": [ diff --git a/q5.d.ts b/q5.d.ts index 0ce8f65..39e8b19 100644 --- a/q5.d.ts +++ b/q5.d.ts @@ -86,6 +86,22 @@ declare global { */ var deviceOrientation: string | null; + /** ⭐️ + * Use preload to load assets before the sketch starts and the + * setup function is run. + */ + function preload(): void; + + /** ⭐️ + * The setup function is called once when the program starts. + */ + function setup(): void; + + /** ⭐️ + * The draw function is run 60 times per second by default. + */ + function draw(): void; + /** ⭐️ * Stops the draw loop. */ @@ -312,6 +328,8 @@ declare global { /** ⬜️ * Saves the current drawing style settings. + * + * This includes the fill, stroke, stroke weight, tint, image mode, rect mode, ellipse mode, text size, text align, and text baseline. */ function pushStyles(): void; @@ -775,6 +793,11 @@ declare global { /** ✍️ * Loads a font from a URL and optionally runs a callback function with the font name once it's loaded + * + * WebGPU: Fonts must be in MSDF format with the file ending + * "-msdf.json". If no font is loaded before `text` is run, then + * the default font is loaded: + * https://q5js.org/fonts/YaHei-msdf.json * @param url - URL of the font to load * @param cb - Optional callback function that receives the font name as an argument once the font is loaded * @returns name of the loaded font @@ -782,7 +805,7 @@ declare global { function loadFont(url: string, cb?: (fontName: string) => void): string; /** ✍️ - * Sets the current font to be used for text elements + * Sets the current font to be used for rendering text * @param fontName - name of the font */ function textFont(fontName: string): void; @@ -812,7 +835,7 @@ declare global { * @param horiz - horizontal alignment ('left', 'center', 'right') * @param vert - vertical alignment ('top', 'middle', 'bottom', 'alphabetic') */ - function textAlign(horiz: 'left' | 'center' | 'right', vert?: 'top' | 'middle' | 'bottom' | 'alphabetic'): void; + function textAlign(horiz: 'left' | 'center' | 'right', vert?: 'top' | 'center' | 'bottom' | 'alphabetic'): void; /** ✍️ * Calculates and returns the width of a given string of text @@ -894,7 +917,7 @@ declare global { /** 🎨 * Sets the color mode for the sketch. Changes the type of color object created by color functions. - * + * * In WebGPU, the default color mode is 'rgb' in float format. * @param mode - color mode ('rgb', 'srgb', or 'oklch') * @param format - color format (1 or 255) for floating point or legacy 8-bit integer representation @@ -1145,7 +1168,7 @@ declare global { */ function noiseDetail(lod: number, falloff: number): void; - // 🔊 q5-sound + // 🔊 sound /** 🔊 * Represents a sound object, extending the native `Audio` to diff --git a/q5.js b/q5.js index 057bbc5..0b3a21a 100644 --- a/q5.js +++ b/q5.js @@ -70,9 +70,9 @@ function Q5(scope, parent, renderer) { let ts = timestamp || performance.now(); $._lastFrameTime ??= ts - $._targetFrameDuration; - if ($._shouldResize) { + if ($._didResize) { $.windowResized(); - $._shouldResize = false; + $._didResize = false; } if ($._loop) looper = raf($._draw); @@ -483,7 +483,7 @@ Q5.modules.canvas = ($, q) => { function parentResized() { if ($.frameCount > 1) { - $._shouldResize = true; + $._didResize = true; $._adjustDisplay(); } } @@ -543,14 +543,9 @@ Q5.modules.canvas = ($, q) => { '_imageMode', '_rectMode', '_ellipseMode', - '_textFont', - '_textLeading', - '_leadingSet', '_textSize', '_textAlign', - '_textBaseline', - '_textStyle', - '_textWrap' + '_textBaseline' ]; $._styles = []; @@ -563,6 +558,15 @@ Q5.modules.canvas = ($, q) => { let styles = $._styles.pop(); for (let s of $._styleNames) $[s] = styles[s]; }; + + if (window && $._scope != 'graphics') { + window.addEventListener('resize', () => { + $._didResize = true; + q.windowWidth = window.innerWidth; + q.windowHeight = window.innerHeight; + q.deviceOrientation = window.screen?.orientation?.type; + }); + } }; Q5.canvasOptions = { @@ -713,15 +717,6 @@ Q5.renderers.q2d.canvas = ($, q) => { document.body.append(vid); return vid; }; - - if (window && $._scope != 'graphics') { - window.addEventListener('resize', () => { - $._shouldResize = true; - q.windowWidth = window.innerWidth; - q.windowHeight = window.innerHeight; - q.deviceOrientation = window.screen?.orientation?.type; - }); - } }; Q5.renderers.q2d.drawing = ($) => { $._doStroke = true; @@ -1424,9 +1419,10 @@ Q5.BLUR = 8; Q5.renderers.q2d.text = ($, q) => { $._textAlign = 'left'; $._textBaseline = 'alphabetic'; + $._textSize = 12; let font = 'sans-serif', - tSize = 12, + leadingSet = false, leading = 15, leadDiff = 3, emphasis = 'normal', @@ -1458,12 +1454,12 @@ Q5.renderers.q2d.text = ($, q) => { styleHash = -1; }; $.textSize = (x) => { - if (x === undefined) return tSize; + if (x === undefined) return $._textSize; if ($._da) x *= $._da; - tSize = x; + $._textSize = x; fontMod = true; styleHash = -1; - if (!$._leadingSet) { + if (!leadingSet) { leading = x * 1.25; leadDiff = leading - x; } @@ -1477,8 +1473,8 @@ Q5.renderers.q2d.text = ($, q) => { if (x === undefined) return leading; if ($._da) x *= $._da; leading = x; - leadDiff = x - tSize; - $._leadingSet = true; + leadDiff = x - $._textSize; + leadingSet = true; styleHash = -1; }; $.textAlign = (horiz, vert) => { @@ -1486,7 +1482,6 @@ Q5.renderers.q2d.text = ($, q) => { if (vert) { $.ctx.textBaseline = $._textBaseline = vert == $.CENTER ? 'middle' : vert; } - styleHash = -1; }; $.textWidth = (str) => $.ctx.measureText(str).width; @@ -1497,7 +1492,7 @@ Q5.renderers.q2d.text = ($, q) => { $.textStroke = $.stroke; let updateStyleHash = () => { - let styleString = font + tSize + emphasis + leading; + let styleString = font + $._textSize + emphasis + leading; let hash = 5381; for (let i = 0; i < styleString.length; i++) { @@ -1530,7 +1525,7 @@ Q5.renderers.q2d.text = ($, q) => { let img, tX, tY; if (fontMod) { - ctx.font = `${emphasis} ${tSize}px ${font}`; + ctx.font = `${emphasis} ${$._textSize}px ${font}`; fontMod = false; } @@ -1551,7 +1546,7 @@ Q5.renderers.q2d.text = ($, q) => { if (str.indexOf('\n') == -1) lines[0] = str; else lines = str.split('\n'); - if (w) { + if (str.length > w) { let wrapped = []; for (let line of lines) { let i = 0; @@ -1563,11 +1558,9 @@ Q5.renderers.q2d.text = ($, q) => { break; } let end = line.lastIndexOf(' ', max); - if (end === -1 || end < i) { - end = max; - } + if (end === -1 || end < i) end = max; wrapped.push(line.slice(i, end)); - i = end; + i = end + 1; } } lines = wrapped; @@ -1595,6 +1588,7 @@ Q5.renderers.q2d.text = ($, q) => { img._top = descent + leadDiff; img._middle = img._top + ascent * 0.5; img._bottom = img._top + ascent; + img._leading = leading; } img._fill = $._fill; @@ -1654,7 +1648,7 @@ Q5.renderers.q2d.text = ($, q) => { else if (ta == 'right') x -= img.width; let bl = $._textBaseline; - if (bl == 'alphabetic') y -= leading; + if (bl == 'alphabetic') y -= img._leading; else if (bl == 'middle') y -= img._middle; else if (bl == 'bottom') y -= img._bottom; else if (bl == 'top') y -= img._top; @@ -2740,10 +2734,11 @@ Q5.modules.util = ($, q) => { fetch(path) .then((r) => { if (type == 'json') return r.json(); - if (type == 'text') return r.text(); + return r.text(); }) .then((r) => { q._preloadCount--; + if (type == 'csv') r = $.CSV.parse(r); Object.assign(ret, r); if (cb) cb(r); }); @@ -2752,6 +2747,21 @@ Q5.modules.util = ($, q) => { $.loadStrings = (path, cb) => $._loadFile(path, cb, 'text'); $.loadJSON = (path, cb) => $._loadFile(path, cb, 'json'); + $.loadCSV = (path, cb) => $._loadFile(path, cb, 'csv'); + + $.CSV = {}; + $.CSV.parse = (csv, sep = ',', lineSep = '\n') => { + let a = [], + lns = csv.split(lineSep), + headers = lns[0].split(sep); + for (let i = 1; i < lns.length; i++) { + let o = {}, + ln = lns[i].split(sep); + headers.forEach((h, i) => (o[h] = JSON.parse(ln[i]))); + a.push(o); + } + return a; + }; if (typeof localStorage == 'object') { $.storeItem = localStorage.setItem; @@ -3064,7 +3074,8 @@ Q5.renderers.webgpu.canvas = ($, q) => { // colors used for each draw call let colorsStack = ($.colorsStack = [1, 1, 1, 1]); - $._envLayout = Q5.device.createBindGroupLayout({ + $._transformLayout = Q5.device.createBindGroupLayout({ + label: 'transformLayout', entries: [ { binding: 0, @@ -3073,14 +3084,9 @@ Q5.renderers.webgpu.canvas = ($, q) => { type: 'uniform', hasDynamicOffset: false } - } - ] - }); - - $._transformLayout = Q5.device.createBindGroupLayout({ - entries: [ + }, { - binding: 0, + binding: 1, visibility: GPUShaderStage.VERTEX, buffer: { type: 'read-only-storage', @@ -3090,9 +3096,9 @@ Q5.renderers.webgpu.canvas = ($, q) => { ] }); - $.bindGroupLayouts = [$._envLayout, $._transformLayout]; + $.bindGroupLayouts = [$._transformLayout]; - const uniformBuffer = Q5.device.createBuffer({ + let uniformBuffer = Q5.device.createBuffer({ size: 8, // Size of two floats usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST }); @@ -3107,18 +3113,6 @@ Q5.renderers.webgpu.canvas = ($, q) => { Q5.device.queue.writeBuffer(uniformBuffer, 0, new Float32Array([$.canvas.hw, $.canvas.hh])); - $._envBindGroup = Q5.device.createBindGroup({ - layout: $._envLayout, - entries: [ - { - binding: 0, - resource: { - buffer: uniformBuffer - } - } - ] - }); - return c; }; @@ -3128,7 +3122,7 @@ Q5.renderers.webgpu.canvas = ($, q) => { // current color index, used to associate a vertex with a color let colorIndex = 0; - const addColor = (r, g, b, a = 1) => { + let addColor = (r, g, b, a = 1) => { if (typeof r == 'string') r = $.color(r); else if (b == undefined) { // grayscale mode `fill(1, 0.5)` @@ -3140,6 +3134,8 @@ Q5.renderers.webgpu.canvas = ($, q) => { colorIndex++; }; + $._fillIndex = $._strokeIndex = -1; + $.fill = (r, g, b, a) => { addColor(r, g, b, a); $._doFill = true; @@ -3171,56 +3167,70 @@ Q5.renderers.webgpu.canvas = ($, q) => { }; $.resetMatrix(); - // Boolean to track if the matrix has been modified + // tracks if the matrix has been modified $._matrixDirty = false; - // Array to store transformation matrices for the render pass + // array to store transformation matrices for the render pass $.transformStates = [$._matrix.slice()]; - // Stack to keep track of transformation matrix indexes + // stack to keep track of transformation matrix indexes $._transformIndexStack = []; $.translate = (x, y, z) => { if (!x && !y && !z) return; // Update the translation values - $._matrix[3] += x; - $._matrix[7] -= y; - $._matrix[11] += z || 0; + $._matrix[12] += x; + $._matrix[13] -= y; + $._matrix[14] += z || 0; $._matrixDirty = true; }; - $.rotate = (r) => { - if (!r) return; - if ($._angleMode) r *= $._DEGTORAD; + $.rotate = (a) => { + if (!a) return; + if ($._angleMode) a *= $._DEGTORAD; - let cosR = Math.cos(r); - let sinR = Math.sin(r); + let cosR = Math.cos(a); + let sinR = Math.sin(a); + + let m = $._matrix; + + let m0 = m[0], + m1 = m[1], + m4 = m[4], + m5 = m[5]; - let m0 = $._matrix[0], - m1 = $._matrix[1], - m4 = $._matrix[4], - m5 = $._matrix[5]; if (!m0 && !m1 && !m4 && !m5) { - $._matrix[0] = cosR; - $._matrix[1] = sinR; - $._matrix[4] = -sinR; - $._matrix[5] = cosR; + m[0] = cosR; + m[1] = sinR; + m[4] = -sinR; + m[5] = cosR; } else { - $._matrix[0] = m0 * cosR + m4 * sinR; - $._matrix[1] = m1 * cosR + m5 * sinR; - $._matrix[4] = m0 * -sinR + m4 * cosR; - $._matrix[5] = m1 * -sinR + m5 * cosR; + m[0] = m0 * cosR + m4 * sinR; + m[1] = m1 * cosR + m5 * sinR; + m[4] = m4 * cosR - m0 * sinR; + m[5] = m5 * cosR - m1 * sinR; } $._matrixDirty = true; }; - $.scale = (sx = 1, sy, sz = 1) => { - sy ??= sx; + $.scale = (x = 1, y, z = 1) => { + y ??= x; - $._matrix[0] *= sx; - $._matrix[5] *= sy; - $._matrix[10] *= sz; + let m = $._matrix; + + m[0] *= x; + m[1] *= x; + m[2] *= x; + m[3] *= x; + m[4] *= y; + m[5] *= y; + m[6] *= y; + m[7] *= y; + m[8] *= z; + m[9] *= z; + m[10] *= z; + m[11] *= z; $._matrixDirty = true; }; @@ -3292,7 +3302,7 @@ Q5.renderers.webgpu.canvas = ($, q) => { if (!$._transformIndexStack.length) { return console.warn('Matrix index stack is empty!'); } - // Pop the last matrix index from the stack and set it as the current matrix index + // Pop the last matrix index and set it as the current matrix index let idx = $._transformIndexStack.pop(); $._matrix = $.transformStates[idx].slice(); $._transformIndex = idx; @@ -3315,7 +3325,6 @@ Q5.renderers.webgpu.canvas = ($, q) => { // left, right, top, bottom let l, r, t, b; if (!mode || mode == 'corner') { - // CORNER l = x; r = x + w; t = -y; @@ -3355,7 +3364,7 @@ Q5.renderers.webgpu.canvas = ($, q) => { $._render = () => { if (transformStates.length > 1 || !$._transformBindGroup) { - const transformBuffer = Q5.device.createBuffer({ + let transformBuffer = Q5.device.createBuffer({ size: transformStates.length * 64, // Size of 16 floats usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST }); @@ -3367,6 +3376,12 @@ Q5.renderers.webgpu.canvas = ($, q) => { entries: [ { binding: 0, + resource: { + buffer: uniformBuffer + } + }, + { + binding: 1, resource: { buffer: transformBuffer } @@ -3375,35 +3390,47 @@ Q5.renderers.webgpu.canvas = ($, q) => { }); } - pass.setBindGroup(0, $._envBindGroup); - pass.setBindGroup(1, $._transformBindGroup); + pass.setBindGroup(0, $._transformBindGroup); for (let m of $._hooks.preRender) m(); let drawVertOffset = 0; let imageVertOffset = 0; + let textCharOffset = 0; let curPipelineIndex = -1; let curTextureIndex = -1; - pass.setPipeline($.pipelines[0]); - for (let i = 0; i < drawStack.length; i += 2) { let v = drawStack[i + 1]; + if (drawStack[i] == -1) { + v(); + continue; + } + if (curPipelineIndex != drawStack[i]) { curPipelineIndex = drawStack[i]; pass.setPipeline($.pipelines[curPipelineIndex]); } if (curPipelineIndex == 0) { - pass.draw(v, 1, drawVertOffset, 0); + // v is the number of vertices + pass.draw(v, 1, drawVertOffset); drawVertOffset += v; } else if (curPipelineIndex == 1) { if (curTextureIndex != v) { - pass.setBindGroup(3, $._textureBindGroups[v]); + // v is the texture index + pass.setBindGroup(2, $._textureBindGroups[v]); } - pass.draw(6, 1, imageVertOffset, 0); + pass.draw(6, 1, imageVertOffset); imageVertOffset += 6; + } else if (curPipelineIndex == 2) { + pass.setBindGroup(2, $._font.bindGroup); + pass.setBindGroup(3, $._textBindGroup); + + // v is the number of characters in the text + pass.draw(4, v, 0, textCharOffset); + textCharOffset += v; } } @@ -3412,7 +3439,7 @@ Q5.renderers.webgpu.canvas = ($, q) => { $._finishRender = () => { pass.end(); - const commandBuffer = $.encoder.finish(); + let commandBuffer = $.encoder.finish(); Q5.device.queue.submit([commandBuffer]); q.pass = $.encoder = null; @@ -3455,8 +3482,8 @@ Q5.renderers.webgpu.drawing = ($, q) => { label: 'drawingVertexShader', code: ` struct VertexOutput { - @builtin(position) position: vec4, - @location(1) colorIndex: f32 + @builtin(position) position: vec4f, + @location(0) colorIndex: f32 }; struct Uniforms { @@ -3465,12 +3492,12 @@ struct Uniforms { }; @group(0) @binding(0) var uniforms: Uniforms; -@group(1) @binding(0) var transforms: array>; +@group(0) @binding(1) var transforms: array>; @vertex -fn vertexMain(@location(0) pos: vec2, @location(1) colorIndex: f32, @location(2) transformIndex: f32) -> VertexOutput { - var vert = vec4(pos, 0.0, 1.0); - vert *= transforms[i32(transformIndex)]; +fn vertexMain(@location(0) pos: vec2f, @location(1) colorIndex: f32, @location(2) transformIndex: f32) -> VertexOutput { + var vert = vec4f(pos, 0.0, 1.0); + vert = transforms[i32(transformIndex)] * vert; vert.x /= uniforms.halfWidth; vert.y /= uniforms.halfHeight; @@ -3485,17 +3512,18 @@ fn vertexMain(@location(0) pos: vec2, @location(1) colorIndex: f32, @locati let fragmentShader = Q5.device.createShaderModule({ label: 'drawingFragmentShader', code: ` -@group(2) @binding(0) var uColors : array>; +@group(1) @binding(0) var colors : array; @fragment -fn fragmentMain(@location(1) colorIndex: f32) -> @location(0) vec4 { - let index = u32(colorIndex); - return mix(uColors[index], uColors[index + 1u], fract(colorIndex)); +fn fragmentMain(@location(0) colorIndex: f32) -> @location(0) vec4f { + let index = i32(colorIndex); + return mix(colors[index], colors[index + 1], fract(colorIndex)); } ` }); colorsLayout = Q5.device.createBindGroupLayout({ + label: 'colorsLayout', entries: [ { binding: 0, @@ -3726,10 +3754,17 @@ fn fragmentMain(@location(1) colorIndex: f32) -> @location(0) vec4 { $.background = (r, g, b, a) => { $.push(); $.resetMatrix(); - if (r.src) $.image(r, -c.hw, -c.hh, c.w, c.h); - else { + if (r.src) { + let og = $._imageMode; + $._imageMode = 'corner'; + $.image(r, -c.hw, -c.hh, c.w, c.h); + $._imageMode = og; + } else { + let og = $._rectMode; + $._rectMode = 'corner'; $.fill(r, g, b, a); $.rect(-c.hw, -c.hh, c.w, c.h); + $._rectMode = og; } $.pop(); }; @@ -3837,7 +3872,7 @@ fn fragmentMain(@location(1) colorIndex: f32) -> @location(0) vec4 { }); // set the bind group once before rendering - $.pass.setBindGroup(2, $._colorsBindGroup); + $.pass.setBindGroup(1, $._colorsBindGroup); }); $._hooks.postRender.push(() => { @@ -3852,8 +3887,8 @@ Q5.renderers.webgpu.image = ($, q) => { label: 'imageVertexShader', code: ` struct VertexOutput { - @builtin(position) position: vec4, - @location(0) texCoord: vec2 + @builtin(position) position: vec4f, + @location(0) texCoord: vec2f }; struct Uniforms { @@ -3862,12 +3897,12 @@ struct Uniforms { }; @group(0) @binding(0) var uniforms: Uniforms; -@group(1) @binding(0) var transforms: array>; +@group(0) @binding(1) var transforms: array>; @vertex -fn vertexMain(@location(0) pos: vec2, @location(1) texCoord: vec2, @location(2) transformIndex: f32) -> VertexOutput { - var vert = vec4(pos, 0.0, 1.0); - vert *= transforms[i32(transformIndex)]; +fn vertexMain(@location(0) pos: vec2f, @location(1) texCoord: vec2f, @location(2) transformIndex: f32) -> VertexOutput { + var vert = vec4f(pos, 0.0, 1.0); + vert = transforms[i32(transformIndex)] * vert; vert.x /= uniforms.halfWidth; vert.y /= uniforms.halfHeight; @@ -3882,11 +3917,11 @@ fn vertexMain(@location(0) pos: vec2, @location(1) texCoord: vec2, @lo let fragmentShader = Q5.device.createShaderModule({ label: 'imageFragmentShader', code: ` -@group(3) @binding(0) var samp: sampler; -@group(3) @binding(1) var texture: texture_2d; +@group(2) @binding(0) var samp: sampler; +@group(2) @binding(1) var texture: texture_2d; @fragment -fn fragmentMain(@location(0) texCoord: vec2) -> @location(0) vec4 { +fn fragmentMain(@location(0) texCoord: vec2f) -> @location(0) vec4f { // Sample the texture using the interpolated texture coordinate return textureSample(texture, samp, texCoord); } @@ -3918,11 +3953,9 @@ fn fragmentMain(@location(0) texCoord: vec2) -> @location(0) vec4 { ] }; - $.bindGroupLayouts.push(textureLayout); - const pipelineLayout = Q5.device.createPipelineLayout({ label: 'imagePipelineLayout', - bindGroupLayouts: $.bindGroupLayouts + bindGroupLayouts: [...$.bindGroupLayouts, textureLayout] }); $.pipelines[1] = Q5.device.createRenderPipeline({ @@ -4079,34 +4112,522 @@ Q5.DILATE = 6; Q5.ERODE = 7; Q5.BLUR = 8; Q5.renderers.webgpu.text = ($, q) => { - let t = $.createGraphics(1, 1); - t.pixelDensity($._pixelDensity); - t._imageMode = 'corner'; + let textShader = Q5.device.createShaderModule({ + label: 'MSDF text shader', + code: ` +// Positions for simple quad geometry +const pos = array(vec2f(0, -1), vec2f(1, -1), vec2f(0, 0), vec2f(1, 0)); - $.loadFont = (f) => { +struct VertexInput { + @builtin(vertex_index) vertex : u32, + @builtin(instance_index) instance : u32, +}; +struct VertexOutput { + @builtin(position) position : vec4f, + @location(0) texcoord : vec2f, + @location(1) colorIndex : f32 +}; +struct Char { + texOffset: vec2f, + texExtent: vec2f, + size: vec2f, + offset: vec2f, +}; +struct Text { + pos: vec2f, + scale: f32, + transformIndex: f32, + fillIndex: f32, + strokeIndex: f32 +}; +struct Uniforms { + halfWidth: f32, + halfHeight: f32 +}; + +@group(0) @binding(0) var uniforms: Uniforms; +@group(0) @binding(1) var transforms: array>; + +@group(1) @binding(0) var colors : array; + +@group(2) @binding(0) var fontTexture: texture_2d; +@group(2) @binding(1) var fontSampler: sampler; +@group(2) @binding(2) var fontChars: array; + +@group(3) @binding(0) var textChars: array; +@group(3) @binding(1) var textMetadata: array; + +@vertex +fn vertexMain(input : VertexInput) -> VertexOutput { + let char = textChars[input.instance]; + + let text = textMetadata[i32(char.w)]; + + let fontChar = fontChars[i32(char.z)]; + + let charPos = ((pos[input.vertex] * fontChar.size + char.xy + fontChar.offset) * text.scale) + text.pos; + + var vert = vec4f(charPos, 0.0, 1.0); + vert = transforms[i32(text.transformIndex)] * vert; + vert.x /= uniforms.halfWidth; + vert.y /= uniforms.halfHeight; + + var output : VertexOutput; + output.position = vert; + output.texcoord = (pos[input.vertex] * vec2f(1, -1)) * fontChar.texExtent + fontChar.texOffset; + output.colorIndex = text.fillIndex; + return output; +} + +fn sampleMsdf(texcoord: vec2f) -> f32 { + let c = textureSample(fontTexture, fontSampler, texcoord); + return max(min(c.r, c.g), min(max(c.r, c.g), c.b)); +} + +@fragment +fn fragmentMain(input : VertexOutput) -> @location(0) vec4f { + // pxRange (AKA distanceRange) comes from the msdfgen tool, + // uses the default which is 4. + let pxRange = 4.0; + let sz = vec2f(textureDimensions(fontTexture, 0)); + let dx = sz.x*length(vec2f(dpdxFine(input.texcoord.x), dpdyFine(input.texcoord.x))); + let dy = sz.y*length(vec2f(dpdxFine(input.texcoord.y), dpdyFine(input.texcoord.y))); + let toPixels = pxRange * inverseSqrt(dx * dx + dy * dy); + let sigDist = sampleMsdf(input.texcoord) - 0.5; + let pxDist = sigDist * toPixels; + let edgeWidth = 0.5; + let alpha = smoothstep(-edgeWidth, edgeWidth, pxDist); + if (alpha < 0.001) { + discard; + } + let fillColor = colors[i32(input.colorIndex)]; + return vec4f(fillColor.rgb, fillColor.a * alpha); +} +` + }); + + class MsdfFont { + constructor(pipeline, bindGroup, lineHeight, chars, kernings) { + this.pipeline = pipeline; + this.bindGroup = bindGroup; + this.lineHeight = lineHeight; + this.chars = chars; + this.kernings = kernings; + let charArray = Object.values(chars); + this.charCount = charArray.length; + this.defaultChar = charArray[0]; + } + getChar(charCode) { + return this.chars[charCode] ?? this.defaultChar; + } + // Gets the distance in pixels a line should advance for a given character code. If the upcoming + // character code is given any kerning between the two characters will be taken into account. + getXAdvance(charCode, nextCharCode = -1) { + let char = this.getChar(charCode); + if (nextCharCode >= 0) { + let kerning = this.kernings.get(charCode); + if (kerning) { + return char.xadvance + (kerning.get(nextCharCode) ?? 0); + } + } + return char.xadvance; + } + } + + let textBindGroupLayout = Q5.device.createBindGroupLayout({ + label: 'MSDF text group layout', + entries: [ + { + binding: 0, + visibility: GPUShaderStage.VERTEX | GPUShaderStage.FRAGMENT, + buffer: { type: 'read-only-storage' } + }, + { + binding: 1, + visibility: GPUShaderStage.VERTEX | GPUShaderStage.FRAGMENT, + buffer: { type: 'read-only-storage' } + } + ] + }); + + let fonts = {}; + + let createFont = async (fontJsonUrl, fontName, cb) => { q._preloadCount++; - return t.loadFont(f, () => { + + let res = await fetch(fontJsonUrl); + if (res.status == 404) { q._preloadCount--; + return ''; + } + let atlas = await res.json(); + + let slashIdx = fontJsonUrl.lastIndexOf('/'); + let baseUrl = slashIdx != -1 ? fontJsonUrl.substring(0, slashIdx + 1) : ''; + // load font image + res = await fetch(baseUrl + atlas.pages[0]); + let img = await createImageBitmap(await res.blob()); + + // convert image to texture + let imgSize = [img.width, img.height, 1]; + let texture = Q5.device.createTexture({ + label: `MSDF ${fontName}`, + size: imgSize, + format: 'rgba8unorm', + usage: GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.COPY_DST | GPUTextureUsage.RENDER_ATTACHMENT }); + Q5.device.queue.copyExternalImageToTexture({ source: img }, { texture }, imgSize); + + // to make q5's default font file smaller, + // the chars and kernings are stored as csv strings + if (typeof atlas.chars == 'string') { + atlas.chars = $.CSV.parse(atlas.chars, ' '); + atlas.kernings = $.CSV.parse(atlas.kernings, ' '); + } + + let charCount = atlas.chars.length; + let charsBuffer = Q5.device.createBuffer({ + size: charCount * 32, + usage: GPUBufferUsage.STORAGE, + mappedAtCreation: true + }); + + let fontChars = new Float32Array(charsBuffer.getMappedRange()); + let u = 1 / atlas.common.scaleW; + let v = 1 / atlas.common.scaleH; + let chars = {}; + let o = 0; // offset + for (let [i, char] of atlas.chars.entries()) { + chars[char.id] = char; + chars[char.id].charIndex = i; + fontChars[o] = char.x * u; // texOffset.x + fontChars[o + 1] = char.y * v; // texOffset.y + fontChars[o + 2] = char.width * u; // texExtent.x + fontChars[o + 3] = char.height * v; // texExtent.y + fontChars[o + 4] = char.width; // size.x + fontChars[o + 5] = char.height; // size.y + fontChars[o + 6] = char.xoffset; // offset.x + fontChars[o + 7] = -char.yoffset; // offset.y + o += 8; + } + charsBuffer.unmap(); + + let fontSampler = Q5.device.createSampler({ + minFilter: 'linear', + magFilter: 'linear', + mipmapFilter: 'linear', + maxAnisotropy: 16 + }); + let fontBindGroupLayout = Q5.device.createBindGroupLayout({ + label: 'MSDF font group layout', + entries: [ + { + binding: 0, + visibility: GPUShaderStage.FRAGMENT, + texture: {} + }, + { + binding: 1, + visibility: GPUShaderStage.FRAGMENT, + sampler: {} + }, + { + binding: 2, + visibility: GPUShaderStage.VERTEX, + buffer: { type: 'read-only-storage' } + } + ] + }); + let fontPipeline = Q5.device.createRenderPipeline({ + label: 'msdf font pipeline', + layout: Q5.device.createPipelineLayout({ + bindGroupLayouts: [...$.bindGroupLayouts, fontBindGroupLayout, textBindGroupLayout] + }), + vertex: { + module: textShader, + entryPoint: 'vertexMain' + }, + fragment: { + module: textShader, + entryPoint: 'fragmentMain', + targets: [ + { + format: 'bgra8unorm', + blend: { + color: { + srcFactor: 'src-alpha', + dstFactor: 'one-minus-src-alpha' + }, + alpha: { + srcFactor: 'one', + dstFactor: 'one' + } + } + } + ] + }, + primitive: { + topology: 'triangle-strip', + stripIndexFormat: 'uint32' + } + }); + + let fontBindGroup = Q5.device.createBindGroup({ + label: 'msdf font bind group', + layout: fontBindGroupLayout, + entries: [ + { + binding: 0, + resource: texture.createView() + }, + { binding: 1, resource: fontSampler }, + { binding: 2, resource: { buffer: charsBuffer } } + ] + }); + + let kernings = new Map(); + if (atlas.kernings) { + for (let kerning of atlas.kernings) { + let charKerning = kernings.get(kerning.first); + if (!charKerning) { + charKerning = new Map(); + kernings.set(kerning.first, charKerning); + } + charKerning.set(kerning.second, kerning.amount); + } + } + + $._font = new MsdfFont(fontPipeline, fontBindGroup, atlas.common.lineHeight, chars, kernings); + + fonts[fontName] = $._font; + $.pipelines[2] = $._font.pipeline; + + q._preloadCount--; + + if (cb) cb(fontName); }; - // directly add these text setting functions to the webgpu renderer - $.textFont = t.textFont; - $.textSize = t.textSize; - $.textLeading = t.textLeading; - $.textStyle = t.textStyle; - $.textAlign = t.textAlign; - $.textWidth = t.textWidth; - $.textAscent = t.textAscent; - $.textDescent = t.textDescent; + // q2d graphics context to use for text image creation + let g = $.createGraphics(1, 1); + g.colorMode($.RGB, 1); + + $.loadFont = (url, cb) => { + let ext = url.slice(url.lastIndexOf('.') + 1); + if (ext != 'json') return g.loadFont(url, cb); + let fontName = url.slice(url.lastIndexOf('/') + 1, url.lastIndexOf('-')); + createFont(url, fontName, cb); + return fontName; + }; + + $._textSize = 18; + $._textAlign = 'left'; + $._textBaseline = 'alphabetic'; + let leadingSet = false, + leading = 22.5, + leadDiff = 4.5, + leadPercent = 1.25; + + $.textFont = (fontName) => { + $._font = fonts[fontName]; + + // replay the change of font in the draw stack + $.drawStack.push(-1, () => { + $._font = fonts[fontName]; + $.pipelines[2] = $._font.pipeline; + }); + }; + $.textSize = (size) => { + $._textSize = size; + if (!leadingSet) { + leading = size * leadPercent; + leadDiff = leading - size; + } + }; + $.textLeading = (lineHeight) => { + $._font.lineHeight = leading = lineHeight; + leadDiff = leading - $._textSize; + leadPercent = leading / $._textSize; + leadingSet = true; + }; + $.textAlign = (horiz, vert) => { + $._textAlign = horiz; + if (vert) $._textBaseline = vert; + }; + + $._charStack = []; + $._textStack = []; + + let measureText = (font, text, charCallback) => { + let maxWidth = 0, + offsetX = 0, + offsetY = 0, + line = 0, + printedCharCount = 0, + lineWidths = [], + nextCharCode = text.charCodeAt(0); + + for (let i = 0; i < text.length; ++i) { + let charCode = nextCharCode; + nextCharCode = i < text.length - 1 ? text.charCodeAt(i + 1) : -1; + switch (charCode) { + case 10: // Newline + lineWidths.push(offsetX); + line++; + maxWidth = Math.max(maxWidth, offsetX); + offsetX = 0; + offsetY -= font.lineHeight * leadPercent; + break; + case 13: // CR + break; + case 32: // Space + // advance the offset without actually adding a character + offsetX += font.getXAdvance(charCode); + break; + case 9: // Tab + offsetX += font.getXAdvance(charCode) * 2; + break; + default: + if (charCallback) { + charCallback(offsetX, offsetY, line, font.getChar(charCode)); + } + offsetX += font.getXAdvance(charCode, nextCharCode); + printedCharCount++; + } + } + lineWidths.push(offsetX); + maxWidth = Math.max(maxWidth, offsetX); + return { + width: maxWidth, + height: lineWidths.length * font.lineHeight * leadPercent, + lineWidths, + printedCharCount + }; + }; - $.textFill = (r, g, b, a) => t.fill($.color(r, g, b, a)); - $.textStroke = (r, g, b, a) => t.stroke($.color(r, g, b, a)); + let initLoadDefaultFont; $.text = (str, x, y, w, h) => { - let img = t.createTextImage(str, w, h); + if (!$._font) { + // check if online and loading the default font hasn't been attempted yet + if (navigator.onLine && !initLoadDefaultFont) { + initLoadDefaultFont = true; + $.loadFont('https://q5js.org/fonts/YaHei-msdf.json'); + } + return; + } - if (img.canvas.textureIndex === undefined) { + if (str.length > w) { + let wrapped = []; + let i = 0; + while (i < str.length) { + let max = i + w; + if (max >= str.length) { + wrapped.push(str.slice(i)); + break; + } + let end = str.lastIndexOf(' ', max); + if (end == -1 || end < i) end = max; + wrapped.push(str.slice(i, end)); + i = end + 1; + } + str = wrapped.join('\n'); + } + + let spaces = 0, // whitespace char count, not literal spaces + hasNewline; + for (let i = 0; i < str.length; i++) { + let c = str[i]; + switch (c) { + case '\n': + hasNewline = true; + case '\r': + case '\t': + case ' ': + spaces++; + } + } + + let charsData = new Float32Array((str.length - spaces) * 4); + + let ta = $._textAlign, + tb = $._textBaseline, + textIndex = $._textStack.length, + o = 0, // offset + measurements; + + if (ta == 'left' && !hasNewline) { + measurements = measureText($._font, str, (textX, textY, line, char) => { + charsData[o] = textX; + charsData[o + 1] = textY; + charsData[o + 2] = char.charIndex; + charsData[o + 3] = textIndex; + o += 4; + }); + + if (tb == 'alphabetic') y -= $._textSize; + else if (tb == 'center') y -= $._textSize * 0.5; + else if (tb == 'bottom') y -= leading; + } else { + // measure the text to get the line widths before setting + // the x position to properly align the text + measurements = measureText($._font, str); + + let offsetY = 0; + if (tb == 'alphabetic') y -= $._textSize; + else if (tb == 'center') offsetY = measurements.height * 0.5; + else if (tb == 'bottom') offsetY = measurements.height; + + measureText($._font, str, (textX, textY, line, char) => { + let offsetX = 0; + if (ta == 'center') { + offsetX = measurements.width * -0.5 - (measurements.width - measurements.lineWidths[line]) * -0.5; + } else if (ta == 'right') { + offsetX = measurements.width - measurements.lineWidths[line]; + } + charsData[o] = textX + offsetX; + charsData[o + 1] = textY + offsetY; + charsData[o + 2] = char.charIndex; + charsData[o + 3] = textIndex; + o += 4; + }); + } + $._charStack.push(charsData); + + let text = new Float32Array(6); + + if ($._matrixDirty) $._saveMatrix(); + + text[0] = x; + text[1] = -y; + text[2] = $._textSize / 44; + text[3] = $._transformIndex; + text[4] = $._fillIndex; + text[5] = $._strokeIndex; + + $._textStack.push(text); + $.drawStack.push(2, measurements.printedCharCount); + }; + + $.textWidth = (str) => { + if (!$._font) return 0; + return measureText($._font, str).width; + }; + + $.createTextImage = (str, w, h) => { + g.textSize($._textSize); + + if ($._doFill) { + let fi = $._fillIndex * 4; + g.fill(colorsStack.slice(fi, fi + 4)); + } + if ($._doStroke) { + let si = $._strokeIndex * 4; + g.stroke(colorsStack.slice(si, si + 4)); + } + + let img = g.createTextImage(str, w, h); + + if (img.canvas.textureIndex == undefined) { $._createTexture(img); } else if (img.modified) { let cnv = img.canvas; @@ -4120,27 +4641,92 @@ Q5.renderers.webgpu.text = ($, q) => { ); img.modified = false; } - - $.textImage(img, x, y); + return img; }; - $.createTextImage = t.createTextImage; - $.textImage = (img, x, y) => { let og = $._imageMode; $._imageMode = 'corner'; - let ta = t._textAlign; + let ta = $._textAlign; if (ta == 'center') x -= img.canvas.hw; else if (ta == 'right') x -= img.width; - let bl = t._textBaseline; - if (bl == 'alphabetic') y -= t._textLeading; - else if (bl == 'middle') y -= img._middle; + let bl = $._textBaseline; + if (bl == 'alphabetic') y -= img._leading; + else if (bl == 'center') y -= img._middle; else if (bl == 'bottom') y -= img._bottom; else if (bl == 'top') y -= img._top; $.image(img, x, y); $._imageMode = og; }; + + $._hooks.preRender.push(() => { + if (!$._charStack.length) return; + + // Calculate total buffer size for text data + let totalTextSize = 0; + for (let charsData of $._charStack) { + totalTextSize += charsData.length * 4; + } + + // Create a single buffer for all text data + let charBuffer = Q5.device.createBuffer({ + label: 'charBuffer', + size: totalTextSize, + usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST, + mappedAtCreation: true + }); + + // Copy all text data into the buffer + let textArray = new Float32Array(charBuffer.getMappedRange()); + let o = 0; + for (let array of $._charStack) { + textArray.set(array, o); + o += array.length; + } + charBuffer.unmap(); + + // Calculate total buffer size for metadata + let totalMetadataSize = $._textStack.length * 6 * 4; + + // Create a single buffer for all metadata + let textBuffer = Q5.device.createBuffer({ + label: 'textBuffer', + size: totalMetadataSize, + usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST, + mappedAtCreation: true + }); + + // Copy all metadata into the buffer + let metadataArray = new Float32Array(textBuffer.getMappedRange()); + o = 0; + for (let array of $._textStack) { + metadataArray.set(array, o); + o += array.length; + } + textBuffer.unmap(); + + // Create a single bind group for the text buffer and metadata buffer + $._textBindGroup = Q5.device.createBindGroup({ + label: 'msdf text bind group', + layout: textBindGroupLayout, + entries: [ + { + binding: 0, + resource: { buffer: charBuffer } + }, + { + binding: 1, + resource: { buffer: textBuffer } + } + ] + }); + }); + + $._hooks.postRender.push(() => { + $._charStack.length = 0; + $._textStack.length = 0; + }); }; diff --git a/q5.min.js b/q5.min.js index 68c33fd..60de0e2 100644 --- a/q5.min.js +++ b/q5.min.js @@ -5,4 +5,4 @@ * @license LGPL-3.0 * @class Q5 */ -function Q5(e,t,r){let a,o=this;if(o._q5=!0,o._parent=t,o._renderer=r||"q2d",o._preloadCount=0,e??="global","auto"==e){if(!window.setup&&!window.draw)return;e="global"}o._scope=e,"global"==e&&(Q5._hasGlobal=o._isGlobal=!0,a=Q5._nodejs?global:window);let n=new Proxy(o,{set:(e,t,r)=>(o[t]=r,o._isGlobal&&(a[t]=r),!0)});o.canvas=o.ctx=o.drawingContext=null,o.pixels=[];let i=null;o.frameCount=0,o.deltaTime=16,o._targetFrameRate=0,o._targetFrameDuration=16.666666666666668,o._frameRate=o._fps=60,o._loop=!0,o._hooks={postCanvas:[],preRender:[],postRender:[]};let s=0;o.millis=()=>performance.now()-s,o.noCanvas=()=>{o.canvas?.remove&&o.canvas.remove(),o.canvas=0,n.ctx=n.drawingContext=0},window&&(o.windowWidth=window.innerWidth,o.windowHeight=window.innerHeight,o.deviceOrientation=window.screen?.orientation?.type),o._incrementPreload=()=>n._preloadCount++,o._decrementPreload=()=>n._preloadCount--,o._draw=e=>{let t=e||performance.now();if(o._lastFrameTime??=t-o._targetFrameDuration,o._shouldResize&&(o.windowResized(),o._shouldResize=!1),o._loop)i=d(o._draw);else if(o.frameCount&&!o._redraw)return;if(i&&o.frameCount){if(t-o._lastFrameTime{o._loop=!1,i=null},o.loop=()=>{o._loop=!0,null==i&&o._draw()},o.isLooping=()=>o._loop,o.redraw=(e=1)=>{o._redraw=!0;for(let t=0;t{o.noLoop(),o.canvas.remove()},o.frameRate=e=>(e&&(o._targetFrameRate=e,o._targetFrameDuration=1e3/e),o._frameRate),o.getTargetFrameRate=()=>o._targetFrameRate,o.getFPS=()=>o._fps,o.Element=function(e){this.elt=e},o._elements=[],o.TWO_PI=o.TAU=2*Math.PI,o.log=o.print=console.log,o.describe=()=>{};for(let e in Q5.modules)Q5.modules[e](o,n);let l=Q5.renderers[o._renderer];for(let e in l)l[e](o,n);for(let e in Q5)"_"!=e[1]&&e[1]==e[1].toUpperCase()&&(o[e]=Q5[e]);if("graphics"==e)return;"global"==e&&(Object.assign(Q5,o),delete Q5.Q5);for(let e of Q5.methods.init)e.call(o);for(let[e,t]of Object.entries(Q5.prototype))"_"!=e[0]&&"function"==typeof o[e]&&(o[e]=t.bind(o));if("global"==e){let e=Object.getOwnPropertyNames(o);for(let t of e)"_"!=t[0]&&(a[t]=o[t])}"function"==typeof e&&e(o),Q5._instanceCount++;let d=window.requestAnimationFrame||function(e){const t=o._lastFrameTime+o._targetFrameDuration;return setTimeout((()=>{e(t)}),t-performance.now())},c=a||o;o._isTouchAware=c.touchStarted||c.touchMoved||c.mouseReleased;let h=c.preload,u=["setup","draw","preload","mouseMoved","mousePressed","mouseReleased","mouseDragged","mouseClicked","keyPressed","keyReleased","keyTyped","touchStarted","touchMoved","touchEnded","windowResized"];for(let e of u)c[e]?o._isGlobal&&(o[e]=()=>{try{return c[e]()}catch(e){throw o._aiErrorAssistance&&o._aiErrorAssistance(e),e}}):o[e]=()=>{};async function p(){if(o._startDone=!0,o._preloadCount>0)return d(p);s=performance.now(),await o.setup(),o.frameCount||(null===o.ctx&&o.createCanvas(100,100),o._setupDone=!0,o.ctx&&o.resetMatrix(),d(o._draw))}(o.setup||o.draw)&&(arguments.length&&"namespace"!=e&&"webgpu"!=r||h?(o.preload(),p()):(c.preload=o.preload=()=>{o._startDone||p()},setTimeout(o.preload,32)))}Q5.renderers={},Q5.modules={},Q5._nodejs="object"==typeof process,Q5._instanceCount=0,Q5._friendlyError=(e,t)=>{throw Error(t+": "+e)},Q5._validateParameters=()=>!0,Q5.methods={init:[],pre:[],post:[],remove:[]},Q5.prototype.registerMethod=(e,t)=>Q5.methods[e].push(t),Q5.prototype.registerPreloadMethod=(e,t)=>Q5.prototype[e]=t[e],Q5._nodejs?global.p5??=global.Q5=Q5:"object"==typeof window?window.p5??=window.Q5=Q5:global.window=0,"object"==typeof document&&document.addEventListener("DOMContentLoaded",(()=>{Q5._hasGlobal||new Q5("auto")})),Q5.modules.canvas=(e,t)=>{e.CENTER="center",e.LEFT="left",e.RIGHT="right",e.TOP="top",e.BOTTOM="bottom",e.BASELINE="alphabetic",e.NORMAL="normal",e.ITALIC="italic",e.BOLD="bold",e.BOLDITALIC="italic bold",e.ROUND="round",e.SQUARE="butt",e.PROJECT="square",e.MITER="miter",e.BEVEL="bevel",e.CHORD=0,e.PIE=1,e.OPEN=2,e.RADIUS="radius",e.CORNER="corner",e.CORNERS="corners",e.CLOSE=1,e.LANDSCAPE="landscape",e.PORTRAIT="portrait",e.BLEND="source-over",e.REMOVE="destination-out",e.ADD="lighter",e.DARKEST="darken",e.LIGHTEST="lighten",e.DIFFERENCE="difference",e.SUBTRACT="subtract",e.EXCLUSION="exclusion",e.MULTIPLY="multiply",e.SCREEN="screen",e.REPLACE="copy",e.OVERLAY="overlay",e.HARD_LIGHT="hard-light",e.SOFT_LIGHT="soft-light",e.DODGE="color-dodge",e.BURN="color-burn",e.P2D="2d",e.WEBGL="webgl",e._OffscreenCanvas=window.OffscreenCanvas||function(){return document.createElement("canvas")},Q5._nodejs?Q5._createNodeJSCanvas&&(t.canvas=Q5._createNodeJSCanvas(100,100)):"image"!=e._scope&&"graphics"!=e._scope||(t.canvas=new e._OffscreenCanvas(100,100)),e.canvas||("object"==typeof document?(t.canvas=document.createElement("canvas"),e.canvas.id="q5Canvas"+Q5._instanceCount,e.canvas.classList.add("q5Canvas")):e.noCanvas());let r=e.canvas;if(r.width=e.width=100,r.height=e.height=100,e._pixelDensity=1,e.displayDensity=()=>window.devicePixelRatio||1,"image"!=e._scope&&(r.renderer=e._renderer,r[e._renderer]=!0,e._pixelDensity=Math.ceil(e.displayDensity())),e._adjustDisplay=()=>{r.style&&(r.style.width=r.w+"px",r.style.height=r.h+"px")},e.createCanvas=function(t,a,o){o??=arguments[3];let n=Object.assign({},Q5.canvasOptions);"object"==typeof o&&Object.assign(n,o),"image"!=e._scope&&("graphics"==e._scope?e._pixelDensity=this._pixelDensity:window.IntersectionObserver&&new IntersectionObserver((e=>{r.visible=e[0].isIntersecting})).observe(r)),e._setCanvasSize(t,a),Object.assign(r,n);let i=e._createCanvas(r.w,r.h,n);if(e._hooks)for(let t of e._hooks.postCanvas)t();return i},e.createGraphics=function(t,r,a){let o=new Q5("graphics");return a??={},a.alpha??=!0,a.colorSpace??=e.canvas.colorSpace,o.createCanvas.call(e,t,r,a),o},e._save=async(e,t,r)=>{if(t=t||"untitled","jpg"==(r=r||"png")||"png"==r||"webp"==r)if(e instanceof OffscreenCanvas){const t=await e.convertToBlob({type:"image/"+r});e=await new Promise((e=>{const r=new FileReader;r.onloadend=()=>e(r.result),r.readAsDataURL(t)}))}else e=e.toDataURL("image/"+r);else{let t="text/plain";"json"==r&&("string"!=typeof e&&(e=JSON.stringify(e)),t="text/json"),e=new Blob([e],{type:t}),e=URL.createObjectURL(e)}let a=document.createElement("a");a.href=e,a.download=t+"."+r,a.click(),URL.revokeObjectURL(a.href)},e.save=(t,r,a)=>{if((!t||"string"==typeof t&&(!r||!a&&r.length<5))&&(a=r,r=t,t=e.canvas),a)return e._save(t,r,a);r?(r=r.split("."),e._save(t,r[0],r.at(-1))):e._save(t)},e._setCanvasSize=(a,o)=>{a??=window.innerWidth,o??=window.innerHeight,r.w=a=Math.ceil(a),r.h=o=Math.ceil(o),r.hw=a/2,r.hh=o/2,r.width=Math.ceil(a*e._pixelDensity),r.height=Math.ceil(o*e._pixelDensity),e._da?e.flexibleCanvas(e._dau):(t.width=a,t.height=o),e.displayMode&&!r.displayMode?e.displayMode():e._adjustDisplay()},"image"!=e._scope){if(r&&"graphics"!=e._scope){function a(){let t=e._parent;t??=document.getElementsByTagName("main")[0],t||(t=document.createElement("main"),document.body.append(t)),r.parent(t)}r.parent=t=>{function a(){e.frameCount>1&&(e._shouldResize=!0,e._adjustDisplay())}r.parentElement&&r.parentElement.removeChild(r),"string"==typeof t&&(t=document.getElementById(t)),t.append(r),"function"==typeof ResizeObserver?(e._ro&&e._ro.disconnect(),e._ro=new ResizeObserver(a),e._ro.observe(t)):e.frameCount||window.addEventListener("resize",a)},document.body?a():document.addEventListener("DOMContentLoaded",a)}e.resizeCanvas=(t,a)=>{if(!e.ctx)return e.createCanvas(t,a);t==r.w&&a==r.h||e._resizeCanvas(t,a)},e.canvas.resize=e.resizeCanvas,e.canvas.save=e.saveCanvas=e.save,e.pixelDensity=t=>t&&t!=e._pixelDensity?(e._pixelDensity=t,e._setCanvasSize(r.w,r.h),t):e._pixelDensity,e.flexibleCanvas=(a=400)=>{a?(e._da=r.width/(a*e._pixelDensity),t.width=e._dau=a,t.height=r.h/r.w*a):e._da=0},e._styleNames=["_doStroke","_doFill","_strokeSet","_fillSet","_tint","_imageMode","_rectMode","_ellipseMode","_textFont","_textLeading","_leadingSet","_textSize","_textAlign","_textBaseline","_textStyle","_textWrap"],e._styles=[],e.pushStyles=()=>{let t={};for(let r of e._styleNames)t[r]=e[r];e._styles.push(t)},e.popStyles=()=>{let t=e._styles.pop();for(let r of e._styleNames)e[r]=t[r]}}},Q5.canvasOptions={alpha:!1,colorSpace:"display-p3"},window.matchMedia&&matchMedia("(dynamic-range: high) and (color-gamut: p3)").matches?Q5.supportsHDR=!0:Q5.canvasOptions.colorSpace="srgb",Q5.renderers.q2d={},Q5.renderers.q2d.canvas=(e,t)=>{let r=e.canvas;e.colorMode&&e.colorMode("rgb","integer"),e._createCanvas=function(a,o,n){return t.ctx=t.drawingContext=r.getContext("2d",n),"image"!=e._scope&&(e.ctx.fillStyle="white",e.ctx.strokeStyle="black",e.ctx.lineCap="round",e.ctx.lineJoin="miter",e.ctx.textAlign="left"),e.ctx.scale(e._pixelDensity,e._pixelDensity),e.ctx.save(),r},e.clear=()=>{e.ctx.save(),e.ctx.resetTransform(),e.ctx.clearRect(0,0,e.canvas.width,e.canvas.height),e.ctx.restore()},"image"!=e._scope&&(e._resizeCanvas=(t,a)=>{let o,n={};for(let t in e.ctx)"function"!=typeof e.ctx[t]&&(n[t]=e.ctx[t]);if(delete n.canvas,e.frameCount>1){o=new e._OffscreenCanvas(r.width,r.height),o.w=r.w,o.h=r.h,o.getContext("2d").drawImage(r,0,0)}e._setCanvasSize(t,a);for(let t in n)e.ctx[t]=n[t];e.scale(e._pixelDensity),o&&e.ctx.drawImage(o,0,0,o.w,o.h)},e.fill=function(t){if(e._doFill=!0,e._fillSet=!0,Q5.Color&&(t._q5Color||("string"!=typeof t?t=e.color(...arguments):e._namedColors[t]&&(t=e.color(...e._namedColors[t]))),t.a<=0))return e._doFill=!1;e.ctx.fillStyle=e._fill=t.toString()},e.noFill=()=>e._doFill=!1,e.stroke=function(t){if(e._doStroke=!0,e._strokeSet=!0,Q5.Color&&(t._q5Color||("string"!=typeof t?t=e.color(...arguments):e._namedColors[t]&&(t=e.color(...e._namedColors[t]))),t.a<=0))return e._doStroke=!1;e.ctx.strokeStyle=e._stroke=t.toString()},e.strokeWeight=t=>{t||(e._doStroke=!1),e._da&&(t*=e._da),e.ctx.lineWidth=e._strokeWeight=t||1e-4},e.noStroke=()=>e._doStroke=!1,e.opacity=t=>e.ctx.globalAlpha=t,e.translate=(t,r)=>{e._da&&(t*=e._da,r*=e._da),e.ctx.translate(t,r)},e.rotate=t=>{e._angleMode&&(t=e.radians(t)),e.ctx.rotate(t)},e.scale=(t,r)=>{r??=t,e.ctx.scale(t,r)},e.applyMatrix=(t,r,a,o,n,i)=>e.ctx.transform(t,r,a,o,n,i),e.shearX=t=>e.ctx.transform(1,0,e.tan(t),1,0,0),e.shearY=t=>e.ctx.transform(1,e.tan(t),0,1,0,0),e.resetMatrix=()=>{e.ctx.resetTransform(),e.scale(e._pixelDensity)},e.pushMatrix=()=>e.ctx.save(),e.popMatrix=()=>e.ctx.restore(),e.push=()=>{e.ctx.save(),e.pushStyles()},e.pop=()=>{e.ctx.restore(),e.popStyles()},e.createCapture=e=>{var t=document.createElement("video");return t.playsinline="playsinline",t.autoplay="autoplay",navigator.mediaDevices.getUserMedia(e).then((e=>{t.srcObject=e})),t.style.position="absolute",t.style.opacity=1e-5,t.style.zIndex=-1e3,document.body.append(t),t},window&&"graphics"!=e._scope&&window.addEventListener("resize",(()=>{e._shouldResize=!0,t.windowWidth=window.innerWidth,t.windowHeight=window.innerHeight,t.deviceOrientation=window.screen?.orientation?.type})))},Q5.renderers.q2d.drawing=e=>{e._doStroke=!0,e._doFill=!0,e._strokeSet=!1,e._fillSet=!1,e._ellipseMode=e.CENTER,e._rectMode=e.CORNER,e._curveDetail=20,e._curveAlpha=0;let t=!0,r=[];function a(){e._doFill&&e.ctx.fill(),e._doStroke&&e.ctx.stroke()}function o(t,r,o,n,i,s,l,d){if(!e._doFill&&!e._doStroke)return;let c=e._angleMode,h=c?360:e.TAU;if((i%=h)<0&&(i+=h),(s%=h)<0&&(s+=h),0!=i||0!=s){if(i>s&&([i,s]=[s,i]),e.ctx.beginPath(),o==n)c&&(i=e.radians(i),s=e.radians(s)),e.ctx.arc(t,r,o/2,i,s);else{for(let a=0;ae.ctx.globalCompositeOperation=t,e.strokeCap=t=>e.ctx.lineCap=t,e.strokeJoin=t=>e.ctx.lineJoin=t,e.ellipseMode=t=>e._ellipseMode=t,e.rectMode=t=>e._rectMode=t,e.curveDetail=t=>e._curveDetail=t,e.curveAlpha=t=>e._curveAlpha=t,e.curveTightness=t=>e._curveAlpha=t,e.background=function(t){e.ctx.save(),e.ctx.resetTransform(),t.canvas?e.image(t,0,0,e.width,e.height):(Q5.Color&&!t._q5Color&&("string"!=typeof t?t=e.color(...arguments):e._namedColors[t]&&(t=e.color(...e._namedColors[t]))),e.ctx.fillStyle=t.toString(),e.ctx.fillRect(0,0,e.canvas.width,e.canvas.height)),e.ctx.restore()},e.line=(t,r,a,o)=>{e._doStroke&&(e._da&&(t*=e._da,r*=e._da,a*=e._da,o*=e._da),e.ctx.beginPath(),e.ctx.moveTo(t,r),e.ctx.lineTo(a,o),e.ctx.stroke())},e.arc=(t,r,a,n,i,s,l,d=25)=>{if(i==s)return e.ellipse(t,r,a,n);l??=e.PIE,e._ellipseMode==e.CENTER?o(t,r,a,n,i,s,l,d):e._ellipseMode==e.RADIUS?o(t,r,2*a,2*n,i,s,l,d):e._ellipseMode==e.CORNER?o(t+a/2,r+n/2,a,n,i,s,l,d):e._ellipseMode==e.CORNERS&&o((t+a)/2,(r+n)/2,a-t,n-r,i,s,l,d)},e.ellipse=(t,r,a,o)=>{o??=a,e._ellipseMode==e.CENTER?n(t,r,a,o):e._ellipseMode==e.RADIUS?n(t,r,2*a,2*o):e._ellipseMode==e.CORNER?n(t+a/2,r+o/2,a,o):e._ellipseMode==e.CORNERS&&n((t+a)/2,(r+o)/2,a-t,o-r)},e.circle=(t,r,o)=>{e._ellipseMode==e.CENTER?(e._da&&(t*=e._da,r*=e._da,o*=e._da),e.ctx.beginPath(),e.ctx.arc(t,r,o/2,0,e.TAU),a()):e.ellipse(t,r,o,o)},e.point=(t,r)=>{t.x&&(r=t.y,t=t.x),e._da&&(t*=e._da,r*=e._da),e.ctx.save(),e.ctx.beginPath(),e.ctx.arc(t,r,e.ctx.lineWidth/2,0,e.TAU),e.ctx.fillStyle=e.ctx.strokeStyle,e.ctx.fill(),e.ctx.restore()},e.rect=(t,r,a,o=a,n,s,l,d)=>{e._rectMode==e.CENTER?i(t-a/2,r-o/2,a,o,n,s,l,d):e._rectMode==e.RADIUS?i(t-a,r-o,2*a,2*o,n,s,l,d):e._rectMode==e.CORNER?i(t,r,a,o,n,s,l,d):e._rectMode==e.CORNERS&&i(t,r,a-t,o-r,n,s,l,d)},e.square=(t,r,a,o,n,i,s)=>e.rect(t,r,a,a,o,n,i,s),e.beginShape=()=>{r=[],e.ctx.beginPath(),t=!0},e.beginContour=()=>{e.ctx.closePath(),r=[],t=!0},e.endContour=()=>{r=[],t=!0},e.vertex=(a,o)=>{e._da&&(a*=e._da,o*=e._da),r=[],t?e.ctx.moveTo(a,o):e.ctx.lineTo(a,o),t=!1},e.bezierVertex=(t,a,o,n,i,s)=>{e._da&&(t*=e._da,a*=e._da,o*=e._da,n*=e._da,i*=e._da,s*=e._da),r=[],e.ctx.bezierCurveTo(t,a,o,n,i,s)},e.quadraticVertex=(t,a,o,n)=>{e._da&&(t*=e._da,a*=e._da,o*=e._da,n*=e._da),r=[],e.ctx.quadraticCurveTo(t,a,o,n)},e.bezier=(t,r,a,o,n,i,s,l)=>{e.beginShape(),e.vertex(t,r),e.bezierVertex(a,o,n,i,s,l),e.endShape()},e.triangle=(t,r,a,o,n,i)=>{e.beginShape(),e.vertex(t,r),e.vertex(a,o),e.vertex(n,i),e.endShape(e.CLOSE)},e.quad=(t,r,a,o,n,i,s,l)=>{e.beginShape(),e.vertex(t,r),e.vertex(a,o),e.vertex(n,i),e.vertex(s,l),e.endShape(e.CLOSE)},e.endShape=t=>{r=[],t&&e.ctx.closePath(),a()},e.curveVertex=(a,o)=>{if(e._da&&(a*=e._da,o*=e._da),r.push([a,o]),r.length<4)return;let n=function(e,t,r,a,o,n,i,s,l,d){function c(e,t,r,a,o,n){let i=Math.pow(a-t,2)+Math.pow(o-r,2);return Math.pow(i,.5*n)+e}let h=[],u=c(0,e,t,r,a,d),p=c(u,r,a,o,n,d),_=c(p,o,n,i,s,d);for(let d=0;d0?(m[e]=1,m[e+1]=0):(m[e]=0,m[e+1]=1));let g=e*m[0]+r*m[1],x=t*m[0]+a*m[1],f=r*m[2]+o*m[3],v=a*m[2]+n*m[3],y=o*m[4]+i*m[5],w=n*m[4]+s*m[5],b=g*m[6]+f*m[7],M=x*m[6]+v*m[7],S=f*m[8]+y*m[9],C=v*m[8]+w*m[9],Q=b*m[2]+S*m[3],R=M*m[2]+C*m[3];h.push([Q,R])}return h}(...r.at(-4),...r.at(-3),...r.at(-2),...r.at(-1),e._curveDetail,e._curveAlpha);for(let r=0;r{e.beginShape(),e.curveVertex(t,r),e.curveVertex(a,o),e.curveVertex(n,i),e.curveVertex(s,l),e.endShape()},e.curvePoint=(e,t,r,a,o)=>{const n=o*o*o,i=o*o;return e*(-.5*n+i-.5*o)+t*(1.5*n-2.5*i+1)+r*(-1.5*n+2*i+.5*o)+a*(.5*n-.5*i)},e.bezierPoint=(e,t,r,a,o)=>{const n=1-o;return Math.pow(n,3)*e+3*Math.pow(n,2)*o*t+3*n*Math.pow(o,2)*r+Math.pow(o,3)*a},e.curveTangent=(e,t,r,a,o)=>{const n=o*o;return e*(-3*n/2+2*o-.5)+t*(9*n/2-5*o)+r*(-9*n/2+4*o+.5)+a*(3*n/2-o)},e.bezierTangent=(e,t,r,a,o)=>{const n=1-o;return 3*a*Math.pow(o,2)-3*r*Math.pow(o,2)+6*r*n*o-6*t*n*o+3*t*Math.pow(n,2)-3*e*Math.pow(n,2)},e.erase=function(t=255,r=255){e.ctx.save(),e.ctx.globalCompositeOperation="destination-out",e.ctx.fillStyle=`rgba(0, 0, 0, ${t/255})`,e.ctx.strokeStyle=`rgba(0, 0, 0, ${r/255})`},e.noErase=function(){e.ctx.globalCompositeOperation="source-over",e.ctx.restore()},e.inFill=(t,r)=>{const a=e._pixelDensity;return e.ctx.isPointInPath(t*a,r*a)},e.inStroke=(t,r)=>{const a=e._pixelDensity;return e.ctx.isPointInStroke(t*a,r*a)}},Q5.renderers.q2d.image=(e,t)=>{Q5.Image??=class{constructor(e,t,r){let a=this;a._scope="image",a.canvas=a.ctx=a.drawingContext=null,a.pixels=[],Q5.modules.canvas(a,a);let o=Q5.renderers.q2d;for(let e of["canvas","image","soft_filters"])o[e]&&o[e](a,a);a._pixelDensity=r.pixelDensity||1,a.createCanvas(e,t,r),delete a.createCanvas,a._loop=!1}get w(){return this.width}get h(){return this.height}},e.createImage=(t,r,a)=>(a??={},a.alpha??=!0,a.colorSpace??=e.canvas.colorSpace||Q5.canvasOptions.colorSpace,new Q5.Image(t,r,a)),e.loadImage=function(r,a,o){if(r.canvas)return r;if("gif"==r.slice(-3).toLowerCase())throw new Error("q5 doesn't support GIFs due to their impact on performance. Use a video or animation instead.");t._preloadCount++;let n=[...arguments].at(-1);o="object"==typeof n?n:null;let i=e.createImage(1,1,o);function s(e){i.resize(e.naturalWidth||e.width,e.naturalHeight||e.height),i.ctx.drawImage(e,0,0),t._preloadCount--,a&&a(i)}if(Q5._nodejs&&global.CairoCanvas)global.CairoCanvas.loadImage(r).then(s).catch((e=>{throw t._preloadCount--,e}));else{let e=new window.Image;e.src=r,e.crossOrigin="Anonymous",e._pixelDensity=1,e.onload=()=>s(e),e.onerror=e=>{throw t._preloadCount--,e}}return i},e.imageMode=t=>e._imageMode=t,e.image=(t,r,a,o,n,i=0,s=0,l,d)=>{let c=t.canvas||t;Q5._createNodeJSCanvas&&(c=c.context.canvas),o??=t.width||t.videoWidth,n??=t.height||t.videoHeight,"center"==e._imageMode&&(r-=.5*o,a-=.5*n),e._da&&(r*=e._da,a*=e._da,o*=e._da,n*=e._da,i*=e._da,s*=e._da,l*=e._da,d*=e._da);let h=t._pixelDensity||1;l?l*=h:l=c.width||c.videoWidth,d?d*=h:d=c.height||c.videoHeight,e.ctx.drawImage(c,i*h,s*h,l,d,r,a,o,n),e._tint&&(e.ctx.globalCompositeOperation="multiply",e.ctx.fillStyle=e._tint.toString(),e.ctx.fillRect(r,a,o,n),e.ctx.globalCompositeOperation="source-over")},e._tint=null;let r=null;e._softFilter=()=>{throw new Error("Load q5-2d-soft-filters.js to use software filters.")},e.filter=(t,r)=>{if(!e.ctx.filter)return e._softFilter(t,r);if("string"==typeof t)f=t;else if(t==Q5.GRAY)f="saturate(0%)";else if(t==Q5.INVERT)f="invert(100%)";else if(t==Q5.BLUR){let t=Math.ceil(r*e._pixelDensity)||1;f=`blur(${t}px)`}else{if(t!=Q5.THRESHOLD)return e._softFilter(t,r);{r??=.5;let e=Math.floor(.5/Math.max(r,1e-5)*100);f=`saturate(0%) brightness(${e}%) contrast(1000000%)`}}e.ctx.filter=f,e.ctx.drawImage(e.canvas,0,0,e.canvas.w,e.canvas.h),e.ctx.filter="none"},"image"==e._scope&&(e.resize=(t,r)=>{let a=new e._OffscreenCanvas(e.canvas.width,e.canvas.height);a.getContext("2d",{colorSpace:e.canvas.colorSpace}).drawImage(e.canvas,0,0),e._setCanvasSize(t,r),e.ctx.clearRect(0,0,e.canvas.width,e.canvas.height),e.ctx.drawImage(a,0,0,e.canvas.width,e.canvas.height)}),e._getImageData=(t,r,a,o)=>e.ctx.getImageData(t,r,a,o,{colorSpace:e.canvas.colorSpace}),e.trim=()=>{let t=e._pixelDensity||1,r=e.canvas.width,a=e.canvas.height,o=e._getImageData(0,0,r,a).data,n=r,i=0,s=a,l=0,d=3;for(let e=0;ei&&(i=t),el&&(l=e)),d+=4;return s=Math.floor(s/t),l=Math.floor(l/t),n=Math.floor(n/t),i=Math.floor(i/t),e.get(n,s,i-n+1,l-s+1)},e.mask=t=>{e.ctx.save(),e.ctx.resetTransform();let r=e.ctx.globalCompositeOperation;e.ctx.globalCompositeOperation="destination-in",e.ctx.drawImage(t.canvas,0,0),e.ctx.globalCompositeOperation=r,e.ctx.restore()},e.get=(t,r,a,o)=>{let n=e._pixelDensity||1;if(void 0!==t&&void 0===a){let a=e._getImageData(t*n,r*n,1,1).data;return new e.Color(a[0],a[1],a[2],a[3]/255)}t=(t||0)*n,r=(r||0)*n;let i=a=a||e.width,s=o=o||e.height;a*=n,o*=n;let l=e.createImage(a,o),d=e._getImageData(t,r,a,o);return l.ctx.putImageData(d,0,0),l._pixelDensity=n,l.width=i,l.height=s,l},e.set=(t,r,a)=>{if(a.canvas){let o=e._tint;return e._tint=null,e.image(a,t,r),void(e._tint=o)}e.pixels.length||e.loadPixels();let o=e._pixelDensity||1;for(let n=0;n{r=e._getImageData(0,0,e.canvas.width,e.canvas.height),t.pixels=r.data},e.updatePixels=()=>{null!=r&&e.ctx.putImageData(r,0,0)},e.smooth=()=>e.ctx.imageSmoothingEnabled=!0,e.noSmooth=()=>e.ctx.imageSmoothingEnabled=!1,"image"!=e._scope&&(e.tint=function(t){e._tint=t._q5Color?t:e.color(...arguments)},e.noTint=()=>e._tint=null)},Q5.THRESHOLD=1,Q5.GRAY=2,Q5.OPAQUE=3,Q5.INVERT=4,Q5.POSTERIZE=5,Q5.DILATE=6,Q5.ERODE=7,Q5.BLUR=8,Q5.renderers.q2d.text=(e,t)=>{e._textAlign="left",e._textBaseline="alphabetic";let r="sans-serif",a=12,o=15,n=3,i="normal",s=!1,l=0,d=[],c=!1,h=!1,u=0,p=12e3,_=e._textCache={};e.loadFont=(e,r)=>{t._preloadCount++;let a=e.split("/").pop().split(".")[0].replace(" ",""),o=new FontFace(a,`url(${e})`);return document.fonts.add(o),o.load().then((()=>{t._preloadCount--,r&&r(a)})),a},e.textFont=e=>{r=e,s=!0,l=-1},e.textSize=t=>{if(void 0===t)return a;e._da&&(t*=e._da),a=t,s=!0,l=-1,e._leadingSet||(o=1.25*t,n=o-t)},e.textStyle=e=>{i=e,s=!0,l=-1},e.textLeading=t=>{if(void 0===t)return o;e._da&&(t*=e._da),o=t,n=t-a,e._leadingSet=!0,l=-1},e.textAlign=(t,r)=>{e.ctx.textAlign=e._textAlign=t,r&&(e.ctx.textBaseline=e._textBaseline=r==e.CENTER?"middle":r),l=-1},e.textWidth=t=>e.ctx.measureText(t).width,e.textAscent=t=>e.ctx.measureText(t).actualBoundingBoxAscent,e.textDescent=t=>e.ctx.measureText(t).actualBoundingBoxDescent,e.textFill=e.fill,e.textStroke=e.stroke;e.textCache=(e,t)=>(t&&(p=t),void 0!==e&&(c=e),c),e.createTextImage=(t,r,a)=>(h=!0,img=e.text(t,0,0,r,a),h=!1,img);let m=[];e.text=(t,g,x,f,v)=>{if(void 0===t||!e._doFill&&!e._doStroke)return;t=t.toString(),e._da&&(g*=e._da,x*=e._da);let y,w,b,M,S=e.ctx;if(s&&(S.font=`${i} ${a}px ${r}`,s=!1),(c||h)&&(-1==l&&(()=>{let e=r+a+i+o,t=5381;for(let r=0;r>>0})(),y=_[t],y&&(y=y[l]),y)){if(y._fill==e._fill&&y._stroke==e._stroke&&y._strokeWeight==e._strokeWeight)return h?y:e.textImage(y,g,x);y.clear()}if(-1==t.indexOf("\n")?m[0]=t:m=t.split("\n"),f){let e=[];for(let t of m){let r=0;for(;r=t.length){e.push(t.slice(r));break}let o=t.lastIndexOf(" ",a);(-1===o||ov)break;if(m.length=0,e._fillSet||(S.fillStyle=M),c||h){if(d.push(l),(_[t]??={})[l]=y,u++,u>p){let e=Math.ceil(u/2),t=d.splice(0,e);for(let e in _){e=_[e];for(let r of t)delete e[r]}u-=e}if(h)return y;e.textImage(y,g,x)}},e.textImage=(t,r,a)=>{let n=e._imageMode;e._imageMode="corner";let i=e._textAlign;"center"==i?r-=t.canvas.hw:"right"==i&&(r-=t.width);let s=e._textBaseline;"alphabetic"==s?a-=o:"middle"==s?a-=t._middle:"bottom"==s?a-=t._bottom:"top"==s&&(a-=t._top),e.image(t,r,a),e._imageMode=n},e.nf=(e,t,r)=>{let a=e<0,o=(e=Math.abs(e)).toFixed(r).split(".");o[0]=o[0].padStart(t,"0");let n=o.join(".");return a&&(n="-"+n),n}},Q5.modules.ai=e=>{e.askAI=(e="")=>{throw Error("Ask AI ✨ "+e)},e._aiErrorAssistance=async t=>{let r=t.message?.includes("Ask AI ✨");if(Q5.disableFriendlyErrors&&!r)return;!r&&Q5.errorTolerant||e.noLoop();let a=t.stack?.split("\n");if(!t.stack||a.length<=1)return;let o=1,n="(";for(-1==navigator.userAgent.indexOf("Chrome")&&(o=0,n="@");a[o].indexOf("q5")>=0;)o++;let i=a[o].split(n).at(-1);i=i.split(":");let s=parseInt(i.at(-2));r&&s++;let l=i.slice(0,-2).join(":"),d=l.split("/").at(-1);try{let e=(await(await fetch(l)).text()).split("\n"),a=e[s-1].trim(),o="",n=1;for(;o.length<1600&&(s-n>=0&&(o=e[s-n].trim()+"\n"+o),s+n10?t.message.slice(10):"Whats+wrong+with+this+line%3F+short+answer")+(r?"":"%0A%0A"+encodeURIComponent(t.name+": "+t.message))+"%0A%0ALine%3A+"+encodeURIComponent(a)+"%0A%0AExcerpt+for+context%3A%0A%0A"+encodeURIComponent(o);if(console.warn("Error in "+d+" on line "+s+":\n\n"+a),console.warn("Ask AI ✨ "+i),r)return window.open(i,"_blank")}catch(e){}}},Q5.modules.color=(e,t)=>{e.RGB=e.RGBA=e._colorMode="rgb",e.OKLCH="oklch",e.colorMode=(r,a)=>{e._colorMode=r;let o="srgb"==e.canvas.colorSpace||"srgb"==r;if(a??=o?"integer":"float",e._colorFormat="float"==a||1==a?1:255,"oklch"==r)t.Color=Q5.ColorOKLCH;else{let r="srgb"==e.canvas.colorSpace;255==e._colorFormat?t.Color=r?Q5.ColorRGBA_8:Q5.ColorRGBA_P3_8:t.Color=r?Q5.ColorRGBA:Q5.ColorRGBA_P3,e._colorMode="rgb"}},e._namedColors={aqua:[0,255,255],black:[0,0,0],blue:[0,0,255],brown:[165,42,42],crimson:[220,20,60],cyan:[0,255,255],darkviolet:[148,0,211],gold:[255,215,0],green:[0,128,0],gray:[128,128,128],grey:[128,128,128],hotpink:[255,105,180],indigo:[75,0,130],khaki:[240,230,140],lightgreen:[144,238,144],lime:[0,255,0],magenta:[255,0,255],navy:[0,0,128],orange:[255,165,0],olive:[128,128,0],peachpuff:[255,218,185],pink:[255,192,203],purple:[128,0,128],red:[255,0,0],skyblue:[135,206,235],tan:[210,180,140],turquoise:[64,224,208],transparent:[0,0,0,0],white:[255,255,255],violet:[238,130,238],yellow:[255,255,0]},e.color=(t,r,a,o)=>{let n=e.Color;if(t._q5Color)return new n(...t.levels);if(null==r){if("string"==typeof t){if("#"==t[0])t.length<=5?(t.length>4&&(o=parseInt(t[4]+t[4],16)),a=parseInt(t[3]+t[3],16),r=parseInt(t[2]+t[2],16),t=parseInt(t[1]+t[1],16)):(t.length>7&&(o=parseInt(t.slice(7,9),16)),a=parseInt(t.slice(5,7),16),r=parseInt(t.slice(3,5),16),t=parseInt(t.slice(1,3),16));else{if(!e._namedColors[t])return console.error("q5 can't parse color: "+t+"\nOnly numeric input, hex, and common named colors are supported."),new n(0,0,0);[t,r,a,o]=e._namedColors[t]}1==e._colorFormat&&(t/=255,r&&(r/=255),a&&(a/=255),o&&(o/=255))}Array.isArray(t)&&([t,r,a,o]=t)}return null==a?new n(t,t,t,r):new n(t,r,a,o)},e.red=e=>e.r,e.green=e=>e.g,e.blue=e=>e.b,e.alpha=e=>e.a,e.lightness=e=>e.l?e.l:100*(.2126*e.r+.7152*e.g+.0722*e.b)/255,e.hue=t=>{if(t.h)return t.h;let r=t.r,a=t.g,o=t.b;255==e._colorFormat&&(r/=255,a/=255,o/=255);let n,i=Math.max(r,a,o),s=Math.min(r,a,o);return n=i==s?0:i==r?60*(a-o)/(i-s):i==a?60*(o-r)/(i-s)+120:60*(r-a)/(i-s)+240,n<0&&(n+=360),n},e.lerpColor=(t,r,a)=>{if("rgb"==e._colorMode)return new e.Color(e.constrain(e.lerp(t.r,r.r,a),0,255),e.constrain(e.lerp(t.g,r.g,a),0,255),e.constrain(e.lerp(t.b,r.b,a),0,255),e.constrain(e.lerp(t.a,r.a,a),0,255));{let o=r.h-t.h;o>180&&(o-=360),o<-180&&(o+=360);let n=t.h+a*o;return n<0&&(n+=360),n>360&&(n-=360),new e.Color(e.constrain(e.lerp(t.l,r.l,a),0,100),e.constrain(e.lerp(t.c,r.c,a),0,100),n,e.constrain(e.lerp(t.a,r.a,a),0,255))}}},Q5.Color=class{constructor(){this._q5Color=!0}},Q5.ColorOKLCH=class extends Q5.Color{constructor(e,t,r,a){super(),this.l=e,this.c=t,this.h=r,this.a=a??1}toString(){return`oklch(${this.l} ${this.c} ${this.h} / ${this.a})`}},Q5.ColorRGBA=class extends Q5.Color{constructor(e,t,r,a){super(),this.r=e,this.g=t,this.b=r,this.a=a??1}get levels(){return[this.r,this.g,this.b,this.a]}toString(){return`color(srgb ${this.r} ${this.g} ${this.b} / ${this.a})`}},Q5.ColorRGBA_P3=class extends Q5.ColorRGBA{toString(){return`color(display-p3 ${this.r} ${this.g} ${this.b} / ${this.a})`}},Q5.ColorRGBA_8=class extends Q5.ColorRGBA{constructor(e,t,r,a){super(e,t,r,a??255)}setRed(e){this.r=e}setGreen(e){this.g=e}setBlue(e){this.b=e}setAlpha(e){this.a=e}get levels(){return[this.r,this.g,this.b,this.a]}toString(){return`rgb(${this.r} ${this.g} ${this.b} / ${this.a/255})`}},Q5.ColorRGBA_P3_8=class extends Q5.ColorRGBA{constructor(e,t,r,a){super(e,t,r,a??255),this._edited=!0}get r(){return this._r}set r(e){this._r=e,this._edited=!0}get g(){return this._g}set g(e){this._g=e,this._edited=!0}get b(){return this._b}set b(e){this._b=e,this._edited=!0}get a(){return this._a}set a(e){this._a=e,this._edited=!0}toString(){if(this._edited){let e=(this._r/255).toFixed(3),t=(this._g/255).toFixed(3),r=(this._b/255).toFixed(3),a=(this._a/255).toFixed(3);this._css=`color(display-p3 ${e} ${t} ${r} / ${a})`,this._edited=!1}return this._css}},Q5.modules.display=e=>{if(!e.canvas||"graphics"==e._scope)return;let t=e.canvas;e.CENTERED="centered",e.FULLSCREEN="fullscreen",e.MAXED="maxed",e.PIXELATED="pixelated",0!=Q5._instanceCount||Q5._nodejs||document.head.insertAdjacentHTML("beforeend",""),e._adjustDisplay=()=>{let r=t.style,a=t.parentElement;r&&a&&t.displayMode&&("pixelated"==t.renderQuality&&(t.classList.add("q5-pixelated"),e.pixelDensity(1),e.noSmooth&&e.noSmooth(),e.textFont&&e.textFont("monospace")),"normal"==t.displayMode?(a.classList.remove("q5-centered","q5-maxed","q5-fullscreen"),r.width=t.w*t.displayScale+"px",r.height=t.h*t.displayScale+"px"):(a.classList.add("q5-"+t.displayMode),a=a.getBoundingClientRect(),t.w/t.h>a.width/a.height?("centered"==t.displayMode?(r.width=t.w*t.displayScale+"px",r.maxWidth="100%"):r.width="100%",r.height="auto",r.maxHeight=""):(r.width="auto",r.maxWidth="","centered"==t.displayMode?(r.height=t.h*t.displayScale+"px",r.maxHeight="100%"):r.height="100%")))},e.displayMode=(r="normal",a="default",o=1)=>{"string"==typeof o&&(o=parseFloat(o.slice(1))),Object.assign(t,{displayMode:r,renderQuality:a,displayScale:o}),e._adjustDisplay()},e.fullscreen=e=>{if(void 0===e)return document.fullscreenElement;e?document.body.requestFullscreen():document.body.exitFullscreen()}},Q5.modules.input=(e,t)=>{if("graphics"==e._scope)return;e.mouseX=0,e.mouseY=0,e.pmouseX=0,e.pmouseY=0,e.touches=[],e.mouseButton=null,e.keyIsPressed=!1,e.mouseIsPressed=!1,e.key=null,e.keyCode=null,e.UP_ARROW=38,e.DOWN_ARROW=40,e.LEFT_ARROW=37,e.RIGHT_ARROW=39,e.SHIFT=16,e.TAB=9,e.BACKSPACE=8,e.ENTER=e.RETURN=13,e.ALT=e.OPTION=18,e.CONTROL=17,e.DELETE=46,e.ESCAPE=27,e.ARROW="default",e.CROSS="crosshair",e.HAND="pointer",e.MOVE="move",e.TEXT="text";let r={},a=[e.LEFT,e.CENTER,e.RIGHT],o=e.canvas;function n(t){const r=e.canvas.getBoundingClientRect(),a=e.canvas.scrollWidth/e.width||1,o=e.canvas.scrollHeight/e.height||1;return{x:(t.clientX-r.left)/a,y:(t.clientY-r.top)/o,id:t.identifier}}if(e._startAudio=()=>{e.getAudioContext&&"suspended"==e.getAudioContext()?.state&&e.userStartAudio()},e._updateMouse=r=>{if(!r.changedTouches)if(o){let a=o.getBoundingClientRect(),n=o.scrollWidth/e.width||1,i=o.scrollHeight/e.height||1;t.mouseX=(r.clientX-a.left)/n,t.mouseY=(r.clientY-a.top)/i,"webgpu"==o.renderer&&(t.mouseX-=o.hw,t.mouseY-=o.hh)}else t.mouseX=r.clientX,t.mouseY=r.clientY},e._onmousedown=r=>{e._startAudio(),e._updateMouse(r),t.mouseIsPressed=!0,t.mouseButton=a[r.button],e.mousePressed(r)},e._onmousemove=t=>{e._updateMouse(t),e.mouseIsPressed?e.mouseDragged(t):e.mouseMoved(t)},e._onmouseup=r=>{e._updateMouse(r),t.mouseIsPressed=!1,e.mouseReleased(r)},e._onclick=r=>{e._updateMouse(r),t.mouseIsPressed=!0,e.mouseClicked(r),t.mouseIsPressed=!1},e.cursor=(t,r,a)=>{let o="";t.includes(".")&&(t=`url("${t}")`,o=", auto"),void 0!==r&&(t+=" "+r+" "+a),e.canvas.style.cursor=t+o},e.noCursor=()=>{e.canvas.style.cursor="none"},e.requestPointerLock=document.body?.requestPointerLock,e.exitPointerLock=document.exitPointerLock,e._onkeydown=a=>{a.repeat||(e._startAudio(),t.keyIsPressed=!0,t.key=a.key,t.keyCode=a.keyCode,r[e.keyCode]=r[e.key.toLowerCase()]=!0,e.keyPressed(a),1==a.key.length&&e.keyTyped(a))},e._onkeyup=a=>{t.keyIsPressed=!1,t.key=a.key,t.keyCode=a.keyCode,r[e.keyCode]=r[e.key.toLowerCase()]=!1,e.keyReleased(a)},e.keyIsDown=e=>!!r["string"==typeof e?e.toLowerCase():e],e._ontouchstart=r=>{e._startAudio(),t.touches=[...r.touches].map(n),e._isTouchAware||(t.mouseX=e.touches[0].x,t.mouseY=e.touches[0].y,t.mouseIsPressed=!0,t.mouseButton=e.LEFT,e.mousePressed(r)||r.preventDefault()),e.touchStarted(r)||r.preventDefault()},e._ontouchmove=r=>{t.touches=[...r.touches].map(n),e._isTouchAware||(t.mouseX=e.touches[0].x,t.mouseY=e.touches[0].y,e.mouseDragged(r)||r.preventDefault()),e.touchMoved(r)||r.preventDefault()},e._ontouchend=r=>{t.touches=[...r.touches].map(n),e._isTouchAware||e.touches.length||(t.mouseIsPressed=!1,e.mouseReleased(r)||r.preventDefault()),e.touchEnded(r)||r.preventDefault()},o&&(o.addEventListener("mousedown",(t=>e._onmousedown(t))),o.addEventListener("mouseup",(t=>e._onmouseup(t))),o.addEventListener("click",(t=>e._onclick(t))),o.addEventListener("touchstart",(t=>e._ontouchstart(t))),o.addEventListener("touchmove",(t=>e._ontouchmove(t))),o.addEventListener("touchcancel",(t=>e._ontouchend(t))),o.addEventListener("touchend",(t=>e._ontouchend(t)))),window){let t=window.addEventListener;t("mousemove",(t=>e._onmousemove(t)),!1),t("keydown",(t=>e._onkeydown(t)),!1),t("keyup",(t=>e._onkeyup(t)),!1)}},Q5.modules.math=(e,t)=>{e.RADIANS=0,e.DEGREES=1,e.PI=Math.PI,e.HALF_PI=Math.PI/2,e.QUARTER_PI=Math.PI/4,e.abs=Math.abs,e.ceil=Math.ceil,e.exp=Math.exp,e.floor=Math.floor,e.loge=Math.log,e.mag=Math.hypot,e.max=Math.max,e.min=Math.min,e.round=Math.round,e.pow=Math.pow,e.sqrt=Math.sqrt,e.SHR3=1,e.LCG=2;let r=0;e.angleMode=t=>{"radians"==t&&(t=0),r=e._angleMode=t};let a=e._DEGTORAD=Math.PI/180,o=e._RADTODEG=180/Math.PI;function n(){let e,t,r=4294967295;return{setSeed(a){e=t=(a??Math.random()*r)>>>0},getSeed:()=>t,rand:()=>(e^=e<<17,e^=e>>13,e^=e<<5,(e>>>0)/r)}}e.degrees=t=>t*e._RADTODEG,e.radians=t=>t*e._DEGTORAD,e.map=Q5.prototype.map=(e,t,r,a,o,n)=>{let i=a+1*(e-t)/(r-t)*(o-a);return n?ae*(1-r)+t*r,e.constrain=(e,t,r)=>Math.min(Math.max(e,t),r),e.dist=function(){let e=arguments;return 4==e.length?Math.hypot(e[0]-e[2],e[1]-e[3]):Math.hypot(e[0]-e[3],e[1]-e[4],e[2]-e[5])},e.norm=(t,r,a)=>e.map(t,r,a,0,1),e.sq=e=>e*e,e.fract=e=>e-Math.floor(e),e.sin=e=>Math.sin(r?e*a:e),e.cos=e=>Math.cos(r?e*a:e),e.tan=e=>Math.tan(r?e*a:e),e.asin=e=>{let t=Math.asin(e);return r?t*o:t},e.acos=e=>{let t=Math.acos(e);return r?t*o:t},e.atan=e=>{let t=Math.atan(e);return r?t*o:t},e.atan2=(e,t)=>{let a=Math.atan2(e,t);return r?a*o:a};let i=n();i.setSeed(),e.randomSeed=e=>i.setSeed(e),e.random=(e,t)=>void 0===e?i.rand():"number"==typeof e?void 0!==t?i.rand()*(t-e)+e:i.rand()*e:e[Math.trunc(e.length*i.rand())],e.randomGenerator=t=>{t==e.LCG?i=function(){const e=4294967296;let t,r;return{setSeed(a){r=t=(a??Math.random()*e)>>>0},getSeed:()=>t,rand:()=>(r=(1664525*r+1013904223)%e,r/e)}}():t==e.SHR3&&(i=n()),i.setSeed()};var s=new function(){var e,t,r,a=new Array(128),o=new Array(256),n=new Array(128),s=new Array(128),l=new Array(256),d=new Array(256),c=()=>4294967296*i.rand()-2147483648,h=()=>.5+2.328306e-10*(c()|0),u=()=>{for(var t,o,i,l,d=3.44262;;){if(t=r*n[e],0==e){do{i=h(),l=h(),t=.2904764*-Math.log(i),o=-Math.log(l)}while(o+o0?d+t:-d-t}if(s[e]+h()*(s[e-1]-s[e]){for(var r;;){if(0==e)return 7.69711-Math.log(h());if(r=t*l[e],d[e]+h()*(d[e-1]-d[e])(r=c(),e=127&r,Math.abs(r)(t=c()>>>0){var e,t,r=2147483648,i=4294967296,c=3.442619855899,h=c,u=.00991256303526217,p=7.697117470131487,_=p,m=.003949659822581572;for(e=u/Math.exp(-.5*c*c),a[0]=Math.floor(c/e*r),a[1]=0,n[0]=e/r,n[127]=c/r,s[0]=1,s[127]=Math.exp(-.5*c*c),t=126;t>=1;t--)c=Math.sqrt(-2*Math.log(u/c+Math.exp(-.5*c*c))),a[t+1]=Math.floor(c/h*r),h=c,s[t]=Math.exp(-.5*c*c),n[t]=c/r;for(e=m/Math.exp(-p),o[0]=Math.floor(p/e*i),o[1]=0,l[0]=e/i,l[255]=p/i,d[0]=1,d[255]=Math.exp(-p),t=254;t>=1;t--)p=-Math.log(m/p+Math.exp(-p)),o[t+1]=Math.floor(p/_*i),_=p,d[t]=Math.exp(-p),l[t]=p/i}};let l;s.hasInit=!1,e.randomGaussian=(e,t)=>(s.hasInit||(s.zigset(),s.hasInit=!0),s.RNOR()*t+e),e.randomExponential=()=>(s.hasInit||(s.zigset(),s.hasInit=!0),s.REXP()),e.PERLIN="perlin",e.SIMPLEX="simplex",e.BLOCKY="blocky",e.Noise=Q5.PerlinNoise,e.noiseMode=e=>{t.Noise=Q5[e[0].toUpperCase()+e.slice(1)+"Noise"],l=null},e.noiseSeed=t=>{l=new e.Noise(t)},e.noise=(t=0,r=0,a=0)=>(l??=new e.Noise,l.noise(t,r,a)),e.noiseDetail=(t,r)=>{l??=new e.Noise,t>0&&(l.octaves=t),r>0&&(l.falloff=r)}},Q5.Noise=class{},Q5.PerlinNoise=class extends Q5.Noise{constructor(e){super(),this.grad3=[[1,1,0],[-1,1,0],[1,-1,0],[-1,-1,0],[1,0,1],[-1,0,1],[1,0,-1],[-1,0,-1],[0,1,1],[0,-1,1],[0,1,-1],[0,-1,-1]],this.octaves=1,this.falloff=.5,this.p=null==e?Array.from({length:256},(()=>Math.floor(256*Math.random()))):this.seedPermutation(e),this.p=this.p.concat(this.p)}seedPermutation(e){let t,r,a=[];for(let e=0;e<256;e++)a[e]=e;for(let o=255;o>0;o--)t=(e=16807*e%2147483647)%(o+1),r=a[o],a[o]=a[t],a[t]=r;return a}dot(e,t,r,a){return e[0]*t+e[1]*r+e[2]*a}mix(e,t,r){return(1-r)*e+r*t}fade(e){return e*e*e*(e*(6*e-15)+10)}noise(e,t,r){let a=this,o=0,n=1,i=1,s=0;for(let l=0;l{e.Sound=Q5.Sound,e.loadSound=(e,r)=>{t._preloadCount++,Q5.aud??=new window.AudioContext;let a=new Q5.Sound(e,r);return a.addEventListener("canplaythrough",(()=>{t._preloadCount--,a.loaded=!0,r&&r(a)})),a},e.getAudioContext=()=>Q5.aud,e.userStartAudio=()=>Q5.aud.resume()},Q5.Sound=class extends Audio{constructor(e){super(e);let t=this;t.load(),t.panner=Q5.aud.createStereoPanner(),t.source=Q5.aud.createMediaElementSource(t),t.source.connect(t.panner),t.panner.connect(Q5.aud.destination),Object.defineProperty(t,"pan",{get:()=>t.panner.pan.value,set:e=>t.panner.pan.value=e})}setVolume(e){this.volume=e}setLoop(e){this.loop=e}setPan(e){this.pan=e}isLoaded(){return this.loaded}isPlaying(){return!this.paused}},Q5.modules.util=(e,t)=>{e._loadFile=(e,r,a)=>{t._preloadCount++;let o={};return fetch(e).then((e=>"json"==a?e.json():"text"==a?e.text():void 0)).then((e=>{t._preloadCount--,Object.assign(o,e),r&&r(e)})),o},e.loadStrings=(t,r)=>e._loadFile(t,r,"text"),e.loadJSON=(t,r)=>e._loadFile(t,r,"json"),"object"==typeof localStorage&&(e.storeItem=localStorage.setItem,e.getItem=localStorage.getItem,e.removeItem=localStorage.removeItem,e.clearStorage=localStorage.clear),e.year=()=>(new Date).getFullYear(),e.day=()=>(new Date).getDay(),e.hour=()=>(new Date).getHours(),e.minute=()=>(new Date).getMinutes(),e.second=()=>(new Date).getSeconds()},Q5.modules.vector=e=>{e.createVector=(t,r,a)=>new Q5.Vector(t,r,a,e)},Q5.Vector=class{constructor(e,t,r,a){this.x=e||0,this.y=t||0,this.z=r||0,this._$=a||window,this._cn=null,this._cnsq=null}set(e,t,r){return this.x=e?.x||e||0,this.y=e?.y||t||0,this.z=e?.z||r||0,this}copy(){return new Q5.Vector(this.x,this.y,this.z)}_arg2v(e,t,r){return void 0!==e?.x?e:void 0!==t?{x:e,y:t,z:r||0}:{x:e,y:e,z:e}}_calcNorm(){this._cnsq=this.x*this.x+this.y*this.y+this.z*this.z,this._cn=Math.sqrt(this._cnsq)}add(){let e=this._arg2v(...arguments);return this.x+=e.x,this.y+=e.y,this.z+=e.z,this}rem(){let e=this._arg2v(...arguments);return this.x%=e.x,this.y%=e.y,this.z%=e.z,this}sub(){let e=this._arg2v(...arguments);return this.x-=e.x,this.y-=e.y,this.z-=e.z,this}mult(){let e=this._arg2v(...arguments);return this.x*=e.x,this.y*=e.y,this.z*=e.z,this}div(){let e=this._arg2v(...arguments);return e.x?this.x/=e.x:this.x=0,e.y?this.y/=e.y:this.y=0,e.z?this.z/=e.z:this.z=0,this}mag(){return this._calcNorm(),this._cn}magSq(){return this._calcNorm(),this._cnsq}dot(){let e=this._arg2v(...arguments);return this.x*e.x+this.y*e.y+this.z*e.z}dist(){let e=this._arg2v(...arguments),t=this.x-e.x,r=this.y-e.y,a=this.z-e.z;return Math.sqrt(t*t+r*r+a*a)}cross(){let e=this._arg2v(...arguments),t=this.y*e.z-this.z*e.y,r=this.z*e.x-this.x*e.z,a=this.x*e.y-this.y*e.x;return this.x=t,this.y=r,this.z=a,this}normalize(){this._calcNorm();let e=this._cn;return 0!=e&&(this.x/=e,this.y/=e,this.z/=e),this._cn=1,this._cnsq=1,this}limit(e){this._calcNorm();let t=this._cn;if(t>e){let r=e/t;this.x*=r,this.y*=r,this.z*=r,this._cn=e,this._cnsq=e*e}return this}setMag(e){this._calcNorm();let t=e/this._cn;return this.x*=t,this.y*=t,this.z*=t,this._cn=e,this._cnsq=e*e,this}heading(){return this._$.atan2(this.y,this.x)}setHeading(e){let t=this.mag();return this.x=t*this._$.cos(e),this.y=t*this._$.sin(e),this}rotate(e){let t=this._$.cos(e),r=this._$.sin(e),a=this.x*t-this.y*r,o=this.x*r+this.y*t;return this.x=a,this.y=o,this}angleBetween(){let e=this._arg2v(...arguments),t=Q5.Vector.cross(this,e);return this._$.atan2(t.mag(),this.dot(e))*Math.sign(t.z||1)}lerp(){let e=[...arguments],t=e.at(-1);if(0==t)return this;let r=this._arg2v(...e.slice(0,-1));return this.x+=(r.x-this.x)*t,this.y+=(r.y-this.y)*t,this.z+=(r.z-this.z)*t,this}slerp(){let e=[...arguments],t=e.at(-1);if(0==t)return this;let r=this._arg2v(...e.slice(0,-1));if(1==t)return this.set(r);let a=this.mag(),o=r.mag();if(0==a||0==o)return this.mult(1-t).add(r.mult(t));let n=Q5.Vector.cross(this,r),i=n.mag(),s=Math.atan2(i,this.dot(r));if(i>0)n.div(i);else{if(se.copy().add(t),Q5.Vector.cross=(e,t)=>e.copy().cross(t),Q5.Vector.dist=(e,t)=>Math.hypot(e.x-t.x,e.y-t.y,e.z-t.z),Q5.Vector.div=(e,t)=>e.copy().div(t),Q5.Vector.dot=(e,t)=>e.copy().dot(t),Q5.Vector.equals=(e,t,r)=>e.equals(t,r),Q5.Vector.lerp=(e,t,r)=>e.copy().lerp(t,r),Q5.Vector.slerp=(e,t,r)=>e.copy().slerp(t,r),Q5.Vector.limit=(e,t)=>e.copy().limit(t),Q5.Vector.heading=e=>this._$.atan2(e.y,e.x),Q5.Vector.magSq=e=>e.x*e.x+e.y*e.y+e.z*e.z,Q5.Vector.mag=e=>Math.sqrt(Q5.Vector.magSq(e)),Q5.Vector.mult=(e,t)=>e.copy().mult(t),Q5.Vector.normalize=e=>e.copy().normalize(),Q5.Vector.rem=(e,t)=>e.copy().rem(t),Q5.Vector.sub=(e,t)=>e.copy().sub(t);for(let e of["fromAngle","fromAngles","random2D","random3D"])Q5.Vector[e]=(t,r,a)=>(new Q5.Vector)[e](t,r,a);Q5.renderers.webgpu={},Q5.renderers.webgpu.canvas=(e,t)=>{let r,a=e.canvas;a.width=e.width=500,a.height=e.height=500,e.colorMode&&e.colorMode("rgb","float"),e.pipelines=[];let o=e.drawStack=[],n=e.colorsStack=[1,1,1,1];e._envLayout=Q5.device.createBindGroupLayout({entries:[{binding:0,visibility:GPUShaderStage.VERTEX,buffer:{type:"uniform",hasDynamicOffset:!1}}]}),e._transformLayout=Q5.device.createBindGroupLayout({entries:[{binding:0,visibility:GPUShaderStage.VERTEX,buffer:{type:"read-only-storage",hasDynamicOffset:!1}}]}),e.bindGroupLayouts=[e._envLayout,e._transformLayout];const i=Q5.device.createBuffer({size:8,usage:GPUBufferUsage.UNIFORM|GPUBufferUsage.COPY_DST});e._createCanvas=(r,o,n)=>(t.ctx=t.drawingContext=a.getContext("webgpu"),n.format??=navigator.gpu.getPreferredCanvasFormat(),n.device??=Q5.device,e.ctx.configure(n),Q5.device.queue.writeBuffer(i,0,new Float32Array([e.canvas.hw,e.canvas.hh])),e._envBindGroup=Q5.device.createBindGroup({layout:e._envLayout,entries:[{binding:0,resource:{buffer:i}}]}),a),e._resizeCanvas=(t,r)=>{e._setCanvasSize(t,r)};let s=0;const l=(t,r,a,o=1)=>{"string"==typeof t?t=e.color(t):null==a&&(o=r??1,r=a=t),t._q5Color?n.push(t.r,t.g,t.b,t.a):n.push(t,r,a,o),s++};e.fill=(t,r,a,o)=>{l(t,r,a,o),e._doFill=!0,e._fillIndex=s},e.stroke=(t,r,a,o)=>{l(t,r,a,o),e._doStroke=!0,e._strokeIndex=s},e.noFill=()=>e._doFill=!1,e.noStroke=()=>e._doStroke=!1,e._strokeWeight=1,e.strokeWeight=t=>e._strokeWeight=Math.abs(t),e.resetMatrix=()=>{e._matrix=[1,0,0,0,0,1,0,0,0,0,1,0,0,0,0,1],e._transformIndex=0},e.resetMatrix(),e._matrixDirty=!1,e.transformStates=[e._matrix.slice()],e._transformIndexStack=[],e.translate=(t,r,a)=>{(t||r||a)&&(e._matrix[3]+=t,e._matrix[7]-=r,e._matrix[11]+=a||0,e._matrixDirty=!0)},e.rotate=t=>{if(!t)return;e._angleMode&&(t*=e._DEGTORAD);let r=Math.cos(t),a=Math.sin(t),o=e._matrix[0],n=e._matrix[1],i=e._matrix[4],s=e._matrix[5];o||n||i||s?(e._matrix[0]=o*r+i*a,e._matrix[1]=n*r+s*a,e._matrix[4]=o*-a+i*r,e._matrix[5]=n*-a+s*r):(e._matrix[0]=r,e._matrix[1]=a,e._matrix[4]=-a,e._matrix[5]=r),e._matrixDirty=!0},e.scale=(t=1,r,a=1)=>{r??=t,e._matrix[0]*=t,e._matrix[5]*=r,e._matrix[10]*=a,e._matrixDirty=!0},e.shearX=t=>{if(!t)return;e._angleMode&&(t*=e._DEGTORAD);let r=Math.tan(t),a=e._matrix[0],o=e._matrix[1],n=e._matrix[4],i=e._matrix[5];e._matrix[0]=a+n*r,e._matrix[1]=o+i*r,e._matrixDirty=!0},e.shearY=t=>{if(!t)return;e._angleMode&&(t*=e._DEGTORAD);let r=Math.tan(t),a=e._matrix[0],o=e._matrix[1],n=e._matrix[4],i=e._matrix[5];e._matrix[4]=n+a*r,e._matrix[5]=i+o*r,e._matrixDirty=!0},e.applyMatrix=(...t)=>{let r;if(r=1==t.length?t[0]:t,9==r.length)r=[r[0],r[1],0,r[2],r[3],r[4],0,r[5],0,0,1,0,r[6],r[7],0,r[8]];else if(16!=r.length)throw new Error("Matrix must be a 3x3 or 4x4 array.");e._matrix=r.slice(),e._matrixDirty=!0},e._saveMatrix=()=>{e.transformStates.push(e._matrix.slice()),e._transformIndex=e.transformStates.length-1,e._matrixDirty=!1},e.pushMatrix=()=>{e._matrixDirty&&e._saveMatrix(),e._transformIndexStack.push(e._transformIndex)},e.popMatrix=()=>{if(!e._transformIndexStack.length)return console.warn("Matrix index stack is empty!");let t=e._transformIndexStack.pop();e._matrix=e.transformStates[t].slice(),e._transformIndex=t,e._matrixDirty=!1},e.push=()=>{e.pushMatrix(),e.pushStyles()},e.pop=()=>{e.popMatrix(),e.popStyles()},e._calcBox=(e,t,r,a,o)=>{let n,i,s,l,d=r/2,c=a/2;return o&&"corner"!=o?"center"==o?(n=e-d,i=e+d,s=-(t-c),l=-(t+c)):(n=e,i=r,s=-t,l=-a):(n=e,i=e+r,s=-t,l=-(t+a)),[n,i,s,l]},e.clear=()=>{},e._beginRender=()=>{e.encoder=Q5.device.createCommandEncoder(),r=t.pass=e.encoder.beginRenderPass({label:"q5-webgpu",colorAttachments:[{view:ctx.getCurrentTexture().createView(),loadOp:"clear",storeOp:"store"}]})},e._render=()=>{if(transformStates.length>1||!e._transformBindGroup){const t=Q5.device.createBuffer({size:64*transformStates.length,usage:GPUBufferUsage.STORAGE|GPUBufferUsage.COPY_DST});Q5.device.queue.writeBuffer(t,0,new Float32Array(transformStates.flat())),e._transformBindGroup=Q5.device.createBindGroup({layout:e._transformLayout,entries:[{binding:0,resource:{buffer:t}}]})}r.setBindGroup(0,e._envBindGroup),r.setBindGroup(1,e._transformBindGroup);for(let t of e._hooks.preRender)t();let t=0,a=0,n=-1;r.setPipeline(e.pipelines[0]);for(let i=0;i{r.end();const a=e.encoder.finish();Q5.device.queue.submit([a]),t.pass=e.encoder=null,e.drawStack.length=0,e.colorsStack.length=4,s=0,rotation=0,e.transformStates.length=1,e._transformIndexStack.length=0}},Q5.webgpu=async function(e,t){if(e&&"global"!=e||(Q5._hasGlobal=!0),!navigator.gpu){console.warn("q5 WebGPU not supported on this browser!");let r=new Q5(e,t);return r.colorMode("rgb",1),r._beginRender=()=>r.translate(r.canvas.hw,r.canvas.hh),r}let r=await navigator.gpu.requestAdapter();if(!r)throw new Error("No appropriate GPUAdapter found.");return Q5.device=await r.requestDevice(),new Q5(e,t,"webgpu")},Q5.renderers.webgpu.drawing=(e,t)=>{let r,a,o=e.canvas,n=e.drawStack,i=e.colorsStack,s=[],l=Q5.device.createShaderModule({label:"drawingVertexShader",code:"\nstruct VertexOutput {\n\t@builtin(position) position: vec4,\n\t@location(1) colorIndex: f32\n};\n\nstruct Uniforms {\n\thalfWidth: f32,\n\thalfHeight: f32\n};\n\n@group(0) @binding(0) var uniforms: Uniforms;\n@group(1) @binding(0) var transforms: array>;\n\n@vertex\nfn vertexMain(@location(0) pos: vec2, @location(1) colorIndex: f32, @location(2) transformIndex: f32) -> VertexOutput {\n\tvar vert = vec4(pos, 0.0, 1.0);\n\tvert *= transforms[i32(transformIndex)];\n\tvert.x /= uniforms.halfWidth;\n\tvert.y /= uniforms.halfHeight;\n\n\tvar output: VertexOutput;\n\toutput.position = vert;\n\toutput.colorIndex = colorIndex;\n\treturn output;\n}\n"}),d=Q5.device.createShaderModule({label:"drawingFragmentShader",code:"\n@group(2) @binding(0) var uColors : array>;\n\n@fragment\nfn fragmentMain(@location(1) colorIndex: f32) -> @location(0) vec4 {\n\tlet index = u32(colorIndex);\n\treturn mix(uColors[index], uColors[index + 1u], fract(colorIndex));\n}\n"});a=Q5.device.createBindGroupLayout({entries:[{binding:0,visibility:GPUShaderStage.FRAGMENT,buffer:{type:"read-only-storage",hasDynamicOffset:!1}}]}),e.bindGroupLayouts.push(a);let c={arrayStride:16,attributes:[{format:"float32x2",offset:0,shaderLocation:0},{format:"float32",offset:8,shaderLocation:1},{format:"float32",offset:12,shaderLocation:2}]},h=["zero","one","src-alpha","one-minus-src-alpha","dst","dst-alpha","one-minus-dst-alpha","one-minus-src"],u=["add","subtract","reverse-subtract","min","max"];const p={normal:[2,3,0,2,3,0],lighter:[2,1,0,2,1,0],subtract:[2,1,2,2,1,2],multiply:[4,0,0,5,0,0],screen:[1,3,0,1,3,0],darken:[1,3,3,1,3,3],lighten:[1,3,4,1,3,4],overlay:[2,3,0,2,3,0],hard_light:[2,3,0,2,3,0],soft_light:[2,3,0,2,3,0],difference:[2,3,2,2,3,2],exclusion:[2,3,0,2,3,0],color_dodge:[1,7,0,1,7,0],color_burn:[6,1,0,6,1,0],linear_dodge:[2,1,0,2,1,0],linear_burn:[2,7,1,2,7,1],vivid_light:[2,7,0,2,7,0],pin_light:[2,7,0,2,7,0],hard_mix:[2,7,0,2,7,0]};e.blendConfigs={};for(const[t,r]of Object.entries(p))e.blendConfigs[t]={color:{srcFactor:h[r[0]],dstFactor:h[r[1]],operation:u[r[2]]},alpha:{srcFactor:h[r[3]],dstFactor:h[r[4]],operation:u[r[5]]}};e._blendMode="normal",e.blendMode=t=>{t!=e._blendMode&&("source-over"==t&&(t="normal"),t=t.toLowerCase().replace(/[ -]/g,"_"),e._blendMode=t,e.pipelines[0]=e._createPipeline(e.blendConfigs[t]))};let _,m=Q5.device.createPipelineLayout({label:"drawingPipelineLayout",bindGroupLayouts:e.bindGroupLayouts});e._createPipeline=e=>Q5.device.createRenderPipeline({label:"drawingPipeline",layout:m,vertex:{module:l,entryPoint:"vertexMain",buffers:[c]},fragment:{module:d,entryPoint:"fragmentMain",targets:[{format:"bgra8unorm",blend:e}]},primitive:{topology:"triangle-list"}}),e.pipelines[0]=e._createPipeline(e.blendConfigs.normal),e.beginShape=()=>{_=[]},e.vertex=(t,r)=>{e._matrixDirty&&e._saveMatrix(),_.push(t,-r,e._fillIndex,e._transformIndex)},e.endShape=t=>{if(!e._doFill)return void(_=[]);let r=_;if(r.length<12)throw new Error("A shape must have at least 3 vertices.");t&&r.push(r[0],r[1],r[2],r[3]);let a=[];for(let e=4;e{e.beginShape(),e.vertex(t,r),e.vertex(a,o),e.vertex(n,i),e.endShape(1)},e.quad=(t,r,a,o,n,i,s,l)=>{e.beginShape(),e.vertex(t,r),e.vertex(a,o),e.vertex(n,i),e.vertex(s,l),e.endShape(1)},e.rectMode=t=>e._rectMode=t,e.rect=(t,a,o,i)=>{let[l,d,c,h]=e._calcBox(t,a,o,i,e._rectMode),u=r??e._fillIndex;e._matrixDirty&&e._saveMatrix();let p=e._transformIndex;s.push(l,c,u,p,d,c,u,p,l,h,u,p,d,c,u,p,l,h,u,p,d,h,u,p),n.push(0,6)},e.square=(t,r,a)=>e.rect(t,r,a,a),e.point=(t,a)=>{r=e._strokeIndex;let o=e._strokeWeight;o<2?(o=Math.round(o),e.rect(t,a,o,o)):e.ellipse(t,a,o,o),r=null},e.line=(t,a,o,n)=>{r=e._strokeIndex,e.push(),e.translate(t,a),e.rotate(e.atan2(n-a,o-t));let i=Math.sqrt((o-t)**2+(n-a)**2),s=e._strokeWeight;e.rect(0,-s/2,i,s),e.pop(),r=null},e.background=(t,r,a,n)=>{e.push(),e.resetMatrix(),t.src?e.image(t,-o.hw,-o.hh,o.w,o.h):(e.fill(t,r,a,n),e.rect(-o.hw,-o.hh,o.w,o.h)),e.pop()};e.ellipseMode=t=>e._ellipseMode=t,e.ellipse=(t,a,o,i)=>{const l=(d=o==i?o:Math.max(o,i))<4?6:d<6?8:d<10?10:d<16?12:d<20?14:d<22?16:d<24?18:d<28?20:d<34?22:d<42?24:d<48?26:d<56?28:d<64?30:d<72?32:d<84?34:d<96?36:d<98?38:d<113?40:d<149?44:d<199?48:d<261?52:d<353?56:d<461?60:d<585?64:d<1200?70:d<1800?80:d<2400?90:100;var d;let c=Math.max(o,1)/2,h=o==i?c:Math.max(i,1)/2,u=0;const p=e.TAU/l,_=r??e._fillIndex;e._matrixDirty&&e._saveMatrix();const m=e._transformIndex;let g,x,f,v;for(let e=0;e<=l;e++)g=f,x=v,f=t+c*Math.cos(u),v=a+h*Math.sin(u),u+=p,0!=e&&s.push(t,a,_,m,g,x,_,m,f,v,_,m);n.push(0,3*l)},e.circle=(t,r,a)=>e.ellipse(t,r,a,a),e._hooks.preRender.push((()=>{e.pass.setPipeline(e.pipelines[0]);const t=new Float32Array(s),r=Q5.device.createBuffer({size:t.byteLength,usage:GPUBufferUsage.VERTEX|GPUBufferUsage.COPY_DST});Q5.device.queue.writeBuffer(r,0,t),e.pass.setVertexBuffer(0,r);const o=Q5.device.createBuffer({size:4*i.length,usage:GPUBufferUsage.STORAGE|GPUBufferUsage.COPY_DST});Q5.device.queue.writeBuffer(o,0,new Float32Array(i)),e._colorsBindGroup=Q5.device.createBindGroup({layout:a,entries:[{binding:0,resource:{buffer:o,offset:0,size:4*i.length}}]}),e.pass.setBindGroup(2,e._colorsBindGroup)})),e._hooks.postRender.push((()=>{s.length=0}))},Q5.renderers.webgpu.image=(e,t)=>{e._textureBindGroups=[];let r=[],a=Q5.device.createShaderModule({label:"imageVertexShader",code:"\nstruct VertexOutput {\n\t@builtin(position) position: vec4,\n\t@location(0) texCoord: vec2\n};\n\nstruct Uniforms {\n\thalfWidth: f32,\n\thalfHeight: f32\n};\n\n@group(0) @binding(0) var uniforms: Uniforms;\n@group(1) @binding(0) var transforms: array>;\n\n@vertex\nfn vertexMain(@location(0) pos: vec2, @location(1) texCoord: vec2, @location(2) transformIndex: f32) -> VertexOutput {\n\tvar vert = vec4(pos, 0.0, 1.0);\n\tvert *= transforms[i32(transformIndex)];\n\tvert.x /= uniforms.halfWidth;\n\tvert.y /= uniforms.halfHeight;\n\n\tvar output: VertexOutput;\n\toutput.position = vert;\n\toutput.texCoord = texCoord;\n\treturn output;\n}\n\t"}),o=Q5.device.createShaderModule({label:"imageFragmentShader",code:"\n@group(3) @binding(0) var samp: sampler;\n@group(3) @binding(1) var texture: texture_2d;\n\n@fragment\nfn fragmentMain(@location(0) texCoord: vec2) -> @location(0) vec4 {\n\t// Sample the texture using the interpolated texture coordinate\n\treturn textureSample(texture, samp, texCoord);\n}\n\t"}),n=Q5.device.createBindGroupLayout({label:"textureLayout",entries:[{binding:0,visibility:GPUShaderStage.FRAGMENT,sampler:{type:"filtering"}},{binding:1,visibility:GPUShaderStage.FRAGMENT,texture:{viewDimension:"2d",sampleType:"float"}}]});e.bindGroupLayouts.push(n);const i=Q5.device.createPipelineLayout({label:"imagePipelineLayout",bindGroupLayouts:e.bindGroupLayouts});e.pipelines[1]=Q5.device.createRenderPipeline({label:"imagePipeline",layout:i,vertex:{module:a,entryPoint:"vertexMain",buffers:[{arrayStride:0,attributes:[]},{arrayStride:20,attributes:[{shaderLocation:0,offset:0,format:"float32x2"},{shaderLocation:1,offset:8,format:"float32x2"},{shaderLocation:2,offset:16,format:"float32"}]}]},fragment:{module:o,entryPoint:"fragmentMain",targets:[{format:"bgra8unorm",blend:e.blendConfigs?.normal||{color:{srcFactor:"src-alpha",dstFactor:"one-minus-src-alpha",operation:"add"},alpha:{srcFactor:"src-alpha",dstFactor:"one-minus-src-alpha",operation:"add"}}}]},primitive:{topology:"triangle-list"}});let s=Q5.device.createSampler({magFilter:"linear",minFilter:"linear"});e._textures=[];let l=0;e._createTexture=t=>{t.canvas&&(t=t.canvas);let r=[t.width,t.height,1],a=Q5.device.createTexture({size:r,format:"bgra8unorm",usage:GPUTextureUsage.TEXTURE_BINDING|GPUTextureUsage.COPY_DST|GPUTextureUsage.RENDER_ATTACHMENT});Q5.device.queue.copyExternalImageToTexture({source:t},{texture:a,colorSpace:e.canvas.colorSpace},r),e._textures[l]=a,t.textureIndex=l;const o=Q5.device.createBindGroup({layout:n,entries:[{binding:0,resource:s},{binding:1,resource:a.createView()}]});e._textureBindGroups[l]=o,l=(l+1)%12e3,e._textures[l]&&(e._textures[l].destroy(),delete e._textures[l],delete e._textureBindGroups[l])},e.loadImage=e.loadTexture=r=>{t._preloadCount++;const a=new Image;return a.crossOrigin="Anonymous",a.onload=()=>{e._createTexture(a),t._preloadCount--},a.src=r,a},e.imageMode=t=>e._imageMode=t,e.image=(t,a,o,n,i)=>{if(t.canvas&&(t=t.canvas),null==t.textureIndex)return;e._matrixDirty&&e._saveMatrix();let s=e._transformIndex;n??=t.width/e._pixelDensity,i??=t.height/e._pixelDensity;let[l,d,c,h]=e._calcBox(a,o,n,i,e._imageMode);r.push(l,c,0,0,s,d,c,1,0,s,l,h,0,1,s,d,c,1,0,s,l,h,0,1,s,d,h,1,1,s),e.drawStack.push(1,t.textureIndex)},e._hooks.preRender.push((()=>{if(!e._textureBindGroups.length)return;e.pass.setPipeline(e.pipelines[1]);const t=new Float32Array(r),a=Q5.device.createBuffer({size:t.byteLength,usage:GPUBufferUsage.VERTEX|GPUBufferUsage.COPY_DST});Q5.device.queue.writeBuffer(a,0,t),e.pass.setVertexBuffer(1,a)})),e._hooks.postRender.push((()=>{r.length=0}))},Q5.THRESHOLD=1,Q5.GRAY=2,Q5.OPAQUE=3,Q5.INVERT=4,Q5.POSTERIZE=5,Q5.DILATE=6,Q5.ERODE=7,Q5.BLUR=8,Q5.renderers.webgpu.text=(e,t)=>{let r=e.createGraphics(1,1);r.pixelDensity(e._pixelDensity),r._imageMode="corner",e.loadFont=e=>(t._preloadCount++,r.loadFont(e,(()=>{t._preloadCount--}))),e.textFont=r.textFont,e.textSize=r.textSize,e.textLeading=r.textLeading,e.textStyle=r.textStyle,e.textAlign=r.textAlign,e.textWidth=r.textWidth,e.textAscent=r.textAscent,e.textDescent=r.textDescent,e.textFill=(t,a,o,n)=>r.fill(e.color(t,a,o,n)),e.textStroke=(t,a,o,n)=>r.stroke(e.color(t,a,o,n)),e.text=(t,a,o,n,i)=>{let s=r.createTextImage(t,n,i);if(void 0===s.canvas.textureIndex)e._createTexture(s);else if(s.modified){let t=s.canvas,r=[t.width,t.height,1],a=e._textures[t.textureIndex];Q5.device.queue.copyExternalImageToTexture({source:t},{texture:a,colorSpace:e.canvas.colorSpace},r),s.modified=!1}e.textImage(s,a,o)},e.createTextImage=r.createTextImage,e.textImage=(t,a,o)=>{let n=e._imageMode;e._imageMode="corner";let i=r._textAlign;"center"==i?a-=t.canvas.hw:"right"==i&&(a-=t.width);let s=r._textBaseline;"alphabetic"==s?o-=r._textLeading:"middle"==s?o-=t._middle:"bottom"==s?o-=t._bottom:"top"==s&&(o-=t._top),e.image(t,a,o),e._imageMode=n}}; +function Q5(e,t,r){let a,n=this;if(n._q5=!0,n._parent=t,n._renderer=r||"q2d",n._preloadCount=0,e??="global","auto"==e){if(!window.setup&&!window.draw)return;e="global"}n._scope=e,"global"==e&&(Q5._hasGlobal=n._isGlobal=!0,a=Q5._nodejs?global:window);let o=new Proxy(n,{set:(e,t,r)=>(n[t]=r,n._isGlobal&&(a[t]=r),!0)});n.canvas=n.ctx=n.drawingContext=null,n.pixels=[];let i=null;n.frameCount=0,n.deltaTime=16,n._targetFrameRate=0,n._targetFrameDuration=16.666666666666668,n._frameRate=n._fps=60,n._loop=!0,n._hooks={postCanvas:[],preRender:[],postRender:[]};let s=0;n.millis=()=>performance.now()-s,n.noCanvas=()=>{n.canvas?.remove&&n.canvas.remove(),n.canvas=0,o.ctx=o.drawingContext=0},window&&(n.windowWidth=window.innerWidth,n.windowHeight=window.innerHeight,n.deviceOrientation=window.screen?.orientation?.type),n._incrementPreload=()=>o._preloadCount++,n._decrementPreload=()=>o._preloadCount--,n._draw=e=>{let t=e||performance.now();if(n._lastFrameTime??=t-n._targetFrameDuration,n._didResize&&(n.windowResized(),n._didResize=!1),n._loop)i=d(n._draw);else if(n.frameCount&&!n._redraw)return;if(i&&n.frameCount){if(t-n._lastFrameTime{n._loop=!1,i=null},n.loop=()=>{n._loop=!0,null==i&&n._draw()},n.isLooping=()=>n._loop,n.redraw=(e=1)=>{n._redraw=!0;for(let t=0;t{n.noLoop(),n.canvas.remove()},n.frameRate=e=>(e&&(n._targetFrameRate=e,n._targetFrameDuration=1e3/e),n._frameRate),n.getTargetFrameRate=()=>n._targetFrameRate,n.getFPS=()=>n._fps,n.Element=function(e){this.elt=e},n._elements=[],n.TWO_PI=n.TAU=2*Math.PI,n.log=n.print=console.log,n.describe=()=>{};for(let e in Q5.modules)Q5.modules[e](n,o);let l=Q5.renderers[n._renderer];for(let e in l)l[e](n,o);for(let e in Q5)"_"!=e[1]&&e[1]==e[1].toUpperCase()&&(n[e]=Q5[e]);if("graphics"==e)return;"global"==e&&(Object.assign(Q5,n),delete Q5.Q5);for(let e of Q5.methods.init)e.call(n);for(let[e,t]of Object.entries(Q5.prototype))"_"!=e[0]&&"function"==typeof n[e]&&(n[e]=t.bind(n));if("global"==e){let e=Object.getOwnPropertyNames(n);for(let t of e)"_"!=t[0]&&(a[t]=n[t])}"function"==typeof e&&e(n),Q5._instanceCount++;let d=window.requestAnimationFrame||function(e){const t=n._lastFrameTime+n._targetFrameDuration;return setTimeout((()=>{e(t)}),t-performance.now())},c=a||n;n._isTouchAware=c.touchStarted||c.touchMoved||c.mouseReleased;let h=c.preload,u=["setup","draw","preload","mouseMoved","mousePressed","mouseReleased","mouseDragged","mouseClicked","keyPressed","keyReleased","keyTyped","touchStarted","touchMoved","touchEnded","windowResized"];for(let e of u)c[e]?n._isGlobal&&(n[e]=()=>{try{return c[e]()}catch(e){throw n._aiErrorAssistance&&n._aiErrorAssistance(e),e}}):n[e]=()=>{};async function p(){if(n._startDone=!0,n._preloadCount>0)return d(p);s=performance.now(),await n.setup(),n.frameCount||(null===n.ctx&&n.createCanvas(100,100),n._setupDone=!0,n.ctx&&n.resetMatrix(),d(n._draw))}(n.setup||n.draw)&&(arguments.length&&"namespace"!=e&&"webgpu"!=r||h?(n.preload(),p()):(c.preload=n.preload=()=>{n._startDone||p()},setTimeout(n.preload,32)))}Q5.renderers={},Q5.modules={},Q5._nodejs="object"==typeof process,Q5._instanceCount=0,Q5._friendlyError=(e,t)=>{throw Error(t+": "+e)},Q5._validateParameters=()=>!0,Q5.methods={init:[],pre:[],post:[],remove:[]},Q5.prototype.registerMethod=(e,t)=>Q5.methods[e].push(t),Q5.prototype.registerPreloadMethod=(e,t)=>Q5.prototype[e]=t[e],Q5._nodejs?global.p5??=global.Q5=Q5:"object"==typeof window?window.p5??=window.Q5=Q5:global.window=0,"object"==typeof document&&document.addEventListener("DOMContentLoaded",(()=>{Q5._hasGlobal||new Q5("auto")})),Q5.modules.canvas=(e,t)=>{e.CENTER="center",e.LEFT="left",e.RIGHT="right",e.TOP="top",e.BOTTOM="bottom",e.BASELINE="alphabetic",e.NORMAL="normal",e.ITALIC="italic",e.BOLD="bold",e.BOLDITALIC="italic bold",e.ROUND="round",e.SQUARE="butt",e.PROJECT="square",e.MITER="miter",e.BEVEL="bevel",e.CHORD=0,e.PIE=1,e.OPEN=2,e.RADIUS="radius",e.CORNER="corner",e.CORNERS="corners",e.CLOSE=1,e.LANDSCAPE="landscape",e.PORTRAIT="portrait",e.BLEND="source-over",e.REMOVE="destination-out",e.ADD="lighter",e.DARKEST="darken",e.LIGHTEST="lighten",e.DIFFERENCE="difference",e.SUBTRACT="subtract",e.EXCLUSION="exclusion",e.MULTIPLY="multiply",e.SCREEN="screen",e.REPLACE="copy",e.OVERLAY="overlay",e.HARD_LIGHT="hard-light",e.SOFT_LIGHT="soft-light",e.DODGE="color-dodge",e.BURN="color-burn",e.P2D="2d",e.WEBGL="webgl",e._OffscreenCanvas=window.OffscreenCanvas||function(){return document.createElement("canvas")},Q5._nodejs?Q5._createNodeJSCanvas&&(t.canvas=Q5._createNodeJSCanvas(100,100)):"image"!=e._scope&&"graphics"!=e._scope||(t.canvas=new e._OffscreenCanvas(100,100)),e.canvas||("object"==typeof document?(t.canvas=document.createElement("canvas"),e.canvas.id="q5Canvas"+Q5._instanceCount,e.canvas.classList.add("q5Canvas")):e.noCanvas());let r=e.canvas;if(r.width=e.width=100,r.height=e.height=100,e._pixelDensity=1,e.displayDensity=()=>window.devicePixelRatio||1,"image"!=e._scope&&(r.renderer=e._renderer,r[e._renderer]=!0,e._pixelDensity=Math.ceil(e.displayDensity())),e._adjustDisplay=()=>{r.style&&(r.style.width=r.w+"px",r.style.height=r.h+"px")},e.createCanvas=function(t,a,n){n??=arguments[3];let o=Object.assign({},Q5.canvasOptions);"object"==typeof n&&Object.assign(o,n),"image"!=e._scope&&("graphics"==e._scope?e._pixelDensity=this._pixelDensity:window.IntersectionObserver&&new IntersectionObserver((e=>{r.visible=e[0].isIntersecting})).observe(r)),e._setCanvasSize(t,a),Object.assign(r,o);let i=e._createCanvas(r.w,r.h,o);if(e._hooks)for(let t of e._hooks.postCanvas)t();return i},e.createGraphics=function(t,r,a){let n=new Q5("graphics");return a??={},a.alpha??=!0,a.colorSpace??=e.canvas.colorSpace,n.createCanvas.call(e,t,r,a),n},e._save=async(e,t,r)=>{if(t=t||"untitled","jpg"==(r=r||"png")||"png"==r||"webp"==r)if(e instanceof OffscreenCanvas){const t=await e.convertToBlob({type:"image/"+r});e=await new Promise((e=>{const r=new FileReader;r.onloadend=()=>e(r.result),r.readAsDataURL(t)}))}else e=e.toDataURL("image/"+r);else{let t="text/plain";"json"==r&&("string"!=typeof e&&(e=JSON.stringify(e)),t="text/json"),e=new Blob([e],{type:t}),e=URL.createObjectURL(e)}let a=document.createElement("a");a.href=e,a.download=t+"."+r,a.click(),URL.revokeObjectURL(a.href)},e.save=(t,r,a)=>{if((!t||"string"==typeof t&&(!r||!a&&r.length<5))&&(a=r,r=t,t=e.canvas),a)return e._save(t,r,a);r?(r=r.split("."),e._save(t,r[0],r.at(-1))):e._save(t)},e._setCanvasSize=(a,n)=>{a??=window.innerWidth,n??=window.innerHeight,r.w=a=Math.ceil(a),r.h=n=Math.ceil(n),r.hw=a/2,r.hh=n/2,r.width=Math.ceil(a*e._pixelDensity),r.height=Math.ceil(n*e._pixelDensity),e._da?e.flexibleCanvas(e._dau):(t.width=a,t.height=n),e.displayMode&&!r.displayMode?e.displayMode():e._adjustDisplay()},"image"!=e._scope){if(r&&"graphics"!=e._scope){function a(){let t=e._parent;t??=document.getElementsByTagName("main")[0],t||(t=document.createElement("main"),document.body.append(t)),r.parent(t)}r.parent=t=>{function a(){e.frameCount>1&&(e._didResize=!0,e._adjustDisplay())}r.parentElement&&r.parentElement.removeChild(r),"string"==typeof t&&(t=document.getElementById(t)),t.append(r),"function"==typeof ResizeObserver?(e._ro&&e._ro.disconnect(),e._ro=new ResizeObserver(a),e._ro.observe(t)):e.frameCount||window.addEventListener("resize",a)},document.body?a():document.addEventListener("DOMContentLoaded",a)}e.resizeCanvas=(t,a)=>{if(!e.ctx)return e.createCanvas(t,a);t==r.w&&a==r.h||e._resizeCanvas(t,a)},e.canvas.resize=e.resizeCanvas,e.canvas.save=e.saveCanvas=e.save,e.pixelDensity=t=>t&&t!=e._pixelDensity?(e._pixelDensity=t,e._setCanvasSize(r.w,r.h),t):e._pixelDensity,e.flexibleCanvas=(a=400)=>{a?(e._da=r.width/(a*e._pixelDensity),t.width=e._dau=a,t.height=r.h/r.w*a):e._da=0},e._styleNames=["_doStroke","_doFill","_strokeSet","_fillSet","_tint","_imageMode","_rectMode","_ellipseMode","_textSize","_textAlign","_textBaseline"],e._styles=[],e.pushStyles=()=>{let t={};for(let r of e._styleNames)t[r]=e[r];e._styles.push(t)},e.popStyles=()=>{let t=e._styles.pop();for(let r of e._styleNames)e[r]=t[r]},window&&"graphics"!=e._scope&&window.addEventListener("resize",(()=>{e._didResize=!0,t.windowWidth=window.innerWidth,t.windowHeight=window.innerHeight,t.deviceOrientation=window.screen?.orientation?.type}))}},Q5.canvasOptions={alpha:!1,colorSpace:"display-p3"},window.matchMedia&&matchMedia("(dynamic-range: high) and (color-gamut: p3)").matches?Q5.supportsHDR=!0:Q5.canvasOptions.colorSpace="srgb",Q5.renderers.q2d={},Q5.renderers.q2d.canvas=(e,t)=>{let r=e.canvas;e.colorMode&&e.colorMode("rgb","integer"),e._createCanvas=function(a,n,o){return t.ctx=t.drawingContext=r.getContext("2d",o),"image"!=e._scope&&(e.ctx.fillStyle="white",e.ctx.strokeStyle="black",e.ctx.lineCap="round",e.ctx.lineJoin="miter",e.ctx.textAlign="left"),e.ctx.scale(e._pixelDensity,e._pixelDensity),e.ctx.save(),r},e.clear=()=>{e.ctx.save(),e.ctx.resetTransform(),e.ctx.clearRect(0,0,e.canvas.width,e.canvas.height),e.ctx.restore()},"image"!=e._scope&&(e._resizeCanvas=(t,a)=>{let n,o={};for(let t in e.ctx)"function"!=typeof e.ctx[t]&&(o[t]=e.ctx[t]);if(delete o.canvas,e.frameCount>1){n=new e._OffscreenCanvas(r.width,r.height),n.w=r.w,n.h=r.h,n.getContext("2d").drawImage(r,0,0)}e._setCanvasSize(t,a);for(let t in o)e.ctx[t]=o[t];e.scale(e._pixelDensity),n&&e.ctx.drawImage(n,0,0,n.w,n.h)},e.fill=function(t){if(e._doFill=!0,e._fillSet=!0,Q5.Color&&(t._q5Color||("string"!=typeof t?t=e.color(...arguments):e._namedColors[t]&&(t=e.color(...e._namedColors[t]))),t.a<=0))return e._doFill=!1;e.ctx.fillStyle=e._fill=t.toString()},e.noFill=()=>e._doFill=!1,e.stroke=function(t){if(e._doStroke=!0,e._strokeSet=!0,Q5.Color&&(t._q5Color||("string"!=typeof t?t=e.color(...arguments):e._namedColors[t]&&(t=e.color(...e._namedColors[t]))),t.a<=0))return e._doStroke=!1;e.ctx.strokeStyle=e._stroke=t.toString()},e.strokeWeight=t=>{t||(e._doStroke=!1),e._da&&(t*=e._da),e.ctx.lineWidth=e._strokeWeight=t||1e-4},e.noStroke=()=>e._doStroke=!1,e.opacity=t=>e.ctx.globalAlpha=t,e.translate=(t,r)=>{e._da&&(t*=e._da,r*=e._da),e.ctx.translate(t,r)},e.rotate=t=>{e._angleMode&&(t=e.radians(t)),e.ctx.rotate(t)},e.scale=(t,r)=>{r??=t,e.ctx.scale(t,r)},e.applyMatrix=(t,r,a,n,o,i)=>e.ctx.transform(t,r,a,n,o,i),e.shearX=t=>e.ctx.transform(1,0,e.tan(t),1,0,0),e.shearY=t=>e.ctx.transform(1,e.tan(t),0,1,0,0),e.resetMatrix=()=>{e.ctx.resetTransform(),e.scale(e._pixelDensity)},e.pushMatrix=()=>e.ctx.save(),e.popMatrix=()=>e.ctx.restore(),e.push=()=>{e.ctx.save(),e.pushStyles()},e.pop=()=>{e.ctx.restore(),e.popStyles()},e.createCapture=e=>{var t=document.createElement("video");return t.playsinline="playsinline",t.autoplay="autoplay",navigator.mediaDevices.getUserMedia(e).then((e=>{t.srcObject=e})),t.style.position="absolute",t.style.opacity=1e-5,t.style.zIndex=-1e3,document.body.append(t),t})},Q5.renderers.q2d.drawing=e=>{e._doStroke=!0,e._doFill=!0,e._strokeSet=!1,e._fillSet=!1,e._ellipseMode=e.CENTER,e._rectMode=e.CORNER,e._curveDetail=20,e._curveAlpha=0;let t=!0,r=[];function a(){e._doFill&&e.ctx.fill(),e._doStroke&&e.ctx.stroke()}function n(t,r,n,o,i,s,l,d){if(!e._doFill&&!e._doStroke)return;let c=e._angleMode,h=c?360:e.TAU;if((i%=h)<0&&(i+=h),(s%=h)<0&&(s+=h),0!=i||0!=s){if(i>s&&([i,s]=[s,i]),e.ctx.beginPath(),n==o)c&&(i=e.radians(i),s=e.radians(s)),e.ctx.arc(t,r,n/2,i,s);else{for(let a=0;ae.ctx.globalCompositeOperation=t,e.strokeCap=t=>e.ctx.lineCap=t,e.strokeJoin=t=>e.ctx.lineJoin=t,e.ellipseMode=t=>e._ellipseMode=t,e.rectMode=t=>e._rectMode=t,e.curveDetail=t=>e._curveDetail=t,e.curveAlpha=t=>e._curveAlpha=t,e.curveTightness=t=>e._curveAlpha=t,e.background=function(t){e.ctx.save(),e.ctx.resetTransform(),t.canvas?e.image(t,0,0,e.width,e.height):(Q5.Color&&!t._q5Color&&("string"!=typeof t?t=e.color(...arguments):e._namedColors[t]&&(t=e.color(...e._namedColors[t]))),e.ctx.fillStyle=t.toString(),e.ctx.fillRect(0,0,e.canvas.width,e.canvas.height)),e.ctx.restore()},e.line=(t,r,a,n)=>{e._doStroke&&(e._da&&(t*=e._da,r*=e._da,a*=e._da,n*=e._da),e.ctx.beginPath(),e.ctx.moveTo(t,r),e.ctx.lineTo(a,n),e.ctx.stroke())},e.arc=(t,r,a,o,i,s,l,d=25)=>{if(i==s)return e.ellipse(t,r,a,o);l??=e.PIE,e._ellipseMode==e.CENTER?n(t,r,a,o,i,s,l,d):e._ellipseMode==e.RADIUS?n(t,r,2*a,2*o,i,s,l,d):e._ellipseMode==e.CORNER?n(t+a/2,r+o/2,a,o,i,s,l,d):e._ellipseMode==e.CORNERS&&n((t+a)/2,(r+o)/2,a-t,o-r,i,s,l,d)},e.ellipse=(t,r,a,n)=>{n??=a,e._ellipseMode==e.CENTER?o(t,r,a,n):e._ellipseMode==e.RADIUS?o(t,r,2*a,2*n):e._ellipseMode==e.CORNER?o(t+a/2,r+n/2,a,n):e._ellipseMode==e.CORNERS&&o((t+a)/2,(r+n)/2,a-t,n-r)},e.circle=(t,r,n)=>{e._ellipseMode==e.CENTER?(e._da&&(t*=e._da,r*=e._da,n*=e._da),e.ctx.beginPath(),e.ctx.arc(t,r,n/2,0,e.TAU),a()):e.ellipse(t,r,n,n)},e.point=(t,r)=>{t.x&&(r=t.y,t=t.x),e._da&&(t*=e._da,r*=e._da),e.ctx.save(),e.ctx.beginPath(),e.ctx.arc(t,r,e.ctx.lineWidth/2,0,e.TAU),e.ctx.fillStyle=e.ctx.strokeStyle,e.ctx.fill(),e.ctx.restore()},e.rect=(t,r,a,n=a,o,s,l,d)=>{e._rectMode==e.CENTER?i(t-a/2,r-n/2,a,n,o,s,l,d):e._rectMode==e.RADIUS?i(t-a,r-n,2*a,2*n,o,s,l,d):e._rectMode==e.CORNER?i(t,r,a,n,o,s,l,d):e._rectMode==e.CORNERS&&i(t,r,a-t,n-r,o,s,l,d)},e.square=(t,r,a,n,o,i,s)=>e.rect(t,r,a,a,n,o,i,s),e.beginShape=()=>{r=[],e.ctx.beginPath(),t=!0},e.beginContour=()=>{e.ctx.closePath(),r=[],t=!0},e.endContour=()=>{r=[],t=!0},e.vertex=(a,n)=>{e._da&&(a*=e._da,n*=e._da),r=[],t?e.ctx.moveTo(a,n):e.ctx.lineTo(a,n),t=!1},e.bezierVertex=(t,a,n,o,i,s)=>{e._da&&(t*=e._da,a*=e._da,n*=e._da,o*=e._da,i*=e._da,s*=e._da),r=[],e.ctx.bezierCurveTo(t,a,n,o,i,s)},e.quadraticVertex=(t,a,n,o)=>{e._da&&(t*=e._da,a*=e._da,n*=e._da,o*=e._da),r=[],e.ctx.quadraticCurveTo(t,a,n,o)},e.bezier=(t,r,a,n,o,i,s,l)=>{e.beginShape(),e.vertex(t,r),e.bezierVertex(a,n,o,i,s,l),e.endShape()},e.triangle=(t,r,a,n,o,i)=>{e.beginShape(),e.vertex(t,r),e.vertex(a,n),e.vertex(o,i),e.endShape(e.CLOSE)},e.quad=(t,r,a,n,o,i,s,l)=>{e.beginShape(),e.vertex(t,r),e.vertex(a,n),e.vertex(o,i),e.vertex(s,l),e.endShape(e.CLOSE)},e.endShape=t=>{r=[],t&&e.ctx.closePath(),a()},e.curveVertex=(a,n)=>{if(e._da&&(a*=e._da,n*=e._da),r.push([a,n]),r.length<4)return;let o=function(e,t,r,a,n,o,i,s,l,d){function c(e,t,r,a,n,o){let i=Math.pow(a-t,2)+Math.pow(n-r,2);return Math.pow(i,.5*o)+e}let h=[],u=c(0,e,t,r,a,d),p=c(u,r,a,n,o,d),f=c(p,n,o,i,s,d);for(let d=0;d0?(g[e]=1,g[e+1]=0):(g[e]=0,g[e+1]=1));let _=e*g[0]+r*g[1],x=t*g[0]+a*g[1],m=r*g[2]+n*g[3],v=a*g[2]+o*g[3],y=n*g[4]+i*g[5],w=o*g[4]+s*g[5],b=_*g[6]+m*g[7],S=x*g[6]+v*g[7],M=m*g[8]+y*g[9],C=v*g[8]+w*g[9],R=b*g[2]+M*g[3],Q=S*g[2]+C*g[3];h.push([R,Q])}return h}(...r.at(-4),...r.at(-3),...r.at(-2),...r.at(-1),e._curveDetail,e._curveAlpha);for(let r=0;r{e.beginShape(),e.curveVertex(t,r),e.curveVertex(a,n),e.curveVertex(o,i),e.curveVertex(s,l),e.endShape()},e.curvePoint=(e,t,r,a,n)=>{const o=n*n*n,i=n*n;return e*(-.5*o+i-.5*n)+t*(1.5*o-2.5*i+1)+r*(-1.5*o+2*i+.5*n)+a*(.5*o-.5*i)},e.bezierPoint=(e,t,r,a,n)=>{const o=1-n;return Math.pow(o,3)*e+3*Math.pow(o,2)*n*t+3*o*Math.pow(n,2)*r+Math.pow(n,3)*a},e.curveTangent=(e,t,r,a,n)=>{const o=n*n;return e*(-3*o/2+2*n-.5)+t*(9*o/2-5*n)+r*(-9*o/2+4*n+.5)+a*(3*o/2-n)},e.bezierTangent=(e,t,r,a,n)=>{const o=1-n;return 3*a*Math.pow(n,2)-3*r*Math.pow(n,2)+6*r*o*n-6*t*o*n+3*t*Math.pow(o,2)-3*e*Math.pow(o,2)},e.erase=function(t=255,r=255){e.ctx.save(),e.ctx.globalCompositeOperation="destination-out",e.ctx.fillStyle=`rgba(0, 0, 0, ${t/255})`,e.ctx.strokeStyle=`rgba(0, 0, 0, ${r/255})`},e.noErase=function(){e.ctx.globalCompositeOperation="source-over",e.ctx.restore()},e.inFill=(t,r)=>{const a=e._pixelDensity;return e.ctx.isPointInPath(t*a,r*a)},e.inStroke=(t,r)=>{const a=e._pixelDensity;return e.ctx.isPointInStroke(t*a,r*a)}},Q5.renderers.q2d.image=(e,t)=>{Q5.Image??=class{constructor(e,t,r){let a=this;a._scope="image",a.canvas=a.ctx=a.drawingContext=null,a.pixels=[],Q5.modules.canvas(a,a);let n=Q5.renderers.q2d;for(let e of["canvas","image","soft_filters"])n[e]&&n[e](a,a);a._pixelDensity=r.pixelDensity||1,a.createCanvas(e,t,r),delete a.createCanvas,a._loop=!1}get w(){return this.width}get h(){return this.height}},e.createImage=(t,r,a)=>(a??={},a.alpha??=!0,a.colorSpace??=e.canvas.colorSpace||Q5.canvasOptions.colorSpace,new Q5.Image(t,r,a)),e.loadImage=function(r,a,n){if(r.canvas)return r;if("gif"==r.slice(-3).toLowerCase())throw new Error("q5 doesn't support GIFs due to their impact on performance. Use a video or animation instead.");t._preloadCount++;let o=[...arguments].at(-1);n="object"==typeof o?o:null;let i=e.createImage(1,1,n);function s(e){i.resize(e.naturalWidth||e.width,e.naturalHeight||e.height),i.ctx.drawImage(e,0,0),t._preloadCount--,a&&a(i)}if(Q5._nodejs&&global.CairoCanvas)global.CairoCanvas.loadImage(r).then(s).catch((e=>{throw t._preloadCount--,e}));else{let e=new window.Image;e.src=r,e.crossOrigin="Anonymous",e._pixelDensity=1,e.onload=()=>s(e),e.onerror=e=>{throw t._preloadCount--,e}}return i},e.imageMode=t=>e._imageMode=t,e.image=(t,r,a,n,o,i=0,s=0,l,d)=>{let c=t.canvas||t;Q5._createNodeJSCanvas&&(c=c.context.canvas),n??=t.width||t.videoWidth,o??=t.height||t.videoHeight,"center"==e._imageMode&&(r-=.5*n,a-=.5*o),e._da&&(r*=e._da,a*=e._da,n*=e._da,o*=e._da,i*=e._da,s*=e._da,l*=e._da,d*=e._da);let h=t._pixelDensity||1;l?l*=h:l=c.width||c.videoWidth,d?d*=h:d=c.height||c.videoHeight,e.ctx.drawImage(c,i*h,s*h,l,d,r,a,n,o),e._tint&&(e.ctx.globalCompositeOperation="multiply",e.ctx.fillStyle=e._tint.toString(),e.ctx.fillRect(r,a,n,o),e.ctx.globalCompositeOperation="source-over")},e._tint=null;let r=null;e._softFilter=()=>{throw new Error("Load q5-2d-soft-filters.js to use software filters.")},e.filter=(t,r)=>{if(!e.ctx.filter)return e._softFilter(t,r);if("string"==typeof t)f=t;else if(t==Q5.GRAY)f="saturate(0%)";else if(t==Q5.INVERT)f="invert(100%)";else if(t==Q5.BLUR){let t=Math.ceil(r*e._pixelDensity)||1;f=`blur(${t}px)`}else{if(t!=Q5.THRESHOLD)return e._softFilter(t,r);{r??=.5;let e=Math.floor(.5/Math.max(r,1e-5)*100);f=`saturate(0%) brightness(${e}%) contrast(1000000%)`}}e.ctx.filter=f,e.ctx.drawImage(e.canvas,0,0,e.canvas.w,e.canvas.h),e.ctx.filter="none"},"image"==e._scope&&(e.resize=(t,r)=>{let a=new e._OffscreenCanvas(e.canvas.width,e.canvas.height);a.getContext("2d",{colorSpace:e.canvas.colorSpace}).drawImage(e.canvas,0,0),e._setCanvasSize(t,r),e.ctx.clearRect(0,0,e.canvas.width,e.canvas.height),e.ctx.drawImage(a,0,0,e.canvas.width,e.canvas.height)}),e._getImageData=(t,r,a,n)=>e.ctx.getImageData(t,r,a,n,{colorSpace:e.canvas.colorSpace}),e.trim=()=>{let t=e._pixelDensity||1,r=e.canvas.width,a=e.canvas.height,n=e._getImageData(0,0,r,a).data,o=r,i=0,s=a,l=0,d=3;for(let e=0;ei&&(i=t),el&&(l=e)),d+=4;return s=Math.floor(s/t),l=Math.floor(l/t),o=Math.floor(o/t),i=Math.floor(i/t),e.get(o,s,i-o+1,l-s+1)},e.mask=t=>{e.ctx.save(),e.ctx.resetTransform();let r=e.ctx.globalCompositeOperation;e.ctx.globalCompositeOperation="destination-in",e.ctx.drawImage(t.canvas,0,0),e.ctx.globalCompositeOperation=r,e.ctx.restore()},e.get=(t,r,a,n)=>{let o=e._pixelDensity||1;if(void 0!==t&&void 0===a){let a=e._getImageData(t*o,r*o,1,1).data;return new e.Color(a[0],a[1],a[2],a[3]/255)}t=(t||0)*o,r=(r||0)*o;let i=a=a||e.width,s=n=n||e.height;a*=o,n*=o;let l=e.createImage(a,n),d=e._getImageData(t,r,a,n);return l.ctx.putImageData(d,0,0),l._pixelDensity=o,l.width=i,l.height=s,l},e.set=(t,r,a)=>{if(a.canvas){let n=e._tint;return e._tint=null,e.image(a,t,r),void(e._tint=n)}e.pixels.length||e.loadPixels();let n=e._pixelDensity||1;for(let o=0;o{r=e._getImageData(0,0,e.canvas.width,e.canvas.height),t.pixels=r.data},e.updatePixels=()=>{null!=r&&e.ctx.putImageData(r,0,0)},e.smooth=()=>e.ctx.imageSmoothingEnabled=!0,e.noSmooth=()=>e.ctx.imageSmoothingEnabled=!1,"image"!=e._scope&&(e.tint=function(t){e._tint=t._q5Color?t:e.color(...arguments)},e.noTint=()=>e._tint=null)},Q5.THRESHOLD=1,Q5.GRAY=2,Q5.OPAQUE=3,Q5.INVERT=4,Q5.POSTERIZE=5,Q5.DILATE=6,Q5.ERODE=7,Q5.BLUR=8,Q5.renderers.q2d.text=(e,t)=>{e._textAlign="left",e._textBaseline="alphabetic",e._textSize=12;let r="sans-serif",a=!1,n=15,o=3,i="normal",s=!1,l=0,d=[],c=!1,h=!1,u=0,p=12e3,f=e._textCache={};e.loadFont=(e,r)=>{t._preloadCount++;let a=e.split("/").pop().split(".")[0].replace(" ",""),n=new FontFace(a,`url(${e})`);return document.fonts.add(n),n.load().then((()=>{t._preloadCount--,r&&r(a)})),a},e.textFont=e=>{r=e,s=!0,l=-1},e.textSize=t=>{if(void 0===t)return e._textSize;e._da&&(t*=e._da),e._textSize=t,s=!0,l=-1,a||(n=1.25*t,o=n-t)},e.textStyle=e=>{i=e,s=!0,l=-1},e.textLeading=t=>{if(void 0===t)return n;e._da&&(t*=e._da),n=t,o=t-e._textSize,a=!0,l=-1},e.textAlign=(t,r)=>{e.ctx.textAlign=e._textAlign=t,r&&(e.ctx.textBaseline=e._textBaseline=r==e.CENTER?"middle":r)},e.textWidth=t=>e.ctx.measureText(t).width,e.textAscent=t=>e.ctx.measureText(t).actualBoundingBoxAscent,e.textDescent=t=>e.ctx.measureText(t).actualBoundingBoxDescent,e.textFill=e.fill,e.textStroke=e.stroke;e.textCache=(e,t)=>(t&&(p=t),void 0!==e&&(c=e),c),e.createTextImage=(t,r,a)=>(h=!0,img=e.text(t,0,0,r,a),h=!1,img);let g=[];e.text=(t,a,_,x,m)=>{if(void 0===t||!e._doFill&&!e._doStroke)return;t=t.toString(),e._da&&(a*=e._da,_*=e._da);let v,y,w,b,S=e.ctx;if(s&&(S.font=`${i} ${e._textSize}px ${r}`,s=!1),(c||h)&&(-1==l&&(()=>{let t=r+e._textSize+i+n,a=5381;for(let e=0;e>>0})(),v=f[t],v&&(v=v[l]),v)){if(v._fill==e._fill&&v._stroke==e._stroke&&v._strokeWeight==e._strokeWeight)return h?v:e.textImage(v,a,_);v.clear()}if(-1==t.indexOf("\n")?g[0]=t:g=t.split("\n"),t.length>x){let e=[];for(let t of g){let r=0;for(;r=t.length){e.push(t.slice(r));break}let n=t.lastIndexOf(" ",a);(-1===n||nm)break;if(g.length=0,e._fillSet||(S.fillStyle=b),c||h){if(d.push(l),(f[t]??={})[l]=v,u++,u>p){let e=Math.ceil(u/2),t=d.splice(0,e);for(let e in f){e=f[e];for(let r of t)delete e[r]}u-=e}if(h)return v;e.textImage(v,a,_)}},e.textImage=(t,r,a)=>{let n=e._imageMode;e._imageMode="corner";let o=e._textAlign;"center"==o?r-=t.canvas.hw:"right"==o&&(r-=t.width);let i=e._textBaseline;"alphabetic"==i?a-=t._leading:"middle"==i?a-=t._middle:"bottom"==i?a-=t._bottom:"top"==i&&(a-=t._top),e.image(t,r,a),e._imageMode=n},e.nf=(e,t,r)=>{let a=e<0,n=(e=Math.abs(e)).toFixed(r).split(".");n[0]=n[0].padStart(t,"0");let o=n.join(".");return a&&(o="-"+o),o}},Q5.modules.ai=e=>{e.askAI=(e="")=>{throw Error("Ask AI ✨ "+e)},e._aiErrorAssistance=async t=>{let r=t.message?.includes("Ask AI ✨");if(Q5.disableFriendlyErrors&&!r)return;!r&&Q5.errorTolerant||e.noLoop();let a=t.stack?.split("\n");if(!t.stack||a.length<=1)return;let n=1,o="(";for(-1==navigator.userAgent.indexOf("Chrome")&&(n=0,o="@");a[n].indexOf("q5")>=0;)n++;let i=a[n].split(o).at(-1);i=i.split(":");let s=parseInt(i.at(-2));r&&s++;let l=i.slice(0,-2).join(":"),d=l.split("/").at(-1);try{let e=(await(await fetch(l)).text()).split("\n"),a=e[s-1].trim(),n="",o=1;for(;n.length<1600&&(s-o>=0&&(n=e[s-o].trim()+"\n"+n),s+o10?t.message.slice(10):"Whats+wrong+with+this+line%3F+short+answer")+(r?"":"%0A%0A"+encodeURIComponent(t.name+": "+t.message))+"%0A%0ALine%3A+"+encodeURIComponent(a)+"%0A%0AExcerpt+for+context%3A%0A%0A"+encodeURIComponent(n);if(console.warn("Error in "+d+" on line "+s+":\n\n"+a),console.warn("Ask AI ✨ "+i),r)return window.open(i,"_blank")}catch(e){}}},Q5.modules.color=(e,t)=>{e.RGB=e.RGBA=e._colorMode="rgb",e.OKLCH="oklch",e.colorMode=(r,a)=>{e._colorMode=r;let n="srgb"==e.canvas.colorSpace||"srgb"==r;if(a??=n?"integer":"float",e._colorFormat="float"==a||1==a?1:255,"oklch"==r)t.Color=Q5.ColorOKLCH;else{let r="srgb"==e.canvas.colorSpace;255==e._colorFormat?t.Color=r?Q5.ColorRGBA_8:Q5.ColorRGBA_P3_8:t.Color=r?Q5.ColorRGBA:Q5.ColorRGBA_P3,e._colorMode="rgb"}},e._namedColors={aqua:[0,255,255],black:[0,0,0],blue:[0,0,255],brown:[165,42,42],crimson:[220,20,60],cyan:[0,255,255],darkviolet:[148,0,211],gold:[255,215,0],green:[0,128,0],gray:[128,128,128],grey:[128,128,128],hotpink:[255,105,180],indigo:[75,0,130],khaki:[240,230,140],lightgreen:[144,238,144],lime:[0,255,0],magenta:[255,0,255],navy:[0,0,128],orange:[255,165,0],olive:[128,128,0],peachpuff:[255,218,185],pink:[255,192,203],purple:[128,0,128],red:[255,0,0],skyblue:[135,206,235],tan:[210,180,140],turquoise:[64,224,208],transparent:[0,0,0,0],white:[255,255,255],violet:[238,130,238],yellow:[255,255,0]},e.color=(t,r,a,n)=>{let o=e.Color;if(t._q5Color)return new o(...t.levels);if(null==r){if("string"==typeof t){if("#"==t[0])t.length<=5?(t.length>4&&(n=parseInt(t[4]+t[4],16)),a=parseInt(t[3]+t[3],16),r=parseInt(t[2]+t[2],16),t=parseInt(t[1]+t[1],16)):(t.length>7&&(n=parseInt(t.slice(7,9),16)),a=parseInt(t.slice(5,7),16),r=parseInt(t.slice(3,5),16),t=parseInt(t.slice(1,3),16));else{if(!e._namedColors[t])return console.error("q5 can't parse color: "+t+"\nOnly numeric input, hex, and common named colors are supported."),new o(0,0,0);[t,r,a,n]=e._namedColors[t]}1==e._colorFormat&&(t/=255,r&&(r/=255),a&&(a/=255),n&&(n/=255))}Array.isArray(t)&&([t,r,a,n]=t)}return null==a?new o(t,t,t,r):new o(t,r,a,n)},e.red=e=>e.r,e.green=e=>e.g,e.blue=e=>e.b,e.alpha=e=>e.a,e.lightness=e=>e.l?e.l:100*(.2126*e.r+.7152*e.g+.0722*e.b)/255,e.hue=t=>{if(t.h)return t.h;let r=t.r,a=t.g,n=t.b;255==e._colorFormat&&(r/=255,a/=255,n/=255);let o,i=Math.max(r,a,n),s=Math.min(r,a,n);return o=i==s?0:i==r?60*(a-n)/(i-s):i==a?60*(n-r)/(i-s)+120:60*(r-a)/(i-s)+240,o<0&&(o+=360),o},e.lerpColor=(t,r,a)=>{if("rgb"==e._colorMode)return new e.Color(e.constrain(e.lerp(t.r,r.r,a),0,255),e.constrain(e.lerp(t.g,r.g,a),0,255),e.constrain(e.lerp(t.b,r.b,a),0,255),e.constrain(e.lerp(t.a,r.a,a),0,255));{let n=r.h-t.h;n>180&&(n-=360),n<-180&&(n+=360);let o=t.h+a*n;return o<0&&(o+=360),o>360&&(o-=360),new e.Color(e.constrain(e.lerp(t.l,r.l,a),0,100),e.constrain(e.lerp(t.c,r.c,a),0,100),o,e.constrain(e.lerp(t.a,r.a,a),0,255))}}},Q5.Color=class{constructor(){this._q5Color=!0}},Q5.ColorOKLCH=class extends Q5.Color{constructor(e,t,r,a){super(),this.l=e,this.c=t,this.h=r,this.a=a??1}toString(){return`oklch(${this.l} ${this.c} ${this.h} / ${this.a})`}},Q5.ColorRGBA=class extends Q5.Color{constructor(e,t,r,a){super(),this.r=e,this.g=t,this.b=r,this.a=a??1}get levels(){return[this.r,this.g,this.b,this.a]}toString(){return`color(srgb ${this.r} ${this.g} ${this.b} / ${this.a})`}},Q5.ColorRGBA_P3=class extends Q5.ColorRGBA{toString(){return`color(display-p3 ${this.r} ${this.g} ${this.b} / ${this.a})`}},Q5.ColorRGBA_8=class extends Q5.ColorRGBA{constructor(e,t,r,a){super(e,t,r,a??255)}setRed(e){this.r=e}setGreen(e){this.g=e}setBlue(e){this.b=e}setAlpha(e){this.a=e}get levels(){return[this.r,this.g,this.b,this.a]}toString(){return`rgb(${this.r} ${this.g} ${this.b} / ${this.a/255})`}},Q5.ColorRGBA_P3_8=class extends Q5.ColorRGBA{constructor(e,t,r,a){super(e,t,r,a??255),this._edited=!0}get r(){return this._r}set r(e){this._r=e,this._edited=!0}get g(){return this._g}set g(e){this._g=e,this._edited=!0}get b(){return this._b}set b(e){this._b=e,this._edited=!0}get a(){return this._a}set a(e){this._a=e,this._edited=!0}toString(){if(this._edited){let e=(this._r/255).toFixed(3),t=(this._g/255).toFixed(3),r=(this._b/255).toFixed(3),a=(this._a/255).toFixed(3);this._css=`color(display-p3 ${e} ${t} ${r} / ${a})`,this._edited=!1}return this._css}},Q5.modules.display=e=>{if(!e.canvas||"graphics"==e._scope)return;let t=e.canvas;e.CENTERED="centered",e.FULLSCREEN="fullscreen",e.MAXED="maxed",e.PIXELATED="pixelated",0!=Q5._instanceCount||Q5._nodejs||document.head.insertAdjacentHTML("beforeend",""),e._adjustDisplay=()=>{let r=t.style,a=t.parentElement;r&&a&&t.displayMode&&("pixelated"==t.renderQuality&&(t.classList.add("q5-pixelated"),e.pixelDensity(1),e.noSmooth&&e.noSmooth(),e.textFont&&e.textFont("monospace")),"normal"==t.displayMode?(a.classList.remove("q5-centered","q5-maxed","q5-fullscreen"),r.width=t.w*t.displayScale+"px",r.height=t.h*t.displayScale+"px"):(a.classList.add("q5-"+t.displayMode),a=a.getBoundingClientRect(),t.w/t.h>a.width/a.height?("centered"==t.displayMode?(r.width=t.w*t.displayScale+"px",r.maxWidth="100%"):r.width="100%",r.height="auto",r.maxHeight=""):(r.width="auto",r.maxWidth="","centered"==t.displayMode?(r.height=t.h*t.displayScale+"px",r.maxHeight="100%"):r.height="100%")))},e.displayMode=(r="normal",a="default",n=1)=>{"string"==typeof n&&(n=parseFloat(n.slice(1))),Object.assign(t,{displayMode:r,renderQuality:a,displayScale:n}),e._adjustDisplay()},e.fullscreen=e=>{if(void 0===e)return document.fullscreenElement;e?document.body.requestFullscreen():document.body.exitFullscreen()}},Q5.modules.input=(e,t)=>{if("graphics"==e._scope)return;e.mouseX=0,e.mouseY=0,e.pmouseX=0,e.pmouseY=0,e.touches=[],e.mouseButton=null,e.keyIsPressed=!1,e.mouseIsPressed=!1,e.key=null,e.keyCode=null,e.UP_ARROW=38,e.DOWN_ARROW=40,e.LEFT_ARROW=37,e.RIGHT_ARROW=39,e.SHIFT=16,e.TAB=9,e.BACKSPACE=8,e.ENTER=e.RETURN=13,e.ALT=e.OPTION=18,e.CONTROL=17,e.DELETE=46,e.ESCAPE=27,e.ARROW="default",e.CROSS="crosshair",e.HAND="pointer",e.MOVE="move",e.TEXT="text";let r={},a=[e.LEFT,e.CENTER,e.RIGHT],n=e.canvas;function o(t){const r=e.canvas.getBoundingClientRect(),a=e.canvas.scrollWidth/e.width||1,n=e.canvas.scrollHeight/e.height||1;return{x:(t.clientX-r.left)/a,y:(t.clientY-r.top)/n,id:t.identifier}}if(e._startAudio=()=>{e.getAudioContext&&"suspended"==e.getAudioContext()?.state&&e.userStartAudio()},e._updateMouse=r=>{if(!r.changedTouches)if(n){let a=n.getBoundingClientRect(),o=n.scrollWidth/e.width||1,i=n.scrollHeight/e.height||1;t.mouseX=(r.clientX-a.left)/o,t.mouseY=(r.clientY-a.top)/i,"webgpu"==n.renderer&&(t.mouseX-=n.hw,t.mouseY-=n.hh)}else t.mouseX=r.clientX,t.mouseY=r.clientY},e._onmousedown=r=>{e._startAudio(),e._updateMouse(r),t.mouseIsPressed=!0,t.mouseButton=a[r.button],e.mousePressed(r)},e._onmousemove=t=>{e._updateMouse(t),e.mouseIsPressed?e.mouseDragged(t):e.mouseMoved(t)},e._onmouseup=r=>{e._updateMouse(r),t.mouseIsPressed=!1,e.mouseReleased(r)},e._onclick=r=>{e._updateMouse(r),t.mouseIsPressed=!0,e.mouseClicked(r),t.mouseIsPressed=!1},e.cursor=(t,r,a)=>{let n="";t.includes(".")&&(t=`url("${t}")`,n=", auto"),void 0!==r&&(t+=" "+r+" "+a),e.canvas.style.cursor=t+n},e.noCursor=()=>{e.canvas.style.cursor="none"},e.requestPointerLock=document.body?.requestPointerLock,e.exitPointerLock=document.exitPointerLock,e._onkeydown=a=>{a.repeat||(e._startAudio(),t.keyIsPressed=!0,t.key=a.key,t.keyCode=a.keyCode,r[e.keyCode]=r[e.key.toLowerCase()]=!0,e.keyPressed(a),1==a.key.length&&e.keyTyped(a))},e._onkeyup=a=>{t.keyIsPressed=!1,t.key=a.key,t.keyCode=a.keyCode,r[e.keyCode]=r[e.key.toLowerCase()]=!1,e.keyReleased(a)},e.keyIsDown=e=>!!r["string"==typeof e?e.toLowerCase():e],e._ontouchstart=r=>{e._startAudio(),t.touches=[...r.touches].map(o),e._isTouchAware||(t.mouseX=e.touches[0].x,t.mouseY=e.touches[0].y,t.mouseIsPressed=!0,t.mouseButton=e.LEFT,e.mousePressed(r)||r.preventDefault()),e.touchStarted(r)||r.preventDefault()},e._ontouchmove=r=>{t.touches=[...r.touches].map(o),e._isTouchAware||(t.mouseX=e.touches[0].x,t.mouseY=e.touches[0].y,e.mouseDragged(r)||r.preventDefault()),e.touchMoved(r)||r.preventDefault()},e._ontouchend=r=>{t.touches=[...r.touches].map(o),e._isTouchAware||e.touches.length||(t.mouseIsPressed=!1,e.mouseReleased(r)||r.preventDefault()),e.touchEnded(r)||r.preventDefault()},n&&(n.addEventListener("mousedown",(t=>e._onmousedown(t))),n.addEventListener("mouseup",(t=>e._onmouseup(t))),n.addEventListener("click",(t=>e._onclick(t))),n.addEventListener("touchstart",(t=>e._ontouchstart(t))),n.addEventListener("touchmove",(t=>e._ontouchmove(t))),n.addEventListener("touchcancel",(t=>e._ontouchend(t))),n.addEventListener("touchend",(t=>e._ontouchend(t)))),window){let t=window.addEventListener;t("mousemove",(t=>e._onmousemove(t)),!1),t("keydown",(t=>e._onkeydown(t)),!1),t("keyup",(t=>e._onkeyup(t)),!1)}},Q5.modules.math=(e,t)=>{e.RADIANS=0,e.DEGREES=1,e.PI=Math.PI,e.HALF_PI=Math.PI/2,e.QUARTER_PI=Math.PI/4,e.abs=Math.abs,e.ceil=Math.ceil,e.exp=Math.exp,e.floor=Math.floor,e.loge=Math.log,e.mag=Math.hypot,e.max=Math.max,e.min=Math.min,e.round=Math.round,e.pow=Math.pow,e.sqrt=Math.sqrt,e.SHR3=1,e.LCG=2;let r=0;e.angleMode=t=>{"radians"==t&&(t=0),r=e._angleMode=t};let a=e._DEGTORAD=Math.PI/180,n=e._RADTODEG=180/Math.PI;function o(){let e,t,r=4294967295;return{setSeed(a){e=t=(a??Math.random()*r)>>>0},getSeed:()=>t,rand:()=>(e^=e<<17,e^=e>>13,e^=e<<5,(e>>>0)/r)}}e.degrees=t=>t*e._RADTODEG,e.radians=t=>t*e._DEGTORAD,e.map=Q5.prototype.map=(e,t,r,a,n,o)=>{let i=a+1*(e-t)/(r-t)*(n-a);return o?ae*(1-r)+t*r,e.constrain=(e,t,r)=>Math.min(Math.max(e,t),r),e.dist=function(){let e=arguments;return 4==e.length?Math.hypot(e[0]-e[2],e[1]-e[3]):Math.hypot(e[0]-e[3],e[1]-e[4],e[2]-e[5])},e.norm=(t,r,a)=>e.map(t,r,a,0,1),e.sq=e=>e*e,e.fract=e=>e-Math.floor(e),e.sin=e=>Math.sin(r?e*a:e),e.cos=e=>Math.cos(r?e*a:e),e.tan=e=>Math.tan(r?e*a:e),e.asin=e=>{let t=Math.asin(e);return r?t*n:t},e.acos=e=>{let t=Math.acos(e);return r?t*n:t},e.atan=e=>{let t=Math.atan(e);return r?t*n:t},e.atan2=(e,t)=>{let a=Math.atan2(e,t);return r?a*n:a};let i=o();i.setSeed(),e.randomSeed=e=>i.setSeed(e),e.random=(e,t)=>void 0===e?i.rand():"number"==typeof e?void 0!==t?i.rand()*(t-e)+e:i.rand()*e:e[Math.trunc(e.length*i.rand())],e.randomGenerator=t=>{t==e.LCG?i=function(){const e=4294967296;let t,r;return{setSeed(a){r=t=(a??Math.random()*e)>>>0},getSeed:()=>t,rand:()=>(r=(1664525*r+1013904223)%e,r/e)}}():t==e.SHR3&&(i=o()),i.setSeed()};var s=new function(){var e,t,r,a=new Array(128),n=new Array(256),o=new Array(128),s=new Array(128),l=new Array(256),d=new Array(256),c=()=>4294967296*i.rand()-2147483648,h=()=>.5+2.328306e-10*(c()|0),u=()=>{for(var t,n,i,l,d=3.44262;;){if(t=r*o[e],0==e){do{i=h(),l=h(),t=.2904764*-Math.log(i),n=-Math.log(l)}while(n+n0?d+t:-d-t}if(s[e]+h()*(s[e-1]-s[e]){for(var r;;){if(0==e)return 7.69711-Math.log(h());if(r=t*l[e],d[e]+h()*(d[e-1]-d[e])(r=c(),e=127&r,Math.abs(r)(t=c()>>>0){var e,t,r=2147483648,i=4294967296,c=3.442619855899,h=c,u=.00991256303526217,p=7.697117470131487,f=p,g=.003949659822581572;for(e=u/Math.exp(-.5*c*c),a[0]=Math.floor(c/e*r),a[1]=0,o[0]=e/r,o[127]=c/r,s[0]=1,s[127]=Math.exp(-.5*c*c),t=126;t>=1;t--)c=Math.sqrt(-2*Math.log(u/c+Math.exp(-.5*c*c))),a[t+1]=Math.floor(c/h*r),h=c,s[t]=Math.exp(-.5*c*c),o[t]=c/r;for(e=g/Math.exp(-p),n[0]=Math.floor(p/e*i),n[1]=0,l[0]=e/i,l[255]=p/i,d[0]=1,d[255]=Math.exp(-p),t=254;t>=1;t--)p=-Math.log(g/p+Math.exp(-p)),n[t+1]=Math.floor(p/f*i),f=p,d[t]=Math.exp(-p),l[t]=p/i}};let l;s.hasInit=!1,e.randomGaussian=(e,t)=>(s.hasInit||(s.zigset(),s.hasInit=!0),s.RNOR()*t+e),e.randomExponential=()=>(s.hasInit||(s.zigset(),s.hasInit=!0),s.REXP()),e.PERLIN="perlin",e.SIMPLEX="simplex",e.BLOCKY="blocky",e.Noise=Q5.PerlinNoise,e.noiseMode=e=>{t.Noise=Q5[e[0].toUpperCase()+e.slice(1)+"Noise"],l=null},e.noiseSeed=t=>{l=new e.Noise(t)},e.noise=(t=0,r=0,a=0)=>(l??=new e.Noise,l.noise(t,r,a)),e.noiseDetail=(t,r)=>{l??=new e.Noise,t>0&&(l.octaves=t),r>0&&(l.falloff=r)}},Q5.Noise=class{},Q5.PerlinNoise=class extends Q5.Noise{constructor(e){super(),this.grad3=[[1,1,0],[-1,1,0],[1,-1,0],[-1,-1,0],[1,0,1],[-1,0,1],[1,0,-1],[-1,0,-1],[0,1,1],[0,-1,1],[0,1,-1],[0,-1,-1]],this.octaves=1,this.falloff=.5,this.p=null==e?Array.from({length:256},(()=>Math.floor(256*Math.random()))):this.seedPermutation(e),this.p=this.p.concat(this.p)}seedPermutation(e){let t,r,a=[];for(let e=0;e<256;e++)a[e]=e;for(let n=255;n>0;n--)t=(e=16807*e%2147483647)%(n+1),r=a[n],a[n]=a[t],a[t]=r;return a}dot(e,t,r,a){return e[0]*t+e[1]*r+e[2]*a}mix(e,t,r){return(1-r)*e+r*t}fade(e){return e*e*e*(e*(6*e-15)+10)}noise(e,t,r){let a=this,n=0,o=1,i=1,s=0;for(let l=0;l{e.Sound=Q5.Sound,e.loadSound=(e,r)=>{t._preloadCount++,Q5.aud??=new window.AudioContext;let a=new Q5.Sound(e,r);return a.addEventListener("canplaythrough",(()=>{t._preloadCount--,a.loaded=!0,r&&r(a)})),a},e.getAudioContext=()=>Q5.aud,e.userStartAudio=()=>Q5.aud.resume()},Q5.Sound=class extends Audio{constructor(e){super(e);let t=this;t.load(),t.panner=Q5.aud.createStereoPanner(),t.source=Q5.aud.createMediaElementSource(t),t.source.connect(t.panner),t.panner.connect(Q5.aud.destination),Object.defineProperty(t,"pan",{get:()=>t.panner.pan.value,set:e=>t.panner.pan.value=e})}setVolume(e){this.volume=e}setLoop(e){this.loop=e}setPan(e){this.pan=e}isLoaded(){return this.loaded}isPlaying(){return!this.paused}},Q5.modules.util=(e,t)=>{e._loadFile=(r,a,n)=>{t._preloadCount++;let o={};return fetch(r).then((e=>"json"==n?e.json():e.text())).then((r=>{t._preloadCount--,"csv"==n&&(r=e.CSV.parse(r)),Object.assign(o,r),a&&a(r)})),o},e.loadStrings=(t,r)=>e._loadFile(t,r,"text"),e.loadJSON=(t,r)=>e._loadFile(t,r,"json"),e.loadCSV=(t,r)=>e._loadFile(t,r,"csv"),e.CSV={},e.CSV.parse=(e,t=",",r="\n")=>{let a=[],n=e.split(r),o=n[0].split(t);for(let e=1;er[e]=JSON.parse(i[t]))),a.push(r)}return a},"object"==typeof localStorage&&(e.storeItem=localStorage.setItem,e.getItem=localStorage.getItem,e.removeItem=localStorage.removeItem,e.clearStorage=localStorage.clear),e.year=()=>(new Date).getFullYear(),e.day=()=>(new Date).getDay(),e.hour=()=>(new Date).getHours(),e.minute=()=>(new Date).getMinutes(),e.second=()=>(new Date).getSeconds()},Q5.modules.vector=e=>{e.createVector=(t,r,a)=>new Q5.Vector(t,r,a,e)},Q5.Vector=class{constructor(e,t,r,a){this.x=e||0,this.y=t||0,this.z=r||0,this._$=a||window,this._cn=null,this._cnsq=null}set(e,t,r){return this.x=e?.x||e||0,this.y=e?.y||t||0,this.z=e?.z||r||0,this}copy(){return new Q5.Vector(this.x,this.y,this.z)}_arg2v(e,t,r){return void 0!==e?.x?e:void 0!==t?{x:e,y:t,z:r||0}:{x:e,y:e,z:e}}_calcNorm(){this._cnsq=this.x*this.x+this.y*this.y+this.z*this.z,this._cn=Math.sqrt(this._cnsq)}add(){let e=this._arg2v(...arguments);return this.x+=e.x,this.y+=e.y,this.z+=e.z,this}rem(){let e=this._arg2v(...arguments);return this.x%=e.x,this.y%=e.y,this.z%=e.z,this}sub(){let e=this._arg2v(...arguments);return this.x-=e.x,this.y-=e.y,this.z-=e.z,this}mult(){let e=this._arg2v(...arguments);return this.x*=e.x,this.y*=e.y,this.z*=e.z,this}div(){let e=this._arg2v(...arguments);return e.x?this.x/=e.x:this.x=0,e.y?this.y/=e.y:this.y=0,e.z?this.z/=e.z:this.z=0,this}mag(){return this._calcNorm(),this._cn}magSq(){return this._calcNorm(),this._cnsq}dot(){let e=this._arg2v(...arguments);return this.x*e.x+this.y*e.y+this.z*e.z}dist(){let e=this._arg2v(...arguments),t=this.x-e.x,r=this.y-e.y,a=this.z-e.z;return Math.sqrt(t*t+r*r+a*a)}cross(){let e=this._arg2v(...arguments),t=this.y*e.z-this.z*e.y,r=this.z*e.x-this.x*e.z,a=this.x*e.y-this.y*e.x;return this.x=t,this.y=r,this.z=a,this}normalize(){this._calcNorm();let e=this._cn;return 0!=e&&(this.x/=e,this.y/=e,this.z/=e),this._cn=1,this._cnsq=1,this}limit(e){this._calcNorm();let t=this._cn;if(t>e){let r=e/t;this.x*=r,this.y*=r,this.z*=r,this._cn=e,this._cnsq=e*e}return this}setMag(e){this._calcNorm();let t=e/this._cn;return this.x*=t,this.y*=t,this.z*=t,this._cn=e,this._cnsq=e*e,this}heading(){return this._$.atan2(this.y,this.x)}setHeading(e){let t=this.mag();return this.x=t*this._$.cos(e),this.y=t*this._$.sin(e),this}rotate(e){let t=this._$.cos(e),r=this._$.sin(e),a=this.x*t-this.y*r,n=this.x*r+this.y*t;return this.x=a,this.y=n,this}angleBetween(){let e=this._arg2v(...arguments),t=Q5.Vector.cross(this,e);return this._$.atan2(t.mag(),this.dot(e))*Math.sign(t.z||1)}lerp(){let e=[...arguments],t=e.at(-1);if(0==t)return this;let r=this._arg2v(...e.slice(0,-1));return this.x+=(r.x-this.x)*t,this.y+=(r.y-this.y)*t,this.z+=(r.z-this.z)*t,this}slerp(){let e=[...arguments],t=e.at(-1);if(0==t)return this;let r=this._arg2v(...e.slice(0,-1));if(1==t)return this.set(r);let a=this.mag(),n=r.mag();if(0==a||0==n)return this.mult(1-t).add(r.mult(t));let o=Q5.Vector.cross(this,r),i=o.mag(),s=Math.atan2(i,this.dot(r));if(i>0)o.div(i);else{if(se.copy().add(t),Q5.Vector.cross=(e,t)=>e.copy().cross(t),Q5.Vector.dist=(e,t)=>Math.hypot(e.x-t.x,e.y-t.y,e.z-t.z),Q5.Vector.div=(e,t)=>e.copy().div(t),Q5.Vector.dot=(e,t)=>e.copy().dot(t),Q5.Vector.equals=(e,t,r)=>e.equals(t,r),Q5.Vector.lerp=(e,t,r)=>e.copy().lerp(t,r),Q5.Vector.slerp=(e,t,r)=>e.copy().slerp(t,r),Q5.Vector.limit=(e,t)=>e.copy().limit(t),Q5.Vector.heading=e=>this._$.atan2(e.y,e.x),Q5.Vector.magSq=e=>e.x*e.x+e.y*e.y+e.z*e.z,Q5.Vector.mag=e=>Math.sqrt(Q5.Vector.magSq(e)),Q5.Vector.mult=(e,t)=>e.copy().mult(t),Q5.Vector.normalize=e=>e.copy().normalize(),Q5.Vector.rem=(e,t)=>e.copy().rem(t),Q5.Vector.sub=(e,t)=>e.copy().sub(t);for(let e of["fromAngle","fromAngles","random2D","random3D"])Q5.Vector[e]=(t,r,a)=>(new Q5.Vector)[e](t,r,a);Q5.renderers.webgpu={},Q5.renderers.webgpu.canvas=(e,t)=>{let r,a=e.canvas;a.width=e.width=500,a.height=e.height=500,e.colorMode&&e.colorMode("rgb","float"),e.pipelines=[];let n=e.drawStack=[],o=e.colorsStack=[1,1,1,1];e._transformLayout=Q5.device.createBindGroupLayout({label:"transformLayout",entries:[{binding:0,visibility:GPUShaderStage.VERTEX,buffer:{type:"uniform",hasDynamicOffset:!1}},{binding:1,visibility:GPUShaderStage.VERTEX,buffer:{type:"read-only-storage",hasDynamicOffset:!1}}]}),e.bindGroupLayouts=[e._transformLayout];let i=Q5.device.createBuffer({size:8,usage:GPUBufferUsage.UNIFORM|GPUBufferUsage.COPY_DST});e._createCanvas=(r,n,o)=>(t.ctx=t.drawingContext=a.getContext("webgpu"),o.format??=navigator.gpu.getPreferredCanvasFormat(),o.device??=Q5.device,e.ctx.configure(o),Q5.device.queue.writeBuffer(i,0,new Float32Array([e.canvas.hw,e.canvas.hh])),a),e._resizeCanvas=(t,r)=>{e._setCanvasSize(t,r)};let s=0,l=(t,r,a,n=1)=>{"string"==typeof t?t=e.color(t):null==a&&(n=r??1,r=a=t),t._q5Color?o.push(t.r,t.g,t.b,t.a):o.push(t,r,a,n),s++};e._fillIndex=e._strokeIndex=-1,e.fill=(t,r,a,n)=>{l(t,r,a,n),e._doFill=!0,e._fillIndex=s},e.stroke=(t,r,a,n)=>{l(t,r,a,n),e._doStroke=!0,e._strokeIndex=s},e.noFill=()=>e._doFill=!1,e.noStroke=()=>e._doStroke=!1,e._strokeWeight=1,e.strokeWeight=t=>e._strokeWeight=Math.abs(t),e.resetMatrix=()=>{e._matrix=[1,0,0,0,0,1,0,0,0,0,1,0,0,0,0,1],e._transformIndex=0},e.resetMatrix(),e._matrixDirty=!1,e.transformStates=[e._matrix.slice()],e._transformIndexStack=[],e.translate=(t,r,a)=>{(t||r||a)&&(e._matrix[12]+=t,e._matrix[13]-=r,e._matrix[14]+=a||0,e._matrixDirty=!0)},e.rotate=t=>{if(!t)return;e._angleMode&&(t*=e._DEGTORAD);let r=Math.cos(t),a=Math.sin(t),n=e._matrix,o=n[0],i=n[1],s=n[4],l=n[5];o||i||s||l?(n[0]=o*r+s*a,n[1]=i*r+l*a,n[4]=s*r-o*a,n[5]=l*r-i*a):(n[0]=r,n[1]=a,n[4]=-a,n[5]=r),e._matrixDirty=!0},e.scale=(t=1,r,a=1)=>{r??=t;let n=e._matrix;n[0]*=t,n[1]*=t,n[2]*=t,n[3]*=t,n[4]*=r,n[5]*=r,n[6]*=r,n[7]*=r,n[8]*=a,n[9]*=a,n[10]*=a,n[11]*=a,e._matrixDirty=!0},e.shearX=t=>{if(!t)return;e._angleMode&&(t*=e._DEGTORAD);let r=Math.tan(t),a=e._matrix[0],n=e._matrix[1],o=e._matrix[4],i=e._matrix[5];e._matrix[0]=a+o*r,e._matrix[1]=n+i*r,e._matrixDirty=!0},e.shearY=t=>{if(!t)return;e._angleMode&&(t*=e._DEGTORAD);let r=Math.tan(t),a=e._matrix[0],n=e._matrix[1],o=e._matrix[4],i=e._matrix[5];e._matrix[4]=o+a*r,e._matrix[5]=i+n*r,e._matrixDirty=!0},e.applyMatrix=(...t)=>{let r;if(r=1==t.length?t[0]:t,9==r.length)r=[r[0],r[1],0,r[2],r[3],r[4],0,r[5],0,0,1,0,r[6],r[7],0,r[8]];else if(16!=r.length)throw new Error("Matrix must be a 3x3 or 4x4 array.");e._matrix=r.slice(),e._matrixDirty=!0},e._saveMatrix=()=>{e.transformStates.push(e._matrix.slice()),e._transformIndex=e.transformStates.length-1,e._matrixDirty=!1},e.pushMatrix=()=>{e._matrixDirty&&e._saveMatrix(),e._transformIndexStack.push(e._transformIndex)},e.popMatrix=()=>{if(!e._transformIndexStack.length)return console.warn("Matrix index stack is empty!");let t=e._transformIndexStack.pop();e._matrix=e.transformStates[t].slice(),e._transformIndex=t,e._matrixDirty=!1},e.push=()=>{e.pushMatrix(),e.pushStyles()},e.pop=()=>{e.popMatrix(),e.popStyles()},e._calcBox=(e,t,r,a,n)=>{let o,i,s,l,d=r/2,c=a/2;return n&&"corner"!=n?"center"==n?(o=e-d,i=e+d,s=-(t-c),l=-(t+c)):(o=e,i=r,s=-t,l=-a):(o=e,i=e+r,s=-t,l=-(t+a)),[o,i,s,l]},e.clear=()=>{},e._beginRender=()=>{e.encoder=Q5.device.createCommandEncoder(),r=t.pass=e.encoder.beginRenderPass({label:"q5-webgpu",colorAttachments:[{view:ctx.getCurrentTexture().createView(),loadOp:"clear",storeOp:"store"}]})},e._render=()=>{if(transformStates.length>1||!e._transformBindGroup){let t=Q5.device.createBuffer({size:64*transformStates.length,usage:GPUBufferUsage.STORAGE|GPUBufferUsage.COPY_DST});Q5.device.queue.writeBuffer(t,0,new Float32Array(transformStates.flat())),e._transformBindGroup=Q5.device.createBindGroup({layout:e._transformLayout,entries:[{binding:0,resource:{buffer:i}},{binding:1,resource:{buffer:t}}]})}r.setBindGroup(0,e._transformBindGroup);for(let t of e._hooks.preRender)t();let t=0,a=0,o=0,s=-1;for(let i=0;i{r.end();let a=e.encoder.finish();Q5.device.queue.submit([a]),t.pass=e.encoder=null,e.drawStack.length=0,e.colorsStack.length=4,s=0,rotation=0,e.transformStates.length=1,e._transformIndexStack.length=0}},Q5.webgpu=async function(e,t){if(e&&"global"!=e||(Q5._hasGlobal=!0),!navigator.gpu){console.warn("q5 WebGPU not supported on this browser!");let r=new Q5(e,t);return r.colorMode("rgb",1),r._beginRender=()=>r.translate(r.canvas.hw,r.canvas.hh),r}let r=await navigator.gpu.requestAdapter();if(!r)throw new Error("No appropriate GPUAdapter found.");return Q5.device=await r.requestDevice(),new Q5(e,t,"webgpu")},Q5.renderers.webgpu.drawing=(e,t)=>{let r,a,n=e.canvas,o=e.drawStack,i=e.colorsStack,s=[],l=Q5.device.createShaderModule({label:"drawingVertexShader",code:"\nstruct VertexOutput {\n\t@builtin(position) position: vec4f,\n\t@location(0) colorIndex: f32\n};\n\nstruct Uniforms {\n\thalfWidth: f32,\n\thalfHeight: f32\n};\n\n@group(0) @binding(0) var uniforms: Uniforms;\n@group(0) @binding(1) var transforms: array>;\n\n@vertex\nfn vertexMain(@location(0) pos: vec2f, @location(1) colorIndex: f32, @location(2) transformIndex: f32) -> VertexOutput {\n\tvar vert = vec4f(pos, 0.0, 1.0);\n\tvert = transforms[i32(transformIndex)] * vert;\n\tvert.x /= uniforms.halfWidth;\n\tvert.y /= uniforms.halfHeight;\n\n\tvar output: VertexOutput;\n\toutput.position = vert;\n\toutput.colorIndex = colorIndex;\n\treturn output;\n}\n"}),d=Q5.device.createShaderModule({label:"drawingFragmentShader",code:"\n@group(1) @binding(0) var colors : array;\n\n@fragment\nfn fragmentMain(@location(0) colorIndex: f32) -> @location(0) vec4f {\n\tlet index = i32(colorIndex);\n\treturn mix(colors[index], colors[index + 1], fract(colorIndex));\n}\n"});a=Q5.device.createBindGroupLayout({label:"colorsLayout",entries:[{binding:0,visibility:GPUShaderStage.FRAGMENT,buffer:{type:"read-only-storage",hasDynamicOffset:!1}}]}),e.bindGroupLayouts.push(a);let c={arrayStride:16,attributes:[{format:"float32x2",offset:0,shaderLocation:0},{format:"float32",offset:8,shaderLocation:1},{format:"float32",offset:12,shaderLocation:2}]},h=["zero","one","src-alpha","one-minus-src-alpha","dst","dst-alpha","one-minus-dst-alpha","one-minus-src"],u=["add","subtract","reverse-subtract","min","max"];const p={normal:[2,3,0,2,3,0],lighter:[2,1,0,2,1,0],subtract:[2,1,2,2,1,2],multiply:[4,0,0,5,0,0],screen:[1,3,0,1,3,0],darken:[1,3,3,1,3,3],lighten:[1,3,4,1,3,4],overlay:[2,3,0,2,3,0],hard_light:[2,3,0,2,3,0],soft_light:[2,3,0,2,3,0],difference:[2,3,2,2,3,2],exclusion:[2,3,0,2,3,0],color_dodge:[1,7,0,1,7,0],color_burn:[6,1,0,6,1,0],linear_dodge:[2,1,0,2,1,0],linear_burn:[2,7,1,2,7,1],vivid_light:[2,7,0,2,7,0],pin_light:[2,7,0,2,7,0],hard_mix:[2,7,0,2,7,0]};e.blendConfigs={};for(const[t,r]of Object.entries(p))e.blendConfigs[t]={color:{srcFactor:h[r[0]],dstFactor:h[r[1]],operation:u[r[2]]},alpha:{srcFactor:h[r[3]],dstFactor:h[r[4]],operation:u[r[5]]}};e._blendMode="normal",e.blendMode=t=>{t!=e._blendMode&&("source-over"==t&&(t="normal"),t=t.toLowerCase().replace(/[ -]/g,"_"),e._blendMode=t,e.pipelines[0]=e._createPipeline(e.blendConfigs[t]))};let f,g=Q5.device.createPipelineLayout({label:"drawingPipelineLayout",bindGroupLayouts:e.bindGroupLayouts});e._createPipeline=e=>Q5.device.createRenderPipeline({label:"drawingPipeline",layout:g,vertex:{module:l,entryPoint:"vertexMain",buffers:[c]},fragment:{module:d,entryPoint:"fragmentMain",targets:[{format:"bgra8unorm",blend:e}]},primitive:{topology:"triangle-list"}}),e.pipelines[0]=e._createPipeline(e.blendConfigs.normal),e.beginShape=()=>{f=[]},e.vertex=(t,r)=>{e._matrixDirty&&e._saveMatrix(),f.push(t,-r,e._fillIndex,e._transformIndex)},e.endShape=t=>{if(!e._doFill)return void(f=[]);let r=f;if(r.length<12)throw new Error("A shape must have at least 3 vertices.");t&&r.push(r[0],r[1],r[2],r[3]);let a=[];for(let e=4;e{e.beginShape(),e.vertex(t,r),e.vertex(a,n),e.vertex(o,i),e.endShape(1)},e.quad=(t,r,a,n,o,i,s,l)=>{e.beginShape(),e.vertex(t,r),e.vertex(a,n),e.vertex(o,i),e.vertex(s,l),e.endShape(1)},e.rectMode=t=>e._rectMode=t,e.rect=(t,a,n,i)=>{let[l,d,c,h]=e._calcBox(t,a,n,i,e._rectMode),u=r??e._fillIndex;e._matrixDirty&&e._saveMatrix();let p=e._transformIndex;s.push(l,c,u,p,d,c,u,p,l,h,u,p,d,c,u,p,l,h,u,p,d,h,u,p),o.push(0,6)},e.square=(t,r,a)=>e.rect(t,r,a,a),e.point=(t,a)=>{r=e._strokeIndex;let n=e._strokeWeight;n<2?(n=Math.round(n),e.rect(t,a,n,n)):e.ellipse(t,a,n,n),r=null},e.line=(t,a,n,o)=>{r=e._strokeIndex,e.push(),e.translate(t,a),e.rotate(e.atan2(o-a,n-t));let i=Math.sqrt((n-t)**2+(o-a)**2),s=e._strokeWeight;e.rect(0,-s/2,i,s),e.pop(),r=null},e.background=(t,r,a,o)=>{if(e.push(),e.resetMatrix(),t.src){let r=e._imageMode;e._imageMode="corner",e.image(t,-n.hw,-n.hh,n.w,n.h),e._imageMode=r}else{let i=e._rectMode;e._rectMode="corner",e.fill(t,r,a,o),e.rect(-n.hw,-n.hh,n.w,n.h),e._rectMode=i}e.pop()};e.ellipseMode=t=>e._ellipseMode=t,e.ellipse=(t,a,n,i)=>{const l=(d=n==i?n:Math.max(n,i))<4?6:d<6?8:d<10?10:d<16?12:d<20?14:d<22?16:d<24?18:d<28?20:d<34?22:d<42?24:d<48?26:d<56?28:d<64?30:d<72?32:d<84?34:d<96?36:d<98?38:d<113?40:d<149?44:d<199?48:d<261?52:d<353?56:d<461?60:d<585?64:d<1200?70:d<1800?80:d<2400?90:100;var d;let c=Math.max(n,1)/2,h=n==i?c:Math.max(i,1)/2,u=0;const p=e.TAU/l,f=r??e._fillIndex;e._matrixDirty&&e._saveMatrix();const g=e._transformIndex;let _,x,m,v;for(let e=0;e<=l;e++)_=m,x=v,m=t+c*Math.cos(u),v=a+h*Math.sin(u),u+=p,0!=e&&s.push(t,a,f,g,_,x,f,g,m,v,f,g);o.push(0,3*l)},e.circle=(t,r,a)=>e.ellipse(t,r,a,a),e._hooks.preRender.push((()=>{e.pass.setPipeline(e.pipelines[0]);const t=new Float32Array(s),r=Q5.device.createBuffer({size:t.byteLength,usage:GPUBufferUsage.VERTEX|GPUBufferUsage.COPY_DST});Q5.device.queue.writeBuffer(r,0,t),e.pass.setVertexBuffer(0,r);const n=Q5.device.createBuffer({size:4*i.length,usage:GPUBufferUsage.STORAGE|GPUBufferUsage.COPY_DST});Q5.device.queue.writeBuffer(n,0,new Float32Array(i)),e._colorsBindGroup=Q5.device.createBindGroup({layout:a,entries:[{binding:0,resource:{buffer:n,offset:0,size:4*i.length}}]}),e.pass.setBindGroup(1,e._colorsBindGroup)})),e._hooks.postRender.push((()=>{s.length=0}))},Q5.renderers.webgpu.image=(e,t)=>{e._textureBindGroups=[];let r=[],a=Q5.device.createShaderModule({label:"imageVertexShader",code:"\nstruct VertexOutput {\n\t@builtin(position) position: vec4f,\n\t@location(0) texCoord: vec2f\n};\n\nstruct Uniforms {\n\thalfWidth: f32,\n\thalfHeight: f32\n};\n\n@group(0) @binding(0) var uniforms: Uniforms;\n@group(0) @binding(1) var transforms: array>;\n\n@vertex\nfn vertexMain(@location(0) pos: vec2f, @location(1) texCoord: vec2f, @location(2) transformIndex: f32) -> VertexOutput {\n\tvar vert = vec4f(pos, 0.0, 1.0);\n\tvert = transforms[i32(transformIndex)] * vert;\n\tvert.x /= uniforms.halfWidth;\n\tvert.y /= uniforms.halfHeight;\n\n\tvar output: VertexOutput;\n\toutput.position = vert;\n\toutput.texCoord = texCoord;\n\treturn output;\n}\n\t"}),n=Q5.device.createShaderModule({label:"imageFragmentShader",code:"\n@group(2) @binding(0) var samp: sampler;\n@group(2) @binding(1) var texture: texture_2d;\n\n@fragment\nfn fragmentMain(@location(0) texCoord: vec2f) -> @location(0) vec4f {\n\t// Sample the texture using the interpolated texture coordinate\n\treturn textureSample(texture, samp, texCoord);\n}\n\t"}),o=Q5.device.createBindGroupLayout({label:"textureLayout",entries:[{binding:0,visibility:GPUShaderStage.FRAGMENT,sampler:{type:"filtering"}},{binding:1,visibility:GPUShaderStage.FRAGMENT,texture:{viewDimension:"2d",sampleType:"float"}}]});const i=Q5.device.createPipelineLayout({label:"imagePipelineLayout",bindGroupLayouts:[...e.bindGroupLayouts,o]});e.pipelines[1]=Q5.device.createRenderPipeline({label:"imagePipeline",layout:i,vertex:{module:a,entryPoint:"vertexMain",buffers:[{arrayStride:0,attributes:[]},{arrayStride:20,attributes:[{shaderLocation:0,offset:0,format:"float32x2"},{shaderLocation:1,offset:8,format:"float32x2"},{shaderLocation:2,offset:16,format:"float32"}]}]},fragment:{module:n,entryPoint:"fragmentMain",targets:[{format:"bgra8unorm",blend:e.blendConfigs?.normal||{color:{srcFactor:"src-alpha",dstFactor:"one-minus-src-alpha",operation:"add"},alpha:{srcFactor:"src-alpha",dstFactor:"one-minus-src-alpha",operation:"add"}}}]},primitive:{topology:"triangle-list"}});let s=Q5.device.createSampler({magFilter:"linear",minFilter:"linear"});e._textures=[];let l=0;e._createTexture=t=>{t.canvas&&(t=t.canvas);let r=[t.width,t.height,1],a=Q5.device.createTexture({size:r,format:"bgra8unorm",usage:GPUTextureUsage.TEXTURE_BINDING|GPUTextureUsage.COPY_DST|GPUTextureUsage.RENDER_ATTACHMENT});Q5.device.queue.copyExternalImageToTexture({source:t},{texture:a,colorSpace:e.canvas.colorSpace},r),e._textures[l]=a,t.textureIndex=l;const n=Q5.device.createBindGroup({layout:o,entries:[{binding:0,resource:s},{binding:1,resource:a.createView()}]});e._textureBindGroups[l]=n,l=(l+1)%12e3,e._textures[l]&&(e._textures[l].destroy(),delete e._textures[l],delete e._textureBindGroups[l])},e.loadImage=e.loadTexture=r=>{t._preloadCount++;const a=new Image;return a.crossOrigin="Anonymous",a.onload=()=>{e._createTexture(a),t._preloadCount--},a.src=r,a},e.imageMode=t=>e._imageMode=t,e.image=(t,a,n,o,i)=>{if(t.canvas&&(t=t.canvas),null==t.textureIndex)return;e._matrixDirty&&e._saveMatrix();let s=e._transformIndex;o??=t.width/e._pixelDensity,i??=t.height/e._pixelDensity;let[l,d,c,h]=e._calcBox(a,n,o,i,e._imageMode);r.push(l,c,0,0,s,d,c,1,0,s,l,h,0,1,s,d,c,1,0,s,l,h,0,1,s,d,h,1,1,s),e.drawStack.push(1,t.textureIndex)},e._hooks.preRender.push((()=>{if(!e._textureBindGroups.length)return;e.pass.setPipeline(e.pipelines[1]);const t=new Float32Array(r),a=Q5.device.createBuffer({size:t.byteLength,usage:GPUBufferUsage.VERTEX|GPUBufferUsage.COPY_DST});Q5.device.queue.writeBuffer(a,0,t),e.pass.setVertexBuffer(1,a)})),e._hooks.postRender.push((()=>{r.length=0}))},Q5.THRESHOLD=1,Q5.GRAY=2,Q5.OPAQUE=3,Q5.INVERT=4,Q5.POSTERIZE=5,Q5.DILATE=6,Q5.ERODE=7,Q5.BLUR=8,Q5.renderers.webgpu.text=(e,t)=>{let r=Q5.device.createShaderModule({label:"MSDF text shader",code:"\n// Positions for simple quad geometry\nconst pos = array(vec2f(0, -1), vec2f(1, -1), vec2f(0, 0), vec2f(1, 0));\n\nstruct VertexInput {\n\t@builtin(vertex_index) vertex : u32,\n\t@builtin(instance_index) instance : u32,\n};\nstruct VertexOutput {\n\t@builtin(position) position : vec4f,\n\t@location(0) texcoord : vec2f,\n\t@location(1) colorIndex : f32\n};\nstruct Char {\n\ttexOffset: vec2f,\n\ttexExtent: vec2f,\n\tsize: vec2f,\n\toffset: vec2f,\n};\nstruct Text {\n\tpos: vec2f,\n\tscale: f32,\n\ttransformIndex: f32,\n\tfillIndex: f32,\n\tstrokeIndex: f32\n};\nstruct Uniforms {\n\thalfWidth: f32,\n\thalfHeight: f32\n};\n\n@group(0) @binding(0) var uniforms: Uniforms;\n@group(0) @binding(1) var transforms: array>;\n\n@group(1) @binding(0) var colors : array;\n\n@group(2) @binding(0) var fontTexture: texture_2d;\n@group(2) @binding(1) var fontSampler: sampler;\n@group(2) @binding(2) var fontChars: array;\n\n@group(3) @binding(0) var textChars: array;\n@group(3) @binding(1) var textMetadata: array;\n\n@vertex\nfn vertexMain(input : VertexInput) -> VertexOutput {\n\tlet char = textChars[input.instance];\n\n\tlet text = textMetadata[i32(char.w)];\n\n\tlet fontChar = fontChars[i32(char.z)];\n\n\tlet charPos = ((pos[input.vertex] * fontChar.size + char.xy + fontChar.offset) * text.scale) + text.pos;\n\n\tvar vert = vec4f(charPos, 0.0, 1.0);\n\tvert = transforms[i32(text.transformIndex)] * vert;\n\tvert.x /= uniforms.halfWidth;\n\tvert.y /= uniforms.halfHeight;\n\n\tvar output : VertexOutput;\n\toutput.position = vert;\n\toutput.texcoord = (pos[input.vertex] * vec2f(1, -1)) * fontChar.texExtent + fontChar.texOffset;\n\toutput.colorIndex = text.fillIndex;\n\treturn output;\n}\n\nfn sampleMsdf(texcoord: vec2f) -> f32 {\n\tlet c = textureSample(fontTexture, fontSampler, texcoord);\n\treturn max(min(c.r, c.g), min(max(c.r, c.g), c.b));\n}\n\n@fragment\nfn fragmentMain(input : VertexOutput) -> @location(0) vec4f {\n\t// pxRange (AKA distanceRange) comes from the msdfgen tool,\n\t// uses the default which is 4.\n\tlet pxRange = 4.0;\n\tlet sz = vec2f(textureDimensions(fontTexture, 0));\n\tlet dx = sz.x*length(vec2f(dpdxFine(input.texcoord.x), dpdyFine(input.texcoord.x)));\n\tlet dy = sz.y*length(vec2f(dpdxFine(input.texcoord.y), dpdyFine(input.texcoord.y)));\n\tlet toPixels = pxRange * inverseSqrt(dx * dx + dy * dy);\n\tlet sigDist = sampleMsdf(input.texcoord) - 0.5;\n\tlet pxDist = sigDist * toPixels;\n\tlet edgeWidth = 0.5;\n\tlet alpha = smoothstep(-edgeWidth, edgeWidth, pxDist);\n\tif (alpha < 0.001) {\n\t\tdiscard;\n\t}\n\tlet fillColor = colors[i32(input.colorIndex)];\n\treturn vec4f(fillColor.rgb, fillColor.a * alpha);\n}\n"});class a{constructor(e,t,r,a,n){this.pipeline=e,this.bindGroup=t,this.lineHeight=r,this.chars=a,this.kernings=n;let o=Object.values(a);this.charCount=o.length,this.defaultChar=o[0]}getChar(e){return this.chars[e]??this.defaultChar}getXAdvance(e,t=-1){let r=this.getChar(e);if(t>=0){let a=this.kernings.get(e);if(a)return r.xadvance+(a.get(t)??0)}return r.xadvance}}let n=Q5.device.createBindGroupLayout({label:"MSDF text group layout",entries:[{binding:0,visibility:GPUShaderStage.VERTEX|GPUShaderStage.FRAGMENT,buffer:{type:"read-only-storage"}},{binding:1,visibility:GPUShaderStage.VERTEX|GPUShaderStage.FRAGMENT,buffer:{type:"read-only-storage"}}]}),o={},i=e.createGraphics(1,1);i.colorMode(e.RGB,1),e.loadFont=(s,l)=>{if("json"!=s.slice(s.lastIndexOf(".")+1))return i.loadFont(s,l);let d=s.slice(s.lastIndexOf("/")+1,s.lastIndexOf("-"));return(async(i,s,l)=>{t._preloadCount++;let d=await fetch(i);if(404==d.status)return t._preloadCount--,"";let c=await d.json(),h=i.lastIndexOf("/"),u=-1!=h?i.substring(0,h+1):"";d=await fetch(u+c.pages[0]);let p=await createImageBitmap(await d.blob()),f=[p.width,p.height,1],g=Q5.device.createTexture({label:`MSDF ${s}`,size:f,format:"rgba8unorm",usage:GPUTextureUsage.TEXTURE_BINDING|GPUTextureUsage.COPY_DST|GPUTextureUsage.RENDER_ATTACHMENT});Q5.device.queue.copyExternalImageToTexture({source:p},{texture:g},f),"string"==typeof c.chars&&(c.chars=e.CSV.parse(c.chars," "),c.kernings=e.CSV.parse(c.kernings," "));let _=c.chars.length,x=Q5.device.createBuffer({size:32*_,usage:GPUBufferUsage.STORAGE,mappedAtCreation:!0}),m=new Float32Array(x.getMappedRange()),v=1/c.common.scaleW,y=1/c.common.scaleH,w={},b=0;for(let[e,t]of c.chars.entries())w[t.id]=t,w[t.id].charIndex=e,m[b]=t.x*v,m[b+1]=t.y*y,m[b+2]=t.width*v,m[b+3]=t.height*y,m[b+4]=t.width,m[b+5]=t.height,m[b+6]=t.xoffset,m[b+7]=-t.yoffset,b+=8;x.unmap();let S=Q5.device.createSampler({minFilter:"linear",magFilter:"linear",mipmapFilter:"linear",maxAnisotropy:16}),M=Q5.device.createBindGroupLayout({label:"MSDF font group layout",entries:[{binding:0,visibility:GPUShaderStage.FRAGMENT,texture:{}},{binding:1,visibility:GPUShaderStage.FRAGMENT,sampler:{}},{binding:2,visibility:GPUShaderStage.VERTEX,buffer:{type:"read-only-storage"}}]}),C=Q5.device.createRenderPipeline({label:"msdf font pipeline",layout:Q5.device.createPipelineLayout({bindGroupLayouts:[...e.bindGroupLayouts,M,n]}),vertex:{module:r,entryPoint:"vertexMain"},fragment:{module:r,entryPoint:"fragmentMain",targets:[{format:"bgra8unorm",blend:{color:{srcFactor:"src-alpha",dstFactor:"one-minus-src-alpha"},alpha:{srcFactor:"one",dstFactor:"one"}}}]},primitive:{topology:"triangle-strip",stripIndexFormat:"uint32"}}),R=Q5.device.createBindGroup({label:"msdf font bind group",layout:M,entries:[{binding:0,resource:g.createView()},{binding:1,resource:S},{binding:2,resource:{buffer:x}}]}),Q=new Map;if(c.kernings)for(let e of c.kernings){let t=Q.get(e.first);t||(t=new Map,Q.set(e.first,t)),t.set(e.second,e.amount)}e._font=new a(C,R,c.common.lineHeight,w,Q),o[s]=e._font,e.pipelines[2]=e._font.pipeline,t._preloadCount--,l&&l(s)})(s,d,l),d},e._textSize=18,e._textAlign="left",e._textBaseline="alphabetic";let s=!1,l=22.5,d=4.5,c=1.25;e.textFont=t=>{e._font=o[t],e.drawStack.push(-1,(()=>{e._font=o[t],e.pipelines[2]=e._font.pipeline}))},e.textSize=t=>{e._textSize=t,s||(l=t*c,d=l-t)},e.textLeading=t=>{e._font.lineHeight=l=t,d=l-e._textSize,c=l/e._textSize,s=!0},e.textAlign=(t,r)=>{e._textAlign=t,r&&(e._textBaseline=r)},e._charStack=[],e._textStack=[];let h,u=(e,t,r)=>{let a=0,n=0,o=0,i=0,s=0,l=[],d=t.charCodeAt(0);for(let h=0;h{if(!e._font)return void(navigator.onLine&&!h&&(h=!0,e.loadFont("https://q5js.org/fonts/YaHei-msdf.json")));if(t.length>n){let e=[],r=0;for(;r=t.length){e.push(t.slice(r));break}let o=t.lastIndexOf(" ",a);(-1==o||o{let o=0;"center"==p?o=-.5*d.width- -.5*(d.width-d.lineWidths[a]):"right"==p&&(o=d.width-d.lineWidths[a]),c[_]=e+o,c[_+1]=t+r,c[_+2]=n.charIndex,c[_+3]=g,_+=4}))}else d=u(e._font,t,((e,t,r,a)=>{c[_]=e,c[_+1]=t,c[_+2]=a.charIndex,c[_+3]=g,_+=4})),"alphabetic"==f?a-=e._textSize:"center"==f?a-=.5*e._textSize:"bottom"==f&&(a-=l);e._charStack.push(c);let x=new Float32Array(6);e._matrixDirty&&e._saveMatrix(),x[0]=r,x[1]=-a,x[2]=e._textSize/44,x[3]=e._transformIndex,x[4]=e._fillIndex,x[5]=e._strokeIndex,e._textStack.push(x),e.drawStack.push(2,d.printedCharCount)},e.textWidth=t=>e._font?u(e._font,t).width:0,e.createTextImage=(t,r,a)=>{if(i.textSize(e._textSize),e._doFill){let t=4*e._fillIndex;i.fill(colorsStack.slice(t,t+4))}if(e._doStroke){let t=4*e._strokeIndex;i.stroke(colorsStack.slice(t,t+4))}let n=i.createTextImage(t,r,a);if(null==n.canvas.textureIndex)e._createTexture(n);else if(n.modified){let t=n.canvas,r=[t.width,t.height,1],a=e._textures[t.textureIndex];Q5.device.queue.copyExternalImageToTexture({source:t},{texture:a,colorSpace:e.canvas.colorSpace},r),n.modified=!1}return n},e.textImage=(t,r,a)=>{let n=e._imageMode;e._imageMode="corner";let o=e._textAlign;"center"==o?r-=t.canvas.hw:"right"==o&&(r-=t.width);let i=e._textBaseline;"alphabetic"==i?a-=t._leading:"center"==i?a-=t._middle:"bottom"==i?a-=t._bottom:"top"==i&&(a-=t._top),e.image(t,r,a),e._imageMode=n},e._hooks.preRender.push((()=>{if(!e._charStack.length)return;let t=0;for(let r of e._charStack)t+=4*r.length;let r=Q5.device.createBuffer({label:"charBuffer",size:t,usage:GPUBufferUsage.STORAGE|GPUBufferUsage.COPY_DST,mappedAtCreation:!0}),a=new Float32Array(r.getMappedRange()),o=0;for(let t of e._charStack)a.set(t,o),o+=t.length;r.unmap();let i=6*e._textStack.length*4,s=Q5.device.createBuffer({label:"textBuffer",size:i,usage:GPUBufferUsage.STORAGE|GPUBufferUsage.COPY_DST,mappedAtCreation:!0}),l=new Float32Array(s.getMappedRange());o=0;for(let t of e._textStack)l.set(t,o),o+=t.length;s.unmap(),e._textBindGroup=Q5.device.createBindGroup({label:"msdf text bind group",layout:n,entries:[{binding:0,resource:{buffer:r}},{binding:1,resource:{buffer:s}}]})})),e._hooks.postRender.push((()=>{e._charStack.length=0,e._textStack.length=0}))}; diff --git a/src/q5-2d-canvas.js b/src/q5-2d-canvas.js index 8e33fc5..6a67d38 100644 --- a/src/q5-2d-canvas.js +++ b/src/q5-2d-canvas.js @@ -138,13 +138,4 @@ Q5.renderers.q2d.canvas = ($, q) => { document.body.append(vid); return vid; }; - - if (window && $._scope != 'graphics') { - window.addEventListener('resize', () => { - $._shouldResize = true; - q.windowWidth = window.innerWidth; - q.windowHeight = window.innerHeight; - q.deviceOrientation = window.screen?.orientation?.type; - }); - } }; diff --git a/src/q5-2d-text.js b/src/q5-2d-text.js index 1877a1c..eb3f5d8 100644 --- a/src/q5-2d-text.js +++ b/src/q5-2d-text.js @@ -1,9 +1,10 @@ Q5.renderers.q2d.text = ($, q) => { $._textAlign = 'left'; $._textBaseline = 'alphabetic'; + $._textSize = 12; let font = 'sans-serif', - tSize = 12, + leadingSet = false, leading = 15, leadDiff = 3, emphasis = 'normal', @@ -35,12 +36,12 @@ Q5.renderers.q2d.text = ($, q) => { styleHash = -1; }; $.textSize = (x) => { - if (x === undefined) return tSize; + if (x === undefined) return $._textSize; if ($._da) x *= $._da; - tSize = x; + $._textSize = x; fontMod = true; styleHash = -1; - if (!$._leadingSet) { + if (!leadingSet) { leading = x * 1.25; leadDiff = leading - x; } @@ -54,8 +55,8 @@ Q5.renderers.q2d.text = ($, q) => { if (x === undefined) return leading; if ($._da) x *= $._da; leading = x; - leadDiff = x - tSize; - $._leadingSet = true; + leadDiff = x - $._textSize; + leadingSet = true; styleHash = -1; }; $.textAlign = (horiz, vert) => { @@ -63,7 +64,6 @@ Q5.renderers.q2d.text = ($, q) => { if (vert) { $.ctx.textBaseline = $._textBaseline = vert == $.CENTER ? 'middle' : vert; } - styleHash = -1; }; $.textWidth = (str) => $.ctx.measureText(str).width; @@ -74,7 +74,7 @@ Q5.renderers.q2d.text = ($, q) => { $.textStroke = $.stroke; let updateStyleHash = () => { - let styleString = font + tSize + emphasis + leading; + let styleString = font + $._textSize + emphasis + leading; let hash = 5381; for (let i = 0; i < styleString.length; i++) { @@ -107,7 +107,7 @@ Q5.renderers.q2d.text = ($, q) => { let img, tX, tY; if (fontMod) { - ctx.font = `${emphasis} ${tSize}px ${font}`; + ctx.font = `${emphasis} ${$._textSize}px ${font}`; fontMod = false; } @@ -128,7 +128,7 @@ Q5.renderers.q2d.text = ($, q) => { if (str.indexOf('\n') == -1) lines[0] = str; else lines = str.split('\n'); - if (w) { + if (str.length > w) { let wrapped = []; for (let line of lines) { let i = 0; @@ -140,11 +140,9 @@ Q5.renderers.q2d.text = ($, q) => { break; } let end = line.lastIndexOf(' ', max); - if (end === -1 || end < i) { - end = max; - } + if (end === -1 || end < i) end = max; wrapped.push(line.slice(i, end)); - i = end; + i = end + 1; } } lines = wrapped; @@ -172,6 +170,7 @@ Q5.renderers.q2d.text = ($, q) => { img._top = descent + leadDiff; img._middle = img._top + ascent * 0.5; img._bottom = img._top + ascent; + img._leading = leading; } img._fill = $._fill; @@ -231,7 +230,7 @@ Q5.renderers.q2d.text = ($, q) => { else if (ta == 'right') x -= img.width; let bl = $._textBaseline; - if (bl == 'alphabetic') y -= leading; + if (bl == 'alphabetic') y -= img._leading; else if (bl == 'middle') y -= img._middle; else if (bl == 'bottom') y -= img._bottom; else if (bl == 'top') y -= img._top; diff --git a/src/q5-canvas.js b/src/q5-canvas.js index dc52dd2..358844f 100644 --- a/src/q5-canvas.js +++ b/src/q5-canvas.js @@ -201,7 +201,7 @@ Q5.modules.canvas = ($, q) => { function parentResized() { if ($.frameCount > 1) { - $._shouldResize = true; + $._didResize = true; $._adjustDisplay(); } } @@ -261,14 +261,9 @@ Q5.modules.canvas = ($, q) => { '_imageMode', '_rectMode', '_ellipseMode', - '_textFont', - '_textLeading', - '_leadingSet', '_textSize', '_textAlign', - '_textBaseline', - '_textStyle', - '_textWrap' + '_textBaseline' ]; $._styles = []; @@ -281,6 +276,15 @@ Q5.modules.canvas = ($, q) => { let styles = $._styles.pop(); for (let s of $._styleNames) $[s] = styles[s]; }; + + if (window && $._scope != 'graphics') { + window.addEventListener('resize', () => { + $._didResize = true; + q.windowWidth = window.innerWidth; + q.windowHeight = window.innerHeight; + q.deviceOrientation = window.screen?.orientation?.type; + }); + } }; Q5.canvasOptions = { diff --git a/src/q5-core.js b/src/q5-core.js index 3ab9cc2..7e2c7b3 100644 --- a/src/q5-core.js +++ b/src/q5-core.js @@ -70,9 +70,9 @@ function Q5(scope, parent, renderer) { let ts = timestamp || performance.now(); $._lastFrameTime ??= ts - $._targetFrameDuration; - if ($._shouldResize) { + if ($._didResize) { $.windowResized(); - $._shouldResize = false; + $._didResize = false; } if ($._loop) looper = raf($._draw); diff --git a/src/q5-util.js b/src/q5-util.js index 3899ee7..33f8fd4 100644 --- a/src/q5-util.js +++ b/src/q5-util.js @@ -5,10 +5,11 @@ Q5.modules.util = ($, q) => { fetch(path) .then((r) => { if (type == 'json') return r.json(); - if (type == 'text') return r.text(); + return r.text(); }) .then((r) => { q._preloadCount--; + if (type == 'csv') r = $.CSV.parse(r); Object.assign(ret, r); if (cb) cb(r); }); @@ -17,6 +18,21 @@ Q5.modules.util = ($, q) => { $.loadStrings = (path, cb) => $._loadFile(path, cb, 'text'); $.loadJSON = (path, cb) => $._loadFile(path, cb, 'json'); + $.loadCSV = (path, cb) => $._loadFile(path, cb, 'csv'); + + $.CSV = {}; + $.CSV.parse = (csv, sep = ',', lineSep = '\n') => { + let a = [], + lns = csv.split(lineSep), + headers = lns[0].split(sep); + for (let i = 1; i < lns.length; i++) { + let o = {}, + ln = lns[i].split(sep); + headers.forEach((h, i) => (o[h] = JSON.parse(ln[i]))); + a.push(o); + } + return a; + }; if (typeof localStorage == 'object') { $.storeItem = localStorage.setItem; diff --git a/src/q5-webgpu-canvas.js b/src/q5-webgpu-canvas.js index dd132cc..c966218 100644 --- a/src/q5-webgpu-canvas.js +++ b/src/q5-webgpu-canvas.js @@ -24,7 +24,8 @@ Q5.renderers.webgpu.canvas = ($, q) => { // colors used for each draw call let colorsStack = ($.colorsStack = [1, 1, 1, 1]); - $._envLayout = Q5.device.createBindGroupLayout({ + $._transformLayout = Q5.device.createBindGroupLayout({ + label: 'transformLayout', entries: [ { binding: 0, @@ -33,14 +34,9 @@ Q5.renderers.webgpu.canvas = ($, q) => { type: 'uniform', hasDynamicOffset: false } - } - ] - }); - - $._transformLayout = Q5.device.createBindGroupLayout({ - entries: [ + }, { - binding: 0, + binding: 1, visibility: GPUShaderStage.VERTEX, buffer: { type: 'read-only-storage', @@ -50,9 +46,9 @@ Q5.renderers.webgpu.canvas = ($, q) => { ] }); - $.bindGroupLayouts = [$._envLayout, $._transformLayout]; + $.bindGroupLayouts = [$._transformLayout]; - const uniformBuffer = Q5.device.createBuffer({ + let uniformBuffer = Q5.device.createBuffer({ size: 8, // Size of two floats usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST }); @@ -67,18 +63,6 @@ Q5.renderers.webgpu.canvas = ($, q) => { Q5.device.queue.writeBuffer(uniformBuffer, 0, new Float32Array([$.canvas.hw, $.canvas.hh])); - $._envBindGroup = Q5.device.createBindGroup({ - layout: $._envLayout, - entries: [ - { - binding: 0, - resource: { - buffer: uniformBuffer - } - } - ] - }); - return c; }; @@ -88,7 +72,7 @@ Q5.renderers.webgpu.canvas = ($, q) => { // current color index, used to associate a vertex with a color let colorIndex = 0; - const addColor = (r, g, b, a = 1) => { + let addColor = (r, g, b, a = 1) => { if (typeof r == 'string') r = $.color(r); else if (b == undefined) { // grayscale mode `fill(1, 0.5)` @@ -100,6 +84,8 @@ Q5.renderers.webgpu.canvas = ($, q) => { colorIndex++; }; + $._fillIndex = $._strokeIndex = -1; + $.fill = (r, g, b, a) => { addColor(r, g, b, a); $._doFill = true; @@ -131,56 +117,70 @@ Q5.renderers.webgpu.canvas = ($, q) => { }; $.resetMatrix(); - // Boolean to track if the matrix has been modified + // tracks if the matrix has been modified $._matrixDirty = false; - // Array to store transformation matrices for the render pass + // array to store transformation matrices for the render pass $.transformStates = [$._matrix.slice()]; - // Stack to keep track of transformation matrix indexes + // stack to keep track of transformation matrix indexes $._transformIndexStack = []; $.translate = (x, y, z) => { if (!x && !y && !z) return; // Update the translation values - $._matrix[3] += x; - $._matrix[7] -= y; - $._matrix[11] += z || 0; + $._matrix[12] += x; + $._matrix[13] -= y; + $._matrix[14] += z || 0; $._matrixDirty = true; }; - $.rotate = (r) => { - if (!r) return; - if ($._angleMode) r *= $._DEGTORAD; + $.rotate = (a) => { + if (!a) return; + if ($._angleMode) a *= $._DEGTORAD; - let cosR = Math.cos(r); - let sinR = Math.sin(r); + let cosR = Math.cos(a); + let sinR = Math.sin(a); + + let m = $._matrix; + + let m0 = m[0], + m1 = m[1], + m4 = m[4], + m5 = m[5]; - let m0 = $._matrix[0], - m1 = $._matrix[1], - m4 = $._matrix[4], - m5 = $._matrix[5]; if (!m0 && !m1 && !m4 && !m5) { - $._matrix[0] = cosR; - $._matrix[1] = sinR; - $._matrix[4] = -sinR; - $._matrix[5] = cosR; + m[0] = cosR; + m[1] = sinR; + m[4] = -sinR; + m[5] = cosR; } else { - $._matrix[0] = m0 * cosR + m4 * sinR; - $._matrix[1] = m1 * cosR + m5 * sinR; - $._matrix[4] = m0 * -sinR + m4 * cosR; - $._matrix[5] = m1 * -sinR + m5 * cosR; + m[0] = m0 * cosR + m4 * sinR; + m[1] = m1 * cosR + m5 * sinR; + m[4] = m4 * cosR - m0 * sinR; + m[5] = m5 * cosR - m1 * sinR; } $._matrixDirty = true; }; - $.scale = (sx = 1, sy, sz = 1) => { - sy ??= sx; + $.scale = (x = 1, y, z = 1) => { + y ??= x; + + let m = $._matrix; - $._matrix[0] *= sx; - $._matrix[5] *= sy; - $._matrix[10] *= sz; + m[0] *= x; + m[1] *= x; + m[2] *= x; + m[3] *= x; + m[4] *= y; + m[5] *= y; + m[6] *= y; + m[7] *= y; + m[8] *= z; + m[9] *= z; + m[10] *= z; + m[11] *= z; $._matrixDirty = true; }; @@ -252,7 +252,7 @@ Q5.renderers.webgpu.canvas = ($, q) => { if (!$._transformIndexStack.length) { return console.warn('Matrix index stack is empty!'); } - // Pop the last matrix index from the stack and set it as the current matrix index + // Pop the last matrix index and set it as the current matrix index let idx = $._transformIndexStack.pop(); $._matrix = $.transformStates[idx].slice(); $._transformIndex = idx; @@ -275,7 +275,6 @@ Q5.renderers.webgpu.canvas = ($, q) => { // left, right, top, bottom let l, r, t, b; if (!mode || mode == 'corner') { - // CORNER l = x; r = x + w; t = -y; @@ -315,7 +314,7 @@ Q5.renderers.webgpu.canvas = ($, q) => { $._render = () => { if (transformStates.length > 1 || !$._transformBindGroup) { - const transformBuffer = Q5.device.createBuffer({ + let transformBuffer = Q5.device.createBuffer({ size: transformStates.length * 64, // Size of 16 floats usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST }); @@ -327,6 +326,12 @@ Q5.renderers.webgpu.canvas = ($, q) => { entries: [ { binding: 0, + resource: { + buffer: uniformBuffer + } + }, + { + binding: 1, resource: { buffer: transformBuffer } @@ -335,35 +340,47 @@ Q5.renderers.webgpu.canvas = ($, q) => { }); } - pass.setBindGroup(0, $._envBindGroup); - pass.setBindGroup(1, $._transformBindGroup); + pass.setBindGroup(0, $._transformBindGroup); for (let m of $._hooks.preRender) m(); let drawVertOffset = 0; let imageVertOffset = 0; + let textCharOffset = 0; let curPipelineIndex = -1; let curTextureIndex = -1; - pass.setPipeline($.pipelines[0]); - for (let i = 0; i < drawStack.length; i += 2) { let v = drawStack[i + 1]; + if (drawStack[i] == -1) { + v(); + continue; + } + if (curPipelineIndex != drawStack[i]) { curPipelineIndex = drawStack[i]; pass.setPipeline($.pipelines[curPipelineIndex]); } if (curPipelineIndex == 0) { - pass.draw(v, 1, drawVertOffset, 0); + // v is the number of vertices + pass.draw(v, 1, drawVertOffset); drawVertOffset += v; } else if (curPipelineIndex == 1) { if (curTextureIndex != v) { - pass.setBindGroup(3, $._textureBindGroups[v]); + // v is the texture index + pass.setBindGroup(2, $._textureBindGroups[v]); } - pass.draw(6, 1, imageVertOffset, 0); + pass.draw(6, 1, imageVertOffset); imageVertOffset += 6; + } else if (curPipelineIndex == 2) { + pass.setBindGroup(2, $._font.bindGroup); + pass.setBindGroup(3, $._textBindGroup); + + // v is the number of characters in the text + pass.draw(4, v, 0, textCharOffset); + textCharOffset += v; } } @@ -372,7 +389,7 @@ Q5.renderers.webgpu.canvas = ($, q) => { $._finishRender = () => { pass.end(); - const commandBuffer = $.encoder.finish(); + let commandBuffer = $.encoder.finish(); Q5.device.queue.submit([commandBuffer]); q.pass = $.encoder = null; diff --git a/src/q5-webgpu-drawing.js b/src/q5-webgpu-drawing.js index 7ea7f00..9a35097 100644 --- a/src/q5-webgpu-drawing.js +++ b/src/q5-webgpu-drawing.js @@ -12,8 +12,8 @@ Q5.renderers.webgpu.drawing = ($, q) => { label: 'drawingVertexShader', code: ` struct VertexOutput { - @builtin(position) position: vec4, - @location(1) colorIndex: f32 + @builtin(position) position: vec4f, + @location(0) colorIndex: f32 }; struct Uniforms { @@ -22,12 +22,12 @@ struct Uniforms { }; @group(0) @binding(0) var uniforms: Uniforms; -@group(1) @binding(0) var transforms: array>; +@group(0) @binding(1) var transforms: array>; @vertex -fn vertexMain(@location(0) pos: vec2, @location(1) colorIndex: f32, @location(2) transformIndex: f32) -> VertexOutput { - var vert = vec4(pos, 0.0, 1.0); - vert *= transforms[i32(transformIndex)]; +fn vertexMain(@location(0) pos: vec2f, @location(1) colorIndex: f32, @location(2) transformIndex: f32) -> VertexOutput { + var vert = vec4f(pos, 0.0, 1.0); + vert = transforms[i32(transformIndex)] * vert; vert.x /= uniforms.halfWidth; vert.y /= uniforms.halfHeight; @@ -42,17 +42,18 @@ fn vertexMain(@location(0) pos: vec2, @location(1) colorIndex: f32, @locati let fragmentShader = Q5.device.createShaderModule({ label: 'drawingFragmentShader', code: ` -@group(2) @binding(0) var uColors : array>; +@group(1) @binding(0) var colors : array; @fragment -fn fragmentMain(@location(1) colorIndex: f32) -> @location(0) vec4 { - let index = u32(colorIndex); - return mix(uColors[index], uColors[index + 1u], fract(colorIndex)); +fn fragmentMain(@location(0) colorIndex: f32) -> @location(0) vec4f { + let index = i32(colorIndex); + return mix(colors[index], colors[index + 1], fract(colorIndex)); } ` }); colorsLayout = Q5.device.createBindGroupLayout({ + label: 'colorsLayout', entries: [ { binding: 0, @@ -283,10 +284,17 @@ fn fragmentMain(@location(1) colorIndex: f32) -> @location(0) vec4 { $.background = (r, g, b, a) => { $.push(); $.resetMatrix(); - if (r.src) $.image(r, -c.hw, -c.hh, c.w, c.h); - else { + if (r.src) { + let og = $._imageMode; + $._imageMode = 'corner'; + $.image(r, -c.hw, -c.hh, c.w, c.h); + $._imageMode = og; + } else { + let og = $._rectMode; + $._rectMode = 'corner'; $.fill(r, g, b, a); $.rect(-c.hw, -c.hh, c.w, c.h); + $._rectMode = og; } $.pop(); }; @@ -394,7 +402,7 @@ fn fragmentMain(@location(1) colorIndex: f32) -> @location(0) vec4 { }); // set the bind group once before rendering - $.pass.setBindGroup(2, $._colorsBindGroup); + $.pass.setBindGroup(1, $._colorsBindGroup); }); $._hooks.postRender.push(() => { diff --git a/src/q5-webgpu-image.js b/src/q5-webgpu-image.js index 462a7bb..da84516 100644 --- a/src/q5-webgpu-image.js +++ b/src/q5-webgpu-image.js @@ -6,8 +6,8 @@ Q5.renderers.webgpu.image = ($, q) => { label: 'imageVertexShader', code: ` struct VertexOutput { - @builtin(position) position: vec4, - @location(0) texCoord: vec2 + @builtin(position) position: vec4f, + @location(0) texCoord: vec2f }; struct Uniforms { @@ -16,12 +16,12 @@ struct Uniforms { }; @group(0) @binding(0) var uniforms: Uniforms; -@group(1) @binding(0) var transforms: array>; +@group(0) @binding(1) var transforms: array>; @vertex -fn vertexMain(@location(0) pos: vec2, @location(1) texCoord: vec2, @location(2) transformIndex: f32) -> VertexOutput { - var vert = vec4(pos, 0.0, 1.0); - vert *= transforms[i32(transformIndex)]; +fn vertexMain(@location(0) pos: vec2f, @location(1) texCoord: vec2f, @location(2) transformIndex: f32) -> VertexOutput { + var vert = vec4f(pos, 0.0, 1.0); + vert = transforms[i32(transformIndex)] * vert; vert.x /= uniforms.halfWidth; vert.y /= uniforms.halfHeight; @@ -36,11 +36,11 @@ fn vertexMain(@location(0) pos: vec2, @location(1) texCoord: vec2, @lo let fragmentShader = Q5.device.createShaderModule({ label: 'imageFragmentShader', code: ` -@group(3) @binding(0) var samp: sampler; -@group(3) @binding(1) var texture: texture_2d; +@group(2) @binding(0) var samp: sampler; +@group(2) @binding(1) var texture: texture_2d; @fragment -fn fragmentMain(@location(0) texCoord: vec2) -> @location(0) vec4 { +fn fragmentMain(@location(0) texCoord: vec2f) -> @location(0) vec4f { // Sample the texture using the interpolated texture coordinate return textureSample(texture, samp, texCoord); } @@ -72,11 +72,9 @@ fn fragmentMain(@location(0) texCoord: vec2) -> @location(0) vec4 { ] }; - $.bindGroupLayouts.push(textureLayout); - const pipelineLayout = Q5.device.createPipelineLayout({ label: 'imagePipelineLayout', - bindGroupLayouts: $.bindGroupLayouts + bindGroupLayouts: [...$.bindGroupLayouts, textureLayout] }); $.pipelines[1] = Q5.device.createRenderPipeline({ diff --git a/src/q5-webgpu-text.js b/src/q5-webgpu-text.js index eff78e6..64cc7b7 100644 --- a/src/q5-webgpu-text.js +++ b/src/q5-webgpu-text.js @@ -1,32 +1,520 @@ Q5.renderers.webgpu.text = ($, q) => { - let t = $.createGraphics(1, 1); - t.pixelDensity($._pixelDensity); - t._imageMode = 'corner'; + let textShader = Q5.device.createShaderModule({ + label: 'MSDF text shader', + code: ` +// Positions for simple quad geometry +const pos = array(vec2f(0, -1), vec2f(1, -1), vec2f(0, 0), vec2f(1, 0)); - $.loadFont = (f) => { +struct VertexInput { + @builtin(vertex_index) vertex : u32, + @builtin(instance_index) instance : u32, +}; +struct VertexOutput { + @builtin(position) position : vec4f, + @location(0) texcoord : vec2f, + @location(1) colorIndex : f32 +}; +struct Char { + texOffset: vec2f, + texExtent: vec2f, + size: vec2f, + offset: vec2f, +}; +struct Text { + pos: vec2f, + scale: f32, + transformIndex: f32, + fillIndex: f32, + strokeIndex: f32 +}; +struct Uniforms { + halfWidth: f32, + halfHeight: f32 +}; + +@group(0) @binding(0) var uniforms: Uniforms; +@group(0) @binding(1) var transforms: array>; + +@group(1) @binding(0) var colors : array; + +@group(2) @binding(0) var fontTexture: texture_2d; +@group(2) @binding(1) var fontSampler: sampler; +@group(2) @binding(2) var fontChars: array; + +@group(3) @binding(0) var textChars: array; +@group(3) @binding(1) var textMetadata: array; + +@vertex +fn vertexMain(input : VertexInput) -> VertexOutput { + let char = textChars[input.instance]; + + let text = textMetadata[i32(char.w)]; + + let fontChar = fontChars[i32(char.z)]; + + let charPos = ((pos[input.vertex] * fontChar.size + char.xy + fontChar.offset) * text.scale) + text.pos; + + var vert = vec4f(charPos, 0.0, 1.0); + vert = transforms[i32(text.transformIndex)] * vert; + vert.x /= uniforms.halfWidth; + vert.y /= uniforms.halfHeight; + + var output : VertexOutput; + output.position = vert; + output.texcoord = (pos[input.vertex] * vec2f(1, -1)) * fontChar.texExtent + fontChar.texOffset; + output.colorIndex = text.fillIndex; + return output; +} + +fn sampleMsdf(texcoord: vec2f) -> f32 { + let c = textureSample(fontTexture, fontSampler, texcoord); + return max(min(c.r, c.g), min(max(c.r, c.g), c.b)); +} + +@fragment +fn fragmentMain(input : VertexOutput) -> @location(0) vec4f { + // pxRange (AKA distanceRange) comes from the msdfgen tool, + // uses the default which is 4. + let pxRange = 4.0; + let sz = vec2f(textureDimensions(fontTexture, 0)); + let dx = sz.x*length(vec2f(dpdxFine(input.texcoord.x), dpdyFine(input.texcoord.x))); + let dy = sz.y*length(vec2f(dpdxFine(input.texcoord.y), dpdyFine(input.texcoord.y))); + let toPixels = pxRange * inverseSqrt(dx * dx + dy * dy); + let sigDist = sampleMsdf(input.texcoord) - 0.5; + let pxDist = sigDist * toPixels; + let edgeWidth = 0.5; + let alpha = smoothstep(-edgeWidth, edgeWidth, pxDist); + if (alpha < 0.001) { + discard; + } + let fillColor = colors[i32(input.colorIndex)]; + return vec4f(fillColor.rgb, fillColor.a * alpha); +} +` + }); + + class MsdfFont { + constructor(pipeline, bindGroup, lineHeight, chars, kernings) { + this.pipeline = pipeline; + this.bindGroup = bindGroup; + this.lineHeight = lineHeight; + this.chars = chars; + this.kernings = kernings; + let charArray = Object.values(chars); + this.charCount = charArray.length; + this.defaultChar = charArray[0]; + } + getChar(charCode) { + return this.chars[charCode] ?? this.defaultChar; + } + // Gets the distance in pixels a line should advance for a given character code. If the upcoming + // character code is given any kerning between the two characters will be taken into account. + getXAdvance(charCode, nextCharCode = -1) { + let char = this.getChar(charCode); + if (nextCharCode >= 0) { + let kerning = this.kernings.get(charCode); + if (kerning) { + return char.xadvance + (kerning.get(nextCharCode) ?? 0); + } + } + return char.xadvance; + } + } + + let textBindGroupLayout = Q5.device.createBindGroupLayout({ + label: 'MSDF text group layout', + entries: [ + { + binding: 0, + visibility: GPUShaderStage.VERTEX | GPUShaderStage.FRAGMENT, + buffer: { type: 'read-only-storage' } + }, + { + binding: 1, + visibility: GPUShaderStage.VERTEX | GPUShaderStage.FRAGMENT, + buffer: { type: 'read-only-storage' } + } + ] + }); + + let fonts = {}; + + let createFont = async (fontJsonUrl, fontName, cb) => { q._preloadCount++; - return t.loadFont(f, () => { + + let res = await fetch(fontJsonUrl); + if (res.status == 404) { q._preloadCount--; + return ''; + } + let atlas = await res.json(); + + let slashIdx = fontJsonUrl.lastIndexOf('/'); + let baseUrl = slashIdx != -1 ? fontJsonUrl.substring(0, slashIdx + 1) : ''; + // load font image + res = await fetch(baseUrl + atlas.pages[0]); + let img = await createImageBitmap(await res.blob()); + + // convert image to texture + let imgSize = [img.width, img.height, 1]; + let texture = Q5.device.createTexture({ + label: `MSDF ${fontName}`, + size: imgSize, + format: 'rgba8unorm', + usage: GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.COPY_DST | GPUTextureUsage.RENDER_ATTACHMENT + }); + Q5.device.queue.copyExternalImageToTexture({ source: img }, { texture }, imgSize); + + // to make q5's default font file smaller, + // the chars and kernings are stored as csv strings + if (typeof atlas.chars == 'string') { + atlas.chars = $.CSV.parse(atlas.chars, ' '); + atlas.kernings = $.CSV.parse(atlas.kernings, ' '); + } + + let charCount = atlas.chars.length; + let charsBuffer = Q5.device.createBuffer({ + size: charCount * 32, + usage: GPUBufferUsage.STORAGE, + mappedAtCreation: true + }); + + let fontChars = new Float32Array(charsBuffer.getMappedRange()); + let u = 1 / atlas.common.scaleW; + let v = 1 / atlas.common.scaleH; + let chars = {}; + let o = 0; // offset + for (let [i, char] of atlas.chars.entries()) { + chars[char.id] = char; + chars[char.id].charIndex = i; + fontChars[o] = char.x * u; // texOffset.x + fontChars[o + 1] = char.y * v; // texOffset.y + fontChars[o + 2] = char.width * u; // texExtent.x + fontChars[o + 3] = char.height * v; // texExtent.y + fontChars[o + 4] = char.width; // size.x + fontChars[o + 5] = char.height; // size.y + fontChars[o + 6] = char.xoffset; // offset.x + fontChars[o + 7] = -char.yoffset; // offset.y + o += 8; + } + charsBuffer.unmap(); + + let fontSampler = Q5.device.createSampler({ + minFilter: 'linear', + magFilter: 'linear', + mipmapFilter: 'linear', + maxAnisotropy: 16 + }); + let fontBindGroupLayout = Q5.device.createBindGroupLayout({ + label: 'MSDF font group layout', + entries: [ + { + binding: 0, + visibility: GPUShaderStage.FRAGMENT, + texture: {} + }, + { + binding: 1, + visibility: GPUShaderStage.FRAGMENT, + sampler: {} + }, + { + binding: 2, + visibility: GPUShaderStage.VERTEX, + buffer: { type: 'read-only-storage' } + } + ] + }); + let fontPipeline = Q5.device.createRenderPipeline({ + label: 'msdf font pipeline', + layout: Q5.device.createPipelineLayout({ + bindGroupLayouts: [...$.bindGroupLayouts, fontBindGroupLayout, textBindGroupLayout] + }), + vertex: { + module: textShader, + entryPoint: 'vertexMain' + }, + fragment: { + module: textShader, + entryPoint: 'fragmentMain', + targets: [ + { + format: 'bgra8unorm', + blend: { + color: { + srcFactor: 'src-alpha', + dstFactor: 'one-minus-src-alpha' + }, + alpha: { + srcFactor: 'one', + dstFactor: 'one' + } + } + } + ] + }, + primitive: { + topology: 'triangle-strip', + stripIndexFormat: 'uint32' + } + }); + + let fontBindGroup = Q5.device.createBindGroup({ + label: 'msdf font bind group', + layout: fontBindGroupLayout, + entries: [ + { + binding: 0, + resource: texture.createView() + }, + { binding: 1, resource: fontSampler }, + { binding: 2, resource: { buffer: charsBuffer } } + ] }); + + let kernings = new Map(); + if (atlas.kernings) { + for (let kerning of atlas.kernings) { + let charKerning = kernings.get(kerning.first); + if (!charKerning) { + charKerning = new Map(); + kernings.set(kerning.first, charKerning); + } + charKerning.set(kerning.second, kerning.amount); + } + } + + $._font = new MsdfFont(fontPipeline, fontBindGroup, atlas.common.lineHeight, chars, kernings); + + fonts[fontName] = $._font; + $.pipelines[2] = $._font.pipeline; + + q._preloadCount--; + + if (cb) cb(fontName); }; - // directly add these text setting functions to the webgpu renderer - $.textFont = t.textFont; - $.textSize = t.textSize; - $.textLeading = t.textLeading; - $.textStyle = t.textStyle; - $.textAlign = t.textAlign; - $.textWidth = t.textWidth; - $.textAscent = t.textAscent; - $.textDescent = t.textDescent; + // q2d graphics context to use for text image creation + let g = $.createGraphics(1, 1); + g.colorMode($.RGB, 1); - $.textFill = (r, g, b, a) => t.fill($.color(r, g, b, a)); - $.textStroke = (r, g, b, a) => t.stroke($.color(r, g, b, a)); + $.loadFont = (url, cb) => { + let ext = url.slice(url.lastIndexOf('.') + 1); + if (ext != 'json') return g.loadFont(url, cb); + let fontName = url.slice(url.lastIndexOf('/') + 1, url.lastIndexOf('-')); + createFont(url, fontName, cb); + return fontName; + }; + + $._textSize = 18; + $._textAlign = 'left'; + $._textBaseline = 'alphabetic'; + let leadingSet = false, + leading = 22.5, + leadDiff = 4.5, + leadPercent = 1.25; + + $.textFont = (fontName) => { + $._font = fonts[fontName]; + + // replay the change of font in the draw stack + $.drawStack.push(-1, () => { + $._font = fonts[fontName]; + $.pipelines[2] = $._font.pipeline; + }); + }; + $.textSize = (size) => { + $._textSize = size; + if (!leadingSet) { + leading = size * leadPercent; + leadDiff = leading - size; + } + }; + $.textLeading = (lineHeight) => { + $._font.lineHeight = leading = lineHeight; + leadDiff = leading - $._textSize; + leadPercent = leading / $._textSize; + leadingSet = true; + }; + $.textAlign = (horiz, vert) => { + $._textAlign = horiz; + if (vert) $._textBaseline = vert; + }; + + $._charStack = []; + $._textStack = []; + + let measureText = (font, text, charCallback) => { + let maxWidth = 0, + offsetX = 0, + offsetY = 0, + line = 0, + printedCharCount = 0, + lineWidths = [], + nextCharCode = text.charCodeAt(0); + + for (let i = 0; i < text.length; ++i) { + let charCode = nextCharCode; + nextCharCode = i < text.length - 1 ? text.charCodeAt(i + 1) : -1; + switch (charCode) { + case 10: // Newline + lineWidths.push(offsetX); + line++; + maxWidth = Math.max(maxWidth, offsetX); + offsetX = 0; + offsetY -= font.lineHeight * leadPercent; + break; + case 13: // CR + break; + case 32: // Space + // advance the offset without actually adding a character + offsetX += font.getXAdvance(charCode); + break; + case 9: // Tab + offsetX += font.getXAdvance(charCode) * 2; + break; + default: + if (charCallback) { + charCallback(offsetX, offsetY, line, font.getChar(charCode)); + } + offsetX += font.getXAdvance(charCode, nextCharCode); + printedCharCount++; + } + } + lineWidths.push(offsetX); + maxWidth = Math.max(maxWidth, offsetX); + return { + width: maxWidth, + height: lineWidths.length * font.lineHeight * leadPercent, + lineWidths, + printedCharCount + }; + }; + + let initLoadDefaultFont; $.text = (str, x, y, w, h) => { - let img = t.createTextImage(str, w, h); + if (!$._font) { + // check if online and loading the default font hasn't been attempted yet + if (navigator.onLine && !initLoadDefaultFont) { + initLoadDefaultFont = true; + $.loadFont('https://q5js.org/fonts/YaHei-msdf.json'); + } + return; + } + + if (str.length > w) { + let wrapped = []; + let i = 0; + while (i < str.length) { + let max = i + w; + if (max >= str.length) { + wrapped.push(str.slice(i)); + break; + } + let end = str.lastIndexOf(' ', max); + if (end == -1 || end < i) end = max; + wrapped.push(str.slice(i, end)); + i = end + 1; + } + str = wrapped.join('\n'); + } - if (img.canvas.textureIndex === undefined) { + let spaces = 0, // whitespace char count, not literal spaces + hasNewline; + for (let i = 0; i < str.length; i++) { + let c = str[i]; + switch (c) { + case '\n': + hasNewline = true; + case '\r': + case '\t': + case ' ': + spaces++; + } + } + + let charsData = new Float32Array((str.length - spaces) * 4); + + let ta = $._textAlign, + tb = $._textBaseline, + textIndex = $._textStack.length, + o = 0, // offset + measurements; + + if (ta == 'left' && !hasNewline) { + measurements = measureText($._font, str, (textX, textY, line, char) => { + charsData[o] = textX; + charsData[o + 1] = textY; + charsData[o + 2] = char.charIndex; + charsData[o + 3] = textIndex; + o += 4; + }); + + if (tb == 'alphabetic') y -= $._textSize; + else if (tb == 'center') y -= $._textSize * 0.5; + else if (tb == 'bottom') y -= leading; + } else { + // measure the text to get the line widths before setting + // the x position to properly align the text + measurements = measureText($._font, str); + + let offsetY = 0; + if (tb == 'alphabetic') y -= $._textSize; + else if (tb == 'center') offsetY = measurements.height * 0.5; + else if (tb == 'bottom') offsetY = measurements.height; + + measureText($._font, str, (textX, textY, line, char) => { + let offsetX = 0; + if (ta == 'center') { + offsetX = measurements.width * -0.5 - (measurements.width - measurements.lineWidths[line]) * -0.5; + } else if (ta == 'right') { + offsetX = measurements.width - measurements.lineWidths[line]; + } + charsData[o] = textX + offsetX; + charsData[o + 1] = textY + offsetY; + charsData[o + 2] = char.charIndex; + charsData[o + 3] = textIndex; + o += 4; + }); + } + $._charStack.push(charsData); + + let text = new Float32Array(6); + + if ($._matrixDirty) $._saveMatrix(); + + text[0] = x; + text[1] = -y; + text[2] = $._textSize / 44; + text[3] = $._transformIndex; + text[4] = $._fillIndex; + text[5] = $._strokeIndex; + + $._textStack.push(text); + $.drawStack.push(2, measurements.printedCharCount); + }; + + $.textWidth = (str) => { + if (!$._font) return 0; + return measureText($._font, str).width; + }; + + $.createTextImage = (str, w, h) => { + g.textSize($._textSize); + + if ($._doFill) { + let fi = $._fillIndex * 4; + g.fill(colorsStack.slice(fi, fi + 4)); + } + if ($._doStroke) { + let si = $._strokeIndex * 4; + g.stroke(colorsStack.slice(si, si + 4)); + } + + let img = g.createTextImage(str, w, h); + + if (img.canvas.textureIndex == undefined) { $._createTexture(img); } else if (img.modified) { let cnv = img.canvas; @@ -40,27 +528,92 @@ Q5.renderers.webgpu.text = ($, q) => { ); img.modified = false; } - - $.textImage(img, x, y); + return img; }; - $.createTextImage = t.createTextImage; - $.textImage = (img, x, y) => { let og = $._imageMode; $._imageMode = 'corner'; - let ta = t._textAlign; + let ta = $._textAlign; if (ta == 'center') x -= img.canvas.hw; else if (ta == 'right') x -= img.width; - let bl = t._textBaseline; - if (bl == 'alphabetic') y -= t._textLeading; - else if (bl == 'middle') y -= img._middle; + let bl = $._textBaseline; + if (bl == 'alphabetic') y -= img._leading; + else if (bl == 'center') y -= img._middle; else if (bl == 'bottom') y -= img._bottom; else if (bl == 'top') y -= img._top; $.image(img, x, y); $._imageMode = og; }; + + $._hooks.preRender.push(() => { + if (!$._charStack.length) return; + + // Calculate total buffer size for text data + let totalTextSize = 0; + for (let charsData of $._charStack) { + totalTextSize += charsData.length * 4; + } + + // Create a single buffer for all text data + let charBuffer = Q5.device.createBuffer({ + label: 'charBuffer', + size: totalTextSize, + usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST, + mappedAtCreation: true + }); + + // Copy all text data into the buffer + let textArray = new Float32Array(charBuffer.getMappedRange()); + let o = 0; + for (let array of $._charStack) { + textArray.set(array, o); + o += array.length; + } + charBuffer.unmap(); + + // Calculate total buffer size for metadata + let totalMetadataSize = $._textStack.length * 6 * 4; + + // Create a single buffer for all metadata + let textBuffer = Q5.device.createBuffer({ + label: 'textBuffer', + size: totalMetadataSize, + usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST, + mappedAtCreation: true + }); + + // Copy all metadata into the buffer + let metadataArray = new Float32Array(textBuffer.getMappedRange()); + o = 0; + for (let array of $._textStack) { + metadataArray.set(array, o); + o += array.length; + } + textBuffer.unmap(); + + // Create a single bind group for the text buffer and metadata buffer + $._textBindGroup = Q5.device.createBindGroup({ + label: 'msdf text bind group', + layout: textBindGroupLayout, + entries: [ + { + binding: 0, + resource: { buffer: charBuffer } + }, + { + binding: 1, + resource: { buffer: textBuffer } + } + ] + }); + }); + + $._hooks.postRender.push(() => { + $._charStack.length = 0; + $._textStack.length = 0; + }); }; diff --git a/src/readme.md b/src/readme.md index 022511b..9c286bd 100644 --- a/src/readme.md +++ b/src/readme.md @@ -54,6 +54,11 @@ WebGPU rendering modules are in development: - [webgpu-drawing](#webgpu-drawing) - [webgpu-image](#webgpu-image) - [webgpu-text](#webgpu-text) + - [Default Font](#default-font) + - [Loading Custom Fonts](#loading-custom-fonts) + - [Displaying Emojis](#displaying-emojis) + - [Lightweight Use](#lightweight-use) + - [Implemented functions](#implemented-functions) - [math](#math) - [noisier](#noisier) @@ -179,13 +184,83 @@ Implemented functions: ## webgpu-text -> Use `textFill` and `textStroke` to set text colors. +The q5 WebGPU text renderer uses the multi-channel signed distance fields (MSDF) technique for high performance and high quality real-time text rendering. Text can be rapidly recolored, rotated, and scaled without any loss in quality or performance. -Internally, q5's WebGPU renderer uses a q5 graphics object to draw text to a Canvas2D canvas via `createTextImage`, then converts that canvas to a WebGPU texture. Each texture is cached, so it doesn't have to be recreated every frame that users want to display the same text. +MSDF, introduced by Chlumsky Viktor in his master's thesis ["Shape Decomposition for Multi-channel Distance Fields" (2015)](https://dspace.cvut.cz/bitstream/handle/10467/62770/F8-DP-2015-Chlumsky-Viktor-thesis.pdf), improves upon the signed distance field (SDF) technique, popularized by Chris Green and [Valve Software](https://www.valvesoftware.com/en/) in ["Improved Alpha-Tested Magnification for Vector Textures and Special Effects" (2007)](https://steamcdn-a.akamaihd.net/apps/valve/2007/SIGGRAPH2007_AlphaTestedMagnification.pdf). -Implemented functions: +| SDF | MSDF | +| -------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------- | +| ![demo-sdf16](https://user-images.githubusercontent.com/18639794/106391905-e679af00-63ef-11eb-96c3-993176330911.png) | ![demo-msdf16](https://user-images.githubusercontent.com/18639794/106391899-e37ebe80-63ef-11eb-988b-4764004bb196.png) | + +### Default Font + +For convenience, if no font is loaded before `text` is run, then q5's default MSDF font is loaded: https://q5js.org/fonts/YaHei-msdf.json + +![YaHei msdf texture](https://q5js.org/fonts/YaHei.png) + +This 512x512 msdf texture (207kb) was made with the [Microsoft YaHei](https://learn.microsoft.com/en-us/typography/font-list/microsoft-yahei) font and stores every character visible on a standard English keyboard, letters with diacritics (accents) used in European languages, and mathematical symbols. + +``` +!"#$%&'()\*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^\_`abcdefghijklmnopqrstuvwxyz{|}~€¡¢£¥©®°²³´·¹º¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖ×ØÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ‘’“”π +``` + +> Do you think any other characters ought to be included in the default set? Let us know! https://github.com/q5js/q5.js/issues + +### Loading Custom Fonts + +You can choose a custom set of characters and convert fonts to MSDF format by using the [msdf-bmfont-xml](https://msdf-bmfont.donmccurdy.com/) website, created by Don McCurdy. + +Here's how to load an MSDF font: + +```js +function preload() { + loadFont('arial-msdf.json'); +} + +function setup() { + createCanvas(200, 200); +} + +function draw() { + fill(0.71, 0.92, 1); + text('Hello, World!', mouseX, mouseY); +} + +Q5.webgpu(); +``` + +### Displaying Emojis + +Full color emoji characters can't be rendered using the MSDF technique, so use `createTextImage` and display them with `textImage`: + +```js +let puppy; + +function setup() { + createCanvas(200, 200); + textSize(100); + puppy = createTextImage('🐶'); +} + +function draw() { + textAlign(CENTER, CENTER); + textImage(puppy, 0, 0); +} + +Q5.webgpu(); +``` + +### Lightweight Use + +For super lightweight use load , which has a limited character set of english letters and some common punctuation symbols that completely fill in a 256x256 texture (73kb). + +``` +!@'",-.0123456789:ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz +``` + +### Implemented functions -`loadFont`,`textFont`, `textSize`, `textLeading`, `textStyle`, `textAlign`, `textWidth`, `textAscent`, `textDescent`, `textFill`, `textStroke`, `text` +`loadFont`, `text`, `textSize`, `textAlign`, `textWidth`, `createTextImage`, `textImage` ## math