diff --git a/.gitignore b/.gitignore index 3e45d080..6d828d26 100644 --- a/.gitignore +++ b/.gitignore @@ -6,3 +6,4 @@ flamegraph.svg **/*.rs.bk *.AppImage AppDir +.vscode diff --git a/assets/cursors.png b/assets/cursors.png index 860df2d5..c3c07df7 100644 Binary files a/assets/cursors.png and b/assets/cursors.png differ diff --git a/config/init.rx b/config/init.rx index 3931b104..764d6a38 100644 --- a/config/init.rx +++ b/config/init.rx @@ -23,6 +23,7 @@ map r :redo -- Redo act map x :swap -- Swap foreground/background colors map/normal b :brush -- Reset brush map/normal g :flood -- Flood tool +map/normal a :lt/sampler {:tool/prev} -- Lookup-texture sample tool map/normal e :brush/set erase {:brush/unset erase} -- Erase (hold) map/normal :brush/set multi {:brush/unset multi} -- Multi-brush (hold) map/normal = :brush/toggle perfect -- Pixel-perfect brush diff --git a/src/cmd.rs b/src/cmd.rs index 2498052a..4782a98a 100644 --- a/src/cmd.rs +++ b/src/cmd.rs @@ -122,6 +122,10 @@ pub enum Command { ViewNext, ViewPrev, + LookupTextureSample, + LookupTextureMode(bool), + LookupTextureSet(i32), + Noop, } @@ -1051,6 +1055,20 @@ impl Default for Commands { .then(tuple::(integer().label(""), integer().label(""))) .map(|((_, i), (x, y))| Command::PaintPalette(i, x, y)) }) + .command("lt/on", "Set current view as lookup texture", |p| { + p.value(Command::LookupTextureMode(true)) }) + .command("lt/off", "Revert view to normal mode", |p| { + p.value(Command::LookupTextureMode(false)) }) + .command("lt/set", "Set another view as lookup texture to current view", |p| { + p.then(integer::().label("")) + .map(|(_, d)| Command::LookupTextureSet(d)) + }) + .command("lt/sample", "Search lookup texture for matching pixels", |p| { + p.value(Command::LookupTextureSample) + }) + .command("lt/sampler", "Switch to lookup texture sampler tool", |p| { + p.value(Command::Tool(Tool::LookupTextureSampler)) + }) } } diff --git a/src/draw.rs b/src/draw.rs index 4dcb4ed4..cdf7b41b 100644 --- a/src/draw.rs +++ b/src/draw.rs @@ -69,6 +69,7 @@ pub mod cursors { const PAN: Cursor = Cursor::new(Rect::new(48., 0., 64., 16.), -8., -8., false); const ERASE: Cursor = Cursor::new(Rect::new(64., 0., 80., 16.), -8., -8., true); const FLOOD: Cursor = Cursor::new(Rect::new(80., 0., 96., 16.), -8., -8., false); + const LOOKUP: Cursor = Cursor::new(Rect::new(96., 0., 112., 16.), -5.5, -8., false); pub fn info(t: &Tool, b: &Brush, m: Mode, in_view: bool, in_selection: bool) -> Option { match (m, t) { @@ -82,6 +83,7 @@ pub mod cursors { Tool::Sampler => self::SAMPLER, Tool::Pan(_) => self::PAN, Tool::FloodFill => self::FLOOD, + Tool::LookupTextureSampler => self::LOOKUP, Tool::Brush => match m { Mode::Visual(_) if in_selection && in_view => self::OMNI, @@ -150,7 +152,7 @@ impl Context { fn draw_ui(session: &Session, canvas: &mut shape2d::Batch, text: &mut TextBatch) { let view = session.active_view(); - if let Some(selection) = session.selection { + if let Some(selection) = &session.selection { let fill = match session.mode { Mode::Visual(VisualState::Selecting { .. }) => { Rgba8::new(color::RED.r, color::RED.g, color::RED.b, 0x55) @@ -212,6 +214,33 @@ fn draw_ui(session: &Session, canvas: &mut shape2d::Batch, text: &mut TextBatch) } } + if let (Some(lss), Some(ltid)) = (&session.lookup_sample_state, view.lookuptexture()) { + match session.mode { + Mode::Visual(VisualState::LookupSampling) => { + let ltv = session.view(ltid); + + let offset = session.offset + ltv.offset; + let t = Matrix4::from_translation(offset.extend(0.)) * Matrix4::from_scale(ltv.zoom); + + for (i, c) in lss.candidates.iter().enumerate() { + let mut stroke = color::RED; + if i as i32 == lss.selected { + stroke = color::GREEN; + } + + canvas.add(Shape::Rectangle( + c.0.transform(t), + self::UI_LAYER, + Rotation::ZERO, + Stroke::new(1., stroke.into()), + Fill::Empty, + )); + } + } + _ => () + }; + } + for v in session.views.iter() { let offset = v.offset + session.offset; @@ -254,7 +283,7 @@ fn draw_ui(session: &Session, canvas: &mut shape2d::Batch, text: &mut TextBatch) if session.settings["ui/view-info"].is_set() { // View info - text.add( + let x = text.add( &format!("{}x{}x{}", v.fw, v.fh, v.animation.len()), offset.x, offset.y - self::LINE_HEIGHT, @@ -262,6 +291,17 @@ fn draw_ui(session: &Session, canvas: &mut shape2d::Batch, text: &mut TextBatch) color::GREY, TextAlign::Left, ); + + if v.is_lookuptexture() { + text.add( + &format!(" LUT"), + x, + offset.y - self::LINE_HEIGHT, + self::TEXT_LAYER, + color::GREEN, + TextAlign::Left, + ); + } } } if session.settings["ui/status"].is_set() { @@ -730,6 +770,20 @@ pub fn draw_view_animation(session: &Session, v: &View) -> sprite2d::Batch ) } +pub fn draw_view_lookuptexture_animation(session: &Session, v: &View) -> sprite2d::Batch { + sprite2d::Batch::singleton( + v.width(), + v.fh, + *v.animation.val(), + Rect::new(-(v.fw as f32) * 2., 0., -(v.fw as f32), v.fh as f32) * v.zoom + (session.offset + v.offset), + self::VIEW_LAYER, + Rgba::TRANSPARENT, + 1., + Repeat::default(), + ) +} + + pub fn draw_view_composites(session: &Session, v: &View) -> sprite2d::Batch { let mut batch = sprite2d::Batch::new(v.width(), v.fh); diff --git a/src/font.rs b/src/font.rs index 945b0cdb..c64708d8 100644 --- a/src/font.rs +++ b/src/font.rs @@ -27,7 +27,7 @@ impl TextBatch { z: ZDepth, color: Rgba8, align: TextAlign, - ) { + ) -> f32 { let offset: usize = 32; let gw = self.gw; @@ -56,6 +56,8 @@ impl TextBatch { ); sx += gw; } + + return sx; } pub fn offset(&mut self, x: f32, y: f32) { diff --git a/src/gl/data/lookuptex.frag b/src/gl/data/lookuptex.frag new file mode 100644 index 00000000..b5e77cf4 --- /dev/null +++ b/src/gl/data/lookuptex.frag @@ -0,0 +1,38 @@ +uniform sampler2D tex; +uniform sampler2D ltex; +uniform vec2 ltexreg; // lookup texture region normalization vector + +in vec2 f_uv; +in vec4 f_color; +in float f_opacity; + +out vec4 fragColor; + +vec3 linearTosRGB(vec3 linear) { + vec3 lower = linear * 12.92; + vec3 higher = 1.055 * pow(linear, vec3(1.0 / 2.4)) - 0.055; + + // Use smoothstep for a smoother transition + vec3 transition = smoothstep(vec3(0.0031308 - 0.00001), vec3(0.0031308 + 0.00001), linear); + + return mix(lower, higher, transition); +} + +void main() { + vec4 texel = texture(tex, f_uv); + texel = vec4(linearTosRGB(texel.rgb), texel.a); // Convert to linear space + texel.rg = texel.rg * ltexreg; + texel.rg = texel.rg + vec2(0.001953125, 0.001953125); + if (texel.a > 0.0) { // Non-transparent pixel + vec4 lt_texel = texture(ltex, texel.rg); + fragColor = vec4( + mix(lt_texel.rgb, f_color.rgb, f_color.a), + lt_texel.a * f_opacity + ); + } else { + fragColor = vec4( + mix(texel.rgb, f_color.rgb, f_color.a), + texel.a * f_opacity + ); + } +} diff --git a/src/gl/data/lookuptex.vert b/src/gl/data/lookuptex.vert new file mode 100644 index 00000000..7bcc2f1a --- /dev/null +++ b/src/gl/data/lookuptex.vert @@ -0,0 +1,28 @@ +uniform mat4 ortho; +uniform mat4 transform; + +in vec3 position; +in vec2 uv; +in vec4 color; +in float opacity; + +out vec2 f_uv; +out vec4 f_color; +out float f_opacity; + +// Convert an sRGB color to linear space. +vec3 linearize(vec3 srgb) { + bvec3 cutoff = lessThan(srgb, vec3(0.04045)); + vec3 higher = pow((srgb + vec3(0.055)) / vec3(1.055), vec3(2.4)); + vec3 lower = srgb / vec3(12.92); + + return mix(higher, lower, cutoff); +} + +void main() { + f_color = vec4(linearize(color.rgb), color.a); + f_uv = uv; + f_opacity = opacity; + + gl_Position = ortho * transform * vec4(position, 1.0); +} diff --git a/src/gl/mod.rs b/src/gl/mod.rs index c1cc93c4..ef4faa88 100644 --- a/src/gl/mod.rs +++ b/src/gl/mod.rs @@ -31,6 +31,7 @@ use luminance::{ use luminance_derive::{Semantics, UniformInterface, Vertex}; use luminance_gl::gl33; +use std::cell::RefCell; use std::collections::BTreeMap; use std::error::Error; use std::fmt; @@ -39,6 +40,7 @@ use std::mem; use std::time; type Backend = gl33::GL33; +type V2 = [f32; 2]; type M44 = [[f32; 4]; 4]; const SAMPLER: Sampler = Sampler { @@ -127,6 +129,24 @@ struct Screen2dInterface { framebuffer: Uniform>, } +#[derive(UniformInterface)] +struct Lookuptex2dInterface { + tex: Uniform>, + ltex: Uniform>, + ltexreg: Uniform, + ortho: Uniform, + transform: Uniform, +} + +#[repr(C)] +#[derive(Copy, Clone, Vertex)] +#[vertex(sem = "VertexSemantics")] +#[rustfmt::skip] +struct Lookuptex2dVertex { + #[allow(dead_code)] position: VertexPosition, + #[allow(dead_code)] uv: VertexUv, +} + pub struct Renderer { pub win_size: LogicalSize, @@ -153,8 +173,9 @@ pub struct Renderer { shape2d: Program, cursor2d: Program, screen2d: Program, + lookuptex2d: Program, - view_data: BTreeMap, + view_data: BTreeMap>, } struct LayerData { @@ -239,6 +260,7 @@ struct ViewData { layer: LayerData, staging_fb: Framebuffer, anim_tess: Option>, + anim_lt_tess: Option>, layer_tess: Option>, } @@ -256,6 +278,7 @@ impl ViewData { layer: LayerData::new(w, h, pixels, ctx), staging_fb, anim_tess: None, + anim_lt_tess: None, layer_tess: None, } } @@ -399,6 +422,10 @@ impl<'a> renderer::Renderer<'a> for Renderer { include_str!("data/screen.vert"), include_str!("data/screen.frag"), ); + let lookuptex2d = ctx.program::( + include_str!("data/lookuptex.vert"), + include_str!("data/lookuptex.frag"), + ); let physical = win_size.to_physical(scale_factor); let present_fb = @@ -450,6 +477,7 @@ impl<'a> renderer::Renderer<'a> for Renderer { shape2d, cursor2d, screen2d, + lookuptex2d, font, cursors, checker, @@ -495,6 +523,7 @@ impl<'a> renderer::Renderer<'a> for Renderer { shape2d, cursor2d, screen2d, + lookuptex2d, scale_factor, present_fb, blending, @@ -575,90 +604,93 @@ impl<'a> renderer::Renderer<'a> for Renderer { None }; + let mut builder = self.ctx.new_pipeline_gate(); let v = session .views .active() .expect("there must always be an active view"); - let v_data = view_data.get(&v.id).unwrap(); - let l_data = &v_data.layer; let view_ortho = Matrix4::ortho(v.width(), v.fh, Origin::TopLeft); - let mut builder = self.ctx.new_pipeline_gate(); - - // Render to view staging buffer. - builder.pipeline::( - &v_data.staging_fb, - pipeline_st, - |pipeline, mut shd_gate| { - // Render staged brush strokes. - if let Some(tess) = staging_tess { - shd_gate.shade(shape2d, |mut iface, uni, mut rdr_gate| { - iface.set(&uni.ortho, view_ortho.into()); - iface.set(&uni.transform, identity); + { + let v_data = view_data.get(&v.id).unwrap().borrow(); + let l_data = &v_data.layer; + + // Render to view staging buffer. + builder.pipeline::( + &v_data.staging_fb, + pipeline_st, + |pipeline, mut shd_gate| { + // Render staged brush strokes. + if let Some(tess) = staging_tess { + shd_gate.shade(shape2d, |mut iface, uni, mut rdr_gate| { + iface.set(&uni.ortho, view_ortho.into()); + iface.set(&uni.transform, identity); + + rdr_gate.render(render_st, |mut tess_gate| tess_gate.render(&tess)) + })?; + } + // Render staging paste buffer. + if let Some(tess) = paste_tess { + let bound_paste = pipeline + .bind_texture(paste) + .expect("binding textures never fails. qed."); + shd_gate.shade(sprite2d, |mut iface, uni, mut rdr_gate| { + iface.set(&uni.ortho, view_ortho.into()); + iface.set(&uni.transform, identity); + iface.set(&uni.tex, bound_paste.binding()); - rdr_gate.render(render_st, |mut tess_gate| tess_gate.render(&tess)) - })?; - } - // Render staging paste buffer. - if let Some(tess) = paste_tess { + rdr_gate.render(render_st, |mut tess_gate| tess_gate.render(&tess)) + })?; + } + Ok(()) + }, + ); + + // Render to view final buffer. + builder.pipeline::( + &l_data.fb, + &pipeline_st.clone().enable_clear_color(false), + |pipeline, mut shd_gate| { let bound_paste = pipeline .bind_texture(paste) .expect("binding textures never fails. qed."); - shd_gate.shade(sprite2d, |mut iface, uni, mut rdr_gate| { - iface.set(&uni.ortho, view_ortho.into()); - iface.set(&uni.transform, identity); - iface.set(&uni.tex, bound_paste.binding()); - - rdr_gate.render(render_st, |mut tess_gate| tess_gate.render(&tess)) - })?; - } - Ok(()) - }, - ); - - // Render to view final buffer. - builder.pipeline::( - &l_data.fb, - &pipeline_st.clone().enable_clear_color(false), - |pipeline, mut shd_gate| { - let bound_paste = pipeline - .bind_texture(paste) - .expect("binding textures never fails. qed."); - - // Render final brush strokes. - if let Some(tess) = final_tess { - shd_gate.shade(shape2d, |mut iface, uni, mut rdr_gate| { - iface.set(&uni.ortho, view_ortho.into()); - iface.set(&uni.transform, identity); - let render_st = if blending == &Blending::Constant { - render_st.clone().set_blending(blending::Blending { - equation: Equation::Additive, - src: Factor::One, - dst: Factor::Zero, - }) - } else { - render_st.clone() - }; - - rdr_gate.render(&render_st, |mut tess_gate| tess_gate.render(&tess)) - })?; - } - if !paste_outputs.is_empty() { - shd_gate.shade(sprite2d, |mut iface, uni, mut rdr_gate| { - iface.set(&uni.ortho, view_ortho.into()); - iface.set(&uni.transform, identity); - iface.set(&uni.tex, bound_paste.binding()); + // Render final brush strokes. + if let Some(tess) = final_tess { + shd_gate.shade(shape2d, |mut iface, uni, mut rdr_gate| { + iface.set(&uni.ortho, view_ortho.into()); + iface.set(&uni.transform, identity); + + let render_st = if blending == &Blending::Constant { + render_st.clone().set_blending(blending::Blending { + equation: Equation::Additive, + src: Factor::One, + dst: Factor::Zero, + }) + } else { + render_st.clone() + }; + + rdr_gate.render(&render_st, |mut tess_gate| tess_gate.render(&tess)) + })?; + } + if !paste_outputs.is_empty() { + shd_gate.shade(sprite2d, |mut iface, uni, mut rdr_gate| { + iface.set(&uni.ortho, view_ortho.into()); + iface.set(&uni.transform, identity); + iface.set(&uni.tex, bound_paste.binding()); - for out in paste_outputs.drain(..) { - rdr_gate.render(render_st, |mut tess_gate| tess_gate.render(&out))?; - } - Ok(()) - })?; - } - Ok(()) - }, - ); + for out in paste_outputs.drain(..) { + rdr_gate + .render(render_st, |mut tess_gate| tess_gate.render(&out))?; + } + Ok(()) + })?; + } + Ok(()) + }, + ); + } // Render to screen framebuffer. let bg = Rgba::from(session.settings["background"].to_rgba8()); @@ -684,7 +716,8 @@ impl<'a> renderer::Renderer<'a> for Renderer { })?; } - for (id, v) in view_data.iter_mut() { + for (id, rcv) in view_data.iter() { + let mut v = rcv.borrow_mut(); if let Some(view) = session.views.get(*id) { let transform = Matrix4::from_translation( @@ -736,7 +769,8 @@ impl<'a> renderer::Renderer<'a> for Renderer { // Render view animations. if session.settings["animation"].is_set() { - for (id, v) in view_data.iter_mut() { + for (id, v) in view_data.iter() { + let v = &mut *v.borrow_mut(); match (&v.anim_tess, session.views.get(*id)) { (Some(tess), Some(view)) if view.animation.len() > 1 => { let bound_layer = pipeline @@ -780,6 +814,45 @@ impl<'a> renderer::Renderer<'a> for Renderer { Ok(()) })?; + shd_gate.shade(lookuptex2d, |mut iface, uni, mut rdr_gate| { + iface.set(&uni.ortho, ortho); + if session.settings["animation"].is_set() { + for (id, v) in view_data.iter() { + let v = &mut *v.borrow_mut(); + match (&v.anim_lt_tess, session.views.get(*id)) { + (Some(tess), Some(view)) if !view.lookuptexture().is_none() => { + let ltid = view.lookuptexture().unwrap(); + match view_data.get(<id) { + Some(rcltv) => { + let mut ltv = rcltv.borrow_mut(); + let bound_layer = pipeline + .bind_texture(v.layer.fb.color_slot()) + .expect("binding textures never fails"); + let lookup_layer = pipeline + .bind_texture(ltv.layer.fb.color_slot()) + .expect("binding textures never fails"); + let t = Matrix4::from_translation( + Vector2::new(0., view.zoom).extend(0.), + ); + // Render layer animation. + iface.set(&uni.tex, bound_layer.binding()); + iface.set(&uni.ltex, lookup_layer.binding()); + iface.set(&uni.ltexreg, [0.5, 1.]); + iface.set(&uni.transform, t.into()); + rdr_gate.render(render_st, |mut tess_gate| { + tess_gate.render(tess) + })?; + } + _ => (), + } + } + _ => (), + } + } + } + Ok(()) + })?; + // Render help. if let Some((win_tess, text_tess)) = help_tess { shd_gate.shade(shape2d, |_iface, _uni, mut rdr_gate| { @@ -859,7 +932,7 @@ impl<'a> renderer::Renderer<'a> for Renderer { let extent = v.extent(); if let Some(vr) = session.views.get_mut(id) { - let v_data = view_data.get_mut(&id).unwrap(); + let mut v_data = view_data.get(&id).unwrap().borrow_mut(); match state { ViewState::Dirty(_) if is_resized => { @@ -939,8 +1012,10 @@ impl Renderer { if let Some((s, pixels)) = session.views.get_snapshot_safe(id) { let (w, h) = (s.width(), s.height()); - self.view_data - .insert(id, ViewData::new(w, h, Some(pixels), &mut self.ctx)); + self.view_data.insert( + id, + RefCell::new(ViewData::new(w, h, Some(pixels), &mut self.ctx)), + ); } } Effect::ViewRemoved(id) => { @@ -983,10 +1058,11 @@ impl Renderer { self.resize_view(v, *w, *h)?; } ViewOp::Clear(color) => { - let view = self + let mut view = self .view_data - .get_mut(&v.id) - .expect("views must have associated view data"); + .get(&v.id) + .expect("views must have associated view data") + .borrow_mut(); view.layer .fb @@ -995,24 +1071,20 @@ impl Renderer { .map_err(Error::Texture)?; } ViewOp::Blit(src, dst) => { - let view = self + let mut view = self .view_data - .get_mut(&v.id) - .expect("views must have associated view data"); + .get(&v.id) + .expect("views must have associated view data") + .borrow_mut(); let (_, texels) = v.layer.get_snapshot_rect(&src.map(|n| n as i32)).unwrap(); // TODO: Handle this nicely? let texels = util::align_u8(&texels); - view.layer - .fb - .color_slot() - .upload_part_raw( - GenMipmaps::No, - [dst.x1 as u32, dst.y1 as u32], - [src.width() as u32, src.height() as u32], - texels, - ) - .map_err(Error::Texture)?; + view.layer.upload_part( + [dst.x1 as u32, dst.y1 as u32], + [src.width() as u32, src.height() as u32], + texels, + )?; } ViewOp::Yank(src) => { let (_, pixels) = v.layer.get_snapshot_rect(&src.map(|n| n)).unwrap(); @@ -1083,17 +1155,60 @@ impl Renderer { ); } ViewOp::SetPixel(rgba, x, y) => { - let fb = &mut self + let layer = &mut self .view_data - .get_mut(&v.id) + .get(&v.id) .expect("views must have associated view data") - .layer - .fb; + .borrow_mut() + .layer; let texels = &[*rgba]; let texels = util::align_u8(texels); - fb.color_slot() - .upload_part_raw(GenMipmaps::No, [*x as u32, *y as u32], [1, 1], texels) - .map_err(Error::Texture)?; + layer.upload_part([*x as u32, *y as u32], [1, 1], texels)?; + } + //TODO: could be more generic + ViewOp::GenerateLookupTextureIR(src, dst) => { + let mut view = self + .view_data + .get(&v.id) + .expect("views must have associated view data") + .borrow_mut(); + + let (_, pixels) = v.layer.get_snapshot_rect(&src.map(|n| n as i32)).unwrap(); // TODO: Handle this nicely? + + let modified: Vec = pixels + .iter() + .enumerate() + .map(|(i, p)| { + if p.a == 0 { + return Rgba8 { + r: 0, + g: 0, + b: 0, + a: 0, + }; + } + + let x = i as f32 % src.width(); + let xf = 256.0 / src.width(); + let y = (i as f32 / src.width()).floor(); + let yf = 256.0 / src.height(); + + Rgba8 { + r: (x * xf).floor() as u8, + g: (y * yf).floor() as u8, + b: p.r.max(p.g).max(p.b), + a: p.a, + } + }) + .collect(); + + let modified = util::align_u8(&modified); + + view.layer.upload_part( + [dst.x1 as u32, dst.y1 as u32], + [src.width() as u32, src.height() as u32], + modified, + )?; } } } @@ -1103,8 +1218,9 @@ impl Renderer { fn handle_view_damaged(&mut self, view: &View) -> Result<(), RendererError> { let layer = &mut self .view_data - .get_mut(&view.id) + .get(&view.id) .expect("views must have associated view data") + .borrow_mut() .layer; let (_, pixels) = view.layer.current_snapshot(); @@ -1156,7 +1272,7 @@ impl Renderer { l.upload_part([0, vh - th], [tw, th], texels)?; } - self.view_data.insert(view.id, view_data); + self.view_data.insert(view.id, RefCell::new(view_data)); Ok(()) } @@ -1167,16 +1283,29 @@ impl Renderer { } // TODO: Does this need to run if the view has only one frame? for v in s.views.iter() { + if v.is_lookuptexture() { + continue; + } // FIXME: When `v.animation.val()` doesn't change, we don't need // to re-create the buffer. let batch = draw::draw_view_animation(s, v); - - if let Some(vd) = self.view_data.get_mut(&v.id) { - vd.anim_tess = Some( + if let Some(vd) = self.view_data.get(&v.id) { + vd.borrow_mut().anim_tess = Some( self.ctx .tessellation::<_, Sprite2dVertex>(batch.vertices().as_slice()), ); } + + // lookup-texture animation enabled + if let Some(_) = v.lookuptexture() { + let ltbatch = draw::draw_view_lookuptexture_animation(s, v); + if let Some(vd) = self.view_data.get(&v.id) { + vd.borrow_mut().anim_lt_tess = Some( + self.ctx + .tessellation::<_, Sprite2dVertex>(ltbatch.vertices().as_slice()), + ); + } + } } } @@ -1184,8 +1313,8 @@ impl Renderer { for v in s.views.iter() { let batch = draw::draw_view_composites(s, v); - if let Some(vd) = self.view_data.get_mut(&v.id) { - vd.layer_tess = Some( + if let Some(vd) = self.view_data.get(&v.id) { + vd.borrow_mut().layer_tess = Some( self.ctx .tessellation::<_, Sprite2dVertex>(batch.vertices().as_slice()), ); diff --git a/src/session.rs b/src/session.rs index a9da6135..09b21ce8 100644 --- a/src/session.rs +++ b/src/session.rs @@ -91,6 +91,7 @@ impl fmt::Display for Mode { Self::Visual(VisualState::Selecting { dragging: true }) => "visual (dragging)".fmt(f), Self::Visual(VisualState::Selecting { .. }) => "visual".fmt(f), Self::Visual(VisualState::Pasting) => "visual (pasting)".fmt(f), + Self::Visual(VisualState::LookupSampling) => "visual (LUT sampling)".fmt(f), Self::Command => "command".fmt(f), Self::Present => "present".fmt(f), Self::Help => "help".fmt(f), @@ -102,6 +103,7 @@ impl fmt::Display for Mode { pub enum VisualState { Selecting { dragging: bool }, Pasting, + LookupSampling, } impl VisualState { @@ -162,6 +164,30 @@ impl Deref for Selection { } } +#[derive(Clone)] +pub struct LookupSampleCandidate(pub Rect, pub Rgba8); + +#[derive(Clone)] +pub struct LookupSampleState { + pub candidates: Vec, + pub selected: i32, + pub color: Rgba8, +} + +impl LookupSampleState { + fn new(color: Rgba8) -> LookupSampleState { + LookupSampleState { + candidates: Vec::new(), + selected: -1, + color: color + } + } + + fn next(&mut self, i: i32) { + self.selected = (self.selected + i) % self.candidates.len() as i32; + } +} + /// Session effects. Eg. view creation/destruction. /// Anything the renderer might want to know. #[derive(Clone, Debug)] @@ -233,6 +259,8 @@ pub enum Tool { Sampler, /// Used to pan the workspace. Pan(PanState), + /// Used to sample colors from lookup texture + LookupTextureSampler, } #[derive(PartialEq, Eq, Debug, Clone, Copy)] @@ -614,6 +642,10 @@ pub struct Session { /// Current pixel selection. pub selection: Option, + /// Current pixel selection. + pub lookup_sample_state: Option, + pub prev_lookup_sample_state: Option, + /// The session's current settings. pub settings: Settings, /// Settings recently changed. @@ -737,6 +769,8 @@ impl Session { mode: Mode::Normal, prev_mode: Option::default(), selection: Option::default(), + lookup_sample_state: Option::default(), + prev_lookup_sample_state: Option::default(), message: Message::default(), avg_time: time::Duration::from_secs(0), frame_number: 0, @@ -1178,6 +1212,11 @@ impl Session { Mode::Command => { self.cmdline.clear(); } + Mode::Visual(VisualState::LookupSampling) => { + if self.lookup_sample_state.is_some() { + self.prev_lookup_sample_state = std::mem::replace(&mut self.lookup_sample_state, None); + } + } _ => {} } @@ -1895,6 +1934,10 @@ impl Session { } debug!("flood fill in: {:?}", start_time.elapsed()); } + Tool::LookupTextureSampler => { + self.command(Command::LookupTextureSample); + self.prev_tool(); + } }, Mode::Command => { // TODO @@ -1918,6 +1961,9 @@ impl Session { self.center_selection(self.cursor); self.command(Command::SelectionPaste); } + Mode::Visual(VisualState::LookupSampling) => { + // TODO + } Mode::Present | Mode::Help => {} } } else { @@ -2100,6 +2146,37 @@ impl Session { return; } } + Mode::Visual(VisualState::LookupSampling) => { + if state == InputState::Pressed { + match key { + platform::Key::Escape => { + self.switch_mode(Mode::Normal); + return; + } + platform::Key::Tab => { + if let Some(lss) = self.lookup_sample_state.as_mut() { + if modifiers.shift { + lss.next(-1); + } else { + lss.next(1); + } + } + return; + } + platform::Key::Return => { + if let Some(lss) = &self.lookup_sample_state { + if lss.selected >= 0 { + self.pick_color(lss.candidates[lss.selected as usize].1) + } + } + self.switch_mode(Mode::Normal); + self.tool(Tool::Brush); + return; + } + _ => {} + } + } + } Mode::Command => { if state == InputState::Pressed { match key { @@ -3006,6 +3083,98 @@ impl Session { v.paint_color(*color, x, y); } } + Command::LookupTextureMode(on) => { + if on { + self.active_view_mut().lookuptexture_on(); + } else { + self.active_view_mut().lookuptexture_off(); + } + } + Command::LookupTextureSet(d) => { + if d == 0 { + self.message( + format!("Cannot set a view as its own lookup texture"), + MessageType::Error, + ); + return; + } + + let current = self.views.active_id; + if let Some(id) = self.views.relativen(current, d) { + self.active_view_mut().lookuptexture_set(id); + if !self.view(id).is_lookuptexture() { + self.activate(id); // TODO: seems to crash on first draw without it + self.view_mut(id).lookuptexture_on(); + } + } + } + Command::LookupTextureSample => { + let v = self.active_view(); + if v.lookuptexture().is_none() { + self.message( + format!("Current view has no lookup texture"), + MessageType::Error, + ); + return; + } + + let lutid = v.lookuptexture().unwrap(); + let lutv = self + .views + .get(lutid) + .expect(&format!("view #{} must exist", lutid)); + + let (_, pixels) = lutv.layer.current_snapshot(); + + let hover = self.hover_color; + if hover.is_none() { + self.message(format!("Not hovering on any color"), MessageType::Error); + return; + } + + let hover = hover.unwrap(); + + let mut lss = LookupSampleState::new(hover); + for (i, pixel) in pixels.iter().cloned().enumerate() { + let vw = lutv.extent.fw * lutv.extent.nframes as u32; + let x = (i as u32 % vw) as f32; + let r = (i as u32 % vw) as f32; + let rf = 256.0 / (lutv.extent.fw as f32); + let y = (lutv.extent.fh - 1 - i as u32 / vw) as f32; + let g = (i as f32 / (vw as f32)).floor(); + let gf = 256.0 / (lutv.extent.fh as f32); + if pixel == hover { + // register potential candidate + lss.candidates.push(LookupSampleCandidate( + Rect::new(x, y, x + 1., y + 1.), + Rgba8::new( + (r * rf).floor() as u8, + (g * gf).floor() as u8, + pixel.r.max(pixel.g).max(pixel.b) as u8, + 255, + ), + )) + } + } + + if let Some(prevlss) = &self.prev_lookup_sample_state { + if prevlss.color == lss.color && + (prevlss.selected as usize) < lss.candidates.len() { + lss.selected = prevlss.selected; + } + } + + + if lss.candidates.len() > 0 { + self.lookup_sample_state = Some(lss); + self.switch_mode(Mode::Visual(VisualState::LookupSampling)); + } else { + self.message( + format!("No matching pixels in lookup texture"), + MessageType::Warning, + ); + } + } }; } diff --git a/src/view.rs b/src/view.rs index 9bb93277..e28b45f8 100644 --- a/src/view.rs +++ b/src/view.rs @@ -107,6 +107,8 @@ pub enum ViewOp { Resize(u32, u32), /// Paint a single pixel. SetPixel(Rgba8, i32, i32), + /// Generate initial lookup texture map for given area. + GenerateLookupTextureIR(Rect, Rect), } /// A view on a sprite or image. @@ -139,6 +141,9 @@ pub struct View { /// Which view snapshot has been saved to disk, if any. saved_snapshot: Option, + /// Which other view is current one's lookup texture + lookuptexture: Option, + lookuptexture_on: bool } /// View animation. @@ -207,6 +212,8 @@ impl View { animation: Animation::new(frames), state: ViewState::Okay, saved_snapshot, + lookuptexture: None, + lookuptexture_on: false, resource, } } @@ -411,6 +418,16 @@ impl View { self.state == ViewState::Okay } + /// Check whether the view is configured as a lookup texture. + pub fn is_lookuptexture(&self) -> bool { + self.lookuptexture_on + } + + /// Return the set lookup texture if exists. + pub fn lookuptexture(&self) -> Option { + self.lookuptexture + } + /// Return the file status as a string. pub fn status(&self) -> String { self.file_status.to_string() @@ -554,6 +571,38 @@ impl View { Ok(e_id) } + + /// Set another view as current view's lookup texture + pub fn lookuptexture_set(&mut self, ltid: ViewId) { + assert!(self.id != ltid, "cannot set a view as its own lookup texture"); + self.lookuptexture = Some(ltid); + } + + /// Set current view as a lookup texture + pub fn lookuptexture_on(&mut self) { + self.lookuptexture_on = true; + + if self.animation.len() == 1 { + let width = self.width() as f32; + let (fw, fh) = (self.fw as f32, self.fh as f32); + + self.extend(); + // build initial intermediate map + self.ops.push(ViewOp::GenerateLookupTextureIR( + Rect::new(0., 0., fw as f32, fh), + Rect::new(width, 0., width + fw, fh), + )); + } + } + + /// Set current view as a lookup texture + pub fn lookuptexture_off(&mut self) { + self.lookuptexture_on = false; + + if self.animation.len() > 1 { + self.shrink(); + } + } } /////////////////////////////////////////////////////////////////////////////// @@ -767,11 +816,32 @@ impl ViewManager { self.range(id..).nth(1) } + /// Get nth `ViewId` *after* given id. + pub fn aftern(&self, id: ViewId, n: usize) -> Option { + self.range(id..).nth(n) + } + /// Get `ViewId` *before* given id. pub fn before(&self, id: ViewId) -> Option { self.range(..id).next_back() } + /// Get nth `ViewId` *before* given id. + pub fn beforen(&self, id: ViewId, n: usize) -> Option { + self.range(..id).nth_back(n) + } + + /// Get nth `ViewId` relative (before or after) to given id + pub fn relativen(&self, id: ViewId, n: i32) -> Option { + if n > 0 { + self.aftern(id, n as usize) + } else if n < 0 { + self.beforen(id, n.abs() as usize - 1) + } else { // n == 0 + Some(id) + } + } + /// Get the first view. pub fn first(&self) -> Option<&View> { self.iter().next() diff --git a/src/view/resource.rs b/src/view/resource.rs index cd8e5010..2821cee6 100644 --- a/src/view/resource.rs +++ b/src/view/resource.rs @@ -307,6 +307,15 @@ impl LayerResource { if !(snapshot_rect.x1 <= rect.x1 && snapshot_rect.y1 <= rect.y1) || !(snapshot_rect.x2 >= rect.x2 && snapshot_rect.y2 >= rect.y2) { + debug!("snaption rect out of bounds: snaphot: ({},{}),({},{}) rect: ({},{}),({},{})", + snapshot_rect.x1, + snapshot_rect.y1, + snapshot_rect.x2, + snapshot_rect.y2, + rect.x1, + rect.y1, + rect.x2, + rect.y2); return None; } debug_assert!(w * h <= total_w * total_h);