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).
This commit is contained in:
Philipp Mildenberger 2024-11-09 13:00:39 +01:00 committed by GitHub
parent 18a6805ddc
commit eb36f1eed0
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 273 additions and 11 deletions

10
Cargo.lock generated
View File

@ -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"

View File

@ -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]

View File

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

View File

@ -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 = "../.." }

View File

@ -0,0 +1,106 @@
<!DOCTYPE html>
<html lang="en">
<head>
<title>
SvgDraw | Xilem Web
</title>
<style>
html,
body {
margin: 0;
padding: 0;
overflow: hidden;
touch-action: none;
}
.controls {
display: block;
left: 0;
right: 0;
margin-inline: auto;
width: fit-content;
height: 1.5rem;
padding-top: 0.25rem;
padding-bottom: 0.25rem;
}
.controls>span {
vertical-align: top;
}
.value-range {
display: inline-block;
position: relative;
height: 1.5rem;
width: 20rem;
}
.value-range::before,
.value-range::after {
display: block;
position: absolute;
z-index: 99;
color: #000;
width: 100%;
line-height: 1rem;
padding-top: 0.25rem;
padding-bottom: 0.25rem;
pointer-events: none;
}
.value-range::before {
text-align: left;
/* hardcoded values aren't optimal here, but this example is not about styling/layouting */
content: "1";
}
.value-range::after {
text-align: right;
content: "30";
}
input[type=range] {
-webkit-appearance: none;
background-color: rgba(255, 255, 255, 0.2);
position: absolute;
top: 50%;
left: 50%;
margin: 0;
padding: 0;
width: 20rem;
height: 1.5rem;
transform: translate(-50%, -50%);
border-radius: 0.5rem;
overflow: hidden;
cursor: col-resize;
}
input[type=range][step] {
background-color: rgba(0, 0, 0, 0.2);
}
input[type=range]::-webkit-slider-thumb {
-webkit-appearance: none;
width: 0;
box-shadow: -20rem 0 0 20rem rgba(0, 0, 0, 0.2);
}
input[type=range]::-moz-range-thumb {
border: none;
width: 0;
box-shadow: -20rem 0 0 20rem rgba(0, 0, 0, 0.2);
}
label {
line-height: 1.5rem;
position: absolute;
}
</style>
</head>
<body>
</body>
</html>

View File

@ -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<Point>,
color: Color,
width: f64,
}
impl SplineLine {
fn new(p: Point, color: Color, width: f64) -> Self {
Self {
points: vec![p],
color,
width,
}
}
fn view<State: 'static>(&self) -> impl SvgPathElement<State> {
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<SplineLine>,
new_line_width: f64,
is_drawing: bool,
}
impl Draw {
fn view(&mut self) -> impl DomFragment<Self> {
let lines = self.lines.iter().map(SplineLine::view).collect::<Vec<_>>();
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();
}

View File

@ -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) => {