Skip to content

Commit

Permalink
xilem_web: Add svgdraw example (#731)
Browse files Browse the repository at this point in the history
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](#715 (comment))
this implicitly also adds the `BezPath` as the example (for more manual
testing opportunities).
  • Loading branch information
Philipp-M authored Nov 9, 2024
1 parent 18a6805 commit eb36f1e
Show file tree
Hide file tree
Showing 7 changed files with 273 additions and 11 deletions.
10 changes: 10 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down
7 changes: 3 additions & 4 deletions xilem_web/src/pointer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -48,17 +49,15 @@ pub enum PointerMsg {
pub struct PointerDetails {
pub id: i32,
pub button: i16,
pub x: f64,
pub y: f64,
pub position: Point,
}

impl PointerDetails {
fn from_pointer_event(e: &PointerEvent) -> Self {
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),
}
}
}
Expand Down
16 changes: 16 additions & 0 deletions xilem_web/web_examples/svgdraw/Cargo.toml
Original file line number Diff line number Diff line change
@@ -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 = "../.." }
106 changes: 106 additions & 0 deletions xilem_web/web_examples/svgdraw/index.html
Original file line number Diff line number Diff line change
@@ -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>
131 changes: 131 additions & 0 deletions xilem_web/web_examples/svgdraw/src/main.rs
Original file line number Diff line number Diff line change
@@ -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();
}
13 changes: 6 additions & 7 deletions xilem_web/web_examples/svgtoy/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -28,25 +28,24 @@ struct AppState {
struct GrabState {
is_down: bool,
id: i32,
dx: f64,
dy: f64,
delta: Vec2,
}

impl GrabState {
fn handle(&mut self, x: &mut f64, y: &mut f64, p: &PointerMsg) {
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) => {
Expand Down

0 comments on commit eb36f1e

Please sign in to comment.