Source Release

This commit is contained in:
Chris Tsang
2020-10-25 14:22:42 +08:00
parent f0a1369dc4
commit f6f4839185
23 changed files with 6333 additions and 3 deletions

2
webapp/.gitignore vendored Normal file
View File

@@ -0,0 +1,2 @@
pkg
wasm-pack.log

58
webapp/Cargo.toml Executable file
View File

@@ -0,0 +1,58 @@
[package]
name = "vtracer-webapp"
version = "0.1.0"
authors = ["Chris Tsang <tyt2y7@gmail.com>"]
edition = "2018"
description = "Semantic Computer Vision"
license = "MIT OR Apache-2.0"
homepage = "http://www.visioncortex.org/vtracer"
repository = "https://github.com/visioncortex/vtracer/"
categories = ["graphics", "computer-vision"]
keywords = ["computer-graphics", "computer-vision"]
exclude = [
"docs/*",
]
[lib]
crate-type = ["cdylib"]
[features]
default = ["console_error_panic_hook"]
[dependencies]
cfg-if = "0.1"
console_log = { version = "0.2", features = ["color"] }
wasm-bindgen = { version = "0.2", features = ["serde-serialize"] }
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
visioncortex = "0.1.0"
# 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 }
[dev-dependencies]
wasm-bindgen-test = "0.2"
[dependencies.web-sys]
version = "0.3"
features = [
"CanvasRenderingContext2d",
"console",
"Document",
"HtmlElement",
"HtmlCanvasElement",
"ImageData",
"Window",
]
# [profile.release]
# opt-level = 'z'
# lto = true
# `wasm-opt` is on by default in for the release profile, but it can be
# disabled by setting it to `false`
[package.metadata.wasm-pack.profile.release]
wasm-opt = false

38
webapp/Readme.md Executable file
View File

@@ -0,0 +1,38 @@
# Vision Cortex 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
```

2
webapp/app/.gitignore vendored Executable file
View File

@@ -0,0 +1,2 @@
node_modules
dist

5
webapp/app/bootstrap.js vendored Executable file
View File

@@ -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));

185
webapp/app/index.html Executable file
View File

@@ -0,0 +1,185 @@
<!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 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>
</div>
<script src="./bootstrap.js"></script>
</body>
</html>

294
webapp/app/index.js Executable file
View File

@@ -0,0 +1,294 @@
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';
// 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([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';
});
// Function to load a given config WITHOUT restarting
function loadConfig(config) {
mode = config.mode;
clustering_mode = config.clustering_mode;
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;
}
// 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);
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('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();
});
function setSourceAndRestart(source) {
img.src = source instanceof File ? URL.createObjectURL(source) : source;
img.onload = function () {
svg.setAttribute('viewBox', `0 0 ${img.naturalWidth} ${img.naturalHeight}`);
canvas.width = img.naturalWidth;
canvas.height = img.naturalHeight;
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('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,
'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,
});
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 = '#000';
canvas.style.display = 'none';
} else {
svg.style.background = '';
canvas.style.display = '';
}
}
run () {
const This = this;
requestAnimationFrame(function tick () {
if (!This.stopped) {
if (!This.converter.tick()) {
progress.value = This.converter.progress();
if (progress.value >= 50) {
canvas.style.display = 'none';
}
if (progress.value >= progress.max) {
progressregion.style.display = 'none';
progress.value = 0;
}
requestAnimationFrame(tick);
}
}
});
}
stop () {
this.stopped = true;
}
}

5320
webapp/app/package-lock.json generated Executable file

File diff suppressed because it is too large Load Diff

25
webapp/app/package.json Executable file
View File

@@ -0,0 +1,25 @@
{
"name": "vtracer-app",
"version": "0.1.0",
"description": "VTracer Webapp",
"author": "Chris Tsang <tyt2y7@gmail.com>",
"license": "proprietary",
"private": true,
"main": "index.js",
"scripts": {
"start": "webpack-dev-server",
"build": "webpack"
},
"keywords": [
"rust",
"wasm"
],
"dependencies": {
"vtracer": "file:../pkg",
"webpack": "^4.41.5"
},
"devDependencies": {
"webpack-cli": "^3.3.11",
"webpack-dev-server": "^3.10.1"
}
}

14
webapp/app/webpack.config.js Executable file
View File

@@ -0,0 +1,14 @@
const path = require('path');
module.exports = {
entry: "./bootstrap.js",
output: {
path: path.resolve(__dirname, "dist"),
filename: "bootstrap.js",
},
mode: "development",
devServer: {
//host: "0.0.0.0",
port: 8080,
}
};

60
webapp/src/canvas.rs Executable file
View File

@@ -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_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));
}
}

7
webapp/src/common.rs Executable file
View File

@@ -0,0 +1,7 @@
pub fn window() -> web_sys::Window {
web_sys::window().unwrap()
}
pub fn document() -> web_sys::Document {
window().document().unwrap()
}

View File

@@ -0,0 +1,97 @@
use wasm_bindgen::prelude::*;
use visioncortex::{clusters::Clusters, Color, ColorName, PointI32, 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,
}
#[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(&params.canvas_id);
let svg = Svg::new_from_id(&params.svg_id);
Self {
canvas,
svg,
clusters: Clusters::default(),
counter: 0,
mode: util::path_simplify_mode(&params.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_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 svg_path = cluster.to_svg_path(
self.mode,
self.params.corner_threshold,
self.params.length_threshold,
self.params.max_iterations,
self.params.splice_threshold
);
let color = Color::color(&ColorName::White);
self.svg.prepend_path_with_fill(
&svg_path,
&PointI32::default(),
&color,
);
}
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
}
}

View File

@@ -0,0 +1,133 @@
use wasm_bindgen::prelude::*;
use visioncortex::{PathSimplifyMode, PointI32};
use visioncortex::color_clusters::{IncrementalBuilder, Clusters, Runner, RunnerConfig};
use crate::canvas::*;
use crate::svg::*;
use serde::Deserialize;
use super::util;
#[derive(Debug, Deserialize)]
pub struct ColorImageConverterParams {
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 color_precision: i32,
pub layer_difference: i32,
}
#[wasm_bindgen]
pub struct ColorImageConverter {
canvas: Canvas,
svg: Svg,
stage: Stage,
counter: usize,
mode: PathSimplifyMode,
params: ColorImageConverterParams,
}
pub enum Stage {
New,
Clustering(IncrementalBuilder),
Vectorize(Clusters),
}
impl ColorImageConverter {
pub fn new(params: ColorImageConverterParams) -> Self {
let canvas = Canvas::new_from_id(&params.canvas_id);
let svg = Svg::new_from_id(&params.svg_id);
Self {
canvas,
svg,
stage: Stage::New,
counter: 0,
mode: util::path_simplify_mode(&params.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 image = self.canvas.get_image_data_as_image(0, 0, width, height);
let runner = Runner::new(RunnerConfig {
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,
}, image);
self.stage = Stage::Clustering(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() {
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 svg_path = cluster.to_svg_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_with_fill(
&svg_path,
&PointI32::new(0, 0),
&cluster.residue_color(),
);
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::Vectorize(clusters) => {
50 + 50 * self.counter as u32 / clusters.view().clusters_output.len() as u32
}
}) as i32
}
}

6
webapp/src/conversion/mod.rs Executable file
View File

@@ -0,0 +1,6 @@
mod binary_image;
mod color_image;
mod util;
pub use binary_image::*;
pub use color_image::*;

10
webapp/src/conversion/util.rs Executable file
View File

@@ -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),
}
}

13
webapp/src/lib.rs Executable file
View File

@@ -0,0 +1,13 @@
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();
}

33
webapp/src/svg.rs Executable file
View File

@@ -0,0 +1,33 @@
use web_sys::Element;
use visioncortex::{Color, PointI32};
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_with_fill(&mut self, path_string: &str, offset: &PointI32, color: &Color) {
let path = document()
.create_element_ns(Some("http://www.w3.org/2000/svg"), "path")
.unwrap();
path.set_attribute("d", path_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();
}
}

13
webapp/src/utils.rs Normal file
View File

@@ -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() {}
}
}