colors, rotations, and more

This commit is contained in:
2024-10-11 00:58:24 -04:00
parent e29f27266a
commit 823a8f79d3
9 changed files with 367 additions and 149 deletions

View File

@@ -24,9 +24,14 @@ build_command: jai build.jai
[Build Debug Macos]
build_command: arch -x86_64 /opt/jai/bin/jai-macos build.jai
run_command: build_debug/console3d
key_binding: Cmd-Shift-B
[Run MacOS]
run_working_dir: .
run_command: term.scpt "cd %RUN_WORKING_DIR%/build_debug && ./console3d"
key_binding: F5
[[settings]]
tab_size: 4
insert_spaces_when_pressing_tab: true

View File

@@ -1,3 +1,10 @@
swap_buffer :: (screen: *Screen) {
assert(false, "No implemented");
using screen;
write_string(cast(string)buffer);
}
clear_screen :: (screen: *Screen) {
for 0..screen.height * screen.width {
print_color(" ", color = .BLACK);
}
}

View File

@@ -1,107 +1,110 @@
draw :: (screen: *Screen, x: s64, y: s64, char: PIXEL_CHAR = .PIXEL_SOLID, color: COLOR = .FG_WHITE) {
using screen;
if x >= 0 && x < width && y >= 0 && y < height {
buffer[y * width + x].char = char;
buffer[y * width + x].color = color;
// "Squashes" two dimensional coordinates onto the one dimensional screen buffer
draw :: (using screen: *Screen, p: Vec2s64, char: u8 = DEFAULT_PIXEL_CHAR) {
if p.x >= 0 && p.x < width && p.y >= 0 && p.y < height {
buffer[p.y * width + p.x] = cast(u8)char;
}
}
draw_line :: (screen: *Screen, x1: s64, y1: s64, x2: s64, y2: s64, char: PIXEL_CHAR = .PIXEL_SOLID, color: COLOR = .FG_WHITE) {
x, y, dx, dy, dx1, dy1, xe, ye, px, py : s64;
// Implementation of the Bresenham line algorithm
// https://en.wikipedia.org/wiki/Bresenham%27s_line_algorithm
draw_line :: (screen: *Screen, p1: Vec2s64, p2: Vec2s64, char: u8 = DEFAULT_PIXEL_CHAR) {
p1_c := p1;
p2_c := p2;
dx = x2 - x1;
dy = y2 - y1;
// p1_c.y /= 2;
// p2_c.y /= 2;
dx1 = abs(dx);
dy1 = abs(dy);
dx := abs(p2_c.x - p1_c.x);
dy := -abs(p2_c.y - p1_c.y);
sx := ifx p1_c.x < p2_c.x then 1 else -1;
sy := ifx p1_c.y < p2_c.y then 1 else -1;
px = 2 * dy1 - dx1;
py = 2 * dx1 - dy1;
err := dx + dy;
e2 : s64;
if dy1 <= dx1 {
if dx >= 0 {
x = x1;
y = y1;
xe = x2;
} else {
x = x2;
y = y2;
xe = x1;
while true {
draw(screen, Vec2s64.{p1_c.x, p1_c.y}, char);
if p1_c.x == p2_c.x && p1_c.y == p2_c.y then break;
e2 = 2 * err;
if e2 >= dy {
err += dy;
p1_c.x += sx;
}
draw(screen, x, y, char, color);
for 0..xe {
x = x + 1;
if px < 0 {
px = px + 2 * dy1;
} else {
if (dx < 0 && dy < 0) || (dx > 0 && dy > 0) then y = y + 1; else y = y - 1;
px = px + 2 * (dy1 - dx1);
}
draw(screen, x, y, char, color);
}
} else {
if dy >= 0 {
x = x1;
y = y1;
ye = y2;
} else {
x = x2;
y = y2;
ye = y1;
}
draw(screen, x, y, char, color);
for 0..ye {
y = y + 1;
if py <= 0 {
py = py + 2 * dx1;
} else {
if (dx < 0 && dy < 0 ) || (dx > 0 && dy > 0) then x = x + 1; else x = x - 1;
py = py + 2 * (dx1 - dy1);
}
draw(screen, x, y, char, color);
if e2 <= dx {
err += dx;
p1_c.y += sy;
}
}
}
draw_triangle :: (screen: *Screen, x1: s64, y1: s64, x2: s64, y2: s64, x3: s64, y3: s64, char: PIXEL_CHAR = .PIXEL_SOLID, color: COLOR = .FG_WHITE) {
draw_line(x1, y1, x2, y2, char, color);
draw_line(x2, y2, x3, y3, char, color);
draw_line(x3, y3, x1, y1, char, color);
draw_triangle :: (screen: *Screen, p1: Vec2s64, p2: Vec2s64, p3: Vec2s64, char: u8 = DEFAULT_PIXEL_CHAR) {
draw_line(screen, p1, p2, char);
draw_line(screen, p2, p3, char);
draw_line(screen, p3, p1, char);
}
clip :: (screen: *Screen, x: *s64, y: *s64) {
using screen;
if x.* < 0 then x.* = 0;
if x.* >= width then x.* = width;
if y.* < 0 then y.* = 0;
if y.* >= height then y.* = height;
draw_triangle :: (screen: *Screen, t: Triangle, char: u8 = DEFAULT_PIXEL_CHAR) {
draw_triangle(screen, t.p1, t.p2, t.p3, char);
}
fill :: (screen: *Screen, x1: s64, y1: s64, x2: s64, y2: s64, char: PIXEL_CHAR = .PIXEL_SOLID, color: COLOR = .FG_WHITE) {
clip(screen, *x1, *y1);
clip(screen, *x2, *y2);
draw_quad :: (screen: *Screen, p1: Vec2s64, p2: Vec2s64, char: u8 = DEFAULT_PIXEL_CHAR) {
// (x1, y1) => top left
// (x2, y2) => bottom right
for x: x1..x2 {
for y: y1..y2 {
draw(screen, x, y, char, color);
draw_line(screen, .{p1.x, p1.y}, .{p1.x, p2.y}, char);
draw_line(screen, .{p1.x, p2.y}, .{p2.x, p2.y}, char);
draw_line(screen, .{p2.x, p2.y}, .{p2.x, p1.y}, char);
draw_line(screen, .{p2.x, p1.y}, .{p1.x, p1.y}, char);
}
draw_quad :: (screen: *Screen, p1: Vec2s64, p2: Vec2s64, p3: Vec2s64, p4: Vec2s64, char: u8 = DEFAULT_PIXEL_CHAR) {
// (x1, y1) => top left
// (x2, y2) => bottom left
// (x3, y3) => bottom right
// (x4, y4) => top right
draw_line(screen, .{p1.x, p1.y}, .{p2.x, p2.y}, char);
draw_line(screen, .{p2.x, p2.y}, .{p3.x, p3.y}, char);
draw_line(screen, .{p3.x, p3.y}, .{p4.x, p4.y}, char);
draw_line(screen, .{p4.x, p4.y}, .{p1.x, p1.y}, char);
}
draw_quad :: (screen: *Screen, q: Quad, char: u8 = DEFAULT_PIXEL_CHAR) {
draw_quad(screen, q.p1, q.p2, q.p3, q.p4, char);
}
draw_text :: (using screen: *Screen, p: Vec2s64, s: string) {
if p.x >= 0 && p.x < width && p.y >= 0 && p.y < height {
for cast([]u8)s {
buffer[(p.y * width + p.x) + it_index] = it;
}
}
}
fill_entire_screen :: (screen: *Screen, char: PIXEL_CHAR = .PIXEL_SOLID, color: COLOR = .BG_BLACK) {
fill(screen, 0, 0, screen.width, screen.height, char, color);
// When attempting to draw outside of the visible screen, "clip" the pixels
// by setting their position to the maximum width / height available
clip :: (using screen: *Screen, p: *Vec2s64) {
if p.x < 0 then p.x = 0;
if p.x >= width then p.x = width;
if p.y < 0 then p.y = 0;
if p.y >= height then p.y = height;
}
draw_string :: (screen: *Screen, x: s64, y: s64, text: string, c: COLOR = .FG_WHITE) {
assert(false, "not implemented");
fill :: (screen: *Screen, p1: Vec2s64, p2: Vec2s64, char: u8 = DEFAULT_PIXEL_CHAR) {
clip(screen, *p1);
clip(screen, *p2);
for x: p1.x..p2.x {
for y: p1.y..p2.y {
draw(screen, .{x, y}, char);
}
}
}
fill_entire_screen :: (screen: *Screen, char: u8 = DEFAULT_PIXEL_CHAR) {
fill(screen, .{0,0}, .{screen.width, screen.height}, char);
}

View File

@@ -0,0 +1,42 @@
get_console_size :: () -> s64, s64 {
//@Hack - This is terrible, but this is typically called only once, so maybe it doesn't matter...
width := string_to_int(get_stdout_from_cmd("tput cols"));
height := string_to_int(get_stdout_from_cmd("tput lines"));
return cast(s64)width, cast(s64)height;
}
#scope_file
get_stdout_from_cmd :: (cmd: string) -> string {
buffer: [256]u8;
stream : *FILE;
data : string;
// m_cmd := tprint("% %", cmd, " 2>&1");
stream = popen(to_c_string(cmd), to_c_string("r"));
if stream {
while !feof(stream) {
if fgets(buffer.data, buffer.count, stream) {
data = tprint("%1%2", data, cast(string)(buffer));
}
}
pclose(stream);
}
return data;
}
to_c_string :: (s: string) -> *u8 {
result := cast(*u8) alloc(s.count + 1);
memcpy(result, s.data, s.count);
result[s.count] = 0;
return result;
}
#import "POSIX";

View File

@@ -0,0 +1,34 @@
get_triangle_centroid :: (t: Triangle) -> Vec2s64 {
return get_triangle_centroid(t.p1, t.p2, t.p3);
}
get_triangle_centroid :: (p1: Vec2s64, p2: Vec2s64, p3: Vec2s64) -> Vec2s64 {
x := (p1.x + p2.x + p3.x) / 3;
y := (p1.y + p2.y + p3.y) / 3;
return .{x, y};
}
make_quad_from_rect :: (p1: Vec2s64, p2: Vec2s64) -> Quad {
return .{
.{ p1.x, p1.y },
.{ p2.x, p1.y },
.{ p2.x, p2.y },
.{ p1.x, p2.y },
};
}
scale :: (factor: float, pn: ..*Vec2s64) {
for pn {
it.x = cast(s64)(it.x * factor);
it.y = cast(s64)(it.y * factor);
}
}
scale :: (factor: float, t: *Triangle) {
scale(factor, *t.p1, *t.p2, *t.p3);
}
scale :: (factor: float, q: *Quad) {
scale(factor, *q.p1, *q.p2, *q.p3, *q.p4);
}

View File

@@ -24,81 +24,53 @@
2 |OPQRSTU|
---------
As displayed in the above figure, the 0th element, 'A', in the sequence represents (0,0) in 2D space, while the 9th element, 'J' represents (2,1).
=> Calculating the two dimensional area of the console output yields the number of elements in the initial 1D sequence:
(7 * 3) = 21
This may appear obvious, however this is an important connection to make, when considering how the console represents its output.
--------------------------------------------------------------
The console application automatically transforms our 1D sequence, to a 2D sequence displayed on your monitor, based on the size of the console window.
The terminal automatically transforms our 1D sequence, to a 2D sequence displayed on your monitor, based on the size of the window.
As displayed in the above figure, the 0th element, 'A', in the sequence represents (0,0) in 2D space, while the 9th element, 'J' represents (2,1).
The console application will automatically draw the characters on your screen for you.
Because the console application is responsible for transforming our 1D array into a 2D output, we must provide the console with a 1D array.
Therefore in order to draw 2D and 3D elements on the screen, we must construct procedures in the code to represent the transformations from
Since the console is responsible for transforming our 1D array into a 2D output, we must provide the console with a 1D array.
In order to draw 2D and 3D elements on the screen, we must construct procedures in the code to represent the transformations from
higher dimensions, down to the first dimension.
This simple 2d -> 1d transformation is implemented as the `draw` procedure found in the `draw.jai` file in this module.
*/
BLANK_PIXEL : u8 : 0x20;
DEFAULT_PIXEL_CHAR : u8 : #char "#";
Vec2s64 :: struct {
x: s64;
y: s64;
}
Triangle :: struct {
p1: Vec2s64;
p3: Vec2s64;
p2: Vec2s64;
}
Quad :: struct {
p1: Vec2s64;
p2: Vec2s64;
p3: Vec2s64;
p4: Vec2s64;
}
Screen :: struct {
width : s64;
height: s64;
buffer: [..] Char_Info;
}
Char_Info :: struct {
color: COLOR;
char : PIXEL_CHAR;
}
COLOR :: enum u32 {
FG_BLACK :: 0x0000;
FG_DARK_BLUE :: 0x0001;
FG_DARK_GREEN :: 0x0002;
FG_DARK_CYAN :: 0x0003;
FG_DARK_RED :: 0x0004;
FG_DARK_MAGENTA :: 0x0005;
FG_DARK_YELLOW :: 0x0006;
FG_GREY :: 0x0007;
FG_DARK_GREY :: 0x0008;
FG_BLUE :: 0x0009;
FG_GREEN :: 0x000A;
FG_CYAN :: 0x000B;
FG_RED :: 0x000C;
FG_MAGENTA :: 0x000D;
FG_YELLOW :: 0x000E;
FG_WHITE :: 0x000F;
BG_BLACK :: 0x0000;
BG_DARK_BLUE :: 0x0010;
BG_DARK_GREEN :: 0x0020;
BG_DARK_CYAN :: 0x0030;
BG_DARK_RED :: 0x0040;
BG_DARK_MAGENTA :: 0x0050;
BG_DARK_YELLOW :: 0x0060;
BG_GREY :: 0x0070;
BG_DARK_GREY :: 0x0080;
BG_BLUE :: 0x0090;
BG_GREEN :: 0x00A0;
BG_CYAN :: 0x00B0;
BG_RED :: 0x00C0;
BG_MAGENTA :: 0x00D0;
BG_YELLOW :: 0x00E0;
BG_WHITE :: 0x00F0;
}
PIXEL_CHAR :: enum u32 {
PIXEL_SOLID :: 0x2588;
PIXEL_THREEQUARTERS :: 0x2593;
PIXEL_HALF :: 0x2592;
PIXEL_QUARTER :: 0x2591;
buffer: [..] u8;
}
init_screen :: (screen: *Screen, width: s64, height: s64) {
buffer : [..] Char_Info;
buffer : [..] u8;
array_resize(*buffer, width * height);
@@ -114,8 +86,35 @@ resize_screen :: (screen: *Screen, width: s64, height: s64) {
array_resize(*screen.buffer, width * height);
}
maybe_resize_screen :: (screen: *Screen) {
w, h := get_console_size();
should_update := false;
if w != screen.width {
should_update = true;
}
if h != screen.height {
should_update = true;
}
if should_update {
resize_screen(screen, w, h);
}
}
#load "draw.jai";
#load "rotate.jai";
#load "file_operations.jai";
#load "console.jai";
#load "math_extras.jai";
#if OS == .MACOS {
#load "macos/console.jai";
} else {
#assert false "Platform unsupported. Sorry.";
}
#scope_module

View File

@@ -0,0 +1,33 @@
// single point rotation
rotate :: (p: *Vec2s64, center: Vec2s64, angle: s64) {
rad := cast(float64)angle * (PI / 180.0);
// translate to center
tx := p.x - center.x;
ty := p.y - center.y;
// apply rotation
rx := cast(s64)(tx * cos(rad) - ty * sin(rad));
ry := cast(s64)(tx * sin(rad) + ty * cos(rad));
// translate back
p.x = rx + center.x;
p.y = ry + center.y;
}
// rotate n points
rotate :: (pn: ..*Vec2s64, center: Vec2s64, angle: s64) {
for pn {
rotate(it, center, angle);
}
}
// rotate 3 points via triangle struct
rotate :: (t: *Triangle, center: Vec2s64, angle: s64) {
rotate(*t.p1, *t.p2, *t.p3, center = center, angle = angle);
}
// rotate 4 points via quad struct
rotate :: (q: *Quad, center: Vec2s64, angle: s64) {
rotate(*q.p1, *q.p2, *q.p3, *q.p4, center = center, angle = angle);
}

View File

@@ -1,18 +1,84 @@
main :: () {
screen := New(Screen);
init_screen(screen, 3, 2);
fill_entire_screen(screen, color = .FG_BLACK);
w, h := get_console_size();
init_screen(screen, w, h);
// draw(screen, 0, 0, color = .FG_GREEN);
draw_line(screen, 0, 0, 0, 1);
// draw(screen, 3, 0, color = .FG_GREEN);
i := 0;
j := 0;
increase := true;
while true {
j += 1;
// Render to terminal output
//swap_buffer(screen);
if i == 0 {
increase = true;
}
print("%\n", screen.buffer);
if i == 100 {
increase = false;
}
if increase {
i += 1;
} else {
i -= 1;
}
fill_entire_screen(screen, BLANK_PIXEL);
t1 := Triangle.{
.{30 - i, 20},
.{95, 10 - (i / 2)},
.{90 + i, 40},
};
scale(0.01 * i, *t1);
draw_triangle(screen, t1, #char "%");
scale(0.02 * i, *t1);
draw_triangle(screen, t1, #char "%");
scale(0.03 * i, *t1);
draw_triangle(screen, t1, #char "%");
t2 := Triangle.{
.{100, 30},
.{140, 30},
.{120, 20},
};
rotate(*t2, get_triangle_centroid(t2), -j);
draw_triangle(screen, t2);
q1 := make_quad_from_rect(
.{100, 10},
.{110, 20}
);
rotate(*q1, .{100, 10}, j);
draw_quad(screen, q1, #char "-");
draw_text(screen, .{10, 10}, "[ TEXT-BASED RENDERING DEMO ]");
if i % 3 == 0 {
set_console_color(.RED);
} else if i % 5 == 0 {
set_console_color(.GREEN);
} else {
set_console_color(.BLUE, .BOLD);
}
swap_buffer(screen);
sleep_milliseconds(20);
reset_temporary_storage();
}
}
#import "Console_Render";
#import "Basic";
#import "Math";
#import "Print_Color";

29
term.scpt Executable file
View File

@@ -0,0 +1,29 @@
#!/usr/bin/osascript
on run argv
if length of argv is equal to 0
set command to ""
else
set command to item 1 of argv
end if
if length of argv is greater than 1
set profile to item 2 of argv
runWithProfile(command, profile)
else
runSimple(command)
end if
end run
on runSimple(command)
tell application "Terminal"
activate
set newTab to do script(command)
end tell
return newTab
end runSimple
on runWithProfile(command, profile)
set newTab to runSimple(command)
tell application "Terminal" to set current settings of newTab to (first settings set whose name is profile)
end runWithProfile