├── .gitignore ├── .travis.yml ├── Cargo.toml ├── ELM_LICENSE.md ├── README.md ├── assets └── NotoSans │ ├── LICENSE-2.0.txt │ ├── NotoSans-Bold.ttf │ ├── NotoSans-BoldItalic.ttf │ ├── NotoSans-Italic.ttf │ └── NotoSans-Regular.ttf ├── examples └── graphics.rs └── src ├── color.rs ├── element.rs ├── form.rs ├── lib.rs ├── text.rs ├── transform_2d.rs └── utils.rs /.gitignore: -------------------------------------------------------------------------------- 1 | # RUST STUFF 2 | 3 | # Compiled files 4 | *.o 5 | *.so 6 | *.rlib 7 | *.dll 8 | 9 | # Executables 10 | *.exe 11 | 12 | # Generated by Cargo 13 | /target/ 14 | Cargo.lock 15 | 16 | 17 | 18 | # MAC STUFF 19 | 20 | .DS_Store 21 | .AppleDouble 22 | .LSOverride 23 | 24 | # Icon must end with two \r 25 | Icon 26 | 27 | # Thumbnails 28 | ._* 29 | 30 | # Files that might appear on external disk 31 | .Spotlight-V100 32 | .Trashes 33 | 34 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | language: rust 3 | os: 4 | - linux 5 | env: 6 | global: 7 | - secure: W/pxVmgtzNXIQNPOm9lsIjSr2nEHGVD8uOGV0be4kdz0bUXCjFDe1j45VVDnXPoJZDrnv7TO0etn3yT7hpuiZGAT40Ovn7LVq7gqtTAoP2U7vbURN55g0MU9dSIAOUdfclAMZez9HgOHWC0P3Tg6bNkNrW5B5wwpmaFVyYwiQkE= 8 | - secure: qlflwsinhvNorlh6l4Hl3tQDytF/LTzlUmw3hA4yj7pwEFUP4BORTvNIlJa+DCft4P4aEU0pgCsC8eb+MQ+q1WOQr2e+EfE+KT/FS9pT6RvqyYUs4QaEznbJkHxMjzkU2N5jf6RGssIEx3ieXD1y+LETxk+KIBFY8DN+wmMYjas= 9 | addons: 10 | apt: 11 | packages: 12 | - libxxf86vm-dev 13 | - libosmesa6-dev 14 | script: 15 | - cargo build --verbose 16 | - cargo test --verbose 17 | - cargo doc --verbose 18 | after_success: | 19 | [ $TRAVIS_BRANCH = master ] && 20 | [ $TRAVIS_PULL_REQUEST = false ] && 21 | cargo doc && 22 | echo "" > target/doc/index.html && 23 | sudo pip install ghp-import && 24 | ghp-import -n target/doc && 25 | git push -fq https://${GH_TOKEN}@github.com/${TRAVIS_REPO_SLUG}.git gh-pages 26 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "elmesque" 3 | version = "0.12.0" 4 | authors = ["mitchmindtree "] 5 | description = "An attempt at porting Elm's incredibly useful, purely functional std graphics modules." 6 | readme = "README.md" 7 | keywords = ["elm", "graphics", "2d", "ui", "shape"] 8 | license = "MIT" 9 | repository = "https://github.com/mitchmindtree/elmesque.git" 10 | homepage = "https://github.com/mitchmindtree/elmesque" 11 | 12 | 13 | [dependencies] 14 | num = "0.1.27" 15 | piston2d-graphics = "0.13.0" 16 | rand = "0.3.12" 17 | rustc-serialize = "0.3.16" 18 | vecmath = "0.2.0" 19 | 20 | [dev-dependencies] 21 | find_folder = "0.3.0" 22 | piston = "0.16.0" 23 | piston_window = "0.33.0" 24 | -------------------------------------------------------------------------------- /ELM_LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright (c) 2013-2015, Evan Czaplicki 2 | 3 | All rights reserved. 4 | 5 | Redistribution and use in source and binary forms, with or without 6 | modification, are permitted provided that the following conditions are met: 7 | 8 | * Redistributions of source code must retain the above copyright 9 | notice, this list of conditions and the following disclaimer. 10 | 11 | * Redistributions in binary form must reproduce the above 12 | copyright notice, this list of conditions and the following 13 | disclaimer in the documentation and/or other materials provided 14 | with the distribution. 15 | 16 | * Neither the name of Evan Czaplicki nor the names of other 17 | contributors may be used to endorse or promote products derived 18 | from this software without specific prior written permission. 19 | 20 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 21 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 22 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 23 | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 24 | OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 25 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 26 | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 27 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 28 | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 29 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 30 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 31 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # elmesque [![Build Status](https://travis-ci.org/mitchmindtree/elmesque.svg?branch=master)](https://travis-ci.org/mitchmindtree/elmesque) 2 | 3 | This crate is an attempt at porting Elm's incredibly useful, purely functional std graphics modules. Its useful for all kinds of 2D freeform graphics and UI design. 4 | 5 | See [the docs](http://mitchmindtree.github.io/elmesque) or checkout [the example](https://github.com/mitchmindtree/elmesque/blob/master/examples/graphics.rs). 6 | 7 | Visit [elm-lang.org](http://elm-lang.org/) to learn more about Elm. 8 | 9 | 10 | All credit and thanks goes to Evan Czaplicki for all algorithms included within. 11 | 12 | Ported to Rust by Mitchell Nordine. 13 | 14 | 15 | 16 | Usage 17 | ----- 18 | 19 | Add elmesque to your cargo dependencies like so. 20 | 21 | ```toml 22 | [dependencies] 23 | elmesque = "*" 24 | ``` 25 | 26 | -------------------------------------------------------------------------------- /assets/NotoSans/LICENSE-2.0.txt: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright [yyyy] [name of copyright owner] 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | -------------------------------------------------------------------------------- /assets/NotoSans/NotoSans-Bold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mitchmindtree/elmesque/aef4ec6b478b21b369061dcaaa4485ec4262670b/assets/NotoSans/NotoSans-Bold.ttf -------------------------------------------------------------------------------- /assets/NotoSans/NotoSans-BoldItalic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mitchmindtree/elmesque/aef4ec6b478b21b369061dcaaa4485ec4262670b/assets/NotoSans/NotoSans-BoldItalic.ttf -------------------------------------------------------------------------------- /assets/NotoSans/NotoSans-Italic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mitchmindtree/elmesque/aef4ec6b478b21b369061dcaaa4485ec4262670b/assets/NotoSans/NotoSans-Italic.ttf -------------------------------------------------------------------------------- /assets/NotoSans/NotoSans-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mitchmindtree/elmesque/aef4ec6b478b21b369061dcaaa4485ec4262670b/assets/NotoSans/NotoSans-Regular.ttf -------------------------------------------------------------------------------- /examples/graphics.rs: -------------------------------------------------------------------------------- 1 | extern crate elmesque; 2 | extern crate find_folder; 3 | extern crate graphics; 4 | extern crate num; 5 | extern crate piston; 6 | extern crate piston_window; 7 | 8 | use elmesque::{Form, Renderer}; 9 | use piston::input::UpdateEvent; 10 | use piston::window::WindowSettings; 11 | use piston_window::{PistonWindow, Glyphs}; 12 | 13 | fn main() { 14 | 15 | // Construct the window. 16 | let window: PistonWindow = 17 | WindowSettings::new("Elmesque", [1180, 580]) 18 | .exit_on_esc(true) 19 | .samples(4) 20 | .vsync(true) 21 | .build() 22 | .unwrap(); 23 | 24 | // Construct the GlyphCache. 25 | let mut glyph_cache = { 26 | let assets = find_folder::Search::ParentsThenKids(3, 3).for_folder("assets").unwrap(); 27 | let font_path = assets.join("NotoSans/NotoSans-Regular.ttf"); 28 | Glyphs::new(&font_path, window.factory.borrow().clone()).unwrap() 29 | }; 30 | 31 | // We'll use this to animate our graphics. 32 | let mut secs = 0.0; 33 | 34 | // Poll events from the window. 35 | for event in window { 36 | event.draw_2d(|context, g| { 37 | let view_dim = context.get_view_size(); 38 | let (w, h) = (view_dim[0], view_dim[1]); 39 | 40 | // Construct the elmesque Renderer with our graphics backend and glyph cache. 41 | let mut renderer = Renderer::new(context, g).character_cache(&mut glyph_cache); 42 | 43 | // Construct some freeform graphics aka a `Form`. 44 | let form = elmesque_demo_form(secs); 45 | 46 | // Convert the form to an `Element` for rendering. 47 | let a = elmesque::form::collage(w as i32, h as i32, vec![form]) 48 | //.crop((secs / 2.0).sin() * (w / 2.0), (secs / 3.0).sin() * (h / 2.0), 400.0, 400.0) 49 | .clear(elmesque::color::black()); 50 | 51 | a.draw(&mut renderer); 52 | }); 53 | event.update(|args| secs += args.dt); 54 | } 55 | 56 | } 57 | 58 | 59 | /// Demo of grouping multiple forms into a new single form, transformable at any stage. 60 | pub fn elmesque_demo_form(secs: f64) -> Form { 61 | use elmesque::color::{blue, dark_blue, light_blue, dark_purple, white}; 62 | use elmesque::form::{circle, group, ngon, oval, point_path, rect, solid, text, traced}; 63 | use elmesque::text::Text; 64 | use elmesque::utils::{degrees}; 65 | use num::Float; 66 | 67 | // Time to get creative! 68 | group(vec![ 69 | 70 | rect(60.0, 40.0).filled(blue()) 71 | .shift(secs.sin() * 50.0, secs.cos() * 50.0) 72 | .alpha(((secs * 200.0).cos() * 0.5 + 0.5) as f32) 73 | .rotate(-secs), 74 | 75 | rect(100.0, 10.0).filled(dark_blue()) 76 | .shift((secs * 5.0).sin() * 200.0, (secs * 5.0).cos() * 200.0) 77 | .alpha(((secs * 2.0).cos() * 0.5 + 0.5) as f32) 78 | .rotate(-(secs * 5.0)), 79 | 80 | rect(10.0, 300.0).filled(blue()) 81 | .alpha(((secs * 3.0).sin() * 0.25 + 0.75) as f32) 82 | .rotate(-(secs * 1.5)), 83 | 84 | rect(5.0, (secs * 0.1).sin() * 600.0 + 300.0).filled(light_blue()) 85 | .alpha(((secs).cos() * 0.25 + 0.75) as f32) 86 | .rotate(secs * 0.75), 87 | 88 | rect(3.0, 2000.0).filled(dark_blue()) 89 | .alpha(((secs * 100.0).cos() * 0.5 + 0.25) as f32) 90 | .rotate(-(secs * 0.5)), 91 | 92 | oval(3.0, 2000.0 * (secs * 60.0).sin()).filled(light_blue()) 93 | .alpha(((secs * 100.0).cos() * 0.5 + 0.25) as f32) 94 | .rotate(-(secs * 0.6)), 95 | 96 | rect(10.0, 750.0).filled(blue()) 97 | .alpha(((secs * 2.0).cos() * 0.5 + 0.25) as f32) 98 | .rotate(-(secs * 1.85)), 99 | 100 | circle((secs * 0.5).sin() * 1500.0).outlined(solid(dark_purple())) 101 | .alpha(((secs * 0.2).sin() * 0.25 + 0.35) as f32) 102 | .rotate(-(secs * 0.5)), 103 | 104 | ngon(12, (secs * 0.1).cos() * 100.0 + 300.0).filled(blue()) 105 | .alpha((0.25 * secs.cos()) as f32) 106 | .rotate(secs * 0.5), 107 | 108 | ngon(9, (secs * 0.1).cos() * 200.0 + 250.0).outlined(solid(dark_blue())) 109 | .alpha(((0.33 * secs).sin() + 0.15) as f32) 110 | .rotate(secs * 0.2), 111 | 112 | rect(300.0, 20.0).filled(light_blue()) 113 | .shift((secs * 1.5).cos() * 250.0, (secs * 1.5).sin() * 250.0) 114 | .alpha(((secs * 4.5).cos() * 0.25 + 0.35) as f32) 115 | .rotate(secs * 1.5 + degrees(90.0)), 116 | 117 | traced( 118 | solid(light_blue()), 119 | point_path(vec![(-500.0, 100.0), (0.0, 250.0 * secs.sin()), (500.0, 100.0)]) 120 | ).alpha(((secs * 0.2).sin() * 0.25 + 0.35) as f32), 121 | 122 | traced( 123 | solid(blue()), 124 | point_path(vec![(-500.0, 0.0), (0.0, 0.0), (500.0, 0.0)]) 125 | ).alpha(((secs * 4.5).cos() * 0.25 + 0.35) as f32), 126 | 127 | traced( 128 | solid(dark_blue()), 129 | point_path(vec![(-500.0, -100.0), (0.0, -250.0 * secs.sin()), (500.0, -100.0)]) 130 | ).alpha(((secs * 0.15).cos() * 0.25 + 0.35) as f32), 131 | 132 | text(Text::from_string("elmesque".to_string()).color(white())), 133 | 134 | ]).rotate(degrees(secs.sin() * 360.0)) 135 | .scale((secs * 0.05).cos() * 0.2 + 0.9) 136 | 137 | } 138 | 139 | -------------------------------------------------------------------------------- /src/color.rs: -------------------------------------------------------------------------------- 1 | //! 2 | //! A library providing simple `Color` and `Gradient` types along with useful transformations and 3 | //! presets. 4 | //! 5 | //! 6 | //! Inspiration taken from [elm-lang's color module] 7 | //! (https://github.com/elm-lang/core/blob/62b22218c42fb8ccc996c86bea450a14991ab815/src/Color.elm) 8 | //! 9 | //! 10 | //! Module for working with colors. Includes [RGB](https://en.wikipedia.org/wiki/RGB_color_model) 11 | //! and [HSL](http://en.wikipedia.org/wiki/HSL_and_HSV) creation, gradients and built-in names. 12 | //! 13 | 14 | use rustc_serialize::hex::ToHex; 15 | use std::ascii::AsciiExt; 16 | use std::f32::consts::PI; 17 | use utils::{clampf32, degrees, fmod, min, max, turns}; 18 | 19 | 20 | /// Color supporting RGB and HSL variants. 21 | #[derive(PartialEq, Copy, Clone, Debug, RustcEncodable, RustcDecodable)] 22 | pub enum Color { 23 | /// Red, Green, Blue, Alpha - All values' scales represented between 0.0 and 1.0. 24 | Rgba(f32, f32, f32, f32), 25 | /// Hue, Saturation, Lightness, Alpha - all valuess scales represented between 0.0 and 1.0. 26 | Hsla(f32, f32, f32, f32), 27 | } 28 | 29 | /// Regional spelling alias. 30 | pub type Colour = Color; 31 | 32 | 33 | /// Create RGB colors with an alpha component for transparency. 34 | /// The alpha component is specified with numbers between 0 and 1. 35 | #[inline] 36 | pub fn rgba(r: f32, g: f32, b: f32, a: f32) -> Color { 37 | Color::Rgba(r, g, b, a) 38 | } 39 | 40 | 41 | /// Create RGB colors from numbers between 0.0 and 1.0. 42 | #[inline] 43 | pub fn rgb(r: f32, g: f32, b: f32) -> Color { 44 | Color::Rgba(r, g, b, 1.0) 45 | } 46 | 47 | 48 | /// Create RGB colors from numbers between 0 and 255 inclusive. 49 | /// The alpha component is specified with numbers between 0 and 1. 50 | #[inline] 51 | pub fn rgba_bytes(r: u8, g: u8, b: u8, a: f32) -> Color { 52 | Color::Rgba(r as f32 / 255.0, g as f32 / 255.0, b as f32 / 255.0, a) 53 | } 54 | 55 | 56 | /// Create RGB colors from numbers between 0 and 255 inclusive. 57 | #[inline] 58 | pub fn rgb_bytes(r: u8, g: u8, b: u8) -> Color { 59 | rgba_bytes(r, g, b, 1.0) 60 | } 61 | 62 | 63 | /// Create [HSL colors](http://en.wikipedia.org/wiki/HSL_and_HSV) with an alpha component for 64 | /// transparency. 65 | #[inline] 66 | pub fn hsla(hue: f32, saturation: f32, lightness: f32, alpha: f32) -> Color { 67 | Color::Hsla(hue - turns((hue / (2.0 * PI)).floor()), saturation, lightness, alpha) 68 | } 69 | 70 | 71 | /// Create [HSL colors](http://en.wikipedia.org/wiki/HSL_and_HSV). This gives you access to colors 72 | /// more like a color wheel, where all hues are arranged in a circle that you specify with radians. 73 | /// 74 | /// red = hsl(degrees(0.0) , 1.0 , 0.5) 75 | /// green = hsl(degrees(120.0) , 1.0 , 0.5) 76 | /// blue = hsl(degrees(240.0) , 1.0 , 0.5) 77 | /// pastel_red = hsl(degrees(0.0) , 0.7 , 0.7) 78 | /// 79 | /// To cycle through all colors, just cycle through degrees. The saturation level is how vibrant 80 | /// the color is, like a dial between grey and bright colors. The lightness level is a dial between 81 | /// white and black. 82 | #[inline] 83 | pub fn hsl(hue: f32, saturation: f32, lightness: f32) -> Color { 84 | hsla(hue, saturation, lightness, 1.0) 85 | } 86 | 87 | 88 | /// Produce a gray based on the input. 0.0 is white, 1.0 is black. 89 | pub fn grayscale(p: f32) -> Color { 90 | Color::Hsla(0.0, 0.0, 1.0-p, 1.0) 91 | } 92 | /// Produce a gray based on the input. 0.0 is white, 1.0 is black. 93 | pub fn greyscale(p: f32) -> Color { 94 | Color::Hsla(0.0, 0.0, 1.0-p, 1.0) 95 | } 96 | 97 | 98 | /// Construct a random color. 99 | pub fn random() -> Color { 100 | rgb(::rand::random(), ::rand::random(), ::rand::random()) 101 | } 102 | 103 | 104 | impl Color { 105 | 106 | /// Produce a complementary color. The two colors will accent each other. This is the same as 107 | /// rotating the hue by 180 degrees. 108 | pub fn complement(self) -> Color { 109 | match self { 110 | Color::Hsla(h, s, l, a) => hsla(h + degrees(180.0), s, l, a), 111 | Color::Rgba(r, g, b, a) => { 112 | let (h, s, l) = rgb_to_hsl(r, g, b); 113 | hsla(h + degrees(180.0), s, l, a) 114 | }, 115 | } 116 | } 117 | 118 | /// Calculate and return the luminance of the Color. 119 | pub fn luminance(&self) -> f32 { 120 | match *self { 121 | Color::Rgba(r, g, b, _) => (r + g + b) / 3.0, 122 | Color::Hsla(_, _, l, _) => l, 123 | } 124 | } 125 | 126 | /// Return either black or white, depending which contrasts the Color the most. This will be 127 | /// useful for determining a readable color for text on any given background Color. 128 | pub fn plain_contrast(self) -> Color { 129 | if self.luminance() > 0.5 { black() } else { white() } 130 | } 131 | 132 | /// Extract the components of a color in the HSL format. 133 | pub fn to_hsl(self) -> Hsla { 134 | match self { 135 | Color::Hsla(h, s, l, a) => Hsla(h, s, l, a), 136 | Color::Rgba(r, g, b, a) => { 137 | let (h, s, l) = rgb_to_hsl(r, g, b); 138 | Hsla(h, s, l, a) 139 | }, 140 | } 141 | } 142 | 143 | /// Extract the components of a color in the RGB format. 144 | pub fn to_rgb(self) -> Rgba { 145 | match self { 146 | Color::Rgba(r, g, b, a) => Rgba(r, g, b, a), 147 | Color::Hsla(h, s, l, a) => { 148 | let (r, g, b) = hsl_to_rgb(h, s, l); 149 | Rgba(r, g, b, a) 150 | }, 151 | } 152 | } 153 | 154 | /// Extract the components of a color in the RGB format within a fixed-size array. 155 | pub fn to_fsa(self) -> [f32; 4] { 156 | let Rgba(r, g, b, a) = self.to_rgb(); 157 | [r, g, b, a] 158 | } 159 | 160 | /// Same as `to_fsa`, except r, g, b and a are represented in byte form. 161 | pub fn to_byte_fsa(self) -> [u8; 4] { 162 | let Rgba(r, g, b, a) = self.to_rgb(); 163 | [f32_to_byte(r), f32_to_byte(g), f32_to_byte(b), f32_to_byte(a)] 164 | } 165 | 166 | /// Return the hex representation of this color in the format #RRGGBBAA 167 | /// e.g. `Color(1.0, 0.0, 5.0, 1.0) == "#FF0080FF"` 168 | pub fn to_hex(self) -> String { 169 | let vals = self.to_byte_fsa(); 170 | let hex = vals.to_hex().to_ascii_uppercase(); 171 | format!("#{}", &hex) 172 | } 173 | 174 | /// Return the same color but with the given luminance. 175 | pub fn with_luminance(self, l: f32) -> Color { 176 | let Hsla(h, s, _, a) = self.to_hsl(); 177 | Color::Hsla(h, s, l, a) 178 | } 179 | 180 | /// Return the same color but with the alpha multiplied by the given alpha. 181 | pub fn alpha(self, alpha: f32) -> Color { 182 | match self { 183 | Color::Rgba(r, g, b, a) => Color::Rgba(r, g, b, a * alpha), 184 | Color::Hsla(h, s, l, a) => Color::Hsla(h, s, l, a * alpha), 185 | } 186 | } 187 | 188 | /// Return the same color but with the given alpha. 189 | pub fn with_alpha(self, a: f32) -> Color { 190 | match self { 191 | Color::Rgba(r, g, b, _) => Color::Rgba(r, g, b, a), 192 | Color::Hsla(h, s, l, _) => Color::Hsla(h, s, l, a), 193 | } 194 | } 195 | 196 | /// Return a highlighted version of the current Color. 197 | pub fn highlighted(self) -> Color { 198 | let luminance = self.luminance(); 199 | let Rgba(r, g, b, a) = self.to_rgb(); 200 | let (r, g, b) = { 201 | if luminance > 0.8 { (r - 0.2, g - 0.2, b - 0.2) } 202 | else if luminance < 0.2 { (r + 0.2, g + 0.2, b + 0.2) } 203 | else { 204 | (clampf32((1.0 - r) * 0.5 * r + r), 205 | clampf32((1.0 - g) * 0.1 * g + g), 206 | clampf32((1.0 - b) * 0.1 * b + b)) 207 | } 208 | }; 209 | let a = clampf32((1.0 - a) * 0.5 + a); 210 | rgba(r, g, b, a) 211 | } 212 | 213 | /// Return a clicked version of the current Color. 214 | pub fn clicked(&self) -> Color { 215 | let luminance = self.luminance(); 216 | let Rgba(r, g, b, a) = self.to_rgb(); 217 | let (r, g, b) = { 218 | if luminance > 0.8 { (r , g - 0.2, b - 0.2) } 219 | else if luminance < 0.2 { (r + 0.4, g + 0.2, b + 0.2) } 220 | else { 221 | (clampf32((1.0 - r) * 0.75 + r), 222 | clampf32((1.0 - g) * 0.25 + g), 223 | clampf32((1.0 - b) * 0.25 + b)) 224 | } 225 | }; 226 | let a = clampf32((1.0 - a) * 0.75 + a); 227 | rgba(r, g, b, a) 228 | } 229 | 230 | /// Return the Color's invert. 231 | pub fn invert(self) -> Color { 232 | let Rgba(r, g, b, a) = self.to_rgb(); 233 | rgba((r - 1.0).abs(), (g - 1.0).abs(), (b - 1.0).abs(), a) 234 | } 235 | 236 | /// Return the red value. 237 | pub fn red(&self) -> f32 { 238 | let Rgba(r, _, _, _) = self.to_rgb(); 239 | r 240 | } 241 | 242 | /// Return the green value. 243 | pub fn green(&self) -> f32 { 244 | let Rgba(_, g, _, _) = self.to_rgb(); 245 | g 246 | } 247 | 248 | /// Return the blue value. 249 | pub fn blue(&self) -> f32 { 250 | let Rgba(_, _, b, _) = self.to_rgb(); 251 | b 252 | } 253 | 254 | /// Set the red value. 255 | pub fn set_red(&mut self, r: f32) { 256 | let Rgba(_, g, b, a) = self.to_rgb(); 257 | *self = rgba(r, g, b, a); 258 | } 259 | 260 | /// Set the green value. 261 | pub fn set_green(&mut self, g: f32) { 262 | let Rgba(r, _, b, a) = self.to_rgb(); 263 | *self = rgba(r, g, b, a); 264 | } 265 | 266 | /// Set the blue value. 267 | pub fn set_blue(&mut self, b: f32) { 268 | let Rgba(r, g, _, a) = self.to_rgb(); 269 | *self = rgba(r, g, b, a); 270 | } 271 | 272 | } 273 | 274 | 275 | /// The parts of HSL along with an alpha for transparency. 276 | #[derive(Copy, Clone, Debug)] 277 | pub struct Hsla(pub f32, pub f32, pub f32, pub f32); 278 | 279 | 280 | /// The parts of RGB along with an alpha for transparency. 281 | #[derive(Copy, Clone, Debug)] 282 | pub struct Rgba(pub f32, pub f32, pub f32, pub f32); 283 | 284 | 285 | /// Convert an f32 color to a byte. 286 | #[inline] 287 | pub fn f32_to_byte(c: f32) -> u8 { (c * 255.0) as u8 } 288 | 289 | 290 | /// Pure function for converting rgb to hsl. 291 | pub fn rgb_to_hsl(r: f32, g: f32, b: f32) -> (f32, f32, f32) { 292 | let c_max = max(max(r, g), b); 293 | let c_min = min(min(r, g), b); 294 | let c = c_max - c_min; 295 | 296 | let hue = if c == 0.0 { 297 | // If there's no difference in the channels we have grayscale, so the hue is undefined. 298 | 0.0 299 | } else { 300 | degrees(60.0) * if c_max == r { fmod(((g - b) / c), 6) } 301 | else if c_max == g { ((b - r) / c) + 2.0 } 302 | else { ((r - g) / c) + 4.0 } 303 | }; 304 | 305 | let lightness = (c_max + c_min) / 2.0; 306 | let saturation = if lightness == 0.0 { 0.0 } 307 | else { c / (1.0 - (2.0 * lightness - 1.0).abs()) }; 308 | (hue, saturation, lightness) 309 | } 310 | 311 | 312 | /// Pure function for converting hsl to rgb. 313 | pub fn hsl_to_rgb(hue: f32, saturation: f32, lightness: f32) -> (f32, f32, f32) { 314 | let chroma = (1.0 - (2.0 * lightness - 1.0).abs()) * saturation; 315 | let hue = hue / degrees(60.0); 316 | let x = chroma * (1.0 - (fmod(hue, 2) - 1.0).abs()); 317 | let (r, g, b) = match hue { 318 | hue if hue < 0.0 => (0.0, 0.0, 0.0), 319 | hue if hue < 1.0 => (chroma, x, 0.0), 320 | hue if hue < 2.0 => (x, chroma, 0.0), 321 | hue if hue < 3.0 => (0.0, chroma, x), 322 | hue if hue < 4.0 => (0.0, x, chroma), 323 | hue if hue < 5.0 => (x, 0.0, chroma), 324 | hue if hue < 6.0 => (chroma, 0.0, x), 325 | _ => (0.0, 0.0, 0.0), 326 | }; 327 | let m = lightness - chroma / 2.0; 328 | (r + m, g + m, b + m) 329 | } 330 | 331 | 332 | /// Linear or Radial Gradient. 333 | #[derive(Clone, Debug)] 334 | pub enum Gradient { 335 | /// Takes a start and end point and then a series of color stops that indicate how to 336 | /// interpolate between the start and end points. 337 | Linear((f64, f64), (f64, f64), Vec<(f64, Color)>), 338 | /// First takes a start point and inner radius. Then takes an end point and outer radius. 339 | /// It then takes a series of color stops that indicate how to interpolate between the 340 | /// inner and outer circles. 341 | Radial((f64, f64), f64, (f64, f64), f64, Vec<(f64, Color)>), 342 | } 343 | 344 | 345 | /// Create a linear gradient. 346 | pub fn linear(start: (f64, f64), end: (f64, f64), colors: Vec<(f64, Color)>) -> Gradient { 347 | Gradient::Linear(start, end, colors) 348 | } 349 | 350 | 351 | /// Create a radial gradient. 352 | pub fn radial(start: (f64, f64), start_r: f64, 353 | end: (f64, f64), end_r: f64, 354 | colors: Vec<(f64, Color)>) -> Gradient { 355 | Gradient::Radial(start, start_r, end, end_r, colors) 356 | } 357 | 358 | 359 | /// Built-in colors. 360 | /// 361 | /// These colors come from the 362 | /// [Tango palette](http://tango.freedesktop.org/Tango_Icon_Theme_Guidelines) which provides 363 | /// aesthetically reasonable defaults for colors. Each color also comes with a light and dark 364 | /// version. 365 | 366 | /// Scarlet Red - Light - #EF2929 367 | pub fn light_red() -> Color { rgb_bytes(239 , 41 , 41 ) } 368 | /// Scarlet Red - Regular - #CC0000 369 | pub fn red() -> Color { rgb_bytes(204 , 0 , 0 ) } 370 | /// Scarlet Red - Dark - #A30000 371 | pub fn dark_red() -> Color { rgb_bytes(164 , 0 , 0 ) } 372 | 373 | /// Orange - Light - #FCAF3E 374 | pub fn light_orange() -> Color { rgb_bytes(252 , 175 , 62 ) } 375 | /// Orange - Regular - #F57900 376 | pub fn orange() -> Color { rgb_bytes(245 , 121 , 0 ) } 377 | /// Orange - Dark - #CE5C00 378 | pub fn dark_orange() -> Color { rgb_bytes(206 , 92 , 0 ) } 379 | 380 | /// Butter - Light - #FCE94F 381 | pub fn light_yellow() -> Color { rgb_bytes(255 , 233 , 79 ) } 382 | /// Butter - Regular - #EDD400 383 | pub fn yellow() -> Color { rgb_bytes(237 , 212 , 0 ) } 384 | /// Butter - Dark - #C4A000 385 | pub fn dark_yellow() -> Color { rgb_bytes(196 , 160 , 0 ) } 386 | 387 | /// Chameleon - Light - #8AE234 388 | pub fn light_green() -> Color { rgb_bytes(138 , 226 , 52 ) } 389 | /// Chameleon - Regular - #73D216 390 | pub fn green() -> Color { rgb_bytes(115 , 210 , 22 ) } 391 | /// Chameleon - Dark - #4E9A06 392 | pub fn dark_green() -> Color { rgb_bytes(78 , 154 , 6 ) } 393 | 394 | /// Sky Blue - Light - #729FCF 395 | pub fn light_blue() -> Color { rgb_bytes(114 , 159 , 207) } 396 | /// Sky Blue - Regular - #3465A4 397 | pub fn blue() -> Color { rgb_bytes(52 , 101 , 164) } 398 | /// Sky Blue - Dark - #204A87 399 | pub fn dark_blue() -> Color { rgb_bytes(32 , 74 , 135) } 400 | 401 | /// Plum - Light - #AD7FA8 402 | pub fn light_purple() -> Color { rgb_bytes(173 , 127 , 168) } 403 | /// Plum - Regular - #75507B 404 | pub fn purple() -> Color { rgb_bytes(117 , 80 , 123) } 405 | /// Plum - Dark - #5C3566 406 | pub fn dark_purple() -> Color { rgb_bytes(92 , 53 , 102) } 407 | 408 | /// Chocolate - Light - #E9B96E 409 | pub fn light_brown() -> Color { rgb_bytes(233 , 185 , 110) } 410 | /// Chocolate - Regular - #C17D11 411 | pub fn brown() -> Color { rgb_bytes(193 , 125 , 17 ) } 412 | /// Chocolate - Dark - #8F5902 413 | pub fn dark_brown() -> Color { rgb_bytes(143 , 89 , 2 ) } 414 | 415 | /// Straight Black. 416 | pub fn black() -> Color { rgb_bytes(0 , 0 , 0 ) } 417 | /// Straight White. 418 | pub fn white() -> Color { rgb_bytes(255 , 255 , 255) } 419 | 420 | /// Alluminium - Light 421 | pub fn light_gray() -> Color { rgb_bytes(238 , 238 , 236) } 422 | /// Alluminium - Regular 423 | pub fn gray() -> Color { rgb_bytes(211 , 215 , 207) } 424 | /// Alluminium - Dark 425 | pub fn dark_gray() -> Color { rgb_bytes(186 , 189 , 182) } 426 | 427 | /// Aluminium - Light - #EEEEEC 428 | pub fn light_grey() -> Color { rgb_bytes(238 , 238 , 236) } 429 | /// Aluminium - Regular - #D3D7CF 430 | pub fn grey() -> Color { rgb_bytes(211 , 215 , 207) } 431 | /// Aluminium - Dark - #BABDB6 432 | pub fn dark_grey() -> Color { rgb_bytes(186 , 189 , 182) } 433 | 434 | /// Charcoal - Light - #888A85 435 | pub fn light_charcoal() -> Color { rgb_bytes(136 , 138 , 133) } 436 | /// Charcoal - Regular - #555753 437 | pub fn charcoal() -> Color { rgb_bytes(85 , 87 , 83 ) } 438 | /// Charcoal - Dark - #2E3436 439 | pub fn dark_charcoal() -> Color { rgb_bytes(46 , 52 , 54 ) } 440 | 441 | 442 | 443 | /// Types that can be colored. 444 | pub trait Colorable: Sized { 445 | 446 | /// Set the color of the widget. 447 | fn color(self, color: Color) -> Self; 448 | 449 | /// Set the color of the widget from rgba values. 450 | fn rgba(self, r: f32, g: f32, b: f32, a: f32) -> Self { 451 | self.color(rgba(r, g, b, a)) 452 | } 453 | 454 | /// Set the color of the widget from rgb values. 455 | fn rgb(self, r: f32, g: f32, b: f32) -> Self { 456 | self.color(rgb(r, g, b)) 457 | } 458 | 459 | /// Set the color of the widget from hsla values. 460 | fn hsla(self, h: f32, s: f32, l: f32, a: f32) -> Self { 461 | self.color(hsla(h, s, l, a)) 462 | } 463 | 464 | /// Set the color of the widget from hsl values. 465 | fn hsl(self, h: f32, s: f32, l: f32) -> Self { 466 | self.color(hsl(h, s, l)) 467 | } 468 | 469 | } 470 | 471 | -------------------------------------------------------------------------------- /src/element.rs: -------------------------------------------------------------------------------- 1 | //! 2 | //! Ported from [elm-lang's `Graphics.Element` module] 3 | //! (https://github.com/elm-lang/core/blob/1.1.1/src/Graphics/Element.elm) 4 | //! 5 | //! 6 | //! Graphical elements that snap together to build complex widgets and layouts. 7 | //! 8 | //! Each element is a rectangle with a known width and height, making them easy to combine and 9 | //! position. 10 | //! 11 | //! 12 | //! # Images 13 | //! 14 | //! image, fitted_image, cropped_image, tiled_image 15 | //! 16 | //! 17 | //! # Styling 18 | //! 19 | //! width, height, size, color, opacity 20 | //! 21 | //! 22 | //! # Inspection 23 | //! 24 | //! width_of, height_of, size_of 25 | //! 26 | //! 27 | //! # Layout 28 | //! 29 | //! flow, up, down, left, right, inward, outward 30 | //! 31 | //! ## Layout Aliases 32 | //! 33 | //! There are some convenience functions for working with `flow` in specific cases: 34 | //! 35 | //! layers, above, below, beside 36 | //! 37 | //! 38 | //! # Positioning 39 | //! empty, spacer, container 40 | //! 41 | //! ## Specific Positions 42 | //! 43 | //! To create a `Position` you can use any of the built-in positions which cover nine common 44 | //! positions: 45 | //! 46 | //! middle, mid_top, mid_bottom, mid_left, mid_right, top_left, top_right, bottom_left, 47 | //! bottom_right 48 | //! 49 | //! If you need more precision, you can create custom positions: 50 | //! 51 | //! absolute, relative, middle_at, mid_top_at, mid_bottom_at, mid_left_at, mid_right_at, 52 | //! top_left_at, top_right_at, bottom_left_at, bottom_right_at 53 | //! 54 | 55 | use color::Color; 56 | use form::{self, Form}; 57 | use graphics::character::CharacterCache; 58 | use graphics::{Context, Graphics, Transformed}; 59 | use self::Three::{P, Z, N}; 60 | use std::path::PathBuf; 61 | use transform_2d; 62 | 63 | 64 | /// An Element's Properties. 65 | #[derive(Clone, Debug)] 66 | pub struct Properties { 67 | pub width: i32, 68 | pub height: i32, 69 | pub opacity: f32, 70 | pub crop: Option<(f64, f64, f64, f64)>, 71 | pub color: Option, 72 | } 73 | 74 | 75 | /// Graphical elements that snap together to build complex widgets and layouts. 76 | /// 77 | /// Each element is a rectangle with a known width and height, making them easy to combine and 78 | /// position. 79 | #[derive(Clone, Debug)] 80 | pub struct Element { 81 | pub props: Properties, 82 | pub element: Prim, 83 | } 84 | 85 | 86 | impl Element { 87 | 88 | /// Create an `Element` with a given width. 89 | #[inline] 90 | pub fn width(self, new_width: i32) -> Element { 91 | let Element { props, element } = self; 92 | let new_props = match element { 93 | Prim::Image(_, w, h, _) | Prim::Collage(w, h, _) => { 94 | Properties { 95 | height: (h as f32 / w as f32 * new_width as f32).round() as i32, 96 | ..props 97 | } 98 | }, 99 | _ => props, 100 | }; 101 | Element { props: new_props, element: element } 102 | } 103 | 104 | /// Create an `Element` with a given height. 105 | #[inline] 106 | pub fn height(self, new_height: i32) -> Element { 107 | let Element { props, element } = self; 108 | let new_props = match element { 109 | Prim::Image(_, w, h, _) | Prim::Collage(w, h, _) => { 110 | Properties { 111 | width: (w as f32 / h as f32 * new_height as f32).round() as i32, 112 | ..props 113 | } 114 | }, 115 | _ => props, 116 | }; 117 | Element { props: new_props, element: element } 118 | } 119 | 120 | /// Create an `Element` with a given size. 121 | #[inline] 122 | pub fn size(self, new_w: i32, new_h: i32) -> Element { 123 | self.height(new_h).width(new_w) 124 | } 125 | 126 | /// Create an `Element` with a given opacity. 127 | #[inline] 128 | pub fn opacity(mut self, opacity: f32) -> Element { 129 | self.props.opacity = opacity; 130 | self 131 | } 132 | 133 | /// Create an `Element with a given background color. 134 | #[inline] 135 | pub fn color(mut self, color: Color) -> Element { 136 | self.props.color = Some(color); 137 | self 138 | } 139 | 140 | /// Crops an `Element` with the given rectangle. 141 | #[inline] 142 | pub fn crop(self, x: f64, y: f64, w: f64, h: f64) -> Element { 143 | let Element { props, element } = self; 144 | let new_props = Properties { crop: Some((x, y, w, h)), ..props }; 145 | Element { props: new_props, element: element } 146 | } 147 | 148 | /// Put an element in a container. This lets you position the element really easily, and there are 149 | /// tons of ways to set the `Position`. 150 | #[inline] 151 | pub fn container(self, w: i32, h: i32, pos: Position) -> Element { 152 | new_element(w, h, Prim::Container(pos, Box::new(self))) 153 | } 154 | 155 | /// Put an element in a cleared wrapper. The color provided will be the color that clears the 156 | /// screen before rendering the contained element. 157 | #[inline] 158 | pub fn clear(self, color: Color) -> Element { 159 | new_element(self.get_width(), self.get_height(), 160 | Prim::Cleared(color, Box::new(self))) 161 | } 162 | 163 | /// Stack elements vertically. To put `a` above `b` you would say: `a.above(b)` 164 | #[inline] 165 | pub fn above(self, other: Element) -> Element { 166 | new_element(::std::cmp::max(self.get_width(), other.get_width()), 167 | self.get_height() + other.get_height(), 168 | Prim::Flow(down(), vec![self, other])) 169 | } 170 | 171 | /// Stack elements vertically. To put `a` below `b` you would say: `a.below(b)` 172 | #[inline] 173 | pub fn below(self, other: Element) -> Element { 174 | other.above(self) 175 | } 176 | 177 | /// Put elements beside each other horizontally. To put `b` to the right of `a` you would say: 178 | /// `a.beside(b)` 179 | #[inline] 180 | pub fn beside(self, other: Element) -> Element { 181 | new_element(self.get_width() + other.get_width(), 182 | ::std::cmp::max(self.get_height(), other.get_height()), 183 | Prim::Flow(right(), vec![self, other])) 184 | } 185 | 186 | /// Return the width of the Element. 187 | pub fn get_width(&self) -> i32 { self.props.width } 188 | 189 | /// Return the height of the Element. 190 | pub fn get_height(&self) -> i32 { self.props.height } 191 | 192 | /// Return the size of the Element's bounding rectangle. 193 | pub fn get_size(&self) -> (i32, i32) { (self.props.width, self.props.height) } 194 | 195 | /// Draw the form with some given graphics backend. 196 | #[inline] 197 | pub fn draw<'a, C, G>(&self, renderer: &mut Renderer<'a, C, G>) 198 | where 199 | C: CharacterCache, 200 | G: Graphics, 201 | { 202 | let Renderer { 203 | context, 204 | ref mut backend, 205 | ref mut maybe_character_cache, 206 | } = *renderer; 207 | let view_size = context.get_view_size(); 208 | let context = context.trans(view_size[0] / 2.0, view_size[1] / 2.0).scale(1.0, -1.0); 209 | draw_element(self, 1.0, *backend, maybe_character_cache, context); 210 | } 211 | 212 | /// Return whether or not a point is over the element. 213 | pub fn is_over(&self, x: i32, y: i32) -> bool { 214 | unimplemented!(); 215 | } 216 | 217 | } 218 | 219 | /// Return the size of the Element. 220 | pub fn size_of(e: &Element) -> (i32, i32) { 221 | (e.props.width, e.props.height) 222 | } 223 | 224 | 225 | /// Construct a new Element from width, height and some Prim. 226 | /// Iterates the global GUID counter by one and returns that as the Element id. 227 | #[inline] 228 | pub fn new_element(w: i32, h: i32, element: Prim) -> Element { 229 | Element { 230 | props: Properties { 231 | width: w, 232 | height: h, 233 | opacity: 1.0, 234 | color: None, 235 | crop: None, 236 | }, 237 | element: element, 238 | } 239 | } 240 | 241 | 242 | /// Create an empty box. this is useful for getting your spacing right and making borders. 243 | pub fn spacer(w: i32, h: i32) -> Element { 244 | new_element(w, h, Prim::Spacer) 245 | } 246 | 247 | 248 | /// An Element that takes up no space. Good for things that appear conditionally. 249 | pub fn empty() -> Element { 250 | spacer(0, 0) 251 | } 252 | 253 | 254 | /// The various kinds of Elements. 255 | #[derive(Clone, Debug)] 256 | pub enum Prim { 257 | Image(ImageStyle, i32, i32, PathBuf), 258 | Container(Position, Box), 259 | Flow(Direction, Vec), 260 | Collage(i32, i32, Vec
), 261 | Cleared(Color, Box), 262 | Spacer, 263 | } 264 | 265 | 266 | /// Styling for the Image Element. 267 | #[derive(Copy, Clone, Debug)] 268 | pub enum ImageStyle { 269 | Plain, 270 | Fitted, 271 | Cropped(i32, i32), 272 | Tiled, 273 | } 274 | 275 | 276 | /// Create an image given a width, height and texture. 277 | pub fn image(w: i32, h: i32, path: PathBuf) -> Element { 278 | new_element(w, h, Prim::Image(ImageStyle::Plain, w, h, path)) 279 | } 280 | 281 | /// Create a fitted image given a width, height and texture. This will crop the picture to best 282 | /// fill the given dimensions. 283 | pub fn fitted_image(w: i32, h: i32, path: PathBuf) -> Element { 284 | new_element(w, h, Prim::Image(ImageStyle::Fitted, w, h, path)) 285 | } 286 | 287 | /// Create a cropped image. Take a rectangle out of the picture starting at the given top left 288 | /// coordinate. 289 | pub fn cropped_image(x: i32, y: i32, w: i32, h: i32, path: PathBuf) -> Element { 290 | new_element(w, h, Prim::Image(ImageStyle::Cropped(x, y), w, h, path)) 291 | } 292 | 293 | /// Create a tiled image given a width, height and texture. 294 | pub fn tiled_image(w: i32, h: i32, path: PathBuf) -> Element { 295 | new_element(w, h, Prim::Image(ImageStyle::Tiled, w, h, path)) 296 | } 297 | 298 | 299 | #[derive(Copy, Clone, Debug)] 300 | pub enum Three { P, Z, N } 301 | #[derive(Copy, Clone, Debug)] 302 | pub enum Pos { Absolute(i32), Relative(f32) } 303 | 304 | /// An element's Position. 305 | #[derive(Copy, Clone, Debug)] 306 | pub struct Position { 307 | horizontal: Three, 308 | vertical: Three, 309 | x: Pos, 310 | y: Pos, 311 | } 312 | 313 | /// The direction for a flow of `Element`s. 314 | #[derive(Copy, Clone, Debug)] 315 | pub enum Direction { Up, Down, Left, Right, In, Out } 316 | 317 | 318 | /// Have a list of elements flow in a particular direction. The `Direction` starts from the first 319 | /// element in the list. The result is an `Element`. 320 | pub fn flow(dir: Direction, elements: Vec) -> Element { 321 | if elements.is_empty() { return empty() } 322 | let max_w = elements.iter().map(|e| e.get_width()).max().unwrap(); 323 | let max_h = elements.iter().map(|e| e.get_height()).max().unwrap(); 324 | let sum_w = elements.iter().fold(0, |total, e| total + e.get_width()); 325 | let sum_h = elements.iter().fold(0, |total, e| total + e.get_height()); 326 | let new_flow = |w: i32, h: i32| new_element(w, h, Prim::Flow(dir, elements)); 327 | match dir { 328 | Direction::Up | Direction::Down => new_flow(max_w, sum_h), 329 | Direction::Left | Direction::Right => new_flow(sum_w, max_h), 330 | Direction::In | Direction::Out => new_flow(max_w, max_h), 331 | } 332 | } 333 | 334 | /// Layer elements on top of each other, starting from the bottom. 335 | pub fn layers(elements: Vec) -> Element { 336 | let max_w = elements.iter().map(|e| e.get_width()).max().unwrap_or(0); 337 | let max_h = elements.iter().map(|e| e.get_height()).max().unwrap_or(0); 338 | new_element(max_w, max_h, Prim::Flow(outward(), elements)) 339 | } 340 | 341 | 342 | /// Repetitive things. 343 | pub fn absolute(i: i32) -> Pos { Pos::Absolute(i) } 344 | pub fn relative(f: f32) -> Pos { Pos::Relative(f) } 345 | 346 | #[inline] 347 | fn p(h: Three, v: Three, x: Pos, y: Pos) -> Position { 348 | Position { horizontal: h, vertical: v, x: x, y: y } 349 | } 350 | 351 | pub fn middle() -> Position { p(Z, Z, relative(0.5), relative(0.5)) } 352 | pub fn top_left() -> Position { p(N, P, absolute(0), absolute(0)) } 353 | pub fn top_right() -> Position { p(P, P, absolute(0), absolute(0)) } 354 | pub fn bottom_left() -> Position { p(N, N, absolute(0), absolute(0)) } 355 | pub fn bottom_right() -> Position { p(P, N, absolute(0), absolute(0)) } 356 | pub fn mid_left() -> Position { p(N, Z, absolute(0), relative(0.5)) } 357 | pub fn mid_right() -> Position { p(P, Z, absolute(0), relative(0.5)) } 358 | pub fn mid_top() -> Position { p(Z, P, relative(0.5), absolute(0)) } 359 | pub fn mid_bottom() -> Position { p(Z, N, relative(0.5), absolute(0)) } 360 | 361 | pub fn middle_at(x: Pos, y: Pos) -> Position { p(Z, Z, x, y) } 362 | pub fn top_left_at(x: Pos, y: Pos) -> Position { p(N, P, x, y) } 363 | pub fn top_right_at(x: Pos, y: Pos) -> Position { p(P, P, x, y) } 364 | pub fn bottom_left_at(x: Pos, y: Pos) -> Position { p(N, N, x, y) } 365 | pub fn bottom_right_at(x: Pos, y: Pos) -> Position { p(P, N, x, y) } 366 | pub fn mid_left_at(x: Pos, y: Pos) -> Position { p(N, Z, x, y) } 367 | pub fn mid_right_at(x: Pos, y: Pos) -> Position { p(P, Z, x, y) } 368 | pub fn mid_top_at(x: Pos, y: Pos) -> Position { p(Z, P, x, y) } 369 | pub fn mid_bottom_at(x: Pos, y: Pos) -> Position { p(Z, N, x, y) } 370 | 371 | pub fn up() -> Direction { Direction::Up } 372 | pub fn down() -> Direction { Direction::Down } 373 | pub fn left() -> Direction { Direction::Left } 374 | pub fn right() -> Direction { Direction::Right } 375 | pub fn inward() -> Direction { Direction::In } 376 | pub fn outward() -> Direction { Direction::Out } 377 | 378 | 379 | 380 | 381 | 382 | 383 | 384 | 385 | /// 386 | /// CUSTOM NON-ELM FUNCTIONS 387 | /// 388 | 389 | 390 | 391 | /// Used for rendering elmesque `Element`s. 392 | pub struct Renderer<'a, C: 'a, G: 'a> { 393 | context: Context, 394 | backend: &'a mut G, 395 | maybe_character_cache: Option<&'a mut C>, 396 | } 397 | 398 | impl<'a, C, G> Renderer<'a, C, G> { 399 | 400 | /// Construct a renderer, used for rendering elmesque `Element`s. 401 | pub fn new(context: Context, backend: &'a mut G) -> Renderer<'a, C, G> { 402 | Renderer { 403 | context: context, 404 | backend: backend, 405 | maybe_character_cache: None, 406 | } 407 | } 408 | 409 | /// Builder method for constructing a Renderer with a GlyphCache for drawing text. 410 | pub fn character_cache(self, character_cache: &'a mut C) -> Renderer<'a, C, G> { 411 | Renderer { maybe_character_cache: Some(character_cache), ..self } 412 | } 413 | 414 | } 415 | 416 | 417 | 418 | /// Draw an Element. 419 | pub fn draw_element<'a, C: CharacterCache, G: Graphics>( 420 | element: &Element, 421 | opacity: f32, 422 | backend: &mut G, 423 | maybe_character_cache: &mut Option<&mut C>, 424 | context: Context, 425 | ) { 426 | let Element { ref props, ref element } = *element; 427 | 428 | // Crop the Element if some crop was given. 429 | // We'll use the `DrawState::scissor` method for this. 430 | // 431 | // Because `DrawState`'s `scissor` `Rect` uses bottom-left origin coords, we'll have to convert 432 | // from our centered-origin coordinate system. 433 | // 434 | // We'll also need to stretch our coords to match the correct viewport.draw_size. 435 | let context = match props.crop { 436 | Some((x, y, w, h)) => { 437 | use utils::{clamp, map_range}; 438 | let Context { draw_state, .. } = context; 439 | 440 | // Our view_dim is our virtual window size which is consistent no matter the display. 441 | let view_dim = context.get_view_size(); 442 | 443 | // Our draw_dim is the actual window size in pixels. Our target crop area must be 444 | // represented in this size. 445 | let draw_dim = match context.viewport { 446 | Some(viewport) => [viewport.draw_size[0] as f64, viewport.draw_size[1] as f64], 447 | None => view_dim, 448 | }; 449 | 450 | // Calculate the distance to the edges of the window from the center. 451 | let left = -view_dim[0] / 2.0; 452 | let right = view_dim[0] / 2.0; 453 | let bottom = -view_dim[1] / 2.0; 454 | let top = view_dim[1] / 2.0; 455 | 456 | // We start with the x and y in the center of our crop area, however we need it to be 457 | // at the top left of the crop area. 458 | let left_x = x - w as f64 / 2.0; 459 | let top_y = y - h as f64 / 2.0; 460 | 461 | // Map the position at the top left of the crop area in view_dim to our draw_dim. 462 | let x = map_range(left_x, left, right, 0, draw_dim[0] as i32); 463 | let y = map_range(top_y, bottom, top, 0, draw_dim[1] as i32); 464 | 465 | // Convert the w and h from our view_dim to the draw_dim. 466 | let w_scale = draw_dim[0] / view_dim[0]; 467 | let h_scale = draw_dim[1] / view_dim[1]; 468 | let w = w * w_scale; 469 | let h = h * h_scale; 470 | 471 | // If we ended up with negative coords for the crop area, we'll use 0 instead as we 472 | // can't represent the negative coords with `u16` (the target DrawState dimension type). 473 | // We'll hold onto the lost negative values (x_neg and y_neg) so that we can compensate 474 | // with the width and height. 475 | let x_neg = if x < 0 { x } else { 0 }; 476 | let y_neg = if y < 0 { y } else { 0 }; 477 | let mut x = ::std::cmp::max(0, x) as u16; 478 | let mut y = ::std::cmp::max(0, y) as u16; 479 | let mut w = ::std::cmp::max(0, (w as i32 + x_neg)) as u16; 480 | let mut h = ::std::cmp::max(0, (h as i32 + y_neg)) as u16; 481 | 482 | // If there was already some scissor set, we must check for the intersection. 483 | if let Some(rect) = draw_state.scissor { 484 | if x + w < rect.x || rect.x + rect.w < x || y + h < rect.y || rect.y + rect.h < y { 485 | // If there is no intersection, we have no scissor. 486 | w = 0; 487 | h = 0; 488 | } else { 489 | // If there is some intersection, calculate the overlapping rect. 490 | let (a_l, a_r, a_b, a_t) = (x, x+w, y, y+h); 491 | let (b_l, b_r, b_b, b_t) = (rect.x, rect.x+rect.w, rect.y, rect.y+rect.h); 492 | let l = if a_l > b_l { a_l } else { b_l }; 493 | let r = if a_r < b_r { a_r } else { b_r }; 494 | let b = if a_b > b_b { a_b } else { b_b }; 495 | let t = if a_t < b_t { a_t } else { b_t }; 496 | x = l; 497 | y = b; 498 | w = r - l; 499 | h = t - b; 500 | } 501 | } 502 | 503 | Context { draw_state: draw_state.scissor(x, y, w, h), ..context } 504 | }, 505 | None => context, 506 | }; 507 | 508 | match *element { 509 | 510 | Prim::Image(style, w, h, ref path) => { 511 | let Properties { width, height, opacity, color, .. } = *props; 512 | match style { 513 | ImageStyle::Plain => { 514 | // let image = graphics::Image { 515 | // color: None, 516 | // rectangle: None, 517 | // source_rectangle: Some([src_x, src_y, w, h]), 518 | // }; 519 | // let image = Image::new(); 520 | // let texture: &Texture = ::std::ops::Deref::deref(&texture); 521 | // image.draw(texture, draw_state, matrix, backend); 522 | unimplemented!(); 523 | }, 524 | ImageStyle::Fitted => { 525 | unimplemented!(); 526 | }, 527 | ImageStyle::Cropped(x, y) => { 528 | unimplemented!(); 529 | }, 530 | ImageStyle::Tiled => { 531 | unimplemented!(); 532 | }, 533 | } 534 | }, 535 | 536 | Prim::Container(position, ref element) => { 537 | let Position { horizontal, vertical, x, y } = position; 538 | let context = match (x, y) { 539 | (Pos::Relative(x), Pos::Relative(y)) => context.trans(x as f64, y as f64), 540 | (Pos::Absolute(x), Pos::Relative(y)) => Context { 541 | transform: transform_2d::matrix(1.0, 0.0, 0.0, 1.0, x as f64, 0.0).0, 542 | ..context 543 | }.trans(0.0, y as f64), 544 | (Pos::Relative(x), Pos::Absolute(y)) => Context { 545 | transform: transform_2d::matrix(1.0, 0.0, 0.0, 1.0, 0.0, y as f64).0, 546 | ..context 547 | }.trans(x as f64, 0.0), 548 | (Pos::Absolute(x), Pos::Absolute(y)) => Context { 549 | transform: transform_2d::matrix(1.0, 0.0, 0.0, 1.0, x as f64, y as f64).0, 550 | ..context 551 | }, 552 | }; 553 | let new_opacity = opacity * props.opacity; 554 | draw_element(element, new_opacity, backend, maybe_character_cache, context); 555 | } 556 | 557 | Prim::Flow(direction, ref elements) => { 558 | let mut context = context; 559 | match direction { 560 | Direction::Up | Direction::Down => { 561 | let multi = if let Direction::Up = direction { 1.0 } else { -1.0 }; 562 | let mut half_prev_height = 0.0; 563 | for element in elements.iter() { 564 | let half_height = element.get_height() as f64 / 2.0; 565 | let new_opacity = opacity * props.opacity; 566 | draw_element(element, new_opacity, backend, maybe_character_cache, context); 567 | let y_trans = half_height + half_prev_height; 568 | context = context.trans(0.0, y_trans * multi); 569 | half_prev_height = half_height; 570 | } 571 | }, 572 | Direction::Left | Direction::Right => { 573 | let multi = if let Direction::Right = direction { 1.0 } else { -1.0 }; 574 | let mut half_prev_width = 0.0; 575 | for element in elements.iter() { 576 | let half_width = element.get_width() as f64 / 2.0; 577 | let new_opacity = opacity * props.opacity; 578 | draw_element(element, new_opacity, backend, maybe_character_cache, context); 579 | let x_trans = half_width + half_prev_width; 580 | context = context.trans(x_trans * multi, 0.0); 581 | half_prev_width = half_width; 582 | } 583 | }, 584 | Direction::Out => { 585 | for element in elements.iter() { 586 | let new_opacity = opacity * props.opacity; 587 | draw_element(element, new_opacity, backend, maybe_character_cache, context); 588 | } 589 | } 590 | Direction::In => { 591 | for element in elements.iter().rev() { 592 | let new_opacity = opacity * props.opacity; 593 | draw_element(element, new_opacity, backend, maybe_character_cache, context); 594 | } 595 | } 596 | } 597 | }, 598 | 599 | Prim::Collage(w, h, ref forms) => { 600 | for form in forms.iter() { 601 | let new_opacity = opacity * props.opacity; 602 | form::draw_form(form, new_opacity, backend, maybe_character_cache, context); 603 | } 604 | }, 605 | 606 | Prim::Cleared(color, ref element) => { 607 | backend.clear_color(color.to_fsa()); 608 | draw_element(element, opacity, backend, maybe_character_cache, context); 609 | }, 610 | 611 | Prim::Spacer => {}, 612 | 613 | } 614 | } 615 | 616 | -------------------------------------------------------------------------------- /src/form.rs: -------------------------------------------------------------------------------- 1 | //! 2 | //! Ported from [elm-lang's `Graphics.Collage` module] 3 | //! (https://github.com/elm-lang/core/blob/62b22218c42fb8ccc996c86bea450a14991ab815/src/Graphics/Collage.elm). 4 | //! 5 | //! 6 | //! This module is for freeform graphics. You can shift, rotate, scale etc. All sorts of forms 7 | //! including lines, shapes, images and elements. 8 | //! 9 | //! Collages use the same coordinate system you might see in an algebra or physics problem. The 10 | //! origin (0, 0) is at the center of the collage, not the top left corner as in some other 11 | //! graphics libraries. Furthermore, the y-axis points up, so shifting a form 10 units in the 12 | //! y-axis will move it up screen. 13 | //! 14 | //! # Creating Forms 15 | //! to_form, filled, textured, gradient, outlined, traced, text, outlined_text 16 | //! 17 | //! # Transforming Forms 18 | //! shift, shift_x, shift_y, scale, rotate, alpha 19 | //! 20 | //! # Grouping Forms 21 | //! Grouping forms makes it easier to write modular graphics code. You can create a form that is a 22 | //! composite of many subforms. From there it is easy to transform it as a single unit. 23 | //! group, group_transform 24 | //! 25 | //! # Shapes 26 | //! rect, oval, square, circle, ngon, polygon 27 | //! 28 | //! # Paths 29 | //! segment, path 30 | //! 31 | //! # Line Styles 32 | //! solid, dashed, dotted, LineStyle, LineCap, LineJoin 33 | //! 34 | 35 | 36 | use color::{Color, Gradient}; 37 | use element::{self, Element, new_element}; 38 | use graphics::{self, Context, Graphics, Transformed}; 39 | use graphics::character::CharacterCache; 40 | use std::f64::consts::PI; 41 | use std::path::PathBuf; 42 | use text::Text; 43 | use transform_2d::{self, Transform2D}; 44 | 45 | 46 | /// A general, freeform 2D graphics structure. 47 | #[derive(Clone, Debug)] 48 | pub struct Form { 49 | pub theta: f64, 50 | pub scale: f64, 51 | pub x: f64, 52 | pub y: f64, 53 | pub alpha: f32, 54 | pub form: BasicForm, 55 | } 56 | 57 | 58 | #[derive(Clone, Debug)] 59 | pub enum FillStyle { 60 | Solid(Color), 61 | Texture(PathBuf), 62 | Grad(Gradient), 63 | } 64 | 65 | 66 | #[derive(Copy, Clone, Debug)] 67 | pub enum LineCap { 68 | Flat, 69 | Round, 70 | Padded, 71 | } 72 | 73 | 74 | #[derive(Copy, Clone, Debug)] 75 | pub enum LineJoin { 76 | Smooth, 77 | Sharp(f64), 78 | Clipped, 79 | } 80 | 81 | 82 | #[derive(Clone, Debug)] 83 | pub struct LineStyle { 84 | pub color: Color, 85 | pub width: f64, 86 | pub cap: LineCap, 87 | pub join: LineJoin, 88 | pub dashing: Vec, 89 | pub dash_offset: i64, 90 | } 91 | 92 | 93 | impl LineStyle { 94 | 95 | /// The default LineStyle. 96 | pub fn default() -> LineStyle { 97 | LineStyle { 98 | color: ::color::black(), 99 | width: 1.0, 100 | cap: LineCap::Flat, 101 | join: LineJoin::Sharp(10.0), 102 | dashing: Vec::new(), 103 | dash_offset: 0, 104 | } 105 | } 106 | 107 | /// The LineStyle with some given width. 108 | #[inline] 109 | pub fn width(self, w: f64) -> LineStyle { 110 | LineStyle { width: w, ..self } 111 | } 112 | 113 | } 114 | 115 | 116 | /// Create a solid line style with a given color. 117 | pub fn solid(color: Color) -> LineStyle { 118 | LineStyle { color: color, ..LineStyle::default() } 119 | } 120 | 121 | /// Create a dashed line style with a given color. Dashing equals `[8, 4]`. 122 | pub fn dashed(color: Color) -> LineStyle { 123 | LineStyle { color: color, dashing: vec![8, 4], ..LineStyle::default() } 124 | } 125 | 126 | /// Create a dotted line style with a given color. Dashing equals `[3, 3]`. 127 | pub fn dotted(color: Color) -> LineStyle { 128 | LineStyle { color: color, dashing: vec![3, 3], ..LineStyle::default() } 129 | } 130 | 131 | 132 | /// The basic variants a Form can consist of. 133 | #[derive(Clone, Debug)] 134 | pub enum BasicForm { 135 | PointPath(LineStyle, PointPath), 136 | Shape(ShapeStyle, Shape), 137 | OutlinedText(LineStyle, Text), 138 | Text(Text), 139 | Image(i32, i32, (i32, i32), PathBuf), 140 | Element(Element), 141 | Group(Transform2D, Vec), 142 | } 143 | 144 | 145 | /// Whether a shape is outlined or filled. 146 | #[derive(Clone, Debug)] 147 | pub enum ShapeStyle { 148 | Line(LineStyle), 149 | Fill(FillStyle), 150 | } 151 | 152 | 153 | impl Form { 154 | 155 | fn new(basic_form: BasicForm) -> Form { 156 | Form { 157 | theta: 0.0, 158 | scale: 1.0, 159 | x: 0.0, 160 | y: 0.0, 161 | alpha: 1.0, 162 | form: basic_form, 163 | } 164 | } 165 | 166 | 167 | /// Move a form by the given amount. this is a relative translation so `shift(10.0, 10.0, form) 168 | /// would move `form` ten pixels up and ten pixels to the right. 169 | #[inline] 170 | pub fn shift(self, x: f64, y: f64) -> Form { 171 | Form { x: self.x + x, y: self.y + y, ..self } 172 | } 173 | 174 | 175 | /// Move a shape in the x direction. This is relative so `shift_x(10.0, form)` moves `form` 10 176 | /// pixels to the right. 177 | #[inline] 178 | pub fn shift_x(self, x: f64) -> Form { 179 | self.shift(x, 0.0) 180 | } 181 | 182 | 183 | /// Move a shape in the y direction. This is relative so `shift_y(10.0, form)` moves `form 184 | /// upwards by 10 pixels. 185 | #[inline] 186 | pub fn shift_y(self, y: f64) -> Form { 187 | self.shift(0.0, y) 188 | } 189 | 190 | 191 | /// Scale a form by a given factor. Scaling by 2 doubles both dimensions and quadruples the 192 | /// area. 193 | #[inline] 194 | pub fn scale(self, scale: f64) -> Form { 195 | Form { scale: self.scale * scale, ..self } 196 | } 197 | 198 | 199 | /// Rotate a form by a given angle. Rotate takes radians and turns things counterclockwise. 200 | /// So to turn `form` 30 degrees to the left you would say `rotate(degrees(30), form)`. 201 | #[inline] 202 | pub fn rotate(self, theta: f64) -> Form { 203 | Form { theta: self.theta + theta, ..self } 204 | } 205 | 206 | 207 | /// Set the alpha of a Form. The default is 1 and 0 is totally transparent. 208 | #[inline] 209 | pub fn alpha(self, alpha: f32) -> Form { 210 | Form { alpha: alpha, ..self } 211 | } 212 | 213 | } 214 | 215 | 216 | /// Turn any `Element` into a `Form`. This lets you use text, gifs, and video in your collage. This 217 | /// means you can move, rotate, and scale an `Element` however you want. 218 | pub fn to_form(element: Element) -> Form { 219 | Form::new(BasicForm::Element(element)) 220 | } 221 | 222 | 223 | /// Flatten many forms into a single `Form`. This lets you move and rotate them as a single unit, 224 | /// making it possible to build small, modular components. 225 | pub fn group(forms: Vec) -> Form { 226 | Form::new(BasicForm::Group(transform_2d::identity(), forms)) 227 | } 228 | 229 | 230 | /// Flatten many forms into a single `Form` and then apply a matrix transformation. 231 | pub fn group_transform(matrix: Transform2D, forms: Vec) -> Form { 232 | Form::new(BasicForm::Group(matrix, forms)) 233 | } 234 | 235 | 236 | /// Trace a path with a given line style. 237 | pub fn traced(style: LineStyle, path: PointPath) -> Form { 238 | Form::new(BasicForm::PointPath(style, path)) 239 | } 240 | 241 | 242 | /// Create a line with a given line style. 243 | pub fn line(style: LineStyle, x1: f64, y1: f64, x2: f64, y2: f64) -> Form { 244 | traced(style, segment((x1, y1), (x2, y2))) 245 | } 246 | 247 | 248 | /// Create a sprite from a sprite sheet. It cuts out a rectangle at a given position. 249 | pub fn sprite(w: i32, h: i32, pos: (i32, i32), path: PathBuf) -> Form { 250 | Form::new(BasicForm::Image(w, h, pos, path)) 251 | } 252 | 253 | 254 | /// A collage is a collection of 2D forms. There are no strict positioning relationships between 255 | /// forms, so you are free to do all kinds of 2D graphics. 256 | pub fn collage(w: i32, h: i32, forms: Vec) -> Element { 257 | new_element(w, h, element::Prim::Collage(w, h, forms)) 258 | } 259 | 260 | 261 | /// A path described by a sequence of points. 262 | #[derive(Clone, Debug)] 263 | pub struct PointPath(pub Vec<(f64, f64)>); 264 | 265 | 266 | /// Create a PointPath that follows a sequence of points. 267 | pub fn point_path(points: Vec<(f64, f64)>) -> PointPath { 268 | PointPath(points) 269 | } 270 | 271 | 272 | /// Create a PointPath along a given line segment. 273 | pub fn segment(a: (f64, f64), b: (f64, f64)) -> PointPath { 274 | PointPath(vec![a, b]) 275 | } 276 | 277 | 278 | /// A shape described by its edges. 279 | #[derive(Clone, Debug)] 280 | pub struct Shape(pub Vec<(f64, f64)>); 281 | 282 | 283 | impl Shape { 284 | 285 | #[inline] 286 | fn fill(self, style: FillStyle) -> Form { 287 | Form::new(BasicForm::Shape(ShapeStyle::Fill(style), self)) 288 | } 289 | 290 | 291 | /// Create a filled-in shape. 292 | #[inline] 293 | pub fn filled(self, color: Color) -> Form { 294 | self.fill(FillStyle::Solid(color)) 295 | } 296 | 297 | 298 | /// Create a textured shape. 299 | /// The texture is described by some path and is tiled to fill the entire shape. 300 | #[inline] 301 | pub fn textured(self, path: PathBuf) -> Form { 302 | self.fill(FillStyle::Texture(path)) 303 | } 304 | 305 | 306 | /// Fill a shape with a gradient. 307 | #[inline] 308 | pub fn gradient(self, grad: Gradient) -> Form { 309 | self.fill(FillStyle::Grad(grad)) 310 | } 311 | 312 | 313 | /// Outline a shape with a given line style. 314 | #[inline] 315 | pub fn outlined(self, style: LineStyle) -> Form { 316 | Form::new(BasicForm::Shape(ShapeStyle::Line(style), self)) 317 | } 318 | 319 | } 320 | 321 | 322 | /// Create an arbitrary polygon by specifying its corners in order. `polygon` will automatically 323 | /// close all shapes, so the given list of points does not need to start and end with the same 324 | /// position. 325 | pub fn polygon(points: Vec<(f64, f64)>) -> Shape { 326 | Shape(points) 327 | } 328 | 329 | 330 | /// A rectangle with a given width and height. 331 | pub fn rect(w: f64, h: f64) -> Shape { 332 | let hw = w / 2.0; 333 | let hh = h / 2.0; 334 | Shape(vec![ (0.0-hw, 0.0-hh), (0.0-hw, hh), (hw, hh), (hw, 0.0-hh) ]) 335 | } 336 | 337 | 338 | /// A square with a given edge length. 339 | pub fn square(n: f64) -> Shape { 340 | rect(n, n) 341 | } 342 | 343 | 344 | /// An oval with a given width and height. 345 | pub fn oval(w: f64, h: f64) -> Shape { 346 | let n: usize = 50; 347 | let t = 2.0 * PI / n as f64; 348 | let hw = w / 2.0; 349 | let hh = h / 2.0; 350 | let f = |i: f64| (hw * (t*i).cos(), hh * (t*i).sin()); 351 | let points = (0..n-1).map(|i| f(i as f64)).collect(); 352 | Shape(points) 353 | } 354 | 355 | 356 | /// A circle with a given radius. 357 | pub fn circle(r: f64) -> Shape { 358 | let d = 2.0 * r; 359 | oval(d, d) 360 | } 361 | 362 | 363 | /// A regular polygon with N sides. The first argument specifies the number of sides and the second 364 | /// is the radius. So to create a pentagon with radius 30, you would say `ngon(5, 30.0)` 365 | pub fn ngon(n: usize, r: f64) -> Shape { 366 | let t = 2.0 * PI / n as f64; 367 | let f = |i: f64| (r * (t*i).cos(), r * (t*i).sin()); 368 | let points = (0..n).map(|i| f(i as f64)).collect(); 369 | Shape(points) 370 | } 371 | 372 | 373 | /// Create some text. Details like size and color are part of the `Text` value itself, so you can 374 | /// mix colors and sizes and fonts easily. 375 | pub fn text(t: Text) -> Form { 376 | Form::new(BasicForm::Text(t)) 377 | } 378 | 379 | 380 | 381 | 382 | 383 | 384 | 385 | 386 | 387 | /// 388 | /// CUSTOM NON-ELM FUNCTIONS. 389 | /// 390 | /// Normally Elm renders to html and javascript, however the aim of elmesque is to render to GL. 391 | /// 392 | 393 | 394 | /// This function draws a form with some given transform using the generic [Piston graphics] 395 | /// (https://github.com/PistonDevelopers/graphics) backend. 396 | pub fn draw_form<'a, C: CharacterCache, G: Graphics>( 397 | form: &Form, 398 | alpha: f32, 399 | backend: &mut G, 400 | maybe_character_cache: &mut Option<&mut C>, 401 | context: Context, 402 | ) { 403 | let Form { theta, scale, x, y, alpha, ref form } = *form; 404 | let context = context.trans(x, y).scale(scale, scale).rot_rad(theta); 405 | match *form { 406 | 407 | BasicForm::PointPath(ref line_style, PointPath(ref points)) => { 408 | // NOTE: join, dashing and dash_offset are not yet handled properly. 409 | let LineStyle { color, width, cap, join, ref dashing, dash_offset } = *line_style; 410 | let color = convert_color(color, alpha); 411 | let mut draw_line = |(x1, y1), (x2, y2)| { 412 | if dashing.is_empty() { 413 | let line = match cap { 414 | LineCap::Flat => graphics::Line::new(color, width / 2.0), 415 | LineCap::Round => graphics::Line::new_round(color, width / 2.0), 416 | LineCap::Padded => unimplemented!(), 417 | }; 418 | line.draw([x1, y1, x2, y2], &context.draw_state, context.transform, backend); 419 | } else { 420 | unimplemented!(); 421 | } 422 | }; 423 | for window in points.windows(2) { 424 | let (a, b) = (window[0], window[1]); 425 | draw_line(a, b); 426 | } 427 | }, 428 | 429 | BasicForm::Shape(ref shape_style, Shape(ref points)) => { 430 | match *shape_style { 431 | ShapeStyle::Line(ref line_style) => { 432 | // NOTE: join, dashing and dash_offset are not yet handled properly. 433 | let LineStyle { color, width, cap, join, ref dashing, dash_offset } = *line_style; 434 | let color = convert_color(color, alpha); 435 | let mut draw_line = |(x1, y1), (x2, y2)| { 436 | let line = match cap { 437 | LineCap::Flat => graphics::Line::new(color, width / 2.0), 438 | LineCap::Round => graphics::Line::new_round(color, width / 2.0), 439 | LineCap::Padded => unimplemented!(), 440 | }; 441 | line.draw([x1, y1, x2, y2], &context.draw_state, context.transform, backend); 442 | }; 443 | for window in points.windows(2) { 444 | let (a, b) = (window[0], window[1]); 445 | draw_line(a, b); 446 | } 447 | if points.len() > 2 { 448 | draw_line(points[points.len()-1], points[0]) 449 | } 450 | }, 451 | ShapeStyle::Fill(ref fill_style) => match *fill_style { 452 | FillStyle::Solid(color) => { 453 | let color = convert_color(color, alpha); 454 | let polygon = graphics::Polygon::new(color); 455 | let points: Vec<_> = points.iter().map(|&(x, y)| [x, y]).collect(); 456 | polygon.draw(&points[..], &context.draw_state, context.transform, backend); 457 | }, 458 | FillStyle::Texture(ref path) => { 459 | unimplemented!(); 460 | }, 461 | FillStyle::Grad(ref gradient) => { 462 | unimplemented!(); 463 | }, 464 | }, 465 | } 466 | }, 467 | 468 | BasicForm::OutlinedText(ref line_style, ref text) => { 469 | unimplemented!(); 470 | }, 471 | 472 | BasicForm::Text(ref text) => { 473 | let context = context.scale(1.0, -1.0); 474 | if let Some(ref mut character_cache) = *maybe_character_cache { 475 | use text::Style as TextStyle; 476 | use text::Position as TextPosition; 477 | use text::TextUnit; 478 | let (total_width, max_height) = text.sequence.iter().fold((0.0, 0.0), |(w, h), unit| { 479 | let TextUnit { ref string, ref style } = *unit; 480 | let TextStyle { ref typeface, height, color, bold, italic, line, monospace } = *style; 481 | let height = height.unwrap_or(16.0); 482 | let new_total_width = w + character_cache.width(height as u32, &string); 483 | let new_max_height = if height > h { height } else { h }; 484 | (new_total_width, new_max_height) 485 | }); 486 | let x_offset = match text.position { 487 | TextPosition::Center => -(total_width / 2.0).floor(), 488 | TextPosition::ToLeft => -total_width.floor(), 489 | TextPosition::ToRight => 0.0 490 | }; 491 | let y_offset = (max_height / 3.0).floor(); // TODO: FIX THIS (3.0) 492 | let context = context.trans(x_offset, y_offset); 493 | for unit in text.sequence.iter() { 494 | let TextUnit { ref string, ref style } = *unit; 495 | let TextStyle { ref typeface, height, color, bold, italic, line, monospace } = *style; 496 | let height = height.unwrap_or(16.0).floor(); 497 | let color = convert_color(color, alpha); 498 | graphics::text::Text::new_color(color, height as u32) 499 | .round() 500 | .draw(&string[..], *character_cache, &context.draw_state, context.transform, backend); 501 | } 502 | } 503 | }, 504 | 505 | BasicForm::Image(src_x, src_y, (w, h), ref path) => { 506 | // let image = graphics::Image { 507 | // color: None, 508 | // rectangle: None, 509 | // source_rectangle: Some([src_x, src_y, w, h]), 510 | // }; 511 | // let texture: &Texture = ::std::ops::Deref::deref(&texture); 512 | // image.draw(texture, draw_state, matrix, backend); 513 | unimplemented!(); 514 | }, 515 | 516 | BasicForm::Group(ref group_transform, ref forms) => { 517 | let Transform2D(matrix) = Transform2D(context.transform.clone()) 518 | .multiply(group_transform.clone()); 519 | let context = Context { transform: matrix, ..context }; 520 | for form in forms.iter() { 521 | draw_form(form, alpha, backend, maybe_character_cache, context); 522 | } 523 | }, 524 | 525 | BasicForm::Element(ref element) => 526 | element::draw_element(element, alpha, backend, maybe_character_cache, context), 527 | } 528 | } 529 | 530 | /// Convert an elmesque color to a piston-graphics color. 531 | fn convert_color(color: Color, alpha: f32) -> [f32; 4] { 532 | use color::hsl_to_rgb; 533 | let ((r, g, b), a) = match color { 534 | Color::Hsla(h, s, l, a) => (hsl_to_rgb(h, s, l), a), 535 | Color::Rgba(r, g, b, a) => ((r, g, b), a), 536 | }; 537 | [r, g, b, a * alpha] 538 | } 539 | 540 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | //! 2 | //! This crate is an attempt at porting Elm's incredibly useful std graphics modules. 3 | //! 4 | //! Visit [elm-lang.org](http://elm-lang.org/). 5 | //! 6 | //! 7 | //! All credit goes to Evan Czaplicki for all algorithms included within. 8 | //! 9 | //! Ported to Rust by Mitchell Nordine. 10 | //! 11 | 12 | extern crate graphics; 13 | extern crate num; 14 | extern crate rand; 15 | extern crate rustc_serialize; 16 | extern crate vecmath; 17 | 18 | pub use color as colour; 19 | pub use element::{Element, Renderer}; 20 | pub use form::{Form}; 21 | 22 | pub mod color; 23 | pub mod element; 24 | pub mod form; 25 | pub mod text; 26 | pub mod transform_2d; 27 | pub mod utils; 28 | -------------------------------------------------------------------------------- /src/text.rs: -------------------------------------------------------------------------------- 1 | 2 | use color::{black, Color}; 3 | use std::path::PathBuf; 4 | 5 | 6 | /// Drawable Text. 7 | #[derive(Clone, Debug)] 8 | pub struct Text { 9 | pub sequence: Vec, 10 | pub position: Position, 11 | } 12 | 13 | 14 | #[derive(Clone, Debug)] 15 | pub struct TextUnit { 16 | pub string: String, 17 | pub style: Style, 18 | } 19 | 20 | /// Styles for lines on text. This allows you to add an underline, an overline, or strike out text. 21 | #[derive(Copy, Clone, Debug)] 22 | pub enum Line { 23 | Under, 24 | Over, 25 | Through, 26 | } 27 | 28 | /// Text position relative to center point 29 | #[derive(Copy, Clone, Debug)] 30 | pub enum Position { 31 | Center, 32 | ToLeft, 33 | ToRight 34 | } 35 | 36 | 37 | /// Represents all the ways you can style `Text`. If the `type_face` list is empty or the `height` 38 | /// is `None`, the users will fall back on their default settings. The following `Style` is black, 39 | /// 16 pixel tall, underlined, and Times New Roman (assuming that typeface is available on the 40 | /// user's computer): 41 | /// 42 | /// Style { 43 | /// type_face: Some("Times New Roman"), 44 | /// height: Some(16), 45 | /// color: black(), 46 | /// bold: false, 47 | /// italic: false, 48 | /// line: Some(Line::Under), 49 | /// } 50 | /// 51 | #[derive(Clone, Debug)] 52 | pub struct Style { 53 | pub typeface: Option, 54 | pub height: Option, 55 | pub color: Color, 56 | pub bold: bool, 57 | pub italic: bool, 58 | pub line: Option, 59 | pub monospace: bool, 60 | } 61 | 62 | impl Style { 63 | pub fn default() -> Style { 64 | Style { 65 | typeface: None, 66 | height: None, 67 | color: black(), 68 | bold: false, 69 | italic: false, 70 | line: None, 71 | monospace: false, 72 | } 73 | } 74 | } 75 | 76 | 77 | impl Text { 78 | 79 | /// Convert a string into text which can be styled and displayed. 80 | pub fn from_string(string: String) -> Text { 81 | Text { 82 | sequence: vec![TextUnit { string: string, style: Style::default(), }], 83 | position: Position::Center 84 | } 85 | } 86 | 87 | /// Text with nothing in it. 88 | pub fn empty() -> Text { 89 | Text::from_string("".to_string()) 90 | } 91 | 92 | /// Put two chunks of text together. 93 | #[inline] 94 | pub fn append(mut self, other: Text) -> Text { 95 | self.sequence.extend(other.sequence.into_iter()); 96 | self 97 | } 98 | 99 | /// Put many chunks of text together. 100 | pub fn concat(texts: Vec) -> Text { 101 | let position = texts.get(0).map(|t| t.position).unwrap_or(Position::Center); 102 | Text { 103 | sequence: texts.into_iter() 104 | .flat_map(|Text { sequence, position }| sequence.into_iter()) 105 | .collect(), 106 | position: position 107 | } 108 | } 109 | 110 | /// Put many chunks of text together with a separator. 111 | pub fn join(separator: Text, texts: Vec) -> Text { 112 | texts.into_iter().fold(Text::empty(), |texts, text| { 113 | texts.append(text).append(separator.clone()) 114 | }) 115 | } 116 | 117 | /// Set the style of some text. For example, if you design a `Style` called `foorter_style` that is 118 | /// specifically for the bottom of your page, you could apply it to text like this: 119 | /// 120 | /// style(footer_style, from_string("the old prince / 2007")) 121 | /// 122 | #[inline] 123 | pub fn style(self, style: Style) -> Text { 124 | let string = String::from_utf8(self.sequence.into_iter().flat_map(|unit| { 125 | unit.string.into_bytes().into_iter() 126 | }).collect()).unwrap(); 127 | Text { 128 | sequence: vec![TextUnit { string: string, style: style }], 129 | ..self 130 | } 131 | } 132 | 133 | /// Provide a path of a typeface to be used for some text. 134 | #[inline] 135 | pub fn typeface(mut self, path: PathBuf) -> Text { 136 | for unit in self.sequence.iter_mut() { 137 | unit.style.typeface = Some(path.clone()); 138 | } 139 | self 140 | } 141 | 142 | /// Switch to a monospace typeface. Good for code snippets. 143 | /// 144 | /// monospace(from_string("(0..3).fold(0, |a, b| a + b)")) 145 | /// 146 | #[inline] 147 | pub fn monospace(mut self) -> Text { 148 | for unit in self.sequence.iter_mut() { 149 | unit.style.monospace = true; 150 | } 151 | self 152 | } 153 | 154 | /// Set the height of some text in pixels. 155 | #[inline] 156 | pub fn height(mut self, h: f64) -> Text { 157 | for unit in self.sequence.iter_mut() { 158 | unit.style.height = Some(h); 159 | } 160 | self 161 | } 162 | 163 | /// Set the color of some text. 164 | #[inline] 165 | pub fn color(mut self, color: Color) -> Text { 166 | for unit in self.sequence.iter_mut() { 167 | unit.style.color = color; 168 | } 169 | self 170 | } 171 | 172 | /// Make the text bold. 173 | #[inline] 174 | pub fn bold(mut self) -> Text { 175 | for unit in self.sequence.iter_mut() { 176 | unit.style.bold = true; 177 | } 178 | self 179 | } 180 | 181 | /// Make the text italic. 182 | #[inline] 183 | pub fn italic(mut self) -> Text { 184 | for unit in self.sequence.iter_mut() { 185 | unit.style.italic = true; 186 | } 187 | self 188 | } 189 | 190 | /// Put lines on text. 191 | #[inline] 192 | pub fn line(mut self, line: Line) -> Text { 193 | for unit in self.sequence.iter_mut() { 194 | unit.style.line = Some(line); 195 | } 196 | self 197 | } 198 | 199 | /// Change the text position relative to it's center point 200 | #[inline] 201 | pub fn position(mut self, position: Position) -> Text { 202 | self.position = position; 203 | self 204 | } 205 | } 206 | 207 | -------------------------------------------------------------------------------- /src/transform_2d.rs: -------------------------------------------------------------------------------- 1 | //! 2 | //! Ported from [elm-lang's Transform2D module] 3 | //! (https://github.com/elm-lang/core/blob/62b22218c42fb8ccc996c86bea450a14991ab815/src/Transform2D.elm) 4 | //! 5 | //! 6 | //! A library for performing 2D matrix transformations. It is used primarily with the 7 | //! `group_transform` function from the `form` module and allows you to do things like rotation, 8 | //! scaling, translation, shearing and reflection. 9 | //! 10 | //! Note that all the matrices in this library are 3*3 matrices of homogeneous coordinates, used 11 | //! for affine transformations. Since the bottom row is always `0 0 1` in these matrices, it is 12 | //! omitted in the diagrams below. 13 | //! 14 | 15 | 16 | use vecmath::{mat2x3_id, Matrix2x3, row_mat2x3_mul}; 17 | 18 | pub type Matrix2d = Matrix2x3; 19 | 20 | /// Represents a 2D transform. 21 | #[derive(Clone, Debug)] 22 | pub struct Transform2D(pub Matrix2d); 23 | 24 | impl Transform2D { 25 | 26 | /// Multiply two transforms together. 27 | /// 28 | /// ma mb mx na nb nx 29 | /// mc md my . nc nd ny 30 | /// 0 0 1 0 0 1 31 | /// 32 | #[inline] 33 | pub fn multiply(self, other: Transform2D) -> Transform2D { 34 | let (Transform2D(m), Transform2D(n)) = (self, other); 35 | Transform2D(row_mat2x3_mul(m, n)) 36 | } 37 | 38 | } 39 | 40 | /// Create an identity transform. Transforming by the identity does not change anything, but it can 41 | /// come in handy as a default or base case. 42 | /// 43 | /// 1 0 0 44 | /// 0 1 0 45 | /// 46 | #[inline] 47 | pub fn identity() -> Transform2D { 48 | Transform2D(mat2x3_id()) 49 | } 50 | 51 | /// Creates a transformation matrix. This lets you create transforms such as scales, shears, 52 | /// reflections and translations. 53 | /// 54 | /// a b x 55 | /// c d y 56 | /// 57 | #[inline] 58 | pub fn matrix(a: f64, b: f64, c: f64, d: f64, x: f64, y: f64) -> Transform2D { 59 | Transform2D([ [a, b, x], [c, d, y] ]) 60 | } 61 | 62 | /// Create a [rotation matrix](http://en.wikipedia.org/wiki/Rotation_matrix). Given an angle t, it 63 | /// creates a counterclockwise rotation matrix. 64 | /// 65 | /// cos t -sin t 0 66 | /// sin t cos t 0 67 | /// 68 | #[inline] 69 | pub fn rotation(t: f64) -> Transform2D { 70 | Transform2D([ [t.cos(), -t.sin(), 0.0], [t.sin(), t.cos(), 0.0] ]) 71 | } 72 | 73 | /// Creates a transformation matrix for translation. 74 | /// 75 | /// 1 0 x 76 | /// 0 1 y 77 | /// 78 | #[inline] 79 | pub fn translation(x: f64, y: f64) -> Transform2D { 80 | matrix(1.0, 0.0, 0.0, 1.0, x, y) 81 | } 82 | 83 | /// Creates a transformation matrix for scaling by all directions. 84 | /// 85 | /// s 0 0 86 | /// 0 s 0 87 | /// 88 | #[inline] 89 | pub fn scale(s: f64) -> Transform2D { 90 | matrix(s, 0.0, 0.0, s, 0.0, 0.0) 91 | } 92 | 93 | /// Creates a transformation for horizontal scaling. 94 | #[inline] 95 | pub fn scale_x(s: f64) -> Transform2D { 96 | matrix(s, 0.0, 0.0, 1.0, 0.0, 0.0) 97 | } 98 | 99 | /// Creates a transformation for vertical scaling. 100 | #[inline] 101 | pub fn scale_y(s: f64) -> Transform2D { 102 | matrix(1.0, 0.0, 0.0, s, 0.0, 0.0) 103 | } 104 | 105 | -------------------------------------------------------------------------------- /src/utils.rs: -------------------------------------------------------------------------------- 1 | 2 | use num::{Float, NumCast}; 3 | use num::PrimInt as Int; 4 | use num::traits::cast; 5 | use std::f32::consts::PI; 6 | 7 | /// Clamp a f32 between 0f32 and 1f32. 8 | pub fn clampf32(f: f32) -> f32 { 9 | if f < 0f32 { 0f32 } else if f > 1f32 { 1f32 } else { f } 10 | } 11 | 12 | /// Convert degrees to radians. 13 | pub fn degrees(d: F) -> F { 14 | d * cast(PI / 180.0).unwrap() 15 | } 16 | 17 | /// Convert turns to radians. 18 | pub fn turns(t: F) -> F { 19 | let f: F = cast(2.0 * PI).unwrap(); 20 | f * t 21 | } 22 | 23 | /// The modulo function. 24 | #[inline] 25 | pub fn modulo(a: I, b: I) -> I { 26 | match a % b { 27 | r if (r > I::zero() && b < I::zero()) 28 | || (r < I::zero() && b > I::zero()) => r + b, 29 | r => r, 30 | } 31 | } 32 | 33 | /// Modulo float. 34 | pub fn fmod(f: f32, n: i32) -> f32 { 35 | let i = f.floor() as i32; 36 | modulo(i, n) as f32 + f - i as f32 37 | } 38 | 39 | /// Return the min between to floats. 40 | pub fn min(a: f32, b: f32) -> f32 { 41 | if a <= b { a } else { b } 42 | } 43 | 44 | /// Return the max between to floats. 45 | pub fn max(a: f32, b: f32) -> f32 { 46 | if a >= b { a } else { b } 47 | } 48 | 49 | /// Clamp a value to a range. 50 | #[inline] 51 | pub fn clamp(val: T, min: T, max: T) -> T { 52 | if val < min { min } else { if val > max { max } else { val } } 53 | } 54 | 55 | /// Map a value from a given range to a new given range. 56 | pub fn map_range 57 | (val: X, in_min: X, in_max: X, out_min: Y, out_max: Y) -> Y { 58 | let val_f: f64 = NumCast::from(val).unwrap(); 59 | let in_min_f: f64 = NumCast::from(in_min).unwrap(); 60 | let in_max_f: f64 = NumCast::from(in_max).unwrap(); 61 | let out_min_f: f64 = NumCast::from(out_min).unwrap(); 62 | let out_max_f: f64 = NumCast::from(out_max).unwrap(); 63 | NumCast::from( 64 | (val_f - in_min_f) / (in_max_f - in_min_f) * (out_max_f - out_min_f) + out_min_f 65 | ).unwrap() 66 | } 67 | 68 | 69 | --------------------------------------------------------------------------------