mirror of
https://github.com/GraphiteEditor/Graphite.git
synced 2026-09-16 06:38:03 +08:00
Animated thumbnails
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
use crate::Color;
|
||||
use glam::{DAffine2, DVec2};
|
||||
use glam::{DAffine2, DVec2, IVec2, UVec2};
|
||||
|
||||
pub trait BoundingBox {
|
||||
fn bounding_box(&self, transform: DAffine2, include_stroke: bool) -> Option<[DVec2; 2]>;
|
||||
@@ -15,10 +15,80 @@ macro_rules! none_impl {
|
||||
};
|
||||
}
|
||||
|
||||
none_impl!(String);
|
||||
none_impl!(bool);
|
||||
none_impl!(f32);
|
||||
none_impl!(f64);
|
||||
none_impl!(DVec2);
|
||||
none_impl!(Option<Color>);
|
||||
none_impl!(Vec<Color>);
|
||||
|
||||
impl BoundingBox for u32 {
|
||||
fn bounding_box(&self, _transform: DAffine2, _include_stroke: bool) -> Option<[DVec2; 2]> {
|
||||
text_bbox(i32_width(*self as i32))
|
||||
}
|
||||
}
|
||||
|
||||
impl BoundingBox for f64 {
|
||||
fn bounding_box(&self, _transform: DAffine2, _include_stroke: bool) -> Option<[DVec2; 2]> {
|
||||
text_bbox(f64_width(*self))
|
||||
}
|
||||
}
|
||||
|
||||
impl BoundingBox for DVec2 {
|
||||
fn bounding_box(&self, _transform: DAffine2, _include_stroke: bool) -> Option<[DVec2; 2]> {
|
||||
let width_x = f64_width(self.x);
|
||||
let width_y = f64_width(self.y);
|
||||
let total_width = width_x + width_y + 50.;
|
||||
text_bbox(total_width)
|
||||
}
|
||||
}
|
||||
|
||||
impl BoundingBox for IVec2 {
|
||||
fn bounding_box(&self, _transform: DAffine2, _include_stroke: bool) -> Option<[DVec2; 2]> {
|
||||
let width_x = i32_width(self.x);
|
||||
let width_y = i32_width(self.y);
|
||||
let total_width = width_x + width_y + 50.;
|
||||
text_bbox(total_width)
|
||||
}
|
||||
}
|
||||
|
||||
impl BoundingBox for UVec2 {
|
||||
fn bounding_box(&self, _transform: DAffine2, _include_stroke: bool) -> Option<[DVec2; 2]> {
|
||||
let width_x = i32_width(self.x as i32);
|
||||
let width_y = i32_width(self.y as i32);
|
||||
let total_width = width_x + width_y + 50.;
|
||||
text_bbox(total_width)
|
||||
}
|
||||
}
|
||||
|
||||
impl BoundingBox for bool {
|
||||
fn bounding_box(&self, _transform: DAffine2, _include_stroke: bool) -> Option<[DVec2; 2]> {
|
||||
text_bbox(60.)
|
||||
}
|
||||
}
|
||||
|
||||
impl BoundingBox for String {
|
||||
fn bounding_box(&self, _transform: DAffine2, _include_stroke: bool) -> Option<[DVec2; 2]> {
|
||||
let width = self.len() * 16;
|
||||
text_bbox(width as f64)
|
||||
}
|
||||
}
|
||||
|
||||
impl BoundingBox for Option<Color> {
|
||||
fn bounding_box(&self, _transform: DAffine2, _include_stroke: bool) -> Option<[DVec2; 2]> {
|
||||
Some([(0., -5.).into(), (150., 110.).into()])
|
||||
}
|
||||
}
|
||||
|
||||
fn f64_width(f64: f64) -> f64 {
|
||||
let left_of_decimal_width = i32_width(f64 as i32);
|
||||
left_of_decimal_width + 5. + 2. * 16.
|
||||
}
|
||||
|
||||
fn i32_width(i32: i32) -> f64 {
|
||||
let number_of_digits = (i32.abs()).checked_ilog10().unwrap_or(0) + 1;
|
||||
let mut width = number_of_digits * 16;
|
||||
if i32 < 0 {
|
||||
width += 20;
|
||||
}
|
||||
width.into()
|
||||
}
|
||||
|
||||
fn text_bbox(width: f64) -> Option<[DVec2; 2]> {
|
||||
Some([(-width / 2., 0.).into(), (width / 2., 30.).into()])
|
||||
}
|
||||
|
||||
@@ -630,8 +630,8 @@ fn get_animation_time(ctx: impl Ctx + ExtractAnimationTime) -> Option<f64> {
|
||||
}
|
||||
|
||||
#[node_macro::node(category("Context Getter"))]
|
||||
fn get_index(ctx: impl Ctx + ExtractIndex) -> Option<usize> {
|
||||
ctx.try_index()
|
||||
fn get_index(ctx: impl Ctx + ExtractIndex) -> Option<u32> {
|
||||
ctx.try_index().map(|index| index as u32)
|
||||
}
|
||||
|
||||
// #[node_macro::node(category("Loop"))]
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use crate::Color;
|
||||
use crate::{Color, bounds::BoundingBox, vector::VectorDataTable};
|
||||
use dyn_any::DynAny;
|
||||
use glam::{DAffine2, DVec2};
|
||||
|
||||
@@ -32,6 +32,12 @@ impl Default for GradientStops {
|
||||
}
|
||||
}
|
||||
|
||||
impl BoundingBox for GradientStops {
|
||||
fn bounding_box(&self, _transform: DAffine2, _include_stroke: bool) -> Option<[DVec2; 2]> {
|
||||
Into::<VectorDataTable>::into(Into::<Gradient>::into(self.clone())).bounding_box(DAffine2::default(), false)
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoIterator for GradientStops {
|
||||
type Item = (f64, Color);
|
||||
type IntoIter = std::vec::IntoIter<(f64, Color)>;
|
||||
@@ -169,6 +175,24 @@ impl std::fmt::Display for Gradient {
|
||||
}
|
||||
}
|
||||
|
||||
impl From<GradientStops> for Gradient {
|
||||
fn from(gradient_stops: GradientStops) -> Gradient {
|
||||
Gradient {
|
||||
stops: gradient_stops.clone(),
|
||||
gradient_type: GradientType::Linear,
|
||||
start: (0., 0.).into(),
|
||||
end: (1., 0.).into(),
|
||||
transform: DAffine2::IDENTITY,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl BoundingBox for Gradient {
|
||||
fn bounding_box(&self, _transform: DAffine2, _include_stroke: bool) -> Option<[DVec2; 2]> {
|
||||
Into::<VectorDataTable>::into(self.clone()).bounding_box(DAffine2::default(), false)
|
||||
}
|
||||
}
|
||||
|
||||
impl Gradient {
|
||||
/// Constructs a new gradient with the colors at 0 and 1 specified.
|
||||
pub fn new(start: DVec2, start_color: Color, end: DVec2, end_color: Color, transform: DAffine2, gradient_type: GradientType) -> Self {
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
use crate::gradient::{Gradient, GradientStops};
|
||||
use crate::instances::Instances;
|
||||
use crate::raster_types::{CPU, GPU, Raster};
|
||||
use crate::vector::VectorData;
|
||||
@@ -54,8 +55,10 @@ impl RenderComplexity for Raster<GPU> {
|
||||
|
||||
impl RenderComplexity for String {}
|
||||
impl RenderComplexity for bool {}
|
||||
impl RenderComplexity for f32 {}
|
||||
impl RenderComplexity for u32 {}
|
||||
impl RenderComplexity for f64 {}
|
||||
impl RenderComplexity for DVec2 {}
|
||||
impl RenderComplexity for Option<Color> {}
|
||||
impl RenderComplexity for Vec<Color> {}
|
||||
impl RenderComplexity for GradientStops {}
|
||||
impl RenderComplexity for Gradient {}
|
||||
|
||||
@@ -5,13 +5,15 @@ mod modification;
|
||||
use super::misc::{dvec2_to_point, point_to_dvec2};
|
||||
use super::style::{PathStyle, Stroke};
|
||||
use crate::bounds::BoundingBox;
|
||||
use crate::gradient::{Gradient, GradientType};
|
||||
use crate::instances::Instances;
|
||||
use crate::math::quad::Quad;
|
||||
use crate::transform::Transform;
|
||||
use crate::vector::click_target::{ClickTargetType, FreePoint};
|
||||
use crate::vector::style::Fill;
|
||||
use crate::{AlphaBlending, Color, GraphicGroupTable};
|
||||
pub use attributes::*;
|
||||
use bezier_rs::{BezierHandles, ManipulatorGroup};
|
||||
use bezier_rs::{BezierHandles, ManipulatorGroup, Subpath};
|
||||
use core::borrow::Borrow;
|
||||
use core::hash::Hash;
|
||||
use dyn_any::DynAny;
|
||||
@@ -511,6 +513,36 @@ impl BoundingBox for VectorDataTable {
|
||||
}
|
||||
}
|
||||
|
||||
/// Convert a Gradient/GradientStops into VectorDataTable for rendering thumbnails
|
||||
impl From<Gradient> for VectorDataTable {
|
||||
fn from(mut gradient: Gradient) -> VectorDataTable {
|
||||
match gradient.gradient_type {
|
||||
GradientType::Linear => {
|
||||
let mut rectangle = VectorData::from_subpath(Subpath::new_rect((0., 0.).into(), (150., 100.).into()));
|
||||
// Handle vertical gradients
|
||||
let intersection = if gradient.start.x == gradient.end.x {
|
||||
DVec2::new(0., 100.)
|
||||
} else {
|
||||
let slope = (gradient.start.y - gradient.end.y) / (gradient.start.x - gradient.end.x);
|
||||
if slope > 100. / 150. { DVec2::new(100. / slope, 100.) } else { DVec2::new(150., slope * 150.) }
|
||||
};
|
||||
gradient.start = (0., 0.).into();
|
||||
gradient.end = intersection;
|
||||
rectangle.style.fill = Fill::Gradient(gradient);
|
||||
Instances::new(rectangle)
|
||||
}
|
||||
GradientType::Radial => {
|
||||
let mut circle = VectorData::from_subpath(Subpath::new_ellipse((-100., -100.).into(), (100., 100.).into()));
|
||||
gradient.start = (0., 0.).into();
|
||||
gradient.end = (100., 0.).into();
|
||||
gradient.transform = DAffine2::IDENTITY;
|
||||
circle.style.fill = Fill::Gradient(gradient);
|
||||
Instances::new(circle)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A selectable part of a curve, either an anchor (start or end of a bézier) or a handle (doesn't necessarily go through the bézier but influences curvature).
|
||||
#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug, DynAny, serde::Serialize, serde::Deserialize)]
|
||||
pub enum ManipulatorPointId {
|
||||
|
||||
@@ -7,6 +7,7 @@ pub use glam::{DAffine2, DVec2, IVec2, UVec2};
|
||||
use graphene_application_io::SurfaceFrame;
|
||||
use graphene_brush::brush_cache::BrushCache;
|
||||
use graphene_brush::brush_stroke::BrushStroke;
|
||||
use graphene_core::gradient::GradientStops;
|
||||
use graphene_core::raster_types::{CPU, GPU};
|
||||
use graphene_core::transform::ReferencePoint;
|
||||
use graphene_core::uuid::NodeId;
|
||||
@@ -562,8 +563,13 @@ thumbnail_render! {
|
||||
graphene_core::raster_types::RasterDataTable<GPU>,
|
||||
graphene_core::GraphicElement,
|
||||
Option<Color>,
|
||||
GradientStops,
|
||||
Vec<Color>,
|
||||
u32,
|
||||
f64,
|
||||
DVec2,
|
||||
bool,
|
||||
String,
|
||||
}
|
||||
|
||||
pub enum ThumbnailRenderResult {
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
use glam::DVec2;
|
||||
pub use graph_craft::document::value::RenderOutputType;
|
||||
use graph_craft::document::value::{EditorMetadata, RenderOutput};
|
||||
pub use graph_craft::wasm_application_io::*;
|
||||
use graphene_application_io::ApplicationIo;
|
||||
use graphene_core::gradient::GradientStops;
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
use graphene_core::instances::Instances;
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
@@ -240,10 +242,11 @@ async fn render<'a: 'n, T: 'n + GraphicElementRendered + WasmNotSend>(
|
||||
graphene_core::Artboard,
|
||||
graphene_core::ArtboardGroupTable,
|
||||
Option<Color>,
|
||||
GradientStops,
|
||||
Vec<Color>,
|
||||
bool,
|
||||
f32,
|
||||
f64,
|
||||
DVec2,
|
||||
String,
|
||||
)]
|
||||
data: T,
|
||||
|
||||
@@ -6,6 +6,7 @@ use glam::{DAffine2, DVec2};
|
||||
use graphene_core::blending::BlendMode;
|
||||
use graphene_core::bounds::BoundingBox;
|
||||
use graphene_core::color::Color;
|
||||
use graphene_core::gradient::{Gradient, GradientStops};
|
||||
use graphene_core::instances::Instance;
|
||||
use graphene_core::math::quad::Quad;
|
||||
use graphene_core::raster::Image;
|
||||
@@ -1185,23 +1186,36 @@ impl GraphicElementRendered for GraphicElement {
|
||||
}
|
||||
}
|
||||
|
||||
/// Used to stop rust complaining about upstream traits adding display implementations to `Option<Color>`. This would not be an issue as we control that crate.
|
||||
trait Primitive: std::fmt::Display + BoundingBox + RenderComplexity {}
|
||||
impl Primitive for String {}
|
||||
trait Primitive: std::fmt::Display + BoundingBox + RenderComplexity {
|
||||
fn precision() -> bool {
|
||||
false
|
||||
}
|
||||
}
|
||||
impl Primitive for u32 {}
|
||||
impl Primitive for f64 {
|
||||
fn precision() -> bool {
|
||||
true
|
||||
}
|
||||
}
|
||||
impl Primitive for DVec2 {
|
||||
fn precision() -> bool {
|
||||
true
|
||||
}
|
||||
}
|
||||
impl Primitive for bool {}
|
||||
impl Primitive for f32 {}
|
||||
impl Primitive for f64 {}
|
||||
impl Primitive for DVec2 {}
|
||||
impl Primitive for String {}
|
||||
|
||||
fn text_attributes(attributes: &mut SvgRenderAttrs) {
|
||||
attributes.push("fill", "black");
|
||||
attributes.push("font-size", "30");
|
||||
attributes.push("dominant-baseline", "hanging");
|
||||
attributes.push("text-anchor", "middle");
|
||||
}
|
||||
|
||||
impl<P: Primitive> GraphicElementRendered for P {
|
||||
fn render_svg(&self, render: &mut SvgRender, _render_params: &RenderParams) {
|
||||
log::debug!("Rendering svg for primative: {}", self);
|
||||
render.parent_tag("text", text_attributes, |render| render.leaf_node(format!("{self}")));
|
||||
let text = if P::precision() { format!("{:.2}", self) } else { format!("{self}") };
|
||||
render.parent_tag("text", text_attributes, |render| render.leaf_node(text));
|
||||
}
|
||||
|
||||
#[cfg(feature = "vello")]
|
||||
@@ -1210,22 +1224,33 @@ impl<P: Primitive> GraphicElementRendered for P {
|
||||
|
||||
impl GraphicElementRendered for Option<Color> {
|
||||
fn render_svg(&self, render: &mut SvgRender, _render_params: &RenderParams) {
|
||||
let Some(color) = self else {
|
||||
render.parent_tag("text", |_| {}, |render| render.leaf_node("Empty color"));
|
||||
return;
|
||||
};
|
||||
let color_info = format!("{:?} #{} {:?}", color, color.to_rgba_hex_srgb(), color.to_rgba8_srgb());
|
||||
|
||||
render.leaf_tag("rect", |attributes| {
|
||||
attributes.push("width", "100");
|
||||
attributes.push("height", "100");
|
||||
attributes.push("y", "40");
|
||||
attributes.push("fill", format!("#{}", color.to_rgb_hex_srgb_from_gamma()));
|
||||
if color.a() < 1. {
|
||||
attributes.push("fill-opacity", ((color.a() * 1000.).round() / 1000.).to_string());
|
||||
match self {
|
||||
Some(color) => {
|
||||
render.leaf_tag("rect", |attributes| {
|
||||
attributes.push("width", "150");
|
||||
attributes.push("height", "100");
|
||||
attributes.push("fill", format!("#{}", color.to_rgb_hex_srgb_from_gamma()));
|
||||
if color.a() < 1. {
|
||||
attributes.push("fill-opacity", ((color.a() * 1000.).round() / 1000.).to_string());
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
render.parent_tag("text", text_attributes, |render| render.leaf_node(color_info))
|
||||
None => {
|
||||
render.leaf_tag("rect", |attributes| {
|
||||
attributes.push("width", "150");
|
||||
attributes.push("height", "100");
|
||||
attributes.push("fill", format!("#ffffff"));
|
||||
});
|
||||
render.leaf_tag("line", |attributes| {
|
||||
attributes.push("x1", "0");
|
||||
attributes.push("y1", "100");
|
||||
attributes.push("x2", "150");
|
||||
attributes.push("y2", "0");
|
||||
attributes.push("stroke", "red");
|
||||
attributes.push("stroke-width", "5");
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "vello")]
|
||||
@@ -1236,7 +1261,7 @@ impl GraphicElementRendered for Vec<Color> {
|
||||
fn render_svg(&self, render: &mut SvgRender, _render_params: &RenderParams) {
|
||||
for (index, &color) in self.iter().enumerate() {
|
||||
render.leaf_tag("rect", |attributes| {
|
||||
attributes.push("width", "100");
|
||||
attributes.push("width", "150");
|
||||
attributes.push("height", "100");
|
||||
attributes.push("x", (index * 120).to_string());
|
||||
attributes.push("y", "40");
|
||||
@@ -1252,6 +1277,23 @@ impl GraphicElementRendered for Vec<Color> {
|
||||
fn render_to_vello(&self, _scene: &mut Scene, _transform: DAffine2, _context: &mut RenderContext, _render_params: &RenderParams) {}
|
||||
}
|
||||
|
||||
impl GraphicElementRendered for GradientStops {
|
||||
fn render_svg(&self, render: &mut SvgRender, render_params: &RenderParams) {
|
||||
// Gradient stops -> Gradient -> Vector data table
|
||||
Into::<VectorDataTable>::into(Into::<Gradient>::into(self.clone())).render_svg(render, render_params);
|
||||
}
|
||||
|
||||
#[cfg(feature = "vello")]
|
||||
fn render_to_vello(&self, _scene: &mut Scene, _transform: DAffine2, _context: &mut RenderContext, _render_params: &RenderParams) {}
|
||||
}
|
||||
|
||||
impl GraphicElementRendered for Gradient {
|
||||
fn render_svg(&self, render: &mut SvgRender, render_params: &RenderParams) {
|
||||
Into::<VectorDataTable>::into(self.clone()).render_svg(render, render_params);
|
||||
}
|
||||
#[cfg(feature = "vello")]
|
||||
fn render_to_vello(&self, _scene: &mut Scene, _transform: DAffine2, _context: &mut RenderContext, _render_params: &RenderParams) {}
|
||||
}
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum SvgSegment {
|
||||
Slice(&'static str),
|
||||
|
||||
Reference in New Issue
Block a user