mirror of
https://github.com/GraphiteEditor/Graphite.git
synced 2026-09-20 03:18:06 +08:00
Make the ColorInput's gradient rendering strategy to use that of SliderInput (#4546)
Make the ColorInput's gradient rendering strategy to use that of SliderInput, fixing a flicker in Firefox when editing gradients
This commit is contained in:
@@ -546,7 +546,15 @@ fn populate_computed_display_fields(layout: &mut Layout) {
|
||||
for instance in layout.iter_mut() {
|
||||
match &mut *instance.widget {
|
||||
Widget::ColorInput(color_input) => {
|
||||
color_input.chosen_gradient = color_input.value.to_css_background_image();
|
||||
color_input.swatch_samples = match &color_input.value {
|
||||
FillChoice::None => Vec::new(),
|
||||
FillChoice::Solid(color) => vec![GradientSample::new(0., graphene_std::Color::from(*color))],
|
||||
FillChoice::Gradient(ramp) => graphene_std::vector::style::Gradient::from(&ramp.stops)
|
||||
.interpolated_samples_or_black(ramp.into())
|
||||
.into_iter()
|
||||
.map(|(position, color, _)| GradientSample::new(position, color))
|
||||
.collect(),
|
||||
};
|
||||
}
|
||||
Widget::TransferCurveInput(curve_input) => {
|
||||
const SAMPLE_COUNT: usize = 128;
|
||||
@@ -578,7 +586,7 @@ fn populate_computed_display_fields(layout: &mut Layout) {
|
||||
slider_input.track_samples = track_gradient
|
||||
.interpolated_samples_or_black(settings)
|
||||
.into_iter()
|
||||
.map(|(position, color, _)| SliderSample::new(position, color))
|
||||
.map(|(position, color, _)| GradientSample::new(position, color))
|
||||
.collect();
|
||||
// The end caps sample the track's boundary colors, which a cyclic wrap makes the wrapped interval's boundary-crossing color rather than the outermost stops'
|
||||
let track_evaluator = track_gradient.evaluator(settings);
|
||||
|
||||
@@ -193,11 +193,11 @@ pub struct ColorInput {
|
||||
// Content
|
||||
#[widget_builder(constructor)]
|
||||
pub value: FillChoice<SRGBA8>,
|
||||
/// CSS `linear-gradient(...)` (or solid-color stand-in) for the swatch's `background-image`. Auto-populated from `value` at layout-send time.
|
||||
/// `None` when `value` is `FillChoice::<SRGBA8>::None`, in which case the frontend uses its "none" fallback styling.
|
||||
#[serde(rename = "chosenGradient")]
|
||||
/// Straight-alpha color samples drawn by the frontend as the stops of an SVG gradient filling the swatch. Auto-populated from `value` at layout-send time.
|
||||
/// Empty when `value` is `FillChoice::<SRGBA8>::None`, in which case the frontend uses its "none" fallback styling.
|
||||
#[serde(rename = "swatchSamples")]
|
||||
#[widget_builder(skip)]
|
||||
pub chosen_gradient: Option<String>,
|
||||
pub swatch_samples: Vec<GradientSample>,
|
||||
#[serde(rename = "allowNone")]
|
||||
#[derivative(Default(value = "true"))]
|
||||
pub allow_none: bool,
|
||||
|
||||
@@ -641,10 +641,10 @@ pub struct SliderInput {
|
||||
/// The path the track's stops interpolate along, used to bake `track_samples` and by the frontend to suppress the midpoint diamonds when stepped.
|
||||
#[serde(rename = "trackInterpolation")]
|
||||
pub track_interpolation: GradientInterpolation,
|
||||
/// Straight-alpha samples the frontend draws as the stops of an SVG gradient filling the track strip. Auto-populated from `track` at layout-send time.
|
||||
/// Straight-alpha color samples drawn by the frontend as the stops of an SVG gradient filling the track strip. Auto-populated from `track` at layout-send time.
|
||||
#[serde(rename = "trackSamples")]
|
||||
#[widget_builder(skip)]
|
||||
pub track_samples: Vec<SliderSample>,
|
||||
pub track_samples: Vec<GradientSample>,
|
||||
/// Hex string for the track strip's leftmost solid-color end-cap. Auto-populated by evaluating `track` at position 0.
|
||||
#[serde(rename = "trackStartCSS")]
|
||||
#[widget_builder(skip)]
|
||||
@@ -748,8 +748,8 @@ impl SliderMarker {
|
||||
|
||||
#[cfg_attr(feature = "wasm", derive(tsify::Tsify))]
|
||||
#[derive(Clone, Debug, Default, PartialEq, serde::Serialize, serde::Deserialize)]
|
||||
pub struct SliderSample {
|
||||
/// Position (0..1) of the sample along the slider track, drawn as the SVG stop's `offset`.
|
||||
pub struct GradientSample {
|
||||
/// Position (0..1) of the sample along the gradient, drawn as the SVG stop's `offset`.
|
||||
position: f64,
|
||||
/// `#rrggbb` hex of the sample's color, drawn as the SVG stop's `stop-color`.
|
||||
color: String,
|
||||
@@ -757,7 +757,7 @@ pub struct SliderSample {
|
||||
alpha: f32,
|
||||
}
|
||||
|
||||
impl SliderSample {
|
||||
impl GradientSample {
|
||||
pub fn new(position: f64, color: Color) -> Self {
|
||||
Self {
|
||||
position,
|
||||
|
||||
@@ -1975,7 +1975,7 @@ fn apply_stops_update(data: &mut GradientToolData, context: &mut ToolActionMessa
|
||||
}
|
||||
|
||||
responses.add(PropertiesPanelMessage::Refresh);
|
||||
// Refresh the tool options so the swatch's `chosen_gradient` (precomputed CSS string) updates live as the user edits stops in the picker.
|
||||
// Refresh the tool options so the swatch's `swatch_samples` update live as the user edits stops in the picker.
|
||||
responses.add(ToolMessage::RefreshToolOptions);
|
||||
}
|
||||
|
||||
|
||||
@@ -3,13 +3,16 @@
|
||||
import ColorPicker from "/src/components/floating-menus/ColorPicker.svelte";
|
||||
import LayoutCol from "/src/components/layout/LayoutCol.svelte";
|
||||
import { contrastingOutlineFactor, fillChoiceColor, fillChoiceGradient } from "/src/utility-functions/colors";
|
||||
import type { FillChoice, MenuDirection, ActionShortcut, SRGBA8 } from "/wrapper/pkg/graphite_wasm_wrapper";
|
||||
import type { FillChoice, GradientSample, MenuDirection, ActionShortcut, SRGBA8 } from "/wrapper/pkg/graphite_wasm_wrapper";
|
||||
|
||||
const dispatch = createEventDispatcher<{ value: FillChoice<SRGBA8>; startHistoryTransaction: undefined }>();
|
||||
|
||||
// Document-unique `id` for this instance's SVG gradient, referenced by its `url(#...)`
|
||||
const gradientId = `color-input-gradient-${String(Math.random()).substring(2)}`;
|
||||
|
||||
// Content
|
||||
export let value: FillChoice<SRGBA8>;
|
||||
export let chosenGradient: string | undefined = undefined;
|
||||
export let swatchSamples: GradientSample[] = [];
|
||||
export let allowNone = false;
|
||||
// export let allowTransparency = false; // TODO: Implement
|
||||
export let menuDirection: MenuDirection = "Bottom";
|
||||
@@ -46,7 +49,18 @@
|
||||
{tooltipDescription}
|
||||
{tooltipShortcut}
|
||||
>
|
||||
<button style:--chosen-gradient={chosenGradient} style:--outline-amount={outlineFactor} on:click={() => (open = true)} tabindex="0" data-floating-menu-spawner></button>
|
||||
<button style:--outline-amount={outlineFactor} on:click={() => (open = true)} tabindex="0" data-floating-menu-spawner>
|
||||
{#if swatchSamples.length > 0}
|
||||
<svg class="swatch" xmlns="http://www.w3.org/2000/svg">
|
||||
<linearGradient id={gradientId} x1="0" y1="0" x2="1" y2="0">
|
||||
{#each swatchSamples as sample}
|
||||
<stop offset={sample.position} stop-color={sample.color} stop-opacity={sample.alpha} />
|
||||
{/each}
|
||||
</linearGradient>
|
||||
<rect width="100%" height="100%" fill={`url(#${gradientId})`} />
|
||||
</svg>
|
||||
{/if}
|
||||
</button>
|
||||
<ColorPicker
|
||||
{open}
|
||||
{disabled}
|
||||
@@ -85,14 +99,12 @@
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
|
||||
&::before {
|
||||
content: "";
|
||||
> .swatch {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
background: var(--chosen-gradient);
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.text-label {
|
||||
@@ -150,8 +162,8 @@
|
||||
background: var(--color-e-nearwhite);
|
||||
background-image: none;
|
||||
|
||||
&::before {
|
||||
background: var(--color-e-nearwhite);
|
||||
> .swatch {
|
||||
display: none;
|
||||
}
|
||||
|
||||
&::after {
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
import { preventEscapeClosingParentFloatingMenu } from "/src/components/layout/FloatingMenu.svelte";
|
||||
import LayoutCol from "/src/components/layout/LayoutCol.svelte";
|
||||
import LayoutRow from "/src/components/layout/LayoutRow.svelte";
|
||||
import type { GradientInterpolation, SliderInputUpdate, SliderMarker, SliderSample } from "/wrapper/pkg/graphite_wasm_wrapper";
|
||||
import type { GradientInterpolation, GradientSample, SliderInputUpdate, SliderMarker } from "/wrapper/pkg/graphite_wasm_wrapper";
|
||||
|
||||
const BUTTON_LEFT = 0;
|
||||
const BUTTON_RIGHT = 2;
|
||||
@@ -13,7 +13,7 @@
|
||||
// Document-unique `id` for this instance's SVG gradient, referenced by its `url(#...)`
|
||||
const gradientId = `slider-input-gradient-${String(Math.random()).substring(2)}`;
|
||||
|
||||
export let trackSamples: SliderSample[];
|
||||
export let trackSamples: GradientSample[];
|
||||
export let trackStartCSS: string;
|
||||
export let trackEndCSS: string;
|
||||
export let trackCyclic = false;
|
||||
|
||||
@@ -90,13 +90,6 @@ impl From<&GradientStops<SRGBA8>> for Gradient {
|
||||
}
|
||||
}
|
||||
|
||||
impl GradientStops<SRGBA8> {
|
||||
/// CSS `background-image` value drawing the stops as an SVG data URI, keeping straight-alpha interpolation.
|
||||
pub fn to_svg_background_image(&self, settings: GradientSettings) -> String {
|
||||
Gradient::from(self).to_svg_background_image(settings)
|
||||
}
|
||||
}
|
||||
|
||||
/// The serialized exchange form of a gradient: its stops, with whole-ramp settings as sibling fields serialized
|
||||
/// only when non-default. The space is the exception: it always serializes, so its absence marks a ramp
|
||||
/// from before the field existed, which deserializes as the gamma those documents rendered with.
|
||||
@@ -1440,28 +1433,6 @@ impl Gradient {
|
||||
if samples.is_empty() { vec![(0., Color::BLACK, None)] } else { samples }
|
||||
}
|
||||
|
||||
/// Build a CSS `background-image` value embedding the gradient as an SVG data URI, sampling the midpoint curves, color
|
||||
/// space, and spline. SVG interpolates its stops with straight alpha, matching the canvas renderers, where a CSS
|
||||
/// `linear-gradient` interpolates premultiplied and would hide the pull a transparent stop's RGB exerts on the render.
|
||||
pub fn to_svg_background_image(&self, settings: GradientSettings) -> String {
|
||||
use std::fmt::Write;
|
||||
|
||||
let mut stops = String::new();
|
||||
for (position, color, _) in self.interpolated_samples_or_black(settings) {
|
||||
let srgba = SRGBA8::from(color);
|
||||
let _ = write!(stops, "<stop offset='{}' stop-color='#{}'", (position * 1e4).round() / 1e4, srgba.to_rgb_hex());
|
||||
if srgba.alpha < 255 {
|
||||
let _ = write!(stops, " stop-opacity='{}'", (color.a() as f64 * 1000.).round() / 1000.);
|
||||
}
|
||||
stops.push_str("/>");
|
||||
}
|
||||
|
||||
// A sizeless SVG stretches to fill the CSS background area; the encoding covers the URI-hostile characters
|
||||
let svg = format!("<svg xmlns='http://www.w3.org/2000/svg'><linearGradient id='g' x1='0' y1='0' x2='1' y2='0'>{stops}</linearGradient><rect width='100%' height='100%' fill='url(#g)'/></svg>");
|
||||
let encoded = svg.replace('%', "%25").replace('#', "%23").replace('<', "%3C").replace('>', "%3E");
|
||||
format!("url(\"data:image/svg+xml,{encoded}\")")
|
||||
}
|
||||
|
||||
/// Produce a set of linearly-interpolated color samples that approximate the gradient's true curve.
|
||||
///
|
||||
/// Each sample is `(position, color, original_midpoint)` where `original_midpoint` is `Some(f64)` with the corresponding
|
||||
@@ -2383,26 +2354,6 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn svg_background_image_percent_encodes_and_keeps_straight_alpha_stops() {
|
||||
let mut gradient = Gradient::from(vec![Color::BLACK, Color::WHITE]);
|
||||
gradient.set_color(1, Color::from_rgbaf32_unchecked(1., 1., 1., 0.5));
|
||||
|
||||
let image = gradient.to_svg_background_image(GradientSettings::default());
|
||||
|
||||
assert!(image.starts_with("url(\"data:image/svg+xml,"), "the value should be an SVG data URI: {image}");
|
||||
assert!(image.contains("stop-opacity='0.5'"), "a transparent stop should emit its straight alpha: {image}");
|
||||
assert!(!image.contains(['#', '<', '>']), "URI-hostile characters should be percent-encoded: {image}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn svg_background_image_paints_a_stopless_gradient_black() {
|
||||
let image = Gradient::from(Vec::new()).to_svg_background_image(GradientSettings::default());
|
||||
|
||||
// The hex color's `#` arrives percent-encoded
|
||||
assert!(image.contains("stop-color='%23000000'"), "a gradient with no stops should paint black rather than nothing: {image}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn clear_spread_evaluates_to_transparency_outside_the_unit_range() {
|
||||
let gradient = Gradient::from(vec![Color::BLACK, Color::WHITE]);
|
||||
|
||||
@@ -66,21 +66,6 @@ impl<C> FillChoice<C> {
|
||||
}
|
||||
}
|
||||
|
||||
impl FillChoice<SRGBA8> {
|
||||
/// Build a CSS `background-image` string representing this fill, or `None` if the fill is [`FillChoice::None`].
|
||||
/// Solid colors become a degenerate gradient between the same color so the CSS variable can always be assigned to a `background-image`.
|
||||
pub fn to_css_background_image(&self) -> Option<String> {
|
||||
match self {
|
||||
Self::None => None,
|
||||
Self::Solid(srgba) => {
|
||||
let hex = srgba.to_rgba_hex();
|
||||
Some(format!("linear-gradient(#{hex}, #{hex})"))
|
||||
}
|
||||
Self::Gradient(ramp) => Some(ramp.stops.to_svg_background_image(ramp.into())),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The stroke (outline) style of an SVG element.
|
||||
#[repr(C)]
|
||||
#[cfg_attr(feature = "wasm", derive(tsify::Tsify))]
|
||||
|
||||
Reference in New Issue
Block a user