Skip to content

Commit

Permalink
Merge pull request #2055 from matze/perceptual-gradient
Browse files Browse the repository at this point in the history
Compute gradients in Oklab color space
  • Loading branch information
hecrj authored Sep 8, 2023
2 parents b5e7fb2 + 90cbab1 commit 89d9c45
Show file tree
Hide file tree
Showing 17 changed files with 705 additions and 485 deletions.
1 change: 1 addition & 0 deletions clippy.toml
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
too-many-arguments-threshold = 20
enum-variant-name-threshold = 10
1 change: 1 addition & 0 deletions core/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ bitflags.workspace = true
log.workspace = true
thiserror.workspace = true
twox-hash.workspace = true
num-traits.workspace = true

palette.workspace = true
palette.optional = true
Expand Down
60 changes: 50 additions & 10 deletions core/src/angle.rs
Original file line number Diff line number Diff line change
@@ -1,32 +1,72 @@
use crate::{Point, Rectangle, Vector};
use std::f32::consts::PI;

#[derive(Debug, Copy, Clone, PartialEq)]
use std::f32::consts::{FRAC_PI_2, PI};
use std::ops::RangeInclusive;

/// Degrees
#[derive(Debug, Copy, Clone, PartialEq, PartialOrd)]
pub struct Degrees(pub f32);

#[derive(Debug, Copy, Clone, PartialEq)]
/// Radians
#[derive(Debug, Copy, Clone, PartialEq, PartialOrd)]
pub struct Radians(pub f32);

impl Radians {
/// The range of radians of a circle.
pub const RANGE: RangeInclusive<Radians> = Radians(0.0)..=Radians(2.0 * PI);
}

impl From<Degrees> for Radians {
fn from(degrees: Degrees) -> Self {
Radians(degrees.0 * PI / 180.0)
Self(degrees.0 * PI / 180.0)
}
}

impl From<f32> for Radians {
fn from(radians: f32) -> Self {
Self(radians)
}
}

impl From<u8> for Radians {
fn from(radians: u8) -> Self {
Self(f32::from(radians))
}
}

impl From<Radians> for f64 {
fn from(radians: Radians) -> Self {
Self::from(radians.0)
}
}

impl num_traits::FromPrimitive for Radians {
fn from_i64(n: i64) -> Option<Self> {
Some(Self(n as f32))
}

fn from_u64(n: u64) -> Option<Self> {
Some(Self(n as f32))
}

fn from_f64(n: f64) -> Option<Self> {
Some(Self(n as f32))
}
}

impl Radians {
/// Calculates the line in which the [`Angle`] intercepts the `bounds`.
pub fn to_distance(&self, bounds: &Rectangle) -> (Point, Point) {
let v1 = Vector::new(f32::cos(self.0), f32::sin(self.0));
let angle = self.0 - FRAC_PI_2;
let r = Vector::new(f32::cos(angle), f32::sin(angle));

let distance_to_rect = f32::min(
f32::abs((bounds.y - bounds.center().y) / v1.y),
f32::abs(((bounds.x + bounds.width) - bounds.center().x) / v1.x),
let distance_to_rect = f32::max(
f32::abs(r.x * bounds.width / 2.0),
f32::abs(r.y * bounds.height / 2.0),
);

let start = bounds.center() + v1 * distance_to_rect;
let end = bounds.center() - v1 * distance_to_rect;
let start = bounds.center() - r * distance_to_rect;
let end = bounds.center() + r * distance_to_rect;

(start, end)
}
Expand Down
8 changes: 8 additions & 0 deletions examples/gradient/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
[package]
name = "gradient"
version = "0.1.0"
edition = "2021"
publish = false

[dependencies]
iced = { path = "../.." }
99 changes: 99 additions & 0 deletions examples/gradient/src/main.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
use iced::gradient;
use iced::widget::{column, container, horizontal_space, row, slider, text};
use iced::{
Alignment, Background, Color, Element, Length, Radians, Sandbox, Settings,
};

pub fn main() -> iced::Result {
Gradient::run(Settings::default())
}

#[derive(Debug, Clone, Copy)]
struct Gradient {
start: Color,
end: Color,
angle: Radians,
}

#[derive(Debug, Clone, Copy)]
enum Message {
StartChanged(Color),
EndChanged(Color),
AngleChanged(Radians),
}

impl Sandbox for Gradient {
type Message = Message;

fn new() -> Self {
Self {
start: Color::WHITE,
end: Color::new(0.0, 0.0, 1.0, 1.0),
angle: Radians(0.0),
}
}

fn title(&self) -> String {
String::from("Gradient")
}

fn update(&mut self, message: Message) {
match message {
Message::StartChanged(color) => self.start = color,
Message::EndChanged(color) => self.end = color,
Message::AngleChanged(angle) => self.angle = angle,
}
}

fn view(&self) -> Element<Message> {
let Self { start, end, angle } = *self;

let gradient_box = container(horizontal_space(Length::Fill))
.width(Length::Fill)
.height(Length::Fill)
.style(move |_: &_| {
let gradient = gradient::Linear::new(angle)
.add_stop(0.0, start)
.add_stop(1.0, end)
.into();

container::Appearance {
background: Some(Background::Gradient(gradient)),
..Default::default()
}
});

let angle_picker = row![
text("Angle").width(64),
slider(Radians::RANGE, self.angle, Message::AngleChanged)
.step(0.01)
]
.spacing(8)
.padding(8)
.align_items(Alignment::Center);

column![
color_picker("Start", self.start).map(Message::StartChanged),
color_picker("End", self.end).map(Message::EndChanged),
angle_picker,
gradient_box
]
.into()
}
}

fn color_picker(label: &str, color: Color) -> Element<'_, Color> {
row![
text(label).width(64),
slider(0.0..=1.0, color.r, move |r| { Color { r, ..color } })
.step(0.01),
slider(0.0..=1.0, color.g, move |g| { Color { g, ..color } })
.step(0.01),
slider(0.0..=1.0, color.b, move |b| { Color { b, ..color } })
.step(0.01),
]
.spacing(8)
.padding(8)
.align_items(Alignment::Center)
.into()
}
19 changes: 18 additions & 1 deletion wgpu/src/quad/gradient.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
use crate::graphics::color;
use crate::graphics::gradient;
use crate::quad::{self, Quad};
use crate::Buffer;
Expand Down Expand Up @@ -78,7 +79,23 @@ impl Pipeline {
device.create_shader_module(wgpu::ShaderModuleDescriptor {
label: Some("iced_wgpu.quad.gradient.shader"),
source: wgpu::ShaderSource::Wgsl(std::borrow::Cow::Borrowed(
include_str!("../shader/quad.wgsl"),
if color::GAMMA_CORRECTION {
concat!(
include_str!("../shader/quad.wgsl"),
"\n",
include_str!("../shader/quad/gradient.wgsl"),
"\n",
include_str!("../shader/color/oklab.wgsl")
)
} else {
concat!(
include_str!("../shader/quad.wgsl"),
"\n",
include_str!("../shader/quad/gradient.wgsl"),
"\n",
include_str!("../shader/color/linear_rgb.wgsl")
)
},
)),
});

Expand Down
6 changes: 5 additions & 1 deletion wgpu/src/quad/solid.rs
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,11 @@ impl Pipeline {
device.create_shader_module(wgpu::ShaderModuleDescriptor {
label: Some("iced_wgpu.quad.solid.shader"),
source: wgpu::ShaderSource::Wgsl(std::borrow::Cow::Borrowed(
include_str!("../shader/quad.wgsl"),
concat!(
include_str!("../shader/quad.wgsl"),
"\n",
include_str!("../shader/quad/solid.wgsl"),
),
)),
});

Expand Down
3 changes: 3 additions & 0 deletions wgpu/src/shader/color/linear_rgb.wgsl
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
fn interpolate_color(from_: vec4<f32>, to_: vec4<f32>, factor: f32) -> vec4<f32> {
return mix(from_, to_, factor);
}
26 changes: 26 additions & 0 deletions wgpu/src/shader/color/oklab.wgsl
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
const to_lms = mat3x4<f32>(
vec4<f32>(0.4121656120, 0.2118591070, 0.0883097947, 0.0),
vec4<f32>(0.5362752080, 0.6807189584, 0.2818474174, 0.0),
vec4<f32>(0.0514575653, 0.1074065790, 0.6302613616, 0.0),
);

const to_rgb = mat3x4<f32>(
vec4<f32>( 4.0767245293, -3.3072168827, 0.2307590544, 0.0),
vec4<f32>(-1.2681437731, 2.6093323231, -0.3411344290, 0.0),
vec4<f32>(-0.0041119885, -0.7034763098, 1.7068625689, 0.0),
);

fn interpolate_color(from_: vec4<f32>, to_: vec4<f32>, factor: f32) -> vec4<f32> {
// To Oklab
let lms_a = pow(from_ * to_lms, vec3<f32>(1.0 / 3.0, 1.0 / 3.0, 1.0 / 3.0));
let lms_b = pow(to_ * to_lms, vec3<f32>(1.0 / 3.0, 1.0 / 3.0, 1.0 / 3.0));
let mixed = mix(lms_a, lms_b, factor);

// Back to linear RGB
var color = to_rgb * (mixed * mixed * mixed);

// Alpha interpolation
color.a = mix(from_.a, to_.a, factor);

return color;
}
Loading

0 comments on commit 89d9c45

Please sign in to comment.