894 lines
29 KiB
Zig
894 lines
29 KiB
Zig
pub fn main() !void {
|
|
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
|
|
defer _ = gpa.deinit();
|
|
const allocator = gpa.allocator();
|
|
|
|
raylib.SetConfigFlags(raylib.FLAG_VSYNC_HINT | raylib.FLAG_WINDOW_RESIZABLE);
|
|
raylib.InitWindow(@intFromFloat(screen_width), @intFromFloat(screen_height), "ReX");
|
|
defer raylib.CloseWindow();
|
|
|
|
raylib.SetWindowMinSize(480, 272);
|
|
raylib.SetWindowMaxSize(1920, 1080);
|
|
scales = Scales.init();
|
|
|
|
datetime = DateTime{ .secs = @intCast(std.time.timestamp()) };
|
|
|
|
global_font = raylib.LoadFontEx("font/SCE-PS3-RD-R-LATIN.TTF", 32, 0, 250);
|
|
raylib.SetTextureFilter(global_font.texture, raylib.TEXTURE_FILTER_BILINEAR);
|
|
|
|
var background = Background.init();
|
|
var crossmenu = CrossMenu.init(allocator);
|
|
defer crossmenu.deinit();
|
|
|
|
const game_column = try crossmenu.appendColumn(.{
|
|
.icon = raylib.LoadTexture("icon-normal-3.png"),
|
|
.title = "Game",
|
|
});
|
|
|
|
try game_column.appendItem(.{
|
|
.icon = raylib.LoadTexture("menu/game/CometCrash/ICON0.PNG"),
|
|
.title = "Comet Crash",
|
|
.subtitle = "3/1/2025 23:11",
|
|
});
|
|
|
|
try game_column.appendItem(.{
|
|
.icon = raylib.LoadTexture("menu/game/LBP1/ICON0.PNG"),
|
|
.title = "LittleBigPlanet",
|
|
.subtitle = "3/1/2025 23:15",
|
|
});
|
|
|
|
icon_shader = raylib.LoadShaderFromMemory(0, @embedFile("shaders/icon.fs"));
|
|
defer raylib.UnloadShader(icon_shader);
|
|
|
|
const light_dir_loc = raylib.GetShaderLocation(icon_shader, "lightDir");
|
|
const light_color_loc = raylib.GetShaderLocation(icon_shader, "lightColor");
|
|
const ambient_color_loc = raylib.GetShaderLocation(icon_shader, "ambientColor");
|
|
|
|
const icon_mask = raylib.LoadTexture("icon-mask.png");
|
|
defer raylib.UnloadTexture(icon_mask);
|
|
|
|
const icon_normal = raylib.LoadTexture("icon-normal-2.png");
|
|
defer raylib.UnloadTexture(icon_normal);
|
|
|
|
raylib.SetShaderValue(icon_shader, light_dir_loc, &raylib.Vector3{ .x = 0.5, .y = 0, .z = 0 }, raylib.SHADER_UNIFORM_VEC3);
|
|
raylib.SetShaderValue(icon_shader, light_color_loc, &raylib.Vector4{ .x = 1, .y = 1, .z = 1, .w = 1.5 }, raylib.SHADER_UNIFORM_VEC4);
|
|
raylib.SetShaderValue(icon_shader, ambient_color_loc, &raylib.Vector4{ .x = 0.94, .y = 0.97, .z = 0.98, .w = 0.8 }, raylib.SHADER_UNIFORM_VEC4);
|
|
|
|
raylib.SetTargetFPS(120);
|
|
while (!raylib.WindowShouldClose()) {
|
|
if (raylib.IsWindowResized()) {
|
|
screen_width = @floatFromInt(raylib.GetScreenWidth());
|
|
screen_height = @floatFromInt(raylib.GetScreenHeight());
|
|
scales.recalculate();
|
|
crossmenu.refresh(false);
|
|
}
|
|
if (raylib.IsKeyPressed('D')) debug_draw = !debug_draw;
|
|
if (raylib.IsKeyPressed('S')) raylib.TakeScreenshot("screenshot.png");
|
|
|
|
datetime.update();
|
|
background.update();
|
|
crossmenu.update();
|
|
|
|
raylib.BeginDrawing();
|
|
defer raylib.EndDrawing();
|
|
|
|
background.draw();
|
|
crossmenu.draw();
|
|
|
|
if (debug_draw) {
|
|
drawDebugGrid();
|
|
|
|
const debug_text = try std.fmt.allocPrint(allocator,
|
|
\\screen size = {d}x{d}
|
|
, .{
|
|
screen_width,
|
|
screen_height,
|
|
});
|
|
defer allocator.free(debug_text);
|
|
raylib.DrawText(@ptrCast(debug_text), 2, 2, 8, raylib.GREEN);
|
|
}
|
|
}
|
|
}
|
|
|
|
var debug_draw = false;
|
|
|
|
var global_font: raylib.Font = undefined;
|
|
var icon_shader: raylib.Shader = undefined;
|
|
|
|
var screen_width: f32 = 480;
|
|
var screen_height: f32 = 272;
|
|
|
|
var scales: Scales = undefined;
|
|
|
|
var datetime: DateTime = undefined;
|
|
|
|
/// Cached scaling and positioning values for dynamic window resizing.
|
|
pub const Scales = struct {
|
|
font_size_curve: Curve,
|
|
icon_size_curve: Curve,
|
|
|
|
icon_height: f32,
|
|
icon_width: f32,
|
|
|
|
item_title_font_size: f32,
|
|
item_subtitle_font_size: f32,
|
|
|
|
column_icon_padding_top_curve: Curve,
|
|
column_item_spacing_curve: Curve,
|
|
column_selected_item_icon_scale_curve: Curve,
|
|
column_title_font_size: f32,
|
|
column_position_center: raylib.Vector2,
|
|
column_position_spacing: f32,
|
|
column_item_spacing: f32,
|
|
column_selected_item_icon_scale: f32,
|
|
column_icon_padding_top: f32,
|
|
column_item_after_start: f32,
|
|
column_item_before_start: f32,
|
|
|
|
pub fn init() Scales {
|
|
var self: Scales = undefined;
|
|
|
|
self.font_size_curve = Curve{ .points = &[_]Curve.Point{
|
|
.{ .x = 0, .y = 18 },
|
|
.{ .x = 0.25, .y = 18 },
|
|
.{ .x = 1, .y = 26 },
|
|
} };
|
|
|
|
self.icon_size_curve = Curve{ .points = &[_]Curve.Point{
|
|
.{ .x = 0, .y = 48 },
|
|
.{ .x = 1, .y = 80 },
|
|
} };
|
|
|
|
self.column_icon_padding_top_curve = Curve{ .points = &[_]Curve.Point{
|
|
.{ .x = 0, .y = 0 },
|
|
.{ .x = 0.25, .y = 0 },
|
|
.{ .x = 1, .y = 16 },
|
|
} };
|
|
|
|
self.column_item_spacing_curve = Curve{ .points = &[_]Curve.Point{
|
|
.{ .x = 0, .y = 10, .right_tangent = -90 },
|
|
.{ .x = 0.25, .y = 0 },
|
|
.{ .x = 1, .y = 0 },
|
|
} };
|
|
|
|
self.column_selected_item_icon_scale_curve = Curve{
|
|
.points = &[_]Curve.Point{
|
|
.{ .x = 0, .y = 1 },
|
|
.{ .x = 0.25, .y = 1.5 },
|
|
.{ .x = 1, .y = 2 },
|
|
},
|
|
};
|
|
|
|
self.recalculate();
|
|
return self;
|
|
}
|
|
|
|
/// Recalculate scales after screen resize.
|
|
pub fn recalculate(self: *Scales) void {
|
|
const height = raylib.Remap(screen_height, 272, 1080, 0, 1);
|
|
const font_size = self.font_size_curve.sample(height);
|
|
const icon_size = self.icon_size_curve.sample(height);
|
|
|
|
self.icon_height = icon_size;
|
|
self.icon_width = icon_size * 1.395833;
|
|
|
|
self.item_title_font_size = font_size;
|
|
self.item_subtitle_font_size = font_size * 0.666667;
|
|
|
|
self.column_title_font_size = font_size * 0.722222;
|
|
self.column_position_center = .{
|
|
.x = std.math.lerp(0.0, screen_width, 0.18),
|
|
.y = std.math.lerp(0.0, screen_height, 0.15),
|
|
};
|
|
self.column_position_spacing = 64;
|
|
self.column_item_spacing = self.column_item_spacing_curve.sample(height);
|
|
self.column_selected_item_icon_scale = self.column_selected_item_icon_scale_curve.sample(height);
|
|
self.column_icon_padding_top = self.column_icon_padding_top_curve.sample(height);
|
|
self.column_item_after_start = self.column_position_center.y + self.icon_height + self.column_title_font_size + 12;
|
|
self.column_item_before_start = self.column_position_center.y - self.column_icon_padding_top;
|
|
}
|
|
};
|
|
|
|
var debug_colors: DebugColors = .{};
|
|
|
|
pub const DebugColors = struct {
|
|
screen_grid_color: raylib.Color = .{ .r = 200, .g = 220, .b = 200, .a = 32 },
|
|
screen_text_color: raylib.Color = .{ .r = 255, .g = 255, .b = 255, .a = 90 },
|
|
};
|
|
|
|
fn drawDebugColorLegend() void {
|
|
const list = [_]struct {}{};
|
|
|
|
var y: c_int = @intFromFloat(screen_height);
|
|
for (list) |item| {
|
|
_ = item;
|
|
_ = &y;
|
|
}
|
|
}
|
|
|
|
fn drawDebugGrid() void {
|
|
const height: c_int = @intFromFloat(screen_height);
|
|
const width: c_int = @intFromFloat(screen_width);
|
|
|
|
var x: c_int = 0;
|
|
while (x < width) : (x += 25) raylib.DrawLine(x, 0, x, height, debug_colors.screen_grid_color);
|
|
var y: c_int = 0;
|
|
while (y < height) : (y += 25) raylib.DrawLine(0, y, width, y, debug_colors.screen_grid_color);
|
|
|
|
x = 100;
|
|
while (x < width) : (x += 100) {
|
|
var buf: [8]u8 = undefined;
|
|
const text = std.fmt.bufPrint(&buf, "{d}", .{x}) catch unreachable;
|
|
buf[text.len] = 0;
|
|
raylib.DrawText(@ptrCast(text), x + 1, height - 15, 10, debug_colors.screen_text_color);
|
|
|
|
const size = raylib.MeasureText(@ptrCast(text), 10);
|
|
raylib.DrawText(@ptrCast(text), width - 15 - size, x, 10, debug_colors.screen_text_color);
|
|
}
|
|
}
|
|
|
|
pub const CrossMenu = struct {
|
|
allocator: Allocator,
|
|
columns: std.ArrayListUnmanaged(Column) = .empty,
|
|
selected: usize = 0,
|
|
|
|
pub fn init(allocator: Allocator) CrossMenu {
|
|
return .{ .allocator = allocator };
|
|
}
|
|
|
|
pub fn deinit(self: *CrossMenu) void {
|
|
for (self.columns.items) |*column| column.deinit();
|
|
self.columns.deinit(self.allocator);
|
|
}
|
|
|
|
pub fn refresh(self: *CrossMenu, comptime animate: bool) void {
|
|
for (self.columns.items) |*column| column.refresh(animate);
|
|
}
|
|
|
|
pub fn update(self: *CrossMenu) void {
|
|
for (self.columns.items) |*column| column.update();
|
|
}
|
|
|
|
pub fn draw(self: *CrossMenu) void {
|
|
for (self.columns.items) |*column| column.draw();
|
|
}
|
|
|
|
pub fn appendColumn(self: *CrossMenu, options: Column.Options) !*Column {
|
|
const column = try self.columns.addOne(self.allocator);
|
|
column.* = Column.init(self.allocator, options);
|
|
return column;
|
|
}
|
|
};
|
|
|
|
// TODO item actions
|
|
// TODO item groups
|
|
// TODO item group sort
|
|
pub const Column = struct {
|
|
items: std.ArrayList(Item),
|
|
selected: usize = 0,
|
|
|
|
icon: Image,
|
|
title: Text,
|
|
|
|
pub const Options = struct {
|
|
icon: raylib.Texture2D,
|
|
title: []const u8,
|
|
};
|
|
|
|
pub fn init(allocator: Allocator, options: Options) Column {
|
|
raylib.SetTextureFilter(options.icon, raylib.TEXTURE_FILTER_BILINEAR);
|
|
raylib.SetTextureWrap(options.icon, raylib.TEXTURE_WRAP_CLAMP);
|
|
|
|
return .{
|
|
.items = .init(allocator),
|
|
.icon = .{
|
|
.texture = options.icon,
|
|
.is_normal_map = true,
|
|
},
|
|
.title = .{
|
|
.string = options.title,
|
|
.align_h = .center,
|
|
},
|
|
};
|
|
}
|
|
|
|
pub fn deinit(self: *Column) void {
|
|
self.items.deinit();
|
|
}
|
|
|
|
pub fn draw(self: *Column) void {
|
|
self.icon.box = .{
|
|
.x = scales.column_position_center.x,
|
|
.y = scales.column_position_center.y,
|
|
.w = scales.icon_width,
|
|
.h = scales.icon_height,
|
|
};
|
|
|
|
self.title.font_size = scales.column_title_font_size;
|
|
self.title.box = .{
|
|
.x = self.icon.box.x - 8,
|
|
.y = self.icon.box.y + self.icon.box.h + 6,
|
|
.w = self.icon.box.w + 16,
|
|
.h = scales.column_title_font_size,
|
|
};
|
|
|
|
for (self.items.items) |*item| item.draw();
|
|
|
|
self.icon.draw();
|
|
self.title.draw();
|
|
}
|
|
|
|
pub fn appendItem(self: *Column, options: Item.Options) !void {
|
|
const item = try self.items.addOne();
|
|
item.* = Item.init(self.items.allocator, options);
|
|
self.refresh(false);
|
|
}
|
|
|
|
pub fn insertItem(self: *Column, idx: usize, options: Item.Options) !void {
|
|
const items = try self.items.addManyAt(idx, 1);
|
|
items[0] = Item.init(self.items.allocator, options);
|
|
self.refresh(false);
|
|
}
|
|
|
|
pub fn removeItem(self: *Column, idx: usize) void {
|
|
_ = try self.items.orderedRemove(idx);
|
|
self.refresh(false);
|
|
}
|
|
|
|
fn update(self: *Column) void {
|
|
const up = raylib.IsKeyPressed(raylib.KEY_UP) or raylib.IsKeyPressedRepeat(raylib.KEY_UP);
|
|
const down = raylib.IsKeyPressed(raylib.KEY_DOWN) or raylib.IsKeyPressedRepeat(raylib.KEY_DOWN);
|
|
if (up and self.selected > 0) self.selected -= 1;
|
|
if (down and self.selected < self.items.items.len - 1) self.selected += 1;
|
|
|
|
if (up or down) self.refresh(true);
|
|
}
|
|
|
|
fn refresh(self: *Column, comptime animate: bool) void {
|
|
var y = scales.column_item_before_start - (scales.icon_height + scales.column_item_spacing) * @as(f32, @floatFromInt(self.selected));
|
|
for (self.items.items[0..self.selected]) |*item| {
|
|
item.position.set(animate, .{ .x = scales.column_position_center.x, .y = y });
|
|
item.icon_scale.set(animate, 1.0);
|
|
y += scales.icon_height + scales.column_item_spacing;
|
|
}
|
|
y = scales.column_item_after_start;
|
|
for (self.items.items[self.selected..], self.selected..) |*item, i| {
|
|
const icon_scale = if (i == self.selected) scales.column_selected_item_icon_scale else 1.0;
|
|
item.position.set(animate, .{ .x = scales.column_position_center.x, .y = y });
|
|
item.icon_scale.set(animate, icon_scale);
|
|
y += scales.icon_height * icon_scale + scales.column_item_spacing + (std.math.pow(f32, 4.0, icon_scale) - 4.0);
|
|
}
|
|
}
|
|
};
|
|
|
|
// TODO animated text fade in/out
|
|
pub const Item = struct {
|
|
position: Tweener(raylib.Vector2, .{}) = .{},
|
|
icon_scale: Tweener(f32, 1.0) = .{},
|
|
|
|
icon: Image,
|
|
title: Text,
|
|
subtitle: Text,
|
|
|
|
pub const Options = struct {
|
|
icon: raylib.Texture2D,
|
|
title: []const u8,
|
|
subtitle: []const u8,
|
|
};
|
|
|
|
pub fn init(_: Allocator, options: Options) Item {
|
|
raylib.SetTextureFilter(options.icon, raylib.TEXTURE_FILTER_BILINEAR);
|
|
raylib.SetTextureWrap(options.icon, raylib.TEXTURE_WRAP_CLAMP);
|
|
|
|
return .{
|
|
.icon = .{ .texture = options.icon },
|
|
.title = .{
|
|
.string = options.title,
|
|
.align_v = .right,
|
|
},
|
|
.subtitle = .{
|
|
.string = options.subtitle,
|
|
.align_v = .left,
|
|
},
|
|
};
|
|
}
|
|
|
|
pub fn draw(self: *Item) void {
|
|
const position = self.position.update();
|
|
const icon_scale = self.icon_scale.update();
|
|
|
|
self.icon.box = .{
|
|
.x = position.x - scales.icon_width / 2.0 * (icon_scale - 1),
|
|
.y = position.y,
|
|
.w = scales.icon_width * icon_scale,
|
|
.h = scales.icon_height * icon_scale,
|
|
};
|
|
|
|
self.title.font_size = scales.item_title_font_size;
|
|
self.title.box = .{
|
|
.x = self.icon.box.x + 8 + self.icon.box.w,
|
|
.y = self.icon.box.y,
|
|
.w = 300,
|
|
.h = self.icon.box.h / 2.0 - 2,
|
|
};
|
|
|
|
self.subtitle.font_size = scales.item_subtitle_font_size;
|
|
self.subtitle.box = .{
|
|
.x = self.icon.box.x + 8 + self.icon.box.w,
|
|
.y = self.icon.box.y + self.icon.box.h / 2.0 + 1,
|
|
.w = 200,
|
|
.h = self.icon.box.h / 2.0 - 2,
|
|
};
|
|
|
|
self.icon.draw();
|
|
self.title.draw();
|
|
self.subtitle.draw();
|
|
}
|
|
};
|
|
|
|
pub fn Tweener(comptime T: type, comptime default: T) type {
|
|
return struct {
|
|
time: f32 = 0.0,
|
|
length: f32 = 0.333,
|
|
|
|
start: T = default,
|
|
target: T = default,
|
|
current: T = default,
|
|
|
|
fn set(self: *Self, comptime animate: bool, value: T) void {
|
|
if (animate) {
|
|
self.time = 0;
|
|
self.start = self.current;
|
|
self.target = value;
|
|
} else {
|
|
self.start = value;
|
|
self.target = value;
|
|
self.current = value;
|
|
}
|
|
}
|
|
|
|
fn update(self: *Self) T {
|
|
self.time += raylib.GetFrameTime();
|
|
self.current = switch (T) {
|
|
f32 => std.math.lerp(self.start, self.target, self.easeOutExpo()),
|
|
raylib.Vector2 => raylib.Vector2Lerp(self.start, self.target, self.easeOutExpo()),
|
|
else => @compileError("no lerp function for type " ++ @typeName(T)),
|
|
};
|
|
return self.current;
|
|
}
|
|
|
|
fn easeOutExpo(self: Self) f32 {
|
|
return 1.0 - std.math.pow(f32, 2, -10 * std.math.clamp(self.time / self.length, 0.0, 1.0));
|
|
}
|
|
|
|
const Self = @This();
|
|
};
|
|
}
|
|
|
|
/// Optimized datetime that only pulls from the system clock every few seconds.
|
|
pub const DateTime = struct {
|
|
refresh_timer: f32 = 0.0,
|
|
secs: u64,
|
|
|
|
pub fn update(self: *DateTime) void {
|
|
self.refresh_timer += raylib.GetFrameTime();
|
|
if (self.refresh_timer >= 5) {
|
|
self.refresh_timer = 0;
|
|
self.secs = @intCast(std.time.timestamp());
|
|
}
|
|
}
|
|
|
|
pub fn getEpochSeconds(self: DateTime) std.time.epoch.EpochSeconds {
|
|
const secs = self.secs + @as(u64, @intFromFloat(self.refresh_timer));
|
|
return .{ .secs = secs };
|
|
}
|
|
|
|
pub fn getDaySeconds(self: DateTime) std.time.epoch.DaySeconds {
|
|
return self.getEpochSeconds().getDaySeconds();
|
|
}
|
|
|
|
pub fn getEpochDay(self: DateTime) std.time.epoch.EpochDay {
|
|
return self.getEpochSeconds().getEpochDay();
|
|
}
|
|
};
|
|
|
|
/// Draws the dynamic gradient background.
|
|
// TODO subdivided plane gradient
|
|
// TODO smoothly transition between gradients over a couple seconds
|
|
// TODO image wallpaper
|
|
// TODO slideshow wallpaper
|
|
// TODO animated image wallpaper
|
|
pub const Background = struct {
|
|
top_left: Color,
|
|
top_right: Color,
|
|
bottom_right: Color,
|
|
bottom_left: Color,
|
|
|
|
pub fn init() Background {
|
|
var self: Background = undefined;
|
|
self.update();
|
|
return self;
|
|
}
|
|
|
|
pub fn update(self: *Background) void {
|
|
const epoch_day = datetime.getEpochDay();
|
|
const year_day = epoch_day.calculateYearDay();
|
|
const month_day = year_day.calculateMonthDay();
|
|
|
|
const day_seconds = datetime.getDaySeconds();
|
|
const t = @sin(raylib.Remap(
|
|
@floatFromInt(day_seconds.secs),
|
|
0,
|
|
std.time.s_per_day / 2,
|
|
0,
|
|
std.math.pi,
|
|
));
|
|
|
|
self.setColors(switch (month_day.month) {
|
|
.jun => lerpColors(DAY_06, NIGHT_06, t),
|
|
.aug => lerpColors(DAY_08, NIGHT_08, t),
|
|
else => @panic("month background colors not implemented"),
|
|
});
|
|
}
|
|
|
|
pub fn setColors(self: *Background, colors: [4]Color) void {
|
|
self.top_left = colors[0];
|
|
self.bottom_right = colors[1];
|
|
self.top_right = colors[2];
|
|
self.bottom_left = colors[3];
|
|
}
|
|
|
|
pub fn draw(self: *Background) void {
|
|
if (debug_draw)
|
|
raylib.ClearBackground(raylib.BLACK)
|
|
else
|
|
raylib.DrawRectangleGradientEx(
|
|
.{
|
|
.x = 0,
|
|
.y = 0,
|
|
.width = screen_width,
|
|
.height = screen_height,
|
|
},
|
|
self.top_left.toRaylib(),
|
|
self.bottom_left.toRaylib(),
|
|
self.top_right.toRaylib(),
|
|
self.bottom_right.toRaylib(),
|
|
);
|
|
}
|
|
|
|
pub const Color = struct {
|
|
r: f32,
|
|
g: f32,
|
|
b: f32,
|
|
|
|
fn toRaylib(self: Color) raylib.Color {
|
|
return .{
|
|
.r = @intFromFloat(std.math.clamp(self.r, 0.0, 1.0) * 255.0),
|
|
.g = @intFromFloat(std.math.clamp(self.g, 0.0, 1.0) * 255.0),
|
|
.b = @intFromFloat(std.math.clamp(self.b, 0.0, 1.0) * 255.0),
|
|
.a = 255,
|
|
};
|
|
}
|
|
|
|
fn lerp(self: Color, other: Color, t: f32) Color {
|
|
return .{
|
|
.r = std.math.lerp(self.r, other.r, t),
|
|
.g = std.math.lerp(self.g, other.g, t),
|
|
.b = std.math.lerp(self.b, other.b, t),
|
|
};
|
|
}
|
|
};
|
|
|
|
fn lerpColors(a: [4]Color, b: [4]Color, t: f32) [4]Color {
|
|
var colors = a;
|
|
for (&colors, a, b) |*color, aa, bb| color.* = aa.lerp(bb, t);
|
|
return colors;
|
|
}
|
|
|
|
pub const DAY_06 = [4]Color{
|
|
.{ .r = 0.408, .g = 0.333, .b = 0.643 },
|
|
.{ .r = 0.518, .g = 0.365, .b = 0.855 },
|
|
.{ .r = 0.761, .g = 0.510, .b = 0.851 },
|
|
.{ .r = 0.569, .g = 0.325, .b = 0.620 },
|
|
};
|
|
|
|
pub const NIGHT_06 = [4]Color{
|
|
.{ .r = 0.039, .g = 0.031, .b = 0.035 },
|
|
.{ .r = 0.031, .g = 0.039, .b = 0.035 },
|
|
.{ .r = 0.824, .g = 0.647, .b = 0.878 },
|
|
.{ .r = 0.784, .g = 0.608, .b = 0.831 },
|
|
};
|
|
|
|
pub const DAY_08 = [4]Color{
|
|
.{ .r = 0.243, .g = 0.608, .b = 0.831 },
|
|
.{ .r = 0.039, .g = 0.690, .b = 0.878 },
|
|
.{ .r = 0.016, .g = 0.306, .b = 0.694 },
|
|
.{ .r = 0.000, .g = 0.027, .b = 0.310 },
|
|
};
|
|
|
|
pub const NIGHT_08 = [4]Color{
|
|
.{ .r = 0.000, .g = 0.145, .b = 0.349 },
|
|
.{ .r = 0.000, .g = 0.008, .b = 0.106 },
|
|
.{ .r = 0.251, .g = 0.494, .b = 0.576 },
|
|
.{ .r = 0.008, .g = 0.537, .b = 0.612 },
|
|
};
|
|
};
|
|
|
|
pub const BoundingBox = struct {
|
|
x: f32 = 0,
|
|
y: f32 = 0,
|
|
w: f32,
|
|
h: f32,
|
|
};
|
|
|
|
/// A texture within a bounding box. Positions and scales based on configurable parameters.
|
|
/// Despite the BoundingBox name, will never crop the texture, even when using `Mode.original`.
|
|
pub const Image = struct {
|
|
box: BoundingBox = undefined,
|
|
texture: raylib.Texture2D,
|
|
// normal: ?raylib.Texture2D = null,
|
|
is_normal_map: bool = false,
|
|
|
|
mode: Mode = .fit,
|
|
align_h: Align = .center,
|
|
align_v: Align = .center,
|
|
|
|
pub fn draw(self: *Image) void {
|
|
const width: f32 = @floatFromInt(self.texture.width);
|
|
const height: f32 = @floatFromInt(self.texture.height);
|
|
const width_scale: f32 = if (width == 0) 0.0 else self.box.w / width;
|
|
const height_scale: f32 = if (height == 0) 0.0 else self.box.h / height;
|
|
const scale = switch (self.mode) {
|
|
.original => 1.0,
|
|
.fit => @min(width_scale, height_scale),
|
|
.fill => @max(width_scale, height_scale),
|
|
};
|
|
const position = raylib.Vector2{
|
|
.x = switch (self.align_h) {
|
|
.left => 0,
|
|
.center => self.box.x + self.box.w / 2.0 - (width * scale) / 2.0,
|
|
.right => self.box.x + self.box.w - width * scale,
|
|
},
|
|
.y = switch (self.align_v) {
|
|
.left => 0,
|
|
.center => self.box.y + self.box.h / 2.0 - (height * scale) / 2.0,
|
|
.right => self.box.y + self.box.h - height * scale,
|
|
},
|
|
};
|
|
if (debug_draw) {
|
|
raylib.DrawRectangleLines(
|
|
@intFromFloat(self.box.x),
|
|
@intFromFloat(self.box.y),
|
|
@intFromFloat(self.box.w),
|
|
@intFromFloat(self.box.h),
|
|
raylib.MAROON,
|
|
);
|
|
} else {
|
|
if (self.is_normal_map) {
|
|
raylib.BeginShaderMode(icon_shader);
|
|
defer raylib.EndShaderMode();
|
|
raylib.DrawTextureEx(self.texture, position, 0, scale, raylib.WHITE);
|
|
} else raylib.DrawTextureEx(self.texture, position, 0, scale, raylib.WHITE);
|
|
}
|
|
}
|
|
|
|
pub const Mode = enum { original, fit, fill };
|
|
pub const Align = enum { left, center, right };
|
|
};
|
|
|
|
/// Draws a string inside a bounding box.
|
|
// TODO ellipsize text
|
|
// TODO clip text and scroll
|
|
pub const Text = struct {
|
|
box: BoundingBox = undefined,
|
|
string: []const u8,
|
|
font_size: f32 = undefined,
|
|
font_spacing: f32 = 1,
|
|
|
|
align_h: Align = .left,
|
|
align_v: Align = .left,
|
|
|
|
pub fn draw(self: *Text) void {
|
|
const size = raylib.MeasureTextEx(
|
|
global_font,
|
|
@ptrCast(self.string),
|
|
self.font_size,
|
|
self.font_spacing,
|
|
);
|
|
if (debug_draw)
|
|
raylib.DrawRectangleLines(
|
|
@intFromFloat(self.box.x),
|
|
@intFromFloat(self.box.y),
|
|
@intFromFloat(self.box.w),
|
|
@intFromFloat(self.box.h),
|
|
raylib.GREEN,
|
|
)
|
|
else
|
|
raylib.DrawTextEx(
|
|
global_font,
|
|
@ptrCast(self.string),
|
|
.{
|
|
.x = switch (self.align_h) {
|
|
.left => self.box.x,
|
|
.center => self.box.x + self.box.w / 2.0 - size.x / 2.0,
|
|
.right => self.box.x + self.box.w - size.x,
|
|
},
|
|
.y = switch (self.align_v) {
|
|
.left => self.box.y,
|
|
.center => self.box.y + self.box.h / 2.0 - size.y / 2.0,
|
|
.right => self.box.y + self.box.h - size.y,
|
|
},
|
|
},
|
|
self.font_size,
|
|
self.font_spacing,
|
|
raylib.WHITE,
|
|
);
|
|
}
|
|
|
|
pub const Align = enum { left, center, right };
|
|
};
|
|
|
|
/// Create ortho camera looking down at the XY plane, with a fovy of 1 for UV-like positioning.
|
|
fn createCamera() raylib.Camera3D {
|
|
var camera = raylib.Camera3D{};
|
|
|
|
camera.position.x = 0;
|
|
camera.position.y = 10;
|
|
camera.position.z = 0;
|
|
|
|
// camera pointing down, oriented correctly
|
|
// TODO this works but looks weird. do better.
|
|
camera.target.x = 0;
|
|
camera.target.y = 0;
|
|
camera.target.z = -0.000000000000001;
|
|
|
|
camera.up.x = 0;
|
|
camera.up.y = 1;
|
|
camera.up.z = 0;
|
|
|
|
camera.fovy = 1;
|
|
camera.projection = raylib.CAMERA_ORTHOGRAPHIC;
|
|
|
|
return camera;
|
|
}
|
|
|
|
// Based on parts of code from the MIT-licensed Godot Engine.
|
|
// https://github.com/godotengine/godot/blob/34b485d62b0a71e85a56fb46cf7ecc59963ed2d6/scene/resources/curve.cpp
|
|
//
|
|
// Copyright (c) 2014-present Godot Engine contributors.
|
|
// Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur.
|
|
//
|
|
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
// of this software and associated documentation files (the "Software"), to deal
|
|
// in the Software without restriction, including without limitation the rights
|
|
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
// copies of the Software, and to permit persons to whom the Software is
|
|
// furnished to do so, subject to the following conditions:
|
|
//
|
|
// The above copyright notice and this permission notice shall be included in all
|
|
// copies or substantial portions of the Software.
|
|
//
|
|
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
// SOFTWARE.
|
|
//
|
|
// -- Godot Engine <https://godotengine.org>
|
|
pub const Curve = struct {
|
|
points: []const Point,
|
|
|
|
pub fn sample(self: *Curve, x: f32) f32 {
|
|
if (self.points.len == 0) return 0;
|
|
if (self.points.len == 1) return self.points[0].y;
|
|
|
|
const i = self.getIndex(x);
|
|
if (i == self.points.len - 1) return self.points[i].y;
|
|
|
|
var local = x - self.points[i].x;
|
|
if (i == 0 and local <= 0) return self.points[0].y;
|
|
|
|
const a = self.points[i];
|
|
const b = self.points[i + 1];
|
|
|
|
var d = b.x - a.x;
|
|
if (std.math.approxEqRel(f32, 0, d, @sqrt(std.math.floatEps(f32))))
|
|
return b.y;
|
|
local /= d;
|
|
d /= 3.0;
|
|
const yac = a.y + d * a.right_tangent;
|
|
const ybc = b.y - d * b.left_tangent;
|
|
|
|
return bezierInterpolate(a.y, yac, ybc, b.y, local);
|
|
}
|
|
|
|
fn getIndex(self: *Curve, x: f32) usize {
|
|
var imin: usize = 0;
|
|
var imax = self.points.len - 1;
|
|
|
|
while (imax - imin > 1) {
|
|
const m = (imin + imax) / 2;
|
|
|
|
const a = self.points[m].x;
|
|
const b = self.points[m + 1].x;
|
|
|
|
if (a < x and b < x)
|
|
imin = m
|
|
else if (a > x)
|
|
imax = m
|
|
else
|
|
return m;
|
|
}
|
|
|
|
if (x > self.points[imax].x) return imax;
|
|
return imin;
|
|
}
|
|
|
|
fn bezierInterpolate(start: f32, control1: f32, control2: f32, end: f32, t: f32) f32 {
|
|
const omt = 1.0 - t;
|
|
const omt2 = omt * omt;
|
|
const omt3 = omt2 * omt;
|
|
const t2 = t * t;
|
|
const t3 = t2 * t;
|
|
|
|
return start * omt3 + control1 * omt2 * t * 3.0 + control2 * omt * t2 * 3.0 + end * t3;
|
|
}
|
|
|
|
// TODO use this
|
|
fn updateAutoTangents(self: *Curve, idx: usize) void {
|
|
var p = &self.points[idx];
|
|
|
|
if (idx > 0) {
|
|
if (p.left_mode == .linear) {
|
|
const v = raylib.Vector2Normalize(.{
|
|
.x = self.points[idx - 1].x - p.x,
|
|
.y = self.points[idx - 1].y - p.y,
|
|
});
|
|
p.left_tangent = v.y / v.x;
|
|
}
|
|
if (self.points[idx - 1].right_mode == .linear) {
|
|
const v = raylib.Vector2Normalize(.{
|
|
.x = self.points[idx - 1].x - p.x,
|
|
.y = self.points[idx - 1].y - p.y,
|
|
});
|
|
self.points[idx - 1].right_tangent = v.y / v.x;
|
|
}
|
|
}
|
|
|
|
if (idx + 1 < self.points.len) {
|
|
if (p.right_mode == .linear) {
|
|
const v = raylib.Vector2Normalize(.{
|
|
.x = self.points[idx + 1].x - p.x,
|
|
.y = self.points[idx + 1].y - p.y,
|
|
});
|
|
p.right_tangent = v.y / v.x;
|
|
}
|
|
if (self.points[idx + 1].left_mode == .linear) {
|
|
const v = raylib.Vector2Normalize(.{
|
|
.x = self.points[idx + 1].x - p.x,
|
|
.y = self.points[idx + 1].y - p.y,
|
|
});
|
|
self.points[idx + 1].right_tangent = v.y / v.x;
|
|
}
|
|
}
|
|
}
|
|
|
|
pub const Point = struct {
|
|
x: f32,
|
|
y: f32,
|
|
left_tangent: f32 = 0.0,
|
|
right_tangent: f32 = 0.0,
|
|
left_mode: TangentMode = .free,
|
|
right_mode: TangentMode = .free,
|
|
|
|
pub const TangentMode = enum { free, linear };
|
|
};
|
|
};
|
|
|
|
const std = @import("std");
|
|
const Allocator = std.mem.Allocator;
|
|
|
|
const c = @import("c.zig");
|
|
const raylib = c.raylib;
|