From eb36f1eed0aa2b1962b38d918043f7b716e5aa23 Mon Sep 17 00:00:00 2001 From: Philipp Mildenberger Date: Sat, 9 Nov 2024 13:00:39 +0100 Subject: [PATCH] xilem_web: Add `svgdraw` example (#731) Add a simple example showing, that svg nodes can also be used similarly as a `CanvasRenderingContext2D` to draw some lines. This takes advantage of a `kurbo::QuadSpline` to avoid sharp edges when the pointer moves fast. See [this](https://xi.zulipchat.com/#narrow/channel/354396-xilem/topic/web.3A.20Canvas.20options.20set.20with.20.60after_build.60.20doesn't.20persist) zulip topic for more context. This could potentially be optimized further (don't clone/recalculate all the lines every reconciliation), but I think in its current state it's also a good test to see how a naive implementation performs, and so far it's not too bad. Btw. as noted [here](https://github.com/linebender/xilem/pull/715#issuecomment-2438408323) this implicitly also adds the `BezPath` as the example (for more manual testing opportunities). --- Cargo.lock | 10 ++ Cargo.toml | 1 + xilem_web/src/pointer.rs | 7 +- xilem_web/web_examples/svgdraw/Cargo.toml | 16 +++ xilem_web/web_examples/svgdraw/index.html | 106 +++++++++++++++++ xilem_web/web_examples/svgdraw/src/main.rs | 131 +++++++++++++++++++++ xilem_web/web_examples/svgtoy/src/main.rs | 13 +- 7 files changed, 273 insertions(+), 11 deletions(-) create mode 100644 xilem_web/web_examples/svgdraw/Cargo.toml create mode 100644 xilem_web/web_examples/svgdraw/index.html create mode 100644 xilem_web/web_examples/svgdraw/src/main.rs diff --git a/Cargo.lock b/Cargo.lock index 153221106..9e07bceb6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3460,6 +3460,16 @@ version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "20e16a0f46cf5fd675563ef54f26e83e20f2366bcf027bcb3cc3ed2b98aaf2ca" +[[package]] +name = "svgdraw" +version = "0.1.0" +dependencies = [ + "console_error_panic_hook", + "wasm-bindgen", + "web-sys", + "xilem_web", +] + [[package]] name = "svgtoy" version = "0.1.0" diff --git a/Cargo.toml b/Cargo.toml index d03900522..25b02b9a5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -15,6 +15,7 @@ members = [ "xilem_web/web_examples/raw_dom_access", "xilem_web/web_examples/spawn_tasks", "xilem_web/web_examples/svgtoy", + "xilem_web/web_examples/svgdraw", ] [workspace.package] diff --git a/xilem_web/src/pointer.rs b/xilem_web/src/pointer.rs index f4d1d493b..8975d4510 100644 --- a/xilem_web/src/pointer.rs +++ b/xilem_web/src/pointer.rs @@ -8,6 +8,7 @@ use crate::{ interfaces::Element, DomView, DynMessage, ViewCtx, }; +use peniko::kurbo::Point; use std::marker::PhantomData; use wasm_bindgen::{prelude::Closure, throw_str, JsCast, UnwrapThrowExt}; use web_sys::PointerEvent; @@ -48,8 +49,7 @@ pub enum PointerMsg { pub struct PointerDetails { pub id: i32, pub button: i16, - pub x: f64, - pub y: f64, + pub position: Point, } impl PointerDetails { @@ -57,8 +57,7 @@ impl PointerDetails { PointerDetails { id: e.pointer_id(), button: e.button(), - x: e.client_x() as f64, - y: e.client_y() as f64, + position: Point::new(e.client_x() as f64, e.client_y() as f64), } } } diff --git a/xilem_web/web_examples/svgdraw/Cargo.toml b/xilem_web/web_examples/svgdraw/Cargo.toml new file mode 100644 index 000000000..ec83c941f --- /dev/null +++ b/xilem_web/web_examples/svgdraw/Cargo.toml @@ -0,0 +1,16 @@ +[package] +name = "svgdraw" +version = "0.1.0" +publish = false +license.workspace = true +edition.workspace = true +rust-version.workspace = true + +[lints] +workspace = true + +[dependencies] +console_error_panic_hook = "0.1" +wasm-bindgen = "0.2.92" +web-sys = "0.3.69" +xilem_web = { path = "../.." } diff --git a/xilem_web/web_examples/svgdraw/index.html b/xilem_web/web_examples/svgdraw/index.html new file mode 100644 index 000000000..0ab6e9445 --- /dev/null +++ b/xilem_web/web_examples/svgdraw/index.html @@ -0,0 +1,106 @@ + + + + + + SvgDraw | Xilem Web + + + + + + + + \ No newline at end of file diff --git a/xilem_web/web_examples/svgdraw/src/main.rs b/xilem_web/web_examples/svgdraw/src/main.rs new file mode 100644 index 000000000..2c23f4c4a --- /dev/null +++ b/xilem_web/web_examples/svgdraw/src/main.rs @@ -0,0 +1,131 @@ +// Copyright 2023 the Xilem Authors +// SPDX-License-Identifier: Apache-2.0 + +//! An example showing how SVG paths can be used for a vector-drawing application + +use wasm_bindgen::UnwrapThrowExt; +use xilem_web::{ + document_body, + elements::{ + html::{div, input, label, span}, + svg::{g, svg}, + }, + input_event_target_value, + interfaces::{Element, SvgGeometryElement, SvgPathElement, SvggElement}, + modifiers::style as s, + svg::{ + kurbo::{BezPath, Point, QuadSpline, Shape, Stroke}, + peniko::Color, + }, + App, DomFragment, PointerMsg, +}; + +const RAINBOW_COLORS: &[Color] = &[ + Color::rgb8(228, 3, 3), // Red + Color::rgb8(255, 140, 0), // Orange + Color::rgb8(255, 237, 0), // Yellow + Color::rgb8(0, 128, 38), // Green + Color::rgb8(0, 76, 255), // Indigo + Color::rgb8(115, 41, 130), // Violet + Color::rgb8(214, 2, 112), // Pink + Color::rgb8(155, 79, 150), // Lavender + Color::rgb8(0, 56, 168), // Blue + Color::rgb8(91, 206, 250), // Light Blue + Color::rgb8(245, 169, 184), // Pink +]; + +fn random_color() -> Color { + #![allow( + clippy::cast_possible_truncation, + reason = "This will never happen here" + )] + RAINBOW_COLORS[(web_sys::js_sys::Math::random() * 1000000.0) as usize % RAINBOW_COLORS.len()] +} + +struct SplineLine { + points: Vec, + color: Color, + width: f64, +} + +impl SplineLine { + fn new(p: Point, color: Color, width: f64) -> Self { + Self { + points: vec![p], + color, + width, + } + } + + fn view(&self) -> impl SvgPathElement { + QuadSpline::new(self.points.clone()) + .to_quads() + .fold(BezPath::new(), |mut b, q| { + b.extend(q.path_elements(0.0)); + b + }) + .stroke(self.color, Stroke::new(self.width)) + } +} + +#[derive(Default)] +struct Draw { + lines: Vec, + new_line_width: f64, + is_drawing: bool, +} + +impl Draw { + fn view(&mut self) -> impl DomFragment { + let lines = self.lines.iter().map(SplineLine::view).collect::>(); + let canvas = svg(g(lines).fill(Color::TRANSPARENT)) + .pointer(|state: &mut Self, e| { + match e { + PointerMsg::Down(p) => { + let l = SplineLine::new(p.position, random_color(), state.new_line_width); + state.lines.push(l); + state.is_drawing = true; + } + PointerMsg::Move(p) => { + if state.is_drawing { + state.lines.last_mut().unwrap().points.push(p.position); + } + } + PointerMsg::Up(_) => state.is_drawing = false, + }; + }) + .style([s("width", "100vw"), s("height", "100vh")]); + + let controls = label(( + span("Stroke width:"), + div(input(()) + .attr("type", "range") + .attr("min", 1) + .attr("max", 30) + .attr("step", 0.01) + .attr("value", self.new_line_width) + .on_input(|state: &mut Self, event| { + state.new_line_width = input_event_target_value(&event) + .unwrap_throw() + .parse() + .unwrap_throw(); + })) + .class("value-range"), + )) + .class("controls"); + (controls, canvas) + } +} + +fn main() { + console_error_panic_hook::set_once(); + App::new( + document_body(), + Draw { + new_line_width: 5.0, + ..Draw::default() + }, + Draw::view, + ) + .run(); +} diff --git a/xilem_web/web_examples/svgtoy/src/main.rs b/xilem_web/web_examples/svgtoy/src/main.rs index f430ae746..628b30df0 100644 --- a/xilem_web/web_examples/svgtoy/src/main.rs +++ b/xilem_web/web_examples/svgtoy/src/main.rs @@ -11,7 +11,7 @@ use xilem_web::{ interfaces::*, modifiers::style as s, svg::{ - kurbo::{Circle, Line, Rect, Stroke}, + kurbo::{Circle, Line, Rect, Stroke, Vec2}, peniko::Color, }, App, DomView, PointerMsg, @@ -28,8 +28,7 @@ struct AppState { struct GrabState { is_down: bool, id: i32, - dx: f64, - dy: f64, + delta: Vec2, } impl GrabState { @@ -37,16 +36,16 @@ impl GrabState { match p { PointerMsg::Down(e) => { if e.button == 0 { - self.dx = *x - e.x; - self.dy = *y - e.y; + self.delta.x = *x - e.position.x; + self.delta.y = *y - e.position.y; self.id = e.id; self.is_down = true; } } PointerMsg::Move(e) => { if self.is_down && self.id == e.id { - *x = self.dx + e.x; - *y = self.dy + e.y; + *x = self.delta.x + e.position.x; + *y = self.delta.y + e.position.y; } } PointerMsg::Up(e) => {