|
| 1 | +use core::f32; |
| 2 | +use skia_safe::{Image, Matrix}; |
| 3 | + |
| 4 | +use meme_generator_core::error::Error; |
| 5 | +use meme_generator_utils::{ |
| 6 | + builder::InputImage, |
| 7 | + encoder::{FrameAlign, GifInfo, make_gif_or_combined_gif}, |
| 8 | + tools::{local_date, new_surface}, |
| 9 | +}; |
| 10 | + |
| 11 | +use crate::{options::NoOptions, register_meme}; |
| 12 | + |
| 13 | +fn backflip(images: Vec<InputImage>, _: Vec<String>, _: NoOptions) -> Result<Vec<u8>, Error> { |
| 14 | + let img = &images[0].image; |
| 15 | + let img_w = img.width(); |
| 16 | + let img_h = img.height(); |
| 17 | + |
| 18 | + let length = ((img_w * img_w + img_h * img_h) as f32).sqrt(); |
| 19 | + let frame_w = (length * 1.3) as i32; |
| 20 | + let bounce_h = img_h as f32 * 1.2; |
| 21 | + let frame_h = (bounce_h + length / 2.0 + img_h as f32 * 0.6) as i32; |
| 22 | + let center_x = (frame_w / 2) as f32; |
| 23 | + let ground_y = (frame_h - img_h / 2) as f32; |
| 24 | + |
| 25 | + let total_frames = 30; |
| 26 | + let bounce1_range = 0.0..0.3; |
| 27 | + let bounce2_range = 0.3..0.6; |
| 28 | + let rise_range = 0.6..0.65; |
| 29 | + let flip_range = 0.65..0.95; |
| 30 | + let land_range = 0.95..1.0; |
| 31 | + |
| 32 | + let func = move |i: usize, images: Vec<Image>| { |
| 33 | + let t = i as f32 / total_frames as f32; |
| 34 | + |
| 35 | + let (y, angle) = if bounce1_range.contains(&t) { |
| 36 | + let local_t = (t - bounce1_range.start) / (bounce1_range.end - bounce1_range.start); |
| 37 | + let y = -4.0 * bounce_h * (local_t - 0.5).powi(2) + bounce_h; |
| 38 | + let rot = 45.0 * (1.0 - 2.0 * (local_t - 0.5).abs()); |
| 39 | + (ground_y - y, rot) |
| 40 | + } else if bounce2_range.contains(&t) { |
| 41 | + let local_t = (t - bounce2_range.start) / (bounce2_range.end - bounce2_range.start); |
| 42 | + let y = -4.0 * bounce_h * (local_t - 0.5).powi(2) + bounce_h; |
| 43 | + let rot = -45.0 * (1.0 - 2.0 * (local_t - 0.5).abs()); |
| 44 | + (ground_y - y, rot) |
| 45 | + } else if rise_range.contains(&t) { |
| 46 | + let local_t = (t - rise_range.start) / (rise_range.end - rise_range.start); |
| 47 | + let y = bounce_h - bounce_h * (1.0 - local_t).powi(2); |
| 48 | + (ground_y - y, 0.0) |
| 49 | + } else if flip_range.contains(&t) { |
| 50 | + let local_t = (t - flip_range.start) / (flip_range.end - flip_range.start); |
| 51 | + let y = bounce_h; |
| 52 | + let rot = 360.0 * local_t; |
| 53 | + (ground_y - y, rot) |
| 54 | + } else if land_range.contains(&t) { |
| 55 | + let local_t = (t - land_range.start) / (land_range.end - land_range.start); |
| 56 | + let y = bounce_h - bounce_h * local_t.powi(2); |
| 57 | + (ground_y - y, 0.0) |
| 58 | + } else { |
| 59 | + (ground_y, 0.0) |
| 60 | + }; |
| 61 | + |
| 62 | + let img = &images[0]; |
| 63 | + let mut surface = new_surface((frame_w, frame_h)); |
| 64 | + let canvas = surface.canvas(); |
| 65 | + let mut matrix = Matrix::new_identity(); |
| 66 | + matrix.pre_translate((center_x, y)); |
| 67 | + matrix.pre_rotate(angle, None); |
| 68 | + matrix.pre_translate((-img_w as f32 / 2.0, -img_h as f32 / 2.0)); |
| 69 | + canvas.concat(&matrix); |
| 70 | + canvas.draw_image(img, (0, 0), None); |
| 71 | + |
| 72 | + Ok(surface.image_snapshot()) |
| 73 | + }; |
| 74 | + |
| 75 | + make_gif_or_combined_gif( |
| 76 | + images, |
| 77 | + func, |
| 78 | + GifInfo { |
| 79 | + frame_num: total_frames, |
| 80 | + duration: 0.05, |
| 81 | + }, |
| 82 | + FrameAlign::ExtendLoop, |
| 83 | + ) |
| 84 | +} |
| 85 | + |
| 86 | +register_meme!( |
| 87 | + "backflip", |
| 88 | + backflip, |
| 89 | + min_images = 1, |
| 90 | + max_images = 1, |
| 91 | + keywords = &["后空翻"], |
| 92 | + date_created = local_date(2025, 6, 29), |
| 93 | + date_modified = local_date(2025, 6, 29), |
| 94 | +); |
0 commit comments