ranger-compiler 3.5.1 → 3.5.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +1064 -20
- package/LICENSE +3 -1
- package/README.md +123 -28
- package/dist/Lang.rgr +1188 -290
- package/dist/api.d.ts +2343 -879
- package/dist/api.js +79923 -56921
- package/dist/lib/JSON.rgr +102 -91
- package/dist/lib/Shell.rgr +4 -4
- package/dist/lib/apple/AppleToolchain.rgr +4 -4
- package/dist/lib/apple/README.md +5 -4
- package/dist/lib/apple/apple_test.rgr +1 -1
- package/dist/lib/core/README.md +1 -1
- package/dist/lib/evg/EVG.rgr +12 -0
- package/dist/lib/evg/EVGA11yFromTree.rgr +302 -0
- package/dist/lib/evg/EVGA11yTree.rgr +894 -0
- package/dist/lib/evg/EVGBox.rgr +267 -0
- package/dist/lib/evg/EVGBoxShorthandTest.rgr +220 -0
- package/dist/lib/evg/EVGCodepoint.rgr +316 -0
- package/dist/lib/evg/EVGColor.rgr +700 -0
- package/dist/lib/evg/EVGCommands.rgr +177 -0
- package/dist/lib/evg/EVGComponent.rgr +331 -0
- package/dist/lib/evg/EVGComponentTest.rgr +387 -0
- package/dist/lib/evg/EVGConnector.rgr +541 -0
- package/dist/lib/evg/EVGConnectorTest.rgr +306 -0
- package/dist/lib/evg/EVGDisplayList.rgr +4186 -0
- package/dist/lib/evg/EVGEasing.rgr +370 -0
- package/dist/lib/evg/EVGEffectTest.rgr +253 -0
- package/dist/lib/evg/EVGElement.rgr +4914 -0
- package/dist/lib/evg/EVGFixedTest.rgr +289 -0
- package/dist/lib/evg/EVGFlexRulesTest.rgr +317 -0
- package/dist/lib/evg/EVGFlexWrapTest.rgr +246 -0
- package/dist/lib/evg/EVGFling.rgr +244 -0
- package/dist/lib/evg/EVGFocus.rgr +400 -0
- package/dist/lib/evg/EVGFocusTest.rgr +399 -0
- package/dist/lib/evg/EVGGradient.rgr +322 -0
- package/dist/lib/evg/EVGGrapheme.rgr +190 -0
- package/dist/lib/evg/EVGGrid.rgr +975 -0
- package/dist/lib/evg/EVGHitTest.rgr +208 -0
- package/dist/lib/evg/EVGHoles.rgr +294 -0
- package/dist/lib/evg/EVGHostMeasurerTest.rgr +281 -0
- package/dist/lib/evg/EVGHostTextMeasurer.rgr +331 -0
- package/dist/lib/evg/EVGHostTree.rgr +834 -0
- package/dist/lib/evg/EVGHostTreeTest.rgr +447 -0
- package/dist/lib/evg/EVGImageDecode.rgr +117 -0
- package/dist/lib/evg/EVGImageMeasurer.rgr +86 -0
- package/dist/lib/evg/EVGInspect.rgr +883 -0
- package/dist/lib/evg/EVGInvalidateTest.rgr +432 -0
- package/dist/lib/evg/EVGJsonTest.rgr +535 -0
- package/dist/lib/evg/EVGLayout.rgr +4220 -0
- package/dist/lib/evg/EVGMeasure.rgr +1267 -0
- package/dist/lib/evg/EVGOverlayTest.rgr +626 -0
- package/dist/lib/evg/EVGPatch.rgr +1721 -0
- package/dist/lib/evg/EVGPatchTest.rgr +692 -0
- package/dist/lib/evg/EVGPopoverTest.rgr +395 -0
- package/dist/lib/evg/EVGReconcile.rgr +226 -0
- package/dist/lib/evg/EVGReconcileTest.rgr +421 -0
- package/dist/lib/evg/EVGReject.rgr +126 -0
- package/dist/lib/evg/EVGRelayoutTest.rgr +375 -0
- package/dist/lib/evg/EVGRtlLayoutTest.rgr +319 -0
- package/dist/lib/evg/EVGRuler.rgr +232 -0
- package/dist/lib/evg/EVGRulerTest.rgr +172 -0
- package/dist/lib/evg/EVGSelectChrome.rgr +178 -0
- package/dist/lib/evg/EVGStyleCacheTest.rgr +419 -0
- package/dist/lib/evg/EVGStyleSheet.rgr +2148 -0
- package/dist/lib/evg/EVGStyleStateTest.rgr +407 -0
- package/dist/lib/evg/EVGStyleVarTest.rgr +471 -0
- package/dist/lib/evg/EVGText.rgr +105 -0
- package/dist/lib/evg/EVGTextEngine.rgr +536 -0
- package/dist/lib/evg/EVGTextMeasurer.rgr +720 -0
- package/dist/lib/evg/EVGTimingTest.rgr +1417 -0
- package/dist/lib/evg/EVGToolbar.rgr +1369 -0
- package/dist/lib/evg/EVGTransition.rgr +728 -0
- package/dist/lib/evg/EVGTreeJson.rgr +721 -0
- package/dist/lib/evg/EVGUnit.rgr +554 -0
- package/dist/lib/evg/EVGViewportUnitTest.rgr +236 -0
- package/dist/lib/evg/EvgApp.rgr +189 -0
- package/dist/lib/evg/EvgBitmapTracer.rgr +4277 -0
- package/dist/lib/evg/EvgBitmapTracerTest.rgr +2119 -0
- package/dist/lib/evg/EvgHost.rgr +451 -0
- package/dist/lib/evg/EvgTest.rgr +91 -0
- package/dist/lib/evg/EvgTraceColor.rgr +236 -0
- package/dist/lib/evg/EvgTraceCurve.rgr +648 -0
- package/dist/lib/evg/EvgTraceFit.rgr +767 -0
- package/dist/lib/evg/EvgTracePath.rgr +435 -0
- package/dist/lib/evg/EvgTraceTypes.rgr +559 -0
- package/dist/lib/evg/EvgViewport.rgr +313 -0
- package/dist/lib/evg/FxDemoDoc.rgr +185 -0
- package/dist/lib/evg/HOSTS.md +227 -0
- package/dist/lib/evg/ISSUES.md +835 -0
- package/dist/lib/evg/PLAN_ACCESSIBILITY.md +650 -0
- package/dist/lib/evg/PLAN_CSS_LAYOUT_AND_FONTS.md +1124 -0
- package/dist/lib/evg/PLAN_EFFECTS.md +431 -0
- package/dist/lib/evg/PLAN_EVG.md +518 -0
- package/dist/lib/evg/PLAN_EVG_RENDERER.md +1674 -0
- package/dist/lib/evg/PLAN_INSPECTOR.md +788 -0
- package/dist/lib/evg/PLAN_LINKS_AND_FORMS.md +157 -0
- package/dist/lib/evg/PLAN_NATIVE_HOSTS.md +671 -0
- package/dist/lib/evg/PLAN_VECTOR_IR.md +664 -0
- package/dist/lib/evg/PLAN_VIEW_TRANSFORM.md +322 -0
- package/dist/lib/evg/PathBuilder.rgr +333 -0
- package/dist/lib/evg/README.md +1236 -0
- package/dist/lib/evg/SPEC.md +1252 -0
- package/dist/lib/evg/SVGPathParser.rgr +1199 -0
- package/dist/lib/evg/SvgParser.rgr +1736 -0
- package/dist/lib/evg/TODO_EVG_RENDERER.md +595 -0
- package/dist/lib/evg/VectorShapes.rgr +331 -0
- package/dist/lib/evg/VectorStroke.rgr +107 -0
- package/dist/lib/evg/VectorViewBox.rgr +380 -0
- package/dist/lib/evg/agent/README.md +290 -0
- package/dist/lib/evg/agent/evg_agent.rgr +899 -0
- package/dist/lib/evg/agent/fixtures/broken.evg.json +7 -0
- package/dist/lib/evg/agent/fixtures/card.evg.json +9 -0
- package/dist/lib/evg/agent/fixtures/connector.css +63 -0
- package/dist/lib/evg/agent/fixtures/connector.evg.json +14 -0
- package/dist/lib/evg/agent/fixtures/drawn.evg.json +129 -0
- package/dist/lib/evg/agent/fixtures/gradient.evg.json +4 -0
- package/dist/lib/evg/agent/fixtures/popover.css +99 -0
- package/dist/lib/evg/agent/fixtures/popover.evg.json +30 -0
- package/dist/lib/evg/agent/roundtrip.sh +74 -0
- package/dist/lib/evg/agent/smoke.sh +230 -0
- package/dist/lib/evg/android/README.md +97 -0
- package/dist/lib/evg/android/androidstubs/AndroidStubs.kt +175 -0
- package/dist/lib/evg/android/androidstubs/Annotation.kt +10 -0
- package/dist/lib/evg/android/androidstubs/App.kt +30 -0
- package/dist/lib/evg/android/androidstubs/Content.kt +41 -0
- package/dist/lib/evg/android/androidstubs/ContentRes.kt +12 -0
- package/dist/lib/evg/android/androidstubs/InputMethod.kt +46 -0
- package/dist/lib/evg/android/androidstubs/Net.kt +6 -0
- package/dist/lib/evg/android/androidstubs/Os.kt +30 -0
- package/dist/lib/evg/android/androidstubs/Util.kt +15 -0
- package/dist/lib/evg/android/androidstubs/View.kt +113 -0
- package/dist/lib/evg/android/androidstubs/Widget.kt +16 -0
- package/dist/lib/evg/android/src/android/kotlin/fi/ranger/evg/AndroidEvgSurface.kt +323 -0
- package/dist/lib/evg/android/src/android/kotlin/fi/ranger/evg/AndroidTextMeasurer.kt +56 -0
- package/dist/lib/evg/android/src/android/kotlin/fi/ranger/evg/RippleEffect.kt +262 -0
- package/dist/lib/evg/android/src/awt/kotlin/fi/ranger/evg/AwtEvgSurface.kt +238 -0
- package/dist/lib/evg/android/src/awt/kotlin/fi/ranger/evg/AwtTextMeasurer.kt +85 -0
- package/dist/lib/evg/android/src/main/kotlin/fi/ranger/evg/EvgEngineThread.kt +115 -0
- package/dist/lib/evg/android/src/main/kotlin/fi/ranger/evg/EvgPainter.kt +231 -0
- package/dist/lib/evg/android/src/main/kotlin/fi/ranger/evg/EvgSurface.kt +117 -0
- package/dist/lib/evg/android/src/main/kotlin/fi/ranger/evg/RecordingSurface.kt +94 -0
- package/dist/lib/evg/apple/README.md +128 -0
- package/dist/lib/evg/apple/Sources/CoreGraphicsEvgSurface.swift +329 -0
- package/dist/lib/evg/apple/Sources/CoreTextMeasurer.swift +70 -0
- package/dist/lib/evg/apple/Sources/EvgEngineQueue.swift +105 -0
- package/dist/lib/evg/apple/Sources/EvgPainter.swift +225 -0
- package/dist/lib/evg/apple/Sources/EvgSurface.swift +163 -0
- package/dist/lib/evg/apple/Sources/RecordingSurface.swift +110 -0
- package/dist/lib/evg/bench/EvgLayoutBench.rgr +34 -0
- package/dist/lib/evg/bench/README.md +64 -0
- package/dist/lib/evg/bench/layout-bench.mjs +316 -0
- package/dist/lib/evg/bench/layout-cases.mjs +510 -0
- package/dist/lib/evg/bench/layout-conformance.mjs +254 -0
- package/dist/lib/evg/bin/.gitignore +17 -0
- package/dist/lib/evg/evg_test.rgr +313 -0
- package/dist/lib/evg/gl/README.md +198 -0
- package/dist/lib/evg/gl/a11y-paint-check.mjs +73 -0
- package/dist/lib/evg/gl/blur-check.mjs +399 -0
- package/dist/lib/evg/gl/boxmodel.json +1 -0
- package/dist/lib/evg/gl/demo.html +46 -0
- package/dist/lib/evg/gl/effect-presets.css +285 -0
- package/dist/lib/evg/gl/effect-presets.js +63 -0
- package/dist/lib/evg/gl/effect-shots.mjs +135 -0
- package/dist/lib/evg/gl/evg-a11y.js +566 -0
- package/dist/lib/evg/gl/evg-binary.js +160 -0
- package/dist/lib/evg/gl/evg-engine.js +296 -0
- package/dist/lib/evg/gl/evg-fx.js +237 -0
- package/dist/lib/evg/gl/evg-gestures.js +209 -0
- package/dist/lib/evg/gl/evg-list.js +167 -0
- package/dist/lib/evg/gl/evg-measure.js +186 -0
- package/dist/lib/evg/gl/evg-textinput.js +303 -0
- package/dist/lib/evg/gl/evg-view.js +144 -0
- package/dist/lib/evg/gl/evg-webgl.js +3942 -0
- package/dist/lib/evg/gl/fx-check.mjs +998 -0
- package/dist/lib/evg/gl/fx-demo.html +166 -0
- package/dist/lib/evg/gl/fx-demo.js +2 -0
- package/dist/lib/evg/gl/fx-serve.mjs +47 -0
- package/dist/lib/evg/gl/gestures-check.mjs +189 -0
- package/dist/lib/evg/gl/list-binary-check.mjs +152 -0
- package/dist/lib/evg/gl/measure-check.mjs +135 -0
- package/dist/lib/evg/gl/rotation-check.mjs +207 -0
- package/dist/lib/evg/gl/shift-check.mjs +113 -0
- package/dist/lib/evg/gl/stroke-check.mjs +168 -0
- package/dist/lib/evg/gl/text-snap-check.mjs +139 -0
- package/dist/lib/evg/gl/view-check.mjs +343 -0
- package/dist/lib/evg/gl/view-policy-check.mjs +184 -0
- package/dist/lib/evg/html/evg-dom.js +318 -0
- package/dist/lib/evg/html/evg-html.js +600 -0
- package/dist/lib/evg/inspect/README.md +415 -0
- package/dist/lib/evg/inspect/browser-smoke.mjs +115 -0
- package/dist/lib/evg/inspect/evg-inspect.js +947 -0
- package/dist/lib/evg/inspect/shots/css.png +0 -0
- package/dist/lib/evg/inspect/shots/dashboard.png +0 -0
- package/dist/lib/evg/inspect/shots/pptx-slide.png +0 -0
- package/dist/lib/evg/inspect/shots/state.png +0 -0
- package/dist/lib/evg/inspect/shots.mjs +226 -0
- package/dist/lib/evg/oracle/css-blur.json +576 -0
- package/dist/lib/evg/oracle/css-box.json +157 -0
- package/dist/lib/evg/oracle/css-timing.json +709 -0
- package/dist/lib/evg/oracle/css_blur_oracle.mjs +529 -0
- package/dist/lib/evg/oracle/css_box_oracle.mjs +106 -0
- package/dist/lib/evg/oracle/css_timing_oracle.mjs +389 -0
- package/dist/lib/evg/original/EVGColor.clj +310 -0
- package/dist/lib/evg/original/EVGColorContext.rgr +178 -0
- package/dist/lib/evg/original/SVGPath.rgr +627 -0
- package/dist/lib/evg/original/Vec2.crgr +103 -0
- package/dist/lib/evg/ranger.json +9 -0
- package/dist/lib/evg/showcase/README.md +246 -0
- package/dist/lib/evg/showcase/assets/emblem.svg +30 -0
- package/dist/lib/evg/showcase/assets/rosette.svg +22 -0
- package/dist/lib/evg/showcase/build.mjs +651 -0
- package/dist/lib/evg/showcase/pages/album.tsx +30 -0
- package/dist/lib/evg/showcase/pages/boxmodel.tsx +37 -0
- package/dist/lib/evg/showcase/pages/cards.tsx +41 -0
- package/dist/lib/evg/showcase/pages/chart_api.tsx +170 -0
- package/dist/lib/evg/showcase/pages/charts.tsx +219 -0
- package/dist/lib/evg/showcase/pages/drawing.tsx +160 -0
- package/dist/lib/evg/showcase/pages/emoji.tsx +91 -0
- package/dist/lib/evg/showcase/pages/flex.tsx +46 -0
- package/dist/lib/evg/showcase/pages/more.tsx +332 -0
- package/dist/lib/evg/showcase/pages/plots.tsx +262 -0
- package/dist/lib/evg/showcase/pages/svg.tsx +63 -0
- package/dist/lib/evg/showcase/pages/tables.tsx +209 -0
- package/dist/lib/evg/showcase/pages/typography.tsx +43 -0
- package/dist/lib/evg/showcase/pages/units.tsx +35 -0
- package/dist/lib/evg/showcase/pages/variants.tsx +233 -0
- package/dist/lib/evg/showcase/pages/vector.tsx +75 -0
- package/dist/lib/evg/showcase/pages/views.tsx +223 -0
- package/dist/lib/evg/showcase/tests/chart_api_smoke.mjs +339 -0
- package/dist/lib/evg/showcase/tests/gl_smoke.mjs +125 -0
- package/dist/lib/evg/showcase/themes/chart_api-default.css +15 -0
- package/dist/lib/evg/showcase/themes/charts-default.css +29 -0
- package/dist/lib/evg/showcase/themes/drawing-default.css +35 -0
- package/dist/lib/evg/showcase/themes/more-default.css +128 -0
- package/dist/lib/evg/showcase/themes/plots-default.css +18 -0
- package/dist/lib/evg/showcase/themes/showcase.css +508 -0
- package/dist/lib/evg/showcase/themes/tables-default.css +123 -0
- package/dist/lib/evg/showcase/themes/variants-default.css +15 -0
- package/dist/lib/evg/showcase/themes/views-default.css +15 -0
- package/dist/lib/evg/tools/bench_vs_potrace.mjs +290 -0
- package/dist/lib/evg/tools/evg_image_tool.rgr +488 -0
- package/dist/lib/evg/tools/evg_trace_bench.rgr +114 -0
- package/dist/lib/evg/tools/evg_trace_cli.rgr +662 -0
- package/dist/lib/evg/tools/evg_trace_cpp_bench.rgr +83 -0
- package/dist/lib/evg/tools/run_trace_bench.sh +74 -0
- package/dist/lib/evg/tools/run_trace_cli_smoke.sh +128 -0
- package/dist/lib/evg/web/responsive/EvgResponsiveCheck.rgr +271 -0
- package/dist/lib/evg/web/responsive/EvgResponsiveDemo.rgr +531 -0
- package/dist/lib/evg/web/responsive/README.md +97 -0
- package/dist/lib/evg/web/responsive/build.mjs +90 -0
- package/dist/lib/evg/web/responsive/dom-check.mjs +169 -0
- package/dist/lib/evg/web/responsive/index.html +177 -0
- package/dist/lib/evg/web/responsive/smoke.mjs +221 -0
- package/dist/lib/evg/web/tools/assets-client.mjs +95 -0
- package/dist/lib/evg/web/tools/boot-bench.mjs +189 -0
- package/dist/lib/evg/web/tools/inline-assets.mjs +119 -0
- package/dist/lib/evg/web/tools/minify.mjs +60 -0
- package/dist/lib/evg/web/tracer/build.mjs +87 -0
- package/dist/lib/evg/web/tracer/index.html +2805 -0
- package/dist/lib/evg/web/tracer/sample.png +0 -0
- package/dist/lib/evg/web/tracer/smoke.mjs +1162 -0
- package/dist/lib/evgr/Cargo.lock +21 -0
- package/dist/lib/evgr/Cargo.toml +19 -0
- package/dist/lib/evgr/README.md +140 -0
- package/dist/lib/evgr/bench/NativeBench.rgr +246 -0
- package/dist/lib/evgr/bench/compare.mjs +252 -0
- package/dist/lib/evgr/bench/speed.mjs +277 -0
- package/dist/lib/evgr/bin/.gitignore +3 -0
- package/dist/lib/evgr/src/bin/bench.rs +106 -0
- package/dist/lib/evgr/src/bin/smoke.rs +13 -0
- package/dist/lib/evgr/src/grid.rs +730 -0
- package/dist/lib/evgr/src/lib.rs +1003 -0
- package/dist/lib/evgr/src/style.rs +468 -0
- package/dist/lib/evgr/src/text.rs +83 -0
- package/dist/lib/image/BitReader.rgr +171 -0
- package/dist/lib/image/Buffer.rgr +173 -0
- package/dist/lib/image/DCT.rgr +283 -0
- package/dist/lib/image/Deflate.rgr +339 -0
- package/dist/lib/image/HuffmanDecoder.rgr +181 -0
- package/dist/lib/image/ImageBuffer.rgr +706 -0
- package/dist/lib/image/JPEGDecoder.rgr +808 -0
- package/dist/lib/image/PNGDecoder.rgr +544 -0
- package/dist/lib/image/PNGEncoder.rgr +388 -0
- package/dist/lib/image/PPMImage.rgr +224 -0
- package/dist/lib/image/ProgressiveJPEGDecoder.rgr +1154 -0
- package/dist/lib/image/README.md +33 -0
- package/dist/lib/image/RasterBuffer.rgr +294 -0
- package/dist/lib/image/VP8BoolDecoder.rgr +163 -0
- package/dist/lib/image/WebPDecoder.rgr +433 -0
- package/dist/lib/image/WebPLossless.rgr +1035 -0
- package/dist/lib/image/WebPLossy.rgr +1929 -0
- package/dist/lib/image/ranger.json +9 -0
- package/dist/lib/image/testdata/webp/anim_2f_24x16.webp +0 -0
- package/dist/lib/image/testdata/webp/fixtures.txt +33 -0
- package/dist/lib/image/testdata/webp/gen_fixtures.py +261 -0
- package/dist/lib/image/testdata/webp/ll_1x1.webp +0 -0
- package/dist/lib/image/testdata/webp/ll_alpha_29x21.webp +0 -0
- package/dist/lib/image/testdata/webp/ll_gradient_64x64.webp +0 -0
- package/dist/lib/image/testdata/webp/ll_meta_32x32.webp +0 -0
- package/dist/lib/image/testdata/webp/ll_noise_17x9.webp +0 -0
- package/dist/lib/image/testdata/webp/ll_pal11_23x11.webp +0 -0
- package/dist/lib/image/testdata/webp/ll_pal2_19x7.webp +0 -0
- package/dist/lib/image/testdata/webp/ll_pal40_16x16.webp +0 -0
- package/dist/lib/image/testdata/webp/ll_pal4_13x5.webp +0 -0
- package/dist/lib/image/testdata/webp/ll_photo_40x30.webp +0 -0
- package/dist/lib/image/testdata/webp/ll_predictors_64x32.webp +0 -0
- package/dist/lib/image/testdata/webp/ly_1x1.webp +0 -0
- package/dist/lib/image/testdata/webp/ly_alph_raw_f0_21x13.webp +0 -0
- package/dist/lib/image/testdata/webp/ly_alph_raw_f1_21x13.webp +0 -0
- package/dist/lib/image/testdata/webp/ly_alph_raw_f2_21x13.webp +0 -0
- package/dist/lib/image/testdata/webp/ly_alph_raw_f3_21x13.webp +0 -0
- package/dist/lib/image/testdata/webp/ly_alph_vp8l_f0_21x13.webp +0 -0
- package/dist/lib/image/testdata/webp/ly_alph_vp8l_f1_21x13.webp +0 -0
- package/dist/lib/image/testdata/webp/ly_alph_vp8l_f2_21x13.webp +0 -0
- package/dist/lib/image/testdata/webp/ly_alph_vp8l_f3_21x13.webp +0 -0
- package/dist/lib/image/testdata/webp/ly_alpha_33x17.webp +0 -0
- package/dist/lib/image/testdata/webp/ly_exif_15x11.webp +0 -0
- package/dist/lib/image/testdata/webp/ly_i16_96x80.webp +0 -0
- package/dist/lib/image/testdata/webp/ly_nofilter_24x20.webp +0 -0
- package/dist/lib/image/testdata/webp/ly_odd_17x9.webp +0 -0
- package/dist/lib/image/testdata/webp/ly_photo_48x40.webp +0 -0
- package/dist/lib/image/testdata/webp/ly_q100_20x18.webp +0 -0
- package/dist/lib/image/testdata/webp/ly_sharp_40x36.webp +0 -0
- package/dist/lib/image/testdata/webp/ly_simple_filter_33x31.webp +0 -0
- package/dist/lib/image/testdata/webp/ly_vpx_parts8_37x45.webp +0 -0
- package/dist/lib/image/testdata/webp/ly_vpx_skip_96x80.webp +0 -0
- package/dist/lib/image/tests/WebPDecodeTool.rgr +81 -0
- package/dist/lib/image/tests/WebPDecoderTest.rgr +1162 -0
- package/dist/lib/image/tests/run_webp_tests.sh +39 -0
- package/dist/lib/rust/RsJson.rgr +468 -0
- package/dist/lib/rust/RsPrelude.rgr +1663 -0
- package/dist/lib/shell_test.rgr +7 -7
- package/dist/lib/stdlib.rgr +21 -5
- package/dist/lib/zip/ranger.json +6 -0
- package/dist/rgrc.js +87318 -67610
- package/package.json +1010 -1040
- package/dist/README.md +0 -117
- package/dist/package.json +0 -47
|
@@ -0,0 +1,2805 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="fi">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
6
|
+
<title>EVG Bitmap Tracer — bitmap → SVG</title>
|
|
7
|
+
<meta name="description" content="Lataa JPEG/PNG, säädä Potrace-tyylisiä parametreja ja vektoroi kuva selaimessa. Ei palvelinta — käännetty Ranger EvgBitmapTracer.">
|
|
8
|
+
<style>
|
|
9
|
+
:root {
|
|
10
|
+
--bg: #12151b; --panel: #171b23; --line: #2a3040; --ink: #e8ecf4;
|
|
11
|
+
--muted: #97a1b4; --accent: #7db3ff; --bad: #ff9a8f; --ok: #8fd39a;
|
|
12
|
+
--stage: #f4f1ea; --mono: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
|
|
13
|
+
}
|
|
14
|
+
* { box-sizing: border-box; }
|
|
15
|
+
body {
|
|
16
|
+
margin: 0; background: var(--bg); color: var(--ink);
|
|
17
|
+
font: 15px/1.5 system-ui, -apple-system, "Segoe UI", sans-serif;
|
|
18
|
+
}
|
|
19
|
+
header { padding: 28px 24px 12px; max-width: 1280px; margin: 0 auto; }
|
|
20
|
+
h1 { margin: 0 0 6px; font-size: 26px; font-weight: 650; letter-spacing: -0.01em; }
|
|
21
|
+
header p { margin: 0; color: var(--muted); max-width: 72ch; }
|
|
22
|
+
header a { color: var(--accent); }
|
|
23
|
+
.crumb { margin: 0 0 10px; font-size: 12px; color: var(--muted); }
|
|
24
|
+
.crumb a { color: var(--muted); text-decoration: none; }
|
|
25
|
+
.crumb a:hover { color: var(--accent); }
|
|
26
|
+
|
|
27
|
+
main {
|
|
28
|
+
max-width: 1280px; margin: 0 auto; padding: 8px 24px 64px;
|
|
29
|
+
display: grid; grid-template-columns: minmax(280px, 340px) minmax(0, 1fr); gap: 18px;
|
|
30
|
+
}
|
|
31
|
+
@media (max-width: 900px) {
|
|
32
|
+
main { grid-template-columns: minmax(0, 1fr); padding: 8px 14px 48px; }
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
section {
|
|
36
|
+
background: var(--panel); border: 1px solid var(--line); border-radius: 10px; overflow: hidden;
|
|
37
|
+
}
|
|
38
|
+
.head {
|
|
39
|
+
display: flex; align-items: center; gap: 10px; flex-wrap: wrap;
|
|
40
|
+
padding: 10px 14px; border-bottom: 1px solid var(--line);
|
|
41
|
+
font-size: 12px; text-transform: uppercase; letter-spacing: 0.08em; color: var(--muted);
|
|
42
|
+
}
|
|
43
|
+
.head .grow { flex: 1; }
|
|
44
|
+
.body { padding: 14px; }
|
|
45
|
+
|
|
46
|
+
label.row {
|
|
47
|
+
display: grid; grid-template-columns: 1fr auto; gap: 6px 10px;
|
|
48
|
+
align-items: center; margin: 0 0 12px; font-size: 13px;
|
|
49
|
+
}
|
|
50
|
+
label.row span.hint { grid-column: 1 / -1; color: var(--muted); font-size: 11.5px; margin-top: -6px; }
|
|
51
|
+
label.row input[type="range"] { width: 100%; grid-column: 1 / -1; }
|
|
52
|
+
/* A select sized by its widest option pushes out of the 300px panel and the
|
|
53
|
+
text is then clipped, so give it the full row like the sliders have. */
|
|
54
|
+
label.row select { grid-column: 1 / -1; width: 100%; min-width: 0; box-sizing: border-box; }
|
|
55
|
+
label.check { display: flex; align-items: center; gap: 8px; margin: 0 0 12px; font-size: 13px; }
|
|
56
|
+
select, input[type="number"], input[type="color"], input[type="text"] {
|
|
57
|
+
font: inherit; font-size: 13px; color: var(--ink); background: #202634;
|
|
58
|
+
border: 1px solid var(--line); border-radius: 6px; padding: 4px 8px;
|
|
59
|
+
}
|
|
60
|
+
input[type="color"] { padding: 0; width: 42px; height: 28px; }
|
|
61
|
+
.val { font-family: var(--mono); font-size: 12px; color: var(--accent); min-width: 3.5ch; text-align: right; }
|
|
62
|
+
|
|
63
|
+
button, .filebtn {
|
|
64
|
+
font: inherit; font-size: 13px; color: var(--ink); background: #202634;
|
|
65
|
+
border: 1px solid var(--line); border-radius: 999px; padding: 6px 14px; cursor: pointer;
|
|
66
|
+
}
|
|
67
|
+
button:hover, .filebtn:hover { border-color: var(--accent); color: var(--accent); }
|
|
68
|
+
button.primary { background: var(--accent); border-color: var(--accent); color: #10131a; font-weight: 600; }
|
|
69
|
+
button.primary:hover { filter: brightness(1.05); color: #10131a; }
|
|
70
|
+
button:disabled { opacity: 0.45; cursor: default; }
|
|
71
|
+
.filebtn { display: inline-block; }
|
|
72
|
+
.filebtn input { display: none; }
|
|
73
|
+
.actions { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 4px; }
|
|
74
|
+
|
|
75
|
+
.stage {
|
|
76
|
+
background: var(--stage); min-height: 280px; display: flex; align-items: center; justify-content: center;
|
|
77
|
+
padding: 16px; overflow: auto;
|
|
78
|
+
}
|
|
79
|
+
.stage img, .stage svg { max-width: 100%; height: auto; display: block; }
|
|
80
|
+
/* Transparency needs something behind it to read against; the cream stage
|
|
81
|
+
looks exactly like a painted cream background. */
|
|
82
|
+
.stage.checker {
|
|
83
|
+
background-color: #fbfbfb;
|
|
84
|
+
background-image:
|
|
85
|
+
linear-gradient(45deg, #cfcfcf 25%, transparent 25%, transparent 75%, #cfcfcf 75%),
|
|
86
|
+
linear-gradient(45deg, #cfcfcf 25%, transparent 25%, transparent 75%, #cfcfcf 75%);
|
|
87
|
+
background-size: 18px 18px;
|
|
88
|
+
background-position: 0 0, 9px 9px;
|
|
89
|
+
}
|
|
90
|
+
.stage.empty { color: #7a7268; font-size: 14px; }
|
|
91
|
+
|
|
92
|
+
/* --- edit mode ------------------------------------------------------- */
|
|
93
|
+
.editbar { display: none; gap: 8px; align-items: center; flex-wrap: wrap;
|
|
94
|
+
padding: 8px 14px; border-top: 1px solid var(--line); background: #1a1f2b; }
|
|
95
|
+
.editbar.on { display: flex; }
|
|
96
|
+
/* The pre-processing bar sits under the picture rather than in the parameter
|
|
97
|
+
column: it is about the bitmap, and you want it where you can see the
|
|
98
|
+
bitmap change. Hidden until asked for — it is an extra stage, not part of
|
|
99
|
+
tracing. */
|
|
100
|
+
.prebar { flex-wrap: wrap; align-items: flex-start; gap: 10px 18px; }
|
|
101
|
+
.prebar label.row { display: grid; grid-template-columns: auto auto; gap: 2px 8px;
|
|
102
|
+
align-items: center; margin: 0; width: 190px; font-size: 12px; }
|
|
103
|
+
.prebar label.row input[type="range"] { grid-column: 1 / -1; width: 100%; }
|
|
104
|
+
.prebar label.row select { grid-column: 1 / -1; width: 100%; }
|
|
105
|
+
.prebar label.check { font-size: 12px; margin: 0; align-self: center; }
|
|
106
|
+
.prebar .hint { grid-column: 1 / -1; font-size: 11px; }
|
|
107
|
+
.prebar > .hint { width: 100%; }
|
|
108
|
+
.editbar .sep { width: 1px; align-self: stretch; background: var(--line); margin: 0 2px; }
|
|
109
|
+
.editbar .note { color: var(--muted); font-size: 12px; }
|
|
110
|
+
/* The wand's own controls only mean anything while the wand is the tool, and
|
|
111
|
+
the bar is already long enough. !important because the labels carry an
|
|
112
|
+
inline display of their own. */
|
|
113
|
+
.editbar .wand-only { display: none !important; }
|
|
114
|
+
.editbar.wand .wand-only { display: inline-flex !important; }
|
|
115
|
+
.tool { padding: 5px 12px; font-size: 12.5px; }
|
|
116
|
+
.tool.active { background: var(--accent); border-color: var(--accent); color: #10131a; font-weight: 600; }
|
|
117
|
+
/* Hit-testing wants the whole shape, not just its painted middle, and a
|
|
118
|
+
shape scaled down to fit the pane is a few screen pixels wide in places. */
|
|
119
|
+
#outStage.editing svg path { cursor: crosshair; }
|
|
120
|
+
#outStage.editing svg path:hover { stroke: #ff2ec4; stroke-width: 1.5; paint-order: stroke; vector-effect: non-scaling-stroke; }
|
|
121
|
+
#outStage.editing.picking svg path { cursor: copy; }
|
|
122
|
+
#outStage.editing.refining svg path { cursor: crosshair; }
|
|
123
|
+
#outStage.editing.refining svg path:hover { stroke: none; }
|
|
124
|
+
#outStage.editing.smoothing svg path { cursor: grab; }
|
|
125
|
+
#outStage.editing.smoothing svg path:hover { stroke: none; }
|
|
126
|
+
#outStage.editing.wanding svg path { cursor: crosshair; }
|
|
127
|
+
#outStage.editing.wanding svg path.wand-off { opacity: .12; }
|
|
128
|
+
.split {
|
|
129
|
+
display: grid; grid-template-columns: 1fr 1fr; gap: 1px; background: var(--line);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
@media (max-width: 700px) { .split { grid-template-columns: 1fr; } }
|
|
133
|
+
.pane { background: var(--panel); display: flex; flex-direction: column; min-width: 0; }
|
|
134
|
+
.pane .stage { flex: 1; }
|
|
135
|
+
|
|
136
|
+
#status {
|
|
137
|
+
padding: 10px 14px; border-top: 1px solid var(--line);
|
|
138
|
+
font-family: var(--mono); font-size: 12px; color: var(--muted); white-space: pre-wrap;
|
|
139
|
+
}
|
|
140
|
+
#status.bad { color: var(--bad); }
|
|
141
|
+
#status.good b { color: var(--ok); font-weight: 600; }
|
|
142
|
+
|
|
143
|
+
.palette { display: flex; flex-wrap: wrap; gap: 4px; padding: 10px 14px; border-top: 1px solid var(--line); }
|
|
144
|
+
.sw { width: 22px; height: 22px; border-radius: 4px; border: 1px solid var(--line); }
|
|
145
|
+
.sw[title] { cursor: default; }
|
|
146
|
+
.palette .sw { cursor: copy; }
|
|
147
|
+
.pal-edit { display: flex; flex-wrap: wrap; gap: 6px; align-items: center; margin: 2px 0 8px; }
|
|
148
|
+
.pal-chip { position: relative; width: 34px; height: 30px; }
|
|
149
|
+
.pal-chip input[type="color"] { width: 34px; height: 30px; padding: 0; }
|
|
150
|
+
.pal-chip button {
|
|
151
|
+
position: absolute; top: -7px; right: -7px; width: 17px; height: 17px; line-height: 1;
|
|
152
|
+
padding: 0; border-radius: 999px; font-size: 12px; background: #202634;
|
|
153
|
+
}
|
|
154
|
+
.pal-add { padding: 6px 10px; }
|
|
155
|
+
.pal-empty { color: var(--muted); font-size: 12px; }
|
|
156
|
+
/* Both id-level, but the .hidden rule is more specific, so it wins in mono
|
|
157
|
+
mode. An inline display on the element would beat the class and leave the
|
|
158
|
+
editor on screen in a mode that ignores it. */
|
|
159
|
+
#row-palEdit, #row-algo { display: block; }
|
|
160
|
+
#row-palEdit.hidden, #row-algo.hidden { display: none; }
|
|
161
|
+
#row-algo { margin-bottom: 14px; }
|
|
162
|
+
/* Shown but not in use: the colors are still editable, and touching one is
|
|
163
|
+
what switches the palette over to them. */
|
|
164
|
+
#row-palEdit.pal-inactive .pal-edit, #row-palEdit.pal-inactive .actions { opacity: 0.45; }
|
|
165
|
+
.pal-note { color: var(--muted); font-size: 11.5px; margin: 0 0 8px; }
|
|
166
|
+
|
|
167
|
+
fieldset {
|
|
168
|
+
border: 1px solid var(--line); border-radius: 8px; margin: 0 0 14px; padding: 10px 12px 2px;
|
|
169
|
+
}
|
|
170
|
+
legend { padding: 0 6px; color: var(--muted); font-size: 11px; text-transform: uppercase; letter-spacing: 0.06em; }
|
|
171
|
+
.mono-only.hidden, .color-only.hidden { display: none; }
|
|
172
|
+
</style>
|
|
173
|
+
</head>
|
|
174
|
+
<body>
|
|
175
|
+
<header>
|
|
176
|
+
<p class="crumb"><a href="../">EVG showcase</a> · <a href="../chart-api/">Chart API</a> · tracer</p>
|
|
177
|
+
<h1>Bitmap → SVG tracer</h1>
|
|
178
|
+
<p>
|
|
179
|
+
Lataa JPEG tai PNG, säädä parametreja ja vektoroi kuva selaimessa.
|
|
180
|
+
Moottori on repositorion <code>EvgBitmapTracer</code> (Potrace-tyylinen
|
|
181
|
+
clean-room), käännetty Rangerista ES6:ksi — ei palvelinta, kuva ei lähde minnekään.
|
|
182
|
+
<a href="https://github.com/terotests/Ranger/tree/master/lib/evg">Lähdekoodi</a>
|
|
183
|
+
</p>
|
|
184
|
+
</header>
|
|
185
|
+
|
|
186
|
+
<main>
|
|
187
|
+
<section>
|
|
188
|
+
<div class="head"><span>Parametrit</span><span class="grow"></span></div>
|
|
189
|
+
<div class="body">
|
|
190
|
+
<div class="actions" style="margin-bottom:14px">
|
|
191
|
+
<label class="filebtn">Valitse kuva<input id="file" type="file" accept="image/jpeg,image/png,image/webp,image/gif,.jpg,.jpeg,.png,.webp"></label>
|
|
192
|
+
<button type="button" id="sample">Esimerkki</button>
|
|
193
|
+
</div>
|
|
194
|
+
<p style="margin:0 0 10px;font-size:12px;color:var(--muted)">Voit myös liittää kuvan leikepöydältä (⌘/Ctrl + V).</p>
|
|
195
|
+
<p style="margin:0 0 14px;font-size:12px;color:var(--muted)">Esimerkki: PD-kopio Hokusain <i>Suuresta aallosta</i> (Wikimedia Commons).</p>
|
|
196
|
+
|
|
197
|
+
<div class="row color-only hidden" id="row-algo">
|
|
198
|
+
<div style="font-size:13px;margin-bottom:6px">Algoritmi</div>
|
|
199
|
+
<select id="contourMode" style="width:100%">
|
|
200
|
+
<option value="off" selected>perus — vakiintunut</option>
|
|
201
|
+
<option value="smooth">jatkuvuus — vähemmän valereunoja</option>
|
|
202
|
+
<option value="overlay">päällekkäiset muodot (kokeellinen)</option>
|
|
203
|
+
</select>
|
|
204
|
+
<span class="hint" style="display:block;margin-top:6px">perus on oletus ja se, joka toimii useimmissa tapauksissa. Muut ovat lisiä, eivät korvaajia.</span>
|
|
205
|
+
</div>
|
|
206
|
+
<label class="row color-only hidden" id="row-cEdge">Jatkuvuuskynnys <span class="val" id="v-contourEdge">3</span>
|
|
207
|
+
<input id="contourEdge" type="range" min="1" max="24" step="1" value="3">
|
|
208
|
+
<span class="hint"><b>Jatkuvuus</b>-algoritmin tärkein säädin. Tätä pienempi askel on jatkuva, joten nostaminen kokoaa pinnat yhteen ja jättää jäljelle sen mikä oikeasti on reuna — silmät ja kulmakarvat. Liian ylhäällä kohde sulaa taustaansa.</span>
|
|
209
|
+
</label>
|
|
210
|
+
<fieldset>
|
|
211
|
+
<legend>Värit</legend>
|
|
212
|
+
<label class="row">Värimäärä <span class="val" id="v-colorCount">1</span>
|
|
213
|
+
<input id="colorCount" type="range" min="1" max="64" step="1" value="1">
|
|
214
|
+
<span class="hint">1 = mono (kynnys). >1 = posterize N väriin. Mitattuna muotokuvasta paletti kasvaa aina 64:ään asti (16→15, 32→24, 64→38 tasoa) — sen yläpuolella tulee enää muutama taso lisää ja tiedosto kasvaa nopeammin kuin kuva paranee.</span>
|
|
215
|
+
</label>
|
|
216
|
+
<label class="row mono-only">Kynnys <span class="val" id="v-threshold">128</span>
|
|
217
|
+
<input id="threshold" type="range" min="-1" max="255" step="1" value="128">
|
|
218
|
+
<span class="hint">−1 = Otsu. Muuten 0–255.</span>
|
|
219
|
+
</label>
|
|
220
|
+
<label class="row mono-only">Täyttöväri
|
|
221
|
+
<input id="fillHex" type="color" value="#000000">
|
|
222
|
+
</label>
|
|
223
|
+
<label class="check mono-only"><input id="blackOnWhite" type="checkbox" checked> Tumma on etuala</label>
|
|
224
|
+
<label class="row color-only hidden">Paletti
|
|
225
|
+
<select id="paletteMode">
|
|
226
|
+
<option value="auto" selected>auto — kuvasta</option>
|
|
227
|
+
<option value="seeded">omat + auto</option>
|
|
228
|
+
<option value="fixed">vain omat värit</option>
|
|
229
|
+
</select>
|
|
230
|
+
<span class="hint">auto poimii sävyt kuvasta.</span>
|
|
231
|
+
</label>
|
|
232
|
+
<label class="row color-only hidden" id="row-bias">Painotus
|
|
233
|
+
<select id="paletteBias">
|
|
234
|
+
<option value="area" selected>pinta-ala</option>
|
|
235
|
+
<option value="balanced">tasapaino</option>
|
|
236
|
+
<option value="distinct">erottuvuus</option>
|
|
237
|
+
</select>
|
|
238
|
+
<span class="hint">pinta-ala = suurimmat alueet voittavat, tasapaino = pieni kirkas kohde pärjää, erottuvuus = pelkkä väriero.</span>
|
|
239
|
+
</label>
|
|
240
|
+
<div class="row color-only hidden" style="display:block;border-left:2px solid #2c3a55;padding-left:10px;margin-left:2px">
|
|
241
|
+
<span class="hint" style="display:block;margin:0 0 6px"><b>Paletin tavoite.</b> Kvantisointi kertoo mitä värejä kuvassa on; se ei kerro millä väreillä kuva kannattaa maalata, eikä siihen ole tarkkaa osumaa johon napsauttaa. Nämä säätimet ajetaan vasta kun alueet on jo päätetty, joten ne muuttavat vain sen millä maalataan — eivät sitä missä reunat kulkevat. Jokainen on oletusarvollaan identiteetti.</span>
|
|
242
|
+
<label class="row color-only hidden">Värikylläisyyden paino <span class="val" id="v-paletteChroma">0</span>
|
|
243
|
+
<input id="paletteChroma" type="range" min="0" max="400" step="25" value="0">
|
|
244
|
+
<span class="hint">Kuinka paljon värikkyys painaa pinta-alan rinnalla kun paletti valitaan. Pieni kylläinen kohde suurella harmaalla pohjalla ei muuten saa omaa sävyä lainkaan — se ei tule likimääräisenä vaan katoaa. Toimii yhdessä <b>Paletin painotus</b> = <i>erottuvuus</i> kanssa.</span>
|
|
245
|
+
</label>
|
|
246
|
+
<label class="row color-only hidden">Sävyn murto <span class="val" id="v-paletteMute">100</span>
|
|
247
|
+
<input id="paletteMute" type="range" min="30" max="200" step="5" value="100">
|
|
248
|
+
<span class="hint">Paletin kylläisyys prosentteina. Alle 100 murtaa värit harmaata kohti, yli 100 rikastaa. 100 = kuvan omat värit.</span>
|
|
249
|
+
</label>
|
|
250
|
+
<label class="row color-only hidden">Sävypohja <span class="val" id="v-paletteTint">0</span>
|
|
251
|
+
<input id="paletteTint" type="range" min="0" max="80" step="5" value="0">
|
|
252
|
+
<span class="hint">Alaraja kylläisyydelle: mikään sävy ei tule tätä harmaampana, vaan neutraali saa paletin oman keskisävyn vivahteen. Näin paletin voi murtaa harmaata kohti ilman että siitä tulee kuollutta harmaata.</span>
|
|
253
|
+
</label>
|
|
254
|
+
<label class="row color-only hidden">Lämpö <span class="val" id="v-paletteWarm">0</span>
|
|
255
|
+
<input id="paletteWarm" type="range" min="-60" max="60" step="5" value="0">
|
|
256
|
+
<span class="hint">Siirto keltainen–sininen -akselilla. Positiivinen on lämpimämpi. Siirto eikä sävykierto, koska lämpötilan muutos on juuri sitä.</span>
|
|
257
|
+
</label>
|
|
258
|
+
<label class="row color-only hidden">Paletin kontrasti <span class="val" id="v-paletteContrast">100</span>
|
|
259
|
+
<input id="paletteContrast" type="range" min="50" max="160" step="5" value="100">
|
|
260
|
+
<span class="hint">Kuinka kaukana paletin sävyt ovat toisistaan vaaleudeltaan. Ei kosketa kuvan omaa kontrastia, vain sitä miten paletti levittäytyy.</span>
|
|
261
|
+
</label>
|
|
262
|
+
</div>
|
|
263
|
+
<div class="row color-only hidden" id="row-palEdit">
|
|
264
|
+
<div style="margin-bottom:6px">Omat värit</div>
|
|
265
|
+
<p class="pal-note" id="palNote" hidden>Eivät ole nyt käytössä — Paletti on <b>auto</b>. Poimi tuloksesta tai muokkaa väriä, niin ne otetaan käyttöön.</p>
|
|
266
|
+
<div class="pal-edit" id="palEdit"></div>
|
|
267
|
+
<div class="actions" style="margin:0 0 10px">
|
|
268
|
+
<button type="button" id="palAdd" class="pal-add">+ väri</button>
|
|
269
|
+
<button type="button" id="palPick" class="pal-add">Poimi tuloksesta</button>
|
|
270
|
+
<select id="palPreset" style="max-width:170px">
|
|
271
|
+
<option value="">valmis paletti…</option>
|
|
272
|
+
<option value="#000000,#ffffff">musta + valkoinen</option>
|
|
273
|
+
<option value="#000000,#ffffff,#ffd400">musta, valkoinen, keltainen</option>
|
|
274
|
+
<option value="#111111,#555555,#999999,#dddddd">harmaasävy 4</option>
|
|
275
|
+
<option value="#1d1a17,#6b4f3a,#b08968,#e8d7c3">seepia 4</option>
|
|
276
|
+
<option value="#0b132b,#1c7293,#f4a259,#f4f1de">rannikko 4</option>
|
|
277
|
+
</select>
|
|
278
|
+
</div>
|
|
279
|
+
</div>
|
|
280
|
+
<label class="row color-only hidden">Tausta
|
|
281
|
+
<select id="bgMode">
|
|
282
|
+
<option value="auto" selected>auto — reunoilta</option>
|
|
283
|
+
<option value="color">väri — poista nimetty</option>
|
|
284
|
+
<option value="luma">luma</option>
|
|
285
|
+
<option value="none">ei mitään</option>
|
|
286
|
+
</select>
|
|
287
|
+
<span class="hint">auto poistaa vain reunaan yhtenäisen taustan. Jos se kieltäytyy, valitse väri.</span>
|
|
288
|
+
</label>
|
|
289
|
+
<label class="row color-only hidden" id="row-bgColor">Taustaväri
|
|
290
|
+
<input id="bgColor" type="color" value="#ffffff">
|
|
291
|
+
<span class="hint">Tulvitetaan reunoilta; sama väri kuvion sisällä säilyy.</span>
|
|
292
|
+
</label>
|
|
293
|
+
<label class="row color-only hidden" id="row-bgTol">Taustan toleranssi <span class="val" id="v-bgTolerance">24</span>
|
|
294
|
+
<input id="bgTolerance" type="range" min="0" max="90" step="1" value="24">
|
|
295
|
+
<span class="hint">Nosta jos taustassa on liukua tai pakkausroskaa.</span>
|
|
296
|
+
</label>
|
|
297
|
+
<label class="row color-only hidden">skipLuma <span class="val" id="v-skipLuma">250</span>
|
|
298
|
+
<input id="skipLuma" type="range" min="0" max="256" step="1" value="250">
|
|
299
|
+
<span class="hint">Vain luma-tilassa: jätä pois pikselit joiden luma ≥ raja.</span>
|
|
300
|
+
</label>
|
|
301
|
+
<label class="row color-only hidden">Sävyjen yhdistäminen <span class="val" id="v-minColorDelta">10</span>
|
|
302
|
+
<input id="minColorDelta" type="range" min="0" max="40" step="1" value="10">
|
|
303
|
+
<span class="hint">Tätä lähempänä olevat sävyt yhdistetään.</span>
|
|
304
|
+
</label>
|
|
305
|
+
<label class="row color-only hidden" id="row-detailShare">Yksityiskohtien karkeus <span class="val" id="v-detailMinShare">8</span>
|
|
306
|
+
<input id="detailMinShare" type="range" min="0" max="30" step="1" value="8">
|
|
307
|
+
<span class="hint">Silmien kaltaiset kohdat kvantisoidaan omalla paletillaan — sillä kokonaisesta naamasta silmälle jää pari sävyä, yksinään zoomattuna koko budjetti. Tämä on se säädin joka erottaa rakenteen rakeesta: <b>0</b> = kaikki yksityiskohdat ja kaikki rae, <b>8</b> = luomi, ripsiraja ja silmän aukko näkyvät ilman raetta, <b>15+</b> = alkaa syödä rakennettakin.</span>
|
|
308
|
+
</label>
|
|
309
|
+
<label class="row color-only hidden" id="row-detailMerge">Yksityiskohtien värien yhdistäminen <span class="val" id="v-detailColorMerge">16</span>
|
|
310
|
+
<input id="detailColorMerge" type="range" min="0" max="48" step="1" value="16">
|
|
311
|
+
<span class="hint">Yksityiskohta-alueet jakavat yhden paletin, ja tämä on se raja jota lähempänä olevat sävyt ovat sama merkintä siinä. Ilman tätä jokainen alue keksii omat värinsä toisistaan tietämättä. Muotokuvassa <b>0</b> = 456 väriä, <b>16</b> = 97, <b>24</b> = 55 — virhe kasvaa 0,5 %. <b>48</b> yhdistää jo liikaa ja kulmakarvat sinertävät.</span>
|
|
312
|
+
</label>
|
|
313
|
+
<label class="row color-only hidden" id="row-detailMax">Yksityiskohtien värikatto <span class="val" id="v-detailColorMax">12</span>
|
|
314
|
+
<input id="detailColorMax" type="range" min="0" max="64" step="2" value="12">
|
|
315
|
+
<span class="hint">Enintään näin monta väriä yksityiskohta-alueet saavat lisätä yhteensä, eli värejä on korkeintaan <b>värimäärä + tämä</b>. Ilman kattoa jokainen alue tuo omansa: muotokuva jolta pyydettiin 6 sävyä palautti 103. Katolla 12 se on 18, virhettä 3,6 % enemmän. <b>0</b> = ei kattoa.</span>
|
|
316
|
+
</label>
|
|
317
|
+
<label class="check color-only hidden" id="row-detailTrue"><input id="detailTrueColor" type="checkbox"> Tarkat värit yksityiskohdissa
|
|
318
|
+
<span class="hint">Silmien ja hampaiden kaltaisissa kohdissa — missä selvästi eri värit kohtaavat muutaman pikselin sisällä — muoto saa pitää oman mitatun värinsä sen sijaan että se pyöristettäisiin palettiin. Vain näin tila voi lisätä tarkkuutta sen sijaan että piirtäisi uudelleen sen mikä jo on. Muotokuvassa suun virhe pieneni 8,7 %, mutta värejä tuli 10 sijaan 109 — eli värimäärä lakkaa olemasta katto.</span>
|
|
319
|
+
</label>
|
|
320
|
+
<label class="row color-only hidden" id="row-oSim">Samankaltaisuuden raja <span class="val" id="v-overlaySimilar">8</span>
|
|
321
|
+
<input id="overlaySimilar" type="range" min="1" max="40" step="1" value="8">
|
|
322
|
+
<span class="hint">Muoto pysähtyy myös kun se on näin samanlainen kuin alla oleva. Käytä gradienttien kanssa.</span>
|
|
323
|
+
</label>
|
|
324
|
+
<label class="row color-only hidden" id="row-eRun">Reunan vähimmäispituus <span class="val" id="v-edgeMinRun">3</span>
|
|
325
|
+
<input id="edgeMinRun" type="range" min="0" max="12" step="1" value="3">
|
|
326
|
+
<span class="hint">Reuna vaatii <b>ainutlaatuisuuden</b> (voimakkain suunnassaan) ja <b>jatkuvuuden</b> (näin pitkä juova). 0 = pois.</span>
|
|
327
|
+
</label>
|
|
328
|
+
<label class="row color-only hidden" id="row-cSpread">Alueen hajonta <span class="val" id="v-contourSpread">48</span>
|
|
329
|
+
<input id="contourSpread" type="range" min="8" max="160" step="4" value="48">
|
|
330
|
+
<span class="hint">Kuinka kauas alue saa ajautua keskiarvostaan — talutushihna liukuvärille.</span>
|
|
331
|
+
</label>
|
|
332
|
+
<label class="check color-only hidden"><input id="gradientFill" type="checkbox"> Gradienttitäytöt (kokeellinen)</label>
|
|
333
|
+
<label class="row color-only hidden" id="row-gGain">Gradientin kynnys <span class="val" id="v-gradientGain">15</span>
|
|
334
|
+
<input id="gradientGain" type="range" min="0" max="60" step="1" value="15">
|
|
335
|
+
<span class="hint">Liuku korvaa tasaisen värin vain jos se selittää näin monta prosenttia enemmän. Toimii hyvin kuvissa joissa on isoja pehmeitä pintoja; yksityiskohtainen valokuva pilkkoutuu sadoiksi alueiksi ja tulos on huono. Vain SVG-vienti.</span>
|
|
336
|
+
</label>
|
|
337
|
+
<label class="row color-only hidden">Tasot
|
|
338
|
+
<select id="layerMode">
|
|
339
|
+
<option value="stacked" selected>päällekkäin — ei saumoja</option>
|
|
340
|
+
<option value="flat">vierekkäin — pienempi SVG</option>
|
|
341
|
+
</select>
|
|
342
|
+
<span class="hint">Päällekkäin: ei valkoisia saumoja. Vierekkäin: pienempi tiedosto, saumat näkyvät.</span>
|
|
343
|
+
</label>
|
|
344
|
+
<label class="row color-only hidden">Pehmennys <span class="val" id="v-smooth">0</span>
|
|
345
|
+
<input id="smooth" type="range" min="0" max="4" step="1" value="0">
|
|
346
|
+
<span class="hint">Mediaani ennen värien poimintaa. 0 = pois (piirrokset), 2 = valokuva.</span>
|
|
347
|
+
</label>
|
|
348
|
+
<label class="row color-only hidden">Pienin alue <span class="val" id="v-minRegion">6</span>
|
|
349
|
+
<input id="minRegion" type="range" min="0" max="400" step="2" value="6">
|
|
350
|
+
<span class="hint">Tätä pienemmät läiskät sulautuvat ympäristöönsä. Valokuvalle 40–150. <b>Päällekkäiset muodot</b> -algoritmissa tämä on se säädin, joka pitää tuloksen kohtuullisen kokoisena: valokuvassa 6 → 24 pudotti muotojen määrän noin neljäsosaan.</span>
|
|
351
|
+
</label>
|
|
352
|
+
<label class="row color-only hidden">Erottuvuusraja <span class="val" id="v-absorbContrast">40</span>
|
|
353
|
+
<input id="absorbContrast" type="range" min="0" max="120" step="4" value="40">
|
|
354
|
+
<span class="hint">Pienin alue nielaisee myös sen, mikä <i>erottuu</i> ympäristöstään, ellei tämä estä sitä: tätä kauempana olevan sävyn saanut läiskä säilyy koostaan riippumatta. Aallon vaahto ja veneet ovat tällaisia. 0 = vanha ehdoton nielaisu.</span>
|
|
355
|
+
</label>
|
|
356
|
+
<div class="row color-only hidden" style="display:block">
|
|
357
|
+
<div class="actions" style="margin:0 0 4px">
|
|
358
|
+
<button type="button" id="presetArt">Piirros</button>
|
|
359
|
+
<button type="button" id="presetPhoto">Valokuva</button>
|
|
360
|
+
<button type="button" id="presetBroken">Murretut sävyt</button>
|
|
361
|
+
</div>
|
|
362
|
+
<span class="hint" style="display:block;margin-top:6px">Valmiit asetukset. Kukin nollaa myös ne säätimet joita se ei itse aseta, joten presetit eivät kasaudu päällekkäin. <b>Murretut sävyt</b> valitsee paletin sen värien eikä uskollisuuden perusteella.</span>
|
|
363
|
+
</div>
|
|
364
|
+
</fieldset>
|
|
365
|
+
|
|
366
|
+
<fieldset>
|
|
367
|
+
<legend>Jälki</legend>
|
|
368
|
+
<label class="row">turdsize <span class="val" id="v-turdsize">2</span>
|
|
369
|
+
<input id="turdsize" type="range" min="0" max="40" step="1" value="2">
|
|
370
|
+
<span class="hint">Pienet täplät pois (|area| ≤ raja).</span>
|
|
371
|
+
</label>
|
|
372
|
+
<label class="row">alphamax <span class="val" id="v-alphamax">1.00</span>
|
|
373
|
+
<input id="alphamax" type="range" min="0" max="1.34" step="0.01" value="1">
|
|
374
|
+
<span class="hint">Korkeampi → pehmeämpi, vähemmän kulmia.</span>
|
|
375
|
+
</label>
|
|
376
|
+
<label class="row">opttolerance <span class="val" id="v-opttolerance">0.20</span>
|
|
377
|
+
<input id="opttolerance" type="range" min="0" max="2" step="0.05" value="0.2">
|
|
378
|
+
</label>
|
|
379
|
+
<label class="check"><input id="optcurve" type="checkbox" checked> optcurve (kuutiot)</label>
|
|
380
|
+
<label class="row">turnpolicy
|
|
381
|
+
<select id="turnpolicy">
|
|
382
|
+
<option value="minority" selected>minority</option>
|
|
383
|
+
<option value="majority">majority</option>
|
|
384
|
+
<option value="black">black</option>
|
|
385
|
+
<option value="white">white</option>
|
|
386
|
+
<option value="left">left</option>
|
|
387
|
+
<option value="right">right</option>
|
|
388
|
+
</select>
|
|
389
|
+
</label>
|
|
390
|
+
</fieldset>
|
|
391
|
+
|
|
392
|
+
<fieldset>
|
|
393
|
+
<legend>Näkymä</legend>
|
|
394
|
+
<label class="check"><input id="checker" type="checkbox" checked> Shakkiruudukko — näyttää läpinäkyvän</label>
|
|
395
|
+
</fieldset>
|
|
396
|
+
|
|
397
|
+
<fieldset>
|
|
398
|
+
<legend>Esikäsittely</legend>
|
|
399
|
+
<label class="row">Max leveys <span class="val" id="v-maxW">640</span>
|
|
400
|
+
<input id="maxW" type="range" min="160" max="1200" step="10" value="640">
|
|
401
|
+
<span class="hint">Suuret kuvat skaalataan ennen jäljitystä.</span>
|
|
402
|
+
</label>
|
|
403
|
+
</fieldset>
|
|
404
|
+
|
|
405
|
+
<div class="actions">
|
|
406
|
+
<button type="button" class="primary" id="run" disabled>Vektoroi</button>
|
|
407
|
+
<button type="button" id="dl" disabled>Lataa SVG</button>
|
|
408
|
+
</div>
|
|
409
|
+
</div>
|
|
410
|
+
</section>
|
|
411
|
+
|
|
412
|
+
<div>
|
|
413
|
+
<section>
|
|
414
|
+
<div class="split">
|
|
415
|
+
<div class="pane">
|
|
416
|
+
<div class="head">Lähde<span class="grow"></span>
|
|
417
|
+
<button type="button" class="tool" id="preToggle">Esikäsittely</button>
|
|
418
|
+
</div>
|
|
419
|
+
<div class="stage empty" id="srcStage">Ei kuvaa</div>
|
|
420
|
+
</div>
|
|
421
|
+
<div class="pane">
|
|
422
|
+
<div class="head">SVG<span class="grow"></span>
|
|
423
|
+
<button type="button" class="tool" id="editToggle" disabled>Muokkaa</button>
|
|
424
|
+
</div>
|
|
425
|
+
<div class="stage empty" id="outStage">—</div>
|
|
426
|
+
</div>
|
|
427
|
+
</div>
|
|
428
|
+
<div class="editbar" id="editbar">
|
|
429
|
+
<button type="button" class="tool active" id="toolRefine">Tarkennin</button>
|
|
430
|
+
<button type="button" class="tool" id="toolMerge">Yhdistä</button>
|
|
431
|
+
<button type="button" class="tool" id="toolSmooth">Pehmennä</button>
|
|
432
|
+
<button type="button" class="tool" id="toolWand">Taikasauva</button>
|
|
433
|
+
<span class="sep"></span>
|
|
434
|
+
<input type="color" id="editColor" value="#c9a227" title="Yhdistämisen kohdeväri">
|
|
435
|
+
<button type="button" class="tool" id="toolPick">Poimi väri</button>
|
|
436
|
+
<span class="sep"></span>
|
|
437
|
+
<span class="note" id="toolHelp"></span>
|
|
438
|
+
<label class="note wand-only" style="display:flex;align-items:center;gap:6px" title="Kuinka kaukana napsautetusta sävystä valinta saa vielä kasvaa. 0 ottaa vain saman värin, iso luku vie yli sävyrajojen.">Sieto
|
|
439
|
+
<input id="wandTol" type="range" min="0" max="180" step="4" value="40" style="width:80px">
|
|
440
|
+
<span class="val" id="v-wandTol">40</span>
|
|
441
|
+
</label>
|
|
442
|
+
<label class="note wand-only" style="display:flex;align-items:center;gap:6px" title="Rajaa kuvan reunat valitun kohteen ympärille, jolloin tuloksesta tulee ikonin kokoinen."><input type="checkbox" id="wandCrop" checked> rajaa reunoihin</label>
|
|
443
|
+
<button type="button" class="tool wand-only" id="wandCut">Eristä</button>
|
|
444
|
+
<label class="note" style="display:flex;align-items:center;gap:6px" title="Kuinka monta omaa väriä tarkennettu paikka saa keksiä reunaltaan kiinnitettyjen lisäksi. Pieni luku pitää sauman näkymättömänä.">Lisävärit
|
|
445
|
+
<input id="refineExtra" type="range" min="0" max="16" step="1" value="4" style="width:70px">
|
|
446
|
+
<span class="val" id="v-refineExtra">4</span>
|
|
447
|
+
</label>
|
|
448
|
+
<label class="note" style="display:flex;align-items:center;gap:6px" title="Kuinka voimakkaasti reunan muodot palautetaan ympäröiviin sävyihin">Sulautus
|
|
449
|
+
<input id="edgeBlend" type="range" min="0" max="120" step="4" value="90" style="width:70px">
|
|
450
|
+
<span class="val" id="v-edgeBlend">90</span>
|
|
451
|
+
</label>
|
|
452
|
+
<label class="note" style="display:flex;align-items:center;gap:6px">Koko
|
|
453
|
+
<input id="refineSize" type="range" min="12" max="120" step="4" value="40" style="width:90px">
|
|
454
|
+
<span class="val" id="v-refineSize">40</span>px
|
|
455
|
+
</label>
|
|
456
|
+
<span class="sep"></span>
|
|
457
|
+
<button type="button" class="tool" id="editUndo">Kumoa</button>
|
|
458
|
+
<button type="button" class="tool" id="editReset">Palauta</button>
|
|
459
|
+
<span class="note" id="editNote">Napsauta muotoa.</span>
|
|
460
|
+
</div>
|
|
461
|
+
<div class="editbar prebar" id="prebar">
|
|
462
|
+
<div style="display:flex;gap:8px;align-items:center;width:100%">
|
|
463
|
+
<button type="button" class="tool" id="preUndo" disabled>Kumoa</button>
|
|
464
|
+
<button type="button" class="tool" id="preReset">Palauta alkuperäinen</button>
|
|
465
|
+
<span class="note" id="preNote">Kuva on tämä. Säätimet muokkaavat sitä.</span>
|
|
466
|
+
</div>
|
|
467
|
+
<label class="row">Valmis ketju
|
|
468
|
+
<select id="prePreset" style="width:100%">
|
|
469
|
+
<option value="none" selected>ei mitään</option>
|
|
470
|
+
<option value="logo">logo — kontrasti + puhdistus</option>
|
|
471
|
+
<option value="lineart">viivapiirros — harmaa + terävöitys</option>
|
|
472
|
+
<option value="photo">valokuva — kohinanpoisto</option>
|
|
473
|
+
<option value="poster">juliste — posterisointi</option>
|
|
474
|
+
</select>
|
|
475
|
+
<span class="hint">Asettaa alla olevat säätimet. Ne jäävät muokattaviksi.</span>
|
|
476
|
+
</label>
|
|
477
|
+
<label class="check"><input id="preGray" type="checkbox"> Harmaasävy</label>
|
|
478
|
+
<label class="check"><input id="preInvert" type="checkbox"> Käännä värit</label>
|
|
479
|
+
<label class="row">Kirkkaus <span class="val" id="v-preBright">0</span>
|
|
480
|
+
<input id="preBright" type="range" min="-60" max="60" step="2" value="0">
|
|
481
|
+
</label>
|
|
482
|
+
<label class="row">Kontrasti <span class="val" id="v-preContrast">0</span>
|
|
483
|
+
<input id="preContrast" type="range" min="-60" max="120" step="2" value="0">
|
|
484
|
+
</label>
|
|
485
|
+
<label class="row">Gamma <span class="val" id="v-preGamma">100</span>
|
|
486
|
+
<input id="preGamma" type="range" min="30" max="250" step="5" value="100">
|
|
487
|
+
<span class="hint">Väli- ja varjoalueet ilman että ääripäät liikkuvat.</span>
|
|
488
|
+
</label>
|
|
489
|
+
<label class="row">Kylläisyys <span class="val" id="v-preSat">100</span>
|
|
490
|
+
<input id="preSat" type="range" min="0" max="250" step="5" value="100">
|
|
491
|
+
</label>
|
|
492
|
+
<label class="check"><input id="preMedian" type="checkbox"> Poista pilkut (mediaani)
|
|
493
|
+
<span class="hint">Vie yksittäisen kohinapikselin mutta jättää reunan — juuri se mitä sumennus ei osaa.</span>
|
|
494
|
+
</label>
|
|
495
|
+
<label class="row">Sumennus <span class="val" id="v-preBlur">0</span>
|
|
496
|
+
<input id="preBlur" type="range" min="0" max="8" step="1" value="0">
|
|
497
|
+
<span class="hint">Tämä säädin ratkaisee eniten: se päättää kuinka paljon yksityiskohtaa ylipäänsä pääsee vektoroitavaksi.</span>
|
|
498
|
+
</label>
|
|
499
|
+
<label class="row">Terävöitys <span class="val" id="v-preSharpen">0</span>
|
|
500
|
+
<input id="preSharpen" type="range" min="0" max="150" step="5" value="0">
|
|
501
|
+
</label>
|
|
502
|
+
<label class="row">Posterisointi <span class="val" id="v-prePost">0</span>
|
|
503
|
+
<input id="prePost" type="range" min="0" max="16" step="1" value="0">
|
|
504
|
+
<span class="hint">0 = pois. Sävytasoja per kanava <b>ennen</b> kvantisointia — eri asia kuin Värimäärä, joka valitsee paletin.</span>
|
|
505
|
+
</label>
|
|
506
|
+
<p class="hint" style="margin:8px 0 0">Kynnys, värimäärä, kohinanpoisto, reunan voimakkuus ja pienin yksityiskohta ovat jo omina säätiminään alempana — ne tekevät työnsä jäljityksen sisällä, missä ne ovat tarkempia kuin bittikartalle tehtynä.</p>
|
|
507
|
+
</div>
|
|
508
|
+
<div class="palette" id="palette" hidden></div>
|
|
509
|
+
<div id="status">Ladataan tracer-bundlea…</div>
|
|
510
|
+
</section>
|
|
511
|
+
</div>
|
|
512
|
+
</main>
|
|
513
|
+
|
|
514
|
+
<script src="evg_bitmap_tracer.js"></script>
|
|
515
|
+
<script>
|
|
516
|
+
(function () {
|
|
517
|
+
var $ = function (id) { return document.getElementById(id); };
|
|
518
|
+
var file = $("file"), sample = $("sample"), runBtn = $("run"), dlBtn = $("dl");
|
|
519
|
+
var srcStage = $("srcStage"), outStage = $("outStage"), statusEl = $("status"), paletteEl = $("palette");
|
|
520
|
+
var colorCount = $("colorCount"), threshold = $("threshold"), skipLuma = $("skipLuma");
|
|
521
|
+
var bgMode = $("bgMode"), minColorDelta = $("minColorDelta"), minRegion = $("minRegion");
|
|
522
|
+
var absorbContrast = $("absorbContrast");
|
|
523
|
+
var paletteChroma = $("paletteChroma"), paletteMute = $("paletteMute"), paletteTint = $("paletteTint");
|
|
524
|
+
var paletteWarm = $("paletteWarm"), paletteContrast = $("paletteContrast");
|
|
525
|
+
var paletteMode = $("paletteMode"), paletteBias = $("paletteBias"), smooth = $("smooth"), layerMode = $("layerMode");
|
|
526
|
+
var bgColor = $("bgColor"), bgTolerance = $("bgTolerance"), checker = $("checker");
|
|
527
|
+
var contourMode = $("contourMode"), contourEdge = $("contourEdge"), contourSpread = $("contourSpread");
|
|
528
|
+
var gradientFill = $("gradientFill"), gradientGain = $("gradientGain");
|
|
529
|
+
var overlaySimilar = $("overlaySimilar"), edgeMinRun = $("edgeMinRun");
|
|
530
|
+
var detailTrueColor = $("detailTrueColor"), detailMinShare = $("detailMinShare");
|
|
531
|
+
var detailColorMerge = $("detailColorMerge"), detailColorMax = $("detailColorMax");
|
|
532
|
+
var palEdit = $("palEdit"), rowBias = $("row-bias"), rowPalEdit = $("row-palEdit");
|
|
533
|
+
var userPalette = ["#000000", "#ffffff"];
|
|
534
|
+
var turdsize = $("turdsize"), alphamax = $("alphamax"), opttolerance = $("opttolerance");
|
|
535
|
+
var optcurve = $("optcurve"), turnpolicy = $("turnpolicy"), blackOnWhite = $("blackOnWhite");
|
|
536
|
+
var fillHex = $("fillHex"), maxW = $("maxW");
|
|
537
|
+
var monoOnly = document.querySelectorAll(".mono-only");
|
|
538
|
+
var colorOnly = document.querySelectorAll(".color-only");
|
|
539
|
+
|
|
540
|
+
var sourceBitmap = null; // { w, h, rgba: Uint8ClampedArray }
|
|
541
|
+
var processedBitmap = null; // the same pixels after the pre-processing chain
|
|
542
|
+
|
|
543
|
+
// ------------------------------------------------------- esikäsittely
|
|
544
|
+
//
|
|
545
|
+
// Filters that run on the bitmap before anything is quantized or traced. The
|
|
546
|
+
// order is fixed and it matters: exposure first, because everything after it
|
|
547
|
+
// reads brightness; then color; then blur, which is the one that decides how
|
|
548
|
+
// much detail survives at all; then sharpening, which only makes sense on
|
|
549
|
+
// what the blur left; and posterize last, because it is a decision about the
|
|
550
|
+
// final levels rather than a correction.
|
|
551
|
+
//
|
|
552
|
+
// Deliberately not a long list. Most of what a filter menu usually offers is
|
|
553
|
+
// already here as a tracing parameter — the color count IS quantization, the
|
|
554
|
+
// smallest area IS despeckle, mono mode IS threshold, the background section
|
|
555
|
+
// IS background removal — and adding a second way to do the same thing makes
|
|
556
|
+
// a page harder to reason about, not more capable. What is here is the work
|
|
557
|
+
// that has to happen *before* the palette is chosen, because afterwards it is
|
|
558
|
+
// too late.
|
|
559
|
+
function lut(fn) {
|
|
560
|
+
var t = new Uint8Array(256);
|
|
561
|
+
for (var i = 0; i < 256; i++) {
|
|
562
|
+
var v = fn(i / 255) * 255;
|
|
563
|
+
t[i] = v < 0 ? 0 : v > 255 ? 255 : v;
|
|
564
|
+
}
|
|
565
|
+
return t;
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
// Separable box blur run three times ≈ a Gaussian, and it is O(n) per pass
|
|
569
|
+
// rather than O(n·r²), which matters at a radius a person would actually pick.
|
|
570
|
+
function boxBlur(src, w, h, r) {
|
|
571
|
+
if (r < 1) return src;
|
|
572
|
+
var tmp = new Uint8ClampedArray(src.length);
|
|
573
|
+
var out = new Uint8ClampedArray(src);
|
|
574
|
+
for (var pass = 0; pass < 3; pass++) {
|
|
575
|
+
blurAxis(out, tmp, w, h, r, true);
|
|
576
|
+
blurAxis(tmp, out, w, h, r, false);
|
|
577
|
+
}
|
|
578
|
+
return out;
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
function blurAxis(src, dst, w, h, r, horiz) {
|
|
582
|
+
var outer = horiz ? h : w, inner = horiz ? w : h;
|
|
583
|
+
var step = horiz ? 4 : w * 4;
|
|
584
|
+
var jump = horiz ? w * 4 : 4;
|
|
585
|
+
var win = r * 2 + 1;
|
|
586
|
+
for (var o = 0; o < outer; o++) {
|
|
587
|
+
var base = o * jump;
|
|
588
|
+
for (var c = 0; c < 4; c++) {
|
|
589
|
+
var sum = 0;
|
|
590
|
+
for (var k = -r; k <= r; k++) {
|
|
591
|
+
var q = k < 0 ? 0 : k >= inner ? inner - 1 : k;
|
|
592
|
+
sum += src[base + q * step + c];
|
|
593
|
+
}
|
|
594
|
+
for (var i = 0; i < inner; i++) {
|
|
595
|
+
dst[base + i * step + c] = sum / win;
|
|
596
|
+
var addI = i + r + 1, subI = i - r;
|
|
597
|
+
var a = addI >= inner ? inner - 1 : addI;
|
|
598
|
+
var b = subI < 0 ? 0 : subI;
|
|
599
|
+
sum += src[base + a * step + c] - src[base + b * step + c];
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
// Median of the 3x3 neighbourhood, per channel. This is the one that removes
|
|
606
|
+
// a stray pixel without softening the edge it sits next to, which is exactly
|
|
607
|
+
// what a blur cannot do and exactly what JPEG speckle needs.
|
|
608
|
+
function median3(src, w, h) {
|
|
609
|
+
var out = new Uint8ClampedArray(src);
|
|
610
|
+
var v = new Array(9);
|
|
611
|
+
for (var y = 1; y < h - 1; y++) {
|
|
612
|
+
for (var x = 1; x < w - 1; x++) {
|
|
613
|
+
var o = (y * w + x) * 4;
|
|
614
|
+
for (var c = 0; c < 3; c++) {
|
|
615
|
+
var n = 0;
|
|
616
|
+
for (var dy = -1; dy <= 1; dy++) {
|
|
617
|
+
for (var dx = -1; dx <= 1; dx++) v[n++] = src[((y + dy) * w + (x + dx)) * 4 + c];
|
|
618
|
+
}
|
|
619
|
+
v.sort(function (a, b) { return a - b; });
|
|
620
|
+
out[o + c] = v[4];
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
return out;
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
function preprocess(src, w, h, o) {
|
|
628
|
+
var out = new Uint8ClampedArray(src);
|
|
629
|
+
var i, n = w * h;
|
|
630
|
+
|
|
631
|
+
// --- exposure: brightness, contrast, gamma, as one lookup ---
|
|
632
|
+
if (o.brightness !== 0 || o.contrast !== 0 || o.gamma !== 100) {
|
|
633
|
+
var g = Math.max(0.05, o.gamma / 100);
|
|
634
|
+
var cc = o.contrast / 100;
|
|
635
|
+
var t = lut(function (u) {
|
|
636
|
+
u = u + o.brightness / 100;
|
|
637
|
+
u = (u - 0.5) * (1 + cc) + 0.5;
|
|
638
|
+
return Math.pow(u < 0 ? 0 : u > 1 ? 1 : u, 1 / g);
|
|
639
|
+
});
|
|
640
|
+
for (i = 0; i < n; i++) {
|
|
641
|
+
var q = i * 4;
|
|
642
|
+
out[q] = t[out[q]]; out[q + 1] = t[out[q + 1]]; out[q + 2] = t[out[q + 2]];
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
// --- color: saturation, then grayscale as its limit ---
|
|
647
|
+
if (o.grayscale || o.saturation !== 100) {
|
|
648
|
+
var sat = o.grayscale ? 0 : o.saturation / 100;
|
|
649
|
+
for (i = 0; i < n; i++) {
|
|
650
|
+
var p = i * 4;
|
|
651
|
+
var lum = 0.2126 * out[p] + 0.7152 * out[p + 1] + 0.0722 * out[p + 2];
|
|
652
|
+
out[p] = lum + (out[p] - lum) * sat;
|
|
653
|
+
out[p + 1] = lum + (out[p + 1] - lum) * sat;
|
|
654
|
+
out[p + 2] = lum + (out[p + 2] - lum) * sat;
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
// --- detail: median first (specks), then blur (everything small) ---
|
|
659
|
+
if (o.median) out = median3(out, w, h);
|
|
660
|
+
if (o.blur > 0) out = boxBlur(out, w, h, o.blur);
|
|
661
|
+
|
|
662
|
+
// --- sharpen: unsharp mask against a blur of what is left ---
|
|
663
|
+
if (o.sharpen > 0) {
|
|
664
|
+
var soft = boxBlur(out, w, h, 1);
|
|
665
|
+
var amt = o.sharpen / 100;
|
|
666
|
+
for (i = 0; i < n * 4; i++) {
|
|
667
|
+
if ((i & 3) === 3) continue;
|
|
668
|
+
out[i] = out[i] + (out[i] - soft[i]) * amt;
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
// --- posterize: a decision about levels, so it goes last ---
|
|
673
|
+
if (o.posterize > 1) {
|
|
674
|
+
var steps = o.posterize - 1;
|
|
675
|
+
var pt = lut(function (u) { return Math.round(u * steps) / steps; });
|
|
676
|
+
for (i = 0; i < n; i++) {
|
|
677
|
+
var z = i * 4;
|
|
678
|
+
out[z] = pt[out[z]]; out[z + 1] = pt[out[z + 1]]; out[z + 2] = pt[out[z + 2]];
|
|
679
|
+
}
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
if (o.invert) {
|
|
683
|
+
for (i = 0; i < n; i++) {
|
|
684
|
+
var w4 = i * 4;
|
|
685
|
+
out[w4] = 255 - out[w4]; out[w4 + 1] = 255 - out[w4 + 1]; out[w4 + 2] = 255 - out[w4 + 2];
|
|
686
|
+
}
|
|
687
|
+
}
|
|
688
|
+
return out;
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
var lastSvg = "";
|
|
692
|
+
var lastPalette = [];
|
|
693
|
+
var debounceTimer = null;
|
|
694
|
+
var busy = false;
|
|
695
|
+
var pending = false;
|
|
696
|
+
|
|
697
|
+
function setStatus(msg, kind) {
|
|
698
|
+
statusEl.textContent = msg;
|
|
699
|
+
statusEl.className = kind || "";
|
|
700
|
+
statusHead = "";
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
// The half of the status line that describes the drawing rather than the
|
|
704
|
+
// trace. An edit changes the shapes and the bytes, and the line said what
|
|
705
|
+
// they were when the tracer finished — so after an hour of editing it was
|
|
706
|
+
// quoting a file that no longer existed.
|
|
707
|
+
var statusHead = "";
|
|
708
|
+
function refreshSize() {
|
|
709
|
+
if (!statusHead) return;
|
|
710
|
+
var live = outStage.querySelector("svg");
|
|
711
|
+
if (!live) return;
|
|
712
|
+
var text = new XMLSerializer().serializeToString(live);
|
|
713
|
+
var shapes = live.querySelectorAll("path").length;
|
|
714
|
+
statusEl.innerHTML = statusHead.replace(/layers=\d+/, "layers=" + shapes)
|
|
715
|
+
+ " · svg=" + text.length + " B";
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
function bindRange(input, label, fmt) {
|
|
719
|
+
function sync() {
|
|
720
|
+
label.textContent = fmt ? fmt(+input.value) : String(input.value);
|
|
721
|
+
}
|
|
722
|
+
input.addEventListener("input", function () {
|
|
723
|
+
sync();
|
|
724
|
+
schedule();
|
|
725
|
+
});
|
|
726
|
+
sync();
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
bindRange(colorCount, $("v-colorCount"));
|
|
730
|
+
bindRange(threshold, $("v-threshold"), function (v) { return v < 0 ? "Otsu" : String(v); });
|
|
731
|
+
bindRange(skipLuma, $("v-skipLuma"));
|
|
732
|
+
bindRange(minColorDelta, $("v-minColorDelta"));
|
|
733
|
+
bindRange(minRegion, $("v-minRegion"));
|
|
734
|
+
bindRange(absorbContrast, $("v-absorbContrast"));
|
|
735
|
+
bindRange(paletteChroma, $("v-paletteChroma"));
|
|
736
|
+
bindRange(paletteMute, $("v-paletteMute"));
|
|
737
|
+
bindRange(paletteTint, $("v-paletteTint"));
|
|
738
|
+
bindRange(paletteWarm, $("v-paletteWarm"));
|
|
739
|
+
bindRange(paletteContrast, $("v-paletteContrast"));
|
|
740
|
+
bindRange(smooth, $("v-smooth"));
|
|
741
|
+
bindRange(bgTolerance, $("v-bgTolerance"));
|
|
742
|
+
bindRange(contourEdge, $("v-contourEdge"));
|
|
743
|
+
bindRange(contourSpread, $("v-contourSpread"));
|
|
744
|
+
bindRange(gradientGain, $("v-gradientGain"));
|
|
745
|
+
bindRange(overlaySimilar, $("v-overlaySimilar"));
|
|
746
|
+
bindRange(detailMinShare, $("v-detailMinShare"));
|
|
747
|
+
bindRange(detailColorMerge, $("v-detailColorMerge"));
|
|
748
|
+
bindRange(detailColorMax, $("v-detailColorMax"));
|
|
749
|
+
bindRange(edgeMinRun, $("v-edgeMinRun"));
|
|
750
|
+
|
|
751
|
+
function syncChecker() {
|
|
752
|
+
[srcStage, outStage].forEach(function (el) {
|
|
753
|
+
el.classList.toggle("checker", checker.checked);
|
|
754
|
+
});
|
|
755
|
+
}
|
|
756
|
+
checker.addEventListener("change", syncChecker);
|
|
757
|
+
syncChecker();
|
|
758
|
+
|
|
759
|
+
// Photographic noise needs both knobs at once, so offer them as a pair.
|
|
760
|
+
// A preset is a set of controls, not two of them: leaving the palette dials
|
|
761
|
+
// where the last preset put them is how a "Piirros" comes back graded.
|
|
762
|
+
function applyPreset(vals) {
|
|
763
|
+
var defaults = {
|
|
764
|
+
smooth: 0, minRegion: 6, absorbContrast: 40,
|
|
765
|
+
paletteChroma: 0, paletteMute: 100, paletteTint: 0,
|
|
766
|
+
paletteWarm: 0, paletteContrast: 100, paletteBias: "area"
|
|
767
|
+
};
|
|
768
|
+
var touched = [];
|
|
769
|
+
Object.keys(defaults).forEach(function (id) {
|
|
770
|
+
var el = $(id);
|
|
771
|
+
if (!el) return;
|
|
772
|
+
var v = vals.hasOwnProperty(id) ? vals[id] : defaults[id];
|
|
773
|
+
el.value = String(v);
|
|
774
|
+
touched.push(el);
|
|
775
|
+
});
|
|
776
|
+
touched.forEach(function (el) {
|
|
777
|
+
el.dispatchEvent(new Event("input", { bubbles: true }));
|
|
778
|
+
el.dispatchEvent(new Event("change", { bubbles: true }));
|
|
779
|
+
});
|
|
780
|
+
}
|
|
781
|
+
$("presetArt").addEventListener("click", function () { applyPreset({ smooth: 0, minRegion: 6 }); });
|
|
782
|
+
$("presetPhoto").addEventListener("click", function () { applyPreset({ smooth: 2, minRegion: 60, absorbContrast: 56 }); });
|
|
783
|
+
// The palette chosen for its colors rather than its fidelity — the setting
|
|
784
|
+
// that puts the ochre of the boat hulls into the Hokusai example at all.
|
|
785
|
+
$("presetBroken").addEventListener("click", function () {
|
|
786
|
+
applyPreset({ smooth: 0, minRegion: 4, absorbContrast: 24, paletteBias: "distinct",
|
|
787
|
+
paletteChroma: 300, paletteMute: 130, paletteTint: 30 });
|
|
788
|
+
});
|
|
789
|
+
bindRange(turdsize, $("v-turdsize"));
|
|
790
|
+
bindRange(alphamax, $("v-alphamax"), function (v) { return v.toFixed(2); });
|
|
791
|
+
bindRange(opttolerance, $("v-opttolerance"), function (v) { return v.toFixed(2); });
|
|
792
|
+
bindRange(maxW, $("v-maxW"));
|
|
793
|
+
|
|
794
|
+
// Editing a color while the palette is on "auto" is the user asking for that
|
|
795
|
+
// color, not a no-op: switch the mode over rather than silently ignoring it.
|
|
796
|
+
function activatePalette() {
|
|
797
|
+
if (paletteMode.value === "auto") {
|
|
798
|
+
paletteMode.value = "fixed";
|
|
799
|
+
syncMode();
|
|
800
|
+
}
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
// The editable list of colors for the "omat" / "omat + auto" palette modes.
|
|
804
|
+
function renderPalEdit() {
|
|
805
|
+
palEdit.innerHTML = "";
|
|
806
|
+
if (!userPalette.length) {
|
|
807
|
+
var empty = document.createElement("span");
|
|
808
|
+
empty.className = "pal-empty";
|
|
809
|
+
empty.textContent = "ei värejä — paletti poimitaan kuvasta";
|
|
810
|
+
palEdit.appendChild(empty);
|
|
811
|
+
return;
|
|
812
|
+
}
|
|
813
|
+
userPalette.forEach(function (hex, i) {
|
|
814
|
+
var chip = document.createElement("span");
|
|
815
|
+
chip.className = "pal-chip";
|
|
816
|
+
var inp = document.createElement("input");
|
|
817
|
+
inp.type = "color";
|
|
818
|
+
inp.value = hex;
|
|
819
|
+
inp.title = hex;
|
|
820
|
+
inp.addEventListener("input", function () {
|
|
821
|
+
userPalette[i] = inp.value;
|
|
822
|
+
inp.title = inp.value;
|
|
823
|
+
activatePalette();
|
|
824
|
+
schedule();
|
|
825
|
+
});
|
|
826
|
+
var del = document.createElement("button");
|
|
827
|
+
del.type = "button";
|
|
828
|
+
del.textContent = "×";
|
|
829
|
+
del.title = "Poista";
|
|
830
|
+
del.addEventListener("click", function () {
|
|
831
|
+
userPalette.splice(i, 1);
|
|
832
|
+
renderPalEdit();
|
|
833
|
+
schedule();
|
|
834
|
+
});
|
|
835
|
+
chip.appendChild(inp);
|
|
836
|
+
chip.appendChild(del);
|
|
837
|
+
palEdit.appendChild(chip);
|
|
838
|
+
});
|
|
839
|
+
}
|
|
840
|
+
|
|
841
|
+
$("palAdd").addEventListener("click", function () {
|
|
842
|
+
userPalette.push("#808080");
|
|
843
|
+
renderPalEdit();
|
|
844
|
+
activatePalette();
|
|
845
|
+
schedule();
|
|
846
|
+
});
|
|
847
|
+
$("palPreset").addEventListener("change", function (e) {
|
|
848
|
+
if (!e.target.value) return;
|
|
849
|
+
userPalette = e.target.value.split(",");
|
|
850
|
+
e.target.value = "";
|
|
851
|
+
renderPalEdit();
|
|
852
|
+
activatePalette();
|
|
853
|
+
schedule();
|
|
854
|
+
});
|
|
855
|
+
// Take the palette the quantizer just produced and make it editable.
|
|
856
|
+
function adoptPalette(hexes) {
|
|
857
|
+
if (!hexes || !hexes.length) return;
|
|
858
|
+
userPalette = hexes.slice();
|
|
859
|
+
renderPalEdit();
|
|
860
|
+
activatePalette();
|
|
861
|
+
schedule();
|
|
862
|
+
}
|
|
863
|
+
$("palPick").addEventListener("click", function () { adoptPalette(lastPalette); });
|
|
864
|
+
|
|
865
|
+
function syncMode() {
|
|
866
|
+
var color = +colorCount.value > 1;
|
|
867
|
+
monoOnly.forEach(function (el) { el.classList.toggle("hidden", color); });
|
|
868
|
+
colorOnly.forEach(function (el) { el.classList.toggle("hidden", !color); });
|
|
869
|
+
// skipLuma only does anything in luma mode.
|
|
870
|
+
skipLuma.closest("label").classList.toggle("hidden", !color || bgMode.value !== "luma");
|
|
871
|
+
$("row-bgColor").classList.toggle("hidden", !color || bgMode.value !== "color");
|
|
872
|
+
var floods = bgMode.value === "auto" || bgMode.value === "color";
|
|
873
|
+
$("row-bgTol").classList.toggle("hidden", !color || !floods);
|
|
874
|
+
var cont = contourMode.value !== "off";
|
|
875
|
+
$("row-oSim").classList.toggle("hidden", !color || contourMode.value !== "overlay");
|
|
876
|
+
$("row-detailTrue").classList.toggle("hidden", !color || contourMode.value !== "overlay");
|
|
877
|
+
$("row-detailShare").classList.toggle("hidden", !color || contourMode.value !== "overlay");
|
|
878
|
+
$("row-detailMerge").classList.toggle("hidden", !color || contourMode.value !== "overlay");
|
|
879
|
+
$("row-detailMax").classList.toggle("hidden", !color || contourMode.value !== "overlay");
|
|
880
|
+
$("row-cEdge").classList.toggle("hidden", !color || !cont);
|
|
881
|
+
$("row-cSpread").classList.toggle("hidden", !color || !cont);
|
|
882
|
+
$("row-eRun").classList.toggle("hidden", !color || !cont);
|
|
883
|
+
gradientFill.closest("label").classList.toggle("hidden", !color);
|
|
884
|
+
$("row-gGain").classList.toggle("hidden", !color || !gradientFill.checked);
|
|
885
|
+
var mode = paletteMode.value;
|
|
886
|
+
rowBias.classList.toggle("hidden", !color || mode === "fixed");
|
|
887
|
+
// Kept on screen even on "auto" — it is how you reach "Poimi tuloksesta",
|
|
888
|
+
// and hiding it is what made the controls look inert.
|
|
889
|
+
rowPalEdit.classList.toggle("hidden", !color);
|
|
890
|
+
rowPalEdit.classList.toggle("pal-inactive", mode === "auto");
|
|
891
|
+
$("palNote").hidden = mode !== "auto";
|
|
892
|
+
// "omat" replaces the count; "omat + auto" still fills up to it.
|
|
893
|
+
colorCount.closest("label").classList.toggle("hidden", color && mode === "fixed");
|
|
894
|
+
}
|
|
895
|
+
colorCount.addEventListener("input", syncMode);
|
|
896
|
+
bgMode.addEventListener("change", syncMode);
|
|
897
|
+
paletteMode.addEventListener("change", syncMode);
|
|
898
|
+
contourMode.addEventListener("change", syncMode);
|
|
899
|
+
gradientFill.addEventListener("change", syncMode);
|
|
900
|
+
renderPalEdit();
|
|
901
|
+
syncMode();
|
|
902
|
+
|
|
903
|
+
["optcurve", "blackOnWhite", "turnpolicy", "fillHex", "bgMode", "paletteMode", "paletteBias", "layerMode", "bgColor", "contourMode", "gradientFill", "detailTrueColor"].forEach(function (id) {
|
|
904
|
+
$(id).addEventListener("change", schedule);
|
|
905
|
+
});
|
|
906
|
+
|
|
907
|
+
if (typeof EvgBitmapTracer !== "function" || typeof ImageBuffer !== "function") {
|
|
908
|
+
setStatus("Tracer-bundlea ei ladattu (EvgBitmapTracer / ImageBuffer puuttuu).", "bad");
|
|
909
|
+
return;
|
|
910
|
+
}
|
|
911
|
+
setStatus("Valmis. Lataa kuva tai kokeile esimerkkiä.");
|
|
912
|
+
runBtn.disabled = false;
|
|
913
|
+
|
|
914
|
+
function schedule() {
|
|
915
|
+
if (!sourceBitmap) return;
|
|
916
|
+
clearTimeout(debounceTimer);
|
|
917
|
+
debounceTimer = setTimeout(function () { vectorize(); }, 280);
|
|
918
|
+
}
|
|
919
|
+
|
|
920
|
+
function loadFile(blob, name) {
|
|
921
|
+
var url = URL.createObjectURL(blob);
|
|
922
|
+
var img = new Image();
|
|
923
|
+
img.onload = function () {
|
|
924
|
+
var lim = +maxW.value;
|
|
925
|
+
var w = img.naturalWidth, h = img.naturalHeight;
|
|
926
|
+
if (w > lim) { h = Math.round(h * lim / w); w = lim; }
|
|
927
|
+
var canvas = document.createElement("canvas");
|
|
928
|
+
canvas.width = w; canvas.height = h;
|
|
929
|
+
var ctx = canvas.getContext("2d", { willReadFrequently: true });
|
|
930
|
+
ctx.drawImage(img, 0, 0, w, h);
|
|
931
|
+
var data = ctx.getImageData(0, 0, w, h);
|
|
932
|
+
sourceBitmap = { w: w, h: h, rgba: data.data, name: name || "image" };
|
|
933
|
+
srcStage.classList.remove("empty");
|
|
934
|
+
srcStage.innerHTML = "";
|
|
935
|
+
var preview = document.createElement("img");
|
|
936
|
+
preview.alt = "Lähde";
|
|
937
|
+
preview.src = canvas.toDataURL("image/png");
|
|
938
|
+
srcStage.appendChild(preview);
|
|
939
|
+
URL.revokeObjectURL(url);
|
|
940
|
+
runBtn.disabled = false;
|
|
941
|
+
vectorize();
|
|
942
|
+
};
|
|
943
|
+
img.onerror = function () {
|
|
944
|
+
URL.revokeObjectURL(url);
|
|
945
|
+
setStatus("Kuvan lukeminen epäonnistui.", "bad");
|
|
946
|
+
};
|
|
947
|
+
img.src = url;
|
|
948
|
+
}
|
|
949
|
+
|
|
950
|
+
file.addEventListener("change", function () {
|
|
951
|
+
var f = file.files && file.files[0];
|
|
952
|
+
if (f) loadFile(f, f.name);
|
|
953
|
+
});
|
|
954
|
+
|
|
955
|
+
// Paste an image straight onto the page — a screenshot, or a copy from
|
|
956
|
+
// another tab. Chrome and Safari put a File on the clipboard; Firefox may
|
|
957
|
+
// offer only an image/* string, which is a data: or blob: URL we can fetch.
|
|
958
|
+
function imageFromClipboard(data) {
|
|
959
|
+
if (!data) return null;
|
|
960
|
+
var items = data.items || [];
|
|
961
|
+
for (var i = 0; i < items.length; i++) {
|
|
962
|
+
if (items[i].kind === "file" && /^image\//.test(items[i].type)) {
|
|
963
|
+
var f = items[i].getAsFile();
|
|
964
|
+
if (f) return { blob: f, name: f.name || "leikepöytä.png" };
|
|
965
|
+
}
|
|
966
|
+
}
|
|
967
|
+
var files = data.files || [];
|
|
968
|
+
for (var j = 0; j < files.length; j++) {
|
|
969
|
+
if (/^image\//.test(files[j].type)) {
|
|
970
|
+
return { blob: files[j], name: files[j].name || "leikepöytä.png" };
|
|
971
|
+
}
|
|
972
|
+
}
|
|
973
|
+
return null;
|
|
974
|
+
}
|
|
975
|
+
|
|
976
|
+
document.addEventListener("paste", function (e) {
|
|
977
|
+
// Never steal a paste that belongs to a field the user is typing in.
|
|
978
|
+
var el = document.activeElement;
|
|
979
|
+
if (el && (el.tagName === "INPUT" || el.tagName === "TEXTAREA") &&
|
|
980
|
+
el.type !== "color" && el.type !== "range" && el.type !== "checkbox") {
|
|
981
|
+
return;
|
|
982
|
+
}
|
|
983
|
+
var found = imageFromClipboard(e.clipboardData);
|
|
984
|
+
if (found) {
|
|
985
|
+
e.preventDefault();
|
|
986
|
+
setStatus("Luetaan leikepöydältä…");
|
|
987
|
+
loadFile(found.blob, found.name);
|
|
988
|
+
return;
|
|
989
|
+
}
|
|
990
|
+
// Firefox path: an image/* entry that is a URL rather than a File.
|
|
991
|
+
var items = e.clipboardData ? e.clipboardData.items || [] : [];
|
|
992
|
+
for (var k = 0; k < items.length; k++) {
|
|
993
|
+
if (items[k].kind === "string" && /^image\//.test(items[k].type)) {
|
|
994
|
+
e.preventDefault();
|
|
995
|
+
items[k].getAsString(function (str) {
|
|
996
|
+
setStatus("Luetaan leikepöydältä…");
|
|
997
|
+
fetch(str).then(function (r) { return r.blob(); })
|
|
998
|
+
.then(function (b) { loadFile(b, "leikepöytä.png"); })
|
|
999
|
+
.catch(function () { setStatus("Leikepöydän kuvaa ei saatu luettua.", "bad"); });
|
|
1000
|
+
});
|
|
1001
|
+
return;
|
|
1002
|
+
}
|
|
1003
|
+
}
|
|
1004
|
+
});
|
|
1005
|
+
|
|
1006
|
+
sample.addEventListener("click", function () {
|
|
1007
|
+
fetch("sample.png").then(function (r) {
|
|
1008
|
+
if (!r.ok) throw new Error("HTTP " + r.status);
|
|
1009
|
+
return r.blob();
|
|
1010
|
+
}).then(function (b) { loadFile(b, "sample.png"); })
|
|
1011
|
+
.catch(function (e) { setStatus("Esimerkkikuvaa ei saatu: " + e.message, "bad"); });
|
|
1012
|
+
});
|
|
1013
|
+
|
|
1014
|
+
// --- pre-processing controls -------------------------------------------
|
|
1015
|
+
var preGray = $("preGray"), preInvert = $("preInvert"), preMedian = $("preMedian");
|
|
1016
|
+
var preBright = $("preBright"), preContrast = $("preContrast"), preGamma = $("preGamma");
|
|
1017
|
+
var preSat = $("preSat"), preBlur = $("preBlur"), preSharpen = $("preSharpen");
|
|
1018
|
+
var prePost = $("prePost"), prePreset = $("prePreset");
|
|
1019
|
+
|
|
1020
|
+
// Undo steps over settings, not over pixels: the chain is a set of values, so
|
|
1021
|
+
// stepping back one is putting the previous set of values back. Snapshots are
|
|
1022
|
+
// taken on "change" rather than "input", so dragging a slider from one place
|
|
1023
|
+
// to another is one step to undo rather than forty.
|
|
1024
|
+
var PRE_IDS = ["preGray", "preInvert", "preMedian", "preBright", "preContrast",
|
|
1025
|
+
"preGamma", "preSat", "preBlur", "preSharpen", "prePost"];
|
|
1026
|
+
|
|
1027
|
+
function preSnapshot() {
|
|
1028
|
+
var q = {};
|
|
1029
|
+
PRE_IDS.forEach(function (id) {
|
|
1030
|
+
var el = $(id);
|
|
1031
|
+
q[id] = el.type === "checkbox" ? el.checked : el.value;
|
|
1032
|
+
});
|
|
1033
|
+
return q;
|
|
1034
|
+
}
|
|
1035
|
+
|
|
1036
|
+
function preApply(q) {
|
|
1037
|
+
PRE_IDS.forEach(function (id) {
|
|
1038
|
+
var el = $(id);
|
|
1039
|
+
if (el.type === "checkbox") el.checked = q[id];
|
|
1040
|
+
else { el.value = q[id]; el.dispatchEvent(new Event("input", { bubbles: true })); }
|
|
1041
|
+
});
|
|
1042
|
+
}
|
|
1043
|
+
|
|
1044
|
+
var preStack = [], prePrev = null;
|
|
1045
|
+
|
|
1046
|
+
function preRemember() {
|
|
1047
|
+
if (prePrev) preStack.push(prePrev);
|
|
1048
|
+
prePrev = preSnapshot();
|
|
1049
|
+
$("preUndo").disabled = preStack.length === 0;
|
|
1050
|
+
}
|
|
1051
|
+
|
|
1052
|
+
|
|
1053
|
+
[[preBright, "v-preBright"], [preContrast, "v-preContrast"], [preGamma, "v-preGamma"],
|
|
1054
|
+
[preSat, "v-preSat"], [preBlur, "v-preBlur"], [preSharpen, "v-preSharpen"],
|
|
1055
|
+
[prePost, "v-prePost"]].forEach(function (pair) {
|
|
1056
|
+
bindRange(pair[0], $(pair[1]));
|
|
1057
|
+
});
|
|
1058
|
+
[preGray, preInvert, preMedian].forEach(function (el) {
|
|
1059
|
+
el.addEventListener("change", function () { preRemember(); schedule(); });
|
|
1060
|
+
});
|
|
1061
|
+
PRE_IDS.forEach(function (id) {
|
|
1062
|
+
var el = $(id);
|
|
1063
|
+
if (el.type !== "checkbox") el.addEventListener("change", preRemember);
|
|
1064
|
+
});
|
|
1065
|
+
|
|
1066
|
+
$("preUndo").addEventListener("click", function () {
|
|
1067
|
+
var q = preStack.pop();
|
|
1068
|
+
if (!q) return;
|
|
1069
|
+
preApply(q);
|
|
1070
|
+
prePrev = preSnapshot();
|
|
1071
|
+
$("preUndo").disabled = preStack.length === 0;
|
|
1072
|
+
schedule();
|
|
1073
|
+
});
|
|
1074
|
+
|
|
1075
|
+
$("preReset").addEventListener("click", function () {
|
|
1076
|
+
preRemember();
|
|
1077
|
+
prePreset.value = "none";
|
|
1078
|
+
preApply(PRE_SNAP_NONE);
|
|
1079
|
+
schedule();
|
|
1080
|
+
});
|
|
1081
|
+
|
|
1082
|
+
var PRE_PRESETS = {
|
|
1083
|
+
none: { gray: false, invert: false, median: false, bright: 0, contrast: 0, gamma: 100, sat: 100, blur: 0, sharpen: 0, post: 0 },
|
|
1084
|
+
logo: { gray: false, invert: false, median: true, bright: 0, contrast: 60, gamma: 100, sat: 120, blur: 1, sharpen: 0, post: 0 },
|
|
1085
|
+
lineart: { gray: true, invert: false, median: true, bright: 0, contrast: 80, gamma: 90, sat: 100, blur: 0, sharpen: 60, post: 0 },
|
|
1086
|
+
photo: { gray: false, invert: false, median: true, bright: 0, contrast: 10, gamma: 100, sat: 100, blur: 2, sharpen: 0, post: 0 },
|
|
1087
|
+
poster: { gray: false, invert: false, median: false, bright: 0, contrast: 30, gamma: 100, sat: 130, blur: 1, sharpen: 0, post: 5 }
|
|
1088
|
+
};
|
|
1089
|
+
|
|
1090
|
+
var PRE_SNAP_NONE = { preGray: false, preInvert: false, preMedian: false,
|
|
1091
|
+
preBright: "0", preContrast: "0", preGamma: "100", preSat: "100",
|
|
1092
|
+
preBlur: "0", preSharpen: "0", prePost: "0" };
|
|
1093
|
+
prePrev = preSnapshot();
|
|
1094
|
+
|
|
1095
|
+
prePreset.addEventListener("change", function () {
|
|
1096
|
+
var q = PRE_PRESETS[prePreset.value];
|
|
1097
|
+
if (!q) return;
|
|
1098
|
+
preRemember();
|
|
1099
|
+
preGray.checked = q.gray; preInvert.checked = q.invert; preMedian.checked = q.median;
|
|
1100
|
+
preBright.value = q.bright; preContrast.value = q.contrast; preGamma.value = q.gamma;
|
|
1101
|
+
preSat.value = q.sat; preBlur.value = q.blur; preSharpen.value = q.sharpen;
|
|
1102
|
+
prePost.value = q.post;
|
|
1103
|
+
[preBright, preContrast, preGamma, preSat, preBlur, preSharpen, prePost].forEach(function (el) {
|
|
1104
|
+
el.dispatchEvent(new Event("input", { bubbles: true }));
|
|
1105
|
+
});
|
|
1106
|
+
});
|
|
1107
|
+
|
|
1108
|
+
// The pixels everything downstream reads. Pre-processing edits the picture,
|
|
1109
|
+
// so the tracer, the refiner and anything else that samples the bitmap have
|
|
1110
|
+
// to sample the edited one — the refiner did not, and re-traced a blurred
|
|
1111
|
+
// picture's patch out of the original, which puts detail back exactly where
|
|
1112
|
+
// the filters were asked to take it away.
|
|
1113
|
+
function workingPixels() {
|
|
1114
|
+
return processedBitmap || sourceBitmap.rgba;
|
|
1115
|
+
}
|
|
1116
|
+
|
|
1117
|
+
function preOptions() {
|
|
1118
|
+
return {
|
|
1119
|
+
grayscale: preGray.checked, invert: preInvert.checked, median: preMedian.checked,
|
|
1120
|
+
brightness: +preBright.value, contrast: +preContrast.value, gamma: +preGamma.value,
|
|
1121
|
+
saturation: +preSat.value, blur: +preBlur.value, sharpen: +preSharpen.value,
|
|
1122
|
+
posterize: +prePost.value
|
|
1123
|
+
};
|
|
1124
|
+
}
|
|
1125
|
+
|
|
1126
|
+
function preIsIdentity(o) {
|
|
1127
|
+
return !o.grayscale && !o.invert && !o.median && o.brightness === 0 && o.contrast === 0
|
|
1128
|
+
&& o.gamma === 100 && o.saturation === 100 && o.blur === 0 && o.sharpen === 0
|
|
1129
|
+
&& o.posterize === 0;
|
|
1130
|
+
}
|
|
1131
|
+
|
|
1132
|
+
// Run the chain and show the result *as* the picture. There is one image, not
|
|
1133
|
+
// an original and a copy of it side by side: the filters edit what you are
|
|
1134
|
+
// working on, and the original is a click away rather than a pane away. Two
|
|
1135
|
+
// panes of the same photograph cost more room than they explain.
|
|
1136
|
+
function buildProcessed() {
|
|
1137
|
+
if (!sourceBitmap) { processedBitmap = null; return; }
|
|
1138
|
+
var o = preOptions();
|
|
1139
|
+
processedBitmap = preIsIdentity(o) ? null
|
|
1140
|
+
: preprocess(sourceBitmap.rgba, sourceBitmap.w, sourceBitmap.h, o);
|
|
1141
|
+
var cv = document.createElement("canvas");
|
|
1142
|
+
cv.width = sourceBitmap.w; cv.height = sourceBitmap.h;
|
|
1143
|
+
cv.getContext("2d").putImageData(
|
|
1144
|
+
new ImageData(processedBitmap || sourceBitmap.rgba, sourceBitmap.w, sourceBitmap.h), 0, 0);
|
|
1145
|
+
srcStage.classList.remove("empty");
|
|
1146
|
+
srcStage.innerHTML = "";
|
|
1147
|
+
var im = document.createElement("img");
|
|
1148
|
+
im.alt = "Lähde";
|
|
1149
|
+
im.src = cv.toDataURL("image/png");
|
|
1150
|
+
srcStage.appendChild(im);
|
|
1151
|
+
$("preNote").textContent = processedBitmap
|
|
1152
|
+
? "Muokattu — Palauta alkuperäinen tuo sen takaisin."
|
|
1153
|
+
: "Kuva on tämä. Säätimet muokkaavat sitä.";
|
|
1154
|
+
}
|
|
1155
|
+
|
|
1156
|
+
$("preToggle").addEventListener("click", function () {
|
|
1157
|
+
var on = $("prebar").classList.toggle("on");
|
|
1158
|
+
$("preToggle").classList.toggle("active", on);
|
|
1159
|
+
});
|
|
1160
|
+
|
|
1161
|
+
runBtn.addEventListener("click", function () { vectorize(); });
|
|
1162
|
+
|
|
1163
|
+
// ---------------------------------------------------------------- edit mode
|
|
1164
|
+
//
|
|
1165
|
+
// Two tools over the traced result. "Yhdistä" repaints a shape in a chosen
|
|
1166
|
+
// color, which is how a shape merges into its neighbours: adjacent areas of
|
|
1167
|
+
// one fill read as one surface, so recoloring the red knuckles of a hand to
|
|
1168
|
+
// the skin beside them makes the hand one shape. "Tarkenna" is the inverse —
|
|
1169
|
+
// it re-traces a small box around the click with a palette built from that
|
|
1170
|
+
// box alone, the same idea as the detail neighbourhoods, and lays the result
|
|
1171
|
+
// on top.
|
|
1172
|
+
// Tarkennin is what a person reaches for first: it is the tool that adds
|
|
1173
|
+
// something to the picture, where the others take things away.
|
|
1174
|
+
var editOn = false, editTool = "refine", picking = false, editStack = [];
|
|
1175
|
+
var editToggle = $("editToggle"), editbar = $("editbar");
|
|
1176
|
+
var toolMerge = $("toolMerge"), toolRefine = $("toolRefine"), toolPick = $("toolPick");
|
|
1177
|
+
var toolWand = $("toolWand"), wandTol = $("wandTol"), wandCrop = $("wandCrop");
|
|
1178
|
+
var editColor = $("editColor"), refineSize = $("refineSize"), editNote = $("editNote");
|
|
1179
|
+
|
|
1180
|
+
// A traced layer is one path holding every ring of its color, so clicking it
|
|
1181
|
+
// selects a whole color rather than a shape. Edit mode splits each path into
|
|
1182
|
+
// one path per outer ring, carrying the rings nested inside it as its holes —
|
|
1183
|
+
// without that grouping the holes would be filled in and every eye would
|
|
1184
|
+
// close. Containment is decided by bounding box, which is what "inside" means
|
|
1185
|
+
// for rings that came from one traced region and cannot partially overlap.
|
|
1186
|
+
function ringsOf(d) {
|
|
1187
|
+
var out = [], parts = d.split(/(?=M)/);
|
|
1188
|
+
for (var i = 0; i < parts.length; i++) {
|
|
1189
|
+
var seg = parts[i].trim();
|
|
1190
|
+
if (!seg) continue;
|
|
1191
|
+
var nums = seg.match(/-?\d+(?:\.\d+)?/g);
|
|
1192
|
+
if (!nums || nums.length < 4) { out.push({ d: seg, box: null }); continue; }
|
|
1193
|
+
var x0 = Infinity, y0 = Infinity, x1 = -Infinity, y1 = -Infinity;
|
|
1194
|
+
for (var k = 0; k + 1 < nums.length; k += 2) {
|
|
1195
|
+
var x = +nums[k], y = +nums[k + 1];
|
|
1196
|
+
if (x < x0) x0 = x; if (x > x1) x1 = x;
|
|
1197
|
+
if (y < y0) y0 = y; if (y > y1) y1 = y;
|
|
1198
|
+
}
|
|
1199
|
+
out.push({ d: seg, box: [x0, y0, x1, y1], area: (x1 - x0) * (y1 - y0) });
|
|
1200
|
+
}
|
|
1201
|
+
return out;
|
|
1202
|
+
}
|
|
1203
|
+
|
|
1204
|
+
// Which ring contains which. A bounding box is not the answer: under evenodd
|
|
1205
|
+
// it is the *parity* of how many rings enclose a ring that decides whether it
|
|
1206
|
+
// is painted or punched out, and a box test both misses an island sitting
|
|
1207
|
+
// inside a hole and invents containment between rings that merely overlap in
|
|
1208
|
+
// their extents. Entering edit mode with the box version repainted 1472
|
|
1209
|
+
// pixels of a portrait before a single edit was made — a stacked layer is one
|
|
1210
|
+
// frame-filling ring with hundreds of rings inside it, and getting their
|
|
1211
|
+
// nesting wrong exposes the layer underneath. Path2D with an exact
|
|
1212
|
+
// point-in-fill test costs a few milliseconds once and is simply correct.
|
|
1213
|
+
function nestDepths(rings, ctx) {
|
|
1214
|
+
var paths = rings.map(function (r) { return new Path2D(r.d); });
|
|
1215
|
+
var pt = rings.map(function (r) {
|
|
1216
|
+
var m = r.d.match(/-?\d+(?:\.\d+)?/g);
|
|
1217
|
+
return m && m.length >= 2 ? { x: +m[0], y: +m[1] } : null;
|
|
1218
|
+
});
|
|
1219
|
+
return rings.map(function (r, i) {
|
|
1220
|
+
if (!pt[i]) return { depth: 0, parent: -1 };
|
|
1221
|
+
var depth = 0, parent = -1, bestArea = Infinity;
|
|
1222
|
+
for (var j = 0; j < rings.length; j++) {
|
|
1223
|
+
if (j === i) continue;
|
|
1224
|
+
if (!ctx.isPointInPath(paths[j], pt[i].x, pt[i].y, "nonzero")) continue;
|
|
1225
|
+
depth++;
|
|
1226
|
+
var a = rings[j].area === undefined ? Infinity : rings[j].area;
|
|
1227
|
+
if (a < bestArea) { bestArea = a; parent = j; } // the immediate parent
|
|
1228
|
+
}
|
|
1229
|
+
return { depth: depth, parent: parent };
|
|
1230
|
+
});
|
|
1231
|
+
}
|
|
1232
|
+
|
|
1233
|
+
// One layer's path data as individual shapes, each carrying its own holes.
|
|
1234
|
+
// Even depth is painted, odd depth is punched out. A shape carries the
|
|
1235
|
+
// odd-depth rings whose immediate parent it is; an island at depth 2 is a
|
|
1236
|
+
// shape of its own, drawn after the hole it sits in. Splitting on M alone and
|
|
1237
|
+
// treating every ring as a shape fills in every hole — every eye closes —
|
|
1238
|
+
// which is why this is one function used everywhere shapes are split.
|
|
1239
|
+
function splitShapes(d, ctx) {
|
|
1240
|
+
var rings = ringsOf(d || "");
|
|
1241
|
+
if (rings.length < 2) return null;
|
|
1242
|
+
var nest = nestDepths(rings, ctx);
|
|
1243
|
+
var order = [];
|
|
1244
|
+
rings.forEach(function (r, i) { if (nest[i].depth % 2 === 0) order.push(i); });
|
|
1245
|
+
order.sort(function (x, y) { return nest[x].depth - nest[y].depth; });
|
|
1246
|
+
return order.map(function (i) {
|
|
1247
|
+
var out = rings[i].d, box = rings[i].box;
|
|
1248
|
+
for (var j = 0; j < rings.length; j++) {
|
|
1249
|
+
if (nest[j].depth % 2 === 1 && nest[j].parent === i) out += " " + rings[j].d;
|
|
1250
|
+
}
|
|
1251
|
+
return { d: out, box: box };
|
|
1252
|
+
});
|
|
1253
|
+
}
|
|
1254
|
+
|
|
1255
|
+
function explode(svg) {
|
|
1256
|
+
var ctx = document.createElement("canvas").getContext("2d");
|
|
1257
|
+
var paths = Array.prototype.slice.call(svg.querySelectorAll("path"));
|
|
1258
|
+
paths.forEach(function (p) {
|
|
1259
|
+
var shapes = splitShapes(p.getAttribute("d") || "", ctx);
|
|
1260
|
+
if (!shapes || !shapes.length) return;
|
|
1261
|
+
shapes.forEach(function (sh) {
|
|
1262
|
+
var el = document.createElementNS("http://www.w3.org/2000/svg", "path");
|
|
1263
|
+
el.setAttribute("d", sh.d);
|
|
1264
|
+
el.setAttribute("fill", p.getAttribute("fill") || "#000");
|
|
1265
|
+
el.setAttribute("fill-rule", "evenodd");
|
|
1266
|
+
p.parentNode.insertBefore(el, p);
|
|
1267
|
+
});
|
|
1268
|
+
p.parentNode.removeChild(p);
|
|
1269
|
+
});
|
|
1270
|
+
}
|
|
1271
|
+
|
|
1272
|
+
function outSvg() { return outStage.querySelector("svg"); }
|
|
1273
|
+
|
|
1274
|
+
// Every edit path already reports what it did here, so this is where the
|
|
1275
|
+
// status line's live half gets refreshed too. Debounced, because the smooth
|
|
1276
|
+
// brush says something on every dab and serializing half a megabyte of SVG a
|
|
1277
|
+
// hundred times a second would make the tool feel worse than the stale number
|
|
1278
|
+
// ever did.
|
|
1279
|
+
var sizeTimer = 0;
|
|
1280
|
+
function setEditNote(msg) {
|
|
1281
|
+
editNote.textContent = msg;
|
|
1282
|
+
if (sizeTimer) clearTimeout(sizeTimer);
|
|
1283
|
+
sizeTimer = setTimeout(function () { sizeTimer = 0; refreshSize(); }, 120);
|
|
1284
|
+
}
|
|
1285
|
+
|
|
1286
|
+
function refreshEditUi() {
|
|
1287
|
+
toolMerge.classList.toggle("active", editTool === "merge" && !picking);
|
|
1288
|
+
toolRefine.classList.toggle("active", editTool === "refine" && !picking);
|
|
1289
|
+
$("toolSmooth").classList.toggle("active", editTool === "smooth" && !picking);
|
|
1290
|
+
toolWand.classList.toggle("active", editTool === "wand" && !picking);
|
|
1291
|
+
toolPick.classList.toggle("active", picking);
|
|
1292
|
+
outStage.classList.toggle("picking", picking);
|
|
1293
|
+
outStage.classList.toggle("refining", !picking && editTool === "refine");
|
|
1294
|
+
outStage.classList.toggle("smoothing", !picking && editTool === "smooth");
|
|
1295
|
+
outStage.classList.toggle("wanding", !picking && editTool === "wand");
|
|
1296
|
+
editbar.classList.toggle("wand", editTool === "wand" && !picking);
|
|
1297
|
+
$("toolHelp").textContent = picking ? "Napsauta muotoa poimiaksesi sen värin."
|
|
1298
|
+
: editTool === "merge" ? "Napsauta muotoja: ne saavat valitun värin ja sulautuvat naapureihinsa."
|
|
1299
|
+
: editTool === "refine" ? "Vedä alueen yli: se vektoroidaan uudelleen omalla paletillaan."
|
|
1300
|
+
: editTool === "wand" ? "Napsauta kohdetta, tai piirrä sen ympäri suurin piirtein — valinta napsahtaa kuvan omiin rajoihin. ⇧ lisää, ⌥ poistaa. Eristä leikkaa muun pois."
|
|
1301
|
+
: "Vedä ääriviivan yli: kulmat pyöristyvät, pienet muodot katoavat.";
|
|
1302
|
+
}
|
|
1303
|
+
|
|
1304
|
+
// ------------------------------------------------------------ taikasauva
|
|
1305
|
+
//
|
|
1306
|
+
// Pick an object out of the drawing and cut everything else away — an icon
|
|
1307
|
+
// out of a photograph. Click a shape and the selection grows outward from it
|
|
1308
|
+
// by color, or draw a rough outline by hand and let the selection snap to
|
|
1309
|
+
// the real boundaries inside it. ⇧ adds, ⌥ removes.
|
|
1310
|
+
//
|
|
1311
|
+
// Everything here works on what is *visible*, and that is not a detail. A
|
|
1312
|
+
// traced picture is stacked: a lower layer's shape is the union of itself
|
|
1313
|
+
// and everything painted on top of it, so on a test image of a box with a
|
|
1314
|
+
// ball overlapping it the blue layer is 240 px wide and includes the ball.
|
|
1315
|
+
// Ask that geometry which shapes touch which and every shape touches the one
|
|
1316
|
+
// under it; delete the shapes on top and the union underneath is uncovered,
|
|
1317
|
+
// which is how cutting out a box returned a box with the ball's bulge on it,
|
|
1318
|
+
// and how cutting a person out of a portrait returned a full rectangle.
|
|
1319
|
+
//
|
|
1320
|
+
// So the drawing is rasterized once, in its own z-order, into a buffer of
|
|
1321
|
+
// shape indices — a z-buffer, giving for every pixel the shape you can
|
|
1322
|
+
// actually see there. Adjacency, "how much of this shape is inside the line
|
|
1323
|
+
// you drew", the enclosure rule and the crop box all read that buffer, and
|
|
1324
|
+
// the cut carries the z-order into a mask so nothing hidden comes back.
|
|
1325
|
+
//
|
|
1326
|
+
// Canvas antialiases, and a blended pixel between two index colors decodes
|
|
1327
|
+
// to a third, unrelated shape. So the raster is drawn twice under two
|
|
1328
|
+
// different index encodings and a pixel is believed only when both agree —
|
|
1329
|
+
// a blend cannot survive that, and the few boundary pixels it discards are
|
|
1330
|
+
// boundary pixels.
|
|
1331
|
+
var WAND_RASTER = 768;
|
|
1332
|
+
var WAND_KEY = 0x9E3779;
|
|
1333
|
+
var SVGNS = "http://www.w3.org/2000/svg";
|
|
1334
|
+
var wandSel = null, wandGraph = null, wandSeq = 0;
|
|
1335
|
+
// A shape the drawn line cuts through is kept only where the line kept it.
|
|
1336
|
+
// These hold that: which shapes are clipped, by which <clipPath>, and the
|
|
1337
|
+
// raster of each clip so the enclosure rule and the crop box see the same
|
|
1338
|
+
// picture the viewer will.
|
|
1339
|
+
var wandClipOf = new Map(), wandClipEls = new Map(), wandClipRaster = new Map();
|
|
1340
|
+
|
|
1341
|
+
function wandRgb(fill) {
|
|
1342
|
+
if (!/^#[0-9a-f]{6}$/i.test(fill || "")) return [0, 0, 0];
|
|
1343
|
+
return [parseInt(fill.slice(1, 3), 16), parseInt(fill.slice(3, 5), 16),
|
|
1344
|
+
parseInt(fill.slice(5, 7), 16)];
|
|
1345
|
+
}
|
|
1346
|
+
|
|
1347
|
+
// A path is a shape of the drawing only if it is drawn. The refiner leaves
|
|
1348
|
+
// its stroke shape inside a <mask>, and the cut leaves one of its own.
|
|
1349
|
+
function wandPaths(svg) {
|
|
1350
|
+
return Array.prototype.slice.call(svg.querySelectorAll("path")).filter(function (el) {
|
|
1351
|
+
for (var p = el.parentNode; p && p !== svg; p = p.parentNode) {
|
|
1352
|
+
var t = p.tagName;
|
|
1353
|
+
if (t === "mask" || t === "defs" || t === "clipPath" || t === "filter") return false;
|
|
1354
|
+
}
|
|
1355
|
+
return true;
|
|
1356
|
+
});
|
|
1357
|
+
}
|
|
1358
|
+
|
|
1359
|
+
// Refined patches live under a translate, so their path data is in the
|
|
1360
|
+
// crop's coordinates and not the picture's. Null for a shape with no
|
|
1361
|
+
// transformed ancestor, which is every shape in a plain trace.
|
|
1362
|
+
function wandMatrix(svg, el) {
|
|
1363
|
+
var has = false;
|
|
1364
|
+
for (var p = el.parentNode; p && p !== svg; p = p.parentNode) {
|
|
1365
|
+
if (p.getAttribute && p.getAttribute("transform")) { has = true; break; }
|
|
1366
|
+
}
|
|
1367
|
+
if (!has) return null;
|
|
1368
|
+
try { return svg.getScreenCTM().inverse().multiply(el.getScreenCTM()); }
|
|
1369
|
+
catch (e) { return null; }
|
|
1370
|
+
}
|
|
1371
|
+
|
|
1372
|
+
function wandFrame(svg) {
|
|
1373
|
+
var vb = (svg.getAttribute("viewBox") || "").split(/[\s,]+/).map(Number);
|
|
1374
|
+
if (vb.length !== 4 || !(vb[2] > 0) || !(vb[3] > 0)) {
|
|
1375
|
+
vb = [0, 0, +svg.getAttribute("width") || 1, +svg.getAttribute("height") || 1];
|
|
1376
|
+
}
|
|
1377
|
+
return vb;
|
|
1378
|
+
}
|
|
1379
|
+
|
|
1380
|
+
function wandBuild(svg) {
|
|
1381
|
+
var els = wandPaths(svg);
|
|
1382
|
+
var index = new Map(), rgb = [], mats = [], paths = [];
|
|
1383
|
+
els.forEach(function (el, i) {
|
|
1384
|
+
index.set(el, i);
|
|
1385
|
+
rgb.push(wandRgb(el.getAttribute("fill")));
|
|
1386
|
+
mats.push(wandMatrix(svg, el));
|
|
1387
|
+
try { paths.push(new Path2D(el.getAttribute("d") || "")); } catch (e) { paths.push(null); }
|
|
1388
|
+
});
|
|
1389
|
+
var vb = wandFrame(svg);
|
|
1390
|
+
var sc = Math.min(4, WAND_RASTER / Math.max(vb[2], vb[3]));
|
|
1391
|
+
var cw = Math.max(1, Math.round(vb[2] * sc)), ch = Math.max(1, Math.round(vb[3] * sc));
|
|
1392
|
+
function pass(mul) {
|
|
1393
|
+
var cv = document.createElement("canvas");
|
|
1394
|
+
cv.width = cw; cv.height = ch;
|
|
1395
|
+
var cx = cv.getContext("2d", { willReadFrequently: true });
|
|
1396
|
+
cx.setTransform(sc, 0, 0, sc, -vb[0] * sc, -vb[1] * sc);
|
|
1397
|
+
for (var i = 0; i < els.length; i++) {
|
|
1398
|
+
if (!paths[i]) continue;
|
|
1399
|
+
var code = Math.imul(i + 1, mul) & 0xffffff;
|
|
1400
|
+
cx.fillStyle = "rgb(" + (code & 255) + "," + ((code >> 8) & 255) + "," + ((code >> 16) & 255) + ")";
|
|
1401
|
+
var m = mats[i];
|
|
1402
|
+
if (m) { cx.save(); cx.transform(m.a, m.b, m.c, m.d, m.e, m.f); }
|
|
1403
|
+
cx.fill(paths[i], "evenodd");
|
|
1404
|
+
if (m) cx.restore();
|
|
1405
|
+
}
|
|
1406
|
+
return cx.getImageData(0, 0, cw, ch).data;
|
|
1407
|
+
}
|
|
1408
|
+
var A = pass(1), B = pass(WAND_KEY);
|
|
1409
|
+
var n = cw * ch, ids = new Int32Array(n), seen = new Int32Array(els.length);
|
|
1410
|
+
for (var p = 0; p < n; p++) {
|
|
1411
|
+
var o = p * 4;
|
|
1412
|
+
var a = A[o] | (A[o + 1] << 8) | (A[o + 2] << 16);
|
|
1413
|
+
if (a <= 0 || a > els.length) continue;
|
|
1414
|
+
var b = B[o] | (B[o + 1] << 8) | (B[o + 2] << 16);
|
|
1415
|
+
if ((Math.imul(a, WAND_KEY) & 0xffffff) !== b) continue; // an antialiased blend
|
|
1416
|
+
ids[p] = a;
|
|
1417
|
+
seen[a - 1]++;
|
|
1418
|
+
}
|
|
1419
|
+
// Who touches whom, as seen rather than as drawn: two shapes are
|
|
1420
|
+
// neighbours when they meet in the z-buffer, so a boundary buried under a
|
|
1421
|
+
// third shape does not count as contact.
|
|
1422
|
+
var nb = new Array(els.length);
|
|
1423
|
+
function link(x, y) {
|
|
1424
|
+
if (x === y || !x || !y) return;
|
|
1425
|
+
var s = nb[x - 1] || (nb[x - 1] = new Set());
|
|
1426
|
+
s.add(y - 1);
|
|
1427
|
+
s = nb[y - 1] || (nb[y - 1] = new Set());
|
|
1428
|
+
s.add(x - 1);
|
|
1429
|
+
}
|
|
1430
|
+
for (var yy = 0; yy < ch; yy++) {
|
|
1431
|
+
var row = yy * cw;
|
|
1432
|
+
for (var xx = 0; xx < cw; xx++) {
|
|
1433
|
+
var id = ids[row + xx];
|
|
1434
|
+
if (xx + 1 < cw) link(id, ids[row + xx + 1]);
|
|
1435
|
+
if (yy + 1 < ch) link(id, ids[row + cw + xx]);
|
|
1436
|
+
}
|
|
1437
|
+
}
|
|
1438
|
+
return { svg: svg, els: els, index: index, rgb: rgb, mats: mats, ids: ids,
|
|
1439
|
+
cw: cw, ch: ch, sc: sc, vb: vb, nb: nb, seen: seen, n: els.length };
|
|
1440
|
+
}
|
|
1441
|
+
|
|
1442
|
+
function wandInvalidate() { wandGraph = null; }
|
|
1443
|
+
|
|
1444
|
+
function wandClear() {
|
|
1445
|
+
if (wandGraph) {
|
|
1446
|
+
wandGraph.els.forEach(function (el) {
|
|
1447
|
+
el.classList.remove("wand-off");
|
|
1448
|
+
el.removeAttribute("clip-path");
|
|
1449
|
+
});
|
|
1450
|
+
}
|
|
1451
|
+
wandClipEls.forEach(function (el) { if (el.parentNode) el.parentNode.removeChild(el); });
|
|
1452
|
+
wandClipOf = new Map();
|
|
1453
|
+
wandClipEls = new Map();
|
|
1454
|
+
wandClipRaster = new Map();
|
|
1455
|
+
wandSel = null;
|
|
1456
|
+
}
|
|
1457
|
+
|
|
1458
|
+
// Where the selection actually lands on screen: its shapes, minus the parts
|
|
1459
|
+
// a drawn line cut away.
|
|
1460
|
+
function wandCells(g, sel) {
|
|
1461
|
+
var n = g.cw * g.ch, eff = new Uint8Array(n);
|
|
1462
|
+
for (var p = 0; p < n; p++) {
|
|
1463
|
+
var id = g.ids[p];
|
|
1464
|
+
if (!id || !sel.has(id - 1)) continue;
|
|
1465
|
+
var clip = wandClipOf.get(id - 1);
|
|
1466
|
+
if (clip !== undefined) {
|
|
1467
|
+
var r = wandClipRaster.get(clip);
|
|
1468
|
+
if (r && !r[p]) continue;
|
|
1469
|
+
}
|
|
1470
|
+
eff[p] = 1;
|
|
1471
|
+
}
|
|
1472
|
+
return eff;
|
|
1473
|
+
}
|
|
1474
|
+
|
|
1475
|
+
function wandReady(svg) {
|
|
1476
|
+
if (wandGraph && wandGraph.svg === svg &&
|
|
1477
|
+
wandGraph.n === wandPaths(svg).length) return wandGraph;
|
|
1478
|
+
wandClear();
|
|
1479
|
+
wandGraph = wandBuild(svg);
|
|
1480
|
+
return wandGraph;
|
|
1481
|
+
}
|
|
1482
|
+
|
|
1483
|
+
// What the selection actually covers on screen, and what it surrounds. The
|
|
1484
|
+
// holes matter: a color flood cannot reach the shapes painted *on top of*
|
|
1485
|
+
// the object — that is what tolerance is for — but the eyes and the tie are
|
|
1486
|
+
// the object, and what they have in common is that the object closes around
|
|
1487
|
+
// them. A shape that merely overlaps, like a ball crossing a box, reaches
|
|
1488
|
+
// the outside and is left alone.
|
|
1489
|
+
function wandEnclose(g, sel) {
|
|
1490
|
+
var n = g.cw * g.ch, v = wandCells(g, sel), covered = 0;
|
|
1491
|
+
for (var p = 0; p < n; p++) if (v[p]) covered++;
|
|
1492
|
+
if (!covered) return;
|
|
1493
|
+
var out = new Uint8Array(n), stack = [];
|
|
1494
|
+
function push(q) { if (!out[q] && !v[q]) { out[q] = 1; stack.push(q); } }
|
|
1495
|
+
for (var x = 0; x < g.cw; x++) { push(x); push((g.ch - 1) * g.cw + x); }
|
|
1496
|
+
for (var y = 0; y < g.ch; y++) { push(y * g.cw); push(y * g.cw + g.cw - 1); }
|
|
1497
|
+
while (stack.length) {
|
|
1498
|
+
var q2 = stack.pop(), qx = q2 % g.cw, qy = (q2 / g.cw) | 0;
|
|
1499
|
+
if (qx > 0) push(q2 - 1);
|
|
1500
|
+
if (qx + 1 < g.cw) push(q2 + 1);
|
|
1501
|
+
if (qy > 0) push(q2 - g.cw);
|
|
1502
|
+
if (qy + 1 < g.ch) push(q2 + g.cw);
|
|
1503
|
+
}
|
|
1504
|
+
var filled = covered;
|
|
1505
|
+
for (var p2 = 0; p2 < n; p2++) if (!v[p2] && !out[p2]) { v[p2] = 1; filled++; }
|
|
1506
|
+
// "Inside the object" stops meaning anything once the object is the
|
|
1507
|
+
// picture. Clicking a portrait's background covers most of the frame and
|
|
1508
|
+
// surrounds the person; closing over that hands back the whole drawing.
|
|
1509
|
+
if (filled > n * 0.8) return;
|
|
1510
|
+
var inside = new Int32Array(g.n);
|
|
1511
|
+
for (var p3 = 0; p3 < n; p3++) {
|
|
1512
|
+
var id3 = g.ids[p3];
|
|
1513
|
+
if (id3 && v[p3]) inside[id3 - 1]++;
|
|
1514
|
+
}
|
|
1515
|
+
for (var i = 0; i < g.n; i++) {
|
|
1516
|
+
if (sel.has(i) || !g.seen[i]) continue;
|
|
1517
|
+
if (inside[i] >= g.seen[i] * 0.9) sel.add(i);
|
|
1518
|
+
}
|
|
1519
|
+
}
|
|
1520
|
+
|
|
1521
|
+
// Grow from one shape across what touches it and is within tol of the color
|
|
1522
|
+
// that was clicked. Against the seed, not against the shape the flood came
|
|
1523
|
+
// from: chaining tolerance from neighbour to neighbour walks a portrait's
|
|
1524
|
+
// cheek into the wall behind it one imperceptible step at a time.
|
|
1525
|
+
function wandGrow(g, seed, tol) {
|
|
1526
|
+
var want = g.rgb[seed], lim = tol * tol;
|
|
1527
|
+
// A colour flood answers for whole shapes, so a shape it reaches stops
|
|
1528
|
+
// being clipped by whatever line was drawn before.
|
|
1529
|
+
var hit = new Set([seed]), queue = [seed];
|
|
1530
|
+
while (queue.length) {
|
|
1531
|
+
var near = g.nb[queue.pop()];
|
|
1532
|
+
if (!near) continue;
|
|
1533
|
+
near.forEach(function (j) {
|
|
1534
|
+
if (hit.has(j)) return;
|
|
1535
|
+
var a = g.rgb[j];
|
|
1536
|
+
var dr = a[0] - want[0], dg = a[1] - want[1], db = a[2] - want[2];
|
|
1537
|
+
if (dr * dr + dg * dg + db * db > lim) return;
|
|
1538
|
+
hit.add(j);
|
|
1539
|
+
queue.push(j);
|
|
1540
|
+
});
|
|
1541
|
+
}
|
|
1542
|
+
wandEnclose(g, hit);
|
|
1543
|
+
return hit;
|
|
1544
|
+
}
|
|
1545
|
+
|
|
1546
|
+
// The hand-drawn version. A photograph has no color boundary that agrees
|
|
1547
|
+
// with the object — a suit and the curtain behind it are the same tone — so
|
|
1548
|
+
// the line you draw says *where*, roughly, and the shapes say *exactly*: a
|
|
1549
|
+
// shape joins the object when more than half of what you can see of it falls
|
|
1550
|
+
// inside the line. Nothing is cut along the line itself, so a rough sweep
|
|
1551
|
+
// still comes out on the picture's own edges.
|
|
1552
|
+
function wandLassoSelect(g, pts) {
|
|
1553
|
+
var cv = document.createElement("canvas");
|
|
1554
|
+
cv.width = g.cw; cv.height = g.ch;
|
|
1555
|
+
var cx = cv.getContext("2d", { willReadFrequently: true });
|
|
1556
|
+
cx.setTransform(g.sc, 0, 0, g.sc, -g.vb[0] * g.sc, -g.vb[1] * g.sc);
|
|
1557
|
+
var poly = "M" + pts.map(function (p) { return p.x + "," + p.y; }).join("L") + "Z";
|
|
1558
|
+
cx.fillStyle = "#fff";
|
|
1559
|
+
cx.fill(new Path2D(poly));
|
|
1560
|
+
var d = cx.getImageData(0, 0, g.cw, g.ch).data;
|
|
1561
|
+
var n = g.cw * g.ch, mark = new Uint8Array(n), inside = new Int32Array(g.n);
|
|
1562
|
+
for (var p = 0; p < n; p++) {
|
|
1563
|
+
if (d[p * 4 + 3] <= 128) continue;
|
|
1564
|
+
mark[p] = 1;
|
|
1565
|
+
var id = g.ids[p];
|
|
1566
|
+
if (id) inside[id - 1]++;
|
|
1567
|
+
}
|
|
1568
|
+
|
|
1569
|
+
// Two thresholds, because a shape is admitted on one question and kept
|
|
1570
|
+
// whole on another. Admitted: does two fifths of what you can see of it
|
|
1571
|
+
// fall inside the line — below that the shape belongs to the background
|
|
1572
|
+
// and even the part of it inside the line goes, which is what clears the
|
|
1573
|
+
// curtain and the flag and the desk out from behind a person. Kept whole:
|
|
1574
|
+
// is it almost entirely inside, in which case the line was a rough guess
|
|
1575
|
+
// and the shape's own edge is the better answer. In between the line is
|
|
1576
|
+
// the answer, and the shape is kept only where the line kept it.
|
|
1577
|
+
//
|
|
1578
|
+
// Taking every admitted shape whole was the first version and it leaked
|
|
1579
|
+
// badly: one large background shape reaching across the line drags its
|
|
1580
|
+
// whole visible area in, and then the enclosure rule closes over
|
|
1581
|
+
// everything that shape surrounds — 178 shapes selected, 231 after
|
|
1582
|
+
// closing, and 72 of those sat outside the line altogether.
|
|
1583
|
+
var hit = new Set(), part = [];
|
|
1584
|
+
for (var k = 0; k < g.n; k++) {
|
|
1585
|
+
if (!g.seen[k] || inside[k] < g.seen[k] * 0.4) continue;
|
|
1586
|
+
hit.add(k);
|
|
1587
|
+
if (inside[k] < g.seen[k] * 0.85) part.push(k);
|
|
1588
|
+
}
|
|
1589
|
+
|
|
1590
|
+
if (part.length) {
|
|
1591
|
+
var svg = g.svg;
|
|
1592
|
+
var cid = "wandclip" + (++wandSeq);
|
|
1593
|
+
var cp = document.createElementNS(SVGNS, "clipPath");
|
|
1594
|
+
cp.setAttribute("id", cid);
|
|
1595
|
+
cp.setAttribute("clipPathUnits", "userSpaceOnUse");
|
|
1596
|
+
var pe = document.createElementNS(SVGNS, "path");
|
|
1597
|
+
pe.setAttribute("d", poly);
|
|
1598
|
+
cp.appendChild(pe);
|
|
1599
|
+
svg.appendChild(cp);
|
|
1600
|
+
wandClipEls.set(cid, cp);
|
|
1601
|
+
wandClipRaster.set(cid, mark);
|
|
1602
|
+
part.forEach(function (k) { wandClipOf.set(k, cid); });
|
|
1603
|
+
}
|
|
1604
|
+
wandEnclose(g, hit);
|
|
1605
|
+
return hit;
|
|
1606
|
+
}
|
|
1607
|
+
|
|
1608
|
+
function wandPaint() {
|
|
1609
|
+
if (!wandGraph) return;
|
|
1610
|
+
wandGraph.els.forEach(function (el, i) {
|
|
1611
|
+
el.classList.toggle("wand-off", !!wandSel && !wandSel.has(i));
|
|
1612
|
+
var clip = wandSel && wandSel.has(i) ? wandClipOf.get(i) : undefined;
|
|
1613
|
+
if (clip === undefined) el.removeAttribute("clip-path");
|
|
1614
|
+
else el.setAttribute("clip-path", "url(#" + clip + ")");
|
|
1615
|
+
});
|
|
1616
|
+
}
|
|
1617
|
+
|
|
1618
|
+
function wandMerge(g, grown, ev, clipped) {
|
|
1619
|
+
if (!wandSel || (!ev.shiftKey && !ev.altKey)) wandSel = new Set();
|
|
1620
|
+
grown.forEach(function (i) {
|
|
1621
|
+
if (ev.altKey) { wandSel.delete(i); wandClipOf.delete(i); return; }
|
|
1622
|
+
wandSel.add(i);
|
|
1623
|
+
if (!clipped) wandClipOf.delete(i);
|
|
1624
|
+
});
|
|
1625
|
+
wandPaint();
|
|
1626
|
+
setEditNote(wandSel.size + " / " + g.n + " muotoa valittu. ⇧ lisää, ⌥ poistaa, Eristä leikkaa muun pois.");
|
|
1627
|
+
}
|
|
1628
|
+
|
|
1629
|
+
function wandClick(el, ev) {
|
|
1630
|
+
var svg = outSvg();
|
|
1631
|
+
if (!svg) return;
|
|
1632
|
+
var g = wandReady(svg);
|
|
1633
|
+
var seed = g.index.get(el);
|
|
1634
|
+
if (seed === undefined) return;
|
|
1635
|
+
wandMerge(g, wandGrow(g, seed, +wandTol.value), ev, false);
|
|
1636
|
+
}
|
|
1637
|
+
|
|
1638
|
+
function wandLassoDone(pts, ev) {
|
|
1639
|
+
var svg = outSvg();
|
|
1640
|
+
if (!svg) return;
|
|
1641
|
+
var g = wandReady(svg);
|
|
1642
|
+
wandMerge(g, wandLassoSelect(g, pts), ev, true);
|
|
1643
|
+
}
|
|
1644
|
+
|
|
1645
|
+
// The cut. Everything outside the selection leaves the document, which is
|
|
1646
|
+
// real transparency rather than a white shape painted over the rest, and
|
|
1647
|
+
// "rajaa reunoihin" pulls the frame in around what is left so the result is
|
|
1648
|
+
// the size of the icon and not of the photograph it came from.
|
|
1649
|
+
function wandApply() {
|
|
1650
|
+
var svg = outSvg();
|
|
1651
|
+
if (!svg || !wandGraph || !wandSel || !wandSel.size) {
|
|
1652
|
+
setEditNote("Valitse ensin kohde taikasauvalla."); return;
|
|
1653
|
+
}
|
|
1654
|
+
var g = wandGraph, keep = [], drop = [], lowest = -1;
|
|
1655
|
+
for (var i = 0; i < g.n; i++) {
|
|
1656
|
+
var el = g.els[i];
|
|
1657
|
+
if (wandSel.has(i)) { keep.push({ el: el, i: i }); if (lowest < 0) lowest = i; }
|
|
1658
|
+
else if (el.parentNode) drop.push({ el: el, i: i, parent: el.parentNode, next: el.nextSibling });
|
|
1659
|
+
}
|
|
1660
|
+
if (!drop.length) { setEditNote("Koko kuva on valittu — ei mitään leikattavaa."); return; }
|
|
1661
|
+
var entry = { kind: "isolate", removed: drop, masked: [], maskEl: null,
|
|
1662
|
+
clipped: [], clipEls: [] };
|
|
1663
|
+
// The clips a drawn line put on the shapes it cut through are part of the
|
|
1664
|
+
// result now, so they stop belonging to the live selection and start
|
|
1665
|
+
// belonging to the undo step.
|
|
1666
|
+
wandGraph.els.forEach(function (el, i) {
|
|
1667
|
+
if (wandClipOf.has(i) && wandSel.has(i)) entry.clipped.push(el);
|
|
1668
|
+
});
|
|
1669
|
+
wandClipEls.forEach(function (el) { entry.clipEls.push(el); });
|
|
1670
|
+
|
|
1671
|
+
// Where the selection is visible, in raster cells, which is also the crop.
|
|
1672
|
+
var eff = wandCells(g, wandSel);
|
|
1673
|
+
var x0 = Infinity, y0 = Infinity, x1 = -Infinity, y1 = -Infinity;
|
|
1674
|
+
for (var p = 0; p < g.cw * g.ch; p++) {
|
|
1675
|
+
if (!eff[p]) continue;
|
|
1676
|
+
var px = p % g.cw, py = (p / g.cw) | 0;
|
|
1677
|
+
if (px < x0) x0 = px; if (px > x1) x1 = px;
|
|
1678
|
+
if (py < y0) y0 = py; if (py > y1) y1 = py;
|
|
1679
|
+
}
|
|
1680
|
+
|
|
1681
|
+
// Put the z-order back. Deleting what is on top of a stacked layer
|
|
1682
|
+
// uncovers geometry that was never visible: cutting a box out of a picture
|
|
1683
|
+
// where a ball overlaps it returned the box with the ball's bulge painted
|
|
1684
|
+
// blue, and cutting a person out of a portrait returned a full rectangle.
|
|
1685
|
+
// The mask is the drawing's own order, white where the selection is and
|
|
1686
|
+
// black where a later shape used to cover it, so a kept shape paints
|
|
1687
|
+
// exactly where it painted before and nowhere else.
|
|
1688
|
+
var above = drop.filter(function (r) { return r.i > lowest; });
|
|
1689
|
+
// Only a kept shape with something removed above it can be uncovered, and
|
|
1690
|
+
// only those need the mask — or a white copy of themselves inside it. On a
|
|
1691
|
+
// box with one ball over it that is one shape out of two; it keeps the cut
|
|
1692
|
+
// from carrying a second copy of the whole selection.
|
|
1693
|
+
function boxOf(i) {
|
|
1694
|
+
var el = g.els[i], b;
|
|
1695
|
+
try { b = el.getBBox(); } catch (e) { return null; }
|
|
1696
|
+
if (!b) return null;
|
|
1697
|
+
var m = g.mats[i];
|
|
1698
|
+
if (!m) return [b.x, b.y, b.x + b.width, b.y + b.height];
|
|
1699
|
+
var xs = [], ys = [];
|
|
1700
|
+
[[b.x, b.y], [b.x + b.width, b.y], [b.x, b.y + b.height],
|
|
1701
|
+
[b.x + b.width, b.y + b.height]].forEach(function (c) {
|
|
1702
|
+
xs.push(m.a * c[0] + m.c * c[1] + m.e);
|
|
1703
|
+
ys.push(m.b * c[0] + m.d * c[1] + m.f);
|
|
1704
|
+
});
|
|
1705
|
+
return [Math.min.apply(null, xs), Math.min.apply(null, ys),
|
|
1706
|
+
Math.max.apply(null, xs), Math.max.apply(null, ys)];
|
|
1707
|
+
}
|
|
1708
|
+
var aboveBox = above.map(function (r) { return boxOf(r.i); });
|
|
1709
|
+
keep.forEach(function (k) {
|
|
1710
|
+
var kb = boxOf(k.i);
|
|
1711
|
+
k.covered = !kb ? true : above.some(function (r, j) {
|
|
1712
|
+
var b = aboveBox[j];
|
|
1713
|
+
return r.i > k.i && b &&
|
|
1714
|
+
!(b[2] < kb[0] || b[0] > kb[2] || b[3] < kb[1] || b[1] > kb[3]);
|
|
1715
|
+
});
|
|
1716
|
+
});
|
|
1717
|
+
if (above.length) {
|
|
1718
|
+
var mid = "wandmask" + (++wandSeq);
|
|
1719
|
+
var mask = document.createElementNS(SVGNS, "mask");
|
|
1720
|
+
mask.setAttribute("id", mid);
|
|
1721
|
+
// In user space with an explicit region. A mask's default region is
|
|
1722
|
+
// -10%/120%, and a percentage resolves to a length measured from user
|
|
1723
|
+
// space zero rather than from the viewBox origin — so on a cropped
|
|
1724
|
+
// drawing the region lands in the wrong place and cuts the object in
|
|
1725
|
+
// half along a straight line. The same trap the refiner's filter region
|
|
1726
|
+
// fell into, with the same symptom: geometry disappearing while the tool
|
|
1727
|
+
// reports success.
|
|
1728
|
+
mask.setAttribute("maskUnits", "userSpaceOnUse");
|
|
1729
|
+
var mpad = Math.max(g.vb[2], g.vb[3]);
|
|
1730
|
+
mask.setAttribute("x", String(g.vb[0] - mpad));
|
|
1731
|
+
mask.setAttribute("y", String(g.vb[1] - mpad));
|
|
1732
|
+
mask.setAttribute("width", String(g.vb[2] + mpad * 2));
|
|
1733
|
+
mask.setAttribute("height", String(g.vb[3] + mpad * 2));
|
|
1734
|
+
var items = [];
|
|
1735
|
+
keep.forEach(function (k) { if (k.covered) items.push({ i: k.i, on: true, el: k.el }); });
|
|
1736
|
+
above.forEach(function (r) { items.push({ i: r.i, on: false, rec: r }); });
|
|
1737
|
+
items.sort(function (a, b) { return a.i - b.i; });
|
|
1738
|
+
items.forEach(function (it) {
|
|
1739
|
+
var node;
|
|
1740
|
+
if (it.on) {
|
|
1741
|
+
// The selection stays in the drawing, so its white copy is a copy.
|
|
1742
|
+
node = document.createElementNS(SVGNS, "path");
|
|
1743
|
+
node.setAttribute("d", it.el.getAttribute("d") || "");
|
|
1744
|
+
node.setAttribute("fill-rule", "evenodd");
|
|
1745
|
+
node.setAttribute("fill", "#fff");
|
|
1746
|
+
} else {
|
|
1747
|
+
// What is leaving the drawing is not copied into the mask, it is
|
|
1748
|
+
// moved there: cloning every removed shape doubled the file, and
|
|
1749
|
+
// these shapes have nowhere else to be.
|
|
1750
|
+
node = it.rec.el;
|
|
1751
|
+
it.rec.fill = node.getAttribute("fill");
|
|
1752
|
+
it.rec.intoMask = true;
|
|
1753
|
+
node.setAttribute("fill", "#000");
|
|
1754
|
+
node.classList.remove("wand-off");
|
|
1755
|
+
}
|
|
1756
|
+
var m = g.mats[it.i];
|
|
1757
|
+
if (!m) { mask.appendChild(node); return; }
|
|
1758
|
+
var wrap = document.createElementNS(SVGNS, "g");
|
|
1759
|
+
wrap.setAttribute("transform",
|
|
1760
|
+
"matrix(" + [m.a, m.b, m.c, m.d, m.e, m.f].join(" ") + ")");
|
|
1761
|
+
wrap.appendChild(node);
|
|
1762
|
+
mask.appendChild(wrap);
|
|
1763
|
+
});
|
|
1764
|
+
svg.appendChild(mask);
|
|
1765
|
+
entry.maskEl = mask;
|
|
1766
|
+
keep.forEach(function (k) {
|
|
1767
|
+
// A mask on an element that also carries a transform resolves in the
|
|
1768
|
+
// coordinates that transform establishes, so a refined patch keeps the
|
|
1769
|
+
// mask it already has and paints as it did.
|
|
1770
|
+
if (g.mats[k.i] || !k.covered) return;
|
|
1771
|
+
k.el.setAttribute("mask", "url(#" + mid + ")");
|
|
1772
|
+
entry.masked.push(k.el);
|
|
1773
|
+
});
|
|
1774
|
+
}
|
|
1775
|
+
|
|
1776
|
+
drop.forEach(function (r) { if (!r.intoMask) r.parent.removeChild(r.el); });
|
|
1777
|
+
|
|
1778
|
+
if (wandCrop.checked && x1 >= x0 && y1 >= y0) {
|
|
1779
|
+
entry.vb = { viewBox: svg.getAttribute("viewBox"), w: svg.getAttribute("width"),
|
|
1780
|
+
h: svg.getAttribute("height") };
|
|
1781
|
+
var ux0 = g.vb[0] + x0 / g.sc, uy0 = g.vb[1] + y0 / g.sc;
|
|
1782
|
+
var ux1 = g.vb[0] + (x1 + 1) / g.sc, uy1 = g.vb[1] + (y1 + 1) / g.sc;
|
|
1783
|
+
var pad = Math.max(1, Math.round(Math.max(ux1 - ux0, uy1 - uy0) * 0.02));
|
|
1784
|
+
ux0 = Math.floor(ux0) - pad; uy0 = Math.floor(uy0) - pad;
|
|
1785
|
+
var w = Math.ceil(ux1) - ux0 + pad, h = Math.ceil(uy1) - uy0 + pad;
|
|
1786
|
+
svg.setAttribute("viewBox", ux0 + " " + uy0 + " " + w + " " + h);
|
|
1787
|
+
svg.setAttribute("width", String(w));
|
|
1788
|
+
svg.setAttribute("height", String(h));
|
|
1789
|
+
}
|
|
1790
|
+
editStack.push(entry);
|
|
1791
|
+
// Not wandClear: that would take the clips off the shapes that now need
|
|
1792
|
+
// them. Only the selection's own marks go.
|
|
1793
|
+
g.els.forEach(function (el) { el.classList.remove("wand-off"); });
|
|
1794
|
+
wandSel = null;
|
|
1795
|
+
wandClipOf = new Map();
|
|
1796
|
+
wandClipEls = new Map();
|
|
1797
|
+
wandClipRaster = new Map();
|
|
1798
|
+
wandInvalidate();
|
|
1799
|
+
setEditNote(keep.length + " muotoa jäljellä, muu on läpinäkyvää.");
|
|
1800
|
+
}
|
|
1801
|
+
|
|
1802
|
+
|
|
1803
|
+
function enterEdit() {
|
|
1804
|
+
var svg = outSvg();
|
|
1805
|
+
if (!svg) return;
|
|
1806
|
+
explode(svg);
|
|
1807
|
+
editOn = true;
|
|
1808
|
+
editStack = [];
|
|
1809
|
+
outStage.classList.add("editing");
|
|
1810
|
+
editbar.classList.add("on");
|
|
1811
|
+
editToggle.classList.add("active");
|
|
1812
|
+
editToggle.textContent = "Valmis";
|
|
1813
|
+
wandInvalidate();
|
|
1814
|
+
setEditNote(svg.querySelectorAll("path").length + " muotoa.");
|
|
1815
|
+
refreshEditUi();
|
|
1816
|
+
}
|
|
1817
|
+
|
|
1818
|
+
function leaveEdit() {
|
|
1819
|
+
wandClear();
|
|
1820
|
+
editOn = false;
|
|
1821
|
+
outStage.classList.remove("editing", "picking", "refining", "smoothing", "wanding");
|
|
1822
|
+
editbar.classList.remove("on", "wand");
|
|
1823
|
+
editToggle.classList.remove("active");
|
|
1824
|
+
editToggle.textContent = "Muokkaa";
|
|
1825
|
+
}
|
|
1826
|
+
|
|
1827
|
+
editToggle.addEventListener("click", function () { editOn ? leaveEdit() : enterEdit(); });
|
|
1828
|
+
toolMerge.addEventListener("click", function () { editTool = "merge"; picking = false; wandClear(); refreshEditUi(); });
|
|
1829
|
+
toolRefine.addEventListener("click", function () { editTool = "refine"; picking = false; wandClear(); refreshEditUi(); });
|
|
1830
|
+
$("toolSmooth").addEventListener("click", function () { editTool = "smooth"; picking = false; wandClear(); refreshEditUi(); });
|
|
1831
|
+
toolWand.addEventListener("click", function () { editTool = "wand"; picking = false; refreshEditUi(); });
|
|
1832
|
+
toolPick.addEventListener("click", function () { picking = !picking; refreshEditUi(); });
|
|
1833
|
+
var edgeBlend = $("edgeBlend"), refineExtra = $("refineExtra");
|
|
1834
|
+
// Not bindRange: that one re-traces the image, and re-tracing replaces the
|
|
1835
|
+
// drawing being edited. These only affect the next stroke.
|
|
1836
|
+
(function () {
|
|
1837
|
+
var lab = $("v-refineSize");
|
|
1838
|
+
function sync() { lab.textContent = String(refineSize.value); }
|
|
1839
|
+
refineSize.addEventListener("input", sync);
|
|
1840
|
+
sync();
|
|
1841
|
+
[[edgeBlend, "v-edgeBlend"], [refineExtra, "v-refineExtra"],
|
|
1842
|
+
[wandTol, "v-wandTol"]].forEach(function (pair) {
|
|
1843
|
+
var lab2 = $(pair[1]);
|
|
1844
|
+
function sync2() { lab2.textContent = String(pair[0].value); }
|
|
1845
|
+
pair[0].addEventListener("input", sync2);
|
|
1846
|
+
sync2();
|
|
1847
|
+
});
|
|
1848
|
+
})();
|
|
1849
|
+
|
|
1850
|
+
$("wandCut").addEventListener("click", wandApply);
|
|
1851
|
+
|
|
1852
|
+
outStage.addEventListener("click", function (ev) {
|
|
1853
|
+
if (!editOn) return;
|
|
1854
|
+
var el = ev.target;
|
|
1855
|
+
if (!el || el.tagName !== "path") return;
|
|
1856
|
+
if (picking) {
|
|
1857
|
+
var f = el.getAttribute("fill") || "#000000";
|
|
1858
|
+
editColor.value = /^#[0-9a-f]{6}$/i.test(f) ? f : "#000000";
|
|
1859
|
+
picking = false;
|
|
1860
|
+
refreshEditUi();
|
|
1861
|
+
setEditNote("Väri poimittu: " + editColor.value);
|
|
1862
|
+
return;
|
|
1863
|
+
}
|
|
1864
|
+
if (editTool === "merge") {
|
|
1865
|
+
editStack.push({ kind: "fill", el: el, was: el.getAttribute("fill") });
|
|
1866
|
+
el.setAttribute("fill", editColor.value);
|
|
1867
|
+
wandInvalidate();
|
|
1868
|
+
setEditNote("Yhdistetty väriin " + editColor.value + ".");
|
|
1869
|
+
} else if (editTool === "wand") {
|
|
1870
|
+
if (performance.now() - lassoAte < 700) { lassoAte = 0; return; }
|
|
1871
|
+
wandClick(el, ev);
|
|
1872
|
+
}
|
|
1873
|
+
});
|
|
1874
|
+
|
|
1875
|
+
// Re-trace a box around the click with a palette built from that box alone.
|
|
1876
|
+
// A whole-frame palette is counting the whole frame, so a small feature gets
|
|
1877
|
+
// a swatch or two of it; a box gets all of them, which is the same reasoning
|
|
1878
|
+
// the detail neighbourhoods use, applied where a person points.
|
|
1879
|
+
// ------------------------------------------------------------ tarkennin
|
|
1880
|
+
//
|
|
1881
|
+
// A brush that re-runs the vectorizer over what it is dragged across. The
|
|
1882
|
+
// stroke collects an area; when it ends, the source pixels under that area
|
|
1883
|
+
// are traced again on their own and the result is laid on top, masked to the
|
|
1884
|
+
// stroke so nothing outside what was painted changes.
|
|
1885
|
+
//
|
|
1886
|
+
// Tracing per dab is not an option — one trace is tens of milliseconds and a
|
|
1887
|
+
// drag is hundreds of dabs — so the stroke gathers and the trace happens once
|
|
1888
|
+
// at the end. Quantizing the crop alone is the whole point: a palette counted
|
|
1889
|
+
// over a whole frame gives a small feature a swatch or two, and the same
|
|
1890
|
+
// feature cropped gets all of them. That is what "zoom in on it" means, done
|
|
1891
|
+
// where a person points.
|
|
1892
|
+
var refineStroke = null; // {pts:[], preview:<path>}
|
|
1893
|
+
|
|
1894
|
+
function refineBegin(u) {
|
|
1895
|
+
var svg = outSvg();
|
|
1896
|
+
if (!svg) return;
|
|
1897
|
+
var el = document.createElementNS("http://www.w3.org/2000/svg", "path");
|
|
1898
|
+
el.setAttribute("fill", "none");
|
|
1899
|
+
el.setAttribute("stroke", "#ff2ec4");
|
|
1900
|
+
el.setAttribute("stroke-opacity", "0.35");
|
|
1901
|
+
el.setAttribute("stroke-linecap", "round");
|
|
1902
|
+
el.setAttribute("stroke-linejoin", "round");
|
|
1903
|
+
el.setAttribute("stroke-width", String(Math.max(6, +refineSize.value)));
|
|
1904
|
+
// The preview is a hint, not a target: it must not take the pointer nor
|
|
1905
|
+
// pick up the hover outline every other shape gets.
|
|
1906
|
+
el.setAttribute("pointer-events", "none");
|
|
1907
|
+
svg.appendChild(el);
|
|
1908
|
+
refineStroke = { pts: [], preview: el };
|
|
1909
|
+
refineExtend(u);
|
|
1910
|
+
}
|
|
1911
|
+
|
|
1912
|
+
function strokeD(pts) {
|
|
1913
|
+
var d = "M" + pts[0].x.toFixed(1) + "," + pts[0].y.toFixed(1);
|
|
1914
|
+
for (var i = 1; i < pts.length; i++) d += "L" + pts[i].x.toFixed(1) + "," + pts[i].y.toFixed(1);
|
|
1915
|
+
// A single dab is a dot, and a zero-length line draws nothing even with a
|
|
1916
|
+
// round cap, so give it somewhere to go.
|
|
1917
|
+
if (pts.length === 1) d += "L" + (pts[0].x + 0.01).toFixed(2) + "," + pts[0].y.toFixed(1);
|
|
1918
|
+
return d;
|
|
1919
|
+
}
|
|
1920
|
+
|
|
1921
|
+
function refineExtend(u) {
|
|
1922
|
+
if (!refineStroke) return;
|
|
1923
|
+
var pts = refineStroke.pts, last = pts[pts.length - 1];
|
|
1924
|
+
if (last && Math.abs(last.x - u.x) < 1.5 && Math.abs(last.y - u.y) < 1.5) return;
|
|
1925
|
+
pts.push({ x: u.x, y: u.y });
|
|
1926
|
+
refineStroke.preview.setAttribute("d", strokeD(pts));
|
|
1927
|
+
}
|
|
1928
|
+
|
|
1929
|
+
function refineFinish() {
|
|
1930
|
+
if (!refineStroke) return;
|
|
1931
|
+
var stroke = refineStroke;
|
|
1932
|
+
refineStroke = null;
|
|
1933
|
+
var svg = outSvg();
|
|
1934
|
+
stroke.preview.remove();
|
|
1935
|
+
if (!sourceBitmap || !stroke.pts.length || !svg) return;
|
|
1936
|
+
|
|
1937
|
+
var half = Math.max(3, (+refineSize.value) / 2);
|
|
1938
|
+
var x0 = Infinity, y0 = Infinity, x1 = -Infinity, y1 = -Infinity;
|
|
1939
|
+
stroke.pts.forEach(function (p) {
|
|
1940
|
+
if (p.x - half < x0) x0 = p.x - half;
|
|
1941
|
+
if (p.x + half > x1) x1 = p.x + half;
|
|
1942
|
+
if (p.y - half < y0) y0 = p.y - half;
|
|
1943
|
+
if (p.y + half > y1) y1 = p.y + half;
|
|
1944
|
+
});
|
|
1945
|
+
// Trace more than the mask will show. The crop and the mask used to end on
|
|
1946
|
+
// exactly the same line: the stroke's own half-width bounded both, so every
|
|
1947
|
+
// shape the tracer made was cut flat against the crop rectangle and the
|
|
1948
|
+
// mask revealed precisely that cut — a straight edge lying along the
|
|
1949
|
+
// stroke, at right angles to whatever the picture was actually doing there.
|
|
1950
|
+
// Measured on a horizontal stroke over smooth skin, the boundary row went
|
|
1951
|
+
// from 0.5 of edge energy to 14.9, which is more than the refined area's own
|
|
1952
|
+
// interior. With a margin the tracer sees context past the mask, and its cut
|
|
1953
|
+
// falls where nothing is shown.
|
|
1954
|
+
var pad = Math.ceil(half) + 4;
|
|
1955
|
+
x0 = Math.max(0, Math.floor(x0) - pad); y0 = Math.max(0, Math.floor(y0) - pad);
|
|
1956
|
+
x1 = Math.min(sourceBitmap.w, Math.ceil(x1) + pad); y1 = Math.min(sourceBitmap.h, Math.ceil(y1) + pad);
|
|
1957
|
+
var bw = x1 - x0, bh = y1 - y0;
|
|
1958
|
+
if (bw < 4 || bh < 4) { setEditNote("Liian pieni alue."); return; }
|
|
1959
|
+
|
|
1960
|
+
setEditNote("Vektoroidaan " + bw + "×" + bh + " px…");
|
|
1961
|
+
var img = new ImageBuffer();
|
|
1962
|
+
img.init(bw, bh);
|
|
1963
|
+
var rgba = workingPixels();
|
|
1964
|
+
for (var y = 0; y < bh; y++) {
|
|
1965
|
+
for (var x = 0; x < bw; x++) {
|
|
1966
|
+
var o = (((y + y0) * sourceBitmap.w) + (x + x0)) * 4;
|
|
1967
|
+
img.setPixelRGBA(x, y, rgba[o], rgba[o + 1], rgba[o + 2], rgba[o + 3]);
|
|
1968
|
+
}
|
|
1969
|
+
}
|
|
1970
|
+
// The colors the patch will meet at its border, pinned into its palette.
|
|
1971
|
+
// This is the fix for the seam, and it is a palette problem before it is
|
|
1972
|
+
// anything else: quantized in ignorance of its surroundings the crop
|
|
1973
|
+
// invents its own near-equivalents of the same tones, and every one of them
|
|
1974
|
+
// shows as a line where the two meet. Seeded, the quantizer keeps those
|
|
1975
|
+
// exact tones and spends its remaining swatches on what the crop actually
|
|
1976
|
+
// adds, so the border matches by construction.
|
|
1977
|
+
var border = borderColors(svg, stroke.pts, half);
|
|
1978
|
+
var around = paletteOf(border);
|
|
1979
|
+
|
|
1980
|
+
var o2 = EvgTraceOptions.defaults();
|
|
1981
|
+
// The crop gets more swatches than the frame, which is the point of the
|
|
1982
|
+
// tool: a palette counted over a whole picture spends itself on the large
|
|
1983
|
+
// dull areas, so a feature gets one or two entries and comes back flat.
|
|
1984
|
+
// Counted over the crop alone, and given room, the same feature gets a
|
|
1985
|
+
// range to describe itself with — on top of the pinned border tones.
|
|
1986
|
+
// How many colors the crop may invent beyond the ones pinned from its
|
|
1987
|
+
// border. This is the seam control, and it is a sharp one: measured on a
|
|
1988
|
+
// portrait, quantizing the crop with the border colors alone leaves a seam
|
|
1989
|
+
// 1.2 above what the picture has there naturally, and letting it add even
|
|
1990
|
+
// two of its own takes that to 6.7. Four plus a firm edge snap comes to
|
|
1991
|
+
// 1.6 with half again as much detail as none — that is the default.
|
|
1992
|
+
var extra = +refineExtra.value;
|
|
1993
|
+
if (border.length) {
|
|
1994
|
+
o2.paletteMode = "seeded";
|
|
1995
|
+
o2.paletteHex = border;
|
|
1996
|
+
o2.colorCount = Math.min(28, border.length + extra);
|
|
1997
|
+
} else {
|
|
1998
|
+
o2.colorCount = extra;
|
|
1999
|
+
}
|
|
2000
|
+
o2.bgMode = "none";
|
|
2001
|
+
o2.minRegion = 2;
|
|
2002
|
+
o2.turdsize = +turdsize.value;
|
|
2003
|
+
o2.alphamax = +alphamax.value;
|
|
2004
|
+
o2.optcurve = !!optcurve.checked;
|
|
2005
|
+
o2.opttolerance = +opttolerance.value;
|
|
2006
|
+
o2.turnpolicy = turnpolicy.value;
|
|
2007
|
+
var t0 = performance.now();
|
|
2008
|
+
var sub = EvgBitmapTracer.fromImageBuffer(img, o2);
|
|
2009
|
+
sub.trace();
|
|
2010
|
+
var ms = Math.round(performance.now() - t0);
|
|
2011
|
+
var layers = sub.getLayers ? sub.getLayers() : [];
|
|
2012
|
+
if (!layers.length) { setEditNote("Alueesta ei irronnut muotoja."); return; }
|
|
2013
|
+
var reach = +edgeBlend.value;
|
|
2014
|
+
var blendCtx = document.createElement("canvas").getContext("2d");
|
|
2015
|
+
|
|
2016
|
+
// Mask it to the stroke. A crop is a rectangle and the brush is not, so
|
|
2017
|
+
// pasting the rectangle would rewrite everything the brush went around.
|
|
2018
|
+
// Masks take a stroked path; clip paths do not, which is why this is a mask.
|
|
2019
|
+
var id = "refmask" + (++refineSeq);
|
|
2020
|
+
var mask = document.createElementNS("http://www.w3.org/2000/svg", "mask");
|
|
2021
|
+
mask.setAttribute("id", id);
|
|
2022
|
+
mask.setAttribute("maskUnits", "userSpaceOnUse");
|
|
2023
|
+
// Feather the mask. Even with the crop padded and the palette seeded, the
|
|
2024
|
+
// mask is still a hard cut between refined and unrefined content, and a
|
|
2025
|
+
// hard cut along a straight stroke reads as a drawn line. Blurring the
|
|
2026
|
+
// mask turns the last couple of pixels into a ramp, so the patch arrives
|
|
2027
|
+
// rather than starts.
|
|
2028
|
+
var fid = id + "f";
|
|
2029
|
+
// In user space with an explicit region, not percentages: a filter region
|
|
2030
|
+
// given in percent is measured against the filtered element's bounding box,
|
|
2031
|
+
// and a straight horizontal stroke has a bounding box of zero height. The
|
|
2032
|
+
// region collapses, the mask renders empty, and the whole patch disappears
|
|
2033
|
+
// while the tool reports success — which is exactly what happened, and what
|
|
2034
|
+
// the smoke test's "did any pixel change" check caught.
|
|
2035
|
+
var fpad = Math.ceil(half) + 8;
|
|
2036
|
+
var filt = document.createElementNS("http://www.w3.org/2000/svg", "filter");
|
|
2037
|
+
filt.setAttribute("id", fid);
|
|
2038
|
+
filt.setAttribute("filterUnits", "userSpaceOnUse");
|
|
2039
|
+
filt.setAttribute("x", String(x0 - fpad));
|
|
2040
|
+
filt.setAttribute("y", String(y0 - fpad));
|
|
2041
|
+
filt.setAttribute("width", String(bw + fpad * 2));
|
|
2042
|
+
filt.setAttribute("height", String(bh + fpad * 2));
|
|
2043
|
+
var blur = document.createElementNS("http://www.w3.org/2000/svg", "feGaussianBlur");
|
|
2044
|
+
blur.setAttribute("stdDeviation", String(Math.max(0.8, half / 8).toFixed(2)));
|
|
2045
|
+
filt.appendChild(blur);
|
|
2046
|
+
mask.appendChild(filt);
|
|
2047
|
+
var mp = document.createElementNS("http://www.w3.org/2000/svg", "path");
|
|
2048
|
+
mp.setAttribute("filter", "url(#" + fid + ")");
|
|
2049
|
+
mp.setAttribute("d", strokeD(stroke.pts));
|
|
2050
|
+
mp.setAttribute("fill", "none");
|
|
2051
|
+
mp.setAttribute("stroke", "#fff");
|
|
2052
|
+
mp.setAttribute("stroke-linecap", "round");
|
|
2053
|
+
mp.setAttribute("stroke-linejoin", "round");
|
|
2054
|
+
mp.setAttribute("stroke-width", String(half * 2));
|
|
2055
|
+
mask.appendChild(mp);
|
|
2056
|
+
svg.appendChild(mask);
|
|
2057
|
+
|
|
2058
|
+
// Two groups, not one, and the reason is a real trap: a mask on an element
|
|
2059
|
+
// that also carries a transform is resolved in the coordinate system that
|
|
2060
|
+
// transform establishes, so a mask drawn in image coordinates lands offset
|
|
2061
|
+
// by the crop origin — far enough to miss the shapes entirely, which masked
|
|
2062
|
+
// the whole result away and changed exactly zero pixels. The outer group
|
|
2063
|
+
// wears the mask in image space, the inner one does the translating.
|
|
2064
|
+
var g = document.createElementNS("http://www.w3.org/2000/svg", "g");
|
|
2065
|
+
g.setAttribute("mask", "url(#" + id + ")");
|
|
2066
|
+
var inner = document.createElementNS("http://www.w3.org/2000/svg", "g");
|
|
2067
|
+
inner.setAttribute("transform", "translate(" + x0 + "," + y0 + ")");
|
|
2068
|
+
var snapped = 0;
|
|
2069
|
+
layers.forEach(function (L) {
|
|
2070
|
+
if (!L.pathData) return;
|
|
2071
|
+
var pe = document.createElementNS("http://www.w3.org/2000/svg", "path");
|
|
2072
|
+
pe.setAttribute("d", L.pathData);
|
|
2073
|
+
pe.setAttribute("fill-rule", "evenodd");
|
|
2074
|
+
inner.appendChild(pe);
|
|
2075
|
+
var fill = (L.fillHex || "#000").toLowerCase();
|
|
2076
|
+
pe.setAttribute("fill", fill);
|
|
2077
|
+
// Split the shape's own extent, not the layer's: a layer is every ring of
|
|
2078
|
+
// one color and can span the whole stroke, so one depth for all of it
|
|
2079
|
+
// would blend the middle and the border alike.
|
|
2080
|
+
var shapes = splitShapes(L.pathData, blendCtx);
|
|
2081
|
+
if (shapes && shapes.length) {
|
|
2082
|
+
pe.remove();
|
|
2083
|
+
shapes.forEach(function (sh) {
|
|
2084
|
+
var use = fill;
|
|
2085
|
+
if (sh.box) {
|
|
2086
|
+
var mid = { x: x0 + (sh.box[0] + sh.box[2]) / 2, y: y0 + (sh.box[1] + sh.box[3]) / 2 };
|
|
2087
|
+
use = blendEdge(fill, mid, stroke.pts, half, around, reach);
|
|
2088
|
+
if (use !== fill) snapped++;
|
|
2089
|
+
}
|
|
2090
|
+
var el2 = document.createElementNS("http://www.w3.org/2000/svg", "path");
|
|
2091
|
+
el2.setAttribute("d", sh.d);
|
|
2092
|
+
el2.setAttribute("fill", use);
|
|
2093
|
+
el2.setAttribute("fill-rule", "evenodd");
|
|
2094
|
+
inner.appendChild(el2);
|
|
2095
|
+
});
|
|
2096
|
+
} else {
|
|
2097
|
+
var r1 = ringsOf(L.pathData);
|
|
2098
|
+
if (r1.length === 1 && r1[0].box) {
|
|
2099
|
+
var mid1 = { x: x0 + (r1[0].box[0] + r1[0].box[2]) / 2, y: y0 + (r1[0].box[1] + r1[0].box[3]) / 2 };
|
|
2100
|
+
var use1 = blendEdge(fill, mid1, stroke.pts, half, around, reach);
|
|
2101
|
+
if (use1 !== fill) snapped++;
|
|
2102
|
+
pe.setAttribute("fill", use1);
|
|
2103
|
+
}
|
|
2104
|
+
}
|
|
2105
|
+
});
|
|
2106
|
+
g.appendChild(inner);
|
|
2107
|
+
svg.appendChild(g);
|
|
2108
|
+
editStack.push({ kind: "add", el: g, also: mask });
|
|
2109
|
+
wandInvalidate();
|
|
2110
|
+
setEditNote("Tarkennettu " + bw + "×" + bh + " px — " + layers.length + " sävyä ("
|
|
2111
|
+
+ border.length + " reunalta kiinnitettyä), " + snapped + " sulautettu, " + ms + " ms.");
|
|
2112
|
+
}
|
|
2113
|
+
|
|
2114
|
+
// The colors actually on the picture at the border of this stroke.
|
|
2115
|
+
//
|
|
2116
|
+
// Not the whole frame's palette: what the patch has to agree with is the
|
|
2117
|
+
// handful of colors it is about to butt up against, and seeding the crop's
|
|
2118
|
+
// quantizer with those is what makes the border right by construction rather
|
|
2119
|
+
// than by correction afterwards. Sampled by walking a ring just outside the
|
|
2120
|
+
// stroke and asking which shape is on top at each step, which is the same
|
|
2121
|
+
// question the eye asks when it sees a seam.
|
|
2122
|
+
function borderColors(svg, pts, half) {
|
|
2123
|
+
var ctx = document.createElement("canvas").getContext("2d");
|
|
2124
|
+
var paths = Array.prototype.slice.call(svg.querySelectorAll("path"));
|
|
2125
|
+
var cache = paths.map(function (e) {
|
|
2126
|
+
return { fill: (e.getAttribute("fill") || "").toLowerCase(), p2: new Path2D(e.getAttribute("d") || "") };
|
|
2127
|
+
});
|
|
2128
|
+
var tally = {}, out = [];
|
|
2129
|
+
var ring = half + 2;
|
|
2130
|
+
for (var i = 0; i < pts.length; i += 2) {
|
|
2131
|
+
for (var a = 0; a < 12; a++) {
|
|
2132
|
+
var th = a / 12 * Math.PI * 2;
|
|
2133
|
+
var x = pts[i].x + ring * Math.cos(th), y = pts[i].y + ring * Math.sin(th);
|
|
2134
|
+
var top = null;
|
|
2135
|
+
for (var k = 0; k < cache.length; k++) {
|
|
2136
|
+
if (!/^#[0-9a-f]{6}$/.test(cache[k].fill)) continue;
|
|
2137
|
+
if (ctx.isPointInPath(cache[k].p2, x, y, "evenodd")) top = cache[k].fill;
|
|
2138
|
+
}
|
|
2139
|
+
if (top) tally[top] = (tally[top] || 0) + 1;
|
|
2140
|
+
}
|
|
2141
|
+
}
|
|
2142
|
+
Object.keys(tally).forEach(function (h) { out.push({ hex: h, n: tally[h] }); });
|
|
2143
|
+
out.sort(function (p, q) { return q.n - p.n; });
|
|
2144
|
+
return out.map(function (o) { return o.hex; });
|
|
2145
|
+
}
|
|
2146
|
+
|
|
2147
|
+
function paletteOf(hexes) {
|
|
2148
|
+
return hexes.map(function (f) {
|
|
2149
|
+
return { hex: f, r: parseInt(f.slice(1, 3), 16), g: parseInt(f.slice(3, 5), 16), b: parseInt(f.slice(5, 7), 16) };
|
|
2150
|
+
});
|
|
2151
|
+
}
|
|
2152
|
+
|
|
2153
|
+
// How far inside the stroke a point lies: 1 at the centre line, 0 at the edge.
|
|
2154
|
+
function strokeDepth(pt, pts, half) {
|
|
2155
|
+
var best = Infinity;
|
|
2156
|
+
for (var i = 0; i < pts.length; i++) {
|
|
2157
|
+
var a = pts[i], b = pts[i + 1] || pts[i];
|
|
2158
|
+
var vx = b.x - a.x, vy = b.y - a.y, len2 = vx * vx + vy * vy;
|
|
2159
|
+
var t = len2 < 1e-9 ? 0 : Math.max(0, Math.min(1, ((pt.x - a.x) * vx + (pt.y - a.y) * vy) / len2));
|
|
2160
|
+
var dx = pt.x - (a.x + t * vx), dy = pt.y - (a.y + t * vy);
|
|
2161
|
+
var d = dx * dx + dy * dy;
|
|
2162
|
+
if (d < best) best = d;
|
|
2163
|
+
}
|
|
2164
|
+
return Math.max(0, 1 - Math.sqrt(best) / half);
|
|
2165
|
+
}
|
|
2166
|
+
|
|
2167
|
+
// Bring the patch back to the surrounding colors at its border.
|
|
2168
|
+
//
|
|
2169
|
+
// The last of the four, and it still earns its keep. Seeding the palette,
|
|
2170
|
+
// padding the crop and feathering the mask take the boundary line out; this
|
|
2171
|
+
// takes the colour disagreement out. Measured across the boundary as excess
|
|
2172
|
+
// over what the picture has there naturally: 5.5 without it, 0.2 at 90, for
|
|
2173
|
+
// 12.8 → 11.9 of detail. Worth the trade, so 90 is the default. A patch
|
|
2174
|
+
// colour with no near neighbour outside keeps its own whatever this is set
|
|
2175
|
+
// to — that is the case none of the four can fix, and should not.
|
|
2176
|
+
//
|
|
2177
|
+
// A crop quantized on its own has colors the frame's palette does not, which
|
|
2178
|
+
// is exactly why it shows detail — and exactly why its edge is visible where
|
|
2179
|
+
// the two meet. So the tolerance for snapping a patch color onto a
|
|
2180
|
+
// surrounding one is widened toward the border: at the edge almost anything
|
|
2181
|
+
// snaps and the seam closes, deep inside almost nothing does and the detail
|
|
2182
|
+
// survives. A color that has no near neighbour in the surrounding palette
|
|
2183
|
+
// even at the edge keeps its own — sometimes the patch genuinely found
|
|
2184
|
+
// something the frame cannot say, and forcing that to the nearest swatch
|
|
2185
|
+
// would throw the find away.
|
|
2186
|
+
function blendEdge(fill, pt, pts, half, palette, reach) {
|
|
2187
|
+
if (reach <= 0 || !palette.length) return fill;
|
|
2188
|
+
var depth = strokeDepth(pt, pts, half);
|
|
2189
|
+
var tol = reach * (1 - depth);
|
|
2190
|
+
if (tol <= 0) return fill;
|
|
2191
|
+
var r = parseInt(fill.slice(1, 3), 16), g = parseInt(fill.slice(3, 5), 16), b = parseInt(fill.slice(5, 7), 16);
|
|
2192
|
+
var best = null, bestD = Infinity;
|
|
2193
|
+
for (var i = 0; i < palette.length; i++) {
|
|
2194
|
+
var c = palette[i];
|
|
2195
|
+
var d = Math.max(Math.abs(c.r - r), Math.abs(c.g - g), Math.abs(c.b - b));
|
|
2196
|
+
if (d < bestD) { bestD = d; best = c; }
|
|
2197
|
+
}
|
|
2198
|
+
return best && bestD <= tol ? best.hex : fill;
|
|
2199
|
+
}
|
|
2200
|
+
|
|
2201
|
+
var refineSeq = 0;
|
|
2202
|
+
|
|
2203
|
+
// ------------------------------------------------------------ pehmennä
|
|
2204
|
+
//
|
|
2205
|
+
// A brush that takes corners out of whatever it is dragged over. Each dab
|
|
2206
|
+
// pulls every point under it toward the midpoint of its neighbours, which is
|
|
2207
|
+
// what rounds a corner off, and then drops points that have become nearly
|
|
2208
|
+
// collinear, which is what makes the outline simpler rather than merely
|
|
2209
|
+
// rounder. Both are local and both are small, so the tool behaves like a
|
|
2210
|
+
// tool: one pass barely shows, a few passes over the same place take the
|
|
2211
|
+
// detail out, and a small shape loses points and area on every pass until
|
|
2212
|
+
// there is nothing left of it and it goes. It only touches shapes of the
|
|
2213
|
+
// chosen color, so a stroke across an eye can smooth the lashes without
|
|
2214
|
+
// touching the white beside them.
|
|
2215
|
+
|
|
2216
|
+
// The tracer emits M/L/C/Z with comma-separated pairs. Smoothing needs points
|
|
2217
|
+
// on the curve, and a cubic's control points are not on it, so a ring has to
|
|
2218
|
+
// be flattened before it can be worked — but only a ring the brush actually
|
|
2219
|
+
// reaches. Potrace-style curve fitting is far more economical than any list
|
|
2220
|
+
// of points (79 shapes here are 1272 cubics), so flattening a whole shape to
|
|
2221
|
+
// touch one edge of it made the file 2.5x bigger for nothing. Rings the brush
|
|
2222
|
+
// never meets keep their original text verbatim.
|
|
2223
|
+
function flattenRing(d) {
|
|
2224
|
+
var pts = [], cur = null, start = null;
|
|
2225
|
+
var re = /([MLCZ])([^MLCZ]*)/gi, m;
|
|
2226
|
+
function nums(t) { return (t.match(/-?\d+(?:\.\d+)?/g) || []).map(Number); }
|
|
2227
|
+
while ((m = re.exec(d))) {
|
|
2228
|
+
var cmd = m[1].toUpperCase(), arr = nums(m[2]);
|
|
2229
|
+
if (cmd === "M") { cur = { x: arr[0], y: arr[1] }; start = cur; pts.push(cur); }
|
|
2230
|
+
else if (cmd === "L") {
|
|
2231
|
+
for (var i = 0; i + 1 < arr.length; i += 2) { cur = { x: arr[i], y: arr[i+1] }; pts.push(cur); }
|
|
2232
|
+
} else if (cmd === "C") {
|
|
2233
|
+
for (var j = 0; j + 5 < arr.length; j += 6) {
|
|
2234
|
+
var c1 = { x: arr[j], y: arr[j+1] }, c2 = { x: arr[j+2], y: arr[j+3] },
|
|
2235
|
+
p3 = { x: arr[j+4], y: arr[j+5] };
|
|
2236
|
+
// Sample by how long the segment is, not by a fixed count: a curve
|
|
2237
|
+
// three pixels across does not need six points on it.
|
|
2238
|
+
var span = Math.hypot(c1.x-cur.x, c1.y-cur.y) + Math.hypot(c2.x-c1.x, c2.y-c1.y) + Math.hypot(p3.x-c2.x, p3.y-c2.y);
|
|
2239
|
+
var steps = Math.max(1, Math.min(8, Math.round(span / 1.6)));
|
|
2240
|
+
for (var k = 1; k <= steps; k++) {
|
|
2241
|
+
var t = k / steps, u = 1 - t;
|
|
2242
|
+
pts.push({
|
|
2243
|
+
x: u*u*u*cur.x + 3*u*u*t*c1.x + 3*u*t*t*c2.x + t*t*t*p3.x,
|
|
2244
|
+
y: u*u*u*cur.y + 3*u*u*t*c1.y + 3*u*t*t*c2.y + t*t*t*p3.y
|
|
2245
|
+
});
|
|
2246
|
+
}
|
|
2247
|
+
cur = p3;
|
|
2248
|
+
}
|
|
2249
|
+
} else if (cmd === "Z") { cur = start; }
|
|
2250
|
+
}
|
|
2251
|
+
while (pts.length > 2 && Math.abs(pts[0].x - pts[pts.length-1].x) < 1e-6
|
|
2252
|
+
&& Math.abs(pts[0].y - pts[pts.length-1].y) < 1e-6) pts.pop();
|
|
2253
|
+
return pts;
|
|
2254
|
+
}
|
|
2255
|
+
|
|
2256
|
+
// Douglas-Peucker. Flattening a ring costs far more than the curve fit it
|
|
2257
|
+
// replaced — potrace describes these shapes in a couple of hundred cubics and
|
|
2258
|
+
// a point list needs thousands — and the whole document pays for it: past
|
|
2259
|
+
// roughly half a megabyte Chromium starts rasterizing an SVG image in
|
|
2260
|
+
// degraded tiles, which shows up as a rectangular patch of blended color that
|
|
2261
|
+
// no single shape accounts for. Dropping points that the straight line
|
|
2262
|
+
// between their neighbours already describes keeps the outline and most of
|
|
2263
|
+
// the size.
|
|
2264
|
+
function simplify(pts, tol) {
|
|
2265
|
+
if (pts.length < 5) return pts;
|
|
2266
|
+
var keep = new Uint8Array(pts.length);
|
|
2267
|
+
keep[0] = 1;
|
|
2268
|
+
keep[pts.length - 1] = 1;
|
|
2269
|
+
var stack = [[0, pts.length - 1]];
|
|
2270
|
+
while (stack.length) {
|
|
2271
|
+
var seg = stack.pop(), i0 = seg[0], i1 = seg[1];
|
|
2272
|
+
var a = pts[i0], b = pts[i1];
|
|
2273
|
+
var vx = b.x - a.x, vy = b.y - a.y, len = Math.hypot(vx, vy);
|
|
2274
|
+
var worst = -1, wd = tol;
|
|
2275
|
+
for (var i = i0 + 1; i < i1; i++) {
|
|
2276
|
+
var p = pts[i];
|
|
2277
|
+
var d = len < 1e-9 ? Math.hypot(p.x - a.x, p.y - a.y)
|
|
2278
|
+
: Math.abs(vx * (a.y - p.y) - vy * (a.x - p.x)) / len;
|
|
2279
|
+
if (d > wd) { wd = d; worst = i; }
|
|
2280
|
+
}
|
|
2281
|
+
if (worst > 0) {
|
|
2282
|
+
keep[worst] = 1;
|
|
2283
|
+
stack.push([i0, worst], [worst, i1]);
|
|
2284
|
+
}
|
|
2285
|
+
}
|
|
2286
|
+
var out = [];
|
|
2287
|
+
for (var k = 0; k < pts.length; k++) if (keep[k]) out.push(pts[k]);
|
|
2288
|
+
return out.length >= 5 ? out : pts;
|
|
2289
|
+
}
|
|
2290
|
+
|
|
2291
|
+
function ringArea(r) {
|
|
2292
|
+
var a = 0;
|
|
2293
|
+
for (var i = 0, n = r.length; i < n; i++) {
|
|
2294
|
+
var p = r[i], q = r[(i + 1) % n];
|
|
2295
|
+
a += p.x * q.y - q.x * p.y;
|
|
2296
|
+
}
|
|
2297
|
+
return Math.abs(a) / 2;
|
|
2298
|
+
}
|
|
2299
|
+
|
|
2300
|
+
// Redraw as curves rather than as the polygon it now is: a Catmull-Rom
|
|
2301
|
+
// through the points, written out as the cubics SVG wants.
|
|
2302
|
+
function curveString(r) {
|
|
2303
|
+
var n = r.length;
|
|
2304
|
+
var d = "M" + r[0].x.toFixed(2) + "," + r[0].y.toFixed(2);
|
|
2305
|
+
for (var i = 0; i < n; i++) {
|
|
2306
|
+
var p0 = r[(i - 1 + n) % n], p1 = r[i], p2 = r[(i + 1) % n], p3 = r[(i + 2) % n];
|
|
2307
|
+
d += "C" + (p1.x + (p2.x - p0.x) / 6).toFixed(2) + "," + (p1.y + (p2.y - p0.y) / 6).toFixed(2)
|
|
2308
|
+
+ " " + (p2.x - (p3.x - p1.x) / 6).toFixed(2) + "," + (p2.y - (p3.y - p1.y) / 6).toFixed(2)
|
|
2309
|
+
+ " " + p2.x.toFixed(2) + "," + p2.y.toFixed(2);
|
|
2310
|
+
}
|
|
2311
|
+
return d + "Z";
|
|
2312
|
+
}
|
|
2313
|
+
|
|
2314
|
+
function ringsFor(el) {
|
|
2315
|
+
if (!el.__rings) {
|
|
2316
|
+
el.__rings = ringsOf(el.getAttribute("d") || "").map(function (r) {
|
|
2317
|
+
return { d: r.d, box: r.box, pts: null };
|
|
2318
|
+
});
|
|
2319
|
+
}
|
|
2320
|
+
return el.__rings;
|
|
2321
|
+
}
|
|
2322
|
+
|
|
2323
|
+
function writeRings(el) {
|
|
2324
|
+
var d = "";
|
|
2325
|
+
el.__rings.forEach(function (r) { d += (r.pts ? curveString(r.pts) : r.d) + " "; });
|
|
2326
|
+
el.setAttribute("d", d.trim());
|
|
2327
|
+
}
|
|
2328
|
+
|
|
2329
|
+
// Pulling points toward their neighbours closes a narrow neck, and once the
|
|
2330
|
+
// two sides of a neck cross, evenodd turns the inside of the ring into a mesh
|
|
2331
|
+
// of alternating filled and empty slivers — on screen a muddy block of a
|
|
2332
|
+
// color that is in no palette. So a dab that makes the outline cross itself
|
|
2333
|
+
// is refused and the ring is left as it was. Only segments the brush touched
|
|
2334
|
+
// can have started a crossing, and a crossing they start is next to them, so
|
|
2335
|
+
// testing that neighbourhood against itself is enough and stays cheap.
|
|
2336
|
+
function segHit(a, b, c, d) {
|
|
2337
|
+
function side(p, q, r) { return (q.x - p.x) * (r.y - p.y) - (q.y - p.y) * (r.x - p.x); }
|
|
2338
|
+
var d1 = side(c, d, a), d2 = side(c, d, b), d3 = side(a, b, c), d4 = side(a, b, d);
|
|
2339
|
+
return ((d1 > 0) !== (d2 > 0)) && ((d3 > 0) !== (d4 > 0));
|
|
2340
|
+
}
|
|
2341
|
+
|
|
2342
|
+
function selfCrosses(ring, cx, cy, radius) {
|
|
2343
|
+
var n = ring.length, reach = radius * 1.5, r2 = reach * reach, near = [];
|
|
2344
|
+
for (var i = 0; i < n; i++) {
|
|
2345
|
+
var p = ring[i], q = ring[(i + 1) % n];
|
|
2346
|
+
var mx = (p.x + q.x) / 2 - cx, my = (p.y + q.y) / 2 - cy;
|
|
2347
|
+
if (mx * mx + my * my <= r2) near.push(i);
|
|
2348
|
+
if (near.length > 400) break; // stay bounded on a huge ring
|
|
2349
|
+
}
|
|
2350
|
+
for (var a = 0; a < near.length; a++) {
|
|
2351
|
+
var i1 = near[a], p1 = ring[i1], q1 = ring[(i1 + 1) % n];
|
|
2352
|
+
for (var b = a + 1; b < near.length; b++) {
|
|
2353
|
+
var i2 = near[b];
|
|
2354
|
+
if (i2 === i1 || (i2 + 1) % n === i1 || (i1 + 1) % n === i2) continue;
|
|
2355
|
+
if (segHit(p1, q1, ring[i2], ring[(i2 + 1) % n])) return true;
|
|
2356
|
+
}
|
|
2357
|
+
}
|
|
2358
|
+
return false;
|
|
2359
|
+
}
|
|
2360
|
+
|
|
2361
|
+
var SMOOTH_PULL = 0.6; // how far a point moves toward its neighbours' midpoint
|
|
2362
|
+
var SMOOTH_DROP = 0.7; // how flat a corner has to be before it is dropped
|
|
2363
|
+
var SMOOTH_MIN_AREA = 6; // a ring smaller than this has stopped being a shape
|
|
2364
|
+
|
|
2365
|
+
function smoothDab(el, cx, cy, radius) {
|
|
2366
|
+
var rings = ringsFor(el), r2 = radius * radius, changed = false, kept = [];
|
|
2367
|
+
for (var s = 0; s < rings.length; s++) {
|
|
2368
|
+
var rec = rings[s], bx = rec.box;
|
|
2369
|
+
if (bx && (cx < bx[0] - radius || cx > bx[2] + radius || cy < bx[1] - radius || cy > bx[3] + radius)) {
|
|
2370
|
+
kept.push(rec);
|
|
2371
|
+
continue;
|
|
2372
|
+
}
|
|
2373
|
+
if (!rec.pts) rec.pts = flattenRing(rec.d);
|
|
2374
|
+
var ring = rec.pts, n = ring.length;
|
|
2375
|
+
if (n < 5) { kept.push(rec); continue; }
|
|
2376
|
+
var hit = false;
|
|
2377
|
+
var moved = ring.map(function (p, i) {
|
|
2378
|
+
var dx = p.x - cx, dy = p.y - cy, d2 = dx*dx + dy*dy;
|
|
2379
|
+
if (d2 > r2) return p;
|
|
2380
|
+
hit = true;
|
|
2381
|
+
var a = ring[(i - 1 + n) % n], b = ring[(i + 1) % n];
|
|
2382
|
+
var t = SMOOTH_PULL * (1 - Math.sqrt(d2) / radius);
|
|
2383
|
+
return { x: p.x + t * ((a.x + b.x) / 2 - p.x), y: p.y + t * ((a.y + b.y) / 2 - p.y) };
|
|
2384
|
+
});
|
|
2385
|
+
if (!hit) { kept.push(rec); continue; }
|
|
2386
|
+
changed = true;
|
|
2387
|
+
// Drop points that are now nearly on the line between their neighbours.
|
|
2388
|
+
// This is the half that simplifies rather than merely rounds, and it is
|
|
2389
|
+
// what makes a small shape lose itself a piece at a time.
|
|
2390
|
+
var thin = [];
|
|
2391
|
+
for (var i = 0; i < moved.length; i++) {
|
|
2392
|
+
var p = moved[i];
|
|
2393
|
+
var a = thin.length ? thin[thin.length - 1] : moved[(i - 1 + moved.length) % moved.length];
|
|
2394
|
+
var b = moved[(i + 1) % moved.length];
|
|
2395
|
+
var dx2 = p.x - cx, dy2 = p.y - cy;
|
|
2396
|
+
if (dx2*dx2 + dy2*dy2 > r2 || moved.length - (moved.length - thin.length) <= 5) { thin.push(p); continue; }
|
|
2397
|
+
var vx = b.x - a.x, vy = b.y - a.y, len = Math.hypot(vx, vy);
|
|
2398
|
+
var dev = len < 1e-6 ? 0 : Math.abs(vx * (a.y - p.y) - vy * (a.x - p.x)) / len;
|
|
2399
|
+
if (dev > SMOOTH_DROP) thin.push(p);
|
|
2400
|
+
}
|
|
2401
|
+
// A ring that fits inside the brush is one of the small shapes this tool
|
|
2402
|
+
// is meant to be able to erase by dwelling on it. Pulling every point
|
|
2403
|
+
// toward its neighbours shrinks such a ring, but only by a few percent a
|
|
2404
|
+
// pass, so on its own the shape survives far longer than "a moment".
|
|
2405
|
+
// Halving its points as well takes its corners away several at a time,
|
|
2406
|
+
// and it collapses in a handful of passes instead of dozens.
|
|
2407
|
+
var whole = true;
|
|
2408
|
+
for (var w = 0; w < thin.length; w++) {
|
|
2409
|
+
var wx = thin[w].x - cx, wy = thin[w].y - cy;
|
|
2410
|
+
if (wx*wx + wy*wy > r2) { whole = false; break; }
|
|
2411
|
+
}
|
|
2412
|
+
if (whole && thin.length > 8) {
|
|
2413
|
+
var half = [];
|
|
2414
|
+
for (var h = 0; h < thin.length; h += 2) half.push(thin[h]);
|
|
2415
|
+
thin = half;
|
|
2416
|
+
}
|
|
2417
|
+
thin = simplify(thin, 0.8);
|
|
2418
|
+
if (selfCrosses(thin, cx, cy, radius)) { kept.push(rec); continue; }
|
|
2419
|
+
if (thin.length >= 5 && ringArea(thin) >= SMOOTH_MIN_AREA) {
|
|
2420
|
+
rec.pts = thin;
|
|
2421
|
+
rec.box = null; // it moved; stop trusting the old box
|
|
2422
|
+
kept.push(rec);
|
|
2423
|
+
}
|
|
2424
|
+
// else: the ring has shrunk out of existence and is simply not kept
|
|
2425
|
+
}
|
|
2426
|
+
if (!changed) return false;
|
|
2427
|
+
el.__rings = kept;
|
|
2428
|
+
if (!kept.length) {
|
|
2429
|
+
// A shape the brush dissolved is part of the stroke that dissolved it.
|
|
2430
|
+
// Recording it separately made that one stroke cost two clicks to take
|
|
2431
|
+
// back, which is not how a brush is supposed to behave.
|
|
2432
|
+
if (!strokeTouched || !strokeTouched.has(el)) {
|
|
2433
|
+
editStack.push({ kind: "remove", el: el, parent: el.parentNode, next: el.nextSibling });
|
|
2434
|
+
}
|
|
2435
|
+
wandInvalidate();
|
|
2436
|
+
el.parentNode.removeChild(el);
|
|
2437
|
+
return true;
|
|
2438
|
+
}
|
|
2439
|
+
writeRings(el);
|
|
2440
|
+
return true;
|
|
2441
|
+
}
|
|
2442
|
+
|
|
2443
|
+
var brushing = false, strokeTouched = null;
|
|
2444
|
+
|
|
2445
|
+
function toUser(ev, svg) {
|
|
2446
|
+
var vb = (svg.getAttribute("viewBox") || "").split(/[\s,]+/).map(Number);
|
|
2447
|
+
var rect = svg.getBoundingClientRect();
|
|
2448
|
+
var vw = vb.length === 4 ? vb[2] : rect.width, vh = vb.length === 4 ? vb[3] : rect.height;
|
|
2449
|
+
return {
|
|
2450
|
+
x: (ev.clientX - rect.left) / rect.width * vw,
|
|
2451
|
+
y: (ev.clientY - rect.top) / rect.height * vh,
|
|
2452
|
+
scale: vw / rect.width
|
|
2453
|
+
};
|
|
2454
|
+
}
|
|
2455
|
+
|
|
2456
|
+
function brushAt(ev) {
|
|
2457
|
+
var svg = outSvg();
|
|
2458
|
+
if (!svg) return;
|
|
2459
|
+
var u = toUser(ev, svg);
|
|
2460
|
+
var radius = Math.max(3, (+refineSize.value) / 2);
|
|
2461
|
+
var want = editColor.value.toLowerCase();
|
|
2462
|
+
var paths = Array.prototype.slice.call(svg.querySelectorAll("path"));
|
|
2463
|
+
var hits = 0;
|
|
2464
|
+
paths.forEach(function (el) {
|
|
2465
|
+
if ((el.getAttribute("fill") || "").toLowerCase() !== want) return;
|
|
2466
|
+
var b;
|
|
2467
|
+
try { b = el.getBBox(); } catch (e) { return; }
|
|
2468
|
+
if (u.x < b.x - radius || u.x > b.x + b.width + radius ||
|
|
2469
|
+
u.y < b.y - radius || u.y > b.y + b.height + radius) return;
|
|
2470
|
+
// A stroke is one action, so it is one undo step however many shapes it
|
|
2471
|
+
// crosses — recording a shape at a time meant a drag over twenty shapes
|
|
2472
|
+
// took twenty clicks to take back.
|
|
2473
|
+
if (!strokeTouched.has(el)) {
|
|
2474
|
+
strokeTouched.set(el, { d: el.getAttribute("d"), parent: el.parentNode,
|
|
2475
|
+
next: el.nextSibling });
|
|
2476
|
+
}
|
|
2477
|
+
if (smoothDab(el, u.x, u.y, radius)) hits++;
|
|
2478
|
+
});
|
|
2479
|
+
if (hits) setEditNote("Pehmennetään " + want + " — " + hits + " muotoa kosketettu.");
|
|
2480
|
+
}
|
|
2481
|
+
|
|
2482
|
+
function endStroke() {
|
|
2483
|
+
if (!brushing) return;
|
|
2484
|
+
brushing = false;
|
|
2485
|
+
if (strokeTouched && strokeTouched.size) {
|
|
2486
|
+
var was = [];
|
|
2487
|
+
strokeTouched.forEach(function (rec, el) {
|
|
2488
|
+
was.push({ el: el, d: rec.d, parent: rec.parent, next: rec.next });
|
|
2489
|
+
});
|
|
2490
|
+
editStack.push({ kind: "stroke", was: was });
|
|
2491
|
+
wandInvalidate();
|
|
2492
|
+
}
|
|
2493
|
+
strokeTouched = null;
|
|
2494
|
+
}
|
|
2495
|
+
|
|
2496
|
+
// The lasso. A drag in wand mode draws the rough outline; a click still
|
|
2497
|
+
// floods by color, so the two live on the same tool and the gesture picks.
|
|
2498
|
+
// The click that ends a drag is swallowed, or releasing the lasso would
|
|
2499
|
+
// immediately reselect whatever was under the pointer.
|
|
2500
|
+
// A drag ends in a click on whatever was under the pointer, and that click
|
|
2501
|
+
// would immediately reselect it, so the drag swallows it. By time, not by a
|
|
2502
|
+
// flag left standing: a drag that ends off the picture never produces the
|
|
2503
|
+
// click it is waiting for, and the flag then eats the next real one — the
|
|
2504
|
+
// first click of the next selection, and only the first, which is exactly
|
|
2505
|
+
// the kind of thing that gets blamed on the picture rather than the tool.
|
|
2506
|
+
var lassoPts = null, lassoEl = null, lassoAte = 0;
|
|
2507
|
+
|
|
2508
|
+
function lassoDraw() {
|
|
2509
|
+
if (!lassoEl || !lassoPts) return;
|
|
2510
|
+
var d = "M" + lassoPts.map(function (p) { return p.x + "," + p.y; }).join("L");
|
|
2511
|
+
lassoEl.setAttribute("d", d + "Z");
|
|
2512
|
+
}
|
|
2513
|
+
|
|
2514
|
+
function lassoEnd(ev) {
|
|
2515
|
+
var pts = lassoPts, el = lassoEl;
|
|
2516
|
+
lassoPts = null; lassoEl = null;
|
|
2517
|
+
if (el && el.parentNode) el.parentNode.removeChild(el);
|
|
2518
|
+
if (!pts || pts.length < 3) return false;
|
|
2519
|
+
var x0 = Infinity, y0 = Infinity, x1 = -Infinity, y1 = -Infinity;
|
|
2520
|
+
pts.forEach(function (p) {
|
|
2521
|
+
if (p.x < x0) x0 = p.x; if (p.x > x1) x1 = p.x;
|
|
2522
|
+
if (p.y < y0) y0 = p.y; if (p.y > y1) y1 = p.y;
|
|
2523
|
+
});
|
|
2524
|
+
// Too small to be an outline: that was a click, and a click means flood.
|
|
2525
|
+
if (x1 - x0 < 6 && y1 - y0 < 6) return false;
|
|
2526
|
+
wandLassoDone(pts, ev);
|
|
2527
|
+
return true;
|
|
2528
|
+
}
|
|
2529
|
+
|
|
2530
|
+
outStage.addEventListener("pointerdown", function (ev) {
|
|
2531
|
+
if (!editOn || picking) return;
|
|
2532
|
+
var svg = outSvg();
|
|
2533
|
+
if (!svg) return;
|
|
2534
|
+
if (editTool === "wand") {
|
|
2535
|
+
ev.preventDefault();
|
|
2536
|
+
lassoAte = 0;
|
|
2537
|
+
lassoPts = [toUser(ev, svg)];
|
|
2538
|
+
lassoEl = document.createElementNS(SVGNS, "path");
|
|
2539
|
+
lassoEl.setAttribute("fill", "none");
|
|
2540
|
+
lassoEl.setAttribute("stroke", "#ff2ec4");
|
|
2541
|
+
lassoEl.setAttribute("stroke-dasharray", "5 4");
|
|
2542
|
+
lassoEl.setAttribute("stroke-width", "1.5");
|
|
2543
|
+
lassoEl.setAttribute("vector-effect", "non-scaling-stroke");
|
|
2544
|
+
lassoEl.setAttribute("pointer-events", "none");
|
|
2545
|
+
svg.appendChild(lassoEl);
|
|
2546
|
+
lassoDraw();
|
|
2547
|
+
return;
|
|
2548
|
+
}
|
|
2549
|
+
if (editTool !== "smooth" && editTool !== "refine") return;
|
|
2550
|
+
ev.preventDefault();
|
|
2551
|
+
brushing = true;
|
|
2552
|
+
if (editTool === "smooth") {
|
|
2553
|
+
strokeTouched = new Map();
|
|
2554
|
+
brushAt(ev);
|
|
2555
|
+
} else {
|
|
2556
|
+
refineBegin(toUser(ev, svg));
|
|
2557
|
+
}
|
|
2558
|
+
});
|
|
2559
|
+
outStage.addEventListener("pointermove", function (ev) {
|
|
2560
|
+
if (lassoPts) {
|
|
2561
|
+
var svg2 = outSvg();
|
|
2562
|
+
if (svg2) { lassoPts.push(toUser(ev, svg2)); lassoDraw(); }
|
|
2563
|
+
return;
|
|
2564
|
+
}
|
|
2565
|
+
if (!brushing) return;
|
|
2566
|
+
if (editTool === "smooth") brushAt(ev);
|
|
2567
|
+
else { var svg = outSvg(); if (svg) refineExtend(toUser(ev, svg)); }
|
|
2568
|
+
});
|
|
2569
|
+
["pointerup", "pointerleave", "pointercancel"].forEach(function (t) {
|
|
2570
|
+
outStage.addEventListener(t, function (ev) {
|
|
2571
|
+
if (lassoPts) { lassoAte = lassoEnd(ev) ? performance.now() : 0; return; }
|
|
2572
|
+
if (!brushing) return;
|
|
2573
|
+
if (editTool === "refine") { brushing = false; refineFinish(); }
|
|
2574
|
+
else endStroke();
|
|
2575
|
+
});
|
|
2576
|
+
});
|
|
2577
|
+
|
|
2578
|
+
$("editUndo").addEventListener("click", function () {
|
|
2579
|
+
var last = editStack.pop();
|
|
2580
|
+
if (!last) { setEditNote("Ei kumottavaa."); return; }
|
|
2581
|
+
if (last.kind === "fill") { last.el.setAttribute("fill", last.was); wandInvalidate(); }
|
|
2582
|
+
else if (last.kind === "d") { last.el.setAttribute("d", last.was); last.el.__rings = null; }
|
|
2583
|
+
else if (last.kind === "stroke") {
|
|
2584
|
+
last.was.forEach(function (w) {
|
|
2585
|
+
w.el.setAttribute("d", w.d);
|
|
2586
|
+
w.el.__rings = null;
|
|
2587
|
+
if (w.el.parentNode) return;
|
|
2588
|
+
// A shape the stroke erased, put back where it was drawn — appending it
|
|
2589
|
+
// to the end would restore the outline but not the stacking order.
|
|
2590
|
+
var parent = w.parent || outSvg();
|
|
2591
|
+
var next = w.next && w.next.parentNode === parent ? w.next : null;
|
|
2592
|
+
parent.insertBefore(w.el, next);
|
|
2593
|
+
});
|
|
2594
|
+
}
|
|
2595
|
+
else if (last.kind === "remove") { last.parent.insertBefore(last.el, last.next); last.el.__rings = null; }
|
|
2596
|
+
else if (last.kind === "isolate") {
|
|
2597
|
+
// Back to front: a removed shape's next sibling is either one that was
|
|
2598
|
+
// kept or one already put back, never one still missing.
|
|
2599
|
+
for (var r = last.removed.length - 1; r >= 0; r--) {
|
|
2600
|
+
var rec = last.removed[r];
|
|
2601
|
+
if (rec.intoMask) {
|
|
2602
|
+
rec.el.setAttribute("fill", rec.fill);
|
|
2603
|
+
rec.intoMask = false;
|
|
2604
|
+
}
|
|
2605
|
+
rec.parent.insertBefore(rec.el, rec.next);
|
|
2606
|
+
}
|
|
2607
|
+
if (last.masked) last.masked.forEach(function (el) { el.removeAttribute("mask"); });
|
|
2608
|
+
if (last.clipped) last.clipped.forEach(function (el) { el.removeAttribute("clip-path"); });
|
|
2609
|
+
if (last.clipEls) {
|
|
2610
|
+
last.clipEls.forEach(function (el) { if (el.parentNode) el.parentNode.removeChild(el); });
|
|
2611
|
+
}
|
|
2612
|
+
if (last.maskEl && last.maskEl.parentNode) last.maskEl.parentNode.removeChild(last.maskEl);
|
|
2613
|
+
if (last.vb) {
|
|
2614
|
+
var isv = outSvg();
|
|
2615
|
+
if (isv) {
|
|
2616
|
+
isv.setAttribute("viewBox", last.vb.viewBox);
|
|
2617
|
+
isv.setAttribute("width", last.vb.w);
|
|
2618
|
+
isv.setAttribute("height", last.vb.h);
|
|
2619
|
+
}
|
|
2620
|
+
}
|
|
2621
|
+
wandClear();
|
|
2622
|
+
wandInvalidate();
|
|
2623
|
+
}
|
|
2624
|
+
else {
|
|
2625
|
+
last.el.parentNode.removeChild(last.el);
|
|
2626
|
+
if (last.also && last.also.parentNode) last.also.parentNode.removeChild(last.also);
|
|
2627
|
+
}
|
|
2628
|
+
setEditNote("Kumottu.");
|
|
2629
|
+
});
|
|
2630
|
+
|
|
2631
|
+
$("editReset").addEventListener("click", function () {
|
|
2632
|
+
if (!lastSvg) return;
|
|
2633
|
+
outStage.innerHTML = lastSvg;
|
|
2634
|
+
editStack = [];
|
|
2635
|
+
wandSel = null;
|
|
2636
|
+
wandInvalidate();
|
|
2637
|
+
if (editOn) { explode(outSvg()); setEditNote("Palautettu jäljitettyyn tulokseen."); }
|
|
2638
|
+
});
|
|
2639
|
+
|
|
2640
|
+
dlBtn.addEventListener("click", function () {
|
|
2641
|
+
if (!lastSvg) return;
|
|
2642
|
+
// What is on screen is what gets saved — edits included.
|
|
2643
|
+
var live = outSvg();
|
|
2644
|
+
if (live) {
|
|
2645
|
+
live = live.cloneNode(true);
|
|
2646
|
+
Array.prototype.forEach.call(live.querySelectorAll(".wand-off"), function (el) {
|
|
2647
|
+
el.classList.remove("wand-off");
|
|
2648
|
+
if (!el.getAttribute("class")) el.removeAttribute("class");
|
|
2649
|
+
});
|
|
2650
|
+
}
|
|
2651
|
+
var text = live ? new XMLSerializer().serializeToString(live) : lastSvg;
|
|
2652
|
+
var blob = new Blob([text], { type: "image/svg+xml;charset=utf-8" });
|
|
2653
|
+
var a = document.createElement("a");
|
|
2654
|
+
a.href = URL.createObjectURL(blob);
|
|
2655
|
+
a.download = (sourceBitmap && sourceBitmap.name ? sourceBitmap.name.replace(/\.[^.]+$/, "") : "trace") + ".svg";
|
|
2656
|
+
a.click();
|
|
2657
|
+
URL.revokeObjectURL(a.href);
|
|
2658
|
+
});
|
|
2659
|
+
|
|
2660
|
+
function vectorize() {
|
|
2661
|
+
if (!sourceBitmap) return;
|
|
2662
|
+
if (busy) { pending = true; return; }
|
|
2663
|
+
busy = true;
|
|
2664
|
+
pending = false;
|
|
2665
|
+
runBtn.disabled = true;
|
|
2666
|
+
setStatus("Jäljitetään…");
|
|
2667
|
+
|
|
2668
|
+
// Yield so the status paints before the sync trace work.
|
|
2669
|
+
setTimeout(function () {
|
|
2670
|
+
try {
|
|
2671
|
+
var t0 = performance.now();
|
|
2672
|
+
var opts = EvgTraceOptions.defaults();
|
|
2673
|
+
opts.colorCount = +colorCount.value;
|
|
2674
|
+
opts.threshold = +threshold.value;
|
|
2675
|
+
opts.skipLuma = +skipLuma.value;
|
|
2676
|
+
opts.bgMode = bgMode.value;
|
|
2677
|
+
opts.minColorDelta = +minColorDelta.value;
|
|
2678
|
+
opts.minRegion = +minRegion.value;
|
|
2679
|
+
opts.absorbContrast = +absorbContrast.value;
|
|
2680
|
+
opts.paletteChroma = +paletteChroma.value;
|
|
2681
|
+
opts.paletteMute = +paletteMute.value;
|
|
2682
|
+
opts.paletteTint = +paletteTint.value;
|
|
2683
|
+
opts.paletteWarm = +paletteWarm.value;
|
|
2684
|
+
opts.paletteContrast = +paletteContrast.value;
|
|
2685
|
+
opts.smooth = +smooth.value;
|
|
2686
|
+
opts.layerMode = layerMode.value;
|
|
2687
|
+
opts.bgColor = bgColor.value;
|
|
2688
|
+
opts.bgTolerance = +bgTolerance.value;
|
|
2689
|
+
opts.contourMode = contourMode.value;
|
|
2690
|
+
opts.contourEdge = +contourEdge.value;
|
|
2691
|
+
opts.contourSpread = +contourSpread.value;
|
|
2692
|
+
opts.gradientFill = !!gradientFill.checked;
|
|
2693
|
+
opts.gradientGain = +gradientGain.value;
|
|
2694
|
+
opts.overlaySimilar = +overlaySimilar.value;
|
|
2695
|
+
opts.detailTrueColor = detailTrueColor.checked;
|
|
2696
|
+
opts.detailMinShare = +detailMinShare.value;
|
|
2697
|
+
opts.detailColorMerge = +detailColorMerge.value;
|
|
2698
|
+
opts.detailColorMax = +detailColorMax.value;
|
|
2699
|
+
opts.edgeMinRun = +edgeMinRun.value;
|
|
2700
|
+
opts.paletteMode = paletteMode.value;
|
|
2701
|
+
opts.paletteBias = paletteBias.value;
|
|
2702
|
+
opts.paletteHex = paletteMode.value === "auto" ? [] : userPalette.slice();
|
|
2703
|
+
opts.turdsize = +turdsize.value;
|
|
2704
|
+
opts.alphamax = +alphamax.value;
|
|
2705
|
+
opts.opttolerance = +opttolerance.value;
|
|
2706
|
+
opts.optcurve = !!optcurve.checked;
|
|
2707
|
+
opts.turnpolicy = turnpolicy.value;
|
|
2708
|
+
opts.blackOnWhite = !!blackOnWhite.checked;
|
|
2709
|
+
opts.fillHex = fillHex.value;
|
|
2710
|
+
|
|
2711
|
+
buildProcessed();
|
|
2712
|
+
var img = new ImageBuffer();
|
|
2713
|
+
img.init(sourceBitmap.w, sourceBitmap.h);
|
|
2714
|
+
var rgba = workingPixels();
|
|
2715
|
+
var n = sourceBitmap.w * sourceBitmap.h;
|
|
2716
|
+
for (var i = 0; i < n; i++) {
|
|
2717
|
+
var o = i * 4;
|
|
2718
|
+
var x = i % sourceBitmap.w;
|
|
2719
|
+
var y = (i / sourceBitmap.w) | 0;
|
|
2720
|
+
img.setPixelRGBA(x, y, rgba[o], rgba[o + 1], rgba[o + 2], rgba[o + 3]);
|
|
2721
|
+
}
|
|
2722
|
+
|
|
2723
|
+
var tr = EvgBitmapTracer.fromImageBuffer(img, opts);
|
|
2724
|
+
tr.trace();
|
|
2725
|
+
var svg = tr.toSVG();
|
|
2726
|
+
var ms = Math.round(performance.now() - t0);
|
|
2727
|
+
lastSvg = svg;
|
|
2728
|
+
|
|
2729
|
+
outStage.classList.remove("empty");
|
|
2730
|
+
// A new trace replaces the drawing edit mode was working on, so it also
|
|
2731
|
+
// ends the session rather than leaving a stale one pointing at nothing.
|
|
2732
|
+
if (editOn) leaveEdit();
|
|
2733
|
+
outStage.innerHTML = svg;
|
|
2734
|
+
dlBtn.disabled = !svg;
|
|
2735
|
+
editToggle.disabled = !svg;
|
|
2736
|
+
|
|
2737
|
+
var layers = tr.getLayers ? tr.getLayers() : [];
|
|
2738
|
+
var layerN = tr.layerCount ? tr.layerCount() : (layers ? layers.length : 0);
|
|
2739
|
+
paletteEl.innerHTML = "";
|
|
2740
|
+
lastPalette = [];
|
|
2741
|
+
if (layers && layers.length > 1) {
|
|
2742
|
+
paletteEl.hidden = false;
|
|
2743
|
+
// One swatch per color, not per layer. A layer is a shape, and the
|
|
2744
|
+
// overlay algorithm makes hundreds of them out of a handful of
|
|
2745
|
+
// colors — the strip was showing 407 squares for 18 colors, which
|
|
2746
|
+
// reads as a palette that has run away when it has not.
|
|
2747
|
+
var seenHex = {};
|
|
2748
|
+
for (var li = 0; li < layers.length; li++) {
|
|
2749
|
+
var hex = layers[li].fillHex || "#000000";
|
|
2750
|
+
if (seenHex[hex]) continue;
|
|
2751
|
+
seenHex[hex] = 1;
|
|
2752
|
+
lastPalette.push(hex);
|
|
2753
|
+
var sw = document.createElement("span");
|
|
2754
|
+
sw.className = "sw";
|
|
2755
|
+
sw.style.background = hex;
|
|
2756
|
+
sw.title = hex + " — klikkaa ottaaksesi tämän paletin muokattavaksi";
|
|
2757
|
+
sw.addEventListener("click", function () { adoptPalette(lastPalette); });
|
|
2758
|
+
paletteEl.appendChild(sw);
|
|
2759
|
+
}
|
|
2760
|
+
} else {
|
|
2761
|
+
paletteEl.hidden = true;
|
|
2762
|
+
}
|
|
2763
|
+
|
|
2764
|
+
var rings = tr.rings ? tr.rings.length : 0;
|
|
2765
|
+
// In "omat" mode the slider is irrelevant — the list is the count.
|
|
2766
|
+
var pmode = paletteMode.value;
|
|
2767
|
+
var asked = pmode === "fixed" ? userPalette.length : +colorCount.value;
|
|
2768
|
+
var collapsed = "";
|
|
2769
|
+
if (asked > 1 && layerN < asked) {
|
|
2770
|
+
collapsed = pmode === "auto"
|
|
2771
|
+
? " (kuvassa ei ole " + asked + " erottuvaa sävyä)"
|
|
2772
|
+
: " (" + (asked - layerN) + "/" + asked + " sävyä jäi käyttämättä)";
|
|
2773
|
+
}
|
|
2774
|
+
var bgNote = "";
|
|
2775
|
+
if (asked > 1 && bgMode.value === "auto" &&
|
|
2776
|
+
tr.backgroundRemoved && !tr.backgroundRemoved()) {
|
|
2777
|
+
bgNote = " · tausta jäi (reuna ei yhtenäinen — kokeile Tausta: väri)";
|
|
2778
|
+
}
|
|
2779
|
+
// Everything up to the shape count is a fact about the trace and stays
|
|
2780
|
+
// put; the shape count and the byte count are facts about the drawing,
|
|
2781
|
+
// and edit mode changes the drawing. Keeping the two apart is what lets
|
|
2782
|
+
// the live half be refreshed without re-running anything.
|
|
2783
|
+
statusHead = "<b>OK</b> · " + sourceBitmap.w + "×" + sourceBitmap.h
|
|
2784
|
+
+ " · " + ms + " ms · layers=" + layerN + collapsed + bgNote
|
|
2785
|
+
+ (rings ? " · rings=" + rings : "");
|
|
2786
|
+
statusEl.innerHTML = statusHead + " · svg=" + svg.length + " B";
|
|
2787
|
+
statusEl.className = "good";
|
|
2788
|
+
} catch (e) {
|
|
2789
|
+
console.error(e);
|
|
2790
|
+
setStatus("Virhe: " + (e && e.message ? e.message : e), "bad");
|
|
2791
|
+
outStage.classList.add("empty");
|
|
2792
|
+
outStage.textContent = "—";
|
|
2793
|
+
dlBtn.disabled = true;
|
|
2794
|
+
editToggle.disabled = true;
|
|
2795
|
+
} finally {
|
|
2796
|
+
busy = false;
|
|
2797
|
+
runBtn.disabled = !sourceBitmap;
|
|
2798
|
+
if (pending) vectorize();
|
|
2799
|
+
}
|
|
2800
|
+
}, 16);
|
|
2801
|
+
}
|
|
2802
|
+
})();
|
|
2803
|
+
</script>
|
|
2804
|
+
</body>
|
|
2805
|
+
</html>
|