diff --git a/src/main.zig b/src/main.zig index 0357341..49d2544 100644 --- a/src/main.zig +++ b/src/main.zig @@ -3,13 +3,13 @@ pub fn main() !void { defer _ = gpa.deinit(); const allocator = gpa.allocator(); - raylib.SetConfigFlags(raylib.FLAG_VSYNC_HINT); // | raylib.FLAG_WINDOW_RESIZABLE); + 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.recalculate(); + scales = Scales.init(); global_font = raylib.LoadFontEx("font/SCE-PS3-RD-R-LATIN.TTF", 32, 0, 250); raylib.SetTextureFilter(global_font.texture, raylib.TEXTURE_FILTER_BILINEAR); @@ -54,6 +54,7 @@ pub fn main() !void { screen_width = @floatFromInt(raylib.GetScreenWidth()); screen_height = @floatFromInt(raylib.GetScreenHeight()); scales.recalculate(); + column.refresh(false); } if (raylib.IsKeyPressed('D')) debug_draw = !debug_draw; if (raylib.IsKeyPressed('S')) raylib.TakeScreenshot("screenshot.png"); @@ -92,43 +93,74 @@ var scales: Scales = undefined; /// Cached scaling and positioning values for dynamic window resizing. pub const Scales = struct { - item_icon_small_width: f32, - item_icon_small_height: f32, - item_icon_large_width: f32, - item_icon_large_height: f32, + 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_scale: f32, + column_item_spacing_curve: Curve, column_title_font_size: f32, column_position_center: raylib.Vector2, column_position_spacing: f32, column_item_spacing: f32, - column_item_start: 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 = 1, .y = 26 }, + } }; + + self.icon_size_curve = Curve{ .points = &[_]Curve.Point{ + .{ .x = 0, .y = 48 }, + .{ .x = 1, .y = 80 }, + } }; + + self.column_item_spacing_curve = Curve{ .points = &[_]Curve.Point{ + .{ .x = 0, .y = 16, .right_tangent = -90 }, + .{ .x = 0.25, .y = 0 }, + .{ .x = 1, .y = 0 }, + } }; + + self.recalculate(); + return self; + } + /// Recalculate scales after screen resize. pub fn recalculate(self: *Scales) void { - self.item_icon_small_width = 67; - self.item_icon_small_height = 48; - self.item_icon_large_width = 67; - self.item_icon_large_height = 48; - self.item_title_font_size = 18; - self.item_subtitle_font_size = 12; + 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.column_icon_scale = 0.75; - self.column_title_font_size = 13; + 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 = 16; - self.column_item_start = 117.8; - self.column_item_before_start = 48; + self.column_item_spacing = self.column_item_spacing_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; } }; +// TODO selected item scale up +// TODO item actions +// TODO item groups +// TODO item group sort pub const Column = struct { icon: raylib.Texture2D, title: []const u8, @@ -155,8 +187,8 @@ pub const Column = struct { .box = .{ .x = scales.column_position_center.x, .y = scales.column_position_center.y, - .w = 67, - .h = 48, + .w = scales.icon_width, + .h = scales.icon_height, }, }; @@ -173,7 +205,6 @@ pub const Column = struct { .align_h = .center, }; - // self.start_y = scales.column_position_center.y + icon.box.h + title.box.h + scales.column_item_spacing; for (self.items.items) |item| item.update(); for (self.items.items) |item| item.draw(); @@ -206,19 +237,21 @@ pub const Column = struct { } fn refresh(self: *Column, comptime animate: bool) void { - var y = scales.column_item_before_start - (scales.item_icon_small_height + scales.column_item_spacing) * @as(f32, @floatFromInt(self.selected)); + 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.setPosition(animate, .{ .x = scales.column_position_center.x, .y = y }); - y += scales.item_icon_small_height + scales.column_item_spacing; + y += scales.icon_height + scales.column_item_spacing; } - y = scales.column_item_start; + y = scales.column_item_after_start; for (self.items.items[self.selected..]) |item| { item.setPosition(animate, .{ .x = scales.column_position_center.x, .y = y }); - y += scales.item_icon_small_height + scales.column_item_spacing; + y += scales.icon_height + scales.column_item_spacing; } } }; +// TODO animated item icon scale +// TODO animated text fade in/out pub const Item = struct { position: raylib.Vector2 = .{ .x = 0, .y = 0 }, animator: Animator, @@ -226,7 +259,6 @@ pub const Item = struct { icon: raylib.Texture2D, title: []const u8, subtitle: []const u8, - large: bool = false, pub fn init(texture: raylib.Texture2D, title: []const u8, subtitle: []const u8) Item { raylib.SetTextureFilter(texture, raylib.TEXTURE_FILTER_BILINEAR); @@ -245,10 +277,10 @@ pub const Item = struct { var icon = Image{ .texture = self.icon, .box = .{ - .x = self.position.x - scales.item_icon_small_width / 2.0 + 67.0 / 2.0, + .x = self.position.x - scales.icon_width / 2.0 + 67.0 / 2.0, .y = self.position.y, - .w = scales.item_icon_small_width, - .h = scales.item_icon_small_height, + .w = scales.icon_width, + .h = scales.icon_height, }, }; @@ -521,6 +553,141 @@ fn createCamera() raylib.Camera3D { 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;