mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-10-25 02:07:09 -06:00
Gradient chips: follow the angle and the gradient type (#2494)
gradient_chip now takes the GradientPaint and lays the gradient out as it would be on an object of the chip's shape: linear along its angle, radial from the centre, freeform from its colour points. Gradient panel swatch menu and the Swatch Options dialog carry the full paint. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
committed by
bflatastic
co-authored by
Claude Opus 5.5
parent
39979d4753
commit
577239d734
@@ -76,7 +76,7 @@ pub fn open(app: &mut VectorcraftApp, name: &str) -> Result<Value, String> {
|
||||
Ok(Value::Null)
|
||||
}
|
||||
Paint::Gradient(g) => {
|
||||
let fields = json!({"__swatch": name, "name": name, "__gradient": g.gradient, "preview": true});
|
||||
let fields = json!({"__swatch": name, "name": name, "__gradient": g, "preview": true});
|
||||
app.ui.dialog = Some(Dialog::new(KIND, fields));
|
||||
Ok(Value::Null)
|
||||
}
|
||||
@@ -105,7 +105,7 @@ fn edit_params(d: &Dialog) -> Value {
|
||||
}
|
||||
|
||||
fn body(app: &mut VectorcraftApp, ui: &mut egui::Ui, d: &mut Dialog) -> bool {
|
||||
let changed = match d.fields.get("__gradient").and_then(|g| serde_json::from_value::<vectorcraft_color::Gradient>(g.clone()).ok()) {
|
||||
let changed = match d.fields.get("__gradient").and_then(|g| serde_json::from_value::<vectorcraft_color::GradientPaint>(g.clone()).ok()) {
|
||||
Some(g) => {
|
||||
let changed = grid(ui, |ui| name_row(ui, d));
|
||||
ui.add_space(6.0);
|
||||
|
||||
@@ -165,14 +165,14 @@ fn save_to_swatches(app: &mut VectorcraftApp, g: &GradientPaint) {
|
||||
}
|
||||
|
||||
/// The document's gradient swatches (groups included): name and gradient.
|
||||
fn gradient_swatches(app: &VectorcraftApp) -> Vec<(String, Gradient)> {
|
||||
fn gradient_swatches(app: &VectorcraftApp) -> Vec<(String, GradientPaint)> {
|
||||
let Some(st) = app.session.active() else { return vec![] };
|
||||
let d = &st.doc;
|
||||
d.swatches
|
||||
.iter()
|
||||
.chain(d.swatch_groups.iter().flat_map(|g| g.swatches.iter()))
|
||||
.filter_map(|s| match &s.paint {
|
||||
Paint::Gradient(g) => Some((s.name.clone(), g.gradient.clone())),
|
||||
Paint::Gradient(g) => Some((s.name.clone(), (**g).clone())),
|
||||
_ => None,
|
||||
})
|
||||
.collect()
|
||||
@@ -370,7 +370,7 @@ fn freeform_section(app: &mut VectorcraftApp, ui: &mut Ui, g: &GradientPaint) {
|
||||
fn thumbnail(app: &mut VectorcraftApp, ui: &mut Ui, g: &GradientPaint, is_grad: bool) {
|
||||
let t = Tokens::get(ui.ctx());
|
||||
let (r, resp) = ui.allocate_exact_size(vec2(40.0, 40.0), Sense::click_and_drag());
|
||||
widgets::gradient_chip(ui, r, &g.gradient);
|
||||
widgets::gradient_chip(ui, r, g);
|
||||
ui.painter().rect_stroke(r, 0.0, Stroke::new(1.0, t.border), StrokeKind::Inside);
|
||||
let resp = resp.on_hover_text(tl!("Gradient Fill: click to apply, drag onto art"));
|
||||
if resp.clicked() && !is_grad {
|
||||
|
||||
@@ -471,7 +471,7 @@ fn bottom_controls(app: &mut VectorcraftApp, ui: &mut Ui, t: &Tokens, cols: usiz
|
||||
let (r, resp) = ui.allocate_exact_size(vec2(chip, chip), Sense::click());
|
||||
match cmd {
|
||||
"paint.lastColor" => widgets::paint_chip(ui, r, &Paint::solid(app.session.last_solid)),
|
||||
"paint.lastGradient" => widgets::gradient_chip(ui, r, &app.session.last_gradient.gradient),
|
||||
"paint.lastGradient" => widgets::gradient_chip(ui, r, &app.session.last_gradient),
|
||||
_ => widgets::paint_chip(ui, r, &Paint::None),
|
||||
}
|
||||
if resp.on_hover_text(tl!(tip)).clicked() {
|
||||
|
||||
@@ -554,7 +554,7 @@ pub fn paint_chip(ui: &Ui, rect: Rect, paint: &Paint) {
|
||||
let [r, g, b, _] = color.to_rgba8(1.0);
|
||||
p.rect_filled(rect, 0.0, Color32::from_rgb(r, g, b));
|
||||
}
|
||||
Paint::Gradient(gp) => gradient_chip(ui, rect, &gp.gradient),
|
||||
Paint::Gradient(gp) => gradient_chip(ui, rect, gp),
|
||||
Paint::Pattern { .. } => {
|
||||
p.rect_filled(rect, 0.0, Color32::from_gray(200));
|
||||
for i in 0..4 {
|
||||
@@ -565,32 +565,68 @@ pub fn paint_chip(ui: &Ui, rect: Rect, paint: &Paint) {
|
||||
}
|
||||
}
|
||||
|
||||
/// Draw a gradient preview into `rect` (linear left to right; radial from the centre).
|
||||
pub fn gradient_chip(ui: &Ui, rect: Rect, gradient: &vectorcraft_color::Gradient) {
|
||||
let p = ui.painter();
|
||||
let radial = gradient.kind == vectorcraft_color::GradientKind::Radial;
|
||||
if radial {
|
||||
// Outer colour fills the corners beyond the largest circle.
|
||||
let (c, _) = gradient.sample(1.0);
|
||||
let [r, g, b, _] = c.to_rgba8(1.0);
|
||||
p.rect_filled(rect, 0.0, Color32::from_rgb(r, g, b));
|
||||
/// Draw a gradient preview into `rect`, laid out as the gradient would be on an object of that
|
||||
/// shape: linear along its angle, radial from the centre, freeform from its colour points.
|
||||
pub fn gradient_chip(ui: &Ui, rect: Rect, gradient: &vectorcraft_color::GradientPaint) {
|
||||
if !(rect.width() > 0.0 && rect.height() > 0.0) {
|
||||
return;
|
||||
}
|
||||
let n = 24;
|
||||
let w = rect.width() / n as f32;
|
||||
for i in 0..n {
|
||||
let tt = (i as f32 + 0.5) / n as f32;
|
||||
if radial {
|
||||
let (c, _) = gradient.sample(1.0 - tt);
|
||||
let [r, g, b, _] = c.to_rgba8(1.0);
|
||||
let s = rect.width().min(rect.height()) * tt / 2.0;
|
||||
p.circle_filled(rect.center(), s.max(0.5), Color32::from_rgb(r, g, b));
|
||||
} else {
|
||||
let (c, _) = gradient.sample(tt);
|
||||
let [r, g, b, _] = c.to_rgba8(1.0);
|
||||
let rr = Rect::from_min_size(pos2(rect.left() + i as f32 * w, rect.top()), vec2(w + 0.5, rect.height()));
|
||||
p.rect_filled(rr, 0.0, Color32::from_rgb(r, g, b));
|
||||
// Freeform sampling is costly per spot, so it gets a coarse grid (the mesh interpolates).
|
||||
let freeform = gradient.gradient.kind == vectorcraft_color::GradientKind::Freeform;
|
||||
let max = if freeform { 16 } else { 64 };
|
||||
let cols = ((rect.width() / 3.0).ceil() as usize).clamp(1, max);
|
||||
let rows = ((rect.height() / 3.0).ceil() as usize).clamp(1, max);
|
||||
let colors = gradient_chip_colors(gradient, rect.width(), rect.height(), cols, rows);
|
||||
let mut mesh = egui::Mesh::default();
|
||||
for (k, color) in colors.into_iter().enumerate() {
|
||||
let (i, j) = (k % (cols + 1), k / (cols + 1));
|
||||
let at = pos2(rect.left() + rect.width() * i as f32 / cols as f32, rect.top() + rect.height() * j as f32 / rows as f32);
|
||||
mesh.colored_vertex(at, color);
|
||||
}
|
||||
for j in 0..rows as u32 {
|
||||
for i in 0..cols as u32 {
|
||||
let v = j * (cols as u32 + 1) + i;
|
||||
mesh.add_triangle(v, v + 1, v + cols as u32 + 1);
|
||||
mesh.add_triangle(v + 1, v + cols as u32 + 2, v + cols as u32 + 1);
|
||||
}
|
||||
}
|
||||
ui.painter().add(egui::Shape::mesh(mesh));
|
||||
}
|
||||
|
||||
/// The colours at the `(cols + 1) x (rows + 1)` grid points (row by row) of a `width` x `height`
|
||||
/// chip showing `gp` as it would look on an object of that size.
|
||||
fn gradient_chip_colors(gp: &vectorcraft_color::GradientPaint, width: f32, height: f32, cols: usize, rows: usize) -> Vec<Color32> {
|
||||
use vectorcraft_color::{GradientGeom, GradientKind};
|
||||
let b = vectorcraft_geom::kurbo::Rect::new(0.0, 0.0, width as f64, height as f64);
|
||||
let rgb = |c: vectorcraft_color::Color| {
|
||||
let [r, g, b, _] = c.to_rgba8(1.0);
|
||||
Color32::from_rgb(r, g, b)
|
||||
};
|
||||
let at = |k: usize| {
|
||||
let (i, j) = (k % (cols + 1), k / (cols + 1));
|
||||
vectorcraft_geom::kurbo::Point::new(b.width() * i as f64 / cols as f64, b.height() * j as f64 / rows as f64)
|
||||
};
|
||||
let n = (cols + 1) * (rows + 1);
|
||||
if gp.gradient.kind == GradientKind::Freeform {
|
||||
let Some(painted) = vectorcraft_color::freeform::painted_box(b) else { return vec![Color32::TRANSPARENT; n] };
|
||||
let field = gp.freeform_on(painted).field(vectorcraft_color::freeform::spread_scale(painted));
|
||||
return (0..n)
|
||||
.map(|k| {
|
||||
let ([r, g, b], _) = field.sample(at(k));
|
||||
let q = |v: f32| (v.clamp(0.0, 1.0) * 255.0).round() as u8;
|
||||
Color32::from_rgb(q(r), q(g), q(b))
|
||||
})
|
||||
.collect();
|
||||
}
|
||||
// A placed gradient keeps its direction; a fresh one is fitted to the chip at its angle.
|
||||
let angle = gp.geom.map_or(gp.angle, |g| g.angle_deg());
|
||||
let geom = GradientGeom::fit(gp.gradient.kind, b, if angle.is_finite() { angle } else { 0.0 });
|
||||
(0..n)
|
||||
.map(|k| {
|
||||
let t = geom.param_at(gp.gradient.kind, at(k)).clamp(0.0, 1.0) as f32;
|
||||
rgb(gp.gradient.sample(t).0)
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// A Fill chip of `paint` into `rect` or, with `ring`, a Stroke chip: a frame `ring` wide round a
|
||||
@@ -2041,3 +2077,37 @@ mod menu_room_tests {
|
||||
assert_eq!(menu_room(300.0, 290.0), 120.0);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod gradient_chip_tests {
|
||||
use super::*;
|
||||
use vectorcraft_color::{Gradient, GradientKind, GradientPaint};
|
||||
|
||||
fn paint(kind: GradientKind, angle: f64) -> GradientPaint {
|
||||
let mut gp = GradientPaint::new(Gradient { kind, ..Gradient::default() });
|
||||
gp.angle = angle;
|
||||
gp
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn linear_chip_follows_the_angle() {
|
||||
// 5 x 5 grid points on a 20 x 20 chip; White (start) to Black (end).
|
||||
let at = |colors: &[Color32], i: usize, j: usize| colors[j * 5 + i];
|
||||
let side = gradient_chip_colors(&paint(GradientKind::Linear, 0.0), 20.0, 20.0, 4, 4);
|
||||
assert_eq!(at(&side, 0, 2), Color32::WHITE);
|
||||
assert_eq!(at(&side, 4, 2), Color32::BLACK);
|
||||
assert_eq!(at(&side, 2, 0), at(&side, 2, 4));
|
||||
// 90 degrees runs bottom to top: constant along rows, white at the bottom.
|
||||
let up = gradient_chip_colors(&paint(GradientKind::Linear, 90.0), 20.0, 20.0, 4, 4);
|
||||
assert_eq!(at(&up, 0, 4), Color32::WHITE);
|
||||
assert_eq!(at(&up, 0, 0), Color32::BLACK);
|
||||
assert_eq!(at(&up, 0, 2), at(&up, 4, 2));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn radial_chip_runs_from_the_centre_out() {
|
||||
let colors = gradient_chip_colors(&paint(GradientKind::Radial, 0.0), 20.0, 20.0, 4, 4);
|
||||
assert_eq!(colors[2 * 5 + 2], Color32::WHITE);
|
||||
assert_eq!(colors[0], Color32::BLACK);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user