media/viewer: fullscreen transition animations
Signed-off-by: William Casarin <jb55@jb55.com>
This commit is contained in:
@@ -25,6 +25,12 @@ pub struct ViewMediaInfo {
|
|||||||
pub medias: Vec<MediaInfo>,
|
pub medias: Vec<MediaInfo>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl ViewMediaInfo {
|
||||||
|
pub fn clicked_media(&self) -> &MediaInfo {
|
||||||
|
&self.medias[self.clicked_index]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Actions generated by media ui interactions
|
/// Actions generated by media ui interactions
|
||||||
pub enum MediaAction {
|
pub enum MediaAction {
|
||||||
/// An image was clicked on in a carousel, we have
|
/// An image was clicked on in a carousel, we have
|
||||||
|
|||||||
@@ -696,7 +696,6 @@ fn chrome_handle_app_action(
|
|||||||
ctx.zaps,
|
ctx.zaps,
|
||||||
ctx.img_cache,
|
ctx.img_cache,
|
||||||
&mut columns.view_state,
|
&mut columns.view_state,
|
||||||
&mut columns.options,
|
|
||||||
ui,
|
ui,
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -753,7 +752,6 @@ fn columns_route_to_profile(
|
|||||||
ctx.zaps,
|
ctx.zaps,
|
||||||
ctx.img_cache,
|
ctx.img_cache,
|
||||||
&mut columns.view_state,
|
&mut columns.view_state,
|
||||||
&mut columns.options,
|
|
||||||
ui,
|
ui,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
use crate::{
|
use crate::{
|
||||||
column::Columns,
|
column::Columns,
|
||||||
nav::{RouterAction, RouterType},
|
nav::{RouterAction, RouterType},
|
||||||
options::AppOptions,
|
|
||||||
route::Route,
|
route::Route,
|
||||||
timeline::{
|
timeline::{
|
||||||
thread::{
|
thread::{
|
||||||
@@ -18,6 +17,7 @@ use notedeck::{
|
|||||||
get_wallet_for, note::ZapTargetAmount, Accounts, GlobalWallet, Images, NoteAction, NoteCache,
|
get_wallet_for, note::ZapTargetAmount, Accounts, GlobalWallet, Images, NoteAction, NoteCache,
|
||||||
NoteZapTargetOwned, UnknownIds, ZapAction, ZapTarget, ZappingError, Zaps,
|
NoteZapTargetOwned, UnknownIds, ZapAction, ZapTarget, ZappingError, Zaps,
|
||||||
};
|
};
|
||||||
|
use notedeck_ui::media::MediaViewerFlags;
|
||||||
use tracing::error;
|
use tracing::error;
|
||||||
|
|
||||||
pub struct NewNotes {
|
pub struct NewNotes {
|
||||||
@@ -54,7 +54,6 @@ fn execute_note_action(
|
|||||||
zaps: &mut Zaps,
|
zaps: &mut Zaps,
|
||||||
images: &mut Images,
|
images: &mut Images,
|
||||||
view_state: &mut ViewState,
|
view_state: &mut ViewState,
|
||||||
app_options: &mut AppOptions,
|
|
||||||
router_type: RouterType,
|
router_type: RouterType,
|
||||||
ui: &mut egui::Ui,
|
ui: &mut egui::Ui,
|
||||||
col: usize,
|
col: usize,
|
||||||
@@ -160,7 +159,10 @@ fn execute_note_action(
|
|||||||
media_action.on_view_media(|medias| {
|
media_action.on_view_media(|medias| {
|
||||||
view_state.media_viewer.media_info = medias.clone();
|
view_state.media_viewer.media_info = medias.clone();
|
||||||
tracing::debug!("on_view_media {:?}", &medias);
|
tracing::debug!("on_view_media {:?}", &medias);
|
||||||
app_options.set(AppOptions::FullscreenMedia, true);
|
view_state
|
||||||
|
.media_viewer
|
||||||
|
.flags
|
||||||
|
.set(MediaViewerFlags::Open, true);
|
||||||
});
|
});
|
||||||
|
|
||||||
media_action.process_default_media_actions(images)
|
media_action.process_default_media_actions(images)
|
||||||
@@ -191,7 +193,6 @@ pub fn execute_and_process_note_action(
|
|||||||
zaps: &mut Zaps,
|
zaps: &mut Zaps,
|
||||||
images: &mut Images,
|
images: &mut Images,
|
||||||
view_state: &mut ViewState,
|
view_state: &mut ViewState,
|
||||||
app_options: &mut AppOptions,
|
|
||||||
ui: &mut egui::Ui,
|
ui: &mut egui::Ui,
|
||||||
) -> Option<RouterAction> {
|
) -> Option<RouterAction> {
|
||||||
let router_type = {
|
let router_type = {
|
||||||
@@ -217,7 +218,6 @@ pub fn execute_and_process_note_action(
|
|||||||
zaps,
|
zaps,
|
||||||
images,
|
images,
|
||||||
view_state,
|
view_state,
|
||||||
app_options,
|
|
||||||
router_type,
|
router_type,
|
||||||
ui,
|
ui,
|
||||||
col,
|
col,
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ use notedeck::{
|
|||||||
Images, JobsCache, Localization, UnknownIds,
|
Images, JobsCache, Localization, UnknownIds,
|
||||||
};
|
};
|
||||||
use notedeck_ui::{
|
use notedeck_ui::{
|
||||||
media::{MediaViewer, MediaViewerState},
|
media::{MediaViewer, MediaViewerFlags, MediaViewerState},
|
||||||
NoteOptions,
|
NoteOptions,
|
||||||
};
|
};
|
||||||
use std::collections::{BTreeSet, HashMap};
|
use std::collections::{BTreeSet, HashMap};
|
||||||
@@ -368,12 +368,7 @@ fn render_damus(
|
|||||||
render_damus_desktop(damus, app_ctx, ui)
|
render_damus_desktop(damus, app_ctx, ui)
|
||||||
};
|
};
|
||||||
|
|
||||||
fullscreen_media_viewer_ui(
|
fullscreen_media_viewer_ui(ui, &mut damus.view_state.media_viewer, app_ctx.img_cache);
|
||||||
ui,
|
|
||||||
&mut damus.options,
|
|
||||||
&mut damus.view_state.media_viewer,
|
|
||||||
app_ctx.img_cache,
|
|
||||||
);
|
|
||||||
|
|
||||||
// We use this for keeping timestamps and things up to date
|
// We use this for keeping timestamps and things up to date
|
||||||
ui.ctx().request_repaint_after(Duration::from_secs(5));
|
ui.ctx().request_repaint_after(Duration::from_secs(5));
|
||||||
@@ -386,33 +381,35 @@ fn render_damus(
|
|||||||
/// an image is clicked
|
/// an image is clicked
|
||||||
fn fullscreen_media_viewer_ui(
|
fn fullscreen_media_viewer_ui(
|
||||||
ui: &mut egui::Ui,
|
ui: &mut egui::Ui,
|
||||||
options: &mut AppOptions,
|
state: &mut MediaViewerState,
|
||||||
viewer_state: &mut MediaViewerState,
|
|
||||||
img_cache: &mut Images,
|
img_cache: &mut Images,
|
||||||
) {
|
) {
|
||||||
if !options.contains(AppOptions::FullscreenMedia) || viewer_state.media_info.medias.is_empty() {
|
if !state.should_show(ui) {
|
||||||
|
if state.scene_rect.is_some() {
|
||||||
|
// if we shouldn't show yet we will have a scene
|
||||||
|
// rect, then we should clear it for next time
|
||||||
|
tracing::debug!("fullscreen_media_viewer_ui: resetting scene rect");
|
||||||
|
state.scene_rect = None;
|
||||||
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Close it?
|
// Close it?
|
||||||
if ui.input(|i| i.key_pressed(egui::Key::Escape)) {
|
if ui.input(|i| i.key_pressed(egui::Key::Escape)) {
|
||||||
fullscreen_media_close(options, viewer_state);
|
fullscreen_media_close(state);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
let resp = MediaViewer::new(viewer_state)
|
let resp = MediaViewer::new(state).fullscreen(true).ui(img_cache, ui);
|
||||||
.fullscreen(true)
|
|
||||||
.ui(img_cache, ui);
|
|
||||||
|
|
||||||
if resp.clicked() {
|
if resp.clicked() {
|
||||||
fullscreen_media_close(options, viewer_state);
|
fullscreen_media_close(state);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Close the fullscreen media player. This also resets the scene_rect state
|
/// Close the fullscreen media player. This also resets the scene_rect state
|
||||||
fn fullscreen_media_close(options: &mut AppOptions, state: &mut MediaViewerState) {
|
fn fullscreen_media_close(state: &mut MediaViewerState) {
|
||||||
options.set(AppOptions::FullscreenMedia, false);
|
state.flags.set(MediaViewerFlags::Open, false);
|
||||||
state.scene_rect = None;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|
|||||||
@@ -460,7 +460,6 @@ fn process_render_nav_action(
|
|||||||
ctx.zaps,
|
ctx.zaps,
|
||||||
ctx.img_cache,
|
ctx.img_cache,
|
||||||
&mut app.view_state,
|
&mut app.view_state,
|
||||||
&mut app.options,
|
|
||||||
ui,
|
ui,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,9 +16,6 @@ bitflags! {
|
|||||||
|
|
||||||
/// Should we scroll to top on the active column?
|
/// Should we scroll to top on the active column?
|
||||||
const ScrollToTop = 1 << 3;
|
const ScrollToTop = 1 << 3;
|
||||||
|
|
||||||
/// Are we showing fullscreen media?
|
|
||||||
const FullscreenMedia = 1 << 4;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
mod viewer;
|
mod viewer;
|
||||||
|
|
||||||
pub use viewer::{MediaViewer, MediaViewerState};
|
pub use viewer::{MediaViewer, MediaViewerFlags, MediaViewerState};
|
||||||
|
|||||||
@@ -1,34 +1,93 @@
|
|||||||
use egui::{pos2, Color32, Rect};
|
use bitflags::bitflags;
|
||||||
|
use egui::{emath::TSTransform, pos2, Color32, Rangef, Rect};
|
||||||
use notedeck::media::{MediaInfo, ViewMediaInfo};
|
use notedeck::media::{MediaInfo, ViewMediaInfo};
|
||||||
use notedeck::{ImageType, Images};
|
use notedeck::{ImageType, Images};
|
||||||
|
|
||||||
|
bitflags! {
|
||||||
|
#[repr(transparent)]
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
|
||||||
|
pub struct MediaViewerFlags: u64 {
|
||||||
|
/// Open the media viewer fullscreen
|
||||||
|
const Fullscreen = 1 << 0;
|
||||||
|
|
||||||
|
/// Enable a transition animation
|
||||||
|
const Transition = 1 << 1;
|
||||||
|
|
||||||
|
/// Are we open or closed?
|
||||||
|
const Open = 1 << 2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// State used in the MediaViewer ui widget.
|
/// State used in the MediaViewer ui widget.
|
||||||
#[derive(Default)]
|
|
||||||
pub struct MediaViewerState {
|
pub struct MediaViewerState {
|
||||||
/// When
|
/// When
|
||||||
pub media_info: ViewMediaInfo,
|
pub media_info: ViewMediaInfo,
|
||||||
pub scene_rect: Option<Rect>,
|
pub scene_rect: Option<Rect>,
|
||||||
|
pub flags: MediaViewerFlags,
|
||||||
|
pub anim_id: egui::Id,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for MediaViewerState {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
anim_id: egui::Id::new("notedeck-fullscreen-media-viewer"),
|
||||||
|
media_info: Default::default(),
|
||||||
|
scene_rect: None,
|
||||||
|
flags: MediaViewerFlags::Transition | MediaViewerFlags::Fullscreen,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl MediaViewerState {
|
||||||
|
pub fn new(anim_id: egui::Id) -> Self {
|
||||||
|
Self {
|
||||||
|
anim_id,
|
||||||
|
..Default::default()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// How much is our media viewer open
|
||||||
|
pub fn open_amount(&self, ui: &mut egui::Ui) -> f32 {
|
||||||
|
ui.ctx()
|
||||||
|
.animate_bool_responsive(self.anim_id, self.flags.contains(MediaViewerFlags::Open))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Should we show the control even if we're closed?
|
||||||
|
/// Needed for transition animation
|
||||||
|
pub fn should_show(&self, ui: &mut egui::Ui) -> bool {
|
||||||
|
if self.flags.contains(MediaViewerFlags::Open) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// we are closing
|
||||||
|
self.open_amount(ui) > 0.0
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A panning, scrolling, optionally fullscreen, and tiling media viewer
|
/// A panning, scrolling, optionally fullscreen, and tiling media viewer
|
||||||
pub struct MediaViewer<'a> {
|
pub struct MediaViewer<'a> {
|
||||||
state: &'a mut MediaViewerState,
|
state: &'a mut MediaViewerState,
|
||||||
fullscreen: bool,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a> MediaViewer<'a> {
|
impl<'a> MediaViewer<'a> {
|
||||||
pub fn new(state: &'a mut MediaViewerState) -> Self {
|
pub fn new(state: &'a mut MediaViewerState) -> Self {
|
||||||
let fullscreen = false;
|
Self { state }
|
||||||
Self { state, fullscreen }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn fullscreen(mut self, enable: bool) -> Self {
|
/// Is this
|
||||||
self.fullscreen = enable;
|
pub fn fullscreen(self, enable: bool) -> Self {
|
||||||
|
self.state.flags.set(MediaViewerFlags::Fullscreen, enable);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Enable open transition animation
|
||||||
|
pub fn transition(self, enable: bool) -> Self {
|
||||||
|
self.state.flags.set(MediaViewerFlags::Transition, enable);
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn ui(&mut self, images: &mut Images, ui: &mut egui::Ui) -> egui::Response {
|
pub fn ui(&mut self, images: &mut Images, ui: &mut egui::Ui) -> egui::Response {
|
||||||
if self.fullscreen {
|
if self.state.flags.contains(MediaViewerFlags::Fullscreen) {
|
||||||
egui::Window::new("Media Viewer")
|
egui::Window::new("Media Viewer")
|
||||||
.title_bar(false)
|
.title_bar(false)
|
||||||
.fixed_size(ui.ctx().screen_rect().size())
|
.fixed_size(ui.ctx().screen_rect().size())
|
||||||
@@ -45,37 +104,100 @@ impl<'a> MediaViewer<'a> {
|
|||||||
|
|
||||||
fn ui_content(&mut self, images: &mut Images, ui: &mut egui::Ui) -> egui::Response {
|
fn ui_content(&mut self, images: &mut Images, ui: &mut egui::Ui) -> egui::Response {
|
||||||
let avail_rect = ui.available_rect_before_wrap();
|
let avail_rect = ui.available_rect_before_wrap();
|
||||||
//let id = ui.id().with("media_viewer");
|
|
||||||
|
|
||||||
let mut scene_rect = if let Some(scene_rect) = self.state.scene_rect {
|
let scene_rect = if let Some(scene_rect) = self.state.scene_rect {
|
||||||
scene_rect
|
scene_rect
|
||||||
} else {
|
} else {
|
||||||
self.state.scene_rect = Some(avail_rect);
|
self.state.scene_rect = Some(avail_rect);
|
||||||
avail_rect
|
avail_rect
|
||||||
};
|
};
|
||||||
|
|
||||||
// Draw background
|
let zoom_range: egui::Rangef = (0.0..=10.0).into();
|
||||||
ui.painter()
|
|
||||||
.rect_filled(avail_rect, 0.0, egui::Color32::from_black_alpha(128));
|
|
||||||
|
|
||||||
let resp = egui::Scene::new()
|
let is_open = self.state.flags.contains(MediaViewerFlags::Open);
|
||||||
.zoom_range(0.0..=10.0) // enhance 🔬
|
let can_transition = self.state.flags.contains(MediaViewerFlags::Transition);
|
||||||
.show(ui, &mut scene_rect, |ui| {
|
let open_amount = self.state.open_amount(ui);
|
||||||
Self::render_image_tiles(&self.state.media_info.medias, images, ui);
|
let transitioning = if !can_transition {
|
||||||
|
false
|
||||||
|
} else if is_open {
|
||||||
|
open_amount < 1.0
|
||||||
|
} else {
|
||||||
|
open_amount > 0.0
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut trans_rect = if transitioning {
|
||||||
|
let clicked_img = &self.state.media_info.clicked_media();
|
||||||
|
let src_pos = &clicked_img.original_position;
|
||||||
|
let in_scene_pos = Self::first_image_rect(ui, clicked_img, images);
|
||||||
|
transition_scene_rect(
|
||||||
|
&avail_rect,
|
||||||
|
&zoom_range,
|
||||||
|
&in_scene_pos,
|
||||||
|
src_pos,
|
||||||
|
open_amount,
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
scene_rect
|
||||||
|
};
|
||||||
|
|
||||||
|
// Draw background
|
||||||
|
ui.painter().rect_filled(
|
||||||
|
avail_rect,
|
||||||
|
0.0,
|
||||||
|
egui::Color32::from_black_alpha((128.0 * open_amount) as u8),
|
||||||
|
);
|
||||||
|
|
||||||
|
let scene = egui::Scene::new().zoom_range(zoom_range);
|
||||||
|
|
||||||
|
// We are opening, so lock controls
|
||||||
|
/* TODO(jb55): 0.32
|
||||||
|
if transitioning {
|
||||||
|
scene = scene.sense(egui::Sense::hover());
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
|
let resp = scene.show(ui, &mut trans_rect, |ui| {
|
||||||
|
Self::render_image_tiles(&self.state.media_info.medias, images, ui, open_amount);
|
||||||
});
|
});
|
||||||
|
|
||||||
self.state.scene_rect = Some(scene_rect);
|
self.state.scene_rect = Some(trans_rect);
|
||||||
|
|
||||||
resp.response
|
resp.response
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The rect of the first image to be placed.
|
||||||
|
/// This is mainly used for the transition animation
|
||||||
|
///
|
||||||
|
/// TODO(jb55): replace this with a "placed" variant once
|
||||||
|
/// we have image layouts
|
||||||
|
fn first_image_rect(ui: &mut egui::Ui, media: &MediaInfo, images: &mut Images) -> Rect {
|
||||||
|
// fetch image texture
|
||||||
|
let Some(texture) = images.latest_texture(ui, &media.url, ImageType::Content(None)) else {
|
||||||
|
tracing::error!("could not get latest texture in first_image_rect");
|
||||||
|
return Rect::ZERO;
|
||||||
|
};
|
||||||
|
|
||||||
|
// the area the next image will be put in.
|
||||||
|
let mut img_rect = ui.available_rect_before_wrap();
|
||||||
|
|
||||||
|
let size = texture.size_vec2();
|
||||||
|
img_rect.set_height(size.y);
|
||||||
|
img_rect.set_width(size.x);
|
||||||
|
img_rect
|
||||||
|
}
|
||||||
|
|
||||||
///
|
///
|
||||||
/// Tile a scene with images.
|
/// Tile a scene with images.
|
||||||
///
|
///
|
||||||
/// TODO(jb55): Let's improve image tiling over time, spiraling outward. We
|
/// TODO(jb55): Let's improve image tiling over time, spiraling outward. We
|
||||||
/// should have a way to click "next" and have the scene smoothly transition and
|
/// should have a way to click "next" and have the scene smoothly transition and
|
||||||
/// focus on the next image
|
/// focus on the next image
|
||||||
fn render_image_tiles(infos: &[MediaInfo], images: &mut Images, ui: &mut egui::Ui) {
|
fn render_image_tiles(
|
||||||
|
infos: &[MediaInfo],
|
||||||
|
images: &mut Images,
|
||||||
|
ui: &mut egui::Ui,
|
||||||
|
open_amount: f32,
|
||||||
|
) {
|
||||||
for info in infos {
|
for info in infos {
|
||||||
let url = &info.url;
|
let url = &info.url;
|
||||||
|
|
||||||
@@ -108,11 +230,71 @@ impl<'a> MediaViewer<'a> {
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
// Paint image
|
// Paint image
|
||||||
ui.painter()
|
ui.painter().image(
|
||||||
.image(texture.id(), img_rect, uv, Color32::WHITE);
|
texture.id(),
|
||||||
|
img_rect,
|
||||||
|
uv,
|
||||||
|
Color32::from_white_alpha((open_amount * 255.0) as u8),
|
||||||
|
);
|
||||||
|
|
||||||
ui.advance_cursor_after_rect(img_rect);
|
ui.advance_cursor_after_rect(img_rect);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Helper: lerp a TSTransform (uniform scale + translation)
|
||||||
|
fn lerp_ts(a: TSTransform, b: TSTransform, t: f32) -> TSTransform {
|
||||||
|
let s = egui::lerp(a.scaling..=b.scaling, t);
|
||||||
|
let p = a.translation + (b.translation - a.translation) * t;
|
||||||
|
TSTransform {
|
||||||
|
scaling: s,
|
||||||
|
translation: p,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Calculate the open/close amount and transition rect
|
||||||
|
pub fn transition_scene_rect(
|
||||||
|
outer_rect: &Rect,
|
||||||
|
zoom_range: &Rangef,
|
||||||
|
image_rect_in_scene: &Rect, // e.g. Rect::from_min_size(Pos2::ZERO, image_size)
|
||||||
|
timeline_global_rect: &Rect, // saved from timeline Response.rect
|
||||||
|
open_amt: f32, // stable ID per media item
|
||||||
|
) -> Rect {
|
||||||
|
// Compute the two endpoints:
|
||||||
|
let from = fit_to_rect_in_scene(timeline_global_rect, image_rect_in_scene, zoom_range);
|
||||||
|
let to = fit_to_rect_in_scene(outer_rect, image_rect_in_scene, zoom_range);
|
||||||
|
|
||||||
|
// Interpolate transform and convert to scene_rect expected by Scene::show:
|
||||||
|
let lerped = lerp_ts(from, to, open_amt);
|
||||||
|
|
||||||
|
lerped.inverse() * (*outer_rect)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Creates a transformation that fits a given scene rectangle into the available screen size.
|
||||||
|
///
|
||||||
|
/// The resulting visual scene bounds can be larger, due to letterboxing.
|
||||||
|
///
|
||||||
|
/// Returns the transformation from `scene` to `global` coordinates.
|
||||||
|
fn fit_to_rect_in_scene(
|
||||||
|
rect_in_global: &Rect,
|
||||||
|
rect_in_scene: &Rect,
|
||||||
|
zoom_range: &Rangef,
|
||||||
|
) -> TSTransform {
|
||||||
|
// Compute the scale factor to fit the bounding rectangle into the available screen size:
|
||||||
|
let scale = rect_in_global.size() / rect_in_scene.size();
|
||||||
|
|
||||||
|
// Use the smaller of the two scales to ensure the whole rectangle fits on the screen:
|
||||||
|
let scale = scale.min_elem();
|
||||||
|
|
||||||
|
// Clamp scale to what is allowed
|
||||||
|
let scale = zoom_range.clamp(scale);
|
||||||
|
|
||||||
|
// Compute the translation to center the bounding rect in the screen:
|
||||||
|
let center_in_global = rect_in_global.center().to_vec2();
|
||||||
|
let center_scene = rect_in_scene.center().to_vec2();
|
||||||
|
|
||||||
|
// Set the transformation to scale and then translate to center.
|
||||||
|
TSTransform::from_translation(center_in_global - scale * center_scene)
|
||||||
|
* TSTransform::from_scaling(scale)
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user