mirror of
https://github.com/storytold/deckcraft.git
synced 2026-11-01 22:47:04 -06:00
feat(pictures): animated GIFs play, with pause (media.gifPlay) (#39)
* feat(pictures): animated GIFs play, with pause (media.gifPlay) GIF pictures used to show only their first frame. The renderer now decodes every frame (cached, capped at 256 MB and 1024 px a side) and draws the frame for each GIF's play time, passed in RenderOpts::gif_times; exports and thumbnails pass none and keep frame 1. The editor canvas and the slide show redraw only when a GIF reaches its next frame. A corner button on hover (or selection) pauses and resumes a GIF from the same frame, through the new media.gifPlay command. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * feat(pictures): animated GIFs play, with pause (media.gifPlay) GIF pictures used to show only their first frame. The renderer now decodes every frame (cached, capped at 256 MB and 1024 px a side) and draws the frame for each GIF's play time, passed in RenderOpts::gif_times; exports and thumbnails pass none and keep frame 1. The editor canvas and the slide show redraw only when a GIF reaches its next frame. A corner button on hover (or selection) pauses and resumes a GIF from the same frame, through the new media.gifPlay command. Also adds my entry to contributors/people.toml. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Animated GIFs: cap frames (1000), canvas (4096²), per-GIF (64 MB) and cache (192 MB) memory A few MB of hostile GIF could decode to millions of tiny frames or a 100 Mpx canvas per frame, and eight cached GIFs could hold 2 GB. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> Co-authored-by: Brandon Thomas <echelon@gmail.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
Brandon Thomas
parent
e8c4c60884
commit
3985d768e6
@@ -273,6 +273,39 @@ fn morph_text_ends_match_the_slides() {
|
||||
assert!(ink > 50, "ink {ink}");
|
||||
}
|
||||
|
||||
/// Two 8×8 frames, red then blue, 100 ms each.
|
||||
fn two_frame_gif() -> Vec<u8> {
|
||||
use image::{Delay, Frame, Rgba, RgbaImage};
|
||||
let frame = |c: [u8; 4]| Frame::from_parts(RgbaImage::from_pixel(8, 8, Rgba(c)), 0, 0, Delay::from_numer_denom_ms(100, 1));
|
||||
let mut out = Vec::new();
|
||||
image::codecs::gif::GifEncoder::new(&mut out).encode_frames([frame([255, 0, 0, 255]), frame([0, 0, 255, 255])]).expect("gif");
|
||||
out
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn animated_gif_shows_the_frame_for_its_time() {
|
||||
let mut p = Presentation::default();
|
||||
let media = p.add_media("a.gif", "image/gif", two_frame_gif());
|
||||
let fill = deckcraft_model::style::PictureFill { media, ..Default::default() };
|
||||
let sh = Shape { id: ShapeId(600), xfrm: Some(Xfrm::new(0.0, 0.0, 960.0, 540.0)), kind: ShapeKind::Picture { fill }, ..Default::default() };
|
||||
std::sync::Arc::make_mut(&mut p.slides[0]).shapes = vec![sh];
|
||||
let at = |t: Option<f64>| {
|
||||
let times: Vec<(ShapeId, f64)> = t.map(|t| vec![(ShapeId(600), t)]).unwrap_or_default();
|
||||
render_slide(&p, 0, &RenderOpts { scale: 0.1, gif_times: ×, ..Default::default() }).pixel(48, 27)
|
||||
};
|
||||
let red = |px: [u8; 4]| px[0] > 200 && px[2] < 60;
|
||||
let blue = |px: [u8; 4]| px[2] > 200 && px[0] < 60;
|
||||
assert!(red(at(None)), "no time: the first frame, as exports draw it");
|
||||
assert!(red(at(Some(0.05))));
|
||||
assert!(blue(at(Some(0.15))));
|
||||
assert!(red(at(Some(0.25))), "it loops");
|
||||
let bytes = p.media(media).map(|m| m.data.clone()).expect("media");
|
||||
let adj = Default::default();
|
||||
assert_eq!(gif_frame(&bytes, &adj, 0.15).map(|f| f.0), Some(1));
|
||||
assert_eq!(gif_frame(&bytes, &adj, f64::NAN).map(|f| f.0), Some(0));
|
||||
assert!(gif_frame(&std::sync::Arc::new(b"\x89PNG".to_vec()), &adj, 0.0).is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn morph_path_ends_match_the_slides() {
|
||||
let from = Xfrm::new(100.0, 100.0, 200.0, 200.0);
|
||||
|
||||
Reference in New Issue
Block a user