mirror of https://github.com/linebender/xilem
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:
parent
18a6805ddc
commit
eb36f1eed0
|
@ -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"
|
||||
|
|
|
@ -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]
|
||||
|
|
|
@ -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),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 = "../.." }
|
|
@ -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>
|
|
@ -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();
|
||||
}
|
|
@ -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) => {
|
||||
|
|
Loading…
Reference in New Issue