Skip to content

Commit b1a2cec

Browse files
committed
Add blurhash to gallery
1 parent 4bbb5cb commit b1a2cec

File tree

4 files changed

+174
-36
lines changed

4 files changed

+174
-36
lines changed

Cargo.lock

+7
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

examples/gallery/Cargo.toml

+2
Original file line numberDiff line numberDiff line change
@@ -19,5 +19,7 @@ bytes.workspace = true
1919
image.workspace = true
2020
tokio.workspace = true
2121

22+
blurhash = "0.2.3"
23+
2224
[lints]
2325
workspace = true

examples/gallery/src/civitai.rs

+29-4
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ use std::sync::Arc;
1010
pub struct Image {
1111
pub id: Id,
1212
url: String,
13+
hash: String,
1314
}
1415

1516
impl Image {
@@ -40,20 +41,37 @@ impl Image {
4041
Ok(response.items)
4142
}
4243

44+
pub async fn blurhash(
45+
self,
46+
width: u32,
47+
height: u32,
48+
) -> Result<Rgba, Error> {
49+
task::spawn_blocking(move || {
50+
let pixels = blurhash::decode(&self.hash, width, height, 1.0)?;
51+
52+
Ok::<_, Error>(Rgba {
53+
width,
54+
height,
55+
pixels: Bytes::from(pixels),
56+
})
57+
})
58+
.await?
59+
}
60+
4361
pub async fn download(self, size: Size) -> Result<Rgba, Error> {
4462
let client = reqwest::Client::new();
4563

4664
let bytes = client
4765
.get(match size {
4866
Size::Original => self.url,
49-
Size::Thumbnail => self
67+
Size::Thumbnail { width } => self
5068
.url
5169
.split("/")
5270
.map(|part| {
5371
if part.starts_with("width=") {
54-
"width=640"
72+
format!("width={width}")
5573
} else {
56-
part
74+
part.to_string()
5775
}
5876
})
5977
.collect::<Vec<_>>()
@@ -107,7 +125,7 @@ impl fmt::Debug for Rgba {
107125
#[derive(Debug, Clone, Copy)]
108126
pub enum Size {
109127
Original,
110-
Thumbnail,
128+
Thumbnail { width: u16 },
111129
}
112130

113131
#[derive(Debug, Clone)]
@@ -117,6 +135,7 @@ pub enum Error {
117135
IOFailed(Arc<io::Error>),
118136
JoinFailed(Arc<task::JoinError>),
119137
ImageDecodingFailed(Arc<image::ImageError>),
138+
BlurhashDecodingFailed(Arc<blurhash::Error>),
120139
}
121140

122141
impl From<reqwest::Error> for Error {
@@ -142,3 +161,9 @@ impl From<image::ImageError> for Error {
142161
Self::ImageDecodingFailed(Arc::new(error))
143162
}
144163
}
164+
165+
impl From<blurhash::Error> for Error {
166+
fn from(error: blurhash::Error) -> Self {
167+
Self::BlurhashDecodingFailed(Arc::new(error))
168+
}
169+
}

examples/gallery/src/main.rs

+136-32
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ fn main() -> iced::Result {
2828

2929
struct Gallery {
3030
images: Vec<Image>,
31-
thumbnails: HashMap<Id, Thumbnail>,
31+
previews: HashMap<Id, Preview>,
3232
viewer: Viewer,
3333
now: Instant,
3434
}
@@ -40,6 +40,7 @@ enum Message {
4040
ImageDownloaded(Result<Rgba, Error>),
4141
ThumbnailDownloaded(Id, Result<Rgba, Error>),
4242
ThumbnailHovered(Id, bool),
43+
BlurhashDecoded(Id, Result<Rgba, Error>),
4344
Open(Id),
4445
Close,
4546
Animate(Instant),
@@ -50,7 +51,7 @@ impl Gallery {
5051
(
5152
Self {
5253
images: Vec::new(),
53-
thumbnails: HashMap::new(),
54+
previews: HashMap::new(),
5455
viewer: Viewer::new(),
5556
now: Instant::now(),
5657
},
@@ -64,7 +65,7 @@ impl Gallery {
6465

6566
pub fn subscription(&self) -> Subscription<Message> {
6667
let is_animating = self
67-
.thumbnails
68+
.previews
6869
.values()
6970
.any(|thumbnail| thumbnail.is_animating(self.now))
7071
|| self.viewer.is_animating(self.now);
@@ -93,28 +94,53 @@ impl Gallery {
9394
return Task::none();
9495
};
9596

96-
Task::perform(image.download(Size::Thumbnail), move |result| {
97-
Message::ThumbnailDownloaded(id, result)
98-
})
97+
Task::batch(vec![
98+
Task::perform(
99+
image.clone().blurhash(
100+
Preview::WIDTH as u32,
101+
Preview::HEIGHT as u32,
102+
),
103+
move |result| Message::BlurhashDecoded(id, result),
104+
),
105+
Task::perform(
106+
image.download(Size::Thumbnail {
107+
width: Preview::WIDTH,
108+
}),
109+
move |result| Message::ThumbnailDownloaded(id, result),
110+
),
111+
])
99112
}
100113
Message::ImageDownloaded(Ok(rgba)) => {
101114
self.viewer.show(rgba);
102115

103116
Task::none()
104117
}
105118
Message::ThumbnailDownloaded(id, Ok(rgba)) => {
106-
let thumbnail = Thumbnail::new(rgba);
107-
let _ = self.thumbnails.insert(id, thumbnail);
119+
let thumbnail = match self.previews.remove(&id) {
120+
Some(Preview::Blurhash(blurhash)) => {
121+
blurhash.to_thumbnail(rgba)
122+
}
123+
_ => Preview::thumbnail(rgba),
124+
};
125+
126+
let _ = self.previews.insert(id, thumbnail);
108127

109128
Task::none()
110129
}
111130
Message::ThumbnailHovered(id, is_hovered) => {
112-
if let Some(thumbnail) = self.thumbnails.get_mut(&id) {
113-
thumbnail.zoom.go_mut(is_hovered);
131+
if let Some(Preview::Thumbnail { zoom, .. }) =
132+
self.previews.get_mut(&id)
133+
{
134+
zoom.go_mut(is_hovered);
114135
}
115136

116137
Task::none()
117138
}
139+
Message::BlurhashDecoded(id, Ok(rgba)) => {
140+
let _ = self.previews.insert(id, Preview::blurhash(rgba));
141+
142+
Task::none()
143+
}
118144
Message::Open(id) => {
119145
let Some(image) = self
120146
.images
@@ -144,7 +170,8 @@ impl Gallery {
144170
}
145171
Message::ImagesListed(Err(error))
146172
| Message::ImageDownloaded(Err(error))
147-
| Message::ThumbnailDownloaded(_, Err(error)) => {
173+
| Message::ThumbnailDownloaded(_, Err(error))
174+
| Message::BlurhashDecoded(_, Err(error)) => {
148175
dbg!(error);
149176

150177
Task::none()
@@ -157,7 +184,7 @@ impl Gallery {
157184
row((0..=Image::LIMIT).map(|_| placeholder()))
158185
} else {
159186
row(self.images.iter().map(|image| {
160-
card(image, self.thumbnails.get(&image.id), self.now)
187+
card(image, self.previews.get(&image.id), self.now)
161188
}))
162189
}
163190
.spacing(10)
@@ -174,31 +201,59 @@ impl Gallery {
174201

175202
fn card<'a>(
176203
metadata: &'a Image,
177-
thumbnail: Option<&'a Thumbnail>,
204+
preview: Option<&'a Preview>,
178205
now: Instant,
179206
) -> Element<'a, Message> {
180-
let image: Element<'_, _> = if let Some(thumbnail) = thumbnail {
181-
image(&thumbnail.handle)
207+
let image: Element<'_, _> = match preview {
208+
Some(Preview::Blurhash(Blurhash { handle, fade_in })) => image(handle)
182209
.width(Fill)
183210
.height(Fill)
184211
.content_fit(ContentFit::Cover)
185-
.opacity(thumbnail.fade_in.interpolate(0.0, 1.0, now))
186-
.scale(thumbnail.zoom.interpolate(1.0, 1.1, now))
187-
.into()
188-
} else {
189-
horizontal_space().into()
212+
.opacity(fade_in.interpolate(0.0, 1.0, now))
213+
.into(),
214+
Some(Preview::Thumbnail {
215+
blurhash,
216+
thumbnail,
217+
fade_in,
218+
zoom,
219+
}) => stack![]
220+
.push_maybe(
221+
blurhash.as_ref().filter(|_| fade_in.is_animating(now)).map(
222+
|(handle, max_opacity)| {
223+
image(handle)
224+
.width(Fill)
225+
.height(Fill)
226+
.content_fit(ContentFit::Cover)
227+
.opacity(fade_in.interpolate(
228+
*max_opacity,
229+
0.0,
230+
now,
231+
))
232+
},
233+
),
234+
)
235+
.push(
236+
image(thumbnail)
237+
.width(Fill)
238+
.height(Fill)
239+
.content_fit(ContentFit::Cover)
240+
.opacity(fade_in.interpolate(0.0, 1.0, now))
241+
.scale(zoom.interpolate(1.0, 1.1, now)),
242+
)
243+
.into(),
244+
None => horizontal_space().into(),
190245
};
191246

192247
let card = mouse_area(
193248
container(image)
194-
.width(Thumbnail::WIDTH)
195-
.height(Thumbnail::HEIGHT)
249+
.width(Preview::WIDTH)
250+
.height(Preview::HEIGHT)
196251
.style(container::dark),
197252
)
198253
.on_enter(Message::ThumbnailHovered(metadata.id, true))
199254
.on_exit(Message::ThumbnailHovered(metadata.id, false));
200255

201-
if thumbnail.is_some() {
256+
if preview.is_some() {
202257
button(card)
203258
.on_press(Message::Open(metadata.id))
204259
.padding(0)
@@ -213,29 +268,71 @@ fn card<'a>(
213268

214269
fn placeholder<'a>() -> Element<'a, Message> {
215270
container(horizontal_space())
216-
.width(Thumbnail::WIDTH)
217-
.height(Thumbnail::HEIGHT)
271+
.width(Preview::WIDTH)
272+
.height(Preview::HEIGHT)
218273
.style(container::dark)
219274
.into()
220275
}
221276

222-
struct Thumbnail {
223-
handle: image::Handle,
277+
struct Blurhash {
224278
fade_in: Animation<bool>,
225-
zoom: Animation<bool>,
279+
handle: image::Handle,
226280
}
227281

228-
impl Thumbnail {
282+
impl Blurhash {
283+
fn to_thumbnail(self, rgba: Rgba) -> Preview {
284+
Preview::Thumbnail {
285+
blurhash: Some((
286+
self.handle,
287+
// Fade out starting at this value for opacity
288+
self.fade_in.interpolate(0.0, 1.0, Instant::now()),
289+
)),
290+
thumbnail: image::Handle::from_rgba(
291+
rgba.width,
292+
rgba.height,
293+
rgba.pixels,
294+
),
295+
fade_in: Animation::new(false).very_slow().go(true),
296+
zoom: Animation::new(false)
297+
.quick()
298+
.easing(animation::Easing::EaseInOut),
299+
}
300+
}
301+
}
302+
303+
enum Preview {
304+
Blurhash(Blurhash),
305+
Thumbnail {
306+
blurhash: Option<(image::Handle, f32)>,
307+
thumbnail: image::Handle,
308+
fade_in: Animation<bool>,
309+
zoom: Animation<bool>,
310+
},
311+
}
312+
313+
impl Preview {
229314
const WIDTH: u16 = 320;
230315
const HEIGHT: u16 = 410;
231316

232-
fn new(rgba: Rgba) -> Self {
233-
Self {
317+
fn blurhash(rgba: Rgba) -> Self {
318+
Self::Blurhash(Blurhash {
319+
fade_in: Animation::new(false).slow().go(true),
234320
handle: image::Handle::from_rgba(
235321
rgba.width,
236322
rgba.height,
237323
rgba.pixels,
238324
),
325+
})
326+
}
327+
328+
fn thumbnail(rgba: Rgba) -> Self {
329+
Self::Thumbnail {
330+
blurhash: None,
331+
thumbnail: image::Handle::from_rgba(
332+
rgba.width,
333+
rgba.height,
334+
rgba.pixels,
335+
),
239336
fade_in: Animation::new(false).slow().go(true),
240337
zoom: Animation::new(false)
241338
.quick()
@@ -244,7 +341,14 @@ impl Thumbnail {
244341
}
245342

246343
fn is_animating(&self, now: Instant) -> bool {
247-
self.fade_in.is_animating(now) || self.zoom.is_animating(now)
344+
match self {
345+
Preview::Blurhash(Blurhash { fade_in, .. }) => {
346+
fade_in.is_animating(now)
347+
}
348+
Preview::Thumbnail { fade_in, zoom, .. } => {
349+
fade_in.is_animating(now) || zoom.is_animating(now)
350+
}
351+
}
248352
}
249353
}
250354

0 commit comments

Comments
 (0)