├── .gitignore ├── Cargo.toml ├── LICENCE ├── README.md ├── README_cn.md ├── benches └── set.rs ├── examples ├── anime │ └── mix.rs ├── files │ ├── lena.png │ ├── lifegame │ │ ├── 132p37.rle │ │ └── c5diagonaltubstretcher.rle │ ├── miku.jpeg │ └── xkcd2898.png ├── imgille.rs ├── life.rs ├── object3d │ ├── cube-colorful.rs │ ├── cube.rs │ ├── obj-mix.rs │ ├── otc-colorful.rs │ └── otc.rs ├── sin.rs └── turtle │ ├── turtle-flower.rs │ └── turtle-multi.rs ├── imgs ├── anime.gif ├── bad-apple.gif ├── basic-sin.png ├── cube.gif ├── lena-gray.png ├── lena.png ├── lifegame.gif ├── mandelbrot.png ├── objects.gif ├── toggle-sin.png ├── turtle-multi.png ├── turtle-star.png ├── xkcd-invert.png └── xkcd.png └── src ├── anime.rs ├── braille.rs ├── canvas.rs ├── color.rs ├── decor.rs ├── defaults.rs ├── extra ├── imgille.rs ├── lifegame.rs ├── math │ ├── figure.rs │ ├── mod.rs │ └── plot.rs ├── mod.rs ├── object3d.rs └── turtle.rs ├── lib.rs ├── term.rs └── utils.rs /.gitignore: -------------------------------------------------------------------------------- 1 | target 2 | Cargo.lock 3 | test-files 4 | examples/test.rs 5 | .vscode -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "rsille" 3 | version = "2.3.1" 4 | edition = "2021" 5 | authors = ["nidhoggfgg "] 6 | description = "A full feature braille code art lib" 7 | keywords = ["braille", "terminal", "tui", "cli", "animation"] 8 | readme = "README.md" 9 | license = "MIT" 10 | repository = "https://github.com/nidhoggfgg/rsille.git" 11 | exclude = ["examples", "imgs"] 12 | 13 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 14 | 15 | [dependencies] 16 | crossterm = "0.27.0" 17 | image = { version = "0.24.8", optional = true } 18 | 19 | [profile.release] 20 | lto = true 21 | # debug = true # only used for perf 22 | 23 | [dev-dependencies] 24 | criterion = "0.4" 25 | 26 | [features] 27 | default = [] 28 | img = ["image"] 29 | 30 | [[bench]] 31 | name = "set" 32 | harness = false 33 | 34 | [[example]] 35 | name = "anime-mix" 36 | path = "examples/anime/mix.rs" 37 | 38 | [[example]] 39 | name = "obj-mix" 40 | path = "examples/object3d/obj-mix.rs" 41 | 42 | [[example]] 43 | name = "cube" 44 | path = "examples/object3d/cube.rs" 45 | 46 | [[example]] 47 | name = "cube-colorful" 48 | path = "examples/object3d/cube-colorful.rs" 49 | 50 | [[example]] 51 | name = "otc" 52 | path = "examples/object3d/otc.rs" 53 | 54 | [[example]] 55 | name = "otc-colorful" 56 | path = "examples/object3d/otc-colorful.rs" 57 | 58 | [[example]] 59 | name = "turtle-flower" 60 | path = "examples/turtle/turtle-flower.rs" 61 | 62 | [[example]] 63 | name = "turtle-multi" 64 | path = "examples/turtle/turtle-multi.rs" 65 | 66 | [[example]] 67 | name = "imgille" 68 | path = "examples/imgille.rs" 69 | required-features = ["img"] 70 | -------------------------------------------------------------------------------- /LICENCE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 nidhoggfgg 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 | 3 | # Rsille 🎨 4 | 5 | [![GitHub License](https://img.shields.io/github/license/nidhoggfgg/rsille?style=for-the-badge)](./LICENCE) 6 | [![Crates.io Version](https://img.shields.io/crates/v/rsille?style=for-the-badge)](https://crates.io/crates/rsille/versions) 7 | [![Crates.io Total Downloads](https://img.shields.io/crates/d/rsille?style=for-the-badge)](https://crates.io/crates/rsille) 8 | [![docs.rs](https://img.shields.io/docsrs/rsille?style=for-the-badge)](https://docs.rs/rsille/latest/rsille/) 9 | 10 | 11 | [English](./README.md) • [中文](./README_cn.md) 12 | 13 |
14 | anime 15 | 3d object 16 | life game 17 | mandelbrot set 18 | lena 19 | turtle 20 |
21 | 22 |
23 | 24 | ## 📚 ToC 25 | 26 | ------ 27 | 28 | * [👀 Try it](#👀-try-it) 29 | * [🔧 Installation](#🔧-installation) 30 | * [🚀 Usage](#🚀-usage) 31 | * [Basics](#basics) 32 | * [Turtle](#turtle) 33 | * [3D Object](#3d-object) 34 | * [Life game](#conways-game-of-life) 35 | * [Images](#images) 36 | * [Animation](#animation) 37 | * [📌 TODO](#📌-todo) 38 | * [📝 License](#📝-license) 39 | 40 | ## 👀 Try it 41 | 42 | All images used in the readme can be found in the [`examples`](./examples/) folder, ready to run without programming! 43 | ``` 44 | git clone https://github.com/nidhoggfgg/rsille.git 45 | cd rsille 46 | cargo run --example cube 47 | ``` 48 | 49 | This will generate a rotating cube. 50 | 51 | For more examples or to try generating those interesting patterns yourself, you can use `cargo run --example`, and you'll find all the executable examples without programming. 52 | 53 | Sure, there is more examples, like [bad-apple](./imgs/bad-apple.gif) and so on. 54 | You can check the usage to know how to use this lib. 55 | 56 | ## 🔧 Installation 57 | 58 | This is a Rust library, so all you need to do is add the following code to your Cargo.toml to use it: 59 | ```toml 60 | [dependencies] 61 | rsille = "2.1.0" 62 | ``` 63 | 64 | ## 🚀 Usage 65 | 66 | Due to space limitations, only some commonly used APIs and general usage methods are provided here. 67 | 68 | For detailed APIs, please visit [doc.rs](https://docs.rs/rsille/latest/rsille/). 69 | 70 | ### Basics 71 | 72 | The most basic function is `set`. 73 | ```rust 74 | use rsille::Canvas; 75 | fn main() { 76 | let mut c = Canvas::new(); 77 | for x in -360..360 { 78 | let x = x as f64; 79 | c.set(x / 10.0, x.to_radians().sin() * 10.0); 80 | } 81 | c.print(); 82 | } 83 | ``` 84 | ![basic-sin](./imgs/basic-sin.png) 85 | 86 | Why is it *(x / 10.0, x.to_radians().sin() * 10.0)* instead of *(x, x.sin())*? 87 | You can try adding, subtracting, multiplying, and dividing to change the coordinates being drawn. A small hint: `⠿` contains 8 points. 88 | If you really don't understand or want to know why, you can directly use the *(x, y)* in your algorithems. I might write an article about braille code later. 89 | 90 | ------ 91 | 92 | The opposite of `set` is `unset`, which erases points that have been set. 93 | 94 | ------ 95 | 96 | Another useful method is `toggle`, which `unset` points that have been `set` and `set` points that have not been `set`. 97 | ```rust 98 | use rsille::Canvas; 99 | fn main() { 100 | let mut c = Canvas::new(); 101 | for x in (0..360).step_by(4) { 102 | let x = x as f64; 103 | c.set(x / 4.0, x.to_radians().sin() * 30.0); 104 | } 105 | 106 | for x in 0..=30 { 107 | for y in 0..=30 { 108 | c.toggle(x, y); 109 | c.toggle(x + 30, y - 30); 110 | c.toggle(x + 60, y); 111 | } 112 | } 113 | c.print(); 114 | } 115 | ``` 116 | ![toggle-sin](./imgs/toggle-sin.png) 117 | 118 | This is example is a littler longer, let's explain it: 119 | * `(0..360).step_by(4)` iterates over *x* in the interval *[0, π)*, and `step_by(4)` is used for downsampling to make the lines finer (too low a value could result in imprecision). 120 | * The first `for` loop is for drawing the graph of *f(x) = sin(x)*. To make it look better, there is a certain scaling applied to both *x* and *y*. 121 | * The second `for` loop is for creating those `toggle` blocks, each of which is 31x31 in size. 122 | 123 | In `toggle`, `f64` are not used because all methods support generics and can accept various numeric types! 124 | However, it is still strongly recommended to use `f64` for higher precision. 125 | 126 | ### Turtle 127 | 128 | In Python, there is an interesting library called turtle. 129 | It allows beginners who are just starting to use Python to experience the joy of programming. 130 | This library also implements most of the methods in turtle. 131 | ```rust 132 | use rsille::{extra::Turtle, Canvas}; 133 | fn main() { 134 | let mut canvas = Canvas::new(); 135 | let mut t = Turtle::new(); 136 | for _ in 0..5 { 137 | t.forward(50); 138 | t.right(144); 139 | } 140 | canvas.paint(&t, 0, 0).unwrap(); 141 | canvas.print(); 142 | } 143 | ``` 144 | ![turtle-star](./imgs/turtle-star.png) 145 | 146 | The two *0* in the `paint` method are not fixed; they place the object to be drawn at *(0, 0)*. 147 | This is arbitrary, but it's a good idea to always draw a single object at *(0, 0)*. 148 | 149 | No more introduction to turtle; you can directly copy Python code and modify it slightly to use. 150 | 151 | ### 3D Object 152 | 153 | This library also supports 3D objects and provides convenient methods for constructing objects easily. 154 | ```rust 155 | use rsille::{extra::Object3D, Animation}; 156 | 157 | fn main() { 158 | let mut anime = Animation::new(); 159 | let cube = Object3D::cube(30); 160 | anime.push( 161 | cube, 162 | |cube| { 163 | cube.rotate((1.0, 2.0, 3.0)); 164 | false 165 | }, 166 | (0, 0), 167 | ); 168 | anime.run(); 169 | } 170 | ``` 171 | ![cube](./imgs/cube.gif) 172 | 173 | Here, `Animation` is used to create animations, which are also internally supported. You can check [Animation](#animation) to see more details. 174 | 175 | `Object3D` mainly has two useful methods: `rotate` for rotating objects and `zoom` for scaling objects. 176 | 177 | ### Conway's Game of Life 178 | 179 | Conway's Game of Life is very interesting, so it is also part of the library. 180 | ```rust 181 | use rsille::{extra::LifeGame, Animation}; 182 | 183 | fn main() { 184 | let mut anime = Animation::new(); 185 | let lg = LifeGame::from(r#"x = 47, y = 47, rule = B3/S23 186 | 18bo$18b3o$21bo$20b2o$$32b2o$32b2o$26bobo$28bo$$22b3o$15b2o5bo2bo$15b2o 187 | 2o5bo3bo$5b2o19bo$5b2o15bo3bo$22bo2bo8b2o$22b3o9b2o$$7b2o36b2o$45bo$7b 188 | o4b3o28bobo$11bo3bo27b2o$10bo5bo13b3ob3o$10bo5bo13bo5bo$10b3ob3o13bo5b 189 | o$2b2o27bo3bo$bobo28b3o4bo$bo$2o36b2o$$11b2o9b3o$11b2o8bo2bo$20bo3bo 190 | 15b2o$20bo19b2o$20bo3bo5b2o$21bo2bo5b2o$22b3o$$18bo$18bobo$13b2o$13b2o 191 | $$25b2o$25bo$26b3o$28bo!"#).unwrap(); 192 | anime.push( 193 | lg, 194 | |lg| lg.update(), 195 | (0, 0), 196 | ); 197 | anime.run(); 198 | } 199 | ``` 200 | ![lifegame](./imgs/lifegame.gif) 201 | 202 | Here, `Animation` is still used, and the `rle` file for Conway's Game of Life is parsed. 203 | Everything needed to parse the `rle` file is already written inside `Lifegame`, with no additional dependencies, and the parsing code is very lightweight. 204 | 205 | ### Images 206 | 207 | Using braille code to draw images is also a good choice. 208 | However, the image library used for parsing `images` is a bit large, so it is not enabled by default. 209 | To use it, please add the following to your Cargo.toml: 210 | ```toml 211 | [dependencies] 212 | rsille = { version = "2.1.0", features = ["img"] } 213 | ``` 214 | 215 | Here's an example of usage. Note: Please fill in the image file path! 216 | ```rust 217 | use rsille::{extra::Imgille, Canvas}; 218 | 219 | fn main() { 220 | let mut canvas = Canvas::new(); 221 | let img = Imgille::new("path/to/img").unwrap(); 222 | canvas.paint(&img, 0, 0).unwrap(); 223 | canvas.print(); 224 | } 225 | ``` 226 | ![lena](./imgs/lena.png) 227 | ![xkcd](./imgs/xkcd.png) 228 | ![xkcd-invert](./imgs/xkcd-invert.png) 229 | 230 | By default, it uses color, which is not very friendly to grayscale images or black and white images (like those from xkcd), and it can reduce clarity! 231 | 232 | So for them, you must definitely call `color(false)`! 233 | 234 | It also supports inversion, such as the two xkcd images above, the one with black as the main color is not inverted, and the one with white as the main color is inverted. Call `invert(true)` to invert the colors. 235 | For color images, not using color is also a good choice, for example: 236 | 237 | ![lena-gray](./imgs/lena-gray.png) 238 | 239 | Additionally, the size of the image will automatically scale with the terminal, whether it's a long or wide terminal, it will always scale correctly! 240 | 241 | ### Animation 242 | 243 | Generating some objects, then drawing them onto the `Canvas`, then updating the objects, and redrawing. 244 | You also need to set the frame rate, handle user input, etc. 245 | Writing your own code to create an animation using the basic `Canvas` is always so troublesome! 246 | Not to mention the appropriate erasing of the screen and preventing flickering, etc. 247 | 248 | So this library wraps up all these troublesome things. It only takes 3 lines of code to create an animation with it! 249 | 250 | 1. Create a new animation `let mut anime = Animation::new()` 251 | 2. Load a drawable object and an update function `anime.push()` 252 | 3. Run `anime.run()` 253 | 254 | It's incredibly simple! 255 | ```rust 256 | use rsille::{extra::Object3D , Animation}; 257 | 258 | fn main() { 259 | let cube = Object3D::cube(30); 260 | let mut anime = Animation::new(); 261 | anime.push(cube, |cube| { cube.rotate((1.0, 2.0, 3.0)); false }, (0, 0)); 262 | anime.run(); 263 | } 264 | ``` 265 | 266 | The parameters for the `push` method are as follows: 267 | 1. The object, just pass it directly. 268 | 2. A closure that returns a boolean as the update function. This closure will run once per frame. When the closure returns true, the object stops updating and will no longer execute the update function, but it will still be drawn on the canvas. When all objects have finished updating, the animation stops. 269 | 3. *(x, y)*, the position where the object is placed, generally only used when there are multiple objects. 270 | 271 | For user input handling, currently, only ctrl+c and esc are available to exit. 272 | In the future, custom handling will likely be supported. 273 | 274 | ## 📌 TODO 275 | 276 | - [ ] Optimize multithreading in `Animation`. 277 | - [ ] Add more features to `Animation`. 278 | - [ ] Add more drawable objects. 279 | - [ ] Add a bounded version for `Lifegame`. 280 | - [ ] More examples. 281 | 282 | ## 📝 License 283 | 284 | [MIT](./LICENCE) -------------------------------------------------------------------------------- /README_cn.md: -------------------------------------------------------------------------------- 1 |
2 | 3 | # Rsille 🎨 4 | 5 | [![GitHub License](https://img.shields.io/github/license/nidhoggfgg/rsille?style=for-the-badge)](./LICENCE) 6 | [![Crates.io Version](https://img.shields.io/crates/v/rsille?style=for-the-badge)](https://crates.io/crates/rsille/versions) 7 | [![Crates.io Total Downloads](https://img.shields.io/crates/d/rsille?style=for-the-badge)](https://crates.io/crates/rsille) 8 | [![docs.rs](https://img.shields.io/docsrs/rsille?style=for-the-badge)](https://docs.rs/rsille/latest/rsille/) 9 | 10 | [English](./README.md) • [中文](./README_cn.md) 11 | 12 |
13 | 14 | 15 | ## 📚 目录 16 | 17 | ------- 18 | 19 | * [尝试一下](#尝试一下) 20 | * [安装](#安装) 21 | * [使用方法](#使用方法) 22 | * [基础](#基础) 23 | * [Turtle](#turtle) 24 | * [3D object](#3d-object) 25 | * [生命游戏](#生命游戏) 26 | * [图片](#图片) 27 | * [动画](#动画) 28 | * [TODO](#todo) 29 | * [许可](#📝-许可) 30 | 31 | 32 | ## 👀 尝试一下 33 | 34 | 所有 readme 中使用的图片都可以在[`example`](./examples/),中找到无需编程直接运行! 35 | ``` 36 | git clone https://github.com/nidhoggfgg/rsille.git 37 | cd rsille 38 | cargo run --example cube 39 | ``` 40 | 这会产生一个旋转的正方体。 41 | 关于更多例子或者想自己尝试生成那些有趣的图案,可以尝试使用 `cargo run --example`,你会找到所有的可执行的例子而无需编程。 42 | 43 | 当然,还有更多的例子,比如 [bad-apple](./imgs/bad-apple.gif) 等。 44 | 下方的使用指南会教你如何使用这个库。 45 | 46 | ## 🔧 安装 47 | 48 | 这是一个 rust 库,所以你只需要将如下代码添加到你的 Cargo.toml 中即可使用 49 | 50 | ```toml 51 | [dependencies] 52 | rsille = "2.1.0" 53 | ``` 54 | 55 | ## 🚀使用方法 56 | 57 | 限于篇幅,此处给出的是比较常用的 api 以及一般的使用方法。 58 | 关于详细的 api,可以去往 [doc.rs](https://docs.rs/rsille/latest/rsille/) 查看 59 | 60 | ### 基础 61 | 62 | 最基础的就是 `set` 63 | ```rust 64 | use rsille::Canvas; 65 | fn main() { 66 | let mut c = Canvas::new(); 67 | for x in -360..360 { 68 | let x = x as f64; 69 | c.set(x / 10.0, x.to_radians().sin() * 10.0); 70 | } 71 | c.print(); 72 | } 73 | ``` 74 | ![basic-sin](./imgs/basic-sin.png) 75 | 76 | 关于为什么是 *(x / 10.0, x.to_radians().sin() * 10.0)* 而不是 *(x, x.sin())*。 77 | 这一点你可以自己尝试加减乘除来更改绘制的坐标,有一个小提示 `⠿` 包含 8 个点。 78 | 如果你真的不理解或者想知道为什么,可以查看 [内部解析](#内部解析)。 79 | 稍后我可能会写一遍文章关于 braille code。 80 | 81 | ------ 82 | 83 | 与 `set` 相反的是 `unset`,作用就是将 `set` 过的点擦,这里就不给出具体的例子了。 84 | 85 | ------ 86 | 87 | 还有一个有用的方法就是 `toggle`,它会将已经 `set` 过的点 `unset`,而 `set` 未 `set` 的点 88 | ```rust 89 | use rsille::Canvas; 90 | fn main() { 91 | let mut c = Canvas::new(); 92 | for x in (0..360).step_by(4) { 93 | let x = x as f64; 94 | c.set(x / 4.0, x.to_radians().sin() * 30.0); 95 | } 96 | 97 | for x in 0..=30 { 98 | for y in 0..=30 { 99 | c.toggle(x, y); 100 | c.toggle(x + 30, y - 30); 101 | c.toggle(x + 60, y); 102 | } 103 | } 104 | c.print(); 105 | } 106 | ``` 107 | ![toggle-sin](./imgs/toggle-sin.png) 108 | 109 | 这个例子稍微有点长,先来解释一下其中的代码。 110 | * `(0..360).step_by(4)` 是在 *[0, π)* 上遍历 *x*,而 `step_by(4)` 是为了降采样,使线条更细(太低会导致不精确) 111 | * 第一个 `for` 就是为了画出 *f(x) = sin(x)* 的图像,为了更好看,这里对 *x* 和 *y* 有一定的缩放 112 | * 第二个 `for` 中就是为了产生那几个 `toggle` 块,它们的大小都是 31x31 113 | 114 | 在 `toggle` 中并没有使用浮点数,这是因为所有的方法都是有泛型支持的,可以传入各种数值! 115 | 不过仍然强烈建议使用 `f64` 来实现更高的精度。 116 | 117 | ### Turtle 118 | 119 | 在 python 中,有个很有趣的库就是 turtle。 120 | 它可以使初次使用 python 的初学者也能体会编程的乐趣。 121 | 在这个库中也实现了 turtle 中大部分的方法。 122 | ```rust 123 | use rsille::{extra::Turtle, Canvas}; 124 | fn main() { 125 | let mut canvas = Canvas::new(); 126 | let mut t = Turtle::new(); 127 | for _ in 0..5 { 128 | t.forward(50); 129 | t.right(144); 130 | } 131 | canvas.paint(&t, 0, 0).unwrap(); 132 | canvas.print(); 133 | } 134 | ``` 135 | ![turtle-star](./imgs/turtle-star.png) 136 | 137 | `paint` 方法中的两个 *0* 并不是固定的,而是将目标绘制在 *(0, 0)*。 138 | 这是任意的,但是对于单个物体,总是绘制在 `(0, 0)` 是不错的。 139 | 140 | turtle 就不过多介绍了,可以直接粘贴 python 的代码稍加修改即可使用。 141 | 142 | ### 3D Object 143 | 144 | 这个库也对三维物体提供了支持,还有内部方便的直接构造物体的方法 145 | ```rust 146 | use rsille::{extra::Object3D, Animation}; 147 | 148 | fn main() { 149 | let mut anime = Animation::new(); 150 | let cube = Object3D::cube(30); 151 | anime.push( 152 | cube, 153 | |cube| { 154 | cube.rotate((1.0, 2.0, 3.0)); 155 | false 156 | }, 157 | (0, 0), 158 | ); 159 | anime.run(); 160 | } 161 | ``` 162 | ![cube](./imgs/cube.gif) 163 | 164 | 这里使用了 `Animation` 来产生动画,这也是内部支持的,可以点开 [动画](#动画) 查看详细信息 165 | 166 | `Object3D` 主要有两个有用的方法,一个是 `rotate` 用于旋转物体,另一个则是 `zoom` 用于缩放物体。 167 | 168 | ### 生命游戏 169 | 170 | 生命游戏非常有意思,所以也作为了库的一部分 171 | ```rust 172 | use rsille::{extra::LifeGame, Animation}; 173 | 174 | fn main() { 175 | let mut anime = Animation::new(); 176 | let lg = LifeGame::from(r#" 177 | x = 47, y = 47, rule = B3/S23 178 | 18bo$18b3o$21bo$20b2o$$32b2o$32b2o$26bobo$28bo$$22b3o$15b2o5bo2bo$15b 179 | 2o5bo3bo$5b2o19bo$5b2o15bo3bo$22bo2bo8b2o$22b3o9b2o$$7b2o36b2o$45bo$7b 180 | o4b3o28bobo$11bo3bo27b2o$10bo5bo13b3ob3o$10bo5bo13bo5bo$10b3ob3o13bo5b 181 | o$2b2o27bo3bo$bobo28b3o4bo$bo$2o36b2o$$11b2o9b3o$11b2o8bo2bo$20bo3bo 182 | 15b2o$20bo19b2o$20bo3bo5b2o$21bo2bo5b2o$22b3o$$18bo$18bobo$13b2o$13b2o 183 | $$25b2o$25bo$26b3o$28bo! 184 | "#).unwrap(); 185 | anime.push( 186 | lg, 187 | |lg| lg.update(), 188 | (0, 0), 189 | ); 190 | anime.run(); 191 | } 192 | ``` 193 | ![lifegame](./imgs/lifegame.gif) 194 | 195 | 这里仍然是使用 `Animation`,同时解析 lifegame 的 `rle` 文件。 196 | 在 `Lifegame` 内部已经编写了解析 `rle` 文件所需的一切,没有添加任何依赖,而且解析的代码是非常轻量化的。 197 | 198 | ### 图片 199 | 200 | 使用 braille code 来绘制图片也是非常不错的选择。 201 | 但是由于解析图片使用的 `image` 库有点大,默认是不启用的。 202 | 想要使用则请将 Cargo.toml 添加如下内容 203 | ```toml 204 | [dependencies] 205 | rsille = { version = "2.1.0", features = ["img"] } 206 | ``` 207 | 208 | 这是一个使用的案例。注意:请填上图片的文件路径! 209 | ```rust 210 | use rsille::{extra::Imgille, Canvas}; 211 | 212 | fn main() { 213 | let mut canvas = Canvas::new(); 214 | let img = Imgille::new("path/to/img").unwrap(); 215 | canvas.paint(&img, 0, 0).unwrap(); 216 | canvas.print(); 217 | } 218 | ``` 219 | ![lena](./imgs/lena.png) 220 | ![xkcd](./imgs/xkcd.png) 221 | ![xkcd-invert](./imgs/xkcd-invert.png) 222 | 223 | 默认是使用颜色,而使用颜色的情况下对灰度图或者黑白图(比如来自 xkcd)等非常不友好,还会降低清晰度! 224 | 225 | 所以对于它们可以一定要调用 `color(false)` ! 226 | 同时还支持反色,比如上方两张 xckd 的图片,黑色为主的是未反色的,白色为主的是反色的,调用 `invert(true)` 即可反色。 227 | 而对于彩色图片,不使用颜色一样是不错的选择,比如: 228 | 229 | ![lena-gray](./imgs/lena-gray.png) 230 | 231 | 另外,图片的大小会随着终端自动缩放,无论是长的终端还是宽的终端,它总是能正确地缩放至合适大小! 232 | 233 | ### 动画 234 | 235 | 生成一些物体,然后绘制到 `Canvas`上,接着更新物体,重新绘制。 236 | 还要设置帧数,处理用户输入等。 237 | 每次自己写代码利用基础的 `Canvas` 绘制一个动画都是如此麻烦! 238 | 更别提还有合适地擦除屏幕以及防止闪烁等问题。 239 | 240 | 所以这个库将这些麻烦事都包装了起来,使用它来产生动画只需要 3 行代码即可! 241 | 242 | 1. 新建一个动画 `let mut anime = Animation::new()` 243 | 2. 装载一个可以绘制的目标以及更新函数 `anime.push()` 244 | 3. 运行 `anime.run()` 245 | 246 | 简直简单到了极点! 247 | ```rust 248 | use rsille::{extra::Object3D, Animation}; 249 | 250 | fn main() { 251 | let cube = Object3D::cube(30); 252 | let mut anime = Animation::new(); 253 | anime.push(cube, |cube| { cube.rotate((1.0, 2.0, 3.0)); false }, (0, 0)); 254 | anime.run(); 255 | } 256 | ``` 257 | 258 | `push` 方法的参数如下: 259 | 1. 目标,直接传入即可 260 | 2. 闭包,一个返回值为 `bool` 的更新函数。每一帧都会运行一次该闭包,当闭包返回值为 `true` 时该目标停止更新,也不会再执行更新函数,但仍然会绘制在画布上。当所有的目标都已经结束更新,动画停止。 261 | 3. *(x, y)*,放置目标所在的位置,一般只有多个目标时使用 262 | 263 | 对于用户输入的处理,目前只有 `ctrl+c` 和 `esc` 退出, 264 | 之后应该会开放自定义。 265 | 266 | ## 📌 TODO 267 | 268 | - [ ] 优化 `Animation` 中多线程 269 | - [ ] 为 `Animation` 添加更多功能 270 | - [ ] 添加更多可绘制的对象 271 | - [ ] 为 `Lifegame` 添加有边界的版本 272 | - [ ] 更多的例子 273 | 274 | ## 📝 许可 275 | 276 | [MIT](./LICENCE) 277 | -------------------------------------------------------------------------------- /benches/set.rs: -------------------------------------------------------------------------------- 1 | use criterion::{criterion_group, criterion_main, Criterion}; 2 | use rsille::Canvas; 3 | 4 | fn render(size: i32, c: &mut Canvas) { 5 | let mut v = vec![]; 6 | c.set(0.0, 0.0); 7 | for i in 0..size { 8 | for j in 0..size { 9 | c.set(i as f64, j as f64); 10 | } 11 | } 12 | c.print_on(&mut v, false).unwrap(); 13 | } 14 | 15 | fn criterion_benchmark(bencher: &mut Criterion) { 16 | let mut c = Canvas::new(); 17 | bencher.bench_function("0 * 0", |b| b.iter(|| render(0, &mut c))); 18 | c.reset(); 19 | bencher.bench_function("10 * 10", |b| b.iter(|| render(10, &mut c))); 20 | c.reset(); 21 | bencher.bench_function("20 * 20", |b| b.iter(|| render(20, &mut c))); 22 | c.reset(); 23 | bencher.bench_function("40 * 40", |b| b.iter(|| render(40, &mut c))); 24 | c.reset(); 25 | bencher.bench_function("100 * 100", |b| b.iter(|| render(100, &mut c))); 26 | c.reset(); 27 | bencher.bench_function("500 * 500 huge", |b| b.iter(|| render(500, &mut c))); 28 | c.reset(); 29 | bencher.bench_function("1000 * 1000 crazy", |b| b.iter(|| render(1000, &mut c))); 30 | c.reset(); 31 | } 32 | 33 | criterion_group!(benches, criterion_benchmark); 34 | criterion_main!(benches); 35 | -------------------------------------------------------------------------------- /examples/anime/mix.rs: -------------------------------------------------------------------------------- 1 | use rsille::{ 2 | color::Color, 3 | extra::{Object3D, Turtle}, 4 | Animation, 5 | }; 6 | 7 | fn main() { 8 | let mut anime = Animation::new(); 9 | let mut t = Turtle::new(); 10 | for i in 0..12 { 11 | t.right(30.0); 12 | for j in 0..36 { 13 | t.color(Color::Rgb { 14 | r: 240 - i * 10, 15 | g: 60 + j * 5, 16 | b: 220, 17 | }); 18 | t.right(10.0); 19 | t.forward(4.2); 20 | } 21 | } 22 | let object = gen_cube(); 23 | let mut k = 0; 24 | t.anime(); 25 | anime.set_fps(60); 26 | anime.push(t, move |t| t.update(), (50.0, 50.0)); 27 | anime.push( 28 | object, 29 | move |obj| { 30 | obj.rotate((1.0, 2.0, 3.0)); 31 | k += 1; 32 | k > 12 * 36 33 | }, 34 | (50.0, 50.0), 35 | ); 36 | anime.run(); 37 | println!("End!"); 38 | } 39 | 40 | fn gen_cube() -> Object3D { 41 | let side_len = 30.0; 42 | #[rustfmt::skip] 43 | let a = [ 44 | (-1, -1, -1), 45 | (-1, -1, 1), 46 | (-1, 1, -1), 47 | ( 1, -1, -1), 48 | (-1, 1, 1), 49 | ( 1, -1, 1), 50 | ( 1, 1, -1), 51 | ( 1, 1, 1), 52 | ]; 53 | let mut points = Vec::new(); 54 | let mut object = Object3D::new(); 55 | for i in a { 56 | let x = side_len / 2.0 * i.0 as f64; 57 | let y = side_len / 2.0 * i.1 as f64; 58 | let z = side_len / 2.0 * i.2 as f64; 59 | points.push((x, y, z)); 60 | } 61 | object.add_points(&points); 62 | object 63 | .add_sides(&[ 64 | (0, 1), 65 | (1, 4), 66 | (4, 2), 67 | (2, 0), 68 | (3, 5), 69 | (5, 7), 70 | (7, 6), 71 | (6, 3), 72 | (1, 5), 73 | (4, 7), 74 | (2, 6), 75 | (0, 3), 76 | ]) 77 | .unwrap(); 78 | object 79 | } 80 | -------------------------------------------------------------------------------- /examples/files/lena.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nidhoggfgg/rsille/836fba037b8d9d14b651412cb7755a7754e08879/examples/files/lena.png -------------------------------------------------------------------------------- /examples/files/lifegame/132p37.rle: -------------------------------------------------------------------------------- 1 | #C Generated by copy.sh/life 2 | x = 47, y = 47, rule = B3/S23 3 | 18bo$18b3o$21bo$20b2o$$32b2o$32b2o$26bobo$28bo$$22b3o$15b2o5bo2bo$15b 4 | 2o5bo3bo$5b2o19bo$5b2o15bo3bo$22bo2bo8b2o$22b3o9b2o$$7b2o36b2o$45bo$7b 5 | o4b3o28bobo$11bo3bo27b2o$10bo5bo13b3ob3o$10bo5bo13bo5bo$10b3ob3o13bo5b 6 | o$2b2o27bo3bo$bobo28b3o4bo$bo$2o36b2o$$11b2o9b3o$11b2o8bo2bo$20bo3bo 7 | 15b2o$20bo19b2o$20bo3bo5b2o$21bo2bo5b2o$22b3o$$18bo$18bobo$13b2o$13b2o 8 | $$25b2o$25bo$26b3o$28bo! -------------------------------------------------------------------------------- /examples/files/lifegame/c5diagonaltubstretcher.rle: -------------------------------------------------------------------------------- 1 | #C Generated by copy.sh/life 2 | x = 55, y = 55, rule = B3/S23 3 | 45b2o$45b2o$44bo2bo$41b2obo2bo$47bo$39b2o3bo2bo$39b2o5bo$40bob5o$41bo$ 4 | $$38b3o$38bo$36b2o$30b2o4bo$30b3o3bo$28bo4bo$28bo3bo$32bo$27b2obobo$ 5 | 25b2o5bo$25b2o4b2o$27b4o$$$20b2o8bo$20b2o7b3o$19bo2bo5bo$16b2obo2bo4bo 6 | bobo$22bo3bobo2bobo$14b2o3bo2bo2b2o5bo2bo$14b2o5bo4bob2o3b3o$15bob5o8b 7 | o$16bo12bobo4b2o3b3o$31bo3bo2bo2b3o$30b2o2bob3ob2o$13b3o17bobo4bo$13bo 8 | 19bobo4b2ob3o$11b2o21b2o5b3obo$5b2o4bo29bobo2b2o$5b3o3bo23b3o3bob2o2bo 9 | $3bo4bo24b3ob4ob5o3bo$3bo3bo25b2o3bo2bo4bo2bobo$7bo25b2o2b5o8bobo$2b2o 10 | bobo29bo2b2o9bobo$2o5bo29b2o2bo10bobo$2o4b2o31bob2o10bo$2b4o33b2o$$42b 11 | o$41bobo$42bobo$43bobo$44bobo$45bo! -------------------------------------------------------------------------------- /examples/files/miku.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nidhoggfgg/rsille/836fba037b8d9d14b651412cb7755a7754e08879/examples/files/miku.jpeg -------------------------------------------------------------------------------- /examples/files/xkcd2898.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nidhoggfgg/rsille/836fba037b8d9d14b651412cb7755a7754e08879/examples/files/xkcd2898.png -------------------------------------------------------------------------------- /examples/imgille.rs: -------------------------------------------------------------------------------- 1 | use std::env; 2 | 3 | use rsille::{extra::Imgille, Canvas}; 4 | 5 | fn main() { 6 | let args: Vec = env::args().collect(); 7 | if args.len() == 2 { 8 | let path = &args[1]; 9 | let mut canvas = Canvas::new(); 10 | let imgille = if let Ok(img) = Imgille::new(path) { 11 | img 12 | } else { 13 | println!("useage: [{}] ", args[0]); 14 | return; 15 | }; 16 | canvas.paint(&imgille, 0.0, 0.0).unwrap(); 17 | canvas.print(); 18 | } else { 19 | println!("useage: [{}] ", args[0]); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /examples/life.rs: -------------------------------------------------------------------------------- 1 | use std::env; 2 | 3 | use rsille::{extra::LifeGame, Animation}; 4 | 5 | fn main() { 6 | let args: Vec = env::args().collect(); 7 | if args.len() != 2 { 8 | println!("useage: [{}] ", args[0]); 9 | return; 10 | } 11 | let lg = if let Ok(lg) = LifeGame::from_path(&args[1]) { 12 | lg 13 | } else { 14 | println!("can't parse {}!", args[1]); 15 | return; 16 | }; 17 | let mut anime = Animation::new(); 18 | anime.push(lg, |lg| lg.update(), (0.0, 0.0)); 19 | anime.run(); 20 | } 21 | -------------------------------------------------------------------------------- /examples/object3d/cube-colorful.rs: -------------------------------------------------------------------------------- 1 | use rsille::{color::Color, extra::Object3D, Animation}; 2 | 3 | fn gen_cube(side_len: f64) -> Object3D { 4 | #[rustfmt::skip] 5 | let a = [ 6 | (-1, -1, -1), 7 | (-1, -1, 1), 8 | (-1, 1, -1), 9 | ( 1, -1, -1), 10 | (-1, 1, 1), 11 | ( 1, -1, 1), 12 | ( 1, 1, -1), 13 | ( 1, 1, 1), 14 | ]; 15 | let mut points = Vec::new(); 16 | let mut object = Object3D::new(); 17 | for i in a { 18 | let x = side_len / 2.0 * i.0 as f64; 19 | let y = side_len / 2.0 * i.1 as f64; 20 | let z = side_len / 2.0 * i.2 as f64; 21 | points.push((x, y, z)); 22 | } 23 | object.add_points(&points); 24 | object 25 | .add_sides_colorful(&[ 26 | ((0, 1), Color::AnsiValue(240)), 27 | ((1, 4), Color::AnsiValue(220)), 28 | ((4, 2), Color::AnsiValue(200)), 29 | ((2, 0), Color::AnsiValue(180)), 30 | ((3, 5), Color::AnsiValue(160)), 31 | ((5, 7), Color::AnsiValue(140)), 32 | ((7, 6), Color::AnsiValue(140)), 33 | ((6, 3), Color::AnsiValue(160)), 34 | ((1, 5), Color::AnsiValue(180)), 35 | ((4, 7), Color::AnsiValue(200)), 36 | ((2, 6), Color::AnsiValue(220)), 37 | ((0, 3), Color::AnsiValue(240)), 38 | ]) 39 | .unwrap(); 40 | object 41 | } 42 | 43 | // just make the rotate looks more "random" 44 | fn gen(k: i32) -> ((f64, f64, f64), f64) { 45 | let rotate = match k { 46 | k if k % 3 == 0 => (1.0, 2.0, 3.0), 47 | k if k % 3 == 1 => (2.0, 3.0, 4.0), 48 | k if k % 3 == 2 => (3.0, 4.0, 5.0), 49 | _ => panic!("impossible"), 50 | }; 51 | let zoom = if k % 60 <= 30 { 52 | 1.0 + (k % 60) as f64 * 0.02 53 | } else { 54 | 1.6 - (k % 60 - 30) as f64 * 0.02 55 | }; 56 | (rotate, zoom) 57 | } 58 | 59 | fn main() { 60 | let side_len = 30.0; 61 | let mut anime = Animation::new(); 62 | let object = gen_cube(side_len); 63 | let mut k = 0; 64 | anime.push( 65 | object, 66 | move |obj| { 67 | let (angle, f) = gen(k); 68 | obj.rotate(angle); 69 | obj.zoom(f); 70 | k += 1; 71 | k > 180 72 | }, 73 | (0.0, 0.0), 74 | ); 75 | anime.set_size(1.5 * side_len, 1.5 * side_len); 76 | anime.set_minx(-1.5 * side_len); 77 | anime.run(); 78 | } 79 | -------------------------------------------------------------------------------- /examples/object3d/cube.rs: -------------------------------------------------------------------------------- 1 | use rsille::{extra::Object3D, Animation}; 2 | 3 | // just make the rotate looks more "random" 4 | fn gen(k: i32) -> ((f64, f64, f64), f64) { 5 | let rotate = match k { 6 | k if k % 3 == 0 => (1.0, 2.0, 3.0), 7 | k if k % 3 == 1 => (2.0, 3.0, 4.0), 8 | k if k % 3 == 2 => (3.0, 4.0, 5.0), 9 | _ => panic!("impossible"), 10 | }; 11 | let zoom = if k % 60 <= 30 { 12 | 1.0 + (k % 60) as f64 * 0.02 13 | } else { 14 | 1.6 - (k % 60 - 30) as f64 * 0.02 15 | }; 16 | (rotate, zoom) 17 | } 18 | 19 | fn main() { 20 | let side_len = 30.0; 21 | let mut anime = Animation::new(); 22 | let object = Object3D::cube(side_len); 23 | let mut k = 0; 24 | anime.push( 25 | object, 26 | move |obj| { 27 | let (angle, zoom) = gen(k); 28 | obj.rotate(angle); 29 | obj.zoom(zoom); 30 | k += 1; 31 | k > 1500 32 | }, 33 | (0.0, 0.0), 34 | ); 35 | anime.set_size(1.5 * side_len, 1.5 * side_len); 36 | anime.run(); 37 | } 38 | -------------------------------------------------------------------------------- /examples/object3d/obj-mix.rs: -------------------------------------------------------------------------------- 1 | use std::sync::{Arc, Mutex}; 2 | 3 | use rsille::{color::Color, extra::Object3D, Animation}; 4 | 5 | fn gen_cube(side_len: f64) -> (Object3D, Object3D) { 6 | #[rustfmt::skip] 7 | let a = [ 8 | (-1, -1, -1), 9 | (-1, -1, 1), 10 | (-1, 1, -1), 11 | ( 1, -1, -1), 12 | (-1, 1, 1), 13 | ( 1, -1, 1), 14 | ( 1, 1, -1), 15 | ( 1, 1, 1), 16 | ]; 17 | let mut points = Vec::new(); 18 | let mut colorful = Object3D::new(); 19 | for i in a { 20 | let x = side_len / 2.0 * i.0 as f64; 21 | let y = side_len / 2.0 * i.1 as f64; 22 | let z = side_len / 2.0 * i.2 as f64; 23 | points.push((x, y, z)); 24 | } 25 | colorful.add_points(&points); 26 | let mut nocolor = colorful.clone(); 27 | colorful 28 | .add_sides_colorful(&[ 29 | ((0, 1), Color::AnsiValue(240)), 30 | ((1, 4), Color::AnsiValue(220)), 31 | ((4, 2), Color::AnsiValue(200)), 32 | ((2, 0), Color::AnsiValue(180)), 33 | ((3, 5), Color::AnsiValue(160)), 34 | ((5, 7), Color::AnsiValue(140)), 35 | ((7, 6), Color::AnsiValue(140)), 36 | ((6, 3), Color::AnsiValue(160)), 37 | ((1, 5), Color::AnsiValue(180)), 38 | ((4, 7), Color::AnsiValue(200)), 39 | ((2, 6), Color::AnsiValue(220)), 40 | ((0, 3), Color::AnsiValue(240)), 41 | ]) 42 | .unwrap(); 43 | nocolor.add_sides(&colorful.sides()).unwrap(); 44 | (colorful, nocolor) 45 | } 46 | 47 | fn gen_octahedron(side_len: f64) -> (Object3D, Object3D) { 48 | let a = [ 49 | (0, 0, 1), 50 | (1, 0, 0), 51 | (0, 1, 0), 52 | (-1, 0, 0), 53 | (0, -1, 0), 54 | (0, 0, -1), 55 | ]; 56 | let mut points = Vec::new(); 57 | let mut colorful = Object3D::new(); 58 | for i in a { 59 | let x = side_len * i.0 as f64; 60 | let y = side_len * i.1 as f64; 61 | let z = side_len * i.2 as f64; 62 | points.push((x, y, z)); 63 | } 64 | colorful.add_points(&points); 65 | let mut nocolor = colorful.clone(); 66 | colorful 67 | .add_sides_colorful(&[ 68 | ((0, 1), Color::AnsiValue(100)), 69 | ((0, 2), Color::AnsiValue(120)), 70 | ((0, 3), Color::AnsiValue(140)), 71 | ((0, 4), Color::AnsiValue(160)), 72 | ((5, 1), Color::AnsiValue(180)), 73 | ((5, 2), Color::AnsiValue(200)), 74 | ((5, 3), Color::AnsiValue(200)), 75 | ((5, 4), Color::AnsiValue(180)), 76 | ((1, 2), Color::AnsiValue(160)), 77 | ((2, 3), Color::AnsiValue(140)), 78 | ((3, 4), Color::AnsiValue(120)), 79 | ((4, 1), Color::AnsiValue(100)), 80 | ]) 81 | .unwrap(); 82 | nocolor.add_sides(&colorful.sides()).unwrap(); 83 | (colorful, nocolor) 84 | } 85 | 86 | fn gen(k: i32) -> (f64, f64, f64) { 87 | match k { 88 | k if k % 4 == 3 => (1.0, 2.0, 3.0), 89 | k if k % 4 == 2 => (-2.0, -3.0, 4.0), 90 | k if k % 4 == 1 => (2.0, 3.0, 4.0), 91 | k if k % 4 == 0 => (-1.0, -2.0, -3.0), 92 | _ => panic!("impossible"), 93 | } 94 | } 95 | 96 | fn main() { 97 | let side_len = 40.0; 98 | let mut anime = Animation::new(); 99 | let (cotc, otc) = gen_octahedron(side_len); 100 | let (ccube, cube) = gen_cube(side_len); 101 | let objs = [ 102 | (otc, (0.0, 0.0)), 103 | (cotc, (70.0, 0.0)), 104 | (ccube, (0.0, 70.0)), 105 | (cube, (70.0, 70.0)), 106 | ]; 107 | let k = Arc::new(Mutex::new(0)); 108 | for (obj, location) in objs { 109 | let k = Arc::clone(&k); 110 | anime.push( 111 | obj.clone(), 112 | move |obj| { 113 | let mut k = k.lock().unwrap(); 114 | let angle = gen(*k); 115 | obj.rotate(angle); 116 | *k += 1; 117 | *k > 1200 118 | }, 119 | location, 120 | ); 121 | } 122 | anime.set_size(110, 110); 123 | anime.run(); 124 | } 125 | -------------------------------------------------------------------------------- /examples/object3d/otc-colorful.rs: -------------------------------------------------------------------------------- 1 | use rsille::{color::Color, extra::Object3D, Animation}; 2 | 3 | fn gen_octahedron(side_len: f64) -> Object3D { 4 | let a = [ 5 | (0, 0, 1), 6 | (1, 0, 0), 7 | (0, 1, 0), 8 | (-1, 0, 0), 9 | (0, -1, 0), 10 | (0, 0, -1), 11 | ]; 12 | let mut points = Vec::new(); 13 | let mut object = Object3D::new(); 14 | for i in a { 15 | let x = side_len * i.0 as f64; 16 | let y = side_len * i.1 as f64; 17 | let z = side_len * i.2 as f64; 18 | points.push((x, y, z)); 19 | } 20 | object.add_points(&points); 21 | object 22 | .add_sides_colorful(&[ 23 | ((0, 1), Color::AnsiValue(100)), 24 | ((0, 2), Color::AnsiValue(120)), 25 | ((0, 3), Color::AnsiValue(140)), 26 | ((0, 4), Color::AnsiValue(160)), 27 | ((5, 1), Color::AnsiValue(180)), 28 | ((5, 2), Color::AnsiValue(200)), 29 | ((5, 3), Color::AnsiValue(200)), 30 | ((5, 4), Color::AnsiValue(180)), 31 | ((1, 2), Color::AnsiValue(160)), 32 | ((2, 3), Color::AnsiValue(140)), 33 | ((3, 4), Color::AnsiValue(120)), 34 | ((4, 1), Color::AnsiValue(100)), 35 | ]) 36 | .unwrap(); 37 | object 38 | } 39 | 40 | // just make the rotate looks more "random" 41 | fn gen(k: i32) -> ((f64, f64, f64), f64) { 42 | let rotate = match k { 43 | k if k % 3 == 0 => (1.0, 2.0, 3.0), 44 | k if k % 3 == 1 => (2.0, 3.0, 4.0), 45 | k if k % 3 == 2 => (3.0, 4.0, 5.0), 46 | _ => panic!("impossible"), 47 | }; 48 | let zoom = if k % 60 <= 30 { 49 | 1.0 + (k % 60) as f64 * 0.02 50 | } else { 51 | 1.6 - (k % 60 - 30) as f64 * 0.02 52 | }; 53 | (rotate, zoom) 54 | } 55 | 56 | fn main() { 57 | let side_len = 40.0; 58 | let mut anime = Animation::new(); 59 | let object = gen_octahedron(side_len); 60 | let mut k = 0; 61 | anime.push( 62 | object, 63 | move |obj| { 64 | let (angle, zoom) = gen(k); 65 | obj.rotate(angle); 66 | obj.zoom(zoom); 67 | k += 1; 68 | if k >= 300 { 69 | true 70 | } else { 71 | false 72 | } 73 | }, 74 | (1.6 * side_len, 1.6 * side_len), 75 | ); 76 | anime.run(); 77 | } 78 | -------------------------------------------------------------------------------- /examples/object3d/otc.rs: -------------------------------------------------------------------------------- 1 | use rsille::{extra::Object3D, Animation}; 2 | 3 | fn gen_octahedron(side_len: f64) -> Object3D { 4 | #[rustfmt::skip] 5 | let a = [ 6 | ( 0, 0, 1), 7 | ( 1, 0, 0), 8 | ( 0, 1, 0), 9 | (-1, 0, 0), 10 | ( 0, -1, 0), 11 | ( 0, 0, -1), 12 | ]; 13 | let mut points = Vec::new(); 14 | let mut object = Object3D::new(); 15 | for i in a { 16 | let x = side_len * i.0 as f64; 17 | let y = side_len * i.1 as f64; 18 | let z = side_len * i.2 as f64; 19 | points.push((x, y, z)); 20 | } 21 | object.add_points(&points); 22 | object 23 | .add_sides(&[ 24 | (0, 1), 25 | (0, 2), 26 | (0, 3), 27 | (0, 4), 28 | (5, 1), 29 | (5, 2), 30 | (5, 3), 31 | (5, 4), 32 | (1, 2), 33 | (2, 3), 34 | (3, 4), 35 | (4, 1), 36 | ]) 37 | .unwrap(); 38 | object 39 | } 40 | 41 | // just make the rotate looks more "random" 42 | fn gen(k: i32) -> ((f64, f64, f64), f64) { 43 | let rotate = match k { 44 | k if k % 3 == 0 => (1.0, 2.0, 3.0), 45 | k if k % 3 == 1 => (2.0, 3.0, 4.0), 46 | k if k % 3 == 2 => (3.0, 4.0, 5.0), 47 | _ => panic!("impossible"), 48 | }; 49 | let zoom = if k % 60 <= 30 { 50 | 1.0 + (k % 60) as f64 * 0.02 51 | } else { 52 | 1.6 - (k % 60 - 30) as f64 * 0.02 53 | }; 54 | (rotate, zoom) 55 | } 56 | 57 | fn main() { 58 | let side_len = 40.0; 59 | let mut anime = Animation::new(); 60 | let object = gen_octahedron(side_len); 61 | let mut k = 0; 62 | anime.push( 63 | object, 64 | move |obj| { 65 | let (angle, zoom) = gen(k); 66 | obj.rotate(angle); 67 | obj.zoom(zoom); 68 | k += 1; 69 | false 70 | }, 71 | (1.6 * side_len, 1.6 * side_len), 72 | ); 73 | anime.run(); 74 | } 75 | -------------------------------------------------------------------------------- /examples/sin.rs: -------------------------------------------------------------------------------- 1 | use rsille::Canvas; 2 | 3 | fn main() { 4 | let mut c = Canvas::new(); 5 | 6 | for x in 0..1800 { 7 | let x = x as f64; 8 | c.set(x / 10.0, 15.0 + x.to_radians().sin() * 10.0); 9 | } 10 | c.print(); 11 | c.reset(); 12 | 13 | for x in (0..1800).step_by(10) { 14 | let x = x as f64; 15 | c.set(x / 10.0, 10.0 + x.to_radians().sin() * 10.0); 16 | c.set(x / 10.0, 10.0 + x.to_radians().cos() * 10.0); 17 | } 18 | c.print(); 19 | c.reset(); 20 | 21 | for x in (0..3600).step_by(20) { 22 | let x = x as f64; 23 | c.set(x / 20.0, 4.0 + x.to_radians().sin() * 4.0); 24 | } 25 | c.print(); 26 | c.reset(); 27 | 28 | for x in (0..360).step_by(4) { 29 | let x = x as f64; 30 | c.set(x / 4.0, 30.0 + x.to_radians().sin() * 30.0); 31 | } 32 | 33 | for x in 0..30 { 34 | for y in 0..30 { 35 | let (x, y) = (x as f64, y as f64); 36 | c.set(x, y); 37 | c.toggle(x + 30.0, y + 30.0); 38 | c.toggle(x + 60.0, y); 39 | } 40 | } 41 | c.print(); 42 | } 43 | -------------------------------------------------------------------------------- /examples/turtle/turtle-flower.rs: -------------------------------------------------------------------------------- 1 | use rsille::{color::Color, extra::Turtle, Canvas}; 2 | 3 | fn main() { 4 | let mut canvas = Canvas::new(); 5 | let mut t = Turtle::new(); 6 | for i in 0..36 { 7 | t.right(10.0); 8 | for j in 0..36 { 9 | t.color(Color::Rgb { 10 | r: 60 + i * 5, 11 | g: 190 - j * 5, 12 | b: 220, 13 | }); 14 | t.right(10.0); 15 | t.forward(8.0); 16 | } 17 | } 18 | canvas.paint(&t, 100.0, 100.0).unwrap(); 19 | canvas.print(); 20 | } 21 | -------------------------------------------------------------------------------- /examples/turtle/turtle-multi.rs: -------------------------------------------------------------------------------- 1 | use rsille::{color::Color, extra::Turtle, Canvas}; 2 | 3 | fn star5() -> (Turtle, (f64, f64)) { 4 | let mut t = Turtle::new(); 5 | for _ in 0..5 { 6 | t.color(Color::Red); 7 | t.forward(100.0); 8 | t.right(144.0); 9 | } 10 | (t, (0.0, 30.0)) 11 | } 12 | 13 | fn spiral() -> (Turtle, (f64, f64)) { 14 | let mut t = Turtle::new(); 15 | let mut length = 1.0; 16 | for i in 0..150 { 17 | t.color(Color::Rgb { 18 | r: 100 + i, 19 | g: 255 - i, 20 | b: 60 + i, 21 | }); 22 | t.forward(length); 23 | t.right(10.0); 24 | length += 0.05; 25 | } 26 | (t, (50.0, 130.0)) 27 | } 28 | 29 | fn circle() -> (Turtle, (f64, f64)) { 30 | let mut t = Turtle::new(); 31 | t.color(Color::AnsiValue(123)); 32 | t.circle(30.0, 360.0); 33 | (t, (123.0, 150.0)) 34 | } 35 | 36 | fn star6() -> (Turtle, (f64, f64)) { 37 | let mut t = Turtle::new(); 38 | for i in 0..6 { 39 | for j in 0..3 { 40 | t.color(Color::AnsiValue(i * 20 + j * 5)); 41 | t.forward(10.0); 42 | t.left(120.0); 43 | } 44 | t.forward(10.0); 45 | t.right(60.0); 46 | } 47 | (t, (120.0, 130.0)) 48 | } 49 | 50 | fn main() { 51 | let mut canvas = Canvas::new(); 52 | let things = vec![star5(), spiral(), circle(), star6()]; 53 | for (t, (x, y)) in things { 54 | canvas.paint(&t, x, y).unwrap(); 55 | } 56 | canvas.print(); 57 | } 58 | -------------------------------------------------------------------------------- /imgs/anime.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nidhoggfgg/rsille/836fba037b8d9d14b651412cb7755a7754e08879/imgs/anime.gif -------------------------------------------------------------------------------- /imgs/bad-apple.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nidhoggfgg/rsille/836fba037b8d9d14b651412cb7755a7754e08879/imgs/bad-apple.gif -------------------------------------------------------------------------------- /imgs/basic-sin.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nidhoggfgg/rsille/836fba037b8d9d14b651412cb7755a7754e08879/imgs/basic-sin.png -------------------------------------------------------------------------------- /imgs/cube.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nidhoggfgg/rsille/836fba037b8d9d14b651412cb7755a7754e08879/imgs/cube.gif -------------------------------------------------------------------------------- /imgs/lena-gray.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nidhoggfgg/rsille/836fba037b8d9d14b651412cb7755a7754e08879/imgs/lena-gray.png -------------------------------------------------------------------------------- /imgs/lena.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nidhoggfgg/rsille/836fba037b8d9d14b651412cb7755a7754e08879/imgs/lena.png -------------------------------------------------------------------------------- /imgs/lifegame.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nidhoggfgg/rsille/836fba037b8d9d14b651412cb7755a7754e08879/imgs/lifegame.gif -------------------------------------------------------------------------------- /imgs/mandelbrot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nidhoggfgg/rsille/836fba037b8d9d14b651412cb7755a7754e08879/imgs/mandelbrot.png -------------------------------------------------------------------------------- /imgs/objects.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nidhoggfgg/rsille/836fba037b8d9d14b651412cb7755a7754e08879/imgs/objects.gif -------------------------------------------------------------------------------- /imgs/toggle-sin.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nidhoggfgg/rsille/836fba037b8d9d14b651412cb7755a7754e08879/imgs/toggle-sin.png -------------------------------------------------------------------------------- /imgs/turtle-multi.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nidhoggfgg/rsille/836fba037b8d9d14b651412cb7755a7754e08879/imgs/turtle-multi.png -------------------------------------------------------------------------------- /imgs/turtle-star.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nidhoggfgg/rsille/836fba037b8d9d14b651412cb7755a7754e08879/imgs/turtle-star.png -------------------------------------------------------------------------------- /imgs/xkcd-invert.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nidhoggfgg/rsille/836fba037b8d9d14b651412cb7755a7754e08879/imgs/xkcd-invert.png -------------------------------------------------------------------------------- /imgs/xkcd.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nidhoggfgg/rsille/836fba037b8d9d14b651412cb7755a7754e08879/imgs/xkcd.png -------------------------------------------------------------------------------- /src/anime.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | io::Write, 3 | sync::{Arc, Mutex}, 4 | thread, 5 | time::{Duration, Instant}, 6 | }; 7 | 8 | use crossterm::{ 9 | cursor::MoveTo, 10 | event::{Event, KeyCode, KeyModifiers}, 11 | queue, 12 | terminal::{disable_raw_mode, enable_raw_mode}, 13 | }; 14 | 15 | use crate::{ 16 | term::{self, get_terminal_size, is_raw_mode}, 17 | Canvas, Paint, 18 | }; 19 | 20 | /// Create an animation 21 | /// 22 | /// Make the animation easy 23 | /// 24 | /// ## Example 25 | /// 26 | /// draw a cube and rotate it 27 | /// ```no_run 28 | /// use rsille::{extra::Object3D, Animation}; 29 | /// let cube = Object3D::cube(30.0); 30 | /// let mut anime = Animation::new(); 31 | /// anime.push(cube, |cube| { 32 | /// cube.rotate((1.0, 2.0, 3.0)); 33 | /// false 34 | /// }, (30, -30)); 35 | /// anime.run(); 36 | /// ``` 37 | pub struct Animation { 38 | canvas: Arc>, 39 | objs: Arc>>>, 40 | fps: u32, 41 | hide_cursor: bool, 42 | size: Option<(i32, i32)>, 43 | end: Arc>, 44 | } 45 | 46 | impl Animation { 47 | /// Create a new animation 48 | /// 49 | /// The default fps is 30 and hide the cursor. 50 | pub fn new() -> Self { 51 | Self { 52 | canvas: Arc::new(Mutex::new(Canvas::new())), 53 | objs: Arc::new(Mutex::new(Vec::new())), 54 | fps: 30, 55 | hide_cursor: true, 56 | size: None, 57 | end: Arc::new(Mutex::new(false)), 58 | } 59 | } 60 | 61 | /// Push an object to the animation 62 | /// 63 | /// * `obj` - the object to paint 64 | /// * `f` - the function to update the object 65 | /// * `xy` - the position to paint the object 66 | pub fn push(&mut self, obj: T, f: F, xy: (N, N)) 67 | where 68 | T: Paint, 69 | F: FnMut(&mut T) -> bool + Send + 'static, 70 | N: Into + Copy, 71 | { 72 | let mut objs = self.objs.lock().unwrap(); 73 | objs.push(Box::new(UserObj { 74 | obj, 75 | f, 76 | xy: (xy.0.into(), xy.1.into()), 77 | is_end: false, 78 | })); 79 | } 80 | 81 | /// Run the animation 82 | /// 83 | /// When all the objects are end or press `ctrl+c` or `esc`, the animation will stop. 84 | pub fn run(&mut self) { 85 | // should be very carefully to change these code 86 | 87 | // init 88 | let duration = Duration::from_secs(1) / self.fps; 89 | let objs = Arc::clone(&self.objs); 90 | let canvas = Arc::clone(&self.canvas); 91 | let mut stdout = std::io::stdout(); 92 | term::clear(); 93 | if self.hide_cursor { 94 | term::hide_cursor(); 95 | } 96 | enable_raw_mode().expect("can't enbale raw mode"); 97 | 98 | // main loop 99 | let end = Arc::clone(&self.end); 100 | let mainloop = thread::spawn(move || loop { 101 | let start_time = Instant::now(); 102 | // must wraped! for drop the objs 103 | { 104 | let mut objs = objs.lock().unwrap(); 105 | let mut end = end.lock().unwrap(); 106 | if *end { 107 | break; 108 | } 109 | if objs.iter().all(|obj| obj.is_end()) { 110 | *end = true; 111 | break; 112 | } 113 | let mut canvas = canvas.lock().unwrap(); 114 | canvas.clear(); 115 | queue!(stdout, MoveTo(0, 0)).unwrap(); 116 | for obj in &mut *objs { 117 | obj.update(); // shouldn't wrap with if obj.is_end() { ... } 118 | obj.paint(&mut canvas); 119 | } 120 | // canvas.print_on(&mut stdout, true).unwrap(); 121 | canvas.print(); 122 | stdout.flush().unwrap(); 123 | } 124 | let elapsed = start_time.elapsed(); 125 | if elapsed < duration { 126 | thread::sleep(duration - elapsed); 127 | } 128 | }); 129 | 130 | // deal with the key 131 | let objs = Arc::clone(&self.objs); 132 | let end = Arc::clone(&self.end); 133 | let _keyloop = thread::spawn(move || loop { 134 | if *end.lock().unwrap() { 135 | break; 136 | } 137 | if crossterm::event::poll(Duration::from_millis(300)).unwrap() { 138 | let event = crossterm::event::read().expect("can't read key"); 139 | let end_fn = || { 140 | let mut objs = objs.lock().unwrap(); 141 | let mut end = end.lock().unwrap(); 142 | for obj in &mut *objs { 143 | obj.end(); 144 | } 145 | *end = true; 146 | }; 147 | if let Event::Resize(_, _) = event { 148 | term::clear(); 149 | } 150 | if let Event::Key(key) = event { 151 | if key.code == KeyCode::Esc { 152 | end_fn(); 153 | break; 154 | } 155 | if key.code == KeyCode::Char('c') && key.modifiers == KeyModifiers::CONTROL { 156 | end_fn(); 157 | break; 158 | } 159 | } 160 | } 161 | }); 162 | 163 | // keyloop.join().unwrap(); 164 | mainloop.join().unwrap(); 165 | term::show_cursor(); 166 | disable_raw_mode().expect("can't disable raw mode"); 167 | } 168 | 169 | /// Set the fps of animation 170 | /// 171 | /// Default is 30 172 | pub fn set_fps(&mut self, fps: u32) { 173 | self.fps = fps; 174 | } 175 | 176 | /// Hide the cursor or not 177 | pub fn hide_cursor(&mut self, hide_cursor: bool) { 178 | self.hide_cursor = hide_cursor; 179 | } 180 | 181 | /// Set the size of the canvas 182 | /// 183 | /// Give a look at [Canvas::set_size](crate::Canvas::set_size) 184 | pub fn set_size(&mut self, width: T, height: T) 185 | where 186 | T: Into, 187 | { 188 | self.canvas.lock().unwrap().set_size(width, height); 189 | } 190 | 191 | /// Set the min `x` of the canvas 192 | pub fn set_minx(&mut self, minx: T) 193 | where 194 | T: Into, 195 | { 196 | self.canvas.lock().unwrap().set_minx(minx); 197 | } 198 | 199 | /// Set the max `y` of the canvas 200 | pub fn set_maxy(&mut self, maxy: T) 201 | where 202 | T: Into + Copy, 203 | { 204 | self.canvas.lock().unwrap().set_maxy(maxy); 205 | } 206 | 207 | // only used in run! 208 | #[allow(unused)] 209 | fn check_size(&self) { 210 | let size = if let Some(size) = self.size { 211 | size 212 | } else { 213 | return; 214 | }; 215 | if !is_raw_mode() { 216 | return; 217 | } 218 | let (rows, cols) = get_terminal_size(); 219 | if (rows as i32) < size.0 || (cols as i32) < size.1 { 220 | println!( 221 | "this anime need at least {}x{} terminal size, but only {}x{}", 222 | size.1, size.0, cols, rows 223 | ); 224 | } 225 | } 226 | } 227 | 228 | struct UserObj { 229 | obj: T, 230 | f: F, 231 | xy: (f64, f64), 232 | is_end: bool, 233 | } 234 | 235 | // a helper trait only for UserObj 236 | trait Update { 237 | fn update(&mut self); 238 | fn is_end(&self) -> bool; 239 | fn end(&mut self); 240 | fn paint(&self, canvas: &mut Canvas); 241 | } 242 | 243 | impl Update for UserObj 244 | where 245 | T: Paint, 246 | F: FnMut(&mut T) -> bool, 247 | { 248 | fn update(&mut self) { 249 | if self.is_end { 250 | return; 251 | } 252 | self.is_end = (self.f)(&mut self.obj); 253 | } 254 | 255 | fn is_end(&self) -> bool { 256 | self.is_end 257 | } 258 | 259 | fn end(&mut self) { 260 | self.is_end = true; 261 | } 262 | 263 | fn paint(&self, canvas: &mut Canvas) { 264 | let (x, y) = self.xy; 265 | canvas.paint(&self.obj, x, y).unwrap(); 266 | } 267 | } 268 | -------------------------------------------------------------------------------- /src/braille.rs: -------------------------------------------------------------------------------- 1 | use core::fmt; 2 | 3 | use crate::utils::round; 4 | 5 | pub const SPACE: char = '⠀'; 6 | // http://www.alanwood.net/unicode/braille_patterns.html 7 | // dots: 8 | // ,___, 9 | // |1 4| 10 | // |2 5| 11 | // |3 6| 12 | // |7 8| 13 | // ````` 14 | #[rustfmt::skip] 15 | const PIXEL_MAP: [[u32; 2]; 4] = [[0x01, 0x08], 16 | [0x02, 0x10], 17 | [0x04, 0x20], 18 | [0x40, 0x80]]; 19 | // braille unicode characters starts at 0x2800 20 | const BASE_CHAR: u32 = 0x2800; 21 | 22 | #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd)] 23 | pub struct Pixel { 24 | code: u32, 25 | } 26 | 27 | impl Pixel { 28 | pub fn new() -> Self { 29 | Self { code: 0 } 30 | } 31 | 32 | pub unsafe fn from_unchecked(code: u32) -> Self { 33 | Self { code } 34 | } 35 | } 36 | 37 | pub trait PixelOp 38 | where 39 | T: Into + Copy, 40 | { 41 | fn unset(&mut self, x: T, y: T); 42 | fn set(&mut self, x: T, y: T); 43 | fn toggle(&mut self, x: T, y: T); 44 | } 45 | 46 | impl PixelOp for Pixel 47 | where 48 | T: Into + Copy, 49 | { 50 | fn unset(&mut self, x: T, y: T) { 51 | let p = get_pixel(x, y); 52 | self.code &= !p; 53 | } 54 | 55 | fn set(&mut self, x: T, y: T) { 56 | let p = get_pixel(x, y); 57 | self.code |= p; 58 | } 59 | 60 | fn toggle(&mut self, x: T, y: T) { 61 | let p = get_pixel(x, y); 62 | if self.code & p != 0 { 63 | self.unset(x, y); 64 | } else { 65 | self.set(x, y); 66 | } 67 | } 68 | } 69 | 70 | impl fmt::Display for Pixel { 71 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 72 | write!(f, "{}", make_braille_unchecked(self.code)) 73 | } 74 | } 75 | 76 | fn get_pixel(x: T, y: T) -> u32 77 | where 78 | T: Into, 79 | { 80 | let (x, y) = (round(x), round(y)); 81 | let y = if y >= 0 { 82 | [3, 2, 1, 0][(y % 4) as usize] 83 | } else { 84 | [3, 0, 1, 2][(y % 4).unsigned_abs() as usize] 85 | }; 86 | PIXEL_MAP[y as usize][(x % 2).unsigned_abs() as usize] 87 | } 88 | 89 | // it's safety, dw :) 90 | fn make_braille_unchecked(p: u32) -> char { 91 | unsafe { char::from_u32_unchecked(BASE_CHAR + p) } 92 | } 93 | -------------------------------------------------------------------------------- /src/canvas.rs: -------------------------------------------------------------------------------- 1 | // IMPORTANT: the algorithm is fixed, be very careful of changing the code 2 | // there isn't a good way to debug 3 | 4 | use std::io::Write; 5 | use std::{cmp, collections::HashMap}; 6 | 7 | use crossterm::{cursor::MoveToNextLine, queue, style::Print}; 8 | 9 | use crate::braille; 10 | use crate::utils::get_pos; 11 | use crate::{ 12 | braille::PixelOp, 13 | term::is_raw_mode, 14 | utils::{round, RsilleErr}, 15 | }; 16 | 17 | use crate::color::{Color, Colored, ColoredChar}; 18 | 19 | /// Implement this for painting on [`Canvas`](struct.Canvas.html) 20 | pub trait Paint: Send + 'static { 21 | /// Paint the object on the canvas 22 | fn paint(&self, canvas: &mut Canvas, x: T, y: T) -> Result<(), RsilleErr> 23 | where 24 | T: Into; 25 | } 26 | 27 | // this is just for err: "Box not impl Paint" xd 28 | impl Paint for Box { 29 | fn paint(&self, canvas: &mut Canvas, x: N, y: N) -> Result<(), RsilleErr> 30 | where 31 | N: Into, 32 | { 33 | canvas.paint(self, x, y) 34 | } 35 | } 36 | 37 | /// The basic canvas 38 | /// 39 | /// Paint anything on the canvas anywhere you want. 40 | /// Don't worry about the (x, y), the size of canvas will auto increase, 41 | /// and it support the negative number. 42 | /// 43 | /// ## Example 44 | /// 45 | /// Draw the `y = 1.5*sin(x)` and `y = cos(x)` 46 | /// ``` 47 | /// use rsille::Canvas; 48 | /// let mut c = Canvas::new(); 49 | /// for x in 0..1000 { 50 | /// let x = x as f64; 51 | /// c.set(x / 10.0, x.to_radians().sin() * 15.0); 52 | /// c.set(x / 10.0, x.to_radians().cos() * 10.0); 53 | /// } 54 | /// c.print(); 55 | /// ``` 56 | /// 57 | /// ## NOTE 58 | /// 59 | /// Take a look at the [`extra`](extra/index.html) module, there are some useful things can paint on the canvas 60 | /// 61 | 62 | #[derive(Debug, Clone)] 63 | pub struct Canvas { 64 | minx: f64, // <= 0 65 | miny: f64, // <= 0 66 | width: i32, // >= 0 67 | height: i32, // >= 0 68 | pixels: HashMap<(i32, i32), Colored>, // (col, row) -> colored 69 | text: HashMap<(i32, i32), ColoredChar>, // (col, row) -> colored char 70 | } 71 | 72 | impl Canvas { 73 | /// Make a new empty canvas 74 | /// 75 | /// The size of the canvas will auto increase 76 | pub fn new() -> Self { 77 | let pixels = HashMap::new(); 78 | let text = HashMap::new(); 79 | let (width, height) = (0, 0); 80 | let (minx, miny) = (0.0, 0.0); 81 | Self { 82 | minx, 83 | miny, 84 | width, 85 | height, 86 | pixels, 87 | text, 88 | } 89 | } 90 | 91 | /// Paint those [`Paint`]() object on the location (x, y) 92 | pub fn paint(&mut self, target: &T, x: N, y: N) -> Result<(), RsilleErr> 93 | where 94 | T: Paint, 95 | N: Into, 96 | { 97 | let (x, y) = (x.into(), y.into()); 98 | target.paint(self, x, y)?; 99 | Ok(()) 100 | } 101 | 102 | /// Print the canvas to the terminal 103 | /// 104 | /// If you want to print the canvas to a buffer, use the [`print_on`](struct.Canvas.html#method.print_on) 105 | pub fn print(&self) { 106 | let is_raw = is_raw_mode(); 107 | let mut stdout = std::io::stdout(); 108 | self.print_on(&mut stdout, is_raw).unwrap(); 109 | } 110 | 111 | /// Print the canvas to the buffer 112 | /// 113 | /// If you want to print the canvas to the terminal, use the [`print`](struct.Canvas.html#method.print) 114 | pub fn print_on(&self, w: &mut W, is_raw: bool) -> Result<(), RsilleErr> 115 | where 116 | W: Write, 117 | { 118 | self.print_impl(w, is_raw).map_err(RsilleErr::to_rsille_err) 119 | } 120 | 121 | fn print_impl(&self, w: &mut W, is_raw: bool) -> std::io::Result<()> 122 | where 123 | W: Write, 124 | { 125 | let (start_col, start_row) = get_pos(self.minx, self.miny); 126 | for row in (start_row..self.height).rev() { 127 | for col in start_col..self.width { 128 | if let Some(text) = self.text.get(&(col, row)) { 129 | text.queue(w)?; 130 | continue; 131 | } 132 | if let Some(pixel) = self.pixels.get(&(col, row)) { 133 | pixel.queue(w)?; 134 | } else { 135 | queue!(w, Print(braille::SPACE))?; 136 | } 137 | } 138 | if is_raw { 139 | queue!(w, MoveToNextLine(1))?; 140 | } else { 141 | queue!(w, Print("\n"))?; 142 | } 143 | } 144 | w.flush()?; 145 | Ok(()) 146 | } 147 | 148 | /// Clear the canvas 149 | /// 150 | /// This method only clear those dots on the canvas, the size of the canvas will not change 151 | /// If you want to clear the size too, use the [`reset`](struct.Canvas.html#method.reset) 152 | pub fn clear(&mut self) { 153 | self.pixels = HashMap::new(); 154 | } 155 | 156 | /// Reset the canvas to a new empty canvas 157 | pub fn reset(&mut self) { 158 | self.minx = 0.0; 159 | self.miny = 0.0; 160 | self.width = 0; 161 | self.height = 0; 162 | self.pixels = HashMap::new(); 163 | } 164 | 165 | /// Set the size of the canvas 166 | /// 167 | /// This method can't fix the size of the canvas, it's just set the canvas size. 168 | /// When the size isn't enough, the canvas will auto increase. 169 | /// And the (width, height) isn't the size of the terminal, it's the size of the canvas! 170 | /// For example, an object `x` from -30 to 30, then it's 60 in width. 171 | /// On the terminal, it's 30 in width(because braille code), but you should set the width to 60 not 30. 172 | pub fn set_size(&mut self, width: T, height: T) 173 | where 174 | T: Into, 175 | { 176 | // start_col, start_row < 0 177 | let (max_col, max_row) = get_pos(width.into(), height.into()); 178 | let (start_col, start_row) = get_pos(self.minx, self.miny); 179 | if max_col > self.width - start_col { 180 | self.width = max_col + start_col; 181 | } 182 | if max_row > self.height - start_row { 183 | self.height = max_row + start_row; 184 | } 185 | } 186 | 187 | /// Set the min `x` of th canvas 188 | /// 189 | /// In most time, no need to call this, only when the animation is moved when running 190 | pub fn set_minx(&mut self, minx: T) 191 | where 192 | T: Into, 193 | { 194 | let minx = minx.into(); 195 | if minx < self.minx { 196 | self.minx = minx; 197 | } 198 | } 199 | 200 | /// Set the max `y` of the canvas 201 | /// 202 | /// In most time, no need to call this, only when the animation is moved when running 203 | pub fn set_maxy(&mut self, maxy: T) 204 | where 205 | T: Into + Copy, 206 | { 207 | let maxy = maxy.into(); 208 | let (_, max_row) = get_pos(0.0, maxy); 209 | if max_row > self.height { 210 | self.height = max_row; 211 | } 212 | } 213 | 214 | /// Draw a dot on (x, y) 215 | /// 216 | /// Just use the (x, y) in your object, the algorithm will find the right location 217 | pub fn set(&mut self, x: T, y: T) 218 | where 219 | T: Into + Copy, 220 | { 221 | self.set_at(x, y, None); 222 | } 223 | 224 | /// Similar to [`set`](struct.Canvas.html#method.set) 225 | /// 226 | /// But it's support color 227 | pub fn set_colorful(&mut self, x: T, y: T, color: Color) 228 | where 229 | T: Into + Copy, 230 | { 231 | self.set_at(x, y, Some(color)); 232 | } 233 | 234 | /// If the (x, y) is already set, then unset it 235 | /// 236 | /// If the (x, y) is unset, then set it 237 | pub fn toggle(&mut self, x: T, y: T) 238 | where 239 | T: Into + Copy, 240 | { 241 | self.toggle_at(x, y); 242 | } 243 | 244 | /// Draw a line on the canvas 245 | /// * `xy1` - the start location 246 | /// * `xy2` - the end location 247 | pub fn line(&mut self, xy1: (T, T), xy2: (T, T)) 248 | where 249 | T: Into, 250 | { 251 | let (x1, y1) = (round(xy1.0), round(xy1.1)); 252 | let (x2, y2) = (round(xy2.0), round(xy2.1)); 253 | let d = |v1, v2| { 254 | if v1 <= v2 { 255 | (v2 - v1, 1.0) 256 | } else { 257 | (v1 - v2, -1.0) 258 | } 259 | }; 260 | 261 | let (xdiff, xdir) = d(x1, x2); 262 | let (ydiff, ydif) = d(y1, y2); 263 | let r = cmp::max(xdiff, ydiff); 264 | 265 | for i in 0..=r { 266 | let r = r as f64; 267 | let i = i as f64; 268 | let (xd, yd) = (xdiff as f64, ydiff as f64); 269 | let x = x1 as f64 + i * xd / r * xdir; 270 | let y = y1 as f64 + i * yd / r * ydif; 271 | self.set(x, y); 272 | } 273 | } 274 | 275 | /// Draw a line on the canvas 276 | /// * `xy1` - the start location 277 | /// * `xy2` - the end location 278 | /// * `c` - the char used in line 279 | /// * `color` - optional, the color 280 | /// 281 | /// It can draw any character on canvas, 282 | /// and when there are both a braille code and any char on *(x, y)*, 283 | /// the character will cover the braille code! 284 | pub fn line_any(&mut self, xy1: (T, T), xy2: (T, T), c: char, color: Option) 285 | where 286 | T: Into, 287 | { 288 | let (x1, y1) = (round(xy1.0), round(xy1.1)); 289 | let (x2, y2) = (round(xy2.0), round(xy2.1)); 290 | let d = |v1, v2| { 291 | if v1 <= v2 { 292 | (v2 - v1, 1.0) 293 | } else { 294 | (v1 - v2, -1.0) 295 | } 296 | }; 297 | 298 | let (xdiff, xdir) = d(x1, x2); 299 | let (ydiff, ydif) = d(y1, y2); 300 | let r = cmp::max(xdiff, ydiff); 301 | 302 | for i in 0..=r { 303 | let r = r as f64; 304 | let i = i as f64; 305 | let (xd, yd) = (xdiff as f64, ydiff as f64); 306 | let x = x1 as f64 + i * xd / r * xdir; 307 | let y = y1 as f64 + i * yd / r * ydif; 308 | self.put(x, y, c, color); 309 | } 310 | } 311 | 312 | /// Draw a line on the canvas with the color 313 | /// * `xy1` - the start location 314 | /// * `xy2` - the end location 315 | pub fn line_colorful(&mut self, xy1: (T, T), xy2: (T, T), color: Color) 316 | where 317 | T: Into + Copy, 318 | { 319 | let (x1, y1) = (round(xy1.0), round(xy1.1)); 320 | let (x2, y2) = (round(xy2.0), round(xy2.1)); 321 | let d = |v1, v2| { 322 | if v1 <= v2 { 323 | (v2 - v1, 1.0) 324 | } else { 325 | (v1 - v2, -1.0) 326 | } 327 | }; 328 | 329 | let (xdiff, xdir) = d(x1, x2); 330 | let (ydiff, ydif) = d(y1, y2); 331 | let r = cmp::max(xdiff, ydiff); 332 | 333 | for i in 0..=r { 334 | let r = r as f64; 335 | let i = i as f64; 336 | let (xd, yd) = (xdiff as f64, ydiff as f64); 337 | let x = x1 as f64 + i * xd / r * xdir; 338 | let y = y1 as f64 + i * yd / r * ydif; 339 | self.set_colorful(x, y, color); 340 | } 341 | } 342 | 343 | /// Put text on canvas 344 | /// 345 | /// It can draw any character on canvas, 346 | /// and when there are both a braille code and any char on *(x, y)*, 347 | /// the character will cover the braille code! 348 | pub fn put_text(&mut self, x: T, y: T, text: &str, color: Option) 349 | where 350 | T: Into, 351 | { 352 | let (col, row) = self.get_pos(x, y); 353 | if let Some(color) = color { 354 | for (i, c) in text.chars().enumerate() { 355 | let mut c = ColoredChar::new(c); 356 | c.set_foregound_color(color); 357 | self.text.insert((col + i as i32, row), c); 358 | } 359 | } else { 360 | for (i, c) in text.chars().enumerate() { 361 | self.text.insert((col + i as i32, row), ColoredChar::new(c)); 362 | } 363 | } 364 | } 365 | 366 | /// Put char on canvas 367 | /// 368 | /// It can draw any character on canvas, 369 | /// and when there are both a braille code and any char on *(x, y)*, 370 | /// the character will cover the braille code! 371 | pub fn put(&mut self, x: T, y: T, c: char, color: Option) 372 | where 373 | T: Into, 374 | { 375 | let (col, row) = self.get_pos(x, y); 376 | let c = if let Some(color) = color { 377 | let mut c = ColoredChar::new(c); 378 | c.set_foregound_color(color); 379 | c 380 | } else { 381 | ColoredChar::new(c) 382 | }; 383 | self.text.insert((col, row), c); 384 | } 385 | 386 | fn set_at(&mut self, x: T, y: T, color: Option) 387 | where 388 | T: Into + Copy, 389 | { 390 | let (col, row) = self.get_pos(x, y); 391 | if let Some(pixel) = self.pixels.get_mut(&(col, row)) { 392 | pixel.set(x, y); 393 | } else { 394 | self.pixels.insert((col, row), Colored::new()); 395 | self.pixels.get_mut(&(col, row)).unwrap().set(x, y); 396 | } 397 | if let Some(color) = color { 398 | self.pixels 399 | .get_mut(&(col, row)) 400 | .unwrap() 401 | .set_foregound_color(color); 402 | } 403 | } 404 | 405 | fn toggle_at(&mut self, x: T, y: T) 406 | where 407 | T: Into + Copy, 408 | { 409 | let (col, row) = self.get_pos(x, y); 410 | if let Some(pixel) = self.pixels.get_mut(&(col, row)) { 411 | pixel.toggle(x, y); 412 | } else { 413 | self.pixels.insert((col, row), Colored::new()); 414 | self.pixels.get_mut(&(col, row)).unwrap().toggle(x, y); 415 | } 416 | } 417 | 418 | fn get_pos(&mut self, x: T, y: T) -> (i32, i32) 419 | where 420 | T: Into, 421 | { 422 | let (x, y) = (x.into(), y.into()); 423 | if x < self.minx { 424 | self.minx = x; 425 | } 426 | if y < self.miny { 427 | self.miny = y; 428 | } 429 | let (col, row) = get_pos(x, y); 430 | if row >= self.height { 431 | self.height = row.abs() + 1; 432 | } 433 | if col >= self.width { 434 | self.width = col.abs() + 1; 435 | } 436 | (col, row) 437 | } 438 | } 439 | -------------------------------------------------------------------------------- /src/color.rs: -------------------------------------------------------------------------------- 1 | //! Colors in Terminal 2 | //! 3 | //! ## Example 4 | //! 5 | //! draw a red star 6 | //! ``` 7 | //! use rsille::{color::Color, extra::Turtle, Canvas}; 8 | //! let mut c = Canvas::new(); 9 | //! let mut t = Turtle::new(); 10 | //! t.color(Color::Red); 11 | //! for _ in 0..5 { 12 | //! t.forward(30.0); 13 | //! t.right(144.0); 14 | //! } 15 | //! c.paint(&t, 0.0, 15.0).unwrap(); 16 | //! c.print(); 17 | //! ``` 18 | 19 | use std::io; 20 | 21 | use crate::braille::{Pixel, PixelOp}; 22 | 23 | pub use crossterm::style::Color; 24 | use crossterm::{ 25 | queue, 26 | style::{Colors, Print, ResetColor, SetColors}, 27 | }; 28 | 29 | #[derive(Debug, Clone, Copy)] 30 | pub(crate) struct Colored { 31 | pixel: Pixel, 32 | color: Colors, 33 | } 34 | 35 | #[allow(unused)] 36 | impl Colored { 37 | pub(crate) fn new() -> Self { 38 | Self { 39 | pixel: Pixel::new(), 40 | color: Colors { 41 | foreground: None, 42 | background: None, 43 | }, 44 | } 45 | } 46 | 47 | pub(crate) unsafe fn from_unchecked(pixel: u32) -> Self { 48 | Self { 49 | pixel: Pixel::from_unchecked(pixel), 50 | color: Colors { 51 | foreground: None, 52 | background: None, 53 | }, 54 | } 55 | } 56 | 57 | pub(crate) fn set_foregound_color(&mut self, color: Color) { 58 | self.color.foreground = Some(color); 59 | } 60 | 61 | pub(crate) fn set_background_color(&mut self, color: Color) { 62 | self.color.background = Some(color); 63 | } 64 | 65 | pub(crate) fn queue(&self, buffer: &mut impl io::Write) -> io::Result<()> { 66 | if self.color.foreground.is_none() && self.color.background.is_none() { 67 | queue!(buffer, Print(format!("{}", self.pixel)),) 68 | } else { 69 | queue!( 70 | buffer, 71 | SetColors(self.color), 72 | Print(format!("{}", self.pixel)), 73 | ResetColor 74 | ) 75 | } 76 | } 77 | } 78 | 79 | impl PixelOp for Colored 80 | where 81 | T: Into + Copy, 82 | { 83 | fn set(&mut self, x: T, y: T) { 84 | self.pixel.set(x, y); 85 | } 86 | 87 | fn unset(&mut self, x: T, y: T) { 88 | self.pixel.unset(x, y); 89 | } 90 | 91 | fn toggle(&mut self, x: T, y: T) { 92 | self.pixel.toggle(x, y); 93 | } 94 | } 95 | 96 | #[derive(Debug, Clone, Copy)] 97 | pub(crate) struct ColoredChar { 98 | c: char, 99 | color: Colors, 100 | } 101 | 102 | #[allow(unused)] 103 | impl ColoredChar { 104 | pub(crate) fn new(c: char) -> Self { 105 | Self { 106 | c, 107 | color: Colors { 108 | foreground: None, 109 | background: None, 110 | }, 111 | } 112 | } 113 | 114 | pub(crate) fn set_foregound_color(&mut self, color: Color) { 115 | self.color.foreground = Some(color); 116 | } 117 | 118 | pub(crate) fn set_background_color(&mut self, color: Color) { 119 | self.color.background = Some(color); 120 | } 121 | 122 | pub(crate) fn queue(&self, buffer: &mut impl io::Write) -> io::Result<()> { 123 | if self.color.foreground.is_none() && self.color.background.is_none() { 124 | queue!(buffer, Print(format!("{}", self.c)),) 125 | } else { 126 | queue!( 127 | buffer, 128 | SetColors(self.color), 129 | Print(format!("{}", self.c)), 130 | ResetColor 131 | ) 132 | } 133 | } 134 | } 135 | -------------------------------------------------------------------------------- /src/decor.rs: -------------------------------------------------------------------------------- 1 | use crate::Canvas; 2 | 3 | /// The box outside the object 4 | /// 5 | /// It include many `char` for making a **box**. 6 | /// But it *not* include the functions for making box! 7 | /// 8 | /// As shown in the figure below, the abbreviation is: 9 | /// `l`: left, `r`: right, `t`: top, `b`: bottom, `e`: edge, `c`: cross 10 | /// 11 | /// ```text 12 | /// lt te tc rt 13 | /// ┌────────┰────────┓ lt: ┌ rt: ┓ 14 | /// │ ┆ ┃ lb: ╰ rb: ╝ 15 | /// │ ┆sv ┃ 16 | /// │ ┆ ┃ te: ─ re: ┃ sv: ┆ 17 | /// lc├╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┦rc ┄┄┄┄┄> be: ═ le: │ sh: ╌ 18 | /// │ cross┆ sh ┃ 19 | /// le│ ┆ ┃re tc: ┰ rc: ┦ 20 | /// │ ┆ ┃ bc: ┸ lc: ├ cross: ┼ 21 | /// ╰════════┸════════╝ 22 | /// lb bc be rb 23 | /// ``` 24 | /// 25 | /// In general, you don't need to manually set up your own. 26 | /// Some useful constructor methods can directly generate specific styles of borders. 27 | pub struct Decor { 28 | /// left top corner 29 | pub lt: char, 30 | /// right top corner 31 | pub rt: char, 32 | /// right top corner 33 | pub rb: char, 34 | /// left bottom corner 35 | pub lb: char, 36 | 37 | /// top edge 38 | pub te: char, 39 | /// right edge 40 | pub re: char, 41 | /// bottom edge 42 | pub be: char, 43 | /// left edge 44 | pub le: char, 45 | 46 | /// top cross 47 | pub tc: char, 48 | /// right cross 49 | pub rc: char, 50 | /// bottom cross 51 | pub bc: char, 52 | /// left cross 53 | pub lc: char, 54 | /// the cross 55 | pub cross: char, 56 | 57 | /// horizontal segmetation 58 | pub sh: char, 59 | /// vertical segmetation 60 | pub sv: char, 61 | } 62 | 63 | impl Decor { 64 | /// Return those corner char 65 | pub fn get_corner(&self) -> (char, char, char, char) { 66 | (self.lt, self.rt, self.rb, self.lb) 67 | } 68 | 69 | /// Return those edge char 70 | pub fn get_edge(&self) -> (char, char, char, char) { 71 | (self.te, self.le, self.be, self.re) 72 | } 73 | 74 | /// Return those cross char 75 | pub fn get_cross(&self) -> (char, char, char, char, char) { 76 | (self.tc, self.rc, self.bc, self.lc, self.cross) 77 | } 78 | } 79 | 80 | #[rustfmt::skip] 81 | impl Decor { 82 | /// The regular style 83 | /// 84 | /// ```text 85 | /// ┌───┬───┐ 86 | /// │ │ │ 87 | /// ├───┼───┤ 88 | /// │ │ │ 89 | /// └───┴───┘ 90 | /// ``` 91 | pub fn simple() -> Self { 92 | Decor { 93 | lt: '┌', rt: '┐', 94 | lb: '└', rb: '┘', 95 | 96 | te: '─', 97 | le: '│', re: '│', 98 | be: '─', 99 | 100 | tc: '┬', 101 | lc: '├', cross: '┼', rc: '┤', 102 | bc: '┴', 103 | 104 | sh: '─', sv: '│', 105 | } 106 | } 107 | 108 | /// The bold style 109 | /// 110 | /// ```text 111 | /// ┏━━━┳━━━┓ 112 | /// ┃ ┃ ┃ 113 | /// ┣━━━╋━━━┫ 114 | /// ┃ ┃ ┃ 115 | /// ┗━━━┻━━━┛ 116 | /// ``` 117 | pub fn bold() -> Self { 118 | Decor { 119 | lt: '┏', rt: '┓', 120 | lb: '┗', rb: '┛', 121 | 122 | te: '━', 123 | le: '┃', re: '┃', 124 | be: '━', 125 | 126 | tc: '┳', 127 | lc: '┣', cross: '╋', rc: '┫', 128 | bc: '┻', 129 | 130 | sh: '━', sv: '┃', 131 | } 132 | } 133 | 134 | /// The style for plot 135 | /// 136 | /// ```text 137 | /// ╭═══╤═══╗ 138 | /// │ ╎ ║ 139 | /// ├┄┄┄┼┄┄┄╢ 140 | /// │ ╎ ║ 141 | /// ╰───┴───╜ 142 | /// ``` 143 | pub fn plot() -> Self { 144 | Decor { 145 | lt: '╭', rt: '╗', 146 | lb: '╰', rb: '╜', 147 | 148 | te: '═', 149 | le: '│', re: '║', 150 | be: '─', 151 | 152 | tc: '╤', 153 | lc: '├', cross: '┼', rc: '╢', 154 | bc: '┴', 155 | 156 | sh: '┄', sv: '╎', 157 | } 158 | } 159 | } 160 | 161 | pub(crate) fn draw_box(canvas: &mut Canvas, start: (f64, f64), end: (f64, f64), decor: &Decor) { 162 | let (lt, rt, rb, lb) = decor.get_corner(); 163 | let (te, le, be, re) = decor.get_edge(); 164 | let (start_x, start_y) = start; 165 | let (end_x, end_y) = end; 166 | canvas.line_any((start_x, start_y), (end_x, start_y), be, None); 167 | canvas.line_any((start_x, start_y), (start_x, end_y), le, None); 168 | canvas.line_any((end_x, end_y), (start_x, end_y), te, None); 169 | canvas.line_any((end_x, end_y), (end_x, start_y), re, None); 170 | canvas.put(start_x, start_y, lb, None); 171 | canvas.put(start_x, end_y, lt, None); 172 | canvas.put(end_x, start_y, rb, None); 173 | canvas.put(end_x, end_y, rt, None); 174 | } 175 | -------------------------------------------------------------------------------- /src/defaults.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | extra::{math::Figure, LifeGame, Object3D, Turtle}, 3 | Animation, Canvas, 4 | }; 5 | 6 | impl Default for Canvas { 7 | fn default() -> Self { 8 | Self::new() 9 | } 10 | } 11 | 12 | impl Default for Object3D { 13 | fn default() -> Self { 14 | Self::new() 15 | } 16 | } 17 | 18 | impl Default for Turtle { 19 | fn default() -> Self { 20 | Self::new() 21 | } 22 | } 23 | 24 | impl Default for LifeGame { 25 | fn default() -> Self { 26 | Self::new() 27 | } 28 | } 29 | 30 | impl Default for Animation { 31 | fn default() -> Self { 32 | Self::new() 33 | } 34 | } 35 | 36 | impl Default for Figure { 37 | fn default() -> Self { 38 | Self::new() 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/extra/imgille.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | term::get_terminal_size, 3 | utils::{get_pos, RsilleErr}, 4 | Paint, 5 | }; 6 | 7 | use image::{ 8 | imageops::FilterType::Lanczos3, io::Reader as ImageReader, DynamicImage, GenericImageView, 9 | }; 10 | 11 | /// Paint the image on canvas 12 | /// 13 | /// ## Example 14 | /// 15 | /// paint the image 16 | /// ```no_run 17 | /// use rsille::{extra::Imgille, Canvas}; 18 | /// let path = "path/to/image"; 19 | /// let mut canvas = Canvas::new(); 20 | /// let imgille = Imgille::new(path).unwrap(); 21 | /// canvas.paint(&imgille, 0, 0).unwrap(); 22 | /// canvas.print(); 23 | /// ``` 24 | /// 25 | /// ## NOTE 26 | /// 27 | /// You can always paint on *(0, 0)*. 28 | /// But if you want, you can move it to other place on the canvas. 29 | /// 30 | /// If your image isn't colorful (like grayscale image), you better set the color to `false`. 31 | /// And give a look at [`thresholds`](#method.thresholds), [`invert`](#method.invert). 32 | 33 | #[derive(Debug, Clone)] 34 | pub struct Imgille { 35 | img: DynamicImage, 36 | color: bool, 37 | thresholds: u8, 38 | invert: bool, 39 | } 40 | 41 | impl Imgille { 42 | /// Construct a new object contains the picture 43 | /// 44 | /// Return `err` when can't open the image or can't decode the image 45 | pub fn new(path: &str) -> Result { 46 | let err = Err(RsilleErr::new(format!("can't open image: {}", path))); 47 | let img = if let Ok(reader) = ImageReader::open(path) { 48 | if let Ok(img) = reader.decode() { 49 | img 50 | } else { 51 | return err; 52 | } 53 | } else { 54 | return err; 55 | }; 56 | Ok(Self { 57 | img, 58 | color: true, 59 | thresholds: 128, 60 | invert: false, 61 | }) 62 | } 63 | 64 | /// Open a image file 65 | pub fn open(&mut self, path: &str) -> Result<(), RsilleErr> { 66 | let img = ImageReader::open(path) 67 | .map_err(RsilleErr::to_rsille_err)? 68 | .decode() 69 | .map_err(RsilleErr::to_rsille_err)?; 70 | self.img = img; 71 | Ok(()) 72 | } 73 | 74 | /// Set if the image should be painted with color or not 75 | /// 76 | /// The default is `true`, but for grayscale image, you better set it to `false` 77 | pub fn color(&mut self, color: bool) { 78 | self.color = color; 79 | } 80 | 81 | /// Set the thresholds for the image 82 | /// 83 | /// It's only for the grayscale image, and when the color is `false`. 84 | /// The default is *128*, but you can set it to other value 85 | pub fn thresholds(&mut self, thresholds: u8) { 86 | self.thresholds = thresholds; 87 | } 88 | 89 | /// If if invert the color of the image 90 | /// 91 | /// black to white, white to black. 92 | /// It's only for the grayscale image, and when the color is `false`. 93 | pub fn invert(&mut self, invert: bool) { 94 | self.invert = invert; 95 | } 96 | } 97 | 98 | impl Paint for Imgille { 99 | fn paint(&self, canvas: &mut crate::Canvas, x: T, y: T) -> Result<(), RsilleErr> 100 | where 101 | T: Into, 102 | { 103 | // some example for resize the image (terminal size is 80*24): 104 | // 800*240 -> 160*48 (fit the width) 105 | // 800*120 -> 160*48 (fit the width) 106 | // 800*480 -> 160*96 (fit the height) 107 | // 800*960 -> 80*96 (fit the height) 108 | // img_height height 109 | // ------------ = ---------- and height = 4 * rest_height, width = 2 * rest_width 110 | // img_width width 111 | // 112 | // important: never optimize with the fill, it's really hard to use and the algo is really complex 113 | // can't stand use the fill anymore, even it would be musch faster 114 | 115 | // calculate the rest size of the terminal 116 | let (x, y) = (x.into(), y.into()); 117 | let (rest_width, rest_height) = get_rest_size(x, y); 118 | let (img_width, img_height) = (self.img.width(), self.img.height()); 119 | 120 | // check the image is bigger than the terminal or not 121 | let img = if img_width > rest_width * 2 || img_height > rest_height * 4 { 122 | // first, try to resize the image to fit the height of the terminal 123 | let f = rest_height as f64 * 4.0 / img_height as f64; 124 | let w = (img_width as f64 * f) as u32; 125 | if rest_width * 2 < w { 126 | // if the width of the image is still bigger than the terminal width 127 | // then resize the image to fit the width of the terminal 128 | let f = rest_width as f64 * 2.0 / img_width as f64; 129 | let h = (img_height as f64 * f) as u32; 130 | self.img.resize_exact(rest_width * 2, h, Lanczos3) 131 | } else { 132 | self.img.resize_exact(w, rest_height * 4, Lanczos3) 133 | } 134 | } else { 135 | // the image is so small, no need to resize it 136 | self.img.clone() 137 | }; 138 | 139 | if !self.color { 140 | // no color 141 | let img = img.grayscale(); 142 | let img = img.as_luma8().unwrap(); 143 | let (iw, ih) = (img.width(), img.height()); 144 | for ny in 0..ih { 145 | for nx in 0..iw { 146 | let pixel = img.get_pixel(nx, ny); 147 | let if_draw = if self.invert { 148 | (pixel.0)[0] > self.thresholds 149 | } else { 150 | (pixel.0)[0] < self.thresholds 151 | }; 152 | if if_draw { 153 | canvas.set(x + nx as f64, y + (ih - ny) as f64); 154 | } 155 | } 156 | } 157 | } else { 158 | // color 159 | let (iw, ih) = (img.width(), img.height()); 160 | for ny in 0..ih { 161 | for nx in 0..iw { 162 | use crate::color::Color; 163 | let pixel = img.get_pixel(nx, ny); 164 | canvas.set_colorful( 165 | x + nx as f64, 166 | y + (ih - ny) as f64, 167 | Color::Rgb { 168 | r: pixel[0], 169 | g: pixel[1], 170 | b: pixel[2], 171 | }, 172 | ); 173 | } 174 | } 175 | }; 176 | Ok(()) 177 | } 178 | } 179 | 180 | fn get_rest_size(x: T, y: T) -> (u32, u32) 181 | where 182 | T: Into, 183 | { 184 | // calculate the rest size of the terminal 185 | let (tw, th) = get_terminal_size(); 186 | let (start_col, start_row) = get_pos(x, y); 187 | let rest_width = if start_col > 0 { 188 | tw as u32 - start_col as u32 189 | } else { 190 | tw as u32 191 | }; 192 | let rest_height = if start_row > 0 { 193 | th as u32 - start_row as u32 194 | } else { 195 | th as u32 196 | }; 197 | (rest_width, rest_height) 198 | } 199 | -------------------------------------------------------------------------------- /src/extra/lifegame.rs: -------------------------------------------------------------------------------- 1 | use std::{collections::HashMap, fs, iter::Peekable}; 2 | 3 | use crate::{utils::RsilleErr, Paint}; 4 | 5 | // same as HashSet<(isize, isize)> 6 | // but when using inplace algorithms, just simply change () to bool or u8 7 | type LiveCells = HashMap<(isize, isize), ()>; 8 | 9 | /// The life game 10 | /// 11 | /// It support `rle` file download from the internet 12 | /// 13 | /// ## Example 14 | /// 15 | /// ```no_run 16 | /// use rsille::{extra::LifeGame, Animation}; 17 | /// let lg = LifeGame::from_path("path/to/rle").unwrap(); 18 | /// let mut anime = Animation::new(); 19 | /// anime.push(lg, |lg| lg.update(), (0, 0)); 20 | /// anime.run(); 21 | /// ``` 22 | #[derive(Debug, Clone)] 23 | pub struct LifeGame { 24 | cells: LiveCells, 25 | // boundless: bool, 26 | } 27 | 28 | impl LifeGame { 29 | /// Return a new life game 30 | pub fn new() -> Self { 31 | Self { 32 | cells: Default::default(), 33 | // boundless: true, 34 | } 35 | } 36 | 37 | /// Read the `rle` format string and build a life game from it 38 | /// 39 | /// Return `err` when can't parse the rle string 40 | pub fn from(rle: &str) -> Result { 41 | parse(rle) 42 | } 43 | 44 | /// Read the `rle` file and build a life game from it 45 | /// 46 | /// Return `err` when can't parse the rle file or can't open file 47 | pub fn from_path(path: &str) -> Result { 48 | let Ok(rle) = fs::read_to_string(path) else { 49 | return Err(RsilleErr::new(format!("can't open rle file: {}", path))); 50 | }; 51 | Self::from(&rle) 52 | } 53 | 54 | /// The next moment of the cells 55 | pub fn update(&mut self) -> bool { 56 | let mut next = self.cells.clone(); 57 | for cell in self.cells.keys() { 58 | let (x, y) = *cell; 59 | for dy in -1..=1 { 60 | for dx in -1..=1 { 61 | let neibor = self.count_neighbors(x + dx, y + dy); 62 | if neibor == 3 { 63 | next.insert((x + dx, y + dy), ()); 64 | } 65 | 66 | if !(2..4).contains(&neibor) { 67 | next.remove(&(x + dx, y + dy)); 68 | } 69 | } 70 | } 71 | } 72 | self.cells = next; 73 | 74 | false 75 | } 76 | 77 | fn count_neighbors(&self, x: isize, y: isize) -> usize { 78 | let mut count = 0; 79 | for dy in -1..=1 { 80 | for dx in -1..=1 { 81 | // the cell itself 82 | if dy == 0 && dx == 0 { 83 | continue; 84 | } 85 | let neighbor_x = x + dx; 86 | let neighbor_y = y + dy; 87 | if self.cells.contains_key(&(neighbor_x, neighbor_y)) { 88 | count += 1; 89 | } 90 | } 91 | } 92 | count 93 | } 94 | } 95 | 96 | impl Paint for LifeGame { 97 | fn paint(&self, canvas: &mut crate::Canvas, x: T, y: T) -> Result<(), RsilleErr> 98 | where 99 | T: Into, 100 | { 101 | let (x, y) = (x.into(), y.into()); 102 | for cell in self.cells.keys() { 103 | canvas.set(x + cell.0 as f64, y + cell.1 as f64); 104 | } 105 | Ok(()) 106 | } 107 | } 108 | 109 | fn parse(rle: &str) -> Result { 110 | let mut lines = rle.lines().peekable(); 111 | let mut cells = HashMap::new(); 112 | // read the head 113 | // let (width, height) = read_head(&mut lines)?; 114 | let _ = read_head(&mut lines)?; 115 | // parse 116 | let (mut x, mut y) = (0_isize, 0_isize); 117 | let mut count = 0_isize; 118 | for line in lines { 119 | let mut chars = line.chars().peekable(); 120 | loop { 121 | let Some(c) = chars.next() else { 122 | break; 123 | }; 124 | match c { 125 | 'b' => { 126 | for _ in 0..=count { 127 | x += 1; 128 | } 129 | count = 0; 130 | } 131 | 'o' => { 132 | for _ in 0..=count { 133 | cells.insert((x, y), ()); 134 | x += 1; 135 | } 136 | count = 0; 137 | } 138 | '$' => { 139 | y += 1; 140 | x = 0; 141 | } 142 | '!' => break, 143 | '1'..='9' => { 144 | let mut count_str = String::new(); 145 | count_str.push(c); 146 | while let Some(digit) = chars.peek() { 147 | if digit.is_ascii_digit() { 148 | let digit = chars.next().unwrap(); 149 | count_str.push(digit); 150 | } else { 151 | break; 152 | } 153 | } 154 | count = count_str.parse().unwrap(); 155 | count -= 1; 156 | } 157 | _ => (), 158 | } 159 | } 160 | } 161 | Ok(LifeGame { 162 | cells, 163 | // boundless: false, 164 | }) 165 | } 166 | 167 | fn read_head<'a, I>(lines: &mut Peekable) -> Result<(usize, usize), RsilleErr> 168 | where 169 | I: Iterator, 170 | { 171 | while let Some(line) = lines.peek() { 172 | if line.starts_with('#') { 173 | lines.next(); 174 | continue; 175 | } 176 | if line.starts_with('x') { 177 | let s: Vec<&str> = line.split(',').collect(); 178 | let width = (s[0].split('=').collect::>())[1] 179 | .trim() 180 | .parse() 181 | .unwrap(); 182 | let height = (s[1].split('=').collect::>())[1] 183 | .trim() 184 | .parse() 185 | .unwrap(); 186 | return Ok((width, height)); 187 | } 188 | if line.is_empty() { 189 | lines.next(); 190 | continue; 191 | } 192 | } 193 | Err(RsilleErr::new("can't parse width or height".to_string())) 194 | } 195 | -------------------------------------------------------------------------------- /src/extra/math/figure.rs: -------------------------------------------------------------------------------- 1 | use std::iter::zip; 2 | 3 | use crate::{ 4 | decor::{draw_box, Decor}, 5 | utils::MIN_DIFFERENCE, 6 | Canvas, Paint, 7 | }; 8 | 9 | /// A help macro for plot functions easy 10 | /// 11 | /// example: 12 | /// ``` 13 | /// use rsille::figure; 14 | /// figure!((|x| x.sin(), (0, 5)), (|x| x.cos(), (0, 5))); 15 | /// ``` 16 | #[macro_export] 17 | macro_rules! figure { 18 | ($(($f: expr, ($start:expr, $end:expr))),*) => { 19 | use rsille::{extra::math::{Figure, Plot}, Canvas}; 20 | let mut canvas = Canvas::new(); 21 | let mut fig = Figure::new(); 22 | $( 23 | let f: fn(f64) -> f64 = $f; 24 | let plot = Plot::new($f, ($start, $end)); 25 | fig.plot(&plot); 26 | )* 27 | canvas.paint(&fig, 0, 0).unwrap(); 28 | canvas.print(); 29 | }; 30 | } 31 | 32 | /// Impl this for plot on [`Figure`](struct.Figure.html) 33 | pub trait Plotable { 34 | /// Plot on the figure 35 | fn plot(&self) -> (Vec, Vec); 36 | } 37 | 38 | /// The figure 39 | /// 40 | /// It's a figure can paint many functions on it. 41 | /// All the math function all automatically draw on the right place. 42 | /// 43 | /// ## Example 44 | /// 45 | /// Draw the `y=sin(x)` and `y=cos(x)` 46 | /// ``` 47 | /// use rsille::{extra::math::{Figure, Plot}, Canvas}; 48 | /// 49 | /// let mut canvas = Canvas::new(); 50 | /// let mut figure = Figure::new(); 51 | /// let p1 = Plot::new(|x| x.sin(), (0, 10)); 52 | /// let p2 = Plot::new(|x| x.cos(), (0, 10)); 53 | /// figure.plot(&p1); 54 | /// figure.plot(&p2); 55 | /// canvas.paint(&figure, 0, 0).unwrap(); 56 | /// canvas.print(); 57 | /// ``` 58 | pub struct Figure { 59 | xs: Vec, 60 | ys: Vec, 61 | scale: (f64, f64), 62 | show_axis: bool, 63 | boxed: bool, 64 | padding: f64, 65 | decor: Decor, 66 | } 67 | 68 | impl Figure { 69 | /// Make a new figure 70 | pub fn new() -> Self { 71 | Self { 72 | xs: Vec::new(), 73 | ys: Vec::new(), 74 | scale: (12.0, 12.0), 75 | show_axis: true, 76 | boxed: true, 77 | padding: 10.0, 78 | decor: Decor::plot(), 79 | } 80 | } 81 | 82 | /// Plot a thing 83 | /// 84 | /// It can plot something impl [`Plotable`](trait.Plotable.html) 85 | pub fn plot

(&mut self, p: &P) 86 | where 87 | P: Plotable, 88 | { 89 | let (xs, ys) = p.plot(); 90 | self.xs.extend(&xs); 91 | self.ys.extend(&ys); 92 | } 93 | } 94 | 95 | impl Paint for Figure { 96 | fn paint(&self, canvas: &mut Canvas, x: T, y: T) -> Result<(), crate::utils::RsilleErr> 97 | where 98 | T: Into, 99 | { 100 | let (x, y) = (x.into(), y.into()); 101 | let pad = self.padding; 102 | let (sx, sy) = self.scale; 103 | for (px, py) in zip(&self.xs, &self.ys) { 104 | canvas.set(x + px * sx, y + py * sy); 105 | } 106 | if self.boxed || self.show_axis { 107 | let minx = self.xs.iter().fold(f64::INFINITY, |a, &b| a.min(b)); 108 | let maxx = self.xs.iter().fold(f64::NEG_INFINITY, |a, &b| a.max(b)); 109 | let miny = self.ys.iter().fold(f64::INFINITY, |a, &b| a.min(b)); 110 | let maxy = self.ys.iter().fold(f64::NEG_INFINITY, |a, &b| a.max(b)); 111 | let start = (x + minx * sx - pad, y + miny * sy - pad); 112 | let end = (x + maxx * sx + pad, y + maxy * sy + pad); 113 | if self.boxed { 114 | draw_box(canvas, start, end, &self.decor); 115 | } 116 | 117 | if self.show_axis { 118 | let (lc, bc) = (self.decor.lc, self.decor.bc); 119 | let (sx, sy) = ((sx / 2.0).floor() * 2.0, (sy / 4.0).floor() * 4.0); 120 | let xlabels = gen_label((minx * sx, maxx * sx), sx, minx); 121 | for (raw, label) in xlabels { 122 | let (px, _) = floor(raw); 123 | canvas.put(x + label, start.1, bc, None); 124 | canvas.put_text(x + label, start.1 - 4.0, &px, None); 125 | } 126 | let ylabels = gen_label((miny * sy, maxy * sy), sy, miny); 127 | for (raw, label) in ylabels { 128 | let (py, l) = floor(raw); 129 | canvas.put(x + start.0, y + label, lc, None); 130 | canvas.put_text(x + start.0 - 2.0 * l as f64, y + label, &py, None); 131 | } 132 | } 133 | } 134 | Ok(()) 135 | } 136 | } 137 | 138 | fn gen_label(range: (f64, f64), step: f64, v: f64) -> Vec<(f64, f64)> { 139 | let mut labels = Vec::new(); 140 | let mut v = v; 141 | let (mut label, max) = (range.0, range.1); 142 | let mut p = 0.0; 143 | loop { 144 | if label > max && (label - max).abs() > 0.01 { 145 | break; 146 | } 147 | if label.abs() - 0.0 < MIN_DIFFERENCE { 148 | label = 0.0; 149 | } 150 | if p == 0.0 && label > 0.0 { 151 | p = 1.0; 152 | } 153 | labels.push((v, label + p)); 154 | v += 1.0; 155 | label += step; 156 | } 157 | labels 158 | } 159 | 160 | fn floor(v: f64) -> (String, usize) { 161 | let a = format!("{v:.2}"); 162 | let len = a.len(); 163 | (a, len) 164 | } 165 | -------------------------------------------------------------------------------- /src/extra/math/mod.rs: -------------------------------------------------------------------------------- 1 | //! This module can be used to paint math function or math plot easily 2 | //! 3 | //! Like if you want to see the `y=sin(x)` and `y=cos(x)` on `(0, 5)`, 4 | //! it's very very easy: 5 | //! ``` 6 | //! use rsille::figure; 7 | //! figure!((|x| x.sin(), (0, 5)), (|x| x.cos(), (0, 5))); 8 | //! ``` 9 | 10 | mod figure; 11 | mod plot; 12 | 13 | pub use figure::Figure; 14 | pub use figure::Plotable; 15 | pub use plot::Plot; 16 | -------------------------------------------------------------------------------- /src/extra/math/plot.rs: -------------------------------------------------------------------------------- 1 | use super::figure::Plotable; 2 | 3 | /// The plot2d 4 | /// 5 | /// Plot `y=f(x)` 6 | #[derive(Debug)] 7 | pub struct Plot { 8 | fny: F, 9 | step: f64, 10 | range: (f64, f64), 11 | } 12 | 13 | impl Plot 14 | where 15 | F: Fn(f64) -> f64, 16 | { 17 | /// Make a plot 18 | /// 19 | /// example: `Plot::new(|x| x.sin(), (0, 10))` 20 | pub fn new(f: F, range: (T, T)) -> Self 21 | where 22 | T: Into, 23 | { 24 | let range = (range.0.into(), range.1.into()); 25 | Self { 26 | fny: f, 27 | step: 0.08, 28 | range, 29 | } 30 | } 31 | 32 | /// Set the step 33 | /// 34 | /// The step won't change the plot, just the step of calculate the `f(x)` 35 | pub fn set_step(&mut self, step: f64) { 36 | self.step = step; 37 | } 38 | } 39 | 40 | impl Plotable for Plot 41 | where 42 | F: Fn(f64) -> f64, 43 | { 44 | fn plot(&self) -> (Vec, Vec) { 45 | let mut x = self.range.0; 46 | let end = self.range.1; 47 | let (mut xs, mut ys) = (Vec::new(), Vec::new()); 48 | loop { 49 | if x > end && (x - end).abs() > 0.01 { 50 | break; 51 | } 52 | xs.push(x); 53 | ys.push((self.fny)(x)); 54 | x += self.step; 55 | } 56 | (xs, ys) 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/extra/mod.rs: -------------------------------------------------------------------------------- 1 | //! Some useful things can paint on the canvas 2 | 3 | #[cfg(feature = "img")] 4 | mod imgille; 5 | mod lifegame; 6 | pub mod math; 7 | mod object3d; 8 | mod turtle; 9 | 10 | #[cfg(feature = "img")] 11 | pub use imgille::Imgille; 12 | pub use lifegame::LifeGame; 13 | pub use object3d::Object3D; 14 | pub use turtle::Turtle; 15 | -------------------------------------------------------------------------------- /src/extra/object3d.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | canvas::Paint, 3 | utils::{check_zoom, mean, RsilleErr, MIN_DIFFERENCE}, 4 | Canvas, 5 | }; 6 | 7 | use crate::color::Color; 8 | use std::collections::HashMap; 9 | 10 | /// A paintable Object in 3D 11 | /// 12 | /// Make object and easy to do something like rotate, zoom and more 13 | /// also, it support colorful output 14 | /// 15 | /// ## Example 16 | /// 17 | /// make a cube and rotate it endlessly 18 | /// ```no_run 19 | /// use rsille::{extra::Object3D, Animation}; 20 | /// let cube = Object3D::cube(30.0); 21 | /// let mut anime = Animation::new(); 22 | /// anime.push(cube, |cube| { 23 | /// cube.rotate((1.0, 2.0, 3.0)); 24 | /// false 25 | /// }, (0, 0)); 26 | /// anime.run(); 27 | /// ``` 28 | /// also try to not paint at *(0,0)*, at other location like *(30, -30)* 29 | #[derive(Debug, Clone)] 30 | pub struct Object3D { 31 | origin_vertices: Vec, 32 | zoomed_vertices: Option>, 33 | center: Point3D, 34 | sides: HashMap<(usize, usize), Color>, 35 | } 36 | 37 | impl Object3D { 38 | /// Construct a new Object3D, with no vertices and sides 39 | pub fn new() -> Self { 40 | Self { 41 | origin_vertices: Vec::new(), 42 | zoomed_vertices: None, 43 | center: Point3D::new(0.0, 0.0, 0.0), 44 | sides: HashMap::new(), 45 | } 46 | } 47 | 48 | /// Return the vertices 49 | pub fn vertices(&self) -> Vec<(f64, f64, f64)> { 50 | self.origin_vertices.iter().map(|p| p.get()).collect() 51 | } 52 | 53 | /// Add vertices to object 54 | pub fn add_points(&mut self, points: &[(f64, f64, f64)]) { 55 | for p in points { 56 | self.origin_vertices.push(Point3D::from(*p)); 57 | } 58 | self.calc_center(); 59 | } 60 | 61 | /// Return the sides 62 | pub fn sides(&self) -> Vec<(usize, usize)> { 63 | self.sides.keys().cloned().collect() 64 | } 65 | 66 | /// Add sides to object 67 | /// 68 | /// For example, there is 3 vertices in the object, 69 | /// you want to connect the first and the second, then it's `[0, 1]`. 70 | /// 71 | /// Return an error if the index is out of range, like only 3 vertices but you want to connect `[0, 4]` 72 | pub fn add_sides(&mut self, sides: &[(usize, usize)]) -> Result<(), RsilleErr> { 73 | let vn = self.origin_vertices.len(); 74 | for side in sides { 75 | if vn <= side.0 || vn <= side.1 { 76 | return Err(RsilleErr::new("wrong add sides!".to_string())); 77 | } 78 | 79 | self.sides.insert(*side, Color::Reset); 80 | } 81 | Ok(()) 82 | } 83 | 84 | /// Add sides and color of those sides 85 | /// 86 | /// Take a look at [`add_sides`](struct.Object3D.html#method.add_sides) for more information 87 | pub fn add_sides_colorful( 88 | &mut self, 89 | sides: &[((usize, usize), Color)], 90 | ) -> Result<(), RsilleErr> { 91 | let vn = self.origin_vertices.len(); 92 | for (side, color) in sides { 93 | if vn <= side.0 || vn <= side.1 { 94 | return Err(RsilleErr::new("wrong add sides!".to_string())); 95 | } 96 | self.sides.insert(*side, *color); 97 | } 98 | Ok(()) 99 | } 100 | 101 | /// Set the color of the side 102 | /// 103 | /// If there isn't the side, it will do nothing 104 | pub fn set_side_colorful(&mut self, side: (usize, usize), color: Color) { 105 | if self.sides.contains_key(&side) { 106 | self.sides.insert(side, color); 107 | } 108 | } 109 | 110 | /// Rotate the whole object 111 | /// 112 | /// Normaly, the rotate won't grow the error of f64. 113 | /// If the error is growing, use [`rotate_new`](struct.Object3D.html#method.rotate_new) 114 | pub fn rotate(&mut self, angle: (f64, f64, f64)) { 115 | for p in &mut self.origin_vertices { 116 | p.rotate(angle); 117 | } 118 | } 119 | 120 | /// Rotate the whole object 121 | /// 122 | /// Similar to [`rotate`](struct.Object3D.html#method.rotate) but will return a new Object3D 123 | pub fn rotate_new(&self, angle: (f64, f64, f64)) -> Self { 124 | let mut obj = self.clone(); 125 | obj.rotate(angle); 126 | obj 127 | } 128 | 129 | /// Zoom the whole object 130 | /// 131 | /// * `factor` - magnification of zoom, must bigger than 0.001 132 | /// 133 | /// Because of the implementation, it won't grow the error of f64 in most time. 134 | /// But if after many times call it, the error is grow, consider use [`zoom_new`](struct.Object3D.html#method.zoom_new) 135 | pub fn zoom(&mut self, factor: f64) { 136 | check_zoom(factor); 137 | let mut vertices = self.origin_vertices.clone(); 138 | vertices 139 | .iter_mut() 140 | .for_each(|v| v.zoom(self.center, factor)); 141 | self.zoomed_vertices = Some(vertices); 142 | } 143 | 144 | /// Zoom the whole object 145 | /// 146 | /// * `factor` - magnification of zoom, must bigger than 0.001 147 | /// 148 | /// Instead of change the original object, it returns a new one. 149 | /// It can forbide the growing of error of f64 150 | pub fn zoom_new(&self, factor: f64) -> Self { 151 | check_zoom(factor); 152 | let mut points: Vec = self.origin_vertices.clone(); 153 | points.iter_mut().for_each(|v| v.zoom(self.center, factor)); 154 | Self { 155 | origin_vertices: points, 156 | zoomed_vertices: None, 157 | sides: self.sides.clone(), 158 | center: self.center, 159 | } 160 | } 161 | 162 | /// Take a closure and run it on all vertices 163 | pub fn map(&mut self, f: F) 164 | where 165 | F: Fn(f64, f64, f64) -> (f64, f64, f64), 166 | { 167 | for p in &mut self.origin_vertices { 168 | let (x, y, z) = f(p.x, p.y, p.z); 169 | p.x = x; 170 | p.y = y; 171 | p.z = z; 172 | } 173 | } 174 | 175 | fn calc_center(&mut self) { 176 | let (mut xs, mut ys, mut zs) = (Vec::new(), Vec::new(), Vec::new()); 177 | for p in &self.origin_vertices { 178 | xs.push(p.x); 179 | ys.push(p.y); 180 | zs.push(p.z); 181 | } 182 | let (mut mx, mut my, mut mz) = (mean(&xs), mean(&ys), mean(&zs)); 183 | 184 | // forbide the lost of f64 185 | if (mx - self.center.x) < MIN_DIFFERENCE { 186 | mx = self.center.x; 187 | } 188 | if (my - self.center.y) < MIN_DIFFERENCE { 189 | my = self.center.y; 190 | } 191 | if (mz - self.center.z) < MIN_DIFFERENCE { 192 | mz = self.center.z; 193 | } 194 | self.center = Point3D::new(mx, my, mz); 195 | } 196 | } 197 | 198 | impl Paint for Object3D { 199 | fn paint(&self, canvas: &mut Canvas, x: T, y: T) -> Result<(), RsilleErr> 200 | where 201 | T: Into, 202 | { 203 | let (x, y) = (x.into(), y.into()); 204 | let points = if let Some(p) = &self.zoomed_vertices { 205 | p 206 | } else { 207 | &self.origin_vertices 208 | }; 209 | 210 | for (side, color) in &self.sides { 211 | let (v1, v2) = (points[side.0], points[side.1]); 212 | let xy1 = (x + v1.x, y + v1.z); 213 | let xy2 = (x + v2.x, y + v2.z); 214 | canvas.line_colorful(xy1, xy2, *color); 215 | } 216 | 217 | Ok(()) 218 | } 219 | } 220 | 221 | impl Object3D { 222 | /// Make a cube 223 | pub fn cube(side_len: T) -> Object3D 224 | where 225 | T: Into, 226 | { 227 | let side_len = side_len.into(); 228 | let mut object = Object3D::new(); 229 | #[rustfmt::skip] 230 | // the vertices of cube 231 | let a = [ 232 | (-1, -1, -1), 233 | (-1, -1, 1), 234 | (-1, 1, -1), 235 | ( 1, -1, -1), 236 | (-1, 1, 1), 237 | ( 1, -1, 1), 238 | ( 1, 1, -1), 239 | ( 1, 1, 1), 240 | ]; 241 | let mut points = Vec::new(); 242 | for i in a { 243 | let x = side_len / 2.0 * i.0 as f64; 244 | let y = side_len / 2.0 * i.1 as f64; 245 | let z = side_len / 2.0 * i.2 as f64; 246 | points.push((x, y, z)); 247 | } 248 | object.add_points(&points); 249 | object 250 | .add_sides(&[ 251 | (0, 1), 252 | (1, 4), 253 | (4, 2), 254 | (2, 0), 255 | (3, 5), 256 | (5, 7), 257 | (7, 6), 258 | (6, 3), 259 | (1, 5), 260 | (4, 7), 261 | (2, 6), 262 | (0, 3), 263 | ]) 264 | .unwrap(); 265 | object 266 | } 267 | } 268 | 269 | /// point in 3D 270 | /// 271 | /// support rotate and zoom, not paintable 272 | #[derive(Debug, Clone, Copy)] 273 | struct Point3D { 274 | pub(crate) x: f64, 275 | pub(crate) y: f64, 276 | pub(crate) z: f64, 277 | } 278 | 279 | #[allow(unused)] 280 | impl Point3D { 281 | /// construct a new point 282 | fn new(x: f64, y: f64, z: f64) -> Self { 283 | Self { x, y, z } 284 | } 285 | 286 | /// similar to new, but use a tuple 287 | fn from(xyz: (f64, f64, f64)) -> Self { 288 | Self { 289 | x: xyz.0, 290 | y: xyz.1, 291 | z: xyz.2, 292 | } 293 | } 294 | 295 | /// get the coordinate 296 | fn get(&self) -> (f64, f64, f64) { 297 | (self.x, self.y, self.z) 298 | } 299 | 300 | /// rotate the point, see [wiki] for more information 301 | /// 302 | /// [wiki]: 303 | fn rotate(&mut self, angle: (f64, f64, f64)) { 304 | self.rotate_x(angle.0); 305 | self.rotate_y(angle.1); 306 | self.rotate_z(angle.2); 307 | 308 | // let (x, y, z) = (self.x, self.y, self.z); 309 | // let (sx, cx) = anlge_x.to_radians().sin_cos(); 310 | // let (sy, cy) = anlge_y.to_radians().sin_cos(); 311 | // let (sz, cz) = angle_z.to_radians().sin_cos(); 312 | // let (t1, t2, t3) = ( 313 | // x * cy + y * sx * sy + z * cx * sy, 314 | // y * cx - z * sx, 315 | // y * sx + z * cx, 316 | // ); 317 | // self.x = cz * t1 - sz * t2; 318 | // self.y = sz * t1 + cz * t2; 319 | // self.z = cz * t3 - sy * x; 320 | } 321 | 322 | /// similar to [rotate] but don't change the original point and return the rotated point 323 | /// 324 | /// [rotate]: struct.Point3D.html#method.rotate 325 | fn rotate_new(&self, angle: (f64, f64, f64)) -> Self { 326 | let mut point = *self; 327 | point.rotate(angle); 328 | point 329 | } 330 | 331 | /// Zoom coordinate of the point with the center 332 | /// 333 | /// It will change the coordinate, so don't call it many times, precision errors of f64 are cumulative! 334 | /// 335 | /// In most time, you shouldn't use it, just use the [`zoom`](struct.Object3D.html#method.zoom) in Object3D 336 | fn zoom(&mut self, center: Self, factor: f64) { 337 | check_zoom(factor); 338 | self.x = (self.x - center.x) * factor; 339 | self.y = (self.y - center.y) * factor; 340 | self.z = (self.z - center.z) * factor; 341 | } 342 | 343 | /// Zoom coordinate of the point with the center and return a new point 344 | /// 345 | /// It won't change the original point, so the precision error isn't matter 346 | fn zoom_new(&self, center: Self, factor: f64) -> Point3D { 347 | check_zoom(factor); 348 | let dx = (self.x - center.x) * factor; 349 | let dy = (self.y - center.y) * factor; 350 | let dz = (self.z - center.y) * factor; 351 | 352 | Self { 353 | x: dx, 354 | y: dy, 355 | z: dz, 356 | } 357 | } 358 | 359 | /// rotate by an angle about the x axis 360 | fn rotate_x(&mut self, angle: f64) { 361 | let (s, c) = angle.to_radians().sin_cos(); 362 | let (y, z) = (self.y, self.z); 363 | self.y = y * c - z * s; 364 | self.z = y * s + z * c; 365 | } 366 | 367 | /// rotate by an angle about the y axis 368 | fn rotate_y(&mut self, angle: f64) { 369 | let (s, c) = angle.to_radians().sin_cos(); 370 | let (x, z) = (self.x, self.z); 371 | self.x = x * c + z * s; 372 | self.z = -x * s + z * c; 373 | } 374 | 375 | /// rotate by an angle about the z axis 376 | fn rotate_z(&mut self, anlge: f64) { 377 | let (s, c) = anlge.to_radians().sin_cos(); 378 | let (x, y) = (self.x, self.y); 379 | self.x = x * c - y * s; 380 | self.y = x * s + y * c; 381 | } 382 | } 383 | -------------------------------------------------------------------------------- /src/extra/turtle.rs: -------------------------------------------------------------------------------- 1 | // TODO: remove the extra things for animation in Turtle 2 | 3 | use std::f64::consts::PI; 4 | 5 | use crate::{ 6 | canvas::Paint, 7 | utils::{RsilleErr, MIN_DIFFERENCE}, 8 | Canvas, 9 | }; 10 | 11 | use crate::color::Color; 12 | 13 | /// The turtle impl of braille code in Rust 14 | /// 15 | /// All the api is similar to turtle in Python. 16 | /// It support both paint directly and animation. 17 | /// If you don't know how to use, you can search turtle on the internet, 18 | /// then you can paste those code you find and use it! 19 | /// 20 | /// ## Example 21 | /// 22 | /// just paint it 23 | /// ``` 24 | /// use rsille::{extra::Turtle, Canvas}; 25 | /// let mut canvas = Canvas::new(); 26 | /// let mut t = Turtle::new(); 27 | /// let mut length = 1.0; 28 | /// for _ in 0..150 { 29 | /// t.forward(length); 30 | /// t.right(10.0); 31 | /// length += 0.05; 32 | /// } 33 | /// canvas.paint(&t, 0, 0).unwrap(); 34 | /// canvas.print(); 35 | /// ``` 36 | /// 37 | /// or a animation 38 | /// ```no_run 39 | /// use rsille::{extra::Turtle, Animation}; 40 | /// let mut anime = Animation::new(); 41 | /// let mut t = Turtle::new(); 42 | /// let mut length = 1.0; 43 | /// for _ in 0..150 { 44 | /// t.forward(length); 45 | /// t.right(10.0); 46 | /// length += 0.05; 47 | /// } 48 | /// t.anime(); // transfrom the turtle to animation 49 | /// anime.push(t, move |t| t.update(), (50, -50)); 50 | /// anime.run(); 51 | /// ``` 52 | /// The (50, -50) isn't fixed, you can try to paint on other place. 53 | /// 54 | /// ## NOTE: 55 | /// 56 | /// There isn't position or heading function, 57 | /// because the position and heading can't know until paint it! 58 | /// When you call forward or circle or any other method, it will only record the procedure. 59 | /// Then in the paint function, it will do those procedures and paint the canvas. 60 | #[derive(Debug, Clone, PartialEq)] 61 | pub struct Turtle { 62 | procedures: Vec, 63 | anime_proc: Option>, 64 | anime_step: f64, 65 | frame_count: usize, 66 | } 67 | 68 | impl Turtle { 69 | /// Return a new Turtle 70 | pub fn new() -> Self { 71 | Self { 72 | procedures: Vec::new(), 73 | anime_proc: None, 74 | anime_step: 10.0, 75 | frame_count: 0, 76 | } 77 | } 78 | 79 | /// Pen up, the turtle won't draw when moving 80 | pub fn penup(&mut self) { 81 | self.add_procedure(Procedure::PenUp); 82 | } 83 | 84 | /// alias: [`penup`](struct.Turtle.html#method.penup) 85 | pub fn up(&mut self) { 86 | self.penup(); 87 | } 88 | 89 | /// Pen down, the turtle will draw when moving 90 | pub fn pendown(&mut self) { 91 | self.add_procedure(Procedure::PenDown); 92 | } 93 | 94 | /// alias: [`pendown`](struct.Turtle.html#method.pendown) 95 | pub fn down(&mut self) { 96 | self.pendown(); 97 | } 98 | 99 | /// Move the turtle forward by the specified distance, in the direction the turtle is headed. 100 | /// * `step` - the distance 101 | pub fn forward(&mut self, step: T) 102 | where 103 | T: Into, 104 | { 105 | self.add_procedure(Procedure::Forward(step.into())); 106 | } 107 | 108 | /// alias: [`forward`](struct.Turtle.html#method.forward) 109 | pub fn fd(&mut self, step: T) 110 | where 111 | T: Into, 112 | { 113 | self.forward(step); 114 | } 115 | 116 | /// Move the turtle backward by distance, opposite to the direction the turtle is headed. 117 | /// * `step` - the distance 118 | /// 119 | /// It won't change the turtle’s heading. 120 | pub fn backward(&mut self, step: T) 121 | where 122 | T: Into, 123 | { 124 | self.forward(-step.into()); 125 | } 126 | 127 | /// alias: [`backward`](struct.Turtle.html#method.backward) 128 | pub fn back(&mut self, step: T) 129 | where 130 | T: Into, 131 | { 132 | self.backward(step); 133 | } 134 | 135 | /// alias: [`backward`](struct.Turtle.html#method.backward) 136 | pub fn bk(&mut self, step: T) 137 | where 138 | T: Into, 139 | { 140 | self.backward(step); 141 | } 142 | 143 | /// Turn turtle right by angle units. 144 | /// * `angle` - the degree 145 | pub fn right(&mut self, angle: T) 146 | where 147 | T: Into, 148 | { 149 | self.add_procedure(Procedure::Right(angle.into())); 150 | } 151 | 152 | /// alias: [`right`](struct.Turtle.html#method.right) 153 | pub fn rt(&mut self, angle: T) 154 | where 155 | T: Into, 156 | { 157 | self.right(angle); 158 | } 159 | 160 | /// Turn turtle left by angle units. 161 | /// * `angle` - the degree 162 | pub fn left(&mut self, angle: T) 163 | where 164 | T: Into, 165 | { 166 | self.right(-angle.into()); 167 | } 168 | 169 | /// alias: [`left`](struct.Turtle.html#method.left) 170 | pub fn lt(&mut self, angle: T) 171 | where 172 | T: Into, 173 | { 174 | self.left(angle); 175 | } 176 | 177 | /// Draw a circle with given radius. 178 | /// * `extent` – an angle – determines which part of the circle is drawn. If extent is not a full circle, one endpoint of the arc is the current pen position. 179 | /// * `radius` - Draw the arc in counterclockwise direction if radius is positive, otherwise in clockwise direction. Finally the direction of the turtle is changed by the amount of extent. 180 | /// 181 | /// The center is radius units left of the turtle; 182 | pub fn circle(&mut self, radius: T, extent: T) 183 | where 184 | T: Into, 185 | { 186 | self.add_procedure(Procedure::Circle(radius.into(), extent.into(), 100)); 187 | } 188 | 189 | /// Draw a circle with given radius. 190 | /// The center is radius units left of the turtle; 191 | /// * `extent` – an angle – determines which part of the circle is drawn. If extent is not a full circle, one endpoint of the arc is the current pen position. 192 | /// * `radius` - Draw the arc in counterclockwise direction if radius is positive, otherwise in clockwise direction. Finally the direction of the turtle is changed by the amount of extent. 193 | /// * `steps` - Suggest 100. As the circle is approximated by an inscribed regular polygon, steps determines the number of steps to use. 194 | /// 195 | /// Suggest use [`circle`](struct.Turtle.html#method.circle) 196 | pub fn circle_with_steps(&mut self, radius: T, extent: T, steps: usize) 197 | where 198 | T: Into, 199 | { 200 | self.add_procedure(Procedure::Circle(radius.into(), extent.into(), steps)); 201 | } 202 | 203 | /// Move turtle to an absolute position. If the pen is down, draw line. 204 | /// * `x` - the position of x 205 | /// * `y` - the position of y 206 | pub fn goto(&mut self, x: T, y: T) 207 | where 208 | T: Into, 209 | { 210 | self.add_procedure(Procedure::Goto(x.into(), y.into())); 211 | } 212 | 213 | /// Move turtle to an absolute position and a line will not be drawn. 214 | /// * `x` - the position of x 215 | /// * `y` - the position of y 216 | pub fn teleport(&mut self, x: T, y: T) 217 | where 218 | T: Into, 219 | { 220 | self.add_procedure(Procedure::Teleport(x.into(), y.into())); 221 | } 222 | 223 | /// Move turtle to the origin 224 | /// * – coordinates (0,0) 225 | /// * – set its heading to its start-orientation 226 | pub fn home(&mut self) { 227 | self.add_procedure(Procedure::Home); 228 | } 229 | 230 | /// Set the color of turtle 231 | pub fn colorful(&mut self, color: Color) { 232 | self.add_procedure(Procedure::Colorful(color)); 233 | } 234 | 235 | /// alias: [`colorful`](struct.Turtle.html#method.colorful) 236 | pub fn color(&mut self, color: Color) { 237 | self.colorful(color); 238 | } 239 | 240 | /// Build the Turtle for animation 241 | /// 242 | /// If you don't need the animation, then don't call this method 243 | pub fn anime(&mut self) { 244 | use Procedure::*; 245 | let mut anime_proc = Vec::new(); 246 | let astep = self.anime_step; 247 | for p in &self.procedures { 248 | match p { 249 | PenDown | PenUp | Right(_) | Teleport(_, _) | Home => { 250 | anime_proc.push(*p); 251 | } 252 | Forward(step) => { 253 | let mut step = *step; 254 | while step > astep { 255 | anime_proc.push(Forward(astep)); 256 | step -= astep; 257 | } 258 | // forbide the lost of f64 259 | if (step - astep).abs() < MIN_DIFFERENCE { 260 | anime_proc.push(Forward(astep)); 261 | } else { 262 | anime_proc.push(Forward(step)); 263 | } 264 | } 265 | Goto(_, _) => { 266 | anime_proc.push(*p); 267 | } 268 | Circle(radius, extent, steps) => { 269 | let mut extent = *extent; 270 | if PI * radius * extent <= 180.0 * astep { 271 | anime_proc.push(*p); 272 | } else { 273 | let e = 180.0 * astep / (PI * radius); 274 | while extent > e { 275 | anime_proc.push(Circle(*radius, e, *steps)); 276 | extent -= e; 277 | } 278 | // forbide the lost of f64 279 | if (extent - e).abs() < MIN_DIFFERENCE { 280 | anime_proc.push(Circle(*radius, e, *steps)); 281 | } else { 282 | anime_proc.push(Circle(*radius, extent, *steps)); 283 | } 284 | } 285 | } 286 | _ => anime_proc.push(*p), 287 | } 288 | } 289 | self.anime_proc = Some(anime_proc); 290 | } 291 | 292 | /// Generate the next frame of the animation 293 | /// 294 | /// Return true if the animation is over 295 | /// 296 | /// If you don't need the animation, don't need to call this method. 297 | /// And you should call [`anime`](struct.Turtle.html#method.anime) first 298 | pub fn update(&mut self) -> bool { 299 | use Procedure::*; 300 | if let Some(procs) = &self.anime_proc { 301 | if self.frame_count >= procs.len() { 302 | return true; 303 | } 304 | while let Some(p) = procs.get(self.frame_count) { 305 | match p { 306 | Forward(_) | Goto(_, _) | Circle(_, _, _) => { 307 | self.frame_count += 1; 308 | break; 309 | } 310 | _ => { 311 | self.frame_count += 1; 312 | } 313 | } 314 | } 315 | } 316 | false 317 | } 318 | 319 | /// Set the step of the animation 320 | /// 321 | /// The default of Turtle is 10.0, if you want to change it, you can call this method 322 | /// 323 | /// For example: 324 | /// * *forward(100.0) -> forward(10.0) * 10* 325 | /// * but *forward(5.0) -> forward(5.0)* 326 | /// 327 | /// Beacuse like *forward(5.0), right(10.0), forward(5.0)* can't fold to *forward(10.0), right(10.0)* 328 | pub fn set_anime_step(&mut self, step: T) 329 | where 330 | T: Into, 331 | { 332 | self.anime_step = step.into(); 333 | } 334 | 335 | fn add_procedure(&mut self, p: Procedure) { 336 | self.procedures.push(p); 337 | } 338 | } 339 | 340 | #[derive(Debug, PartialEq, Clone, Copy)] 341 | enum Procedure { 342 | PenDown, 343 | PenUp, 344 | Forward(f64), // step 345 | // Backward(f64), == - Forward 346 | Right(f64), // angle 347 | // Left(f64), == - Right 348 | Teleport(f64, f64), // (x, y) 349 | Home, 350 | Goto(f64, f64), // (x, y) 351 | Circle(f64, f64, usize), // (radius, extent, steps) 352 | Colorful(Color), 353 | } 354 | 355 | fn forward( 356 | canvas: &mut Canvas, 357 | x: f64, 358 | y: f64, 359 | heading: f64, 360 | pen: bool, 361 | step: f64, 362 | color: Color, 363 | ) -> (f64, f64) { 364 | let (sr, cr) = heading.to_radians().sin_cos(); 365 | let txy = (x + cr * step, y + sr * step); 366 | if pen { 367 | canvas.line_colorful((x, y), txy, color); 368 | } 369 | txy 370 | } 371 | 372 | impl Paint for Turtle { 373 | fn paint(&self, canvas: &mut Canvas, x: T, y: T) -> Result<(), RsilleErr> 374 | where 375 | T: Into, 376 | { 377 | use Procedure::*; 378 | let (x, y) = (x.into(), y.into()); 379 | let (home_x, home_y) = (x, y); 380 | let (mut pen, mut heading, mut x, mut y) = (true, 0.0, x, y); 381 | let mut color = Color::Reset; 382 | let procs = if let Some(procs) = &self.anime_proc { 383 | &procs[0..self.frame_count] 384 | } else { 385 | &self.procedures 386 | }; 387 | 388 | for p in procs { 389 | match p { 390 | PenDown => { 391 | pen = true; 392 | } 393 | PenUp => { 394 | pen = false; 395 | } 396 | Forward(step) => { 397 | (x, y) = forward(canvas, x, y, heading, pen, *step, color); 398 | } 399 | Right(angle) => { 400 | heading -= angle; 401 | } 402 | Teleport(tx, ty) => { 403 | x = *tx; 404 | y = *ty; 405 | } 406 | Home => { 407 | (x, y) = (home_x, home_y); 408 | } 409 | Goto(tx, ty) => { 410 | if pen { 411 | canvas.line_colorful((x, y), (*tx, *ty), color); 412 | } 413 | x = *tx; 414 | y = *ty; 415 | } 416 | Circle(radius, extent, steps) => { 417 | let angle = extent / *steps as f64; 418 | for _ in 0..*steps { 419 | (x, y) = forward( 420 | canvas, 421 | x, 422 | y, 423 | heading, 424 | pen, 425 | 2.0 * radius * (angle / 2.0).to_radians().sin(), 426 | color, 427 | ); 428 | heading -= angle; 429 | } 430 | } 431 | Colorful(c) => { 432 | color = *c; 433 | } 434 | } 435 | } 436 | Ok(()) 437 | } 438 | } 439 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | #![warn(missing_docs)] 2 | //! This crate is a rust lib for making [braille] art. 3 | //! 4 | //! You can use the basic canvas to paint something, 5 | //! or you can use Turtle to paint things like in python. 6 | //! And there are more useful things like colorful output, 3D object, life game and so on! 7 | //! 8 | //! ## Examples 9 | //! 10 | //! draw the sin(x) 11 | //! ``` 12 | //! use rsille::Canvas; 13 | //! let mut c = Canvas::new(); 14 | //! for x in 0..1800 { 15 | //! let x = x as f64; 16 | //! c.set(x / 10.0, 15.0 + x.to_radians().sin() * 10.0); 17 | //! } 18 | //! c.print(); 19 | //! ``` 20 | //! 21 | //! draw a star 22 | //! ``` 23 | //! use rsille::{extra::Turtle, Canvas}; 24 | //! let mut c = Canvas::new(); 25 | //! let mut t = Turtle::new(); 26 | //! for _ in 0..5 { 27 | //! t.forward(30.0); 28 | //! t.right(144.0); 29 | //! } 30 | //! c.paint(&t, 0.0, 15.0).unwrap(); 31 | //! c.print(); 32 | //! ``` 33 | //! 34 | //! life game 35 | //! ```no_run 36 | //! use rsille::{extra::LifeGame, Animation}; 37 | //! let lg = LifeGame::from_path("path/to/rle").unwrap(); 38 | //! let mut anime = Animation::new(); 39 | //! anime.push(lg, |lg| lg.update(), (0.0, 0.0)); 40 | //! anime.run(); 41 | //! ``` 42 | //! 43 | //! Want more examples? check the [examples](https://github.com/nidhoggfgg/rsille/tree/main/examples) 44 | //! 45 | //! ## Extra 46 | //! 47 | //! Useful things can paint on canvas: 48 | //! 1. [`Object3d`](extra/struct.Object3D.html) the 3d object 49 | //! 2. [`Turtle`](extra/struct.Turtle.html) similar to the turtle in python 50 | //! 3. [`Imagille`](extra/struct.Imgille.html) paint image to braille code 51 | //! 4. [`Lifegame`](extra/struct.LifeGame.html) the life game in braille code 52 | //! 53 | //! ## NOTE 54 | //! 55 | //! About (x, y), you can simply put on (0.0, 0.0) for one object, it will find the right position to paint. 56 | //! But if you want to build a animation or paint multiple objects, 57 | //! you may should find the right position by yourself. 58 | //! 59 | //! For example, this is a dot: 60 | //! ```text 61 | //! $ ./path/to/file # your terminal 62 | //! +--------+--------> x 63 | //! | 64 | //! | 65 | //! + ⣿ 66 | //! | 67 | //! | 68 | //! v y 69 | //! ``` 70 | //! This dot in terminal is (10, 4), because the left top corner is (1, 1). 71 | //! The y-axis is also facing down. 72 | //! It's different from the math coordinate system. 73 | //! 74 | //! But in the canvas, it's use the math coordinate system, and include the braille. 75 | //! Like a braille code "⣿", it has 2x4 dots, and it be thought as 8 dots not 1 dots. 76 | //! In the canvas, this dots B is `x` from 18.0 to 19.0, `y` from 8.0 to 11.0. 77 | //! And the dot A (the left top) is on (4.0, 11.0). 78 | //! ```text 79 | //! ^ y 80 | //! | 81 | //! | A B 82 | //! + ⠁ ⣿ 83 | //! | 84 | //! | 85 | //! +--------+--------> x 86 | //! 0.0 87 | //! ``` 88 | //! 89 | //! If you really don't know how to find the right position. 90 | //! Just try to paint your things at anyway and try to move it to another place and try again. :) 91 | //! And try to set x > 0 and y < 0, then the object will always in the screen and won't move. 92 | //! 93 | //! ## Other 94 | //! 95 | //! It's inspired by [drawille], but it has more features and fast 96 | //! 97 | //! [braille]: http://www.alanwood.net/unicode/braille_patterns.html 98 | //! [drawille]: https://github.com/asciimoo/drawille 99 | 100 | mod anime; 101 | mod braille; 102 | mod canvas; 103 | pub mod color; 104 | mod decor; 105 | mod defaults; 106 | pub mod extra; 107 | pub mod term; 108 | mod utils; 109 | 110 | pub use anime::Animation; 111 | pub use canvas::Canvas; 112 | pub use canvas::Paint; 113 | pub use decor::Decor; 114 | pub use utils::RsilleErr; 115 | -------------------------------------------------------------------------------- /src/term.rs: -------------------------------------------------------------------------------- 1 | //! Some useful function to print styles in terminal 2 | //! 3 | //! ## NOTE: 4 | //! 5 | //! Many functions put some escape sequences to the stdout, 6 | //! so use them only when you know what you are doing 7 | 8 | use crossterm::{cursor, execute, terminal}; 9 | 10 | /// Get the *(width, height)* of terminal 11 | /// 12 | /// If can't get the size, return *(80, 24)* 13 | pub fn get_terminal_size() -> (u16, u16) { 14 | terminal::size().unwrap_or((80, 24)) 15 | } 16 | 17 | /// Check the terminal is in raw mode or not 18 | /// 19 | /// If can't get the mode, return false 20 | pub fn is_raw_mode() -> bool { 21 | terminal::is_raw_mode_enabled().unwrap_or(false) 22 | } 23 | 24 | /// Clear the screen 25 | pub fn clear() { 26 | execute!(std::io::stdout(), terminal::Clear(terminal::ClearType::All)).unwrap(); 27 | } 28 | 29 | /// Hide the cursor 30 | pub fn hide_cursor() { 31 | execute!(std::io::stdout(), cursor::Hide).unwrap(); 32 | } 33 | 34 | /// Show the cursor 35 | pub fn show_cursor() { 36 | execute!(std::io::stdout(), cursor::Show).unwrap(); 37 | } 38 | 39 | /// Move cursor to *(x, y)* 40 | pub fn move_to(x: u32, y: u32) { 41 | execute!(std::io::stdout(), cursor::MoveTo(x as u16, y as u16)).unwrap(); 42 | } 43 | 44 | /// When output is longer than the terminal width, put the rest output in next line (default) 45 | pub fn enable_wrap() { 46 | execute!(std::io::stdout(), terminal::EnableLineWrap).unwrap(); 47 | } 48 | 49 | /// When output is longer than the terminal width, don't put the rest output in next line 50 | pub fn disable_wrap() { 51 | execute!(std::io::stdout(), terminal::DisableLineWrap).unwrap(); 52 | } 53 | -------------------------------------------------------------------------------- /src/utils.rs: -------------------------------------------------------------------------------- 1 | use core::fmt; 2 | use std::error::Error; 3 | 4 | use crate::braille::Pixel; 5 | 6 | pub const MIN_ZOOM: f64 = 0.001; 7 | pub const MIN_DIFFERENCE: f64 = 1E-10; 8 | 9 | pub(crate) fn round(v: T) -> i32 10 | where 11 | T: Into, 12 | { 13 | v.into().round() as i32 14 | } 15 | 16 | pub(crate) fn mean(a: &[f64]) -> f64 { 17 | let sum = a.iter().sum::(); 18 | sum / a.len() as f64 19 | } 20 | 21 | pub(crate) fn check_zoom(v: f64) { 22 | if v <= MIN_ZOOM { 23 | panic!("zoom too small!"); 24 | } 25 | } 26 | 27 | // the the (col, row) of (x, y) 28 | pub fn get_pos(x: T, y: T) -> (i32, i32) 29 | where 30 | T: Into, 31 | { 32 | let (x, y) = (round(x), round(y)); 33 | let row = if y < 0 { (y + 1) / 4 - 1 } else { y / 4 }; 34 | let col = if x < 0 { (x - 1) / 2 } else { x / 2 }; 35 | (col, row) 36 | } 37 | 38 | #[allow(unused)] 39 | pub(crate) fn make_braille(c: char) -> Option { 40 | let c = c as u32; 41 | if (0x2800..=0x28FF).contains(&c) { 42 | unsafe { Some(Pixel::from_unchecked(c - 0x2800)) } 43 | } else { 44 | None 45 | } 46 | } 47 | 48 | /// The error type used by this crate 49 | #[derive(Debug, Clone)] 50 | pub struct RsilleErr { 51 | msg: String, 52 | } 53 | 54 | impl RsilleErr { 55 | /// Return a new RsilleErr 56 | pub fn new(msg: String) -> Self { 57 | Self { msg } 58 | } 59 | 60 | /// Transform other error type to RsilleErr 61 | pub fn to_rsille_err(e: E) -> RsilleErr { 62 | RsilleErr::new(e.to_string()) 63 | } 64 | } 65 | 66 | impl fmt::Display for RsilleErr { 67 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 68 | write!(f, "{}", self.msg) 69 | } 70 | } 71 | 72 | // pub trait Toi32 { 73 | // fn to_i32(&self) -> i32; 74 | // } 75 | // 76 | // impl Toi32 for f64 { 77 | // fn to_i32(&self) -> i32 { 78 | // self.round() as i32 79 | // } 80 | // } 81 | // 82 | // impl Toi32 for f32 { 83 | // fn to_i32(&self) -> i32 { 84 | // self.round() as i32 85 | // } 86 | // } 87 | // 88 | // impl Toi32 for i32 { 89 | // fn to_i32(&self) -> i32 { 90 | // *self 91 | // } 92 | // } 93 | // 94 | // // bad, but it's simple :) 95 | // // only for usize, isize, i64, u64 and so on 96 | // macro_rules! impl_round { 97 | // ($t:ty) => { 98 | // impl Toi32 for $t { 99 | // #[inline] 100 | // fn to_i32(&self) -> i32 { 101 | // *self as i32 102 | // } 103 | // } 104 | // }; 105 | // } 106 | // 107 | // impl_round!(usize); 108 | // impl_round!(isize); 109 | // impl_round!(u64); 110 | // impl_round!(i64); 111 | // impl_round!(u16); 112 | // impl_round!(i16); 113 | // impl_round!(u8); 114 | // impl_round!(i8); 115 | --------------------------------------------------------------------------------