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.

1173 lines
39 KiB

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