color picker and palette editor
git clone https://git.lucas.co/cce-color-editor.git
Update components and system settings
src/main.rs | 404 +++++++++++++++++++++++++++++++++++++++++++++++++++++++-----
1 file changed, 372 insertions(+), 32 deletions(-)
diff --git a/src/main.rs b/src/main.rs
index abf3259..0f46b0c 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -49,6 +49,21 @@ fn quad_vertices(x: f32, y: f32, w: f32, h: f32, sw: f32, sh: f32, c: [f32; 4])
]
}
+fn gradient_quad_vertices(x: f32, y: f32, w: f32, h: f32, sw: f32, sh: f32, c0: [f32; 4], c1: [f32; 4]) -> [Vertex; 6] {
+ let x0 = (x / sw) * 2.0 - 1.0;
+ let y0 = 1.0 - (y / sh) * 2.0;
+ let x1 = ((x + w) / sw) * 2.0 - 1.0;
+ let y1 = 1.0 - ((y + h) / sh) * 2.0;
+ [
+ Vertex { position: [x0, y0], color: c0 },
+ Vertex { position: [x1, y0], color: c1 },
+ Vertex { position: [x0, y1], color: c0 },
+ Vertex { position: [x1, y0], color: c1 },
+ Vertex { position: [x1, y1], color: c1 },
+ Vertex { position: [x0, y1], color: c0 },
+ ]
+}
+
fn make_text_buffer(fs: &mut FontSystem, text: &str, size: f32) -> Buffer {
let metrics = Metrics::new(size, size * 1.4);
let mut buf = Buffer::new(fs, metrics);
@@ -71,6 +86,54 @@ fn parse_hex(hex: &str) -> Option<(f32, f32, f32)> {
}
}
+fn rgb_to_hsl(r: f32, g: f32, b: f32) -> (f32, f32, f32) {
+ let max = r.max(g.max(b));
+ let min = r.min(g.min(b));
+ let mut h = 0.0;
+ let mut s = 0.0;
+ let l = (max + min) / 2.0;
+
+ if max != min {
+ let d = max - min;
+ s = if l > 0.5 { d / (2.0 - max - min) } else { d / (max + min) };
+ if max == r {
+ h = (g - b) / d + (if g < b { 6.0 } else { 0.0 });
+ } else if max == g {
+ h = (b - r) / d + 2.0;
+ } else if max == b {
+ h = (r - g) / d + 4.0;
+ }
+ h /= 6.0;
+ }
+
+ (h, s, l)
+}
+
+fn hsl_to_rgb(h: f32, s: f32, l: f32) -> (f32, f32, f32) {
+ if s == 0.0 {
+ return (l, l, l);
+ }
+
+ let q = if l < 0.5 { l * (1.0 + s) } else { l + s - l * s };
+ let p = 2.0 * l - q;
+
+ let r = hue_to_rgb(p, q, h + 1.0 / 3.0);
+ let g = hue_to_rgb(p, q, h);
+ let b = hue_to_rgb(p, q, h - 1.0 / 3.0);
+
+ (r, g, b)
+}
+
+fn hue_to_rgb(p: f32, q: f32, mut t: f32) -> f32 {
+ if t < 0.0 { t += 1.0; }
+ if t > 1.0 { t -= 1.0; }
+ if t < 1.0 / 6.0 { return p + (q - p) * 6.0 * t; }
+ if t < 1.0 / 2.0 { return q; }
+ if t < 2.0 / 3.0 { return p + (q - p) * (2.0 / 3.0 - t) * 6.0; }
+ p
+}
+
+
const HEADER_H: f32 = 36.0;
const SLIDER_ROW_H: f32 = 36.0;
const SLIDER_START_Y: f32 = 48.0;
@@ -80,21 +143,27 @@ const SLIDER_TRACK_W: f32 = 280.0;
const SLIDER_TRACK_H: f32 = 20.0;
const SLIDER_VALUE_X: f32 = 320.0;
const PREVIEW_X: f32 = 12.0;
-const PREVIEW_Y: f32 = 168.0;
+const PREVIEW_Y: f32 = 276.0;
const PREVIEW_W: f32 = 160.0;
const PREVIEW_H: f32 = 72.0;
-const BUTTON_Y: f32 = 252.0;
+const BUTTON_Y: f32 = 360.0;
const BUTTON_H: f32 = 32.0;
const BUTTON_W: f32 = 100.0;
const BUTTON_GAP: f32 = 12.0;
const WIN_W: f32 = 380.0;
-const WIN_H: f32 = 320.0;
+const WIN_H: f32 = 428.0;
struct RectWidget {
x: f32, y: f32, w: f32, h: f32,
color: [f32; 4],
}
+struct GradientRectWidget {
+ x: f32, y: f32, w: f32, h: f32,
+ c0: [f32; 4],
+ c1: [f32; 4],
+}
+
struct TextItem {
buffer: Buffer,
x: f32, y: f32,
@@ -102,7 +171,7 @@ struct TextItem {
}
#[derive(Clone, Copy, PartialEq)]
-enum DragTarget { Red, Green, Blue }
+enum DragTarget { Red, Green, Blue, Hue, Saturation, Lightness }
#[derive(Clone, Copy, PartialEq)]
enum Action { Apply, Cancel }
@@ -125,6 +194,9 @@ struct ColorApp {
red: f32,
green: f32,
blue: f32,
+ hue: f32,
+ saturation: f32,
+ lightness: f32,
font_system: FontSystem,
swash_cache: SwashCache,
@@ -133,6 +205,7 @@ struct ColorApp {
text_viewport: Viewport,
rects: Vec<RectWidget>,
+ gradient_rects: Vec<GradientRectWidget>,
text_items: Vec<TextItem>,
action_buttons: Vec<HitButton>,
@@ -233,12 +306,16 @@ impl ColorApp {
let scale_factor = (window.scale_factor() as f32).max(2.0) as f64;
+ let (hue, saturation, lightness) = rgb_to_hsl(red, green, blue);
let mut app = Self {
window, surface, device, queue, config, render_pipeline,
vertex_buffer, vertex_count: 0,
red, green, blue,
+ hue, saturation, lightness,
font_system, swash_cache, text_atlas, text_renderer, text_viewport,
- rects: Vec::new(), text_items: Vec::new(),
+ rects: Vec::new(),
+ gradient_rects: Vec::new(),
+ text_items: Vec::new(),
action_buttons: Vec::new(),
cursor_x: 0.0, cursor_y: 0.0,
dragging: None,
@@ -261,6 +338,7 @@ impl ColorApp {
fn rebuild_layout(&mut self, sw: f32, sh: f32) {
let s = self.scale_factor as f32;
let mut rects = Vec::new();
+ let mut gradient_rects = Vec::new();
let mut text_items = Vec::new();
let mut action_buttons = Vec::new();
@@ -279,40 +357,103 @@ impl ColorApp {
color: color::CONTENT_BG,
});
- let channels = [self.red, self.green, self.blue];
- let labels = ['R', 'G', 'B'];
- let fill_colors = [
- [1.0, 0.0, 0.0, 1.0],
- [0.0, 1.0, 0.0, 1.0],
- [0.0, 0.0, 1.0, 1.0],
- ];
+ let channels = [self.red, self.green, self.blue, self.hue, self.saturation, self.lightness];
+ let labels = ['R', 'G', 'B', 'H', 'S', 'L'];
+
+ let red = self.red;
+ let green = self.green;
+ let blue = self.blue;
+ let hue = self.hue;
+ let saturation = self.saturation;
+ let lightness = self.lightness;
+
+ let get_color_at = |i: usize, t: f32| -> [f32; 4] {
+ match i {
+ 0 => [t, green, blue, 1.0],
+ 1 => [red, t, blue, 1.0],
+ 2 => [red, green, t, 1.0],
+ 3 => {
+ let (r, g, b) = hsl_to_rgb(t, saturation, lightness);
+ [r, g, b, 1.0]
+ }
+ 4 => {
+ let (r, g, b) = hsl_to_rgb(hue, t, lightness);
+ [r, g, b, 1.0]
+ }
+ _ => {
+ let (r, g, b) = hsl_to_rgb(hue, saturation, t);
+ [r, g, b, 1.0]
+ }
+ }
+ };
- for i in 0..3 {
+ for i in 0..6 {
let row_y = (SLIDER_START_Y + i as f32 * SLIDER_ROW_H) * s;
let track_y = row_y + ((SLIDER_ROW_H - SLIDER_TRACK_H) / 2.0) * s;
+ // Draw track border
rects.push(RectWidget {
- x: SLIDER_TRACK_X * s, y: track_y,
- w: SLIDER_TRACK_W * s, h: SLIDER_TRACK_H * s,
- color: [0.18, 0.18, 0.22, 1.0],
+ x: (SLIDER_TRACK_X - 1.0) * s,
+ y: track_y - 1.0 * s,
+ w: (SLIDER_TRACK_W + 2.0) * s,
+ h: (SLIDER_TRACK_H + 2.0) * s,
+ color: [0.08, 0.08, 0.10, 1.0],
});
- let fill_w = SLIDER_TRACK_W * channels[i] * s;
- if fill_w > 0.0 {
- rects.push(RectWidget {
- x: SLIDER_TRACK_X * s, y: track_y,
- w: fill_w, h: SLIDER_TRACK_H * s,
- color: fill_colors[i],
+ // Draw gradient track
+ let n_segments = if i == 3 { 30 } else { 10 };
+ for j in 0..n_segments {
+ let t0 = j as f32 / n_segments as f32;
+ let t1 = (j + 1) as f32 / n_segments as f32;
+ let c0 = get_color_at(i, t0);
+ let c1 = get_color_at(i, t1);
+ gradient_rects.push(GradientRectWidget {
+ x: (SLIDER_TRACK_X + t0 * SLIDER_TRACK_W) * s,
+ y: track_y,
+ w: ((t1 - t0) * SLIDER_TRACK_W) * s,
+ h: SLIDER_TRACK_H * s,
+ c0,
+ c1,
});
}
+ // Draw vertical bar indicator at current position
+ let indicator_w = 4.0;
+ let indicator_h = SLIDER_TRACK_H + 4.0;
+ let indicator_x = SLIDER_TRACK_X + channels[i] * SLIDER_TRACK_W - indicator_w / 2.0;
+ let indicator_y = (SLIDER_START_Y + i as f32 * SLIDER_ROW_H) + ((SLIDER_ROW_H - indicator_h) / 2.0);
+
+ // Indicator border/shadow
+ rects.push(RectWidget {
+ x: (indicator_x - 1.0) * s,
+ y: (indicator_y - 1.0) * s,
+ w: (indicator_w + 2.0) * s,
+ h: (indicator_h + 2.0) * s,
+ color: [0.05, 0.05, 0.05, 0.95],
+ });
+
+ // Indicator body
+ rects.push(RectWidget {
+ x: indicator_x * s,
+ y: indicator_y * s,
+ w: indicator_w * s,
+ h: indicator_h * s,
+ color: [1.0, 1.0, 1.0, 1.0],
+ });
+
text_items.push(TextItem {
buffer: make_text_buffer(&mut self.font_system, &labels[i].to_string(), 12.0 * s),
x: SLIDER_LABEL_X * s, y: row_y + 4.0 * s,
color: glyphon::Color::rgb(0xaa, 0xaa, 0xbb),
});
- let val = format!("{}", (channels[i] * 255.0) as u8);
+ let val = if i < 3 {
+ format!("{}", (channels[i] * 255.0) as u8)
+ } else if i == 3 {
+ format!("{}°", (channels[i] * 360.0).round() as u16)
+ } else {
+ format!("{}%", (channels[i] * 100.0).round() as u8)
+ };
text_items.push(TextItem {
buffer: make_text_buffer(&mut self.font_system, &val, 11.0 * s),
x: SLIDER_VALUE_X * s, y: row_y + 4.0 * s,
@@ -374,6 +515,7 @@ impl ColorApp {
});
self.rects = rects;
+ self.gradient_rects = gradient_rects;
self.text_items = text_items;
self.action_buttons = action_buttons;
self.needs_rebuild = false;
@@ -392,6 +534,9 @@ impl ColorApp {
for r in &self.rects {
verts.extend(quad_vertices(r.x, r.y, r.w, r.h, sw, sh, r.color));
}
+ for g in &self.gradient_rects {
+ verts.extend(gradient_quad_vertices(g.x, g.y, g.w, g.h, sw, sh, g.c0, g.c1));
+ }
verts
}
@@ -449,33 +594,226 @@ impl ColorApp {
self.cursor_y = position.y as f32;
if let Some(drag) = self.dragging {
let i = match drag {
- DragTarget::Red => 0, DragTarget::Green => 1, DragTarget::Blue => 2,
+ DragTarget::Red => 0,
+ DragTarget::Green => 1,
+ DragTarget::Blue => 2,
+ DragTarget::Hue => 3,
+ DragTarget::Saturation => 4,
+ DragTarget::Lightness => 5,
};
let s = self.scale_factor as f32;
let (tx, _, tw, _) = Self::slider_physical_rect(i, s);
let new_val = ((self.cursor_x - tx) / tw).clamp(0.0, 1.0);
- let old = match i { 0 => self.red, 1 => self.green, _ => self.blue };
+ let old = match i {
+ 0 => self.red,
+ 1 => self.green,
+ 2 => self.blue,
+ 3 => self.hue,
+ 4 => self.saturation,
+ _ => self.lightness,
+ };
if (new_val - old).abs() > 0.002 {
- match i { 0 => self.red = new_val, 1 => self.green = new_val, _ => self.blue = new_val };
+ match i {
+ 0 => {
+ self.red = new_val;
+ let (h, sat, l) = rgb_to_hsl(self.red, self.green, self.blue);
+ self.saturation = sat;
+ self.lightness = l;
+ if sat > 0.001 && l > 0.001 && l < 0.999 {
+ self.hue = h;
+ }
+ }
+ 1 => {
+ self.green = new_val;
+ let (h, sat, l) = rgb_to_hsl(self.red, self.green, self.blue);
+ self.saturation = sat;
+ self.lightness = l;
+ if sat > 0.001 && l > 0.001 && l < 0.999 {
+ self.hue = h;
+ }
+ }
+ 2 => {
+ self.blue = new_val;
+ let (h, sat, l) = rgb_to_hsl(self.red, self.green, self.blue);
+ self.saturation = sat;
+ self.lightness = l;
+ if sat > 0.001 && l > 0.001 && l < 0.999 {
+ self.hue = h;
+ }
+ }
+ 3 => {
+ self.hue = new_val;
+ let (r, g, b) = hsl_to_rgb(self.hue, self.saturation, self.lightness);
+ self.red = r;
+ self.green = g;
+ self.blue = b;
+ }
+ 4 => {
+ self.saturation = new_val;
+ let (r, g, b) = hsl_to_rgb(self.hue, self.saturation, self.lightness);
+ self.red = r;
+ self.green = g;
+ self.blue = b;
+ }
+ _ => {
+ self.lightness = new_val;
+ let (r, g, b) = hsl_to_rgb(self.hue, self.saturation, self.lightness);
+ self.red = r;
+ self.green = g;
+ self.blue = b;
+ }
+ }
self.needs_rebuild = true;
}
}
false
}
+ WindowEvent::MouseWheel { delta, .. } => {
+ let s = self.scale_factor as f32;
+ let (px, py) = (self.cursor_x, self.cursor_y);
+ let scroll_amount = match delta {
+ winit::event::MouseScrollDelta::LineDelta(_x, y) => *y,
+ winit::event::MouseScrollDelta::PixelDelta(pos) => (pos.y as f32) / 60.0,
+ };
+ if scroll_amount.abs() > 0.0001 {
+ for i in 0..6 {
+ let (tx, ty, tw, th) = Self::slider_physical_rect(i, s);
+ if px >= tx && px <= tx + tw && py >= ty - 4.0 * s && py <= ty + th + 4.0 * s {
+ let step = 0.02;
+ let old_val = match i {
+ 0 => self.red,
+ 1 => self.green,
+ 2 => self.blue,
+ 3 => self.hue,
+ 4 => self.saturation,
+ _ => self.lightness,
+ };
+ let new_val = (old_val + scroll_amount * step).clamp(0.0, 1.0);
+ if (new_val - old_val).abs() > 0.0001 {
+ match i {
+ 0 => {
+ self.red = new_val;
+ let (h, sat, l) = rgb_to_hsl(self.red, self.green, self.blue);
+ self.saturation = sat;
+ self.lightness = l;
+ if sat > 0.001 && l > 0.001 && l < 0.999 {
+ self.hue = h;
+ }
+ }
+ 1 => {
+ self.green = new_val;
+ let (h, sat, l) = rgb_to_hsl(self.red, self.green, self.blue);
+ self.saturation = sat;
+ self.lightness = l;
+ if sat > 0.001 && l > 0.001 && l < 0.999 {
+ self.hue = h;
+ }
+ }
+ 2 => {
+ self.blue = new_val;
+ let (h, sat, l) = rgb_to_hsl(self.red, self.green, self.blue);
+ self.saturation = sat;
+ self.lightness = l;
+ if sat > 0.001 && l > 0.001 && l < 0.999 {
+ self.hue = h;
+ }
+ }
+ 3 => {
+ self.hue = new_val;
+ let (r, g, b) = hsl_to_rgb(self.hue, self.saturation, self.lightness);
+ self.red = r;
+ self.green = g;
+ self.blue = b;
+ }
+ 4 => {
+ self.saturation = new_val;
+ let (r, g, b) = hsl_to_rgb(self.hue, self.saturation, self.lightness);
+ self.red = r;
+ self.green = g;
+ self.blue = b;
+ }
+ _ => {
+ self.lightness = new_val;
+ let (r, g, b) = hsl_to_rgb(self.hue, self.saturation, self.lightness);
+ self.red = r;
+ self.green = g;
+ self.blue = b;
+ }
+ }
+ self.needs_rebuild = true;
+ return true;
+ }
+ }
+ }
+ }
+ false
+ }
WindowEvent::MouseInput { state, button, .. } => {
if *button != MouseButton::Left { return false; }
match state {
ElementState::Pressed => {
let s = self.scale_factor as f32;
let (px, py) = (self.cursor_x, self.cursor_y);
- for i in 0..3 {
+ for i in 0..6 {
let (tx, ty, tw, th) = Self::slider_physical_rect(i, s);
if px >= tx && px <= tx + tw && py >= ty && py <= ty + th {
let val = ((px - tx) / tw).clamp(0.0, 1.0);
- match i { 0 => self.red = val, 1 => self.green = val, _ => self.blue = val };
- self.dragging = Some(match i {
- 0 => DragTarget::Red, 1 => DragTarget::Green, _ => DragTarget::Blue,
- });
+ match i {
+ 0 => {
+ self.red = val;
+ let (h, sat, l) = rgb_to_hsl(self.red, self.green, self.blue);
+ self.saturation = sat;
+ self.lightness = l;
+ if sat > 0.001 && l > 0.001 && l < 0.999 {
+ self.hue = h;
+ }
+ self.dragging = Some(DragTarget::Red);
+ }
+ 1 => {
+ self.green = val;
+ let (h, sat, l) = rgb_to_hsl(self.red, self.green, self.blue);
+ self.saturation = sat;
+ self.lightness = l;
+ if sat > 0.001 && l > 0.001 && l < 0.999 {
+ self.hue = h;
+ }
+ self.dragging = Some(DragTarget::Green);
+ }
+ 2 => {
+ self.blue = val;
+ let (h, sat, l) = rgb_to_hsl(self.red, self.green, self.blue);
+ self.saturation = sat;
+ self.lightness = l;
+ if sat > 0.001 && l > 0.001 && l < 0.999 {
+ self.hue = h;
+ }
+ self.dragging = Some(DragTarget::Blue);
+ }
+ 3 => {
+ self.hue = val;
+ let (r, g, b) = hsl_to_rgb(self.hue, self.saturation, self.lightness);
+ self.red = r;
+ self.green = g;
+ self.blue = b;
+ self.dragging = Some(DragTarget::Hue);
+ }
+ 4 => {
+ self.saturation = val;
+ let (r, g, b) = hsl_to_rgb(self.hue, self.saturation, self.lightness);
+ self.red = r;
+ self.green = g;
+ self.blue = b;
+ self.dragging = Some(DragTarget::Saturation);
+ }
+ _ => {
+ self.lightness = val;
+ let (r, g, b) = hsl_to_rgb(self.hue, self.saturation, self.lightness);
+ self.red = r;
+ self.green = g;
+ self.blue = b;
+ self.dragging = Some(DragTarget::Lightness);
+ }
+ }
self.needs_rebuild = true;
return true;
}
@@ -572,11 +910,13 @@ impl AppWrapper {
impl ApplicationHandler for AppWrapper {
fn resumed(&mut self, event_loop: &ActiveEventLoop) {
if self.state.is_some() { return; }
+ let size = winit::dpi::LogicalSize::new(WIN_W, WIN_H);
let window = Arc::new(event_loop.create_window(
WindowAttributes::default()
.with_name("clear-color-interface", "clear-color-interface")
.with_title("Clear Color Interface")
- .with_inner_size(winit::dpi::LogicalSize::new(WIN_W, WIN_H)),
+ .with_inner_size(size)
+ .with_min_inner_size(size),
).unwrap());
let state = pollster::block_on(ColorApp::new(
window, self.initial_red, self.initial_green, self.initial_blue,