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 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;