circular_maze.cpp 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635
  1. /*
  2. */
  3. #include "common/sketchbook.hpp"
  4. using namespace common;
  5. using support::wrap;
  6. constexpr float2 size(range2f x)
  7. {
  8. return x.upper() - x.lower();
  9. }
  10. [[nodiscard]]
  11. constexpr float2 rotate(float2 v, float angle)
  12. {
  13. return rotate(v, protractor<>::tau(angle));
  14. }
  15. [[nodiscard]]
  16. constexpr float distance(float2 a, float2 b)
  17. {
  18. return support::root2(quadrance(a-b));
  19. }
  20. // ugh! dealing with modulo arithmetic is harder than i thought...
  21. // could be because i chose (0,1) instead of (-1,1)?
  22. // there has got to be a better way to do this regardless.
  23. // maybe just mod/wrap at the last moment and otherwise work in normal arithmetic
  24. // hint; mod_cmp used in both of below,
  25. // fix a, consider b and (b +/- mod),
  26. // calculate diff and abs_diff and chose min by abs_diff
  27. // return both chosen abs_diff and corresponding diff
  28. [[nodiscard]]
  29. constexpr float mod_distance(float a, float b, float mod)
  30. {
  31. auto minmax = std::minmax(a,b);
  32. return std::min( minmax.second - minmax.first, minmax.first + mod - minmax.second);
  33. }
  34. [[nodiscard]]
  35. constexpr float mod_difference(float a, float b, float mod)
  36. {
  37. const auto distance = mod_distance(a,b,mod);
  38. const auto difference = b > a ? +distance : -distance;
  39. return support::abs(b-a) > distance ? -difference : +difference;
  40. }
  41. constexpr float cord_length(float slice_angle, float radius = 1.f)
  42. {
  43. // very clear - self descriptive
  44. // very simple - naive direct implementation
  45. // fast?
  46. // + distance can be replaced with quadrance
  47. return distance(
  48. rotate(float2::i(radius), slice_angle),
  49. float2::i(radius)
  50. );
  51. // // very obscure - need a picture to understand
  52. // // very complicated - need advanced calculus to implement
  53. // // slow?
  54. // return 2 * radius * sin(slice_angle/2 * tau);
  55. };
  56. constexpr range2f fit(float2 area, float2 unit)
  57. {
  58. // N dimensional =)
  59. auto scale = area/unit;
  60. auto min_dimension = support::min_element(scale) - scale.begin();
  61. auto fit_area = scale[min_dimension] * unit;
  62. auto centering_mask = float2::one() - float2::unit(min_dimension);
  63. return range2f{float2::zero(), fit_area}
  64. + (area - fit_area)/2 * (centering_mask);
  65. };
  66. class circular_maze
  67. {
  68. int layers = 10;
  69. float2 _screen_size;
  70. float fov;
  71. rangef fov_range;
  72. range2f bounds;
  73. float corridor_radius;
  74. float wall_width;
  75. float initial_radius;
  76. float2 center;
  77. using float_NxM = std::vector<std::vector<float>>;
  78. float_NxM walls;
  79. float_NxM paths;
  80. std::optional<float> closest_wall(int level, float angle)
  81. {
  82. auto closest = support::min_element(walls[level], [&](auto a, auto b)
  83. {
  84. return abs(a - angle) < abs(b - angle);
  85. });
  86. if(closest != walls[level].end())
  87. return *closest;
  88. return std::nullopt;
  89. }
  90. float path_edge_proximity(int level, float angle)
  91. {
  92. float proximity = std::numeric_limits<float>::infinity();
  93. const float radius = corridor_radius/tau/2/(initial_radius + level * corridor_radius);
  94. for(auto&& path : paths[level])
  95. {
  96. proximity = std::min(mod_distance(path + radius, angle, 1.f), proximity);
  97. proximity = std::min(mod_distance(path - radius, angle, 1.f), proximity);
  98. }
  99. return proximity * tau * (level*corridor_radius + initial_radius);
  100. }
  101. float wall_proximity(int level, float angle)
  102. {
  103. auto closest = closest_wall(level, angle);
  104. return closest
  105. ? mod_distance(*closest, angle, 1.f) * tau * (level * corridor_radius + initial_radius)
  106. : std::numeric_limits<float>::infinity();
  107. }
  108. float proximity(int level, float angle)
  109. {
  110. return std::min(
  111. path_edge_proximity(level,angle),
  112. wall_proximity(level,angle)
  113. );
  114. }
  115. public:
  116. float current_angle = 0;
  117. float player_level = -1;
  118. auto get_corridor_radius() const {return corridor_radius;}
  119. void edit_view()
  120. {
  121. fov = 1.f;
  122. fov_range = rangef{-100,+100};
  123. bounds = range2f{fit(_screen_size,{cord_length(fov),1.f})};
  124. corridor_radius = size(bounds).y() / ((layers+2) * 2);
  125. wall_width = (corridor_radius/6);
  126. initial_radius = (corridor_radius * 2);
  127. center = float2{bounds.lower()+(bounds.upper() - bounds.lower()) * float2{0.5f,0.5f}};
  128. }
  129. circular_maze(float2 screen_size) :
  130. layers(7),
  131. _screen_size(screen_size),
  132. fov(1.f/8),
  133. fov_range{-fov/2,+fov/2},
  134. bounds{fit(screen_size,{cord_length(fov),1.f})},
  135. corridor_radius( size(bounds).y() / (layers+2) ),
  136. wall_width(corridor_radius/6),
  137. initial_radius(corridor_radius * 2),
  138. center{bounds.lower()+(bounds.upper() - bounds.lower()) * float2{0.5f,1.f}}
  139. {
  140. walls.resize(layers);
  141. paths.resize(layers);
  142. for(int i = 0; i < layers * 5; ++i)
  143. {
  144. int level;
  145. float angle;
  146. int breaker = 2000;
  147. do
  148. {
  149. level = trand_int({0,layers});
  150. angle = trand_float();
  151. }
  152. while(proximity(level, angle) < corridor_radius && breaker --> 0);
  153. paths[level].push_back(angle);
  154. }
  155. for(int i = 0; i < layers*layers; ++i)
  156. {
  157. int level;
  158. float angle;
  159. int breaker = 2000;
  160. do
  161. {
  162. level = trand_int({0,layers-1});
  163. angle = trand_float();
  164. }
  165. while((proximity(level, angle) < corridor_radius ||
  166. path_edge_proximity(level + 1, angle) < corridor_radius) && breaker --> 0);
  167. walls[level].push_back(angle);
  168. }
  169. }
  170. const float2& screen_size() { return _screen_size; }
  171. std::optional<float> hit_test(float angle, float level, const float_NxM& elements)
  172. {
  173. if(level < 0 || level >= layers)
  174. return std::nullopt;
  175. for(auto&& element : elements[level])
  176. {
  177. const auto radius = level * corridor_radius + initial_radius;
  178. const auto player_position = -float2::j(radius);
  179. const auto element_position = rotate(float2::i(radius), wrap(angle + element, 1.f));
  180. if(quadrance(player_position - element_position) < corridor_radius * corridor_radius / 4)
  181. return element;
  182. }
  183. return std::nullopt;
  184. }
  185. std::optional<float> wall_hit_test(float angle)
  186. {
  187. return hit_test(angle, player_level, walls);
  188. }
  189. std::optional<float> path_hit_test(float angle, float level, float direction)
  190. {
  191. return hit_test(angle, level + (direction+1)/2, paths);
  192. }
  193. void circular_move(float velocity)
  194. {
  195. const auto radius = player_level * corridor_radius + initial_radius;
  196. const auto corridor_angle = corridor_radius/tau/radius;
  197. const auto max_angular_velocity = corridor_angle*0.8;
  198. float angular_velocity = velocity/tau/radius;
  199. if(abs(angular_velocity) > max_angular_velocity)
  200. angular_velocity = std::copysign(max_angular_velocity, angular_velocity);
  201. auto new_angle = wrap(current_angle + angular_velocity, 1.f);
  202. auto hit_wall = wall_hit_test(new_angle);
  203. if(!hit_wall)
  204. {
  205. current_angle = new_angle;
  206. }
  207. else
  208. {
  209. const auto offset = std::copysign(corridor_angle*0.51f, mod_difference(current_angle + *hit_wall, 3.f/4, 1.f));
  210. current_angle = wrap(3.f/4 - *hit_wall - offset, 1.f);
  211. }
  212. }
  213. void draw(vg::frame& frame)
  214. {
  215. frame.begin_sketch()
  216. .rectangle(rect{ frame.size })
  217. .fill(0x1d4151_rgb)
  218. ;
  219. const auto fov_range_up = fov_range - 1.f/4;
  220. {auto sketch = frame.begin_sketch();
  221. float radius = initial_radius;
  222. for(int i = 0; i < layers; ++i)
  223. {
  224. sketch.arc(center, fov_range_up * tau, radius - corridor_radius/2);
  225. radius += corridor_radius;
  226. }
  227. sketch.line_width(wall_width).outline(0xfbfbf9_rgb);
  228. }
  229. {auto sketch = frame.begin_sketch();
  230. float radius = initial_radius - corridor_radius/2;
  231. for(size_t level = 0; level < paths.size(); ++level, radius += corridor_radius)
  232. {
  233. float path_arc_angle = (corridor_radius * 0.8)/tau/radius;
  234. auto path_arc_range = range{-path_arc_angle, path_arc_angle}/2;
  235. for(size_t angle = 0; angle < paths[level].size(); ++angle)
  236. {
  237. const auto path_angle = wrap(
  238. paths[level][angle] + current_angle,
  239. 1.f);
  240. if(!(fov_range_up + 1.f).intersects(path_arc_range + path_angle))
  241. continue;
  242. // TODO: approximate arc with a polygon so that we don't need to convert to radians,
  243. // here and everywhere
  244. sketch.arc(center, (path_arc_range + path_angle) * tau, radius);
  245. }
  246. } sketch.line_width(wall_width + 3).outline(0x1d4151_rgb); }
  247. float radius = initial_radius - corridor_radius/2;
  248. for(size_t level = 0; level < walls.size(); ++level, radius += corridor_radius)
  249. {
  250. const float wall_arc_angle = wall_width/tau/radius;
  251. const auto wall_arc_range = range{-wall_arc_angle, wall_arc_angle}/2;
  252. for(size_t angle = 0; angle < walls[level].size(); ++angle)
  253. {
  254. const auto wall_angle = wrap(
  255. walls[level][angle] + current_angle,
  256. 1.f);
  257. const auto intersection = (fov_range_up + 1.f).intersection(wall_arc_range + wall_angle);
  258. if(!intersection.valid())
  259. continue;
  260. const auto visible_wall_angle = intersection.upper() - intersection.lower();
  261. const auto visible_wall_width = wall_width * visible_wall_angle/wall_arc_angle;
  262. const auto wall_anchor = intersection.lower() == (fov_range_up + 1.f).lower() ? .5f : -.5f;
  263. // TODO: welp, looks like even here, polygon would be better, to render different widths with one draw call
  264. frame.begin_sketch()
  265. .line(
  266. center + rotate(
  267. float2::i(initial_radius + corridor_radius*(float(level)-0.5f)),
  268. wall_angle + wall_anchor * (wall_arc_angle - visible_wall_angle)
  269. ),
  270. center + rotate(
  271. float2::i(initial_radius + corridor_radius*(float(level)+0.5f)),
  272. wall_angle + wall_anchor * (wall_arc_angle - visible_wall_angle)
  273. )
  274. )
  275. .line_width(visible_wall_width).outline(0xfbfbf9_rgb);
  276. }
  277. }
  278. const auto player_diameter =
  279. corridor_radius - wall_width -3;
  280. const auto player_center =
  281. center - float2::j(player_level * corridor_radius + initial_radius);
  282. frame.begin_sketch().ellipse(rect{
  283. float2::one(player_diameter),
  284. player_center,
  285. half
  286. }).fill(paint::radial_gradient(
  287. player_center,
  288. {player_diameter/2 * .3f, player_diameter/2},
  289. {rgba_vector(0x00feed'ff_rgba), rgba_vector(0x00000000_rgba)}));
  290. };
  291. void diagram(vg::frame& frame, Program::duration delta)
  292. {
  293. std::cout << 1/delta.count() << '\n';
  294. const auto FPS_ratio = (1/delta.count()) / 144;
  295. frame.begin_sketch()
  296. .rectangle(rect{float2{144.f, 10.f}, float2::one(10.f)})
  297. .fill(0xffffff_rgb);
  298. frame.begin_sketch()
  299. .rectangle(rect{float2{120.f, 10.f}, float2::one(10.f)})
  300. .fill(0xaaaaaa_rgb);
  301. frame.begin_sketch()
  302. .rectangle(rect{float2{75.f, 10.f}, float2::one(10.f)})
  303. .fill(0x777777_rgb);
  304. frame.begin_sketch()
  305. .rectangle(rect{float2{60.f, 10.f}, float2::one(10.f)})
  306. .fill(0x333333_rgb);
  307. frame.begin_sketch()
  308. .rectangle(rect{float2{30.f, 10.f}, float2::one(10.f)})
  309. .fill(0xff0000_rgb);
  310. frame.begin_sketch()
  311. .rectangle(rect{float2{144.f * FPS_ratio, 10.f}, float2::one(10.f)})
  312. .fill(0x00ff00_rgb);
  313. auto fov_range_up = fov_range - 1.f/4;
  314. auto sketch = frame.begin_sketch();
  315. float radius = initial_radius;
  316. for(int i = 0; i < layers; ++i)
  317. {
  318. sketch.arc(center, fov_range_up * tau, radius);
  319. radius += corridor_radius;
  320. }
  321. for(size_t level = 0; level < walls.size(); ++level)
  322. {
  323. for(size_t angle = 0; angle < walls[level].size(); ++angle)
  324. {
  325. const auto wall_angle = wrap(
  326. walls[level][angle] + current_angle,
  327. 1.f);
  328. if(!(fov_range_up + 1.f).contains(wall_angle))
  329. continue;
  330. sketch.ellipse(rect{
  331. float2::one(4),
  332. center +
  333. rotate(
  334. float2::i(initial_radius + corridor_radius*level),
  335. wall_angle
  336. ),
  337. half
  338. });
  339. }
  340. }
  341. for(size_t level = 0; level < paths.size(); ++level)
  342. {
  343. for(size_t angle = 0; angle < paths[level].size(); ++angle)
  344. {
  345. const auto path_angle = wrap(
  346. paths[level][angle] + current_angle,
  347. 1.f);
  348. if(!(fov_range_up + 1.f).contains(path_angle))
  349. continue;
  350. sketch.line(
  351. center + rotate(
  352. float2::i(initial_radius + corridor_radius*level),
  353. path_angle
  354. ),
  355. center + rotate(
  356. float2::i(initial_radius + corridor_radius*(float(level)-1)),
  357. path_angle
  358. )
  359. );
  360. }
  361. }
  362. sketch.ellipse(rect{
  363. float2::one(corridor_radius),
  364. center - float2::j(player_level * corridor_radius + initial_radius),
  365. half
  366. });
  367. sketch.line_width(1).outline(0x0_rgb);
  368. }
  369. } maze(float2::one(400));
  370. using radial_motion_t = movement<float, motion::quadratic_curve>;
  371. using circular_motion_t = movement<float, motion::quadratic_curve>;
  372. melody<circular_motion_t, radial_motion_t>
  373. complex_radial_motion;
  374. radial_motion_t simple_radial_motion;
  375. struct radial_movement
  376. {
  377. float path = 0;
  378. float level = 0;
  379. };
  380. std::queue<radial_movement> radial_movements;
  381. void make_radial_movement(float direction)
  382. {
  383. auto level = !empty(radial_movements) ? radial_movements.back().level : maze.player_level;
  384. auto angle = !empty(radial_movements)
  385. ? wrap(3/4.f - radial_movements.back().path, 1.f)
  386. : maze.current_angle;
  387. auto path = maze.path_hit_test(angle, level, direction);
  388. if(path)
  389. radial_movements.push({*path, level + direction});
  390. }
  391. bool diagram = false;
  392. std::optional<float2> drag = std::nullopt;
  393. std::optional<float2> jerk = std::nullopt;
  394. float2 possible_jerk = float2::zero();
  395. float circular_velocity = 0.f;
  396. std::vector<float2> drag_path;
  397. void start(Program& program)
  398. {
  399. program.fullscreen = true;
  400. program.draw_once = [](auto frame)
  401. {
  402. maze = circular_maze(frame.size);
  403. };
  404. program.key_down = [](scancode code, keycode)
  405. {
  406. switch(code)
  407. {
  408. case scancode::j:
  409. case scancode::down:
  410. make_radial_movement(-1);
  411. break;
  412. case scancode::k:
  413. case scancode::up:
  414. make_radial_movement(+1);
  415. break;
  416. case scancode::d:
  417. diagram = !diagram;
  418. break;
  419. case scancode::e:
  420. maze.edit_view();
  421. break;
  422. default: break;
  423. }
  424. };
  425. program.mouse_down = [](float2 position, auto)
  426. {
  427. drag = float2::zero();
  428. circular_velocity = 0;
  429. possible_jerk = float2::zero();
  430. if(diagram)
  431. {
  432. drag_path.clear();
  433. drag_path.push_back(position);
  434. }
  435. };
  436. program.mouse_up = [](auto, auto)
  437. {
  438. drag = std::nullopt;
  439. jerk = std::nullopt;
  440. };
  441. program.mouse_move = [](auto position, float2 motion)
  442. {
  443. if(drag)
  444. {
  445. *drag += motion;
  446. if(!jerk)
  447. {
  448. // TODO: use mouse_motion::window_normalized_motion
  449. possible_jerk += motion / (maze.get_corridor_radius());
  450. if(trunc(possible_jerk) != float2::zero())
  451. jerk = signum(trunc(possible_jerk));
  452. }
  453. if(diagram)
  454. drag_path.push_back(position);
  455. }
  456. };
  457. program.draw_loop = [](auto frame, auto delta)
  458. {
  459. maze.draw(frame);
  460. if(diagram)
  461. {
  462. maze.diagram(frame, delta);
  463. { auto sketch = frame.begin_sketch();
  464. sketch.move(float2::zero());
  465. if(not drag_path.empty())
  466. sketch.move(drag_path[0]);
  467. for(auto&& position : drag_path)
  468. sketch.vertex(position);
  469. sketch.line_width(1).outline(0xFF00FF_rgb);
  470. }
  471. { auto sketch = frame.begin_sketch();
  472. sketch.move(float2::zero());
  473. for(auto&& position : drag_path)
  474. sketch.rectangle(rect{float2::one(5), position, half});
  475. sketch.line_width(1).outline(0xFF00FF_rgb);
  476. }
  477. }
  478. if(!complex_radial_motion.done())
  479. {
  480. float unwrapped_angle = maze.current_angle;
  481. auto result = complex_radial_motion.move(std::forward_as_tuple(
  482. unwrapped_angle,
  483. maze.player_level)
  484. , delta);
  485. maze.current_angle = wrap(unwrapped_angle, 1.f);
  486. if(result.done)
  487. {
  488. radial_movements.pop();
  489. circular_velocity = 0;
  490. }
  491. }
  492. else if(!simple_radial_motion.done())
  493. {
  494. auto result = simple_radial_motion.move(maze.player_level, delta);
  495. if(result.done)
  496. radial_movements.pop();
  497. }
  498. else
  499. {
  500. if(!empty(radial_movements))
  501. {
  502. auto movement = radial_movements.front();
  503. auto circular_distance = mod_difference(maze.current_angle, wrap(3/4.f - movement.path, 1.f), 1.f);
  504. auto radial_distance = movement.level - maze.player_level;
  505. auto radial_motion = radial_motion_t
  506. {
  507. abs(radial_distance) * 100ms,
  508. maze.player_level, movement.level
  509. };
  510. if(abs(circular_distance) > 0)
  511. {
  512. complex_radial_motion = melody(
  513. circular_motion_t{ abs(circular_distance) * 10s,
  514. maze.current_angle,
  515. maze.current_angle + circular_distance },
  516. radial_motion
  517. );
  518. }
  519. else
  520. simple_radial_motion = radial_motion;
  521. }
  522. if(pressed(scancode::h) || pressed(scancode::left))
  523. {
  524. maze.circular_move(1);
  525. circular_velocity = 1;
  526. }
  527. if(pressed(scancode::l) || pressed(scancode::right))
  528. {
  529. maze.circular_move(-1);
  530. circular_velocity = -1;
  531. }
  532. if(drag)
  533. {
  534. circular_velocity += drag->x();
  535. maze.circular_move(drag->x()/3);
  536. if(jerk && (*jerk * float2::j() != float2::zero()))
  537. {
  538. make_radial_movement(-jerk->y());
  539. jerk = float2::zero(); // prevents further jerks on this drag TODO: should probably add some kind of a timer on this, to eventually reset back to nullopt
  540. }
  541. drag = float2::zero();
  542. }
  543. else
  544. {
  545. maze.circular_move(circular_velocity/3);
  546. }
  547. circular_velocity *= 0.8f;
  548. }
  549. };
  550. }