ranger-compiler 3.5.0 → 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 +1160 -19
  2. package/LICENSE +3 -1
  3. package/README.md +124 -29
  4. package/dist/Lang.rgr +1188 -290
  5. package/dist/api.d.ts +2343 -879
  6. package/dist/api.js +79924 -56922
  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,998 @@
1
+ /**
2
+ * Element-scoped surface effects, in a real GL context.
3
+ *
4
+ * node lib/evg/gl/fx-check.mjs [--write-png out.png]
5
+ *
6
+ * `evg-surface-effect` used to mean one post-process over the whole page, fed
7
+ * by hand from the application's `pointerdown`. It now means an effect on an
8
+ * ELEMENT: the stylesheet says which box has it and what starts it, the
9
+ * display list carries the box the layout worked out, and a registered plugin
10
+ * turns that into pixels. This checks the half of that claim which only pixels
11
+ * can settle.
12
+ *
13
+ * Four things, and each one fails differently:
14
+ *
15
+ * IT IS INSIDE THE BOX. A starfield on a card must put stars on the card and
16
+ * nothing whatsoever outside it. The mask is in the shared `main`, not in
17
+ * the plugin, so this is really asking whether a plugin CAN leak — and the
18
+ * page around the box is left untouched at exactly its own colour, which is
19
+ * a pixel comparison and not an impression.
20
+ *
21
+ * IT IS UNDER THE CONTENT. A source effect is drawn where the element's own
22
+ * background is drawn, so anything the element contains is painted over it.
23
+ * Get the paint order wrong and a starfield is a sticker over the text — and
24
+ * it still looks like a starfield in a screenshot, which is why this is
25
+ * checked with a rectangle whose colour is known exactly.
26
+ *
27
+ * IT MOVES. The same scene at two clocks must differ. A shader whose time
28
+ * uniform never arrives draws a perfectly good still picture, and every
29
+ * other check here passes on it.
30
+ *
31
+ * A FILTER BENDS WHAT IS ALREADY THERE, inside its box and nowhere else.
32
+ * The ripple is the filter, and the case that matters is the surface OUTSIDE
33
+ * the box: it has to come back identical to the same page drawn with no
34
+ * effect at all, or "scoped to an element" is decoration on a full-screen
35
+ * pass.
36
+ *
37
+ * And two that are about not breaking: an effect nobody registered draws the
38
+ * page without it rather than nothing, and the whole-surface effect that
39
+ * predates all of this still runs.
40
+ *
41
+ * Exit code 0 when every probe holds.
42
+ */
43
+
44
+ import fs from "node:fs";
45
+ import path from "node:path";
46
+ import { fileURLToPath, pathToFileURL } from "node:url";
47
+ import { MissingDomDeps, requireHostTool, findChromium } from "../../../gallery/ui/conformance/dom-adapter.mjs";
48
+ import { createEffectDriver } from "./evg-fx.js";
49
+ import { listOf } from "./evg-list.js";
50
+ import { readPresets } from "./effect-shots.mjs";
51
+ import { createRequire } from "node:module";
52
+
53
+ const HERE = path.dirname(fileURLToPath(import.meta.url));
54
+ const W = 320;
55
+ const H = 220;
56
+ // The card, in page pixels. Everything below reads pixels relative to it.
57
+ const BOX = [60, 40, 200, 140];
58
+
59
+ const rect = (x, y, w, h, c, extra = {}) => ({ k: 0, x, y, w, h, c, ...extra });
60
+
61
+ /** The page under the card: white, so anything the effect leaks is visible. */
62
+ const PAGE_WHITE = rect(0, 0, W, H, [255, 255, 255, 1]);
63
+ /** The card itself: nearly black, because a starfield over white is a smudge. */
64
+ const SKY = [8, 10, 26, 1];
65
+
66
+ const starfield = (p, over) => ({
67
+ width: W, height: H,
68
+ cmds: [PAGE_WHITE, rect(BOX[0], BOX[1], BOX[2], BOX[3], SKY, { efx: "sky" })].concat(over || []),
69
+ effects: [{ id: "sky", kind: "starfield", on: "always", box: BOX, p: { density: 2.2, nebula: 0.7, seed: 3, ...p }, time: p && p.__t || 0 }],
70
+ });
71
+
72
+ const SCENES = {
73
+ // A sky on a card, at rest.
74
+ sky: starfield({ speed: 0 }),
75
+ // The same page with the effect taken out — the reference for "did anything
76
+ // outside the box move". Comparing two renders rather than checking the
77
+ // page against white on purpose: the painter antialiases the page rectangle
78
+ // against the canvas edge, so the outermost column is not 255 in either
79
+ // frame and never was the effect's doing.
80
+ skyPlain: { width: W, height: H, cmds: [PAGE_WHITE, rect(BOX[0], BOX[1], BOX[2], BOX[3], SKY)] },
81
+ // The same page with the effect DECLARED and switched off by the host. It
82
+ // has to come back as the page without it, pixel for pixel — an effect that
83
+ // is off is not an effect that is faint.
84
+ skyOff: (() => {
85
+ const scene = starfield({ speed: 0 });
86
+ scene.effects[0].off = true;
87
+ return scene;
88
+ })(),
89
+ // The same sky five seconds later, drifting.
90
+ skyLater: starfield({ speed: 40, __t: 5 }),
91
+ // The same sky with an opaque plate drawn AFTER the card — the element's
92
+ // content, in effect.
93
+ skyUnderPlate: starfield({ speed: 0 }, [rect(100, 70, 80, 60, [220, 30, 40, 1])]),
94
+ // An effect name nothing has registered.
95
+ unknown: {
96
+ width: W, height: H,
97
+ cmds: [PAGE_WHITE, rect(BOX[0], BOX[1], BOX[2], BOX[3], [30, 140, 70, 1], { efx: "nope" })],
98
+ effects: [{ id: "nope", kind: "aurora-borealis", on: "always", box: BOX, p: {} }],
99
+ },
100
+ };
101
+
102
+ /** Smoke on the same card, at a clock. A SOURCE effect like the starfield, so
103
+ * the arrangement is the same: a dark card, and the smoke is everything on it
104
+ * that is not the card. */
105
+ const smoke = (p) => ({
106
+ width: W, height: H,
107
+ cmds: [PAGE_WHITE, rect(BOX[0], BOX[1], BOX[2], BOX[3], SKY, { efx: "fog" })],
108
+ effects: [{
109
+ id: "fog", kind: "smoke", on: "always", box: BOX,
110
+ p: { scale: 90, seed: 2, ...p }, time: (p && p.__t) || 0,
111
+ }],
112
+ });
113
+ SCENES.smoke = smoke({});
114
+ SCENES.smokeLater = smoke({ rise: 0.35, __t: 4 });
115
+ SCENES.smokeTall = smoke({ height: 8 });
116
+
117
+ /** A hard black/white edge down the page, with and without a ripple in a box
118
+ * around the middle of it. The edge is what a displacement is visible on. */
119
+ const edgeCmds = (efx) => [
120
+ rect(0, 0, W / 2, H, [0, 0, 0, 1]),
121
+ rect(W / 2, 0, W / 2, H, [255, 255, 255, 1]),
122
+ rect(BOX[0], BOX[1], BOX[2], BOX[3], [0, 0, 0, 0], efx ? { efx: "wet" } : {}),
123
+ ];
124
+ SCENES.edgePlain = { width: W, height: H, cmds: edgeCmds(false) };
125
+ SCENES.edgeRippled = {
126
+ width: W, height: H,
127
+ cmds: edgeCmds(true),
128
+ effects: [{
129
+ id: "wet", kind: "ripple", on: "press", box: BOX,
130
+ // Slow and strong, so one ring is well inside the box at this age and the
131
+ // displacement is far larger than any rounding.
132
+ p: { speed: 120, width: 34, strength: 26, decay: 0.6, rings: 1, shine: 0, highlight: 0 },
133
+ // OFF THE EDGE, not on it. A ring centred on the edge pushes the surface
134
+ // along the edge, which moves a vertical boundary by nothing at all — the
135
+ // displacement is radial, so it has to arrive at an angle to be visible.
136
+ events: [[BOX[0] + 60, BOX[1] + BOX[3] / 2, 0.42]],
137
+ time: 0.42,
138
+ }],
139
+ };
140
+
141
+ /** A page of stripes, a glass pane over part of it, and a plate drawn AFTER
142
+ * the pane. Stripes because a refraction is a DISPLACEMENT: on a flat colour
143
+ * the most badly broken lens in the world looks perfect, and every check
144
+ * below would pass on a shader that returned its input. */
145
+ const stripes = () => {
146
+ const out = [rect(0, 0, W, H, [10, 12, 30, 1])];
147
+ for (let i = 0; i < W / 20; i += 1) {
148
+ out.push(rect(i * 20, 0, 10, H, i % 2 ? [90, 140, 255, 1] : [240, 120, 190, 1]));
149
+ }
150
+ return out;
151
+ };
152
+ const GLASS = [50, 40, 220, 140];
153
+ // tint and shine off: what is left is the refraction alone, which is the thing
154
+ // these checks are about. A lift across the pane would pass "the middle is
155
+ // untouched" only by luck, and fail it honestly.
156
+ const GLASS_P = { thickness: 26, strength: 34, power: 2.2, disperse: 0.08, shine: 0, tint: 0 };
157
+ SCENES.stripesPlain = { width: W, height: H, cmds: stripes() };
158
+ // The same pane with a SWEEP on it, at two different clocks. A bar of light
159
+ // that never moves is a gradient somebody drew.
160
+ const swept = (t, extra) => ({
161
+ width: W, height: H,
162
+ cmds: stripes().concat([rect(GLASS[0], GLASS[1], GLASS[2], GLASS[3], [0, 0, 0, 0], { efx: "pane" })]),
163
+ effects: [{
164
+ id: "pane", kind: "liquid-glass", box: GLASS, r: 28, time: t,
165
+ // `sweep-rim: 0` and a full duty: the bar itself, crossing evenly, which
166
+ // is what the two checks below are about. The shaping is checked
167
+ // separately, because a bar kept to the rim and a bar that is not there
168
+ // look the same in the middle of a pane.
169
+ p: { ...GLASS_P, sweep: 0.9, "sweep-width": 0.05, "sweep-speed": 0.5,
170
+ "sweep-edge": 2.5, "sweep-rim": 0, "sweep-duty": 1, ...extra },
171
+ }],
172
+ });
173
+ SCENES.sweptEarly = swept(0.2);
174
+ SCENES.sweptLater = swept(1.2);
175
+ // KEPT TO THE RIM. The same bar, weighted entirely to the bevel: the middle of
176
+ // the pane has to come back as if there were no bar at all, which is the
177
+ // difference between a light catching an edge and a line drawn across a window.
178
+ SCENES.sweptRim = swept(0.9, { "sweep-rim": 1 });
179
+ // AND PARKED. A third of the cycle crossing, the rest off the pane — at a
180
+ // clock past the pass there is no bar anywhere.
181
+ SCENES.sweptParked = swept(1.4, { "sweep-duty": 0.3 });
182
+ SCENES.stripesGlass = {
183
+ width: W, height: H,
184
+ cmds: stripes().concat([
185
+ rect(GLASS[0], GLASS[1], GLASS[2], GLASS[3], [0, 0, 0, 0], { efx: "pane" }),
186
+ // The element's own content, drawn after it: this must come back exactly
187
+ // its own colour, or the pane is refracting what is in front of it. Up in
188
+ // the pane's top-left corner, where it straddles the rim band — the part
189
+ // most bent, and so the part where a wrong order would show first.
190
+ rect(GLASS[0] + 12, GLASS[1] + 12, 70, 34, [20, 230, 120, 1]),
191
+ ]),
192
+ effects: [{ id: "pane", kind: "liquid-glass", box: GLASS, r: 28, p: GLASS_P }],
193
+ };
194
+
195
+ /** And the ORIGINAL whole-surface effect, which must still work unchanged. */
196
+ SCENES.legacy = {
197
+ width: W, height: H,
198
+ cmds: edgeCmds(false),
199
+ effect: {
200
+ kind: "ripple", drops: [[BOX[0] + 60, BOX[1] + BOX[3] / 2, 0.42]],
201
+ speed: 120, width: 34, strength: 26, decay: 0.6, highlight: 0,
202
+ rings: 1, stagger: 0.09, falloff: 0.62, shine: 0, gloss: 120, bump: 70,
203
+ light: [-0.45, -0.65, 0.62],
204
+ },
205
+ };
206
+
207
+ const painter = fs.readFileSync(path.join(HERE, "evg-webgl.js"), "utf8").replace(/^export /gm, "");
208
+
209
+ // THE CONTEXT A REAL PAGE ASKS FOR, and not the one that makes checking easy.
210
+ //
211
+ // These checks used to run with `preserveDrawingBuffer: true`, which on this
212
+ // driver hands out a SINGLE-SAMPLED default framebuffer. Every page in this
213
+ // repository asks for `antialias: true` without it and gets a MULTISAMPLED one
214
+ // — and a multisampled default framebuffer cannot be copied out of, which is
215
+ // exactly what a backdrop effect does mid-frame. So the checks passed on a
216
+ // painter that wiped the top half of the real demo page: the copy failed
217
+ // silently, and the pass wrote the black it got back over everything painted
218
+ // before the pane.
219
+ //
220
+ // So: the same attributes a page uses, and the pixels read the way a person
221
+ // sees them — `drawImage` onto a 2-D canvas in the SAME task as the render,
222
+ // while the drawing buffer is still valid, which is what a screenshot does and
223
+ // what `preserveDrawingBuffer` was standing in for.
224
+ const pageFor = (scene) => `<!doctype html><meta charset="utf-8"><body style="margin:0;background:#fff">
225
+ <canvas id="c" width="${W}" height="${H}" style="width:${W}px;height:${H}px"></canvas>
226
+ <canvas id="read" width="${W}" height="${H}" style="display:none"></canvas>
227
+ <script>
228
+ ${painter}
229
+ (async () => {
230
+ try {
231
+ const c = document.getElementById("c");
232
+ const gl = c.getContext("webgl2", { antialias: true, alpha: false });
233
+ const doc = ${JSON.stringify(scene)};
234
+ const list = { cmds: doc.cmds };
235
+ if (doc.effect) list.effect = doc.effect;
236
+ if (doc.effects) list.effects = doc.effects;
237
+ window.__stats = renderDisplayList(gl, { list, width: doc.width, height: doc.height }, { dpr: 1 });
238
+ const ctx = document.getElementById("read").getContext("2d", { willReadFrequently: true });
239
+ ctx.drawImage(c, 0, 0);
240
+ window.__PX = Array.from(ctx.getImageData(0, 0, ${W}, ${H}).data);
241
+ } catch (e) { window.__ERR__ = String(e && e.stack || e); }
242
+ window.__DONE__ = true;
243
+ })();
244
+ </script></body>`;
245
+
246
+ // ---------------------------------------------------------------------------
247
+ // The driver, which needs no browser at all
248
+ // ---------------------------------------------------------------------------
249
+ //
250
+ // Everything above is about pixels. This is about WHO GETS THE EVENT, which is
251
+ // the other half of "the document decides": a press inside a box whose sheet
252
+ // asked for one, and nothing at all otherwise. It runs first because it needs
253
+ // no GPU, so a failure here is not hidden behind a browser launch.
254
+ let bad = 0, checked = 0;
255
+ const say = (ok, name, detail) => {
256
+ checked += 1;
257
+ if (ok) { console.log(" PASS " + name); return; }
258
+ bad += 1;
259
+ console.log(" FAIL " + name + (detail ? "\n " + detail : ""));
260
+ };
261
+
262
+ console.log("=== element-scoped surface effects ===");
263
+ console.log("");
264
+ console.log("-- the driver: which box gets the press");
265
+ {
266
+ const list = {
267
+ effects: [
268
+ { id: "sky", kind: "starfield", on: "always", box: [0, 0, 300, 100], p: {} },
269
+ { id: "pool", kind: "ripple", on: "press drag", box: [40, 120, 120, 80], p: {} },
270
+ { id: "quiet", kind: "ripple", box: [200, 120, 80, 80], p: {} },
271
+ ],
272
+ };
273
+ const fx = createEffectDriver();
274
+ fx.tick(0, list);
275
+
276
+ say(fx.press(100, 160) !== null, "a press inside the box that asked for one lands");
277
+ say(list.effects[1].events.length === 1 || fx.tick(0, list) === true && list.effects[1].events.length === 1,
278
+ "and the instance carries it");
279
+ say(fx.press(240, 160) === null, "a press on a box that declared no trigger does nothing",
280
+ "`evg-surface-effect` without `evg-effect-on` is an effect the application drives — " +
281
+ "the driver must not adopt it");
282
+ say(fx.press(150, 250) === null, "and a press on nothing at all does nothing");
283
+ say(fx.hover(100, 160) === null, "a hover where only a press was asked for does nothing");
284
+
285
+ // AGEING. The event is the same object the painter reads, so what the
286
+ // driver does to it is what the shader sees.
287
+ fx.tick(1000, list);
288
+ const e = list.effects[1].events[0];
289
+ say(Math.abs(e[2] - 1) < 0.01, `a second later it is a second old (${e[2].toFixed(2)})`);
290
+ fx.tick(2500, list);
291
+ say(list.effects[1].events.length === 0, "and past its life it is gone",
292
+ "an event that never retires is a page that never stops drawing");
293
+
294
+ // BUSY. A page with an `always` effect never stops; one with only idle
295
+ // filters does.
296
+ say(fx.busy() === true, "a page with an `always` effect is always busy");
297
+ const idle = { effects: [{ id: "pool", kind: "ripple", on: "press", box: [0, 0, 10, 10], p: {} }] };
298
+ const fx2 = createEffectDriver();
299
+ fx2.tick(0, idle);
300
+ say(fx2.busy() === false, "a page whose only effect is idle is not",
301
+ "the host would keep asking for frames for an effect with nothing in flight");
302
+
303
+ // SWITCHED OFF, in the driver as well as in the painter: an effect that is
304
+ // not running must not swallow the press either.
305
+ const withOff = {
306
+ effects: [
307
+ { id: "sleeping", kind: "ripple", on: "press", box: [0, 0, 200, 200], p: {}, off: true },
308
+ { id: "awake", kind: "starfield", on: "always", box: [0, 0, 200, 200], p: {}, off: true },
309
+ ],
310
+ };
311
+ const fxOff = createEffectDriver();
312
+ fxOff.tick(0, withOff);
313
+ say(fxOff.press(100, 100) === null, "a press on an effect that is switched off does nothing");
314
+ say(fxOff.busy() === false, "and an `always` effect that is off does not keep the page drawing",
315
+ "a switched-off starfield would burn a frame a second doing nothing");
316
+
317
+ // THE TOPMOST ONE WINS, because two effects that overlap are a stack.
318
+ const stacked = {
319
+ effects: [
320
+ { id: "under", kind: "ripple", on: "press", box: [0, 0, 200, 200], p: {} },
321
+ { id: "over", kind: "ripple", on: "press", box: [50, 50, 100, 100], p: {} },
322
+ ],
323
+ };
324
+ const fx3 = createEffectDriver();
325
+ fx3.tick(0, stacked);
326
+ const hit = fx3.press(100, 100);
327
+ say(hit && hit.id === "over", "where two overlap, the one painted last takes the press",
328
+ "paint order decides what is on top, and a press goes to what is on top");
329
+ }
330
+
331
+ console.log("");
332
+ console.log("-- the demo document, straight out of the engine");
333
+ {
334
+ // The page the demo draws is generated by `npm run evg:fx:doc` from a Ranger
335
+ // document whose effects are declared in a stylesheet. If that file is here,
336
+ // hold it to what the sheet says — this is the seam between "CSS said so"
337
+ // and "the painter drew it", and the only place both ends are visible.
338
+ const demo = path.join(HERE, "fx-demo.js");
339
+ if (!fs.existsSync(demo)) {
340
+ console.log(" SKIP fx-demo.js is not built — run `npm run evg:fx:doc`");
341
+ } else {
342
+ const sandbox = { window: {} };
343
+ new Function("window", fs.readFileSync(demo, "utf8"))(sandbox.window);
344
+ const effects = sandbox.window.EVG_FX_DEMO.list.effects || [];
345
+ const byId = Object.fromEntries(effects.map((e) => [e.id, e]));
346
+ say(effects.length === 4, `four effects, one per element that declared one (${effects.length})`);
347
+ say(byId.sky && byId.sky.kind === "starfield" && /always/.test(byId.sky.on || ""),
348
+ "the sky is a starfield that runs on its own clock");
349
+ say(byId.sky && byId.sky.p.density === 1.6 && byId.sky.p.hue2 === 305,
350
+ "carrying the `evg-fx-*` numbers the sheet wrote",
351
+ "a parameter the engine has never heard of did not survive the trip");
352
+ say(byId.pool && byId.pool.box[2] === 250 && byId.pool.box[3] === 150,
353
+ "and a pool's box is the one the layout gave it",
354
+ "the box is not a rectangle anybody typed — it is where the element ended up");
355
+ const carriers = (sandbox.window.EVG_FX_DEMO.list.cmds || []).filter((c) => c.efx).map((c) => c.efx);
356
+ say(carriers.length === 4, `each one anchored in the command stream (${carriers.join(", ")})`);
357
+ say(byId.glass && byId.glass.kind === "liquid-glass" && /always/.test(byId.glass.on || ""),
358
+ "the pane is a backdrop effect that runs on its own clock",
359
+ "the demo's pane has a moving sweep on it, and a sweep needs frames: " +
360
+ "`always` is what asks the host for them");
361
+ say(byId.glass && byId.glass.p.sweep > 0 && byId.glass.p["sweep-speed"] > 0,
362
+ "with the sweep and its speed as parameters under dashed names",
363
+ "`evg-fx-sweep-speed` did not survive the trip as `sweep-speed`");
364
+ }
365
+ }
366
+
367
+ console.log("");
368
+ console.log("-- and the two writers agree about it");
369
+ {
370
+ // The display list is read two ways: `toJson` for a host on the other side of
371
+ // a string, and `evg-list.js` straight off the Ranger object for a host in
372
+ // the same process. They have to produce the same thing key for key, and
373
+ // `rt:scroll` already holds them together on every frame it draws — on a page
374
+ // with no effects on it. This is the same claim for a page that has three.
375
+ const built = path.join(HERE, "..", "bin", "FxDemoDoc.cjs");
376
+ if (!fs.existsSync(built)) {
377
+ console.log(" SKIP FxDemoDoc.cjs is not built — run `npm run evg:fx:doc`");
378
+ } else {
379
+ const rgr = createRequire(import.meta.url)(built);
380
+ const root = rgr.FxDemoDoc.document();
381
+ const lay = new rgr.EVGLayout();
382
+ lay.setPageSize(900, 560);
383
+ lay.layout(root);
384
+ const dl = new rgr.EVGDisplayList();
385
+ dl.setTextEngine(lay.getTextEngine());
386
+ dl.build(root);
387
+ const fromObject = listOf(dl).effects;
388
+ const fromJson = JSON.parse(dl.toJson()).effects;
389
+ // Keys sorted and coordinates rounded: `toJson` writes two decimals and the
390
+ // object reader hands back what the layout computed, which is the one
391
+ // difference between them that is meant to be there.
392
+ const shape = (e) => JSON.stringify(e, (k, v) =>
393
+ (v && typeof v === "object" && !Array.isArray(v))
394
+ ? Object.fromEntries(Object.keys(v).sort().map((n) => [n, v[n]]))
395
+ : (typeof v === "number" ? Math.round(v * 100) / 100 : v));
396
+ const same = fromObject.length === fromJson.length &&
397
+ fromObject.every((e, i) => shape(e) === shape(fromJson[i]));
398
+ say(same, `both readers see the same ${fromObject.length} effects`,
399
+ "the object reader and the JSON writer disagree:\n " +
400
+ JSON.stringify(fromObject) + "\n " + JSON.stringify(fromJson));
401
+ const carriers = listOf(dl).cmds.filter((c) => c.efx).length;
402
+ say(carriers === JSON.parse(dl.toJson()).cmds.filter((c) => c.efx).length,
403
+ `and the same ${carriers} rectangles carrying one`);
404
+ }
405
+ }
406
+
407
+ console.log("");
408
+ console.log("-- the parameter reference says what the registry says");
409
+ {
410
+ // PLAN_EFFECTS.md lists every effect's parameters and defaults, and a list
411
+ // of numbers in prose is the kind of documentation that is wrong six months
412
+ // later. Both sides are read here: the `params` object out of the painter,
413
+ // and the table under the effect's own heading. A parameter added to a
414
+ // plugin and not written down fails this; so does a default changed in one
415
+ // place. It is about the DOC, so it runs before the browser starts.
416
+ const painter = fs.readFileSync(path.join(HERE, "evg-webgl.js"), "utf8");
417
+ const plan = fs.readFileSync(path.join(HERE, "..", "PLAN_EFFECTS.md"), "utf8");
418
+ const ref = plan.slice(plan.indexOf("### Every parameter"), plan.indexOf("### Presets"));
419
+ const missing = [];
420
+ let seen = 0;
421
+ for (const m of painter.matchAll(/name: "([a-z-]+)",\s*\n\s*layer: "([a-z]+)",([\s\S]*?)params: \{([\s\S]*?)\},/g)) {
422
+ const [, name, layer, , body] = m;
423
+ seen += 1;
424
+ const head = "**`" + name + "`** (" + layer + ")";
425
+ if (!ref.includes(head)) { missing.push(name + ": no section"); continue; }
426
+ const table = ref.split(head)[1].split("\n\n**")[0];
427
+ const numbers = table.match(/-?[\d.]+/g) || [];
428
+ for (const d of body.matchAll(/"?([a-zA-Z-]+)"?\s*:\s*(-?[\d.]+)/g)) {
429
+ if (!table.includes("`" + d[1] + "`")) missing.push(name + "." + d[1]);
430
+ else if (!numbers.includes(d[2])) missing.push(name + "." + d[1] + " = " + d[2]);
431
+ }
432
+ }
433
+ say(seen > 0 && missing.length === 0,
434
+ `every parameter of all ${seen} effects is documented, with its default`,
435
+ "not in PLAN_EFFECTS.md, or written with another default: " + missing.join(", "));
436
+ }
437
+
438
+ console.log("");
439
+ console.log("-- the starfield presets, through the engine's own parser");
440
+ {
441
+ // `starfield-presets.css` is five paste-able skies, and `starfield-shots.mjs`
442
+ // reads them with a regex to paint them. This reads the SAME file with the
443
+ // cascade — EVGStyleSheet, applied to a tree, laid out, built — and holds the
444
+ // two readings against each other. A preset with a property the engine does
445
+ // not take would still paint from the regex and would vanish here.
446
+ const built = path.join(HERE, "..", "bin", "FxDemoDoc.cjs");
447
+ const presets = readPresets();
448
+ say(presets.length >= 4, `the file holds ${presets.length} skies`);
449
+ if (!fs.existsSync(built)) {
450
+ console.log(" SKIP FxDemoDoc.cjs is not built — run `npm run evg:fx:doc`");
451
+ } else {
452
+ const rgr = createRequire(import.meta.url)(built);
453
+ const root = new rgr.EVGElement();
454
+ root.setAttribute("width", "600px");
455
+ root.setAttribute("height", `${presets.length * 120}px`);
456
+ for (const p of presets) {
457
+ const box = rgr.EVGElement.createDiv();
458
+ box.id = p.name;
459
+ box.className = p.name;
460
+ box.setAttribute("width", "600px");
461
+ box.setAttribute("height", "100px");
462
+ root.addChild(box);
463
+ }
464
+ const sheet = new rgr.EVGStyleSheet();
465
+ sheet.parse(fs.readFileSync(path.join(HERE, "effect-presets.css"), "utf8"));
466
+ sheet.setViewport(600, presets.length * 120, false);
467
+ sheet.applyTree(root, "");
468
+ say(sheet.getErrorCount() === 0,
469
+ `the cascade takes every declaration in it (${sheet.getErrorCount()} refused)`,
470
+ sheet.getErrorCount() > 0 ? sheet.getError(0) : "");
471
+
472
+ const lay = new rgr.EVGLayout();
473
+ lay.setPageSize(600, presets.length * 120);
474
+ lay.layout(root);
475
+ const dl = new rgr.EVGDisplayList();
476
+ dl.setTextEngine(lay.getTextEngine());
477
+ dl.build(root);
478
+ const fromEngine = listOf(dl).effects || [];
479
+ say(fromEngine.length === presets.length,
480
+ `each one becomes an instance (${fromEngine.length})`,
481
+ fromEngine.map((e) => e.id).join(","));
482
+ const wrongKind = presets.filter((p) => {
483
+ const e = fromEngine.find((x) => x.id === p.name);
484
+ return !e || e.kind !== p.kind;
485
+ }).map((p) => p.name);
486
+ say(wrongKind.length === 0,
487
+ `each one naming the effect its block declares (${[...new Set(presets.map((p) => p.kind))].join(", ")})`,
488
+ "these came out as something else: " + wrongKind.join(", "));
489
+
490
+ // AND THE SAME NUMBERS, both ways. This is the check that keeps the
491
+ // pictures honest: the shots script and the engine read one file, and if
492
+ // they disagree the pictures are of something nobody can paste.
493
+ let worst = null;
494
+ for (const p of presets) {
495
+ const e = fromEngine.find((x) => x.id === p.name);
496
+ if (!e) { worst = `${p.name} is missing`; break; }
497
+ for (const [k, v] of Object.entries(p.params)) {
498
+ if (Math.abs((e.p[k] === undefined ? NaN : e.p[k]) - v) > 0.001) {
499
+ worst = `${p.name}: ${k} is ${e.p[k]} through the cascade and ${v} in the file`;
500
+ break;
501
+ }
502
+ }
503
+ if (worst) break;
504
+ }
505
+ say(worst === null, "with the same parameters the file declares", worst || "");
506
+ }
507
+ }
508
+
509
+ let chromium;
510
+ try {
511
+ ({ chromium } = requireHostTool("playwright-core"));
512
+ } catch (e) {
513
+ console.error(e instanceof MissingDomDeps ? e.message : String(e));
514
+ process.exit(3);
515
+ }
516
+ // SwiftShader, for the same reason every other painter check uses it: a check
517
+ // that only runs on a machine with a GPU is not a check.
518
+ const browser = await chromium.launch({
519
+ executablePath: findChromium(),
520
+ args: ["--use-gl=swiftshader", "--enable-unsafe-swiftshader"],
521
+ });
522
+ const page = await browser.newPage({ viewport: { width: W + 40, height: H + 40 } });
523
+ const pageErrors = [];
524
+ page.on("pageerror", (e) => pageErrors.push(String(e.message)));
525
+ const tmp = path.join(HERE, ".fx-check.html");
526
+
527
+ /** The same page at a size of its own, for a scene that does not fit the one
528
+ * above. The preset sheet is the only caller: a dozen tiles big enough to
529
+ * hold a field of raindrops is 440 pixels across, and every other scene here
530
+ * is about one box and fits in 320. */
531
+ async function frameOfSize(scene, w, h) {
532
+ const big = pageFor(scene).replace(
533
+ `<canvas id="c" width="${W}" height="${H}" style="width:${W}px;height:${H}px"></canvas>`,
534
+ `<canvas id="c" width="${w}" height="${h}" style="width:${w}px;height:${h}px"></canvas>`,
535
+ ).replace(
536
+ `<canvas id="read" width="${W}" height="${H}" style="display:none"></canvas>`,
537
+ `<canvas id="read" width="${w}" height="${h}" style="display:none"></canvas>`,
538
+ ).replace(`ctx.getImageData(0, 0, ${W}, ${H})`, `ctx.getImageData(0, 0, ${w}, ${h})`);
539
+ fs.writeFileSync(tmp, big);
540
+ await page.goto(pathToFileURL(tmp).href);
541
+ await page.waitForFunction("window.__DONE__ === true", { timeout: 30000 });
542
+ const err = await page.evaluate(() => window.__ERR__ || null);
543
+ if (err) throw new Error("painter threw: " + err);
544
+ const out = await page.evaluate(() => ({ px: window.__PX, stats: window.__stats }));
545
+ if (!out.px) throw new Error("the page read no pixels back");
546
+ const px = Uint8Array.from(out.px);
547
+ return { px, w, h, at: (x, y) => { const i = (y * w + x) * 4; return [px[i], px[i + 1], px[i + 2], px[i + 3]]; } };
548
+ }
549
+
550
+ /** Render a scene once and hand back the whole canvas as RGBA bytes, plus what
551
+ * the painter reported about it. Whole frames rather than probes: most of
552
+ * what is claimed here is about a REGION, and a region is not three pixels. */
553
+ async function frameOf(scene, png) {
554
+ fs.writeFileSync(tmp, pageFor(scene));
555
+ await page.goto(pathToFileURL(tmp).href);
556
+ await page.waitForFunction("window.__DONE__ === true", { timeout: 30000 });
557
+ const err = await page.evaluate(() => window.__ERR__ || null);
558
+ if (err) throw new Error("painter threw: " + err);
559
+ if (png) await page.locator("#c").screenshot({ path: png });
560
+ const out = await page.evaluate(() => ({ px: window.__PX, stats: window.__stats }));
561
+ if (!out.px) throw new Error("the page read no pixels back");
562
+ // Already top-down: this came off a 2-D canvas, which counts from the top the
563
+ // way the display list does.
564
+ return { px: Uint8Array.from(out.px), stats: out.stats };
565
+ }
566
+
567
+ const at = (px, x, y) => {
568
+ const i = (y * W + x) * 4;
569
+ return [px[i], px[i + 1], px[i + 2], px[i + 3]];
570
+ };
571
+ const lum = (p) => 0.2126 * p[0] + 0.7152 * p[1] + 0.0722 * p[2];
572
+ const inside = (x, y) => x >= BOX[0] + 2 && y >= BOX[1] + 2 && x < BOX[0] + BOX[2] - 2 && y < BOX[1] + BOX[3] - 2;
573
+
574
+ const writeAt = process.argv.indexOf("--write-png");
575
+ const pngPath = writeAt > -1 ? process.argv[writeAt + 1] : null;
576
+
577
+ console.log("");
578
+ console.log("-- a starfield on a card");
579
+ const sky = await frameOf(SCENES.sky, pngPath);
580
+
581
+ {
582
+ // INSIDE: the card is nearly black, and stars are the only thing on it that
583
+ // is not. A field with nothing in it reads as the background everywhere.
584
+ let lit = 0, brightest = 0;
585
+ for (let y = BOX[1] + 2; y < BOX[1] + BOX[3] - 2; y += 1) {
586
+ for (let x = BOX[0] + 2; x < BOX[0] + BOX[2] - 2; x += 1) {
587
+ const l = lum(at(sky.px, x, y));
588
+ if (l > lum(SKY) + 12) lit += 1;
589
+ brightest = Math.max(brightest, l);
590
+ }
591
+ }
592
+ const area = (BOX[2] - 4) * (BOX[3] - 4);
593
+ say(lit > area * 0.005, `something is shining on it (${lit} of ${area} pixels lit)`,
594
+ "a starfield that lights nothing is a starfield whose parameters never arrived");
595
+ say(brightest > 150, `and some of it is bright (peak ${Math.round(brightest)})`,
596
+ "stars with no core: the brightest pixel on the card is barely off the background");
597
+ }
598
+
599
+ {
600
+ // OUTSIDE: byte for byte the page drawn without the effect at all. Not "close
601
+ // to white" — the same scene twice, so the only thing a difference can be is
602
+ // the effect reaching past its own box.
603
+ const plainSky = await frameOf(SCENES.skyPlain);
604
+ let worst = 0, where = null;
605
+ for (let y = 0; y < H; y += 1) {
606
+ for (let x = 0; x < W; x += 1) {
607
+ if (inside(x, y)) continue;
608
+ // The box's own edge is antialiased; step clear of it on both sides.
609
+ if (x >= BOX[0] - 2 && x < BOX[0] + BOX[2] + 2 && y >= BOX[1] - 2 && y < BOX[1] + BOX[3] + 2) continue;
610
+ const d = Math.abs(lum(at(sky.px, x, y)) - lum(at(plainSky.px, x, y)));
611
+ if (d > worst) { worst = d; where = [x, y]; }
612
+ }
613
+ }
614
+ say(worst <= 1, `and nothing at all outside the card (worst ${worst.toFixed(1)})`,
615
+ `the page around the box changed at ${where} — the effect is painting past the element that declared it`);
616
+ }
617
+
618
+ console.log("");
619
+ console.log("-- and the host can switch one off");
620
+ {
621
+ const offFrame = await frameOf(SCENES.skyOff);
622
+ const plainSky = await frameOf(SCENES.skyPlain);
623
+ let worst = 0, where = null;
624
+ for (let y = 0; y < H; y += 1) {
625
+ for (let x = 0; x < W; x += 1) {
626
+ const d = Math.abs(lum(at(offFrame.px, x, y)) - lum(at(plainSky.px, x, y)));
627
+ if (d > worst) { worst = d; where = [x, y]; }
628
+ }
629
+ }
630
+ say(worst <= 1, `an instance marked off draws nothing at all (worst ${worst.toFixed(1)})`,
631
+ `${where} still shows the effect — \`off\` is being read as "dim" somewhere`);
632
+ say(offFrame.stats && offFrame.stats.fxDrawn === 0,
633
+ "and costs no pass at all",
634
+ `the painter reported ${offFrame.stats && offFrame.stats.fxDrawn} effect passes for a page whose only effect is off`);
635
+ }
636
+
637
+ console.log("");
638
+ console.log("-- and the card's content is drawn over it");
639
+ {
640
+ const plated = await frameOf(SCENES.skyUnderPlate);
641
+ let worst = 0;
642
+ for (let y = 75; y < 125; y += 1) {
643
+ for (let x = 105; x < 175; x += 1) {
644
+ const p = at(plated.px, x, y);
645
+ worst = Math.max(worst, Math.abs(p[0] - 220), Math.abs(p[1] - 30), Math.abs(p[2] - 40));
646
+ }
647
+ }
648
+ say(worst <= 2, `the plate over it is its own colour (worst ${worst})`,
649
+ "a star came through an opaque rectangle: the source effect is being drawn " +
650
+ "after the element's content instead of at its background");
651
+ }
652
+
653
+ console.log("");
654
+ console.log("-- it moves");
655
+ {
656
+ const later = await frameOf(SCENES.skyLater);
657
+ let diff = 0, n = 0;
658
+ for (let y = BOX[1] + 2; y < BOX[1] + BOX[3] - 2; y += 2) {
659
+ for (let x = BOX[0] + 2; x < BOX[0] + BOX[2] - 2; x += 2) {
660
+ diff += Math.abs(lum(at(sky.px, x, y)) - lum(at(later.px, x, y)));
661
+ n += 1;
662
+ }
663
+ }
664
+ const mean = diff / n;
665
+ say(mean > 1.5, `five seconds later it is a different sky (mean |Δ| ${mean.toFixed(2)})`,
666
+ "the two frames are the same picture: the clock is not reaching the shader, " +
667
+ "so the field is a still image that happens to look like stars");
668
+ }
669
+
670
+ console.log("");
671
+ console.log("-- a filter bends its own box and nothing else");
672
+ {
673
+ const plain = await frameOf(SCENES.edgePlain);
674
+ const wet = await frameOf(SCENES.edgeRippled);
675
+ // INSIDE: the edge moved. Sampled along the row through the ring's crest.
676
+ let moved = 0;
677
+ for (let y = BOX[1] + 10; y < BOX[1] + BOX[3] - 10; y += 3) {
678
+ for (let x = BOX[0] + 4; x < BOX[0] + BOX[2] - 4; x += 1) {
679
+ if (Math.abs(lum(at(plain.px, x, y)) - lum(at(wet.px, x, y))) > 20) { moved += 1; }
680
+ }
681
+ }
682
+ say(moved > 60, `the edge inside the box is displaced (${moved} pixels differ)`,
683
+ "the ripple ran and changed nothing: either the instance was not live or " +
684
+ "the events never reached the shader");
685
+
686
+ // OUTSIDE: identical. This is the claim that the pass is scoped at all.
687
+ let worst = 0, where = null;
688
+ for (let y = 0; y < H; y += 1) {
689
+ for (let x = 0; x < W; x += 1) {
690
+ if (x >= BOX[0] - 2 && x < BOX[0] + BOX[2] + 2 && y >= BOX[1] - 2 && y < BOX[1] + BOX[3] + 2) continue;
691
+ const d = Math.abs(lum(at(plain.px, x, y)) - lum(at(wet.px, x, y)));
692
+ if (d > worst) { worst = d; where = [x, y]; }
693
+ }
694
+ }
695
+ say(worst <= 2, `and the surface outside it is untouched (worst ${worst.toFixed(1)})`,
696
+ `${where} changed: the filter is a full-screen pass wearing a box`);
697
+ }
698
+
699
+ console.log("");
700
+ console.log("-- liquid glass bends what is behind it, at the rim");
701
+ {
702
+ const plain = await frameOf(SCENES.stripesPlain);
703
+ const glass = await frameOf(SCENES.stripesGlass);
704
+ const differs = (x, y) => Math.abs(lum(at(plain.px, x, y)) - lum(at(glass.px, x, y))) > 24;
705
+
706
+ // THE RIM. Sampled a few pixels inside the left and right edges, down the
707
+ // middle of the pane's height so the corners are not doing the work.
708
+ let rim = 0, rimSeen = 0;
709
+ for (let y = GLASS[1] + 40; y < GLASS[1] + GLASS[3] - 40; y += 2) {
710
+ for (const x of [GLASS[0] + 3, GLASS[0] + 8, GLASS[0] + GLASS[2] - 9, GLASS[0] + GLASS[2] - 4]) {
711
+ rimSeen += 1;
712
+ if (differs(x, y)) rim += 1;
713
+ }
714
+ }
715
+ say(rim > rimSeen * 0.5, `the stripes move through the rim (${rim} of ${rimSeen} samples)`,
716
+ "a lens that bends nothing at its edge is a pane of glass nobody can see — " +
717
+ "either the distance field or the displacement never reached the shader");
718
+
719
+ // THE MIDDLE. A pane is flat across the middle: what is behind it is where
720
+ // it was. This is what separates a lens from a filter over the whole box.
721
+ //
722
+ // Well clear of both: 45px in from every edge, which is comfortably past the
723
+ // 26px the pane's bevel reaches, and below the plate in the corner.
724
+ let mid = 0, midSeen = 0;
725
+ for (let y = GLASS[1] + 50; y < GLASS[1] + GLASS[3] - 45; y += 2) {
726
+ for (let x = GLASS[0] + 45; x < GLASS[0] + GLASS[2] - 45; x += 2) {
727
+ // Skip the plate the scene draws over the pane.
728
+ if (x >= GLASS[0] + 8 && x <= GLASS[0] + 86 && y >= GLASS[1] + 8 && y <= GLASS[1] + 50) continue;
729
+ midSeen += 1;
730
+ if (differs(x, y)) mid += 1;
731
+ }
732
+ }
733
+ say(midSeen > 100 && mid < midSeen * 0.02,
734
+ `and stand still across the middle (${mid} of ${midSeen} moved)`,
735
+ "the flat part of the pane is displacing too: the bevel profile is not " +
736
+ "falling to zero away from the edge");
737
+
738
+ // OUTSIDE. Byte for byte the page without the pane.
739
+ let worst = 0, where = null;
740
+ for (let y = 0; y < H; y += 1) {
741
+ for (let x = 0; x < W; x += 1) {
742
+ if (x >= GLASS[0] - 2 && x < GLASS[0] + GLASS[2] + 2 && y >= GLASS[1] - 2 && y < GLASS[1] + GLASS[3] + 2) continue;
743
+ const d = Math.abs(lum(at(plain.px, x, y)) - lum(at(glass.px, x, y)));
744
+ if (d > worst) { worst = d; where = [x, y]; }
745
+ }
746
+ }
747
+ say(worst <= 2, `with the page around it untouched (worst ${worst.toFixed(1)})`,
748
+ `${where} changed — the pane is refracting outside its own box`);
749
+
750
+ // AND WHAT IS IN FRONT STAYS SHARP. The whole reason a backdrop effect runs
751
+ // in paint order rather than over the finished frame: a pane that smears its
752
+ // own label ran too late.
753
+ let plateWorst = 0;
754
+ for (let y = GLASS[1] + 18; y < GLASS[1] + 40; y += 1) {
755
+ for (let x = GLASS[0] + 18; x < GLASS[0] + 76; x += 1) {
756
+ const p = at(glass.px, x, y);
757
+ plateWorst = Math.max(plateWorst, Math.abs(p[0] - 20), Math.abs(p[1] - 230), Math.abs(p[2] - 120));
758
+ }
759
+ }
760
+ say(plateWorst <= 2, `and the element's own content sharp on top (worst ${plateWorst})`,
761
+ "the plate over the pane came back bent: the backdrop pass is running after " +
762
+ "the element's content instead of before it");
763
+ }
764
+
765
+ console.log("");
766
+ console.log("-- and a sweep crosses it");
767
+ {
768
+ const early = await frameOf(SCENES.sweptEarly);
769
+ const later = await frameOf(SCENES.sweptLater);
770
+ const plainGlass = await frameOf(SCENES.stripesGlass);
771
+
772
+ // IT IS THERE: the pane with a bar on it is brighter somewhere than the
773
+ // same pane without one. BOTH clocks are looked at, because the bar travels
774
+ // from before one edge to past the other — at some clocks it is off the
775
+ // pane entirely, which is what makes it a sweep rather than a stripe.
776
+ let lit = 0, seen = 0;
777
+ for (let y = GLASS[1] + 20; y < GLASS[1] + GLASS[3] - 20; y += 2) {
778
+ for (let x = GLASS[0] + 20; x < GLASS[0] + GLASS[2] - 20; x += 2) {
779
+ seen += 1;
780
+ const base = lum(at(plainGlass.px, x, y));
781
+ if (lum(at(early.px, x, y)) > base + 20 || lum(at(later.px, x, y)) > base + 20) lit += 1;
782
+ }
783
+ }
784
+ say(lit > 20, `the bar lights part of the pane (${lit} of ${seen} samples)`,
785
+ "a sweep that lights nothing: the parameter never reached the shader");
786
+
787
+ // AND IT MOVES: the same pane at a later clock is lit somewhere else. The
788
+ // check that a still picture passes and a stopped clock does not.
789
+ let moved = 0;
790
+ for (let y = GLASS[1] + 20; y < GLASS[1] + GLASS[3] - 20; y += 2) {
791
+ for (let x = GLASS[0] + 20; x < GLASS[0] + GLASS[2] - 20; x += 2) {
792
+ if (Math.abs(lum(at(early.px, x, y)) - lum(at(later.px, x, y))) > 20) moved += 1;
793
+ }
794
+ }
795
+ say(moved > 20, `and it has moved a second later (${moved} samples differ)`,
796
+ "the bar is in the same place at two clocks — `sweep-speed` is not being read, " +
797
+ "or the clock is not reaching a backdrop effect");
798
+
799
+ // KEPT TO THE RIM. With `sweep-rim: 1` the flat middle of the pane must be
800
+ // what it was without any bar: a glint on a bevel, not a stripe over glass.
801
+ const rimmed = await frameOf(SCENES.sweptRim);
802
+ let midMoved = 0, midSeen = 0;
803
+ for (let y = GLASS[1] + 50; y < GLASS[1] + GLASS[3] - 45; y += 2) {
804
+ for (let x = GLASS[0] + 45; x < GLASS[0] + GLASS[2] - 45; x += 2) {
805
+ midSeen += 1;
806
+ if (Math.abs(lum(at(rimmed.px, x, y)) - lum(at(plainGlass.px, x, y))) > 12) midMoved += 1;
807
+ }
808
+ }
809
+ say(midSeen > 100 && midMoved < midSeen * 0.02,
810
+ `a rim-weighted bar leaves the middle alone (${midMoved} of ${midSeen} lit)`,
811
+ "the bar is crossing the flat part of the pane at full strength — " +
812
+ "`sweep-rim` is not weighting it toward the bevel");
813
+
814
+ // AND PARKED between passes: at a clock past the crossing there is no bar
815
+ // anywhere on the pane, which is what makes it a glint and not a stripe with
816
+ // an animation on it.
817
+ const parked = await frameOf(SCENES.sweptParked);
818
+ let anywhere = 0;
819
+ for (let y = GLASS[1] + 6; y < GLASS[1] + GLASS[3] - 6; y += 2) {
820
+ for (let x = GLASS[0] + 6; x < GLASS[0] + GLASS[2] - 6; x += 2) {
821
+ if (lum(at(parked.px, x, y)) > lum(at(plainGlass.px, x, y)) + 12) anywhere += 1;
822
+ }
823
+ }
824
+ say(anywhere < 30, `and between passes the pane is clean (${anywhere} pixels still lit)`,
825
+ "the bar never leaves: `sweep-duty` is not parking it off the pane");
826
+
827
+ // AND IT IS OFF UNLESS ASKED FOR: the default pane has no bar on it, which
828
+ // is why `sweep` defaults to 0.
829
+ say(plainGlass.stats !== undefined, "a pane with no sweep declared still draws");
830
+ }
831
+
832
+ console.log("");
833
+ console.log("-- smoke banks up along the floor of its box");
834
+ {
835
+ const fog = await frameOf(SCENES.smoke);
836
+ const band = (from, to) => {
837
+ let lit = 0, seen = 0;
838
+ for (let y = BOX[1] + Math.round(BOX[3] * from); y < BOX[1] + Math.round(BOX[3] * to); y += 1) {
839
+ for (let x = BOX[0] + 3; x < BOX[0] + BOX[2] - 3; x += 1) {
840
+ seen += 1;
841
+ if (lum(at(fog.px, x, y)) > lum(SKY) + 12) lit += 1;
842
+ }
843
+ }
844
+ return lit / Math.max(seen, 1);
845
+ };
846
+ const low = band(0.72, 0.98);
847
+ const high = band(0.02, 0.28);
848
+ say(low > 0.5, `the floor of the box is full of it (${Math.round(low * 100)}% of it lit)`,
849
+ "a smoke effect that leaves its own floor empty");
850
+ // THE WHOLE SHAPE OF IT, in one number: what makes this smoke rather than
851
+ // fog is that it is heavy at the bottom and thins as it climbs. A field with
852
+ // the height term dropped would light both bands alike and pass every other
853
+ // check here.
854
+ say(high < low * 0.6, `and thins toward the top (${Math.round(high * 100)}% up there)`,
855
+ `top ${Math.round(high * 100)}% vs floor ${Math.round(low * 100)}% — the height term is not being read`);
856
+
857
+ // AND `height` IS WHAT DECIDES THAT. The same smoke told to fill the box
858
+ // reaches the top of it, which is the parameter arriving rather than a
859
+ // gradient somebody baked in.
860
+ const tall = await frameOf(SCENES.smokeTall);
861
+ let up = 0, seen = 0;
862
+ for (let y = BOX[1] + 4; y < BOX[1] + Math.round(BOX[3] * 0.28); y += 1) {
863
+ for (let x = BOX[0] + 3; x < BOX[0] + BOX[2] - 3; x += 1) {
864
+ seen += 1;
865
+ if (lum(at(tall.px, x, y)) > lum(SKY) + 12) up += 1;
866
+ }
867
+ }
868
+ say(up / seen > high * 2, `and a taller one fills the box (${Math.round(up / seen * 100)}% at the top)`,
869
+ "`height` changed nothing: the same smoke at 0.9 and at 8");
870
+
871
+ // IT RISES. Two clocks, and the same pixels are not the same smoke.
872
+ const later = await frameOf(SCENES.smokeLater);
873
+ let moved = 0, looked = 0;
874
+ for (let y = BOX[1] + 20; y < BOX[1] + BOX[3] - 10; y += 2) {
875
+ for (let x = BOX[0] + 6; x < BOX[0] + BOX[2] - 6; x += 2) {
876
+ looked += 1;
877
+ if (Math.abs(lum(at(fog.px, x, y)) - lum(at(later.px, x, y))) > 14) moved += 1;
878
+ }
879
+ }
880
+ say(moved > looked * 0.1, `and it moves (${moved} of ${looked} samples differ four seconds on)`,
881
+ "the same smoke at two clocks: `rise` is not reaching the shader");
882
+ }
883
+
884
+ console.log("");
885
+ console.log("-- and every preset paints something of its own");
886
+ {
887
+ // One scene with all of them in it, because the claim is comparative: each
888
+ // has to light up, and no two may come out the same colour. Five separate
889
+ // renders would answer the first question and not the second.
890
+ const presets = readPresets();
891
+ // A grid of its own page, because this page is 320x220 and there are a dozen
892
+ // presets. 150 pixels is enough to answer both questions asked below and
893
+ // nowhere near enough to judge one by, which is what `effect-shots.mjs` is
894
+ // for. NOT SMALLER: a raindrop field at `density: 0.6` puts its cells 77
895
+ // pixels apart and drops in fewer than half of them, so a 100-pixel tile
896
+ // came up empty often enough to fail a check about the preset.
897
+ const TILE = 150, STEP = 156, COLS = 4;
898
+ const tiles = presets.map((p, i) => ({
899
+ p, x: 4 + (i % COLS) * STEP, y: 4 + Math.floor(i / COLS) * STEP,
900
+ }));
901
+ // The ground under the effect and the effect on a transparent box of its
902
+ // own — the arrangement a page uses, and the only one a BACKDROP effect
903
+ // survives: it runs before the element's own background, so an opaque box
904
+ // carrying one would paint over what it just drew.
905
+ const PW = 4 + COLS * STEP;
906
+ const PH = 4 + Math.ceil(presets.length / COLS) * STEP;
907
+ const sceneWith = (off) => ({
908
+ width: PW, height: PH,
909
+ cmds: [rect(0, 0, PW, PH, [10, 11, 18, 1])].concat(
910
+ tiles.flatMap((t) => [
911
+ rect(t.x, t.y, TILE, TILE, t.p.background),
912
+ // A LIT EDGE ACROSS THE GROUND, because a lens over a flat colour is
913
+ // invisible by construction: refraction of one colour is that colour.
914
+ // The two backdrop kinds here bend what is behind them, so what is
915
+ // behind them has to have something in it to bend.
916
+ rect(t.x + 18, t.y + 46, TILE - 36, 16, [86, 104, 168, 1]),
917
+ rect(t.x + 18, t.y + 96, TILE - 36, 8, [150, 160, 210, 1]),
918
+ rect(t.x, t.y, TILE, TILE, [0, 0, 0, 0], { efx: t.p.name }),
919
+ ])),
920
+ effects: tiles.map((t) => ({
921
+ id: t.p.name, kind: t.p.kind, on: t.p.trigger || "always", off,
922
+ box: [t.x, t.y, TILE, TILE], p: t.p.params, time: 3.2,
923
+ })),
924
+ });
925
+ const sheet = await frameOfSize(sceneWith(false), PW, PH);
926
+ // AGAINST THE SAME TILES WITH THE EFFECT OFF, and not against "is it
927
+ // bright": two of these skies are sparse on purpose and one effect is
928
+ // drops on a dark pane, so "did it light up" is the wrong question. What is
929
+ // asked is whether each preset DID something, and the only honest reference
930
+ // for that is the same box without it.
931
+ const bare = await frameOfSize(sceneWith(true), PW, PH);
932
+ const look = (t) => {
933
+ let r = 0, g = 0, b = 0, n = 0, moved = 0;
934
+ for (let y = t.y + 4; y < t.y + TILE - 4; y += 2) {
935
+ for (let x = t.x + 4; x < t.x + TILE - 4; x += 2) {
936
+ const px = sheet.at(x, y);
937
+ r += px[0]; g += px[1]; b += px[2]; n += 1;
938
+ if (Math.abs(lum(px) - lum(bare.at(x, y))) > 6) moved += 1;
939
+ }
940
+ }
941
+ return { r: r / n, g: g / n, b: b / n, moved };
942
+ };
943
+ const shot = tiles.map(look);
944
+ const dead = tiles.filter((t, i) => shot[i].moved < 5).map((t) => t.p.name);
945
+ say(dead.length === 0, `all ${tiles.length} presets paint something`,
946
+ "these changed nothing at all: " + dead.join(", "));
947
+ console.log(" changed: " + tiles.map((t, i) => t.p.name + "=" + shot[i].moved).join(" "));
948
+
949
+ // AND THE SKIES ARE DIFFERENT COLOURS, which is what those five are for.
950
+ // Not asked of the others: two fields of raindrops on two dark panes are
951
+ // meant to look alike, and a check that demanded otherwise would be asking
952
+ // for a difference nobody wants.
953
+ let same = null;
954
+ const skies = tiles.map((t, i) => ({ t, s: shot[i] })).filter((x) => x.t.p.kind === "starfield");
955
+ for (let i = 0; i < skies.length && !same; i += 1) {
956
+ for (let j = i + 1; j < skies.length; j += 1) {
957
+ const a = skies[i].s, b = skies[j].s;
958
+ const d = Math.abs(a.r - b.r) + Math.abs(a.g - b.g) + Math.abs(a.b - b.b);
959
+ if (d < 6) { same = `${skies[i].t.p.name} and ${skies[j].t.p.name} are the same colour (Δ ${d.toFixed(1)})`; break; }
960
+ }
961
+ }
962
+ say(same === null, `and no two of the ${skies.length} skies are the same colour`, same || "");
963
+ }
964
+
965
+ console.log("");
966
+ console.log("-- what must not break");
967
+ {
968
+ const unknown = await frameOf(SCENES.unknown);
969
+ const p = at(unknown.px, BOX[0] + 20, BOX[1] + 20);
970
+ say(Math.abs(p[0] - 30) <= 2 && Math.abs(p[1] - 140) <= 2,
971
+ "an effect nobody registered draws the page without it",
972
+ `the card came back ${p} instead of its own green — an unknown plugin took the frame with it`);
973
+
974
+ const legacy = await frameOf(SCENES.legacy);
975
+ const plain = await frameOf(SCENES.edgePlain);
976
+ let moved = 0;
977
+ for (let y = 40; y < H - 40; y += 3) {
978
+ for (let x = 40; x < W - 40; x += 1) {
979
+ if (Math.abs(lum(at(plain.px, x, y)) - lum(at(legacy.px, x, y))) > 20) moved += 1;
980
+ }
981
+ }
982
+ say(moved > 60, `the whole-surface effect still runs (${moved} pixels differ)`,
983
+ "`list.effect` — the original, application-driven ripple — stopped working");
984
+ say(legacy.stats && legacy.stats.rippled === 1, "and still reports that it took the post-pass");
985
+ }
986
+
987
+ await browser.close();
988
+ fs.rmSync(tmp, { force: true });
989
+ if (pageErrors.length) {
990
+ console.log("");
991
+ console.log("page errors:");
992
+ for (const e of pageErrors) console.log(" " + e);
993
+ bad += pageErrors.length;
994
+ }
995
+ console.log("");
996
+ console.log(`passed=${checked - bad} failed=${bad}`);
997
+ if (bad > 0) { console.log("FAILURES"); process.exit(1); }
998
+ console.log("ALL PASS");