├── .github └── workflows │ ├── crosscompile.yml │ └── release.yml ├── .gitignore ├── Cargo.toml ├── LICENSE ├── README.md ├── animation.gif └── src ├── lib.rs └── main.rs /.github/workflows/crosscompile.yml: -------------------------------------------------------------------------------- 1 | name: Cross-compile 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | pull_request: 7 | branches: [ main ] 8 | 9 | env: 10 | CARGO_TERM_COLOR: always 11 | 12 | jobs: 13 | build: 14 | name: Build 15 | runs-on: ${{ matrix.os }} 16 | strategy: 17 | fail-fast: false 18 | matrix: 19 | os: [ubuntu-latest, macos-latest, windows-latest] 20 | include: 21 | - os: ubuntu-latest 22 | packages: pkg-config libx11-dev libxi-dev libgl1-mesa-dev libasound2-dev gcc-mingw-w64 23 | 24 | steps: 25 | - uses: actions/checkout@v4 26 | - name: Install packages (Linux) 27 | if: runner.os == 'Linux' 28 | run: | 29 | sudo apt-get update 30 | sudo apt-get -yq --no-install-suggests --no-install-recommends install ${{ matrix.packages }} 31 | - name: Output rust version 32 | run: rustup --version 33 | - name: Build binaries 34 | run: cargo build --release --all-targets 35 | - name: Machete 36 | uses: bnjbvr/cargo-machete@main 37 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | push: 5 | tags: 6 | - 'v*' 7 | 8 | jobs: 9 | build: 10 | name: Release for ${{ matrix.config.target }} / ${{ matrix.config.os }} 11 | runs-on: ${{ matrix.config.os }} 12 | strategy: 13 | fail-fast: false 14 | matrix: 15 | config: 16 | - os: ubuntu-latest 17 | artifact_name: quad_gif 18 | zip_file: quad-gif-linux.tar.gz 19 | asset_name: quad-gif-linux-$tag.tar.gz 20 | target: 'x86_64-unknown-linux-gnu' 21 | - os: windows-latest 22 | artifact_name: quad_gif.exe 23 | zip_file: quad-gif-windows.zip 24 | asset_name: quad-gif-windows-$tag.zip 25 | target: 'x86_64-pc-windows-msvc' 26 | - os: macos-latest 27 | artifact_name: quad_gif 28 | zip_file: quad-gif-macos.zip 29 | asset_name: quad-gif-macos-$tag.zip 30 | target: 'x86_64-apple-darwin' 31 | - os: ubuntu-latest 32 | artifact_name: quad_gif.wasm 33 | zip_file: quad-gif-wasm.zip 34 | asset_name: quad-gif-wasm-$tag.zip 35 | target: 'wasm32-unknown-unknown' 36 | include: 37 | - os: ubuntu-latest 38 | packages: libx11-dev libxi-dev libgl1-mesa-dev gcc-mingw-w64 libasound2-dev 39 | 40 | steps: 41 | - uses: actions/checkout@v4 42 | - uses: Swatinem/rust-cache@v2 43 | - name: Install packages (Linux) 44 | if: runner.os == 'Linux' 45 | run: | 46 | sudo apt-get update 47 | sudo apt-get -yq --no-install-suggests --no-install-recommends install ${{ matrix.packages }} 48 | - uses: actions-rs/toolchain@v1 49 | with: 50 | toolchain: stable 51 | target: ${{ matrix.config.target }} 52 | override: true 53 | - name: Workaround MinGW issue # https://github.com/rust-lang/rust/issues/47048 54 | if: runner.os == 'Linux' && matrix.config.target == 'x86_64-pc-windows-gnu' 55 | run: | 56 | sudo cp /usr/x86_64-w64-mingw32/lib/dllcrt2.o ~/.rustup/toolchains/stable-x86_64-unknown-linux-gnu/lib/rustlib/x86_64-pc-windows-gnu/lib/dllcrt2.o 57 | sudo cp /usr/x86_64-w64-mingw32/lib/crt2.o ~/.rustup/toolchains/stable-x86_64-unknown-linux-gnu/lib/rustlib/x86_64-pc-windows-gnu/lib/crt2.o 58 | echo "[target.x86_64-pc-windows-gnu]" >> ~/.cargo/config 59 | echo "linker = \"/usr/bin/x86_64-w64-mingw32-gcc\"" >> ~/.cargo/config 60 | - name: Output rust version 61 | run: rustup --version 62 | - name: Build binaries for target "${{ matrix.config.target }}" 63 | run: cargo build --release --all-targets --target=${{ matrix.config.target }} 64 | - name: Zip release archive 65 | if: matrix.config.target == 'wasm32-unknown-unknown' 66 | run: zip --move -j ${{ matrix.config.zip_file }} target/${{ matrix.config.target }}/release/${{ matrix.config.artifact_name }} 67 | - name: Zip release archive 68 | if: matrix.config.target == 'x86_64-unknown-linux-gnu' 69 | run: tar --remove-files -zcf ${{ matrix.config.zip_file }} -C target/${{ matrix.config.target }}/release/ ${{ matrix.config.artifact_name }} 70 | - name: Zip release archive 71 | if: runner.os == 'Windows' || runner.os == 'macOS' 72 | run: 7z a -sdel -tzip ${{ matrix.config.zip_file }} ./target/${{ matrix.config.target }}/release/${{ matrix.config.artifact_name }} 73 | - name: Upload binaries to release 74 | uses: svenstaro/upload-release-action@v2 75 | with: 76 | repo_token: ${{ secrets.GITHUB_TOKEN }} 77 | file: ${{ matrix.config.zip_file }} 78 | asset_name: ${{ matrix.config.asset_name }} 79 | tag: ${{ github.ref }} 80 | - name: Remove packaged zip file after upload 81 | uses: JesseTG/rm@v1.0.3 82 | with: 83 | path: ${{ matrix.config.zip_file }} 84 | 85 | publish: 86 | runs-on: ubuntu-latest 87 | needs: build 88 | steps: 89 | - uses: actions/checkout@v4 90 | - uses: Swatinem/rust-cache@v2 91 | - name: Publish release to crates.io 92 | uses: katyo/publish-crates@v1 93 | with: 94 | registry-token: ${{ secrets.CARGO_REGISTRY_TOKEN }} 95 | args: --allow-dirty 96 | check-repo: false 97 | ignore-unpublished-changes: true 98 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | /demo 3 | /nyan-cat.gif 4 | /.direnv/ 5 | /Cargo.lock 6 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "quad-gif" 3 | version = "0.4.0" 4 | edition = "2024" 5 | license = "MIT" 6 | description = "Display GIF animations with Macroquad." 7 | homepage = "https://github.com/ollej/quad-gif" 8 | repository = "https://github.com/ollej/quad-gif" 9 | readme = "README.md" 10 | keywords = ["graphics", "image", "gif", "animation", "macroquad"] 11 | categories = ["command-line-utilities", "graphics", "multimedia::images", "rendering"] 12 | 13 | [dependencies] 14 | macroquad = { version = "0.4", default-features = false } 15 | gif = "0.11.4" 16 | gif-dispose = "3.1.1" 17 | rgb = "0.8.34" 18 | 19 | [profile.dev] 20 | debug = 1 # less precise locations 21 | 22 | # Doesn't work with android build 23 | [profile.dev.package.'*'] 24 | debug = false # no debug symbols for deps 25 | opt-level = 2 26 | 27 | [profile.release] 28 | opt-level = 'z' 29 | lto = true 30 | panic = 'abort' 31 | codegen-units = 1 32 | strip = true 33 | 34 | [lib] 35 | name = "quad_gif" 36 | path = "src/lib.rs" 37 | 38 | [[bin]] 39 | name = "quad_gif" 40 | path = "src/main.rs" 41 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | Copyright (c) 2022 Olle Wreede 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 5 | 6 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 7 | 8 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 9 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # quad-gif 2 | 3 | [![Cross-compile](https://github.com/ollej/quad-gif/actions/workflows/crosscompile.yml/badge.svg)](https://github.com/ollej/quad-gif/actions/workflows/crosscompile.yml) [![Crates.io](https://img.shields.io/crates/v/quad-gif)](https://crates.io/crates/quad-gif) [![docs.rs](https://img.shields.io/docsrs/quad-gif)](https://docs.rs/quad-gif/latest/quad_gif/) [![Crates.io](https://img.shields.io/crates/l/quad-gif)](https://opensource.org/licenses/MIT) 4 | 5 | Display looping GIF animations with Macroquad. 6 | 7 | The animation will loop forever, regardless of how many iterations are set in 8 | the file. 9 | 10 | [Documentation](https://docs.rs/quad-gif/latest/quad_gif/) on docs.rs 11 | 12 | ## Usage 13 | 14 | There is a binary file included that can be used to show a GIF file. 15 | 16 | ``` 17 | quad_gif 0.4.0 18 | Display a GIF file. 19 | 20 | Usage: quad_gif 21 | ``` 22 | 23 | ## API usage 24 | 25 | The library can be used in a Macroquad application to show an animation. 26 | 27 | ```rust 28 | use macroquad::prelude::*; 29 | use quad_gif; 30 | 31 | #[macroquad::main("quad-gif")] 32 | async fn main() { 33 | let mut animation = quad_gif::GifAnimation::load("animation.gif".to_string()).await; 34 | 35 | clear_background(WHITE); 36 | loop { 37 | animation.draw(); 38 | animation.tick(); 39 | next_frame().await 40 | } 41 | } 42 | ``` 43 | 44 | ## License 45 | 46 | Copyright 2022 Olle Wreede, released under the MIT License. 47 | 48 | ## Attribution 49 | 50 | Animated Ferris in Action by A. L. Palmer 51 | 52 | Happy as a Rustacean at Rust Fest Berlin 2016 (www.rustfest.eu) 53 | -------------------------------------------------------------------------------- /animation.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ollej/quad-gif/2dff0517b7a0a1224237f478b2268bb0ce84cc13/animation.gif -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | //! Simple crate to load and draw a GIF animation using Macroquad. 2 | //! 3 | //! The animation will loop forever, regardless of how many iterations are set in 4 | //! the file. 5 | //! 6 | //! ```rust 7 | //! use macroquad::prelude::*; 8 | //! use quad_gif; 9 | //! 10 | //! #[macroquad::main("quad-gif")] 11 | //! async fn main() { 12 | //! let mut animation = quad_gif::GifAnimation::load("animation.gif".to_string()).await; 13 | //! 14 | //! clear_background(WHITE); 15 | //! loop { 16 | //! animation.draw(); 17 | //! animation.tick(); 18 | //! next_frame().await 19 | //! } 20 | //! } 21 | //! ``` 22 | 23 | use macroquad::prelude::*; 24 | use rgb::ComponentBytes; 25 | 26 | /// Struct containing textures to display every frame. 27 | pub struct GifAnimation { 28 | pub frames: Vec, 29 | pub width: u16, 30 | pub height: u16, 31 | pub current_frame: usize, 32 | elapsed_time: f32, 33 | paused: bool, 34 | } 35 | 36 | impl GifAnimation { 37 | /// Instantiate with a vector of [`AnimationFrame`], width and height. 38 | /// 39 | /// Can be used to create an animation from your own textures instead 40 | /// of loading a GIF file. 41 | /// 42 | /// [`AnimationFrame`]: struct.AnimationFrame 43 | pub fn new(frames: Vec, width: u16, height: u16) -> Self { 44 | Self { 45 | frames, 46 | width, 47 | height, 48 | current_frame: 0, 49 | elapsed_time: 0., 50 | paused: false, 51 | } 52 | } 53 | 54 | /// Load and decode a GIF file using Macroquad. 55 | /// 56 | /// ```rust 57 | /// let mut gif_animation = GifAnimation::load("filename.gif").await; 58 | /// ``` 59 | pub async fn load(filename: String) -> Self { 60 | let file_bytes = load_file(&filename).await.expect("Couldn't load file"); 61 | Self::from_gif_bytes(&file_bytes) 62 | } 63 | 64 | /// Instantiate a new `GifAnimation` from bytes. 65 | /// 66 | /// ```rust 67 | /// let bytes: [u8] = ... 68 | /// let mut gif_animation = GifAnimation::from_gif_bytes(&bytes); 69 | /// ``` 70 | pub fn from_gif_bytes(file_bytes: &[u8]) -> GifAnimation { 71 | let (frames, width, height) = Self::decode_gif(&file_bytes); 72 | GifAnimation::new(frames, width, height) 73 | } 74 | 75 | fn decode_gif(file: &[u8]) -> (Vec, u16, u16) { 76 | let mut options = gif::DecodeOptions::new(); 77 | options.set_color_output(gif::ColorOutput::Indexed); 78 | let mut decoder = options.read_info(&*file).unwrap(); 79 | let mut screen = gif_dispose::Screen::new_decoder(&decoder); 80 | 81 | let mut frames: Vec = Vec::new(); 82 | while let Some(frame) = decoder.read_next_frame().unwrap() { 83 | screen.blit_frame(&frame).expect("Couldn't blit frame"); 84 | let (pixels, frame_width, frame_height) = screen.pixels.as_contiguous_buf(); 85 | frames.push(AnimationFrame { 86 | texture: Texture2D::from_rgba8( 87 | frame_width as u16, 88 | frame_height as u16, 89 | pixels.as_bytes(), 90 | ), 91 | delay: frame.delay as f32 / 100., 92 | }); 93 | } 94 | (frames, decoder.width(), decoder.height()) 95 | } 96 | 97 | fn pos_x(&self) -> f32 { 98 | screen_width() / 2. - self.width as f32 / 2. 99 | } 100 | 101 | fn pos_y(&self) -> f32 { 102 | screen_height() / 2. - self.height as f32 / 2. 103 | } 104 | 105 | /// Draw the texture of the current frame at the middle of the screen. 106 | /// 107 | /// ```rust 108 | /// gif_animation.draw(); 109 | /// ``` 110 | pub fn draw(&self) { 111 | self.draw_at(self.pos_x(), self.pos_y()); 112 | } 113 | 114 | /// Draw the texture of the current frame at given X/Y position. 115 | /// 116 | /// ```rust 117 | /// gif_animation.draw_at(42.0, 47.0); 118 | /// ``` 119 | pub fn draw_at(&self, pos_x: f32, pos_y: f32) { 120 | draw_texture_ex( 121 | &self.frame().texture, 122 | pos_x, 123 | pos_y, 124 | WHITE, 125 | DrawTextureParams::default(), 126 | ); 127 | } 128 | 129 | /// Update method that needs to be called in the loop to 130 | /// advance to next frame when necessary. 131 | /// 132 | /// ```rust 133 | /// gif_animation.tick(); 134 | /// ``` 135 | pub fn tick(&mut self) { 136 | if !self.paused { 137 | self.elapsed_time += get_frame_time(); 138 | if self.elapsed_time > self.frame().delay { 139 | self.advance_frame(); 140 | } 141 | } 142 | } 143 | 144 | /// Toggle whether the animation should be playing or be paused. 145 | /// 146 | /// ```rust 147 | /// gif_animation.toggle_paused(); 148 | /// ``` 149 | pub fn toggle_paused(&mut self) { 150 | self.paused ^= true; 151 | } 152 | 153 | pub fn frame(&self) -> &AnimationFrame { 154 | self.frames.get(self.current_frame).unwrap() 155 | } 156 | 157 | fn advance_frame(&mut self) { 158 | self.current_frame = if self.current_frame == self.frames.len() - 1 { 159 | 0 160 | } else { 161 | self.current_frame + 1 162 | }; 163 | self.elapsed_time = 0.0; 164 | } 165 | } 166 | 167 | /// Struct for a single frame. Contains the texture to draw, 168 | /// and a delay for how many seconds the frame should show before 169 | /// advancing to the next frame. 170 | #[derive(Debug)] 171 | pub struct AnimationFrame { 172 | pub texture: Texture2D, 173 | pub delay: f32, 174 | } 175 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | #![windows_subsystem = "windows"] 2 | use macroquad::prelude::*; 3 | use quad_gif; 4 | use std::env; 5 | #[cfg(not(debug_assertions))] 6 | use std::process; 7 | 8 | fn window_conf() -> Conf { 9 | Conf { 10 | window_title: "quad-gif".to_owned(), 11 | fullscreen: true, 12 | ..Default::default() 13 | } 14 | } 15 | 16 | fn get_filename() -> String { 17 | env::args().nth(1).unwrap_or_else(|| default_filename()) 18 | } 19 | 20 | #[cfg(debug_assertions)] 21 | fn default_filename() -> String { 22 | "animation.gif".to_string() 23 | } 24 | 25 | #[cfg(not(debug_assertions))] 26 | fn default_filename() -> String { 27 | explain_usage() 28 | } 29 | 30 | #[cfg(not(debug_assertions))] 31 | fn explain_usage() -> ! { 32 | println!("Display a GIF file.\n\nUsage: quad-gif "); 33 | process::exit(1) 34 | } 35 | 36 | /// Binary to display a looping GIF animation. 37 | /// 38 | /// The filename is required, except in debug, where it defaults to `animation.gif`. 39 | /// 40 | /// quad-gif 0.2.0 41 | /// Display a GIF file. 42 | /// 43 | /// Usage: quad-gif 44 | #[macroquad::main(window_conf)] 45 | async fn main() { 46 | let mut animation = quad_gif::GifAnimation::load(get_filename()).await; 47 | 48 | loop { 49 | #[cfg(not(target_arch = "wasm32"))] 50 | if is_key_pressed(KeyCode::Q) | is_key_pressed(KeyCode::Escape) { 51 | break; 52 | } 53 | if is_key_pressed(KeyCode::P) | is_key_pressed(KeyCode::Space) { 54 | animation.toggle_paused(); 55 | } 56 | 57 | clear_background(WHITE); 58 | animation.draw(); 59 | animation.tick(); 60 | 61 | next_frame().await 62 | } 63 | } 64 | --------------------------------------------------------------------------------