Improve rendered SVG output syntax for better compatibility and terseness (#1880)
* Improve rendered SVG output syntax for better compatibility and terseness * Fix CI failing on boolean operations without wasm32? * Attempt 2
This commit is contained in:
parent
22ebe9a2cb
commit
44ffb635e9
File diff suppressed because one or more lines are too long
|
|
@ -128,7 +128,7 @@ impl LayoutHolder for PathTool {
|
|||
let related_seperator = Separator::new(SeparatorType::Related).widget_holder();
|
||||
let unrelated_seperator = Separator::new(SeparatorType::Unrelated).widget_holder();
|
||||
|
||||
let colinear_handles_tooltip = "Ensures both handles remain 180° apart";
|
||||
let colinear_handles_tooltip = "Keep both handles unbent, each 180° apart, when moving either";
|
||||
let colinear_handles_state = manipulator_angle.and_then(|angle| match angle {
|
||||
ManipulatorAngle::Colinear => Some(true),
|
||||
ManipulatorAngle::Free => Some(false),
|
||||
|
|
|
|||
|
|
@ -677,10 +677,7 @@ impl Fsm for PenToolFsmState {
|
|||
]),
|
||||
HintGroup(vec![HintInfo::keys([Key::Shift], "Snap 15°"), HintInfo::keys([Key::Control], "Lock Angle")]),
|
||||
HintGroup(vec![HintInfo::mouse(MouseMotion::Lmb, "Add Sharp Point"), HintInfo::mouse(MouseMotion::LmbDrag, "Add Smooth Point")]),
|
||||
HintGroup(vec![
|
||||
HintInfo::mouse(MouseMotion::Lmb, ""),
|
||||
HintInfo::mouse(MouseMotion::LmbDrag, "Bend from Prev. Point").prepend_slash(),
|
||||
]),
|
||||
HintGroup(vec![HintInfo::mouse(MouseMotion::Lmb, ""), HintInfo::mouse(MouseMotion::LmbDrag, "Bend Prev. Point").prepend_slash()]),
|
||||
]),
|
||||
PenToolFsmState::DraggingHandle => HintData(vec![
|
||||
HintGroup(vec![
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ use graphene_core::transform::{Footprint, Transform};
|
|||
use graphene_core::vector::style::ViewMode;
|
||||
use graphene_core::vector::VectorData;
|
||||
use graphene_core::{Color, GraphicElement, SurfaceFrame};
|
||||
use graphene_std::renderer::format_transform_matrix;
|
||||
use graphene_std::wasm_application_io::{WasmApplicationIo, WasmEditorApi};
|
||||
use interpreted_executor::dynamic_executor::{DynamicExecutor, ResolvedDocumentNodeTypes};
|
||||
|
||||
|
|
@ -657,18 +658,11 @@ impl NodeGraphExecutor {
|
|||
responses.add(DocumentMessage::RenderRulers);
|
||||
}
|
||||
TaggedValue::RenderOutput(graphene_std::wasm_application_io::RenderOutput::CanvasFrame(frame)) => {
|
||||
// Send to frontend
|
||||
let matrix = frame
|
||||
.transform
|
||||
.to_cols_array()
|
||||
.iter()
|
||||
.enumerate()
|
||||
.fold(String::new(), |val, (i, entry)| val + &(entry.to_string() + if i == 5 { "" } else { "," }));
|
||||
let matrix = format_transform_matrix(frame.transform);
|
||||
let transform = if matrix.is_empty() { String::new() } else { format!(" transform=\"{}\"", matrix) };
|
||||
let svg = format!(
|
||||
r#"
|
||||
<svg><foreignObject width="{}" height="{}" transform="matrix({})"><div data-canvas-placeholder="canvas{}"></div></foreignObject></svg>
|
||||
"#,
|
||||
frame.resolution.x, frame.resolution.y, matrix, frame.surface_id.0
|
||||
r#"<svg><foreignObject width="{}" height="{}"{transform}><div data-canvas-placeholder="canvas{}"></div></foreignObject></svg>"#,
|
||||
frame.resolution.x, frame.resolution.y, frame.surface_id.0
|
||||
);
|
||||
responses.add(FrontendMessage::UpdateDocumentArtwork { svg });
|
||||
responses.add(DocumentMessage::RenderScrollbars);
|
||||
|
|
|
|||
|
|
@ -34,53 +34,56 @@
|
|||
style={`${styleName} ${extraStyles}`.trim() || undefined}
|
||||
title={tooltip}
|
||||
bind:this={self}
|
||||
on:focus
|
||||
on:blur
|
||||
on:fullscreenchange
|
||||
on:fullscreenerror
|
||||
on:scroll
|
||||
on:cut
|
||||
on:copy
|
||||
on:paste
|
||||
on:keydown
|
||||
on:keypress
|
||||
on:keyup
|
||||
on:auxclick
|
||||
on:blur
|
||||
on:click
|
||||
on:contextmenu
|
||||
on:dblclick
|
||||
on:mousedown
|
||||
on:mouseenter
|
||||
on:mouseleave
|
||||
on:mousemove
|
||||
on:mouseover
|
||||
on:mouseout
|
||||
on:mouseup
|
||||
on:select
|
||||
on:drag
|
||||
on:dragend
|
||||
on:dragenter
|
||||
on:dragstart
|
||||
on:dragleave
|
||||
on:dragover
|
||||
on:drop
|
||||
on:touchcancel
|
||||
on:touchend
|
||||
on:pointerover
|
||||
on:pointerenter
|
||||
on:dragstart
|
||||
on:mouseup
|
||||
on:pointerdown
|
||||
on:pointermove
|
||||
on:pointerup
|
||||
on:pointercancel
|
||||
on:pointerout
|
||||
on:pointerenter
|
||||
on:pointerleave
|
||||
on:gotpointercapture
|
||||
on:lostpointercapture
|
||||
on:scroll
|
||||
{...$$restProps}
|
||||
>
|
||||
<slot />
|
||||
</div>
|
||||
|
||||
<!-- Unused (each impacts performance, see <https://github.com/GraphiteEditor/Graphite/issues/1877>):
|
||||
on:contextmenu
|
||||
on:copy
|
||||
on:cut
|
||||
on:drag
|
||||
on:dragenter
|
||||
on:drop
|
||||
on:focus
|
||||
on:fullscreenchange
|
||||
on:fullscreenerror
|
||||
on:gotpointercapture
|
||||
on:keydown
|
||||
on:keypress
|
||||
on:keyup
|
||||
on:lostpointercapture
|
||||
on:mousedown
|
||||
on:mouseenter
|
||||
on:mouseleave
|
||||
on:mousemove
|
||||
on:mouseout
|
||||
on:mouseover
|
||||
on:paste
|
||||
on:pointercancel
|
||||
on:pointermove
|
||||
on:pointerout
|
||||
on:pointerover
|
||||
on:pointerup
|
||||
on:select
|
||||
on:touchcancel
|
||||
on:touchend
|
||||
-->
|
||||
|
||||
<style lang="scss" global>
|
||||
.layout-col {
|
||||
display: flex;
|
||||
|
|
|
|||
|
|
@ -34,53 +34,56 @@
|
|||
style={`${styleName} ${extraStyles}`.trim() || undefined}
|
||||
title={tooltip}
|
||||
bind:this={self}
|
||||
on:focus
|
||||
on:blur
|
||||
on:fullscreenchange
|
||||
on:fullscreenerror
|
||||
on:scroll
|
||||
on:cut
|
||||
on:copy
|
||||
on:paste
|
||||
on:keydown
|
||||
on:keypress
|
||||
on:keyup
|
||||
on:auxclick
|
||||
on:blur
|
||||
on:click
|
||||
on:contextmenu
|
||||
on:dblclick
|
||||
on:mousedown
|
||||
on:mouseenter
|
||||
on:mouseleave
|
||||
on:mousemove
|
||||
on:mouseover
|
||||
on:mouseout
|
||||
on:mouseup
|
||||
on:select
|
||||
on:drag
|
||||
on:dragend
|
||||
on:dragenter
|
||||
on:dragstart
|
||||
on:dragleave
|
||||
on:dragover
|
||||
on:drop
|
||||
on:touchcancel
|
||||
on:touchend
|
||||
on:pointerover
|
||||
on:pointerenter
|
||||
on:dragstart
|
||||
on:mouseup
|
||||
on:pointerdown
|
||||
on:pointermove
|
||||
on:pointerup
|
||||
on:pointercancel
|
||||
on:pointerout
|
||||
on:pointerenter
|
||||
on:pointerleave
|
||||
on:gotpointercapture
|
||||
on:lostpointercapture
|
||||
on:scroll
|
||||
{...$$restProps}
|
||||
>
|
||||
<slot />
|
||||
</div>
|
||||
|
||||
<!-- Unused (each impacts performance, see <https://github.com/GraphiteEditor/Graphite/issues/1877>):
|
||||
on:contextmenu
|
||||
on:copy
|
||||
on:cut
|
||||
on:drag
|
||||
on:dragenter
|
||||
on:drop
|
||||
on:focus
|
||||
on:fullscreenchange
|
||||
on:fullscreenerror
|
||||
on:gotpointercapture
|
||||
on:keydown
|
||||
on:keypress
|
||||
on:keyup
|
||||
on:lostpointercapture
|
||||
on:mousedown
|
||||
on:mouseenter
|
||||
on:mouseleave
|
||||
on:mousemove
|
||||
on:mouseout
|
||||
on:mouseover
|
||||
on:paste
|
||||
on:pointercancel
|
||||
on:pointermove
|
||||
on:pointerout
|
||||
on:pointerover
|
||||
on:pointerup
|
||||
on:select
|
||||
on:touchcancel
|
||||
on:touchend
|
||||
-->
|
||||
|
||||
<style lang="scss" global>
|
||||
.layout-row {
|
||||
display: flex;
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
mod quad;
|
||||
pub use quad::Quad;
|
||||
|
||||
use crate::raster::bbox::Bbox;
|
||||
use crate::raster::{BlendMode, Image, ImageFrame};
|
||||
|
|
@ -8,12 +9,13 @@ use crate::vector::style::{Fill, Stroke, ViewMode};
|
|||
use crate::vector::PointId;
|
||||
use crate::SurfaceFrame;
|
||||
use crate::{vector::VectorData, Artboard, Color, GraphicElement, GraphicGroup};
|
||||
pub use quad::Quad;
|
||||
|
||||
use bezier_rs::Subpath;
|
||||
|
||||
use base64::Engine;
|
||||
use glam::{DAffine2, DVec2};
|
||||
use num_traits::Zero;
|
||||
use std::fmt::Write;
|
||||
#[cfg(feature = "vello")]
|
||||
use vello::*;
|
||||
|
||||
|
|
@ -107,11 +109,10 @@ impl SvgRender {
|
|||
.map(|size| format!("viewbox=\"0 0 {} {}\" width=\"{}\" height=\"{}\"", size.x, size.y, size.x, size.y))
|
||||
.unwrap_or_default();
|
||||
|
||||
let svg_header = format!(
|
||||
r#"<svg xmlns="http://www.w3.org/2000/svg" {}><defs>{defs}</defs><g transform="{}">"#,
|
||||
view_box,
|
||||
format_transform_matrix(transform)
|
||||
);
|
||||
let matrix = format_transform_matrix(transform);
|
||||
let transform = if matrix.is_empty() { String::new() } else { format!(r#" transform="{}""#, matrix) };
|
||||
|
||||
let svg_header = format!(r#"<svg xmlns="http://www.w3.org/2000/svg" {}><defs>{defs}</defs><g{transform}>"#, view_box);
|
||||
self.svg.insert(0, svg_header.into());
|
||||
self.svg.push("</g></svg>".into());
|
||||
}
|
||||
|
|
@ -193,17 +194,16 @@ impl RenderParams {
|
|||
}
|
||||
|
||||
pub fn format_transform_matrix(transform: DAffine2) -> String {
|
||||
use std::fmt::Write;
|
||||
let mut result = "matrix(".to_string();
|
||||
let cols = transform.to_cols_array();
|
||||
for (index, item) in cols.iter().enumerate() {
|
||||
write!(result, "{item}").unwrap();
|
||||
if index != cols.len() - 1 {
|
||||
result.push_str(", ");
|
||||
}
|
||||
if transform == DAffine2::IDENTITY {
|
||||
return String::new();
|
||||
}
|
||||
result.push(')');
|
||||
result
|
||||
|
||||
transform.to_cols_array().iter().enumerate().fold("matrix(".to_string(), |val, (i, num)| {
|
||||
let num = if num.abs() < 1_000_000_000. { (num * 1_000_000_000.).round() / 1_000_000_000. } else { *num };
|
||||
let num = if num.is_zero() { "0".to_string() } else { num.to_string() };
|
||||
let comma = if i == 5 { "" } else { "," };
|
||||
val + &(num + comma)
|
||||
}) + ")"
|
||||
}
|
||||
|
||||
pub fn to_transform(transform: DAffine2) -> usvg::Transform {
|
||||
|
|
@ -234,7 +234,10 @@ impl GraphicElementRendered for GraphicGroup {
|
|||
render.parent_tag(
|
||||
"g",
|
||||
|attributes| {
|
||||
attributes.push("transform", format_transform_matrix(self.transform));
|
||||
let matrix = format_transform_matrix(self.transform);
|
||||
if !matrix.is_empty() {
|
||||
attributes.push("transform", matrix);
|
||||
}
|
||||
|
||||
if self.alpha_blending.opacity < 1. {
|
||||
attributes.push("opacity", self.alpha_blending.opacity.to_string());
|
||||
|
|
@ -305,8 +308,6 @@ impl GraphicElementRendered for VectorData {
|
|||
}
|
||||
|
||||
render.leaf_tag("path", |attributes| {
|
||||
attributes.push("class", "vector-data");
|
||||
|
||||
attributes.push("d", path);
|
||||
|
||||
let fill_and_stroke = self
|
||||
|
|
@ -443,8 +444,10 @@ impl GraphicElementRendered for Artboard {
|
|||
if !render_params.hide_artboards {
|
||||
// Background
|
||||
render.leaf_tag("rect", |attributes| {
|
||||
attributes.push("class", "artboard-bg");
|
||||
attributes.push("fill", format!("#{}", self.background.rgba_hex()));
|
||||
attributes.push("fill", format!("#{}", self.background.rgb_hex()));
|
||||
if self.background.a() < 1. {
|
||||
attributes.push("fill-opacity", ((self.background.a() * 1000.).round() / 1000.).to_string());
|
||||
}
|
||||
attributes.push("x", self.location.x.min(self.location.x + self.dimensions.x).to_string());
|
||||
attributes.push("y", self.location.y.min(self.location.y + self.dimensions.y).to_string());
|
||||
attributes.push("width", self.dimensions.x.abs().to_string());
|
||||
|
|
@ -456,7 +459,6 @@ impl GraphicElementRendered for Artboard {
|
|||
render.parent_tag(
|
||||
"text",
|
||||
|attributes| {
|
||||
attributes.push("class", "artboard-label");
|
||||
attributes.push("fill", "white");
|
||||
attributes.push("x", (self.location.x.min(self.location.x + self.dimensions.x)).to_string());
|
||||
attributes.push("y", (self.location.y.min(self.location.y + self.dimensions.y) - 4).to_string());
|
||||
|
|
@ -475,23 +477,22 @@ impl GraphicElementRendered for Artboard {
|
|||
"g",
|
||||
// Group tag attributes
|
||||
|attributes| {
|
||||
attributes.push("class", "artboard");
|
||||
|
||||
attributes.push(
|
||||
"transform",
|
||||
format_transform_matrix(DAffine2::from_translation(self.location.as_dvec2()) * self.graphic_group.transform),
|
||||
);
|
||||
let matrix = format_transform_matrix(DAffine2::from_translation(self.location.as_dvec2()) * self.graphic_group.transform);
|
||||
if !matrix.is_empty() {
|
||||
attributes.push("transform", matrix);
|
||||
}
|
||||
|
||||
if self.clip {
|
||||
let id = format!("artboard-{}", generate_uuid());
|
||||
let selector = format!("url(#{id})");
|
||||
use std::fmt::Write;
|
||||
|
||||
let matrix = format_transform_matrix(self.graphic_group.transform.inverse());
|
||||
let transform = if matrix.is_empty() { String::new() } else { format!(r#" transform="{matrix}""#) };
|
||||
|
||||
write!(
|
||||
&mut attributes.0.svg_defs,
|
||||
r##"<clipPath id="{id}"><rect x="0" y="0" width="{}" height="{}" transform="{}"/></clipPath>"##,
|
||||
self.dimensions.x,
|
||||
self.dimensions.y,
|
||||
format_transform_matrix(self.graphic_group.transform.inverse())
|
||||
r##"<clipPath id="{id}"><rect x="0" y="0" width="{}" height="{}"{transform} /></clipPath>"##,
|
||||
self.dimensions.x, self.dimensions.y
|
||||
)
|
||||
.unwrap();
|
||||
attributes.push("clip-path", selector);
|
||||
|
|
@ -580,18 +581,16 @@ impl GraphicElementRendered for crate::ArtboardGroup {
|
|||
impl GraphicElementRendered for SurfaceFrame {
|
||||
fn render_svg(&self, render: &mut SvgRender, _render_params: &RenderParams) {
|
||||
let transform = self.transform;
|
||||
|
||||
let (width, height) = (transform.transform_vector2(DVec2::new(1., 0.)).length(), transform.transform_vector2(DVec2::new(0., 1.)).length());
|
||||
let matrix = (transform * DAffine2::from_scale((width, height).into()).inverse())
|
||||
.to_cols_array()
|
||||
.iter()
|
||||
.enumerate()
|
||||
.fold(String::new(), |val, (i, entry)| val + &(entry.to_string() + if i == 5 { "" } else { "," }));
|
||||
|
||||
let matrix = format_transform_matrix(transform * DAffine2::from_scale((width, height).into()).inverse());
|
||||
let transform = if matrix.is_empty() { String::new() } else { format!(r#" transform="{}""#, matrix) };
|
||||
|
||||
let canvas = format!(
|
||||
r#"<foreignObject width="{}" height="{}" transform="matrix({})"><div data-canvas-placeholder="canvas{}"></div></foreignObject>"#,
|
||||
r#"<foreignObject width="{}" height="{}"{transform}><div data-canvas-placeholder="canvas{}"></div></foreignObject>"#,
|
||||
width.abs(),
|
||||
height.abs(),
|
||||
matrix,
|
||||
self.surface_id
|
||||
);
|
||||
render.svg.push(canvas.into())
|
||||
|
|
@ -617,7 +616,7 @@ impl GraphicElementRendered for SurfaceFrame {
|
|||
|
||||
impl GraphicElementRendered for ImageFrame<Color> {
|
||||
fn render_svg(&self, render: &mut SvgRender, render_params: &RenderParams) {
|
||||
let transform: String = format_transform_matrix(self.transform * render.transform);
|
||||
let transform = self.transform * render.transform;
|
||||
|
||||
match render_params.image_render_mode {
|
||||
ImageRenderMode::Base64 => {
|
||||
|
|
@ -638,8 +637,11 @@ impl GraphicElementRendered for ImageFrame<Color> {
|
|||
attributes.push("width", 1.to_string());
|
||||
attributes.push("height", 1.to_string());
|
||||
attributes.push("preserveAspectRatio", "none");
|
||||
attributes.push("transform", transform);
|
||||
attributes.push("href", base64_string);
|
||||
let matrix = format_transform_matrix(transform);
|
||||
if !matrix.is_empty() {
|
||||
attributes.push("transform", matrix);
|
||||
}
|
||||
if self.alpha_blending.blend_mode != BlendMode::default() {
|
||||
attributes.push("style", self.alpha_blending.blend_mode.render());
|
||||
}
|
||||
|
|
@ -765,7 +767,10 @@ impl GraphicElementRendered for Option<Color> {
|
|||
attributes.push("width", "100");
|
||||
attributes.push("height", "100");
|
||||
attributes.push("y", "40");
|
||||
attributes.push("fill", format!("#{}", color.rgba_hex()));
|
||||
attributes.push("fill", format!("#{}", color.rgb_hex()));
|
||||
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))
|
||||
}
|
||||
|
|
@ -785,7 +790,10 @@ impl GraphicElementRendered for Vec<Color> {
|
|||
attributes.push("height", "100");
|
||||
attributes.push("x", (index * 120).to_string());
|
||||
attributes.push("y", "40");
|
||||
attributes.push("fill", format!("#{}", color.rgba_hex()));
|
||||
attributes.push("fill", format!("#{}", color.rgb_hex()));
|
||||
if color.a() < 1. {
|
||||
attributes.push("fill-opacity", ((color.a() * 1000.).round() / 1000.).to_string());
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,20 +1,18 @@
|
|||
use core::hash::Hash;
|
||||
use half::f16;
|
||||
use super::discrete_srgb::{float_to_srgb_u8, srgb_u8_to_float};
|
||||
use super::{Alpha, AssociatedAlpha, Luminance, LuminanceMut, Pixel, RGBMut, Rec709Primaries, RGB, SRGB};
|
||||
|
||||
use dyn_any::{DynAny, StaticType};
|
||||
#[cfg(feature = "serde")]
|
||||
#[cfg(target_arch = "spirv")]
|
||||
use spirv_std::num_traits::float::Float;
|
||||
|
||||
#[cfg(target_arch = "spirv")]
|
||||
use spirv_std::num_traits::Euclid;
|
||||
|
||||
use bytemuck::{Pod, Zeroable};
|
||||
use core::hash::Hash;
|
||||
use half::f16;
|
||||
use std::fmt::Write;
|
||||
|
||||
use super::{
|
||||
discrete_srgb::{float_to_srgb_u8, srgb_u8_to_float},
|
||||
Alpha, AssociatedAlpha, Luminance, LuminanceMut, Pixel, RGBMut, Rec709Primaries, RGB, SRGB,
|
||||
};
|
||||
#[repr(C)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
#[derive(Debug, Default, Clone, Copy, PartialEq, DynAny, Pod, Zeroable)]
|
||||
|
|
@ -813,12 +811,12 @@ impl Color {
|
|||
/// ```
|
||||
/// use graphene_core::raster::color::Color;
|
||||
/// let color = Color::from_rgba8_srgb(0x52, 0x67, 0xFA, 0x61).to_gamma_srgb();
|
||||
/// assert_eq!("3240A261", color.rgba_hex())
|
||||
/// assert_eq!("3240a261", color.rgba_hex())
|
||||
/// ```
|
||||
#[cfg(feature = "std")]
|
||||
pub fn rgba_hex(&self) -> String {
|
||||
format!(
|
||||
"{:02X?}{:02X?}{:02X?}{:02X?}",
|
||||
"{:02x?}{:02x?}{:02x?}{:02x?}",
|
||||
(self.r() * 255.) as u8,
|
||||
(self.g() * 255.) as u8,
|
||||
(self.b() * 255.) as u8,
|
||||
|
|
@ -826,15 +824,34 @@ impl Color {
|
|||
)
|
||||
}
|
||||
|
||||
/// Return a 6-character RGB, or 8-character RGBA, hex string (without a # prefix). The shorter form is used if the alpha is 1.
|
||||
///
|
||||
/// # Examples
|
||||
/// ```
|
||||
/// use graphene_core::raster::color::Color;
|
||||
/// let color1 = Color::from_rgba8_srgb(0x52, 0x67, 0xFA, 0x61).to_gamma_srgb();
|
||||
/// assert_eq!("3240a261", color1.rgb_optional_a_hex())
|
||||
/// let color2 = Color::from_rgba8_srgb(0x52, 0x67, 0xFA, 0x61).to_gamma_srgb();
|
||||
/// assert_eq!("3240a2", color2.rgb_optional_a_hex())
|
||||
/// ```
|
||||
#[cfg(feature = "std")]
|
||||
pub fn rgb_optional_a_hex(&self) -> String {
|
||||
let mut result = format!("{:02x?}{:02x?}{:02x?}", (self.r() * 255.) as u8, (self.g() * 255.) as u8, (self.b() * 255.) as u8);
|
||||
if self.a() < 1. {
|
||||
let _ = write!(&mut result, "{:02x?}", (self.a() * 255.) as u8);
|
||||
}
|
||||
result
|
||||
}
|
||||
|
||||
/// Return a 6-character RGB hex string (without a # prefix).
|
||||
/// ```
|
||||
/// use graphene_core::raster::color::Color;
|
||||
/// let color = Color::from_rgba8_srgb(0x52, 0x67, 0xFA, 0x61).to_gamma_srgb();
|
||||
/// assert_eq!("3240A2", color.rgb_hex())
|
||||
/// assert_eq!("3240a2", color.rgb_hex())
|
||||
/// ```
|
||||
#[cfg(feature = "std")]
|
||||
pub fn rgb_hex(&self) -> String {
|
||||
format!("{:02X?}{:02X?}{:02X?}", (self.r() * 255.) as u8, (self.g() * 255.) as u8, (self.b() * 255.) as u8)
|
||||
format!("{:02x?}{:02x?}{:02x?}", (self.r() * 255.) as u8, (self.g() * 255.) as u8, (self.b() * 255.) as u8)
|
||||
}
|
||||
|
||||
/// Return the all components as a u8 slice, first component is red, followed by green, followed by blue, followed by alpha.
|
||||
|
|
|
|||
|
|
@ -1,25 +1,13 @@
|
|||
//! Contains stylistic options for SVG elements.
|
||||
|
||||
use crate::consts::{LAYER_OUTLINE_STROKE_COLOR, LAYER_OUTLINE_STROKE_WEIGHT};
|
||||
use crate::renderer::format_transform_matrix;
|
||||
use crate::Color;
|
||||
|
||||
use dyn_any::{DynAny, StaticType};
|
||||
use glam::{DAffine2, DVec2};
|
||||
|
||||
use std::fmt::{self, Display, Write};
|
||||
|
||||
/// Precision of the opacity value in digits after the decimal point.
|
||||
/// A value of 3 would correspond to a precision of 10^-3.
|
||||
const OPACITY_PRECISION: usize = 3;
|
||||
|
||||
fn format_opacity(attribute: &str, opacity: f32) -> String {
|
||||
if (opacity - 1.).abs() > 10_f32.powi(-(OPACITY_PRECISION as i32)) {
|
||||
format!(r#" {attribute}="{opacity:.OPACITY_PRECISION$}""#)
|
||||
} else {
|
||||
String::new()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default, PartialEq, Eq, Clone, Copy, Debug, Hash, serde::Serialize, serde::Deserialize, DynAny, specta::Type)]
|
||||
pub enum GradientType {
|
||||
#[default]
|
||||
|
|
@ -134,7 +122,15 @@ impl Gradient {
|
|||
|
||||
let mut stop = String::new();
|
||||
for (position, color) in self.stops.0.iter() {
|
||||
let _ = write!(stop, r##"<stop offset="{}" stop-color="#{}" />"##, position, color.with_alpha(color.a()).rgba_hex());
|
||||
stop.push_str("<stop");
|
||||
if *position != 0. {
|
||||
let _ = write!(stop, r#" offset="{}""#, (position * 1_000_000.).round() / 1_000_000.);
|
||||
}
|
||||
let _ = write!(stop, r##" stop-color="#{}""##, color.rgb_hex());
|
||||
if color.a() < 1. {
|
||||
let _ = write!(stop, r#" stop-opacity="{}""#, (color.a() * 1000.).round() / 1000.);
|
||||
}
|
||||
stop.push_str(" />")
|
||||
}
|
||||
|
||||
let mod_gradient = transformed_bound_transform.inverse();
|
||||
|
|
@ -143,28 +139,25 @@ impl Gradient {
|
|||
let start = mod_points.transform_point2(self.start);
|
||||
let end = mod_points.transform_point2(self.end);
|
||||
|
||||
let transform = mod_gradient
|
||||
.to_cols_array()
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(i, entry)| entry.to_string() + if i == 5 { "" } else { "," })
|
||||
.collect::<String>();
|
||||
|
||||
let gradient_id = crate::uuid::generate_uuid();
|
||||
|
||||
let matrix = format_transform_matrix(mod_gradient);
|
||||
let gradient_transform = if matrix.is_empty() { String::new() } else { format!(r#" gradientTransform="{}""#, matrix) };
|
||||
|
||||
match self.gradient_type {
|
||||
GradientType::Linear => {
|
||||
let _ = write!(
|
||||
svg_defs,
|
||||
r#"<linearGradient id="{}" x1="{}" x2="{}" y1="{}" y2="{}" gradientTransform="matrix({})">{}</linearGradient>"#,
|
||||
gradient_id, start.x, end.x, start.y, end.y, transform, stop
|
||||
r#"<linearGradient id="{}" x1="{}" x2="{}" y1="{}" y2="{}"{gradient_transform}>{}</linearGradient>"#,
|
||||
gradient_id, start.x, end.x, start.y, end.y, stop
|
||||
);
|
||||
}
|
||||
GradientType::Radial => {
|
||||
let radius = (f64::powi(start.x - end.x, 2) + f64::powi(start.y - end.y, 2)).sqrt();
|
||||
let _ = write!(
|
||||
svg_defs,
|
||||
r#"<radialGradient id="{}" cx="{}" cy="{}" r="{}" gradientTransform="matrix({})">{}</radialGradient>"#,
|
||||
gradient_id, start.x, start.y, radius, transform, stop
|
||||
r#"<radialGradient id="{}" cx="{}" cy="{}" r="{}"{gradient_transform}>{}</radialGradient>"#,
|
||||
gradient_id, start.x, start.y, radius, stop
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -277,7 +270,13 @@ impl Fill {
|
|||
pub fn render(&self, svg_defs: &mut String, multiplied_transform: DAffine2, bounds: [DVec2; 2], transformed_bounds: [DVec2; 2]) -> String {
|
||||
match self {
|
||||
Self::None => r#" fill="none""#.to_string(),
|
||||
Self::Solid(color) => format!(r##" fill="#{}"{}"##, color.rgb_hex(), format_opacity("fill-opacity", color.a())),
|
||||
Self::Solid(color) => {
|
||||
let mut result = format!(r##" fill="#{}""##, color.rgb_hex());
|
||||
if color.a() < 1. {
|
||||
let _ = write!(result, r#" fill-opacity="{}""#, (color.a() * 1000.).round() / 1000.);
|
||||
}
|
||||
result
|
||||
}
|
||||
Self::Gradient(gradient) => {
|
||||
let gradient_id = gradient.render_defs(svg_defs, multiplied_transform, bounds, transformed_bounds);
|
||||
format!(r##" fill="url('#{gradient_id}')""##)
|
||||
|
|
@ -505,21 +504,45 @@ impl Stroke {
|
|||
|
||||
/// Provide the SVG attributes for the stroke.
|
||||
pub fn render(&self) -> String {
|
||||
if let Some(color) = self.color {
|
||||
format!(
|
||||
r##" stroke="#{}"{} stroke-width="{}" stroke-dasharray="{}" stroke-dashoffset="{}" stroke-linecap="{}" stroke-linejoin="{}" stroke-miterlimit="{}" "##,
|
||||
color.rgb_hex(),
|
||||
format_opacity("stroke-opacity", color.a()),
|
||||
self.weight,
|
||||
self.dash_lengths(),
|
||||
self.dash_offset,
|
||||
self.line_cap,
|
||||
self.line_join,
|
||||
self.line_join_miter_limit
|
||||
)
|
||||
} else {
|
||||
String::new()
|
||||
// Don't render a stroke at all if it would be invisible
|
||||
let Some(color) = self.color else { return String::new() };
|
||||
if self.weight <= 0. || color.a() == 0. {
|
||||
return String::new();
|
||||
}
|
||||
|
||||
// Set to None if the value is the SVG default
|
||||
let weight = (self.weight != 1.).then_some(self.weight);
|
||||
let dash_array = (!self.dash_lengths.is_empty()).then_some(self.dash_lengths());
|
||||
let dash_offset = (self.dash_offset != 0.).then_some(self.dash_offset);
|
||||
let line_cap = (self.line_cap != LineCap::Butt).then_some(self.line_cap);
|
||||
let line_join = (self.line_join != LineJoin::Miter).then_some(self.line_join);
|
||||
let line_join_miter_limit = (self.line_join_miter_limit != 4.).then_some(self.line_join_miter_limit);
|
||||
|
||||
// Render the needed stroke attributes
|
||||
let mut attributes = format!(r##" stroke="#{}""##, color.rgb_hex());
|
||||
if color.a() < 1. {
|
||||
let _ = write!(&mut attributes, r#" stroke-opacity="{}""#, (color.a() * 1000.).round() / 1000.);
|
||||
}
|
||||
if let Some(weight) = weight {
|
||||
let _ = write!(&mut attributes, r#" stroke-width="{}""#, weight);
|
||||
}
|
||||
if let Some(dash_array) = dash_array {
|
||||
let _ = write!(&mut attributes, r#" stroke-dasharray="{}""#, dash_array);
|
||||
}
|
||||
if let Some(dash_offset) = dash_offset {
|
||||
let _ = write!(&mut attributes, r#" stroke-dashoffset="{}""#, dash_offset);
|
||||
}
|
||||
if let Some(line_cap) = line_cap {
|
||||
let _ = write!(&mut attributes, r#" stroke-linecap="{}""#, line_cap);
|
||||
}
|
||||
if let Some(line_join) = line_join {
|
||||
let _ = write!(&mut attributes, r#" stroke-linejoin="{}""#, line_join);
|
||||
}
|
||||
if let Some(line_join_miter_limit) = line_join_miter_limit {
|
||||
let _ = write!(&mut attributes, r#" stroke-miterlimit="{}""#, line_join_miter_limit);
|
||||
}
|
||||
|
||||
attributes
|
||||
}
|
||||
|
||||
pub fn with_color(mut self, color: &Option<Color>) -> Option<Self> {
|
||||
|
|
|
|||
|
|
@ -279,6 +279,7 @@ pub fn convert_usvg_path(path: &usvg::Path) -> Vec<Subpath<PointId>> {
|
|||
subpaths
|
||||
}
|
||||
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
#[wasm_bindgen(module = "/../../frontend/src/utility-functions/computational-geometry.ts")]
|
||||
extern "C" {
|
||||
#[wasm_bindgen(js_name = booleanUnion)]
|
||||
|
|
@ -290,3 +291,19 @@ extern "C" {
|
|||
#[wasm_bindgen(js_name = booleanDifference)]
|
||||
fn boolean_difference(path1: String, path2: String) -> String;
|
||||
}
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
fn boolean_union(_path1: String, _path2: String) -> String {
|
||||
String::from("M0,0 L1,0 L1,1 L0,1 Z")
|
||||
}
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
fn boolean_subtract(_path1: String, _path2: String) -> String {
|
||||
String::from("M0,0 L1,0 L1,1 L0,1 Z")
|
||||
}
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
fn boolean_intersect(_path1: String, _path2: String) -> String {
|
||||
String::from("M0,0 L1,0 L1,1 L0,1 Z")
|
||||
}
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
fn boolean_difference(_path1: String, _path2: String) -> String {
|
||||
String::from("M0,0 L1,0 L1,1 L0,1 Z")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -93,7 +93,10 @@ fn render_svg(data: impl GraphicElementRendered, mut render: SvgRender, render_p
|
|||
attributes.push("y", "0");
|
||||
attributes.push("width", footprint.resolution.x.to_string());
|
||||
attributes.push("height", footprint.resolution.y.to_string());
|
||||
attributes.push("transform", format_transform_matrix(footprint.transform.inverse()));
|
||||
let matrix = format_transform_matrix(footprint.transform.inverse());
|
||||
if !matrix.is_empty() {
|
||||
attributes.push("transform", matrix);
|
||||
}
|
||||
attributes.push("fill", "white");
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,13 +5,8 @@ title = "Picking a task"
|
|||
order = 2 # Page number after chapter intro
|
||||
+++
|
||||
|
||||
The [task board](https://github.com/orgs/GraphiteEditor/projects/1/views/1) provides a list of [available tasks](https://github.com/orgs/GraphiteEditor/projects/1/views/5), as well as a [beginner-friendly](https://github.com/orgs/GraphiteEditor/projects/1/views/6) subset.
|
||||
The [task board](https://github.com/orgs/GraphiteEditor/projects/1/views/1) provides a list of [available tasks](https://github.com/orgs/GraphiteEditor/projects/1/views/5), as well as a [beginner-friendly](https://github.com/orgs/GraphiteEditor/projects/1/views/6) subset. Issues partially or fully involving web development can also be seen [listed here](https://github.com/orgs/GraphiteEditor/projects/1/views/5?filterQuery=status%3AShort-Term%2CMedium-Term%2CLonger-Term+label%3AWeb+-label%3ARust) which may involve HTML/CSS/TypeScript/Svelte, although depending on the task, it may also involve Rust (which can be a good way to get gently introduced to the language if you come from a web background).
|
||||
|
||||
If you have Rust and/or web experience, you may also pick based on:
|
||||
- [Only Rust](https://github.com/orgs/GraphiteEditor/projects/1/views/5?filterQuery=status%3AShort-Term%2CMedium-Term%2CLonger-Term+label%3ARust+-label%3AWeb) tasks
|
||||
- [Only web](https://github.com/orgs/GraphiteEditor/projects/1/views/5?filterQuery=status%3AShort-Term%2CMedium-Term%2CLonger-Term+label%3AWeb+-label%3ARust) tasks (HTML/CSS/TypeScript/Svelte)
|
||||
- [Combined Rust and web](https://github.com/orgs/GraphiteEditor/projects/1/views/5?filterQuery=status%3AShort-Term%2CMedium-Term%2CLonger-Term+label%3ARust+label%3AWeb) tasks
|
||||
Writing new documentation by commenting existing code is another valuable way to contribute as you learn from reading code.
|
||||
|
||||
Feel free to pick whatever task interests you, then comment on the issue that you would like to start. After commenting, you can dig in right away, then we will assign the issue to your GitHub user to keep the work status of tasks organized.
|
||||
|
||||
Writing new documentation by commenting existing code is another valuable way to contribute as you learn.
|
||||
Feel free to pick whatever task interests you, then comment on the issue that you would like to start. After commenting, you can dig in right away, then we will assign the issue to you once you have a PR ready. (Always remembering to leave a comment is important, since GitHub doesn't allow assigning issues to people who haven't commented on them.)
|
||||
|
|
|
|||
Loading…
Reference in New Issue