You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

1290 lines
44 KiB

11 months ago
10 months ago
11 months ago
10 months ago
11 months ago
10 months ago
10 months ago
8 months ago
10 months ago
10 months ago
10 months ago
10 months ago
10 months ago
11 months ago
11 months ago
10 months ago
11 months ago
11 months ago
10 months ago
11 months ago
11 months ago
11 months ago
11 months ago
11 months ago
10 months ago
10 months ago
10 months ago
10 months ago
11 months ago
10 months ago
11 months ago
11 months ago
11 months ago
11 months ago
11 months ago
11 months ago
11 months ago
11 months ago
11 months ago
11 months ago
11 months ago
11 months ago
9 months ago
9 months ago
9 months ago
11 months ago
11 months ago
11 months ago
10 months ago
11 months ago
11 months ago
10 months ago
11 months ago
11 months ago
11 months ago
11 months ago
11 months ago
11 months ago
11 months ago
11 months ago
11 months ago
11 months ago
10 months ago
11 months ago
11 months ago
11 months ago
10 months ago
11 months ago
11 months ago
  1. using OpenTK.Graphics.OpenGL4;
  2. using OpenTK.Mathematics;
  3. using OpenTK.Windowing.Common;
  4. using OpenTK.Windowing.Common.Input;
  5. using OpenTK.Windowing.Desktop;
  6. using OpenTK.Windowing.GraphicsLibraryFramework;
  7. // https://docs.sixlabors.com/api/ImageSharp/SixLabors.ImageSharp.Image.html
  8. using SixLabors.Fonts;
  9. using SixLabors.ImageSharp.Drawing.Processing;
  10. using SixLabors.ImageSharp.Drawing;
  11. using SixLabors.ImageSharp.Formats.Jpeg;
  12. using System;
  13. using System.Diagnostics;
  14. namespace SemiColinGames;
  15. public class FpsCounter {
  16. private readonly int[] frameTimes = new int[30];
  17. private double fps = 0;
  18. private int idx = 0;
  19. public int Fps {
  20. get => (int) Math.Ceiling(fps);
  21. }
  22. public void Update() {
  23. var now = Environment.TickCount; // ms
  24. if (frameTimes[idx] != 0) {
  25. var timeElapsed = now - frameTimes[idx];
  26. fps = 1000.0 * frameTimes.Length / timeElapsed;
  27. }
  28. frameTimes[idx] = now;
  29. idx = (idx + 1) % frameTimes.Length;
  30. }
  31. }
  32. public class CameraInfo {
  33. public static float AspectRatio = 6000f / 4000f;
  34. }
  35. public enum ToolStatus {
  36. Active,
  37. Done,
  38. Canceled
  39. }
  40. public class Transform {
  41. float activeScale;
  42. Vector2i activeOffset;
  43. Vector2i photoSize;
  44. public Transform(float scale, Vector2i offset, Vector2i photoSize) {
  45. activeScale = scale;
  46. activeOffset = offset;
  47. this.photoSize = photoSize;
  48. }
  49. public Vector2i ScreenToImageDelta(int x, int y) {
  50. return new((int) (x / activeScale), (int) (y / activeScale));
  51. }
  52. public Vector2i ScreenToImage(int x, int y) {
  53. int rx = (int) ((x - activeOffset.X) / activeScale);
  54. int ry = (int) ((y - activeOffset.Y) / activeScale);
  55. rx = Math.Clamp(rx, 0, photoSize.X);
  56. ry = Math.Clamp(ry, 0, photoSize.Y);
  57. return new(rx, ry);
  58. }
  59. public Vector2i ScreenToImage(Vector2i position) {
  60. return ScreenToImage(position.X, position.Y);
  61. }
  62. public Vector2i ImageToScreen(int x, int y) {
  63. int rx = (int) ((x * activeScale) + activeOffset.X);
  64. int ry = (int) ((y * activeScale) + activeOffset.Y);
  65. return new(rx, ry);
  66. }
  67. public Vector2i ImageToScreen(Vector2i position) {
  68. return ImageToScreen(position.X, position.Y);
  69. }
  70. }
  71. public interface ITool {
  72. ToolStatus HandleInput(KeyboardState input, MouseState mouse, Transform transform, Game game, Photo photo, UiGeometry geometry);
  73. string Status();
  74. }
  75. public class ViewTool : ITool {
  76. private bool dragging = false;
  77. public ToolStatus HandleInput(KeyboardState input, MouseState mouse, Transform transform, Game game, Photo photo, UiGeometry geometry) {
  78. Vector2i mousePosition = (Vector2i) mouse.Position;
  79. if (mouse.IsButtonPressed(MouseButton.Button1) && geometry.PhotoBox.ContainsInclusive(mousePosition)) {
  80. dragging = true;
  81. }
  82. if (!mouse.IsButtonDown(MouseButton.Button1)) {
  83. dragging = false;
  84. }
  85. if (dragging) {
  86. Vector2 delta = mouse.Delta;
  87. Vector2i imageDelta = transform.ScreenToImageDelta((int) delta.X, (int) delta.Y);
  88. photo.ViewOffset = Vector2i.Add(photo.ViewOffset, imageDelta);
  89. }
  90. return ToolStatus.Active;
  91. }
  92. public string Status() {
  93. return "";
  94. }
  95. }
  96. public class CropTool : ITool {
  97. enum Mode { Sizing, Translating };
  98. Photo photo;
  99. Vector2i mouseDragStart;
  100. Vector2i mouseDragEnd;
  101. Mode mode;
  102. string status = "";
  103. public CropTool(Photo photo) {
  104. this.photo = photo;
  105. mouseDragStart = new(photo.CropRectangle.Left, photo.CropRectangle.Top);
  106. mouseDragEnd = new(photo.CropRectangle.Right, photo.CropRectangle.Bottom);
  107. }
  108. public ToolStatus HandleInput(KeyboardState input, MouseState mouse, Transform transform, Game game, Photo photo, UiGeometry geometry) {
  109. Vector2i mousePosition = (Vector2i) mouse.Position;
  110. Vector2i imagePosition = transform.ScreenToImage(mousePosition);
  111. List<Vector2i> corners = Util.RectangleCorners(photo.CropRectangle);
  112. Vector2i? oppositeCorner = null;
  113. bool mouseNearHandle = false;
  114. for (int i = 0; i < 4; i++) {
  115. Vector2i corner = corners[i];
  116. Vector2i handlePosition = transform.ImageToScreen(corner.X, corner.Y);
  117. if (Vector2i.Subtract(mousePosition, handlePosition).ManhattanLength < 10) {
  118. mouseNearHandle = true;
  119. oppositeCorner = corners[(i + 2) % 4];
  120. break;
  121. }
  122. }
  123. bool mouseInRectangle = photo.CropRectangle.Contains(imagePosition.X, imagePosition.Y);
  124. if (mouse.IsButtonPressed(MouseButton.Button1)) {
  125. if (mouseNearHandle || !mouseInRectangle) {
  126. mode = Mode.Sizing;
  127. } else {
  128. mode = Mode.Translating;
  129. }
  130. }
  131. if (mouseNearHandle) {
  132. game.Cursor = MouseCursor.Hand;
  133. } else if (mouseInRectangle) {
  134. game.Cursor = MouseCursor.Default;
  135. } else {
  136. game.Cursor = MouseCursor.Crosshair;
  137. }
  138. if (mode == Mode.Sizing) {
  139. if (mouse.IsButtonPressed(MouseButton.Button1)) {
  140. mouseDragStart = oppositeCorner ?? imagePosition;
  141. }
  142. if (mouse.IsButtonDown(MouseButton.Button1)) {
  143. mouseDragEnd = imagePosition;
  144. }
  145. var (left, right, top, bottom) = GetCrop();
  146. if (left != right && top != bottom) {
  147. photo.CropRectangle = Rectangle.FromLTRB(left, top, right, bottom);
  148. } else {
  149. photo.CropRectangle = Rectangle.Empty;
  150. }
  151. } else {
  152. if (mouse.IsButtonDown(MouseButton.Button1)) {
  153. Vector2 delta = mouse.Delta;
  154. Vector2i imageDelta = transform.ScreenToImageDelta((int) delta.X, (int) delta.Y);
  155. photo.CropRectangle.Offset(imageDelta.X, imageDelta.Y);
  156. if (photo.CropRectangle.Left < 0) {
  157. photo.CropRectangle.Offset(-photo.CropRectangle.Left, 0);
  158. }
  159. if (photo.CropRectangle.Right > photo.Size.X) {
  160. int overshoot = photo.CropRectangle.Right - photo.Size.X;
  161. photo.CropRectangle.Offset(-overshoot, 0);
  162. }
  163. if (photo.CropRectangle.Top < 0) {
  164. photo.CropRectangle.Offset(0, -photo.CropRectangle.Top);
  165. }
  166. if (photo.CropRectangle.Bottom > photo.Size.Y) {
  167. int overshoot = photo.CropRectangle.Bottom - photo.Size.Y;
  168. photo.CropRectangle.Offset(0, -overshoot);
  169. }
  170. }
  171. }
  172. Rectangle r = photo.CropRectangle;
  173. status = $"({r.Left}, {r.Top}, {r.Right}, {r.Bottom}) {r.Width}x{r.Height}";
  174. if (input.IsKeyPressed(Keys.Enter)) {
  175. game.Cursor = MouseCursor.Default;
  176. if (photo.Rating < 1) {
  177. photo.Rating = 1;
  178. }
  179. photo.ViewOffset = new(photo.Size.X / 2 - Rectangle.Center(r).X,
  180. photo.Size.Y / 2 - Rectangle.Center(r).Y);
  181. return ToolStatus.Done;
  182. }
  183. if (input.IsKeyPressed(Keys.Escape)) {
  184. game.Cursor = MouseCursor.Default;
  185. photo.CropRectangle = Rectangle.Empty;
  186. return ToolStatus.Canceled;
  187. }
  188. return ToolStatus.Active;
  189. }
  190. // left, right, top, bottom
  191. (int, int, int, int) GetCrop() {
  192. // FIXME: allow for unconstrained crop, 1:1, etc.
  193. float aspectRatio = 1f * photo.Size.X / photo.Size.Y;
  194. Vector2i start = mouseDragStart;
  195. Vector2i end = mouseDragEnd;
  196. int width = Math.Abs(end.X - start.X);
  197. int height = Math.Abs(end.Y - start.Y);
  198. int heightChange = Math.Min(height, (int) (width / aspectRatio));
  199. int widthChange = (int) (heightChange * aspectRatio);
  200. if (end.X < start.X) {
  201. widthChange *= -1;
  202. }
  203. if (end.Y < start.Y) {
  204. heightChange *= -1;
  205. }
  206. end.Y = start.Y + heightChange;
  207. end.X = start.X + widthChange;
  208. int left = Math.Min(start.X, end.X);
  209. int right = Math.Max(start.X, end.X);
  210. int top = Math.Min(start.Y, end.Y);
  211. int bottom = Math.Max(start.Y, end.Y);
  212. return (left, right, top, bottom);
  213. }
  214. public string Status() {
  215. return "[crop] " + status;
  216. }
  217. }
  218. public class StraightenTool : ITool {
  219. Photo photo;
  220. int initialRotation;
  221. public StraightenTool(Photo photo) {
  222. this.photo = photo;
  223. initialRotation = photo.RotationDegreeHundredths;
  224. }
  225. public ToolStatus HandleInput(KeyboardState input, MouseState mouse, Transform transform, Game game, Photo photo, UiGeometry geometry) {
  226. if (input.IsKeyPressed(Keys.D0)) {
  227. photo.RotationDegreeHundredths = 0;
  228. }
  229. if (input.IsKeyPressed(Keys.Left)) {
  230. if (input.IsKeyDown(Keys.LeftControl)) {
  231. photo.RotationDegreeHundredths += 100;
  232. } else if (input.IsKeyDown(Keys.LeftShift)) {
  233. photo.RotationDegreeHundredths += 1;
  234. } else {
  235. photo.RotationDegreeHundredths += 10;
  236. }
  237. }
  238. if (input.IsKeyPressed(Keys.Right)) {
  239. if (input.IsKeyDown(Keys.LeftControl)) {
  240. photo.RotationDegreeHundredths -= 100;
  241. } else if (input.IsKeyDown(Keys.LeftShift)) {
  242. photo.RotationDegreeHundredths -= 1;
  243. } else {
  244. photo.RotationDegreeHundredths -= 10;
  245. }
  246. }
  247. if (input.IsKeyPressed(Keys.Enter)) {
  248. Matrix2 rotation = Matrix2.CreateRotation(MathHelper.DegreesToRadians(photo.RotationDegreeHundredths / 100f));
  249. Vector2 center = new(photo.Size.X / 2f, photo.Size.Y / 2f);
  250. float[] xCoords = new float[4];
  251. float[] yCoords = new float[4];
  252. List<Vector2i> corners = Util.RectangleCorners(new Rectangle(0, 0, photo.Size.X, photo.Size.Y));
  253. for (int i = 0; i < 4; i++) {
  254. Vector2i corner = corners[i];
  255. Vector2 rotated = Util.RotateAboutCenter(corner, center, rotation);
  256. xCoords[i] = rotated.X;
  257. yCoords[i] = rotated.Y;
  258. }
  259. Array.Sort(xCoords);
  260. Array.Sort(yCoords);
  261. for (int i = 3; i >= 0; i--) {
  262. xCoords[i] -= xCoords[0];
  263. yCoords[i] -= yCoords[0];
  264. }
  265. // FIXME: we can get a bigger crop using a better algorithm -- this is
  266. // too conservative. Also, preserve aspect ratio? Maybe we need to
  267. // commit this as a separate crop immediately so that further crops are
  268. // working on an image that isn't weirdly rotated. Or keep the
  269. // straighten-crop as a separate operation from an ordinary crop.
  270. int left = (int) Math.Ceiling(xCoords[1]);
  271. int right = (int) Math.Floor(xCoords[2]);
  272. int top = (int) Math.Ceiling(yCoords[1]);
  273. int bottom = (int) Math.Floor(yCoords[2]);
  274. photo.CropRectangle = Rectangle.FromLTRB(left, top, right, bottom);
  275. return ToolStatus.Done;
  276. }
  277. if (input.IsKeyPressed(Keys.Escape)) {
  278. photo.RotationDegreeHundredths = initialRotation;
  279. return ToolStatus.Canceled;
  280. }
  281. return ToolStatus.Active;
  282. }
  283. public string Status() {
  284. return String.Format("[straighten] {0:F2}", photo.RotationDegreeHundredths / 100f);
  285. }
  286. }
  287. public class UiGeometry {
  288. public static Vector2i MIN_WINDOW_SIZE = new(1024, 768);
  289. public readonly Vector2i WindowSize;
  290. public readonly Box2i ThumbnailBox;
  291. public readonly List<Box2i> ThumbnailBoxes = new();
  292. public readonly List<Box2i> StarBoxes = new();
  293. public readonly Box2i PhotoBox;
  294. public readonly Box2i StatusBox;
  295. public UiGeometry() : this(MIN_WINDOW_SIZE, 0) {}
  296. public UiGeometry(Vector2i windowSize, int starSize) {
  297. WindowSize = windowSize;
  298. int numThumbnailsPerColumn = Math.Max(WindowSize.Y / 100, 1);
  299. int thumbnailHeight = WindowSize.Y / numThumbnailsPerColumn;
  300. int thumbnailWidth = (int) (1.0 * thumbnailHeight * CameraInfo.AspectRatio);
  301. Console.WriteLine($"thumbnail size: {thumbnailWidth}x{thumbnailHeight}");
  302. int thumbnailColumns = 3;
  303. for (int j = thumbnailColumns; j > 0; j--) {
  304. for (int i = 0; i < numThumbnailsPerColumn; i++) {
  305. Box2i box = Util.MakeBox(WindowSize.X - thumbnailWidth * j, i * thumbnailHeight,
  306. thumbnailWidth, thumbnailHeight);
  307. ThumbnailBoxes.Add(box);
  308. }
  309. }
  310. int statusBoxHeight = 40;
  311. PhotoBox = new Box2i(
  312. 0, 0, WindowSize.X - thumbnailWidth * thumbnailColumns, WindowSize.Y - statusBoxHeight);
  313. StatusBox = new Box2i(
  314. 0, WindowSize.Y - statusBoxHeight, WindowSize.X - thumbnailWidth * thumbnailColumns, WindowSize.Y);
  315. ThumbnailBox = new Box2i(
  316. ThumbnailBoxes[0].Min.X, ThumbnailBoxes[0].Min.Y, WindowSize.X, WindowSize.Y);
  317. int starSpacing = 10;
  318. int starBoxLeft = (int) (PhotoBox.Center.X - 2.5 * starSize - starSpacing * 2);
  319. for (int i = 0; i < 5; i++) {
  320. Box2i box = Util.MakeBox(
  321. starBoxLeft + i * (starSize + starSpacing), PhotoBox.Max.Y - starSize - 10,
  322. starSize, starSize);
  323. StarBoxes.Add(box);
  324. }
  325. }
  326. }
  327. public static class Util {
  328. public const float PI = (float) Math.PI;
  329. public static int Lerp(int start, int end, double fraction) {
  330. return start + (int) ((end - start) * fraction);
  331. }
  332. public static Box2i MakeBox(int left, int top, int width, int height) {
  333. return new Box2i(left, top, left + width, top + height);
  334. }
  335. // resulting items are ordered such that a corner's opposite is 2 indexes away.
  336. public static List<Vector2i> RectangleCorners(Rectangle r) {
  337. List<Vector2i> result = new(4);
  338. result.Add(new(r.Left, r.Top));
  339. result.Add(new(r.Right, r.Top));
  340. result.Add(new(r.Right, r.Bottom));
  341. result.Add(new(r.Left, r.Bottom));
  342. return result;
  343. }
  344. public static Vector2 RotateAboutCenter(Vector2 vec, Vector2 center, Matrix2 transform) {
  345. Vector2 centerRelative = vec - center;
  346. centerRelative *= transform;
  347. return centerRelative + center;
  348. }
  349. public static Image<Rgba32> MakeImage(float width, float height) {
  350. return new((int) Math.Ceiling(width), (int) Math.Ceiling(height));
  351. }
  352. // https://sirv.com/help/articles/rotate-photos-to-be-upright/
  353. // FIXME: could use AutoOrientProcessor instead?
  354. public static void RotateImageFromExif(Image<Rgba32> image, ushort orientation) {
  355. if (orientation <= 1) {
  356. return;
  357. }
  358. var operations = new Dictionary<ushort, (RotateMode, FlipMode)> {
  359. { 2, (RotateMode.None, FlipMode.Horizontal) },
  360. { 3, (RotateMode.Rotate180, FlipMode.None) },
  361. { 4, (RotateMode.None, FlipMode.Vertical) },
  362. { 5, (RotateMode.Rotate90, FlipMode.Vertical) },
  363. { 6, (RotateMode.Rotate90, FlipMode.None) },
  364. { 7, (RotateMode.Rotate270, FlipMode.Vertical) },
  365. { 8, (RotateMode.Rotate270, FlipMode.None) },
  366. };
  367. var (rotate, flip) = operations[orientation];
  368. image.Mutate(x => x.RotateFlip(rotate, flip));
  369. }
  370. public static Texture RenderText(string text) {
  371. return RenderText(text, 16);
  372. }
  373. public static Texture RenderText(string text, int size) {
  374. // Make sure that 0-length text doesn't end up as a 0-size texture, which
  375. // might cause problems.
  376. if (text.Length == 0) {
  377. text = " ";
  378. }
  379. Font font = SystemFonts.CreateFont("Consolas", size, FontStyle.Bold);
  380. TextOptions options = new(font);
  381. FontRectangle rect = TextMeasurer.Measure(text, new TextOptions(font));
  382. Image<Rgba32> image = MakeImage(rect.Width, rect.Height);
  383. IBrush brush = Brushes.Solid(Color.White);
  384. image.Mutate(x => x.DrawText(options, text, brush));
  385. Texture texture = new Texture(image);
  386. image.Dispose();
  387. return texture;
  388. }
  389. // FIXME: make a real icon stored as a PNG...
  390. public static OpenTK.Windowing.Common.Input.Image[] RenderAppIcon() {
  391. int size = 64;
  392. Font font = SystemFonts.CreateFont("MS Mincho", size, FontStyle.Bold);
  393. TextOptions options = new(font);
  394. Image<Rgba32> image = MakeImage(size, size);
  395. IBrush brush = Brushes.Solid(Color.Black);
  396. image.Mutate(x => x.DrawText(options, "撮", brush));
  397. byte[] pixelBytes = new byte[size * size * 4];
  398. image.CopyPixelDataTo(pixelBytes);
  399. image.Dispose();
  400. OpenTK.Windowing.Common.Input.Image opentkImage = new(size, size, pixelBytes);
  401. return new OpenTK.Windowing.Common.Input.Image[]{ opentkImage };
  402. }
  403. public static Texture RenderStar(float radius, bool filled) {
  404. IPath path = new Star(x: radius, y: radius + 1, prongs: 5,
  405. innerRadii: radius * 0.4f, outerRadii: radius, angle: Util.PI);
  406. // We add a little bit to the width & height because the reported
  407. // path.Bounds are often a little tighter than they should be & a couple
  408. // pixels end up obviously missing...
  409. Image<Rgba32> image = MakeImage(path.Bounds.Width + 2, path.Bounds.Height + 2);
  410. IBrush brush = Brushes.Solid(Color.White);
  411. IPen white = Pens.Solid(Color.White, 1.5f);
  412. IPen black = Pens.Solid(Color.Black, 3f);
  413. image.Mutate(x => x.Draw(black, path));
  414. if (filled) {
  415. image.Mutate(x => x.Fill(brush, path));
  416. }
  417. image.Mutate(x => x.Draw(white, path));
  418. Texture texture = new Texture(image);
  419. image.Dispose();
  420. return texture;
  421. }
  422. }
  423. public class Toast {
  424. private string message = "";
  425. private double time;
  426. private double expiryTime;
  427. public void Set(string message) {
  428. this.message = message;
  429. this.expiryTime = time + 5.0;
  430. }
  431. public string Get() {
  432. return message;
  433. }
  434. public void Update(double elapsed) {
  435. time += elapsed;
  436. if (time > expiryTime) {
  437. message = "";
  438. }
  439. }
  440. }
  441. public class Game : GameWindow {
  442. public Game(GameWindowSettings gwSettings, NativeWindowSettings nwSettings) :
  443. base(gwSettings, nwSettings) {
  444. activeTool = viewTool;
  445. geometry = new UiGeometry(nwSettings.Size, STAR_FILLED.Size.X);
  446. }
  447. private static string outputRoot = @"c:\users\colin\desktop\totte-output";
  448. // private static string outputRoot = @"c:\users\colin\pictures\photos";
  449. private static Texture TEXTURE_WHITE = new(new Image<Rgba32>(1, 1, new Rgba32(255, 255, 255)));
  450. private static Texture TEXTURE_BLACK = new(new Image<Rgba32>(1, 1, new Rgba32(0, 0, 0)));
  451. private static Texture STAR_FILLED = Util.RenderStar(20, true);
  452. private static Texture STAR_EMPTY = Util.RenderStar(20, false);
  453. private static Texture STAR_SMALL = Util.RenderStar(6, true);
  454. UiGeometry geometry;
  455. FpsCounter fpsCounter = new();
  456. // Four points, each consisting of (x, y, z, tex_x, tex_y).
  457. float[] vertices = new float[20];
  458. // Indices to draw a rectangle from two triangles.
  459. uint[] indices = {
  460. 0, 1, 3, // first triangle
  461. 1, 2, 3 // second triangle
  462. };
  463. int VertexBufferObject;
  464. int ElementBufferObject;
  465. int VertexArrayObject;
  466. List<Photo> allPhotos = new();
  467. List<Photo> photos = new();
  468. HashSet<Photo> loadedImages = new();
  469. HashSet<Photo> loadingImages = new();
  470. readonly object loadedImagesLock = new();
  471. readonly ViewTool viewTool = new ViewTool();
  472. Toast toast = new();
  473. ITool activeTool;
  474. int photoIndex = 0;
  475. int ribbonIndex = 0;
  476. Vector2i mousePosition;
  477. float activeScale = 1f;
  478. Vector2i activeOffset;
  479. Transform transform = new(1f, Vector2i.Zero, Vector2i.Zero);
  480. Shader shader = new();
  481. Matrix4 projection;
  482. float zoomLevel = 0f;
  483. double timeSinceEvent = 0;
  484. // Variables that are protected by locks:
  485. readonly object numThumbnailsLoadedLock = new();
  486. int numThumbnailsLoaded = 0;
  487. readonly object exportPhotosLock = new(); // locks the entire ExportPhotos() function.
  488. int numPhotosToExport = 0;
  489. readonly object numPhotosExportedLock = new();
  490. int numPhotosExported = 0;
  491. protected override void OnUpdateFrame(FrameEventArgs e) {
  492. base.OnUpdateFrame(e);
  493. toast.Update(e.Time);
  494. KeyboardState input = KeyboardState;
  495. bool mouseInWindow = ClientRectangle.ContainsInclusive((Vector2i) MouseState.Position);
  496. if (input.IsAnyKeyDown ||
  497. MouseState.IsAnyButtonDown ||
  498. (mouseInWindow && MouseState.Delta != Vector2i.Zero)) {
  499. timeSinceEvent = 0;
  500. } else {
  501. timeSinceEvent += e.Time;
  502. }
  503. if (IsFocused && timeSinceEvent < 1) {
  504. RenderFrequency = 30;
  505. UpdateFrequency = 30;
  506. } else {
  507. RenderFrequency = 5;
  508. UpdateFrequency = 5;
  509. }
  510. Photo previousPhoto = photos[photoIndex];
  511. bool shiftIsDown = input.IsKeyDown(Keys.LeftShift) || input.IsKeyDown(Keys.RightShift);
  512. bool altIsDown = input.IsKeyDown(Keys.LeftAlt) || input.IsKeyDown(Keys.RightAlt);
  513. bool ctrlIsDown = input.IsKeyDown(Keys.LeftControl) || input.IsKeyDown(Keys.RightControl);
  514. // FIXME: add a confirm dialog before closing. (Also for the window-close button.)
  515. // FIXME: don't quit if there's pending file-write operations.
  516. // Close when Ctrl-Q is pressed.
  517. if (input.IsKeyPressed(Keys.Q) && ctrlIsDown) {
  518. Close();
  519. }
  520. mousePosition = (Vector2i) MouseState.Position;
  521. // Look for mouse clicks on thumbnails or stars.
  522. //
  523. // Note that we don't bounds-check photoIndex until after all the possible
  524. // inputs that might affect it. That simplifies this logic significantly.
  525. if (MouseState.IsButtonPressed(MouseButton.Button1)) {
  526. for (int i = 0; i < geometry.StarBoxes.Count; i++) {
  527. if (geometry.StarBoxes[i].ContainsInclusive(mousePosition)) {
  528. photos[photoIndex].Rating = i + 1;
  529. }
  530. }
  531. for (int i = 0; i < geometry.ThumbnailBoxes.Count; i++) {
  532. if (geometry.ThumbnailBoxes[i].ContainsInclusive(mousePosition)) {
  533. photoIndex = ribbonIndex + i;
  534. }
  535. }
  536. }
  537. if (MouseState.IsButtonPressed(MouseButton.Button4)) {
  538. photoIndex--;
  539. }
  540. if (MouseState.IsButtonPressed(MouseButton.Button5)) {
  541. photoIndex++;
  542. }
  543. if (MouseState.ScrollDelta.Y < 0) {
  544. photoIndex++;
  545. }
  546. if (MouseState.ScrollDelta.Y > 0) {
  547. photoIndex--;
  548. }
  549. if (input.IsKeyPressed(Keys.Down)) {
  550. photoIndex++;
  551. }
  552. if (input.IsKeyPressed(Keys.Up)) {
  553. photoIndex--;
  554. }
  555. if (input.IsKeyPressed(Keys.Home)) {
  556. photoIndex = 0;
  557. }
  558. if (input.IsKeyPressed(Keys.End)) {
  559. photoIndex = photos.Count - 1;
  560. }
  561. if (input.IsKeyPressed(Keys.PageDown)) {
  562. photoIndex += 10;
  563. }
  564. if (input.IsKeyPressed(Keys.PageUp)) {
  565. photoIndex -= 10;
  566. }
  567. if (input.IsKeyPressed(Keys.P) && ctrlIsDown) {
  568. ExportPhotos();
  569. }
  570. // Make sure the photoIndex is actually valid.
  571. if (photos.Count == 0) {
  572. photoIndex = 0;
  573. } else {
  574. photoIndex = Math.Clamp(photoIndex, 0, photos.Count - 1);
  575. }
  576. // Handle presses of the "rating" keys -- 0-5 and `.
  577. // A normal press just sets the rating of the current photo.
  578. // If the user is holding "shift", we instead filter to only show photos
  579. // of that rating or higher.
  580. int rating = -1;
  581. if (input.IsKeyPressed(Keys.D0) || input.IsKeyPressed(Keys.GraveAccent)) {
  582. rating = 0;
  583. }
  584. if (input.IsKeyPressed(Keys.D1)) {
  585. rating = 1;
  586. }
  587. if (input.IsKeyPressed(Keys.D2)) {
  588. rating = 2;
  589. }
  590. if (input.IsKeyPressed(Keys.D3)) {
  591. rating = 3;
  592. }
  593. if (input.IsKeyPressed(Keys.D4)) {
  594. rating = 4;
  595. }
  596. if (input.IsKeyPressed(Keys.D5)) {
  597. rating = 5;
  598. }
  599. if (rating >= 0) {
  600. if (shiftIsDown) {
  601. FilterByRating(rating);
  602. } else {
  603. if (photos.Count > 0) {
  604. photos[photoIndex].Rating = rating;
  605. }
  606. }
  607. }
  608. if (input.IsKeyPressed(Keys.Q)) {
  609. if (photos[photoIndex].CropRectangle != Rectangle.Empty) {
  610. Photo photo = photos[photoIndex];
  611. Rectangle r = photos[photoIndex].CropRectangle;
  612. photo.ViewOffset = new(photo.Size.X / 2 - Rectangle.Center(r).X,
  613. photo.Size.Y / 2 - Rectangle.Center(r).Y);
  614. } else {
  615. photos[photoIndex].ViewOffset = Vector2i.Zero;
  616. }
  617. zoomLevel = 0f;
  618. }
  619. if (input.IsKeyPressed(Keys.W)) {
  620. zoomLevel = 1f;
  621. }
  622. if (input.IsKeyPressed(Keys.E)) {
  623. zoomLevel = 2f;
  624. }
  625. if (input.IsKeyPressed(Keys.R)) {
  626. zoomLevel = 4f;
  627. }
  628. if (input.IsKeyPressed(Keys.T)) {
  629. zoomLevel = 8f;
  630. }
  631. if (input.IsKeyPressed(Keys.Y)) {
  632. zoomLevel = 16f;
  633. }
  634. // Handle tool switching.
  635. // FIXME: prevent photo switching when tools other than ViewTool are active?
  636. if (photos[photoIndex] != previousPhoto) {
  637. activeTool = viewTool;
  638. }
  639. if (activeTool == viewTool) {
  640. if (input.IsKeyPressed(Keys.C)) {
  641. activeTool = new CropTool(photos[photoIndex]);
  642. }
  643. if (input.IsKeyPressed(Keys.X)) {
  644. activeTool = new StraightenTool(photos[photoIndex]);
  645. }
  646. }
  647. // Delegate input to the active tool.
  648. ToolStatus status = activeTool.HandleInput(
  649. KeyboardState, MouseState, transform, this, photos[photoIndex], geometry);
  650. // Change back to the default tool if the active tool is done.
  651. if (status != ToolStatus.Active) {
  652. activeTool = viewTool;
  653. }
  654. }
  655. void FilterByRating(int rating) {
  656. Console.WriteLine("filter to " + rating);
  657. Photo previouslyActive = photos.Count > 0 ? photos[photoIndex] : allPhotos[0];
  658. photos = allPhotos.Where(p => p.Rating >= rating).ToList();
  659. // Move photoIndex to wherever the previously active photo was, or if it
  660. // was filtered out, to whichever unfiltered photo comes before it. This
  661. // is O(n) in the length of allPhotos, but how bad can it be? :)
  662. photoIndex = -1;
  663. for (int i = 0; i < allPhotos.Count; i++) {
  664. Photo candidate = allPhotos[i];
  665. if (candidate.Rating >= rating) {
  666. photoIndex++;
  667. }
  668. if (candidate == previouslyActive) {
  669. break;
  670. }
  671. }
  672. photoIndex = Math.Max(0, photoIndex);
  673. }
  674. // FIXME: switch to immediate mode??
  675. // https://gamedev.stackexchange.com/questions/198805/opentk-immediate-mode-on-net-core-doesnt-work
  676. // https://www.youtube.com/watch?v=Q23Kf9QEaO4
  677. protected override void OnLoad() {
  678. base.OnLoad();
  679. GL.ClearColor(0f, 0f, 0f, 1f);
  680. GL.Enable(EnableCap.Blend);
  681. GL.BlendFunc(BlendingFactor.SrcAlpha, BlendingFactor.OneMinusSrcAlpha);
  682. VertexArrayObject = GL.GenVertexArray();
  683. GL.BindVertexArray(VertexArrayObject);
  684. VertexBufferObject = GL.GenBuffer();
  685. ElementBufferObject = GL.GenBuffer();
  686. GL.BindBuffer(BufferTarget.ArrayBuffer, VertexBufferObject);
  687. GL.BufferData(BufferTarget.ArrayBuffer, vertices.Length * sizeof(float),
  688. vertices, BufferUsageHint.DynamicDraw);
  689. GL.BindBuffer(BufferTarget.ElementArrayBuffer, ElementBufferObject);
  690. GL.BufferData(BufferTarget.ElementArrayBuffer, indices.Length * sizeof(uint),
  691. indices, BufferUsageHint.DynamicDraw);
  692. shader.Init();
  693. shader.Use();
  694. // Because there's 5 floats between the start of the first vertex and the start of the second,
  695. // the stride is 5 * sizeof(float).
  696. // This will now pass the new vertex array to the buffer.
  697. var vertexLocation = shader.GetAttribLocation("aPosition");
  698. GL.EnableVertexAttribArray(vertexLocation);
  699. GL.VertexAttribPointer(vertexLocation, 3, VertexAttribPointerType.Float,
  700. false, 5 * sizeof(float), 0);
  701. // Next, we also setup texture coordinates. It works in much the same way.
  702. // We add an offset of 3, since the texture coordinates comes after the position data.
  703. // We also change the amount of data to 2 because there's only 2 floats for texture coordinates.
  704. var texCoordLocation = shader.GetAttribLocation("aTexCoord");
  705. GL.EnableVertexAttribArray(texCoordLocation);
  706. GL.VertexAttribPointer(texCoordLocation, 2, VertexAttribPointerType.Float,
  707. false, 5 * sizeof(float), 3 * sizeof(float));
  708. // Load photos from a directory.
  709. // string[] files = Directory.GetFiles(@"c:\users\colin\desktop\photos-test\");
  710. // string[] files = Directory.GetFiles(@"c:\users\colin\pictures\photos\2023\07\14\");
  711. // string[] files = Directory.GetFiles(@"c:\users\colin\pictures\photos\2023\09\06\jpg");
  712. // string[] files = Directory.GetFiles(@"G:\DCIM\100EOSR6\");
  713. // string[] files = Directory.GetFiles(@"c:\users\colin\desktop\totte-output\2023\08\03");
  714. string[] files = Directory.GetFiles(@"c:\users\colin\desktop\17\2-jpg");
  715. // string[] files = Directory.GetFiles(@"C:\Users\colin\Pictures\photos\2018\06\23");
  716. // string[] files = Directory.GetFiles(@"C:\Users\colin\Desktop\Germany all\104D7000");
  717. // string[] files = Directory.GetFiles(@"C:\Users\colin\Desktop\many-birds\");
  718. for (int i = 0; i < files.Count(); i++) {
  719. string file = files[i];
  720. if (file.ToLower().EndsWith(".jpg")) {
  721. Photo photo = new Photo(file, TEXTURE_BLACK);
  722. allPhotos.Add(photo);
  723. }
  724. }
  725. allPhotos.Sort(ComparePhotosByDate);
  726. // Fix up photos with missing GPS. We start at the end and work our way
  727. // backwards, because if one photo is missing GPS, it's probably because
  728. // the camera was turned off for a while, and whichever photo *after* it
  729. // has GPS data is probably more accurate.
  730. GpsInfo? lastGps = null;
  731. for (int i = allPhotos.Count - 1; i >= 0; i--) {
  732. Photo p = allPhotos[i];
  733. if (p.Gps == null) {
  734. Console.WriteLine("fixing GPS for " + p.Filename);
  735. p.Gps = lastGps;
  736. } else {
  737. lastGps = p.Gps;
  738. }
  739. }
  740. photos = allPhotos;
  741. LoadThumbnailsAsync();
  742. }
  743. private static int ComparePhotosByDate(Photo x, Photo y) {
  744. int compare = x.DateTimeOriginal.CompareTo(y.DateTimeOriginal);
  745. if (compare != 0) {
  746. return compare;
  747. }
  748. // If the photos have the same seconds value, sort by filename
  749. // (since cameras usually increment the filename for successive shots.)
  750. return x.Filename.CompareTo(y.Filename);
  751. }
  752. protected override void OnUnload() {
  753. base.OnUnload();
  754. }
  755. private void UnloadImages() {
  756. // Unload images that haven't been touched in a while.
  757. lock (loadedImagesLock) {
  758. while (loadedImages.Count > 20) {
  759. long earliestTime = long.MaxValue;
  760. Photo? earliest = null;
  761. foreach (Photo photo in loadedImages) {
  762. if (photo.LastTouch < earliestTime) {
  763. earliest = photo;
  764. earliestTime = photo.LastTouch;
  765. }
  766. }
  767. if (earliest != null) {
  768. Console.WriteLine($"loadedImages.Count: {loadedImages.Count}, " +
  769. $"evicting {earliest.Filename} @ {earliestTime}");
  770. earliest.Unload();
  771. loadedImages.Remove(earliest);
  772. }
  773. }
  774. }
  775. }
  776. private async void LoadImagesAsync() {
  777. foreach (Photo p in loadingImages) {
  778. if (p.Loaded) {
  779. lock (loadedImagesLock) {
  780. loadedImages.Add(p);
  781. loadingImages.Remove(p);
  782. }
  783. }
  784. }
  785. // Start loading any images that are in our window but not yet loaded.
  786. int minLoadedImage = Math.Max(0, photoIndex - 5);
  787. int maxLoadedImage = Math.Min(photoIndex + 10, photos.Count - 1);
  788. List<Photo> toLoad = new();
  789. for (int i = minLoadedImage; i <= maxLoadedImage; i++) {
  790. lock (loadedImagesLock) {
  791. if (!loadedImages.Contains(photos[i]) && !loadingImages.Contains(photos[i])) {
  792. Console.WriteLine("loading " + i);
  793. loadingImages.Add(photos[i]);
  794. toLoad.Add(photos[i]);
  795. }
  796. }
  797. }
  798. foreach (Photo p in toLoad) {
  799. // await Task.Run( () => { p.LoadAsync(p.Size); });
  800. await Task.Run( () => { p.LoadAsync(geometry.PhotoBox.Size); });
  801. }
  802. }
  803. private async void LoadThumbnailsAsync() {
  804. List<Task> tasks = new();
  805. foreach (Photo p in allPhotos) {
  806. tasks.Add(Task.Run( () => {
  807. p.LoadThumbnailAsync(new Vector2i(150, 150));
  808. lock (numThumbnailsLoadedLock) {
  809. numThumbnailsLoaded++;
  810. toast.Set($"[{numThumbnailsLoaded}/{allPhotos.Count}] Loading thumbnails");
  811. }
  812. }));
  813. }
  814. await Task.WhenAll(tasks).ContinueWith(t => { toast.Set("Loading thumbnails: done!"); });
  815. }
  816. // To find the JPEG compression level of a file from the command line:
  817. // $ identify -verbose image.jpg | grep Quality:
  818. private async void ExportPhotos() {
  819. List<Task> tasks = new();
  820. lock (exportPhotosLock) {
  821. // Don't ExportPhotos() if one is already active.
  822. lock (numPhotosExportedLock) {
  823. if (numPhotosToExport > 0 && numPhotosExported != numPhotosToExport) {
  824. Console.WriteLine("ExportPhotos: skipping because another export is already in progress.");
  825. return;
  826. }
  827. }
  828. numPhotosToExport = photos.Count;
  829. numPhotosExported = 0;
  830. foreach (Photo p in photos) {
  831. tasks.Add(Task.Run( () => {
  832. p.SaveAsync(outputRoot, toast);
  833. lock (numPhotosExportedLock) {
  834. numPhotosExported++;
  835. }
  836. }));
  837. }
  838. }
  839. await Task.WhenAll(tasks).ContinueWith(t => { toast.Set("Exporting photos: done!"); });
  840. }
  841. protected override void OnRenderFrame(FrameEventArgs e) {
  842. base.OnRenderFrame(e);
  843. fpsCounter.Update();
  844. UnloadImages();
  845. LoadImagesAsync();
  846. GL.Clear(ClearBufferMask.ColorBufferBit);
  847. GL.BindBuffer(BufferTarget.ArrayBuffer, VertexBufferObject);
  848. GL.ActiveTexture(TextureUnit.Texture0);
  849. if (photos.Count > 0) {
  850. DrawPhotos();
  851. } else {
  852. DrawText("No photos found.", 10, 10);
  853. }
  854. SwapBuffers();
  855. }
  856. void DrawPhotos() {
  857. Photo activePhoto = photos[photoIndex];
  858. Texture active = activePhoto.Texture();
  859. bool cropActive = activeTool is CropTool;
  860. bool straightenActive = activeTool is StraightenTool;
  861. float scaleX = 1f * geometry.PhotoBox.Size.X / active.Size.X;
  862. float scaleY = 1f * geometry.PhotoBox.Size.Y / active.Size.Y;
  863. float scale = Math.Min(scaleX, scaleY);
  864. if (zoomLevel > 0f) {
  865. scale = zoomLevel;
  866. } else if (!cropActive && activePhoto.CropRectangle != Rectangle.Empty) {
  867. scale *= 0.95f * active.Size.X / activePhoto.CropRectangle.Width;
  868. }
  869. activeScale = scale;
  870. Vector2i renderSize = (Vector2i) (((Vector2) active.Size) * scale);
  871. Vector2i center = (Vector2i) geometry.PhotoBox.Center;
  872. Vector2i offset = new((int) (activePhoto.ViewOffset.X * scale),
  873. (int) (activePhoto.ViewOffset.Y * scale));
  874. Box2i photoBox = Util.MakeBox(center.X - renderSize.X / 2 + offset.X,
  875. center.Y - renderSize.Y / 2 + offset.Y,
  876. renderSize.X, renderSize.Y);
  877. activeOffset = new(photoBox.Min.X, photoBox.Min.Y);
  878. transform = new Transform(activeScale, activeOffset, activePhoto.Size);
  879. DrawTexture(active, photoBox, Color4.White, activePhoto.RotationDegreeHundredths / 100f);
  880. for (int i = 0; i < 5; i++) {
  881. Texture star = (activePhoto.Rating > i) ? STAR_FILLED : STAR_EMPTY;
  882. DrawTexture(star, geometry.StarBoxes[i].Min.X, geometry.StarBoxes[i].Min.Y);
  883. }
  884. if (straightenActive) {
  885. DrawStraightenGuides();
  886. } else {
  887. DrawCropRectangle(cropActive);
  888. }
  889. // Draw thumbnail boxes.
  890. ribbonIndex = Math.Clamp(photoIndex - (geometry.ThumbnailBoxes.Count - 1) / 2,
  891. 0, Math.Max(0, photos.Count - geometry.ThumbnailBoxes.Count));
  892. DrawFilledBox(geometry.ThumbnailBox, Color4.Black);
  893. for (int i = 0; i < geometry.ThumbnailBoxes.Count; i++) {
  894. if (ribbonIndex + i >= photos.Count) {
  895. break;
  896. }
  897. Photo photo = photos[ribbonIndex + i];
  898. Box2i box = geometry.ThumbnailBoxes[i];
  899. DrawTexture(photo.ThumbnailTexture(), box);
  900. for (int j = 0; j < photo.Rating; j++) {
  901. DrawTexture(STAR_SMALL, box.Min.X + 8 + ((STAR_SMALL.Size.X + 2) * j), box.Min.Y + 8);
  902. }
  903. if (ribbonIndex + i == photoIndex) {
  904. DrawBox(box, 5, Color4.Black);
  905. DrawBox(box, 3, Color4.White);
  906. }
  907. }
  908. // Draw status box.
  909. int statusPadding = 4;
  910. DrawFilledBox(geometry.StatusBox, Color4.Black);
  911. // First line.
  912. int y = geometry.StatusBox.Min.Y + statusPadding;
  913. DrawText(String.Format("{0,4}/{1,-4}", photoIndex + 1, photos.Count),
  914. geometry.StatusBox.Min.X, y);
  915. DrawText(activePhoto.Description(), geometry.StatusBox.Min.X + 88, y);
  916. // Second line.
  917. y += 20;
  918. string status = activeTool.Status();
  919. if (status.Length != 0) {
  920. status += " ";
  921. }
  922. DrawText(status + toast.Get(), geometry.StatusBox.Min.X, y);
  923. DrawText(String.Format("FPS: {0,2}", fpsCounter.Fps), geometry.StatusBox.Max.X - 66, y);
  924. if (activePhoto.Loaded) {
  925. DrawText($"{(scale * 100):F1}%", geometry.StatusBox.Max.X - 136, y);
  926. }
  927. }
  928. void DrawCropRectangle(bool active) {
  929. Photo activePhoto = photos[photoIndex];
  930. if (activePhoto.CropRectangle == Rectangle.Empty) {
  931. return;
  932. }
  933. Vector2i leftTop = transform.ImageToScreen(activePhoto.CropRectangle.Left,
  934. activePhoto.CropRectangle.Top);
  935. Vector2i rightBottom = transform.ImageToScreen(activePhoto.CropRectangle.Right,
  936. activePhoto.CropRectangle.Bottom);
  937. var (left, top) = leftTop;
  938. var (right, bottom) = rightBottom;
  939. Color4 shadeColor = new Color4(0, 0, 0, 0.75f);
  940. DrawFilledBox(new Box2i(0, 0, left, geometry.PhotoBox.Max.Y), shadeColor);
  941. DrawFilledBox(new Box2i(left, 0, geometry.PhotoBox.Max.X, top), shadeColor);
  942. DrawFilledBox(new Box2i(left, bottom, geometry.PhotoBox.Max.X, geometry.PhotoBox.Max.Y),
  943. shadeColor);
  944. DrawFilledBox(new Box2i(right, top, geometry.PhotoBox.Max.X, bottom), shadeColor);
  945. DrawBox(new Box2i(left - 1, top - 1, right + 1, bottom + 1), 1, Color4.White);
  946. if (active) {
  947. // handles
  948. int handleThickness = 3;
  949. int handleLength = 16;
  950. // top-left
  951. DrawFilledBox(new Box2i(left - handleThickness, top - handleThickness,
  952. left + handleLength, top), Color4.White);
  953. DrawFilledBox(new Box2i(left - handleThickness, top - handleThickness,
  954. left, top + handleLength), Color4.White);
  955. // top-right
  956. DrawFilledBox(new Box2i(right - handleLength, top - handleThickness,
  957. right + handleThickness, top), Color4.White);
  958. DrawFilledBox(new Box2i(right, top - handleThickness,
  959. right + handleThickness, top + handleLength), Color4.White);
  960. // bottom-left
  961. DrawFilledBox(new Box2i(left - handleThickness, bottom,
  962. left + handleLength, bottom + handleThickness), Color4.White);
  963. DrawFilledBox(new Box2i(left - handleThickness, bottom - handleLength,
  964. left, bottom + handleThickness), Color4.White);
  965. // bottom-right
  966. DrawFilledBox(new Box2i(right - handleLength, bottom,
  967. right + handleThickness, bottom + handleThickness), Color4.White);
  968. DrawFilledBox(new Box2i(right + handleThickness, bottom - handleLength,
  969. right, bottom + handleThickness), Color4.White);
  970. // thirds
  971. DrawHorizontalLine(left, Util.Lerp(top, bottom, 1.0 / 3), right, Color4.White);
  972. DrawHorizontalLine(left, Util.Lerp(top, bottom, 2.0 / 3), right, Color4.White);
  973. DrawVerticalLine(Util.Lerp(left, right, 1.0 / 3), top, bottom, Color4.White);
  974. DrawVerticalLine(Util.Lerp(left, right, 2.0 / 3), top, bottom, Color4.White);
  975. }
  976. }
  977. void DrawStraightenGuides() {
  978. Box2i box = geometry.PhotoBox;
  979. int lineSpacing = 48;
  980. Color4 color = new(1f, 1f, 1f, 0.5f);
  981. for (int y = box.Min.Y + lineSpacing / 2; y < box.Max.Y; y += lineSpacing) {
  982. DrawHorizontalLine(box.Min.X, y, box.Max.X, color);
  983. // DrawHorizontalLine(box.Min.X, y + 1, box.Max.X, color);
  984. }
  985. for (int x = box.Min.X + lineSpacing / 2; x < box.Max.X; x += lineSpacing) {
  986. DrawVerticalLine(x, box.Min.Y, box.Max.Y, color);
  987. // DrawVerticalLine(x + 1, box.Min.Y, box.Max.Y, color);
  988. }
  989. }
  990. public void DrawTexture(Texture texture, int x, int y) {
  991. DrawTexture(texture, Util.MakeBox(x, y, texture.Size.X, texture.Size.Y));
  992. }
  993. public void DrawTexture(Texture texture, Box2i box) {
  994. DrawTexture(texture, box, Color4.White);
  995. }
  996. public void DrawTexture(Texture texture, Box2i box, Color4 color) {
  997. DrawTexture(texture, box, color, 0);
  998. }
  999. public void DrawTexture(Texture texture, Box2i box, Color4 color, float rotationDegrees) {
  1000. GL.Uniform4(shader.GetUniformLocation("color"), color);
  1001. SetVertices(box.Min.X, box.Min.Y, box.Size.X, box.Size.Y, rotationDegrees);
  1002. GL.BufferData(BufferTarget.ArrayBuffer, vertices.Length * sizeof(float), vertices,
  1003. BufferUsageHint.DynamicDraw);
  1004. GL.BindTexture(TextureTarget.Texture2D, texture.Handle);
  1005. GL.DrawElements(PrimitiveType.Triangles, indices.Length, DrawElementsType.UnsignedInt, 0);
  1006. }
  1007. public void DrawHorizontalLine(int left, int top, int right, Color4 color) {
  1008. DrawTexture(TEXTURE_WHITE, Util.MakeBox(left, top, right - left, 1), color);
  1009. }
  1010. public void DrawVerticalLine(int left, int top, int bottom, Color4 color) {
  1011. DrawTexture(TEXTURE_WHITE, Util.MakeBox(left, top, 1, bottom - top), color);
  1012. }
  1013. public void DrawBox(Box2i box, int thickness, Color4 color) {
  1014. DrawTexture(TEXTURE_WHITE,
  1015. Util.MakeBox(box.Min.X, box.Min.Y, box.Size.X, thickness), color);
  1016. DrawTexture(TEXTURE_WHITE,
  1017. Util.MakeBox(box.Min.X, box.Min.Y, thickness, box.Size.Y), color);
  1018. DrawTexture(TEXTURE_WHITE,
  1019. Util.MakeBox(box.Min.X, box.Max.Y - thickness, box.Size.X, thickness), color);
  1020. DrawTexture(TEXTURE_WHITE,
  1021. Util.MakeBox(box.Max.X - thickness, box.Min.Y, thickness, box.Size.Y), color);
  1022. }
  1023. public void DrawFilledBox(Box2i box, Color4 color) {
  1024. DrawTexture(TEXTURE_WHITE, Util.MakeBox(box.Min.X, box.Min.Y, box.Size.X, box.Size.Y), color);
  1025. }
  1026. public void DrawText(string text, int x, int y) {
  1027. Texture label = Util.RenderText(text);
  1028. DrawTexture(label, x, y);
  1029. label.Dispose();
  1030. }
  1031. protected override void OnResize(ResizeEventArgs e) {
  1032. base.OnResize(e);
  1033. Console.WriteLine($"OnResize: {e.Width}x{e.Height}");
  1034. geometry = new UiGeometry(e.Size, STAR_FILLED.Size.X);
  1035. projection = Matrix4.CreateOrthographicOffCenter(0f, e.Width, e.Height, 0f, -1f, 1f);
  1036. GL.UniformMatrix4(shader.GetUniformLocation("projection"), true, ref projection);
  1037. GL.Viewport(0, 0, e.Width, e.Height);
  1038. }
  1039. private void SetVertices(float left, float top, float width, float height, float rotationDegrees) {
  1040. Matrix2 transform = Matrix2.CreateRotation(MathHelper.DegreesToRadians(rotationDegrees));
  1041. Vector2 center = new(left + width / 2, top + height / 2);
  1042. Vector2 topLeft = Util.RotateAboutCenter(new Vector2(left, top), center, transform);
  1043. Vector2 topRight = Util.RotateAboutCenter(new Vector2(left + width, top), center, transform);
  1044. Vector2 bottomRight = Util.RotateAboutCenter(new Vector2(left + width, top + height), center, transform);
  1045. Vector2 bottomLeft = Util.RotateAboutCenter(new Vector2(left, top + height), center, transform);
  1046. // top left
  1047. vertices[0] = topLeft.X;
  1048. vertices[1] = topLeft.Y;
  1049. vertices[2] = 0f;
  1050. vertices[3] = 0f;
  1051. vertices[4] = 0f;
  1052. // top right
  1053. vertices[5] = topRight.X;
  1054. vertices[6] = topRight.Y;
  1055. vertices[7] = 0f;
  1056. vertices[8] = 1f;
  1057. vertices[9] = 0f;
  1058. // bottom right
  1059. vertices[10] = bottomRight.X;
  1060. vertices[11] = bottomRight.Y;
  1061. vertices[12] = 0f;
  1062. vertices[13] = 1f;
  1063. vertices[14] = 1f;
  1064. // bottom left
  1065. vertices[15] = bottomLeft.X;
  1066. vertices[16] = bottomLeft.Y;
  1067. vertices[17] = 0f;
  1068. vertices[18] = 0f;
  1069. vertices[19] = 1f;
  1070. }
  1071. }
  1072. static class Program {
  1073. static void Main(string[] args) {
  1074. List<MonitorInfo> monitors = Monitors.GetMonitors();
  1075. MonitorInfo bestMonitor = monitors[0];
  1076. int bestResolution = bestMonitor.HorizontalResolution * bestMonitor.VerticalResolution;
  1077. for (int i = 1; i < monitors.Count; i++) {
  1078. MonitorInfo monitor = monitors[i];
  1079. int resolution = monitor.HorizontalResolution * monitor.VerticalResolution;
  1080. if (resolution > bestResolution) {
  1081. bestResolution = resolution;
  1082. bestMonitor = monitor;
  1083. }
  1084. }
  1085. Console.WriteLine(
  1086. $"best monitor: {bestMonitor.HorizontalResolution}x{bestMonitor.VerticalResolution}");
  1087. GameWindowSettings gwSettings = new();
  1088. gwSettings.UpdateFrequency = 30.0;
  1089. gwSettings.RenderFrequency = 30.0;
  1090. NativeWindowSettings nwSettings = new();
  1091. nwSettings.WindowState = WindowState.Normal;
  1092. nwSettings.CurrentMonitor = bestMonitor.Handle;
  1093. nwSettings.Location = new Vector2i(bestMonitor.WorkArea.Min.X + 1,
  1094. bestMonitor.WorkArea.Min.Y + 31);
  1095. nwSettings.Size = new Vector2i(bestMonitor.WorkArea.Size.X - 2,
  1096. bestMonitor.WorkArea.Size.Y - 32);
  1097. // nwSettings.Size = new Vector2i(1600, 900);
  1098. nwSettings.MinimumSize = UiGeometry.MIN_WINDOW_SIZE;
  1099. nwSettings.Title = "Totte";
  1100. nwSettings.IsEventDriven = false;
  1101. nwSettings.Icon = new WindowIcon(Util.RenderAppIcon());
  1102. using (Game game = new(gwSettings, nwSettings)) {
  1103. game.Run();
  1104. }
  1105. }
  1106. }