mirror of
https://github.com/GraphiteEditor/Graphite.git
synced 2025-08-03 13:02:20 +00:00
Refactor the node macro and simply most of the node implementations (#1942)
* Add support structure for new node macro to gcore * Fix compile issues and code generation * Implement new node_fn macro * Implement property translation * Fix NodeIO type generation * Start translating math nodes * Move node implementation to outer scope to allow usage of local imports * Add expose attribute to allow controlling the parameter exposure * Add rust analyzer support for #[implementations] attribute * Migrate logic nodes * Handle where clause properly * Implement argument ident pattern preservation * Implement adjustment layer mapping * Fix node registry types * Fix module paths * Improve demo artwork comptibility * Improve macro error reporting * Fix handling of impl node implementations * Fix nodeio type computation * Fix opacity node and graph type resolution * Fix loading of demo artworks * Fix eslint * Fix typo in macro test * Remove node definitions for Adjustment Nodes * Fix type alias property generation and make adjustments footprint aware * Convert vector nodes * Implement path overrides * Fix stroke node * Fix painted dreams * Implement experimental type level specialization * Fix poisson disk sampling -> all demo artworks should work again * Port text node + make node macro more robust by implementing lifetime substitution * Fix vector node tests * Fix red dress demo + ci * Fix clippy warnings * Code review * Fix primary input issues * Improve math nodes and audit others * Set no_properties when no automatic properties are derived * Port vector generator nodes (could not derive all definitions yet) * Various QA changes and add min/max/mode_range to number parameters * Add min and max for f64 and u32 * Convert gpu nodes and clean up unused nodes * Partially port transform node * Allow implementations on call arg * Port path modify node * Start porting graphic element nodes * Transform nodes in graphic_element.rs * Port brush node * Port nodes in wasm_executior * Rename node macro * Fix formatting * Fix Mandelbrot node * Formatting * Fix Load Image and Load Resource nodes, add scope input to node macro * Remove unnecessary underscores * Begin attemping to make nodes resolution-aware * Infer a generic manual compositon type on generic call arg * Various fixes and work towards merging * Final changes for merge! * Fix tests, probably * More free line removals! --------- Co-authored-by: Keavon Chambers <keavon@keavon.com>
This commit is contained in:
parent
ca0d102296
commit
e352c7fa71
92 changed files with 4255 additions and 7275 deletions
|
@ -1,6 +1,6 @@
|
|||
use core::{fmt::Debug, marker::PhantomData};
|
||||
use core::fmt::Debug;
|
||||
|
||||
use crate::Node;
|
||||
use crate::{registry::types::Percentage, transform::Footprint};
|
||||
|
||||
use bytemuck::{Pod, Zeroable};
|
||||
use glam::DVec2;
|
||||
|
@ -282,422 +282,50 @@ impl<'i, T: BitmapMut + Bitmap> BitmapMut for &'i mut T {
|
|||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
pub struct MapNode<MapFn> {
|
||||
map_fn: MapFn,
|
||||
}
|
||||
|
||||
#[node_macro::node_fn(MapNode)]
|
||||
fn map_node<_Iter: Iterator, MapFnNode>(input: _Iter, map_fn: &'input MapFnNode) -> MapFnIterator<'input, _Iter, MapFnNode>
|
||||
where
|
||||
MapFnNode: for<'any_input> Node<'any_input, _Iter::Item>,
|
||||
{
|
||||
MapFnIterator::new(input, map_fn)
|
||||
}
|
||||
|
||||
#[must_use = "iterators are lazy and do nothing unless consumed"]
|
||||
pub struct MapFnIterator<'i, Iter, MapFn> {
|
||||
iter: Iter,
|
||||
map_fn: &'i MapFn,
|
||||
}
|
||||
|
||||
impl<'i, Iter: Debug, MapFn> Debug for MapFnIterator<'i, Iter, MapFn> {
|
||||
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
|
||||
f.debug_struct("MapFnIterator").field("iter", &self.iter).field("map_fn", &"MapFn").finish()
|
||||
}
|
||||
}
|
||||
|
||||
impl<'i, Iter: Clone, MapFn> Clone for MapFnIterator<'i, Iter, MapFn> {
|
||||
fn clone(&self) -> Self {
|
||||
Self {
|
||||
iter: self.iter.clone(),
|
||||
map_fn: self.map_fn,
|
||||
}
|
||||
}
|
||||
}
|
||||
impl<'i, Iter: Copy, MapFn> Copy for MapFnIterator<'i, Iter, MapFn> {}
|
||||
|
||||
impl<'i, Iter, MapFn> MapFnIterator<'i, Iter, MapFn> {
|
||||
pub fn new(iter: Iter, map_fn: &'i MapFn) -> Self {
|
||||
Self { iter, map_fn }
|
||||
}
|
||||
}
|
||||
|
||||
impl<'i, I: Iterator + 'i, F> Iterator for MapFnIterator<'i, I, F>
|
||||
where
|
||||
F: Node<'i, I::Item> + 'i,
|
||||
Self: 'i,
|
||||
{
|
||||
type Item = F::Output;
|
||||
|
||||
#[inline]
|
||||
fn next(&mut self) -> Option<F::Output> {
|
||||
self.iter.next().map(|x| self.map_fn.eval(x))
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn size_hint(&self) -> (usize, Option<usize>) {
|
||||
self.iter.size_hint()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub struct WeightedAvgNode {}
|
||||
|
||||
#[node_macro::node_fn(WeightedAvgNode)]
|
||||
fn weighted_avg_node<_Iter: Iterator<Item = (Color, f32)>>(input: _Iter) -> Color
|
||||
where
|
||||
_Iter: Clone,
|
||||
{
|
||||
let total_weight: f32 = input.clone().map(|(_, weight)| weight).sum();
|
||||
let total_r: f32 = input.clone().map(|(color, weight)| color.r() * weight).sum();
|
||||
let total_g: f32 = input.clone().map(|(color, weight)| color.g() * weight).sum();
|
||||
let total_b: f32 = input.clone().map(|(color, weight)| color.b() * weight).sum();
|
||||
let total_a: f32 = input.map(|(color, weight)| color.a() * weight).sum();
|
||||
Color::from_rgbaf32_unchecked(total_r / total_weight, total_g / total_weight, total_b / total_weight, total_a / total_weight)
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct GaussianNode<Sigma> {
|
||||
sigma: Sigma,
|
||||
}
|
||||
#[node_macro::node_fn(GaussianNode)]
|
||||
fn gaussian_node(input: f32, sigma: f64) -> f32 {
|
||||
let sigma = sigma as f32;
|
||||
(1.0 / (2.0 * core::f32::consts::PI * sigma * sigma).sqrt()) * (-input * input / (2.0 * sigma * sigma)).exp()
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub struct DistanceNode;
|
||||
|
||||
#[node_macro::node_fn(DistanceNode)]
|
||||
fn distance_node(input: (i32, i32)) -> f32 {
|
||||
let (x, y) = input;
|
||||
((x * x + y * y) as f32).sqrt()
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub struct ImageIndexIterNode<P> {
|
||||
_p: core::marker::PhantomData<P>,
|
||||
}
|
||||
|
||||
#[node_macro::node_fn(ImageIndexIterNode<_P>)]
|
||||
fn image_index_iter_node<_P>(input: ImageSlice<'input, _P>) -> core::ops::Range<u32> {
|
||||
0..(input.width * input.height)
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct WindowNode<P, Radius: for<'i> Node<'i, (), Output = u32>, Image: for<'i> Node<'i, (), Output = ImageSlice<'i, P>>> {
|
||||
radius: Radius,
|
||||
image: Image,
|
||||
_pixel: core::marker::PhantomData<P>,
|
||||
}
|
||||
|
||||
impl<'input, P: 'input, S0: 'input, S1: 'input> Node<'input, u32> for WindowNode<P, S0, S1>
|
||||
where
|
||||
S0: for<'any_input> Node<'any_input, (), Output = u32>,
|
||||
S1: for<'any_input> Node<'any_input, (), Output = ImageSlice<'any_input, P>>,
|
||||
{
|
||||
type Output = ImageWindowIterator<'input, P>;
|
||||
#[inline]
|
||||
fn eval(&'input self, input: u32) -> Self::Output {
|
||||
let radius = self.radius.eval(());
|
||||
let image = self.image.eval(());
|
||||
{
|
||||
let iter = ImageWindowIterator::new(image, radius, input);
|
||||
iter
|
||||
}
|
||||
}
|
||||
}
|
||||
impl<P, S0, S1> WindowNode<P, S0, S1>
|
||||
where
|
||||
S0: for<'any_input> Node<'any_input, (), Output = u32>,
|
||||
S1: for<'any_input> Node<'any_input, (), Output = ImageSlice<'any_input, P>>,
|
||||
{
|
||||
pub const fn new(radius: S0, image: S1) -> Self {
|
||||
Self {
|
||||
radius,
|
||||
image,
|
||||
_pixel: core::marker::PhantomData,
|
||||
}
|
||||
}
|
||||
}
|
||||
/*
|
||||
#[node_macro::node_fn(WindowNode)]
|
||||
fn window_node(input: u32, radius: u32, image: ImageSlice<'input>) -> ImageWindowIterator<'input> {
|
||||
let iter = ImageWindowIterator::new(image, radius, input);
|
||||
iter
|
||||
}*/
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub struct ImageWindowIterator<'a, P> {
|
||||
image: ImageSlice<'a, P>,
|
||||
radius: u32,
|
||||
index: u32,
|
||||
x: u32,
|
||||
y: u32,
|
||||
}
|
||||
|
||||
impl<'a, P> ImageWindowIterator<'a, P> {
|
||||
fn new(image: ImageSlice<'a, P>, radius: u32, index: u32) -> Self {
|
||||
let start_x = index as i32 % image.width as i32;
|
||||
let start_y = index as i32 / image.width as i32;
|
||||
let min_x = (start_x - radius as i32).max(0) as u32;
|
||||
let min_y = (start_y - radius as i32).max(0) as u32;
|
||||
|
||||
Self {
|
||||
image,
|
||||
radius,
|
||||
index,
|
||||
x: min_x,
|
||||
y: min_y,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(target_arch = "spirv"))]
|
||||
impl<'a, P: Copy> Iterator for ImageWindowIterator<'a, P> {
|
||||
type Item = (P, (i32, i32));
|
||||
#[inline]
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
let start_x = self.index as i32 % self.image.width as i32;
|
||||
let start_y = self.index as i32 / self.image.width as i32;
|
||||
let radius = self.radius as i32;
|
||||
|
||||
let min_x = (start_x - radius).max(0) as u32;
|
||||
let max_x = (start_x + radius).min(self.image.width as i32 - 1) as u32;
|
||||
let max_y = (start_y + radius).min(self.image.height as i32 - 1) as u32;
|
||||
if self.y > max_y {
|
||||
return None;
|
||||
}
|
||||
#[cfg(target_arch = "spirv")]
|
||||
let value = None;
|
||||
#[cfg(not(target_arch = "spirv"))]
|
||||
let value = Some((self.image.data[(self.x + self.y * self.image.width) as usize], (self.x as i32 - start_x, self.y as i32 - start_y)));
|
||||
|
||||
self.x += 1;
|
||||
if self.x > max_x {
|
||||
self.x = min_x;
|
||||
self.y += 1;
|
||||
}
|
||||
value
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct MapSecondNode<First, Second, MapFn> {
|
||||
map_fn: MapFn,
|
||||
_first: PhantomData<First>,
|
||||
_second: PhantomData<Second>,
|
||||
}
|
||||
|
||||
#[node_macro::node_fn(MapSecondNode< _First, _Second>)]
|
||||
fn map_snd_node<MapFn, _First, _Second>(input: (_First, _Second), map_fn: &'input MapFn) -> (_First, <MapFn as Node<'input, _Second>>::Output)
|
||||
where
|
||||
MapFn: for<'any_input> Node<'any_input, _Second>,
|
||||
{
|
||||
let (a, b) = input;
|
||||
(a, map_fn.eval(b))
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct BrightenColorNode<Brightness> {
|
||||
brightness: Brightness,
|
||||
}
|
||||
#[node_macro::node_fn(BrightenColorNode)]
|
||||
fn brighten_color_node(color: Color, brightness: f32) -> Color {
|
||||
let per_channel = |col: f32| (col + brightness / 255.).clamp(0., 1.);
|
||||
Color::from_rgbaf32_unchecked(per_channel(color.r()), per_channel(color.g()), per_channel(color.b()), color.a())
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct ForEachNode<MapNode> {
|
||||
map_node: MapNode,
|
||||
}
|
||||
|
||||
#[node_macro::node_fn(ForEachNode)]
|
||||
fn map_node<_Iter: Iterator, MapNode>(input: _Iter, map_node: &'input MapNode) -> ()
|
||||
where
|
||||
MapNode: for<'any_input> Node<'any_input, _Iter::Item, Output = ()> + 'input,
|
||||
{
|
||||
input.for_each(|x| map_node.eval(x));
|
||||
}
|
||||
|
||||
#[cfg(target_arch = "spirv")]
|
||||
const NOTHING: () = ();
|
||||
|
||||
use dyn_any::{StaticType, StaticTypeSized};
|
||||
#[derive(Clone, Debug, PartialEq, Copy)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
|
||||
pub struct ImageSlice<'a, Pixel> {
|
||||
pub width: u32,
|
||||
pub height: u32,
|
||||
#[cfg(not(target_arch = "spirv"))]
|
||||
pub data: &'a [Pixel],
|
||||
#[cfg(target_arch = "spirv")]
|
||||
pub data: &'a (),
|
||||
#[cfg(target_arch = "spirv")]
|
||||
pub _marker: PhantomData<Pixel>,
|
||||
}
|
||||
|
||||
unsafe impl<P: StaticTypeSized> StaticType for ImageSlice<'_, P> {
|
||||
type Static = ImageSlice<'static, P::Static>;
|
||||
}
|
||||
|
||||
#[allow(clippy::derivable_impls)]
|
||||
impl<'a, P> Default for ImageSlice<'a, P> {
|
||||
#[cfg(not(target_arch = "spirv"))]
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
width: Default::default(),
|
||||
height: Default::default(),
|
||||
data: Default::default(),
|
||||
}
|
||||
}
|
||||
#[cfg(target_arch = "spirv")]
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
width: Default::default(),
|
||||
height: Default::default(),
|
||||
data: &NOTHING,
|
||||
_marker: PhantomData,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(target_arch = "spirv"))]
|
||||
impl<P: Copy + Debug + Pixel> Bitmap for ImageSlice<'_, P> {
|
||||
type Pixel = P;
|
||||
fn get_pixel(&self, x: u32, y: u32) -> Option<P> {
|
||||
self.data.get((x + y * self.width) as usize).copied()
|
||||
}
|
||||
fn width(&self) -> u32 {
|
||||
self.width
|
||||
}
|
||||
fn height(&self) -> u32 {
|
||||
self.height
|
||||
}
|
||||
}
|
||||
|
||||
impl<P> ImageSlice<'_, P> {
|
||||
#[cfg(not(target_arch = "spirv"))]
|
||||
pub const fn empty() -> Self {
|
||||
Self { width: 0, height: 0, data: &[] }
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(target_arch = "spirv"))]
|
||||
impl<'a, P: 'a> IntoIterator for ImageSlice<'a, P> {
|
||||
type Item = &'a P;
|
||||
type IntoIter = core::slice::Iter<'a, P>;
|
||||
fn into_iter(self) -> Self::IntoIter {
|
||||
self.data.iter()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(target_arch = "spirv"))]
|
||||
impl<'a, P: 'a> IntoIterator for &'a ImageSlice<'a, P> {
|
||||
type Item = &'a P;
|
||||
type IntoIter = core::slice::Iter<'a, P>;
|
||||
fn into_iter(self) -> Self::IntoIter {
|
||||
self.data.iter()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct ImageDimensionsNode<P> {
|
||||
_p: PhantomData<P>,
|
||||
}
|
||||
|
||||
#[node_macro::node_fn(ImageDimensionsNode<_P>)]
|
||||
fn dimensions_node<_P>(input: ImageSlice<'input, _P>) -> (u32, u32) {
|
||||
(input.width, input.height)
|
||||
}
|
||||
|
||||
#[cfg(feature = "alloc")]
|
||||
pub use self::image::{CollectNode, Image, ImageFrame, ImageRefNode, MapImageSliceNode};
|
||||
pub use self::image::{Image, ImageFrame};
|
||||
#[cfg(feature = "alloc")]
|
||||
pub(crate) mod image;
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
use crate::{ops::CloneNode, structural::Then, value::ValueNode, Node};
|
||||
|
||||
#[ignore]
|
||||
#[test]
|
||||
fn map_node() {
|
||||
// let array = &mut [Color::from_rgbaf32(1.0, 0.0, 0.0, 1.0).unwrap()];
|
||||
|
||||
// LuminanceNode.eval(Color::from_rgbf32_unchecked(1., 0., 0.));
|
||||
|
||||
/*let map = ForEachNode(MutWrapper(LuminanceNode));
|
||||
(&map).eval(array.iter_mut());
|
||||
assert_eq!(array[0], Color::from_rgbaf32(0.33333334, 0.33333334, 0.33333334, 1.0).unwrap());*/
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn window_node() {
|
||||
use alloc::vec;
|
||||
let radius = ValueNode::new(1u32).then(CloneNode::new());
|
||||
let image = ValueNode::<_>::new(Image {
|
||||
width: 5,
|
||||
height: 5,
|
||||
data: vec![Color::from_rgbf32_unchecked(1., 0., 0.); 25],
|
||||
base64_string: None,
|
||||
});
|
||||
let image = image.then(ImageRefNode::new());
|
||||
let window = WindowNode::new(radius, image);
|
||||
let vec = window.eval(0);
|
||||
assert_eq!(vec.count(), 4);
|
||||
let vec = window.eval(5);
|
||||
assert_eq!(vec.count(), 6);
|
||||
let vec = window.eval(12);
|
||||
assert_eq!(vec.count(), 9);
|
||||
}
|
||||
|
||||
// TODO: I can't be bothered to fix this test rn
|
||||
// #[test]
|
||||
// fn blur_node() {
|
||||
// use alloc::vec;
|
||||
// let radius = ValueNode::new(1u32).then(CloneNode::new());
|
||||
// let sigma = ValueNode::new(3f64).then(CloneNode::new());
|
||||
// let radius = ValueNode::new(1u32).then(CloneNode::new());
|
||||
// let image = ValueNode::<_>::new(Image {
|
||||
// width: 5,
|
||||
// height: 5,
|
||||
// data: vec![Color::from_rgbf32_unchecked(1., 0., 0.); 25],
|
||||
// });
|
||||
// let image = image.then(ImageRefNode::new());
|
||||
// let window = WindowNode::new(radius, image);
|
||||
// let window: TypeNode<_, u32, ImageWindowIterator<'_>> = TypeNode::new(window);
|
||||
// let distance = ValueNode::new(DistanceNode::new());
|
||||
// let pos_to_dist = MapSecondNode::new(distance);
|
||||
// let type_erased = &window as &dyn for<'a> Node<'a, u32, Output = ImageWindowIterator<'a>>;
|
||||
// type_erased.eval(0);
|
||||
// let map_pos_to_dist = MapNode::new(ValueNode::new(pos_to_dist));
|
||||
|
||||
// let type_erased = &map_pos_to_dist as &dyn for<'a> Node<'a, u32, Output = ImageWindowIterator<'a>>;
|
||||
// type_erased.eval(0);
|
||||
|
||||
// let distance = window.then(map_pos_to_dist);
|
||||
// let map_gaussian = MapSecondNode::new(ValueNode(GaussianNode::new(sigma)));
|
||||
// let map_gaussian: TypeNode<_, (_, f32), (_, f32)> = TypeNode::new(map_gaussian);
|
||||
// let map_gaussian = ValueNode(map_gaussian);
|
||||
// let map_gaussian: TypeNode<_, (), &_> = TypeNode::new(map_gaussian);
|
||||
// let map_distances = MapNode::new(map_gaussian);
|
||||
// let map_distances: TypeNode<_, _, MapFnIterator<'_, '_, _, _>> = TypeNode::new(map_distances);
|
||||
// let gaussian_iter = distance.then(map_distances);
|
||||
// let avg = gaussian_iter.then(WeightedAvgNode::new());
|
||||
// let avg: TypeNode<_, u32, Color> = TypeNode::new(avg);
|
||||
// let blur_iter = MapNode::new(ValueNode::new(avg));
|
||||
// let blur = image.then(ImageIndexIterNode).then(blur_iter);
|
||||
// let blur: TypeNode<_, (), MapFnIterator<_, _>> = TypeNode::new(blur);
|
||||
// let collect = CollectNode::new();
|
||||
// let vec = collect.eval(0..10);
|
||||
// assert_eq!(vec.len(), 10);
|
||||
// let _ = blur.eval(());
|
||||
// let vec = blur.then(collect);
|
||||
// let _image = vec.eval(());
|
||||
// }
|
||||
trait SetBlendMode {
|
||||
fn set_blend_mode(&mut self, blend_mode: BlendMode);
|
||||
}
|
||||
|
||||
impl SetBlendMode for crate::vector::VectorData {
|
||||
fn set_blend_mode(&mut self, blend_mode: BlendMode) {
|
||||
self.alpha_blending.blend_mode = blend_mode;
|
||||
}
|
||||
}
|
||||
impl SetBlendMode for crate::GraphicGroup {
|
||||
fn set_blend_mode(&mut self, blend_mode: BlendMode) {
|
||||
self.alpha_blending.blend_mode = blend_mode;
|
||||
}
|
||||
}
|
||||
impl SetBlendMode for ImageFrame<Color> {
|
||||
fn set_blend_mode(&mut self, blend_mode: BlendMode) {
|
||||
self.alpha_blending.blend_mode = blend_mode;
|
||||
}
|
||||
}
|
||||
|
||||
#[node_macro::node(category("Style"))]
|
||||
async fn blend_mode<T: SetBlendMode>(
|
||||
footprint: Footprint,
|
||||
#[implementations((Footprint, crate::vector::VectorData), (Footprint, crate::GraphicGroup), (Footprint, ImageFrame<Color>))] value: impl Node<Footprint, Output = T>,
|
||||
blend_mode: BlendMode,
|
||||
) -> T {
|
||||
let mut value = value.eval(footprint).await;
|
||||
value.set_blend_mode(blend_mode);
|
||||
value
|
||||
}
|
||||
|
||||
#[node_macro::node(category("Style"))]
|
||||
async fn opacity<T: MultiplyAlpha>(
|
||||
footprint: Footprint,
|
||||
#[implementations((Footprint, crate::vector::VectorData), (Footprint, crate::GraphicGroup), (Footprint, ImageFrame<Color>))] value: impl Node<Footprint, Output = T>,
|
||||
#[default(100.)] factor: Percentage,
|
||||
) -> T {
|
||||
let mut value = value.eval(footprint).await;
|
||||
let opacity_multiplier = factor / 100.;
|
||||
value.multiply_alpha(opacity_multiplier);
|
||||
value
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue