From 823a8f79d3c5324a6c0f5b032118c77037adf5f3 Mon Sep 17 00:00:00 2001 From: Max Amundsen Date: Fri, 11 Oct 2024 00:58:24 -0400 Subject: [PATCH] colors, rotations, and more --- .focus-config | 7 +- modules/Console_Render/console.jai | 9 +- modules/Console_Render/draw.jai | 167 ++++++++++++----------- modules/Console_Render/macos/console.jai | 42 ++++++ modules/Console_Render/math_extras.jai | 34 +++++ modules/Console_Render/module.jai | 111 ++++++++------- modules/Console_Render/rotate.jai | 33 +++++ src/main.jai | 84 ++++++++++-- term.scpt | 29 ++++ 9 files changed, 367 insertions(+), 149 deletions(-) create mode 100644 modules/Console_Render/macos/console.jai create mode 100644 modules/Console_Render/math_extras.jai create mode 100644 modules/Console_Render/rotate.jai create mode 100755 term.scpt diff --git a/.focus-config b/.focus-config index 87bb07d..6d578f8 100644 --- a/.focus-config +++ b/.focus-config @@ -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 diff --git a/modules/Console_Render/console.jai b/modules/Console_Render/console.jai index e510285..ac9a7f4 100644 --- a/modules/Console_Render/console.jai +++ b/modules/Console_Render/console.jai @@ -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); + } } \ No newline at end of file diff --git a/modules/Console_Render/draw.jai b/modules/Console_Render/draw.jai index 3f350a4..129b639 100644 --- a/modules/Console_Render/draw.jai +++ b/modules/Console_Render/draw.jai @@ -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); } \ No newline at end of file diff --git a/modules/Console_Render/macos/console.jai b/modules/Console_Render/macos/console.jai new file mode 100644 index 0000000..aaa110e --- /dev/null +++ b/modules/Console_Render/macos/console.jai @@ -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"; \ No newline at end of file diff --git a/modules/Console_Render/math_extras.jai b/modules/Console_Render/math_extras.jai new file mode 100644 index 0000000..42e518a --- /dev/null +++ b/modules/Console_Render/math_extras.jai @@ -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); +} \ No newline at end of file diff --git a/modules/Console_Render/module.jai b/modules/Console_Render/module.jai index 3c54b9d..2ff6b51 100644 --- a/modules/Console_Render/module.jai +++ b/modules/Console_Render/module.jai @@ -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 diff --git a/modules/Console_Render/rotate.jai b/modules/Console_Render/rotate.jai new file mode 100644 index 0000000..d39b834 --- /dev/null +++ b/modules/Console_Render/rotate.jai @@ -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); +} \ No newline at end of file diff --git a/src/main.jai b/src/main.jai index bf8ed35..d3d81f6 100644 --- a/src/main.jai +++ b/src/main.jai @@ -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"; \ No newline at end of file +#import "Basic"; +#import "Math"; +#import "Print_Color"; \ No newline at end of file diff --git a/term.scpt b/term.scpt new file mode 100755 index 0000000..09e7d00 --- /dev/null +++ b/term.scpt @@ -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 \ No newline at end of file