Files
hcie-rust-v3.05/hcie-io/examples/gui.rs

1146 lines
64 KiB
Rust
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#![allow(deprecated)]
use eframe::{egui, App, Frame, NativeOptions};
use egui::{ColorImage, TextureOptions, TextureHandle, TextureFilter, Color32, Stroke, Rect, Vec2, Pos2, StrokeKind};
use hcie_blend::BlendMode;
const THUMB_W: usize = 40;
const THUMB_H: usize = 30;
const CHECKER_SIZE: f32 = 12.0;
const ZOOM_MIN: f32 = 0.01;
const ZOOM_MAX: f32 = 64.0;
struct LayerEntry {
layer_id: u64,
parent_id: Option<u64>,
clipping_mask: bool,
name: String,
visible: bool,
opacity: f32,
blend_mode: BlendMode,
layer_type: String,
width: u32,
height: u32,
pixels: Vec<u8>,
dirty: bool,
thumbnail: Option<TextureHandle>,
adjustment: Option<hcie_blend::Adjustment>,
mask_pixels: Option<Vec<u8>>,
mask_bounds: Option<[i32; 4]>,
mask_default_color: u8,
curve_knots: [Vec<(f32, f32)>; 3],
master_knots: Vec<(f32, f32)>,
effects: Vec<hcie_fx::LayerEffect>,
fill_opacity: f32,
}
struct HcieIoApp {
layers: Vec<LayerEntry>,
canvas_w: u32,
canvas_h: u32,
status: String,
temp_path: String,
temp_format: String,
zoom: f32,
pan_offset: Vec2,
composite_texture: Option<TextureHandle>,
composite_dirty: bool,
checked_path: String,
layer_id_counter: usize,
selected_layer: Option<usize>,
load_on_start: Option<std::path::PathBuf>,
curve_channel: usize,
curve_drag_knot: Option<usize>,
ref_texture: Option<TextureHandle>,
ref_path: Option<String>,
ref_pixels: Option<(Vec<u8>, u32, u32)>,
diff_texture: Option<TextureHandle>,
view_mode: usize,
compare_stats: String,
}
impl Default for HcieIoApp {
fn default() -> Self {
Self {
layers: vec![],
canvas_w: 800,
canvas_h: 600,
status: String::new(),
temp_path: "output.png".to_string(),
temp_format: "png".to_string(),
zoom: 1.0,
pan_offset: Vec2::ZERO,
composite_texture: None,
composite_dirty: false,
checked_path: String::new(),
layer_id_counter: 0,
selected_layer: None,
load_on_start: Some(std::path::PathBuf::from("_images/_psd_stil_test/sultan_test/sultan.psd")),
curve_channel: 0,
curve_drag_knot: None,
ref_texture: None,
ref_path: None,
ref_pixels: None,
diff_texture: None,
view_mode: 0,
compare_stats: String::new(),
}
}
}
impl HcieIoApp {
fn load_file(&mut self, path: &std::path::Path) {
let ext = path.extension().and_then(|s| s.to_str()).unwrap_or("").to_lowercase();
let result = match ext.as_str() {
"psd" => {
// Get canvas size from PSD header, not from first layer
if let Ok(bytes) = std::fs::read(path) {
if let Ok(psd) = hcie_psd::Psd::from_bytes(&bytes) {
self.canvas_w = psd.width();
self.canvas_h = psd.height();
}
}
hcie_psd::psd_import::import_psd(path)
},
"kra" => hcie_kra::import_kra(path),
_ => hcie_io::load_image(path).map(|l| vec![hcie_protocol::Layer::from_rgba(l.name, l.width, l.height, l.pixels)]),
};
match result {
Ok(layers) => {
if layers.is_empty() { self.status = "No layers found".to_string(); return; }
// canvas_w/h already set from PSD header above (for PSD files)
if ext != "psd" {
self.canvas_w = layers[0].width;
self.canvas_h = layers[0].height;
}
self.layers.clear();
for l in layers {
self.layer_id_counter += 1;
let curve_knots = if let Some(hcie_blend::Adjustment::Curves { lut_r, lut_g, lut_b }) = &l.adjustment {
if !l.curve_points.is_empty() {
let k = l.curve_points.iter().map(|&(o, i)| (i as f32, o as f32)).collect::<Vec<_>>();
[k.clone(), k.clone(), k]
} else {
[extract_knots(&lut_r[..]), extract_knots(&lut_g[..]), extract_knots(&lut_b[..])]
}
} else {
[vec![], vec![], vec![]]
};
let master_knots = curve_knots[0].clone();
self.layers.push(LayerEntry {
layer_id: l.id,
parent_id: l.parent_id,
name: l.name.clone(),
visible: l.visible,
opacity: l.opacity,
blend_mode: to_blend_mode(l.blend_mode),
clipping_mask: l.clipping_mask,
layer_type: format!("{:?}", l.layer_type),
width: l.width,
height: l.height,
pixels: l.pixels.clone(),
dirty: true,
thumbnail: None,
adjustment: l.adjustment.clone(),
mask_pixels: l.mask_pixels.clone(),
mask_bounds: l.mask_bounds,
mask_default_color: l.mask_default_color,
curve_knots,
master_knots,
effects: l.effects.iter().map(|e| hcie_fx::protocol_to_hcie_fx_effect(e)).collect(),
fill_opacity: l.fill_opacity,
});
}
self.selected_layer = if self.layers.is_empty() { None } else { Some(0) };
self.composite_dirty = true;
self.status = format!("Loaded {} layers: {}x{}", self.layers.len(), self.canvas_w, self.canvas_h);
// Auto-load reference PNG if exists next to PSD
let stem = path.file_stem().and_then(|s| s.to_str()).unwrap_or("");
let parent = path.parent().unwrap_or(std::path::Path::new("."));
let ref_png = parent.join(format!("{}.png", stem));
if ref_png.exists() {
self.load_reference(&ref_png);
}
}
Err(e) => self.status = format!("Error: {}", e),
}
}
fn load_reference(&mut self, path: &std::path::Path) {
match image::open(path) {
Ok(img) => {
let rgba = img.to_rgba8();
let w = rgba.width();
let h = rgba.height();
self.ref_pixels = Some((rgba.into_raw(), w, h));
self.ref_path = Some(path.display().to_string());
self.ref_texture = None;
self.diff_texture = None;
self.compare_stats = String::new();
self.status = format!("{} + Ref: {}x{}", self.status, w, h);
}
Err(e) => {
self.status = format!("{} (ref load error: {})", self.status, e);
}
}
}
fn save_image(&mut self) -> bool {
let layer = hcie_io::Layer::from_rgba("Export", self.canvas_w, self.canvas_h, self.composite());
match hcie_io::save_image(&layer, std::path::Path::new(&self.temp_path), &self.temp_format) {
Ok(()) => { self.status = "Saved successfully!".to_string(); true }
Err(e) => { self.status = format!("Save error: {}", e); false }
}
}
fn composite(&self) -> Vec<u8> {
let comp_layers: Vec<hcie_composite::Layer> = self.layers.iter().map(|l| hcie_composite::Layer {
name: l.name.clone(),
layer_type: hcie_protocol::LayerType::default(),
data: hcie_protocol::LayerData::default(),
pixels: l.pixels.clone(),
width: l.width,
height: l.height,
visible: l.visible,
opacity: l.opacity,
blend_mode: from_blend_mode(l.blend_mode),
locked: false,
dirty: false,
id: l.layer_id,
parent_id: l.parent_id,
styles: vec![],
clipping_mask: l.clipping_mask,
collapsed: false,
adjustment: l.adjustment.clone(),
mask_pixels: l.mask_pixels.clone(),
mask_bounds: l.mask_bounds,
mask_default_color: l.mask_default_color,
curve_points: vec![],
fill_opacity: l.fill_opacity,
effects: l.effects.iter().map(|e| hcie_fx::hcie_fx_effect_to_protocol(e)).collect(),
effects_dirty: std::sync::atomic::AtomicBool::new(false),
effects_cache: std::sync::Mutex::new(None),
adjustment_raw: None,
is_section_divider: false,
image_resources_raw: None,
}).collect();
hcie_composite::composite_layers(&comp_layers, self.canvas_w, self.canvas_h)
}
fn render_composition(&mut self, ctx: &egui::Context) {
let needs_render = self.composite_dirty
|| self.composite_texture.is_none()
|| self.composite_texture.as_ref().map_or(true, |t| t.size() != [self.canvas_w as usize, self.canvas_h as usize]);
if !needs_render && self.ref_texture.is_some() { return; }
let buf = self.composite();
self.composite_dirty = false;
let options = TextureOptions {
magnification: TextureFilter::Nearest,
minification: TextureFilter::Linear,
..Default::default()
};
let image = ColorImage::from_rgba_unmultiplied(
[self.canvas_w as usize, self.canvas_h as usize], &buf,
);
if let Some(tex) = &mut self.composite_texture {
if tex.size() == [self.canvas_w as usize, self.canvas_h as usize] {
tex.set(image, options);
} else {
self.composite_texture = Some(ctx.load_texture("composite-view", image, options));
}
} else {
self.composite_texture = Some(ctx.load_texture("composite-view", image, options));
}
// Create reference texture from stored pixels
if let Some((ref px, rw, rh)) = &self.ref_pixels {
if self.ref_texture.is_none() {
let ref_img = ColorImage::from_rgba_unmultiplied([*rw as usize, *rh as usize], px);
self.ref_texture = Some(ctx.load_texture("ref-view", ref_img, options));
}
// Generate diff image and stats
if self.diff_texture.is_none() {
let comp = &buf;
let comp_w = self.canvas_w;
let comp_h = self.canvas_h;
let total = (*rw * *rh) as usize;
let mut diff_pixels = Vec::with_capacity(total * 4);
let mut diff_sum = 0.0f64;
let mut max_diff = 0u32;
let mut match_count = 0usize;
let mut diff_counts = [0u32; 256];
for i in 0..total {
let cx = (i as u32 % rw).min(comp_w.saturating_sub(1)) as usize;
let cy = (i as u32 / rw).min(comp_h.saturating_sub(1)) as usize;
let ci = (cy * comp_w as usize + cx) * 4;
let ri = i * 4;
let dr = if ci + 3 < comp.len() { (comp[ci] as i32 - px[ri] as i32).unsigned_abs() } else { 255 };
let dg = if ci + 3 < comp.len() { (comp[ci+1] as i32 - px[ri+1] as i32).unsigned_abs() } else { 255 };
let db = if ci + 3 < comp.len() { (comp[ci+2] as i32 - px[ri+2] as i32).unsigned_abs() } else { 255 };
let da = if ci + 3 < comp.len() { (comp[ci+3] as i32 - px[ri+3] as i32).unsigned_abs() } else { 255 };
let pix_diff = dr.max(dg).max(db).max(da);
if pix_diff > max_diff { max_diff = pix_diff; }
diff_sum += (dr + dg + db + da) as f64 / 4.0;
if pix_diff <= 5 { match_count += 1; }
diff_counts[pix_diff.min(255) as usize] += 1;
let d = (pix_diff.min(255) * 4).min(255) as u8;
diff_pixels.extend_from_slice(&[d, d, d, 255]);
}
let mean_diff = diff_sum / total as f64;
let match_pct = match_count as f64 / total as f64 * 100.0;
let diff_img = ColorImage::from_rgba_unmultiplied([*rw as usize, *rh as usize], &diff_pixels);
self.diff_texture = Some(ctx.load_texture("diff-view", diff_img, options));
let mut stats = format!("MAE: {:.2} MaxDiff: {} Match(<=5): {:.1}% ({}/{})",
mean_diff, max_diff, match_pct, match_count, total);
stats.push_str("\nDiff histogram (0-20):");
for d in 0..=20 {
if diff_counts[d] > 0 {
stats.push_str(&format!("\n diff={}: {} px", d, diff_counts[d]));
}
}
self.compare_stats = stats;
}
}
}
fn update_layer_thumb(&mut self, idx: usize, ctx: &egui::Context) {
let layer = &self.layers[idx];
if !layer.dirty && self.layers[idx].thumbnail.is_some() { return; }
let thumb = thumbnail_nearest(&layer.pixels, layer.width, layer.height, THUMB_W as u32, THUMB_H as u32);
let pixels: Vec<Color32> = thumb.chunks_exact(4)
.map(|c| Color32::from_rgba_unmultiplied(c[0], c[1], c[2], c[3])).collect();
let img = ColorImage::new([THUMB_W, THUMB_H], pixels);
let opts = TextureOptions { magnification: TextureFilter::Nearest, minification: TextureFilter::Nearest, ..Default::default() };
let name = format!("thumb-{}", idx);
if let Some(tex) = &mut self.layers[idx].thumbnail {
tex.set(img, opts);
} else {
self.layers[idx].thumbnail = Some(ctx.load_texture(name, img, opts));
}
self.layers[idx].dirty = false;
}
}
impl App for HcieIoApp {
fn ui(&mut self, ui: &mut egui::Ui, _frame: &mut Frame) {
let ctx = ui.ctx();
// Load startup file once
if let Some(path) = self.load_on_start.take() {
self.checked_path = path.display().to_string();
self.load_file(&path);
}
egui::CentralPanel::default().show(ctx, |ui| {
ui.horizontal(|ui| {
ui.heading("hcie-io Test GUI");
ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
if ui.button("📁 Load").clicked() {
if let Some(path) = rfd::FileDialog::new()
.add_filter("PSD", &["psd"])
.add_filter("KRA", &["kra"])
.add_filter("Images", &["png", "jpg", "jpeg", "webp", "bmp", "gif"]).pick_file()
{
self.checked_path = path.display().to_string();
self.load_file(&path);
}
}
});
});
ui.horizontal(|ui| {
ui.label("Path:");
ui.add(egui::TextEdit::singleline(&mut self.checked_path).desired_width(200.0));
ui.label("Fmt:");
ui.add(egui::TextEdit::singleline(&mut self.temp_format).desired_width(60.0));
if ui.button("💾 Save").clicked() { self.save_image(); }
});
ui.horizontal(|ui| {
if ui.button("").clicked() { self.zoom = (self.zoom / 1.2).clamp(ZOOM_MIN, ZOOM_MAX); }
ui.label(format!("{:.0}%", self.zoom * 100.0));
if ui.button("").clicked() { self.zoom = (self.zoom * 1.2).clamp(ZOOM_MIN, ZOOM_MAX); }
if ui.button("1:1").clicked() { self.zoom = 1.0; }
if ui.button("⊙ Reset").clicked() { self.pan_offset = Vec2::ZERO; self.zoom = 1.0; }
if self.ref_pixels.is_some() {
ui.separator();
ui.label("View:");
ui.selectable_value(&mut self.view_mode, 0, "Composite");
ui.selectable_value(&mut self.view_mode, 1, "Reference");
ui.selectable_value(&mut self.view_mode, 2, "Diff");
}
ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
ui.label(format!("{}", self.status));
});
});
let available = ui.available_size();
let panel_w = 200.0_f32;
let canvas_w = available.x - panel_w * 2.0 - 12.0;
// ── Render composition pipeline (before drawing) ──
self.render_composition(ui.ctx());
ui.horizontal(|ui| {
// Layer panel
ui.allocate_ui(Vec2::new(panel_w, available.y), |ui| {
ui.vertical(|ui| {
ui.strong("Layers");
ui.separator();
let mut toggled: Vec<(usize, bool)> = Vec::new();
let num = self.layers.len();
egui::ScrollArea::vertical().show(ui, |ui| {
for i in (0..num).rev() {
self.update_layer_thumb(i, ui.ctx());
let is_sel = self.selected_layer == Some(i);
let row_h = 36.0;
let (row_id, row_rect) = ui.allocate_space(Vec2::new(ui.available_width(), row_h));
let row_resp = ui.interact(row_rect, row_id, egui::Sense::click());
let layer = &self.layers[i];
let indent = if layer.parent_id.is_some() || layer.layer_type.contains("Group") { 16.0 } else { 0.0 };
let inner = Rect::from_min_max(
Pos2::new(row_rect.min.x + 2.0 + indent, row_rect.min.y + 2.0),
Pos2::new(row_rect.max.x - 2.0, row_rect.max.y - 2.0),
);
let eye_rect = Rect::from_min_size(inner.left_top(), Vec2::new(20.0, row_h));
if row_resp.clicked() {
if let Some(press_pos) = ui.input(|i| i.pointer.press_origin()) {
if eye_rect.contains(press_pos) {
toggled.push((i, !layer.visible));
} else {
self.selected_layer = Some(i);
}
} else {
self.selected_layer = Some(i);
}
}
if is_sel {
ui.painter().rect_filled(row_rect, 2.0, ui.visuals().selection.bg_fill);
} else if row_resp.hovered() {
ui.painter().rect_filled(row_rect, 2.0, ui.visuals().extreme_bg_color.gamma_multiply(0.5));
}
let (icon, color) = if layer.visible {
("👁", ui.visuals().strong_text_color())
} else {
("👁", ui.visuals().weak_text_color().gamma_multiply(0.25))
};
ui.painter().text(eye_rect.center(), egui::Align2::CENTER_CENTER, icon,
egui::FontId::proportional(16.0), color);
let thumb_rect = Rect::from_min_size(Pos2::new(inner.left() + 22.0, inner.top() + 3.0),
Vec2::new(THUMB_W as f32, THUMB_H as f32));
checkerboard_painter(ui.painter(), thumb_rect, 4.0, Color32::from_gray(160), Color32::from_gray(200));
if let Some(tex) = &layer.thumbnail {
ui.painter().image(tex.id(), thumb_rect,
Rect::from_min_max(Pos2::ZERO, Pos2::new(1.0, 1.0)), Color32::WHITE);
}
ui.painter().text(Pos2::new(thumb_rect.right() + 6.0, inner.center().y),
egui::Align2::LEFT_CENTER, &layer.name, egui::FontId::proportional(12.0),
ui.visuals().text_color());
}
});
for (i, vis) in toggled { self.layers[i].visible = vis; self.composite_dirty = true; }
});
});
// Properties panel
ui.allocate_ui(Vec2::new(panel_w, available.y), |ui| {
ui.vertical(|ui| {
ui.strong("Properties");
ui.separator();
if let Some(i) = self.selected_layer.filter(|&i| i < self.layers.len()) {
let layer = &mut self.layers[i];
ui.label(format!("Name: {}", layer.name));
ui.label(format!("Type: {}", layer.layer_type));
ui.label(format!("Size: {}x{} px", layer.width, layer.height));
ui.label(format!("Blend: {:?}", layer.blend_mode));
egui::ComboBox::from_id_source("blend_mode")
.selected_text(format!("{:?}", layer.blend_mode))
.show_ui(ui, |ui| {
let modes = [
BlendMode::Normal, BlendMode::Dissolve, BlendMode::Darken, BlendMode::Multiply,
BlendMode::ColorBurn, BlendMode::LinearBurn, BlendMode::DarkerColor,
BlendMode::Lighten, BlendMode::Screen, BlendMode::ColorDodge,
BlendMode::LinearDodge, BlendMode::LighterColor, BlendMode::Overlay,
BlendMode::SoftLight, BlendMode::HardLight, BlendMode::VividLight,
BlendMode::LinearLight, BlendMode::PinLight, BlendMode::HardMix,
BlendMode::Difference, BlendMode::Exclusion, BlendMode::Subtract,
BlendMode::Divide, BlendMode::Hue, BlendMode::Saturation,
BlendMode::Color, BlendMode::Luminosity,
];
for m in modes {
if ui.selectable_value(&mut layer.blend_mode, m, format!("{:?}", m)).changed() {
self.composite_dirty = true;
}
}
});
if ui.add(egui::Slider::new(&mut layer.opacity, 0.0..=1.0).text("Opacity")).changed() {
self.composite_dirty = true;
}
if ui.checkbox(&mut layer.visible, "Visible").changed() { self.composite_dirty = true; }
ui.label(format!("Pixels: {} bytes", layer.pixels.len()));
if let Some(hcie_blend::Adjustment::Curves { lut_r, lut_g, lut_b }) = &mut layer.adjustment {
ui.separator();
ui.strong("Curves Adjustment");
let mut curve_ch = self.curve_channel;
let mut ch_changed = None;
ui.horizontal(|ui| {
let btn = egui::SelectableLabel::new(curve_ch == 3, egui::RichText::new("Master").strong().color(Color32::from_gray(200)));
if ui.add(btn).clicked() { ch_changed = Some(3); }
ui.separator();
for (idx, name, col) in [
(0usize, "R", Color32::from_rgb(255, 80, 80)),
(1, "G", Color32::from_rgb(80, 220, 80)),
(2, "B", Color32::from_rgb(80, 120, 255)),
] {
let btn = egui::SelectableLabel::new(curve_ch == idx, egui::RichText::new(name).color(col));
if ui.add(btn).clicked() { ch_changed = Some(idx); }
}
});
if let Some(ch) = ch_changed {
self.curve_channel = ch;
curve_ch = ch;
self.curve_drag_knot = None;
}
let sz = ui.available_width().min(220.0).max(120.0);
let (resp, painter) = ui.allocate_painter(Vec2::new(sz, sz), egui::Sense::click_and_drag());
let rect = resp.rect;
let to_screen = |ix: f32, iy: f32| -> Pos2 {
Pos2::new(rect.min.x + (ix / 255.0) * sz, rect.max.y - (iy / 255.0) * sz)
};
let from_screen = |p: Pos2| -> (f32, f32) {
(((p.x - rect.min.x) / sz * 255.0).clamp(0.0, 255.0),
((rect.max.y - p.y) / sz * 255.0).clamp(0.0, 255.0))
};
painter.rect_filled(rect, 0.0, Color32::from_gray(25));
for i in 0..=4 {
let t = i as f32 / 4.0;
let x = rect.min.x + t * sz;
let y = rect.min.y + (1.0 - t) * sz;
painter.line_segment([Pos2::new(x, rect.min.y), Pos2::new(x, rect.max.y)], Stroke::new(0.5, Color32::from_gray(50)));
painter.line_segment([Pos2::new(rect.min.x, y), Pos2::new(rect.max.x, y)], Stroke::new(0.5, Color32::from_gray(50)));
}
painter.line_segment([
Pos2::new(rect.min.x, rect.max.y),
Pos2::new(rect.max.x, rect.min.y),
], Stroke::new(0.5, Color32::from_gray(70)));
let knot_radius = 5.0f32;
let colors = [
Color32::from_rgb(255, 80, 80),
Color32::from_rgb(80, 220, 80),
Color32::from_rgb(80, 120, 255),
];
// Draw master curve (gray, always visible)
if !layer.master_knots.is_empty() {
let ms = spline_lut(&layer.master_knots);
let mut mpts = Vec::with_capacity(256);
for i in 0..256u16 { mpts.push(to_screen(i as f32, ms[i as usize] as f32)); }
for w in mpts.windows(2) { painter.line_segment([w[0], w[1]], Stroke::new(1.5, Color32::from_gray(120))); }
}
// Draw per-channel curves when a specific channel is selected
if curve_ch < 3 && !layer.curve_knots[curve_ch].is_empty() {
let cs = spline_lut(&layer.curve_knots[curve_ch]);
let mut cpts = Vec::with_capacity(256);
for i in 0..256u16 { cpts.push(to_screen(i as f32, cs[i as usize] as f32)); }
for w in cpts.windows(2) { painter.line_segment([w[0], w[1]], Stroke::new(2.0, colors[curve_ch])); }
}
// Hover info
let hover_pos = ui.input(|i| i.pointer.hover_pos());
if resp.hovered() {
if let Some(pos) = hover_pos {
if rect.contains(pos) {
let (hx, _) = from_screen(pos);
let hi = hx.round().clamp(0.0, 255.0) as usize;
painter.line_segment([Pos2::new(pos.x, rect.min.y), Pos2::new(pos.x, rect.max.y)], Stroke::new(0.3, Color32::from_gray(100)));
painter.line_segment([Pos2::new(rect.min.x, pos.y), Pos2::new(rect.max.x, pos.y)], Stroke::new(0.3, Color32::from_gray(100)));
// Show master value
if !layer.master_knots.is_empty() {
let ms = spline_lut(&layer.master_knots);
let mv = ms[hi];
let dp = to_screen(hi as f32, mv as f32);
painter.circle_filled(dp, 3.0, Color32::from_gray(160));
}
// Show channel value
if curve_ch < 3 && !layer.curve_knots[curve_ch].is_empty() {
let cs = spline_lut(&layer.curve_knots[curve_ch]);
let cv = cs[hi];
let dp = to_screen(hi as f32, cv as f32);
painter.circle_filled(dp, 3.0, colors[curve_ch]);
ui.label(format!("Ch {} | {}{}", ["R","G","B"][curve_ch], hi, cv));
} else if curve_ch == 3 {
let ms = spline_lut(&layer.master_knots);
ui.label(format!("Master | {}{}", hi, ms[hi]));
}
}
}
}
// Find nearest knot on the ACTIVE curve
// Copy active knots for immutable reference during interaction
let active_knots = if curve_ch == 3 { layer.master_knots.clone() } else { layer.curve_knots[curve_ch].clone() };
let find_nearest = |pos: Pos2, knots: &[(f32, f32)]| -> Option<usize> {
let mut best = None;
let mut best_d = knot_radius * 2.5;
for (ki, &(kx, ky)) in knots.iter().enumerate() {
let kp = to_screen(kx, ky);
let d = (kp.x - pos.x).hypot(kp.y - pos.y);
if d < best_d { best_d = d; best = Some(ki); }
}
best
};
// Draw knots on active curve
for (ki, &(kx, ky)) in active_knots.iter().enumerate() {
let kp = to_screen(kx, ky);
let is_drag = self.curve_drag_knot == Some(ki);
let is_hover = hover_pos.map_or(false, |hp| (kp.x - hp.x).hypot(kp.y - hp.y) < knot_radius * 2.0);
let fill = if is_drag { Color32::WHITE } else if is_hover { Color32::from_gray(220) } else { Color32::from_gray(160) };
let sw = if is_drag || is_hover { 2.0 } else { 1.0 };
let sc = if curve_ch == 3 { Color32::from_gray(255) } else { colors[curve_ch] };
painter.circle_filled(kp, knot_radius, fill);
painter.circle_stroke(kp, knot_radius, Stroke::new(sw, sc));
}
let mut changed = false;
let pointer_pos = ui.input(|i| i.pointer.latest_pos());
let pointer_down = ui.input(|i| i.pointer.primary_down());
let pointer_released = ui.input(|i| i.pointer.primary_released());
let pointer_pressed = ui.input(|i| i.pointer.primary_pressed());
if let Some(pos) = pointer_pos {
if rect.contains(pos) {
if pointer_pressed {
if let Some(ki) = find_nearest(pos, &active_knots) {
self.curve_drag_knot = Some(ki);
} else {
let (nx, ny) = from_screen(pos);
let knots = if curve_ch == 3 { &mut layer.master_knots } else { &mut layer.curve_knots[curve_ch] };
knots.push((nx, ny));
knots.sort_by(|a, b| a.0.partial_cmp(&b.0).unwrap());
self.curve_drag_knot = knots.iter().position(|&(kx, ky)| (kx - nx).abs() < 0.01 && (ky - ny).abs() < 0.01);
changed = true;
}
}
if pointer_down {
if let (Some(ki), true) = (self.curve_drag_knot, true) {
let (_nx, ny) = from_screen(pos);
if curve_ch == 3 {
if ki < layer.master_knots.len() {
let kx = layer.master_knots[ki].0;
layer.master_knots[ki] = (kx, ny);
changed = true;
}
} else {
if ki < layer.curve_knots[curve_ch].len() {
let kx = layer.curve_knots[curve_ch][ki].0;
layer.curve_knots[curve_ch][ki] = (kx, ny);
changed = true;
}
}
}
}
if pointer_released {
self.curve_drag_knot = None;
}
} else if pointer_released {
self.curve_drag_knot = None;
}
}
// Right-click to remove
let right_clicked = ui.input(|i| i.pointer.secondary_released());
if right_clicked {
if let Some(pos) = pointer_pos {
if rect.contains(pos) {
if let Some(ki) = find_nearest(pos, &active_knots) {
let knots = if curve_ch == 3 { &mut layer.master_knots } else { &mut layer.curve_knots[curve_ch] };
if ki != 0 && ki != knots.len() - 1 {
knots.remove(ki);
changed = true;
self.curve_drag_knot = None;
}
}
}
}
}
if changed {
// Rebuild master LUT
let new_master = spline_lut(&layer.master_knots);
// Apply master to all channels, then apply per-channel overrides
for ch in 0..3usize {
let new_lut = if layer.curve_knots[ch].len() <= 2 {
// No per-channel override, use master directly
new_master.clone()
} else {
// Per-channel override: use per-channel knots
spline_lut(&layer.curve_knots[ch])
};
match ch {
0 => *lut_r = new_lut,
1 => *lut_g = new_lut,
_ => *lut_b = new_lut,
}
}
layer.dirty = true;
self.composite_dirty = true;
}
}
// Layer Effects panel
if !layer.effects.is_empty() {
ui.separator();
ui.horizontal(|ui| {
ui.strong("Layer Effects");
ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
ui.label(egui::RichText::new(format!("{} effects", layer.effects.len()))
.color(Color32::from_gray(120)).small());
});
});
ui.add_space(2.0);
let fx_icons = [
("", Color32::from_rgb(180, 140, 100)), // Drop Shadow
("", Color32::from_rgb(100, 140, 180)), // Inner Shadow
("", Color32::from_rgb(200, 180, 100)), // Outer Glow
("", Color32::from_rgb(100, 180, 200)), // Inner Glow
("", Color32::from_rgb(180, 180, 180)), // Bevel
("", Color32::from_rgb(160, 140, 180)), // Satin
("", Color32::from_rgb(200, 100, 100)), // Color Overlay
("", Color32::from_rgb(100, 200, 100)), // Gradient Overlay
("", Color32::from_rgb(100, 100, 200)), // Pattern Overlay
("", Color32::from_rgb(200, 200, 200)), // Stroke
];
for (ei, fx) in layer.effects.iter().enumerate() {
let enabled = fx.is_enabled();
let name = fx.effect_name();
let icon_idx = match fx {
hcie_fx::LayerEffect::DropShadow { .. } => 0,
hcie_fx::LayerEffect::InnerShadow { .. } => 1,
hcie_fx::LayerEffect::OuterGlow { .. } => 2,
hcie_fx::LayerEffect::InnerGlow { .. } => 3,
hcie_fx::LayerEffect::BevelEmboss { .. } => 4,
hcie_fx::LayerEffect::Satin { .. } => 5,
hcie_fx::LayerEffect::ColorOverlay { .. } => 6,
hcie_fx::LayerEffect::GradientOverlay { .. } => 7,
hcie_fx::LayerEffect::PatternOverlay { .. } => 8,
hcie_fx::LayerEffect::Stroke { .. } => 9,
};
let (icon, icon_color) = fx_icons[icon_idx];
let text_color = if enabled {
ui.visuals().text_color()
} else {
ui.visuals().weak_text_color().gamma_multiply(0.5)
};
// Effect header row
let _bg_color = if enabled {
Color32::from_rgba_premultiplied(60, 60, 80, 180)
} else {
Color32::from_rgba_premultiplied(40, 40, 45, 120)
};
let header_rect = ui.available_rect_before_wrap();
let _header_rect = Rect::from_min_size(header_rect.min, Vec2::new(header_rect.width(), 20.0));
ui.horizontal(|ui| {
// Visibility icon (eye)
let eye = if enabled { "" } else { "" };
let eye_color = if enabled { Color32::from_rgb(100, 200, 100) } else { Color32::from_gray(80) };
ui.label(egui::RichText::new(eye).color(eye_color).size(10.0));
// Effect icon
ui.label(egui::RichText::new(icon).color(icon_color).size(12.0));
// Effect name
ui.label(egui::RichText::new(name).color(text_color).strong());
// Status badge
ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
if enabled {
ui.label(egui::RichText::new("ON")
.color(Color32::from_rgb(80, 180, 80)).small().strong());
} else {
ui.label(egui::RichText::new("OFF")
.color(Color32::from_gray(80)).small());
}
});
});
// Effect details (indented)
if enabled {
match fx {
hcie_fx::LayerEffect::DropShadow { blend_mode, color, opacity, angle, distance, size, spread, .. }
| hcie_fx::LayerEffect::InnerShadow { blend_mode, color, opacity, angle, distance, size, choke: spread, .. } => {
ui.indent(ei, |ui| {
ui.horizontal(|ui| {
// Color swatch
let c = Color32::from_rgb(color[0], color[1], color[2]);
let (rect, _) = ui.allocate_exact_size(Vec2::new(12.0, 12.0), egui::Sense::hover());
ui.painter().rect_filled(rect, 2.0, c);
ui.label(egui::RichText::new(format!("{:?}", blend_mode)).small());
ui.label(egui::RichText::new(format!("{:.0}%", opacity * 100.0)).small().color(Color32::from_gray(160)));
});
ui.label(egui::RichText::new(format!(
"Angle {:.0}° Distance {:.0}px Size {:.0}px Spread {:.0}%",
angle, distance, size, spread
)).small().color(Color32::from_gray(140)));
});
}
hcie_fx::LayerEffect::OuterGlow { blend_mode, color, opacity, size, spread, .. }
| hcie_fx::LayerEffect::InnerGlow { blend_mode, color, opacity, size, choke: spread, .. } => {
ui.indent(ei, |ui| {
ui.horizontal(|ui| {
let c = Color32::from_rgb(color[0], color[1], color[2]);
let (rect, _) = ui.allocate_exact_size(Vec2::new(12.0, 12.0), egui::Sense::hover());
ui.painter().rect_filled(rect, 2.0, c);
ui.label(egui::RichText::new(format!("{:?}", blend_mode)).small());
ui.label(egui::RichText::new(format!("{:.0}%", opacity * 100.0)).small().color(Color32::from_gray(160)));
});
ui.label(egui::RichText::new(format!("Size {:.0}px Spread {:.0}%", size, spread)).small().color(Color32::from_gray(140)));
});
}
hcie_fx::LayerEffect::BevelEmboss { style, technique, depth, size, angle, altitude, highlight_blend, shadow_blend, .. } => {
ui.indent(ei, |ui| {
ui.label(egui::RichText::new(format!("{:?} · {:?}", style, technique)).small());
ui.label(egui::RichText::new(format!(
"Depth {:.1} Size {:.0}px Angle {:.0}° Altitude {:.0}°",
depth, size, angle, altitude
)).small().color(Color32::from_gray(140)));
ui.label(egui::RichText::new(format!(
"Highlight: {:?} Shadow: {:?}",
highlight_blend, shadow_blend
)).small().color(Color32::from_gray(140)));
});
}
hcie_fx::LayerEffect::Satin { blend_mode, color, opacity, angle, distance, size, invert, .. } => {
ui.indent(ei, |ui| {
ui.horizontal(|ui| {
let c = Color32::from_rgb(color[0], color[1], color[2]);
let (rect, _) = ui.allocate_exact_size(Vec2::new(12.0, 12.0), egui::Sense::hover());
ui.painter().rect_filled(rect, 2.0, c);
ui.label(egui::RichText::new(format!("{:?}", blend_mode)).small());
ui.label(egui::RichText::new(format!("{:.0}%", opacity * 100.0)).small().color(Color32::from_gray(160)));
});
ui.label(egui::RichText::new(format!(
"Angle {:.0}° Distance {:.0}px Size {:.0}px Invert: {}",
angle, distance, size, if *invert { "Yes" } else { "No" }
)).small().color(Color32::from_gray(140)));
});
}
hcie_fx::LayerEffect::ColorOverlay { blend_mode, color, opacity, .. } => {
ui.indent(ei, |ui| {
ui.horizontal(|ui| {
let c = Color32::from_rgb(color[0], color[1], color[2]);
let (rect, _) = ui.allocate_exact_size(Vec2::new(12.0, 12.0), egui::Sense::hover());
ui.painter().rect_filled(rect, 2.0, c);
ui.label(egui::RichText::new(format!("{:?}", blend_mode)).small());
ui.label(egui::RichText::new(format!("{:.0}%", opacity * 100.0)).small().color(Color32::from_gray(160)));
});
});
}
hcie_fx::LayerEffect::GradientOverlay { blend_mode, opacity, angle, scale, .. } => {
ui.indent(ei, |ui| {
ui.label(egui::RichText::new(format!("{:?}", blend_mode)).small());
ui.label(egui::RichText::new(format!(
"Opacity {:.0}% Angle {:.0}° Scale {:.0}%",
opacity * 100.0, angle, scale
)).small().color(Color32::from_gray(140)));
});
}
hcie_fx::LayerEffect::PatternOverlay { blend_mode, opacity, scale, .. } => {
ui.indent(ei, |ui| {
ui.label(egui::RichText::new(format!("{:?}", blend_mode)).small());
ui.label(egui::RichText::new(format!(
"Opacity {:.0}% Scale {:.0}%",
opacity * 100.0, scale
)).small().color(Color32::from_gray(140)));
});
}
hcie_fx::LayerEffect::Stroke { position, fill_type, blend_mode, opacity, size, color, .. } => {
ui.indent(ei, |ui| {
ui.horizontal(|ui| {
let c = Color32::from_rgb(color[0], color[1], color[2]);
let (rect, _) = ui.allocate_exact_size(Vec2::new(12.0, 12.0), egui::Sense::hover());
ui.painter().rect_filled(rect, 2.0, c);
ui.label(egui::RichText::new(format!("{:?} {:?}", position, fill_type)).small());
});
ui.label(egui::RichText::new(format!(
"{:?} {:.0}% {:.0}px",
blend_mode, opacity * 100.0, size
)).small().color(Color32::from_gray(140)));
});
}
}
}
ui.add_space(2.0);
}
}
} else { ui.label("No layer selected"); }
});
});
// Canvas
ui.allocate_ui(Vec2::new(canvas_w, available.y), |ui| {
ui.vertical(|ui| {
let (resp, painter) = ui.allocate_painter(ui.available_size() - Vec2::new(0.0, if !self.compare_stats.is_empty() { 120.0 } else { 0.0 }), egui::Sense::click_and_drag());
let rect = resp.rect;
let scroll = ui.input(|i| i.smooth_scroll_delta.y);
if scroll != 0.0 && resp.hovered() {
let factor = if ui.input(|i| i.modifiers.command) {
if scroll > 0.0 { 1.1 } else { 1.0 / 1.1 }
} else { (scroll / 200.0).exp() };
let old = self.zoom;
self.zoom = (old * factor).clamp(ZOOM_MIN, ZOOM_MAX);
if let Some(mpos) = ui.input(|i| i.pointer.hover_pos()) {
let actual = self.zoom / old;
self.pan_offset += (mpos - rect.center() - self.pan_offset) * (1.0 - actual);
}
}
if resp.dragged_by(egui::PointerButton::Middle) { self.pan_offset += resp.drag_delta(); }
checkerboard_painter(&painter, rect, CHECKER_SIZE, Color32::from_gray(160), Color32::from_gray(200));
let active_tex = match self.view_mode {
1 => self.ref_texture.as_ref(),
2 => self.diff_texture.as_ref(),
_ => self.composite_texture.as_ref(),
};
if let Some(tex) = active_tex {
let tex_w = tex.size()[0] as f32;
let tex_h = tex.size()[1] as f32;
let cw = tex_w * self.zoom;
let ch = tex_h * self.zoom;
let center = rect.center() + self.pan_offset;
let img_rect = Rect::from_center_size(center, Vec2::new(cw, ch));
painter.image(tex.id(), img_rect, Rect::from_min_max(Pos2::ZERO, Pos2::new(1.0, 1.0)), Color32::WHITE);
painter.rect_stroke(img_rect, 0.0, Stroke::new(1.0, Color32::from_gray(100)), StrokeKind::Outside);
} else {
let center = rect.center() + self.pan_offset;
let r = Rect::from_center_size(center, Vec2::new(self.canvas_w as f32 * self.zoom, self.canvas_h as f32 * self.zoom));
painter.rect_stroke(r, 0.0, Stroke::new(1.0, Color32::from_gray(100)), StrokeKind::Outside);
}
// Show comparison stats below canvas
if !self.compare_stats.is_empty() {
ui.separator();
ui.strong("Comparison Stats");
ui.label(egui::RichText::new(&self.compare_stats).monospace().small());
}
});
});
});
});
if self.composite_dirty {
ctx.request_repaint();
}
}
}
fn to_blend_mode(mode: hcie_protocol::BlendMode) -> BlendMode {
match mode {
hcie_protocol::BlendMode::Normal => BlendMode::Normal,
hcie_protocol::BlendMode::Dissolve => BlendMode::Dissolve,
hcie_protocol::BlendMode::Darken => BlendMode::Darken,
hcie_protocol::BlendMode::Multiply => BlendMode::Multiply,
hcie_protocol::BlendMode::ColorBurn => BlendMode::ColorBurn,
hcie_protocol::BlendMode::LinearBurn => BlendMode::LinearBurn,
hcie_protocol::BlendMode::DarkerColor => BlendMode::DarkerColor,
hcie_protocol::BlendMode::Lighten => BlendMode::Lighten,
hcie_protocol::BlendMode::Screen => BlendMode::Screen,
hcie_protocol::BlendMode::ColorDodge => BlendMode::ColorDodge,
hcie_protocol::BlendMode::LinearDodge => BlendMode::LinearDodge,
hcie_protocol::BlendMode::LighterColor=> BlendMode::LighterColor,
hcie_protocol::BlendMode::Overlay => BlendMode::Overlay,
hcie_protocol::BlendMode::SoftLight => BlendMode::SoftLight,
hcie_protocol::BlendMode::HardLight => BlendMode::HardLight,
hcie_protocol::BlendMode::VividLight => BlendMode::VividLight,
hcie_protocol::BlendMode::LinearLight => BlendMode::LinearLight,
hcie_protocol::BlendMode::PinLight => BlendMode::PinLight,
hcie_protocol::BlendMode::HardMix => BlendMode::HardMix,
hcie_protocol::BlendMode::Difference => BlendMode::Difference,
hcie_protocol::BlendMode::Exclusion => BlendMode::Exclusion,
hcie_protocol::BlendMode::Subtract => BlendMode::Subtract,
hcie_protocol::BlendMode::Divide => BlendMode::Divide,
hcie_protocol::BlendMode::Hue => BlendMode::Hue,
hcie_protocol::BlendMode::Saturation => BlendMode::Saturation,
hcie_protocol::BlendMode::Color => BlendMode::Color,
hcie_protocol::BlendMode::Luminosity => BlendMode::Luminosity,
hcie_protocol::BlendMode::PassThrough => BlendMode::Normal,
}
}
fn from_blend_mode(mode: BlendMode) -> hcie_protocol::BlendMode {
match mode {
BlendMode::Normal => hcie_protocol::BlendMode::Normal,
BlendMode::Dissolve => hcie_protocol::BlendMode::Dissolve,
BlendMode::Darken => hcie_protocol::BlendMode::Darken,
BlendMode::Multiply => hcie_protocol::BlendMode::Multiply,
BlendMode::ColorBurn => hcie_protocol::BlendMode::ColorBurn,
BlendMode::LinearBurn => hcie_protocol::BlendMode::LinearBurn,
BlendMode::DarkerColor => hcie_protocol::BlendMode::DarkerColor,
BlendMode::Lighten => hcie_protocol::BlendMode::Lighten,
BlendMode::Screen => hcie_protocol::BlendMode::Screen,
BlendMode::ColorDodge => hcie_protocol::BlendMode::ColorDodge,
BlendMode::LinearDodge => hcie_protocol::BlendMode::LinearDodge,
BlendMode::LighterColor=> hcie_protocol::BlendMode::LighterColor,
BlendMode::Overlay => hcie_protocol::BlendMode::Overlay,
BlendMode::SoftLight => hcie_protocol::BlendMode::SoftLight,
BlendMode::HardLight => hcie_protocol::BlendMode::HardLight,
BlendMode::VividLight => hcie_protocol::BlendMode::VividLight,
BlendMode::LinearLight => hcie_protocol::BlendMode::LinearLight,
BlendMode::PinLight => hcie_protocol::BlendMode::PinLight,
BlendMode::HardMix => hcie_protocol::BlendMode::HardMix,
BlendMode::Difference => hcie_protocol::BlendMode::Difference,
BlendMode::Exclusion => hcie_protocol::BlendMode::Exclusion,
BlendMode::Subtract => hcie_protocol::BlendMode::Subtract,
BlendMode::Divide => hcie_protocol::BlendMode::Divide,
BlendMode::Hue => hcie_protocol::BlendMode::Hue,
BlendMode::Saturation => hcie_protocol::BlendMode::Saturation,
BlendMode::Color => hcie_protocol::BlendMode::Color,
BlendMode::Luminosity => hcie_protocol::BlendMode::Luminosity,
BlendMode::PassThrough => hcie_protocol::BlendMode::Normal,
}
}
fn thumbnail_nearest(pixels: &[u8], sw: u32, sh: u32, dw: u32, dh: u32) -> Vec<u8> {
let mut out = vec![0u8; (dw * dh * 4) as usize];
for dy in 0..dh {
for dx in 0..dw {
let sx = (dx * sw / dw).min(sw.saturating_sub(1));
let sy = (dy * sh / dh).min(sh.saturating_sub(1));
let si = ((sy * sw + sx) * 4) as usize;
let di = ((dy * dw + dx) * 4) as usize;
out[di..di + 4].copy_from_slice(&pixels[si..si + 4]);
}
}
out
}
fn checkerboard_painter(painter: &egui::Painter, rect: Rect, ts: f32, ca: Color32, cb: Color32) {
let mut y = rect.min.y;
let mut row = 0;
while y < rect.max.y {
let mut x = rect.min.x;
let mut col = 0;
while x < rect.max.x {
let fill = if (row + col) % 2 == 0 { ca } else { cb };
painter.rect_filled(Rect::from_min_max(
Pos2::new(x, y),
Pos2::new((x + ts).min(rect.max.x), (y + ts).min(rect.max.y)),
), 0.0, fill);
x += ts; col += 1;
}
y += ts; row += 1;
}
}
fn extract_knots(lut: &[u8]) -> Vec<(f32, f32)> {
let mut knots = vec![(0.0, lut[0] as f32), (255.0, lut[255] as f32)];
fn sub(lut: &[u8], out: &mut Vec<(f32, f32)>, x0: f32, y0: f32, x1: f32, y1: f32, d: usize) {
if d > 8 { return; }
let xm = (x0 + x1) / 2.0;
let ym = lut[xm.round() as usize].min(255) as f32;
let yi = (y0 + y1) / 2.0;
if (ym - yi).abs() > 5.0 {
out.push((xm, ym));
sub(lut, out, x0, y0, xm, ym, d + 1);
sub(lut, out, xm, ym, x1, y1, d + 1);
}
}
sub(lut, &mut knots, 0.0, lut[0] as f32, 255.0, lut[255] as f32, 0);
knots.sort_by(|a, b| a.0.partial_cmp(&b.0).unwrap());
knots.dedup_by(|a, b| (a.0 - b.0).abs() < 0.5);
knots
}
fn spline_lut(knots: &[(f32, f32)]) -> Vec<u8> {
let n = knots.len();
let mut lut = vec![0u8; 256];
if n == 0 { for i in 0..256 { lut[i] = i as u8; } return lut; }
if n == 1 { let v = knots[0].1.clamp(0.0, 255.0).round() as u8; return vec![v; 256]; }
let mut pts: Vec<(f32, f32)> = knots.to_vec();
pts.sort_by(|a, b| a.0.partial_cmp(&b.0).unwrap());
pts.dedup_by(|a, b| (a.0 - b.0).abs() < 0.01);
let n = pts.len();
if n < 2 { let v = pts[0].1.clamp(0.0, 255.0).round() as u8; return vec![v; 256]; }
let mut h = vec![0.0f32; n - 1];
for i in 0..n - 1 { h[i] = pts[i + 1].0 - pts[i].0; }
let mut alpha = vec![0.0f32; n];
for i in 1..n - 1 {
alpha[i] = 3.0 * ((pts[i + 1].1 - pts[i].1) / h[i] - (pts[i].1 - pts[i - 1].1) / h[i - 1]);
}
let mut l = vec![1.0f32; n];
let mut mu = vec![0.0f32; n];
let mut z = vec![0.0f32; n];
for i in 1..n - 1 {
l[i] = 2.0 * (pts[i + 1].0 - pts[i - 1].0) - h[i - 1] * mu[i - 1];
if l[i].abs() < 1e-12 { continue; }
mu[i] = h[i] / l[i];
z[i] = (alpha[i] - h[i - 1] * z[i - 1]) / l[i];
}
let mut c = vec![0.0f32; n];
let mut b = vec![0.0f32; n.saturating_sub(1)];
let mut d = vec![0.0f32; n.saturating_sub(1)];
for j in (0..n - 1).rev() {
c[j] = z[j] - mu[j] * c[j + 1];
b[j] = (pts[j + 1].1 - pts[j].1) / h[j] - h[j] * (c[j + 1] + 2.0 * c[j]) / 3.0;
d[j] = (c[j + 1] - c[j]) / (3.0 * h[j]);
}
for i in 0..256 {
let x = i as f32;
if x <= pts[0].0 {
lut[i] = pts[0].1.clamp(0.0, 255.0).round() as u8;
} else if x >= pts[n - 1].0 {
lut[i] = pts[n - 1].1.clamp(0.0, 255.0).round() as u8;
} else {
for j in 0..n - 1 {
if x >= pts[j].0 && x <= pts[j + 1].0 {
let dx = x - pts[j].0;
let y = pts[j].1 + b[j] * dx + c[j] * dx * dx + d[j] * dx * dx * dx;
lut[i] = y.clamp(0.0, 255.0).round() as u8;
break;
}
}
}
}
lut
}
fn main() -> eframe::Result {
let mut opts = NativeOptions::default();
opts.viewport.inner_size = Some(Vec2::new(1400.0, 800.0));
eframe::run_native("hcie-io Test", opts, Box::new(|_cc| Ok(Box::new(HcieIoApp::default()))))
}