Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
[package]
name = "freedraw"
version = "0.0.0-development"
edition = "2021"
version = "2.0.0"
edition = "2024"
authors = ["Jorge Soares"]
description = "A Rust port of the perfect-freehand library for creating smooth, beautiful freehand lines"
license = "MIT"
Expand Down
8 changes: 4 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -121,11 +121,11 @@ let options = StrokeOptions {
streamline: Some(0.5), // How much to streamline the stroke
simulate_pressure: Some(true), // Whether to simulate pressure based on velocity
start: Some(TaperOptions { // Tapering options for the start of the line
taper: Some(TaperType::Bool(true)),
taper: Some(4.0),
..Default::default()
}),
end: Some(TaperOptions { // Tapering options for the end of the line
taper: Some(TaperType::Bool(true)),
taper: Some(4.0),
..Default::default()
}),
..Default::default()
Expand All @@ -148,7 +148,7 @@ The `start` and `end` options accept a `TaperOptions` struct:
| Property | Type | Default | Description |
| -------- | ----------------- | ------- | ---------------------------------------------------------------------------------------- |
| `cap` | boolean | true | Whether to draw a cap. |
| `taper` | TaperType | None | The distance to taper. Can be a numerical value or boolean. |
| `taper` | number | None | The distance to taper. |
| `easing` | EasingType | linear | An easing function for the tapering effect. |

## Input Points
Expand Down Expand Up @@ -185,4 +185,4 @@ Check out the examples directory for more usage examples:

[MIT License](LICENSE)

Original JavaScript library by [Steve Ruiz](https://twitter.com/steveruizok)
Original JavaScript library by [Steve Ruiz](https://twitter.com/steveruizok)
2 changes: 1 addition & 1 deletion demo/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
[package]
name = "freedraw-demo"
version = "0.1.0"
edition = "2021"
edition = "2024"

# Main binary (Yew app)
[[bin]]
Expand Down
107 changes: 35 additions & 72 deletions demo/src/app.rs
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
use yew::prelude::*;
use web_sys::{wasm_bindgen::JsCast, Element, HtmlElement, DomRect};
use web_sys::wasm_bindgen::JsCast;
use wasm_bindgen::closure::Closure;
use wasm_bindgen::JsValue;
use js_sys::Promise;
use freedraw::{get_stroke, get_svg_path_from_stroke, StrokeOptions, InputPoint, TaperOptions, TaperType};
use freedraw::{StrokeOptions, TaperOptions};

#[derive(Properties, PartialEq)]
pub struct SvgExampleProps {
Expand Down Expand Up @@ -48,7 +47,6 @@ impl StrokeStyle {
smoothing: Some(0.5),
streamline: Some(0.5),
simulate_pressure: Some(false),
closed: Some(false),
..Default::default()
},
false,
Expand All @@ -61,7 +59,6 @@ impl StrokeStyle {
smoothing: Some(0.5),
streamline: Some(0.5),
simulate_pressure: Some(false),
closed: Some(false),
..Default::default()
},
true,
Expand All @@ -75,14 +72,13 @@ impl StrokeStyle {
streamline: Some(0.5),
simulate_pressure: Some(false),
start: Some(TaperOptions {
taper: Some(TaperType::Bool(true)),
taper: Some(4.0),
..Default::default()
}),
end: Some(TaperOptions {
taper: Some(TaperType::Bool(true)),
taper: Some(4.0),
..Default::default()
}),
closed: Some(false),
..Default::default()
},
false,
Expand All @@ -96,14 +92,13 @@ impl StrokeStyle {
streamline: Some(0.5),
simulate_pressure: Some(false),
start: Some(TaperOptions {
taper: Some(TaperType::Bool(true)),
taper: Some(4.0),
..Default::default()
}),
end: Some(TaperOptions {
taper: Some(TaperType::Bool(true)),
taper: Some(4.0),
..Default::default()
}),
closed: Some(false),
..Default::default()
},
true,
Expand All @@ -116,7 +111,6 @@ impl StrokeStyle {
smoothing: Some(0.5),
streamline: Some(0.5),
simulate_pressure: Some(false),
closed: Some(false),
..Default::default()
},
false,
Expand All @@ -129,7 +123,6 @@ impl StrokeStyle {
smoothing: Some(0.5),
streamline: Some(0.5),
simulate_pressure: Some(false),
closed: Some(false),
..Default::default()
},
true,
Expand All @@ -142,7 +135,6 @@ impl StrokeStyle {
smoothing: Some(0.5),
streamline: Some(0.5),
simulate_pressure: Some(false),
closed: Some(false),
..Default::default()
},
false,
Expand All @@ -155,42 +147,29 @@ impl StrokeStyle {
smoothing: Some(0.5),
streamline: Some(0.5),
simulate_pressure: Some(false),
closed: Some(false),
..Default::default()
},
true,
false
)
}
}

fn to_str(&self) -> &'static str {
match self {
StrokeStyle::Default => "Default",
StrokeStyle::DefaultWithStroke => "Default with Stroke",
StrokeStyle::Tapered => "Tapered",
StrokeStyle::TaperedWithStroke => "Tapered with Stroke",
StrokeStyle::Thin => "Thin",
StrokeStyle::ThinWithStroke => "Thin with Stroke",
StrokeStyle::Thick => "Thick",
StrokeStyle::ThickWithStroke => "Thick with Stroke"
}
}
}

// Added new DrawingBoard component for interactive drawing
#[function_component(DrawingBoard)]
#[expect(deprecated)]
fn drawing_board() -> Html {
use web_sys::{HtmlCanvasElement, CanvasRenderingContext2d, window};
use freedraw::{get_stroke, get_svg_path_from_stroke, StrokeOptions, InputPoint};
use freedraw::{get_stroke, get_svg_path_from_stroke, InputPoint};

let canvas_ref = use_node_ref();
// Instead of a simple array of points, we'll track multiple strokes
let strokes = use_state(|| Vec::<Vec<(f64, f64)>>::new());
let current_stroke = use_state(|| Vec::<(f64, f64)>::new());
let strokes = use_state(Vec::<Vec<(f64, f64)>>::new);
let current_stroke = use_state(Vec::<(f64, f64)>::new);
let drawing = use_state(|| false);
let svg_paths = use_state(|| Vec::<(String, bool)>::new());
let copy_message = use_state(|| String::new());
let svg_paths = use_state(Vec::<(String, bool)>::new);
let copy_message = use_state(String::new);
let show_trace = use_state(|| false);
let stroke_style = use_state(|| StrokeStyle::Default);

Expand All @@ -217,7 +196,7 @@ fn drawing_board() -> Html {

// Process all completed strokes
for stroke_points in strokes.iter() {
if stroke_points.len() > 0 {
if !stroke_points.is_empty() {
let input_points: Vec<InputPoint> = stroke_points
.iter()
.map(|(x, y)| InputPoint::Array([*x, *y], None))
Expand All @@ -236,7 +215,7 @@ fn drawing_board() -> Html {
}

// Process the current stroke if it exists
if current_stroke.len() > 0 {
if !current_stroke.is_empty() {
let input_points: Vec<InputPoint> = current_stroke
.iter()
.map(|(x, y)| InputPoint::Array([*x, *y], None))
Expand Down Expand Up @@ -269,23 +248,18 @@ fn drawing_board() -> Html {
let (x, y) = get_canvas_coordinates(&canvas, e.client_x(), e.client_y());

// Start a new current stroke
let mut new_stroke = Vec::new();
new_stroke.push((x, y));
current_stroke.set(new_stroke);
current_stroke.set(vec![(x, y)]);

if *show_trace {
if let Ok(ctx_option) = canvas.get_context("2d") {
if let Some(ctx_js) = ctx_option {
if let Ok(ctx) = ctx_js.dyn_into::<CanvasRenderingContext2d>() {
if *show_trace
&& let Ok(ctx_option) = canvas.get_context("2d")
&& let Some(ctx_js) = ctx_option
&& let Ok(ctx) = ctx_js.dyn_into::<CanvasRenderingContext2d>() {
ctx.begin_path();
ctx.set_stroke_style(&JsValue::from_str("red"));
ctx.set_line_width(2.0);
ctx.move_to(x, y);
ctx.stroke();
}
}
}
}
}
})
};
Expand All @@ -297,8 +271,8 @@ fn drawing_board() -> Html {
let update_svg_paths = update_svg_paths.clone();
let show_trace = show_trace.clone();
Callback::from(move |e: web_sys::MouseEvent| {
if *drawing {
if let Some(canvas) = canvas_ref.cast::<HtmlCanvasElement>() {
if *drawing
&& let Some(canvas) = canvas_ref.cast::<HtmlCanvasElement>() {
let (x, y) = get_canvas_coordinates(&canvas, e.client_x(), e.client_y());

// Add to the current stroke
Expand All @@ -309,20 +283,16 @@ fn drawing_board() -> Html {
// Update SVG in real-time
update_svg_paths();

if *show_trace {
if let Ok(ctx_option) = canvas.get_context("2d") {
if let Some(ctx_js) = ctx_option {
if let Ok(ctx) = ctx_js.dyn_into::<CanvasRenderingContext2d>() {
if *show_trace
&& let Ok(ctx_option) = canvas.get_context("2d")
&& let Some(ctx_js) = ctx_option
&& let Ok(ctx) = ctx_js.dyn_into::<CanvasRenderingContext2d>() {
ctx.set_stroke_style(&JsValue::from_str("red"));
ctx.set_line_width(2.0);
ctx.line_to(x, y);
ctx.stroke();
}
}
}
}
}
}
})
};

Expand Down Expand Up @@ -358,15 +328,12 @@ fn drawing_board() -> Html {
svg_paths.set(Vec::new());

// Clear the canvas
if let Some(canvas) = canvas_ref.cast::<HtmlCanvasElement>() {
if let Ok(ctx_option) = canvas.get_context("2d") {
if let Some(ctx_js) = ctx_option {
if let Ok(ctx) = ctx_js.dyn_into::<CanvasRenderingContext2d>() {
if let Some(canvas) = canvas_ref.cast::<HtmlCanvasElement>()
&& let Ok(ctx_option) = canvas.get_context("2d")
&& let Some(ctx_js) = ctx_option
&& let Ok(ctx) = ctx_js.dyn_into::<CanvasRenderingContext2d>() {
ctx.clear_rect(0.0, 0.0, canvas.width() as f64, canvas.height() as f64);
}
}
}
}
})
};

Expand All @@ -379,17 +346,13 @@ fn drawing_board() -> Html {
show_trace.set(new_value);

// Clear the canvas when toggling off to hide existing traces
if !new_value {
if let Some(canvas) = canvas_ref.cast::<HtmlCanvasElement>() {
if let Ok(ctx_option) = canvas.get_context("2d") {
if let Some(ctx_js) = ctx_option {
if let Ok(ctx) = ctx_js.dyn_into::<CanvasRenderingContext2d>() {
if !new_value
&& let Some(canvas) = canvas_ref.cast::<HtmlCanvasElement>()
&& let Ok(ctx_option) = canvas.get_context("2d")
&& let Some(ctx_js) = ctx_option
&& let Ok(ctx) = ctx_js.dyn_into::<CanvasRenderingContext2d>() {
ctx.clear_rect(0.0, 0.0, canvas.width() as f64, canvas.height() as f64);
}
}
}
}
}
})
};

Expand Down Expand Up @@ -847,4 +810,4 @@ pub fn app() -> Html {
</div>
</div>
}
}
}
14 changes: 7 additions & 7 deletions demo/src/svg_conversion.rs
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
use freedraw::{get_stroke, get_svg_path_from_stroke, InputPoint, StrokeOptions, TaperOptions, TaperType};
use freedraw::{get_stroke, get_svg_path_from_stroke, InputPoint, StrokeOptions, TaperOptions};
use std::fs::{self, File};
use std::io::{Read, Write};
use std::path::Path;
use serde_json::Value;

fn load_test_data(filename: &str) -> serde_json::Value {
let mut file = File::open(format!("../tests/{}", filename)).expect(&format!("Could not open {}", filename));
let mut file = File::open(format!("../tests/{}", filename)).unwrap_or_else(|_| panic!("Could not open {}", filename));
let mut contents = String::new();
file.read_to_string(&mut contents).expect("Could not read file");
serde_json::from_str(&contents).expect("Could not parse JSON")
Expand Down Expand Up @@ -223,11 +223,11 @@ fn process_raw_points(name: &str, points: &[Value], color: &str) {
streamline: Some(streamline),
simulate_pressure: Some(simulate_pressure),
start: Some(TaperOptions {
taper: Some(TaperType::Bool(true)),
taper: Some(4.0),
..Default::default()
}),
end: Some(TaperOptions {
taper: Some(TaperType::Bool(true)),
taper: Some(4.0),
..Default::default()
}),
..Default::default()
Expand Down Expand Up @@ -311,11 +311,11 @@ pub fn main() {
streamline: Some(streamline),
simulate_pressure: Some(simulate_pressure),
start: Some(TaperOptions {
taper: Some(TaperType::Bool(true)),
taper: Some(4.0),
..Default::default()
}),
end: Some(TaperOptions {
taper: Some(TaperType::Bool(true)),
taper: Some(4.0),
..Default::default()
}),
..Default::default()
Expand Down Expand Up @@ -375,4 +375,4 @@ pub fn main() {
}

println!("\nAll SVG files generated in the examples/svg directory.");
}
}
13 changes: 8 additions & 5 deletions src/get_stroke.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,21 @@ use crate::get_stroke_outline_points::get_stroke_outline_points;
use crate::get_stroke_points::get_stroke_points;
use crate::types::{InputPoint, StrokeOptions};

/// Get an array of points describing a polygon that surrounds the input points.
/// Get an outline polygon that surrounds the input points.
///
/// This is a convenience wrapper around `get_stroke_points` and
/// `get_stroke_outline_points`.
///
/// # Arguments
/// * `points` - An array of points (with optional pressure data)
/// * `options` - Options for the stroke generation
/// * `points` - Input points with optional pressure data (0.0..=1.0).
/// * `options` - Stroke options controlling smoothing, pressure, and caps.
///
/// # Returns
/// An array of points (as `[x, y]`) that define the outline of the stroke
/// A list of `[x, y]` points in winding order forming the stroke outline.
pub fn get_stroke(
points: &[InputPoint],
options: &StrokeOptions,
) -> Vec<[f64; 2]> {
let stroke_points = get_stroke_points(points, options);
get_stroke_outline_points(&stroke_points, options)
}
}
Loading