Skip to content

Commit

Permalink
Ellipse tool (not perfect yet)
Browse files Browse the repository at this point in the history
  • Loading branch information
yds12 committed Aug 27, 2024
1 parent c57d7ea commit 2bac7e8
Show file tree
Hide file tree
Showing 19 changed files with 147 additions and 32 deletions.
4 changes: 2 additions & 2 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion docs/features.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ Features (+ means done, (-) means least important):
- Shapes
- Lines (+)
- Rectangles (+)
- Ovals [v0.2]
- Ovals [v0.2] (+)
- properties of recently created objects (lines, shapes)
- Selection (+)
- Rectangular (+)
Expand Down
2 changes: 1 addition & 1 deletion lapix/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
[package]
name = "lapix"
authors = ["Y. D. Santos <[email protected]>"]
version = "0.1.1"
version = "0.1.2"
edition = "2021"
description = "Image editor backend library for pixel art"
homepage = "https://github.com/yds12/tarsila"
Expand Down
20 changes: 20 additions & 0 deletions lapix/src/canvas.rs
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,26 @@ impl<IMG: Bitmap> Canvas<IMG> {
reversals
}

/// Draw an ellipse (outline) between two points in the canvas with a
/// certain color. Returns a set of reversals (points and the colors they
/// need to be set to in order to reverse the action).
pub fn ellipse(
&mut self,
p1: Point<i32>,
p2: Point<i32>,
color: Color,
) -> Vec<(Point<i32>, Color)> {
let rect = graphics::ellipse(p1, p2);
let mut reversals = Vec::new();

for p in rect {
if let Some(action) = self.set_pixel(p, color) {
reversals.push(action);
}
}
reversals
}

/// Set an area of the canvas (determined by a rectangle) to a certain
/// color. Returns a set of reversals (points and colors they need to be set
/// to in order to reverse the action).
Expand Down
21 changes: 9 additions & 12 deletions lapix/src/color.rs
Original file line number Diff line number Diff line change
Expand Up @@ -131,23 +131,20 @@ impl ColorF32 {
}

let max = self.value();
let min = [self.r, self.g, self.b]
.into_iter()
.map(|c| (c * 1000.) as i32)
.min()
.unwrap() as f32
/ 1000.;
let min = std::cmp::min(
std::cmp::min((self.r * 1000.0) as i32, (self.g * 1000.0) as i32),
(self.b * 1000.0) as i32,
) as f32;

return (max - min) / max;

Check failure on line 139 in lapix/src/color.rs

View workflow job for this annotation

GitHub Actions / Clippy

unneeded `return` statement
}

pub fn value(&self) -> f32 {
[self.r, self.g, self.b]
.into_iter()
.map(|c| (c * 1000.) as i32)
.max()
.unwrap() as f32
/ 1000.
std::cmp::max(
std::cmp::max((self.r * 1000.0) as i32, (self.g * 1000.0) as i32),
(self.b * 1000.0) as i32,
) as f32
/ 1000.0
}
}

Expand Down
17 changes: 16 additions & 1 deletion lapix/src/event.rs
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,11 @@ pub enum Event {
/// Draw a rectangle with corners at this point and the point specified at
/// `RectStart`
RectEnd(Point<i32>),
/// Start drawing an ellipse at the specified point
EllipseStart(Point<i32>),
/// Draw an ellipse with corners at this point and the point specified at
/// `EllipseStart`
EllipseEnd(Point<i32>),
/// Create a new layer above the current layer
NewLayerAbove,
/// Create a new layer below the current layer
Expand Down Expand Up @@ -138,6 +143,7 @@ impl Event {
| Self::BrushStroke(_)
| Self::LineEnd(_)
| Self::RectEnd(_)
| Self::EllipseEnd(_)
| Self::Bucket(_)
| Self::MoveStart(_)
| Self::MoveEnd(_)
Expand Down Expand Up @@ -177,7 +183,12 @@ impl Event {
pub fn type_repeatable(&self) -> bool {
!matches!(
self,
Self::LineStart(_) | Self::LineEnd(_) | Self::RectStart(_) | Self::RectEnd(_)
Self::LineStart(_)
| Self::LineEnd(_)
| Self::RectStart(_)
| Self::RectEnd(_)
| Self::EllipseStart(_)
| Self::EllipseEnd(_)
)
}

Expand All @@ -200,6 +211,8 @@ impl Event {
| Self::LineEnd(_)
| Self::RectStart(_)
| Self::RectEnd(_)
| Self::EllipseStart(_)
| Self::EllipseEnd(_)
| Self::NewLayerAbove
| Self::NewLayerBelow
| Self::FlipHorizontal
Expand Down Expand Up @@ -231,6 +244,7 @@ impl Event {
| Self::SetTool(Tool::Eyedropper)
| Self::SetTool(Tool::Eraser)
| Self::SetTool(Tool::Rectangle)
| Self::SetTool(Tool::Ellipse)
| Self::SetTool(Tool::Line)
| Self::MoveLayerDown(_)
| Self::MoveLayerUp(_)
Expand All @@ -249,6 +263,7 @@ impl Event {
| Self::Copy
| Self::LineEnd(_)
| Self::RectEnd(_)
| Self::EllipseEnd(_)
| Self::FlipHorizontal
| Self::FlipVertical
| Self::DeleteSelection
Expand Down
9 changes: 9 additions & 0 deletions lapix/src/floating.rs
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,15 @@ impl<IMG: Bitmap> FreeImage<IMG> {
FreeImage::from_pixels(span + Point::ONE, graphics::rectangle(p0, p), color, offset)
}

/// Creates a free image with an ellipse between two points in a certain
/// color.
pub fn ellipse_preview(p0: Point<i32>, p: Point<i32>, color: Color) -> Self {
let span = p.abs_diff(p0);
let offset = p.rect_min_corner(p0);

FreeImage::from_pixels(span + Point::ONE, graphics::ellipse(p0, p), color, offset)
}

/// Change the position of the free image considering that the passed point
/// is the mouse position where it was released, and that the initial mouse
/// position is defined by the pivot.
Expand Down
55 changes: 54 additions & 1 deletion lapix/src/graphics.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
//! Functions to calculate graphics like lines, rectangles, etc. in a discrete
//! 2D space

use crate::Point;
use std::collections::HashSet;

use crate::{Point, Rect};

/// Get the distance between two [`Point`]s
pub fn distance(p1: Point<i32>, p2: Point<i32>) -> f32 {
Expand Down Expand Up @@ -42,6 +44,57 @@ pub fn rectangle(p1: Point<i32>, p2: Point<i32>) -> Vec<Point<i32>> {
vec![l1, l2, l3, l4].into_iter().flatten().collect()
}

/// Get the set of [`Point`]s needed to draw an ellipse between two points
/// TODO there are still some imperfections here
pub fn ellipse(p1: Point<i32>, p2: Point<i32>) -> Vec<Point<i32>> {
let a = (p1.x - p2.x).abs() as f32 / 2.0;
let b = (p1.y - p2.y).abs() as f32 / 2.0;

let low_x = std::cmp::min(p1.x, p2.x);
let low_y = std::cmp::min(p1.y, p2.y);
let high_x = std::cmp::max(p1.x, p2.x);
let high_y = std::cmp::max(p1.y, p2.y);
let bounds = Rect::new(low_x, low_y, high_x - low_x, high_y - low_y);
let xspan = ((p1.x - p2.x).abs() as f32 / 2.0).round() as i32;
let yspan = ((p1.y - p2.y).abs() as f32 / 2.0).round() as i32;

let mut points = HashSet::new();

let sampling_level = yspan;
let step = 1. / sampling_level as f32;

// For each x, we'll find the corresponding y values
for x in 0..(xspan) {
for delta in 0..sampling_level {
let x = x as f32 + (delta as f32 * step);
// Formula:
// sqrt(((a2-x2)*b2)/a2)
let inner = (a.powf(2.0) - x.powf(2.0)) * b.powf(2.0) / a.powf(2.0);
let root = inner.sqrt();

let ys = vec![root, -root];

for y in ys {
let xx = x.round() as i32 + low_x + xspan;
let yy = y.round() as i32 + low_y + yspan;

if bounds.contains(xx, yy) {
points.insert(Point::new(xx, yy));
}

let xx = -x.round() as i32 + low_x + xspan;
let yy = y.round() as i32 + low_y + yspan;

if bounds.contains(xx, yy) {
points.insert(Point::new(xx, yy));
}
}
}
}

points.into_iter().collect()
}

#[cfg(test)]
mod tests {
use super::*;
Expand Down
6 changes: 3 additions & 3 deletions lapix/src/palette.rs
Original file line number Diff line number Diff line change
Expand Up @@ -71,9 +71,9 @@ impl Palette {

pub fn sort(&mut self) {
fn sort_val(color: &Color) -> i32 {
(color.hue() as i32) * 1_000_000 +
(color.saturation() * 10_000.) as i32 +
(color.value() * 10_000.) as i32
(color.hue() as i32) * 1_000_000
+ (color.saturation() * 10_000.) as i32
+ (color.value() * 10_000.) as i32
}
self.0.sort_by(|a, b| sort_val(a).cmp(&sort_val(b)));

Check failure on line 78 in lapix/src/palette.rs

View workflow job for this annotation

GitHub Actions / Clippy

consider using `sort_by_key`
}
Expand Down
18 changes: 17 additions & 1 deletion lapix/src/state.rs
Original file line number Diff line number Diff line change
Expand Up @@ -147,7 +147,7 @@ impl<IMG: Bitmap + Serialize + for<'de> Deserialize<'de>> State<IMG> {
)?;
self.end_action();
}
Event::LineStart(_) | Event::RectStart(_) => (),
Event::LineStart(_) | Event::RectStart(_) | Event::EllipseStart(_) => (),
Event::BrushStart | Event::EraseStart => self.start_action(),
Event::BrushEnd | Event::EraseEnd => self.end_action(),
Event::LineEnd(p) => {
Expand All @@ -172,6 +172,17 @@ impl<IMG: Bitmap + Serialize + for<'de> Deserialize<'de>> State<IMG> {
self.single_pixels_action(reversals);
self.free_image = None;
}
Event::EllipseEnd(p) => {
let last_event = self.events.last();
let p0: Point<i32> = match last_event {
Some(Event::EllipseStart(p0)) => *p0,
_ => return Err(Error::DrawingNotStarted),
};
let color = self.main_color;
let reversals = self.canvas_mut().ellipse(p0, p, color);
self.single_pixels_action(reversals);
self.free_image = None;
}
Event::BrushStroke(p) => {
let last_event = self.events.last();

Expand Down Expand Up @@ -517,6 +528,7 @@ impl<IMG: Bitmap + Serialize + for<'de> Deserialize<'de>> State<IMG> {
Some(Event::MoveStart(_)) => self.move_free_image(mouse_canvas)?,
Some(Event::LineStart(p)) => self.update_line_preview(*p, mouse_canvas),
Some(Event::RectStart(p)) => self.update_rect_preview(*p, mouse_canvas),
Some(Event::EllipseStart(p)) => self.update_ellipse_preview(*p, mouse_canvas),
_ => (),
}

Expand Down Expand Up @@ -553,6 +565,10 @@ impl<IMG: Bitmap + Serialize + for<'de> Deserialize<'de>> State<IMG> {
self.free_image = Some(FreeImage::rect_preview(p0, p, self.main_color()));
}

fn update_ellipse_preview(&mut self, p0: Point<i32>, p: Point<i32>) {
self.free_image = Some(FreeImage::ellipse_preview(p0, p, self.main_color()));
}

fn save_image(&self, path: &str) -> Result<()> {
let blended = self.layers.blended();

Expand Down
2 changes: 2 additions & 0 deletions lapix/src/tool.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ pub enum Tool {
Selection,
Move,
Rectangle,
Ellipse,
}

impl Display for Tool {
Expand All @@ -25,6 +26,7 @@ impl Display for Tool {
Self::Selection => "selection",
Self::Move => "move",
Self::Rectangle => "rectangle",
Self::Ellipse => "ellipse",
};

f.write_str(st)
Expand Down
4 changes: 2 additions & 2 deletions tarsila/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
[package]
name = "tarsila"
authors = ["Y. D. Santos <[email protected]>"]
version = "0.1.1"
version = "0.1.2"
edition = "2021"
description = "GUI image editor for pixel art"
homepage = "https://github.com/yds12/tarsila"
Expand All @@ -15,7 +15,7 @@ license = "MIT/Apache-2.0"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[dependencies]
lapix = { path = "../lapix", version = "0.1.1" }
lapix = { path = "../lapix", version = "0.1.2" }
egui-macroquad = { version = "0.15.0", default-features = false }
macroquad = "0.3.25"
rfd = "0.10.0"
Expand Down
Binary file added tarsila/res/icon/ellipse.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
4 changes: 3 additions & 1 deletion tarsila/src/gui/toolbar.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ use macroquad::prelude::*;
use std::collections::HashMap;

const TOOL_BTN_IMG_SIZE: Size<usize> = Size { x: 16, y: 16 };
const TOOLS: [Tool; 8] = [
const TOOLS: [Tool; 9] = [
Tool::Brush,
Tool::Bucket,
Tool::Eraser,
Expand All @@ -14,6 +14,7 @@ const TOOLS: [Tool; 8] = [
Tool::Selection,
Tool::Move,
Tool::Rectangle,
Tool::Ellipse,
];

pub struct Toolbar {
Expand Down Expand Up @@ -147,6 +148,7 @@ impl ToolButton {
Tool::Selection => "selection tool (S)",
Tool::Move => "move tool (M)",
Tool::Rectangle => "rectangle tool (R)",
Tool::Ellipse => "ellipse tool (O)",
}
}
}
4 changes: 4 additions & 0 deletions tarsila/src/input/bindings.rs
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,10 @@ impl KeyBindings {
InputEvent::KeyPress(mq::KeyCode::R.into()).into(),
Event::SetTool(Tool::Rectangle).into(),
),
(
InputEvent::KeyPress(mq::KeyCode::O.into()).into(),
Event::SetTool(Tool::Ellipse).into(),
),
(
InputEvent::KeyPress(mq::KeyCode::I.into()).into(),
Event::SetTool(Tool::Eyedropper).into(),
Expand Down
7 changes: 0 additions & 7 deletions tarsila/src/input/mapper.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,6 @@ pub struct InputMapper;

impl InputMapper {
pub fn map(&self, key_bindings: &KeyBindings, input_events: Vec<InputEvent>) -> Vec<Effect> {
if !input_events.is_empty() {
//dbg!(&input_events);
}

let mut fx = Vec::new();

for (keys, action) in key_bindings.iter() {
Expand All @@ -19,9 +15,6 @@ impl InputMapper {
}
}

if !fx.is_empty() {
//dbg!(&fx);
}
fx
}
}
1 change: 1 addition & 0 deletions tarsila/src/mouse.rs
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ impl CursorSet {
(Tool::Selection, (0., 0.).into()),
(Tool::Move, (-8., -8.).into()),
(Tool::Rectangle, (0., -16.).into()),
(Tool::Ellipse, (0., -16.).into()),
];

let mut hm: HashMap<_, _> = tools
Expand Down
Loading

0 comments on commit 2bac7e8

Please sign in to comment.