Merge pull request #1448 from bungoboingo/fear/linear-gradients

Add linear gradient support to canvas widget
This commit is contained in:
Héctor Ramón 2022-11-03 18:57:09 +01:00 committed by GitHub
commit d222b5c8b0
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
47 changed files with 2234 additions and 971 deletions

View file

@ -2,7 +2,7 @@ use std::{f32::consts::PI, time::Instant};
use iced::executor;
use iced::widget::canvas::{
self, Cache, Canvas, Cursor, Geometry, Path, Stroke,
self, stroke, Cache, Canvas, Cursor, Geometry, Path, Stroke,
};
use iced::{
Application, Command, Element, Length, Point, Rectangle, Settings,
@ -52,11 +52,6 @@ impl Application for Arc {
Command::none()
}
fn subscription(&self) -> Subscription<Message> {
iced::time::every(std::time::Duration::from_millis(10))
.map(|_| Message::Tick)
}
fn view(&self) -> Element<Message> {
Canvas::new(self)
.width(Length::Fill)
@ -67,6 +62,11 @@ impl Application for Arc {
fn theme(&self) -> Theme {
Theme::Dark
}
fn subscription(&self) -> Subscription<Message> {
iced::time::every(std::time::Duration::from_millis(10))
.map(|_| Message::Tick)
}
}
impl<Message> canvas::Program<Message> for Arc {
@ -114,7 +114,7 @@ impl<Message> canvas::Program<Message> for Arc {
frame.stroke(
&path,
Stroke {
color: palette.text,
style: stroke::Style::Solid(palette.text),
width: 10.0,
..Stroke::default()
},

View file

@ -1,5 +1,7 @@
use iced::executor;
use iced::widget::canvas::{Cache, Cursor, Geometry, LineCap, Path, Stroke};
use iced::widget::canvas::{
stroke, Cache, Cursor, Geometry, LineCap, Path, Stroke,
};
use iced::widget::{canvas, container};
use iced::{
Application, Color, Command, Element, Length, Point, Rectangle, Settings,
@ -24,9 +26,9 @@ enum Message {
}
impl Application for Clock {
type Executor = executor::Default;
type Message = Message;
type Theme = Theme;
type Executor = executor::Default;
type Flags = ();
fn new(_flags: ()) -> (Self, Command<Message>) {
@ -59,15 +61,6 @@ impl Application for Clock {
Command::none()
}
fn subscription(&self) -> Subscription<Message> {
iced::time::every(std::time::Duration::from_millis(500)).map(|_| {
Message::Tick(
time::OffsetDateTime::now_local()
.unwrap_or_else(|_| time::OffsetDateTime::now_utc()),
)
})
}
fn view(&self) -> Element<Message> {
let canvas = canvas(self as &Self)
.width(Length::Fill)
@ -79,6 +72,15 @@ impl Application for Clock {
.padding(20)
.into()
}
fn subscription(&self) -> Subscription<Message> {
iced::time::every(std::time::Duration::from_millis(500)).map(|_| {
Message::Tick(
time::OffsetDateTime::now_local()
.unwrap_or_else(|_| time::OffsetDateTime::now_utc()),
)
})
}
}
impl<Message> canvas::Program<Message> for Clock {
@ -104,33 +106,41 @@ impl<Message> canvas::Program<Message> for Clock {
let long_hand =
Path::line(Point::ORIGIN, Point::new(0.0, -0.8 * radius));
let thin_stroke = Stroke {
width: radius / 100.0,
color: Color::WHITE,
line_cap: LineCap::Round,
..Stroke::default()
let width = radius / 100.0;
let thin_stroke = || -> Stroke {
Stroke {
width,
style: stroke::Style::Solid(Color::WHITE),
line_cap: LineCap::Round,
..Stroke::default()
}
};
let wide_stroke = Stroke {
width: thin_stroke.width * 3.0,
..thin_stroke
let wide_stroke = || -> Stroke {
Stroke {
width: width * 3.0,
style: stroke::Style::Solid(Color::WHITE),
line_cap: LineCap::Round,
..Stroke::default()
}
};
frame.translate(Vector::new(center.x, center.y));
frame.with_save(|frame| {
frame.rotate(hand_rotation(self.now.hour(), 12));
frame.stroke(&short_hand, wide_stroke);
frame.stroke(&short_hand, wide_stroke());
});
frame.with_save(|frame| {
frame.rotate(hand_rotation(self.now.minute(), 60));
frame.stroke(&long_hand, wide_stroke);
frame.stroke(&long_hand, wide_stroke());
});
frame.with_save(|frame| {
frame.rotate(hand_rotation(self.now.second(), 60));
frame.stroke(&long_hand, thin_stroke);
frame.stroke(&long_hand, thin_stroke());
})
});

View file

@ -1,11 +0,0 @@
[package]
name = "geometry"
version = "0.1.0"
authors = ["Héctor Ramón Jiménez <hector0193@gmail.com>"]
edition = "2021"
publish = false
[dependencies]
iced = { path = "../.." }
iced_native = { path = "../../native" }
iced_graphics = { path = "../../graphics" }

View file

@ -1,18 +0,0 @@
## Geometry
A custom widget showcasing how to draw geometry with the `Mesh2D` primitive in [`iced_wgpu`](../../wgpu).
The __[`main`]__ file contains all the code of the example.
<div align="center">
<a href="https://gfycat.com/activeunfitkangaroo">
<img src="https://thumbs.gfycat.com/ActiveUnfitKangaroo-small.gif">
</a>
</div>
You can run it with `cargo run`:
```
cargo run --package geometry
```
[`main`]: src/main.rs

View file

@ -1,217 +0,0 @@
//! This example showcases a simple native custom widget that renders using
//! arbitrary low-level geometry.
mod rainbow {
// For now, to implement a custom native widget you will need to add
// `iced_native` and `iced_wgpu` to your dependencies.
//
// Then, you simply need to define your widget type and implement the
// `iced_native::Widget` trait with the `iced_wgpu::Renderer`.
//
// Of course, you can choose to make the implementation renderer-agnostic,
// if you wish to, by creating your own `Renderer` trait, which could be
// implemented by `iced_wgpu` and other renderers.
use iced_graphics::renderer::{self, Renderer};
use iced_graphics::{Backend, Primitive};
use iced_native::widget::{self, Widget};
use iced_native::{
layout, Element, Layout, Length, Point, Rectangle, Size, Vector,
};
#[derive(Default)]
pub struct Rainbow;
impl Rainbow {
pub fn new() -> Self {
Self
}
}
pub fn rainbow() -> Rainbow {
Rainbow
}
impl<Message, B, T> Widget<Message, Renderer<B, T>> for Rainbow
where
B: Backend,
{
fn width(&self) -> Length {
Length::Fill
}
fn height(&self) -> Length {
Length::Shrink
}
fn layout(
&self,
_renderer: &Renderer<B, T>,
limits: &layout::Limits,
) -> layout::Node {
let size = limits.width(Length::Fill).resolve(Size::ZERO);
layout::Node::new(Size::new(size.width, size.width))
}
fn draw(
&self,
_tree: &widget::Tree,
renderer: &mut Renderer<B, T>,
_theme: &T,
_style: &renderer::Style,
layout: Layout<'_>,
cursor_position: Point,
_viewport: &Rectangle,
) {
use iced_graphics::triangle::{Mesh2D, Vertex2D};
use iced_native::Renderer as _;
let b = layout.bounds();
// R O Y G B I V
let color_r = [1.0, 0.0, 0.0, 1.0];
let color_o = [1.0, 0.5, 0.0, 1.0];
let color_y = [1.0, 1.0, 0.0, 1.0];
let color_g = [0.0, 1.0, 0.0, 1.0];
let color_gb = [0.0, 1.0, 0.5, 1.0];
let color_b = [0.0, 0.2, 1.0, 1.0];
let color_i = [0.5, 0.0, 1.0, 1.0];
let color_v = [0.75, 0.0, 0.5, 1.0];
let posn_center = {
if b.contains(cursor_position) {
[cursor_position.x - b.x, cursor_position.y - b.y]
} else {
[b.width / 2.0, b.height / 2.0]
}
};
let posn_tl = [0.0, 0.0];
let posn_t = [b.width / 2.0, 0.0];
let posn_tr = [b.width, 0.0];
let posn_r = [b.width, b.height / 2.0];
let posn_br = [b.width, b.height];
let posn_b = [(b.width / 2.0), b.height];
let posn_bl = [0.0, b.height];
let posn_l = [0.0, b.height / 2.0];
let mesh = Primitive::Mesh2D {
size: b.size(),
buffers: Mesh2D {
vertices: vec![
Vertex2D {
position: posn_center,
color: [1.0, 1.0, 1.0, 1.0],
},
Vertex2D {
position: posn_tl,
color: color_r,
},
Vertex2D {
position: posn_t,
color: color_o,
},
Vertex2D {
position: posn_tr,
color: color_y,
},
Vertex2D {
position: posn_r,
color: color_g,
},
Vertex2D {
position: posn_br,
color: color_gb,
},
Vertex2D {
position: posn_b,
color: color_b,
},
Vertex2D {
position: posn_bl,
color: color_i,
},
Vertex2D {
position: posn_l,
color: color_v,
},
],
indices: vec![
0, 1, 2, // TL
0, 2, 3, // T
0, 3, 4, // TR
0, 4, 5, // R
0, 5, 6, // BR
0, 6, 7, // B
0, 7, 8, // BL
0, 8, 1, // L
],
},
};
renderer.with_translation(Vector::new(b.x, b.y), |renderer| {
renderer.draw_primitive(mesh);
});
}
}
impl<'a, Message, B, T> From<Rainbow> for Element<'a, Message, Renderer<B, T>>
where
B: Backend,
{
fn from(rainbow: Rainbow) -> Self {
Self::new(rainbow)
}
}
}
use iced::widget::{column, container, scrollable};
use iced::{Alignment, Element, Length, Sandbox, Settings};
use rainbow::rainbow;
pub fn main() -> iced::Result {
Example::run(Settings::default())
}
struct Example;
impl Sandbox for Example {
type Message = ();
fn new() -> Self {
Example
}
fn title(&self) -> String {
String::from("Custom 2D geometry - Iced")
}
fn update(&mut self, _: ()) {}
fn view(&self) -> Element<()> {
let content = column![
rainbow(),
"In this example we draw a custom widget Rainbow, using \
the Mesh2D primitive. This primitive supplies a list of \
triangles, expressed as vertices and indices.",
"Move your cursor over it, and see the center vertex \
follow you!",
"Every Vertex2D defines its own color. You could use the \
Mesh2D primitive to render virtually any two-dimensional \
geometry for your widget.",
]
.padding(20)
.spacing(20)
.max_width(500)
.align_items(Alignment::Start);
let scrollable =
scrollable(container(content).width(Length::Fill).center_x());
container(scrollable)
.width(Length::Fill)
.height(Length::Fill)
.center_y()
.into()
}
}

View file

@ -0,0 +1,10 @@
[package]
name = "modern_art"
version = "0.1.0"
authors = ["Bingus <shankern@protonmail.com>"]
edition = "2021"
publish = false
[dependencies]
iced = { path = "../..", features = ["canvas", "tokio", "debug"] }
rand = "0.8.5"

View file

@ -0,0 +1,140 @@
use iced::widget::canvas::{
self, gradient::Location, gradient::Position, Cache, Canvas, Cursor, Frame,
Geometry, Gradient,
};
use iced::{
executor, Application, Color, Command, Element, Length, Point, Rectangle,
Renderer, Settings, Size, Theme,
};
use rand::{thread_rng, Rng};
fn main() -> iced::Result {
ModernArt::run(Settings {
antialiasing: true,
..Settings::default()
})
}
#[derive(Debug, Clone, Copy)]
enum Message {}
struct ModernArt {
cache: Cache,
}
impl Application for ModernArt {
type Executor = executor::Default;
type Message = Message;
type Theme = Theme;
type Flags = ();
fn new(_flags: Self::Flags) -> (Self, Command<Self::Message>) {
(
ModernArt {
cache: Default::default(),
},
Command::none(),
)
}
fn title(&self) -> String {
String::from("Modern Art")
}
fn update(&mut self, _message: Message) -> Command<Message> {
Command::none()
}
fn view(&self) -> Element<'_, Self::Message, Renderer<Self::Theme>> {
Canvas::new(self)
.width(Length::Fill)
.height(Length::Fill)
.into()
}
}
impl<Message> canvas::Program<Message> for ModernArt {
type State = ();
fn draw(
&self,
_state: &Self::State,
_theme: &Theme,
bounds: Rectangle,
_cursor: Cursor,
) -> Vec<Geometry> {
let geometry = self.cache.draw(bounds.size(), |frame| {
let num_squares = thread_rng().gen_range(0..1200);
let mut i = 0;
while i <= num_squares {
generate_box(frame, bounds.size());
i += 1;
}
});
vec![geometry]
}
}
fn random_direction() -> Location {
match thread_rng().gen_range(0..8) {
0 => Location::TopLeft,
1 => Location::Top,
2 => Location::TopRight,
3 => Location::Right,
4 => Location::BottomRight,
5 => Location::Bottom,
6 => Location::BottomLeft,
7 => Location::Left,
_ => Location::TopLeft,
}
}
fn generate_box(frame: &mut Frame, bounds: Size) -> bool {
let solid = rand::random::<bool>();
let random_color = || -> Color {
Color::from_rgb(
thread_rng().gen_range(0.0..1.0),
thread_rng().gen_range(0.0..1.0),
thread_rng().gen_range(0.0..1.0),
)
};
let gradient = |top_left: Point, size: Size| -> Gradient {
let mut builder = Gradient::linear(Position::Relative {
top_left,
size,
start: random_direction(),
end: random_direction(),
});
let stops = thread_rng().gen_range(1..15u32);
let mut i = 0;
while i <= stops {
builder = builder.add_stop(i as f32 / stops as f32, random_color());
i += 1;
}
builder.build().unwrap()
};
let top_left = Point::new(
thread_rng().gen_range(0.0..bounds.width),
thread_rng().gen_range(0.0..bounds.height),
);
let size = Size::new(
thread_rng().gen_range(50.0..200.0),
thread_rng().gen_range(50.0..200.0),
);
if solid {
frame.fill_rectangle(top_left, size, random_color());
} else {
frame.fill_rectangle(top_left, size, gradient(top_left, size));
};
solid
}

View file

@ -2,7 +2,8 @@
//! a circle around each fingertip. This only works on touch-enabled
//! computers like Microsoft Surface.
use iced::widget::canvas::event;
use iced::widget::canvas::{self, Canvas, Cursor, Geometry, Stroke};
use iced::widget::canvas::stroke::{self, Stroke};
use iced::widget::canvas::{self, Canvas, Cursor, Geometry};
use iced::{
executor, touch, window, Application, Color, Command, Element, Length,
Point, Rectangle, Settings, Subscription, Theme,
@ -186,7 +187,7 @@ impl canvas::Program<Message> for State {
frame.stroke(
&path,
Stroke {
color: Color::BLACK,
style: stroke::Style::Solid(Color::BLACK),
width: 3.0,
..Stroke::default()
},

View file

@ -11,7 +11,9 @@ use iced::executor;
use iced::theme::{self, Theme};
use iced::time;
use iced::widget::canvas;
use iced::widget::canvas::{Cursor, Path, Stroke};
use iced::widget::canvas::gradient::{self, Gradient};
use iced::widget::canvas::stroke::{self, Stroke};
use iced::widget::canvas::{Cursor, Path};
use iced::window;
use iced::{
Application, Color, Command, Element, Length, Point, Rectangle, Settings,
@ -37,9 +39,9 @@ enum Message {
}
impl Application for SolarSystem {
type Executor = executor::Default;
type Message = Message;
type Theme = Theme;
type Executor = executor::Default;
type Flags = ();
fn new(_flags: ()) -> (Self, Command<Message>) {
@ -65,10 +67,6 @@ impl Application for SolarSystem {
Command::none()
}
fn subscription(&self) -> Subscription<Message> {
time::every(std::time::Duration::from_millis(10)).map(Message::Tick)
}
fn view(&self) -> Element<Message> {
canvas(&self.state)
.width(Length::Fill)
@ -86,6 +84,10 @@ impl Application for SolarSystem {
text_color: Color::WHITE,
})
}
fn subscription(&self) -> Subscription<Message> {
time::every(time::Duration::from_millis(10)).map(Message::Tick)
}
}
#[derive(Debug)]
@ -178,8 +180,10 @@ impl<Message> canvas::Program<Message> for State {
frame.stroke(
&orbit,
Stroke {
style: stroke::Style::Solid(Color::from_rgba8(
0, 153, 255, 0.1,
)),
width: 1.0,
color: Color::from_rgba8(0, 153, 255, 0.1),
line_dash: canvas::LineDash {
offset: 0,
segments: &[3.0, 6.0],
@ -198,15 +202,18 @@ impl<Message> canvas::Program<Message> for State {
frame.translate(Vector::new(Self::ORBIT_RADIUS, 0.0));
let earth = Path::circle(Point::ORIGIN, Self::EARTH_RADIUS);
let shadow = Path::rectangle(
Point::new(0.0, -Self::EARTH_RADIUS),
Size::new(
Self::EARTH_RADIUS * 4.0,
Self::EARTH_RADIUS * 2.0,
),
);
frame.fill(&earth, Color::from_rgb8(0x6B, 0x93, 0xD6));
let earth_fill =
Gradient::linear(gradient::Position::Absolute {
start: Point::new(-Self::EARTH_RADIUS, 0.0),
end: Point::new(Self::EARTH_RADIUS, 0.0),
})
.add_stop(0.2, Color::from_rgb(0.15, 0.50, 1.0))
.add_stop(0.8, Color::from_rgb(0.0, 0.20, 0.47))
.build()
.expect("Build Earth fill gradient");
frame.fill(&earth, earth_fill);
frame.with_save(|frame| {
frame.rotate(rotation * 10.0);
@ -215,14 +222,6 @@ impl<Message> canvas::Program<Message> for State {
let moon = Path::circle(Point::ORIGIN, Self::MOON_RADIUS);
frame.fill(&moon, Color::WHITE);
});
frame.fill(
&shadow,
Color {
a: 0.7,
..Color::BLACK
},
);
});
});