ranger-compiler 3.5.1 → 3.5.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (338) hide show
  1. package/CHANGELOG.md +1064 -20
  2. package/LICENSE +3 -1
  3. package/README.md +123 -28
  4. package/dist/Lang.rgr +1188 -290
  5. package/dist/api.d.ts +2343 -879
  6. package/dist/api.js +79923 -56921
  7. package/dist/lib/JSON.rgr +102 -91
  8. package/dist/lib/Shell.rgr +4 -4
  9. package/dist/lib/apple/AppleToolchain.rgr +4 -4
  10. package/dist/lib/apple/README.md +5 -4
  11. package/dist/lib/apple/apple_test.rgr +1 -1
  12. package/dist/lib/core/README.md +1 -1
  13. package/dist/lib/evg/EVG.rgr +12 -0
  14. package/dist/lib/evg/EVGA11yFromTree.rgr +302 -0
  15. package/dist/lib/evg/EVGA11yTree.rgr +894 -0
  16. package/dist/lib/evg/EVGBox.rgr +267 -0
  17. package/dist/lib/evg/EVGBoxShorthandTest.rgr +220 -0
  18. package/dist/lib/evg/EVGCodepoint.rgr +316 -0
  19. package/dist/lib/evg/EVGColor.rgr +700 -0
  20. package/dist/lib/evg/EVGCommands.rgr +177 -0
  21. package/dist/lib/evg/EVGComponent.rgr +331 -0
  22. package/dist/lib/evg/EVGComponentTest.rgr +387 -0
  23. package/dist/lib/evg/EVGConnector.rgr +541 -0
  24. package/dist/lib/evg/EVGConnectorTest.rgr +306 -0
  25. package/dist/lib/evg/EVGDisplayList.rgr +4186 -0
  26. package/dist/lib/evg/EVGEasing.rgr +370 -0
  27. package/dist/lib/evg/EVGEffectTest.rgr +253 -0
  28. package/dist/lib/evg/EVGElement.rgr +4914 -0
  29. package/dist/lib/evg/EVGFixedTest.rgr +289 -0
  30. package/dist/lib/evg/EVGFlexRulesTest.rgr +317 -0
  31. package/dist/lib/evg/EVGFlexWrapTest.rgr +246 -0
  32. package/dist/lib/evg/EVGFling.rgr +244 -0
  33. package/dist/lib/evg/EVGFocus.rgr +400 -0
  34. package/dist/lib/evg/EVGFocusTest.rgr +399 -0
  35. package/dist/lib/evg/EVGGradient.rgr +322 -0
  36. package/dist/lib/evg/EVGGrapheme.rgr +190 -0
  37. package/dist/lib/evg/EVGGrid.rgr +975 -0
  38. package/dist/lib/evg/EVGHitTest.rgr +208 -0
  39. package/dist/lib/evg/EVGHoles.rgr +294 -0
  40. package/dist/lib/evg/EVGHostMeasurerTest.rgr +281 -0
  41. package/dist/lib/evg/EVGHostTextMeasurer.rgr +331 -0
  42. package/dist/lib/evg/EVGHostTree.rgr +834 -0
  43. package/dist/lib/evg/EVGHostTreeTest.rgr +447 -0
  44. package/dist/lib/evg/EVGImageDecode.rgr +117 -0
  45. package/dist/lib/evg/EVGImageMeasurer.rgr +86 -0
  46. package/dist/lib/evg/EVGInspect.rgr +883 -0
  47. package/dist/lib/evg/EVGInvalidateTest.rgr +432 -0
  48. package/dist/lib/evg/EVGJsonTest.rgr +535 -0
  49. package/dist/lib/evg/EVGLayout.rgr +4220 -0
  50. package/dist/lib/evg/EVGMeasure.rgr +1267 -0
  51. package/dist/lib/evg/EVGOverlayTest.rgr +626 -0
  52. package/dist/lib/evg/EVGPatch.rgr +1721 -0
  53. package/dist/lib/evg/EVGPatchTest.rgr +692 -0
  54. package/dist/lib/evg/EVGPopoverTest.rgr +395 -0
  55. package/dist/lib/evg/EVGReconcile.rgr +226 -0
  56. package/dist/lib/evg/EVGReconcileTest.rgr +421 -0
  57. package/dist/lib/evg/EVGReject.rgr +126 -0
  58. package/dist/lib/evg/EVGRelayoutTest.rgr +375 -0
  59. package/dist/lib/evg/EVGRtlLayoutTest.rgr +319 -0
  60. package/dist/lib/evg/EVGRuler.rgr +232 -0
  61. package/dist/lib/evg/EVGRulerTest.rgr +172 -0
  62. package/dist/lib/evg/EVGSelectChrome.rgr +178 -0
  63. package/dist/lib/evg/EVGStyleCacheTest.rgr +419 -0
  64. package/dist/lib/evg/EVGStyleSheet.rgr +2148 -0
  65. package/dist/lib/evg/EVGStyleStateTest.rgr +407 -0
  66. package/dist/lib/evg/EVGStyleVarTest.rgr +471 -0
  67. package/dist/lib/evg/EVGText.rgr +105 -0
  68. package/dist/lib/evg/EVGTextEngine.rgr +536 -0
  69. package/dist/lib/evg/EVGTextMeasurer.rgr +720 -0
  70. package/dist/lib/evg/EVGTimingTest.rgr +1417 -0
  71. package/dist/lib/evg/EVGToolbar.rgr +1369 -0
  72. package/dist/lib/evg/EVGTransition.rgr +728 -0
  73. package/dist/lib/evg/EVGTreeJson.rgr +721 -0
  74. package/dist/lib/evg/EVGUnit.rgr +554 -0
  75. package/dist/lib/evg/EVGViewportUnitTest.rgr +236 -0
  76. package/dist/lib/evg/EvgApp.rgr +189 -0
  77. package/dist/lib/evg/EvgBitmapTracer.rgr +4277 -0
  78. package/dist/lib/evg/EvgBitmapTracerTest.rgr +2119 -0
  79. package/dist/lib/evg/EvgHost.rgr +451 -0
  80. package/dist/lib/evg/EvgTest.rgr +91 -0
  81. package/dist/lib/evg/EvgTraceColor.rgr +236 -0
  82. package/dist/lib/evg/EvgTraceCurve.rgr +648 -0
  83. package/dist/lib/evg/EvgTraceFit.rgr +767 -0
  84. package/dist/lib/evg/EvgTracePath.rgr +435 -0
  85. package/dist/lib/evg/EvgTraceTypes.rgr +559 -0
  86. package/dist/lib/evg/EvgViewport.rgr +313 -0
  87. package/dist/lib/evg/FxDemoDoc.rgr +185 -0
  88. package/dist/lib/evg/HOSTS.md +227 -0
  89. package/dist/lib/evg/ISSUES.md +835 -0
  90. package/dist/lib/evg/PLAN_ACCESSIBILITY.md +650 -0
  91. package/dist/lib/evg/PLAN_CSS_LAYOUT_AND_FONTS.md +1124 -0
  92. package/dist/lib/evg/PLAN_EFFECTS.md +431 -0
  93. package/dist/lib/evg/PLAN_EVG.md +518 -0
  94. package/dist/lib/evg/PLAN_EVG_RENDERER.md +1674 -0
  95. package/dist/lib/evg/PLAN_INSPECTOR.md +788 -0
  96. package/dist/lib/evg/PLAN_LINKS_AND_FORMS.md +157 -0
  97. package/dist/lib/evg/PLAN_NATIVE_HOSTS.md +671 -0
  98. package/dist/lib/evg/PLAN_VECTOR_IR.md +664 -0
  99. package/dist/lib/evg/PLAN_VIEW_TRANSFORM.md +322 -0
  100. package/dist/lib/evg/PathBuilder.rgr +333 -0
  101. package/dist/lib/evg/README.md +1236 -0
  102. package/dist/lib/evg/SPEC.md +1252 -0
  103. package/dist/lib/evg/SVGPathParser.rgr +1199 -0
  104. package/dist/lib/evg/SvgParser.rgr +1736 -0
  105. package/dist/lib/evg/TODO_EVG_RENDERER.md +595 -0
  106. package/dist/lib/evg/VectorShapes.rgr +331 -0
  107. package/dist/lib/evg/VectorStroke.rgr +107 -0
  108. package/dist/lib/evg/VectorViewBox.rgr +380 -0
  109. package/dist/lib/evg/agent/README.md +290 -0
  110. package/dist/lib/evg/agent/evg_agent.rgr +899 -0
  111. package/dist/lib/evg/agent/fixtures/broken.evg.json +7 -0
  112. package/dist/lib/evg/agent/fixtures/card.evg.json +9 -0
  113. package/dist/lib/evg/agent/fixtures/connector.css +63 -0
  114. package/dist/lib/evg/agent/fixtures/connector.evg.json +14 -0
  115. package/dist/lib/evg/agent/fixtures/drawn.evg.json +129 -0
  116. package/dist/lib/evg/agent/fixtures/gradient.evg.json +4 -0
  117. package/dist/lib/evg/agent/fixtures/popover.css +99 -0
  118. package/dist/lib/evg/agent/fixtures/popover.evg.json +30 -0
  119. package/dist/lib/evg/agent/roundtrip.sh +74 -0
  120. package/dist/lib/evg/agent/smoke.sh +230 -0
  121. package/dist/lib/evg/android/README.md +97 -0
  122. package/dist/lib/evg/android/androidstubs/AndroidStubs.kt +175 -0
  123. package/dist/lib/evg/android/androidstubs/Annotation.kt +10 -0
  124. package/dist/lib/evg/android/androidstubs/App.kt +30 -0
  125. package/dist/lib/evg/android/androidstubs/Content.kt +41 -0
  126. package/dist/lib/evg/android/androidstubs/ContentRes.kt +12 -0
  127. package/dist/lib/evg/android/androidstubs/InputMethod.kt +46 -0
  128. package/dist/lib/evg/android/androidstubs/Net.kt +6 -0
  129. package/dist/lib/evg/android/androidstubs/Os.kt +30 -0
  130. package/dist/lib/evg/android/androidstubs/Util.kt +15 -0
  131. package/dist/lib/evg/android/androidstubs/View.kt +113 -0
  132. package/dist/lib/evg/android/androidstubs/Widget.kt +16 -0
  133. package/dist/lib/evg/android/src/android/kotlin/fi/ranger/evg/AndroidEvgSurface.kt +323 -0
  134. package/dist/lib/evg/android/src/android/kotlin/fi/ranger/evg/AndroidTextMeasurer.kt +56 -0
  135. package/dist/lib/evg/android/src/android/kotlin/fi/ranger/evg/RippleEffect.kt +262 -0
  136. package/dist/lib/evg/android/src/awt/kotlin/fi/ranger/evg/AwtEvgSurface.kt +238 -0
  137. package/dist/lib/evg/android/src/awt/kotlin/fi/ranger/evg/AwtTextMeasurer.kt +85 -0
  138. package/dist/lib/evg/android/src/main/kotlin/fi/ranger/evg/EvgEngineThread.kt +115 -0
  139. package/dist/lib/evg/android/src/main/kotlin/fi/ranger/evg/EvgPainter.kt +231 -0
  140. package/dist/lib/evg/android/src/main/kotlin/fi/ranger/evg/EvgSurface.kt +117 -0
  141. package/dist/lib/evg/android/src/main/kotlin/fi/ranger/evg/RecordingSurface.kt +94 -0
  142. package/dist/lib/evg/apple/README.md +128 -0
  143. package/dist/lib/evg/apple/Sources/CoreGraphicsEvgSurface.swift +329 -0
  144. package/dist/lib/evg/apple/Sources/CoreTextMeasurer.swift +70 -0
  145. package/dist/lib/evg/apple/Sources/EvgEngineQueue.swift +105 -0
  146. package/dist/lib/evg/apple/Sources/EvgPainter.swift +225 -0
  147. package/dist/lib/evg/apple/Sources/EvgSurface.swift +163 -0
  148. package/dist/lib/evg/apple/Sources/RecordingSurface.swift +110 -0
  149. package/dist/lib/evg/bench/EvgLayoutBench.rgr +34 -0
  150. package/dist/lib/evg/bench/README.md +64 -0
  151. package/dist/lib/evg/bench/layout-bench.mjs +316 -0
  152. package/dist/lib/evg/bench/layout-cases.mjs +510 -0
  153. package/dist/lib/evg/bench/layout-conformance.mjs +254 -0
  154. package/dist/lib/evg/bin/.gitignore +17 -0
  155. package/dist/lib/evg/evg_test.rgr +313 -0
  156. package/dist/lib/evg/gl/README.md +198 -0
  157. package/dist/lib/evg/gl/a11y-paint-check.mjs +73 -0
  158. package/dist/lib/evg/gl/blur-check.mjs +399 -0
  159. package/dist/lib/evg/gl/boxmodel.json +1 -0
  160. package/dist/lib/evg/gl/demo.html +46 -0
  161. package/dist/lib/evg/gl/effect-presets.css +285 -0
  162. package/dist/lib/evg/gl/effect-presets.js +63 -0
  163. package/dist/lib/evg/gl/effect-shots.mjs +135 -0
  164. package/dist/lib/evg/gl/evg-a11y.js +566 -0
  165. package/dist/lib/evg/gl/evg-binary.js +160 -0
  166. package/dist/lib/evg/gl/evg-engine.js +296 -0
  167. package/dist/lib/evg/gl/evg-fx.js +237 -0
  168. package/dist/lib/evg/gl/evg-gestures.js +209 -0
  169. package/dist/lib/evg/gl/evg-list.js +167 -0
  170. package/dist/lib/evg/gl/evg-measure.js +186 -0
  171. package/dist/lib/evg/gl/evg-textinput.js +303 -0
  172. package/dist/lib/evg/gl/evg-view.js +144 -0
  173. package/dist/lib/evg/gl/evg-webgl.js +3942 -0
  174. package/dist/lib/evg/gl/fx-check.mjs +998 -0
  175. package/dist/lib/evg/gl/fx-demo.html +166 -0
  176. package/dist/lib/evg/gl/fx-demo.js +2 -0
  177. package/dist/lib/evg/gl/fx-serve.mjs +47 -0
  178. package/dist/lib/evg/gl/gestures-check.mjs +189 -0
  179. package/dist/lib/evg/gl/list-binary-check.mjs +152 -0
  180. package/dist/lib/evg/gl/measure-check.mjs +135 -0
  181. package/dist/lib/evg/gl/rotation-check.mjs +207 -0
  182. package/dist/lib/evg/gl/shift-check.mjs +113 -0
  183. package/dist/lib/evg/gl/stroke-check.mjs +168 -0
  184. package/dist/lib/evg/gl/text-snap-check.mjs +139 -0
  185. package/dist/lib/evg/gl/view-check.mjs +343 -0
  186. package/dist/lib/evg/gl/view-policy-check.mjs +184 -0
  187. package/dist/lib/evg/html/evg-dom.js +318 -0
  188. package/dist/lib/evg/html/evg-html.js +600 -0
  189. package/dist/lib/evg/inspect/README.md +415 -0
  190. package/dist/lib/evg/inspect/browser-smoke.mjs +115 -0
  191. package/dist/lib/evg/inspect/evg-inspect.js +947 -0
  192. package/dist/lib/evg/inspect/shots/css.png +0 -0
  193. package/dist/lib/evg/inspect/shots/dashboard.png +0 -0
  194. package/dist/lib/evg/inspect/shots/pptx-slide.png +0 -0
  195. package/dist/lib/evg/inspect/shots/state.png +0 -0
  196. package/dist/lib/evg/inspect/shots.mjs +226 -0
  197. package/dist/lib/evg/oracle/css-blur.json +576 -0
  198. package/dist/lib/evg/oracle/css-box.json +157 -0
  199. package/dist/lib/evg/oracle/css-timing.json +709 -0
  200. package/dist/lib/evg/oracle/css_blur_oracle.mjs +529 -0
  201. package/dist/lib/evg/oracle/css_box_oracle.mjs +106 -0
  202. package/dist/lib/evg/oracle/css_timing_oracle.mjs +389 -0
  203. package/dist/lib/evg/original/EVGColor.clj +310 -0
  204. package/dist/lib/evg/original/EVGColorContext.rgr +178 -0
  205. package/dist/lib/evg/original/SVGPath.rgr +627 -0
  206. package/dist/lib/evg/original/Vec2.crgr +103 -0
  207. package/dist/lib/evg/ranger.json +9 -0
  208. package/dist/lib/evg/showcase/README.md +246 -0
  209. package/dist/lib/evg/showcase/assets/emblem.svg +30 -0
  210. package/dist/lib/evg/showcase/assets/rosette.svg +22 -0
  211. package/dist/lib/evg/showcase/build.mjs +651 -0
  212. package/dist/lib/evg/showcase/pages/album.tsx +30 -0
  213. package/dist/lib/evg/showcase/pages/boxmodel.tsx +37 -0
  214. package/dist/lib/evg/showcase/pages/cards.tsx +41 -0
  215. package/dist/lib/evg/showcase/pages/chart_api.tsx +170 -0
  216. package/dist/lib/evg/showcase/pages/charts.tsx +219 -0
  217. package/dist/lib/evg/showcase/pages/drawing.tsx +160 -0
  218. package/dist/lib/evg/showcase/pages/emoji.tsx +91 -0
  219. package/dist/lib/evg/showcase/pages/flex.tsx +46 -0
  220. package/dist/lib/evg/showcase/pages/more.tsx +332 -0
  221. package/dist/lib/evg/showcase/pages/plots.tsx +262 -0
  222. package/dist/lib/evg/showcase/pages/svg.tsx +63 -0
  223. package/dist/lib/evg/showcase/pages/tables.tsx +209 -0
  224. package/dist/lib/evg/showcase/pages/typography.tsx +43 -0
  225. package/dist/lib/evg/showcase/pages/units.tsx +35 -0
  226. package/dist/lib/evg/showcase/pages/variants.tsx +233 -0
  227. package/dist/lib/evg/showcase/pages/vector.tsx +75 -0
  228. package/dist/lib/evg/showcase/pages/views.tsx +223 -0
  229. package/dist/lib/evg/showcase/tests/chart_api_smoke.mjs +339 -0
  230. package/dist/lib/evg/showcase/tests/gl_smoke.mjs +125 -0
  231. package/dist/lib/evg/showcase/themes/chart_api-default.css +15 -0
  232. package/dist/lib/evg/showcase/themes/charts-default.css +29 -0
  233. package/dist/lib/evg/showcase/themes/drawing-default.css +35 -0
  234. package/dist/lib/evg/showcase/themes/more-default.css +128 -0
  235. package/dist/lib/evg/showcase/themes/plots-default.css +18 -0
  236. package/dist/lib/evg/showcase/themes/showcase.css +508 -0
  237. package/dist/lib/evg/showcase/themes/tables-default.css +123 -0
  238. package/dist/lib/evg/showcase/themes/variants-default.css +15 -0
  239. package/dist/lib/evg/showcase/themes/views-default.css +15 -0
  240. package/dist/lib/evg/tools/bench_vs_potrace.mjs +290 -0
  241. package/dist/lib/evg/tools/evg_image_tool.rgr +488 -0
  242. package/dist/lib/evg/tools/evg_trace_bench.rgr +114 -0
  243. package/dist/lib/evg/tools/evg_trace_cli.rgr +662 -0
  244. package/dist/lib/evg/tools/evg_trace_cpp_bench.rgr +83 -0
  245. package/dist/lib/evg/tools/run_trace_bench.sh +74 -0
  246. package/dist/lib/evg/tools/run_trace_cli_smoke.sh +128 -0
  247. package/dist/lib/evg/web/responsive/EvgResponsiveCheck.rgr +271 -0
  248. package/dist/lib/evg/web/responsive/EvgResponsiveDemo.rgr +531 -0
  249. package/dist/lib/evg/web/responsive/README.md +97 -0
  250. package/dist/lib/evg/web/responsive/build.mjs +90 -0
  251. package/dist/lib/evg/web/responsive/dom-check.mjs +169 -0
  252. package/dist/lib/evg/web/responsive/index.html +177 -0
  253. package/dist/lib/evg/web/responsive/smoke.mjs +221 -0
  254. package/dist/lib/evg/web/tools/assets-client.mjs +95 -0
  255. package/dist/lib/evg/web/tools/boot-bench.mjs +189 -0
  256. package/dist/lib/evg/web/tools/inline-assets.mjs +119 -0
  257. package/dist/lib/evg/web/tools/minify.mjs +60 -0
  258. package/dist/lib/evg/web/tracer/build.mjs +87 -0
  259. package/dist/lib/evg/web/tracer/index.html +2805 -0
  260. package/dist/lib/evg/web/tracer/sample.png +0 -0
  261. package/dist/lib/evg/web/tracer/smoke.mjs +1162 -0
  262. package/dist/lib/evgr/Cargo.lock +21 -0
  263. package/dist/lib/evgr/Cargo.toml +19 -0
  264. package/dist/lib/evgr/README.md +140 -0
  265. package/dist/lib/evgr/bench/NativeBench.rgr +246 -0
  266. package/dist/lib/evgr/bench/compare.mjs +252 -0
  267. package/dist/lib/evgr/bench/speed.mjs +277 -0
  268. package/dist/lib/evgr/bin/.gitignore +3 -0
  269. package/dist/lib/evgr/src/bin/bench.rs +106 -0
  270. package/dist/lib/evgr/src/bin/smoke.rs +13 -0
  271. package/dist/lib/evgr/src/grid.rs +730 -0
  272. package/dist/lib/evgr/src/lib.rs +1003 -0
  273. package/dist/lib/evgr/src/style.rs +468 -0
  274. package/dist/lib/evgr/src/text.rs +83 -0
  275. package/dist/lib/image/BitReader.rgr +171 -0
  276. package/dist/lib/image/Buffer.rgr +173 -0
  277. package/dist/lib/image/DCT.rgr +283 -0
  278. package/dist/lib/image/Deflate.rgr +339 -0
  279. package/dist/lib/image/HuffmanDecoder.rgr +181 -0
  280. package/dist/lib/image/ImageBuffer.rgr +706 -0
  281. package/dist/lib/image/JPEGDecoder.rgr +808 -0
  282. package/dist/lib/image/PNGDecoder.rgr +544 -0
  283. package/dist/lib/image/PNGEncoder.rgr +388 -0
  284. package/dist/lib/image/PPMImage.rgr +224 -0
  285. package/dist/lib/image/ProgressiveJPEGDecoder.rgr +1154 -0
  286. package/dist/lib/image/README.md +33 -0
  287. package/dist/lib/image/RasterBuffer.rgr +294 -0
  288. package/dist/lib/image/VP8BoolDecoder.rgr +163 -0
  289. package/dist/lib/image/WebPDecoder.rgr +433 -0
  290. package/dist/lib/image/WebPLossless.rgr +1035 -0
  291. package/dist/lib/image/WebPLossy.rgr +1929 -0
  292. package/dist/lib/image/ranger.json +9 -0
  293. package/dist/lib/image/testdata/webp/anim_2f_24x16.webp +0 -0
  294. package/dist/lib/image/testdata/webp/fixtures.txt +33 -0
  295. package/dist/lib/image/testdata/webp/gen_fixtures.py +261 -0
  296. package/dist/lib/image/testdata/webp/ll_1x1.webp +0 -0
  297. package/dist/lib/image/testdata/webp/ll_alpha_29x21.webp +0 -0
  298. package/dist/lib/image/testdata/webp/ll_gradient_64x64.webp +0 -0
  299. package/dist/lib/image/testdata/webp/ll_meta_32x32.webp +0 -0
  300. package/dist/lib/image/testdata/webp/ll_noise_17x9.webp +0 -0
  301. package/dist/lib/image/testdata/webp/ll_pal11_23x11.webp +0 -0
  302. package/dist/lib/image/testdata/webp/ll_pal2_19x7.webp +0 -0
  303. package/dist/lib/image/testdata/webp/ll_pal40_16x16.webp +0 -0
  304. package/dist/lib/image/testdata/webp/ll_pal4_13x5.webp +0 -0
  305. package/dist/lib/image/testdata/webp/ll_photo_40x30.webp +0 -0
  306. package/dist/lib/image/testdata/webp/ll_predictors_64x32.webp +0 -0
  307. package/dist/lib/image/testdata/webp/ly_1x1.webp +0 -0
  308. package/dist/lib/image/testdata/webp/ly_alph_raw_f0_21x13.webp +0 -0
  309. package/dist/lib/image/testdata/webp/ly_alph_raw_f1_21x13.webp +0 -0
  310. package/dist/lib/image/testdata/webp/ly_alph_raw_f2_21x13.webp +0 -0
  311. package/dist/lib/image/testdata/webp/ly_alph_raw_f3_21x13.webp +0 -0
  312. package/dist/lib/image/testdata/webp/ly_alph_vp8l_f0_21x13.webp +0 -0
  313. package/dist/lib/image/testdata/webp/ly_alph_vp8l_f1_21x13.webp +0 -0
  314. package/dist/lib/image/testdata/webp/ly_alph_vp8l_f2_21x13.webp +0 -0
  315. package/dist/lib/image/testdata/webp/ly_alph_vp8l_f3_21x13.webp +0 -0
  316. package/dist/lib/image/testdata/webp/ly_alpha_33x17.webp +0 -0
  317. package/dist/lib/image/testdata/webp/ly_exif_15x11.webp +0 -0
  318. package/dist/lib/image/testdata/webp/ly_i16_96x80.webp +0 -0
  319. package/dist/lib/image/testdata/webp/ly_nofilter_24x20.webp +0 -0
  320. package/dist/lib/image/testdata/webp/ly_odd_17x9.webp +0 -0
  321. package/dist/lib/image/testdata/webp/ly_photo_48x40.webp +0 -0
  322. package/dist/lib/image/testdata/webp/ly_q100_20x18.webp +0 -0
  323. package/dist/lib/image/testdata/webp/ly_sharp_40x36.webp +0 -0
  324. package/dist/lib/image/testdata/webp/ly_simple_filter_33x31.webp +0 -0
  325. package/dist/lib/image/testdata/webp/ly_vpx_parts8_37x45.webp +0 -0
  326. package/dist/lib/image/testdata/webp/ly_vpx_skip_96x80.webp +0 -0
  327. package/dist/lib/image/tests/WebPDecodeTool.rgr +81 -0
  328. package/dist/lib/image/tests/WebPDecoderTest.rgr +1162 -0
  329. package/dist/lib/image/tests/run_webp_tests.sh +39 -0
  330. package/dist/lib/rust/RsJson.rgr +468 -0
  331. package/dist/lib/rust/RsPrelude.rgr +1663 -0
  332. package/dist/lib/shell_test.rgr +7 -7
  333. package/dist/lib/stdlib.rgr +21 -5
  334. package/dist/lib/zip/ranger.json +6 -0
  335. package/dist/rgrc.js +87318 -67610
  336. package/package.json +1010 -1040
  337. package/dist/README.md +0 -117
  338. package/dist/package.json +0 -47
@@ -0,0 +1,4186 @@
1
+ ; SPDX-License-Identifier: MIT
2
+
3
+ ; EVGDisplayList.rgr — one traversal, a flat list of draw commands
4
+ ;
5
+ ; EVG computes geometry and stops there; something else has to paint it. Today
6
+ ; five painters each walk the laid-out tree themselves and each decide again
7
+ ; what a box means:
8
+ ;
9
+ ; EVGPDFRenderer -> PDF operators
10
+ ; evg_png_tool + raster -> CPU pixels
11
+ ; EVGHTMLRenderer -> absolutely positioned <div>s
12
+ ; v2 ui/UIContext -> framebuffer fillRect
13
+ ; game_engine/ui/UILayer -> interactive HUD on the same framebuffer
14
+ ;
15
+ ; Five copies of the same walk is how border-radius came to work in PDF and
16
+ ; silently not in PNG: one painter read `el.box.borderRadius` and the other
17
+ ; read a stale `el.borderRadius` that nothing wrote. A sixth backend (GL,
18
+ ; WebGL, SDL2) should not be a sixth copy.
19
+ ;
20
+ ; So: walk once, emit commands, and let a backend consume them.
21
+ ;
22
+ ; Two backends consume this today and neither walks a tree: `gl/evg-webgl.js`
23
+ ; and `html/evg-html.js`. The second one is worth knowing about from here,
24
+ ; because it says something about the shape of this list rather than about the
25
+ ; DOM: SVG has a native answer for seven of the eight kinds, and the two it
26
+ ; answers BETTER than the GPU does are PATH (rings and a fill rule, no stencil)
27
+ ; and the CLIP STACK (nested clip paths intersect by construction, where a
28
+ ; scissor is one rectangle that has to be intersected by hand). The two are
29
+ ; checked against each other pixel for pixel — see
30
+ ; gallery/pptx/web/html/parity.mjs.
31
+ ;
32
+ ; What neither of them can do is keep an element's IDENTITY, because a command
33
+ ; below carries none: an `id` here is the one field that would let a DOM
34
+ ; backend patch nodes instead of rebuilding them, and let a transition, a focus
35
+ ; ring or a native <input> attach to something that survives a frame. A command is
36
+ ; deliberately dumb — absolute pixels, resolved colours, no tree, no units —
37
+ ; because that is what a GPU backend wants: quads it can batch, glyph runs it
38
+ ; can look up in an atlas, and clip rectangles it can push and pop.
39
+ ;
40
+ ; This module is pure. It reads a laid-out tree and allocates no device
41
+ ; resources, so it compiles to every target Ranger has, which is what lets the
42
+ ; same list drive WebGL in a browser and SDL2 on a Raspberry Pi.
43
+
44
+ Import "EVGElement.rgr"
45
+ Import "EVGColor.rgr"
46
+ Import "EVGTextEngine.rgr"
47
+ Import "SVGPathParser.rgr"
48
+ Import "VectorViewBox.rgr"
49
+ Import "SvgParser.rgr"
50
+
51
+ ; Command kinds. Kept as ints so a backend can switch on them cheaply.
52
+ ; 0 RECT filled box, possibly rounded
53
+ ; 1 BORDER stroked outline, possibly rounded
54
+ ; 2 IMAGE textured quad, src names the file
55
+ ; 3 TEXT one run on one line, already positioned at its baseline
56
+ ; 4 PUSH_CLIP / 5 POP_CLIP scissor rectangle
57
+ ; 6 PATH filled vector outline, as flattened rings
58
+ ; 7 STROKE stroked vector outline, as flattened rings
59
+ class EVGDrawCmd {
60
+ def kind:int 0
61
+ def x:double 0.0
62
+ def y:double 0.0
63
+ def w:double 0.0
64
+ def h:double 0.0
65
+ ; The TOP-LEFT radius, and — when `perCorner` is set — the other three.
66
+ ; One number is what every backend but the WebGL painter reads, and it is
67
+ ; what a box with one radius has always emitted, so nothing that consumes
68
+ ; this had to change to keep working.
69
+ def radius:double 0.0
70
+ def perCorner:boolean false
71
+ def radiusTR:double 0.0
72
+ def radiusBR:double 0.0
73
+ def radiusBL:double 0.0
74
+ def thickness:double 0.0
75
+ ; Colour, pre-resolved to 0-255 + alpha 0-1 so a backend needs no EVGColor.
76
+ def r:int 0
77
+ def g:int 0
78
+ def b:int 0
79
+ def a:double 1.0
80
+ ; TEXT
81
+ def text:string ""
82
+ def fontFamily:string ""
83
+ def fontSize:double 0.0
84
+ ; Optional paragraph alignment for SoftCanvas: left / center / right.
85
+ ; When set, `w` is the text-box content width used to measure & place the run.
86
+ def textAlign:string ""
87
+ def fontWeight:string ""
88
+ ; CSS `letter-spacing` in pixels, zero for the font's own. It rides on
89
+ ; the command rather than being folded into `x` because a run is drawn
90
+ ; as one string: the painter has to space it, nobody else can.
91
+ def letterSpacing:double 0.0
92
+ ; STROKE (kind 7): 0 butt / 1 round / 2 square, and 0 miter / 1 round /
93
+ ; 2 bevel. Zero on both is the chain of butt-jointed bars the painter
94
+ ; drew before, so a backend that ignores them is not wrong, only plain.
95
+ def strokeCap:int 0
96
+ def strokeJoin:int 0
97
+ ; `stroke-dasharray`, verbatim: "6 4", "2 2 6 2". A string because the
98
+ ; record that crosses a thread is fixed-width and the pattern is not,
99
+ ; and the string pool already carries the text and the font name. An
100
+ ; empty one is a solid line, which is every stroke drawn before this.
101
+ def strokeDash:string ""
102
+ def strokeDashOffset:double 0.0
103
+ def maxWidth:double 0.0
104
+ ; Optional 2-stop gradient for RECT (kind 0)
105
+ def hasGrad:boolean false
106
+ def gradDir:int 0
107
+ def r2:int 0
108
+ def g2:int 0
109
+ def b2:int 0
110
+ def a2:double 1.0
111
+ ; `backdrop-filter: blur(Npx)` on a RECT: soften what is already painted
112
+ ; behind this rectangle before drawing it, clipped to the rectangle and its
113
+ ; radius. 0 is no filter.
114
+ ;
115
+ ; It is on the RECT and not a command of its own because it is a property
116
+ ; of the surface, and a backend that cannot do it draws the rectangle and
117
+ ; loses only the softening — which is the right failure. A separate
118
+ ; "blur this region" command would leave such a backend with a hole.
119
+ def backdropBlur:double 0.0
120
+ ; The EFFECT INSTANCE this rectangle belongs to, by name, or "" for the
121
+ ; rectangle that carries none — which is every rectangle on nearly every
122
+ ; page ever drawn.
123
+ ;
124
+ ; A NAME and not an index, because the commands of a clean subtree are
125
+ ; KEPT and re-emitted from the cache without the walk that built them: an
126
+ ; index into an array rebuilt this frame would point at whatever happens
127
+ ; to be there now. The name is the element's own `id`, so it survives the
128
+ ; cache, a reflow, and being read by a person.
129
+ ;
130
+ ; WHY ON THE RECTANGLE. The effect has to be drawn SOMEWHERE in paint
131
+ ; order — a starfield behind a card's text is not the same picture as one
132
+ ; over it — and paint order is what the command stream is. This is where
133
+ ; the element's own background sits, so this is where its effect goes.
134
+ def effectId:string ""
135
+ ; Optional drop shadow under RECT / PATH
136
+ def hasShadow:boolean false
137
+ def shadowX:double 0.0
138
+ def shadowY:double 0.0
139
+ def shadowBlur:double 0.0
140
+ def shadowR:int 0
141
+ def shadowG:int 0
142
+ def shadowB:int 0
143
+ def shadowA:double 0.35
144
+ ; IMAGE
145
+ def src:string ""
146
+ ; An image drawn mirrored in its own box: a deck can say `flipH` / `flipV`
147
+ ; on a picture, and mirroring is the texture read backwards rather than a
148
+ ; transform of the quad.
149
+ def flipH:boolean false
150
+ def flipV:boolean false
151
+ ; The window into the source, in the bitmap's own 0..1 space, when the
152
+ ; picture is shown cropped rather than whole. `hasCrop` is what says the
153
+ ; four numbers mean anything: 0,0,1,1 is a legal window and is also what
154
+ ; an uncropped command leaves behind, so the flag cannot be inferred.
155
+ def hasCrop:boolean false
156
+ def cropX:double 0.0
157
+ def cropY:double 0.0
158
+ def cropW:double 1.0
159
+ def cropH:double 1.0
160
+ ; PATH / STROKE. The rings are already flattened and already in page
161
+ ; coordinates: a GPU backend should not have to carry a path parser, and
162
+ ; the curve subdivision belongs where the drawn size is known. `ringEnds`
163
+ ; gives the index in `pts` one past each ring's last coordinate, so a shape
164
+ ; with a hole survives as two rings rather than one confused outline.
165
+ def pts:[double]
166
+ def ringEnds:[int]
167
+ def evenOdd:boolean false
168
+ ; Rotation in degrees, as the element asked for it. Text is what first
169
+ ; needed this — an axis title on its side.
170
+ def rotate:double 0.0
171
+ ; What it turns ABOUT. Unset means the command's own box centre, which is
172
+ ; right for a lone rotated label and wrong for everything else: a box, its
173
+ ; text and its children have to turn about ONE point or they come apart.
174
+ ;
175
+ ; A text command is where that shows first and worst. The painter draws
176
+ ; glyphs in a quad sized to the ink, not to the line box the command
177
+ ; carries, so "rotate each command about its own centre" puts the words
178
+ ; somewhere the box is not. A backend that ignores these two fields keeps
179
+ ; the old behaviour exactly, which is why they are additive rather than a
180
+ ; change to `rotate`.
181
+ ; Which element emitted this, as an `EVGElement.inspectSlot`, or -1 when
182
+ ; nothing was inspecting. Written only while `EVGDisplayList.attribute` is
183
+ ; on, and written to the JSON only then, so a list built without an
184
+ ; inspector is byte-identical to the one built before this field existed.
185
+ def node:int (0 - 1)
186
+
187
+ def rotOriginX:double 0.0
188
+ def rotOriginY:double 0.0
189
+ def hasRotOrigin:boolean false
190
+ ; On a PUSH_CLIP: which scroll layer this clip opens, 1-based, or 0 for
191
+ ; a clip that is not one. See `EVGDisplayList.refreshLayers`.
192
+ def layer:int 0
193
+
194
+ Constructor () {
195
+ }
196
+
197
+ fn kindName:string () {
198
+ if (kind == 0) {
199
+ return "RECT"
200
+ }
201
+ if (kind == 1) {
202
+ return "BORDER"
203
+ }
204
+ if (kind == 2) {
205
+ return "IMAGE"
206
+ }
207
+ if (kind == 3) {
208
+ return "TEXT"
209
+ }
210
+ if (kind == 4) {
211
+ return "PUSH_CLIP"
212
+ }
213
+ if (kind == 5) {
214
+ return "POP_CLIP"
215
+ }
216
+ if (kind == 6) {
217
+ return "PATH"
218
+ }
219
+ return "STROKE"
220
+ }
221
+ }
222
+
223
+ ; ==============================================================================
224
+ ; EVGSceneBinary — the same display list, as typed arrays instead of text
225
+ ; ==============================================================================
226
+ ;
227
+ ; `toJson` is the bridge every host has used to get a picture out of the
228
+ ; engine, and on a chart-heavy slide it is what the frame costs. Measured, one
229
+ ; slide of 10 084 commands and 88 656 point coordinates:
230
+ ;
231
+ ; buildFrame 12 ms the layout — the actual work
232
+ ; toJson 62 ms turning it into 1.47 MB of text
233
+ ; JSON.parse 19 ms the host turning that text back into objects
234
+ ;
235
+ ; Five sixths of a frame spent handing over a picture that took a sixth to
236
+ ; compute. And the text is not the cost either: assembling 1.47 MB with 200 000
237
+ ; concatenations is 3 ms, and formatting every number is 25. It is death by
238
+ ; half a million small operations, so no faster string writer would help.
239
+ ;
240
+ ; Typed arrays do not have the problem at all. `int_buffer` is an `Int32Array`
241
+ ; on the JavaScript target and a flat block of memory everywhere else; filling
242
+ ; 500 000 of them takes 3 ms, and the host reads the SAME array — nothing is
243
+ ; parsed, nothing is copied, nothing is allocated per command.
244
+ ;
245
+ ; WHY FIXED POINT. `toJson` writes two decimals and no more — `num` rounds to
246
+ ; hundredths — so a coordinate multiplied by 100 and rounded is not an
247
+ ; approximation of what the JSON carried, it is exactly it. The two bridges
248
+ ; therefore describe the same picture to the last unit, which is what lets a
249
+ ; test compare them field by field.
250
+ ;
251
+ ; WHY THE STRINGS ARE STILL STRINGS. There are 203 text runs and 1 158
252
+ ; characters in that same frame, against 88 656 coordinates. Text is a
253
+ ; rounding error in this payload; packing it into the array would cost more in
254
+ ; complexity than it saves in time, so a command carries an INDEX into a small
255
+ ; pool and the pool crosses as an array of strings.
256
+ class EVGSceneBinary {
257
+ ; One fixed-size record per command; see `EVGDisplayList.STRIDE`.
258
+ def cmds:int_buffer (int_buffer_alloc 0)
259
+ ; Every ring's coordinates, in hundredths, one command's after another's.
260
+ def pts:int_buffer (int_buffer_alloc 0)
261
+ ; Where each command's rings stop, in the same order.
262
+ def ends:int_buffer (int_buffer_alloc 0)
263
+ ; The text, font, weight and image names a command names by index.
264
+ def strings:[string]
265
+ def count:int 0
266
+ def width:double 0.0
267
+ def height:double 0.0
268
+ ; The camera, beside the page size rather than inside the record: three
269
+ ; numbers on the envelope leave the per-command stride alone, so a reader
270
+ ; that knows nothing about views reads this buffer exactly as before.
271
+ def viewX:double 0.0
272
+ def viewY:double 0.0
273
+ def viewScale:double 1.0
274
+ def hasView:boolean false
275
+
276
+ Constructor () {
277
+ def s:[string]
278
+ strings = s
279
+ }
280
+ }
281
+
282
+ class EVGDisplayList {
283
+ def cmds:[EVGDrawCmd]
284
+ ; Attribution, off by default. On, every command records the element that
285
+ ; emitted it — see `addCmd` and `walk`.
286
+ def attribute:boolean false
287
+ def curNode:int (0 - 1)
288
+ ; Overlay surfaces met during the first pass, drawn by the second. See
289
+ ; `build`.
290
+ def deferredOverlays:[EVGElement]
291
+ ; How many turns the walk is inside. A clip is a scissor rectangle and
292
+ ; a turn cannot be written into one, so inside a turn there is no clip.
293
+ def turnedDepth:int 0
294
+ ; The same engine layout measured with. A paragraph is emitted one command
295
+ ; per LINE, broken exactly where layout broke it — a backend that got the
296
+ ; whole string would draw one long run straight out of its box, which is
297
+ ; the disagreement between measure and paint this engine exists to avoid.
298
+ def textEngine:EVGTextEngine (new EVGTextEngine())
299
+
300
+ ; THE CAMERA, and not an element property.
301
+ ;
302
+ ; A display list is written in the coordinates its commands are drawn at,
303
+ ; and a host that pans or zooms has always done that by putting a
304
+ ; transform on a world element — which means the tree changes when the
305
+ ; view does, so the walk runs, the layout runs, and every path is
306
+ ; flattened again for a move that changed no geometry at all.
307
+ ;
308
+ ; With a view, the list is in SCENE coordinates and carries the camera
309
+ ; beside them: a painter multiplies at the last moment, so a frame
310
+ ; already built can be drawn somewhere else for the cost of a uniform.
311
+ ; See PLAN_VIEW_TRANSFORM.md.
312
+ ;
313
+ ; NO VIEW IS AN IDENTITY VIEW, which is exactly the list every consumer
314
+ ; gets today — a PDF, a PNG, an SVG export and every golden test see
315
+ ; byte-identical output unless a host asks for a camera. `hasView` is
316
+ ; what keeps it byte-identical: the numbers are written only when they
317
+ ; are not the identity.
318
+ def viewX:double 0.0
319
+ def viewY:double 0.0
320
+ def viewScale:double 1.0
321
+ def hasView:boolean false
322
+ ; See `setRegion`.
323
+ def regionLeft:double 0.0
324
+ def regionTop:double 0.0
325
+ def regionRight:double 0.0
326
+ def regionBottom:double 0.0
327
+ def hasRegion:boolean false
328
+
329
+ Constructor () {
330
+ }
331
+
332
+ ; The camera for this list. Scene coordinates times `scale` plus (x, y)
333
+ ; land on the page. A rotating camera would want a matrix here; nothing
334
+ ; asks for one, and a rotation that belongs to the PICTURE is an element
335
+ ; transform and is baked as it always was.
336
+ fn setView:void (x:double y:double scale:double) {
337
+ viewX = x
338
+ viewY = y
339
+ viewScale = scale
340
+ hasView = (((x != 0.0) || (y != 0.0)) || (scale != 1.0))
341
+ }
342
+
343
+ ; THE REGION THIS LIST IS BUILT FOR, in scene coordinates.
344
+ ;
345
+ ; Culling asks "is this inside the clip", and the outermost clip has
346
+ ; always been the page — the right question while the list is in page
347
+ ; coordinates. A list with a camera is not: the scene is the whole board
348
+ ; and the page is a window onto it, so the question becomes "is this
349
+ ; inside the part of the scene this frame was built to show".
350
+ ;
351
+ ; That part is the viewport mapped back through the camera and grown by
352
+ ; an overscan, so a pan has somewhere to go before the frame has to be
353
+ ; built again — the same trick the scroll layers already play in one
354
+ ; dimension (`setOverscan`), in two.
355
+ ;
356
+ ; Unset is what every caller had: culling starts off and the first clip
357
+ ; turns it on.
358
+ fn setRegion:void (l:double t:double r:double b:double) {
359
+ regionLeft = l
360
+ regionTop = t
361
+ regionRight = r
362
+ regionBottom = b
363
+ hasRegion = true
364
+ }
365
+
366
+ fn clearRegion:void () {
367
+ hasRegion = false
368
+ }
369
+
370
+ fn clearView:void () {
371
+ viewX = 0.0
372
+ viewY = 0.0
373
+ viewScale = 1.0
374
+ hasView = false
375
+ }
376
+
377
+ fn setTextEngine:void (e:EVGTextEngine) {
378
+ textEngine = e
379
+ }
380
+
381
+ ; Every command in this file is appended here, so the element being walked
382
+ ; is stamped on it in one place rather than at twenty. The walk sets
383
+ ; `curNode`; this is the only thing that reads it.
384
+ fn addCmd:void (c:EVGDrawCmd) {
385
+ c.node = curNode
386
+ push cmds c
387
+ }
388
+
389
+ fn count:int () {
390
+ return (array_length cmds)
391
+ }
392
+
393
+ fn at:EVGDrawCmd (i:int) {
394
+ return (itemAt cmds i)
395
+ }
396
+
397
+ ; ------------------------------------------------------------ primitives --
398
+ ; Every component that draws into a display list needs the same four
399
+ ; commands, and each one that encoded them for itself encoded them slightly
400
+ ; differently. They live here, where `cmds` does, so a dialog, a grid and a
401
+ ; chart put the same bytes in the list.
402
+ ; ------------------------------------------- commands a host built --------
403
+ ; Some content is neither a box nor an SVG: a chart is a list of commands a
404
+ ; host produced for one size and one set of numbers. It cannot be made
405
+ ; during the walk, because it needs its card's rectangle and the walk is
406
+ ; what hands that out — so it is made after the layout and handed in here,
407
+ ; AGAINST THE ELEMENT IT BELONGS TO.
408
+ ;
409
+ ; The walk then emits it where that element is. Appended after the tree
410
+ ; instead — which is what a host has to do without this — it paints over
411
+ ; every dialog and every dropdown on the page, because "after everything"
412
+ ; is exactly what the end of the list means. And it lands outside the
413
+ ; scroll layer's range, so a kept list moves the page out from under it.
414
+ ;
415
+ ; Registered BEFORE `build`, which does not clear the registry: a list is
416
+ ; built once and a fresh one is made for the next frame.
417
+ def paintIds:[string]
418
+ def paintStart:[int]
419
+ def paintCount:[int]
420
+ def paintPool:[EVGDrawCmd]
421
+
422
+ ; Take another list's commands and hold them against `id`. The commands
423
+ ; are in page coordinates — the host emitted them where the element is —
424
+ ; so nothing here moves them.
425
+ fn paintAt:void (id:string from:EVGDisplayList) {
426
+ if ((strlen id) == 0) {
427
+ return
428
+ }
429
+ push paintIds id
430
+ push paintStart (array_length paintPool)
431
+ def n:int (from.count())
432
+ def i:int 0
433
+ while (i < n) {
434
+ push paintPool (from.at(i))
435
+ i = (i + 1)
436
+ }
437
+ push paintCount n
438
+ }
439
+
440
+ fn paintedCount:int () {
441
+ return (array_length paintIds)
442
+ }
443
+
444
+ ; Emit what was registered for this element, if anything. Called from the
445
+ ; walk with the element's own clip still open, so a chart is clipped by its
446
+ ; card the way its children are.
447
+ fn emitPainted:void (id:string) {
448
+ def i:int 0
449
+ def n:int (array_length paintIds)
450
+ while (i < n) {
451
+ if ((itemAt paintIds i) == id) {
452
+ def from:int (itemAt paintStart i)
453
+ def count:int (itemAt paintCount i)
454
+ def k:int 0
455
+ while (k < count) {
456
+ this.addCmd((itemAt paintPool (from + k)))
457
+ k = (k + 1)
458
+ }
459
+ }
460
+ i = (i + 1)
461
+ }
462
+ }
463
+
464
+ ; ------------------------------------------------------------ the ring ----
465
+ ; WHERE THE KEYBOARD IS. `EVGFocus` decides which element has the focus;
466
+ ; this is what makes it visible, and it is in the list rather than in a
467
+ ; host so a renderer gets the ring for the same reason it gets a border.
468
+ ; An id nothing in the tree carries draws nothing, which is what a focus
469
+ ; that outlived its element should look like.
470
+ def focusRingId@(mutable):string ""
471
+ def focusRingR@(mutable):int 125
472
+ def focusRingG@(mutable):int 211
473
+ def focusRingB@(mutable):int 252
474
+ def focusRingA@(mutable):double 1.0
475
+ def focusRingWidth@(mutable):double 2.0
476
+ ; Outside the box, the way a browser's outline sits outside the border.
477
+ def focusRingPad@(mutable):double 2.0
478
+ ; WHERE THE RING ENDED UP, so a kept list can move it. The ring is drawn
479
+ ; last of all and outside every clip, which puts it outside every layer's
480
+ ; range too — so a frame that only scrolled moved the whole page under a
481
+ ; ring that stayed where it was. `refreshRing` is what stops that.
482
+ def ringCmd@(mutable):int (0 - 1)
483
+ def ringEl@(mutable+optional):EVGElement
484
+
485
+ fn setFocusRing:void (id:string) {
486
+ focusRingId = id
487
+ }
488
+
489
+ fn setFocusRingStyle:void (r:int g:int b:int alpha:double width:double pad:double) {
490
+ focusRingR = r
491
+ focusRingG = g
492
+ focusRingB = b
493
+ focusRingA = alpha
494
+ focusRingWidth = width
495
+ focusRingPad = pad
496
+ }
497
+
498
+ ; Last of all, so it is over everything and outside every clip: a ring
499
+ ; half-hidden by the panel the focused button sits in is a ring that does
500
+ ; not say where the keyboard is.
501
+ fn paintFocusRing:void (root:EVGElement) {
502
+ if ((strlen focusRingId) == 0) {
503
+ return
504
+ }
505
+ def found@(optional):EVGElement (EVGDisplayList.ringTarget(root focusRingId))
506
+ if found {
507
+ this.ringAround((unwrap found))
508
+ }
509
+ }
510
+
511
+ fn ringAround:void (el:EVGElement) {
512
+ if (el.calculatedWidth <= 0.0) {
513
+ return
514
+ }
515
+ if (el.calculatedHeight <= 0.0) {
516
+ return
517
+ }
518
+ def col:EVGColor (EVGColor.rgba(focusRingR focusRingG focusRingB focusRingA))
519
+ def c:EVGDrawCmd (new EVGDrawCmd)
520
+ c.kind = 1
521
+ c.x = (el.calculatedX - focusRingPad)
522
+ c.y = (el.calculatedY - focusRingPad)
523
+ c.w = (el.calculatedWidth + (focusRingPad * 2.0))
524
+ c.h = (el.calculatedHeight + (focusRingPad * 2.0))
525
+ c.thickness = focusRingWidth
526
+ ; The element's own corner, opened out by however far the ring sits
527
+ ; off it, so a pill keeps its ends.
528
+ def rad:double el.box.borderRadiusPx
529
+ if (rad > 0.0) {
530
+ c.radius = (rad + focusRingPad)
531
+ }
532
+ c.r = (col.red())
533
+ c.g = (col.green())
534
+ c.b = (col.blue())
535
+ c.a = (col.alpha())
536
+ ; WHICH LAYER IT SCROLLS IN. The ring is emitted last and outside
537
+ ; every clip — so a button inside a panel is not half-ringed by the
538
+ ; panel's edge — and outside every clip is outside every LAYER too.
539
+ ; A host that moves a kept frame by a per-layer offset rather than by
540
+ ; rebuilding it then moved the whole page and left the ring behind:
541
+ ; `refreshRing` keeps the LIST right and this keeps the FRAME right.
542
+ ; The painter reads `layer` off a command that carries one.
543
+ c.layer = (this.layerAround(el))
544
+ this.addCmd(c)
545
+ ringCmd = ((array_length cmds) - 1)
546
+ ringEl = el
547
+ }
548
+
549
+ ; The scroll layer an element is inside, as the painter numbers them
550
+ ; (1-based, 0 for none) — the innermost, which is the last one whose
551
+ ; container holds it.
552
+ fn layerAround:int (el:EVGElement) {
553
+ def found@(mutable):int 0
554
+ def i:int 0
555
+ while (i < (array_length layerEls)) {
556
+ if ((itemAt layerKind i) == 0) {
557
+ if (EVGDisplayList.holds((itemAt layerEls i) el)) {
558
+ found = (i + 1)
559
+ }
560
+ }
561
+ i = (i + 1)
562
+ }
563
+ return found
564
+ }
565
+
566
+ sfn holds:boolean (parent:EVGElement el:EVGElement) {
567
+ if (parent == el) {
568
+ return true
569
+ }
570
+ def i:int 0
571
+ def n:int (parent.getChildCount())
572
+ while (i < n) {
573
+ if (EVGDisplayList.holds((parent.getChild(i)) el)) {
574
+ return true
575
+ }
576
+ i = (i + 1)
577
+ }
578
+ return false
579
+ }
580
+
581
+ ; The ring against the box it is round, as that box stands now. A scroll
582
+ ; moves the boxes, and the layers' commands with them; this is the one
583
+ ; command that belongs to no layer, so it is put back by hand — one
584
+ ; element read, and no walk of anything.
585
+ fn refreshRing:void () {
586
+ if (ringCmd < 0) {
587
+ return
588
+ }
589
+ if (ringCmd >= (array_length cmds)) {
590
+ return
591
+ }
592
+ if ringEl {
593
+ def el:EVGElement (unwrap ringEl)
594
+ def c:EVGDrawCmd (itemAt cmds ringCmd)
595
+ c.x = (el.calculatedX - focusRingPad)
596
+ c.y = (el.calculatedY - focusRingPad)
597
+ c.w = (el.calculatedWidth + (focusRingPad * 2.0))
598
+ c.h = (el.calculatedHeight + (focusRingPad * 2.0))
599
+ }
600
+ }
601
+
602
+ ; THE VISIBLE ONE. An id can be in the tree twice — this app draws the same
603
+ ; navigation as a rail and as a bar, and the stylesheet folds one of them
604
+ ; away at every width — so a search that takes the first match rings the
605
+ ; copy that is not on the screen, which is a ring nobody can see. A hidden
606
+ ; branch is not searched, and a box laid out to nothing is not a target.
607
+ sfn ringTarget@(optional):EVGElement (el:EVGElement id:string) {
608
+ def none@(optional):EVGElement
609
+ if el.a11yHidden {
610
+ return none
611
+ }
612
+ if (el.display == "none") {
613
+ return none
614
+ }
615
+ if (el.id == id) {
616
+ if (el.calculatedWidth > 0.0) {
617
+ if (el.calculatedHeight > 0.0) {
618
+ def hit@(optional):EVGElement el
619
+ return hit
620
+ }
621
+ }
622
+ }
623
+ def i:int 0
624
+ def n:int (el.getChildCount())
625
+ while (i < n) {
626
+ def found@(optional):EVGElement (EVGDisplayList.ringTarget((el.getChild(i)) id))
627
+ if found {
628
+ return found
629
+ }
630
+ i = (i + 1)
631
+ }
632
+ return none
633
+ }
634
+
635
+ fn addRect:void (x:double y:double w:double h:double col:EVGColor) {
636
+ def c:EVGDrawCmd (new EVGDrawCmd)
637
+ c.kind = 0
638
+ c.x = x
639
+ c.y = y
640
+ c.w = w
641
+ c.h = h
642
+ c.r = (col.red())
643
+ c.g = (col.green())
644
+ c.b = (col.blue())
645
+ c.a = (col.alpha())
646
+ this.addCmd(c)
647
+ }
648
+
649
+ fn addFrame:void (x:double y:double w:double h:double thickness:double col:EVGColor) {
650
+ this.addTurnedFrame(x y w h thickness col 0.0 0.0 0.0)
651
+ }
652
+
653
+ ; The same, turned about a point of its own. A selection ring around a
654
+ ; layer that is not upright has to lean the way the layer does or it says
655
+ ; the wrong thing about it — an axis-aligned box around a turned shape
656
+ ; reads as "this is straight and bigger than it looks".
657
+ fn addTurnedFrame:void (x:double y:double w:double h:double thickness:double col:EVGColor deg:double ox:double oy:double) {
658
+ def c:EVGDrawCmd (new EVGDrawCmd)
659
+ c.kind = 1
660
+ c.x = x
661
+ c.y = y
662
+ c.w = w
663
+ c.h = h
664
+ c.thickness = thickness
665
+ c.r = (col.red())
666
+ c.g = (col.green())
667
+ c.b = (col.blue())
668
+ c.a = (col.alpha())
669
+ if ((deg > 0.001) || (deg < (0.0 - 0.001))) {
670
+ c.rotate = deg
671
+ c.rotOriginX = ox
672
+ c.rotOriginY = oy
673
+ c.hasRotOrigin = true
674
+ }
675
+ this.addCmd(c)
676
+ }
677
+
678
+ ; The measured width and height come from the caller, because the list has
679
+ ; no text renderer of its own and the measurement must be the one the
680
+ ; caller laid out with.
681
+ ; A textured quad. `src` names the picture rather than carrying it: every
682
+ ; backend here already resolves a name to a texture — the browser host
683
+ ; keys a blob URL by it, the PDF renderer opens the file — and two format
684
+ ; readers now emit one, which is why this moved out of `PptxToEvg` and
685
+ ; into the layer both of them draw through.
686
+ fn addImage:void (src:string x:double y:double w:double h:double flipH:boolean flipV:boolean rotate:double) {
687
+ if ((strlen src) == 0) {
688
+ return
689
+ }
690
+ def c:EVGDrawCmd (new EVGDrawCmd)
691
+ c.kind = 2
692
+ c.x = x
693
+ c.y = y
694
+ c.w = w
695
+ c.h = h
696
+ c.src = src
697
+ c.a = 1.0
698
+ c.flipH = flipH
699
+ c.flipV = flipV
700
+ c.rotate = rotate
701
+ this.addCmd(c)
702
+ }
703
+
704
+ fn addText:void (text:string x:double y:double size:double col:EVGColor family:string bold:boolean italic:boolean width:double height:double) {
705
+ if ((strlen text) == 0) {
706
+ return
707
+ }
708
+ def c:EVGDrawCmd (new EVGDrawCmd)
709
+ c.kind = 3
710
+ c.x = x
711
+ c.y = y
712
+ c.w = width
713
+ c.h = height
714
+ c.text = text
715
+ c.fontFamily = family
716
+ c.fontSize = size
717
+ if bold {
718
+ c.fontWeight = "bold"
719
+ }
720
+ if italic {
721
+ c.textAlign = "italic"
722
+ }
723
+ c.r = (col.red())
724
+ c.g = (col.green())
725
+ c.b = (col.blue())
726
+ c.a = (col.alpha())
727
+ this.addCmd(c)
728
+ }
729
+
730
+ ; One side's contribution to the box-wide scale factor: the smaller of what
731
+ ; we have and this side's length over the two radii that share it. A side
732
+ ; whose radii are zero contributes nothing, which is why the guard is on
733
+ ; the SUM and not on the side.
734
+ ; A radius in pixels, re-resolving a percentage against the box's OWN size.
735
+ ; `already` is what the box worked out from its parent, which is right for
736
+ ; every unit but a percentage.
737
+ sfn radiusPx:double (isPct:boolean pct:double already:double own:double) {
738
+ if isPct {
739
+ return ((pct / 100.0) * own)
740
+ }
741
+ return already
742
+ }
743
+
744
+ sfn sideFactor:double (soFar:double side:double sum:double) {
745
+ if (sum <= 0.0) {
746
+ return soFar
747
+ }
748
+ def f:double (side / sum)
749
+ if (f < soFar) {
750
+ return f
751
+ }
752
+ return soFar
753
+ }
754
+
755
+ fn addClip:void (x:double y:double w:double h:double) {
756
+ def c:EVGDrawCmd (new EVGDrawCmd)
757
+ c.kind = 4
758
+ c.x = x
759
+ c.y = y
760
+ c.w = w
761
+ c.h = h
762
+ this.addCmd(c)
763
+ }
764
+
765
+ ; A stroked polyline and a filled polygon, given in page coordinates.
766
+ ; The SVG walk below already emits kind 6/7 commands, but only from a
767
+ ; parsed `<path>`: anything that computes its own geometry — a chart line,
768
+ ; a graph edge — had no way to say so, and each such caller either
769
+ ; approximated the curve with thin rects or drew nothing on the GPU. The
770
+ ; points are already flattened, which is the contract kind 6/7 carry.
771
+ fn addPolyline:void (pts:[double] thickness:double col:EVGColor) {
772
+ if ((array_length pts) < 4) {
773
+ return
774
+ }
775
+ def c (new EVGDrawCmd())
776
+ c.kind = 7
777
+ c.thickness = thickness
778
+ c.r = (col.red())
779
+ c.g = (col.green())
780
+ c.b = (col.blue())
781
+ c.a = (col.alpha())
782
+ def i:int 0
783
+ while (i < (array_length pts)) {
784
+ push c.pts (itemAt pts i)
785
+ i = (i + 1)
786
+ }
787
+ push c.ringEnds (array_length c.pts)
788
+ this.setPolyBounds(c)
789
+ this.addCmd(c)
790
+ }
791
+
792
+ ; A filled shape made of SEVERAL rings, with holes.
793
+ ;
794
+ ; `addPolygon` takes one ring, so a caller with an outline — a magnifier's
795
+ ; glass, a letter O, any shape drawn as outer-boundary-then-inner — had to
796
+ ; call it once per ring, and each call fills independently. The hole comes
797
+ ; out solid. `EVGDrawCmd` has carried `ringEnds` and `evenOdd` all along
798
+ ; for the SVG walk to use; this is the same thing for anyone who computes
799
+ ; rings themselves.
800
+ ;
801
+ ; `rings` is a list of flattened point arrays, already in page coordinates.
802
+ fn addPolyRings:void (rings:[PathRing] col:EVGColor evenOddFill:boolean) {
803
+ if ((array_length rings) == 0) {
804
+ return
805
+ }
806
+ def c (new EVGDrawCmd())
807
+ c.kind = 6
808
+ c.evenOdd = evenOddFill
809
+ c.r = (col.red())
810
+ c.g = (col.green())
811
+ c.b = (col.blue())
812
+ c.a = (col.alpha())
813
+ def i:int 0
814
+ while (i < (array_length rings)) {
815
+ def ring:PathRing (itemAt rings i)
816
+ def j:int 0
817
+ while (j < (array_length ring.pts)) {
818
+ push c.pts (itemAt ring.pts j)
819
+ j = (j + 1)
820
+ }
821
+ push c.ringEnds (array_length c.pts)
822
+ i = (i + 1)
823
+ }
824
+ if ((array_length c.pts) < 6) {
825
+ return
826
+ }
827
+ this.setPolyBounds(c)
828
+ this.addCmd(c)
829
+ }
830
+
831
+ fn addPolygon:void (pts:[double] col:EVGColor) {
832
+ if ((array_length pts) < 6) {
833
+ return
834
+ }
835
+ def c (new EVGDrawCmd())
836
+ c.kind = 6
837
+ c.r = (col.red())
838
+ c.g = (col.green())
839
+ c.b = (col.blue())
840
+ c.a = (col.alpha())
841
+ def i:int 0
842
+ while (i < (array_length pts)) {
843
+ push c.pts (itemAt pts i)
844
+ i = (i + 1)
845
+ }
846
+ push c.ringEnds (array_length c.pts)
847
+ this.setPolyBounds(c)
848
+ this.addCmd(c)
849
+ }
850
+
851
+ ; A path command still carries a box, because the GL backend scissors and
852
+ ; batches by it. Derived from the points rather than left at zero, which
853
+ ; clipped every computed path to a 0x0 rectangle.
854
+ fn setPolyBounds:void (c:EVGDrawCmd) {
855
+ def minX:double 0.0
856
+ def minY:double 0.0
857
+ def maxX:double 0.0
858
+ def maxY:double 0.0
859
+ def n:int (idiv (array_length c.pts) 2)
860
+ def i:int 0
861
+ while (i < n) {
862
+ def x:double (itemAt c.pts (i * 2))
863
+ def yat:int ((i * 2) + 1)
864
+ def y:double (itemAt c.pts yat)
865
+ if (i == 0) {
866
+ minX = x
867
+ maxX = x
868
+ minY = y
869
+ maxY = y
870
+ } {
871
+ if (x < minX) { minX = x }
872
+ if (x > maxX) { maxX = x }
873
+ if (y < minY) { minY = y }
874
+ if (y > maxY) { maxY = y }
875
+ }
876
+ i = (i + 1)
877
+ }
878
+ c.x = minX
879
+ c.y = minY
880
+ c.w = (maxX - minX)
881
+ c.h = (maxY - minY)
882
+ }
883
+
884
+ fn addClipEnd:void () {
885
+ def c:EVGDrawCmd (new EVGDrawCmd)
886
+ c.kind = 5
887
+ this.addCmd(c)
888
+ }
889
+
890
+ ; --- COMMANDS APPENDED AFTER THE WALK, INSIDE A SCROLL ---------------------
891
+ ;
892
+ ; The walk puts a scroll container's commands inside its layer, so a kept
893
+ ; list moves them — see `refreshLayers`. Something APPENDED AFTERWARDS is
894
+ ; not inside anything: an application drawing a chart into a box the
895
+ ; layout placed cannot do it until the layout has placed it, so it comes
896
+ ; after the whole tree is down.
897
+ ;
898
+ ; Those commands used to sit at the position they were built at while the
899
+ ; kept list moved the page around them. On a scrolling diary that is a
900
+ ; chart's line floating over the page, on top of everything, snapping into
901
+ ; its card the moment the scroll stopped and the list was built again.
902
+ ;
903
+ ; This opens a clip that is A LAYER OF THE SAME CONTAINER: what is
904
+ ; appended between it and `addLayerClipEnd` moves exactly as the
905
+ ; container's own content moves, and stops being valid at exactly the
906
+ ; moment the container's content does — the cull range is the same one.
907
+ ; When the container has no layer in this list, because it does not scroll
908
+ ; or the list was built with culling off, this is an ordinary clip, which
909
+ ; is the right answer in both cases.
910
+ fn addLayerClip:void (el:EVGElement x:double y:double w:double h:double) {
911
+ def src:int (this.contentLayerFor(el))
912
+ def cp (new EVGDrawCmd())
913
+ cp.kind = 4
914
+ cp.x = x
915
+ cp.y = y
916
+ cp.w = w
917
+ cp.h = h
918
+ if (src >= 0) {
919
+ cp.layer = ((array_length layerEls) + 1)
920
+ push layerStack (array_length layerEls)
921
+ push layerEls el
922
+ ; Built now, so the offset it was built at is the offset now.
923
+ push layerTop el.scrollTop
924
+ push layerLeft el.scrollLeft
925
+ push layerShiftX 0.0
926
+ push layerShiftY 0.0
927
+ push layerClip (array_length cmds)
928
+ push layerFirst ((array_length cmds) + 1)
929
+ push layerLast ((array_length cmds) + 1)
930
+ ; The same range the content was built for, so the two go stale
931
+ ; together and one rebuild answers for both.
932
+ push layerCullL (itemAt layerCullL src)
933
+ push layerCullT (itemAt layerCullT src)
934
+ push layerCullR (itemAt layerCullR src)
935
+ push layerCullB (itemAt layerCullB src)
936
+ push layerKind 0
937
+ push layerScale 1.0
938
+ push layerPct 0
939
+ push layerLabel (0 - 1)
940
+ }
941
+ this.addCmd(cp)
942
+ }
943
+
944
+ fn addLayerClipEnd:void () {
945
+ if ((array_length layerStack) > 0) {
946
+ def done:int (itemAt layerStack ((array_length layerStack) - 1))
947
+ set layerLast done (array_length cmds)
948
+ removeLast layerStack
949
+ }
950
+ def pp (new EVGDrawCmd())
951
+ pp.kind = 5
952
+ this.addCmd(pp)
953
+ }
954
+
955
+ ; Is this rectangle inside what the list was BUILT FOR — the overscan
956
+ ; range of `el`'s scroll layer, not merely the part of it on the screen?
957
+ ;
958
+ ; The difference is what a kept list lives on. `emitCharts` and anything
959
+ ; else appending after the walk has to ask the same question the walk
960
+ ; asked, or it emits only what is visible NOW and the kept list has
961
+ ; nothing to show for a card that scrolls in: the card arrives complete
962
+ ; except for the one thing drawn outside the walk, which appears when the
963
+ ; list is next built — when the scroll stops.
964
+ ;
965
+ ; With no layer for `el` there is nothing kept and nothing to overscan, so
966
+ ; the answer is the plain overlap with the element's own box.
967
+ fn layerCovers:boolean (el:EVGElement x:double y:double w:double h:double) {
968
+ def i:int (this.contentLayerFor(el))
969
+ if (i < 0) {
970
+ if ((y + h) < el.calculatedY) {
971
+ return false
972
+ }
973
+ if (y > (el.calculatedY + el.calculatedHeight)) {
974
+ return false
975
+ }
976
+ if ((x + w) < el.calculatedX) {
977
+ return false
978
+ }
979
+ if (x > (el.calculatedX + el.calculatedWidth)) {
980
+ return false
981
+ }
982
+ return true
983
+ }
984
+ if ((y + h) < (itemAt layerCullT i)) {
985
+ return false
986
+ }
987
+ if (y > (itemAt layerCullB i)) {
988
+ return false
989
+ }
990
+ if ((x + w) < (itemAt layerCullL i)) {
991
+ return false
992
+ }
993
+ if (x > (itemAt layerCullR i)) {
994
+ return false
995
+ }
996
+ return true
997
+ }
998
+
999
+ ; The layer this element's CONTENT was built into, or -1. `layerFor` above
1000
+ ; answers with whichever layer of the element comes first; a scroll
1001
+ ; container has two — its content and its scrollbar thumb — and only the
1002
+ ; content's range is the one an appended command belongs in.
1003
+ fn contentLayerFor:int (el:EVGElement) {
1004
+ def i:int 0
1005
+ while (i < (array_length layerEls)) {
1006
+ if ((itemAt layerKind i) == 0) {
1007
+ if ((itemAt layerEls i) == el) {
1008
+ return i
1009
+ }
1010
+ }
1011
+ i = (i + 1)
1012
+ }
1013
+ return (0 - 1)
1014
+ }
1015
+
1016
+
1017
+ ; The element's imported document, parsed once and kept on the element
1018
+ ; — see `EVGElement.svgDoc`. Every frame that draws the element asked
1019
+ ; for a fresh parse of the same markup before this.
1020
+ fn svgDocumentOf:SvgDocument (el:EVGElement) {
1021
+ def key@(mutable):string el.svgSource
1022
+ if el.fillColor.isSet {
1023
+ def fill:EVGColor el.fillColor
1024
+ key = (key + ("|" + ((to_string fill.r) + ("," + ((to_string fill.g) + ("," + ((to_string fill.b) + ("," + (to_string fill.a)))))))))
1025
+ }
1026
+ if el.svgDoc {
1027
+ if (el.svgDocKey == key) {
1028
+ return (unwrap el.svgDoc)
1029
+ }
1030
+ }
1031
+ def sp (new SvgParser())
1032
+ if el.fillColor.isSet {
1033
+ sp.setInitialFill((unwrap el.fillColor))
1034
+ }
1035
+ def doc:SvgDocument (sp.parse(el.svgSource))
1036
+ el.svgDoc = doc
1037
+ el.svgDocKey = key
1038
+ return doc
1039
+ }
1040
+
1041
+ ; A `<path>` element, flattened into rings and pushed as a fill and/or a
1042
+ ; stroke. The transform is the one the raster and PDF targets build: the
1043
+ ; viewBox mapping, then the element's position — so the points come out in
1044
+ ; page coordinates and a backend can draw them as they are.
1045
+ ; An imported SVG document (PLAN_VECTOR_IR.md Stage 4). Without this a
1046
+ ; <Svg> would simply be absent from a GL backend while appearing in the
1047
+ ; other three, which is the class of divergence this file exists to stop —
1048
+ ; the same reason border-radius once worked in PDF and silently not in PNG.
1049
+ ;
1050
+ ; The importer has already resolved paint and baked group transforms, so
1051
+ ; this is the single-path walk with the per-shape paint coming from the item
1052
+ ; rather than from the element.
1053
+ fn walkSvgDocument:void (el:EVGElement x:double y:double w:double h:double) {
1054
+ def doc:SvgDocument (this.svgDocumentOf(el))
1055
+ if ((doc.itemCount()) == 0) {
1056
+ return
1057
+ }
1058
+
1059
+ def vb:ViewBoxRect (doc.effectiveViewBox())
1060
+ def m:Matrix2D (VectorViewBox.resolve(vb w h "xMidYMid meet"))
1061
+ def steps:int (this.flattenSteps(w h))
1062
+ def scale:double m.a
1063
+ if (scale < 0.0) {
1064
+ scale = (0.0 - scale)
1065
+ }
1066
+ if (scale <= 0.0) {
1067
+ scale = 1.0
1068
+ }
1069
+
1070
+ def k:int 0
1071
+ while (k < (doc.itemCount())) {
1072
+ def item:SvgVectorItem (itemAt doc.items k)
1073
+ def parser:SVGPathParser (SVGPathParser.fromCommands(item.commands))
1074
+ def rings:[PathRing] (parser.flattenRings(steps m.a m.b m.c m.d (m.e + x) (m.f + y)))
1075
+ if ((array_length rings) > 0) {
1076
+ if (item.hasFill()) {
1077
+ def cf (new EVGDrawCmd())
1078
+ cf.kind = 6
1079
+ cf.x = x
1080
+ cf.y = y
1081
+ cf.w = w
1082
+ cf.h = h
1083
+ cf.evenOdd = (item.fillRule == "evenodd")
1084
+ cf.r = (item.fillColor.red())
1085
+ cf.g = (item.fillColor.green())
1086
+ cf.b = (item.fillColor.blue())
1087
+ cf.a = (item.fillColor.alpha())
1088
+ this.copyRings(cf rings)
1089
+ this.addCmd(cf)
1090
+ }
1091
+ if (item.hasStroke()) {
1092
+ def cs (new EVGDrawCmd())
1093
+ cs.kind = 7
1094
+ cs.x = x
1095
+ cs.y = y
1096
+ cs.w = w
1097
+ cs.h = h
1098
+ cs.thickness = (item.strokeWidth * scale)
1099
+ cs.r = (item.strokeColor.red())
1100
+ cs.g = (item.strokeColor.green())
1101
+ cs.b = (item.strokeColor.blue())
1102
+ cs.a = (item.strokeColor.alpha())
1103
+ this.copyRings(cs rings)
1104
+ this.addCmd(cs)
1105
+ }
1106
+ }
1107
+ k = (k + 1)
1108
+ }
1109
+ }
1110
+
1111
+ ; Subdivision chosen from the size it is drawn at, as every other target
1112
+ ; does: a large shape must not show facets, a small one must not pay for
1113
+ ; detail no pixel can show.
1114
+ ;
1115
+ ; DRAWN at, not laid out at. A transform is the difference between the
1116
+ ; two, and on a canvas that zooms it is a large one: a Figma board at
1117
+ ; 10% was flattening every glyph in it into the 48 segments a curve
1118
+ ; 640 layout pixels wide deserves, to draw it five pixels long. The
1119
+ ; scale of the transforms this subtree is under is carried down in
1120
+ ; `drawScale` — see `walk` — so the count follows the pixels.
1121
+ fn flattenSteps:int (w:double h:double) {
1122
+ def span:double (w * drawScale)
1123
+ def tall:double (h * drawScale)
1124
+ if (tall > span) {
1125
+ span = tall
1126
+ }
1127
+ def steps:int (to_int (span / 6.0))
1128
+ if (steps < 4) {
1129
+ steps = 4
1130
+ }
1131
+ if (steps > 48) {
1132
+ steps = 48
1133
+ }
1134
+ return steps
1135
+ }
1136
+
1137
+ fn walkPath:void (el:EVGElement x:double y:double w:double h:double) {
1138
+ if ((strlen el.svgSource) > 0) {
1139
+ this.walkSvgDocument(el x y w h)
1140
+ return
1141
+ }
1142
+
1143
+ def pathData:string el.svgPath
1144
+ if ((strlen pathData) == 0) {
1145
+ return
1146
+ }
1147
+ def steps:int (this.flattenSteps(w h))
1148
+ def rings:[PathRing] (el.ringsCache)
1149
+ def sameBox:boolean ((((el.ringsX == x) && (el.ringsY == y)) && (el.ringsW == w)) && (el.ringsH == h))
1150
+ ; …and the FIT, which decides the matrix the rings were flattened
1151
+ ; through. Left out of the key, a shape that started stretching went
1152
+ ; on drawing the rings it had before it did.
1153
+ def sameGeom:boolean ((((el.ringsSteps == steps) && (el.ringsPath == pathData)) && (el.ringsViewBox == el.viewBox)) && (el.ringsFit == el.preserveAspectRatio))
1154
+ def fresh:boolean ((el.ringsHave && sameBox) && sameGeom)
1155
+ if (fresh) {
1156
+ } {
1157
+ def parser (new SVGPathParser())
1158
+ parser.parse(pathData)
1159
+ def b:PathBounds (parser.getBounds())
1160
+ def vb:ViewBoxRect (VectorViewBox.effectiveViewBox(el.viewBox b.minX b.minY b.width b.height))
1161
+ def m:Matrix2D (VectorViewBox.resolve(vb w h el.preserveAspectRatio))
1162
+ rings = (parser.flattenRings(steps m.a m.b m.c m.d (m.e + x) (m.f + y)))
1163
+ ; Kept for the next walk — see the note on `EVGElement.ringsCache`.
1164
+ el.ringsCache = rings
1165
+ el.ringsHave = true
1166
+ el.ringsPath = pathData
1167
+ el.ringsX = x
1168
+ el.ringsY = y
1169
+ el.ringsW = w
1170
+ el.ringsH = h
1171
+ el.ringsSteps = steps
1172
+ el.ringsScale = m.a
1173
+ el.ringsViewBox = el.viewBox
1174
+ el.ringsFit = el.preserveAspectRatio
1175
+ }
1176
+ if ((array_length rings) == 0) {
1177
+ return
1178
+ }
1179
+
1180
+ def fillColor:EVGColor el.fillColor
1181
+ if (fillColor.isSet == false) {
1182
+ fillColor = el.backgroundColor
1183
+ }
1184
+ if fillColor.isSet {
1185
+ def cf (new EVGDrawCmd())
1186
+ cf.kind = 6
1187
+ cf.x = x
1188
+ cf.y = y
1189
+ cf.w = w
1190
+ cf.h = h
1191
+ cf.evenOdd = (el.fillRule == "evenodd")
1192
+ cf.r = (fillColor.red())
1193
+ cf.g = (fillColor.green())
1194
+ cf.b = (fillColor.blue())
1195
+ cf.a = (fillColor.alpha())
1196
+ ; A GRADIENT ON A SHAPE THAT IS NOT ITS BOX. `hasGrad` was
1197
+ ; written for rectangles only, so a vector filled with a ramp
1198
+ ; kept its outline and lost the shading — or, flattened to the
1199
+ ; average of its two ends, kept the shading and lost the ramp.
1200
+ ; The command carries both: the rings say what shape it is, the
1201
+ ; two stops and the direction say how it is shaded, and the box
1202
+ ; the stops run across is the command's own x/y/w/h.
1203
+ if (EVGDisplayList.hasLinearGradient(el)) {
1204
+ EVGDisplayList.applyGradient(cf el)
1205
+ }
1206
+ this.copyRings(cf rings)
1207
+ this.addCmd(cf)
1208
+ }
1209
+
1210
+ ; A CONNECTOR'S HEADS, which are filled while its shaft is stroked.
1211
+ ; They are a path of their own — see `EVGElement.arrowPath` — because
1212
+ ; one path cannot be half filled: a fill closes every subpath it has,
1213
+ ; and an orthogonal shaft closed is a triangle nobody asked for.
1214
+ if ((strlen el.arrowPath) > 0) {
1215
+ def hc:EVGColor (el.arrowFillColor())
1216
+ if hc.isSet {
1217
+ def hp (new SVGPathParser())
1218
+ hp.parse(el.arrowPath)
1219
+ def hb:PathBounds (hp.getBounds())
1220
+ def hvb:ViewBoxRect (VectorViewBox.effectiveViewBox(el.viewBox hb.minX hb.minY hb.width hb.height))
1221
+ def hm:Matrix2D (VectorViewBox.resolve(hvb w h el.preserveAspectRatio))
1222
+ def hrings:[PathRing] (hp.flattenRings(steps hm.a hm.b hm.c hm.d (hm.e + x) (hm.f + y)))
1223
+ if ((array_length hrings) > 0) {
1224
+ def ch (new EVGDrawCmd())
1225
+ ch.kind = 6
1226
+ ch.x = x
1227
+ ch.y = y
1228
+ ch.w = w
1229
+ ch.h = h
1230
+ ch.r = (hc.red())
1231
+ ch.g = (hc.green())
1232
+ ch.b = (hc.blue())
1233
+ ch.a = (hc.alpha())
1234
+ this.copyRings(ch hrings)
1235
+ this.addCmd(ch)
1236
+ }
1237
+ }
1238
+ }
1239
+
1240
+ if el.strokeColor.isSet {
1241
+ if (el.strokeWidth > 0.0) {
1242
+ def sc:EVGColor el.strokeColor
1243
+ def cs (new EVGDrawCmd())
1244
+ cs.kind = 7
1245
+ cs.x = x
1246
+ cs.y = y
1247
+ cs.w = w
1248
+ cs.h = h
1249
+ ; stroke-width is in user units, so it scales with the geometry.
1250
+ def scale:double el.ringsScale
1251
+ if (scale < 0.0) {
1252
+ scale = (0.0 - scale)
1253
+ }
1254
+ if (scale <= 0.0) {
1255
+ scale = 1.0
1256
+ }
1257
+ cs.thickness = (el.strokeWidth * scale)
1258
+ cs.strokeCap = (EVGDisplayList.capCode(el.strokeLineCap))
1259
+ cs.strokeJoin = (EVGDisplayList.joinCode(el.strokeLineJoin))
1260
+ cs.strokeDash = el.strokeDashArray
1261
+ cs.strokeDashOffset = el.strokeDashOffset
1262
+ cs.r = (sc.red())
1263
+ cs.g = (sc.green())
1264
+ cs.b = (sc.blue())
1265
+ cs.a = (sc.alpha())
1266
+ this.copyRings(cs rings)
1267
+ this.addCmd(cs)
1268
+ }
1269
+ }
1270
+ }
1271
+
1272
+ fn copyRings:void (c:EVGDrawCmd rings:[PathRing]) {
1273
+ def i:int 0
1274
+ while (i < (array_length rings)) {
1275
+ def ring:PathRing (itemAt rings i)
1276
+ def k:int 0
1277
+ while (k < (array_length ring.pts)) {
1278
+ push c.pts (itemAt ring.pts k)
1279
+ k = (k + 1)
1280
+ }
1281
+ ; A stroke is drawn segment by segment, so a ring Z closed gets
1282
+ ; its closing edge as one more point — back to where it began.
1283
+ ; A fill closes by itself and is left as it is.
1284
+ if ((c.kind == 7) && ring.closed) {
1285
+ if ((ring.pointCount()) >= 2) {
1286
+ push c.pts (itemAt ring.pts 0)
1287
+ push c.pts (itemAt ring.pts 1)
1288
+ }
1289
+ }
1290
+ push c.ringEnds (array_length c.pts)
1291
+ i = (i + 1)
1292
+ }
1293
+ }
1294
+
1295
+ ; Walk a LAID-OUT tree. Call after EVGLayout.layout(root).
1296
+ ;
1297
+ ; Two passes, because an overlay surface has to be drawn ABOVE the page and
1298
+ ; not merely later in the tree. The first pass draws everything that is in
1299
+ ; flow and puts each surface it meets aside; the second draws those, in the
1300
+ ; order they were reached. A surface met INSIDE a surface is appended while
1301
+ ; that loop is running, so a submenu lands after the menu that opened it.
1302
+ ;
1303
+ ; That order is a STACK — the order things opened in — not a z-index. There
1304
+ ; is no number to get wrong and nothing for two components to disagree
1305
+ ; about, and `EVGHitTest` builds the same list so that what is clicked is
1306
+ ; what is seen.
1307
+ ;
1308
+ ; Deferring is also what takes a surface out of an ancestor's clip: the
1309
+ ; second pass runs after the clip was popped, which is what a dropdown
1310
+ ; inside a scrolling panel needs and what `overflow: hidden` would
1311
+ ; otherwise forbid.
1312
+ ; --- VIEWPORT CULLING -----------------------------------------------------
1313
+ ;
1314
+ ; A scrolled document is mostly not on the screen. Walking all of it to
1315
+ ; emit commands for the tenth that is was the largest thing left in a
1316
+ ; scroll frame of `gallery/realtrainer` once the layout stopped running:
1317
+ ; `walkOpaque` alone — resolving corners, colours and gradients for rows
1318
+ ; ten thousand pixels below the fold — was 17% of it, and the text it drew
1319
+ ; had to be re-broken into lines to be drawn at all.
1320
+ ;
1321
+ ; So a subtree that cannot reach the clip it is inside is not walked. Two
1322
+ ; things make that safe rather than merely fast:
1323
+ ;
1324
+ ; It is tested against `EVGElement.paintTop`/`paintBottom` — the union
1325
+ ; of the subtree, written by the layout — and NOT against the element's
1326
+ ; own box, which says nothing about where its children are.
1327
+ ;
1328
+ ; It happens only inside a real CLIP. An element that overflows a
1329
+ ; container that does not clip is drawn, and correctly: there is no clip
1330
+ ; rectangle in force, so nothing is culled. The scroll containers are
1331
+ ; exactly the elements that clip, which is why this needs no notion of
1332
+ ; scrolling at all.
1333
+ ;
1334
+ ; A subtree carrying a transform is never culled — see `paintUnbounded`.
1335
+ ;
1336
+ ; THE MARGIN is what keeps a fling from showing blank rows: the clip is
1337
+ ; grown by `overscanBefore` above and `overscanAfter` below before
1338
+ ; anything is tested, so the rows just past the edge are already drawn
1339
+ ; when they arrive. A host that knows which way the finger went can make
1340
+ ; the two different — big ahead, small behind — through `setOverscan`.
1341
+ ; The default is one clip height each way, which also covers the shadow of
1342
+ ; a box just off the edge.
1343
+ def culling:boolean true
1344
+ ; --- SCROLL LAYERS -------------------------------------------------------
1345
+ ;
1346
+ ; A scroll container's clip is a LAYER: the commands between its push
1347
+ ; and its pop are the content the container shows, drawn with the
1348
+ ; overscan above and below, in page coordinates at the offset the
1349
+ ; container had when the list was built. Scrolling the container does
1350
+ ; not need a new list — the same commands, moved by the difference —
1351
+ ; until the offset leaves the range the overscan covered. That is what
1352
+ ; `refreshLayers` does, and it is what lets a host keep this list, and
1353
+ ; whatever it made from it, across a whole fling: a browser keeps its
1354
+ ; vertex buffers on the card and changes one uniform per frame; a
1355
+ ; CoreGraphics or Android painter paints the same commands, moved, and
1356
+ ; pays no layout and no walk.
1357
+ ;
1358
+ ; The commands ARE moved, in place, so a painter that knows nothing of
1359
+ ; layers draws the right frame. A painter that keeps something built from
1360
+ ; the commands — the browser's buffers — reads `layerShiftX/Y`, the total
1361
+ ; move since the build, and applies the part it has not built in.
1362
+ ;
1363
+ ; A layer is recorded only while culling is on: an uncut list is asked for
1364
+ ; by a check that wants every command, and it should not be kept.
1365
+ ;
1366
+ ; `buildSeq` names the build, for a host that keeps what it made from a
1367
+ ; list and has to know whether the list it was handed is the one it
1368
+ ; built from. The app writes it: the list cannot number its own builds
1369
+ ; across lists, and it is lists the host is telling apart.
1370
+ def buildSeq:int 0
1371
+ def layerEls:[EVGElement]
1372
+ def layerTop:[double]
1373
+ def layerLeft:[double]
1374
+ def layerShiftX:[double]
1375
+ def layerShiftY:[double]
1376
+ def layerCullL:[double]
1377
+ def layerCullT:[double]
1378
+ def layerCullR:[double]
1379
+ def layerCullB:[double]
1380
+ def layerClip:[int]
1381
+ def layerFirst:[int]
1382
+ def layerLast:[int]
1383
+ def layerStack:[int]
1384
+ ; What a layer is: 0 the content of a scroll container, 1 the THUMB of
1385
+ ; its scrollbar — a layer of its own, so a kept list moves it the way it
1386
+ ; moves the content, by the scroll and `layerScale`, the thumb's pixels
1387
+ ; per pixel of content.
1388
+ def layerKind:[int]
1389
+ def layerScale:[double]
1390
+
1391
+ ; --- SCROLLBARS ----------------------------------------------------------
1392
+ ;
1393
+ ; A scroll container draws its own bar: a track along its right edge
1394
+ ; and a thumb whose length is the container's share of its content and
1395
+ ; whose place is the offset. It is drawn by the list rather than by a
1396
+ ; host, so every host has the same one, and it is drawn in three states
1397
+ ; a host reports through `setScrollbarState`: 0, at rest — a thin,
1398
+ ; faint thumb and no track, so it says where the page is without
1399
+ ; asking to be looked at; 1, scrolling — wider and brighter, with the
1400
+ ; track, which is what makes a fling readable; 2, the pointer on it or
1401
+ ; dragging it — wider still. A container the pointer is over is named
1402
+ ; through `setScrollbarHover` and drawn in state 2 whatever the rest are.
1403
+ ;
1404
+ ; The thumb is a layer (`layerKind` 1): a kept list moves it per frame
1405
+ ; with the content, and a host that keeps its frame moves it with a
1406
+ ; uniform. The bar's states are not per-frame — they change when a
1407
+ ; scroll starts and ends and when the pointer arrives and leaves — and
1408
+ ; each change is one build.
1409
+ ;
1410
+ ; Off unless an app asks: a list is also what the PDF and PNG targets and
1411
+ ; the conformance oracles draw from, and a bar in those is a difference
1412
+ ; nobody asked for.
1413
+ ; --- FRAGMENTS -------------------------------------------------------------
1414
+ ;
1415
+ ; A subtree an app keeps (`EVGElement.keepLayout`) draws the same
1416
+ ; commands each time it is walked: the text broken into the same lines,
1417
+ ; the same icons flattened. So the first walk of it keeps the commands
1418
+ ; it emitted, and the walks after emit THE SAME COMMANDS again — not
1419
+ ; copies: the list that held them is the list being replaced. They are
1420
+ ; where the subtree is, or are moved to it first: a kept list moves its
1421
+ ; commands with the scroll exactly as the layout moves the elements, so
1422
+ ; after a scroll they agree already, and after a rebuild that moved the
1423
+ ; subtree by the flow they are moved by the difference, in place. The
1424
+ ; difference is read off the first command's distance from the element,
1425
+ ; which a move of both leaves alone.
1426
+ ;
1427
+ ; They hold while the sheet has written no paint in the subtree and the
1428
+ ; layout has not laid it out for real since (`EVGElement.paintStamp`),
1429
+ ; and while nothing is in flight anywhere, because a flight writes paint
1430
+ ; between the sheet's passes. The recording walks the whole subtree,
1431
+ ; culling off, so a card half over the edge is recorded whole and shown
1432
+ ; whole when it arrives; a subtree that opened a scroll layer is not
1433
+ ; kept, its layer would not be the new list's. On a diary of a hundred
1434
+ ; workouts this is the difference between a list built by walking
1435
+ ; twelve thousand commands' worth of elements and one built by walking
1436
+ ; the shell.
1437
+ def fragments:boolean true
1438
+ ; How many kept subtrees this build drew from what they had, and how
1439
+ ; many it walked — for a check, and for a bench.
1440
+ def fragHits:int 0
1441
+ def fragMisses:int 0
1442
+ def fragEls:[EVGElement]
1443
+ def fragLists:[[EVGDrawCmd]]
1444
+ def fragStamps:[int]
1445
+ def fragOffX:[double]
1446
+ def fragOffY:[double]
1447
+
1448
+ fn setFragments:void (on:boolean) {
1449
+ fragments = on
1450
+ }
1451
+
1452
+ ; The fragments the last list kept, handed to this one by the app.
1453
+ fn takeFragments:void (from:EVGDisplayList) {
1454
+ fragEls = from.fragEls
1455
+ fragLists = from.fragLists
1456
+ fragStamps = from.fragStamps
1457
+ fragOffX = from.fragOffX
1458
+ fragOffY = from.fragOffY
1459
+ }
1460
+
1461
+ fn forgetFragments:void () {
1462
+ clear fragEls
1463
+ clear fragLists
1464
+ clear fragStamps
1465
+ clear fragOffX
1466
+ clear fragOffY
1467
+ }
1468
+
1469
+ fn fragmentOf:int (el:EVGElement) {
1470
+ def i:int 0
1471
+ while (i < (array_length fragEls)) {
1472
+ if ((itemAt fragEls i) == el) {
1473
+ return i
1474
+ }
1475
+ i = (i + 1)
1476
+ }
1477
+ return (0 - 1)
1478
+ }
1479
+
1480
+ ; Emit a kept subtree's commands again, moved to where it is now.
1481
+ ; False when there is nothing kept that holds.
1482
+ fn emitFragment:boolean (el:EVGElement) {
1483
+ def fi:int (this.fragmentOf(el))
1484
+ if (fi < 0) {
1485
+ return false
1486
+ }
1487
+ if ((itemAt fragStamps fi) != el.paintStamp) {
1488
+ return false
1489
+ }
1490
+ def kept:[EVGDrawCmd] (itemAt fragLists fi)
1491
+ if ((array_length kept) == 0) {
1492
+ return false
1493
+ }
1494
+ def first:EVGDrawCmd (itemAt kept 0)
1495
+ def dx:double (el.calculatedX - (first.x + (itemAt fragOffX fi)))
1496
+ def dy:double (el.calculatedY - (first.y + (itemAt fragOffY fi)))
1497
+ for kept c:EVGDrawCmd ci {
1498
+ if ((dx != 0.0) || (dy != 0.0)) {
1499
+ c.x = (c.x + dx)
1500
+ c.y = (c.y + dy)
1501
+ if c.hasRotOrigin {
1502
+ c.rotOriginX = (c.rotOriginX + dx)
1503
+ c.rotOriginY = (c.rotOriginY + dy)
1504
+ }
1505
+ def n:int (array_length c.pts)
1506
+ def p:int 0
1507
+ while (p < n) {
1508
+ set c.pts p ((itemAt c.pts p) + dx)
1509
+ set c.pts (p + 1) ((itemAt c.pts (p + 1)) + dy)
1510
+ p = (p + 2)
1511
+ }
1512
+ }
1513
+ this.addCmd(c)
1514
+ }
1515
+ return true
1516
+ }
1517
+
1518
+ ; Keep what the walk just emitted for a subtree — or not, when it
1519
+ ; opened a scroll layer, or emitted nothing to be placed by.
1520
+ fn recordFragment:void (el:EVGElement start:int) {
1521
+ def kept:[EVGDrawCmd]
1522
+ def i:int start
1523
+ def n:int (array_length cmds)
1524
+ while (i < n) {
1525
+ def c:EVGDrawCmd (itemAt cmds i)
1526
+ if (c.layer > 0) {
1527
+ return
1528
+ }
1529
+ push kept c
1530
+ i = (i + 1)
1531
+ }
1532
+ if ((array_length kept) == 0) {
1533
+ return
1534
+ }
1535
+ def first:EVGDrawCmd (itemAt kept 0)
1536
+ def offX:double (el.calculatedX - first.x)
1537
+ def offY:double (el.calculatedY - first.y)
1538
+ def fi:int (this.fragmentOf(el))
1539
+ if (fi < 0) {
1540
+ push fragEls el
1541
+ push fragLists kept
1542
+ push fragStamps el.paintStamp
1543
+ push fragOffX offX
1544
+ push fragOffY offY
1545
+ return
1546
+ }
1547
+ set fragLists fi kept
1548
+ set fragStamps fi el.paintStamp
1549
+ set fragOffX fi offX
1550
+ set fragOffY fi offY
1551
+ }
1552
+
1553
+ def scrollbars:boolean false
1554
+ def barState:int 0
1555
+ def barHover@(optional):EVGElement
1556
+ ; The container the pointer is NEAR — within `barNearPx` of its right
1557
+ ; edge — whose bar is up for it, so it can be found and taken. Near is
1558
+ ; not on: nothing under the pointer is kept from a press by it.
1559
+ def barNear@(optional):EVGElement
1560
+ ; The thumb's label — "34 %" — and the percent it was written for: a
1561
+ ; scroll that changes the number rewrites the text in place, which a
1562
+ ; painter reads as it draws and a host that keeps its frame is told
1563
+ ; about through `frameSeq`.
1564
+ def layerPct:[int]
1565
+ def layerLabel:[int]
1566
+ ; Bumped whenever a command in a KEPT list changed in a way a frame
1567
+ ; built from it cannot follow with a shift — a label's text. A host that
1568
+ ; keeps its frame rebuilds it when this moves; the list is kept.
1569
+ def frameSeq:int 0
1570
+ ; What is painted under the element being walked: the nearest set
1571
+ ; background up the tree, kept as the walk descends. The bar is ink on
1572
+ ; it — light ink on a dark page, dark on a light one — because the
1573
+ ; container's own text colour is not inherited onto it and a black thumb
1574
+ ; on a black page was reported as no thumb at all.
1575
+ def backdropR:int 255
1576
+ def backdropG:int 255
1577
+ def backdropB:int 255
1578
+
1579
+ fn setScrollbars:void (on:boolean) {
1580
+ scrollbars = on
1581
+ }
1582
+
1583
+ fn setScrollbarState:void (state:int) {
1584
+ barState = state
1585
+ }
1586
+
1587
+ fn setScrollbarHover:void (el@(optional):EVGElement) {
1588
+ barHover = el
1589
+ }
1590
+
1591
+ fn setScrollbarNear:void (el@(optional):EVGElement) {
1592
+ barNear = el
1593
+ }
1594
+
1595
+ ; How far from a container's right edge the pointer brings its bar up.
1596
+ sfn barNearPx:double () {
1597
+ return 36.0
1598
+ }
1599
+
1600
+ sfn pctLabel:string (pct:int) {
1601
+ return ((to_string pct) + " %")
1602
+ }
1603
+
1604
+ ; How wide the thumb is in a state, in page pixels; `thin` is CSS's
1605
+ ; `scrollbar-width: thin`.
1606
+ sfn barWidth:double (state:int thin:boolean) {
1607
+ if thin {
1608
+ if (state >= 2) {
1609
+ return 7.0
1610
+ }
1611
+ if (state == 1) {
1612
+ return 5.0
1613
+ }
1614
+ return 3.0
1615
+ }
1616
+ if (state >= 2) {
1617
+ return 11.0
1618
+ }
1619
+ if (state == 1) {
1620
+ return 8.0
1621
+ }
1622
+ return 4.0
1623
+ }
1624
+
1625
+ ; The bar for a container whose clip was just popped: the track when the
1626
+ ; bar is up, and the thumb as a layer of its own, clipped to the track.
1627
+ fn addScrollbar:void (el:EVGElement x:double y:double w:double h:double) {
1628
+ def maxY:double (el.maxScrollTop())
1629
+ if (maxY <= 0.0) {
1630
+ return
1631
+ }
1632
+ def state@(mutable):int barState
1633
+ if barNear {
1634
+ if ((unwrap barNear) == el) {
1635
+ if (state < 1) {
1636
+ state = 1
1637
+ }
1638
+ }
1639
+ }
1640
+ if barHover {
1641
+ if ((unwrap barHover) == el) {
1642
+ state = 2
1643
+ }
1644
+ }
1645
+ if (el.scrollbarWidth == "none") {
1646
+ return
1647
+ }
1648
+ def width:double (EVGDisplayList.barWidth(state (el.scrollbarWidth == "thin")))
1649
+ def margin:double 3.0
1650
+ def trackX:double ((x + w) - (width + margin))
1651
+ def trackY:double (y + margin)
1652
+ def trackH:double (h - (margin * 2.0))
1653
+ if (trackH <= (width * 2.0)) {
1654
+ return
1655
+ }
1656
+ ; The thumb's length is the container's share of its content, and
1657
+ ; never so short a finger could not find it.
1658
+ def thumbH@(mutable):double ((trackH * h) / (h + maxY))
1659
+ if (thumbH < 24.0) {
1660
+ thumbH = 24.0
1661
+ }
1662
+ if (thumbH > trackH) {
1663
+ thumbH = trackH
1664
+ }
1665
+ def travel:double (trackH - thumbH)
1666
+ def scale:double (travel / maxY)
1667
+ def thumbY:double (trackY + (scale * el.scrollTop))
1668
+ ; The container's own text colour, thinned: a bar that is the page's
1669
+ ; ink at a fraction reads as part of the page on any theme.
1670
+ def lum:int (idiv ((backdropR * 299) + ((backdropG * 587) + (backdropB * 114))) 1000)
1671
+ ; The thumb's colour: the sheet's `scrollbar-color`, else ink on the
1672
+ ; backdrop. Its alpha is the colour's own, thinned at rest and full
1673
+ ; when lit — so a sheet that asks for a translucent thumb gets one
1674
+ ; in every state.
1675
+ def base@(mutable):EVGColor (EVGColor.rgba(30 30 30 1.0))
1676
+ if (lum < 128) {
1677
+ base = (EVGColor.rgba(235 235 235 1.0))
1678
+ }
1679
+ if el.scrollbarThumb.isSet {
1680
+ base = (unwrap el.scrollbarThumb)
1681
+ }
1682
+ def cr:int (base.red())
1683
+ def cg:int (base.green())
1684
+ def cb:int (base.blue())
1685
+ def own:double (base.alpha())
1686
+ def thumbA@(mutable):double (own * 0.5)
1687
+ if (state == 1) {
1688
+ thumbA = (own * 0.72)
1689
+ }
1690
+ if (state >= 2) {
1691
+ thumbA = (own * 0.92)
1692
+ }
1693
+ ; Lit from the left: the thumb is a gradient across itself, from the
1694
+ ; colour lightened to the colour, which is what makes a flat bar
1695
+ ; read as a rounded one.
1696
+ def lit:EVGColor (base.lighten(0.38))
1697
+ ; The ink of the label is what the pill is NOT: dark on a light
1698
+ ; pill, light on a dark one.
1699
+ def pillLum:int (idiv ((cr * 299) + ((cg * 587) + (cb * 114))) 1000)
1700
+ def tr@(mutable):int 20
1701
+ def tg@(mutable):int 20
1702
+ def tb@(mutable):int 24
1703
+ if (pillLum < 140) {
1704
+ tr = 250
1705
+ tg = 250
1706
+ tb = 250
1707
+ }
1708
+ if (state > 0) {
1709
+ def track (new EVGDrawCmd())
1710
+ track.kind = 0
1711
+ track.x = trackX
1712
+ track.y = trackY
1713
+ track.w = width
1714
+ track.h = trackH
1715
+ track.radius = (width / 2.0)
1716
+ if el.scrollbarTrack.isSet {
1717
+ track.r = (el.scrollbarTrack.red())
1718
+ track.g = (el.scrollbarTrack.green())
1719
+ track.b = (el.scrollbarTrack.blue())
1720
+ track.a = (el.scrollbarTrack.alpha())
1721
+ } {
1722
+ track.r = cr
1723
+ track.g = cg
1724
+ track.b = cb
1725
+ track.a = 0.12
1726
+ }
1727
+ this.addCmd(track)
1728
+ }
1729
+ ; The clip reaches left of the track to hold the label; the thumb
1730
+ ; never leaves the track on its own, the offset is clamped.
1731
+ def cp (new EVGDrawCmd())
1732
+ cp.kind = 4
1733
+ cp.x = (trackX - 64.0)
1734
+ cp.y = trackY
1735
+ cp.w = (width + 64.0)
1736
+ cp.h = trackH
1737
+ cp.layer = ((array_length layerEls) + 1)
1738
+ push layerEls el
1739
+ push layerTop el.scrollTop
1740
+ push layerLeft el.scrollLeft
1741
+ push layerShiftX 0.0
1742
+ push layerShiftY 0.0
1743
+ push layerClip (array_length cmds)
1744
+ push layerFirst ((array_length cmds) + 1)
1745
+ push layerLast ((array_length cmds) + 2)
1746
+ push layerCullL 0.0
1747
+ push layerCullT 0.0
1748
+ push layerCullR 0.0
1749
+ push layerCullB 0.0
1750
+ push layerKind 1
1751
+ push layerScale scale
1752
+ def pct:int (to_int (((el.scrollTop * 100.0) / maxY) + 0.5))
1753
+ push layerPct pct
1754
+ push layerLabel (0 - 1)
1755
+ this.addCmd(cp)
1756
+ def thumb (new EVGDrawCmd())
1757
+ thumb.kind = 0
1758
+ thumb.x = trackX
1759
+ thumb.y = thumbY
1760
+ thumb.w = width
1761
+ thumb.h = thumbH
1762
+ thumb.radius = (width / 2.0)
1763
+ thumb.hasGrad = true
1764
+ thumb.gradDir = 1
1765
+ thumb.r = (lit.red())
1766
+ thumb.g = (lit.green())
1767
+ thumb.b = (lit.blue())
1768
+ thumb.a = thumbA
1769
+ thumb.r2 = cr
1770
+ thumb.g2 = cg
1771
+ thumb.b2 = cb
1772
+ thumb.a2 = thumbA
1773
+ this.addCmd(thumb)
1774
+ if ((state >= 1) && (el.scrollbarLabel != "none")) {
1775
+ ; Where the page is, as a number beside the thumb: a pill in the
1776
+ ; bar's ink with the text in the backdrop's, so it reads on any
1777
+ ; page. It moves with the thumb — it is in the thumb's layer —
1778
+ ; and its text is rewritten in place as the number changes.
1779
+ def pillW:double 42.0
1780
+ def pillH:double 18.0
1781
+ def pillX:double ((trackX - pillW) - 6.0)
1782
+ def pillY:double ((thumbY + (thumbH / 2.0)) - (pillH / 2.0))
1783
+ def pill (new EVGDrawCmd())
1784
+ pill.kind = 0
1785
+ pill.x = pillX
1786
+ pill.y = pillY
1787
+ pill.w = pillW
1788
+ pill.h = pillH
1789
+ pill.radius = (pillH / 2.0)
1790
+ pill.hasGrad = true
1791
+ pill.gradDir = 0
1792
+ pill.r = (lit.red())
1793
+ pill.g = (lit.green())
1794
+ pill.b = (lit.blue())
1795
+ pill.a = (own * 0.94)
1796
+ pill.r2 = cr
1797
+ pill.g2 = cg
1798
+ pill.b2 = cb
1799
+ pill.a2 = (own * 0.94)
1800
+ this.addCmd(pill)
1801
+ def label (new EVGDrawCmd())
1802
+ label.kind = 3
1803
+ label.x = (pillX + 7.0)
1804
+ label.y = (pillY + 2.0)
1805
+ label.w = (pillW - 10.0)
1806
+ label.h = (pillH - 4.0)
1807
+ label.text = (EVGDisplayList.pctLabel(pct))
1808
+ label.fontFamily = "sans-serif"
1809
+ label.fontSize = 11.0
1810
+ label.fontWeight = "600"
1811
+ label.r = tr
1812
+ label.g = tg
1813
+ label.b = tb
1814
+ label.a = 1.0
1815
+ set layerLabel ((array_length layerEls) - 1) (array_length cmds)
1816
+ this.addCmd(label)
1817
+ set layerLast ((array_length layerEls) - 1) (array_length cmds)
1818
+ }
1819
+ def pp (new EVGDrawCmd())
1820
+ pp.kind = 5
1821
+ this.addCmd(pp)
1822
+ }
1823
+
1824
+ ; The track column of a bar that is up, under a point: the layer index
1825
+ ; of its thumb, or -1. A press here jumps the page to the point.
1826
+ fn trackAt:int (px:double py:double) {
1827
+ def i:int 0
1828
+ while (i < (array_length layerEls)) {
1829
+ if ((itemAt layerKind i) == 1) {
1830
+ def cp:EVGDrawCmd (itemAt cmds (itemAt layerClip i))
1831
+ def t:EVGDrawCmd (itemAt cmds (itemAt layerFirst i))
1832
+ if ((px >= (t.x - 4.0)) && (px <= ((t.x + t.w) + 3.0))) {
1833
+ if ((py >= cp.y) && (py <= (cp.y + cp.h))) {
1834
+ return i
1835
+ }
1836
+ }
1837
+ }
1838
+ i = (i + 1)
1839
+ }
1840
+ return (0 - 1)
1841
+ }
1842
+
1843
+ ; The offset that puts a thumb's middle at `py`.
1844
+ fn trackScrollFor:double (i:int py:double) {
1845
+ def cp:EVGDrawCmd (itemAt cmds (itemAt layerClip i))
1846
+ def t:EVGDrawCmd (itemAt cmds (itemAt layerFirst i))
1847
+ def scale:double (itemAt layerScale i)
1848
+ if (scale <= 0.0) {
1849
+ return 0.0
1850
+ }
1851
+ return (((py - cp.y) - (t.h / 2.0)) / scale)
1852
+ }
1853
+
1854
+ ; The scroll container whose right edge the point is near, as the index
1855
+ ; of its CONTENT layer, or -1.
1856
+ fn nearBarAt:int (px:double py:double) {
1857
+ def i:int 0
1858
+ while (i < (array_length layerEls)) {
1859
+ if ((itemAt layerKind i) == 0) {
1860
+ def cp:EVGDrawCmd (itemAt cmds (itemAt layerClip i))
1861
+ if ((px >= ((cp.x + cp.w) - (EVGDisplayList.barNearPx()))) && (px <= (cp.x + cp.w))) {
1862
+ if ((py >= cp.y) && (py <= (cp.y + cp.h))) {
1863
+ return i
1864
+ }
1865
+ }
1866
+ }
1867
+ i = (i + 1)
1868
+ }
1869
+ return (0 - 1)
1870
+ }
1871
+
1872
+ ; The thumb under a point, as a layer index, or -1. A finger's width of
1873
+ ; slack to the left of a thumb at rest, which is four pixels wide.
1874
+ fn thumbAt:int (px:double py:double) {
1875
+ def i:int 0
1876
+ while (i < (array_length layerEls)) {
1877
+ if ((itemAt layerKind i) == 1) {
1878
+ def t:EVGDrawCmd (itemAt cmds (itemAt layerFirst i))
1879
+ if ((px >= (t.x - 8.0)) && (px <= ((t.x + t.w) + 3.0))) {
1880
+ if ((py >= t.y) && (py <= (t.y + t.h))) {
1881
+ return i
1882
+ }
1883
+ }
1884
+ }
1885
+ i = (i + 1)
1886
+ }
1887
+ return (0 - 1)
1888
+ }
1889
+
1890
+ fn layerElement:EVGElement (i:int) {
1891
+ return (itemAt layerEls i)
1892
+ }
1893
+
1894
+ fn thumbScale:double (i:int) {
1895
+ return (itemAt layerScale i)
1896
+ }
1897
+
1898
+ ; The thumbs, as JSON, for a check to look at.
1899
+ fn thumbsJson:string () {
1900
+ def out@(mutable):string "["
1901
+ def n@(mutable):int 0
1902
+ def i:int 0
1903
+ while (i < (array_length layerEls)) {
1904
+ if ((itemAt layerKind i) == 1) {
1905
+ def t:EVGDrawCmd (itemAt cmds (itemAt layerFirst i))
1906
+ if (n > 0) {
1907
+ out = (out + ",")
1908
+ }
1909
+ out = (out + "{\"x\":" + (EVGDisplayList.num(t.x)) + ",\"y\":" + (EVGDisplayList.num(t.y)))
1910
+ out = (out + ",\"w\":" + (EVGDisplayList.num(t.w)) + ",\"h\":" + (EVGDisplayList.num(t.h)) + ",\"a\":" + (EVGDisplayList.num(t.a)))
1911
+ out = (out + ",\"c\":[" + (to_string t.r2) + "," + (to_string t.g2) + "," + (to_string t.b2) + "]}")
1912
+ n = (n + 1)
1913
+ }
1914
+ i = (i + 1)
1915
+ }
1916
+ return (out + "]")
1917
+ }
1918
+
1919
+ fn layerCount:int () {
1920
+ return (array_length layerEls)
1921
+ }
1922
+
1923
+ ; The layer this container's clip opens, or -1.
1924
+ fn layerFor:int (el:EVGElement) {
1925
+ def i:int 0
1926
+ while (i < (array_length layerEls)) {
1927
+ if ((itemAt layerEls i) == el) {
1928
+ return i
1929
+ }
1930
+ i = (i + 1)
1931
+ }
1932
+ return (0 - 1)
1933
+ }
1934
+
1935
+ ; Move every command in a layer's range — the content, the clips inside
1936
+ ; it, the paths — by (dx, dy). The layer's own clip is not in the range:
1937
+ ; the container stays where it is, its content moves.
1938
+ fn shiftRange:void (first:int last:int dx:double dy:double) {
1939
+ def i:int first
1940
+ while (i < last) {
1941
+ def c:EVGDrawCmd (itemAt cmds i)
1942
+ c.x = (c.x + dx)
1943
+ c.y = (c.y + dy)
1944
+ if c.hasRotOrigin {
1945
+ c.rotOriginX = (c.rotOriginX + dx)
1946
+ c.rotOriginY = (c.rotOriginY + dy)
1947
+ }
1948
+ def n:int (array_length c.pts)
1949
+ if (n > 0) {
1950
+ def p:int 0
1951
+ while (p < n) {
1952
+ set c.pts p ((itemAt c.pts p) + dx)
1953
+ set c.pts (p + 1) ((itemAt c.pts (p + 1)) + dy)
1954
+ p = (p + 2)
1955
+ }
1956
+ }
1957
+ i = (i + 1)
1958
+ }
1959
+ }
1960
+
1961
+ ; Bring the layers up to their containers' current offsets. True when
1962
+ ; every layer still covers what its container shows — the list is good
1963
+ ; for this frame, moved — and false when one has scrolled past what was
1964
+ ; built for it, which is the caller's cue to build again.
1965
+ fn refreshLayers:boolean () {
1966
+ def i:int 0
1967
+ while (i < (array_length layerEls)) {
1968
+ def el:EVGElement (itemAt layerEls i)
1969
+ if ((itemAt layerKind i) == 1) {
1970
+ def ty:double ((el.scrollTop - (itemAt layerTop i)) * (itemAt layerScale i))
1971
+ def dsy:double (ty - (itemAt layerShiftY i))
1972
+ if (dsy != 0.0) {
1973
+ this.shiftRange((itemAt layerFirst i) (itemAt layerLast i) 0.0 dsy)
1974
+ set layerShiftY i ty
1975
+ }
1976
+ if ((itemAt layerLabel i) >= 0) {
1977
+ def maxY:double (el.maxScrollTop())
1978
+ if (maxY > 0.0) {
1979
+ def pct:int (to_int (((el.scrollTop * 100.0) / maxY) + 0.5))
1980
+ if (pct != (itemAt layerPct i)) {
1981
+ set layerPct i pct
1982
+ def label:EVGDrawCmd (itemAt cmds (itemAt layerLabel i))
1983
+ label.text = (EVGDisplayList.pctLabel(pct))
1984
+ frameSeq = (frameSeq + 1)
1985
+ }
1986
+ }
1987
+ }
1988
+ i = (i + 1)
1989
+ continue
1990
+ }
1991
+ def dy:double ((itemAt layerTop i) - el.scrollTop)
1992
+ def dx:double ((itemAt layerLeft i) - el.scrollLeft)
1993
+ ; The container's clip as it stands — an outer layer may have
1994
+ ; moved it, and moved this layer's built range with it — has to
1995
+ ; sit inside the built range once that range is moved by the
1996
+ ; content's whole shift. Outside it, a row the viewport shows was
1997
+ ; never built.
1998
+ def cp:EVGDrawCmd (itemAt cmds (itemAt layerClip i))
1999
+ if (cp.y < ((itemAt layerCullT i) + dy)) {
2000
+ return false
2001
+ }
2002
+ if ((cp.y + cp.h) > ((itemAt layerCullB i) + dy)) {
2003
+ return false
2004
+ }
2005
+ if (cp.x < ((itemAt layerCullL i) + dx)) {
2006
+ return false
2007
+ }
2008
+ if ((cp.x + cp.w) > ((itemAt layerCullR i) + dx)) {
2009
+ return false
2010
+ }
2011
+ def sx:double (dx - (itemAt layerShiftX i))
2012
+ def sy:double (dy - (itemAt layerShiftY i))
2013
+ if ((sx != 0.0) || (sy != 0.0)) {
2014
+ this.shiftRange((itemAt layerFirst i) (itemAt layerLast i) sx sy)
2015
+ set layerShiftX i dx
2016
+ set layerShiftY i dy
2017
+ ; The layers inside this one moved with its content, and so
2018
+ ; did the ranges they were built for.
2019
+ def j:int (i + 1)
2020
+ while (j < (array_length layerEls)) {
2021
+ if ((itemAt layerClip j) > (itemAt layerClip i)) {
2022
+ if ((itemAt layerClip j) < (itemAt layerLast i)) {
2023
+ set layerCullL j ((itemAt layerCullL j) + sx)
2024
+ set layerCullR j ((itemAt layerCullR j) + sx)
2025
+ set layerCullT j ((itemAt layerCullT j) + sy)
2026
+ set layerCullB j ((itemAt layerCullB j) + sy)
2027
+ }
2028
+ }
2029
+ j = (j + 1)
2030
+ }
2031
+ }
2032
+ i = (i + 1)
2033
+ }
2034
+ ; The layers are where their containers are; the ring belongs to none
2035
+ ; of them and is put back against its own box.
2036
+ this.refreshRing()
2037
+ return true
2038
+ }
2039
+ def overscanBefore:double (0.0 - 1.0)
2040
+ def overscanAfter:double (0.0 - 1.0)
2041
+ ; The clip in force, and whether there is one. Not a stack: the rectangle
2042
+ ; is intersected on the way down and restored on the way back up, which is
2043
+ ; the stack, held in the recursion.
2044
+ ; How much bigger or smaller than its layout box this subtree will be
2045
+ ; drawn, from the transforms above it. One on a page that does not zoom.
2046
+ def drawScale:double 1.0
2047
+ def cullOn:boolean false
2048
+ def cullLeft:double 0.0
2049
+ def cullTop:double 0.0
2050
+ def cullRight:double 0.0
2051
+ def cullBottom:double 0.0
2052
+
2053
+ fn setCulling:void (on:boolean) {
2054
+ culling = on
2055
+ }
2056
+
2057
+ ; How far past the clip to draw anyway, in pixels, above and below. A
2058
+ ; negative value means "one clip height", which is the default. Give the
2059
+ ; direction of travel more room than the direction behind it.
2060
+ fn setOverscan:void (before:double after:double) {
2061
+ overscanBefore = before
2062
+ overscanAfter = after
2063
+ }
2064
+
2065
+ ; Is this subtree worth walking into?
2066
+ fn cullable:boolean (el:EVGElement) {
2067
+ if (cullOn == false) {
2068
+ return false
2069
+ }
2070
+ if el.paintUnbounded {
2071
+ return false
2072
+ }
2073
+ if (el.paintBottom < cullTop) {
2074
+ return true
2075
+ }
2076
+ if (el.paintTop > cullBottom) {
2077
+ return true
2078
+ }
2079
+ if (el.paintRight < cullLeft) {
2080
+ return true
2081
+ }
2082
+ if (el.paintLeft > cullRight) {
2083
+ return true
2084
+ }
2085
+ return false
2086
+ }
2087
+
2088
+ fn build:void (root:EVGElement) {
2089
+ clear cmds
2090
+ clear deferredOverlays
2091
+ clear layerEls
2092
+ clear layerTop
2093
+ clear layerLeft
2094
+ clear layerShiftX
2095
+ clear layerShiftY
2096
+ clear layerCullL
2097
+ clear layerCullT
2098
+ clear layerCullR
2099
+ clear layerCullB
2100
+ clear layerClip
2101
+ clear layerFirst
2102
+ clear layerLast
2103
+ clear layerStack
2104
+ clear layerKind
2105
+ clear layerScale
2106
+ clear layerPct
2107
+ clear layerLabel
2108
+ fragHits = 0
2109
+ fragMisses = 0
2110
+ cullOn = false
2111
+ if hasRegion {
2112
+ ; The walk starts inside the region rather than outside every
2113
+ ; clip. A clip met on the way down still narrows it, which is
2114
+ ; what `walk` already does with a nested one.
2115
+ cullOn = true
2116
+ cullLeft = regionLeft
2117
+ cullTop = regionTop
2118
+ cullRight = regionRight
2119
+ cullBottom = regionBottom
2120
+ }
2121
+ drawScale = 1.0
2122
+ effectKind = ""
2123
+ def noX:[double]
2124
+ def noY:[double]
2125
+ def noT:[double]
2126
+ effectXs = noX
2127
+ effectYs = noY
2128
+ effectAges = noT
2129
+ clear fxIds
2130
+ clear fxKinds
2131
+ clear fxTriggers
2132
+ clear fxX
2133
+ clear fxY
2134
+ clear fxW
2135
+ clear fxH
2136
+ clear fxRadius
2137
+ clear fxParamStart
2138
+ clear fxParamCount
2139
+ clear fxParamNames
2140
+ clear fxParamValues
2141
+ this.readEffect(root)
2142
+ this.collectEffects(root)
2143
+ this.walk(root)
2144
+ def i:int 0
2145
+ while (i < (array_length deferredOverlays)) {
2146
+ ; Outside every clip: a surface is placed against the page, not
2147
+ ; against whatever it was written inside.
2148
+ cullOn = false
2149
+ this.walk((itemAt deferredOverlays i))
2150
+ i = (i + 1)
2151
+ }
2152
+ cullOn = false
2153
+ ringCmd = (0 - 1)
2154
+ def noRing@(optional):EVGElement
2155
+ ringEl = noRing
2156
+ this.paintFocusRing(root)
2157
+ }
2158
+
2159
+ ; Apply `opacity` to everything this element and its subtree just emitted.
2160
+ ;
2161
+ ; The attribute has been in SPEC.md and on EVGElement all along, and
2162
+ ; rangerflow's fade highlighter and book's slabs already set it — but
2163
+ ; nothing ever read it, so every one of them painted at full strength. It
2164
+ ; belongs here rather than in each painter: the display list is numbers, so
2165
+ ; scaling the alpha once means WebGL, SDL, SoftCanvas, PDF and HTML all get
2166
+ ; it without knowing the attribute exists.
2167
+ ;
2168
+ ; This scales each command's alpha rather than compositing the subtree
2169
+ ; offscreen the way CSS `opacity` does. The difference shows only where two
2170
+ ; faded children OVERLAP — there they blend with each other instead of as
2171
+ ; one group. A display list cannot express the group form without a layer,
2172
+ ; and this is the approximation every immediate-mode renderer makes.
2173
+ fn fadeFrom:void (start:int factor:double) {
2174
+ if (factor >= 1.0) {
2175
+ return
2176
+ }
2177
+ def i:int start
2178
+ while (i < (array_length cmds)) {
2179
+ def c:EVGDrawCmd (itemAt cmds i)
2180
+ c.a = (c.a * factor)
2181
+ i = (i + 1)
2182
+ }
2183
+ }
2184
+
2185
+ ; A `transform` applied to everything this element and its children drew.
2186
+ ;
2187
+ ; The same shape as `fadeFrom` and for the same reason: a transform is a
2188
+ ; property of a SUBTREE, and the display list has already flattened the
2189
+ ; subtree by the time it is known what was in it. Recording where the
2190
+ ; element's output began and rewriting that range afterwards is how both are
2191
+ ; expressed without a transform stack in every backend. Nested transforms
2192
+ ; compose because an outer element's range covers the inner one's output and
2193
+ ; is rewritten after it.
2194
+ ;
2195
+ ; It is a similarity — rotation, uniform scale, translation — so each
2196
+ ; command survives as a command rather than needing a matrix in the wire
2197
+ ; format. Two different treatments, for two different kinds of command:
2198
+ ;
2199
+ ; a box (rect, border, image, text) keeps its axis-aligned rectangle,
2200
+ ; moved and resized, and carries the ANGLE with the origin it turns
2201
+ ; about. The painter does the turning.
2202
+ ;
2203
+ ; a path is turned in its GEOMETRY, here, because its shape does not live
2204
+ ; in `x/y/w/h` and no backend's quad rotation would reach it. That is the
2205
+ ; same choice `PathBuilder.rotateAbout` already makes, and it means SVG,
2206
+ ; PDF and raster all draw a turned icon without knowing what a transform
2207
+ ; is.
2208
+ fn transformFrom:void (start:int el:EVGElement) {
2209
+ def deg:double el.rotate
2210
+ def sc:double el.scale
2211
+ def tx:double el.translateX
2212
+ def ty:double el.translateY
2213
+ def flips:boolean el.flipY
2214
+ def turns:boolean (deg != 0.0)
2215
+ def scales:boolean (((fabs (sc - 1.0)) > 0.000001) || flips)
2216
+ def shifts:boolean ((tx != 0.0) || (ty != 0.0))
2217
+ if (((turns == false) && (scales == false)) && (shifts == false)) {
2218
+ return
2219
+ }
2220
+ ; The y axis carries the reflection. A mirrored layer is a similarity
2221
+ ; still — a turn, a factor and a flip — and read as a turn alone it
2222
+ ; comes out backwards: a mirrored arrow on a board pointed the other
2223
+ ; way, its head where its tail belonged.
2224
+ def scy:double sc
2225
+ if flips {
2226
+ scy = (0.0 - sc)
2227
+ }
2228
+ ; What it turns about. `transform-origin` decides, and its default is
2229
+ ; 50% 50% — the element's own centre. Measured against a browser rather
2230
+ ; than assumed: a 100x40 box given a quarter turn about the default
2231
+ ; comes back with its centre exactly where it started.
2232
+ ;
2233
+ ; Resolved HERE and not earlier, because a percentage needs the laid-out
2234
+ ; box and this is the first place that exists.
2235
+ def ox:double (el.calculatedX + (EVGElement.resolveOrigin(el.transformOriginX el.calculatedWidth)))
2236
+ def oy:double (el.calculatedY + (EVGElement.resolveOrigin(el.transformOriginY el.calculatedHeight)))
2237
+ def rad:double ((deg * 3.14159265358979) / 180.0)
2238
+ def cs:double (cos rad)
2239
+ def sn:double (sin rad)
2240
+ def i:int start
2241
+ def n:int (array_length cmds)
2242
+ while (i < n) {
2243
+ def c:EVGDrawCmd (itemAt cmds i)
2244
+ if ((array_length c.pts) > 0) {
2245
+ this.mapPoints(c ox oy sc scy cs sn tx ty)
2246
+ } {
2247
+ if scales {
2248
+ c.x = (ox + ((c.x - ox) * sc))
2249
+ c.y = (oy + ((c.y - oy) * scy))
2250
+ c.w = (c.w * sc)
2251
+ c.h = (c.h * scy)
2252
+ c.radius = ((fabs c.radius) * (fabs sc))
2253
+ c.thickness = ((fabs c.thickness) * (fabs sc))
2254
+ ; Type scales with the box. A button that grows on hover
2255
+ ; and keeps its old type size looks like a rendering fault.
2256
+ c.fontSize = ((fabs c.fontSize) * (fabs sc))
2257
+ ; A box has no negative side. The flip put the top edge
2258
+ ; below the bottom one, so the rectangle is turned the
2259
+ ; right way up and its corners follow: what was rounded
2260
+ ; at the top is rounded at the bottom now. An image is
2261
+ ; sampled the other way instead of moved.
2262
+ if (c.h < 0.0) {
2263
+ c.y = (c.y + c.h)
2264
+ c.h = (0.0 - c.h)
2265
+ if c.perCorner {
2266
+ def swTL:double c.radius
2267
+ c.radius = c.radiusBL
2268
+ c.radiusBL = swTL
2269
+ def swTR:double c.radiusTR
2270
+ c.radiusTR = c.radiusBR
2271
+ c.radiusBR = swTR
2272
+ }
2273
+ c.flipV = (c.flipV == false)
2274
+ }
2275
+ if (c.w < 0.0) {
2276
+ c.x = (c.x + c.w)
2277
+ c.w = (0.0 - c.w)
2278
+ if c.perCorner {
2279
+ def swTL2:double c.radius
2280
+ c.radius = c.radiusTR
2281
+ c.radiusTR = swTL2
2282
+ def swBL2:double c.radiusBL
2283
+ c.radiusBL = c.radiusBR
2284
+ c.radiusBR = swBL2
2285
+ }
2286
+ c.flipH = (c.flipH == false)
2287
+ }
2288
+ }
2289
+ if turns {
2290
+ ; A command already carrying its own rotation — a sideways
2291
+ ; axis label inside a turned panel — adds the two angles and
2292
+ ; has its pivot carried round by the outer one. Its own
2293
+ ; centre stops being the pivot, which is right: it is being
2294
+ ; carried, not spun in place.
2295
+ c.rotate = (c.rotate + deg)
2296
+ if (c.hasRotOrigin == false) {
2297
+ c.rotOriginX = ox
2298
+ c.rotOriginY = oy
2299
+ c.hasRotOrigin = true
2300
+ } {
2301
+ def rx:double (c.rotOriginX - ox)
2302
+ def ry:double (c.rotOriginY - oy)
2303
+ c.rotOriginX = (ox + ((rx * cs) - (ry * sn)))
2304
+ c.rotOriginY = (oy + ((rx * sn) + (ry * cs)))
2305
+ }
2306
+ }
2307
+ if shifts {
2308
+ c.x = (c.x + tx)
2309
+ c.y = (c.y + ty)
2310
+ if c.hasRotOrigin {
2311
+ c.rotOriginX = (c.rotOriginX + tx)
2312
+ c.rotOriginY = (c.rotOriginY + ty)
2313
+ }
2314
+ }
2315
+ }
2316
+ i = (i + 1)
2317
+ }
2318
+ }
2319
+
2320
+ ; The whole similarity applied to a path's points. Rebuilt rather than
2321
+ ; written in place: the array has no element assignment, and a fresh list
2322
+ ; pushed back is clearer than an index dance anyway.
2323
+ fn mapPoints:void (c:EVGDrawCmd ox:double oy:double sc:double scy:double cs:double sn:double tx:double ty:double) {
2324
+ def moved:[double]
2325
+ def i:int 0
2326
+ def n:int (array_length c.pts)
2327
+ while ((i + 1) < n) {
2328
+ def px:double ((itemAt c.pts i) - ox)
2329
+ def py:double ((itemAt c.pts (i + 1)) - oy)
2330
+ def sx:double (px * sc)
2331
+ def sy:double (py * scy)
2332
+ push moved (((ox + ((sx * cs) - (sy * sn))) + tx))
2333
+ push moved (((oy + ((sx * sn) + (sy * cs))) + ty))
2334
+ i = (i + 2)
2335
+ }
2336
+ clear c.pts
2337
+ def j:int 0
2338
+ while (j < (array_length moved)) {
2339
+ push c.pts (itemAt moved j)
2340
+ j = (j + 1)
2341
+ }
2342
+ }
2343
+
2344
+ ; --- the background gradient -------------------------------------------
2345
+ ;
2346
+ ; TWO SHAPES AND NO MORE: down the box and across it. That is what `gd`
2347
+ ; can say, and what every backend reading this list implements — the GL
2348
+ ; shader mixes along one axis of the quad and the software canvas does the
2349
+ ; same. So a linear gradient is snapped to the nearer axis and its stops
2350
+ ; are swapped when it points the other way, which is exact for the four
2351
+ ; right angles and an approximation for everything between them.
2352
+ ;
2353
+ ; A RADIAL gradient is not a direction at all, so it is not turned into
2354
+ ; one: the box keeps whatever flat colour it had, and a viewer sees a
2355
+ ; missing effect rather than a wrong one.
2356
+ ;
2357
+ ; Two stops, because the command carries two. A gradient authored with
2358
+ ; more keeps its first and its last, and the ones between are dropped —
2359
+ ; recorded here rather than hidden, since the alternative is a list format
2360
+ ; change every backend would have to follow.
2361
+ sfn normAngle:double (a:double) {
2362
+ def v:double a
2363
+ while (v < 0.0) {
2364
+ v = (v + 360.0)
2365
+ }
2366
+ while (v >= 360.0) {
2367
+ v = (v - 360.0)
2368
+ }
2369
+ return v
2370
+ }
2371
+
2372
+ ; The element's drop shadow onto the command that carries its box.
2373
+ ; `hasShadow` and slots 31-35 have been on the command and in the binary
2374
+ ; all along and NOTHING EVER WROTE ONE FROM AN ELEMENT — the same gap a
2375
+ ; gradient had. A board of FigJam stickies is 154 shadows, and every one
2376
+ ; of them was a note sitting flat on the page.
2377
+ ;
2378
+ ; A shadow with no colour is not a shadow. An offset of zero with a
2379
+ ; radius is, though — that is the soft halo under a sticky — so the test
2380
+ ; is the colour and the radius, never the offset.
2381
+ sfn applyShadow:void (c:EVGDrawCmd el:EVGElement) {
2382
+ def col:EVGColor el.shadowColor
2383
+ if (col.isSet == false) {
2384
+ return
2385
+ }
2386
+ if ((col.alpha()) <= 0.0) {
2387
+ return
2388
+ }
2389
+ def blur:double 0.0
2390
+ if el.shadowRadius.isSet {
2391
+ blur = el.shadowRadius.pixels
2392
+ }
2393
+ def dx:double 0.0
2394
+ if el.shadowOffsetX.isSet {
2395
+ dx = el.shadowOffsetX.pixels
2396
+ }
2397
+ def dy:double 0.0
2398
+ if el.shadowOffsetY.isSet {
2399
+ dy = el.shadowOffsetY.pixels
2400
+ }
2401
+ if (blur < 0.0) {
2402
+ blur = 0.0
2403
+ }
2404
+ ; Nothing offset and nothing to soften draws exactly under the box it
2405
+ ; would be hidden by, so it is not written at all.
2406
+ if (((blur == 0.0) && (dx == 0.0)) && (dy == 0.0)) {
2407
+ return
2408
+ }
2409
+ c.hasShadow = true
2410
+ c.shadowX = dx
2411
+ c.shadowY = dy
2412
+ c.shadowBlur = blur
2413
+ c.shadowR = (col.red())
2414
+ c.shadowG = (col.green())
2415
+ c.shadowB = (col.blue())
2416
+ c.shadowA = (col.alpha())
2417
+ }
2418
+
2419
+ ; The direction code as an angle — in TWO conventions, because the
2420
+ ; painters do not share one and pretending otherwise rotates a gradient by
2421
+ ; ninety degrees in one target and not the other.
2422
+ ;
2423
+ ; This list stores a direction as 0 or 1 because that is all a GPU backend
2424
+ ; needs. `applyGradient` above is what reduces a CSS angle to that code, so
2425
+ ; the inverses belong beside it rather than in each painter, where the two
2426
+ ; would be free to disagree.
2427
+ ;
2428
+ ; `dir` 1 is the horizontal axis, 0 the vertical one, and the stops are
2429
+ ; already in the order the axis is drawn in.
2430
+
2431
+ ; Screen space, y downwards, 0 degrees to the right: what the rasteriser's
2432
+ ; `renderLinearGradient` takes, since it builds its direction vector
2433
+ ; straight from (cos, sin).
2434
+ sfn rasterAngleOfDir:double (dir:int) {
2435
+ if (dir == 1) {
2436
+ return 0.0
2437
+ }
2438
+ return 90.0
2439
+ }
2440
+
2441
+ ; CSS, where an angle is the direction the gradient TRAVELS and 0 is to the
2442
+ ; top: 90 runs left to right, 180 runs top to bottom. The PDF renderer
2443
+ ; parses `linear-gradient(…deg…)` and reduces it the same way this file
2444
+ ; does, so it wants the angle in the units it was written in.
2445
+ sfn cssAngleOfDir:double (dir:int) {
2446
+ if (dir == 1) {
2447
+ return 90.0
2448
+ }
2449
+ return 180.0
2450
+ }
2451
+
2452
+ sfn hasLinearGradient:boolean (el:EVGElement) {
2453
+ ; The RGU1 pair — `gradient-from` / `gradient-to` / `gradient-dir` — is
2454
+ ; already this list's own vocabulary: a direction and two colours, with
2455
+ ; nothing to interpret.
2456
+ if el.gradientSet {
2457
+ return true
2458
+ }
2459
+ def g:EVGGradient el.gradient
2460
+ if (g.isSet == false) {
2461
+ return false
2462
+ }
2463
+ if (g.isLinear == false) {
2464
+ return false
2465
+ }
2466
+ return ((g.getStopCount()) > 1)
2467
+ }
2468
+
2469
+ sfn applyGradient:void (c:EVGDrawCmd el:EVGElement) {
2470
+ if el.gradientSet {
2471
+ def a0:EVGColor el.gradientFrom
2472
+ def b0:EVGColor el.gradientTo
2473
+ c.hasGrad = true
2474
+ c.gradDir = el.gradientDir
2475
+ c.r = (a0.red())
2476
+ c.g = (a0.green())
2477
+ c.b = (a0.blue())
2478
+ c.a = (a0.alpha())
2479
+ c.r2 = (b0.red())
2480
+ c.g2 = (b0.green())
2481
+ c.b2 = (b0.blue())
2482
+ c.a2 = (b0.alpha())
2483
+ return
2484
+ }
2485
+ def g:EVGGradient el.gradient
2486
+ def ang:double (EVGDisplayList.normAngle(g.angle))
2487
+ ; CSS reads the angle as the direction the gradient TRAVELS, with 0 to
2488
+ ; the top and 90 to the right; the list reads a start corner. So the
2489
+ ; two directions that run up or left are the same axis with the stops
2490
+ ; the other way round.
2491
+ def dir:int 0
2492
+ def swap:boolean false
2493
+ if ((ang >= 45.0) && (ang < 135.0)) {
2494
+ dir = 1
2495
+ }
2496
+ if ((ang >= 225.0) && (ang < 315.0)) {
2497
+ dir = 1
2498
+ swap = true
2499
+ }
2500
+ if ((ang < 45.0) || (ang >= 315.0)) {
2501
+ swap = true
2502
+ }
2503
+ def from:EVGColor (g.getStartColor())
2504
+ def to:EVGColor (g.getEndColor())
2505
+ if swap {
2506
+ def tmp:EVGColor from
2507
+ from = to
2508
+ to = tmp
2509
+ }
2510
+ c.hasGrad = true
2511
+ c.gradDir = dir
2512
+ c.r = (from.red())
2513
+ c.g = (from.green())
2514
+ c.b = (from.blue())
2515
+ c.a = (from.alpha())
2516
+ c.r2 = (to.red())
2517
+ c.g2 = (to.green())
2518
+ c.b2 = (to.blue())
2519
+ c.a2 = (to.alpha())
2520
+ }
2521
+
2522
+ fn walk:void (el:EVGElement) {
2523
+ ; `display: none` paints nothing, and nothing under it.
2524
+ if (el.isHidden()) {
2525
+ return
2526
+ }
2527
+ ; Nor does a subtree that cannot reach the clip it is inside.
2528
+ if (this.cullable(el)) {
2529
+ return
2530
+ }
2531
+ ; A subtree still owed a scroll is paid it before it is drawn — see
2532
+ ; `EVGElement.shiftDx`. Its own box, which the test above read, was
2533
+ ; already where it should be.
2534
+ el.settleShift()
2535
+ ; Where this element's own output begins, so `opacity` can scale
2536
+ ; exactly what it and its children add — nested fades then multiply as
2537
+ ; the recursion unwinds. A transform uses the same range.
2538
+ def emitStart:int (array_length cmds)
2539
+ ; The element every command appended below belongs to. Saved and put
2540
+ ; back rather than cleared, because `walkOpaque` recurses through here
2541
+ ; and a child must not leave its own slot behind for the commands its
2542
+ ; parent emits afterwards.
2543
+ ; A kept subtree, from what it emitted last time when that holds.
2544
+ def keeping:boolean ((fragments && el.keepLayout) && (attribute == false))
2545
+ if keeping {
2546
+ if (this.emitFragment(el)) {
2547
+ fragHits = (fragHits + 1)
2548
+ return
2549
+ }
2550
+ fragMisses = (fragMisses + 1)
2551
+ }
2552
+ def prevNode:int curNode
2553
+ if attribute {
2554
+ curNode = el.inspectSlot
2555
+ }
2556
+ def hadCull:boolean cullOn
2557
+ def hadCullL:double cullLeft
2558
+ def hadCullT:double cullTop
2559
+ def hadCullR:double cullRight
2560
+ def hadCullB:double cullBottom
2561
+ if keeping {
2562
+ cullOn = false
2563
+ }
2564
+ def hadScale:double drawScale
2565
+ def hadTurned:int turnedDepth
2566
+ if (el.hasTransform()) {
2567
+ def sc:double el.scale
2568
+ if (sc < 0.0) {
2569
+ sc = (0.0 - sc)
2570
+ }
2571
+ if (sc > 0.0) {
2572
+ drawScale = (drawScale * sc)
2573
+ }
2574
+ if cullOn {
2575
+ this.cullThroughTransform(el)
2576
+ }
2577
+ ; A clip inside a turn is a rectangle in a space the painter
2578
+ ; cannot scissor to — see `clipsContent` below. Counted here,
2579
+ ; where the turn is known, and counted for this element as well
2580
+ ; as its children: its own clip is turned by it too.
2581
+ if (el.rotate != 0.0) {
2582
+ turnedDepth = (turnedDepth + 1)
2583
+ }
2584
+ }
2585
+ this.walkOpaque(el)
2586
+ turnedDepth = hadTurned
2587
+ drawScale = hadScale
2588
+ cullOn = hadCull
2589
+ cullLeft = hadCullL
2590
+ cullTop = hadCullT
2591
+ cullRight = hadCullR
2592
+ cullBottom = hadCullB
2593
+ curNode = prevNode
2594
+ this.fadeFrom(emitStart el.opacity)
2595
+ this.transformFrom(emitStart el)
2596
+ if keeping {
2597
+ this.recordFragment(el emitStart)
2598
+ }
2599
+ }
2600
+
2601
+ ; The clip in force, carried into the space this element's children are
2602
+ ; laid out in. A transform moves the pixels after the boxes are placed,
2603
+ ; so a clip outside it is a rectangle in a different space from the
2604
+ ; boxes below it — and testing one against the other is what made a
2605
+ ; panned canvas draw nothing: a Figma board lays its frames out at
2606
+ ; x = -13264 and a `translate` puts them on the screen, so every frame
2607
+ ; measured as ten thousand pixels off and was skipped whole.
2608
+ ;
2609
+ ; `paintUnbounded` already spares the element that carries the
2610
+ ; transform. Its children are not spared by it — they carry no
2611
+ ; transform of their own — which is why the rectangle has to travel
2612
+ ; rather than the flag. A rotation, which no rectangle here can follow,
2613
+ ; turns culling off for the subtree instead of guessing at one.
2614
+ fn cullThroughTransform:void (el:EVGElement) {
2615
+ if (el.rotate != 0.0) {
2616
+ cullOn = false
2617
+ return
2618
+ }
2619
+ def sc:double el.scale
2620
+ if ((fabs sc) < 0.000001) {
2621
+ cullOn = false
2622
+ return
2623
+ }
2624
+ def ox:double (el.calculatedX + (EVGElement.resolveOrigin(el.transformOriginX el.calculatedWidth)))
2625
+ def oy:double (el.calculatedY + (EVGElement.resolveOrigin(el.transformOriginY el.calculatedHeight)))
2626
+ def l:double (ox + (((cullLeft - el.translateX) - ox) / sc))
2627
+ def r:double (ox + (((cullRight - el.translateX) - ox) / sc))
2628
+ def t:double (oy + (((cullTop - el.translateY) - oy) / sc))
2629
+ def b:double (oy + (((cullBottom - el.translateY) - oy) / sc))
2630
+ if (l > r) {
2631
+ def sw:double l
2632
+ l = r
2633
+ r = sw
2634
+ }
2635
+ if (t > b) {
2636
+ def sw2:double t
2637
+ t = b
2638
+ b = sw2
2639
+ }
2640
+ cullLeft = l
2641
+ cullTop = t
2642
+ cullRight = r
2643
+ cullBottom = b
2644
+ }
2645
+
2646
+ fn walkOpaque:void (el:EVGElement) {
2647
+ def x:double el.calculatedX
2648
+ def y:double el.calculatedY
2649
+ def w:double el.calculatedWidth
2650
+ def h:double el.calculatedHeight
2651
+
2652
+ def radius:double el.box.borderRadiusPx
2653
+ ; THE FOUR CORNERS, and the scale-down that makes them legal.
2654
+ ;
2655
+ ; CSS: if two radii on one side add up to more than that side, EVERY
2656
+ ; radius on the box is multiplied by the same factor — the smallest
2657
+ ; ratio over the four sides. Per-corner clamping is the classic wrong
2658
+ ; answer and it comes out lopsided. Measured: 80px corners on a 100x40
2659
+ ; box are drawn at 40, a factor of 0.5 applied to all four, and 60/20
2660
+ ; on a 50x200 box become 37.5 and 12.5.
2661
+ ;
2662
+ ; It is done here and not in the box because it needs the DRAWN width
2663
+ ; and height, which nothing knows until the layout has finished.
2664
+ ; A PERCENTAGE IS AGAINST THIS BOX, not its parent. The box resolves
2665
+ ; units when only the parent's dimensions are known, so it had nothing
2666
+ ; else to use — `border-radius: 50%` on a 200x100 box inside a
2667
+ ; 800x600 page came out 300, which the shader then clamped to 50 and
2668
+ ; the PDF and raster backends did not. Re-resolved here against `w`,
2669
+ ; where the box's own width is finally a number. Circular corners, so
2670
+ ; one base: the browser would make an ellipse of it, and that is
2671
+ ; recorded as a divergence rather than approximated.
2672
+ def bx:EVGBox el.box
2673
+ def uTL:EVGUnit bx.borderRadiusTL
2674
+ def uTR:EVGUnit bx.borderRadiusTR
2675
+ def uBR:EVGUnit bx.borderRadiusBR
2676
+ def uBL:EVGUnit bx.borderRadiusBL
2677
+ def uAll:EVGUnit bx.borderRadius
2678
+ def rTL:double (EVGDisplayList.radiusPx((uTL.isSet && (uTL.isPercent())) uTL.value bx.borderRadiusTLPx w))
2679
+ def rTR:double (EVGDisplayList.radiusPx((uTR.isSet && (uTR.isPercent())) uTR.value bx.borderRadiusTRPx w))
2680
+ def rBR:double (EVGDisplayList.radiusPx((uBR.isSet && (uBR.isPercent())) uBR.value bx.borderRadiusBRPx w))
2681
+ def rBL:double (EVGDisplayList.radiusPx((uBL.isSet && (uBL.isPercent())) uBL.value bx.borderRadiusBLPx w))
2682
+ if ((bx.hasPerCornerRadius()) == false) {
2683
+ def uni:double (EVGDisplayList.radiusPx((uAll.isSet && (uAll.isPercent())) uAll.value bx.borderRadiusPx w))
2684
+ rTL = uni
2685
+ rTR = uni
2686
+ rBR = uni
2687
+ rBL = uni
2688
+ }
2689
+ def f:double 1.0
2690
+ f = (EVGDisplayList.sideFactor(f w (rTL + rTR)))
2691
+ f = (EVGDisplayList.sideFactor(f h (rTR + rBR)))
2692
+ f = (EVGDisplayList.sideFactor(f w (rBL + rBR)))
2693
+ f = (EVGDisplayList.sideFactor(f h (rTL + rBL)))
2694
+ if (f < 1.0) {
2695
+ rTL = (rTL * f)
2696
+ rTR = (rTR * f)
2697
+ rBR = (rBR * f)
2698
+ rBL = (rBL * f)
2699
+ }
2700
+ ; And the single number every other backend reads is the SCALED one.
2701
+ ; It used to be the authored value: an 80-pixel radius on a 100-pixel
2702
+ ; box went out as 80, and only the WebGL shader's own clamp made it
2703
+ ; look right — PDF and the rasteriser drew what they were told.
2704
+ radius = rTL
2705
+ def perCorner:boolean (el.box.hasPerCornerRadius())
2706
+
2707
+ ; Background
2708
+ ;
2709
+ ; `painted` guards the case below it: an element carrying only a
2710
+ ; `backdrop-filter` and no background of its own still has to emit a
2711
+ ; rectangle, because the filter is the thing being drawn. CSS applies
2712
+ ; `backdrop-filter` whether or not there is a background — a scrim that
2713
+ ; softens the page and tints it not at all is an ordinary thing to want
2714
+ ; — and gating the emit on a colour dropped that case entirely.
2715
+ def painted:boolean false
2716
+ def bgSet:boolean false
2717
+ def bg:EVGColor (new EVGColor())
2718
+ if el.backgroundColor {
2719
+ def b0:EVGColor (unwrap el.backgroundColor)
2720
+ if b0.isSet {
2721
+ bg = b0
2722
+ bgSet = true
2723
+ }
2724
+ }
2725
+ ; A gradient is a background too, and until now it was the one kind
2726
+ ; that never arrived: `hasGrad` has been on the command, in the JSON
2727
+ ; and in the GL shader all along, and nothing wrote one from an
2728
+ ; ELEMENT. So `background: linear-gradient(...)` with no
2729
+ ; `background-color` beside it emitted no rectangle at all, and with
2730
+ ; one it was painted flat.
2731
+ def gradOk:boolean (EVGDisplayList.hasLinearGradient(el))
2732
+ ; A PATH IS NOT ITS BOX. Its paint belongs on the path command, where
2733
+ ; `walkPath` puts it, and a background rectangle beside it draws the
2734
+ ; box the shape is not: a shaded pill came out sitting on a shaded
2735
+ ; rectangle, grey leaking past its own rounded corner. The same is
2736
+ ; true of a flat colour and always was — a path element carries it as
2737
+ ; `fill` and never as `background-color`, so nothing noticed until a
2738
+ ; gradient could be on both.
2739
+ def paintsItsBox:boolean ((el.drawsPath()) == false)
2740
+ if ((bgSet || gradOk) && paintsItsBox) {
2741
+ def c (new EVGDrawCmd())
2742
+ c.kind = 0
2743
+ c.x = x
2744
+ c.y = y
2745
+ c.w = w
2746
+ c.h = h
2747
+ c.radius = radius
2748
+ c.perCorner = perCorner
2749
+ if perCorner {
2750
+ c.radius = rTL
2751
+ c.radiusTR = rTR
2752
+ c.radiusBR = rBR
2753
+ c.radiusBL = rBL
2754
+ }
2755
+ if bgSet {
2756
+ c.r = (bg.red())
2757
+ c.g = (bg.green())
2758
+ c.b = (bg.blue())
2759
+ c.a = (bg.alpha())
2760
+ }
2761
+ ; The gradient outranks the flat colour, the way `background-image`
2762
+ ; is painted over `background-color` in a browser.
2763
+ if gradOk {
2764
+ EVGDisplayList.applyGradient(c el)
2765
+ }
2766
+ c.backdropBlur = el.backdropBlur
2767
+ ; An ELEMENT-SCOPED EFFECT rides the element's own background,
2768
+ ; which is where it belongs in paint order: the element's picture
2769
+ ; first, then the effect, then its children on top.
2770
+ if ((strlen el.surfaceEffect) > 0) {
2771
+ c.effectId = el.effectRuntimeId
2772
+ }
2773
+ EVGDisplayList.applyShadow(c el)
2774
+ this.addCmd(c)
2775
+ painted = true
2776
+ }
2777
+ if (painted == false) {
2778
+ ; A SHADOW UNDER SOMETHING WITH NO BACKGROUND. A FigJam sticky is
2779
+ ; a container that paints nothing itself — its body arrives as a
2780
+ ; child path — and the shadow is on the container. Gated on the
2781
+ ; background, the shadow of every sticky on a board was dropped.
2782
+ ;
2783
+ ; The rectangle is the element's own box, fully transparent, and
2784
+ ; carries only the shadow: the same shape the content fills, and
2785
+ ; a backend that cannot draw shadows draws nothing, which is
2786
+ ; what it did before this existed.
2787
+ def probe (new EVGDrawCmd())
2788
+ EVGDisplayList.applyShadow(probe el)
2789
+ if ((probe.hasShadow && (el.backdropBlur <= 0.0)) && paintsItsBox) {
2790
+ def cs (new EVGDrawCmd())
2791
+ cs.kind = 0
2792
+ cs.x = x
2793
+ cs.y = y
2794
+ cs.w = w
2795
+ cs.h = h
2796
+ cs.radius = radius
2797
+ cs.perCorner = perCorner
2798
+ if perCorner {
2799
+ cs.radius = rTL
2800
+ cs.radiusTR = rTR
2801
+ cs.radiusBR = rBR
2802
+ cs.radiusBL = rBL
2803
+ }
2804
+ cs.a = 0.0
2805
+ EVGDisplayList.applyShadow(cs el)
2806
+ this.addCmd(cs)
2807
+ }
2808
+ if (el.backdropBlur > 0.0) {
2809
+ def cb (new EVGDrawCmd())
2810
+ cb.kind = 0
2811
+ cb.x = x
2812
+ cb.y = y
2813
+ cb.w = w
2814
+ cb.h = h
2815
+ cb.radius = radius
2816
+ cb.perCorner = perCorner
2817
+ if perCorner {
2818
+ cb.radius = rTL
2819
+ cb.radiusTR = rTR
2820
+ cb.radiusBR = rBR
2821
+ cb.radiusBL = rBL
2822
+ }
2823
+ ; Fully transparent: the rectangle exists to carry the filter,
2824
+ ; and a backend that cannot filter draws nothing, which is
2825
+ ; exactly right.
2826
+ cb.a = 0.0
2827
+ cb.backdropBlur = el.backdropBlur
2828
+ EVGDisplayList.applyShadow(cb el)
2829
+ this.addCmd(cb)
2830
+ }
2831
+ ; AN EFFECT ON A BOX THAT PAINTS NOTHING, which is the ordinary
2832
+ ; case: a starfield is declared on an empty div and the stars are
2833
+ ; the background. Same transparent carrier the filter above uses,
2834
+ ; and a backend that has never heard of effects draws nothing,
2835
+ ; which is exactly right.
2836
+ if ((strlen el.surfaceEffect) > 0) {
2837
+ def cf (new EVGDrawCmd())
2838
+ cf.kind = 0
2839
+ cf.x = x
2840
+ cf.y = y
2841
+ cf.w = w
2842
+ cf.h = h
2843
+ cf.radius = radius
2844
+ cf.perCorner = perCorner
2845
+ if perCorner {
2846
+ cf.radius = rTL
2847
+ cf.radiusTR = rTR
2848
+ cf.radiusBR = rBR
2849
+ cf.radiusBL = rBL
2850
+ }
2851
+ cf.a = 0.0
2852
+ cf.effectId = el.effectRuntimeId
2853
+ this.addCmd(cf)
2854
+ }
2855
+ }
2856
+
2857
+ ; Border. Read through the element's own accessor rather than a field
2858
+ ; picked by hand: a CSS-declared border lands on `box`, an inline
2859
+ ; camelCase one on the element, and this walk used to read one of each
2860
+ ; — a width from the box and a colour from the element — so a
2861
+ ; stylesheet border was silently dropped from every GPU backend.
2862
+ def bw:double (el.effectiveBorderWidthPx())
2863
+ if (bw > 0.0) {
2864
+ def bc:EVGColor (el.effectiveBorderColor())
2865
+ def c2 (new EVGDrawCmd())
2866
+ c2.kind = 1
2867
+ c2.x = x
2868
+ c2.y = y
2869
+ c2.w = w
2870
+ c2.h = h
2871
+ c2.radius = radius
2872
+ c2.perCorner = perCorner
2873
+ if perCorner {
2874
+ c2.radius = rTL
2875
+ c2.radiusTR = rTR
2876
+ c2.radiusBR = rBR
2877
+ c2.radiusBL = rBL
2878
+ }
2879
+ c2.thickness = bw
2880
+ c2.r = (bc.red())
2881
+ c2.g = (bc.green())
2882
+ c2.b = (bc.blue())
2883
+ c2.a = (bc.alpha())
2884
+ this.addCmd(c2)
2885
+ }
2886
+
2887
+
2888
+ ; Vector paths. Flattened here, in page coordinates, because this is
2889
+ ; where the drawn size is known — a GPU backend gets rings of points
2890
+ ; and needs no path parser and no viewBox arithmetic of its own.
2891
+ if (el.drawsPath()) {
2892
+ this.walkPath(el x y w h)
2893
+ }
2894
+ ; Image
2895
+ if ((strlen el.src) > 0) {
2896
+ def c3 (new EVGDrawCmd())
2897
+ c3.kind = 2
2898
+ c3.x = x
2899
+ c3.y = y
2900
+ c3.w = w
2901
+ c3.h = h
2902
+ c3.radius = radius
2903
+ c3.perCorner = perCorner
2904
+ if perCorner {
2905
+ c3.radius = rTL
2906
+ c3.radiusTR = rTR
2907
+ c3.radiusBR = rBR
2908
+ c3.radiusBL = rBL
2909
+ }
2910
+ c3.src = el.src
2911
+ if el.imageViewBoxSet {
2912
+ c3.hasCrop = true
2913
+ c3.cropX = el.imageViewBoxX
2914
+ c3.cropY = el.imageViewBoxY
2915
+ c3.cropW = el.imageViewBoxW
2916
+ c3.cropH = el.imageViewBoxH
2917
+ }
2918
+ this.addCmd(c3)
2919
+ }
2920
+
2921
+ ; Text. One command per element; a backend that shapes lines itself can
2922
+ ; re-break, and one that does not can ask EVGTextEngine for the same
2923
+ ; lines layout used.
2924
+ if ((strlen el.textContent) > 0) {
2925
+ def face:string (el.effectiveFontFamily())
2926
+ def fs:double el.inheritedFontSize
2927
+ if el.fontSize.isSet {
2928
+ fs = el.fontSize.pixels
2929
+ }
2930
+ if (fs <= 0.0) {
2931
+ fs = 14.0
2932
+ }
2933
+ ; The same rule the layout reserved the space by, so the line the
2934
+ ; painter draws lands where the box was sized for it — and it is
2935
+ ; the same CALL, not the same formula written twice.
2936
+ ; `EVGElement.lineBoxFor` is where `normal`, a number and a length
2937
+ ; are told apart; `normal` is the FACE's line box and not 1.2, and
2938
+ ; a length is not a multiplier.
2939
+ def lineBox:double (el.lineBoxFor(fs (textEngine.lineHeightFor(face fs))))
2940
+ if (lineBox <= 0.0) {
2941
+ lineBox = (fs * 1.2)
2942
+ }
2943
+ def avail:double (el.box.getInnerWidth(w))
2944
+ ; The same width the layout counted the lines at — see
2945
+ ; `EVGElement.wrapWidth`, which is zero for a `white-space` that
2946
+ ; does not wrap. `avail` itself is still the box, because that is
2947
+ ; what `text-align` centres inside.
2948
+ ; The line objects, not just their strings: a paragraph gap is
2949
+ ; a property of WHICH line this is, and the strings cannot say.
2950
+ def broken:[EVGTextLine] (textEngine.breakLinesSpaced(el.textContent face fs (el.wrapWidth(avail)) el.letterSpacing))
2951
+ def lines:[string]
2952
+ def bi:int 0
2953
+ while (bi < (array_length broken)) {
2954
+ def bl:EVGTextLine (itemAt broken bi)
2955
+ push lines bl.text
2956
+ bi = (bi + 1)
2957
+ }
2958
+ def tr:int 0
2959
+ def tg:int 0
2960
+ def tb:int 0
2961
+ def ta:double 1.0
2962
+ if el.color {
2963
+ def tc:EVGColor (unwrap el.color)
2964
+ tr = (tc.red())
2965
+ tg = (tc.green())
2966
+ tb = (tc.blue())
2967
+ ta = (tc.alpha())
2968
+ }
2969
+ def li:int 0
2970
+ ; What the paragraph gaps above this line add to its place.
2971
+ def paraShift:double 0.0
2972
+ while (li < (array_length lines)) {
2973
+ def lnObj:EVGTextLine (itemAt broken li)
2974
+ if lnObj.startsParagraph {
2975
+ paraShift = (paraShift + el.paragraphSpacing)
2976
+ }
2977
+ def c4 (new EVGDrawCmd())
2978
+ c4.kind = 3
2979
+ ; `text-align`, which until now was parsed by EVGElement and
2980
+ ; read by absolutely nothing — a documented attribute that did
2981
+ ; not exist, which costs an afternoon every time someone
2982
+ ; believes it.
2983
+ ;
2984
+ ; The offset is measured, not guessed: the painter draws a run
2985
+ ; at c.x and sizes the quad to the ink, so centring means
2986
+ ; asking the same engine that broke the line how wide the line
2987
+ ; came out and moving the start by the slack.
2988
+ def indent:double 0.0
2989
+ if ((el.textAlign == "center") || (el.textAlign == "right")) {
2990
+ def m:EVGTextMetrics (textEngine.measureRunSpaced((itemAt lines li) face fs el.letterSpacing))
2991
+ def slack:double (avail - m.width)
2992
+ if (slack > 0.0) {
2993
+ if (el.textAlign == "center") {
2994
+ indent = (slack / 2.0)
2995
+ } {
2996
+ indent = slack
2997
+ }
2998
+ }
2999
+ }
3000
+ ; THE BORDER, as well as the padding. `avail` above is
3001
+ ; `getInnerWidth`, which takes both off — and the layout's own
3002
+ ; `calculatedBaseline` is measured from the border edge and
3003
+ ; adds both. This did not, so a text element with a border
3004
+ ; drew its text one border-width high and one left: over its
3005
+ ; own top border, and measured against a width that assumed it
3006
+ ; started inside it. EVG disagreeing with EVG, which is the
3007
+ ; kind of divergence that survives longest because both halves
3008
+ ; look right on their own.
3009
+ def bw:double el.box.borderWidthPx
3010
+ c4.x = (((x + bw) + el.box.paddingLeftPx) + indent)
3011
+ c4.y = (((((y + bw) + el.box.paddingTopPx) + el.textShiftY) + ((to_double li) * lineBox)) + paraShift)
3012
+ c4.w = avail
3013
+ c4.h = lineBox
3014
+ c4.text = (itemAt lines li)
3015
+ c4.fontFamily = face
3016
+ c4.fontSize = fs
3017
+ ; The element's weight, which `addText` has always carried
3018
+ ; and this walk never did: a `font-weight: 700` span was
3019
+ ; laid out bold and painted regular by every GPU backend.
3020
+ if ((strlen el.fontWeight) > 0) {
3021
+ if (el.fontWeight != "normal") {
3022
+ c4.fontWeight = el.fontWeight
3023
+ }
3024
+ }
3025
+ c4.letterSpacing = el.letterSpacing
3026
+ c4.r = tr
3027
+ c4.g = tg
3028
+ c4.b = tb
3029
+ c4.a = ta
3030
+ this.addCmd(c4)
3031
+ li = (li + 1)
3032
+ }
3033
+ }
3034
+
3035
+ ; Children, clipped when the element says so. EVERY overflow value but
3036
+ ; `visible` clips — this used to name `hidden` alone, which meant a
3037
+ ; scroll container drew its content straight over whatever was beside
3038
+ ; it, and the one thing a scrolling box has to do is stay inside itself.
3039
+ ; The backdrop under this element's children, and under its own
3040
+ ; scrollbar: its background when it paints one, else what was there.
3041
+ def hadBdR:int backdropR
3042
+ def hadBdG:int backdropG
3043
+ def hadBdB:int backdropB
3044
+ def ownBg:EVGColor el.backgroundColor
3045
+ if ownBg.isSet {
3046
+ if ((ownBg.alpha()) > 0.5) {
3047
+ backdropR = (ownBg.red())
3048
+ backdropG = (ownBg.green())
3049
+ backdropB = (ownBg.blue())
3050
+ }
3051
+ }
3052
+ def clips:boolean (el.clipsContent())
3053
+ ; NOT INSIDE A TURN. A clip crosses the list as x/y/w/h and every
3054
+ ; backend makes it a scissor, which is axis-aligned; the rotation
3055
+ ; that moved the pixels cannot move the rectangle with them. The
3056
+ ; two then disagree, and a turned frame clipped away the very
3057
+ ; thing it contains — a quarter-turned arrow on a FigJam board drew
3058
+ ; nothing at all, its content sent a hundred pixels above a scissor
3059
+ ; that stayed where the layout put it.
3060
+ ;
3061
+ ; So the clip is dropped rather than applied wrongly, which is the
3062
+ ; same choice `cullThroughTransform` already makes for a rotation:
3063
+ ; content that overflows a turned frame is drawn instead of being
3064
+ ; cut in the wrong place. Anything else needs a stencil, and that is
3065
+ ; a list format every backend would have to follow.
3066
+ if (turnedDepth > 0) {
3067
+ clips = false
3068
+ }
3069
+ ; A clip that can scroll is a layer — see `refreshLayers`.
3070
+ def scrolls:boolean false
3071
+ if ((clips && culling) && ((w > 0.0) && (h > 0.0))) {
3072
+ if (((el.maxScrollTop()) > 0.0) || ((el.maxScrollLeft()) > 0.0)) {
3073
+ scrolls = true
3074
+ }
3075
+ }
3076
+ if clips {
3077
+ def cp (new EVGDrawCmd())
3078
+ cp.kind = 4
3079
+ cp.x = x
3080
+ cp.y = y
3081
+ cp.w = w
3082
+ cp.h = h
3083
+ if scrolls {
3084
+ cp.layer = ((array_length layerEls) + 1)
3085
+ push layerStack (array_length layerEls)
3086
+ push layerEls el
3087
+ push layerTop el.scrollTop
3088
+ push layerLeft el.scrollLeft
3089
+ push layerShiftX 0.0
3090
+ push layerShiftY 0.0
3091
+ push layerClip (array_length cmds)
3092
+ push layerFirst ((array_length cmds) + 1)
3093
+ push layerLast ((array_length cmds) + 1)
3094
+ push layerCullL 0.0
3095
+ push layerCullT 0.0
3096
+ push layerCullR 0.0
3097
+ push layerCullB 0.0
3098
+ push layerKind 0
3099
+ push layerScale 1.0
3100
+ push layerPct 0
3101
+ push layerLabel (0 - 1)
3102
+ }
3103
+ this.addCmd(cp)
3104
+ }
3105
+ ; The clip narrows what is worth walking, and it is restored on the
3106
+ ; way out — the recursion is the stack. See the note on culling above.
3107
+ def hadCull:boolean cullOn
3108
+ def hadL:double cullLeft
3109
+ def hadT:double cullTop
3110
+ def hadR:double cullRight
3111
+ def hadB:double cullBottom
3112
+ if (clips && culling) {
3113
+ def before:double overscanBefore
3114
+ def after:double overscanAfter
3115
+ if (before < 0.0) { before = h }
3116
+ if (after < 0.0) { after = h }
3117
+ def l:double (x - w)
3118
+ def t:double (y - before)
3119
+ def r:double ((x + w) + w)
3120
+ def b:double ((y + h) + after)
3121
+ if cullOn {
3122
+ ; Nested clips intersect: the inner one can only narrow.
3123
+ if (l < cullLeft) { l = cullLeft }
3124
+ if (t < cullTop) { t = cullTop }
3125
+ if (r > cullRight) { r = cullRight }
3126
+ if (b > cullBottom) { b = cullBottom }
3127
+ }
3128
+ cullOn = true
3129
+ cullLeft = l
3130
+ cullTop = t
3131
+ cullRight = r
3132
+ cullBottom = b
3133
+ if scrolls {
3134
+ def li:int (itemAt layerStack ((array_length layerStack) - 1))
3135
+ set layerCullL li l
3136
+ set layerCullT li t
3137
+ set layerCullR li r
3138
+ set layerCullB li b
3139
+ }
3140
+ }
3141
+ def i:int 0
3142
+ while (i < (el.getChildCount())) {
3143
+ def kid:EVGElement (el.getChild(i))
3144
+ ; A surface is not drawn here. `build` comes back to it once the
3145
+ ; whole normal tree is down — see the note there.
3146
+ if (kid.isSurface()) {
3147
+ push deferredOverlays kid
3148
+ } {
3149
+ this.walk(kid)
3150
+ }
3151
+ i = (i + 1)
3152
+ }
3153
+ ; …and after them, whatever a host built for this element — see
3154
+ ; `paintAt`. Inside the clip and inside the scroll layer's range,
3155
+ ; because that is where the element itself is.
3156
+ if ((array_length paintIds) > 0) {
3157
+ if ((strlen el.id) > 0) {
3158
+ this.emitPainted(el.id)
3159
+ }
3160
+ }
3161
+ cullOn = hadCull
3162
+ cullLeft = hadL
3163
+ cullTop = hadT
3164
+ cullRight = hadR
3165
+ cullBottom = hadB
3166
+ if clips {
3167
+ if scrolls {
3168
+ def done:int (itemAt layerStack ((array_length layerStack) - 1))
3169
+ set layerLast done (array_length cmds)
3170
+ removeLast layerStack
3171
+ }
3172
+ def pp (new EVGDrawCmd())
3173
+ pp.kind = 5
3174
+ this.addCmd(pp)
3175
+ if (scrolls && scrollbars) {
3176
+ this.addScrollbar(el x y w h)
3177
+ }
3178
+ }
3179
+ backdropR = hadBdR
3180
+ backdropG = hadBdG
3181
+ backdropB = hadBdB
3182
+ }
3183
+
3184
+ ; Serialize to JSON — the wire format between the engine and a backend.
3185
+ ;
3186
+ ; One flat array of commands. A backend needs no Ranger runtime to consume
3187
+ ; it, which is what lets the WebGL renderer be plain JavaScript while the
3188
+ ; SDL2 one is the same list decoded in C++.
3189
+ ; How many ints one command occupies. Fixed, so a reader indexes rather
3190
+ ; than walks: command `i` starts at `i * stride()`. Read it through this
3191
+ ; function and never inline the number — it has changed once already, when
3192
+ ; a rotation needed an origin to turn about.
3193
+ ;
3194
+ ; That instruction was there and a reader across the bridge had inlined it
3195
+ ; anyway. The record went 24 -> 26, `pptx-host.mjs` went on multiplying by
3196
+ ; 24, and from the second command on every field was read two ints early —
3197
+ ; colours as coordinates, coordinates as flags — until a ring count taken
3198
+ ; out of somebody's colour reached `new Array(n)`. So a comment is not the
3199
+ ; mechanism: `sceneStride()` on the decoding side now DERIVES this by
3200
+ ; dividing the buffer by the command count, which is exact because the
3201
+ ; buffer is allocated as count * stride here, and it is checked against the
3202
+ ; JSON path for every fixture by `pptx/web/host/scene-binary-check.mjs`.
3203
+ ; Growing this number is safe; a reader that wants the new fields opts in.
3204
+ sfn capCode:int (name:string) {
3205
+ if (name == "round") {
3206
+ return 1
3207
+ }
3208
+ if (name == "square") {
3209
+ return 2
3210
+ }
3211
+ return 0
3212
+ }
3213
+
3214
+ sfn joinCode:int (name:string) {
3215
+ if (name == "round") {
3216
+ return 1
3217
+ }
3218
+ if (name == "bevel") {
3219
+ return 2
3220
+ }
3221
+ return 0
3222
+ }
3223
+
3224
+ sfn stride:int () {
3225
+ return 45
3226
+ }
3227
+
3228
+ ; A value in hundredths, which is the precision `num` writes and therefore
3229
+ ; the precision the JSON bridge has always carried.
3230
+ sfn fixed:int (v:double) {
3231
+ if (v < 0.0) {
3232
+ return (0 - (to_int (((0.0 - v) * 100.0) + 0.5)))
3233
+ }
3234
+ return (to_int ((v * 100.0) + 0.5))
3235
+ }
3236
+
3237
+ ; Three channels in one int, because a colour is three bytes and an
3238
+ ; Int32Array holds four.
3239
+ sfn packRgb:int (r:int g:int b:int) {
3240
+ return (((r * 65536) + (g * 256)) + b)
3241
+ }
3242
+
3243
+ ; The display list as typed arrays. See the banner on `EVGSceneBinary`.
3244
+ fn toBinary:EVGSceneBinary () {
3245
+ def out (new EVGSceneBinary())
3246
+ def n:int (array_length cmds)
3247
+ out.count = n
3248
+ out.viewX = viewX
3249
+ out.viewY = viewY
3250
+ out.viewScale = viewScale
3251
+ out.hasView = hasView
3252
+ def stride:int (EVGDisplayList.stride())
3253
+ def totalPts:int 0
3254
+ def totalEnds:int 0
3255
+ def i:int 0
3256
+ while (i < n) {
3257
+ def c:EVGDrawCmd (itemAt cmds i)
3258
+ def pc:int (array_length c.pts)
3259
+ totalPts = (totalPts + pc)
3260
+ if (pc > 0) {
3261
+ def ec:int (array_length c.ringEnds)
3262
+ if (ec == 0) {
3263
+ ; One ring covering everything, which is what a command
3264
+ ; that set `pts` and no `ringEnds` means — the same rule
3265
+ ; `toJson` applies, and for the same reason.
3266
+ totalEnds = (totalEnds + 1)
3267
+ } {
3268
+ totalEnds = (totalEnds + ec)
3269
+ }
3270
+ }
3271
+ i = (i + 1)
3272
+ }
3273
+ def recs:int_buffer (int_buffer_alloc (n * stride))
3274
+ def pbuf:int_buffer (int_buffer_alloc totalPts)
3275
+ def ebuf:int_buffer (int_buffer_alloc totalEnds)
3276
+ def pool:[string]
3277
+ def poolIndex:[string:int]
3278
+ def pAt:int 0
3279
+ def eAt:int 0
3280
+ def k:int 0
3281
+ while (k < n) {
3282
+ def c2:EVGDrawCmd (itemAt cmds k)
3283
+ def base:int (k * stride)
3284
+ int_buffer_set recs base c2.kind
3285
+ int_buffer_set recs (base + 1) (EVGDisplayList.fixed(c2.x))
3286
+ int_buffer_set recs (base + 2) (EVGDisplayList.fixed(c2.y))
3287
+ int_buffer_set recs (base + 3) (EVGDisplayList.fixed(c2.w))
3288
+ int_buffer_set recs (base + 4) (EVGDisplayList.fixed(c2.h))
3289
+ int_buffer_set recs (base + 5) (EVGDisplayList.fixed(c2.radius))
3290
+ int_buffer_set recs (base + 6) (EVGDisplayList.fixed(c2.thickness))
3291
+ int_buffer_set recs (base + 7) (EVGDisplayList.packRgb(c2.r c2.g c2.b))
3292
+ int_buffer_set recs (base + 8) (EVGDisplayList.fixed(c2.a))
3293
+ def flags:int 0
3294
+ if c2.hasGrad {
3295
+ flags = (flags + 1)
3296
+ }
3297
+ if (c2.textAlign == "italic") {
3298
+ flags = (flags + 2)
3299
+ }
3300
+ if c2.flipH {
3301
+ flags = (flags + 4)
3302
+ }
3303
+ if c2.flipV {
3304
+ flags = (flags + 8)
3305
+ }
3306
+ if c2.evenOdd {
3307
+ flags = (flags + 16)
3308
+ }
3309
+ ; Whether slots 24 and 25 mean anything. A zero origin is a legal
3310
+ ; place to turn about, so the flag cannot be inferred from the
3311
+ ; numbers being zero.
3312
+ if c2.hasRotOrigin {
3313
+ flags = (flags + 32)
3314
+ }
3315
+ ; Slots 27-29 mean something: the other three corners.
3316
+ if c2.perCorner {
3317
+ flags = (flags + 64)
3318
+ }
3319
+ ; Slots 31-35 mean something: a drop shadow.
3320
+ if c2.hasShadow {
3321
+ flags = (flags + 128)
3322
+ }
3323
+ ; Slots 41-44 mean something: the crop window into the source.
3324
+ if c2.hasCrop {
3325
+ flags = (flags + 256)
3326
+ }
3327
+ int_buffer_set recs (base + 9) flags
3328
+ int_buffer_set recs (base + 10) c2.gradDir
3329
+ int_buffer_set recs (base + 11) (EVGDisplayList.packRgb(c2.r2 c2.g2 c2.b2))
3330
+ int_buffer_set recs (base + 12) (EVGDisplayList.fixed(c2.a2))
3331
+ int_buffer_set recs (base + 13) (EVGDisplayList.fixed(c2.fontSize))
3332
+ int_buffer_set recs (base + 14) (EVGDisplayList.fixed(c2.rotate))
3333
+ int_buffer_set recs (base + 24) (EVGDisplayList.fixed(c2.rotOriginX))
3334
+ int_buffer_set recs (base + 25) (EVGDisplayList.fixed(c2.rotOriginY))
3335
+ ; No flag beside it: zero is "no filter", which is a value a blur
3336
+ ; radius cannot otherwise take. Contrast slot 24/25, where zero is
3337
+ ; a legal place to turn about and the flag is the only way to tell.
3338
+ int_buffer_set recs (base + 26) (EVGDisplayList.fixed(c2.backdropBlur))
3339
+ ; --- what a host that keeps the frame across a thread needs ------
3340
+ ;
3341
+ ; The record grew from 27 to 36 when the list started crossing to
3342
+ ; a painter on another thread (PLAN_NATIVE_HOSTS.md S1): a painter
3343
+ ; fed from this buffer alone could not draw the three corners a
3344
+ ; box rounds differently, could not tell a scroll layer's clip
3345
+ ; from any other clip, and lost every shadow. The JSON bridge
3346
+ ; carries the first two already; the shadow neither bridge did.
3347
+ ; A reader derives the width from the buffer (ISSUES #4) and
3348
+ ; reads the slots it knows, so the pptx decoder, which reads 23,
3349
+ ; is untouched by this.
3350
+ int_buffer_set recs (base + 27) (EVGDisplayList.fixed(c2.radiusTR))
3351
+ int_buffer_set recs (base + 28) (EVGDisplayList.fixed(c2.radiusBR))
3352
+ int_buffer_set recs (base + 29) (EVGDisplayList.fixed(c2.radiusBL))
3353
+ int_buffer_set recs (base + 30) c2.layer
3354
+ int_buffer_set recs (base + 31) (EVGDisplayList.fixed(c2.shadowX))
3355
+ int_buffer_set recs (base + 32) (EVGDisplayList.fixed(c2.shadowY))
3356
+ int_buffer_set recs (base + 33) (EVGDisplayList.fixed(c2.shadowBlur))
3357
+ int_buffer_set recs (base + 34) (EVGDisplayList.packRgb(c2.shadowR c2.shadowG c2.shadowB))
3358
+ int_buffer_set recs (base + 35) (EVGDisplayList.fixed(c2.shadowA))
3359
+ ; 36: `letter-spacing`. The record is read by its width and not
3360
+ ; by a version — a reader that wants 36 fields reads a 37-wide
3361
+ ; buffer and ignores this, which is how the shadow slots were
3362
+ ; added and why the pptx decoder that reads 23 is still right.
3363
+ int_buffer_set recs (base + 36) (EVGDisplayList.fixed(c2.letterSpacing))
3364
+ ; 37, 38: a stroke's cap and join, on the same terms.
3365
+ int_buffer_set recs (base + 37) c2.strokeCap
3366
+ int_buffer_set recs (base + 38) c2.strokeJoin
3367
+ ; 39, 40: the dash pattern — its text in the pool, like the
3368
+ ; font name, because the record is fixed-width and a pattern
3369
+ ; is not — and how far into it the line starts.
3370
+ def dashIdx:int (0 - 1)
3371
+ if ((strlen c2.strokeDash) > 0) {
3372
+ dashIdx = (EVGDisplayList.intern(pool poolIndex c2.strokeDash))
3373
+ }
3374
+ int_buffer_set recs (base + 39) dashIdx
3375
+ int_buffer_set recs (base + 40) (EVGDisplayList.fixed(c2.strokeDashOffset))
3376
+ ; 41-44: the crop window, in TEN-THOUSANDTHS. Every other slot is
3377
+ ; hundredths because that is what `num` writes for a pixel; this
3378
+ ; one is a fraction of a bitmap, where a hundredth is a whole
3379
+ ; percent of the picture. `frac4` writes the JSON to match.
3380
+ int_buffer_set recs (base + 41) (EVGDisplayList.fixed(c2.cropX * 100.0))
3381
+ int_buffer_set recs (base + 42) (EVGDisplayList.fixed(c2.cropY * 100.0))
3382
+ int_buffer_set recs (base + 43) (EVGDisplayList.fixed((c2.cropX + c2.cropW) * 100.0))
3383
+ int_buffer_set recs (base + 44) (EVGDisplayList.fixed((c2.cropY + c2.cropH) * 100.0))
3384
+ ; A command with no text names none of the three text strings, and
3385
+ ; `toJson` writes none of them either — so the reader must be able
3386
+ ; to tell "no text" from "empty text", which -1 says and 0 would
3387
+ ; not.
3388
+ def textIdx:int (0 - 1)
3389
+ def fontIdx:int (0 - 1)
3390
+ def weightIdx:int (0 - 1)
3391
+ if ((strlen c2.text) > 0) {
3392
+ textIdx = (EVGDisplayList.intern(pool poolIndex c2.text))
3393
+ fontIdx = (EVGDisplayList.intern(pool poolIndex c2.fontFamily))
3394
+ if ((strlen c2.fontWeight) > 0) {
3395
+ weightIdx = (EVGDisplayList.intern(pool poolIndex c2.fontWeight))
3396
+ }
3397
+ }
3398
+ int_buffer_set recs (base + 15) textIdx
3399
+ int_buffer_set recs (base + 16) fontIdx
3400
+ int_buffer_set recs (base + 17) weightIdx
3401
+ def srcIdx:int (0 - 1)
3402
+ if ((strlen c2.src) > 0) {
3403
+ srcIdx = (EVGDisplayList.intern(pool poolIndex c2.src))
3404
+ }
3405
+ int_buffer_set recs (base + 18) srcIdx
3406
+ def pc2:int (array_length c2.pts)
3407
+ int_buffer_set recs (base + 19) pAt
3408
+ int_buffer_set recs (base + 20) pc2
3409
+ def eStart:int eAt
3410
+ if (pc2 > 0) {
3411
+ def pi:int 0
3412
+ while (pi < pc2) {
3413
+ int_buffer_set pbuf (pAt + pi) (EVGDisplayList.fixed((itemAt c2.pts pi)))
3414
+ pi = (pi + 1)
3415
+ }
3416
+ pAt = (pAt + pc2)
3417
+ def ec2:int (array_length c2.ringEnds)
3418
+ if (ec2 == 0) {
3419
+ int_buffer_set ebuf eAt pc2
3420
+ eAt = (eAt + 1)
3421
+ } {
3422
+ def ei:int 0
3423
+ while (ei < ec2) {
3424
+ int_buffer_set ebuf (eAt + ei) (itemAt c2.ringEnds ei)
3425
+ ei = (ei + 1)
3426
+ }
3427
+ eAt = (eAt + ec2)
3428
+ }
3429
+ }
3430
+ int_buffer_set recs (base + 21) eStart
3431
+ int_buffer_set recs (base + 22) (eAt - eStart)
3432
+ k = (k + 1)
3433
+ }
3434
+ out.cmds = recs
3435
+ out.pts = pbuf
3436
+ out.ends = ebuf
3437
+ out.strings = pool
3438
+ return out
3439
+ }
3440
+
3441
+ ; One entry per distinct string. A slide names the same font on every run
3442
+ ; it has, so the pool is a fraction of the number of commands.
3443
+ sfn intern:int (pool:[string] index:[string:int] value:string) {
3444
+ if (has index value) {
3445
+ return (unwrap (get index value))
3446
+ }
3447
+ def at:int (array_length pool)
3448
+ push pool value
3449
+ set index value at
3450
+ return at
3451
+ }
3452
+
3453
+ ; The surface's post-process, written beside the commands and not as one of
3454
+ ; them, because it applies to everything that was drawn rather than to a
3455
+ ; rectangle in paint order. A backend that has never heard of it draws the
3456
+ ; page and ignores the field, which is the whole reason it is here and not
3457
+ ; in the command stream.
3458
+ def effectKind:string ""
3459
+ ; Every drop still in flight: x, y and how long ago, in step.
3460
+ def effectXs:[double]
3461
+ def effectYs:[double]
3462
+ def effectAges:[double]
3463
+ def effectSpeed:double 0.0
3464
+ def effectWidth:double 0.0
3465
+ def effectStrength:double 0.0
3466
+ def effectDecay:double 0.0
3467
+ def effectHighlight:double 0.0
3468
+ def effectRings:double 1.0
3469
+ def effectStagger:double 0.0
3470
+ def effectFalloff:double 0.0
3471
+ def effectShine:double 0.0
3472
+ def effectGloss:double 1.0
3473
+ def effectBump:double 0.0
3474
+ def effectLightX:double 0.0
3475
+ def effectLightY:double 0.0
3476
+ def effectLightZ:double 1.0
3477
+
3478
+ ; --- AND THE SAME THING PER ELEMENT ---------------------------------------
3479
+ ;
3480
+ ; Everything above is the ORIGINAL surface effect: one per document, over
3481
+ ; the whole page, its drops pushed in by the application. It stays exactly
3482
+ ; as it was — `landing/`, the dashboard demo and their gates all draw
3483
+ ; through it.
3484
+ ;
3485
+ ; This is the general form. Every element carrying `evg-surface-effect`
3486
+ ; becomes one INSTANCE here, with the box the layout gave it, the trigger
3487
+ ; the sheet declared and whatever `evg-fx-*` parameters were written on
3488
+ ; it. The display list does not know what any of them mean — `ripple` and
3489
+ ; `starfield` are strings here — and that is the point: a renderer plugin
3490
+ ; the engine has never heard of gets its parameters through unharmed.
3491
+ ;
3492
+ ; Parallel arrays, and the parameters in one flat pair with a start and a
3493
+ ; count per instance, because an array of objects would be an allocation
3494
+ ; per frame for a page that has two of these.
3495
+ def fxIds:[string]
3496
+ def fxKinds:[string]
3497
+ def fxTriggers:[string]
3498
+ def fxX:[double]
3499
+ def fxY:[double]
3500
+ def fxW:[double]
3501
+ def fxH:[double]
3502
+ def fxRadius:[double]
3503
+ def fxParamStart:[int]
3504
+ def fxParamCount:[int]
3505
+ def fxParamNames:[string]
3506
+ def fxParamValues:[double]
3507
+
3508
+ ; Pick the effect up off the tree. The first element that names one wins:
3509
+ ; a surface has one post-process, and two would be a question about order
3510
+ ; that nobody asked.
3511
+ fn readEffect:void (el:EVGElement) {
3512
+ if ((strlen effectKind) > 0) {
3513
+ return
3514
+ }
3515
+ if ((strlen el.surfaceEffect) > 0) {
3516
+ effectKind = el.surfaceEffect
3517
+ effectXs = el.rippleXs
3518
+ effectYs = el.rippleYs
3519
+ effectAges = el.rippleAges
3520
+ effectSpeed = el.rippleSpeed
3521
+ effectWidth = el.rippleWidth
3522
+ effectStrength = el.rippleStrength
3523
+ effectDecay = el.rippleDecay
3524
+ effectHighlight = el.rippleHighlight
3525
+ effectRings = el.rippleRings
3526
+ effectStagger = el.rippleStagger
3527
+ effectFalloff = el.rippleFalloff
3528
+ effectShine = el.rippleShine
3529
+ effectGloss = el.rippleGloss
3530
+ effectBump = el.rippleBump
3531
+ effectLightX = el.rippleLightX
3532
+ effectLightY = el.rippleLightY
3533
+ effectLightZ = el.rippleLightZ
3534
+ }
3535
+ ; Nothing below carries one, so there is nothing to find down there.
3536
+ ; `paintHasEffect` is the layout's answer for the whole subtree; an
3537
+ ; element whose own effect was set since the layout is still read
3538
+ ; above, so only the search is short-circuited, never the element.
3539
+ if ((el.paintHasEffect == false) && ((strlen el.surfaceEffect) == 0)) {
3540
+ return
3541
+ }
3542
+ def i:int 0
3543
+ while (i < (el.getChildCount())) {
3544
+ this.readEffect((el.getChild(i)))
3545
+ i = (i + 1)
3546
+ }
3547
+ }
3548
+
3549
+ ; --- THE EFFECT INSTANCES -------------------------------------------------
3550
+ ;
3551
+ ; Every element that names an effect, in tree order, with the box the
3552
+ ; layout gave it. Pruned by `paintHasEffect` exactly as `readEffect` above
3553
+ ; is: a page with no effect on it pays one comparison at the root.
3554
+ fn collectEffects:void (el:EVGElement) {
3555
+ if ((strlen el.surfaceEffect) > 0) {
3556
+ this.addEffect(el)
3557
+ }
3558
+ if ((el.paintHasEffect == false) && ((strlen el.surfaceEffect) == 0)) {
3559
+ return
3560
+ }
3561
+ def i:int 0
3562
+ while (i < (el.getChildCount())) {
3563
+ this.collectEffects((el.getChild(i)))
3564
+ i = (i + 1)
3565
+ }
3566
+ }
3567
+
3568
+ ; One parameter on the instance being built, unless it already has one
3569
+ ; under that name. The page's own `evg-fx-*` are pushed first and the
3570
+ ; kind's defaults after, so what the author wrote wins and the JSON
3571
+ ; carries each name once.
3572
+ fn pushParam:void (start:int key:string value:double) {
3573
+ def i:int start
3574
+ while (i < (array_length fxParamNames)) {
3575
+ if ((itemAt fxParamNames i) == key) {
3576
+ return
3577
+ }
3578
+ i = (i + 1)
3579
+ }
3580
+ push fxParamNames key
3581
+ push fxParamValues value
3582
+ }
3583
+
3584
+ fn addEffect:void (el:EVGElement) {
3585
+ ; ITS NAME. The element's `id` when it has one, so a host can drive
3586
+ ; "hero-sky" by the name the document uses for it. Without one it is
3587
+ ; positional and changes the moment another effect is declared above
3588
+ ; it — which is why an effect meant to be driven should carry an id,
3589
+ ; and why this does not silently pretend otherwise.
3590
+ def id:string el.id
3591
+ if ((strlen id) == 0) {
3592
+ id = ("fx" + (to_string (array_length fxIds)))
3593
+ }
3594
+ el.effectRuntimeId = id
3595
+ push fxIds id
3596
+ push fxKinds el.surfaceEffect
3597
+ push fxTriggers el.effectTrigger
3598
+ push fxX el.calculatedX
3599
+ push fxY el.calculatedY
3600
+ push fxW el.calculatedWidth
3601
+ push fxH el.calculatedHeight
3602
+ push fxRadius el.box.borderRadiusPx
3603
+ def start:int (array_length fxParamNames)
3604
+ push fxParamStart start
3605
+ def i:int 0
3606
+ while (i < (array_length el.fxNames)) {
3607
+ this.pushParam(start (itemAt el.fxNames i) (itemAt el.fxValues i))
3608
+ i = (i + 1)
3609
+ }
3610
+ ; THE RIPPLE'S OWN PROPERTIES, AS PARAMETERS. `evg-ripple-speed` was
3611
+ ; a named field back when the ripple was the only effect there was.
3612
+ ; An element-scoped ripple reads the same numbers through the generic
3613
+ ; channel, so a page that already styles a ripple keeps styling it the
3614
+ ; way it always did and the plugin sees one kind of parameter.
3615
+ if (el.surfaceEffect == "ripple") {
3616
+ this.pushParam(start "speed" el.rippleSpeed)
3617
+ this.pushParam(start "width" el.rippleWidth)
3618
+ this.pushParam(start "strength" el.rippleStrength)
3619
+ this.pushParam(start "decay" el.rippleDecay)
3620
+ this.pushParam(start "highlight" el.rippleHighlight)
3621
+ this.pushParam(start "rings" el.rippleRings)
3622
+ this.pushParam(start "stagger" el.rippleStagger)
3623
+ this.pushParam(start "falloff" el.rippleFalloff)
3624
+ this.pushParam(start "shine" el.rippleShine)
3625
+ this.pushParam(start "gloss" el.rippleGloss)
3626
+ this.pushParam(start "bump" el.rippleBump)
3627
+ this.pushParam(start "lightX" el.rippleLightX)
3628
+ this.pushParam(start "lightY" el.rippleLightY)
3629
+ this.pushParam(start "lightZ" el.rippleLightZ)
3630
+ }
3631
+ push fxParamCount ((array_length fxParamNames) - start)
3632
+ }
3633
+
3634
+ fn toJson:string () {
3635
+ def out:string "{"
3636
+ if ((strlen effectKind) > 0) {
3637
+ out = (out + "\"effect\":{\"kind\":\"" + effectKind + "\"")
3638
+ out = (out + ",\"drops\":[")
3639
+ def di:int 0
3640
+ while (di < (array_length effectAges)) {
3641
+ if (di > 0) {
3642
+ out = (out + ",")
3643
+ }
3644
+ out = (out + "[" + (EVGDisplayList.num((itemAt effectXs di))))
3645
+ out = (out + "," + (EVGDisplayList.num((itemAt effectYs di))))
3646
+ out = (out + "," + (EVGDisplayList.num((itemAt effectAges di))) + "]")
3647
+ di = (di + 1)
3648
+ }
3649
+ out = (out + "]")
3650
+ out = (out + ",\"speed\":" + (EVGDisplayList.num(effectSpeed)))
3651
+ out = (out + ",\"width\":" + (EVGDisplayList.num(effectWidth)))
3652
+ out = (out + ",\"strength\":" + (EVGDisplayList.num(effectStrength)))
3653
+ out = (out + ",\"decay\":" + (EVGDisplayList.num(effectDecay)))
3654
+ out = (out + ",\"highlight\":" + (EVGDisplayList.num(effectHighlight)))
3655
+ out = (out + ",\"rings\":" + (EVGDisplayList.num(effectRings)))
3656
+ out = (out + ",\"stagger\":" + (EVGDisplayList.num(effectStagger)))
3657
+ out = (out + ",\"falloff\":" + (EVGDisplayList.num(effectFalloff)))
3658
+ out = (out + ",\"shine\":" + (EVGDisplayList.num(effectShine)))
3659
+ out = (out + ",\"gloss\":" + (EVGDisplayList.num(effectGloss)))
3660
+ out = (out + ",\"bump\":" + (EVGDisplayList.num(effectBump)))
3661
+ out = (out + ",\"light\":[" + (EVGDisplayList.num(effectLightX)) + "," +
3662
+ (EVGDisplayList.num(effectLightY)) + "," +
3663
+ (EVGDisplayList.num(effectLightZ)) + "]")
3664
+ out = (out + "},")
3665
+ }
3666
+ ; THE ELEMENT-SCOPED EFFECTS, which is the general form of the block
3667
+ ; above: one entry per element that declared one, each with the box
3668
+ ; the layout gave it and whatever parameters were written on it. A
3669
+ ; page with none writes nothing here, so every golden file that
3670
+ ; predates this is the string it always was.
3671
+ if ((array_length fxIds) > 0) {
3672
+ out = (out + "\"effects\":[")
3673
+ def fi:int 0
3674
+ while (fi < (array_length fxIds)) {
3675
+ if (fi > 0) {
3676
+ out = (out + ",")
3677
+ }
3678
+ out = (out + "{\"id\":" + (EVGDisplayList.jsonString((itemAt fxIds fi))))
3679
+ out = (out + ",\"kind\":" + (EVGDisplayList.jsonString((itemAt fxKinds fi))))
3680
+ if ((strlen (itemAt fxTriggers fi)) > 0) {
3681
+ out = (out + ",\"on\":" + (EVGDisplayList.jsonString((itemAt fxTriggers fi))))
3682
+ }
3683
+ out = (out + ",\"box\":[" + (EVGDisplayList.num((itemAt fxX fi))))
3684
+ out = (out + "," + (EVGDisplayList.num((itemAt fxY fi))))
3685
+ out = (out + "," + (EVGDisplayList.num((itemAt fxW fi))))
3686
+ out = (out + "," + (EVGDisplayList.num((itemAt fxH fi))) + "]")
3687
+ if ((itemAt fxRadius fi) > 0.0) {
3688
+ out = (out + ",\"r\":" + (EVGDisplayList.num((itemAt fxRadius fi))))
3689
+ }
3690
+ out = (out + ",\"p\":{")
3691
+ def ps:int (itemAt fxParamStart fi)
3692
+ def pn:int (itemAt fxParamCount fi)
3693
+ def pi:int 0
3694
+ while (pi < pn) {
3695
+ if (pi > 0) {
3696
+ out = (out + ",")
3697
+ }
3698
+ out = (out + (EVGDisplayList.jsonString((itemAt fxParamNames (ps + pi)))))
3699
+ out = (out + ":" + (EVGDisplayList.num((itemAt fxParamValues (ps + pi)))))
3700
+ pi = (pi + 1)
3701
+ }
3702
+ out = (out + "}}")
3703
+ fi = (fi + 1)
3704
+ }
3705
+ out = (out + "],")
3706
+ }
3707
+ ; The camera on the envelope, not a field per command. Written only
3708
+ ; when there is one, so a list with no view is the string it always
3709
+ ; was — which is what every golden file in this repository compares.
3710
+ if hasView {
3711
+ out = (out + "\"view\":[" + (EVGDisplayList.fine(viewX)) + ",")
3712
+ out = (out + (EVGDisplayList.fine(viewY)) + "," + (EVGDisplayList.fine(viewScale)) + "],")
3713
+ }
3714
+ out = (out + "\"cmds\":[")
3715
+ def i:int 0
3716
+ while (i < (array_length cmds)) {
3717
+ def c:EVGDrawCmd (itemAt cmds i)
3718
+ if (i > 0) {
3719
+ out = out + ","
3720
+ }
3721
+ out = out + "{\"k\":" + (to_string c.kind)
3722
+ if (c.layer > 0) {
3723
+ out = out + ",\"layer\":" + (to_string c.layer)
3724
+ }
3725
+ if attribute {
3726
+ out = out + ",\"n\":" + (to_string c.node)
3727
+ }
3728
+ out = out + ",\"x\":" + (EVGDisplayList.num(c.x))
3729
+ out = out + ",\"y\":" + (EVGDisplayList.num(c.y))
3730
+ out = out + ",\"w\":" + (EVGDisplayList.num(c.w))
3731
+ out = out + ",\"h\":" + (EVGDisplayList.num(c.h))
3732
+ if c.perCorner {
3733
+ out = out + ",\"rc\":[" + (EVGDisplayList.num(c.radius)) + "," +
3734
+ (EVGDisplayList.num(c.radiusTR)) + "," +
3735
+ (EVGDisplayList.num(c.radiusBR)) + "," +
3736
+ (EVGDisplayList.num(c.radiusBL)) + "]"
3737
+ }
3738
+ if (c.radius > 0.0) {
3739
+ out = out + ",\"r\":" + (EVGDisplayList.num(c.radius))
3740
+ }
3741
+ if (c.thickness > 0.0) {
3742
+ out = out + ",\"t\":" + (EVGDisplayList.num(c.thickness))
3743
+ if (c.strokeCap != 0) {
3744
+ out = out + ",\"cap\":" + (to_string c.strokeCap)
3745
+ }
3746
+ if (c.strokeJoin != 0) {
3747
+ out = out + ",\"join\":" + (to_string c.strokeJoin)
3748
+ }
3749
+ if ((strlen c.strokeDash) > 0) {
3750
+ out = out + ",\"dash\":" + (EVGDisplayList.jsonString(c.strokeDash))
3751
+ if (c.strokeDashOffset != 0.0) {
3752
+ out = out + ",\"dashoff\":" + (EVGDisplayList.num(c.strokeDashOffset))
3753
+ }
3754
+ }
3755
+ }
3756
+ out = out + ",\"c\":[" + (to_string c.r) + "," + (to_string c.g)
3757
+ out = out + "," + (to_string c.b) + "," + (EVGDisplayList.num(c.a)) + "]"
3758
+ ; A two-stop gradient was on the command and in the software
3759
+ ; canvas but never in the JSON, so every backend on the other side
3760
+ ; of it painted the first stop flat across the box. `gd` is the
3761
+ ; direction (1 = across, otherwise down) and `c2` the far stop.
3762
+ if c.hasGrad {
3763
+ out = out + ",\"gd\":" + (to_string c.gradDir)
3764
+ out = out + ",\"c2\":[" + (to_string c.r2) + "," + (to_string c.g2)
3765
+ out = out + "," + (to_string c.b2) + "," + (EVGDisplayList.num(c.a2)) + "]"
3766
+ }
3767
+ if ((strlen c.text) > 0) {
3768
+ out = out + ",\"text\":" + (EVGDisplayList.jsonString(c.text))
3769
+ out = out + ",\"font\":" + (EVGDisplayList.jsonString(c.fontFamily))
3770
+ out = out + ",\"size\":" + (EVGDisplayList.num(c.fontSize))
3771
+ ; Weight / slant so a canvas backend paints the same face the
3772
+ ; SoftCanvas rasterizer picked.
3773
+ if (c.letterSpacing != 0.0) {
3774
+ out = out + ",\"ls\":" + (EVGDisplayList.num(c.letterSpacing))
3775
+ }
3776
+ if ((strlen c.fontWeight) > 0) {
3777
+ out = out + ",\"weight\":" + (EVGDisplayList.jsonString(c.fontWeight))
3778
+ }
3779
+ if (c.textAlign == "italic") {
3780
+ out = out + ",\"italic\":true"
3781
+ }
3782
+ }
3783
+ if ((strlen c.src) > 0) {
3784
+ out = out + ",\"src\":" + (EVGDisplayList.jsonString(c.src))
3785
+ }
3786
+ if c.flipH {
3787
+ out = out + ",\"fx\":true"
3788
+ }
3789
+ if c.flipV {
3790
+ out = out + ",\"fy\":true"
3791
+ }
3792
+ ; Only when there IS a crop, so a list built without one is the
3793
+ ; same bytes it was before this field existed.
3794
+ if c.hasCrop {
3795
+ out = out + ",\"cu\":[" + (EVGDisplayList.frac4(c.cropX)) + "," + (EVGDisplayList.frac4(c.cropY))
3796
+ out = out + "," + (EVGDisplayList.frac4(c.cropX + c.cropW)) + "," + (EVGDisplayList.frac4(c.cropY + c.cropH)) + "]"
3797
+ }
3798
+ if (c.rotate != 0.0) {
3799
+ out = out + ",\"rot\":" + (EVGDisplayList.num(c.rotate))
3800
+ if c.hasRotOrigin {
3801
+ out = out + ",\"rox\":" + (EVGDisplayList.num(c.rotOriginX))
3802
+ out = out + ",\"roy\":" + (EVGDisplayList.num(c.rotOriginY))
3803
+ }
3804
+ }
3805
+ if ((strlen c.effectId) > 0) {
3806
+ out = out + ",\"efx\":" + (EVGDisplayList.jsonString(c.effectId))
3807
+ }
3808
+ if (c.backdropBlur > 0.0) {
3809
+ out = out + ",\"bb\":" + (EVGDisplayList.num(c.backdropBlur))
3810
+ }
3811
+ ; The drop shadow, in the shape both painters read it: an offset,
3812
+ ; a blur and a colour. The binary bridge has carried this since
3813
+ ; slot 31; the JSON never did, so the two bridges disagreed about
3814
+ ; a frame and only one of them was right.
3815
+ if c.hasShadow {
3816
+ out = out + ",\"sh\":{\"x\":" + (EVGDisplayList.num(c.shadowX))
3817
+ out = out + ",\"y\":" + (EVGDisplayList.num(c.shadowY))
3818
+ out = out + ",\"blur\":" + (EVGDisplayList.num(c.shadowBlur))
3819
+ out = out + ",\"c\":[" + (to_string c.shadowR) + "," + (to_string c.shadowG)
3820
+ out = out + "," + (to_string c.shadowB) + "," + (EVGDisplayList.num(c.shadowA)) + "]}"
3821
+ }
3822
+ ; The GEOMETRY decides whether there is a path, not the ring list:
3823
+ ; a command built by hand — and there are several, in this gallery
3824
+ ; and outside it — sets `pts` and forgets `ringEnds`, and gating
3825
+ ; the points on the ring list dropped every one of those shapes on
3826
+ ; the floor. A backend saw a path command with no path and skipped
3827
+ ; it, silently, so an arrow or a bracket simply was not there. One
3828
+ ; ring covering everything is what a single-contour path means
3829
+ ; anyway, so say it here rather than expecting each caller to.
3830
+ if ((array_length c.pts) > 0) {
3831
+ out = out + ",\"pts\":["
3832
+ def pi:int 0
3833
+ while (pi < (array_length c.pts)) {
3834
+ if (pi > 0) {
3835
+ out = out + ","
3836
+ }
3837
+ out = out + (EVGDisplayList.num((itemAt c.pts pi)))
3838
+ pi = (pi + 1)
3839
+ }
3840
+ out = out + "],\"ends\":["
3841
+ if ((array_length c.ringEnds) == 0) {
3842
+ out = out + (to_string (array_length c.pts))
3843
+ } {
3844
+ def ei:int 0
3845
+ while (ei < (array_length c.ringEnds)) {
3846
+ if (ei > 0) {
3847
+ out = out + ","
3848
+ }
3849
+ out = out + (to_string (itemAt c.ringEnds ei))
3850
+ ei = (ei + 1)
3851
+ }
3852
+ }
3853
+ out = out + "]"
3854
+ if c.evenOdd {
3855
+ out = out + ",\"eo\":1"
3856
+ }
3857
+ }
3858
+ out = out + "}"
3859
+ i = (i + 1)
3860
+ }
3861
+ ; The build's number and the layers' shifts, AFTER the commands: a
3862
+ ; reader that wants only the commands takes the document from its
3863
+ ; first byte, and one did.
3864
+ out = (out + "],\"seq\":" + (to_string buildSeq) + ",\"shifts\":[")
3865
+ def si:int 0
3866
+ while (si < (array_length layerEls)) {
3867
+ if (si > 0) {
3868
+ out = (out + ",")
3869
+ }
3870
+ out = (out + "[" + (EVGDisplayList.num((itemAt layerShiftX si))) + "," + (EVGDisplayList.num((itemAt layerShiftY si))) + "]")
3871
+ si = (si + 1)
3872
+ }
3873
+ out = (out + "]}")
3874
+ return out
3875
+ }
3876
+
3877
+ ; Two decimals is plenty for a pixel and keeps the payload small.
3878
+ ; SIX decimals, for the handful of numbers where two is not enough: the
3879
+ ; camera. Two is right for a coordinate — nothing is drawn to a hundredth
3880
+ ; of a pixel and the payload halves — but a SCALE is multiplied into every
3881
+ ; coordinate in the list. A zoom of 0.4053 written as "0.41" draws the whole
3882
+ ; diagram 1.2% too large: the picture stops matching where the pointer
3883
+ ; thinks it is, and it is invisible at zoom 1, which is where every check
3884
+ ; was looking. Measured on the rangerflow board, as an 8-pixel slip at the
3885
+ ; bottom of the canvas.
3886
+ sfn fine:string (v:double) {
3887
+ def limit:double 2000000000.0
3888
+ def x:double 0.0
3889
+ if (v > 0.0) {
3890
+ x = v
3891
+ }
3892
+ if (v < 0.0) {
3893
+ x = v
3894
+ }
3895
+ if (x > limit) {
3896
+ x = limit
3897
+ }
3898
+ if (x < (0.0 - limit)) {
3899
+ x = (0.0 - limit)
3900
+ }
3901
+ def neg:boolean (x < 0.0)
3902
+ def av:double x
3903
+ if neg {
3904
+ av = (0.0 - x)
3905
+ }
3906
+ ; The whole and the fraction are scaled separately: a pan of 30,000
3907
+ ; pixels times a million overflows the 32-bit integer the divide below
3908
+ ; truncates to, and the number would come back wrapped rather than
3909
+ ; merely rounded.
3910
+ def whole:int (to_int av)
3911
+ def rest:double (av - (to_double whole))
3912
+ def frac:int (to_int ((rest * 1000000.0) + 0.5))
3913
+ if (frac >= 1000000) {
3914
+ whole = (whole + 1)
3915
+ frac = 0
3916
+ }
3917
+ def fs:string (to_string frac)
3918
+ while ((strlen fs) < 6) {
3919
+ fs = ("0" + fs)
3920
+ }
3921
+ def out:string ((to_string whole) + "." + fs)
3922
+ if neg {
3923
+ if ((whole > 0) || (frac > 0)) {
3924
+ out = ("-" + out)
3925
+ }
3926
+ }
3927
+ return out
3928
+ }
3929
+
3930
+ sfn num:string (v:double) {
3931
+ ; WHATEVER COMES IN, A JSON NUMBER GOES OUT. This is the only writer of
3932
+ ; every coordinate, size, radius and colour in the list, so a value it
3933
+ ; cannot spell does not make one command wrong — it makes the whole
3934
+ ; frame unparseable, and the page that was drawing it stops. Reported
3935
+ ; as a ripple frozen mid-flight with `SyntaxError: Expected ',' or '}'
3936
+ ; after property` beside it: the paint loop threw, and a loop that
3937
+ ; throws does not ask for another frame.
3938
+ ;
3939
+ ; Two ways in. A value that is not a number came out as "0.NaN", since
3940
+ ; the fraction was formatted from a NaN. And the integer divide below
3941
+ ; TRUNCATES TO 32 BITS, so a magnitude past 2^31 wrapped: 1e10 came out
3942
+ ; as "1410065408.858993459200", which parses and is simply wrong, and
3943
+ ; past about 1e19 the leftover went exponential — "1.0000000001e+21"
3944
+ ; as a FRACTION — which is where the JSON stops being JSON.
3945
+ ;
3946
+ ; So: NaN becomes zero, and the magnitude is pinned inside the range
3947
+ ; the arithmetic below is exact over. Ten million pixels is four
3948
+ ; thousand times the tallest page in this gallery, and it keeps the
3949
+ ; scaled value under 2^31 with a factor of two to spare, so nothing a
3950
+ ; real page draws is touched. Clamping does not make a wrong value
3951
+ ; right — the frame will look wrong where the bad number was — but a
3952
+ ; page that keeps drawing can be looked at, and one that has stopped
3953
+ ; cannot.
3954
+ def limit:double 10000000.0
3955
+ ; A value is kept only if it compares as either side of zero. Zero
3956
+ ; itself fails both and stays the zero it already is, which is why the
3957
+ ; test can be this blunt — and a NaN fails both as well, because a NaN
3958
+ ; fails EVERY comparison. That is the whole test, with no library
3959
+ ; behind it, and it catches one thing more that an equality test does
3960
+ ; not: a slot read past the end of an array, which on this target is
3961
+ ; not a number at all and is equal to itself, so `v == v` waved it
3962
+ ; through and the arithmetic below turned it back into a NaN.
3963
+ def x:double 0.0
3964
+ if (v > 0.0) {
3965
+ x = v
3966
+ }
3967
+ if (v < 0.0) {
3968
+ x = v
3969
+ }
3970
+ ; Infinities pass the comparison above and are pinned here with
3971
+ ; everything else too big to spell.
3972
+ if (x > limit) {
3973
+ x = limit
3974
+ }
3975
+ if (x < (0.0 - limit)) {
3976
+ x = (0.0 - limit)
3977
+ }
3978
+ ; Sign first, magnitude second. `to_int` FLOORS rather than truncating
3979
+ ; here, so the old two-branch rounding turned -45 into "-46.99": the
3980
+ ; scaled value floored to -4501 and then the integer divide floored
3981
+ ; again. Every negative coordinate in every scene was off by one and a
3982
+ ; bit — invisible until a rotation angle was written through it.
3983
+ def neg:boolean (x < 0.0)
3984
+ def av:double x
3985
+ if neg {
3986
+ av = (0.0 - x)
3987
+ }
3988
+ def scaled:int (to_int ((av * 100.0) + 0.5))
3989
+ def whole:int (idiv scaled 100)
3990
+ def frac:int (scaled - (whole * 100))
3991
+ def fs:string (to_string frac)
3992
+ if (frac < 10) {
3993
+ fs = "0" + fs
3994
+ }
3995
+ def out:string ((to_string whole) + "." + fs)
3996
+ if neg {
3997
+ if (scaled > 0) {
3998
+ out = ("-" + out)
3999
+ }
4000
+ }
4001
+ return out
4002
+ }
4003
+
4004
+
4005
+ ; Four decimals, for a value that lives between 0 and 1. `num` writes
4006
+ ; hundredths, which is right for a coordinate in pixels and far too
4007
+ ; coarse for a fraction of a bitmap: a crop window rounded to 0.01 moves
4008
+ ; by a full percent of the picture, sixteen pixels on a wide screenshot.
4009
+ ; The binary bridge stores the same value as ten-thousandths, so the two
4010
+ ; carry the same number to the same place.
4011
+ sfn frac4:string (v:double) {
4012
+ def x:double 0.0
4013
+ if (v > 0.0) {
4014
+ x = v
4015
+ }
4016
+ if (v < 0.0) {
4017
+ x = v
4018
+ }
4019
+ if (x > 1000.0) {
4020
+ x = 1000.0
4021
+ }
4022
+ if (x < (0.0 - 1000.0)) {
4023
+ x = (0.0 - 1000.0)
4024
+ }
4025
+ def neg:boolean (x < 0.0)
4026
+ def av:double x
4027
+ if neg {
4028
+ av = (0.0 - x)
4029
+ }
4030
+ def scaled:int (to_int ((av * 10000.0) + 0.5))
4031
+ def whole:int (idiv scaled 10000)
4032
+ def frac:int (scaled - (whole * 10000))
4033
+ def fs:string (to_string frac)
4034
+ while ((strlen fs) < 4) {
4035
+ fs = ("0" + fs)
4036
+ }
4037
+ def out:string ((to_string whole) + "." + fs)
4038
+ if neg {
4039
+ if (scaled > 0) {
4040
+ out = ("-" + out)
4041
+ }
4042
+ }
4043
+ return out
4044
+ }
4045
+
4046
+ sfn jsonString:string (v:string) {
4047
+ def out:string "\""
4048
+ def i:int 0
4049
+ while (i < (strlen v)) {
4050
+ def c:int (charAt v i)
4051
+ if (c == 34) {
4052
+ out = out + "\\\""
4053
+ } {
4054
+ if (c == 92) {
4055
+ out = out + "\\\\"
4056
+ } {
4057
+ if (c < 32) {
4058
+ out = out + " "
4059
+ } {
4060
+ out = out + (strfromcode c)
4061
+ }
4062
+ }
4063
+ }
4064
+ i = (i + 1)
4065
+ }
4066
+ return (out + "\"")
4067
+ }
4068
+
4069
+ ; A backend-shaped summary: how many quads, how many text runs, how deep
4070
+ ; the clip stack goes. Useful for sizing a vertex buffer up front.
4071
+ ; Move every command by (dx, dy). A host builds a page's list in the page's
4072
+ ; own coordinates and then places it in the window; doing that by walking
4073
+ ; the list is what lets the page renderer stay ignorant of where on the
4074
+ ; desk it landed. POP_CLIP carries no geometry, so it is left alone.
4075
+ fn offsetBy:void (dx:double dy:double) {
4076
+ def i:int 0
4077
+ while (i < (array_length cmds)) {
4078
+ def c:EVGDrawCmd (itemAt cmds i)
4079
+ if (c.kind != 5) {
4080
+ c.x = (c.x + dx)
4081
+ c.y = (c.y + dy)
4082
+ def pi:int 0
4083
+ while (pi < (array_length c.pts)) {
4084
+ def even:boolean ((pi % 2) == 0)
4085
+ if even {
4086
+ set_at c.pts pi ((itemAt c.pts pi) + dx)
4087
+ } {
4088
+ set_at c.pts pi ((itemAt c.pts pi) + dy)
4089
+ }
4090
+ pi = (pi + 1)
4091
+ }
4092
+ }
4093
+ i = (i + 1)
4094
+ }
4095
+ }
4096
+
4097
+ ; Scale every command by `s` about the origin. The other half of what a
4098
+ ; host needs to PLACE a list built in its own coordinates: a drawing made
4099
+ ; at one size and shown at another, a page laid out in one unit and drawn
4100
+ ; in a second. Everything that is a length scales — the box, the points,
4101
+ ; the stroke, the type size, a corner radius, a shadow's offset — and
4102
+ ; nothing that is not: colours, text, a dash pattern's text (which is in
4103
+ ; user units and rides on the stroke), and a clip's absence of geometry.
4104
+ ;
4105
+ ; A camera (`setView`) is the cheaper way to draw the WHOLE list at a
4106
+ ; scale; this is for one list inside another, where a camera cannot say
4107
+ ; "this part and not the rest".
4108
+ fn scaleBy:void (s:double) {
4109
+ if (s == 1.0) { return }
4110
+ def i:int 0
4111
+ while (i < (array_length cmds)) {
4112
+ def c:EVGDrawCmd (itemAt cmds i)
4113
+ if (c.kind != 5) {
4114
+ c.x = (c.x * s)
4115
+ c.y = (c.y * s)
4116
+ c.w = (c.w * s)
4117
+ c.h = (c.h * s)
4118
+ c.radius = (c.radius * s)
4119
+ c.radiusTR = (c.radiusTR * s)
4120
+ c.radiusBR = (c.radiusBR * s)
4121
+ c.radiusBL = (c.radiusBL * s)
4122
+ c.thickness = (c.thickness * s)
4123
+ c.fontSize = (c.fontSize * s)
4124
+ c.letterSpacing = (c.letterSpacing * s)
4125
+ c.maxWidth = (c.maxWidth * s)
4126
+ c.shadowX = (c.shadowX * s)
4127
+ c.shadowY = (c.shadowY * s)
4128
+ c.shadowBlur = (c.shadowBlur * s)
4129
+ c.strokeDashOffset = (c.strokeDashOffset * s)
4130
+ c.rotOriginX = (c.rotOriginX * s)
4131
+ c.rotOriginY = (c.rotOriginY * s)
4132
+ def pi:int 0
4133
+ while (pi < (array_length c.pts)) {
4134
+ set_at c.pts pi ((itemAt c.pts pi) * s)
4135
+ pi = (pi + 1)
4136
+ }
4137
+ }
4138
+ i = (i + 1)
4139
+ }
4140
+ }
4141
+
4142
+ ; Append another list's commands to this one, in order. The commands are
4143
+ ; shared rather than copied - the source list is finished with by the time
4144
+ ; a caller does this, and copying a page of text would not be free.
4145
+ fn appendFrom:void (src:EVGDisplayList) {
4146
+ def i:int 0
4147
+ while (i < (array_length src.cmds)) {
4148
+ push cmds (itemAt src.cmds i)
4149
+ i = (i + 1)
4150
+ }
4151
+ }
4152
+
4153
+ fn summary:string () {
4154
+ def rects:int 0
4155
+ def borders:int 0
4156
+ def images:int 0
4157
+ def texts:int 0
4158
+ def clips:int 0
4159
+ def i:int 0
4160
+ while (i < (array_length cmds)) {
4161
+ def c:EVGDrawCmd (itemAt cmds i)
4162
+ if (c.kind == 0) {
4163
+ rects = (rects + 1)
4164
+ }
4165
+ if (c.kind == 1) {
4166
+ borders = (borders + 1)
4167
+ }
4168
+ if (c.kind == 2) {
4169
+ images = (images + 1)
4170
+ }
4171
+ if (c.kind == 3) {
4172
+ texts = (texts + 1)
4173
+ }
4174
+ if (c.kind == 4) {
4175
+ clips = (clips + 1)
4176
+ }
4177
+ i = (i + 1)
4178
+ }
4179
+ def s:string ("rects=" + (to_string rects))
4180
+ s = s + " borders=" + (to_string borders)
4181
+ s = s + " images=" + (to_string images)
4182
+ s = s + " text=" + (to_string texts)
4183
+ s = s + " clips=" + (to_string clips)
4184
+ return s
4185
+ }
4186
+ }