mirror of
https://github.com/visioncortex/vtracer.git
synced 2026-09-27 14:31:19 -07:00
Publish desktop updater 1.0.0-alpha.3
This commit is contained in:
@@ -0,0 +1,2 @@
|
||||
pkg
|
||||
wasm-pack.log
|
||||
@@ -0,0 +1,45 @@
|
||||
[package]
|
||||
name = "vtracer-webapp"
|
||||
version = "0.4.0"
|
||||
authors = ["Chris Tsang <chris.2y3@outlook.com>"]
|
||||
edition = "2021"
|
||||
description = "A web app to convert images into vector graphics."
|
||||
license = "MIT OR Apache-2.0"
|
||||
homepage = "http://www.visioncortex.org/vtracer"
|
||||
repository = "https://github.com/visioncortex/vtracer/"
|
||||
categories = ["graphics"]
|
||||
keywords = ["svg", "computer-graphics"]
|
||||
|
||||
[lib]
|
||||
crate-type = ["cdylib"]
|
||||
|
||||
[features]
|
||||
default = ["console_error_panic_hook"]
|
||||
|
||||
[dependencies]
|
||||
cfg-if = "1.0"
|
||||
console_log = { version = "1.0", features = ["color"] }
|
||||
# `serde-serialize` was removed upstream in 0.2.84; params arrive as a JSON
|
||||
# string and are parsed with serde_json, so the feature was never needed.
|
||||
wasm-bindgen = "0.2"
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
serde_json = "1.0"
|
||||
visioncortex = "0.9"
|
||||
|
||||
# The `console_error_panic_hook` crate provides better debugging of panics by
|
||||
# logging them with `console.error`. This is great for development, but requires
|
||||
# all the `std::fmt` and `std::panicking` infrastructure, so isn't great for
|
||||
# code size when deploying.
|
||||
console_error_panic_hook = { version = "0.1", optional = true }
|
||||
|
||||
[dependencies.web-sys]
|
||||
version = "0.3"
|
||||
features = [
|
||||
"CanvasRenderingContext2d",
|
||||
"console",
|
||||
"Document",
|
||||
"HtmlElement",
|
||||
"HtmlCanvasElement",
|
||||
"ImageData",
|
||||
"Window",
|
||||
]
|
||||
@@ -0,0 +1,201 @@
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright [yyyy] [name of copyright owner]
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
@@ -0,0 +1,25 @@
|
||||
Copyright (c) 2020 Tsang Hao Fung
|
||||
|
||||
Permission is hereby granted, free of charge, to any
|
||||
person obtaining a copy of this software and associated
|
||||
documentation files (the "Software"), to deal in the
|
||||
Software without restriction, including without
|
||||
limitation the rights to use, copy, modify, merge,
|
||||
publish, distribute, sublicense, and/or sell copies of
|
||||
the Software, and to permit persons to whom the Software
|
||||
is furnished to do so, subject to the following
|
||||
conditions:
|
||||
|
||||
The above copyright notice and this permission notice
|
||||
shall be included in all copies or substantial portions
|
||||
of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF
|
||||
ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED
|
||||
TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
|
||||
PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT
|
||||
SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
||||
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
||||
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR
|
||||
IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||
DEALINGS IN THE SOFTWARE.
|
||||
@@ -0,0 +1,42 @@
|
||||
<div align="center">
|
||||
<img src="https://github.com/visioncortex/vtracer/raw/master/docs/images/visioncortex-banner.png">
|
||||
</div>
|
||||
|
||||
# visioncortex VTracer
|
||||
|
||||
A web app to convert raster images into vector graphics.
|
||||
|
||||
## Setup
|
||||
|
||||
0. `sudo apt install git build-essential`
|
||||
1. https://www.rust-lang.org/tools/install
|
||||
2. https://rustwasm.github.io/wasm-pack/installer/
|
||||
3. https://github.com/nvm-sh/nvm
|
||||
```
|
||||
nvm install --lts
|
||||
```
|
||||
|
||||
## Getting Started
|
||||
|
||||
0. Setup
|
||||
```
|
||||
cd app
|
||||
npm install
|
||||
```
|
||||
|
||||
1. Build wasm
|
||||
```
|
||||
wasm-pack build
|
||||
```
|
||||
|
||||
2. Start development server
|
||||
```
|
||||
cd app
|
||||
npm run start
|
||||
```
|
||||
Open browser on http://localhost:8080/
|
||||
|
||||
3. Release
|
||||
```
|
||||
npm run build
|
||||
```
|
||||
@@ -0,0 +1,2 @@
|
||||
node_modules
|
||||
dist
|
||||
Symlink
+1
@@ -0,0 +1 @@
|
||||
../../docs/assets
|
||||
Vendored
+5
@@ -0,0 +1,5 @@
|
||||
// A dependency graph that contains any wasm must all be imported
|
||||
// asynchronously. This `bootstrap.js` file does the single async import, so
|
||||
// that no one else needs to worry about it again.
|
||||
import("./index.js")
|
||||
.catch(e => console.error("Error importing `index.js`:", e));
|
||||
@@ -0,0 +1,205 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>VTracer</title>
|
||||
<link rel="apple-touch-icon" sizes="192x192" href="./assets/apple-icon.png">
|
||||
<link rel="icon" type="image/png" sizes="192x192" href="./assets/apple-icon.png">
|
||||
<style>
|
||||
html, body {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
margin: 0;
|
||||
}
|
||||
.button {
|
||||
cursor: pointer;
|
||||
font-family: sans-serif;
|
||||
border-radius: 2px;
|
||||
border: 1px solid #aaa;
|
||||
padding: 4px 8px;
|
||||
}
|
||||
#droptext {
|
||||
border: 2px dashed #000;
|
||||
}
|
||||
#droptext.hovering {
|
||||
border: 2px dashed #fff;
|
||||
}
|
||||
#canvas-container {
|
||||
position: absolute;
|
||||
left: 2.5%;
|
||||
top: 5%;
|
||||
width: 95%;
|
||||
height: 90%;
|
||||
}
|
||||
#svg, #frame {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
margin-bottom: 50px;
|
||||
}
|
||||
#svg > path:hover {
|
||||
stroke: #ff0;
|
||||
}
|
||||
#droptext {
|
||||
height: 100%;
|
||||
text-align: center;
|
||||
}
|
||||
#options {
|
||||
position: absolute;
|
||||
left: 2.5%;
|
||||
top: 5%;
|
||||
padding: 50px;
|
||||
background: rgba(255,255,255,0.5);
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<input type="file" id="imageInput" accept="image/*" style="display: none;">
|
||||
<div>
|
||||
<div id="progressregion" style="display: none;">
|
||||
<progress id="progressbar" value="0" max="100" style="width: 98%;"></progress>
|
||||
</div>
|
||||
<div>
|
||||
<div id="drop">
|
||||
<div id="canvas-container">
|
||||
<div id="droptext">
|
||||
<p>Drag an image here or <a href="#" id="imageSelect">Select file</a></p>
|
||||
</div>
|
||||
<canvas id="frame"></canvas>
|
||||
<svg id="svg" version="1.1" xmlns="http://www.w3.org/2000/svg"></svg>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="options">
|
||||
<div>
|
||||
<a class="button" id="export">Download as SVG</a>
|
||||
</div>
|
||||
|
||||
<p></p>
|
||||
|
||||
<div>
|
||||
<div title="Algorithm for segmentation and grouping of pixel clusters">
|
||||
Clustering
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div>
|
||||
<button id="clustering-binary" title="Black & White (Binary Image)">B/W</button>
|
||||
<button id="clustering-color" title="True Color Image">Color</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div>
|
||||
<button id="clustering-cutout" title="Shapes disjoint with others">Cutout</button>
|
||||
<button id="clustering-stacked" title="Stack shapes on top of another">Stacked</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div title="Discard patches small than X px in size">
|
||||
Filter Speckle <span>(Cleaner)</span>
|
||||
</div>
|
||||
</div>
|
||||
<div id="filterspecklevalue">
|
||||
4
|
||||
</div>
|
||||
<div>
|
||||
<input id="filterspeckle" type="range" min="1" max="16" step="1" value="4">
|
||||
</div>
|
||||
|
||||
<div class="clustering-color-options">
|
||||
<div title="Number of significant bits to use in a RGB channel">
|
||||
Color Precision <span>(More accurate)</span>
|
||||
</div>
|
||||
</div>
|
||||
<div id="colorprecisionvalue" class="clustering-color-options">
|
||||
6
|
||||
</div>
|
||||
<div class="clustering-color-options">
|
||||
<input id="colorprecision" type="range" min="1" max="8" step="1" value="6">
|
||||
</div>
|
||||
|
||||
<div class="clustering-color-options">
|
||||
<div title="Color difference between gradient layers">
|
||||
Gradient Step <span>(Less layers)</span>
|
||||
</div>
|
||||
</div>
|
||||
<div id="layerdifferencevalue" class="clustering-color-options">
|
||||
16
|
||||
</div>
|
||||
<div class="clustering-color-options">
|
||||
<input id="layerdifference" type="range" min="0" max="255" step="1" value="16">
|
||||
</div>
|
||||
|
||||
<p></p>
|
||||
|
||||
<div>
|
||||
<div title="Algorithm for converting clusters to shapes">
|
||||
Curve Fitting
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div>
|
||||
<button id="none" title="Exact cluster boundary">Pixel</button>
|
||||
<button id="polygon" title="Simplify to Polygon">Polygon</button>
|
||||
<button id="spline" class="selected" title="Smooth and Curve-fit">Spline</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="spline-options">
|
||||
<div title="Minimum Momentary Angle (in degrees) to be considered a corner (to be kept after smoothing)">
|
||||
Corner Threshold <span>(Smoother)</span>
|
||||
</div>
|
||||
</div>
|
||||
<div id="cornervalue" class="spline-options">
|
||||
60
|
||||
</div>
|
||||
<div class="spline-options">
|
||||
<input id="corner" type="range" min="0" max="180" step="1" value="60">
|
||||
</div>
|
||||
|
||||
<div class="spline-options">
|
||||
<div title="Perform Iterative Subdivide Smooth until all segments are shorter than this length">
|
||||
Segment Length <span>(More coarse)</span>
|
||||
</div>
|
||||
</div>
|
||||
<div id="lengthvalue" class="spline-options">
|
||||
4
|
||||
</div>
|
||||
<div class="spline-options">
|
||||
<input id="length" type="range" min="3.5" max="10" step="0.5" value="4">
|
||||
</div>
|
||||
|
||||
<div class="spline-options">
|
||||
<div title="Minimum Angle Displacement (in degrees) to be considered a cutting point between curves">
|
||||
Splice Threshold <span>(More accurate)</span>
|
||||
</div>
|
||||
</div>
|
||||
<div id="splicevalue" class="spline-options">
|
||||
45
|
||||
</div>
|
||||
<div class="spline-options">
|
||||
<input id="splice" type="range" min="0" max="180" step="1" value="45">
|
||||
</div>
|
||||
|
||||
<div style="display: none">
|
||||
<div class="spline-options">
|
||||
<div title="Number of decimal places used in svg path string">
|
||||
Path Precision <span>(More digits)</span>
|
||||
</div>
|
||||
</div>
|
||||
<div id="pathprecisionvalue" class="spline-options">
|
||||
8
|
||||
</div>
|
||||
<div class="spline-options">
|
||||
<input id="pathprecision" class="uk-range" type="range" min="0" max="16" step="1" value="8">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<script src="./bootstrap.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,490 @@
|
||||
import { BinaryImageConverter, ColorImageConverter } from 'vtracer';
|
||||
|
||||
let runner;
|
||||
const canvas = document.getElementById('frame');
|
||||
const ctx = canvas.getContext('2d');
|
||||
const svg = document.getElementById('svg');
|
||||
const img = new Image();
|
||||
const progress = document.getElementById('progressbar');
|
||||
const progressregion = document.getElementById('progressregion');
|
||||
let mode = 'spline', clustering_mode = 'color', clustering_hierarchical = 'stacked';
|
||||
|
||||
// Hide canas and svg on load
|
||||
canvas.style.display = 'none';
|
||||
svg.style.display = 'none';
|
||||
|
||||
// Paste from clipboard
|
||||
document.addEventListener('paste', function (e) {
|
||||
if (e.clipboardData) {
|
||||
var items = e.clipboardData.items;
|
||||
if (!items) return;
|
||||
|
||||
//access data directly
|
||||
for (var i = 0; i < items.length; i++) {
|
||||
if (items[i].type.indexOf("image") !== -1) {
|
||||
//image
|
||||
var blob = items[i].getAsFile();
|
||||
var URLObj = window.URL || window.webkitURL;
|
||||
var source = URLObj.createObjectURL(blob);
|
||||
setSourceAndRestart(source);
|
||||
}
|
||||
}
|
||||
e.preventDefault();
|
||||
}
|
||||
});
|
||||
|
||||
// Download as SVG
|
||||
document.getElementById('export').addEventListener('click', function (e) {
|
||||
const blob = new Blob([
|
||||
`<?xml version="1.0" encoding="UTF-8"?>\n`,
|
||||
`<!-- Generator: visioncortex VTracer -->\n`,
|
||||
new XMLSerializer().serializeToString(svg)
|
||||
], {type: 'octet/stream'}),
|
||||
url = window.URL.createObjectURL(blob);
|
||||
|
||||
this.href = url;
|
||||
this.target = '_blank';
|
||||
|
||||
this.download = 'export-' + new Date().toISOString().slice(0, 19).replace(/:/g, '').replace('T', ' ') + '.svg';
|
||||
});
|
||||
|
||||
// Store template config
|
||||
var presetConfigs = [
|
||||
{
|
||||
src: 'assets/samples/K1_drawing.jpg',
|
||||
clustering_mode: 'binary',
|
||||
clustering_hierarchical: 'stacked',
|
||||
filter_speckle: 4,
|
||||
color_precision: 6,
|
||||
path_precision: 8,
|
||||
layer_difference: 16,
|
||||
mode: 'spline',
|
||||
corner_threshold: 60,
|
||||
length_threshold: 4,
|
||||
splice_threshold: 45,
|
||||
source: 'https://commons.wikimedia.org/wiki/File:K1_drawing.jpg',
|
||||
credit: '<a href="https://commons.wikimedia.org/">Wikimedia</a>',
|
||||
},
|
||||
{
|
||||
src: 'assets/samples/Cityscape Sunset_DFM3-01.jpg',
|
||||
clustering_mode: 'color',
|
||||
clustering_hierarchical: 'stacked',
|
||||
filter_speckle: 4,
|
||||
color_precision: 8,
|
||||
path_precision: 8,
|
||||
layer_difference: 25,
|
||||
mode: 'spline',
|
||||
corner_threshold: 60,
|
||||
length_threshold: 4,
|
||||
splice_threshold: 45,
|
||||
source: 'https://www.vecteezy.com/vector-art/227400-beautiful-cityscape-at-sunset',
|
||||
credit: '<a href="https://www.vecteezy.com/free-vector/building">Building Vectors by Vecteezy</a>',
|
||||
},
|
||||
{
|
||||
src: 'assets/samples/Gum Tree Vector.jpg',
|
||||
clustering_mode: 'color',
|
||||
clustering_hierarchical: 'stacked',
|
||||
filter_speckle: 4,
|
||||
color_precision: 8,
|
||||
path_precision: 8,
|
||||
layer_difference: 28,
|
||||
mode: 'spline',
|
||||
corner_threshold: 60,
|
||||
length_threshold: 4,
|
||||
splice_threshold: 45,
|
||||
source: 'https://www.vecteezy.com/vector-art/172177-gum-tree-vector',
|
||||
credit: '<a href="https://www.vecteezy.com/free-vector/nature">Nature Vectors by Vecteezy</a>',
|
||||
},
|
||||
{
|
||||
src: 'assets/samples/vectorstock_31191940.png',
|
||||
clustering_mode: 'color',
|
||||
clustering_hierarchical: 'stacked',
|
||||
filter_speckle: 8,
|
||||
color_precision: 7,
|
||||
path_precision: 8,
|
||||
layer_difference: 64,
|
||||
mode: 'spline',
|
||||
corner_threshold: 60,
|
||||
length_threshold: 4,
|
||||
splice_threshold: 45,
|
||||
source: 'https://www.vectorstock.com/royalty-free-vector/dessert-poster-design-with-chocolate-cake-mousses-vector-31191940',
|
||||
credit: '<a href="https://www.vectorstock.com/royalty-free-vector/dessert-poster-design-with-chocolate-cake-mousses-vector-31191940">Vector image by VectorStock / vectorstock</a>',
|
||||
},
|
||||
{
|
||||
src: 'assets/samples/angel-luciano-LATYeZyw88c-unsplash-s.jpg',
|
||||
clustering_mode: 'color',
|
||||
clustering_hierarchical: 'stacked',
|
||||
filter_speckle: 10,
|
||||
color_precision: 8,
|
||||
path_precision: 8,
|
||||
layer_difference: 48,
|
||||
mode: 'spline',
|
||||
corner_threshold: 180,
|
||||
length_threshold: 4,
|
||||
splice_threshold: 45,
|
||||
source: 'https://unsplash.com/photos/LATYeZyw88c',
|
||||
credit: '<span>Photo by <a href="https://unsplash.com/@roaming_angel?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText">Angel Luciano</a> on <a href="https://unsplash.com/s/photos/dog?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText">Unsplash</a></span>',
|
||||
},
|
||||
{
|
||||
src: 'assets/samples/tank-unit-preview.png',
|
||||
clustering_mode: 'color',
|
||||
clustering_hierarchical: 'stacked',
|
||||
filter_speckle: 0,
|
||||
color_precision: 8,
|
||||
path_precision: 8,
|
||||
layer_difference: 0,
|
||||
mode: 'none',
|
||||
corner_threshold: 180,
|
||||
length_threshold: 4,
|
||||
splice_threshold: 45,
|
||||
source: 'https://opengameart.org/content/sideview-sci-fi-patreon-collection',
|
||||
credit: '<span>Artwork by <a href="https://opengameart.org/content/sideview-sci-fi-patreon-collection">Luis Zuno</a> on <a href="https://opengameart.org/">opengameart.org</a></span>',
|
||||
},
|
||||
];
|
||||
|
||||
// Insert gallery items dynamically
|
||||
if (document.getElementById('galleryslider')) {
|
||||
for (let i = 0; i < presetConfigs.length; i++) {
|
||||
document.getElementById('galleryslider').innerHTML +=
|
||||
`<li>
|
||||
<div class="galleryitem uk-panel uk-flex uk-flex-center">
|
||||
<a href="#">
|
||||
<img src="${presetConfigs[i].src}" title="${presetConfigs[i].source}">
|
||||
</a>
|
||||
</div>
|
||||
</li>`;
|
||||
document.getElementById('credits-modal-content').innerHTML +=
|
||||
`<p>${presetConfigs[i].credit}</p>`;
|
||||
}
|
||||
}
|
||||
|
||||
// Function to load a given config WITHOUT restarting
|
||||
function loadConfig(config) {
|
||||
mode = config.mode;
|
||||
clustering_mode = config.clustering_mode;
|
||||
clustering_hierarchical = config.clustering_hierarchical;
|
||||
|
||||
globalcorner = config.corner_threshold;
|
||||
document.getElementById('cornervalue').innerHTML = globalcorner;
|
||||
document.getElementById('corner').value = globalcorner;
|
||||
|
||||
globallength = config.length_threshold;
|
||||
document.getElementById('lengthvalue').innerHTML = globallength;
|
||||
document.getElementById('length').value = globallength;
|
||||
|
||||
globalsplice = config.splice_threshold;
|
||||
document.getElementById('splicevalue').innerHTML = globalsplice;
|
||||
document.getElementById('splice').value = globalsplice;
|
||||
|
||||
globalfilterspeckle = config.filter_speckle;
|
||||
document.getElementById('filterspecklevalue').innerHTML = globalfilterspeckle;
|
||||
document.getElementById('filterspeckle').value = globalfilterspeckle;
|
||||
|
||||
globalcolorprecision = config.color_precision;
|
||||
document.getElementById('colorprecisionvalue').innerHTML = globalcolorprecision;
|
||||
document.getElementById('colorprecision').value = globalcolorprecision;
|
||||
|
||||
globallayerdifference = config.layer_difference;
|
||||
document.getElementById('layerdifferencevalue').innerHTML = globallayerdifference;
|
||||
document.getElementById('layerdifference').value = globallayerdifference;
|
||||
|
||||
globalpathprecision = config.path_precision;
|
||||
document.getElementById('pathprecisionvalue').innerHTML = globalpathprecision;
|
||||
document.getElementById('pathprecision').value = globalpathprecision;
|
||||
}
|
||||
|
||||
// Choose template from gallery
|
||||
let chooseGalleryButtons = document.querySelectorAll('.galleryitem a');
|
||||
chooseGalleryButtons.forEach(item => {
|
||||
item.addEventListener('click', function (e) {
|
||||
// Load preset template config
|
||||
let i = Array.prototype.indexOf.call(chooseGalleryButtons, item);
|
||||
if (presetConfigs.length > i) {
|
||||
loadConfig(presetConfigs[i]);
|
||||
}
|
||||
|
||||
// Set source as specified
|
||||
setSourceAndRestart(this.firstElementChild.src);
|
||||
});
|
||||
});
|
||||
|
||||
// Upload button
|
||||
var imageSelect = document.getElementById('imageSelect'),
|
||||
imageInput = document.getElementById('imageInput');
|
||||
imageSelect.addEventListener('click', function (e) {
|
||||
imageInput.click();
|
||||
e.preventDefault();
|
||||
});
|
||||
|
||||
imageInput.addEventListener('change', function (e) {
|
||||
setSourceAndRestart(this.files[0]);
|
||||
});
|
||||
|
||||
// Drag-n-Drop
|
||||
var drop = document.getElementById('drop');
|
||||
var droptext = document.getElementById('droptext');
|
||||
drop.addEventListener('dragenter', function (e) {
|
||||
if (e.preventDefault) e.preventDefault();
|
||||
e.dataTransfer.dropEffect = 'copy';
|
||||
droptext.classList.add('hovering');
|
||||
return false;
|
||||
});
|
||||
|
||||
drop.addEventListener('dragleave', function (e) {
|
||||
if (e.preventDefault) e.preventDefault();
|
||||
e.dataTransfer.dropEffect = 'copy';
|
||||
droptext.classList.remove('hovering');
|
||||
return false;
|
||||
});
|
||||
|
||||
drop.addEventListener('dragover', function (e) {
|
||||
if (e.preventDefault) e.preventDefault();
|
||||
e.dataTransfer.dropEffect = 'copy';
|
||||
droptext.classList.add('hovering');
|
||||
return false;
|
||||
});
|
||||
|
||||
drop.addEventListener('drop', function (e) {
|
||||
if (e.preventDefault) e.preventDefault();
|
||||
droptext.classList.remove('hovering');
|
||||
setSourceAndRestart(e.dataTransfer.files[0]);
|
||||
return false;
|
||||
});
|
||||
|
||||
// Get Input from UI controls
|
||||
var globalcorner = parseInt(document.getElementById('corner').value),
|
||||
globallength = parseFloat(document.getElementById('length').value),
|
||||
globalsplice = parseInt(document.getElementById('splice').value),
|
||||
globalfilterspeckle = parseInt(document.getElementById('filterspeckle').value),
|
||||
globalcolorprecision = parseInt(document.getElementById('colorprecision').value),
|
||||
globallayerdifference = parseInt(document.getElementById('layerdifference').value),
|
||||
globalpathprecision = parseInt(document.getElementById('pathprecision').value);
|
||||
|
||||
// Load past inputs from localStorage
|
||||
/*
|
||||
if (localStorage.VSsettings) {
|
||||
var settings = JSON.parse(localStorage.VSsettings);
|
||||
document.getElementById('cornervalue').innerHTML = document.getElementById('corner').value = globalcorner = settings.globalcorner;
|
||||
document.getElementById('lengthvalue').innerHTML = document.getElementById('length').value = globallength = settings.globallength;
|
||||
document.getElementById('splicevalue').innerHTML = document.getElementById('splice').value = globalsplice = settings.globalsplice;
|
||||
}
|
||||
*/
|
||||
|
||||
document.getElementById('none').addEventListener('click', function (e) {
|
||||
mode = 'none';
|
||||
restart();
|
||||
}, false);
|
||||
|
||||
document.getElementById('polygon').addEventListener('click', function (e) {
|
||||
mode = 'polygon';
|
||||
restart();
|
||||
}, false);
|
||||
|
||||
document.getElementById('spline').addEventListener('click', function (e) {
|
||||
mode = 'spline';
|
||||
restart();
|
||||
}, false);
|
||||
|
||||
document.getElementById('clustering-binary').addEventListener('click', function (e) {
|
||||
clustering_mode = 'binary';
|
||||
restart();
|
||||
}, false);
|
||||
|
||||
document.getElementById('clustering-color').addEventListener('click', function (e) {
|
||||
clustering_mode = 'color';
|
||||
restart();
|
||||
}, false);
|
||||
|
||||
document.getElementById('clustering-cutout').addEventListener('click', function (e) {
|
||||
clustering_hierarchical = 'cutout';
|
||||
restart();
|
||||
}, false);
|
||||
|
||||
document.getElementById('clustering-stacked').addEventListener('click', function (e) {
|
||||
clustering_hierarchical = 'stacked';
|
||||
restart();
|
||||
}, false);
|
||||
|
||||
document.getElementById('filterspeckle').addEventListener('change', function (e) {
|
||||
globalfilterspeckle = parseInt(this.value);
|
||||
document.getElementById('filterspecklevalue').innerHTML = this.value;
|
||||
restart();
|
||||
});
|
||||
|
||||
document.getElementById('colorprecision').addEventListener('change', function (e) {
|
||||
globalcolorprecision = parseInt(this.value);
|
||||
document.getElementById('colorprecisionvalue').innerHTML = this.value;
|
||||
restart();
|
||||
});
|
||||
|
||||
document.getElementById('layerdifference').addEventListener('change', function (e) {
|
||||
globallayerdifference = parseInt(this.value);
|
||||
document.getElementById('layerdifferencevalue').innerHTML = this.value;
|
||||
restart();
|
||||
});
|
||||
|
||||
document.getElementById('corner').addEventListener('change', function (e) {
|
||||
globalcorner = parseInt(this.value);
|
||||
document.getElementById('cornervalue').innerHTML = this.value;
|
||||
restart();
|
||||
});
|
||||
|
||||
document.getElementById('length').addEventListener('change', function (e) {
|
||||
globallength = parseFloat(this.value);
|
||||
document.getElementById('lengthvalue').innerHTML = this.value;
|
||||
restart();
|
||||
});
|
||||
|
||||
document.getElementById('splice').addEventListener('change', function (e) {
|
||||
globalsplice = parseInt(this.value);
|
||||
document.getElementById('splicevalue').innerHTML = this.value;
|
||||
restart();
|
||||
});
|
||||
|
||||
document.getElementById('pathprecision').addEventListener('change', function (e) {
|
||||
globalpathprecision = parseInt(this.value);
|
||||
document.getElementById('pathprecisionvalue').innerHTML = this.value;
|
||||
restart();
|
||||
});
|
||||
|
||||
// Save inputs before unloading
|
||||
/*
|
||||
window.addEventListener('beforeunload', function () {
|
||||
localStorage.VSsettings = JSON.stringify({
|
||||
globalcorner: globalcorner,
|
||||
globallength: globallength,
|
||||
globalsplice: globalsplice,
|
||||
});
|
||||
});
|
||||
*/
|
||||
|
||||
function setSourceAndRestart(source) {
|
||||
img.src = source instanceof File ? URL.createObjectURL(source) : source;
|
||||
img.onload = function () {
|
||||
const width = img.naturalWidth, height = img.naturalHeight;
|
||||
svg.setAttribute('viewBox', `0 0 ${width} ${height}`);
|
||||
canvas.width = img.naturalWidth;
|
||||
canvas.height = img.naturalHeight;
|
||||
if (height > width) {
|
||||
document.getElementById('canvas-container').style.width = '50%';
|
||||
document.getElementById('canvas-container').style.marginBottom = (height / width * 50) + '%';
|
||||
} else {
|
||||
document.getElementById('canvas-container').style.width = '';
|
||||
document.getElementById('canvas-container').style.marginBottom = (height / width * 100) + '%';
|
||||
}
|
||||
ctx.drawImage(img, 0, 0, canvas.width, canvas.height);
|
||||
ctx.getImageData(0, 0, canvas.width, canvas.height);
|
||||
restart();
|
||||
}
|
||||
// Show display
|
||||
canvas.style.display = 'block';
|
||||
svg.style.display = 'block';
|
||||
// Hide upload text
|
||||
droptext.style.display = 'none';
|
||||
}
|
||||
|
||||
function restart() {
|
||||
document.getElementById('clustering-binary').classList.remove('selected');
|
||||
document.getElementById('clustering-color').classList.remove('selected');
|
||||
document.getElementById('clustering-' + clustering_mode).classList.add('selected');
|
||||
Array.from(document.getElementsByClassName('clustering-color-options')).forEach((el) => {
|
||||
el.style.display = clustering_mode == 'color' ? '' : 'none';
|
||||
});
|
||||
|
||||
document.getElementById('clustering-cutout').classList.remove('selected');
|
||||
document.getElementById('clustering-stacked').classList.remove('selected');
|
||||
document.getElementById('clustering-' + clustering_hierarchical).classList.add('selected');
|
||||
|
||||
document.getElementById('none').classList.remove('selected');
|
||||
document.getElementById('polygon').classList.remove('selected');
|
||||
document.getElementById('spline').classList.remove('selected');
|
||||
document.getElementById(mode).classList.add('selected');
|
||||
Array.from(document.getElementsByClassName('spline-options')).forEach((el) => {
|
||||
el.style.display = mode == 'spline' ? '' : 'none';
|
||||
});
|
||||
|
||||
if (!img.src) {
|
||||
return;
|
||||
}
|
||||
while (svg.firstChild) {
|
||||
svg.removeChild(svg.firstChild);
|
||||
}
|
||||
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
||||
ctx.drawImage(img, 0, 0);
|
||||
let converter_params = JSON.stringify({
|
||||
'canvas_id': canvas.id,
|
||||
'svg_id': svg.id,
|
||||
'mode': mode,
|
||||
'clustering_mode': clustering_mode,
|
||||
'hierarchical': clustering_hierarchical,
|
||||
'corner_threshold': deg2rad(globalcorner),
|
||||
'length_threshold': globallength,
|
||||
'max_iterations': 10,
|
||||
'splice_threshold': deg2rad(globalsplice),
|
||||
'filter_speckle': globalfilterspeckle*globalfilterspeckle,
|
||||
'color_precision': 8-globalcolorprecision,
|
||||
'layer_difference': globallayerdifference,
|
||||
'path_precision': globalpathprecision,
|
||||
});
|
||||
if (runner) {
|
||||
runner.stop();
|
||||
}
|
||||
runner = new ConverterRunner(converter_params);
|
||||
progress.value = 0;
|
||||
progressregion.style.display = 'block';
|
||||
runner.run();
|
||||
}
|
||||
|
||||
function deg2rad(deg) {
|
||||
return deg/180*3.141592654;
|
||||
}
|
||||
|
||||
class ConverterRunner {
|
||||
constructor (converter_params) {
|
||||
this.converter =
|
||||
clustering_mode == 'color' ?
|
||||
ColorImageConverter.new_with_string(converter_params):
|
||||
BinaryImageConverter.new_with_string(converter_params);
|
||||
this.converter.init();
|
||||
this.stopped = false;
|
||||
if (clustering_mode == 'binary') {
|
||||
svg.style.background = '#fff';
|
||||
canvas.style.display = 'none';
|
||||
} else {
|
||||
svg.style.background = '';
|
||||
canvas.style.display = '';
|
||||
}
|
||||
canvas.style.opacity = '';
|
||||
}
|
||||
|
||||
run () {
|
||||
const This = this;
|
||||
setTimeout(function tick () {
|
||||
if (!This.stopped) {
|
||||
let done = false;
|
||||
const startTick = performance.now();
|
||||
while (!(done = This.converter.tick()) &&
|
||||
performance.now() - startTick < 25) {
|
||||
}
|
||||
progress.value = This.converter.progress();
|
||||
if (progress.value >= 50) {
|
||||
canvas.style.display = 'none';
|
||||
} else {
|
||||
canvas.style.opacity = (50 - progress.value) / 25;
|
||||
}
|
||||
if (progress.value >= progress.max) {
|
||||
progressregion.style.display = 'none';
|
||||
progress.value = 0;
|
||||
}
|
||||
if (!done) {
|
||||
setTimeout(tick, 1);
|
||||
}
|
||||
}
|
||||
}, 1);
|
||||
}
|
||||
|
||||
stop () {
|
||||
this.stopped = true;
|
||||
this.converter.free();
|
||||
}
|
||||
}
|
||||
Generated
+4376
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,25 @@
|
||||
{
|
||||
"name": "vtracer-app",
|
||||
"version": "0.1.0",
|
||||
"description": "VTracer Webapp",
|
||||
"author": "Chris Tsang <chris.2y3@outlook.com>",
|
||||
"license": "proprietary",
|
||||
"private": true,
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"start": "webpack serve",
|
||||
"build": "webpack"
|
||||
},
|
||||
"keywords": [
|
||||
"rust",
|
||||
"wasm"
|
||||
],
|
||||
"dependencies": {
|
||||
"vtracer": "file:../pkg",
|
||||
"webpack": "^5.101.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"webpack-cli": "^6.0.1",
|
||||
"webpack-dev-server": "^5.2.2"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
const path = require('path');
|
||||
|
||||
module.exports = {
|
||||
entry: "./bootstrap.js",
|
||||
output: {
|
||||
path: path.resolve(__dirname, "dist"),
|
||||
filename: "bootstrap.js",
|
||||
clean: true,
|
||||
},
|
||||
mode: "development",
|
||||
// wasm-pack's `bundler` target emits ESM imports of the .wasm module; webpack
|
||||
// 5 handles those natively once this experiment is on.
|
||||
experiments: {
|
||||
asyncWebAssembly: true,
|
||||
},
|
||||
devServer: {
|
||||
//host: "0.0.0.0",
|
||||
port: 8080,
|
||||
// dev-server 5 defaults its static root to ./public, which would leave
|
||||
// index.html unserved. `assets` symlinks to ../../docs/assets for samples.
|
||||
static: { directory: __dirname },
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,60 @@
|
||||
use wasm_bindgen::{JsCast};
|
||||
use web_sys::{console, CanvasRenderingContext2d, HtmlCanvasElement};
|
||||
use visioncortex::{ColorImage};
|
||||
|
||||
use super::common::document;
|
||||
|
||||
pub struct Canvas {
|
||||
html_canvas: HtmlCanvasElement,
|
||||
cctx: CanvasRenderingContext2d,
|
||||
}
|
||||
|
||||
impl Canvas {
|
||||
pub fn new_from_id(canvas_id: &str) -> Canvas {
|
||||
let html_canvas = document().get_element_by_id(canvas_id).unwrap();
|
||||
let html_canvas: HtmlCanvasElement = html_canvas
|
||||
.dyn_into::<HtmlCanvasElement>()
|
||||
.map_err(|_| ())
|
||||
.unwrap();
|
||||
|
||||
let cctx = html_canvas
|
||||
.get_context("2d")
|
||||
.unwrap()
|
||||
.unwrap()
|
||||
.dyn_into::<CanvasRenderingContext2d>()
|
||||
.unwrap();
|
||||
|
||||
Canvas {
|
||||
html_canvas,
|
||||
cctx,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn width(&self) -> usize {
|
||||
self.html_canvas.width() as usize
|
||||
}
|
||||
|
||||
pub fn height(&self) -> usize {
|
||||
self.html_canvas.height() as usize
|
||||
}
|
||||
|
||||
pub fn get_image_data(&self, x: u32, y: u32, width: u32, height: u32) -> Vec<u8> {
|
||||
let image = self
|
||||
.cctx
|
||||
.get_image_data(x as f64, y as f64, width as f64, height as f64)
|
||||
.unwrap();
|
||||
image.data().to_vec()
|
||||
}
|
||||
|
||||
pub fn get_image_data_as_color_image(&self, x: u32, y: u32, width: u32, height: u32) -> ColorImage {
|
||||
ColorImage {
|
||||
pixels: self.get_image_data(x, y, width, height),
|
||||
width: width as usize,
|
||||
height: height as usize,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn log(&self, string: &str) {
|
||||
console::log_1(&wasm_bindgen::JsValue::from_str(string));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
pub fn window() -> web_sys::Window {
|
||||
web_sys::window().unwrap()
|
||||
}
|
||||
|
||||
pub fn document() -> web_sys::Document {
|
||||
window().document().unwrap()
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
use wasm_bindgen::prelude::*;
|
||||
use visioncortex::{clusters::Clusters, Color, ColorName, PathSimplifyMode};
|
||||
|
||||
use crate::{canvas::*};
|
||||
use crate::svg::*;
|
||||
|
||||
use serde::Deserialize;
|
||||
use super::util;
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct BinaryImageConverterParams {
|
||||
pub canvas_id: String,
|
||||
pub svg_id: String,
|
||||
pub mode: String,
|
||||
pub corner_threshold: f64,
|
||||
pub length_threshold: f64,
|
||||
pub max_iterations: usize,
|
||||
pub splice_threshold: f64,
|
||||
pub filter_speckle: usize,
|
||||
pub path_precision: u32,
|
||||
}
|
||||
|
||||
#[wasm_bindgen]
|
||||
pub struct BinaryImageConverter {
|
||||
canvas: Canvas,
|
||||
svg: Svg,
|
||||
clusters: Clusters,
|
||||
counter: usize,
|
||||
mode: PathSimplifyMode,
|
||||
params: BinaryImageConverterParams,
|
||||
}
|
||||
|
||||
impl BinaryImageConverter {
|
||||
pub fn new(params: BinaryImageConverterParams) -> Self {
|
||||
let canvas = Canvas::new_from_id(¶ms.canvas_id);
|
||||
let svg = Svg::new_from_id(¶ms.svg_id);
|
||||
Self {
|
||||
canvas,
|
||||
svg,
|
||||
clusters: Clusters::default(),
|
||||
counter: 0,
|
||||
mode: util::path_simplify_mode(¶ms.mode),
|
||||
params,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[wasm_bindgen]
|
||||
impl BinaryImageConverter {
|
||||
pub fn new_with_string(params: String) -> Self {
|
||||
let params: BinaryImageConverterParams = serde_json::from_str(params.as_str()).unwrap();
|
||||
Self::new(params)
|
||||
}
|
||||
|
||||
pub fn init(&mut self) {
|
||||
let width = self.canvas.width() as u32;
|
||||
let height = self.canvas.height() as u32;
|
||||
let image = self.canvas.get_image_data_as_color_image(0, 0, width, height);
|
||||
let binary_image = image.to_binary_image(|x| x.r < 128);
|
||||
self.clusters = binary_image.to_clusters(false);
|
||||
self.canvas.log(&format!(
|
||||
"clusters.len() = {}, self.clusters.rect.left = {}",
|
||||
self.clusters.len(),
|
||||
self.clusters.rect.left
|
||||
));
|
||||
}
|
||||
|
||||
pub fn tick(&mut self) -> bool {
|
||||
if self.counter < self.clusters.len() {
|
||||
self.canvas.log(&format!("tick {}", self.counter));
|
||||
let cluster = self.clusters.get_cluster(self.counter);
|
||||
if cluster.size() >= self.params.filter_speckle {
|
||||
let paths = cluster.to_compound_path(
|
||||
self.mode,
|
||||
self.params.corner_threshold,
|
||||
self.params.length_threshold,
|
||||
self.params.max_iterations,
|
||||
self.params.splice_threshold
|
||||
);
|
||||
let color = Color::color(&ColorName::Black);
|
||||
self.svg.prepend_path(
|
||||
&paths,
|
||||
&color,
|
||||
Some(self.params.path_precision),
|
||||
);
|
||||
}
|
||||
self.counter += 1;
|
||||
false
|
||||
} else {
|
||||
self.canvas.log("done");
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
pub fn progress(&self) -> u32 {
|
||||
100 * self.counter as u32 / self.clusters.len() as u32
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,284 @@
|
||||
use wasm_bindgen::prelude::*;
|
||||
use visioncortex::{Color, ColorImage, PathSimplifyMode};
|
||||
use visioncortex::color_clusters::{
|
||||
Cluster, Clusters, ClustersView, IncrementalBuilder, KeyingAction, NeighbourInfo, Runner,
|
||||
RunnerConfig, HIERARCHICAL_MAX,
|
||||
};
|
||||
|
||||
use crate::canvas::*;
|
||||
use crate::svg::*;
|
||||
|
||||
use serde::Deserialize;
|
||||
use super::util;
|
||||
|
||||
const KEYING_THRESHOLD: f32 = 0.2;
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct ColorImageConverterParams {
|
||||
pub canvas_id: String,
|
||||
pub svg_id: String,
|
||||
pub mode: String,
|
||||
pub hierarchical: String,
|
||||
pub corner_threshold: f64,
|
||||
pub length_threshold: f64,
|
||||
pub max_iterations: usize,
|
||||
pub splice_threshold: f64,
|
||||
pub filter_speckle: usize,
|
||||
pub color_precision: i32,
|
||||
pub layer_difference: i32,
|
||||
pub path_precision: u32,
|
||||
}
|
||||
|
||||
#[wasm_bindgen]
|
||||
pub struct ColorImageConverter {
|
||||
canvas: Canvas,
|
||||
svg: Svg,
|
||||
stage: Stage,
|
||||
counter: usize,
|
||||
mode: PathSimplifyMode,
|
||||
params: ColorImageConverterParams,
|
||||
}
|
||||
|
||||
pub enum Stage {
|
||||
New,
|
||||
Clustering(Box<dyn ClusterBuilder>),
|
||||
Reclustering(Box<dyn ClusterBuilder>),
|
||||
Vectorize(Clusters),
|
||||
}
|
||||
|
||||
/// visioncortex 0.9 parameterises `IncrementalBuilder` over its four closures,
|
||||
/// and `Runner::start()` returns them as anonymous `impl Fn` types, so the
|
||||
/// builder can no longer be named in a field. The stage machine only ever steps
|
||||
/// it, so erase the closures behind a trait object.
|
||||
pub trait ClusterBuilder {
|
||||
fn tick(&mut self) -> bool;
|
||||
fn progress(&self) -> u32;
|
||||
fn result(&mut self) -> Clusters;
|
||||
}
|
||||
|
||||
impl<C, D, P, H> ClusterBuilder for IncrementalBuilder<C, D, P, H>
|
||||
where
|
||||
C: Fn(Color, Color) -> bool,
|
||||
D: Fn(Color, Color) -> i32,
|
||||
P: Fn(&ClustersView, &Cluster, &[NeighbourInfo]) -> bool,
|
||||
H: Fn(&ClustersView, &Cluster, &[NeighbourInfo]) -> bool,
|
||||
{
|
||||
fn tick(&mut self) -> bool {
|
||||
IncrementalBuilder::tick(self)
|
||||
}
|
||||
|
||||
fn progress(&self) -> u32 {
|
||||
IncrementalBuilder::progress(self)
|
||||
}
|
||||
|
||||
fn result(&mut self) -> Clusters {
|
||||
IncrementalBuilder::result(self)
|
||||
}
|
||||
}
|
||||
|
||||
impl ColorImageConverter {
|
||||
pub fn new(params: ColorImageConverterParams) -> Self {
|
||||
let canvas = Canvas::new_from_id(¶ms.canvas_id);
|
||||
let svg = Svg::new_from_id(¶ms.svg_id);
|
||||
Self {
|
||||
canvas,
|
||||
svg,
|
||||
stage: Stage::New,
|
||||
counter: 0,
|
||||
mode: util::path_simplify_mode(¶ms.mode),
|
||||
params,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[wasm_bindgen]
|
||||
impl ColorImageConverter {
|
||||
|
||||
pub fn new_with_string(params: String) -> Self {
|
||||
let params: ColorImageConverterParams = serde_json::from_str(params.as_str()).unwrap();
|
||||
Self::new(params)
|
||||
}
|
||||
|
||||
pub fn init(&mut self) {
|
||||
let width = self.canvas.width() as u32;
|
||||
let height = self.canvas.height() as u32;
|
||||
let mut image = self.canvas.get_image_data_as_color_image(0, 0, width, height);
|
||||
|
||||
let key_color = if Self::should_key_image(&image) {
|
||||
if let Ok(key_color) = Self::find_unused_color_in_image(&image) {
|
||||
for y in 0..height as usize {
|
||||
for x in 0..width as usize {
|
||||
if image.get_pixel(x, y).a == 0 {
|
||||
image.set_pixel(x, y, &key_color);
|
||||
}
|
||||
}
|
||||
}
|
||||
key_color
|
||||
} else {
|
||||
Color::default()
|
||||
}
|
||||
} else {
|
||||
// The default color is all zeroes, which is treated by visioncortex as a special value meaning no keying will be applied.
|
||||
Color::default()
|
||||
};
|
||||
|
||||
let runner = Runner::new(RunnerConfig {
|
||||
diagonal: self.params.layer_difference == 0,
|
||||
hierarchical: HIERARCHICAL_MAX,
|
||||
batch_size: 25600,
|
||||
good_min_area: self.params.filter_speckle,
|
||||
good_max_area: (width * height) as usize,
|
||||
is_same_color_a: self.params.color_precision,
|
||||
is_same_color_b: 1,
|
||||
deepen_diff: self.params.layer_difference,
|
||||
hollow_neighbours: 1,
|
||||
key_color,
|
||||
keying_action: if self.params.hierarchical == "cutout" {
|
||||
KeyingAction::Keep
|
||||
} else {
|
||||
KeyingAction::Discard
|
||||
},
|
||||
}, image);
|
||||
self.stage = Stage::Clustering(Box::new(runner.start()));
|
||||
}
|
||||
|
||||
pub fn tick(&mut self) -> bool {
|
||||
match &mut self.stage {
|
||||
Stage::New => {
|
||||
panic!("uninitialized");
|
||||
},
|
||||
Stage::Clustering(builder) => {
|
||||
self.canvas.log("Clustering tick");
|
||||
if builder.tick() {
|
||||
match self.params.hierarchical.as_str() {
|
||||
"stacked" => {
|
||||
self.stage = Stage::Vectorize(builder.result());
|
||||
},
|
||||
"cutout" => {
|
||||
let clusters = builder.result();
|
||||
let view = clusters.view();
|
||||
let image = view.to_color_image();
|
||||
let runner = Runner::new(RunnerConfig {
|
||||
diagonal: false,
|
||||
hierarchical: 64,
|
||||
batch_size: 25600,
|
||||
good_min_area: 0,
|
||||
good_max_area: (image.width * image.height) as usize,
|
||||
is_same_color_a: 0,
|
||||
is_same_color_b: 1,
|
||||
deepen_diff: 0,
|
||||
hollow_neighbours: 0,
|
||||
key_color: Default::default(),
|
||||
keying_action: KeyingAction::Discard,
|
||||
}, image);
|
||||
self.stage = Stage::Reclustering(Box::new(runner.start()));
|
||||
},
|
||||
_ => panic!("unknown hierarchical `{}`", self.params.hierarchical)
|
||||
}
|
||||
}
|
||||
false
|
||||
},
|
||||
Stage::Reclustering(builder) => {
|
||||
self.canvas.log("Reclustering tick");
|
||||
if builder.tick() {
|
||||
self.stage = Stage::Vectorize(builder.result())
|
||||
}
|
||||
false
|
||||
},
|
||||
Stage::Vectorize(clusters) => {
|
||||
let view = clusters.view();
|
||||
if self.counter < view.clusters_output.len() {
|
||||
self.canvas.log("Vectorize tick");
|
||||
let cluster = view.get_cluster(view.clusters_output[self.counter]);
|
||||
let paths = cluster.to_compound_path(
|
||||
&view, false, self.mode,
|
||||
self.params.corner_threshold,
|
||||
self.params.length_threshold,
|
||||
self.params.max_iterations,
|
||||
self.params.splice_threshold
|
||||
);
|
||||
self.svg.prepend_path(
|
||||
&paths,
|
||||
&cluster.residue_color(),
|
||||
Some(self.params.path_precision),
|
||||
);
|
||||
self.counter += 1;
|
||||
false
|
||||
} else {
|
||||
self.canvas.log("done");
|
||||
true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn progress(&self) -> i32 {
|
||||
(match &self.stage {
|
||||
Stage::New => {
|
||||
0
|
||||
},
|
||||
Stage::Clustering(builder) => {
|
||||
builder.progress() / 2
|
||||
},
|
||||
Stage::Reclustering(_builder) => {
|
||||
50
|
||||
},
|
||||
Stage::Vectorize(clusters) => {
|
||||
50 + 50 * self.counter as u32 / clusters.view().clusters_output.len() as u32
|
||||
}
|
||||
}) as i32
|
||||
}
|
||||
|
||||
fn color_exists_in_image(img: &ColorImage, color: Color) -> bool {
|
||||
for y in 0..img.height {
|
||||
for x in 0..img.width {
|
||||
let pixel_color = img.get_pixel(x, y);
|
||||
if pixel_color.r == color.r && pixel_color.g == color.g && pixel_color.b == color.b {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
fn find_unused_color_in_image(img: &ColorImage) -> Result<Color, String> {
|
||||
let special_colors = IntoIterator::into_iter([
|
||||
Color::new(255, 0, 0),
|
||||
Color::new(0, 255, 0),
|
||||
Color::new(0, 0, 255),
|
||||
Color::new(255, 255, 0),
|
||||
Color::new(0, 255, 255),
|
||||
Color::new(255, 0, 255),
|
||||
Color::new(128, 128, 128),
|
||||
]);
|
||||
for color in special_colors {
|
||||
if !Self::color_exists_in_image(img, color) {
|
||||
return Ok(color);
|
||||
}
|
||||
}
|
||||
Err(String::from("unable to find unused color in image to use as key"))
|
||||
}
|
||||
|
||||
fn should_key_image(img: &ColorImage) -> bool {
|
||||
if img.width == 0 || img.height == 0 {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check for transparency at several scanlines
|
||||
let threshold = ((img.width * 2) as f32 * KEYING_THRESHOLD) as usize;
|
||||
let mut num_transparent_pixels = 0;
|
||||
let y_positions = [0, img.height / 4, img.height / 2, 3 * img.height / 4, img.height - 1];
|
||||
for y in y_positions {
|
||||
for x in 0..img.width {
|
||||
if img.get_pixel(x, y).a == 0 {
|
||||
num_transparent_pixels += 1;
|
||||
}
|
||||
if num_transparent_pixels >= threshold {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
false
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
mod binary_image;
|
||||
mod color_image;
|
||||
mod util;
|
||||
@@ -0,0 +1,10 @@
|
||||
use visioncortex::PathSimplifyMode;
|
||||
|
||||
pub fn path_simplify_mode(s: &str) -> PathSimplifyMode {
|
||||
match s {
|
||||
"polygon" => PathSimplifyMode::Polygon,
|
||||
"spline" => PathSimplifyMode::Spline,
|
||||
"none" => PathSimplifyMode::None,
|
||||
_ => panic!("unknown PathSimplifyMode {}", s),
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
// Copyright 2020 Tsang Hao Fung. See the COPYRIGHT
|
||||
// file at the top-level directory of this distribution and at
|
||||
// http://rust-lang.org/COPYRIGHT.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or
|
||||
// http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
|
||||
// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your
|
||||
// option. This file may not be copied, modified, or distributed
|
||||
// except according to those terms.
|
||||
#![doc(
|
||||
html_logo_url = "https://github.com/visioncortex/vtracer/raw/master/docs/images/visioncortex icon.png"
|
||||
)]
|
||||
use wasm_bindgen::prelude::*;
|
||||
|
||||
mod conversion;
|
||||
mod canvas;
|
||||
mod common;
|
||||
mod svg;
|
||||
mod utils;
|
||||
|
||||
#[wasm_bindgen(start)]
|
||||
pub fn main() {
|
||||
utils::set_panic_hook();
|
||||
console_log::init().unwrap();
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
use web_sys::Element;
|
||||
use visioncortex::{Color, CompoundPath, PointF64};
|
||||
use super::common::document;
|
||||
|
||||
pub struct Svg {
|
||||
element: Element,
|
||||
}
|
||||
|
||||
impl Svg {
|
||||
pub fn new_from_id(svg_id: &str) -> Self {
|
||||
let element = document().get_element_by_id(svg_id).unwrap();
|
||||
|
||||
Self { element }
|
||||
}
|
||||
|
||||
pub fn prepend_path(&mut self, paths: &CompoundPath, color: &Color, precision: Option<u32>) {
|
||||
let path = document()
|
||||
.create_element_ns(Some("http://www.w3.org/2000/svg"), "path")
|
||||
.unwrap();
|
||||
let (string, offset) = paths.to_svg_string(true, PointF64::default(), precision);
|
||||
path.set_attribute("d", &string).unwrap();
|
||||
path.set_attribute(
|
||||
"transform",
|
||||
format!("translate({},{})", offset.x, offset.y).as_str(),
|
||||
)
|
||||
.unwrap();
|
||||
path.set_attribute(
|
||||
"style",
|
||||
format!("fill: {};", color.to_hex_string()).as_str(),
|
||||
)
|
||||
.unwrap();
|
||||
self.element.prepend_with_node_1(&path).unwrap();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
extern crate cfg_if;
|
||||
|
||||
cfg_if::cfg_if! {
|
||||
// When the `console_error_panic_hook` feature is enabled, we can call the
|
||||
// `set_panic_hook` function to get better error messages if we ever panic.
|
||||
if #[cfg(feature = "console_error_panic_hook")] {
|
||||
extern crate console_error_panic_hook;
|
||||
pub use self::console_error_panic_hook::set_once as set_panic_hook;
|
||||
} else {
|
||||
#[inline]
|
||||
pub fn set_panic_hook() {}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user