1
2use glam::{Vec2, Vec3, Vec4};
5use std::collections::HashMap;
6
7#[derive(Debug, Clone, Copy, PartialEq)]
12pub enum ColorSpace {
13 Linear,
14 Gamma22,
15 Gamma18,
16 AcesCg,
17 AcesAp0,
18 Rec709,
19 Rec2020,
20 DciP3,
21 DisplayP3,
22 SRgb,
23 LogC,
24 SLog3,
25 RedWideGamut,
26}
27
28impl ColorSpace {
29 pub fn label(self) -> &'static str {
30 match self {
31 ColorSpace::Linear => "Linear",
32 ColorSpace::Gamma22 => "Gamma 2.2",
33 ColorSpace::Gamma18 => "Gamma 1.8",
34 ColorSpace::AcesCg => "ACEScg",
35 ColorSpace::AcesAp0 => "ACES AP0",
36 ColorSpace::Rec709 => "Rec.709",
37 ColorSpace::Rec2020 => "Rec.2020",
38 ColorSpace::DciP3 => "DCI-P3",
39 ColorSpace::DisplayP3 => "Display P3",
40 ColorSpace::SRgb => "sRGB",
41 ColorSpace::LogC => "Log C",
42 ColorSpace::SLog3 => "S-Log3",
43 ColorSpace::RedWideGamut => "REDWideGamutRGB",
44 }
45 }
46
47 pub fn is_hdr(self) -> bool {
48 matches!(self, ColorSpace::AcesCg | ColorSpace::AcesAp0 | ColorSpace::Rec2020 | ColorSpace::LogC | ColorSpace::SLog3)
49 }
50
51 pub fn to_linear(self, v: f32) -> f32 {
52 match self {
53 ColorSpace::Linear => v,
54 ColorSpace::Gamma22 | ColorSpace::SRgb => v.powf(2.2),
55 ColorSpace::Gamma18 => v.powf(1.8),
56 _ => v,
57 }
58 }
59
60 pub fn from_linear(self, v: f32) -> f32 {
61 match self {
62 ColorSpace::Linear => v,
63 ColorSpace::Gamma22 | ColorSpace::SRgb => v.max(0.0).powf(1.0 / 2.2),
64 ColorSpace::Gamma18 => v.max(0.0).powf(1.0 / 1.8),
65 _ => v,
66 }
67 }
68}
69
70#[derive(Debug, Clone, Copy, PartialEq)]
75pub enum ToneMappingMode {
76 None,
77 Aces,
78 FilmicAces,
79 Reinhard,
80 ReinhardExtended,
81 Uncharted2,
82 Hable,
83 CustomCurve,
84 AgX,
85 Tony,
86}
87
88impl ToneMappingMode {
89 pub fn label(self) -> &'static str {
90 match self {
91 ToneMappingMode::None => "None",
92 ToneMappingMode::Aces => "ACES",
93 ToneMappingMode::FilmicAces => "Filmic ACES",
94 ToneMappingMode::Reinhard => "Reinhard",
95 ToneMappingMode::ReinhardExtended => "Reinhard Extended",
96 ToneMappingMode::Uncharted2 => "Uncharted 2",
97 ToneMappingMode::Hable => "Hable",
98 ToneMappingMode::CustomCurve => "Custom Curve",
99 ToneMappingMode::AgX => "AgX",
100 ToneMappingMode::Tony => "Tony",
101 }
102 }
103
104 pub fn apply(self, x: f32) -> f32 {
105 match self {
106 ToneMappingMode::None => x,
107 ToneMappingMode::Reinhard => x / (1.0 + x),
108 ToneMappingMode::ReinhardExtended => {
109 let white = 4.0_f32;
110 x * (1.0 + x / (white * white)) / (1.0 + x)
111 }
112 ToneMappingMode::Aces => {
113 let a = 2.51_f32;
115 let b = 0.03_f32;
116 let c = 2.43_f32;
117 let d = 0.59_f32;
118 let e = 0.14_f32;
119 ((x * (a * x + b)) / (x * (c * x + d) + e)).clamp(0.0, 1.0)
120 }
121 ToneMappingMode::Uncharted2 | ToneMappingMode::Hable => {
122 let a = 0.15_f32;
123 let b = 0.50_f32;
124 let c = 0.10_f32;
125 let d = 0.20_f32;
126 let e_val = 0.02_f32;
127 let f = 0.30_f32;
128 let w = 11.2_f32;
129 let tone = |v: f32| ((v * (a * v + c * b) + d * e_val) / (v * (a * v + b) + d * f)) - e_val / f;
130 let curr = tone(x * 2.0);
131 let white_scale = 1.0 / tone(w);
132 (curr * white_scale).clamp(0.0, 1.0)
133 }
134 _ => x / (1.0 + x),
135 }
136 }
137
138 pub fn apply_vec3(self, color: Vec3) -> Vec3 {
139 Vec3::new(self.apply(color.x), self.apply(color.y), self.apply(color.z))
140 }
141}
142
143#[derive(Debug, Clone, Copy)]
148pub struct ColorWheelValue {
149 pub hue_offset: f32, pub saturation: f32, pub lightness: f32, pub tint: Vec3,
153}
154
155impl Default for ColorWheelValue {
156 fn default() -> Self {
157 Self { hue_offset: 0.0, saturation: 1.0, lightness: 0.0, tint: Vec3::ZERO }
158 }
159}
160
161impl ColorWheelValue {
162 pub fn apply(&self, color: Vec3) -> Vec3 {
163 let c = color + Vec3::splat(self.lightness);
165 let lum = c.dot(Vec3::new(0.2126, 0.7152, 0.0722));
167 let c = Vec3::splat(lum).lerp(c, self.saturation);
168 let h = self.hue_offset * std::f32::consts::PI / 180.0;
170 let cos_h = h.cos();
171 let sin_h = h.sin();
172 let u = Vec3::new(0.213, 0.715, 0.072);
173 let w = Vec3::new(0.143, -0.140, -0.283); let c = Vec3::new(
175 c.dot(u + Vec3::new(cos_h * (1.0 - u.x), -sin_h * w.z, sin_h * w.y)),
176 c.dot(u + Vec3::new(sin_h * w.z, cos_h * (1.0 - u.y), -sin_h * w.x)),
177 c.dot(u + Vec3::new(-sin_h * w.y, sin_h * w.x, cos_h * (1.0 - u.z))),
178 );
179 c + self.tint
181 }
182}
183
184#[derive(Debug, Clone, Copy)]
185pub struct LiftGammaGain {
186 pub lift: Vec4, pub gamma: Vec4,
188 pub gain: Vec4,
189}
190
191impl Default for LiftGammaGain {
192 fn default() -> Self {
193 Self {
194 lift: Vec4::new(0.0, 0.0, 0.0, 0.0),
195 gamma: Vec4::new(1.0, 1.0, 1.0, 1.0),
196 gain: Vec4::new(1.0, 1.0, 1.0, 1.0),
197 }
198 }
199}
200
201impl LiftGammaGain {
202 pub fn apply(&self, color: Vec3) -> Vec3 {
203 let lift = Vec3::new(self.lift.x + self.lift.w, self.lift.y + self.lift.w, self.lift.z + self.lift.w);
204 let gamma = Vec3::new(self.gamma.x * self.gamma.w, self.gamma.y * self.gamma.w, self.gamma.z * self.gamma.w);
205 let gain = Vec3::new(self.gain.x * self.gain.w, self.gain.y * self.gain.w, self.gain.z * self.gain.w);
206 let c = color * gain + lift;
207 Vec3::new(
208 c.x.max(0.0).powf(1.0 / gamma.x.max(0.001)),
209 c.y.max(0.0).powf(1.0 / gamma.y.max(0.001)),
210 c.z.max(0.0).powf(1.0 / gamma.z.max(0.001)),
211 )
212 }
213}
214
215#[derive(Debug, Clone)]
220pub struct ColorCurveChannel {
221 pub points: Vec<Vec2>,
222 pub enabled: bool,
223}
224
225impl ColorCurveChannel {
226 pub fn identity() -> Self {
227 Self {
228 points: vec![Vec2::new(0.0, 0.0), Vec2::new(1.0, 1.0)],
229 enabled: true,
230 }
231 }
232
233 pub fn evaluate(&self, t: f32) -> f32 {
234 if !self.enabled { return t; }
235 let t = t.clamp(0.0, 1.0);
236 if self.points.len() < 2 { return t; }
237 let i = self.points.partition_point(|p| p.x <= t);
238 if i == 0 { return self.points[0].y; }
239 if i >= self.points.len() { return self.points[self.points.len()-1].y; }
240 let a = self.points[i-1];
241 let b = self.points[i];
242 let u = (t - a.x) / (b.x - a.x).max(1e-6);
243 let u = u * u * (3.0 - 2.0 * u);
245 a.y + (b.y - a.y) * u
246 }
247
248 pub fn add_point(&mut self, t: f32, v: f32) {
249 let i = self.points.partition_point(|p| p.x < t);
250 self.points.insert(i, Vec2::new(t, v));
251 }
252
253 pub fn remove_point(&mut self, idx: usize) {
254 if self.points.len() > 2 {
255 self.points.remove(idx);
256 }
257 }
258
259 pub fn reset(&mut self) {
260 self.points = vec![Vec2::new(0.0, 0.0), Vec2::new(1.0, 1.0)];
261 }
262}
263
264#[derive(Debug, Clone)]
265pub struct ColorCurves {
266 pub master: ColorCurveChannel,
267 pub red: ColorCurveChannel,
268 pub green: ColorCurveChannel,
269 pub blue: ColorCurveChannel,
270 pub hue_vs_hue: ColorCurveChannel,
271 pub hue_vs_sat: ColorCurveChannel,
272 pub hue_vs_lum: ColorCurveChannel,
273 pub lum_vs_sat: ColorCurveChannel,
274 pub sat_vs_sat: ColorCurveChannel,
275}
276
277impl Default for ColorCurves {
278 fn default() -> Self {
279 Self {
280 master: ColorCurveChannel::identity(),
281 red: ColorCurveChannel::identity(),
282 green: ColorCurveChannel::identity(),
283 blue: ColorCurveChannel::identity(),
284 hue_vs_hue: ColorCurveChannel::identity(),
285 hue_vs_sat: ColorCurveChannel::identity(),
286 hue_vs_lum: ColorCurveChannel::identity(),
287 lum_vs_sat: ColorCurveChannel::identity(),
288 sat_vs_sat: ColorCurveChannel::identity(),
289 }
290 }
291}
292
293impl ColorCurves {
294 pub fn apply(&self, color: Vec3) -> Vec3 {
295 Vec3::new(
296 self.red.evaluate(self.master.evaluate(color.x)),
297 self.green.evaluate(self.master.evaluate(color.y)),
298 self.blue.evaluate(self.master.evaluate(color.z)),
299 )
300 }
301}
302
303#[derive(Debug, Clone, Copy, Default)]
308pub struct ShadowsMidtonesHighlights {
309 pub shadows: Vec4, pub midtones: Vec4,
311 pub highlights: Vec4,
312 pub shadows_start: f32,
313 pub shadows_end: f32,
314 pub highlights_start: f32,
315 pub highlights_end: f32,
316}
317
318impl ShadowsMidtonesHighlights {
319 pub fn new() -> Self {
320 Self {
321 shadows: Vec4::new(0.0, 0.0, 0.0, 1.0),
322 midtones: Vec4::new(0.0, 0.0, 0.0, 1.0),
323 highlights: Vec4::new(0.0, 0.0, 0.0, 1.0),
324 shadows_start: 0.0,
325 shadows_end: 0.3,
326 highlights_start: 0.55,
327 highlights_end: 1.0,
328 }
329 }
330
331 pub fn apply(&self, color: Vec3) -> Vec3 {
332 let lum = color.dot(Vec3::new(0.2126, 0.7152, 0.0722));
333 let shadow_w = (1.0 - (lum - self.shadows_start) / (self.shadows_end - self.shadows_start).max(1e-6)).clamp(0.0, 1.0);
334 let highlight_w = ((lum - self.highlights_start) / (self.highlights_end - self.highlights_start).max(1e-6)).clamp(0.0, 1.0);
335 let midtone_w = (1.0 - shadow_w - highlight_w).clamp(0.0, 1.0);
336 let s = Vec3::new(self.shadows.x, self.shadows.y, self.shadows.z) * self.shadows.w;
337 let m = Vec3::new(self.midtones.x, self.midtones.y, self.midtones.z) * self.midtones.w;
338 let h = Vec3::new(self.highlights.x, self.highlights.y, self.highlights.z) * self.highlights.w;
339 color + s * shadow_w + m * midtone_w + h * highlight_w
340 }
341}
342
343#[derive(Debug, Clone, Copy, PartialEq)]
348pub enum LutSize {
349 Lut16,
350 Lut32,
351 Lut48,
352 Lut64,
353}
354
355impl LutSize {
356 pub fn dim(self) -> usize {
357 match self {
358 LutSize::Lut16 => 16,
359 LutSize::Lut32 => 32,
360 LutSize::Lut48 => 48,
361 LutSize::Lut64 => 64,
362 }
363 }
364
365 pub fn byte_size(self) -> usize {
366 let d = self.dim();
367 d * d * d * 4 * 4 }
369}
370
371#[derive(Debug, Clone)]
372pub struct Lut3D {
373 pub size: LutSize,
374 pub data: Vec<Vec3>, pub name: String,
376 pub source_space: ColorSpace,
377 pub target_space: ColorSpace,
378}
379
380impl Lut3D {
381 pub fn identity(size: LutSize) -> Self {
382 let dim = size.dim();
383 let n = dim * dim * dim;
384 let mut data = Vec::with_capacity(n);
385 for b in 0..dim {
386 for g in 0..dim {
387 for r in 0..dim {
388 data.push(Vec3::new(
389 r as f32 / (dim - 1) as f32,
390 g as f32 / (dim - 1) as f32,
391 b as f32 / (dim - 1) as f32,
392 ));
393 }
394 }
395 }
396 Self {
397 size,
398 data,
399 name: "Identity".into(),
400 source_space: ColorSpace::Linear,
401 target_space: ColorSpace::SRgb,
402 }
403 }
404
405 pub fn apply(&self, color: Vec3) -> Vec3 {
406 let dim = self.size.dim();
407 let c = color.clamp(Vec3::ZERO, Vec3::ONE);
408 let sc = c * (dim - 1) as f32;
409 let x0 = sc.x.floor() as usize;
410 let y0 = sc.y.floor() as usize;
411 let z0 = sc.z.floor() as usize;
412 let x1 = (x0 + 1).min(dim - 1);
413 let y1 = (y0 + 1).min(dim - 1);
414 let z1 = (z0 + 1).min(dim - 1);
415 let fx = sc.x.fract();
416 let fy = sc.y.fract();
417 let fz = sc.z.fract();
418 let idx = |r: usize, g: usize, b: usize| b * dim * dim + g * dim + r;
419 let c000 = self.data[idx(x0, y0, z0)];
421 let c100 = self.data[idx(x1, y0, z0)];
422 let c010 = self.data[idx(x0, y1, z0)];
423 let c110 = self.data[idx(x1, y1, z0)];
424 let c001 = self.data[idx(x0, y0, z1)];
425 let c101 = self.data[idx(x1, y0, z1)];
426 let c011 = self.data[idx(x0, y1, z1)];
427 let c111 = self.data[idx(x1, y1, z1)];
428 let c00 = c000.lerp(c100, fx);
429 let c01 = c010.lerp(c110, fx);
430 let c10 = c001.lerp(c101, fx);
431 let c11 = c011.lerp(c111, fx);
432 let c0 = c00.lerp(c01, fy);
433 let c1 = c10.lerp(c11, fy);
434 c0.lerp(c1, fz)
435 }
436
437 pub fn bake_from_grade(&mut self, grade: &ColorGrade) {
438 let dim = self.size.dim();
439 for b in 0..dim {
440 for g in 0..dim {
441 for r in 0..dim {
442 let input = Vec3::new(
443 r as f32 / (dim - 1) as f32,
444 g as f32 / (dim - 1) as f32,
445 b as f32 / (dim - 1) as f32,
446 );
447 let output = grade.apply(input);
448 self.data[b * dim * dim + g * dim + r] = output;
449 }
450 }
451 }
452 }
453
454 pub fn export_cube_format(&self) -> String {
455 let dim = self.size.dim();
456 let mut out = String::new();
457 out.push_str(&format!("LUT_3D_SIZE {}\n\n", dim));
458 for entry in &self.data {
459 out.push_str(&format!("{:.6} {:.6} {:.6}\n", entry.x, entry.y, entry.z));
460 }
461 out
462 }
463}
464
465#[derive(Debug, Clone)]
470pub struct ColorGrade {
471 pub exposure: f32,
472 pub contrast: f32,
473 pub brightness: f32,
474 pub saturation: f32,
475 pub hue_shift: f32,
476 pub temperature: f32, pub tint: f32,
478 pub tone_mapping: ToneMappingMode,
479 pub lift_gamma_gain: LiftGammaGain,
480 pub shadows_midtones_highlights: ShadowsMidtonesHighlights,
481 pub curves: ColorCurves,
482 pub color_filter: Vec3,
483 pub channel_mixer_r: Vec3,
484 pub channel_mixer_g: Vec3,
485 pub channel_mixer_b: Vec3,
486 pub color_wheels: [ColorWheelValue; 3], pub lut_contribution: f32,
488 pub output_colorspace: ColorSpace,
489 pub post_exposure: f32,
490}
491
492impl Default for ColorGrade {
493 fn default() -> Self {
494 Self {
495 exposure: 0.0,
496 contrast: 0.0,
497 brightness: 0.0,
498 saturation: 1.0,
499 hue_shift: 0.0,
500 temperature: 0.0,
501 tint: 0.0,
502 tone_mapping: ToneMappingMode::Aces,
503 lift_gamma_gain: LiftGammaGain::default(),
504 shadows_midtones_highlights: ShadowsMidtonesHighlights::new(),
505 curves: ColorCurves::default(),
506 color_filter: Vec3::ONE,
507 channel_mixer_r: Vec3::new(1.0, 0.0, 0.0),
508 channel_mixer_g: Vec3::new(0.0, 1.0, 0.0),
509 channel_mixer_b: Vec3::new(0.0, 0.0, 1.0),
510 color_wheels: [ColorWheelValue::default(); 3],
511 lut_contribution: 1.0,
512 output_colorspace: ColorSpace::SRgb,
513 post_exposure: 0.0,
514 }
515 }
516}
517
518impl ColorGrade {
519 pub fn apply(&self, input: Vec3) -> Vec3 {
520 let exposure_mult = 2.0_f32.powf(self.exposure);
522 let c = input * exposure_mult;
523 let temp_k = self.temperature * 100.0;
525 let wb_r = 1.0 + temp_k * 0.0002;
526 let wb_b = 1.0 - temp_k * 0.0002;
527 let wb_g = 1.0 + self.tint * 0.001;
528 let c = Vec3::new(c.x * wb_r, c.y * wb_g, c.z * wb_b);
529 let c = (c - Vec3::splat(0.5)) * (1.0 + self.contrast * 0.01) + Vec3::splat(0.5);
531 let c = c + Vec3::splat(self.brightness * 0.01);
532 let lum = c.dot(Vec3::new(0.2126, 0.7152, 0.0722));
534 let c = Vec3::splat(lum).lerp(c, self.saturation);
535 let c = c * self.color_filter;
537 let r = c.dot(self.channel_mixer_r);
539 let g = c.dot(self.channel_mixer_g);
540 let b = c.dot(self.channel_mixer_b);
541 let c = Vec3::new(r, g, b);
542 let c = self.lift_gamma_gain.apply(c);
544 let c = self.shadows_midtones_highlights.apply(c);
546 let c = self.curves.apply(c);
548 let c = self.tone_mapping.apply_vec3(c);
550 let c = c * 2.0_f32.powf(self.post_exposure);
552 Vec3::new(
554 self.output_colorspace.from_linear(c.x),
555 self.output_colorspace.from_linear(c.y),
556 self.output_colorspace.from_linear(c.z),
557 ).max(Vec3::ZERO)
558 }
559}
560
561#[derive(Debug, Clone)]
566pub struct ColorGradePreset {
567 pub name: String,
568 pub category: String,
569 pub description: String,
570 pub grade: ColorGrade,
571 pub thumbnail: Option<Vec<Vec3>>,
572}
573
574impl ColorGradePreset {
575 pub fn cinematic() -> Self {
576 let mut grade = ColorGrade::default();
577 grade.contrast = 15.0;
578 grade.saturation = 0.85;
579 grade.temperature = -10.0;
580 grade.lift_gamma_gain.lift = Vec4::new(-0.02, -0.02, 0.02, 0.0);
581 grade.lift_gamma_gain.gain = Vec4::new(1.1, 1.05, 0.95, 1.0);
582 grade.tone_mapping = ToneMappingMode::FilmicAces;
583 Self {
584 name: "Cinematic".into(),
585 category: "Film".into(),
586 description: "Filmic contrast with desaturated cool tones".into(),
587 grade,
588 thumbnail: None,
589 }
590 }
591
592 pub fn vintage() -> Self {
593 let mut grade = ColorGrade::default();
594 grade.saturation = 0.7;
595 grade.temperature = 25.0;
596 grade.tint = 5.0;
597 grade.lift_gamma_gain.lift = Vec4::new(0.03, 0.02, 0.0, 0.0);
598 grade.lift_gamma_gain.gain = Vec4::new(1.0, 0.95, 0.85, 1.0);
599 grade.tone_mapping = ToneMappingMode::Hable;
600 Self {
601 name: "Vintage".into(),
602 category: "Stylistic".into(),
603 description: "Warm desaturated vintage look".into(),
604 grade,
605 thumbnail: None,
606 }
607 }
608
609 pub fn horror() -> Self {
610 let mut grade = ColorGrade::default();
611 grade.saturation = 0.3;
612 grade.contrast = 25.0;
613 grade.temperature = -30.0;
614 grade.lift_gamma_gain.lift = Vec4::new(0.0, -0.03, 0.0, -0.05);
615 grade.tone_mapping = ToneMappingMode::Reinhard;
616 Self {
617 name: "Horror".into(),
618 category: "Stylistic".into(),
619 description: "Desaturated cold high-contrast horror look".into(),
620 grade,
621 thumbnail: None,
622 }
623 }
624
625 pub fn neon_noir() -> Self {
626 let mut grade = ColorGrade::default();
627 grade.saturation = 1.4;
628 grade.contrast = 20.0;
629 grade.temperature = -15.0;
630 grade.lift_gamma_gain.lift = Vec4::new(-0.05, 0.0, 0.1, 0.0);
631 grade.lift_gamma_gain.gain = Vec4::new(0.9, 0.95, 1.15, 1.0);
632 grade.tone_mapping = ToneMappingMode::ReinhardExtended;
633 Self {
634 name: "Neon Noir".into(),
635 category: "Stylistic".into(),
636 description: "High-saturation cyberpunk neon look".into(),
637 grade,
638 thumbnail: None,
639 }
640 }
641
642 pub fn natural() -> Self {
643 Self {
644 name: "Natural".into(),
645 category: "Neutral".into(),
646 description: "Neutral natural grading".into(),
647 grade: ColorGrade::default(),
648 thumbnail: None,
649 }
650 }
651}
652
653#[derive(Debug, Clone, Copy, PartialEq)]
658pub enum ColorGradingTab {
659 Grading,
660 ToneMapping,
661 Curves,
662 Wheels,
663 Lut,
664 Presets,
665 Scopes,
666}
667
668#[derive(Debug, Clone, Copy, PartialEq)]
669pub enum ScopeMode {
670 Waveform,
671 Parade,
672 Histogram,
673 Vectorscope,
674}
675
676#[derive(Debug, Clone)]
677pub struct ColorGradingEditor {
678 pub grade: ColorGrade,
679 pub active_tab: ColorGradingTab,
680 pub active_lut: Option<Lut3D>,
681 pub lut_size: LutSize,
682 pub presets: Vec<ColorGradePreset>,
683 pub history: Vec<ColorGrade>,
684 pub history_pos: usize,
685 pub scope_mode: ScopeMode,
686 pub show_clipping: bool,
687 pub show_scopes: bool,
688 pub preview_split: bool,
689 pub split_position: f32,
690 pub input_space: ColorSpace,
691 pub working_space: ColorSpace,
692 pub output_space: ColorSpace,
693 pub preset_search: String,
694}
695
696impl ColorGradingEditor {
697 pub fn new() -> Self {
698 Self {
699 grade: ColorGrade::default(),
700 active_tab: ColorGradingTab::Grading,
701 active_lut: None,
702 lut_size: LutSize::Lut32,
703 presets: vec![
704 ColorGradePreset::natural(),
705 ColorGradePreset::cinematic(),
706 ColorGradePreset::vintage(),
707 ColorGradePreset::horror(),
708 ColorGradePreset::neon_noir(),
709 ],
710 history: Vec::new(),
711 history_pos: 0,
712 scope_mode: ScopeMode::Waveform,
713 show_clipping: false,
714 show_scopes: true,
715 preview_split: false,
716 split_position: 0.5,
717 input_space: ColorSpace::Linear,
718 working_space: ColorSpace::AcesCg,
719 output_space: ColorSpace::SRgb,
720 preset_search: String::new(),
721 }
722 }
723
724 pub fn snapshot(&mut self) {
725 self.history.truncate(self.history_pos);
726 self.history.push(self.grade.clone());
727 self.history_pos = self.history.len();
728 }
729
730 pub fn undo(&mut self) {
731 if self.history_pos > 1 {
732 self.history_pos -= 1;
733 self.grade = self.history[self.history_pos - 1].clone();
734 }
735 }
736
737 pub fn redo(&mut self) {
738 if self.history_pos < self.history.len() {
739 self.grade = self.history[self.history_pos].clone();
740 self.history_pos += 1;
741 }
742 }
743
744 pub fn apply_preset(&mut self, preset: &ColorGradePreset) {
745 self.snapshot();
746 self.grade = preset.grade.clone();
747 }
748
749 pub fn generate_lut(&mut self) {
750 let mut lut = Lut3D::identity(self.lut_size);
751 lut.bake_from_grade(&self.grade);
752 lut.name = "Baked Grade".into();
753 self.active_lut = Some(lut);
754 }
755
756 pub fn export_lut_cube(&self) -> Option<String> {
757 self.active_lut.as_ref().map(|l| l.export_cube_format())
758 }
759
760 pub fn search_presets(&self, query: &str) -> Vec<&ColorGradePreset> {
761 let q = query.to_lowercase();
762 self.presets.iter().filter(|p| {
763 p.name.to_lowercase().contains(&q) ||
764 p.category.to_lowercase().contains(&q) ||
765 p.description.to_lowercase().contains(&q)
766 }).collect()
767 }
768
769 pub fn grade_pixel(&self, input: Vec3) -> Vec3 {
770 let graded = self.grade.apply(input);
771 if let Some(lut) = &self.active_lut {
772 let from_lut = lut.apply(graded);
773 graded.lerp(from_lut, self.grade.lut_contribution)
774 } else {
775 graded
776 }
777 }
778
779 pub fn reset_to_identity(&mut self) {
780 self.snapshot();
781 self.grade = ColorGrade::default();
782 }
783}
784
785#[cfg(test)]
789mod tests {
790 use super::*;
791
792 #[test]
793 fn test_tone_mapping() {
794 let tm = ToneMappingMode::Aces;
795 let v = tm.apply(5.0);
796 assert!(v >= 0.0 && v <= 1.0);
797 }
798
799 #[test]
800 fn test_color_grade() {
801 let grade = ColorGrade::default();
802 let c = grade.apply(Vec3::new(0.5, 0.5, 0.5));
803 assert!(c.x >= 0.0 && c.x <= 1.0);
804 }
805
806 #[test]
807 fn test_lut_identity() {
808 let lut = Lut3D::identity(LutSize::Lut16);
809 let c = lut.apply(Vec3::new(0.5, 0.5, 0.5));
810 assert!((c.x - 0.5).abs() < 0.1);
811 }
812
813 #[test]
814 fn test_lut_bake() {
815 let mut lut = Lut3D::identity(LutSize::Lut16);
816 let grade = ColorGrade::default();
817 lut.bake_from_grade(&grade);
818 assert!(!lut.data.is_empty());
819 }
820
821 #[test]
822 fn test_editor() {
823 let mut ed = ColorGradingEditor::new();
824 ed.grade.exposure = 1.0;
825 ed.snapshot();
826 ed.undo();
827 assert!((ed.grade.exposure).abs() < 1e-5);
828 ed.generate_lut();
829 assert!(ed.active_lut.is_some());
830 }
831}