mirror of
https://github.com/mfontanini/presenterm.git
synced 2025-05-05 15:32:58 +00:00
perf: pre-scale ascii images when transitions are enabled (#550)
Some checks failed
Deploy docs / build-and-deploy (push) Has been cancelled
Merge checks / Checks (push) Has been cancelled
Merge checks / Validate nix flake (push) Has been cancelled
Merge checks / Validate bat assets (push) Has been cancelled
Merge checks / Validate JSON schemas (push) Has been cancelled
Some checks failed
Deploy docs / build-and-deploy (push) Has been cancelled
Merge checks / Checks (push) Has been cancelled
Merge checks / Validate nix flake (push) Has been cancelled
Merge checks / Validate bat assets (push) Has been cancelled
Merge checks / Validate JSON schemas (push) Has been cancelled
This pre-scales images when the presentation is reloaded and transitions are enabled. For now only filesystem images are scaled but the next PR will do this for generated ones as well. The end result of this is when you transition between slides and there's images in them, the animation is smooth the first time because the scaling, which is costly, is done ahead of time.
This commit is contained in:
commit
a97e66fedf
@ -13,6 +13,7 @@ use crate::{
|
||||
},
|
||||
render::{
|
||||
ErrorSource, RenderError, RenderResult, TerminalDrawer, TerminalDrawerOptions,
|
||||
ascii_scaler::AsciiScaler,
|
||||
engine::{MaxSize, RenderEngine, RenderEngineOptions},
|
||||
operation::RenderAsyncState,
|
||||
properties::WindowSize,
|
||||
@ -109,7 +110,7 @@ impl<'a> Presenter<'a> {
|
||||
self.resources.watch_presentation_file(path.to_path_buf());
|
||||
}
|
||||
self.state = PresenterState::Presenting(Presentation::from(vec![]));
|
||||
self.try_reload(path, true);
|
||||
self.try_reload(path, true)?;
|
||||
|
||||
let drawer_options = TerminalDrawerOptions {
|
||||
font_size_fallback: self.options.font_size_fallback,
|
||||
@ -148,10 +149,11 @@ impl<'a> Presenter<'a> {
|
||||
break;
|
||||
}
|
||||
CommandSideEffect::Reload => {
|
||||
self.try_reload(path, false);
|
||||
self.try_reload(path, false)?;
|
||||
break;
|
||||
}
|
||||
CommandSideEffect::Redraw => {
|
||||
self.try_scale_transition_images()?;
|
||||
break;
|
||||
}
|
||||
CommandSideEffect::NextSlide => {
|
||||
@ -326,9 +328,9 @@ impl<'a> Presenter<'a> {
|
||||
if needs_redraw { CommandSideEffect::Redraw } else { CommandSideEffect::None }
|
||||
}
|
||||
|
||||
fn try_reload(&mut self, path: &Path, force: bool) {
|
||||
fn try_reload(&mut self, path: &Path, force: bool) -> RenderResult {
|
||||
if matches!(self.options.mode, PresentMode::Presentation) && !force {
|
||||
return;
|
||||
return Ok(());
|
||||
}
|
||||
self.slides_with_pending_async_renders.clear();
|
||||
self.resources.clear_watches();
|
||||
@ -344,12 +346,25 @@ impl<'a> Presenter<'a> {
|
||||
}
|
||||
self.slides_with_pending_async_renders = presentation.slides_with_async_renders().into_iter().collect();
|
||||
self.state = self.validate_overflows(presentation);
|
||||
self.try_scale_transition_images()?;
|
||||
}
|
||||
Err(e) => {
|
||||
let presentation = mem::take(&mut self.state).into_presentation();
|
||||
self.state = PresenterState::failure(e, presentation, ErrorSource::Presentation, FailureMode::Other);
|
||||
}
|
||||
};
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn try_scale_transition_images(&self) -> RenderResult {
|
||||
if self.options.transition.is_none() {
|
||||
return Ok(());
|
||||
}
|
||||
let options = RenderEngineOptions { max_size: self.options.max_size.clone(), ..Default::default() };
|
||||
let scaler = AsciiScaler::new(options, self.resources.image_registry());
|
||||
let dimensions = WindowSize::current(self.options.font_size_fallback)?;
|
||||
scaler.process(self.state.presentation(), &dimensions)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn is_displaying_other_error(&self) -> bool {
|
||||
|
106
src/render/ascii_scaler.rs
Normal file
106
src/render/ascii_scaler.rs
Normal file
@ -0,0 +1,106 @@
|
||||
use super::{
|
||||
RenderError,
|
||||
engine::{RenderEngine, RenderEngineOptions},
|
||||
};
|
||||
use crate::{
|
||||
ImageRegistry, WindowSize,
|
||||
presentation::Presentation,
|
||||
terminal::{
|
||||
image::{Image, ImageSource},
|
||||
printer::{TerminalCommand, TerminalError, TerminalIo},
|
||||
},
|
||||
};
|
||||
use std::thread;
|
||||
use unicode_width::UnicodeWidthStr;
|
||||
|
||||
pub(crate) struct AsciiScaler {
|
||||
options: RenderEngineOptions,
|
||||
registry: ImageRegistry,
|
||||
}
|
||||
|
||||
impl AsciiScaler {
|
||||
pub(crate) fn new(options: RenderEngineOptions, registry: ImageRegistry) -> Self {
|
||||
Self { options, registry }
|
||||
}
|
||||
|
||||
pub(crate) fn process(self, presentation: &Presentation, dimensions: &WindowSize) -> Result<(), RenderError> {
|
||||
let mut collector = ImageCollector::default();
|
||||
for slide in presentation.iter_slides() {
|
||||
let engine = RenderEngine::new(&mut collector, dimensions.clone(), self.options.clone());
|
||||
engine.render(slide.iter_operations())?;
|
||||
}
|
||||
thread::spawn(move || Self::scale(collector.images, self.registry));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn scale(images: Vec<ScalableImage>, registry: ImageRegistry) {
|
||||
for image in images {
|
||||
let ascii_image = registry.as_ascii(&image.image);
|
||||
ascii_image.cache_scaling(image.columns, image.rows);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct ScalableImage {
|
||||
image: Image,
|
||||
rows: u16,
|
||||
columns: u16,
|
||||
}
|
||||
|
||||
struct ImageCollector {
|
||||
current_column: u16,
|
||||
current_row: u16,
|
||||
current_row_height: u16,
|
||||
images: Vec<ScalableImage>,
|
||||
}
|
||||
|
||||
impl Default for ImageCollector {
|
||||
fn default() -> Self {
|
||||
Self { current_row: 0, current_column: 0, current_row_height: 1, images: Default::default() }
|
||||
}
|
||||
}
|
||||
|
||||
impl TerminalIo for ImageCollector {
|
||||
fn execute(&mut self, command: &TerminalCommand<'_>) -> Result<(), TerminalError> {
|
||||
use TerminalCommand::*;
|
||||
match command {
|
||||
MoveTo { column, row } => {
|
||||
self.current_column = *column;
|
||||
self.current_row = *row;
|
||||
}
|
||||
MoveToRow(row) => self.current_row = *row,
|
||||
MoveToColumn(column) => self.current_column = *column,
|
||||
MoveDown(amount) => self.current_row = self.current_row.saturating_add(*amount),
|
||||
MoveRight(amount) => self.current_column = self.current_column.saturating_add(*amount),
|
||||
MoveLeft(amount) => self.current_column = self.current_column.saturating_sub(*amount),
|
||||
MoveToNextLine => {
|
||||
self.current_row = self.current_row.saturating_add(1);
|
||||
self.current_column = 0;
|
||||
self.current_row_height = 1;
|
||||
}
|
||||
PrintText { content, style } => {
|
||||
self.current_column = self.current_column.saturating_add(content.width() as u16);
|
||||
self.current_row_height = self.current_row_height.max(style.size as u16);
|
||||
}
|
||||
PrintImage { image, options } => {
|
||||
// we can only really cache filesystem images for now
|
||||
if matches!(image.source, ImageSource::Filesystem(_)) {
|
||||
let image =
|
||||
ScalableImage { image: image.clone(), rows: options.rows * 2, columns: options.columns };
|
||||
self.images.push(image);
|
||||
}
|
||||
}
|
||||
ClearScreen => {
|
||||
self.current_column = 0;
|
||||
self.current_row = 0;
|
||||
self.current_row_height = 1;
|
||||
}
|
||||
BeginUpdate | EndUpdate | Flush | SetColors(_) | SetBackgroundColor(_) => (),
|
||||
};
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn cursor_row(&self) -> u16 {
|
||||
self.current_row
|
||||
}
|
||||
}
|
@ -1,3 +1,4 @@
|
||||
pub(crate) mod ascii_scaler;
|
||||
pub(crate) mod engine;
|
||||
pub(crate) mod layout;
|
||||
pub(crate) mod operation;
|
||||
|
@ -23,6 +23,18 @@ pub(crate) struct AsciiImage {
|
||||
cached_sizes: Arc<Mutex<HashMap<(u16, u16), RgbaImage>>>,
|
||||
}
|
||||
|
||||
impl AsciiImage {
|
||||
pub(crate) fn cache_scaling(&self, columns: u16, rows: u16) {
|
||||
let mut cached_sizes = self.cached_sizes.lock().unwrap();
|
||||
// lookup on cache/resize the image and store it in cache
|
||||
let cache_key = (columns, rows);
|
||||
if cached_sizes.get(&cache_key).is_none() {
|
||||
let image = self.image.resize_exact(columns as u32, rows as u32, FilterType::Triangle);
|
||||
cached_sizes.insert(cache_key, image.into_rgba8());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ImageProperties for AsciiImage {
|
||||
fn dimensions(&self) -> (u32, u32) {
|
||||
self.image.dimensions()
|
||||
@ -91,20 +103,14 @@ impl PrintImage for AsciiPrinter {
|
||||
{
|
||||
let columns = options.columns;
|
||||
let rows = options.rows * 2;
|
||||
let mut cached_sizes = image.cached_sizes.lock().unwrap();
|
||||
// Scale it first
|
||||
image.cache_scaling(columns, rows);
|
||||
|
||||
// lookup on cache/resize the image and store it in cache
|
||||
let cache_key = (columns, rows);
|
||||
let image = match cached_sizes.get(&cache_key) {
|
||||
Some(image) => image,
|
||||
None => {
|
||||
let image = image.image.resize_exact(columns as u32, rows as u32, FilterType::Triangle);
|
||||
cached_sizes.insert(cache_key, image.into_rgba8());
|
||||
cached_sizes.get(&cache_key).unwrap()
|
||||
}
|
||||
};
|
||||
// The strategy here is taken from viuer: use half vertical ascii blocks in combination
|
||||
// with foreground/background colors to fit 2 vertical pixels per cell. That is, cell (x, y)
|
||||
// will contain the pixels at (x, y) and (x, y + 1) combined.
|
||||
let cached_sizes = image.cached_sizes.lock().unwrap();
|
||||
let image = cached_sizes.get(&cache_key).expect("scaled image no longer there");
|
||||
|
||||
let default_background = options.background_color.map(Color::from);
|
||||
|
||||
// Iterate pixel rows in pairs to be able to merge both pixels in a single iteration.
|
||||
|
Loading…
x
Reference in New Issue
Block a user