implement justified layout

This commit is contained in:
Johannes Heuel
2022-08-17 20:16:52 +02:00
parent 9b8e38eca6
commit 43d0566db9
14 changed files with 521 additions and 929 deletions

View File

@@ -0,0 +1,137 @@
use crate::gallery::Picture;
use common::OutputPicture;
use pathfinding::prelude::dijkstra;
use weblog::console_log;
use yew::prelude::*;
use yew::{function_component, html};
#[derive(Clone, Debug, Properties, PartialEq)]
pub struct GridProps {
#[prop_or_default]
pub pictures: Vec<OutputPicture>,
#[prop_or_default]
pub width: u32,
}
fn get_common_height(row: &[OutputPicture], container_width: u32, margin: u32) -> f32 {
let row_width: u32 = container_width - row.len() as u32 * (margin * 2);
let total_aspect_ratio: f32 = row
.iter()
.map(|p| (p.width as f32) / (p.height as f32))
.sum();
row_width as f32 / total_aspect_ratio
}
fn cost(
photos: &[OutputPicture],
i: usize,
j: usize,
width: u32,
target_height: u32,
margin: u32,
) -> u32 {
let common_height = get_common_height(&photos[i..j], width, margin);
(common_height - target_height as f32).powi(2) as u32
}
fn make_successors(
target_height: u32,
container_width: u32,
photos: &Vec<OutputPicture>,
limit_node_search: usize,
margin: u32,
) -> Vec<Vec<(usize, u32)>> {
let mut results = vec![Vec::new(); photos.len()];
(0..photos.len()).for_each(|start| {
for j in start + 1..photos.len() + 1 {
if j - start > limit_node_search {
break;
}
results[start].push((
j,
cost(photos, start, j, container_width, target_height, margin),
));
}
});
results
}
// guesstimate how many neighboring nodes should be searched based on
// the aspect ratio of the container with images having an avg AR of 1.5
// as the minimum amount of photos per row, plus some nodes
fn find_ideal_node_search(target_row_height: u32, container_width: u32) -> usize {
let row_aspect_ratio = container_width as f32 / target_row_height as f32;
(row_aspect_ratio / 1.5) as usize + 8
}
fn compute_row_layout(
container_width: u32,
limit_node_search: usize,
target_height: u32,
margin: u32,
photos: &Vec<OutputPicture>,
) -> Option<Vec<(u32, u32)>> {
console_log!("compute row layout for width: {}", container_width);
if photos.is_empty() {
return None;
}
let successors = make_successors(
target_height,
container_width,
photos,
limit_node_search,
margin,
);
let path = dijkstra(&0, |p| successors[*p].clone(), |p| *p == photos.len());
let (path, _cost) = if let Some(p) = path {
p
} else {
(Vec::new(), 0)
};
let mut dimensions: Vec<(u32, u32)> = Vec::with_capacity(photos.len());
for i in 1..path.len() {
let row = &photos[path[i - 1]..path[i]];
let height = get_common_height(row, container_width, margin) as u32;
(path[i - 1]..path[i]).for_each(|j| {
let ratio = photos[j].width as f32 / photos[j].height as f32;
dimensions.push(((height as f32 * ratio) as u32, height));
});
}
Some(dimensions)
}
#[function_component(Grid)]
pub fn grid(props: &GridProps) -> Html {
let target_height = 300;
let container_width = if props.width == 0 { 0 } else { props.width - 4 };
let limit_node_search = find_ideal_node_search(target_height, container_width);
let margin = 2;
let dimensions = compute_row_layout(
container_width,
limit_node_search,
target_height,
margin,
&props.pictures,
);
let dimensions = if let Some(d) = dimensions {
d
} else {
props.pictures.iter().map(|p| (p.width, p.height)).collect()
};
html! {
<div style={
concat!(
"display: flex;",
"flex-wrap: wrap;",
"flex-direction: row;",
"border: 1px solid black;",
)}>
{ props.pictures.iter().zip(dimensions).map(|(p, d)|
html!{<Picture margin={margin} picture={p.clone()} width={d.0} height={d.1} />}
).collect::<Html>()}
</div>
}
}

View File

@@ -0,0 +1,5 @@
pub mod grid;
pub use grid::Grid;
pub mod picture;
pub use picture::Picture;

View File

@@ -0,0 +1,27 @@
use common::OutputPicture;
use yew::prelude::*;
#[derive(Clone, Debug, Properties, PartialEq)]
pub struct PictureProps {
pub picture: OutputPicture,
pub width: u32,
pub height: u32,
pub margin: u32,
}
#[function_component(Picture)]
pub fn picture(props: &PictureProps) -> Html {
let thumb = if let Some(thumb) = &props.picture.thumbnail {
format!("/api/pictures/{}", thumb)
} else {
"".into()
};
let margin = props.margin.to_string();
let width = props.width.to_string();
let height = props.height.to_string();
html! {
<>
<img style={format!("margin: {}px; display: block;", margin)} src={thumb} width={width} height={height} />
</>
}
}

View File

@@ -3,173 +3,52 @@
#[global_allocator]
static ALLOC: wee_alloc::WeeAlloc = wee_alloc::WeeAlloc::INIT;
mod gallery;
use common::OutputPicture;
use console_error_panic_hook::set_once as set_panic_hook;
use gloo_net::http::Request;
use wasm_bindgen::prelude::*;
use ybc::TileCtx::{Ancestor, Child, Parent};
use yew_hooks::prelude::*;
use gallery::Grid;
use yew::prelude::*;
// use pathfinding::directed::dijkstra;
use yew::format::Json;
use yew::format::Nothing;
use yew::services::fetch::FetchService;
use yew::services::ConsoleService;
use yew::services::fetch::FetchTask;
use yew::services::fetch::Request;
use yew::services::fetch::Response;
#[function_component(App)]
fn app() -> Html {
let node = use_node_ref();
let size = use_size(node.clone());
pub enum Msg {
GetPictures,
ReceiveResponse(Result<Vec<OutputPicture>, anyhow::Error>),
}
struct App {
pictures: Option<Vec<OutputPicture>>,
fetch_task: Option<FetchTask>,
link: ComponentLink<Self>,
error: Option<String>,
}
impl App {
fn view_fetching(&self) -> Html {
if self.fetch_task.is_some() {
html! { <p>{ "Fetching data..." }</p> }
} else {
html! { <p></p> }
}
}
fn view_error(&self) -> Html {
if let Some(ref error) = self.error {
html! { <p>{ error.clone() }</p> }
} else {
html! {}
}
}
}
impl Component for App {
type Message = Msg;
type Properties = ();
fn create(_: Self::Properties, link: ComponentLink<Self>) -> Self {
Self {
fetch_task: None,
link,
pictures: None,
error: None,
}
let pictures = use_state(|| vec![]);
{
let pictures = pictures.clone();
use_effect_with_deps(
move |_| {
let pictures = pictures.clone();
wasm_bindgen_futures::spawn_local(async move {
let url = "/api/pictures/";
let fetched_pictures: Vec<OutputPicture> = Request::get(url)
.send()
.await
.unwrap()
.json()
.await
.unwrap();
pictures.set(fetched_pictures);
});
|| ()
},
(),
);
}
fn update(&mut self, msg: Self::Message) -> bool {
match msg {
Msg::GetPictures => {
let request = Request::get("/api/pictures/")
.body(Nothing)
.expect("Could not build that request");
let callback =
self.link
.callback(|response: Response<Json<Result<Vec<OutputPicture>, anyhow::Error>>>| {
let Json(data) = response.into_body();
Msg::ReceiveResponse(data)
});
let task = FetchService::fetch(request, callback).expect("failed to start request");
self.fetch_task = Some(task);
true
}
Msg::ReceiveResponse(response) => {
match response {
Ok(pictures) => {
self.pictures = Some(pictures);
}
Err(error) => {
self.error = Some(error.to_string());
}
}
self.fetch_task = None;
true
}
}
}
fn change(&mut self, _: Self::Properties) -> bool {
false
}
fn view(&self) -> Html {
html! {
<>
<ybc::Navbar
classes=classes!("is-success")
padded=true
navbrand=html!{
<ybc::NavbarItem>
<ybc::Title classes=classes!("has-text-white") size=ybc::HeaderSize::Is4>{"Trunk | Yew | YBC"}</ybc::Title>
</ybc::NavbarItem>
}
navstart=html!{}
navend=html!{
<>
<ybc::NavbarItem>
<ybc::ButtonAnchor classes=classes!("is-black", "is-outlined") rel=String::from("noopener noreferrer") target=String::from("_blank") href="https://github.com/thedodd/trunk">
{"Trunk"}
</ybc::ButtonAnchor>
</ybc::NavbarItem>
<ybc::NavbarItem>
<ybc::ButtonAnchor classes=classes!("is-black", "is-outlined") rel=String::from("noopener noreferrer") target=String::from("_blank") href="https://yew.rs">
{"Yew"}
</ybc::ButtonAnchor>
</ybc::NavbarItem>
<ybc::NavbarItem>
<ybc::ButtonAnchor classes=classes!("is-black", "is-outlined") rel=String::from("noopener noreferrer") target=String::from("_blank") href="https://github.com/thedodd/ybc">
{"YBC"}
</ybc::ButtonAnchor>
</ybc::NavbarItem>
</>
}
/>
<ybc::Hero
classes=classes!("is-light")
size=ybc::HeroSize::FullheightWithNavbar
body=html!{
<ybc::Container classes=classes!("is-centered")>
<ybc::Tile ctx=Ancestor>
<>
<button onclick=self.link.callback(|_| Msg::GetPictures)>
{ "Load pictures" }
</button>
{ self.view_fetching() }
{ self.view_error() }
</>
<ybc::Tile ctx=Parent size=ybc::TileSize::Twelve>
<ybc::Tile ctx=Parent>
<ybc::Tile ctx=Child classes=classes!("notification", "is-success")>
<ybc::Subtitle size=ybc::HeaderSize::Is3 classes=classes!("has-text-white")>{"Trunk"}</ybc::Subtitle>
<p>{"Trunk is a WASM web application bundler for Rust."}</p>
</ybc::Tile>
</ybc::Tile>
<ybc::Tile ctx=Parent>
<ybc::Tile ctx=Child classes=classes!("notification", "is-success")>
<ybc::Icon size=ybc::Size::Large classes=classes!("is-pulled-right")><img src="yew.svg"/></ybc::Icon>
<ybc::Subtitle size=ybc::HeaderSize::Is3 classes=classes!("has-text-white")>
{"Yew"}
</ybc::Subtitle>
<p>{"Yew is a modern Rust framework for creating multi-threaded front-end web apps with WebAssembly."}</p>
</ybc::Tile>
</ybc::Tile>
<ybc::Tile ctx=Parent>
<ybc::Tile ctx=Child classes=classes!("notification", "is-success")>
<ybc::Subtitle size=ybc::HeaderSize::Is3 classes=classes!("has-text-white")>{"YBC"}</ybc::Subtitle>
<p>{"A Yew component library based on the Bulma CSS framework."}</p>
</ybc::Tile>
</ybc::Tile>
</ybc::Tile>
</ybc::Tile>
</ybc::Container>
}>
</ybc::Hero>
</>
}
html! {
<>
<div ref={node} style={"position: 'relative'"} >
<Grid pictures={(*pictures).clone()} width={size.0} />
</div>
</>
}
}
@@ -185,11 +64,11 @@ fn main() {
// Show off some feature flag enabling patterns.
#[cfg(feature = "demo-abc")]
{
ConsoleService::log("feature `demo-abc` enabled");
// ConsoleService::log("feature `demo-abc` enabled");
}
#[cfg(feature = "demo-xyz")]
{
ConsoleService::log("feature `demo-xyz` enabled");
// ConsoleService::log("feature `demo-xyz` enabled");
}
yew::start_app::<App>();