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.
- package/CHANGELOG.md +1064 -20
- package/LICENSE +3 -1
- package/README.md +123 -28
- package/dist/Lang.rgr +1188 -290
- package/dist/api.d.ts +2343 -879
- package/dist/api.js +79923 -56921
- package/dist/lib/JSON.rgr +102 -91
- package/dist/lib/Shell.rgr +4 -4
- package/dist/lib/apple/AppleToolchain.rgr +4 -4
- package/dist/lib/apple/README.md +5 -4
- package/dist/lib/apple/apple_test.rgr +1 -1
- package/dist/lib/core/README.md +1 -1
- package/dist/lib/evg/EVG.rgr +12 -0
- package/dist/lib/evg/EVGA11yFromTree.rgr +302 -0
- package/dist/lib/evg/EVGA11yTree.rgr +894 -0
- package/dist/lib/evg/EVGBox.rgr +267 -0
- package/dist/lib/evg/EVGBoxShorthandTest.rgr +220 -0
- package/dist/lib/evg/EVGCodepoint.rgr +316 -0
- package/dist/lib/evg/EVGColor.rgr +700 -0
- package/dist/lib/evg/EVGCommands.rgr +177 -0
- package/dist/lib/evg/EVGComponent.rgr +331 -0
- package/dist/lib/evg/EVGComponentTest.rgr +387 -0
- package/dist/lib/evg/EVGConnector.rgr +541 -0
- package/dist/lib/evg/EVGConnectorTest.rgr +306 -0
- package/dist/lib/evg/EVGDisplayList.rgr +4186 -0
- package/dist/lib/evg/EVGEasing.rgr +370 -0
- package/dist/lib/evg/EVGEffectTest.rgr +253 -0
- package/dist/lib/evg/EVGElement.rgr +4914 -0
- package/dist/lib/evg/EVGFixedTest.rgr +289 -0
- package/dist/lib/evg/EVGFlexRulesTest.rgr +317 -0
- package/dist/lib/evg/EVGFlexWrapTest.rgr +246 -0
- package/dist/lib/evg/EVGFling.rgr +244 -0
- package/dist/lib/evg/EVGFocus.rgr +400 -0
- package/dist/lib/evg/EVGFocusTest.rgr +399 -0
- package/dist/lib/evg/EVGGradient.rgr +322 -0
- package/dist/lib/evg/EVGGrapheme.rgr +190 -0
- package/dist/lib/evg/EVGGrid.rgr +975 -0
- package/dist/lib/evg/EVGHitTest.rgr +208 -0
- package/dist/lib/evg/EVGHoles.rgr +294 -0
- package/dist/lib/evg/EVGHostMeasurerTest.rgr +281 -0
- package/dist/lib/evg/EVGHostTextMeasurer.rgr +331 -0
- package/dist/lib/evg/EVGHostTree.rgr +834 -0
- package/dist/lib/evg/EVGHostTreeTest.rgr +447 -0
- package/dist/lib/evg/EVGImageDecode.rgr +117 -0
- package/dist/lib/evg/EVGImageMeasurer.rgr +86 -0
- package/dist/lib/evg/EVGInspect.rgr +883 -0
- package/dist/lib/evg/EVGInvalidateTest.rgr +432 -0
- package/dist/lib/evg/EVGJsonTest.rgr +535 -0
- package/dist/lib/evg/EVGLayout.rgr +4220 -0
- package/dist/lib/evg/EVGMeasure.rgr +1267 -0
- package/dist/lib/evg/EVGOverlayTest.rgr +626 -0
- package/dist/lib/evg/EVGPatch.rgr +1721 -0
- package/dist/lib/evg/EVGPatchTest.rgr +692 -0
- package/dist/lib/evg/EVGPopoverTest.rgr +395 -0
- package/dist/lib/evg/EVGReconcile.rgr +226 -0
- package/dist/lib/evg/EVGReconcileTest.rgr +421 -0
- package/dist/lib/evg/EVGReject.rgr +126 -0
- package/dist/lib/evg/EVGRelayoutTest.rgr +375 -0
- package/dist/lib/evg/EVGRtlLayoutTest.rgr +319 -0
- package/dist/lib/evg/EVGRuler.rgr +232 -0
- package/dist/lib/evg/EVGRulerTest.rgr +172 -0
- package/dist/lib/evg/EVGSelectChrome.rgr +178 -0
- package/dist/lib/evg/EVGStyleCacheTest.rgr +419 -0
- package/dist/lib/evg/EVGStyleSheet.rgr +2148 -0
- package/dist/lib/evg/EVGStyleStateTest.rgr +407 -0
- package/dist/lib/evg/EVGStyleVarTest.rgr +471 -0
- package/dist/lib/evg/EVGText.rgr +105 -0
- package/dist/lib/evg/EVGTextEngine.rgr +536 -0
- package/dist/lib/evg/EVGTextMeasurer.rgr +720 -0
- package/dist/lib/evg/EVGTimingTest.rgr +1417 -0
- package/dist/lib/evg/EVGToolbar.rgr +1369 -0
- package/dist/lib/evg/EVGTransition.rgr +728 -0
- package/dist/lib/evg/EVGTreeJson.rgr +721 -0
- package/dist/lib/evg/EVGUnit.rgr +554 -0
- package/dist/lib/evg/EVGViewportUnitTest.rgr +236 -0
- package/dist/lib/evg/EvgApp.rgr +189 -0
- package/dist/lib/evg/EvgBitmapTracer.rgr +4277 -0
- package/dist/lib/evg/EvgBitmapTracerTest.rgr +2119 -0
- package/dist/lib/evg/EvgHost.rgr +451 -0
- package/dist/lib/evg/EvgTest.rgr +91 -0
- package/dist/lib/evg/EvgTraceColor.rgr +236 -0
- package/dist/lib/evg/EvgTraceCurve.rgr +648 -0
- package/dist/lib/evg/EvgTraceFit.rgr +767 -0
- package/dist/lib/evg/EvgTracePath.rgr +435 -0
- package/dist/lib/evg/EvgTraceTypes.rgr +559 -0
- package/dist/lib/evg/EvgViewport.rgr +313 -0
- package/dist/lib/evg/FxDemoDoc.rgr +185 -0
- package/dist/lib/evg/HOSTS.md +227 -0
- package/dist/lib/evg/ISSUES.md +835 -0
- package/dist/lib/evg/PLAN_ACCESSIBILITY.md +650 -0
- package/dist/lib/evg/PLAN_CSS_LAYOUT_AND_FONTS.md +1124 -0
- package/dist/lib/evg/PLAN_EFFECTS.md +431 -0
- package/dist/lib/evg/PLAN_EVG.md +518 -0
- package/dist/lib/evg/PLAN_EVG_RENDERER.md +1674 -0
- package/dist/lib/evg/PLAN_INSPECTOR.md +788 -0
- package/dist/lib/evg/PLAN_LINKS_AND_FORMS.md +157 -0
- package/dist/lib/evg/PLAN_NATIVE_HOSTS.md +671 -0
- package/dist/lib/evg/PLAN_VECTOR_IR.md +664 -0
- package/dist/lib/evg/PLAN_VIEW_TRANSFORM.md +322 -0
- package/dist/lib/evg/PathBuilder.rgr +333 -0
- package/dist/lib/evg/README.md +1236 -0
- package/dist/lib/evg/SPEC.md +1252 -0
- package/dist/lib/evg/SVGPathParser.rgr +1199 -0
- package/dist/lib/evg/SvgParser.rgr +1736 -0
- package/dist/lib/evg/TODO_EVG_RENDERER.md +595 -0
- package/dist/lib/evg/VectorShapes.rgr +331 -0
- package/dist/lib/evg/VectorStroke.rgr +107 -0
- package/dist/lib/evg/VectorViewBox.rgr +380 -0
- package/dist/lib/evg/agent/README.md +290 -0
- package/dist/lib/evg/agent/evg_agent.rgr +899 -0
- package/dist/lib/evg/agent/fixtures/broken.evg.json +7 -0
- package/dist/lib/evg/agent/fixtures/card.evg.json +9 -0
- package/dist/lib/evg/agent/fixtures/connector.css +63 -0
- package/dist/lib/evg/agent/fixtures/connector.evg.json +14 -0
- package/dist/lib/evg/agent/fixtures/drawn.evg.json +129 -0
- package/dist/lib/evg/agent/fixtures/gradient.evg.json +4 -0
- package/dist/lib/evg/agent/fixtures/popover.css +99 -0
- package/dist/lib/evg/agent/fixtures/popover.evg.json +30 -0
- package/dist/lib/evg/agent/roundtrip.sh +74 -0
- package/dist/lib/evg/agent/smoke.sh +230 -0
- package/dist/lib/evg/android/README.md +97 -0
- package/dist/lib/evg/android/androidstubs/AndroidStubs.kt +175 -0
- package/dist/lib/evg/android/androidstubs/Annotation.kt +10 -0
- package/dist/lib/evg/android/androidstubs/App.kt +30 -0
- package/dist/lib/evg/android/androidstubs/Content.kt +41 -0
- package/dist/lib/evg/android/androidstubs/ContentRes.kt +12 -0
- package/dist/lib/evg/android/androidstubs/InputMethod.kt +46 -0
- package/dist/lib/evg/android/androidstubs/Net.kt +6 -0
- package/dist/lib/evg/android/androidstubs/Os.kt +30 -0
- package/dist/lib/evg/android/androidstubs/Util.kt +15 -0
- package/dist/lib/evg/android/androidstubs/View.kt +113 -0
- package/dist/lib/evg/android/androidstubs/Widget.kt +16 -0
- package/dist/lib/evg/android/src/android/kotlin/fi/ranger/evg/AndroidEvgSurface.kt +323 -0
- package/dist/lib/evg/android/src/android/kotlin/fi/ranger/evg/AndroidTextMeasurer.kt +56 -0
- package/dist/lib/evg/android/src/android/kotlin/fi/ranger/evg/RippleEffect.kt +262 -0
- package/dist/lib/evg/android/src/awt/kotlin/fi/ranger/evg/AwtEvgSurface.kt +238 -0
- package/dist/lib/evg/android/src/awt/kotlin/fi/ranger/evg/AwtTextMeasurer.kt +85 -0
- package/dist/lib/evg/android/src/main/kotlin/fi/ranger/evg/EvgEngineThread.kt +115 -0
- package/dist/lib/evg/android/src/main/kotlin/fi/ranger/evg/EvgPainter.kt +231 -0
- package/dist/lib/evg/android/src/main/kotlin/fi/ranger/evg/EvgSurface.kt +117 -0
- package/dist/lib/evg/android/src/main/kotlin/fi/ranger/evg/RecordingSurface.kt +94 -0
- package/dist/lib/evg/apple/README.md +128 -0
- package/dist/lib/evg/apple/Sources/CoreGraphicsEvgSurface.swift +329 -0
- package/dist/lib/evg/apple/Sources/CoreTextMeasurer.swift +70 -0
- package/dist/lib/evg/apple/Sources/EvgEngineQueue.swift +105 -0
- package/dist/lib/evg/apple/Sources/EvgPainter.swift +225 -0
- package/dist/lib/evg/apple/Sources/EvgSurface.swift +163 -0
- package/dist/lib/evg/apple/Sources/RecordingSurface.swift +110 -0
- package/dist/lib/evg/bench/EvgLayoutBench.rgr +34 -0
- package/dist/lib/evg/bench/README.md +64 -0
- package/dist/lib/evg/bench/layout-bench.mjs +316 -0
- package/dist/lib/evg/bench/layout-cases.mjs +510 -0
- package/dist/lib/evg/bench/layout-conformance.mjs +254 -0
- package/dist/lib/evg/bin/.gitignore +17 -0
- package/dist/lib/evg/evg_test.rgr +313 -0
- package/dist/lib/evg/gl/README.md +198 -0
- package/dist/lib/evg/gl/a11y-paint-check.mjs +73 -0
- package/dist/lib/evg/gl/blur-check.mjs +399 -0
- package/dist/lib/evg/gl/boxmodel.json +1 -0
- package/dist/lib/evg/gl/demo.html +46 -0
- package/dist/lib/evg/gl/effect-presets.css +285 -0
- package/dist/lib/evg/gl/effect-presets.js +63 -0
- package/dist/lib/evg/gl/effect-shots.mjs +135 -0
- package/dist/lib/evg/gl/evg-a11y.js +566 -0
- package/dist/lib/evg/gl/evg-binary.js +160 -0
- package/dist/lib/evg/gl/evg-engine.js +296 -0
- package/dist/lib/evg/gl/evg-fx.js +237 -0
- package/dist/lib/evg/gl/evg-gestures.js +209 -0
- package/dist/lib/evg/gl/evg-list.js +167 -0
- package/dist/lib/evg/gl/evg-measure.js +186 -0
- package/dist/lib/evg/gl/evg-textinput.js +303 -0
- package/dist/lib/evg/gl/evg-view.js +144 -0
- package/dist/lib/evg/gl/evg-webgl.js +3942 -0
- package/dist/lib/evg/gl/fx-check.mjs +998 -0
- package/dist/lib/evg/gl/fx-demo.html +166 -0
- package/dist/lib/evg/gl/fx-demo.js +2 -0
- package/dist/lib/evg/gl/fx-serve.mjs +47 -0
- package/dist/lib/evg/gl/gestures-check.mjs +189 -0
- package/dist/lib/evg/gl/list-binary-check.mjs +152 -0
- package/dist/lib/evg/gl/measure-check.mjs +135 -0
- package/dist/lib/evg/gl/rotation-check.mjs +207 -0
- package/dist/lib/evg/gl/shift-check.mjs +113 -0
- package/dist/lib/evg/gl/stroke-check.mjs +168 -0
- package/dist/lib/evg/gl/text-snap-check.mjs +139 -0
- package/dist/lib/evg/gl/view-check.mjs +343 -0
- package/dist/lib/evg/gl/view-policy-check.mjs +184 -0
- package/dist/lib/evg/html/evg-dom.js +318 -0
- package/dist/lib/evg/html/evg-html.js +600 -0
- package/dist/lib/evg/inspect/README.md +415 -0
- package/dist/lib/evg/inspect/browser-smoke.mjs +115 -0
- package/dist/lib/evg/inspect/evg-inspect.js +947 -0
- package/dist/lib/evg/inspect/shots/css.png +0 -0
- package/dist/lib/evg/inspect/shots/dashboard.png +0 -0
- package/dist/lib/evg/inspect/shots/pptx-slide.png +0 -0
- package/dist/lib/evg/inspect/shots/state.png +0 -0
- package/dist/lib/evg/inspect/shots.mjs +226 -0
- package/dist/lib/evg/oracle/css-blur.json +576 -0
- package/dist/lib/evg/oracle/css-box.json +157 -0
- package/dist/lib/evg/oracle/css-timing.json +709 -0
- package/dist/lib/evg/oracle/css_blur_oracle.mjs +529 -0
- package/dist/lib/evg/oracle/css_box_oracle.mjs +106 -0
- package/dist/lib/evg/oracle/css_timing_oracle.mjs +389 -0
- package/dist/lib/evg/original/EVGColor.clj +310 -0
- package/dist/lib/evg/original/EVGColorContext.rgr +178 -0
- package/dist/lib/evg/original/SVGPath.rgr +627 -0
- package/dist/lib/evg/original/Vec2.crgr +103 -0
- package/dist/lib/evg/ranger.json +9 -0
- package/dist/lib/evg/showcase/README.md +246 -0
- package/dist/lib/evg/showcase/assets/emblem.svg +30 -0
- package/dist/lib/evg/showcase/assets/rosette.svg +22 -0
- package/dist/lib/evg/showcase/build.mjs +651 -0
- package/dist/lib/evg/showcase/pages/album.tsx +30 -0
- package/dist/lib/evg/showcase/pages/boxmodel.tsx +37 -0
- package/dist/lib/evg/showcase/pages/cards.tsx +41 -0
- package/dist/lib/evg/showcase/pages/chart_api.tsx +170 -0
- package/dist/lib/evg/showcase/pages/charts.tsx +219 -0
- package/dist/lib/evg/showcase/pages/drawing.tsx +160 -0
- package/dist/lib/evg/showcase/pages/emoji.tsx +91 -0
- package/dist/lib/evg/showcase/pages/flex.tsx +46 -0
- package/dist/lib/evg/showcase/pages/more.tsx +332 -0
- package/dist/lib/evg/showcase/pages/plots.tsx +262 -0
- package/dist/lib/evg/showcase/pages/svg.tsx +63 -0
- package/dist/lib/evg/showcase/pages/tables.tsx +209 -0
- package/dist/lib/evg/showcase/pages/typography.tsx +43 -0
- package/dist/lib/evg/showcase/pages/units.tsx +35 -0
- package/dist/lib/evg/showcase/pages/variants.tsx +233 -0
- package/dist/lib/evg/showcase/pages/vector.tsx +75 -0
- package/dist/lib/evg/showcase/pages/views.tsx +223 -0
- package/dist/lib/evg/showcase/tests/chart_api_smoke.mjs +339 -0
- package/dist/lib/evg/showcase/tests/gl_smoke.mjs +125 -0
- package/dist/lib/evg/showcase/themes/chart_api-default.css +15 -0
- package/dist/lib/evg/showcase/themes/charts-default.css +29 -0
- package/dist/lib/evg/showcase/themes/drawing-default.css +35 -0
- package/dist/lib/evg/showcase/themes/more-default.css +128 -0
- package/dist/lib/evg/showcase/themes/plots-default.css +18 -0
- package/dist/lib/evg/showcase/themes/showcase.css +508 -0
- package/dist/lib/evg/showcase/themes/tables-default.css +123 -0
- package/dist/lib/evg/showcase/themes/variants-default.css +15 -0
- package/dist/lib/evg/showcase/themes/views-default.css +15 -0
- package/dist/lib/evg/tools/bench_vs_potrace.mjs +290 -0
- package/dist/lib/evg/tools/evg_image_tool.rgr +488 -0
- package/dist/lib/evg/tools/evg_trace_bench.rgr +114 -0
- package/dist/lib/evg/tools/evg_trace_cli.rgr +662 -0
- package/dist/lib/evg/tools/evg_trace_cpp_bench.rgr +83 -0
- package/dist/lib/evg/tools/run_trace_bench.sh +74 -0
- package/dist/lib/evg/tools/run_trace_cli_smoke.sh +128 -0
- package/dist/lib/evg/web/responsive/EvgResponsiveCheck.rgr +271 -0
- package/dist/lib/evg/web/responsive/EvgResponsiveDemo.rgr +531 -0
- package/dist/lib/evg/web/responsive/README.md +97 -0
- package/dist/lib/evg/web/responsive/build.mjs +90 -0
- package/dist/lib/evg/web/responsive/dom-check.mjs +169 -0
- package/dist/lib/evg/web/responsive/index.html +177 -0
- package/dist/lib/evg/web/responsive/smoke.mjs +221 -0
- package/dist/lib/evg/web/tools/assets-client.mjs +95 -0
- package/dist/lib/evg/web/tools/boot-bench.mjs +189 -0
- package/dist/lib/evg/web/tools/inline-assets.mjs +119 -0
- package/dist/lib/evg/web/tools/minify.mjs +60 -0
- package/dist/lib/evg/web/tracer/build.mjs +87 -0
- package/dist/lib/evg/web/tracer/index.html +2805 -0
- package/dist/lib/evg/web/tracer/sample.png +0 -0
- package/dist/lib/evg/web/tracer/smoke.mjs +1162 -0
- package/dist/lib/evgr/Cargo.lock +21 -0
- package/dist/lib/evgr/Cargo.toml +19 -0
- package/dist/lib/evgr/README.md +140 -0
- package/dist/lib/evgr/bench/NativeBench.rgr +246 -0
- package/dist/lib/evgr/bench/compare.mjs +252 -0
- package/dist/lib/evgr/bench/speed.mjs +277 -0
- package/dist/lib/evgr/bin/.gitignore +3 -0
- package/dist/lib/evgr/src/bin/bench.rs +106 -0
- package/dist/lib/evgr/src/bin/smoke.rs +13 -0
- package/dist/lib/evgr/src/grid.rs +730 -0
- package/dist/lib/evgr/src/lib.rs +1003 -0
- package/dist/lib/evgr/src/style.rs +468 -0
- package/dist/lib/evgr/src/text.rs +83 -0
- package/dist/lib/image/BitReader.rgr +171 -0
- package/dist/lib/image/Buffer.rgr +173 -0
- package/dist/lib/image/DCT.rgr +283 -0
- package/dist/lib/image/Deflate.rgr +339 -0
- package/dist/lib/image/HuffmanDecoder.rgr +181 -0
- package/dist/lib/image/ImageBuffer.rgr +706 -0
- package/dist/lib/image/JPEGDecoder.rgr +808 -0
- package/dist/lib/image/PNGDecoder.rgr +544 -0
- package/dist/lib/image/PNGEncoder.rgr +388 -0
- package/dist/lib/image/PPMImage.rgr +224 -0
- package/dist/lib/image/ProgressiveJPEGDecoder.rgr +1154 -0
- package/dist/lib/image/README.md +33 -0
- package/dist/lib/image/RasterBuffer.rgr +294 -0
- package/dist/lib/image/VP8BoolDecoder.rgr +163 -0
- package/dist/lib/image/WebPDecoder.rgr +433 -0
- package/dist/lib/image/WebPLossless.rgr +1035 -0
- package/dist/lib/image/WebPLossy.rgr +1929 -0
- package/dist/lib/image/ranger.json +9 -0
- package/dist/lib/image/testdata/webp/anim_2f_24x16.webp +0 -0
- package/dist/lib/image/testdata/webp/fixtures.txt +33 -0
- package/dist/lib/image/testdata/webp/gen_fixtures.py +261 -0
- package/dist/lib/image/testdata/webp/ll_1x1.webp +0 -0
- package/dist/lib/image/testdata/webp/ll_alpha_29x21.webp +0 -0
- package/dist/lib/image/testdata/webp/ll_gradient_64x64.webp +0 -0
- package/dist/lib/image/testdata/webp/ll_meta_32x32.webp +0 -0
- package/dist/lib/image/testdata/webp/ll_noise_17x9.webp +0 -0
- package/dist/lib/image/testdata/webp/ll_pal11_23x11.webp +0 -0
- package/dist/lib/image/testdata/webp/ll_pal2_19x7.webp +0 -0
- package/dist/lib/image/testdata/webp/ll_pal40_16x16.webp +0 -0
- package/dist/lib/image/testdata/webp/ll_pal4_13x5.webp +0 -0
- package/dist/lib/image/testdata/webp/ll_photo_40x30.webp +0 -0
- package/dist/lib/image/testdata/webp/ll_predictors_64x32.webp +0 -0
- package/dist/lib/image/testdata/webp/ly_1x1.webp +0 -0
- package/dist/lib/image/testdata/webp/ly_alph_raw_f0_21x13.webp +0 -0
- package/dist/lib/image/testdata/webp/ly_alph_raw_f1_21x13.webp +0 -0
- package/dist/lib/image/testdata/webp/ly_alph_raw_f2_21x13.webp +0 -0
- package/dist/lib/image/testdata/webp/ly_alph_raw_f3_21x13.webp +0 -0
- package/dist/lib/image/testdata/webp/ly_alph_vp8l_f0_21x13.webp +0 -0
- package/dist/lib/image/testdata/webp/ly_alph_vp8l_f1_21x13.webp +0 -0
- package/dist/lib/image/testdata/webp/ly_alph_vp8l_f2_21x13.webp +0 -0
- package/dist/lib/image/testdata/webp/ly_alph_vp8l_f3_21x13.webp +0 -0
- package/dist/lib/image/testdata/webp/ly_alpha_33x17.webp +0 -0
- package/dist/lib/image/testdata/webp/ly_exif_15x11.webp +0 -0
- package/dist/lib/image/testdata/webp/ly_i16_96x80.webp +0 -0
- package/dist/lib/image/testdata/webp/ly_nofilter_24x20.webp +0 -0
- package/dist/lib/image/testdata/webp/ly_odd_17x9.webp +0 -0
- package/dist/lib/image/testdata/webp/ly_photo_48x40.webp +0 -0
- package/dist/lib/image/testdata/webp/ly_q100_20x18.webp +0 -0
- package/dist/lib/image/testdata/webp/ly_sharp_40x36.webp +0 -0
- package/dist/lib/image/testdata/webp/ly_simple_filter_33x31.webp +0 -0
- package/dist/lib/image/testdata/webp/ly_vpx_parts8_37x45.webp +0 -0
- package/dist/lib/image/testdata/webp/ly_vpx_skip_96x80.webp +0 -0
- package/dist/lib/image/tests/WebPDecodeTool.rgr +81 -0
- package/dist/lib/image/tests/WebPDecoderTest.rgr +1162 -0
- package/dist/lib/image/tests/run_webp_tests.sh +39 -0
- package/dist/lib/rust/RsJson.rgr +468 -0
- package/dist/lib/rust/RsPrelude.rgr +1663 -0
- package/dist/lib/shell_test.rgr +7 -7
- package/dist/lib/stdlib.rgr +21 -5
- package/dist/lib/zip/ranger.json +6 -0
- package/dist/rgrc.js +87318 -67610
- package/package.json +1010 -1040
- package/dist/README.md +0 -117
- package/dist/package.json +0 -47
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
# EVG on the GPU
|
|
2
|
+
|
|
3
|
+
Running the CSS palette — flex, grid, the stylesheet layer, real TTF metrics
|
|
4
|
+
and kerning — on a GPU, without a browser doing the layout.
|
|
5
|
+
|
|
6
|
+
```
|
|
7
|
+
npm run evg:displaylist -- page.tsx out.json -css sheet.css -theme editorial
|
|
8
|
+
```
|
|
9
|
+
|
|
10
|
+
## The seam
|
|
11
|
+
|
|
12
|
+
`EVGDisplayList` walks a laid-out tree once and emits flat draw commands:
|
|
13
|
+
filled rect, border, image quad, text run, push/pop clip. Absolute pixels,
|
|
14
|
+
colours resolved to 0–255 + alpha, no tree and no units left.
|
|
15
|
+
|
|
16
|
+
```
|
|
17
|
+
JSX + CSS ──► EVG layout ──► display list ──┬─► WebGL 2 (evg-webgl.js)
|
|
18
|
+
├─► SVG / DOM (../html/evg-html.js)
|
|
19
|
+
├─► SDL2 + GL (C++ target)
|
|
20
|
+
├─► PDF, PNG, HTML (existing)
|
|
21
|
+
└─► framebuffer (v2 game UI)
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
The SVG backend is the second consumer of this seam and the evidence that the
|
|
25
|
+
seam is one: it is 500 lines, it shares no code with this file, and the two are
|
|
26
|
+
differenced pixel for pixel over the same frames — 0.022% on a sheet built to
|
|
27
|
+
exercise every command kind, 0.000% on every slide of the .pptx deck. See
|
|
28
|
+
[`gallery/pptx/web/html/`](../../../gallery/pptx/web/html/README.md), which is the slide
|
|
29
|
+
editor with this painter swapped out for that one and nothing else changed.
|
|
30
|
+
|
|
31
|
+
Everything above the seam is the code the PDF is made with. Everything below
|
|
32
|
+
knows only about quads, glyph runs and scissor rectangles.
|
|
33
|
+
|
|
34
|
+
This matters for more than tidiness: five painters already walk the tree
|
|
35
|
+
themselves, and each decides again what a box means. That is how border-radius
|
|
36
|
+
came to work in PDF and silently not in PNG — one painter read
|
|
37
|
+
`box.borderRadius`, another a stale `el.borderRadius` that nothing wrote. A
|
|
38
|
+
GPU backend should not become a sixth copy of that walk.
|
|
39
|
+
|
|
40
|
+
## WebGL 2 — working
|
|
41
|
+
|
|
42
|
+
```
|
|
43
|
+
python3 -m http.server 8000 --directory <repo root>
|
|
44
|
+
# open /lib/evg/gl/demo.html?list=boxmodel.json
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
One quad per command, drawn instanced. Rounded corners and borders come from a
|
|
48
|
+
signed distance function in the fragment shader, so a rounded rect costs the
|
|
49
|
+
same two triangles as a square one and needs no tessellation:
|
|
50
|
+
|
|
51
|
+
```glsl
|
|
52
|
+
sdRoundedBox(p, b, r) = length(max(|p| - b + r, 0)) + min(max(qx, qy), 0) - r
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
Antialiasing is one `fwidth()` smoothstep. It is **clamped** — on a very large
|
|
56
|
+
quad the derivative jumps across the triangle strip's diagonal and drew a
|
|
57
|
+
visible hairline across the page.
|
|
58
|
+
|
|
59
|
+
Text: the list carries the run, the face and the size, and the position is
|
|
60
|
+
EVG's — measured from the same TTF the PDF is set in, kerning included. Only
|
|
61
|
+
the glyph *images* come from the platform. Here a 2D canvas rasterizes each run
|
|
62
|
+
into an atlas, one slot per run rather than per glyph, which keeps the run
|
|
63
|
+
intact and therefore keeps EVG's kerning exactly.
|
|
64
|
+
|
|
65
|
+
## Images
|
|
66
|
+
|
|
67
|
+
`object-fit: cover` is the only fit the raster and PDF targets implement, and
|
|
68
|
+
it is done here the same way they do it — by cropping, not by squashing. The
|
|
69
|
+
crop is a UV rectangle computed from the source and box aspect ratios, so the
|
|
70
|
+
GPU samples only the covered region and the quad stays two triangles. A radius
|
|
71
|
+
on the element clips the photo through the same distance field the rectangles
|
|
72
|
+
use.
|
|
73
|
+
|
|
74
|
+
A photo needs its own texture bound, which a single instanced draw cannot do,
|
|
75
|
+
so the list is split into **runs**: consecutive quads that share the atlas are
|
|
76
|
+
one draw, and an image breaks the run and is drawn on its own. Runs read the
|
|
77
|
+
same instance buffers offset to their first instance, because WebGL 2 has no
|
|
78
|
+
base-instance parameter. That keeps paint order exactly as the list has it —
|
|
79
|
+
`boxmodel` is one run, `album` is six.
|
|
80
|
+
|
|
81
|
+
The published gallery copies the referenced photos next to the viewer and
|
|
82
|
+
rewrites the list to point at the copies. Without that the `src` was the path
|
|
83
|
+
the author wrote — relative to the page source — which resolves in the
|
|
84
|
+
repository and nowhere else, so the deployed viewer fetched four 404s and drew
|
|
85
|
+
a page with holes in it. The PDF and PNG targets never hit this: they read the
|
|
86
|
+
file at render time and embed the pixels.
|
|
87
|
+
|
|
88
|
+
Verified against `showcase/pages/boxmodel.tsx`: 20 commands, 15 rects with
|
|
89
|
+
their radii, one border, 4 text runs, rendering to the same geometry as the
|
|
90
|
+
PDF — and against `album`, whose four photos now draw.
|
|
91
|
+
|
|
92
|
+
Not done yet: a real clip stack. A single scissor rect is straightforward;
|
|
93
|
+
nested clips need a stack. It is marked in the source.
|
|
94
|
+
|
|
95
|
+
## SDL2 + OpenGL — the portable half is ready
|
|
96
|
+
|
|
97
|
+
The display list module compiles to **C++, Rust and Go** as well as ES6, which
|
|
98
|
+
is the whole reason this shape was chosen — the same list can drive WebGL in a
|
|
99
|
+
browser and SDL2 on a Raspberry Pi or a Mac.
|
|
100
|
+
|
|
101
|
+
What is already in the repo:
|
|
102
|
+
|
|
103
|
+
- `EVGDisplayList` compiles cleanly to the C++ target (checked).
|
|
104
|
+
- `ranger_games/sprite_char_sdl.rgr` is an existing Ranger → C++ → SDL2
|
|
105
|
+
front-end with a real window and gamepad, so the platform pattern is
|
|
106
|
+
established rather than hypothetical.
|
|
107
|
+
- `imaging/raster/RasterText` already rasterizes glyph outlines — including
|
|
108
|
+
composite glyphs, so `ä`/`ö` work — which is the atlas source on a platform
|
|
109
|
+
with no canvas.
|
|
110
|
+
|
|
111
|
+
What is missing is the platform shim: create an SDL2 window with a GL context,
|
|
112
|
+
upload the same vertex data, and use the same shader source. The shaders are
|
|
113
|
+
plain GLSL ES 3.00 and need no change for desktop GL 3.3 beyond the `#version`
|
|
114
|
+
line.
|
|
115
|
+
|
|
116
|
+
The honest gap: none of that is compiled or run here, because this container
|
|
117
|
+
has neither SDL2 nor a GPU. Treat the C++ compile as evidence that the
|
|
118
|
+
portable half holds, not that the app runs.
|
|
119
|
+
|
|
120
|
+
## The engine in a Worker
|
|
121
|
+
|
|
122
|
+
Everything above the seam is pure Ranger, so it can run in a Worker; only the
|
|
123
|
+
painter needs the page. [`evg-engine.js`](evg-engine.js) is the harness —
|
|
124
|
+
`serveEngine` in the worker, `connectEngine` on the page — and what crosses
|
|
125
|
+
back per frame is one of three replies: a new build as `toBinary()`'s typed
|
|
126
|
+
arrays, transferred, and read by [`evg-binary.js`](evg-binary.js) into the
|
|
127
|
+
same commands `evg-list.js` reads off the object; a kept list that only moved,
|
|
128
|
+
as the layers' shifts; or nothing. Input crosses the other way as the calls
|
|
129
|
+
the host already made, batched into the frame request, and the hit test runs
|
|
130
|
+
where the tree is. `gallery/realtrainer/web/main-worker.js` is a host written
|
|
131
|
+
that way; `?engine=worker` on the RealTrainer page runs it, and `rt:frame`
|
|
132
|
+
drives both hosts through one check. `npm run evg:binary:check` holds the two
|
|
133
|
+
readers against each other on real frames. PLAN_NATIVE_HOSTS.md S1.
|
|
134
|
+
|
|
135
|
+
## Moving the view — [`evg-gestures.js`](evg-gestures.js)
|
|
136
|
+
|
|
137
|
+
> The gestures move a view; the *view* itself is still baked into the
|
|
138
|
+
> coordinates of every command, so a canvas rebuilds its whole list for every
|
|
139
|
+
> frame of a pan. It does not have to:
|
|
140
|
+
> [`../PLAN_VIEW_TRANSFORM.md`](../PLAN_VIEW_TRANSFORM.md) designs the camera
|
|
141
|
+
> as a uniform beside `uShift`, measured on the canvases in this repository.
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
Every page that draws a scene bigger than its canvas needs the same four
|
|
145
|
+
gestures, and every standalone had written its own: drag to pan, wheel to
|
|
146
|
+
zoom, a press that does not travel is a click — and, missing from all of
|
|
147
|
+
them, two fingers to pinch.
|
|
148
|
+
|
|
149
|
+
```js
|
|
150
|
+
attachViewGestures(canvas, {
|
|
151
|
+
view: () => ({ x, y, sc }), // the pan and scale in force
|
|
152
|
+
setView: (x, y, sc) => { … }, // schedule this one — it does not paint
|
|
153
|
+
onTap: (clientX, clientY) => { … },
|
|
154
|
+
});
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
It never touches the scene: it reads the view the host keeps and hands back
|
|
158
|
+
another, so the host goes on deciding what a view is and when to paint one.
|
|
159
|
+
|
|
160
|
+
The anchor is what makes a zoom feel like a zoom — the point under the
|
|
161
|
+
cursor, or under the midpoint of two fingers, stays where it is. Keeping the
|
|
162
|
+
scene point `(screen - pan) / scale` where it was gives
|
|
163
|
+
`pan' = screen - (screen - pan) * (scale' / scale)`, which is also what moves
|
|
164
|
+
the picture when the fingers travel: a pinch pans and zooms in one expression
|
|
165
|
+
rather than two that fight.
|
|
166
|
+
|
|
167
|
+
A trackpad pinch is not a touch — browsers report it as a wheel with `ctrl`
|
|
168
|
+
held, a fraction of a notch at a time, so it gets its own rate or it crawls
|
|
169
|
+
where a wheel flies; Safari's own `gesture*` events are read too. A finger
|
|
170
|
+
lifted out of a pinch leaves the other one panning rather than stopping the
|
|
171
|
+
picture until both are lifted. `npm run evg:gestures:check` drives all of it
|
|
172
|
+
against a canvas that is not one — 19 checks, including the pinch no headless
|
|
173
|
+
driver will send.
|
|
174
|
+
|
|
175
|
+
## Nobody can read it — so there is a second list
|
|
176
|
+
|
|
177
|
+
A GPU frame is invisible to a screen reader: NVDA asks the platform for a tree
|
|
178
|
+
of roles and names, and this file produces quads. So the app emits a second list
|
|
179
|
+
beside the display list — `EVGA11yTree`, meaning where this one carries geometry
|
|
180
|
+
— and `evg-a11y.js` mirrors it as real DOM over the canvas, positioned at the
|
|
181
|
+
rectangles that were painted.
|
|
182
|
+
|
|
183
|
+
```
|
|
184
|
+
GridApp ─┬─ sceneJson() ─► evg-webgl.js ─► pixels
|
|
185
|
+
└─ a11yJson() ─► evg-a11y.js ─► DOM ─► VoiceOver / NVDA / Orca
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
The canvas becomes `aria-hidden`. The sheet is a `role="grid"` that claims all
|
|
189
|
+
ten thousand rows and emits the forty on screen; the caret cell is the one tab
|
|
190
|
+
stop in the application; a dialog is modal and hides the sheet behind it. The
|
|
191
|
+
mirror never tells the app where focus is — the app tells the mirror, or the two
|
|
192
|
+
chase each other.
|
|
193
|
+
|
|
194
|
+
Checked in a real browser by `npm run datagrid:web:test`, which asks the DOM
|
|
195
|
+
what a reader would be handed. Turn it off with `?a11y=0`.
|
|
196
|
+
|
|
197
|
+
Design, state and how to try it with VoiceOver:
|
|
198
|
+
[../PLAN_ACCESSIBILITY.md](../PLAN_ACCESSIBILITY.md).
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// SPDX-License-Identifier: MIT
|
|
3
|
+
//
|
|
4
|
+
// The accessibility mirror may not paint.
|
|
5
|
+
//
|
|
6
|
+
// node lib/evg/gl/a11y-paint-check.mjs
|
|
7
|
+
//
|
|
8
|
+
// The mirror is real DOM over the canvas — that is the whole point of it, and
|
|
9
|
+
// it is why a reader can find a button EVG drew. The canvas underneath has
|
|
10
|
+
// already drawn every one of those nodes, so a mirror node that puts one
|
|
11
|
+
// pixel on the screen is a SECOND COPY OF THE PAGE. And because the mirror is
|
|
12
|
+
// rebuilt on its own schedule rather than once a frame, that copy is at an
|
|
13
|
+
// older scroll offset: two pages superimposed, sliding over each other.
|
|
14
|
+
//
|
|
15
|
+
// `color: transparent` reads as enough and is not. The mirror makes NATIVE
|
|
16
|
+
// controls on purpose — a real <button> and a real <input>, because that is
|
|
17
|
+
// what readers understand and what the text bridge edits in — and a native
|
|
18
|
+
// control on iOS paints through `-webkit-text-fill-color` and
|
|
19
|
+
// `-webkit-appearance`, neither of which `color` reaches.
|
|
20
|
+
//
|
|
21
|
+
// So this is a list, and the list is the point: every way a user agent can
|
|
22
|
+
// decide to draw something has to be turned off by name. It runs without a
|
|
23
|
+
// DOM — `styleBase` only ever writes to `el.style`, so a plain object is a
|
|
24
|
+
// good enough element for the question being asked.
|
|
25
|
+
|
|
26
|
+
import { styleBase } from "./evg-a11y.js";
|
|
27
|
+
|
|
28
|
+
const MUST_BE = {
|
|
29
|
+
background: "transparent",
|
|
30
|
+
color: "transparent",
|
|
31
|
+
webkitTextFillColor: "transparent",
|
|
32
|
+
webkitAppearance: "none",
|
|
33
|
+
appearance: "none",
|
|
34
|
+
caretColor: "transparent",
|
|
35
|
+
textShadow: "none",
|
|
36
|
+
boxShadow: "none",
|
|
37
|
+
border: "0",
|
|
38
|
+
outline: "none",
|
|
39
|
+
overflow: "hidden",
|
|
40
|
+
pointerEvents: "none",
|
|
41
|
+
userSelect: "none",
|
|
42
|
+
// NOT A PAINTING RULE, and here anyway because it lives in the same style
|
|
43
|
+
// block and is the same kind of invisible-until-a-phone mistake: iOS Safari
|
|
44
|
+
// zooms the whole page in when it focuses a text field computing to under
|
|
45
|
+
// 16px, and the mirror's `<input>` is what a tap on a drawn field focuses.
|
|
46
|
+
// It was 10px, which is small enough to trigger it and invisible enough
|
|
47
|
+
// that nothing on a desktop ever said so.
|
|
48
|
+
fontSize: "16px",
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
const el = { style: {} };
|
|
52
|
+
styleBase(el);
|
|
53
|
+
|
|
54
|
+
let failed = 0;
|
|
55
|
+
for (const [prop, want] of Object.entries(MUST_BE)) {
|
|
56
|
+
const got = el.style[prop];
|
|
57
|
+
if (got === want) console.log(" PASS " + prop + ": " + want);
|
|
58
|
+
else {
|
|
59
|
+
failed += 1;
|
|
60
|
+
console.log(" FAIL " + prop + " is " + JSON.stringify(got) + ", want " + JSON.stringify(want));
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
console.log("");
|
|
65
|
+
if (failed > 0) {
|
|
66
|
+
console.log(` ${failed} way(s) the mirror could still paint, or zoom a phone`);
|
|
67
|
+
process.exit(1);
|
|
68
|
+
}
|
|
69
|
+
console.log(` ${Object.keys(MUST_BE).length} ways it cannot paint, or make a phone zoom`);
|
|
70
|
+
// The marker `scripts/run-gallery-editor-tests.sh` greps for. The compiler
|
|
71
|
+
// prints `[FAIL]` and still exits 0, so that runner refuses to take a zero
|
|
72
|
+
// exit as a pass — a suite has to SAY it passed.
|
|
73
|
+
console.log("ALL PASS");
|
|
@@ -0,0 +1,399 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The painter's backdrop blur, checked in a real GPU context against the
|
|
3
|
+
* browser's own numbers.
|
|
4
|
+
*
|
|
5
|
+
* node lib/evg/gl/blur-check.mjs [--write-png out.png]
|
|
6
|
+
*
|
|
7
|
+
* `oracle/css_blur_oracle.mjs` measured what Chrome does with
|
|
8
|
+
* `backdrop-filter: blur()`. This draws the same picture through the WebGL
|
|
9
|
+
* painter and reads the same pixels back, so the two can be compared where it
|
|
10
|
+
* counts — in the framebuffer, not in a comment.
|
|
11
|
+
*
|
|
12
|
+
* Three claims, and each one fails differently:
|
|
13
|
+
*
|
|
14
|
+
* THE PROFILE. A hard black/white edge behind a blurred pane. The ramp
|
|
15
|
+
* across it is the kernel's step response, and it is compared sample by
|
|
16
|
+
* sample against what the browser produced. A true Gaussian instead of the
|
|
17
|
+
* three-box approximation is out by up to 14 luminance levels here — small
|
|
18
|
+
* enough to look fine and large enough to be the wrong filter.
|
|
19
|
+
*
|
|
20
|
+
* THE EDGE. A flat grey behind the pane must come out flat: same value at
|
|
21
|
+
* the pane's border as at its centre. An implementation that blurs only the
|
|
22
|
+
* pixels inside the box gets a pale rim, because its samples fall off the
|
|
23
|
+
* edge of what it copied. This is the case that catches the mistake nobody
|
|
24
|
+
* notices until they put a scrim over a photograph.
|
|
25
|
+
*
|
|
26
|
+
* THE CLIP. Outside a rounded corner, the backdrop is untouched.
|
|
27
|
+
*
|
|
28
|
+
* Exit code 0 when every probe is within tolerance.
|
|
29
|
+
*/
|
|
30
|
+
|
|
31
|
+
import fs from "node:fs";
|
|
32
|
+
import path from "node:path";
|
|
33
|
+
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
34
|
+
import { requireDom, findChromium, assertDomInstalled } from "../../../gallery/ui/conformance/dom-adapter.mjs";
|
|
35
|
+
|
|
36
|
+
const HERE = path.dirname(fileURLToPath(import.meta.url));
|
|
37
|
+
const ORACLE = JSON.parse(fs.readFileSync(path.join(HERE, "..", "oracle", "css-blur.json"), "utf8"));
|
|
38
|
+
|
|
39
|
+
const W = 400;
|
|
40
|
+
const H = 200;
|
|
41
|
+
// The same geometry the oracle used: a 400x200 backdrop, a 200x120 pane at
|
|
42
|
+
// (100,40). Same numbers on both sides or the comparison is not one.
|
|
43
|
+
const PANE = { x: 100, y: 40, w: 200, h: 120 };
|
|
44
|
+
|
|
45
|
+
/** The three scenes, matching the oracle's cases. */
|
|
46
|
+
const SCENES = {
|
|
47
|
+
// Black left of x=200, white right of it, under a pane that blurs and
|
|
48
|
+
// paints nothing of its own.
|
|
49
|
+
edge: (blur) => ({
|
|
50
|
+
width: W, height: H,
|
|
51
|
+
cmds: [
|
|
52
|
+
{ k: 0, x: 0, y: 0, w: 200, h: H, c: [0, 0, 0, 1] },
|
|
53
|
+
{ k: 0, x: 200, y: 0, w: 200, h: H, c: [255, 255, 255, 1] },
|
|
54
|
+
{ k: 0, ...PANE, c: [0, 0, 0, 0], bb: blur },
|
|
55
|
+
],
|
|
56
|
+
}),
|
|
57
|
+
// Flat mid grey everywhere behind.
|
|
58
|
+
uniform: (blur) => ({
|
|
59
|
+
width: W, height: H,
|
|
60
|
+
cmds: [
|
|
61
|
+
{ k: 0, x: 0, y: 0, w: W, h: H, c: [128, 128, 128, 1] },
|
|
62
|
+
{ k: 0, ...PANE, c: [0, 0, 0, 0], bb: blur },
|
|
63
|
+
],
|
|
64
|
+
}),
|
|
65
|
+
// White page, a black stripe starting EXACTLY at the pane's left edge. This
|
|
66
|
+
// is the case that decides whether the backdrop is the element's own region
|
|
67
|
+
// or the page around it, and cases that do not straddle the border cannot
|
|
68
|
+
// tell — which is how this implementation shipped the wrong rule for an
|
|
69
|
+
// afternoon and passed every check it had.
|
|
70
|
+
acrossTheEdge: (blur) => ({
|
|
71
|
+
width: W, height: H,
|
|
72
|
+
cmds: [
|
|
73
|
+
{ k: 0, x: 0, y: 0, w: W, h: H, c: [255, 255, 255, 1] },
|
|
74
|
+
// From x=99, one pixel left of the pane, so this rectangle's own
|
|
75
|
+
// antialiased edge falls OUTSIDE the region the blur samples. EVG
|
|
76
|
+
// softens a rectangle's border by about a third of a pixel and a
|
|
77
|
+
// browser's div does not; with the two edges on top of each other that
|
|
78
|
+
// seam blurs inward and reads as a bleed this is meant to be measuring.
|
|
79
|
+
{ k: 0, x: 99, y: 0, w: 101, h: H, c: [0, 0, 0, 1] },
|
|
80
|
+
{ k: 0, ...PANE, r: 40, c: [0, 0, 0, 0], bb: blur },
|
|
81
|
+
],
|
|
82
|
+
}),
|
|
83
|
+
// A hard band across the top of a rounded pane, over white. This is the only
|
|
84
|
+
// scene where the blur ITSELF can be caught being drawn square: everywhere
|
|
85
|
+
// else the backdrop near the corner is uniform, and blurred uniform is
|
|
86
|
+
// uniform, so a square clip and a rounded one are the same picture.
|
|
87
|
+
banded: (blur) => ({
|
|
88
|
+
width: W, height: H,
|
|
89
|
+
cmds: [
|
|
90
|
+
{ k: 0, x: 0, y: 0, w: W, h: H, c: [255, 255, 255, 1] },
|
|
91
|
+
{ k: 0, x: 0, y: 39, w: W, h: 31, c: [0, 0, 0, 1] },
|
|
92
|
+
{ k: 0, ...PANE, r: 40, c: [0, 0, 0, 0], bb: blur },
|
|
93
|
+
],
|
|
94
|
+
}),
|
|
95
|
+
// A narrow black column down the pane's left side. The banded scene cannot
|
|
96
|
+
// see a blur drawn square, because near the top-left corner everything in it
|
|
97
|
+
// is black either way; turning the feature ninety degrees means the raw
|
|
98
|
+
// backdrop outside the corner is black while the blurred one is nearly
|
|
99
|
+
// white, and the two cannot be confused.
|
|
100
|
+
verticalColumn: (blur) => ({
|
|
101
|
+
width: W, height: H,
|
|
102
|
+
cmds: [
|
|
103
|
+
{ k: 0, x: 0, y: 0, w: W, h: H, c: [255, 255, 255, 1] },
|
|
104
|
+
{ k: 0, x: 95, y: 0, w: 14, h: H, c: [0, 0, 0, 1] },
|
|
105
|
+
{ k: 0, ...PANE, r: 40, c: [0, 0, 0, 0], bb: blur },
|
|
106
|
+
],
|
|
107
|
+
}),
|
|
108
|
+
// The same column, moved fifty pixels clear of the nearest border. Two edges
|
|
109
|
+
// close together is the case a kernel is easiest to get wrong on, and with
|
|
110
|
+
// no border within reach it is the kernel alone being compared.
|
|
111
|
+
innerColumn: (blur) => ({
|
|
112
|
+
width: W, height: H,
|
|
113
|
+
cmds: [
|
|
114
|
+
{ k: 0, x: 0, y: 0, w: W, h: H, c: [255, 255, 255, 1] },
|
|
115
|
+
{ k: 0, x: 150, y: 0, w: 14, h: H, c: [0, 0, 0, 1] },
|
|
116
|
+
{ k: 0, ...PANE, c: [0, 0, 0, 0], bb: blur },
|
|
117
|
+
],
|
|
118
|
+
}),
|
|
119
|
+
// The same small pane, alone and after a bigger one — the pair for case 6.
|
|
120
|
+
//
|
|
121
|
+
// The painter keeps its blur targets between draws and grows them without
|
|
122
|
+
// shrinking, so a second, smaller pane reads out of a texture larger than
|
|
123
|
+
// its own region, with whatever the first pane left in the rest of it right
|
|
124
|
+
// there to be sampled. Every other scene here renders one pane into a fresh
|
|
125
|
+
// context and cannot see that.
|
|
126
|
+
//
|
|
127
|
+
// The two panes must NOT overlap. The first version put a big pane over the
|
|
128
|
+
// same band and compared the result with the small pane alone, and the
|
|
129
|
+
// difference it found — 12 against 189 — was not a bug at all: the big pane
|
|
130
|
+
// had already softened that backdrop, so the small one was blurring an
|
|
131
|
+
// already-blurred band. Correct behaviour, wrong reference.
|
|
132
|
+
smallAlone: (blur) => ({
|
|
133
|
+
width: W, height: H,
|
|
134
|
+
cmds: [
|
|
135
|
+
{ k: 0, x: 0, y: 0, w: W, h: H, c: [255, 255, 255, 1] },
|
|
136
|
+
{ k: 0, x: 0, y: 39, w: W, h: 31, c: [0, 0, 0, 1] },
|
|
137
|
+
{ k: 0, x: 200, y: 30, w: 150, h: 120, c: [0, 0, 0, 0], bb: blur },
|
|
138
|
+
],
|
|
139
|
+
}),
|
|
140
|
+
smallAfterBig: (blur) => ({
|
|
141
|
+
width: W, height: H,
|
|
142
|
+
cmds: [
|
|
143
|
+
{ k: 0, x: 0, y: 0, w: W, h: H, c: [255, 255, 255, 1] },
|
|
144
|
+
{ k: 0, x: 0, y: 39, w: W, h: 31, c: [0, 0, 0, 1] },
|
|
145
|
+
// Its own dark patch, well clear of the small pane, so anything left
|
|
146
|
+
// behind in the reused texture is distinctive rather than the same white
|
|
147
|
+
// the small pane would see anyway.
|
|
148
|
+
{ k: 0, x: 10, y: 100, w: 170, h: 80, c: [20, 20, 20, 1] },
|
|
149
|
+
// 180 wide by 90 tall: wider than the small pane and shorter, so the
|
|
150
|
+
// shared texture ends up bigger than the small pane's region on one axis
|
|
151
|
+
// and the leftovers are inside it.
|
|
152
|
+
{ k: 0, x: 5, y: 95, w: 180, h: 90, c: [0, 0, 0, 0], bb: blur },
|
|
153
|
+
{ k: 0, x: 200, y: 30, w: 150, h: 120, c: [0, 0, 0, 0], bb: blur },
|
|
154
|
+
],
|
|
155
|
+
}),
|
|
156
|
+
// A rounded pane with a translucent white fill over black.
|
|
157
|
+
rounded: (blur) => ({
|
|
158
|
+
width: W, height: H,
|
|
159
|
+
cmds: [
|
|
160
|
+
{ k: 0, x: 0, y: 0, w: W, h: H, c: [0, 0, 0, 1] },
|
|
161
|
+
{ k: 0, ...PANE, r: 40, c: [255, 255, 255, 0.85], bb: blur },
|
|
162
|
+
],
|
|
163
|
+
}),
|
|
164
|
+
};
|
|
165
|
+
|
|
166
|
+
const painter = fs.readFileSync(path.join(HERE, "evg-webgl.js"), "utf8").replace(/^export /gm, "");
|
|
167
|
+
|
|
168
|
+
const pageFor = (scene) => `<!doctype html><meta charset="utf-8"><body style="margin:0;background:#fff">
|
|
169
|
+
<canvas id="c" width="${W}" height="${H}" style="width:${W}px;height:${H}px"></canvas>
|
|
170
|
+
<script>
|
|
171
|
+
${painter}
|
|
172
|
+
(async () => {
|
|
173
|
+
try {
|
|
174
|
+
const c = document.getElementById("c");
|
|
175
|
+
const gl = c.getContext("webgl2", { antialias: true, preserveDrawingBuffer: true });
|
|
176
|
+
const doc = ${JSON.stringify(scene)};
|
|
177
|
+
window.__stats = renderDisplayList(gl, { list: { cmds: doc.cmds }, width: doc.width, height: doc.height }, { dpr: 1 });
|
|
178
|
+
} catch (e) { window.__ERR__ = String(e && e.stack || e); }
|
|
179
|
+
window.__DONE__ = true;
|
|
180
|
+
})();
|
|
181
|
+
</script></body>`;
|
|
182
|
+
|
|
183
|
+
assertDomInstalled();
|
|
184
|
+
const { chromium } = requireDom("playwright-core");
|
|
185
|
+
// SwiftShader, for the same reason the rotation check uses it: a check that
|
|
186
|
+
// only runs on a machine with a GPU is not a check.
|
|
187
|
+
const browser = await chromium.launch({
|
|
188
|
+
executablePath: findChromium(),
|
|
189
|
+
args: ["--use-gl=swiftshader", "--enable-unsafe-swiftshader"],
|
|
190
|
+
});
|
|
191
|
+
const page = await browser.newPage({ viewport: { width: W + 40, height: H + 40 } });
|
|
192
|
+
const pageErrors = [];
|
|
193
|
+
page.on("pageerror", (e) => pageErrors.push(String(e.message)));
|
|
194
|
+
|
|
195
|
+
const tmp = path.join(HERE, ".blur-check.html");
|
|
196
|
+
|
|
197
|
+
/** Render one scene and hand back a row of RGBA readings. */
|
|
198
|
+
async function rowOf(scene, y, xs) {
|
|
199
|
+
fs.writeFileSync(tmp, pageFor(scene));
|
|
200
|
+
await page.goto(pathToFileURL(tmp).href);
|
|
201
|
+
await page.waitForFunction("window.__DONE__ === true", { timeout: 30000 });
|
|
202
|
+
const err = await page.evaluate(() => window.__ERR__ || null);
|
|
203
|
+
if (err) throw new Error("painter threw: " + err);
|
|
204
|
+
const write = process.argv.indexOf("--write-png");
|
|
205
|
+
if (write > -1 && process.argv[write + 1]) {
|
|
206
|
+
await page.locator("#c").screenshot({ path: process.argv[write + 1] });
|
|
207
|
+
}
|
|
208
|
+
return page.evaluate(([yy, xxs, hh]) => {
|
|
209
|
+
const gl = document.getElementById("c").getContext("webgl2");
|
|
210
|
+
const px = new Uint8Array(4);
|
|
211
|
+
return xxs.map((x) => {
|
|
212
|
+
// GL reads from the bottom; the display list counts from the top.
|
|
213
|
+
gl.readPixels(x, hh - 1 - yy, 1, 1, gl.RGBA, gl.UNSIGNED_BYTE, px);
|
|
214
|
+
return [px[0], px[1], px[2], px[3]];
|
|
215
|
+
});
|
|
216
|
+
}, [y, xs, H]);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
const lum = (p) => Math.round(0.2126 * p[0] + 0.7152 * p[1] + 0.0722 * p[2]);
|
|
220
|
+
let bad = 0, checked = 0;
|
|
221
|
+
const say = (ok, name, detail) => {
|
|
222
|
+
checked++;
|
|
223
|
+
if (ok) { console.log(" PASS " + name); return; }
|
|
224
|
+
bad++;
|
|
225
|
+
console.log(" FAIL " + name + "\n " + detail);
|
|
226
|
+
};
|
|
227
|
+
|
|
228
|
+
console.log("=== the painter's backdrop blur, against the browser's own numbers ===");
|
|
229
|
+
|
|
230
|
+
// --- 1. the profile, at each radius the oracle measured ---------------------
|
|
231
|
+
// Tolerance of 6 luminance levels out of 255. Wide enough for SwiftShader's
|
|
232
|
+
// rounding and a slightly different sample grid; far tighter than the 14 that
|
|
233
|
+
// separates the three-box kernel from a true Gaussian, which is the mistake
|
|
234
|
+
// this is here to catch.
|
|
235
|
+
const TOL = 6;
|
|
236
|
+
console.log("");
|
|
237
|
+
console.log("-- the ramp across a hard edge");
|
|
238
|
+
for (const [label, v] of Object.entries(ORACLE.stepResponse)) {
|
|
239
|
+
const r = parseInt(label.match(/\d+/)[0], 10);
|
|
240
|
+
// Sample inside the pane only: the pane spans x 100..300 and the edge is at
|
|
241
|
+
// x=200, so +-40 is comfortably inside it.
|
|
242
|
+
const xs = [];
|
|
243
|
+
for (let x = v.xFrom; x < v.xTo; x++) xs.push(200 + x);
|
|
244
|
+
const row = await rowOf(SCENES.edge(r), 100, xs);
|
|
245
|
+
let worst = 0, worstAt = 0;
|
|
246
|
+
for (let i = 0; i < xs.length; i++) {
|
|
247
|
+
const d = Math.abs(lum(row[i]) - v.luma[i]);
|
|
248
|
+
if (d > worst) { worst = d; worstAt = v.xFrom + i; }
|
|
249
|
+
}
|
|
250
|
+
say(worst <= TOL, `${label}: the ramp matches the browser's (worst ${worst})`,
|
|
251
|
+
`worst error ${worst} luminance levels at x=${worstAt}, tolerance ${TOL}`);
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// --- 2. the element's own edge over a flat backdrop --------------------------
|
|
255
|
+
console.log("");
|
|
256
|
+
console.log("-- a flat backdrop stays flat to the pane's edge");
|
|
257
|
+
{
|
|
258
|
+
const want = ORACLE.uniformBackdrop.centre;
|
|
259
|
+
const xs = ORACLE.uniformBackdrop.insideLeft.map((s) => 100 + s.dx);
|
|
260
|
+
const row = await rowOf(SCENES.uniform(12), 100, xs);
|
|
261
|
+
const got = row.map(lum);
|
|
262
|
+
const worst = Math.max(...got.map((g) => Math.abs(g - want)));
|
|
263
|
+
say(worst <= TOL, `no rim at the edge (worst ${worst})`,
|
|
264
|
+
`wanted ${want} everywhere, got ${got.join(" ")} — a ramp here means the ` +
|
|
265
|
+
`blur sampled only inside the box`);
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// --- 3. the rounded corner ---------------------------------------------------
|
|
269
|
+
console.log("");
|
|
270
|
+
console.log("-- the blur is clipped to the rounded box");
|
|
271
|
+
{
|
|
272
|
+
const row = await rowOf(SCENES.rounded(12), 44, [104, 140]);
|
|
273
|
+
const outside = lum(row[0]);
|
|
274
|
+
const inside = lum(row[1]);
|
|
275
|
+
const wantOutside = lum(ORACLE.rounded.outsideTheCorner);
|
|
276
|
+
say(Math.abs(outside - wantOutside) <= TOL,
|
|
277
|
+
`outside the corner the backdrop is untouched (${outside})`,
|
|
278
|
+
`wanted ${wantOutside}, got ${outside}`);
|
|
279
|
+
const rowIn = await rowOf(SCENES.rounded(12), 80, [140]);
|
|
280
|
+
const wantInside = lum(ORACLE.rounded.insideTheCorner);
|
|
281
|
+
say(Math.abs(lum(rowIn[0]) - wantInside) <= TOL,
|
|
282
|
+
`and inside it the fill is over the blur (${lum(rowIn[0])})`,
|
|
283
|
+
`wanted ${wantInside}, got ${lum(rowIn[0])}`);
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
// --- 4. the case the others are blind to -------------------------------------
|
|
287
|
+
console.log("");
|
|
288
|
+
console.log("-- a feature that straddles the pane's own border");
|
|
289
|
+
{
|
|
290
|
+
const want = ORACLE.acrossTheEdge;
|
|
291
|
+
const xs = [102, 105, 110, 120];
|
|
292
|
+
const row = await rowOf(SCENES.acrossTheEdge(12), 100, xs);
|
|
293
|
+
const got = row.map(lum);
|
|
294
|
+
const wantL = want.insideEdge.map(lum);
|
|
295
|
+
const worst = Math.max(...got.map((g, i) => Math.abs(g - wantL[i])));
|
|
296
|
+
say(worst <= TOL,
|
|
297
|
+
`the white outside the pane does not bleed in (${got.join(" ")})`,
|
|
298
|
+
`wanted ${wantL.join(" ")}, got ${got.join(" ")} — anything much above ` +
|
|
299
|
+
`zero here means the copied region reached past the element, and the page ` +
|
|
300
|
+
`behind it is showing through its own border`);
|
|
301
|
+
|
|
302
|
+
// Outside the rounded corner the backdrop is untouched black. A blur drawn
|
|
303
|
+
// as a square rather than a rounded box lightens this pixel, because it
|
|
304
|
+
// carries the white from x < 100 across the corner.
|
|
305
|
+
const corner = await rowOf(SCENES.acrossTheEdge(12), 43, [103]);
|
|
306
|
+
const wantCorner = lum(want.outsideCorner);
|
|
307
|
+
say(Math.abs(lum(corner[0]) - wantCorner) <= TOL,
|
|
308
|
+
`and the corner outside the radius is untouched (${lum(corner[0])})`,
|
|
309
|
+
`wanted ${wantCorner}, got ${lum(corner[0])} — a square clip shows here`);
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
// --- 5. the blur itself, clipped to the radius -------------------------------
|
|
313
|
+
console.log("");
|
|
314
|
+
console.log("-- the BLUR, not just the fill, respects the radius");
|
|
315
|
+
{
|
|
316
|
+
const want = ORACLE.bandedCorner;
|
|
317
|
+
const row = await rowOf(SCENES.banded(12), 43, [103]);
|
|
318
|
+
const got = lum(row[0]);
|
|
319
|
+
say(Math.abs(got - lum(want.outsideCorner)) <= TOL,
|
|
320
|
+
`outside the corner the band is still hard (${got})`,
|
|
321
|
+
`wanted ${lum(want.outsideCorner)}, got ${got} — grey here means the ` +
|
|
322
|
+
`blurred backdrop was drawn as a rectangle and spilled past the radius`);
|
|
323
|
+
const inBand = await rowOf(SCENES.banded(12), 50, [200]);
|
|
324
|
+
say(Math.abs(lum(inBand[0]) - lum(want.insideBand)) <= TOL,
|
|
325
|
+
`and inside the shape the band is softened (${lum(inBand[0])})`,
|
|
326
|
+
`wanted ${lum(want.insideBand)}, got ${lum(inBand[0])}`);
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
// --- 5b. and the radius, where the two answers are far apart -----------------
|
|
330
|
+
console.log("");
|
|
331
|
+
console.log("-- outside the corner, the raw backdrop and the blurred one differ");
|
|
332
|
+
{
|
|
333
|
+
const want = ORACLE.verticalCorner;
|
|
334
|
+
const outside = lum((await rowOf(SCENES.verticalColumn(12), 43, [104]))[0]);
|
|
335
|
+
const inside = lum((await rowOf(SCENES.verticalColumn(12), 100, [104]))[0]);
|
|
336
|
+
say(Math.abs(outside - lum(want.outsideCorner)) <= TOL,
|
|
337
|
+
`outside the radius the column is still hard (${outside})`,
|
|
338
|
+
`wanted ${lum(want.outsideCorner)}, got ${outside} — the blurred value at ` +
|
|
339
|
+
`this x is ${lum(want.wellInside)}, so anything near that means the blur ` +
|
|
340
|
+
`was drawn as a rectangle`);
|
|
341
|
+
// Deliberately NOT compared with the browser: this x is four pixels from the
|
|
342
|
+
// pane's border, and inside a kernel's reach of it Chrome stops producing a
|
|
343
|
+
// blur profile at all — see `borderBand` in the oracle. What is checked here
|
|
344
|
+
// is only that the blur HAPPENED, which is what makes the probe above mean
|
|
345
|
+
// something: raw black outside the corner, softened inside it.
|
|
346
|
+
say(inside > 40,
|
|
347
|
+
`and at the same x inside the shape it is softened (${inside})`,
|
|
348
|
+
`expected the column to have blurred away from black here, got ${inside}`);
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
// --- 5c. the kernel again, on a narrow feature clear of every border ---------
|
|
352
|
+
// The step response is one edge. This is two, close together, which is where a
|
|
353
|
+
// kernel that is nearly right stops being right — and it is 50px from the
|
|
354
|
+
// nearest border, so nothing about the element's edge is mixed in.
|
|
355
|
+
console.log("");
|
|
356
|
+
console.log("-- a narrow column, well clear of the borders");
|
|
357
|
+
{
|
|
358
|
+
const want = ORACLE.innerColumn;
|
|
359
|
+
const row = (await rowOf(SCENES.innerColumn(12), 100, want.xs)).map(lum);
|
|
360
|
+
let worst = 0, at = 0;
|
|
361
|
+
for (let i = 0; i < row.length; i++) {
|
|
362
|
+
const d = Math.abs(row[i] - want.luma[i]);
|
|
363
|
+
if (d > worst) { worst = d; at = want.xs[i]; }
|
|
364
|
+
}
|
|
365
|
+
say(worst <= TOL, `the profile matches the browser's (worst ${worst})`,
|
|
366
|
+
`worst ${worst} levels at x=${at}\n browser ${want.luma.join(" ")}` +
|
|
367
|
+
`\n painter ${row.join(" ")}`);
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
// --- 6. a second pane must not read the first one's leftovers ----------------
|
|
371
|
+
// Not against the browser: against the SAME pane drawn alone. The claim is
|
|
372
|
+
// that a pane looks the same whether or not a bigger one was blurred before it
|
|
373
|
+
// in the frame, and the reference for that is the pane on its own.
|
|
374
|
+
console.log("");
|
|
375
|
+
console.log("-- a pane looks the same after a bigger one has been blurred");
|
|
376
|
+
{
|
|
377
|
+
const xs = [205, 240, 275, 310, 345];
|
|
378
|
+
const alone = (await rowOf(SCENES.smallAlone(12), 55, xs)).map(lum);
|
|
379
|
+
const after = (await rowOf(SCENES.smallAfterBig(12), 55, xs)).map(lum);
|
|
380
|
+
const worst = Math.max(...alone.map((a, i) => Math.abs(a - after[i])));
|
|
381
|
+
say(worst <= TOL,
|
|
382
|
+
`the second pane is unchanged by the first (worst ${worst})`,
|
|
383
|
+
`alone ${alone.join(" ")} vs after ${after.join(" ")} — a difference here ` +
|
|
384
|
+
`means the blur passes sampled outside their own region and found what the ` +
|
|
385
|
+
`bigger pane left in the reused texture`);
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
await browser.close();
|
|
389
|
+
fs.rmSync(tmp, { force: true });
|
|
390
|
+
if (pageErrors.length) {
|
|
391
|
+
console.log("");
|
|
392
|
+
console.log("page errors:");
|
|
393
|
+
for (const e of pageErrors) console.log(" " + e);
|
|
394
|
+
bad += pageErrors.length;
|
|
395
|
+
}
|
|
396
|
+
console.log("");
|
|
397
|
+
console.log(`passed=${checked - bad} failed=${bad}`);
|
|
398
|
+
if (bad > 0) { console.log("FAILURES"); process.exit(1); }
|
|
399
|
+
console.log("ALL PASS");
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"width":595.00,"height":842.00,"list":{"cmds":[{"k":0,"x":0.00,"y":0.00,"w":595.00,"h":842.00,"c":[251,247,240,1.00]},{"k":3,"x":44.00,"y":44.00,"w":258.74,"h":40.80,"c":[59,47,42,1.00],"text":"Laatikkomalli","font":"Cinzel","size":34.00},{"k":0,"x":44.00,"y":98.80,"w":88.00,"h":3.00,"c":[176,141,87,1.00]},{"k":3,"x":44.00,"y":127.80,"w":76.17,"h":15.60,"c":[138,115,96,1.00],"text":"Täyte ja reunus","font":"Josefin Sans","size":13.00},{"k":0,"x":44.00,"y":151.40,"w":159.67,"h":90.00,"r":6.00,"c":[232,220,200,1.00]},{"k":0,"x":58.00,"y":165.40,"w":131.67,"h":22.00,"r":3.00,"c":[176,141,87,1.00]},{"k":0,"x":217.67,"y":151.40,"w":159.67,"h":90.00,"r":6.00,"c":[232,220,200,1.00]},{"k":0,"x":225.67,"y":155.40,"w":131.67,"h":22.00,"r":3.00,"c":[176,141,87,1.00]},{"k":0,"x":391.33,"y":151.40,"w":159.67,"h":90.00,"r":6.00,"c":[232,220,200,1.00]},{"k":0,"x":404.33,"y":164.40,"w":133.67,"h":22.00,"r":3.00,"c":[176,141,87,1.00]},{"k":3,"x":44.00,"y":267.40,"w":95.23,"h":15.60,"c":[138,115,96,1.00],"text":"Sisäkkäiset laatikot","font":"Josefin Sans","size":13.00},{"k":0,"x":44.00,"y":299.00,"w":507.00,"h":120.00,"r":6.00,"c":[239,230,214,1.00]},{"k":0,"x":56.00,"y":319.00,"w":483.00,"h":88.00,"r":6.00,"c":[229,216,194,1.00]},{"k":0,"x":68.00,"y":339.00,"w":459.00,"h":56.00,"r":6.00,"c":[216,199,171,1.00]},{"k":0,"x":80.00,"y":359.00,"w":435.00,"h":24.00,"r":6.00,"c":[176,141,87,1.00]},{"k":3,"x":44.00,"y":445.00,"w":103.09,"h":15.60,"c":[138,115,96,1.00],"text":"Marginaalit erikseen","font":"Josefin Sans","size":13.00},{"k":0,"x":44.00,"y":468.60,"w":507.00,"h":90.00,"r":6.00,"c":[232,220,200,1.00]},{"k":0,"x":58.00,"y":482.60,"w":479.00,"h":21.00,"r":3.00,"c":[176,141,87,1.00]},{"k":0,"x":82.00,"y":523.60,"w":383.20,"h":21.00,"r":3.00,"c":[176,141,87,1.00]}]}}
|