Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(canvas): implement half block marker #550

Merged
merged 3 commits into from
Sep 30, 2023
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
266 changes: 139 additions & 127 deletions examples/canvas.rs
Original file line number Diff line number Diff line change
@@ -1,28 +1,29 @@
use std::{
error::Error,
io,
io::{self, stdout, Stdout},
time::{Duration, Instant},
};

use crossterm::{
event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode},
execute,
event::{self, Event, KeyCode},
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
ExecutableCommand,
};
use ratatui::{
prelude::*,
widgets::{canvas::*, *},
};

fn main() -> io::Result<()> {
App::run()
}

struct App {
x: f64,
y: f64,
ball: Rectangle,
ball: Circle,
playground: Rect,
vx: f64,
vy: f64,
dir_x: bool,
dir_y: bool,
tick_count: u64,
marker: Marker,
}
Expand All @@ -32,155 +33,166 @@ impl App {
App {
x: 0.0,
y: 0.0,
ball: Rectangle {
x: 10.0,
y: 30.0,
width: 10.0,
height: 10.0,
ball: Circle {
x: 20.0,
y: 40.0,
radius: 10.0,
color: Color::Yellow,
},
playground: Rect::new(10, 10, 100, 100),
vx: 1.0,
vy: 1.0,
dir_x: true,
dir_y: true,
vx: 0.1,
vy: 0.1,
tick_count: 0,
marker: Marker::Dot,
}
}

pub fn run() -> io::Result<()> {
let mut terminal = init_terminal()?;
let mut app = App::new();
let mut last_tick = Instant::now();
let tick_rate = Duration::from_millis(16);
loop {
let _ = terminal.draw(|frame| app.ui(frame));
let timeout = tick_rate.saturating_sub(last_tick.elapsed());
if event::poll(timeout)? {
if let Event::Key(key) = event::read()? {
match key.code {
KeyCode::Char('q') => break,
KeyCode::Down => app.y += 1.0,
KeyCode::Up => app.y -= 1.0,
KeyCode::Right => app.x += 1.0,
KeyCode::Left => app.x -= 1.0,
_ => {}
}
}
}

if last_tick.elapsed() >= tick_rate {
app.on_tick();
last_tick = Instant::now();
}
}
restore_terminal()
}

fn on_tick(&mut self) {
self.tick_count += 1;
// only change marker every 4 ticks (1s) to avoid stroboscopic effect
if (self.tick_count % 4) == 0 {
// only change marker every 180 ticks (3s) to avoid stroboscopic effect
if (self.tick_count % 180) == 0 {
self.marker = match self.marker {
Marker::Dot => Marker::Block,
Marker::Block => Marker::Bar,
Marker::Bar => Marker::Braille,
Marker::Braille => Marker::Dot,
Marker::Dot => Marker::Braille,
Marker::Braille => Marker::Block,
Marker::Block => Marker::HalfBlock,
Marker::HalfBlock => Marker::Bar,
Marker::Bar => Marker::Dot,
};
}
if self.ball.x < self.playground.left() as f64
|| self.ball.x + self.ball.width > self.playground.right() as f64
// bounce the ball by flipping the velocity vector
let ball = &self.ball;
let playground = self.playground;
if ball.x - ball.radius < playground.left() as f64
|| ball.x + ball.radius > playground.right() as f64
{
self.dir_x = !self.dir_x;
self.vx = -self.vx;
}
if self.ball.y < self.playground.top() as f64
|| self.ball.y + self.ball.height > self.playground.bottom() as f64
if ball.y - ball.radius < playground.top() as f64
|| ball.y + ball.radius > playground.bottom() as f64
{
self.dir_y = !self.dir_y;
}

if self.dir_x {
self.ball.x += self.vx;
} else {
self.ball.x -= self.vx;
self.vy = -self.vy;
}

if self.dir_y {
self.ball.y += self.vy;
} else {
self.ball.y -= self.vy
}
self.ball.x += self.vx;
self.ball.y += self.vy;
}
}

fn main() -> Result<(), Box<dyn Error>> {
// setup terminal
enable_raw_mode()?;
let mut stdout = io::stdout();
execute!(stdout, EnterAlternateScreen, EnableMouseCapture)?;
let backend = CrosstermBackend::new(stdout);
let mut terminal = Terminal::new(backend)?;
fn ui(&self, frame: &mut Frame) {
let main_layout = Layout::default()
.direction(Direction::Horizontal)
.constraints([Constraint::Percentage(50), Constraint::Percentage(50)].as_ref())
.split(frame.size());

// create app and run it
let tick_rate = Duration::from_millis(250);
let app = App::new();
let res = run_app(&mut terminal, app, tick_rate);
let right_layout = Layout::default()
.direction(Direction::Vertical)
.constraints([Constraint::Percentage(50), Constraint::Percentage(50)].as_ref())
.split(main_layout[1]);

// restore terminal
disable_raw_mode()?;
execute!(
terminal.backend_mut(),
LeaveAlternateScreen,
DisableMouseCapture
)?;
terminal.show_cursor()?;

if let Err(err) = res {
println!("{err:?}");
frame.render_widget(self.map_canvas(), main_layout[0]);
frame.render_widget(self.pong_canvas(), right_layout[0]);
frame.render_widget(self.boxes_canvas(right_layout[1]), right_layout[1]);
}

Ok(())
}
fn map_canvas(&self) -> impl Widget + '_ {
Canvas::default()
.block(Block::default().borders(Borders::ALL).title("World"))
.marker(self.marker)
.paint(|ctx| {
ctx.draw(&Map {
color: Color::Green,
resolution: MapResolution::High,
});
ctx.print(self.x, -self.y, "You are here".yellow());
})
.x_bounds([-180.0, 180.0])
.y_bounds([-90.0, 90.0])
}

fn run_app<B: Backend>(
terminal: &mut Terminal<B>,
mut app: App,
tick_rate: Duration,
) -> io::Result<()> {
let mut last_tick = Instant::now();
loop {
terminal.draw(|f| ui(f, &app))?;

let timeout = tick_rate
.checked_sub(last_tick.elapsed())
.unwrap_or_else(|| Duration::from_secs(0));
if event::poll(timeout)? {
if let Event::Key(key) = event::read()? {
match key.code {
KeyCode::Char('q') => {
return Ok(());
}
KeyCode::Down => {
app.y += 1.0;
}
KeyCode::Up => {
app.y -= 1.0;
}
KeyCode::Right => {
app.x += 1.0;
fn pong_canvas(&self) -> impl Widget + '_ {
Canvas::default()
.block(Block::default().borders(Borders::ALL).title("Pong"))
.marker(self.marker)
.paint(|ctx| {
ctx.draw(&self.ball);
})
.x_bounds([10.0, 110.0])
.y_bounds([10.0, 110.0])
}

fn boxes_canvas(&self, area: Rect) -> impl Widget {
let (left, right, bottom, top) =
(0.0, area.width as f64, 0.0, area.height as f64 * 2.0 - 4.0);
Canvas::default()
.block(Block::default().borders(Borders::ALL).title("Rects"))
.marker(self.marker)
.x_bounds([left, right])
.y_bounds([bottom, top])
.paint(|ctx| {
for i in 0..=11 {
ctx.draw(&Rectangle {
x: (i * i + 3 * i) as f64 / 2.0 + 2.0,
y: 2.0,
width: i as f64,
height: i as f64,
color: Color::Red,
});
ctx.draw(&Rectangle {
x: (i * i + 3 * i) as f64 / 2.0 + 2.0,
y: 21.0,
width: i as f64,
height: i as f64,
color: Color::Blue,
});
}
for i in 0..100 {
if i % 10 != 0 {
ctx.print(i as f64 + 1.0, 0.0, format!("{i}", i = i % 10));
}
KeyCode::Left => {
app.x -= 1.0;
if i % 2 == 0 && i % 10 != 0 {
ctx.print(0.0, i as f64, format!("{i}", i = i % 10));
}
_ => {}
}
}
}

if last_tick.elapsed() >= tick_rate {
app.on_tick();
last_tick = Instant::now();
}
})
}
}

fn ui(f: &mut Frame, app: &App) {
let chunks = Layout::default()
.direction(Direction::Horizontal)
.constraints([Constraint::Percentage(50), Constraint::Percentage(50)].as_ref())
.split(f.size());
let canvas = Canvas::default()
.block(Block::default().borders(Borders::ALL).title("World"))
.marker(app.marker)
.paint(|ctx| {
ctx.draw(&Map {
color: Color::White,
resolution: MapResolution::High,
});
ctx.print(app.x, -app.y, "You are here".yellow());
})
.x_bounds([-180.0, 180.0])
.y_bounds([-90.0, 90.0]);
f.render_widget(canvas, chunks[0]);
let canvas = Canvas::default()
.block(Block::default().borders(Borders::ALL).title("Pong"))
.marker(app.marker)
.paint(|ctx| {
ctx.draw(&app.ball);
})
.x_bounds([10.0, 110.0])
.y_bounds([10.0, 110.0]);
f.render_widget(canvas, chunks[1]);
fn init_terminal() -> io::Result<Terminal<CrosstermBackend<Stdout>>> {
enable_raw_mode()?;
stdout().execute(EnterAlternateScreen)?;
Terminal::new(CrosstermBackend::new(stdout()))
}

fn restore_terminal() -> io::Result<()> {
disable_raw_mode()?;
stdout().execute(LeaveAlternateScreen)?;
Ok(())
}
3 changes: 2 additions & 1 deletion examples/canvas.tape
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,12 @@
# To run this script, install vhs and run `vhs ./examples/canvas.tape`
Output "target/canvas.gif"
Set Theme "OceanicMaterial"
Set FontSize 12
Set Width 1200
Set Height 800
Hide
Type "cargo run --example=canvas --features=crossterm"
Enter
Sleep 1s
Show
Sleep 5s
Sleep 30s
10 changes: 10 additions & 0 deletions src/symbols.rs
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,12 @@ pub mod block {
};
}

pub mod half_block {
pub const UPPER: char = '▀';
pub const LOWER: char = '▄';
pub const FULL: char = '█';
}

pub mod bar {
pub const FULL: &str = "█";
pub const SEVEN_EIGHTHS: &str = "▇";
Expand Down Expand Up @@ -408,6 +414,10 @@ pub enum Marker {
/// Braille Patterns. If your terminal does not support this, you will see unicode replacement
/// characters (�) instead of Braille dots.
Braille,
/// Use the unicode block and half block characters ("█", "▄", and "▀") to represent points in
/// a grid that is double the resolution of the terminal. Because each terminal cell is
/// generally about twice as tall as it is wide, this allows for a square grid of pixels.
HalfBlock,
}

pub mod scrollbar {
Expand Down
Loading