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