Visualize poster and thumbnail cropping against video on poster image page

This commit is contained in:
Simon Repp
2025-07-17 12:25:54 +02:00
parent 7768f51994
commit 6802d7e7e3
4 changed files with 155 additions and 7 deletions
+77 -6
View File
@@ -372,6 +372,83 @@ p a {
.permalink_split {
display: flex;
}
.poster_preview_outer {
aspect-ratio: 1.78;
background: top left/1rem var(--checkerboard);
}
.poster_preview_outer img {
display: block;
max-height: 100%;
max-width: 100%;
}
.poster_preview_outer .poster_frame {
margin-inline: auto;
max-height: 100%;
max-width: 100%;
position: relative;
}
.poster_preview_outer .thumbnail_frame_outer {
align-items: center;
bottom: 0;
container-type: size;
display: flex;
justify-content: center;
left: 0;
position: absolute;
right: 0;
top: 0;
z-index: 2;
}
.poster_preview_outer .thumbnail_frame {
aspect-ratio: var(--poster-aspect);
background: hsla(202.5, 100%, 49.6%, 0.5);
box-sizing: border-box;
border: 2px dashed #fff;
height: 100%;
width: auto;
}
@container (aspect-ratio < 1) {
.poster_preview_outer .thumbnail_frame {
height: auto;
width: 100%;
}
}
.poster_preview_outer .video_frame_outer {
align-items: center;
aspect-ratio: var(--video-aspect);
display: flex;
justify-content: center;
margin-inline: auto;
max-height: 100%;
max-width: 100%;
position: relative;
}
.poster_preview_outer .video_frame {
bottom: 0;
background: hsla(331.8, 100%, 50%, 0.5);
border: 2px dashed #fff;
box-sizing: border-box;
left: 0;
position: absolute;
right: 0;
top: 0;
z-index: 1;
}
.poster_preview_legend { margin-block: 1rem; }
.poster_preview_legend .dot {
border-radius: 50%;
display: inline-block;
height: 1em;
width: 1em;
}
.poster_preview_legend .dot.thumbnail_frame {
background: hsla(202.5, 100%, 49.6%, 0.5);
border: 2px dashed #fff;
}
.poster_preview_legend .dot.video_frame {
background: hsla(331.8, 100%, 50%, 0.5);
border: 2px dashed #fff;
}
pre.error {
background: var(--danger);
border-radius: .2rem;
@@ -562,12 +639,6 @@ svg { fill: currentColor; }
font-size: .8rem;
text-align: right;
}
.thumbnail {
aspect-ratio: 1.78;
background: top left/1rem var(--checkerboard);
object-fit: contain;
width: 100%;
}
#toggle-splash svg { pointer-events: none; }
.tree_view_options {
align-items: center;
+36 -1
View File
@@ -9,6 +9,7 @@ use actix_web::web::{Data, Path as ActixPath};
use indoc::formatdoc;
use crate::{
AspectRatio,
Context,
Language,
SitePath,
@@ -84,9 +85,43 @@ pub fn edit_poster_page(
let delete_poster_action = routes::video_delete_poster(video);
let update_poster_action = routes::video_update_poster(video);
let poster_aspect_numeric = site.poster_aspect.numeric();
let poster_aspect_representation = site.poster_aspect.representation();
let video_aspect_numeric = video.recommended_version()
.and_then(|video_file| video_file.video_meta.as_ref())
.map(|video_meta| video_meta.aspect_ratio())
.unwrap_or(poster_aspect_numeric);
let video_aspect_representation = AspectRatio::approximate_representation(video_aspect_numeric);
let t_thumbnail_format = translations.thumbnail_format;
let t_video_format = translations.video_format;
formatdoc!(r#"
<img {alt}class="thumbnail" src="{src}">
<style>
:root {{
--poster-aspect: {poster_aspect_numeric};
--video-aspect: {video_aspect_numeric};
}}
</style>
<div class="poster_preview_outer">
<div class="video_frame_outer">
<div class="video_frame"></div>
<div class="poster_frame">
<div class="thumbnail_frame_outer">
<div class="thumbnail_frame"></div>
</div>
<img {alt} src="{src}">
</div>
</div>
</div>
<div>{file_name}</div>
<div class="poster_preview_legend">
<div>
<span class="dot video_frame"></span> {t_video_format} ({video_aspect_representation})
</div>
<div>
<span class="dot thumbnail_frame"></span> {t_thumbnail_format} ({poster_aspect_representation})
</div>
</div>
<div class="button_group">
{poster_upload_button}
<form action="{delete_poster_action}" data-confirm data-confirm-label="{t_delete_poster}" data-confirm-text="{t_really_delete}" method="post">
+36
View File
@@ -78,6 +78,35 @@ pub enum SiteContent {
}
impl AspectRatio {
/// For some common, handpicked aspect ratios we try to automatically display
/// them to the user (e.g. 16:9 instead of 1.7777778 or such).
pub fn approximate_representation(numeric_ratio: f32) -> String {
const RATIOS: [(f32, &str); 10] = [
(2.37, "64:27"),
(1.78, "16:9"),
(1.66, "5:3"),
(1.5, "3:2"),
(1.33, "4:3"),
(0.75, "3:4"),
(0.67, "2:3"),
(0.6, "3:5"),
(0.56, "9:16"),
(0.42, "27:64")
];
// Could be adjusted up/down if we feel our approximations are too
// strictly/loosely determined.
const TOLERANCE: f32 = 0.01;
for ratio in RATIOS {
if (ratio.0 - numeric_ratio).abs() <= TOLERANCE {
return ratio.1.to_string();
}
}
format!("{numeric_ratio:.2}")
}
pub fn from_representation(representation: &str) -> Result<AspectRatio, String> {
if let Ok(ratio) = representation.parse::<f32>() {
let aspect_ratio = AspectRatio::Custom {
@@ -110,6 +139,13 @@ impl AspectRatio {
AspectRatio::Custom { value, .. } => *value
}
}
pub fn representation(&self) -> &str {
match self {
AspectRatio::Default => "16:9",
AspectRatio::Custom { representation, .. } => representation
}
}
}
impl Site {
+6
View File
@@ -231,6 +231,7 @@ pub struct Translations {
pub theme: &'static str,
pub this_video_can_be_seamlessly_converted_to_a_video_in_a_collection: &'static str,
pub this_video_can_be_seamlessly_converted_to_a_video_in_a_playlist: &'static str,
pub thumbnail_format: &'static str,
pub title: &'static str,
pub title_ascending: &'static str,
pub title_descending: &'static str,
@@ -259,6 +260,7 @@ pub struct Translations {
pub version: &'static str,
pub video: &'static str,
pub video_files: &'static str,
pub video_format: &'static str,
pub video_manifest: &'static str,
pub video_not_yet_available: &'static str,
pub video_order: &'static str,
@@ -513,6 +515,7 @@ impl Translations {
theme: "Theme",
this_video_can_be_seamlessly_converted_to_a_video_in_a_collection: "Dieses Video kann problemlos zu einem Video in einer Collection konvertiert werden.",
this_video_can_be_seamlessly_converted_to_a_video_in_a_playlist: "Dieses Video kann problemlos zu einem Video in einer Playlist konvertiert werden.",
thumbnail_format: "Vorschauformat",
title: "Titel",
title_ascending: "Titel Aufsteigend (A, B, C)",
title_descending: "Titel Absteigend (Z, Y, X)",
@@ -541,6 +544,7 @@ impl Translations {
version: "Version",
video: "Video",
video_files: "Videodateien",
video_format: "Videoformat",
video_manifest: "Video Manifest",
video_not_yet_available: "Video noch nicht verfügbar.",
video_order: "Video Reihenfolge",
@@ -796,6 +800,7 @@ impl Translations {
theme: "Theme",
this_video_can_be_seamlessly_converted_to_a_video_in_a_collection: "This video can be seamlessly converted to a video in a collection.",
this_video_can_be_seamlessly_converted_to_a_video_in_a_playlist: "This video can be seamlessly converted to a video in a playlist.",
thumbnail_format: "Thumbnail format",
title: "Title",
title_ascending: "Title Ascending (A, B, C)",
title_descending: "Title Descending (Z, Y, X)",
@@ -824,6 +829,7 @@ impl Translations {
version: "Version",
video: "Video",
video_files: "Video files",
video_format: "Video format",
video_manifest: "Video manifest",
video_not_yet_available: "Video not yet available.",
video_order: "Video Order",