Compare commits
22 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| c2a5626afa | |||
| d0593e716a | |||
| 9ce7df176a | |||
| 5928c0a7f5 | |||
| b2c95a50cd | |||
| bcd3ffb40e | |||
| 20da3efd3c | |||
| 0c27d1f06a | |||
| 0c7c7fc808 | |||
| 97ac767844 | |||
| 0b37051d40 | |||
| 2695d7b59b | |||
| 433a68d6d6 | |||
| bb3b66780b | |||
| 049ba7f503 | |||
| 9d67b7c554 | |||
| fa5d2906c0 | |||
| fae37a709e | |||
| 5c9d5cd757 | |||
| 1b49880f85 | |||
| 0b0d69839a | |||
| 7ea4600176 |
@@ -0,0 +1 @@
|
||||
docs/** linguist-generated
|
||||
@@ -0,0 +1,22 @@
|
||||
name: Rust
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ master ]
|
||||
pull_request:
|
||||
branches: [ master ]
|
||||
|
||||
env:
|
||||
CARGO_TERM_COLOR: always
|
||||
|
||||
jobs:
|
||||
build:
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Build
|
||||
run: cargo build --verbose
|
||||
- name: Run tests
|
||||
run: cargo test --verbose
|
||||
@@ -1,7 +1,7 @@
|
||||
<div align="center">
|
||||
|
||||
<img src="docs/images/visioncortex-banner.png">
|
||||
<h1>visioncortex VTracer</h1>
|
||||
<h1>VTracer</h1>
|
||||
|
||||
<p>
|
||||
<strong>Raster to Vector Graphics Converter built on top of visioncortex</strong>
|
||||
@@ -26,6 +26,8 @@ Comparing to [Potrace]() which only accept binarized inputs (Black & White pixma
|
||||
|
||||
Comparing to Adobe Illustrator's Live Trace, VTracer's output is much more compact (less shapes) as we adopt a stacking strategy and avoid producing shapes with holes.
|
||||
|
||||
VTracer is originally designed for processing high resolution scans of historic blueprints up to gigapixels. At the same time, VTracer can also handle low resolution pixel art, simulating `image-rendering: pixelated` for retro game artworks.
|
||||
|
||||
A technical description of the algorithm is on [visioncortex.org/vtracer-docs](//www.visioncortex.org/vtracer-docs).
|
||||
|
||||
## Web App
|
||||
@@ -37,31 +39,35 @@ VTracer and its [core library](//github.com/visioncortex/visioncortex) is implem
|
||||

|
||||
|
||||
## Command Line
|
||||
```
|
||||
visioncortex VTracer
|
||||
A cmd app to convert images into vector graphics.
|
||||
|
||||
USAGE:
|
||||
vtracer [OPTIONS] --input <input> --output <output>
|
||||
|
||||
FLAGS:
|
||||
-h, --help Prints help information
|
||||
-V, --version Prints version information
|
||||
|
||||
OPTIONS:
|
||||
--colormode <color_mode> True color image `color` (default) or Binary image `bw`
|
||||
-p, --color_precision <color_precision> Number of significant bits to use in an RGB channel
|
||||
-c, --corner_threshold <corner_threshold> Minimum momentary angle (degree) to be considered a corner
|
||||
-f, --filter_speckle <filter_speckle> Discard patches smaller than X px in size
|
||||
-g, --gradient_step <gradient_step> Color difference between gradient layers
|
||||
-i, --input <input> Path to input raster image
|
||||
-m, --mode <mode> Curver fitting mode `pixel`, `polygon`, `spline`
|
||||
-o, --output <output> Path to output vector graphics
|
||||
--preset <preset> Use one of the preset configs `bw`, `poster`, `photo`
|
||||
-l, --segment_length <segment_length>
|
||||
Perform iterative subdivide smooth until all segments are shorter than this length
|
||||
|
||||
-s, --splice_threshold <splice_threshold> Minimum angle displacement (degree) to splice a spline
|
||||
|
||||
```sh
|
||||
visioncortex VTracer 0.3.0
|
||||
A cmd app to convert images into vector graphics.
|
||||
|
||||
USAGE:
|
||||
vtracer [OPTIONS] --input <input> --output <output>
|
||||
|
||||
FLAGS:
|
||||
-h, --help Prints help information
|
||||
-V, --version Prints version information
|
||||
|
||||
OPTIONS:
|
||||
--colormode <color_mode> True color image `color` (default) or Binary image `bw`
|
||||
-p, --color_precision <color_precision> Number of significant bits to use in an RGB channel
|
||||
-c, --corner_threshold <corner_threshold> Minimum momentary angle (degree) to be considered a corner
|
||||
-f, --filter_speckle <filter_speckle> Discard patches smaller than X px in size
|
||||
-g, --gradient_step <gradient_step> Color difference between gradient layers
|
||||
--hierarchical <hierarchical>
|
||||
Hierarchical clustering `stacked` (default) or non-stacked `cutout`. Only applies to color mode.
|
||||
|
||||
-i, --input <input> Path to input raster image
|
||||
-m, --mode <mode> Curver fitting mode `pixel`, `polygon`, `spline`
|
||||
-o, --output <output> Path to output vector graphics
|
||||
--preset <preset> Use one of the preset configs `bw`, `poster`, `photo`
|
||||
-l, --segment_length <segment_length>
|
||||
Perform iterative subdivide smooth until all segments are shorter than this length
|
||||
|
||||
-s, --splice_threshold <splice_threshold> Minimum angle displacement (degree) to splice a spline
|
||||
```
|
||||
|
||||
### Usage
|
||||
@@ -75,5 +81,5 @@ The library can be found on [crates.io/vtracer](//crates.io/crates/vtracer).
|
||||
|
||||
### Install
|
||||
```
|
||||
vtracer = "0.1.0"
|
||||
vtracer = "*"
|
||||
```
|
||||
|
||||
@@ -1,10 +1,16 @@
|
||||
Version 0.2.0 (2020-10-31)
|
||||
Version 0.3.0 (2021-01-24)
|
||||
==========================
|
||||
|
||||
- Added cutout mode
|
||||
|
||||
|
||||
Version 0.2.0 (2020-11-15)
|
||||
==========================
|
||||
|
||||
- Use relative & closed paths
|
||||
|
||||
|
||||
Version 0.1.1 (2020-10-31)
|
||||
Version 0.1.1 (2020-11-01)
|
||||
==========================
|
||||
|
||||
- SVG namespace
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "vtracer"
|
||||
version = "0.2.0"
|
||||
version = "0.4.0"
|
||||
authors = ["Chris Tsang <tyt2y7@gmail.com>"]
|
||||
edition = "2018"
|
||||
description = "A cmd app to convert images into vector graphics."
|
||||
@@ -13,4 +13,4 @@ keywords = ["svg", "computer-graphics"]
|
||||
[dependencies]
|
||||
clap = "2.33.3"
|
||||
image = "0.23.10"
|
||||
visioncortex = "0.3.0"
|
||||
visioncortex = "0.6.0"
|
||||
@@ -1,7 +1,7 @@
|
||||
use std::str::FromStr;
|
||||
use std::path::PathBuf;
|
||||
use clap::{Arg, App};
|
||||
use visioncortex::path::PathSimplifyMode;
|
||||
use visioncortex::PathSimplifyMode;
|
||||
|
||||
pub enum Preset {
|
||||
Bw,
|
||||
@@ -9,18 +9,22 @@ pub enum Preset {
|
||||
Photo
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum ColorMode {
|
||||
Color,
|
||||
Binary,
|
||||
}
|
||||
|
||||
pub enum Hierarchical {
|
||||
Stacked,
|
||||
Cutout,
|
||||
}
|
||||
|
||||
/// Converter config
|
||||
#[derive(Debug)]
|
||||
pub struct Config {
|
||||
pub input_path: PathBuf,
|
||||
pub output_path: PathBuf,
|
||||
pub color_mode: ColorMode,
|
||||
pub hierarchical: Hierarchical,
|
||||
pub filter_speckle: usize,
|
||||
pub color_precision: i32,
|
||||
pub layer_difference: i32,
|
||||
@@ -29,13 +33,14 @@ pub struct Config {
|
||||
pub length_threshold: f64,
|
||||
pub max_iterations: usize,
|
||||
pub splice_threshold: i32,
|
||||
pub path_precision: Option<u32>,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub(crate) struct ConverterConfig {
|
||||
pub input_path: PathBuf,
|
||||
pub output_path: PathBuf,
|
||||
pub color_mode: ColorMode,
|
||||
pub hierarchical: Hierarchical,
|
||||
pub filter_speckle_area: usize,
|
||||
pub color_precision_loss: i32,
|
||||
pub layer_difference: i32,
|
||||
@@ -44,6 +49,7 @@ pub(crate) struct ConverterConfig {
|
||||
pub length_threshold: f64,
|
||||
pub max_iterations: usize,
|
||||
pub splice_threshold: f64,
|
||||
pub path_precision: Option<u32>,
|
||||
}
|
||||
|
||||
impl Default for Config {
|
||||
@@ -52,6 +58,7 @@ impl Default for Config {
|
||||
input_path: PathBuf::default(),
|
||||
output_path: PathBuf::default(),
|
||||
color_mode: ColorMode::Color,
|
||||
hierarchical: Hierarchical::Stacked,
|
||||
mode: PathSimplifyMode::Spline,
|
||||
filter_speckle: 4,
|
||||
color_precision: 6,
|
||||
@@ -60,6 +67,7 @@ impl Default for Config {
|
||||
length_threshold: 4.0,
|
||||
splice_threshold: 45,
|
||||
max_iterations: 10,
|
||||
path_precision: Some(8),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -76,6 +84,18 @@ impl FromStr for ColorMode {
|
||||
}
|
||||
}
|
||||
|
||||
impl FromStr for Hierarchical {
|
||||
type Err = String;
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
match s {
|
||||
"stacked" => Ok(Self::Stacked),
|
||||
"cutout" => Ok(Self::Cutout),
|
||||
_ => Err(format!("unknown Hierarchical {}", s)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl FromStr for Preset {
|
||||
type Err = String;
|
||||
|
||||
@@ -122,6 +142,14 @@ impl Config {
|
||||
.takes_value(true)
|
||||
.help("True color image `color` (default) or Binary image `bw`"));
|
||||
|
||||
let app = app.arg(Arg::with_name("hierarchical")
|
||||
.long("hierarchical")
|
||||
.takes_value(true)
|
||||
.help(
|
||||
"Hierarchical clustering `stacked` (default) or non-stacked `cutout`. \
|
||||
Only applies to color mode. "
|
||||
));
|
||||
|
||||
let app = app.arg(Arg::with_name("preset")
|
||||
.long("preset")
|
||||
.takes_value(true)
|
||||
@@ -169,6 +197,11 @@ impl Config {
|
||||
.takes_value(true)
|
||||
.help("Curver fitting mode `pixel`, `polygon`, `spline`"));
|
||||
|
||||
let app = app.arg(Arg::with_name("path_precision")
|
||||
.long("path_precision")
|
||||
.takes_value(true)
|
||||
.help("Number of deciaml places to use in path string"));
|
||||
|
||||
// Extract matches
|
||||
let matches = app.get_matches();
|
||||
|
||||
@@ -187,6 +220,10 @@ impl Config {
|
||||
config.color_mode = ColorMode::from_str(if value.trim() == "bw" || value.trim() == "BW" {"binary"} else {"color"}).unwrap()
|
||||
}
|
||||
|
||||
if let Some(value) = matches.value_of("hierarchical") {
|
||||
config.hierarchical = Hierarchical::from_str(value).unwrap()
|
||||
}
|
||||
|
||||
if let Some(value) = matches.value_of("mode") {
|
||||
let value = value.trim();
|
||||
config.mode = path_simplify_mode_from_str(if value == "pixel" {
|
||||
@@ -196,7 +233,7 @@ impl Config {
|
||||
} else if value == "spline" {
|
||||
"spline"
|
||||
} else {
|
||||
panic!("Parser Error: Curve fitting mode is invalid with value {}", value);
|
||||
panic!("Parser Error: Curve fitting mode is invalid: {}", value);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -208,7 +245,7 @@ impl Config {
|
||||
}
|
||||
config.filter_speckle = value;
|
||||
} else {
|
||||
panic!("Parser Error: Filter speckle is not a positive integer with value {}.", value);
|
||||
panic!("Parser Error: Filter speckle is not a positive integer: {}.", value);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -220,7 +257,7 @@ impl Config {
|
||||
}
|
||||
config.color_precision = value;
|
||||
} else {
|
||||
panic!("Parser Error: Color precision is not an integer with value {}.", value);
|
||||
panic!("Parser Error: Color precision is not an integer: {}.", value);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -232,7 +269,7 @@ impl Config {
|
||||
}
|
||||
config.layer_difference = value;
|
||||
} else {
|
||||
panic!("Parser Error: Gradient step is not an integer with value {}.", value);
|
||||
panic!("Parser Error: Gradient step is not an integer: {}.", value);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -244,7 +281,7 @@ impl Config {
|
||||
}
|
||||
config.corner_threshold = value
|
||||
} else {
|
||||
panic!("Parser Error: Corner threshold is not numeric with value {}.", value);
|
||||
panic!("Parser Error: Corner threshold is not numeric: {}.", value);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -256,7 +293,7 @@ impl Config {
|
||||
}
|
||||
config.length_threshold = value;
|
||||
} else {
|
||||
panic!("Parser Error: Segment length is not numeric with value {}.", value);
|
||||
panic!("Parser Error: Segment length is not numeric: {}.", value);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -268,7 +305,16 @@ impl Config {
|
||||
}
|
||||
config.splice_threshold = value;
|
||||
} else {
|
||||
panic!("Parser Error: Segment length is not numeric with value {}.", value);
|
||||
panic!("Parser Error: Segment length is not numeric: {}.", value);
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(value) = matches.value_of("path_precision") {
|
||||
if value.trim().parse::<u32>().is_ok() { // is numeric
|
||||
let value = value.trim().parse::<u32>().ok();
|
||||
config.path_precision = value;
|
||||
} else {
|
||||
panic!("Parser Error: Path precision is not an unsigned integer: {}.", value);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -283,6 +329,7 @@ impl Config {
|
||||
input_path,
|
||||
output_path,
|
||||
color_mode: ColorMode::Binary,
|
||||
hierarchical: Hierarchical::Stacked,
|
||||
filter_speckle: 4,
|
||||
color_precision: 6,
|
||||
layer_difference: 16,
|
||||
@@ -291,11 +338,13 @@ impl Config {
|
||||
length_threshold: 4.0,
|
||||
max_iterations: 10,
|
||||
splice_threshold: 45,
|
||||
path_precision: Some(8),
|
||||
},
|
||||
Preset::Poster => Self {
|
||||
input_path,
|
||||
output_path,
|
||||
color_mode: ColorMode::Color,
|
||||
hierarchical: Hierarchical::Stacked,
|
||||
filter_speckle: 4,
|
||||
color_precision: 8,
|
||||
layer_difference: 16,
|
||||
@@ -304,11 +353,13 @@ impl Config {
|
||||
length_threshold: 4.0,
|
||||
max_iterations: 10,
|
||||
splice_threshold: 45,
|
||||
path_precision: Some(8),
|
||||
},
|
||||
Preset::Photo => Self {
|
||||
input_path,
|
||||
output_path,
|
||||
color_mode: ColorMode::Color,
|
||||
hierarchical: Hierarchical::Stacked,
|
||||
filter_speckle: 10,
|
||||
color_precision: 8,
|
||||
layer_difference: 48,
|
||||
@@ -317,6 +368,7 @@ impl Config {
|
||||
length_threshold: 4.0,
|
||||
max_iterations: 10,
|
||||
splice_threshold: 45,
|
||||
path_precision: Some(8),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -326,6 +378,7 @@ impl Config {
|
||||
input_path: self.input_path,
|
||||
output_path: self.output_path,
|
||||
color_mode: self.color_mode,
|
||||
hierarchical: self.hierarchical,
|
||||
filter_speckle_area: self.filter_speckle * self.filter_speckle,
|
||||
color_precision_loss: 8 - self.color_precision,
|
||||
layer_difference: self.layer_difference,
|
||||
@@ -334,6 +387,7 @@ impl Config {
|
||||
length_threshold: self.length_threshold,
|
||||
max_iterations: self.max_iterations,
|
||||
splice_threshold: deg2rad(self.splice_threshold),
|
||||
path_precision: self.path_precision,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
use std::path::PathBuf;
|
||||
use std::{fs::File, io::Write};
|
||||
|
||||
use visioncortex::{Color, ColorImage, ColorName, color_clusters::RunnerConfig, color_clusters::Runner};
|
||||
use super::config::{Config, ColorMode, ConverterConfig};
|
||||
use visioncortex::{Color, ColorImage, ColorName};
|
||||
use visioncortex::color_clusters::{Runner, RunnerConfig, HIERARCHICAL_MAX};
|
||||
use super::config::{Config, ColorMode, Hierarchical, ConverterConfig};
|
||||
use super::svg::SvgFile;
|
||||
|
||||
/// Convert an image file into svg file
|
||||
@@ -26,6 +27,8 @@ fn color_image_to_svg(config: ConverterConfig) -> Result<(), String> {
|
||||
}
|
||||
|
||||
let runner = Runner::new(RunnerConfig {
|
||||
diagonal: config.layer_difference == 0,
|
||||
hierarchical: HIERARCHICAL_MAX,
|
||||
batch_size: 25600,
|
||||
good_min_area: config.filter_speckle_area,
|
||||
good_max_area: (width * height),
|
||||
@@ -35,7 +38,27 @@ fn color_image_to_svg(config: ConverterConfig) -> Result<(), String> {
|
||||
hollow_neighbours: 1,
|
||||
}, img);
|
||||
|
||||
let clusters = runner.run();
|
||||
let mut clusters = runner.run();
|
||||
|
||||
match config.hierarchical {
|
||||
Hierarchical::Stacked => {}
|
||||
Hierarchical::Cutout => {
|
||||
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,
|
||||
}, image);
|
||||
clusters = runner.run();
|
||||
},
|
||||
}
|
||||
|
||||
let view = clusters.view();
|
||||
|
||||
@@ -51,7 +74,7 @@ fn color_image_to_svg(config: ConverterConfig) -> Result<(), String> {
|
||||
config.max_iterations,
|
||||
config.splice_threshold
|
||||
);
|
||||
svg.add_path(paths, cluster.residue_color());
|
||||
svg.add_path(paths, cluster.residue_color(), config.path_precision);
|
||||
}
|
||||
|
||||
write_svg(svg, config.output_path)
|
||||
@@ -83,7 +106,7 @@ fn binary_image_to_svg(config: ConverterConfig) -> Result<(), String> {
|
||||
config.max_iterations,
|
||||
config.splice_threshold,
|
||||
);
|
||||
svg.add_path(paths, Color::color(&ColorName::Black));
|
||||
svg.add_path(paths, Color::color(&ColorName::Black), config.path_precision);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@ pub struct SvgFile {
|
||||
pub struct SvgPath {
|
||||
pub path: CompoundPath,
|
||||
pub color: Color,
|
||||
pub path_precision: Option<u32>,
|
||||
}
|
||||
|
||||
impl SvgFile {
|
||||
@@ -21,10 +22,11 @@ impl SvgFile {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn add_path(&mut self, path: CompoundPath, color: Color) {
|
||||
pub fn add_path(&mut self, path: CompoundPath, color: Color, path_precision: Option<u32>) {
|
||||
self.paths.push(SvgPath {
|
||||
path,
|
||||
color,
|
||||
path_precision,
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -47,7 +49,7 @@ impl fmt::Display for SvgFile {
|
||||
|
||||
impl fmt::Display for SvgPath {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
let (string, offset) = self.path.to_svg_string(true, PointF64::default());
|
||||
let (string, offset) = self.path.to_svg_string(true, PointF64::default(), self.path_precision);
|
||||
writeln!(
|
||||
f, "<path d=\"{}\" fill=\"{}\" transform=\"translate({},{})\"/>",
|
||||
string, self.color.to_hex_string(),
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
Copyright (c) 2020 Tsang Hao Fung
|
||||
|
||||
All Rights Reserved
|
||||
The content in the /docs directory is for GitHub pages, and is not covered under open source licenses.
|
||||
|
||||
@@ -1,23 +0,0 @@
|
||||
K1_drawing
|
||||
B/W 4
|
||||
https://commons.wikimedia.org/wiki/File:K1_drawing.jpg
|
||||
|
||||
Cityscape Sunset_DFM3-01
|
||||
Color 4,8,32
|
||||
<a href="https://www.vecteezy.com/free-vector/building">Building Vectors by Vecteezy</a>
|
||||
|
||||
Gum Tree Vector
|
||||
Color 4,8,32
|
||||
<a href="https://www.vecteezy.com/free-vector/nature">Nature Vectors by Vecteezy</a>
|
||||
|
||||
vectorstock_31191940
|
||||
Color 8,7,64
|
||||
<a href="https://www.vectorstock.com/royalty-free-vector/dessert-poster-design-with-chocolate-cake-mousses-vector-31191940">Vector image by VectorStock / vectorstock</a>
|
||||
|
||||
averie-woodard-4nulm-JUYFo-unsplash
|
||||
Color 8,7,24
|
||||
<span>Photo by <a href="https://unsplash.com/@averieclaire?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText">averie woodard</a> on <a href="https://unsplash.com/s/photos/portrait?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText">Unsplash</a></span>
|
||||
|
||||
angel-luciano-LATYeZyw88c-unsplash
|
||||
Color 10,8,48 180
|
||||
<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>
|
||||
|
Before Width: | Height: | Size: 304 KiB |
|
Before Width: | Height: | Size: 3.9 MiB |
|
After Width: | Height: | Size: 9.7 KiB |
@@ -258,7 +258,7 @@
|
||||
/******/ promises.push(installedWasmModuleData);
|
||||
/******/ else {
|
||||
/******/ var importObject = wasmImportObjects[wasmModuleId]();
|
||||
/******/ var req = fetch(__webpack_require__.p + "" + {"../pkg/vtracer_webapp_bg.wasm":"7ef94c5089d40af744f2"}[wasmModuleId] + ".module.wasm");
|
||||
/******/ var req = fetch(__webpack_require__.p + "" + {"../pkg/vtracer_webapp_bg.wasm":"bad37dda49ff45dee696"}[wasmModuleId] + ".module.wasm");
|
||||
/******/ var promise;
|
||||
/******/ if(importObject instanceof Promise && typeof WebAssembly.compileStreaming === 'function') {
|
||||
/******/ promise = Promise.all([WebAssembly.compileStreaming(req), importObject]).then(function(items) {
|
||||
|
||||
|
Before Width: | Height: | Size: 160 KiB After Width: | Height: | Size: 154 KiB |
|
Before Width: | Height: | Size: 192 KiB After Width: | Height: | Size: 243 KiB |
|
Before Width: | Height: | Size: 18 KiB After Width: | Height: | Size: 5.8 KiB |
@@ -79,8 +79,7 @@
|
||||
}
|
||||
#canvas-container {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
height: max-content;
|
||||
}
|
||||
#svg, #frame {
|
||||
position: absolute;
|
||||
@@ -91,7 +90,7 @@
|
||||
stroke: #ff0;
|
||||
}
|
||||
#droptext {
|
||||
height: 100%;
|
||||
height: 480px;
|
||||
color: #CC972E;
|
||||
}
|
||||
input[type=range]::-webkit-slider-thumb {
|
||||
@@ -107,11 +106,11 @@
|
||||
background: #00275D;
|
||||
border-color: #CC972E;
|
||||
}
|
||||
#gallery, #options {
|
||||
#drop, #gallery, #options {
|
||||
border-top: 1px solid #686868;
|
||||
}
|
||||
#gallery {
|
||||
border-bottom: 1px solid #686868;
|
||||
padding-bottom: 0;
|
||||
}
|
||||
.galleryitem {
|
||||
cursor: pointer;
|
||||
@@ -140,7 +139,7 @@
|
||||
<a id="vc-logo" class="uk-logo" href="//www.visioncortex.org"><img style="height: 50px; margin: -30px 0 -25px 0;" src="./assets/visioncortex-logo.svg"></a>
|
||||
<a id="vc-logo-light" class="uk-logo" href="//www.visioncortex.org"><img style="height: 50px; margin: -30px 0 -25px 0;" src="./assets/visioncortex-logo-light.svg"></a>
|
||||
|
||||
VTracer
|
||||
/ VTracer
|
||||
</div>
|
||||
<div class="uk-navbar-right uk-padding-small">
|
||||
<a class="uk-button uk-button-default" href="//github.com/visioncortex/vtracer">GitHub</a>
|
||||
@@ -152,6 +151,15 @@
|
||||
<progress id="progressbar" class="uk-progress uk-align-right" value="0" max="100" style="width: 98%;"></progress>
|
||||
</div>
|
||||
<div class="uk-width-3-4">
|
||||
<div id="drop" class="uk-padding uk-flex uk-flex-center">
|
||||
<div id="canvas-container" class="uk-width-1-1">
|
||||
<div id="droptext" class="uk-flex uk-flex-middle uk-flex-center">
|
||||
<p>Drag an image here, Cmd-V to paste 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 id="gallery" class="uk-padding">
|
||||
<div class="uk-inline" uk-slider>
|
||||
<ul id="galleryslider" class="uk-slider-items uk-child-width-1-6 uk-grid uk-grid-small">
|
||||
@@ -160,16 +168,7 @@
|
||||
<a class="uk-position-center-left uk-position-small uk-hidden-hover" href="#" uk-slidenav-previous uk-slider-item="previous"></a>
|
||||
<a class="uk-position-center-right uk-position-small uk-hidden-hover" href="#" uk-slidenav-next uk-slider-item="next"></a>
|
||||
</div>
|
||||
<a class="uk-align-right uk-text-meta" style="margin:-5px;" uk-toggle="target: #credits-modal">Credits</a>
|
||||
</div>
|
||||
<div id="drop" class="uk-padding uk-flex uk-flex-center">
|
||||
<div id="canvas-container" class="uk-width-1-1" style="height: 480px;">
|
||||
<div id="droptext" class="uk-flex uk-flex-middle uk-flex-center">
|
||||
<p>Drag an image here, Cmd-V to paste 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>
|
||||
<a class="uk-align-right uk-text-meta" style="margin:-5px;" uk-toggle="target: #credits-modal">Photo Credits</a>
|
||||
</div>
|
||||
</div>
|
||||
<div id="options" class="uk-width-1-4 uk-height-1-1 uk-grid-small uk-flex uk-flex-center" uk-grid>
|
||||
@@ -193,6 +192,13 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="clustering-color-options uk-width-1-1 uk-flex uk-flex-right">
|
||||
<div class="uk-button-group">
|
||||
<button id="clustering-cutout" class="uk-button uk-button-default uk-button-small" uk-tooltip="Shapes disjoint with others">Cutout</button>
|
||||
<button id="clustering-stacked" class="selected uk-button uk-button-default uk-button-small" uk-tooltip="Stack shapes on top of another">Stacked</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="uk-width-1-1 uk-flex uk-flex-right">
|
||||
<div uk-tooltip="pos: left; title: Discard patches small than X px in size">
|
||||
Filter Speckle <span class="uk-text-meta">(Cleaner)</span>
|
||||
@@ -202,7 +208,7 @@
|
||||
4
|
||||
</div>
|
||||
<div class="uk-width-5-6">
|
||||
<input id="filterspeckle" class="uk-range" type="range" min="1" max="128" step="1" value="4">
|
||||
<input id="filterspeckle" class="uk-range" type="range" min="0" max="128" step="1" value="4">
|
||||
</div>
|
||||
|
||||
<div class="clustering-color-options uk-width-1-1 uk-flex uk-flex-right">
|
||||
|
||||
@@ -22,7 +22,7 @@ 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.3.0"
|
||||
visioncortex = "0.6.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
|
||||
|
||||
@@ -90,6 +90,13 @@
|
||||
</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>
|
||||
@@ -178,6 +185,19 @@
|
||||
<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>
|
||||
|
||||
@@ -7,7 +7,7 @@ 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';
|
||||
let mode = 'spline', clustering_mode = 'color', clustering_hierarchical = 'stacked';
|
||||
|
||||
// Hide canas and svg on load
|
||||
canvas.style.display = 'none';
|
||||
@@ -15,22 +15,22 @@ svg.style.display = 'none';
|
||||
|
||||
// Paste from clipboard
|
||||
document.addEventListener('paste', function (e) {
|
||||
if (e.clipboardData) {
|
||||
var items = e.clipboardData.items;
|
||||
if (!items) return;
|
||||
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();
|
||||
}
|
||||
//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
|
||||
@@ -49,8 +49,10 @@ 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,
|
||||
@@ -62,8 +64,10 @@ var presetConfigs = [
|
||||
{
|
||||
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,
|
||||
@@ -75,8 +79,10 @@ var presetConfigs = [
|
||||
{
|
||||
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,
|
||||
@@ -88,8 +94,10 @@ var presetConfigs = [
|
||||
{
|
||||
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,
|
||||
@@ -98,24 +106,13 @@ var presetConfigs = [
|
||||
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/averie-woodard-4nulm-JUYFo-unsplash-s.jpg',
|
||||
clustering_mode: 'color',
|
||||
filter_speckle: 8,
|
||||
color_precision: 7,
|
||||
layer_difference: 24,
|
||||
mode: 'spline',
|
||||
corner_threshold: 60,
|
||||
length_threshold: 4,
|
||||
splice_threshold: 45,
|
||||
source: 'https://unsplash.com/photos/4nulm-JUYFo',
|
||||
credit: '<span>Photo by <a href="https://unsplash.com/@averieclaire?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText">averie woodard</a> on <a href="https://unsplash.com/s/photos/portrait?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText">Unsplash</a></span>',
|
||||
},
|
||||
{
|
||||
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,
|
||||
@@ -124,66 +121,87 @@ var presetConfigs = [
|
||||
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
|
||||
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>`;
|
||||
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;
|
||||
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;
|
||||
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;
|
||||
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;
|
||||
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;
|
||||
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]);
|
||||
}
|
||||
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);
|
||||
});
|
||||
// Set source as specified
|
||||
setSourceAndRestart(this.firstElementChild.src);
|
||||
});
|
||||
});
|
||||
|
||||
// Upload button
|
||||
@@ -235,7 +253,8 @@ var globalcorner = parseInt(document.getElementById('corner').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);
|
||||
globallayerdifference = parseInt(document.getElementById('layerdifference').value),
|
||||
globalpathprecision = parseInt(document.getElementById('pathprecision').value);
|
||||
|
||||
// Load past inputs from localStorage
|
||||
/*
|
||||
@@ -272,6 +291,16 @@ document.getElementById('clustering-color').addEventListener('click', function (
|
||||
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;
|
||||
@@ -308,6 +337,12 @@ document.getElementById('splice').addEventListener('change', function (e) {
|
||||
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 () {
|
||||
@@ -322,9 +357,19 @@ window.addEventListener('beforeunload', function () {
|
||||
function setSourceAndRestart(source) {
|
||||
img.src = source instanceof File ? URL.createObjectURL(source) : source;
|
||||
img.onload = function () {
|
||||
svg.setAttribute('viewBox', `0 0 ${img.naturalWidth} ${img.naturalHeight}`);
|
||||
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
|
||||
@@ -342,6 +387,10 @@ function restart() {
|
||||
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');
|
||||
@@ -363,6 +412,7 @@ function restart() {
|
||||
'svg_id': svg.id,
|
||||
'mode': mode,
|
||||
'clustering_mode': clustering_mode,
|
||||
'hierarchical': clustering_hierarchical,
|
||||
'corner_threshold': deg2rad(globalcorner),
|
||||
'length_threshold': globallength,
|
||||
'max_iterations': 10,
|
||||
@@ -370,6 +420,7 @@ function restart() {
|
||||
'filter_speckle': globalfilterspeckle*globalfilterspeckle,
|
||||
'color_precision': 8-globalcolorprecision,
|
||||
'layer_difference': globallayerdifference,
|
||||
'path_precision': globalpathprecision,
|
||||
});
|
||||
if (runner) {
|
||||
runner.stop();
|
||||
|
||||
@@ -46,7 +46,7 @@ impl Canvas {
|
||||
image.data().to_vec()
|
||||
}
|
||||
|
||||
pub fn get_image_data_as_image(&self, x: u32, y: u32, width: u32, height: u32) -> ColorImage {
|
||||
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,
|
||||
|
||||
@@ -17,6 +17,7 @@ pub struct BinaryImageConverterParams {
|
||||
pub max_iterations: usize,
|
||||
pub splice_threshold: f64,
|
||||
pub filter_speckle: usize,
|
||||
pub path_precision: u32,
|
||||
}
|
||||
|
||||
#[wasm_bindgen]
|
||||
@@ -54,7 +55,7 @@ impl BinaryImageConverter {
|
||||
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 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!(
|
||||
@@ -80,6 +81,7 @@ impl BinaryImageConverter {
|
||||
self.svg.prepend_path(
|
||||
&paths,
|
||||
&color,
|
||||
Some(self.params.path_precision),
|
||||
);
|
||||
}
|
||||
self.counter += 1;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use wasm_bindgen::prelude::*;
|
||||
use visioncortex::PathSimplifyMode;
|
||||
use visioncortex::color_clusters::{IncrementalBuilder, Clusters, Runner, RunnerConfig};
|
||||
use visioncortex::color_clusters::{IncrementalBuilder, Clusters, Runner, RunnerConfig, HIERARCHICAL_MAX};
|
||||
|
||||
use crate::canvas::*;
|
||||
use crate::svg::*;
|
||||
@@ -13,6 +13,7 @@ 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,
|
||||
@@ -20,6 +21,7 @@ pub struct ColorImageConverterParams {
|
||||
pub filter_speckle: usize,
|
||||
pub color_precision: i32,
|
||||
pub layer_difference: i32,
|
||||
pub path_precision: u32,
|
||||
}
|
||||
|
||||
#[wasm_bindgen]
|
||||
@@ -35,6 +37,7 @@ pub struct ColorImageConverter {
|
||||
pub enum Stage {
|
||||
New,
|
||||
Clustering(IncrementalBuilder),
|
||||
Reclustering(IncrementalBuilder),
|
||||
Vectorize(Clusters),
|
||||
}
|
||||
|
||||
@@ -64,8 +67,10 @@ impl ColorImageConverter {
|
||||
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 image = self.canvas.get_image_data_as_color_image(0, 0, width, height);
|
||||
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,
|
||||
@@ -84,6 +89,35 @@ impl ColorImageConverter {
|
||||
},
|
||||
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,
|
||||
}, image);
|
||||
self.stage = Stage::Reclustering(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())
|
||||
}
|
||||
@@ -104,6 +138,7 @@ impl ColorImageConverter {
|
||||
self.svg.prepend_path(
|
||||
&paths,
|
||||
&cluster.residue_color(),
|
||||
Some(self.params.path_precision),
|
||||
);
|
||||
self.counter += 1;
|
||||
false
|
||||
@@ -123,6 +158,9 @@ impl ColorImageConverter {
|
||||
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
|
||||
}
|
||||
|
||||
@@ -13,11 +13,11 @@ impl Svg {
|
||||
Self { element }
|
||||
}
|
||||
|
||||
pub fn prepend_path(&mut self, paths: &CompoundPath, color: &Color) {
|
||||
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());
|
||||
let (string, offset) = paths.to_svg_string(true, PointF64::default(), precision);
|
||||
path.set_attribute("d", &string).unwrap();
|
||||
path.set_attribute(
|
||||
"transform",
|
||||
|
||||